ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+Flask+Stockfish实战:构建象棋打谱与AI分析系统

Python+Flask+Stockfish实战:构建象棋打谱与AI分析系统 想做一个“象棋打谱与 AI 分析”的小工具不只是把 PGN 文件打开后逐手翻棋还希望每一步都能看到当前局面评估和候选招法。这个需求非常适合初学者用来串联 Python 后端、Web 界面和开源象棋引擎三条技术线。这里会带你从空目录开始用 python-chess、Flask 和 Stockfish 搭出一个可以加载棋谱、逐步复盘、显示局面评分和最佳招法的 Web 应用。代码刻意保持最小后续可以继续扩展成带用户登录、棋谱库和自动训练的完整项目。需要先说明一点本文使用国际象棋通用的 PGN、FEN、SAN 数据格式来演示打谱与 AI 分析链路。如果你的目标是中国象棋Xiangqi这些术语需要替换为对应的棋谱格式和棋盘渲染方案引擎也不能用 Stockfish但“加载棋谱 - 维护棋局状态 - 调用引擎分析 - 回显结果”的整体流程是一样的。1. 先想清楚“打谱”和“AI 分析”分别要解决什么问题1.1 打谱不是读文本而是维护一组“走法状态”打谱就是把棋谱按顺序一步步走并且随时可以后退、前进、跳转到某一手。很多人第一次实现时会误以为只需要把 PGN 字符串展示出来实际上打谱软件的核心工作是这样的解析 PGN得到一列合法走法。用一个当前索引记录“现在走到第几步”。每次前进或后退时从初始局面开始把索引之前的所有走法依次应用到棋盘上。根据当前棋盘状态生成 FEN渲染棋盘。这种“从初始局面重放”的方式在几十手棋内非常直观性能也足够。即使到了 100 手也是在内存中做几十次 push响应时间基本可以忽略。等后续要支持上千个棋谱、频繁跳转时再考虑缓存中间局面。1.2 AI 分析的本质是调用现成引擎而不是自己写搜索很多初学者一提到 AI 分析就以为要自己实现蒙特卡洛搜索或神经网络。实际工程里更常见的做法是调用开源的 UCI 象棋引擎。UCI 是一种标准通信协议程序启动引擎子进程后通过标准输入输出发送命令引擎返回局面评估、最佳招法和后续变化。本文选择的组合是python-chess负责 PGN 解析、局面表示、合法走法判断、UCI 引擎封装。Stockfish开源象棋引擎负责分析当前局面。Flask提供 Web 界面和 HTTP 接口。python-chess 自带engine模块可以直接把 UCI 引擎封装成类似engine.analyse(board, limit)的方式省去手动解析引擎输出的麻烦。1.3 初学者最容易误解的三件事第一PGN 不是棋盘状态。PGN 是“棋谱文本”要经过解析得到走法列表再应用到棋盘对象上才能得到当前局面。第二FEN 也不是棋盘对象。FEN 只是一串描述局面的字符串适合保存、传输和在界面上展示不适合直接拿着做走法判断。程序中应始终以chess.Board对象为准FEN 只作为接口输出。第三引擎返回的“最佳招法”不一定绝对正确。Stockfish 的推荐是在某个搜索深度和时间限制下得到的深度越深越准确但耗时也越长。学习环境里用浅层分析即可生产环境要设计好耗时、超时和用户等待体验。下面先做一个术语速查表方便后面阅读代码时对照。术语含义在本项目中的用途PGN通用棋谱文本格式用户粘贴或上传的棋谱FEN用字符串描述棋盘局面的格式接口返回当前局面前端展示SAN标准代数记谱如 Nf3、e4在棋谱列表中展示给用户UCI程序与引擎之间的通信协议计算最佳招法、局面评分引擎分析局面的外部程序Stockfish 本体2. 搭建环境和校验引擎失败多发生在这一层2.1 版本与环境清单建议环境如下项目建议值说明Python3.10 或更高低版本可能影响依赖兼容性Flask3.x用于 Web 服务python-chess最新稳定版负责棋谱和引擎封装Stockfish15 或更高稳定版开源象棋引擎操作系统Windows / macOS / Linux 均可引擎路径处理略有差异这里不写死精确版本因为依赖会持续更新。落地前先用下面的命令确认当前环境可以正常安装再继续写代码否则后面报错很难定位是代码问题还是安装问题。2.2 安装依赖和 Stockfish 引擎先创建虚拟环境mkdir chess-replay cd chess-replay python -m venv .venvWindows 下激活.venv\Scripts\activateLinux 或 macOS 下激活source .venv/bin/activate安装 Python 依赖pip install flask python-chessStockfish 的安装方式按系统不同有所区别。Ubuntu/Debian 可以直接用系统包sudo apt update sudo apt install stockfishmacOS 可以用 Homebrewbrew install stockfishWindows 用户通常下载预编译版本解压后得到stockfish.exe。这时不要急着写代码先确认你能在命令行里手动启动引擎。Windows 下可以在 PowerShell 中执行 C:\path\to\stockfish.exe输入uci回车能看到引擎输出类似id name Stockfish的信息说明引擎可用。最后输入quit退出。2.3 验证环境是否可用运行下面命令确认 Python 依赖已就绪python -c import chess; print(chess.__version__) python -c import flask; print(flask.__version__)确认引擎在 PATH 中which stockfish如果没有把引擎加入 PATH后续启动程序时可以通过环境变量指定完整路径。这一步非常关键很多初学者把 Stockfish 解压后忘了路径导致程序启动时报FileNotFoundError但以为自己写错了代码。3. 用最小代码实现 PGN 加载和棋盘状态管理3.1 项目结构为了保持初学者友好整个项目只保留三个文件加一个模板目录chess-replay/ ├── app.py ├── engine_client.py ├── templates/ │ └── index.html先把状态管理和 Web 接口写在app.py把引擎相关的封装独立到engine_client.py。这样后面替换引擎、扩展分析参数时不需要改动前端和棋谱处理逻辑。3.2 app.py 实现核心状态ReplayState是这个项目的中枢。它保存棋谱对象、走法列表和当前索引并提供board()方法实时生成当前局面。import io import os from threading import Lock import chess import chess.pgn import chess.svg from flask import Flask, render_template, request, jsonify, Response app Flask(__name__) app.secret_key dev-only-secret ENGINE_PATH os.getenv(STOCKFISH_PATH, stockfish) ANALYSIS_DEPTH int(os.getenv(ANALYSIS_DEPTH, 15)) ANALYSIS_TIME float(os.getenv(ANALYSIS_TIME, 1.0)) state_lock Lock() current_state None class ReplayState: def __init__(self): self.game chess.pgn.Game() self.moves [] self.index 0 def reset(self, gameNone, movesNone): self.game game or chess.pgn.Game() self.moves moves or [] self.index len(self.moves) def board(self): board self.game.board() for move in self.moves[: self.index]: board.push(move) return board def to_dict(self): board self.board() sans [] b self.game.board() for move in self.moves[: self.index]: sans.append(b.san(move)) b.push(move) return { fen: board.fen(), san_moves: sans, index: self.index, total: len(self.moves), turn: 白方 if board.turn else 黑方, is_game_over: board.is_game_over(), } def get_state(): global current_state with state_lock: if current_state is None: current_state ReplayState() return current_state def parse_pgn_text(text): game chess.pgn.read_game(io.StringIO(text)) if game is None: raise ValueError(无法解析 PGN请检查文本格式。) moves [node.move for node in game.mainline()] return game, moves这段代码有四个关键点第一self.index表示当前走到第几步而不是当前是第几回合。比如 index 为 0 时是初始局面index 为 1 时是白方走完第一手后的局面。第二board()每次都从初始局面重放。这样做的优点是状态不容易出错缺点是大棋谱下重复计算会慢一点。学习阶段完全可以接受。第三parse_pgn_text只读取第一局的主变。如果 PGN 里有变着和注释mainline()会忽略它们。这对于最小版本是合理的后续如果要做完整棋谱库再改成多叉树结构。第四get_state()使用了全局对象。多个浏览器访问同一个服务时大家会看到同一盘棋。学习环境演示够用生产环境必须换成按用户隔离的会话状态。接下来补上 Web 路由app.route(/) def index(): return render_template(index.html) app.route(/board.svg) def board_svg(): state get_state() board state.board() svg chess.svg.board(board, size400) return Response(svg, mimetypeimage/svgxml) app.route(/api/state) def state_api(): return jsonify(get_state().to_dict()) app.post(/api/pgn) def load_pgn(): payload request.get_json(forceTrue) text payload.get(pgn, ) try: game, moves parse_pgn_text(text) state get_state() state.reset(gamegame, movesmoves) return jsonify({ok: True, state: state.to_dict()}) except Exception as exc: return jsonify({ok: False, error: str(exc)}), 400 app.post(/api/nav) def nav(): payload request.get_json(forceTrue) action payload.get(action) state get_state() if action first: state.index 0 elif action prev: state.index max(0, state.index - 1) elif action next: state.index min(len(state.moves), state.index 1) elif action last: state.index len(state.moves) else: return jsonify({ok: False, error: f未知操作: {action}}), 400 return jsonify({ok: True, state: state.to_dict()}) app.post(/api/move) def add_move(): payload request.get_json(forceTrue) text payload.get(move, ).strip() state get_state() board state.board() try: move chess.Move.from_uci(text) if move not in board.legal_moves: return jsonify({ok: False, error: 不是合法走法}), 400 except ValueError: try: move board.parse_san(text) except ValueError: return jsonify({ok: False, error: f无法识别走法: {text}}), 400 state.moves state.moves[: state.index] state.moves.append(move) state.index 1 return jsonify({ok: True, state: state.to_dict()})/api/move的作用是让打谱者手动输入走法。输入e2e4这种 UCI 格式可以输入Nf3这种 SAN 格式也可以。推荐的输入方式是 UCI因为它是引擎和程序之间最底层的通信格式不容易产生歧义。如果当前第 5 手之后还有走法再输入新走法时会自动截断后面的分支这和大多数打谱软件的行为一致。3.3 templates/index.html 打谱界面模板保持无外部依赖用原生 JavaScript 调用接口。重点是把 FEN、走法列表、棋盘 SVG 和 AI 分析结果显示出来。!DOCTYPE html html langzh-CN head meta charsetutf-8 title象棋打谱与AI分析/title style body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; margin: 24px; background: #fafafa; } .wrap { max-width: 960px; margin: 0 auto; } .board-area { margin: 16px 0; } #boardImg { width: 400px; height: 400px; border: 1px solid #ccc; background: #fff; } .btn-row button { margin: 4px 4px 4px 0; padding: 6px 14px; } textarea { width: 100%; height: 120px; font-family: Consolas, Monaco, monospace; } #analysis { margin-top: 12px; padding: 12px; background: #fff; border: 1px solid #ddd; border-radius: 6px; line-height: 1.8; } pre { background: #f5f5f5; padding: 8px; overflow-x: auto; } /style /head body div classwrap h1象棋打谱与AI分析/h1 textarea idpgnInput placeholder粘贴 PGN 棋谱/textarea button onclickloadPgn()加载PGN/button button onclicknewGame()新建棋局/button span idstatus/span div classboard-area img idboardImg alt棋盘 / /div div classbtn-row button onclicknav(first)首步/button button onclicknav(prev)上一步/button span idmoveInfo0/0/span button onclicknav(next)下一步/button button onclicknav(last)末步/button button idanalyzeBtn onclickanalyze()AI分析/button /div div input idmoveInput placeholder输入 e2e4 或 Nf3 / button onclickaddMove()走子/button /div div idmoveList/div div idanalysis/div script async function refresh() { const res await fetch(/api/state); const data await res.json(); document.getElementById(moveInfo).textContent ${data.index}/${data.total}; document.getElementById(moveList).textContent data.san_moves.join( ); document.getElementById(boardImg).src /board.svg?t${Date.now()}; } async function loadPgn() { const pgn document.getElementById(pgnInput).value; const res await fetch(/api/pgn, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({pgn}) }); const data await res.json(); if (!data.ok) { document.getElementById(status).textContent 错误 data.error; return; } document.getElementById(status).textContent 加载成功; document.getElementById(analysis).textContent ; await refresh(); } async function newGame() { await fetch(/api/pgn, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({pgn: }) }).catch(() {}); document.getElementById(status).textContent 已新建棋局; document.getElementById(analysis).textContent ; await refresh(); } async function nav(action) { await fetch(/api/nav, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({action}) }); await refresh(); } async function addMove() { const move document.getElementById(moveInput).value.trim(); const res await fetch(/api/move, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({move}) }); const data await res.json(); if (!data.ok) { document.getElementById(status).textContent 错误 data.error; return; } document.getElementById(moveInput).value ; document.getElementById(status).textContent 走子成功; await refresh(); } async function analyze() { const btn document.getElementById(analyzeBtn); btn.disabled true; btn.textContent 分析中...; try { const res await fetch(/api/analysis, {method: POST}); const data await res.json(); if (data.error) { document.getElementById(analysis).textContent 错误 data.error; } else { const pv data.pv data.pv.length ? data.pv.join( ) : -; document.getElementById(analysis).innerHTML div局面评估 data.score_text /div div推荐招法 (data.best_move || -) /div div后续变化 pv /div; } } finally { btn.disabled false; btn.textContent AI分析; } } refresh(); /script /div /body /html这里用img src/board.svg的方式渲染棋盘虽然不如前端棋子拖拽交互丰富但胜在代码量小、逻辑直观。初学者理解打谱状态流转比学习复杂的前端棋盘库更重要。等需要鼠标点击走子时再引入成熟的棋盘 UI 组件即可。4. 接入 Stockfish给出局面评估和最佳招法4.1 为什么用 UCI 协议而不是自己写 AIStockfish 是一个独立进程python-chess 通过 UCI 协议和它通信。UCI 命令包括uci、isready、position fen ...、go depth 15、quit等。python-chess 的chess.engine模块把这些命令封装成了 Python API所以我们不需要手工拼接字符串。封装引擎的目的是隔离细节。比如以后想从 Stockfish 换成其他支持 UCI 的引擎只要引擎路径变了代码逻辑基本不用动。4.2 engine_client.py 封装创建engine_client.pyimport chess import chess.engine def format_score(score): if score is None: return - score score.pov(chess.WHITE) if score.is_mate(): return f#{score.mate()} cp score.score() return f{cp / 100:.2f} class EngineClient: def __init__(self, engine_path): self.engine_path engine_path self.engine None def start(self): if self.engine is None: self.engine chess.engine.SimpleEngine.popen_uci(self.engine_path) return self.engine def analyze_board(self, board, depth15, time_limit1.0): engine self.start() info engine.analyse( board, chess.engine.Limit(depthdepth, timetime_limit) ) score info.get(score) pv info.get(pv, []) best_move_uci board.uci(pv[0]) if pv else None pv_uci [board.uci(m) for m in pv[:10]] return { score_text: format_score(score), best_move: best_move_uci, pv: pv_uci, } def close(self): if self.engine is not None: self.engine.quit() self.engine Noneformat_score里做了两件事。第一把分数统一成白方视角这样无论轮到哪一方走正数都表示白方优势负数表示黑方优势。第二如果分数是杀棋就显示成#加步数而不是一个很大的数值。chess.engine.Limit(depthdepth, timetime_limit)同时设置了深度和用时上限。引擎在达到任一条件后就会停止搜索并返回结果。这里要注意深度越大单次分析越慢时间限制越小结果可能越不稳定。初学者建议先用depth12、time0.5跑通流程再逐步调大。4.3 AI 分析接口在app.py中引入EngineClient并添加分析接口。先导入模块from engine_client import EngineClient engine_client EngineClient(ENGINE_PATH)然后添加路由app.post(/api/analysis) def analyze(): state get_state() board state.board() if board.is_game_over(): return jsonify({ score_text: 对局已结束, best_move: None, pv: [], result: board.result(), }) try: result engine_client.analyze_board( board, depthANALYSIS_DEPTH, time_limitANALYSIS_TIME ) return jsonify(result) except Exception as exc: return jsonify({error: str(exc)}), 500这里有两个值得注意的细节。第一先判断board.is_game_over()。如果已经将死或者无子可动引擎没有后续走法可分析直接返回对局结果更合理。第二引擎分析可能因为路径错误、引擎崩溃、权限不足等原因抛出异常。学习环境下捕获异常并返回错误信息可以节省大量排查时间。生产环境则要记录详细日志并返回统一的错误结构。在程序退出时关闭引擎进程import atexit atexit.register(engine_client.close)如果忘记关闭开发过程中会残留多个 Stockfish 进程占用 CPU 和内存。这个问题在反复重启 Flask 服务时尤其明显。4.4 在界面里展示评分前端的analyze()函数已经实现了展示逻辑。点击“AI分析”后会把当前局面发给/api/analysis然后把局面评估、推荐招法和后续变化渲染到#analysis区域。在实际使用中你可能会看到一个评分是0.35这表示白方约等于 0.35 个兵的棋子价值优势#3表示当前局面下可以直接将杀而且最多 3 步完成。这些数值是引擎视角的分析结果不是最终结论。5. 完整运行验证从空棋盘到整局分析5.1 启动服务在项目根目录下启动python app.py默认会运行在http://127.0.0.1:5000。打开浏览器访问这个地址能看棋盘和按钮说明 Flask 和模板渲染正常。如果 Stockfish 不在 PATH 中需要指定路径。Linux 或 macOSexport STOCKFISH_PATH/path/to/stockfish python app.pyWindows PowerShell$env:STOCKFISH_PATHC:\path\to\stockfish.exe python app.py这一步正确的判断标准是点击“AI分析”后页面能返回评分和推荐招法而不是弹出“引擎找不到”的错误。5.2 验证路径一加载 PGN 和翻谱在页面文本框中粘贴下面这盘简短棋谱[Event Example] [Site Local] [Date 2024.01.01] [Round 1] [White A] [Black B] [Result *] 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 *点击“加载PGN”。如果成功状态栏显示“加载成功”走法列表会显示e4 e5 Nf3 Nc6 Bb5 a6步数是0/6。然后点击“末步”棋盘会走到第 6 手结束的局面。再用“上一步”“下一步”来回切换。每切换一次棋盘 SVG 都会重新生成FEN 会对应变化。这一步验证的是打谱功能是否完整。5.3 验证路径二AI 局面分析将棋局跳转到任意一步点击“AI分析”。正常情况下几秒内会返回结果。在调用接口时如果界面卡住可能是引擎路径错误也可能是分析时间设置太长。先用短时间参数定位可以通过 curl 直接验证接口curl -X POST http://127.0.0.1:5000/api/analysis返回结果类似{ best_move: e2e4, pv: [e2e4, e7e5, g1f3], score_text: 0.30 }pv是引擎预测的主变后续走法最多显示 10 手。best_move是当前局面推荐走的第一步。5.4 预期输出和异常判断正常启动时控制台会出现 Flask 的运行日志。分析接口调用后不会在控制台打印引擎内部日志因为那是 UCI 子进程的输出。如果接口报错错误信息会返回到前端并且 Flask 页面会显示在“分析”区域。一个典型验证路径如下表操作预期结果异常表现加载合法 PGN状态栏“加载成功”走法列表出现提示“无法解析 PGN”点击末步棋盘显示最后一手局面棋盘不变接口 400点击 AI 分析出现评分数值和推荐招法500 错误控制台有异常输入非法走法提示“不是合法走法”
返回列表