diff --git a/README.md b/README.md index 2c43207..66b694e 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# ÒÀÀµ: uv sync +# AwesomeXiangQi + +Xiangqi.com ÔÚÏ߶ÔÞÄ×Զ¯»úÆ÷ÈË: Ôƿâ(chessdb.cn)ÓÅÏÈ + ±¾µØƤ¿¨Ó㶵µס£ + +ʹÓÃ: uv sync ºó uv run online_bot.py, ²ÎÊý¼û uv run online_bot.py --help diff --git a/engine_server.py b/engine_server.py deleted file mode 100644 index 303d0d1..0000000 --- a/engine_server.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -"""本地引擎服务(云库优先 + 皮卡鱼兜底)。供油猴脚本调用。""" -from xiangqi_bot.server import main - -if __name__ == '__main__': - main() diff --git a/pyproject.toml b/pyproject.toml index 4b26bc9..9085f65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,5 +8,4 @@ dependencies = [ ] [project.scripts] -xiangqi-server = "xiangqi_bot.server:main" xiangqi-bot = "xiangqi_bot.bot:main" diff --git a/xiangqi-engine.user.js b/xiangqi-engine.user.js deleted file mode 100644 index 20f8671..0000000 --- a/xiangqi-engine.user.js +++ /dev/null @@ -1,624 +0,0 @@ -// ==UserScript== -// @name Xiangqi.com 本地皮卡鱼引擎对接 -// @name:en Xiangqi.com Local Pikafish Engine -// @namespace awesome-xiangqi -// @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/* -// @run-at document-start -// @grant unsafeWindow -// ==/UserScript== - -(function () { - "use strict"; - - const ENGINE_URL = "http://127.0.0.1:9377"; - const NS = "42/xiangqi,"; - const FIRST_MOVE_EXTRA_SEC = 5; - const VERSION = "1.7.0"; - const PATCH_MARK = Symbol.for("xiangqi.pikafish.ws.patched"); - const uW = typeof unsafeWindow !== "undefined" ? unsafeWindow : window; - - const storage = (() => { - const mem = new Map(); - const safe = { - get(k) { - try { - return localStorage.getItem(k); - } catch (_) { - return mem.has(k) ? mem.get(k) : null; - } - }, - set(k, v) { - mem.set(k, String(v)); - try { - localStorage.setItem(k, String(v)); - } catch (_) {} - }, - }; - return { - get: (k, d = null) => { - const v = safe.get(k); - return v == null ? d : v; - }, - set: safe.set, - }; - })(); - - 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), - moveDelay: Math.max(0, +storage.get("pf_movedelay", 1) || 1), // 每步最小间隔秒(深度/云库秒回时保证节奏) - sideOverride: ["auto", "red", "black"].includes( - storage.get("pf_side", "auto"), - ) - ? storage.get("pf_side", "auto") - : "auto", - }; - - 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); - storage.set("pf_movedelay", cfg.moveDelay); - storage.set("pf_side", cfg.sideOverride); - } - - const G = { - id: null, - moves: [], - fen: null, - mySide: null, - over: false, - thinking: false, - firstMoveDeadline: 0, - lastMoveAt: 0, - reqId: 0, - turnTimer: 0, - sideCache: { at: 0, side: null }, - evCount: 0, - }; - - const ui = {}; - const clampInt = (v, min, max, dflt) => { - const n = Number.parseInt(v, 10); - if (Number.isNaN(n)) return dflt; - return Math.min(max, Math.max(min, n)); - }; - const sideName = (s) => (s === "red" ? "红方" : s === "black" ? "黑方" : "?"); - const setStatus = (msg, keepScore = false) => { - if (!ui.status) return; - if (ui.status.__txt !== msg) { - ui.status.__txt = msg; - ui.status.textContent = msg; - } - if (!keepScore && ui.score) ui.score.textContent = ""; - }; - const setScore = (txt) => { - if (ui.score && ui.score.textContent !== txt) ui.score.textContent = txt; - }; - const setSideHint = (txt) => { - if (ui.sideHint && ui.sideHint.textContent !== txt) - ui.sideHint.textContent = txt; - }; - - function jwtUsername() { - try { - for (let i = 0; i < localStorage.length; i++) { - const k = localStorage.key(i); - if (!/jwt/i.test(k || "")) continue; - const v = String(localStorage.getItem(k) || ""); - const m = v.match(/^eyJ[\w-]+\.(eyJ[\w-]+)\./); - if (!m) continue; - const payload = JSON.parse( - decodeURIComponent( - encodeURIComponent( - atob(m[1].replace(/-/g, "+").replace(/_/g, "/")), - ), - ), - ); - if (payload && payload.sub) return payload.sub; - } - } catch (_) {} - return null; - } - - function isMyTurn() { - if (!G.mySide || G.over) return false; - return G.mySide === "red" - ? G.moves.length % 2 === 0 - : G.moves.length % 2 === 1; - } - - function parseFrame(raw) { - if (typeof raw !== "string" || !raw.startsWith(NS)) return null; - const body = raw.slice(NS.length); - const m = body.match(/^(\d+)?(\[[\s\S]*\])$/); - if (!m) return null; - try { - const arr = JSON.parse(m[2]); - return { name: arr[0], data: arr.length > 1 ? arr[1] : {} }; - } catch (_) { - return null; - } - } - - function detectSideFromBoard(force = false) { - const now = Date.now(); - if (!force && now - G.sideCache.at < 1500 && G.sideCache.side) { - G.mySide = G.sideCache.side; - return G.mySide; - } - try { - // 直接全文档查棋子: 收窄到 .board/[class*=board]/svg 容易命中无棋子的节点导致识别失败 - const avgY = (cls) => { - const els = [...document.querySelectorAll("img[alt]")].filter((i) => { - if (i.closest("#pf-panel")) return false; - const a = i.alt || ""; - return ( - a.includes(cls) && - /pawn|rook|cannon|horse|elephant|advisor|king/i.test(a) - ); - }); - if (!els.length) return null; - return ( - els.reduce((s, p) => s + p.getBoundingClientRect().y, 0) / els.length - ); - }; - const redY = avgY("red"); - const blkY = avgY("brown") ?? avgY("black"); - if (redY == null || blkY == null) return null; - G.mySide = redY > blkY ? "red" : "black"; - G.sideCache = { at: now, side: G.mySide }; - setSideHint(`我方: ${sideName(G.mySide)}`); - return G.mySide; - } catch (_) { - return null; - } - } - - function detectSide(d) { - const want = cfg.sideOverride !== "auto" ? cfg.sideOverride : jwtUsername(); - const r = d.rplayer || {}; - const b = d.bplayer || {}; - G.mySide = null; - G.sideCache = { at: 0, side: null }; - if (want && (r.username === want || r.identity === want)) G.mySide = "red"; - else if (want && (b.username === want || b.identity === want)) - G.mySide = "black"; - if (!G.mySide && cfg.sideOverride === "auto") detectSideFromBoard(true); - setSideHint( - G.mySide ? `我方: ${sideName(G.mySide)}` : "未识别执子, 请在下方手动指定", - ); - } - - let liveWS = null; - - 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( - () => ctrl.abort(new Error("引擎请求超时")), - timeoutMs, - ); - const onAbort = () => ctrl.abort(signal.reason); - if (signal) { - if (signal.aborted) ctrl.abort(signal.reason); - else signal.addEventListener("abort", onAbort, { once: true }); - } - try { - const resp = await fetch(ENGINE_URL + "/bestmove", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - moves, - fen: fen || undefined, - movetime, - depth: depth > 0 ? depth : undefined, - cloud: useCloud !== false, - cloud_always: !!cloudAlways, - }), - signal: ctrl.signal, - }); - const text = await resp.text(); - let out = null; - try { - out = text ? JSON.parse(text) : null; - } catch (_) {} - if (!resp.ok) - throw new Error((out && out.error) || `HTTP ${resp.status}`); - if ( - !out || - typeof out !== "object" || - !out.ok || - typeof out.move !== "string" || - !out.move - ) { - throw new Error((out && out.error) || "引擎返回异常"); - } - return out; - } finally { - clearTimeout(timer); - if (signal) signal.removeEventListener("abort", onAbort); - } - } - - function sendMove(move, thinkMs) { - if (!liveWS || liveWS.readyState !== WebSocket.OPEN) return false; - const frame = - NS + - `["g_v2.move",${JSON.stringify({ - game_id: G.id, - move, - thinking_time: Math.max(0, Math.round(thinkMs || 0)), - })}]`; - liveWS.send(frame); - return true; - } - - function scheduleTurn(delay = 0) { - if (G.turnTimer) return; - G.turnTimer = setTimeout( - () => { - G.turnTimer = 0; - maybeMove(); - }, - Math.max(0, delay), - ); - } - - async function maybeMove(force = false) { - if ((!cfg.enabled && !force) || G.over || G.thinking || !G.id) return; - if (!G.mySide) detectSideFromBoard(); - if (!G.mySide) return; - if (!isMyTurn()) { - if (force) setStatus("现在不是你走棋"); - return; - } - - const t0 = Date.now(); - if (G.moves.length === 0 && G.mySide === "red" && !force) { - const totalMs = (cfg.firstDelay + FIRST_MOVE_EXTRA_SEC) * 1000; - if (!G.firstMoveDeadline) G.firstMoveDeadline = Date.now() + totalMs; - const remain = G.firstMoveDeadline - Date.now(); - if (remain > 0) { - setStatus(`红方首步延迟中: ${Math.ceil(remain / 1000)}s`); - scheduleTurn(Math.min(remain, 1000)); - return; - } - } - - // 每步最小间隔(深度/云库秒回时保持行棋节奏), 计入 thinking_time - G.thinking = 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(), - fen: G.fen, - movetime: cfg.movetime, - depth: cfg.depth, - useCloud: cfg.useCloud, - cloudAlways: cfg.cloudAlways, - }); - if (reqId !== G.reqId) return; - if (G.over) throw new Error("对局已结束"); - const move = out.move.trim(); - // 列 a-i, 行 1-10 (两位行号合法) - if (!/^[a-i](?:10|[1-9])[a-i](?:10|[1-9])$/i.test(move)) - throw new Error(`非法走法: ${move}`); - if (G.moves[G.moves.length - 1] === move) throw new Error("引擎重复走子"); - if (!isMyTurn()) { - setStatus("轮次已变化, 放弃本次走子"); - return; - } - if (!sendMove(move, Date.now() - t0)) - throw new Error("未找到 socket 连接"); - - G.moves.push(move); - G.lastMoveAt = Date.now(); - setScore( - `${out.source === "cloud" ? "☁ 云库" : "♞ 引擎"} · 评分 ${out.score ?? "?"} · ${out.ms ?? "?"}ms`, - ); - setStatus(`已走: ${move}`); - } catch (e) { - if (reqId === G.reqId) - setStatus(`引擎错误: ${e && e.message ? e.message : e}`, true); - } finally { - if (reqId === G.reqId) G.thinking = false; - } - } - - function resetGame() { - G.over = false; - G.thinking = false; - G.firstMoveDeadline = 0; - G.sideCache = { at: 0, side: null }; - } - - function pushMoveIfNew(mv) { - if (mv && G.moves[G.moves.length - 1] !== mv) { - G.moves.push(mv); - return true; - } - return false; - } - - function onGameEvent(name, data) { - const d = (data && data.data) || {}; - if (name === "game.get.success") { - G.id = d.id || G.id; - G.moves = Array.isArray(d.move_history) - ? d.move_history.map((m) => String((m && m.move) || m).split(" ")[0]) - : []; - G.fen = d.fen || null; - resetGame(); - detectSide(d); - setStatus( - `已同步对局 ${G.id}(${G.moves.length} 步, 我方${sideName(G.mySide)})`, - ); - scheduleTurn(50); - } else if (name === "game.start.success") { - resetGame(); - scheduleTurn(600); - scheduleTurn(1800); - } else if (name === "game.move.opponent") { - const mv = String(d.move || "").split(" ")[0]; - if (d.fen) G.fen = d.fen; - if (mv && pushMoveIfNew(mv)) { - setStatus(`对手: ${mv}`); - scheduleTurn(80); - } - } else if (name === "game.move.success") { - const mv = String(d.move || "").split(" ")[0]; - if (d.fen) G.fen = d.fen; - if (mv && pushMoveIfNew(mv)) setStatus(`已走: ${mv}`); - G.thinking = false; - } else if (name === "game.server.end") { - G.over = true; - G.thinking = false; - setStatus(`对局结束(end_reason=${d.end_reason})`); - } - } - - function onInFrame(raw, ws) { - liveWS = ws; - const ev = parseFrame(raw); - if (!ev) return; - G.evCount++; - try { - onGameEvent(ev.name, ev.data); - } catch (e) { - console.error("[pf]", e); - } - } - - function patchWebSocket() { - const SP = uW.WebSocket && uW.WebSocket.prototype; - if (!SP || SP[PATCH_MARK]) return; - - const wrappedMap = new WeakMap(); - const origSend = SP.send; - SP.send = function (d) { - if (typeof d === "string" && d.startsWith(NS)) liveWS = this; - return origSend.apply(this, arguments); - }; - - const origAEL = SP.addEventListener; - SP.addEventListener = function (type, fn, opts) { - if (type === "message" && typeof fn === "function") { - let wrapped = wrappedMap.get(fn); - if (!wrapped) { - wrapped = function (ev) { - onInFrame(String(ev.data), this); - fn.call(this, ev); - }; - wrappedMap.set(fn, wrapped); - } - return origAEL.call(this, type, wrapped, opts); - } - return origAEL.apply(this, arguments); - }; - - const origREL = SP.removeEventListener; - SP.removeEventListener = function (type, fn, opts) { - if ( - type === "message" && - typeof fn === "function" && - wrappedMap.has(fn) - ) { - const wrapped = wrappedMap.get(fn); - wrappedMap.delete(fn); - return origREL.call(this, type, wrapped, opts); - } - return origREL.apply(this, arguments); - }; - - const omDesc = Object.getOwnPropertyDescriptor(SP, "onmessage"); - if (omDesc && omDesc.set) { - Object.defineProperty(SP, "onmessage", { - configurable: true, - get() { - return this.__pfOrigOM || null; - }, - set(fn) { - this.__pfOrigOM = typeof fn === "function" ? fn : null; - const wrapped = - typeof fn === "function" - ? function (ev) { - onInFrame(String(ev.data), this); - fn.call(this, ev); - } - : null; - omDesc.set.call(this, wrapped); - }, - }); - } - - SP[PATCH_MARK] = true; - } - - patchWebSocket(); - - setInterval(() => { - if (cfg.enabled && !document.hidden) scheduleTurn(0); - }, 1000); - - try { - uW.__pf = { - version: VERSION, - G, - cfg, - maybeMove, - detectSide, - detectSideFromBoard, - saveCfg, - }; - } catch (_) {} - - function buildPanel() { - if (ui.panel) return; - const panel = document.createElement("div"); - panel.id = "pf-panel"; - panel.innerHTML = ` - -
🐴 皮卡鱼
-
-
-
-
-
每步用时(ms)
-
深度(0=用时间)
-
首步延迟(秒+5)
-
每步延迟(秒)
-
我方执子 -
-
-
等待对局…
-
`; - document.documentElement.appendChild(panel); - - ui.panel = panel; - ui.status = panel.querySelector("#pf-status"); - ui.score = panel.querySelector("#pf-score"); - ui.sideHint = panel.querySelector("#pf-side-hint"); - const dot = panel.querySelector("#pf-dot"); - - const cb = panel.querySelector("#pf-enabled"); - cb.checked = cfg.enabled; - cb.addEventListener("change", () => { - cfg.enabled = cb.checked; - saveCfg(); - if (cfg.enabled) scheduleTurn(0); - }); - - const cloudCb = panel.querySelector("#pf-cloud"); - cloudCb.checked = cfg.useCloud; - cloudCb.addEventListener("change", () => { - cfg.useCloud = cloudCb.checked; - saveCfg(); - 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", () => { - cfg.movetime = clampInt(mt.value, 50, 600000, 800); - mt.value = cfg.movetime; - saveCfg(); - }); - - const dp = panel.querySelector("#pf-depth"); - dp.value = cfg.depth; - dp.addEventListener("change", () => { - cfg.depth = clampInt(dp.value, 0, 40, 0); - dp.value = cfg.depth; - saveCfg(); - }); - - const fd = panel.querySelector("#pf-delay"); - fd.value = cfg.firstDelay; - fd.addEventListener("change", () => { - cfg.firstDelay = clampInt(fd.value, 0, 120, 0); - fd.value = cfg.firstDelay; - G.firstMoveDeadline = 0; - 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", () => { - cfg.sideOverride = sel.value; - saveCfg(); - }); - - panel - .querySelector("#pf-now") - .addEventListener("click", () => maybeMove(true)); - - const ping = async () => { - try { - const r = await fetch(ENGINE_URL + "/health", { cache: "no-store" }); - dot.style.background = r.ok ? "#2ecc71" : "#e74c3c"; - } catch (_) { - dot.style.background = "#7f8c8d"; - } - }; - ping(); - setInterval(ping, 5000); - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", buildPanel, { once: true }); - } else { - buildPanel(); - } -})(); diff --git a/xiangqi_bot/__init__.py b/xiangqi_bot/__init__.py index 64bf834..40a96af 100644 --- a/xiangqi_bot/__init__.py +++ b/xiangqi_bot/__init__.py @@ -1 +1 @@ -from xiangqi_bot import coords, cloud, pikafish, server, bot +# -*- coding: utf-8 -*- diff --git a/xiangqi_bot/server.py b/xiangqi_bot/server.py deleted file mode 100644 index 64c1672..0000000 --- a/xiangqi_bot/server.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -"""本地 HTTP 引擎服务: 云库优先, 皮卡鱼兜底。供油猴脚本调用。""" -import argparse -import logging - -from aiohttp import web - -from .cloud import CloudBook -from .coords import fen_from_moves, site_to_ucci, ucci_to_site -from .pikafish import Pikafish - -logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)s %(message)s') -log = logging.getLogger('xiangqi.server') - - -class Service: - def __init__(self, args): - self.args = args - self.cloud = CloudBook(min_score=args.cloud_min_score) - self.engine = None # 惰性启动(首个请求前/启动时) - - async def startup(self, app): - self.engine = await Pikafish( - threads=self.args.threads, hash_mb=self.args.hash, - movetime=self.args.movetime, - log=(lambda m: log.debug(m)) if self.args.quiet else (lambda m: log.info(m)), - ).start() - - async def shutdown(self, app): - if self.engine: - await self.engine.quit() - await self.cloud.close() - - 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. 云库优先(请求可显式关闭; 全程云库模式无视分数门槛和深度强制) - use_cloud = req_cloud if req_cloud is not None else True - if use_cloud: - try: - 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 - if cloud_hit: - mv, score, note = cloud_hit - return {'ok': True, 'source': 'cloud', 'move': ucci_to_site(mv), - 'score': f'cp {score}', 'note': note, 'ms': 0} - - # 2. 皮卡鱼兜底 - try: - mv, score = await self.engine.bestmove(moves=moves_ucci, fen=fen, - movetime=movetime, depth=depth) - except Exception: - log.exception('engine crashed, restarting') - await self.engine.restart() - raise - return {'ok': True, 'source': 'engine', 'move': ucci_to_site(mv), - 'score': score, 'note': None} - - -@web.middleware -async def cors_middleware(request, handler): - """油猴脚本从 https 页面跨域调用本服务: 处理 OPTIONS 预检并补 CORS 头。""" - if request.method == 'OPTIONS': - resp = web.Response(status=204) - else: - resp = await handler(request) - resp.headers['Access-Control-Allow-Origin'] = '*' - resp.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' - resp.headers['Access-Control-Allow-Headers'] = 'Content-Type' - return resp - - -def make_app(args): - svc = Service(args) - - async def health(_): - return web.json_response({ - 'ok': True, - 'engine': svc.engine.path if svc.engine else None, - 'cloud': {'min_score': svc.cloud.min_score, - 'hit': svc.cloud.ok_count, 'miss': svc.cloud.miss_count}, - }) - - async def bestmove(request): - try: - req = await request.json() - except Exception: - return web.json_response({'ok': False, 'error': 'bad json'}, status=400) - moves = req.get('moves', []) - if not isinstance(moves, list) or not all(isinstance(m, str) for m in moves): - return web.json_response({'ok': False, 'error': 'moves 格式错误'}, status=400) - try: - out = await svc.bestmove( - moves, - fen=req.get('fen'), - 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: - return web.json_response({'ok': False, 'error': str(e)}, status=500) - - app = web.Application(middlewares=[cors_middleware]) - app.router.add_get('/health', health) - app.router.add_post('/bestmove', bestmove) - app.on_startup.append(svc.startup) - app.on_cleanup.append(svc.shutdown) - return app - - -def main(): - p = argparse.ArgumentParser(description='皮卡鱼 + 云库本地服务') - p.add_argument('--port', type=int, default=9377) - p.add_argument('--movetime', type=int, default=800, help='每步思考毫秒(深度未指定时)') - p.add_argument('--depth', type=int, default=None, help='固定深度(优先于 movetime)') - p.add_argument('--threads', type=int, default=None, help='线程数(默认=CPU核心数)') - p.add_argument('--hash', type=int, default=256, help='哈希表 MB') - p.add_argument('--cloud-min-score', type=int, default=15, - help='云库着法采纳阈值(厘兵, 红方视角)') - p.add_argument('--quiet', action='store_true', help='不打印引擎 info') - args = p.parse_args() - - app = make_app(args) - mode = f'depth={args.depth}' if args.depth else f'movetime={args.movetime}ms' - print(f'[server] http://127.0.0.1:{args.port} ({mode}, ' - f'threads={args.threads or "max"}, hash={args.hash}MB, ' - f'cloud_min_score={args.cloud_min_score})', flush=True) - web.run_app(app, host='127.0.0.1', port=args.port, print=None) - - -if __name__ == '__main__': - main()