
不管你是刚学 Python 的程序员还是摸过几年棋但想用技术把象棋老本行“榨出价值”的爱好者“自制一个象棋打谱与 AI 分析软件”都属于那种看起来唬人、其实路径很清晰的项目。很多人一听到“AI 下棋”第一反应就是深度学习、强化学习、几千个自对弈棋谱——但真实开发场景里一个完全够用的象棋分析软件核心工作往往是另一件事把棋谱数据管好、把棋盘画对、把搜索引擎的接口用好。本文就从这个判断出发带你从零搭出一个可在本地运行的象棋打谱与 AI 分析工具。先说清楚这篇文章能给你什么。看完之后你会有三样收获第一理解象棋软件的基本架构知道棋谱的 FEN、PGN 这些格式到底在表达什么第二用手写 UI 和开源棋库跑通一个“能摆棋、能回放、能保存棋谱”的本地工具第三接入一个棋力分析接口让程序告诉你“当前局面谁占优、最佳着法是什么”。整个过程不需要 GPU不需要训练模型只需要 Python 基础语法和一些耐心。这篇文章真正想解决的问题其实不是“教你写出一个打败大师的引擎”而是“让你用最少的成本拥有一个自己做主的象棋工具”。市面上现成打谱软件不少但如果你想要自定义界面、把自己研究过的变例存成特定格式、或者只想理解棋谱文件背后的原理自己动手永远是效率最高的路径。而且这个项目足够小非常适合做 Python 入门到进阶的过渡练手项目。1. 这篇文章真正要解决的问题先泼一盆冷水如果你想做一个能在中象领域击败特级大师的 AI本文不适合你。那种项目需要的工程量、算力和棋力知识远超一篇博客能覆盖的范围。但如果你想要的是下面这些能力那这篇内容正好落在你的需求区间打开一个.pgn棋谱文件把每一步棋按顺序在棋盘上呈现出来支持上一步、下一步、跳转到特定回合。手动在棋盘上点击棋子走棋并把走棋记录保存为标准棋谱。对当前局面进行 AI 评估得到“红方优势分数”和“推荐着法”。通过调节搜索深度或时间让 AI 分析在自己电脑上跑得动。这些能力恰好构成了一个“打谱软件”的核心。从工程上看它涉及三块相对独立的技术点棋谱数据模型FEN/PGN 解析、棋盘交互界面UI 绘制与事件处理、AI 分析服务搜索引擎或外部引擎调用。很多初学者会在这类项目上踩同一个坑一上来就想着写一个“包含 AI 训练”的全套系统结果数据管理还没做完就被各种棋规细节劝退。更聪明的做法是分层推进先让棋谱能读、能存再让棋盘能点、能画最后才把 AI 分析接到某个环节上。这个顺序不会让你觉得第一步就非常困难而且每完成一层你都会看到一个可运行、可演示的结果学习反馈非常及时。一句话总结本文的中心判断对初学者来说象棋打谱与 AI 分析软件的关键不在于“训练一个会下棋的模型”而在于“把棋谱数据、棋盘交互和搜索引擎组装成一个完整可用的本地工具”。这个判断也决定了全文的技术选型和代码设计。2. 象棋棋谱的核心概念FEN 与 PGN在写代码之前必须先理解象棋软件内部是怎么“记忆”一局棋的。如果你自己设计数据结构可能会想用一个二维数组保存棋盘上每个格子的棋子再用一个列表保存每一步走棋的记录。这个方案没有错但现实中象棋软件普遍采用两个通用标准FEN 和 PGN。理解这两个格式等于你直接获得了和所有主流象棋工具对话的能力。2.1 FEN一个字符串描述整个棋盘局面FENForsyth-Edwards Notation最初是国际象棋的标准中象工具也广泛借用这种思路。中国象棋的 FEN 变体通常用类似下面的形式描述局面rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1这段字符串用斜杠分成 10 行对应棋盘从第 10 行到第 1 行。字母r、n、b、a、k、c、p分别代表黑方的车、马、象、士、将、炮、卒大写字母则代表红方对应棋子。数字表示连续的空格数。初学者最容易犯的错是搞混行顺序。在 FEN 中第一个符号对应的是棋盘最上方黑方底线不是红方底线。如果你显示棋盘时解析顺序反了整个棋盘会上下颠倒。2.2 PGN记录一局棋的完整过程PGNPortable Game Notation同样源自国际象棋中象软件也使用它的结构。一个 PGN 文件由若干“事件”组成每个事件有标签对Tag Pair和走棋序列[Event 友谊赛] [Site Local] [Date 2025.01.01] [Round 1] [Red 张三] [Black 李四] [Result *] 1. 炮二平五 马8进7 2. 马二进三 车9平8 *走棋序列用中文字符记录包含回合号、红方着法、黑方着法。解析 PGN 时需要注意注释用花括号{}包裹可能会穿插在着法中间。结果*表示棋局未结束。某些棋谱会带变例Variation用圆括号包裹如果只需要主线棋谱解析时要能跳过它们。2.3 为什么不能只用一个数组你可能会问既然用二维数组也能表示棋盘为什么要学 FEN 和 PGN答案是互操作性和持久化。当你把局面保存为 FEN 字符串你可以直接把它粘贴到任何支持该格式的棋盘软件里查看当你保存 PGN 文件你可以在主流打谱软件里继续研究。自己做项目时这种“标准格式优先”的习惯能避免项目后期陷入自造数据格式的泥潭。当然完全自研解析器需要处理很多细节初学者在这部分容易写出一堆 bug。更务实的做法是用成熟的开源库完成 FEN/PGN 的解析与合法性判断自己把精力放在棋盘交互和 AI 分析上。下面第 3 章的环境准备就围绕这个思路展开。3. 环境准备与前置条件在动手写代码之前先确认你的电脑具备以下环境。因为本文面向初学者这里尽量使用跨平台、安装简单的方案。3.1 开发环境清单项目建议方案说明操作系统Windows / macOS / Linux 均可以下代码基于 Python跨平台无压力Python 版本3.9 或更高版本请以实际环境为准本文不依赖新版本特性图形界面库Tkinter内置Python 自带无需额外安装象棋库python-chess带中国象棋扩展处理 FEN/PGN 和走棋合法性AI 分析Stockfish 中象版本或自己写的搜索算法后者不依赖外部程序更偏教学注意python-chess本身是国际象棋库但社区有适配中国象棋的分支或配套方案。如果你想减少折腾可以在初期自己写一个极简的棋盘模型只处理中象的走法生成和 FEN 解析。本文会给出一个能跑的简化版本不依赖python-chess也能完成多数功能。3.2 创建一个项目目录建议新建一个干净的目录方便管理代码和棋谱文件mkdir xiangqi-tool cd xiangqi-tool后续所有代码都放在这个目录下。如果你用 IDE推荐 VS Code 或 PyCharm两个都支持 Python 调试初学者可以从 VS Code 开始插件安装简单。3.3 安装依赖如果你想用现成库解析棋谱可以安装python-chess以实际版本为准命令如下pip install python-chess如果安装失败或版本冲突可以先跳过直接使用本文后面提供的简化解析代码。记住依赖库永远是可选的核心功能必须自己能掌控。4. 核心流程拆解一个完整的象棋打谱与 AI 分析软件无论看起来多复杂实际上可以拆成四条主流程。我们按学习顺序逐一拆解后面代码实现会对应到这些流程里。4.1 流程一棋盘局面加载与显示这是整个软件的地基。你的程序需要能够读取一个 FEN 字符串初始化一个 10 行 9 列的棋盘数组。根据棋盘数组在图形界面中绘制棋盘和棋子。支持红黑双方棋子在不同坐标下的显示。很多人第一步就卡在“坐标映射”上。中象棋盘是一个 10 横 9 竖的网格但棋子不是放在格子中心而是放在交叉点上。所以你要用 9 列、10 行的交叉点坐标来表示位置。例如红方最左边的车坐标可以记为(0, 9)其中 0 表示第 0 列9 表示第 9 行从黑方到红方。4.2 流程二走棋与悔棋当用户点击某个棋子再点击一个目标交叉点程序需要判断源位置是否有当前回合玩家的棋子。判断目标位置是否是合法着法不能吃自己的子不能走出不符合棋子规则的位置不能导致自己老将被将军等等。如果有合法着法就更新棋盘数组并记录一步棋。支持撤销也就是把棋盘恢复到上一步。对于初学者完整实现所有棋子的走法规则是一个不小的挑战但也是最能加深理解的部分。尤其是“炮”的走法和“马”的蹩马腿、“象”的塞象眼这些规则写错了很难排查。4.3 流程三棋谱导入与回放一个打谱软件必须能打开已有棋谱文件。这个流程包含读取 PGN 文本。解析回合数和着法列表。从起始局面开始逐条执行着法。提供“上一步”“下一步”“自动播放”等交互。在实现回放时不要每走一步都重新解析整个 PGN。更好做法是解析一次得到有序着法列表然后维护一个“当前步数”索引根据索引在棋盘上执行或回退。4.4 流程四AI 分析接口AI 分析有两种接入方式。一种是调用外部引擎如 Stockfish 的中国象棋版本通过命令行协议沟通另一种是自己写一个简单的搜索引擎例如使用极大极小搜索Minimax加 Alpha-Beta 剪枝。对于初学者我更推荐先理解第二种方式因为它能让你清楚看到“AI 是怎么想棋的”。虽然棋力不高但足够分析明显优劣局面、提供推荐着法。如果之后想要更强的棋力再切换为外部引擎即可。5. 完整示例与代码实现下面进入本文最核心的实操部分。我们分模块构造一个小而完整的程序不依赖额外象棋库用纯 Python 实现核心逻辑用 Tkinter 做界面。这样你可以一步一步复制运行不会因为某个库版本不兼容而卡死。5.1 棋盘数据模型首先定义棋盘。我们用一个 10 行 9 列的二维列表表示棋盘列表元素为None表示空字符串表示棋子。字符规则红子用大写黑子用小写。# 文件路径xiangqi_tool/board_model.py class BoardModel: 中国象棋棋盘数据模型。 棋盘坐标row 从 0 到 9黑方底线到红方底线col 从 0 到 8左到右。 棋子编码红 r 车n 马b 相a 仕k 帅c 炮p 兵 黑 R 车N 马B 象A 士K 将C 炮P 卒。 START_FEN rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w def __init__(self): self.board self._parse_fen(self.START_FEN) self.turn w # w 红方b 黑方 self.move_history [] self._current_index -1 def _parse_fen(self, fen: str): 解析中国象棋 FEN 字符串只处理棋盘部分。 例如 rnbakabnr/9/1c5c1/... - list of list parts fen.split( ) board_part parts[0] rows board_part.split(/) board [] for row_str in rows: row [] for ch in row_str: if ch.isdigit(): # 数字代表连续的空格 row.extend([None] * int(ch)) else: row.append(ch) board.append(row) return board def get_piece(self, row: int, col: int): return self.board[row][col] def set_piece(self, row: int, col: int, piece): self.board[row][col] piece def to_fen(self) - str: 把当前棋盘状态转回 FEN 字符串。 rows [] for row in self.board: row_str empty_count 0 for piece in row: if piece is None: empty_count 1 else: if empty_count: row_str str(empty_count) empty_count 0 row_str piece if empty_count: row_str str(empty_count) rows.append(row_str) return /.join(rows) self.turn关键逻辑解释_parse_fen中遇到数字字符串就扩展为多个None遇到字母就是棋子。这个解析函数只处理棋盘部分忽略其他字段。to_fen是反向操作把二维数组压缩成 FEN 字符串。调试时非常有用可以随时对比局面。你可以先单独测试这个类# 测试代码 if __name__ __main__: bm BoardModel() print(bm.to_fen()) print(bm.get_piece(0, 0)) # 输出 r黑方最左边车预期输出rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w r如果你的输出与上面一致说明 FEN 解析和序列化都正确。这是后面所有功能的基础。5.2 走棋规则与走法生成这是整个项目最容易出错的部分。为了控制篇幅我们先实现三种棋子的移动判断车、炮、马。剩余棋子帅、仕、相、兵可以按同样思路扩展。先看代码# 文件路径xiangqi_tool/rules.py def is_valid_move(board, start_r, start_c, end_r, end_c, turn): 简化的走棋合法性判断。 只实现车、炮、马三种棋子的规则其余棋子暂时放行在真实项目中必须补全。 piece board[start_r][start_c] if piece is None: return False # 判断走棋方是否一致大写红方(w)小写黑方(b) if turn w and not piece.isupper(): return False if turn b and not piece.islower(): return False target board[end_r][end_c] if target is not None: # 不能吃自己的棋子 if turn w and target.isupper(): return False if turn b and target.islower(): return False piece_lower piece.lower() dr end_r - start_r dc end_c - start_c if piece_lower r: # 车 return _is_valid_chariot(board, start_r, start_c, end_r, end_c) if piece_lower c: # 炮 return _is_valid_cannon(board, start_r, start_c, end_r, end_c) if piece_lower n: # 马 return _is_valid_horse(board, start_r, start_c, end_r, end_c) # 其他棋子规则未实现先返回 True 方便演示 return True def _is_valid_chariot(board, start_r, start_c, end_r, end_c): 车的规则直线中间不能有棋子。 if start_r end_r: step_c 1 if end_c start_c else -1 for c in range(start_c step_c, end_c, step_c): if board[start_r][c] is not None: return False return True if start_c end_c: step_r 1 if end_r start_r else -1 for r in range(start_r step_r, end_r, step_r): if board[r][start_c] is not None: return False return True return False def _is_valid_cannon(board, start_r, start_c, end_r, end_c): 炮的规则移动时中间不能有棋子吃子时必须隔一个棋子炮架。 if start_r end_r and start_c end_c: return False if start_r ! end_r and start_c ! end_c: return False # 统计路径上的棋子数量 count 0 if start_r end_r: step_c 1 if end_c start_c else -1 for c in range(start_c step_c, end_c, step_c): if board[start_r][c] is not None: count 1 else: step_r 1 if end_r start_r else -1 for r in range(start_r step_r, end_r, step_r): if board[r][start_c] is not None: count 1 target board[end_r][end_c] if target is None: # 不吃子路径上必须没有棋子 return count 0 else: # 吃子路径上必须恰好有一个棋子炮架 return count 1 def _is_valid_horse(board, start_r, start_c, end_r, end_c): 马的规则走“日”字注意蹩马腿。 dr end_r - start_r dc end_c - start_c if dr 2 and dc 1: # 马脚在 start_r 1, start_c if board[start_r 1][start_c] is not None: return False return True if dr -2 and dc 1: if board[start_r - 1][start_c] is not None: return False return True if dr 1 and dc 2: if board[start_r][start_c 1] is not None: return False return True if dr 1 and dc -2: if board[start_r][start_c - 1] is not None: return False return True if dr 2 and dc -1: if board[start_r 1][start_c] is not None: return False return True if dr -2 and dc -1: if board[start_r - 1][start_c] is not None: return False return True if dr -1 and dc 2: if board[start_r][start_c 1] is not None: return False return True if dr -1 and dc -2: if board[start_r][start_c - 1] is not None: return False return True return False这段代码虽然简化了帅、仕、相、兵的规则但足以演示完整的“点击走棋 → 检查合法性 → 更新棋盘”链路。在实际项目中你必须把剩余规则补全否则会出现“帅走出九宫”“兵后退”等错误局面。5.3 极简 AI极大极小搜索与 Alpha-Beta 剪枝现在写一个能分析局面的 AI。先从最简单的思路开始给每个棋子设定一个价值评估当前局面的分数。然后使用极大极小搜索模拟几步棋找到对当前方最有利的走法。# 文件路径xiangqi_tool/ai_search.py PIECE_VALUES { k: 10000, a: 200, b: 200, n: 400, r: 600, c: 450, p: 100, } def evaluate_board(board_model): 简单的局面评估函数红方为正黑方为负。 score 0 board board_model.board for row in board: for piece in row: if piece is None: continue if piece.isupper(): score PIECE_VALUES.get(piece.lower(), 0) else: score - PIECE_VALUES.get(piece.lower(), 0) return score接着实现一个伪走法生成器。为了演示我们只生成车的合法走法。要让 AI 完整可用需要把第 5.2 节的规则补全后再在这个生成器中遍历所有棋子的所有可能落点。def generate_legal_moves(board_model, turn): 生成所有合法走法。这里只演示车和炮的走法真实项目需要补全。 返回 (start_r, start_c, end_r, end_c) 元组列表。 moves [] board board_model.board piece_filter str.isupper if turn w else str.islower for r in range(10): for c in range(9): piece board[r][c] if piece is None or not piece_filter(piece): continue # 对每个方向生成所有可能的落点 for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: nr, nc r dr, c dc while 0 nr 10 and 0 nc 9: if is_valid_move(board, r, c, nr, nc, turn): moves.append((r, c, nr, nc)) # 如果是车碰到棋子后就不能再前进 if board[nr][nc] is not None: break nr dr nc dc return moves然后是最核心的搜索函数def minimax(board_model, depth, alpha, beta, maximizing): 极大极小搜索 Alpha-Beta 剪枝。 maximizingTrue 表示当前轮到红方希望分数最大。 if depth 0: return evaluate_board(board_model) turn w if maximizing else b moves generate_legal_moves(board_model, turn) if not moves: # 真实项目中需要判断是否被将死这里简单返回一个极值 return -100000 if maximizing else 100000 if maximizing: max_eval -float(inf) for move in moves: make_move(board_model, move) eval_score minimax(board_model, depth - 1, alpha, beta, False) undo_move(board_model, move) max_eval max(max_eval, eval_score) alpha max(alpha, eval_score) if beta alpha: break return max_eval else: min_eval float(inf) for move in moves: make_move(board_model, move) eval_score minimax(board_model, depth - 1, alpha, beta, True) undo_move(board_model, move) min_eval min(min_eval, eval_score) beta min(beta, eval_score) if beta alpha: break return min_eval调用搜索时需要临时修改棋盘所以还需要实现make_move和undo_movedef make_move(board_model, move): r, c, nr, nc move board_model.set_piece(nr, nc, board_model.get_piece(r, c)) board_model.set_piece(r, c, None) board_model.turn b if board_model.turn w else w def undo_move(board_model, move, captured_pieceNone): r, c, nr, nc move board_model.set_piece(r, c, board_model.get_piece(nr, nc)) board_model.set_piece(nr, nc, captured_piece) board_model.turn b if board_model.turn w else w注意undo_move需要记住被吃掉的棋子否则撤销后会丢失棋子。更干净的做法是让make_move返回被吃棋子这样undo_move可以接收它。示例代码为清晰起见做了简化实际工程中建议用栈保存每步的完整信息。最后提供一个入口函数分析当前局面并返回最佳着法def get_best_move(board_model, depth3): 返回 (best_move, best_value) 元组。 turn board_model.turn maximizing (turn w) moves generate_legal_moves(board_model, turn) best_move None best_value -float(inf) if maximizing else float(inf) for move in moves: captured board_model.get_piece(move[2], move[3]) make_move(board_model, move) value minimax(board_model, depth - 1, -float(inf), float(inf), not maximizing) undo_move(board_model, move, captured) if maximizing and value best_value: best_value value best_move move elif not maximizing and value best_value: best_value value best_move move return best_move, best_value这个 AI 的棋力非常有限因为评估函数只看子力不看位置和威胁。但作为初学者理解“搜索树如何工作”的教材它已经足够。你可以在后续逐步加入位置分值表比如“马靠边不厉害”“兵过河有价值”这些优化会让棋力肉眼可见地提升。5.4 Tkinter 界面画棋盘与响应点击最后是图形界面。使用 Python 内置的 Tkinter不需要安装第三方 GUI 库。# 文件路径xiangqi_tool/gui.py import tkinter as tk from board_model import BoardModel from rules import is_valid_move from ai_search import get_best_move PIECE_TEXT { r: 车, n: 马, b: 相, a: 仕, k: 帅, c: 炮, p: 兵, R: 車, N: 馬, B: 象, A: 士, K: 將, C: 砲, P: 卒, } class XiangqiGUI: def __init__(self, root): self.root root self.root.title(自制象棋打谱与AI分析) self.model BoardModel() self.cell_size 60 self.board_offset_x 40 self.board_offset_y 40 self.selected None # 记录了选中的 (row, col) self.canvas tk.Canvas(root, width40 8 * 60 40, height40 9 * 60 40) self.canvas.pack(sidetk.LEFT, padx10, pady10) self.info_label tk.Label(root, text轮到你走棋红方, font(Arial, 14)) self.info_label.pack(sidetk.TOP, pady10) btn_frame tk.Frame(root) btn_frame.pack(sidetk.TOP, pady5) tk.Button(btn_frame, textAI 分析, commandself.ai_analyze).pack(sidetk.LEFT, padx5) tk.Button(btn_frame, text撤销, commandself.undo_move).pack(sidetk.LEFT, padx5) tk.Button(btn_frame, text重置棋盘, commandself.reset_board).pack(sidetk.LEFT, padx5) self.canvas.bind(Button-1, self.on_click) self.draw_board() def draw_board(self): self.canvas.delete(all) # 绘制网格 for i in range(10): start_y self.board_offset_y i * self.cell_size self.canvas.create_line(self.board_offset_x, start_y, self.board_offset_x 8 * self.cell_size, start_y) for j in range(9): start_x self.board_offset_x j * self.cell_size self.canvas.create_line(start_x, self.board_offset_y, start_x, self.board_offset_y 9 * self.cell_size) # 绘制棋子 for r in range(10): for c in range(9): piece self.model.get_piece(r, c) if piece: x self.board_offset_x c * self.cell_size y self.board_offset_y r * self.cell_size self.canvas.create_oval(x - 20, y - 20, x 20, y 20, fillwhite, outlineblack, width2) color red if piece.isupper() else black self.canvas.create_text(x, y, textPIECE_TEXT[piece], fillcolor, font(Arial, 16, bold)) # 高亮选中 if self.selected: r, c self.selected x self.board_offset_x c * self.cell_size y self.board_offset_y r * self.cell_size self.canvas.create_oval(x - 24, y - 24, x 24, y 24, outlineblue, width3) def on_click(self, event): # 将像素坐标转换为棋盘坐标 c round((event.x - self.board_offset_x) / self.cell_size) r round((event.y - self.board_offset_y) / self.cell_size) if not (0 r 10 and 0 c 9): return piece self.model.get_piece(r, c) if self.selected is None: # 第一次点击选中棋子 if piece is not None: self.selected (r, c) self.draw_board() else: sr, sc self.selected if (sr, sc) (r, c): self.selected None self.draw_board() return # 尝试走棋 if is_valid_move(self.model.board, sr, sc, r, c, self.model.turn): self.model.set_piece(r, c, piece) self.model.set_piece(sr, sc, None) self.model.move_history.append((sr, sc, r, c, piece)) self.model.turn b if self.model.turn w else w self.selected None self.draw_board() self.update_info() else: # 如果点击的是自己的棋子则切换选中 if piece is not None and self.model.turn (w if piece.isupper() else b): self.selected (r, c) self.draw_board() def ai_analyze(self): move, value get_best_move(self.model, depth2) if move: info fAI 建议走法({move[0]},{move[1]}) - ({move[2]},{move[3]})评估分 {value:.2f} self.info_label.config(textinfo) def undo_move(self): if self.model.move_history: sr, sc, r, c, captured self.model.move_history.pop() self.model.set_piece(sr, sc, self.model.get_piece(r, c)) self.model.set_piece(r, c, captured) self.model.turn b if self.model.turn w else w self.selected None self.draw_board() self.update_info() def reset_board(self): self.model BoardModel() self.selected None self.draw_board() self.update_info() def update_info(self): turn_text 红方 if self.model.turn w else 黑方 self.info_label.config(textf轮到 {turn_text} 走棋) if __name__ __main__: root tk.Tk() app XiangqiGUI(root) root.mainloop()运行这个 GUI 脚本你会看到棋盘窗口点击棋子和目标位置就能走棋。右上角的按钮可以触发 AI 分析、撤销和重置。5.5 导入 PGN 棋谱解析与回放如果你不想每次都手动摆棋可以加一个“导入 PGN”功能。下面是一个极简的 PGN 解析器只提取回合和着法不做变例处理# 文件路径xiangqi_tool/pgn_parser.py import re def parse_pgn_moves(pgn_text: str): 从 PGN 文本中提取着法列表。 只处理中文着法例如 炮二平五 马8进7。 返回一个字符串列表每个元素是一步着法。 # 去掉标签对 text re.sub(r\[.*?\], , pgn_text) # 去掉注释 text re.sub(r\{.*?\}, , text) # 去掉变例圆括号内容初学者先只保留主线 text re.sub(r\(.*?\), , text) # 分割所有 token tokens text.split() moves [] for token in tokens: token token.strip() if not token: continue # 跳过回合号如 1. 1... if re.match(r^\d\.*$, token): continue # 跳过结果符号 if token in (*, 1-0, 0-1, 1/2-1/2): continue moves.append(token) return moves有了着法列表你可以从初始局面开始逐条执行。不过要“执行中文着法”还需要一个把中文着法转换成棋盘坐标的函数这部分逻辑相对繁琐需要知道棋子类型、起始行列规则、目标位置。一个更简单的替代方案是让 AI 搜索找到“与中文着法相同落点”的走法从而确定起终点。这种方案虽然效率低但代码量少很多。更现实的建议是初期先不追求完整中文着法解析而是手动通过 GUI 走棋然后把每一步记录为内部数据结构保存时再转换成自己的棋谱格式。等到核心功能都稳定了再回头补 PGN 导入导出。6. 运行结果与效果验证在终端运行 GUI 程序python gui.py预期效果一个 9×10 的棋盘窗口打开红方棋子显示为红色汉字黑方为黑色汉字。点击红方棋子再点击合法空位棋子移动轮换到黑方。点击“AI 分析”信息栏会显示建议走法和评估分数且该分数为红方视角分数。点击“撤销”上一步走棋被回退。如果运行失败按以下顺序排查在终端运行python board_model.py确认 FEN 解析和序列化正常。在终端运行python -c from rules import is_valid_move; print(ok)确认规则模块没有语法错误。单独运行python ai_search.py看搜索函数是否报错。如果递归深度过大导致速度慢先把depth改为 1。确保 GUI 脚本和模块在同一个目录下否则需要调整import路径。7. 常见问题与排查思路问题现象可能原因排查方式解决方案棋盘显示上下颠倒FEN 解析时行顺序反转打印_parse_fen的结果对照初始 FEN检查START_FEN中第一行应是黑方底线生成列表时不要 reverse点击棋子没有反应选中的棋子不属于当前回合玩家检查on_click中 turn 判断逻辑在选中时打印棋子大小写和当前 turn确认条件分支走棋提示非法走棋规则函数写错单独测试is_valid_move使用固定局面逐个测试车、炮、马规则特别注意炮吃子的炮架逻辑AI 分析耗时过长搜索深度太大走法生成没有剪枝打印generate_legal_moves的数量降低 depth或先实现 Alpha-Beta 剪枝再增加深度撤销后棋子丢失undo_move没有恢复被吃棋子检查撤销前是否保存了 captured在走棋时保存被吃棋子并在撤销时恢复Tkinter 窗口显示黑块字体或图形库问题尝试换一个中文字体在create_text中指定系统支持的字体如 Microsoft YaHei8. 最佳实践与工程建议现在项目基础功能已经能跑最后分享一些工程建议帮助你把“玩具项目”提升到“可维护的小工具”水平。8.1 先把规则写完整再扩展 AI我见过很多初学者兴致勃勃写 AI结果 AI 搜索时的走法生成器只覆盖了一半规则导致搜索到非法局面评分完全失真。建议按“车 → 炮 → 马 → 兵 → 帅 → 仕 → 相”的顺序把走法规则完整实现并且为每个棋子写几个单测用例。比如炮的“隔子吃子”、马的“蹩马腿”、帅的“九宫限制”、兵的“过河前后方向变化”这些边界条件最容易出错。8.2 记录走棋历史时不要只记录起终点在 GUI 中我为了简洁只记录了起终点和被吃棋子。但真实打谱软件需要记录更多信息走棋时间、注释、局面评估分、变例标记。一个更健壮的设计是定义一个Move类包含起终点、棋子类型、被吃棋子、是否将军、是否吃子等字段。这样后续做棋谱分析、导出 PGN 都会方便很多。8.3 评估函数是 AI 棋力的上限搜索深度是下限如果你的评估函数只数棋子数量那 AI 再深也没有大局观。初学者可以逐步加入以下因子棋子位置价值表。例如马在河沿价值高炮在中路价值高老兵到底线的兵价值低。棋子机动性。计算当前棋子的合法走法数量作为位置分的一部分。威胁与保护。一个棋子被对方攻击、但被己方保护时可以小幅调整分数。这些优化都不需要深度学习但要理解“搜索 评估”是传统棋类 AI 的核心公式。8.4 外部引擎接入时注意输入输出格式如果你后续不满足于自写 AI想接入更强的引擎传统做法是通过 UCI 协议通信。你需要把当前局面转成引擎能接受的格式中象引擎通常使用 FEN 变体或自定义格式然后启动子进程发送position和go指令解析引擎返回的info和bestmove。这一步的坑大多在格式转换上建议先用命令行手动验证引擎能读懂你的 FEN再写代码。8.5 界面与逻辑分离虽然本文 GUI 和逻辑放在同一个目录下但你在后续扩展时应该坚持“界面层不直接操作棋盘数组”的原则。比如把BoardModel作为一个纯逻辑对象所有走棋都通过它的方法执行UI 只负责调用和绘制。这样将来你换一个 GUI 框架比如 PyQt 或 Web 前端逻辑层完全不需要重写。8.6 版本管理与打包项目虽然小但建议从一开始就使用 Git 管理这样你修改规则或 AI 后可以随时回退。如果想把程序分享给不会 Python 的朋友可以用 PyInstaller 打包成可执行文件。打包时需要包含所有模块文件并注意 Tkinter 图标和资源文件的路径。9. 总结与后续学习方向走到这里你已经拥有一个能在本地运行的中国象棋打谱与 AI 分析软件雏形。它支持点击走棋、撤销、重置、简单 AI 分析和基础棋谱数据模型。虽然距离商业级打谱软件还很远但你已经亲手搭建了它的所有核心骨架专用数据格式、交互界面、搜索算法和评估函数。这些知识完全迁移到国际象棋、围棋或其它棋盘游戏项目中。下一步的实践路径我建议按难度递增来选如果对规则更感兴趣把帅、仕、相、兵的走法规则补完整并加入将军、将死、困毙判断。这些内容会极大提升你对棋盘状态机的理解。如果对算法更感兴趣把研究重点放在评估函数上尝试使用简单的“位置分值表”改善棋力然后观察搜索深度从 2 增加到 4 时棋力和耗时的变化。如果对工程组织更感兴趣为项目补充 PGN 文件导入导出、棋谱收藏夹、备注功能并增加单元测试。最后提醒一句在做这个项目时别被“AI”这个词吓住。真正的价值不在于你能让程序有多强大而在于你能完整地理解它每一步在做什么。祝编码顺利棋盘常新。