Python实战:打造象棋打谱与AI分析桌面小软件 初学 Python 想做点带“AI 味”的小项目象棋打谱加分析是一个性价比很高的方向既有图形界面又有数据交互还能把搜索算法、局面评估这些 AI 基础概念串起来。市面上的象棋软件虽然很多但有的带广告有的交互不顺手有的功能封闭不方便自定制。自己动手做一个既能按自己习惯整理棋谱也能顺便把 AI 搜索原理打通。这篇文章会从零开始带着你完成一个“象棋打谱 AI 分析”的桌面小软件。整体用 Python 开发界面用 Tkinter棋盘和走法合法性用 python-chess 库处理AI 部分先实现一个极简启发式搜索再预留外部 UCCI 引擎接入位置。文章会尽量使用初学者能看懂的写法涉及的关键概念、坐标转换、搜索原理、常见报错都会展开说明。如果你有 Python 基础只是没写过完整小项目这篇文章正好适合你。学完以后你会得到一个能正常打开、点击走子、记录棋谱的棋盘程序一个能“摆棋、复盘、撤销”的基础打谱流程一个能给出推荐着法和评估分的简单 AI如何接入更专业象棋引擎做深度分析的思路。1. 自制象棋软件从哪里开始先把概念捋清楚1.1 “打谱”到底是在打什么“打谱”是中国象棋领域的一个常用词本质是“按棋谱摆棋、走棋、复盘”。棋谱记录了双方每一步的落子顺序比如“炮二平五、马八进七”这类中文记谱或者更底层的坐标移动字符串。自己做打谱软件核心要解决的问题有三个如何用程序表示棋盘上的局面如何判断一个走法是否合法如何把一组走法保存下来并支持回放。第一个问题看起来简单但如果不做封装很容易写出大量重复的棋盘数组操作。比如要判断“马走日”“蹩马腿”“炮需要隔子吃”这些规则手写起来工作量不小。因此这里推荐借助现成的棋盘库把更多精力放在界面和 AI 分析上。1.2 AI 分析在象棋中做了什么事情象棋 AI 分析简单说就是让程序从当前局面出发计算哪一步更有可能取得优势。传统象棋 AI 的基本流程是生成当前局面下所有合法走法对每个走法后的局面进行评估通过搜索算法往前多看几步找到综合评分最高的走法。其中“评估”需要衡量子力价值、位置控制、将军威胁等信息“搜索”通常使用 minimax 搜索或带 alpha-beta 剪枝的优化版本。初学者可以先从“子力价值 一层搜索”入手后面再慢慢加深。可以说象棋 AI 是一个很好的入门项目因为它把数据结构、递归、评估函数、优化剪枝这些基础点非常自然地结合在一起。1.3 为什么这个技术组合适合初学者很多初学者一想到写象棋软件就打算用 C 和 Qt或者用网页 Canvas 实现。其实用 Python Tkinter 更合适Python 语法简单适合快速验证思路Tkinter 是 Python 自带的标准库不需要额外安装 GUI 框架python-chess 支持多种象棋变体其中就包括中国象棋可以帮我们处理大部分规则问题代码规模可控即使只写几百行也能做出一个像模像样的桌面程序。这篇教程选择的路线是“先会用库再理解底层”。我们不需要自己写完整的象棋规则引擎但需要理解棋局数据是怎么表示的这样才能在界面和 AI 模块之间顺畅对接。2. 环境准备与项目结构2.1 Python 与依赖安装本文示例以 Python 3.9 以上版本为主。Tkinter 通常随 Python 一起提供如果你在命令行中执行下面的代码没有报错说明 Tkinter 可用python -c import tkinter如果提示找不到 tkinter说明当前 Python 环境没有安装 GUI 组件。Windows 上安装 Python 时记得勾选“tcl/tk and IDLE”Ubuntu 或 Debian 可以通过包管理器补装sudo apt install python3-tk接下来安装 python-chess 库pip install python-chess这里没有固定版本号因为 python-chess 在后续版本中才加入了中国象棋变体支持。如果你的环境版本较旧导入时发现不支持variantxiangqi建议先升级到最新版本pip install -U python-chess如果只想做一个纯本地的打谱工具到这里依赖就足够了。想连外部引擎做深度分析还要准备一个支持中国象棋的 UCCI 或 UCI 引擎具体我们会在第 6 节说明。2.2 项目目录设计为了避免所有代码堆在一个文件里建议按功能拆分成下面几个文件xiangqi-studio/ ├── main.py # 程序入口负责创建 Tkinter 窗口 ├── game.py # 游戏状态管理保存当前局面和棋谱 ├── board_view.py # 棋盘绘制与点击交互可并入 main.py ├── ai.py # 内置简单 AI评估 搜索 ├── engine_client.py # 外部 UCCI 引擎客户端 └── requirements.txt # 依赖说明对于初学者来说项目结构不用过于复杂。你可以先把board_view.py的内容写在main.py里等代码变长后再拆分。不过从第一篇项目代码开始就养成模块化习惯后面会省很多事。2.3 版本与兼容性提示本文涉及的棋盘库、引擎、GUI 组件都在持续更新文章中的代码表示的是“常见环境下的示例”不一定能原样跑通所有版本。如果你运行时报错先检查两个地方一是 python-chess 版本是否够新二是引擎路径是否设置正确。3. 用 python-chess 封装棋盘与走法3.1 初始化中国象棋棋盘python-chess 最初主要支持国际象棋后来加入了变体功能其中中国象棋可以用variantxiangqi创建。先看一个最简单的例子import chess board chess.Board(variantxiangqi) print(board)运行后控制台会输出一个 10 行 9 列的棋盘红方在下黑方在上。这就是 python-chess 对中国象棋变体的内置初始局面。如果你的 python-chess 版本不支持variantxiangqi也可以尝试使用import chess.variant board chess.variant.XiangqiBoard()两种写法的效果类似具体使用哪一种以你本机库版本实际支持的 API 为准。3.2 合法走法与坐标系统中国象棋的棋盘是 9 列 10 行。在 python-chess 中列通常用字母 a 到 i 表示行用数字 0 到 9 表示。一个走法由“起点方格 终点方格”组成比如h2e2表示红方从 h2 走到 e2相当于“炮二平五”的中文记谱。获取当前局面的所有合法走法很简单for move in board.legal_moves: print(move.uci())判断一个走法是否合法可以直接用成员运算符move chess.Move.from_uci(h2e2) if move in board.legal_moves: board.push(move) print(走法合法已执行)这里要注意board.push()会修改棋盘状态执行后棋子会真正移动。如果只是试探一个走法而不想真正落子可以先复制棋盘副本或者走之后再pop()退回来。3.3 封装一个 Game 类为了让 UI 层和 AI 层都使用同一套状态管理我们可以封装一个简单的Game类负责保存棋谱、前进和后退。# 文件路径game.py import chess class Game: def __init__(self): self.move_list [] # 保存所有走过的 UCI 字符串 self.current 0 # 当前显示到第几步 self.board self._create_board() staticmethod def _create_board(): return chess.Board(variantxiangqi) def reset(self): self.move_list.clear() self.current 0 self.board self._create_board() def apply_move(self, uci_move): move chess.Move.from_uci(uci_move) if move not in self.board.legal_moves: return False # 如果当前不是最后一手说明用户从中途开始改棋需要截断后面的谱 self.move_list self.move_list[:self.current] [uci_move] self.current 1 self.board.push(move) return True def go_to(self, index): 定位到第 index 步支持前进和后退。 if index 0 or index len(self.move_list): return self.current index self.board self._create_board() for uci in self.move_list[:self.current]: self.board.push(chess.Move.from_uci(uci)) def undo(self): self.go_to(self.current - 1) def redo(self): self.go_to(self.current 1) def analyze_moves(self): 返回当前回放位置的棋谱列表方便界面展示。 return self.move_list[:self.current]封装之后Tkinter 界面只需要调用apply_move()下棋调用undo()/redo()回放不用关心棋盘底层细节。这对初学者来说可以明显减少“状态不同步”的 bug。4. Tkinter 棋盘界面从网格到可点击棋子4.1 绘制棋盘底图Tkinter 自带的 Canvas 组件很适合画棋盘。核心思路是先确定格子大小和边距再根据行列坐标计算像素坐标。中国象棋棋盘共 9 列 10 行如果每个格子宽度是 64 像素那么棋盘总宽大约是 8 × 64总高大约是 9 × 64。为了方便可以在棋盘四周留出 40 像素边距。下面是一个最简单的棋盘绘制代码# 文件路径board_view.py核心片段 import tkinter as tk CELL 64 MARGIN 40 BOARD_COLS 9 BOARD_ROWS 10 def draw_board(canvas): canvas.delete(all) w (BOARD_COLS - 1) * CELL h (BOARD_ROWS - 1) * CELL x0, y0 MARGIN, MARGIN # 画竖线 for col in range(BOARD_COLS): canvas.create_line(x0 col * CELL, y0, x0 col * CELL, y0 h) # 画横线 for row in range(BOARD_ROWS): canvas.create_line(x0, y0 row * CELL, x0 w, y0 row * CELL) # 河界文字 canvas.create_text(x0 w / 2, y0 4.5 * CELL, text楚河 汉界, font(微软雅黑, 18, bold), fill#8B4513)这里没有画炮位、兵位标记和九宫斜线不影响后续功能。如果你想更还原棋盘可以继续添加create_line和create_oval绘制细节。4.2 绘制棋子棋盘底图画好后需要把 python-chess 中的棋盘状态显示到界面上。遍历board.piece_map()可以拿到每个格子上的棋子PIECE_TEXT { # 红方 K: 帅, A: 仕, B: 相, N: 马, R: 车, C: 炮, P: 兵, # 黑方 k: 将, a: 士, b: 象, n: 马, r: 车, c: 炮, p: 卒, } def draw_pieces(canvas, board, selectedNone): canvas.delete(piece) for square, piece in board.piece_map().items(): col chess.square_file(square) row chess.square_rank(square) x MARGIN col * CELL y MARGIN row * CELL symbol piece.symbol() text PIECE_TEXT.get(symbol, symbol) color #B22222 if piece.color chess.WHITE else #000000 canvas.create_oval(x - 24, y - 24, x 24, y 24, fill#F5DEB3, outline#8B4513, width2, tagspiece) canvas.create_text(x, y, texttext, font(微软雅黑, 20, bold), fillcolor, tagspiece) # 高亮当前选中的棋子 if selected is not None: x MARGIN chess.square_file(selected) * CELL y MARGIN chess.square_rank(selected) * CELL canvas.create_oval(x - 27, y - 27, x 27, y 27, outline#00FF00, width3, tagspiece)代码中chess.square_file()和chess.square_rank()用于把方格编号转换成行列。注意PIECE_TEXT中的符号映射基于 python-chess 内部表示如果你的库版本符号不同打印piece.symbol()后自行调整即可。4.3 点击走子与打谱操作点击处理是整个界面最关键的部分。基本交互逻辑是第一次点击选中己方棋子第二次点击如果目标位置是合法落点则移动如果点击的是己方另一颗棋子则切换选中。def on_click(event): if event.x MARGIN or event.y MARGIN: return col round((event.x - MARGIN) / CELL) row round((event.y - MARGIN) / CELL) if not (0 col 9 and 0 row 10): return square chess.square(col, row) piece game.board.piece_at(square) if selected is None: # 必须先选中己方棋子 if piece and piece.color game.board.turn: selected square draw_pieces(canvas, game.board, selected) else: move chess.Move(selected, square) if move in game.board.legal_moves: game.apply_move(move.uci()) selected None draw_board(canvas) draw_pieces(canvas, game.board) record_text.insert(tk.END, move.uci() ) else: # 如果点的是另一颗己方棋子重新选中 if piece and piece.color game.board.turn: selected square draw_pieces(canvas, game.board, selected) else: selected None draw_pieces(canvas, game.board)这里有一个值得留意的细节点击位置不一定落在格子正中心。用round()将像素坐标四舍五入到最近格子能有效避免“点边缘但选中错格子”的问题。4.4 棋谱回放撤销、重做打谱软件一个重要的功能是“看棋谱回到某一步”。在Game类中undo()和redo()已经封装好了界面只需要绑定按钮事件def on_undo(): game.undo() draw_board(canvas) draw_pieces(canvas, game.board) update_record_text() def on_redo(): game.redo() draw_board(canvas) draw_pieces(canvas, game.board) update_record_text()update_record_text()可以把当前回放位置的棋谱显示到Text组件中方便用户看到当前走到第几步。到这里一个能“摆棋、走棋、撤销、重做”的基础打谱软件已经形成了。接下来要解决的是“AI 分析”部分。5. 内置简单 AI从启发式评估到极小极大搜索5.1 评估函数给局面打分AI 要“分析局面”第一步是给局面一个数值。最简单的评估方法是计算双方子力价值差。红方分数高说明红方占优黑方分数高说明黑方占优。# 文件路径ai.py PIECE_SCORE { K: 10000, A: 300, B: 300, N: 400, R: 700, C: 500, P: 100, k: -10000, a: -300, b: -300, n: -400, r: -700, c: -500, p: -100, } def evaluate(board): 返回红方视角的粗略评估分数。 score 0 for piece in board.piece_map().values(): score PIECE_SCORE.get(piece.symbol(), 0) if board.is_checkmate(): # 当前走棋方已经被将死 return -100000 if board.turn else 100000 return score这里的棋子分值用的是常见参考值车 700炮 500马 400兵 100仕相 300。中国象棋中车的价值通常最高炮和马的配合也很重要初学者可以先用这套分值体验效果。piece.symbol()返回的是棋盘内部符号如果和实际不一致用print(piece.symbol())排查一下再调整字典键名即可。5.2 极小极大搜索往前多算几步只评估一步AI 只能看到“吃了什么子”看不到后续反击。通过递归搜索可以让 AI 往前多看几步。下面是一个极简的极小极大搜索def search(board, depth): if depth 0 or board.is_game_over(): return evaluate(board) best -float(inf) for move in board.legal_moves: board.push(move) score -search(board, depth - 1) board.pop() if score best: best score return best def best_move(board, depth2): best None best_score -float(inf) for move in board.legal_moves: board.push(move) score -search(board, depth - 1) board.pop() if score best_score: best_score score best move return best, best_score这段代码的核心逻辑是先模拟对手走出一步然后递归看对手的应对再用正负号交替表示“红方优势”和“黑方优势”。如果红方行动我们希望分数高如果黑方行动黑方也会选择对自己最有利、也就是对红方最不利的走法所以用负数取反。对于初学者来说深度设置为 2 或 3 才能保证速度。如果你把深度调到 4 以上会明显感觉到计算变慢。性能优化不在本文讨论范围简单理解成“搜索深度越深算得越准也越慢”即可。5.3 把这个 AI 接进软件在 Tkinter 界面里加一个“AI 分析”按钮点击后调用best_move()并显示推荐走法def on_analyze(): move, score best_move(game.board, depth2) info_label.config( textfAI 推荐走法: {move.uci()}评估分数: {score:.2f} )这样用户每走一步都可以点“AI 分析”程序会基于当前局面给出一个参考建议。虽然这个 AI 很“初级”但它已经具备完整流程合法走法生成、局面评估、递归搜索、推荐展示。理解了这一步后面接触专业引擎会更容易。6. 接外部 UCCI 引擎让分析更可靠6.1 UCCI 协议基本概念内置简单 AI 适合学习和演示但真要对棋局做深度复盘还需要接入专业象棋引擎。中国象棋引擎常用的协议是 UCCI可以看成是 UCI 协议的中国象棋变体。UCCI 交互方式一般如下通过命令行启动引擎程序通过标准输入发送命令引擎通过标准输出返回状态和分析结果。常见的命令包括ucci、isready、position、go等。不同引擎对命令细节可能有差异接入前最好先看一下引擎自带的说明文档。6.2 最小引擎客户端示例可以使用 Python 的subprocess模块启动外部引擎进程。下面是一个不完整但思路清晰的示例# 文件路径engine_client.py思路演示 import subprocess class UCCIEngine: def __init__(self, engine_path): self.process subprocess.Popen( [engine_path], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, textTrue, encodingutf-8, ) self.send(ucci) def send(self, command): self.process.stdin.write(command \n) self.process.stdin.flush() def analyze(self, board, depth12): moves .join(m.uci() for m in board.move_stack) self.send(position startpos moves moves) self.send(fgo depth {depth}) # 这里需要根据引擎返回格式解析 bestmove # 示例不做完整解析只演示发送过程实际项目里你需要使用一个后台线程读取引擎输出并设置超时时间防止引擎卡住导致 GUI 无响应。初学者如果暂时接入失败可以先用内置 AI 完成主要功能外部引擎作为后续进阶优化。6.3 外部引擎接入的注意点引擎文件要下载到本地路径中尽量不要包含中文和空格不要在主线程里阻塞等待引擎结果应使用子线程或异步读取如果引擎协议不是标准 UCCI需要先阅读引擎文档分析超时后要合理结束引擎进程避免残留。接入外部引擎后软件的能力会从“初学者版的几步搜索”升级到“专业级的深度分析”复盘体验会好很多。7. 完整运行流程与预期效果7.1 启动软件把所有代码整理好后在命令行执行python main.py运行后会出现一个 Tkinter 窗口画面中央是 9 列 10 行的中国象棋棋盘红黑双方棋子按初始位置摆放。7.2 打谱流程在棋盘上点击红方棋子再点击目标位置棋步会生效并记录到右侧棋谱区域。点击“撤销”按钮可以回退上一步“重做”按钮可以回到刚才撤销的位置。如果中途修改棋谱后续棋步会被自动截断这是打谱软件的正常行为。7.3 AI 分析流程点击“AI 分析”按钮程序会计算当前局面下的推荐走法。以深度 2 为例几秒钟内通常会返回结果。棋力虽然一般但足够演示“AI 如何分析局面”的完整链路。如果要接入更强的引擎把引擎路径配置到UCCIEngine类中再通过类似按钮触发分析即可。8. 常见问题与排查思路问题现象常见原因解决思路导入 chess 后无法使用variantxiangqipython-chess 版本较旧不支持中国象棋变体升级 python-chess 到最新版本改用chess.variant.XiangqiBoard()运行后提示 Tkinter 不存在Python 环境缺少 GUI 组件重新安装带 Tk 的 Python或在系统安装python3-tk点击棋子没有反应选中棋子颜色不是当前行棋方坐标转换不对打印点击行列和board.turn调试检查