diff --git a/xiangqi-engine.user.js b/xiangqi-engine.user.js index 9d91b05..d5d7f43 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.5.0 +// @version 1.6.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.5.0"; + const VERSION = "1.6.0"; const PATCH_MARK = Symbol.for("xiangqi.pikafish.ws.patched"); const uW = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; @@ -49,6 +49,7 @@ const cfg = { enabled: storage.get("pf_enabled", "0") === "1", useCloud: storage.get("pf_usecloud", "1") === "1", + cloudAlways: storage.get("pf_cloudalways", "0") === "1", 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), @@ -62,6 +63,7 @@ function saveCfg() { storage.set("pf_enabled", cfg.enabled ? "1" : "0"); storage.set("pf_usecloud", cfg.useCloud ? "1" : "0"); + storage.set("pf_cloudalways", cfg.cloudAlways ? "1" : "0"); storage.set("pf_movetime", cfg.movetime); storage.set("pf_depth", cfg.depth); storage.set("pf_firstdelay", cfg.firstDelay); @@ -197,7 +199,7 @@ let liveWS = null; - async function engineBestmove({ moves, fen, movetime, depth, useCloud, signal }) { + async function engineBestmove({ moves, fen, movetime, depth, useCloud, cloudAlways, signal }) { const timeoutMs = Math.max(15000, movetime * 4 + 2000); const ctrl = new AbortController(); const timer = setTimeout( @@ -219,6 +221,7 @@ movetime, depth: depth > 0 ? depth : undefined, cloud: useCloud !== false, + cloud_always: !!cloudAlways, }), signal: ctrl.signal, }); @@ -300,6 +303,7 @@ movetime: cfg.movetime, depth: cfg.depth, useCloud: cfg.useCloud, + cloudAlways: cfg.cloudAlways, }); if (reqId !== G.reqId) return; if (G.over) throw new Error("对局已结束"); @@ -498,7 +502,8 @@
🐴 皮卡鱼
-
+
+
每步用时(ms)
深度(0=用时间)
首步延迟(秒+5)
@@ -532,6 +537,13 @@ if (cfg.enabled) scheduleTurn(0); }); + const caCb = panel.querySelector("#pf-cloudalways"); + caCb.checked = cfg.cloudAlways; + caCb.addEventListener("change", () => { + cfg.cloudAlways = caCb.checked; + saveCfg(); + }); + const mt = panel.querySelector("#pf-mt"); mt.value = cfg.movetime; mt.addEventListener("change", () => { diff --git a/xiangqi_bot/cloud.py b/xiangqi_bot/cloud.py index cdded9c..f928df9 100644 --- a/xiangqi_bot/cloud.py +++ b/xiangqi_bot/cloud.py @@ -67,22 +67,20 @@ class CloudBook: self._cache[fen] = (now + CACHE_TTL, result) return result - async def best(self, fen, moves_count=0): - """返回 (move, score_mover, depth_note)。 + async def best(self, fen, moves_count=0, any_score=False): + """返回 (move, score_mover, note)。 - 云库分数为红方视角; 轮黑时取负。分数不足 min_score 时放弃(交给引擎)。 - moves_count 用于开局阶段放宽采纳阈值(前 8 回合云库几乎必赢)。 + 实测 chessdb 的 score 就是行棋方视角(与皮卡鱼 UCI 口径一致), 不需翻转。 + any_score=True(全程云库): 无条件采纳云库最佳着法。 + 否则: 开局(≤16步)直接采纳, 中后局要求分数 ≥ min_score。 """ entries = await self.query_all(fen) if not entries: return None - side_is_red = (fen.split()[1] == 'w') - sign = 1 if side_is_red else -1 - scored = [(mv, sign * sc) for mv, sc in entries] - scored.sort(key=lambda x: -x[1]) + scored = sorted(entries, key=lambda x: -x[1]) mv, sc = scored[0] - # 开局阈值低(云库碾压引擎), 中后局要求云库分足够好 - threshold = self.min_score if moves_count >= 16 else 0 + threshold = (-10 ** 9) if any_score else ( + self.min_score if moves_count >= 16 else 0) if sc < threshold: return None return mv, sc, f'{len(scored)}候选' diff --git a/xiangqi_bot/server.py b/xiangqi_bot/server.py index 68b485d..64c1672 100644 --- a/xiangqi_bot/server.py +++ b/xiangqi_bot/server.py @@ -31,15 +31,18 @@ class Service: await self.engine.quit() await self.cloud.close() - async def bestmove(self, moves_site, fen=None, movetime=None, depth=None, req_cloud=None): + async def bestmove(self, moves_site, fen=None, movetime=None, depth=None, + req_cloud=None, cloud_always=None): moves_ucci = [site_to_ucci(m) for m in moves_site] fen = fen or fen_from_moves(moves_ucci) - # 1. 云库优先(请求可显式关闭; 深度模式强制引擎) + # 1. 云库优先(请求可显式关闭; 全程云库模式无视分数门槛和深度强制) use_cloud = req_cloud if req_cloud is not None else True - if use_cloud and not depth: + if use_cloud: try: - cloud_hit = await self.cloud.best(fen, moves_count=len(moves_ucci)) + cloud_hit = await self.cloud.best( + fen, moves_count=len(moves_ucci), + any_score=bool(cloud_always)) except Exception as e: log.warning('cloud error: %s', e) cloud_hit = None @@ -99,6 +102,7 @@ def make_app(args): movetime=req.get('movetime'), depth=req.get('depth'), req_cloud=req.get('cloud'), + cloud_always=req.get('cloud_always'), ) return web.json_response(out) except Exception as e: