初学 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 在后续版本中才加入了中国象棋变体支持。如果你的环境版本较旧,导入时发现不支持variant="xiangqi",建议先升级到最新版本:
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 最初主要支持国际象棋,后来加入了变体功能,其中中国象棋可以用variant="xiangqi"创建。
先看一个最简单的例子:
import chess board = chess.Board(variant="xiangqi") print(board)运行后,控制台会输出一个 10 行 9 列的棋盘,红方在下,黑方在上。这就是 python-chess 对中国象棋变体的内置初始局面。
如果你的 python-chess 版本不支持variant="xiangqi",也可以尝试使用:
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(variant="xiangqi") 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, selected=None): 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", width=2, tags="piece") canvas.create_text(x, y, text=text, font=("微软雅黑", 20, "bold"), fill=color, tags="piece") # 高亮当前选中的棋子 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", width=3, tags="piece")代码中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, depth=2): 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, depth=2) info_label.config( text=f"AI 推荐走法: {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], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8", ) self.send("ucci") def send(self, command): self.process.stdin.write(command + "\n") self.process.stdin.flush() def analyze(self, board, depth=12): moves = " ".join(m.uci() for m in board.move_stack) self.send("position startpos moves " + moves) self.send(f"go 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 后无法使用variant="xiangqi" | python-chess 版本较旧,不支持中国象棋变体 | 升级 python-chess 到最新版本,改用chess.variant.XiangqiBoard() |
| 运行后提示 Tkinter 不存在 | Python 环境缺少 GUI 组件 | 重新安装带 Tk 的 Python,或在系统安装python3-tk |
| 点击棋子没有反应 | 选中棋子颜色不是当前行棋方;坐标转换不对 | 打印点击行列和board.turn调试;检查 |