每步延迟: 深度/云库秒回时保证行棋节奏(计入 thinking_time); 油猴 v1.7.0, bot 加 --move-delay

This commit is contained in:
AwesomeXiangQi committed 2026-09-13 14:36:30 +08:00
1 parent 1ed6a176cb
commit adf3abb5fc
2 files changed
+39 -4

No files matched your search

+28 -4
View File
@@ -2,7 +2,7 @@
// @name Xiangqi.com 本地皮卡鱼引擎对接 // @name Xiangqi.com 本地皮卡鱼引擎对接
// @name:en Xiangqi.com Local Pikafish Engine // @name:en Xiangqi.com Local Pikafish Engine
// @namespace awesome-xiangqi // @namespace awesome-xiangqi
// @version 1.6.0 // @version 1.7.0
// @description 在 play.xiangqi.com 对局中调用本地皮卡鱼引擎(http://127.0.0.1:9377)自动/手动走子 // @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 // @description:en Hook the site WebSocket and let a local Pikafish engine play for you
// @match https://play.xiangqi.com/* // @match https://play.xiangqi.com/*
@@ -16,7 +16,7 @@
const ENGINE_URL = "http://127.0.0.1:9377"; const ENGINE_URL = "http://127.0.0.1:9377";
const NS = "42/xiangqi,"; const NS = "42/xiangqi,";
const FIRST_MOVE_EXTRA_SEC = 5; 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 PATCH_MARK = Symbol.for("xiangqi.pikafish.ws.patched");
const uW = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; const uW = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
@@ -53,6 +53,7 @@
movetime: Math.max(50, +storage.get("pf_movetime", 800) || 800), movetime: Math.max(50, +storage.get("pf_movetime", 800) || 800),
depth: Math.max(0, +storage.get("pf_depth", 0) || 0), depth: Math.max(0, +storage.get("pf_depth", 0) || 0),
firstDelay: Math.max(0, +storage.get("pf_firstdelay", 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( sideOverride: ["auto", "red", "black"].includes(
storage.get("pf_side", "auto"), storage.get("pf_side", "auto"),
) )
@@ -67,6 +68,7 @@
storage.set("pf_movetime", cfg.movetime); storage.set("pf_movetime", cfg.movetime);
storage.set("pf_depth", cfg.depth); storage.set("pf_depth", cfg.depth);
storage.set("pf_firstdelay", cfg.firstDelay); storage.set("pf_firstdelay", cfg.firstDelay);
storage.set("pf_movedelay", cfg.moveDelay);
storage.set("pf_side", cfg.sideOverride); storage.set("pf_side", cfg.sideOverride);
} }
@@ -78,6 +80,7 @@
over: false, over: false,
thinking: false, thinking: false,
firstMoveDeadline: 0, firstMoveDeadline: 0,
lastMoveAt: 0,
reqId: 0, reqId: 0,
turnTimer: 0, turnTimer: 0,
sideCache: { at: 0, side: null }, sideCache: { at: 0, side: null },
@@ -293,9 +296,20 @@
} }
} }
const reqId = ++G.reqId; // 每步最小间隔(深度/云库秒回时保持行棋节奏), 计入 thinking_time
G.thinking = true; 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 { try {
const out = await engineBestmove({ const out = await engineBestmove({
moves: G.moves.slice(), moves: G.moves.slice(),
@@ -320,6 +334,7 @@
throw new Error("未找到 socket 连接"); throw new Error("未找到 socket 连接");
G.moves.push(move); G.moves.push(move);
G.lastMoveAt = Date.now();
setScore( setScore(
`${out.source === "cloud" ? "☁ 云库" : "♞ 引擎"} · 评分 ${out.score ?? "?"} · ${out.ms ?? "?"}ms`, `${out.source === "cloud" ? "☁ 云库" : "♞ 引擎"} · 评分 ${out.score ?? "?"} · ${out.ms ?? "?"}ms`,
); );
@@ -507,6 +522,7 @@
<div class="row"><span>每步用时(ms)</span><input type="number" id="pf-mt" min="50" step="50"></div> <div class="row"><span>每步用时(ms)</span><input type="number" id="pf-mt" min="50" step="50"></div>
<div class="row"><span>深度(0=用时间)</span><input type="number" id="pf-depth" min="0" max="40" step="1"></div> <div class="row"><span>深度(0=用时间)</span><input type="number" id="pf-depth" min="0" max="40" step="1"></div>
<div class="row"><span>首步延迟(秒+5)</span><input type="number" id="pf-delay" min="0" max="120" step="1"></div> <div class="row"><span>首步延迟(秒+5)</span><input type="number" id="pf-delay" min="0" max="120" step="1"></div>
<div class="row"><span>每步延迟(秒)</span><input type="number" id="pf-mvdelay" min="0" max="60" step="1"></div>
<div class="row"><span>我方执子</span> <div class="row"><span>我方执子</span>
<select id="pf-side"><option value="auto">自动识别</option> <select id="pf-side"><option value="auto">自动识别</option>
<option value="red">红方</option><option value="black">黑方</option></select></div> <option value="red">红方</option><option value="black">黑方</option></select></div>
@@ -569,6 +585,14 @@
saveCfg(); 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"); const sel = panel.querySelector("#pf-side");
sel.value = cfg.sideOverride; sel.value = cfg.sideOverride;
sel.addEventListener("change", () => { sel.addEventListener("change", () => {
+11
View File
@@ -50,6 +50,7 @@ class OnlineBot:
self.thinking = False self.thinking = False
self.game_over = False self.game_over = False
self.first_deadline = None self.first_deadline = None
self.last_move_at = 0.0
self._ack = 0 self._ack = 0
self._send_lock = asyncio.Lock() self._send_lock = asyncio.Lock()
@@ -199,6 +200,14 @@ class OnlineBot:
fen = fen_from_moves(self.moves) fen = fen_from_moves(self.moves)
t0 = asyncio.get_event_loop().time() 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 mv = score = None
source = 'engine' source = 'engine'
@@ -215,6 +224,7 @@ class OnlineBot:
site_mv = ucci_to_site(mv) site_mv = ucci_to_site(mv)
self.moves.append(mv) self.moves.append(mv)
self.last_move_at = asyncio.get_event_loop().time()
think_ms = int((asyncio.get_event_loop().time() - t0) * 1000) 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) 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, 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('--hash', type=int, default=256, help='哈希表 MB')
p.add_argument('--config', default='10/2', help='匹配时制 总分钟/加秒') p.add_argument('--config', default='10/2', help='匹配时制 总分钟/加秒')
p.add_argument('--first-delay', type=int, default=0, help='红方首步延迟秒(实际=此值+5)') 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('--cloud-min-score', type=int, default=15, help='云库采纳阈值(厘兵)')
p.add_argument('--no-cloud', action='store_true', help='禁用云库') p.add_argument('--no-cloud', action='store_true', help='禁用云库')
args = p.parse_args() args = p.parse_args()