From f51847b6c8c4d35037f1db17a673dab07926bf11 Mon Sep 17 00:00:00 2001 From: AwesomeXiangQi Date: Sun, 13 Sep 2026 14:36:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AF=8F=E6=AD=A5=E5=BB=B6=E8=BF=9F:=20?= =?UTF-8?q?=E6=B7=B1=E5=BA=A6/=E4=BA=91=E5=BA=93=E7=A7=92=E5=9B=9E?= =?UTF-8?q?=E6=97=B6=E4=BF=9D=E8=AF=81=E8=A1=8C=E6=A3=8B=E8=8A=82=E5=A5=8F?= =?UTF-8?q?(=E8=AE=A1=E5=85=A5=20thinking=5Ftime);=20=E6=B2=B9=E7=8C=B4=20?= =?UTF-8?q?v1.7.0,=20bot=20=E5=8A=A0=20--move-delay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- xiangqi-engine.user.js | 32 ++++++++++++++++++++++++++++---- xiangqi_bot/bot.py | 11 +++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/xiangqi-engine.user.js b/xiangqi-engine.user.js index d5d7f43..20f8671 100644 --- a/xiangqi-engine.user.js +++ b/xiangqi-engine.user.js @@ -2,7 +2,7 @@ // @name Xiangqi.com 本地皮卡鱼引擎对接 // @name:en Xiangqi.com Local Pikafish Engine // @namespace awesome-xiangqi -// @version 1.6.0 +// @version 1.7.0 // @description 在 play.xiangqi.com 对局中调用本地皮卡鱼引擎(http://127.0.0.1:9377)自动/手动走子 // @description:en Hook the site WebSocket and let a local Pikafish engine play for you // @match https://play.xiangqi.com/* @@ -16,7 +16,7 @@ const ENGINE_URL = "http://127.0.0.1:9377"; const NS = "42/xiangqi,"; const FIRST_MOVE_EXTRA_SEC = 5; - const VERSION = "1.6.0"; + const VERSION = "1.7.0"; const PATCH_MARK = Symbol.for("xiangqi.pikafish.ws.patched"); const uW = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; @@ -53,6 +53,7 @@ movetime: Math.max(50, +storage.get("pf_movetime", 800) || 800), depth: Math.max(0, +storage.get("pf_depth", 0) || 0), firstDelay: Math.max(0, +storage.get("pf_firstdelay", 0) || 0), + moveDelay: Math.max(0, +storage.get("pf_movedelay", 1) || 1), // 每步最小间隔秒(深度/云库秒回时保证节奏) sideOverride: ["auto", "red", "black"].includes( storage.get("pf_side", "auto"), ) @@ -67,6 +68,7 @@ storage.set("pf_movetime", cfg.movetime); storage.set("pf_depth", cfg.depth); storage.set("pf_firstdelay", cfg.firstDelay); + storage.set("pf_movedelay", cfg.moveDelay); storage.set("pf_side", cfg.sideOverride); } @@ -78,6 +80,7 @@ over: false, thinking: false, firstMoveDeadline: 0, + lastMoveAt: 0, reqId: 0, turnTimer: 0, sideCache: { at: 0, side: null }, @@ -293,9 +296,20 @@ } } - const reqId = ++G.reqId; + // 每步最小间隔(深度/云库秒回时保持行棋节奏), 计入 thinking_time G.thinking = true; - setStatus("皮卡鱼思考中…", true); + if (!force && cfg.moveDelay > 0) { + const remainMs = cfg.moveDelay * 1000 - (Date.now() - (G.lastMoveAt || 0)); + if (remainMs > 0) { + setStatus(`行棋节奏延迟: ${Math.ceil(remainMs / 1000)}s`, true); + await new Promise((r) => setTimeout(r, remainMs)); + if (G.over || !isMyTurn()) { + G.thinking = false; + return; + } + } + } + const reqId = ++G.reqId; try { const out = await engineBestmove({ moves: G.moves.slice(), @@ -320,6 +334,7 @@ throw new Error("未找到 socket 连接"); G.moves.push(move); + G.lastMoveAt = Date.now(); setScore( `${out.source === "cloud" ? "☁ 云库" : "♞ 引擎"} · 评分 ${out.score ?? "?"} · ${out.ms ?? "?"}ms`, ); @@ -507,6 +522,7 @@
每步用时(ms)
深度(0=用时间)
首步延迟(秒+5)
+
每步延迟(秒)
我方执子
@@ -569,6 +585,14 @@ saveCfg(); }); + const mv = panel.querySelector("#pf-mvdelay"); + mv.value = cfg.moveDelay; + mv.addEventListener("change", () => { + cfg.moveDelay = clampInt(mv.value, 0, 60, 1); + mv.value = cfg.moveDelay; + saveCfg(); + }); + const sel = panel.querySelector("#pf-side"); sel.value = cfg.sideOverride; sel.addEventListener("change", () => { diff --git a/xiangqi_bot/bot.py b/xiangqi_bot/bot.py index 3e5a6f8..e58a475 100644 --- a/xiangqi_bot/bot.py +++ b/xiangqi_bot/bot.py @@ -50,6 +50,7 @@ class OnlineBot: self.thinking = False self.game_over = False self.first_deadline = None + self.last_move_at = 0.0 self._ack = 0 self._send_lock = asyncio.Lock() @@ -199,6 +200,14 @@ class OnlineBot: fen = fen_from_moves(self.moves) t0 = asyncio.get_event_loop().time() + # 每步最小间隔(深度/云库秒回时保持节奏) + if self.args.move_delay: + gap = self.args.move_delay - (t0 - self.last_move_at) + if gap > 0: + log.info('[game] 行棋节奏延迟 %.1fs', gap) + await asyncio.sleep(gap) + if self.game_over or not self.is_my_turn(): + return mv = score = None source = 'engine' @@ -215,6 +224,7 @@ class OnlineBot: site_mv = ucci_to_site(mv) self.moves.append(mv) + self.last_move_at = asyncio.get_event_loop().time() think_ms = int((asyncio.get_event_loop().time() - t0) * 1000) log.info('[game] %s: %s → %s %s %dms', source, mv, site_mv, score, think_ms) self.emit('g_v2.move', {'game_id': self.game_id, 'move': site_mv, @@ -244,6 +254,7 @@ def main(): p.add_argument('--hash', type=int, default=256, help='哈希表 MB') p.add_argument('--config', default='10/2', help='匹配时制 总分钟/加秒') p.add_argument('--first-delay', type=int, default=0, help='红方首步延迟秒(实际=此值+5)') + p.add_argument('--move-delay', type=float, default=1.0, help='每步最小间隔秒(深度/云库秒回时保持节奏)') p.add_argument('--cloud-min-score', type=int, default=15, help='云库采纳阈值(厘兵)') p.add_argument('--no-cloud', action='store_true', help='禁用云库') args = p.parse_args()