
如果你只想“看别人怎么处理一盘棋”那打开任何一个棋谱网站就够了但如果你想“把自己下过的棋、收藏的棋谱系统整理成数据库并用 AI 对关键局面做复盘分析”市面上的免费软件要么老得不能再老要么捆绑广告、格式不兼容要么停更好几年连现代引擎都接不进去。这篇教程要解决的就是这个问题不依赖商业软件自己做一个“能读棋谱、能浏览着法、能调起 AI 引擎分析局面”的打谱分析工具。我先给一个明确判断自制象棋打谱与 AI 分析软件真正的技术难点不是界面而是“棋谱格式解析、棋盘状态管理、引擎协议对接”这三件事。这三件事在开源社区里早已有了成熟方案初学者完全可以在几百行代码内跑通一个最小可用版本。需要提前说明的是下文用国际象棋作为演示对象因为 python-chess 和 Stockfish 这套组合的生态最完整、资料最多、最容易跑通。但文章后半部分会专门讲如何把同一套架构迁移到中国象棋——二者在棋谱、棋盘、引擎协议上的差异恰恰是初学者理解“抽象与复用”的最好教材。读完这篇文章后你将得到一套可运行的“命令行打谱器”加载 PGN 棋谱、逐手前进后退、对当前局面发起 AI 分析。对 PGN / FEN / UCI 协议 / 开源引擎这几个关键概念的透彻理解。一份从「国际象棋跑通」到「中国象棋迁移」的完整路线图。1. 这篇文章真正要解决的问题1.1 打谱不是“看一遍棋谱”而是“整理棋谱”象棋爱好者的典型烦恼是棋谱积累到一定数量后全散了。有的是截图有的是文本有的是 QQ 聊天记录里翻出来的想按开局分类、想快速定位到某个中局局面基本靠人工翻。这就是“打谱”软件的原始需求把棋谱读进来、按着法走、能前进后退、能保存和检索。很多初学者会误以为要自己做一个打谱软件就得从零实现棋盘、棋子、胜负判断、着法生成……这会把入门门槛抬得极高。但真实情况是棋盘状态管理这件事拿来主义才是正道。成熟的棋类库已经帮你处理了“每一步是否合法”“如何生成所有走法”“如何判断将军/将死”等底层细节你要做的只是把它们接入自己的产品逻辑。1.2 AI 分析解决的是“不知道哪一步下错了”打谱软件的另一半需求是对局面做 AI 评估。以前学棋复盘只能靠老师或自己看。现在完全可以让引擎告诉你当前局面的红方/白方优势多少最佳着法是哪一步如果换了一种走法局面评分会发生什么变化这里的核心不是“AI 有多强”而是“协议要打通”。引擎是一个独立的进程它通过标准协议和外部程序通信。你会写出一个“客户端”把当前局面发给引擎进程引擎把评估结果和推荐着法返回给你。这个通信过程不涉及任何商业 API全部在本地完成。1.3 谁是这篇文章的读者这篇文章最适合下面三类人会一点 Python 基础的象棋爱好者不想再忍受旧软件想做一个自己能掌控的工具。想通过实际项目学习软件架构的初学者这个项目麻雀虽小但涉及格式化解析、外部进程通信、UI 与逻辑分离能学到很多课本上不讲的工程细节。想给中国象棋做分析软件的人我先带你跑通国际象棋这条路然后再告诉你中国象棋要替换哪些部件。一句话这不是一篇“介绍某个成品软件”的推荐文章而是一篇“从需求到代码”的完整实现教程。2. 核心概念与整体架构在写代码之前要先建立几个关键概念。初学者最容易被一堆英文缩写劝退其实它们背后的逻辑非常简单。概念通俗解释在项目中的作用PGN棋谱的“文本格式”记录对局信息和着法序列打谱软件的输入文件格式FEN用一串字符描述棋盘上某个局面的快照在“当前局面”和“引擎分析”之间传递数据UCI引擎与外部程序之间的通信协议让 Python 程序能控制 Stockfish 引擎Stockfish开源的棋力引擎计算能力强且免费提供 AI 分析能力python-chessPython 开源库封装了棋盘、棋谱、引擎客户端省去自己实现棋盘和着法生成的痛苦2.1 棋谱文件PGN 与 FENPGNPortable Game Notation可以理解成“棋谱界的纯文本格式”。它由两部分组成头信息区用方括号记录对局时间、选手、赛事等。着法区按“1. e4 e5 2. Nf3 Nc6 …”这样的格式记录每一步棋。FENForsyth–Edwards Notation则是棋盘局面的快照。它把双方棋子位置、轮走方、王车易位权、吃过路兵等信息压缩成长度为几段的字符串。比如国际象棋的初始局面 FEN 是rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1为什么 FEN 重要因为你要把“当前局面”传给 AI 引擎不可能用一张图片传过去一个标准字符串就搞定了。在你自己的软件体系里FEN 也是“棋谱层”和“引擎层”之间的通用接口。2.2 引擎协议UCI如果一个引擎想被各种棋软调用它就得讲一种大家都听得懂的语言。国际象棋开源的交流协议叫 UCIUniversal Chess Interface。用大白话说UCI 协议就是一套“命令行对话规则”。你启动 Stockfish 进程后往它的标准输入里写position startpos moves e2e4 e7e5它就会记住局面你再写go depth 15它就开始思考并把思考结果最佳着法、评估分数、主变着法写到标准输出里。python-chess 的chess.engine模块已经替你封装好了这套对话。你要做的只是engine chess.engine.SimpleEngine.popen_uci(stockfish) info engine.analyse(board, chess.engine.Limit(time1.0))这两行代码的背后就是本地进程的创建、UCI 协议的握手、局面下传、结果接收和解析。初学者不需要关心每个细节但必须理解引擎不是库而是一个独立进程程序通过协议和它说话。2.3 整体架构三层分离我建议把整个软件拆成三层交互层命令行 / 界面 ↓ 应用逻辑层加载棋谱、前进后退、调用分析 ↓ 能力层棋盘规则库 引擎进程这样拆的好处非常明显以后你想从命令行换成图形界面根本不需要改动棋谱解析和引擎调用代码想把国际象棋换成中国象棋也只需要替换“能力层”和“棋谱格式解析”部分交互逻辑大体可以复用。3. 技术选型与环境准备3.1 技术组合这里选择的是最容易让初学者跑通的组合Python 3.8 以上版本。python-chess 库负责棋盘状态、PGN 解析、UCI 引擎客户端。Stockfish 引擎负责计算最佳着法和评估分数。交互方式先做成命令行跑通后再考虑界面。对中国象棋方向可以提前知道一个结论国际象棋这套组合跑通后迁移到中国象棋时棋盘和棋谱部分需要换成中国象棋实现引擎从 Stockfish 换成支持 UCCI 协议的开源中国象棋引擎如皮卡鱼等。这部分在最后一节专门展开。3.2 安装 python-chesspip 安装即可pip install python-chess安装完成后执行一次导入验证python -c import chess; print(chess.__version__)如果输出一个版本号例如1.999或更高说明安装成功。版本号以你实际安装到的版本为准本文的代码基于 python-chess 1.x 的公开 API 编写。3.3 安装 Stockfish 引擎Stockfish 不是一个 Python 包而是一个可执行的二进制程序。安装方式依操作系统而定。Linux 上如果软件源里有该包可以直接安装sudo apt install stockfishWindows / macOS 上推荐去 Stockfish 官方网站下载对应平台的最新稳定版压缩包解压后把stockfish.exe放到一个固定目录然后在 Python 代码里使用绝对路径指向它。验证引擎是否能被调用stockfish # 进入引擎交互界面后输入 uci回车 # 如果引擎正常会输出一堆以 id 和 option 开头的文本这一步非常重要。很多初学者在 Python 代码里报“引擎启动失败”最后发现根本原因是引擎本身没有安装好而不是代码问题。3.4 准备一份演示棋谱为了方便测试我准备了一份标准的国际象棋 PGN 演示文件。复制到项目文件夹下命名为demo.pgn[Event Demo Game] [Site CSDN] [Date 2024.01.06] [Round 1] [White PlayerA] [Black PlayerB] [Result *] 1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 *这份棋谱是西班牙开局的前几个回合格式完全合法足够验证后面的所有功能。4. 核心流程拆解从“打开一个棋谱”到“得到 AI 分析结果”整个流程可以拆成四步解析 PGN 文件用 python-chess 的chess.pgn.read_game()把文件内容解析成Game对象。重建棋盘状态从初始局面开始按顺序执行棋谱里的每一步着法直到当前浏览的位置。发起 AI 分析把当前局面转换成 FEN交给engine.analyse()。解析评估结果把引擎返回的分数转换成人类可读的格式比如“白方 0.35 兵”或“黑方第 3 步杀棋”。为什么要拆成这四步因为每一步都对应一类独立的问题。如果你一上来就写一个 500 行的界面程序一旦出错你根本分不清是棋谱解析的问题、棋盘状态更新的问题还是引擎通信的问题。先拆流程、再写代码是初学者最应该养成的习惯。4.1 棋谱解析为什么不能直接读文本PGN 文件本质上是文本文件但你不能用正则表达式简单匹配“1. e4 e5”就完事。原因在于 PGN 里有各种分支变例、注释、NAG 符号如!和?还有可能出现的换行格式不统一。chess.pgn.read_game()可以帮你处理这些边界情况并生成一棵“棋局树”——主线是一个分支每个变例是一个子分支。4.2 棋盘状态管理从零重建还是增量前进浏览棋谱时有一个容易犯错的设计选择前进到第 20 手时是“在之前的棋盘上再走一步”还是“从初始局面重新走到第 20 手”推荐后者。原因很简单如果用户在某一步退回去选择了棋谱中的另一个分支局面的“历史”就变了。从初始局面重建可以避免状态污染。虽然性能上多了一些计算但一盘棋最多几百手现代计算机完全可以忽略这个开销。后面的代码正是采用“从初始局面重建到当前索引”的方案。5. 完整示例与代码实现这一节给出完整的可运行代码。建议按照文件拆分的方式组织项目chess-tool/ ├── demo.pgn ├── pgn_loader.py ├── analysis.py └── main.py5.1 棋谱加载模块pgn_loader.py这个文件负责读取 PGN并按着法顺序重建棋盘。# 文件pgn_loader.py import chess import chess.pgn def load_pgn(path): 读取 PGN 文件返回 chess.pgn.Game 对象。 with open(path, encodingutf-8) as f: game chess.pgn.read_game(f) if game is None: raise ValueError(PGN 文件为空或格式无法解析) return game def replay(game, max_plyNone): 按着法顺序重建棋盘并逐手打印局面。 board game.board() move_count 0 for move in game.mainline_moves(): board.push(move) move_count 1 if max_ply and move_count max_ply: break print(f第 {move_count} 手: {board.san(move)}) return board if __name__ __main__: import sys path sys.argv[1] if len(sys.argv) 1 else demo.pgn game load_pgn(path) print(对局信息:, game.headers.get(White, ?), vs, game.headers.get(Black, ?)) replay(game, max_ply10)关键逻辑说明chess.pgn.read_game()只读第一局棋。如果文件里有多局棋需要循环调用直到返回None。game.mainline_moves()返回主线上的着法迭代器它只会走主线分支不会进入变例。这是打谱软件最基础的部分。game.headers是一个字典保存了 Event、White、Black 这些头信息。5.2 AI 分析模块analysis.py这个文件负责启动引擎、分析局面、格式化结果。# 文件analysis.py import chess import chess.engine # 如果 stockfish 不在 PATH 中请改成绝对路径 # 例如 Windows: rC:\stockfish\stockfish.exe ENGINE_PATH stockfish def make_engine(pathENGINE_PATH): 启动 UCI 引擎返回 SimpleEngine 客户端。 return chess.engine.SimpleEngine.popen_uci(path) def analyze_fen(engine, fen, time_limit1.0): 分析一个 FEN 局面返回最佳着法和评估分数。 board chess.Board(fen) info engine.analyse(board, chess.engine.Limit(timetime_limit)) # 从信息中提取最佳着法主变第一手 best_move info.get(pv, [None])[0] # 分数默认从当前轮走方视角给出这里转换为白方视角 score info[score].pov(chess.WHITE) if score.is_mate(): mate_in score.mate() score_text f杀棋{mate_in} 步 else: score_cp score.score() score_text f{score_cp / 100.0:.2f} 兵 return best_move, score_text def format_score(score): 将引擎分数格式化为可读字符串。 if score.is_mate(): return f# {score.mate()} return f{score.score() / 100.0:.2f} if __name__ __main__: engine make_engine() try: best, text analyze_fen(engine, chess.STARTING_FEN) print(初始局面最佳着法:, best) print(白方优势:, text) finally: engine.quit()关键逻辑说明SimpleEngine.popen_uci()会创建一个子进程并完成 UCI 握手。这一步如果抛异常最常见的原因就是路径不对。engine.analyse()返回的info是一个字典。info[score]是相对当前轮走方视角的分数通常我们统一转成白方视角否则会误解“谁领先”。info.get(pv, [None])[0]取主变着法的第一步也就是引擎认为当前局面的最佳着法。引擎分数可能有两种形态cpcentipawn百分之一兵或mate杀棋步数。代码里分别处理这是初学者最容易忽略的坑。5.3 命令行打谱器main.py最后把前两个模块组合起来形成一个可以交互的命令行打谱工具。# 文件main.py import chess import chess.pgn import chess.engine from pgn_loader import load_pgn from analysis import make_engine, format_score ENGINE_PATH stockfish class MoveHistory: 管理棋谱着法序列与当前浏览位置。 def __init__(self, game): self.game game self.moves list(game.mainline_moves()) self.index 0 def current_board(self): 从初始局面重建到当前索引。 board chess.Board() for move in self.moves[: self.index]: board.push(move) return board def next(self): if self.index len(self.moves): self.index 1 return self.current_board() def prev(self): if self.index 0: self.index - 1 return self.current_board() def status(self): return f{self.index}/{len(self.moves)} def main(): pgn_path input(请输入 PGN 文件路径直接回车使用 demo.pgn: ).strip() or demo.pgn try: game load_pgn(pgn_path) except Exception as e: print(加载失败:, e) return history MoveHistory(game) print(白方:, game.headers.get(White, ?), 黑方:, game.headers.get(Black, ?)) try: engine make_engine(ENGINE_PATH) print(引擎启动成功:, ENGINE_PATH) except Exception as e: engine None print(引擎启动失败AI 分析不可用:, e) while True: board history.current_board() print(\n 当前局面 ) print(board) print(当前进度:, history.status()) cmd input([n]下一步 [b]上一步 [a]AI分析 [q]退出: ).strip().lower() if cmd q: break elif cmd n: history.next() elif cmd b: history.prev() elif cmd a: if engine is None: print(引擎未启动无法分析) continue print(引擎思考中请稍候...) try: info engine.analyse(board, chess.engine.Limit(time1.0)) best info.get(pv, [None])[0] score info[score].pov(chess.WHITE) print(最佳着法:, best, 白方分数:, format_score(score)) except Exception as e: print(分析出错:, e) if engine is not None: engine.quit() if __name__ __main__: main()这段代码把前面两个模块串成了一个完整工具。MoveHistory类封装了“当前看到第几手”这个核心状态current_board()每次从初始局面重建保证了状态一致性engine.analyse()被放在 try 块里避免分析异常导致整个程序退出。6. 运行结果与效果验证6.1 运行顺序在项目目录下依次执行python pgn_loader.py demo.pgn预期输出类似对局信息: PlayerA vs PlayerB 第 1 手: e4 第 2 手: e5 第 3 手: Nf3 ...这个命令用于验证 PGN 解析和着法重建是否正常。验证 AI 分析模块python analysis.py预期输出类似初始局面最佳着法: e2e4 白方优势: 0.30 兵需要注意具体数值和最佳着法会因引擎版本、分析时间、线程数而不同这不代表程序出错。6.2 交互式验证启动主程序python main.py输入demo.pgn后你会看到 ASCII 棋盘。按n下一步按b上一步按a对当前局面进行引擎分析。如果一切正常按a后终端里会出现类似下面的内容数值是示意不同引擎与机器会不同最佳着法: d4 白方分数: 0.28判断成功的关键是你确实能看到“最佳着法”和一个可读的分数而不是异常栈。6.3 如果失败先看哪里出现问题时最优先看两个位置引擎是否能在命令行单独启动如果stockfish在你的终端里都跑不起来那 Python 里必然是启动失败的。错误来自哪里如果报FileNotFoundError是引擎路径问题如果 PGN 解析后game is None是棋谱格式问题如果KeyError出现在info[score]可能是引擎版本对 python-chess 的兼容性问题。7. 常见问题与排查思路问题现象可能原因排查方式解决方案FileNotFoundError引擎路径错误或 stockfish 不在 PATH 中在终端执行 stockfish 命令或检查绝对路径修改代码中ENGINE_PATH为引擎二进制绝对路径引擎启动成功但分析时卡住ELO 设置过高、线程数过大或引擎等待输入观察 CPU 占用缩短Limit(time...)换用Limit(time1.0)或Limit(depth10)限制计算量PGN 加载后显示“文件为空”文件编码不是 UTF-8或文件里没有完整棋局用文本编辑器查看文件编码另存为 UTF-8或使用encodinggb18030兼容中文环境分数显示为# -3类似文本引擎返回的是“杀棋步数”而不是兵分检查代码是否调用score.is_mate()使用分析模块中的format_score()统一处理棋谱中的变例全部消失只读取了mainline_moves()审阅game.variations结构后续功能中递归遍历变例树Windows 下路径含空格导致报错路径字符串没有正确转义打印传入的路径字符串使用原始字符串rC:\stockfish\stockfish.exe或统一正斜杠中文 PGN 注释出现乱码文件编码与读取编码不一致查看原始文件用什么编码保存统一使用 UTF-8 保存或按实际编码读取8. 最佳实践与工程建议8.1 先把“逻辑”和“界面”分开这篇文章的代码虽然是命令行版本但分类已经体现出分层思想pgn_loader.py只负责棋谱解析analysis.py只负责引擎通信main.py负责交互。将来做图形界面时你只需要替换main.py中的交互部分核心代码可以原封不动复用。初学者常见的反面教材是把所有代码写在一个文件里且界面渲染、棋谱解析、状态管理混在一起最后改一个界面 bug 要动全局。8.2 统一用 FEN 作为各层之间的“接口”在一个完整的打谱软件里棋盘状态的传递建议统一用 FEN 字符串。无论是“从棋谱重建第 20 手局面”还是“把当前局面发给引擎”都用 FEN 作为中转。这样做的优势是国际象棋的棋盘表示、中国象棋的棋盘表示甚至前端展示层的棋盘组件都可以通过 FEN 对接。你不需要为每一层设计一套单独的数据结构极大的降低了系统复杂度。8.3 引擎进程的生命周期管理引擎进程是昂贵的资源启动一次要几十毫秒到上百毫秒频繁开关会严重影响体验。在桌面软件里应该保持一个长期运行的引擎实例并确保程序退出时调用engine.quit()。使用try/finally是标准做法。另外如果你需要同时对多个局面做分析例如批量分析一整局棋的每一步不要串行地反复调用engine.analyse()。可以先启动引擎的一个实例利用engine.analyse()的并发能力更稳妥的做法是维护多个引擎进程组成一个小进程池。初学者从这个项目的规模出发先保证try/finally正确释放资源就已经足够。8.4 不要把引擎“玩坏”了AI 分析的结果天然有随机性。同一局面同一引擎分析时间越长通常越准但“准”不等于“唯一正确答案”。做复盘分析时建议给每次分析设定固定的时间或深度限制否则批量分析会慢到无法接受。对关键胜负转折点用更大时间限制做深度分析。不要迷信单次评估分数可以多次分析取一致性结论。8.5 从国际象棋迁移到中国象棋的具体路径如果你最终目标是做一个中国象棋打谱与分析软件下面的对应关系可以直接参考能力国际象棋方案中国象棋方案棋盘规则与着法生成python-chess自己实现 9×10 棋盘或使用开源中国象棋库棋谱格式PGNXQF / CBR / 中国象棋扩展 PGN局面快照格式FEN中国象棋有类似的局面串但棋子编码不同引擎协议UCIUCCI中国象棋版协议推荐引擎Stockfish开源中国象棋引擎如皮卡鱼等迁移时界面逻辑和交互流程基本不需要大改主要工作是替换“能力层”。这也是为什么我在前面反复强调要分层你不可能第一天就写出一套中国象棋的完整实现但先把国际象棋的最小体系跑通你就掌握了“棋谱解析、状态管理、引擎协议”这三板的底层套路。8.6 后续扩展方向界面、批量分析与打包跑通命令行版本之后可以按下面的顺序继续扩展图形界面先用 Tkinter 做一个简单棋盘窗口熟练后换 PySide6 或 Pygame。分支变例浏览递归遍历game.variations支持进入变例、退出变例。批量分析写一个脚本把一整局棋的所有局面依次交给引擎分析生成每手评分曲线。开局库与残局库给常用开局建立索引残局局面则交给引擎深度计算。打包发布用 PyInstaller 把 Python 程序和引擎二进制一起打包分发给同样有打谱需求的朋友。9. 总结与后续学习方向这篇文章讲清楚了一件事自制象棋打谱与 AI 分析软件的难点不在“写一个棋软”而在把“棋谱解析、状态管理、引擎进程通信”这三块能力拼装成完整流程。文中用 python-chess 和 Stockfish 搭建了一个最小但完整的命令行工具你可以直接跑起来看效果后面又从工程角度给出了分层设计、引擎生命周期管理、以及迁移到中国象棋的对应关系。建议你的下一步不是急着加各种酷炫功能而是亲手把main.py里的交互逻辑改成你想要的模式比如加入“保存当前局面的注释”“把某个局面的 FEN 复制到剪贴板”“每次分析后自动显示主变前 5 手”。把这些小功能做好你对这个项目的理解会远超“能跑通”的阶段。如果你后续打算走中国象棋路线可以先在纸上画出“棋盘类”“棋谱解析类”“引擎客户端类”三个模块的接口再对照本文的代码逐模块替换。过程中遇到任何报错先回到第一性原理是棋谱没过是棋盘状态不对还是引擎通信没通这三条主线清晰了剩下的就只是耐心调试的问题。