ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python实现工程级中国象棋AI:从局面表示到Alpha-Beta搜索

Python实现工程级中国象棋AI:从局面表示到Alpha-Beta搜索 简介这是一份面向Python初学者与AI入门开发者的学习型中国象棋AI项目源码聚焦策略类游戏的智能决策实现可用于课程设计、算法实践或兴趣拓展。资源共43个文件含10个核心Python脚本如Chessboard.py、Chessman.py、Point.py及UI交互模块cli_game.py、31张GIF/JPG素材涵盖红黑双方棋子、棋盘界面与动态效果以及readme.txt说明文档和.gitignore配置文件整体压缩包仅805KB轻量易部署。已有397人学习下载体现了社区对轻量级AI实战案例的持续关注。读者可完整获取模块化代码结构Chess_Core封装棋规与数据结构Chess_AI实现评估函数与搜索逻辑Chess_UI提供命令行交互入口所有源码均采用面向对象设计辅以清晰注释与分层目录便于理解AI决策链路并二次开发扩展功能。1. 为什么用 Python 写中国象棋 AI 不是“玩具项目”而是工程级落地方案很多人第一次看到“基于Python语言开发的中国象棋AI设计源码”这个标题下意识会想Python 做游戏AI性能不够吧是不是学生课设真能下赢人——这恰恰是最大误解的起点。我带过三届校队AI对弈系统开发也给两家棋牌类SaaS公司做过引擎层重构结论很明确Python 不是不能做专业级象棋AI而是它在「算法验证→策略迭代→工程封装→跨平台部署」这条链路上综合效率远超C原生开发。关键不在单步搜索速度而在你能否在2小时内把一个新启发式评估函数比如加入“士象全残局权重衰减”写完、测通、集成进Alpha-Beta剪枝框架并在本地和树莓派上同步跑通。真实场景里90%的翻车不是因为Python慢而是评估函数逻辑错、局面表示漏判将帅照面、或走法生成没覆盖“马腿被别炮隔山打牛”的复合约束。本篇不讲抽象博弈论只拆解一个可立即 clone、改参数、加规则、跑出胜率曲线的最小可行象棋AI系统——从零构建一个支持标准规则、带局面哈希缓存、可调搜索深度、输出PGN日志、且能在Windows/macOS/Linux三端无依赖运行的Python象棋AI源码骨架。适合刚学完Python基础、想动手理解AI决策机制的工程师也适合需要快速嵌入AI陪练模块的产品团队。2. 从零搭建可运行的象棋AI核心骨架局面表示、走法生成与基础搜索2.1 用二维数组枚举定义棋盘状态为什么不用类嵌套而选扁平结构中国象棋的9×10棋盘天然适配二维数组但关键在于如何编码“红黑方”“兵种”“是否存活”“是否被将军”四重状态。常见错误是过度面向对象为每个棋子建一个Piece类再搞Color枚举、Type枚举、Status类……结果内存暴涨、遍历变慢、序列化困难。我们采用工业级轻量方案单整数编码 位运算分离。# chess/core.py from enum import IntEnum class Piece(IntEnum): EMPTY 0 RED_KING 1 RED_ADVISOR 2 RED_ELEPHANT 3 RED_HORSE 4 RED_CHARIOT 5 RED_CANNON 6 RED_SOLDIER 7 BLACK_KING -1 BLACK_ADVISOR -2 BLACK_ELEPHANT -3 BLACK_HORSE -4 BLACK_CHARIOT -5 BLACK_CANNON -6 BLACK_SOLDIER -7 # 棋盘9列×10行索引[0][0]为红方左下角红帅初始位 BOARD_SIZE (10, 9) # 行×列符合numpy习惯 INIT_BOARD [ [-5, -4, -3, -2, -1, -2, -3, -4, -5], # 黑方底线 [ 0, 0, 0, 0, 0, 0, 0, 0, 0], [-6, 0, 0, 0, 0, 0, 0, 0, -6], [-7, 0, -7, 0, -7, 0, -7, 0, -7], [ 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 7, 0, 7, 0, 7, 0, 7, 0, 7], [ 6, 0, 0, 0, 0, 0, 0, 0, 6], [ 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 5, 4, 3, 2, 1, 2, 3, 4, 5], # 红方底线 ]逻辑说明用正负号区分红黑红为正黑为负绝对值对应Piece枚举。这样board[r][c] 0即红方棋子abs(board[r][c]) Piece.RED_SOLDIER可快速判断兵种。避免字典查表或isinstance判断CPU缓存友好。参数说明BOARD_SIZE固定为(10,9)不可修改所有坐标计算如马走日、象飞田均基于此INIT_BOARD是标准开局直接赋值初始化不走构造函数。2.2 走法生成器覆盖全部16种特殊规则重点处理“将帅照面”与“炮隔山打牛”走法生成是象棋AI最易出错的模块。网上90%的Python象棋源码在此翻车忽略“将帅不能照面”即同一直线无子间隔时禁止移动、误判“炮吃子必须隔一子”、漏掉“马腿被别”马走日时“日”字中心点有子则禁走。我们采用分兵种预生成 全局合法性后验双保险策略# chess/move_gen.py def generate_moves(board: List[List[int]], is_red_turn: bool) - List[Tuple[int, int, int, int]]: 返回所有合法走法列表(from_r, from_c, to_r, to_c) moves [] for r in range(10): for c in range(9): piece board[r][c] if piece 0 or (is_red_turn and piece 0) or (not is_red_turn and piece 0): continue # 非己方棋子跳过 moves.extend(_gen_piece_moves(board, r, c, piece, is_red_turn)) # 后验过滤剔除导致将被吃的走法即走完后己方将处于被将军状态 valid_moves [] for move in moves: new_board make_move(board, move) if not is_in_check(new_board, is_red_turn): valid_moves.append(move) return valid_moves def _gen_piece_moves(board, r, c, piece, is_red_turn): 按兵种生成基础走法未过滤将军 moves [] abs_p abs(piece) if abs_p Piece.RED_KING or abs_p Piece.BLACK_KING: moves.extend(_gen_king_moves(board, r, c, is_red_turn)) elif abs_p in (Piece.RED_ADVISOR, Piece.BLACK_ADVISOR): moves.extend(_gen_advisor_moves(board, r, c, is_red_turn)) elif abs_p in (Piece.RED_ELEPHANT, Piece.BLACK_ELEPHANT): moves.extend(_gen_elephant_moves(board, r, c, is_red_turn)) elif abs_p in (Piece.RED_HORSE, Piece.BLACK_HORSE): moves.extend(_gen_horse_moves(board, r, c, is_red_turn)) elif abs_p in (Piece.RED_CHARIOT, Piece.BLACK_CHARIOT): moves.extend(_gen_chariot_moves(board, r, c, is_red_turn)) elif abs_p in (Piece.RED_CANNON, Piece.BLACK_CANNON): moves.extend(_gen_cannon_moves(board, r, c, is_red_turn)) elif abs_p in (Piece.RED_SOLDIER, Piece.BLACK_SOLDIER): moves.extend(_gen_soldier_moves(board, r, c, is_red_turn)) return moves def _gen_chariot_moves(board, r, c, is_red_turn): 车横竖直线遇子停止 moves [] directions [(0,1), (0,-1), (1,0), (-1,0)] for dr, dc in directions: nr, nc r dr, c dc while 0 nr 10 and 0 nc 9: if board[nr][nc] 0: moves.append((r, c, nr, nc)) else: if (is_red_turn and board[nr][nc] 0) or (not is_red_turn and board[nr][nc] 0): moves.append((r, c, nr, nc)) # 可吃子 break nr dr nc dc return moves逻辑说明_gen_chariot_moves示例展示了“直线扫描遇子终止”模式所有长距离棋子车、炮、马均按此范式实现。_gen_cannon_moves需额外记录“隔山”计数遇到第一个子记为“山”之后再遇子才可吃代码略长故未展开但逻辑完全可复现。参数说明is_red_turn决定当前轮到哪方影响吃子判断红方只能吃黑子所有坐标(r,c)均为0-basedr0是红方底线帅初始行r9是黑方底线将初始行。2.3 实现带Alpha-Beta剪枝的极小化极大搜索控制搜索深度与时间预算纯Minimax在象棋中不可行——分支因子平均约35深度5即35⁵≈5200万节点。必须引入Alpha-Beta剪枝。但Python版常犯两个错误1剪枝条件写反alpha/beta更新顺序错2未设深度限制导致无限递归。我们采用迭代加深 时间熔断双保险# chess/search.py import time from typing import Tuple, Optional def search_best_move(board: List[List[int]], is_red_turn: bool, max_depth: int 3, time_limit: float 1.0) - Optional[Tuple[int, int, int, int]]: 返回最佳走法超时则返回当前已知最优解 start_time time.time() best_move None alpha, beta float(-inf), float(inf) # 迭代加深从深度1开始逐步加深确保总有解 for depth in range(1, max_depth 1): if time.time() - start_time time_limit * 0.8: # 预留20%时间收尾 break score, move _alphabeta(board, is_red_turn, depth, alpha, beta, start_time, time_limit) if move is not None: best_move move alpha max(alpha, score) # 更新alpha用于下一轮 return best_move def _alphabeta(board, is_red_turn, depth, alpha, beta, start_time, time_limit) - Tuple[float, Optional[Tuple[int,int,int,int]]]: if time.time() - start_time time_limit: return 0.0, None # 熔断 if depth 0 or is_game_over(board): return evaluate_board(board, is_red_turn), None moves generate_moves(board, is_red_turn) if not moves: return -10000 if is_red_turn else 10000, None # 将死 best_score float(-inf) if is_red_turn else float(inf) best_move None for move in moves: new_board make_move(board, move) score, _ _alphabeta(new_board, not is_red_turn, depth-1, alpha, beta, start_time, time_limit) if is_red_turn: if score best_score: best_score score best_move move alpha max(alpha, score) else: if score best_score: best_score score best_move move beta min(beta, score) if beta alpha: # 剪枝点beta剪枝Max层或alpha剪枝Min层 break return best_score, best_move逻辑说明search_best_move外层用迭代加深ID确保即使深度3超时也能返回深度2的可靠解内层_alphabeta严格遵循“Max层更新alphaMin层更新betabeta≤alpha时剪枝”原则。注意make_move需深拷贝棋盘用copy.deepcopy或[row[:] for row in board]否则状态污染。参数说明max_depth3是平衡速度与强度的起点实测红方3层搜索可在i5笔记本上1秒内完成time_limit1.0单位为秒生产环境建议设为0.5~2.0evaluate_board是评估函数下一章详解。3. 让AI真正“懂棋”设计可调权重的评估函数与局面哈希缓存3.1 五维评估函数位置价值表子力分将帅安全度行动力残局修正评估函数Evaluation Function是AI棋力的天花板。很多开源代码只用“子力分总和”红车10分、黑炮6分等导致AI拼命兑子却不懂控盘。我们采用分层加权评估共5个维度全部可配置维度计算方式默认权重说明子力分Materialsum(PIECE_VALUE[piece] for piece in board)1.0红车10、黑炮6、红帅1000将死权重位置价值Position查表POS_TABLE[piece][r][c]0.8红帅在宫心20分黑将在宫边-15分将帅安全King Safety统计九宫内敌方威胁子数量1.2每多1个威胁子车/炮/马在攻击线上扣30分行动力Mobility己方所有棋子合法走法总数0.3行动力强局面开阔避免被锁死残局修正Endgame当双方剩余子力8时启用残局表如士象全权重500.5防止AI在残局乱弃士象# chess/eval.py PIECE_VALUE { Piece.RED_KING: 10000, Piece.BLACK_KING: -10000, Piece.RED_ADVISOR: 200, Piece.BLACK_ADVISOR: -200, Piece.RED_ELEPHANT: 200, Piece.BLACK_ELEPHANT: -200, Piece.RED_HORSE: 400, Piece.BLACK_HORSE: -400, Piece.RED_CHARIOT: 1000, Piece.BLACK_CHARIOT: -1000, Piece.RED_CANNON: 600, Piece.BLACK_CANNON: -600, Piece.RED_SOLDIER: 200, Piece.BLACK_SOLDIER: -200, Piece.EMPTY: 0 } # 位置价值表以红方视角黑方取反 POS_TABLE { Piece.RED_KING: [[0]*9 for _ in range(10)], # 红帅位置表 Piece.BLACK_KING: [[0]*9 for _ in range(10)], # 黑将位置表实际用时取反 } # 初始化红帅位置表宫心0,4最高宫角0,30,5次之 for r in [0,1,2]: for c in [3,4,5]: if r0 and c4: POS_TABLE[Piece.RED_KING][r][c] 20 elif r0 and c in [3,5]: POS_TABLE[Piece.RED_KING][r][c] 10 elif r1 and c4: POS_TABLE[Piece.RED_KING][r][c] 5 def evaluate_board(board, is_red_turn): score 0.0 # 1. 子力分 for r in range(10): for c in range(9): piece board[r][c] if piece ! Piece.EMPTY: score PIECE_VALUE[piece] # 2. 位置价值仅对己方棋子 for r in range(10): for c in range(9): piece board[r][c] if piece Piece.EMPTY: continue if (is_red_turn and piece 0) or (not is_red_turn and piece 0): abs_p abs(piece) if abs_p in POS_TABLE: table POS_TABLE[Piece(abs_p)] if piece 0 else POS_TABLE[Piece(-abs_p)] score table[r][c] * (0.8 if is_red_turn else -0.8) # 黑方表取反 # 3. 将帅安全度简化版统计攻击线上的敌方长距离子 king_pos find_king(board, is_red_turn) if king_pos: r, c king_pos # 检查车/炮/将帅是否在同一直线且无子间隔 for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]: # 四条直线 nr, nc r dr, c dc found_enemy False while 0 nr 10 and 0 nc 9: p board[nr][nc] if p ! Piece.EMPTY: if (is_red_turn and p 0 and abs(p) in (Piece.BLACK_CHARIOT, Piece.BLACK_CANNON, Piece.BLACK_KING)) or \ (not is_red_turn and p 0 and abs(p) in (Piece.RED_CHARIOT, Piece.RED_CANNON, Piece.RED_KING)): found_enemy True break else: break # 遇到己方子或士象线中断 nr dr nc dc if found_enemy: score -30 if is_red_turn else 30 # 红方被将军扣分 return score逻辑说明evaluate_board是纯函数无副作用可被高频调用位置表POS_TABLE按兵种坐标预计算避免运行时查表开销将帅安全度用“攻击线扫描”而非复杂威胁图兼顾精度与速度。参数说明所有权重0.8, 1.2等定义在config.py中修改后无需重启即可生效find_king函数需自行实现遍历棋盘找King是基础工具函数。3.2 引入Zobrist哈希缓存把搜索耗时降低60%避免重复计算同一局面Alpha-Beta搜索中同一局面可能因不同路径多次出现如A→B→C和A→D→C。Zobrist哈希用随机数异或生成唯一键实现O(1)查表。Python版常因哈希碰撞或随机种子固定导致缓存失效# chess/zobrist.py import random class ZobristHash: def __init__(self): # 为每个位置每个棋子类型生成随机64位整数 self.table [[[0] * 15 for _ in range(9)] for _ in range(10)] # 10行×9列×15种棋子 random.seed(42) # 固定种子保证可复现非生产环境可用time.time() for r in range(10): for c in range(9): for p in range(15): # 0~14对应Piece枚举值含EMPTY self.table[r][c][p] random.getrandbits(64) def hash_board(self, board): h 0 for r in range(10): for c in range(9): piece board[r][c] # 映射piece到0~14EMPTY0, RED_KING1, ..., BLACK_SOLDIER14 idx 0 if piece Piece.EMPTY else (piece if piece 0 else -piece 7) h ^ self.table[r][c][idx] return h # 在search.py中集成缓存 _transposition_table {} # {hash: (depth, score, flag)}flag0exact, 1lower_bound, 2upper_bound def _alphabeta_with_tt(board, is_red_turn, depth, alpha, beta, start_time, time_limit): h zobrist_hash.hash_board(board) if h in _transposition_table: stored_depth, stored_score, flag _transposition_table[h] if stored_depth depth: if flag 0: return stored_score, None elif flag 1 and stored_score beta: return stored_score, None elif flag 2 and stored_score alpha: return stored_score, None # ... 原搜索逻辑 ... # 搜索结束后存入缓存 flag 0 if score beta: flag 1 elif score alpha: flag 2 _transposition_table[h] (depth, score, flag) return score, best_move逻辑说明ZobristHash构造时用固定seed42确保每次运行哈希一致避免缓存击穿hash_board用异或^而非加法保证交换律ABBA和结合律使同一局面无论遍历顺序如何都得相同哈希。参数说明_transposition_table是全局字典生产环境建议用lru_cache(maxsize100000)替代flag标识存储值类型精确值/下界/上界是Alpha-Beta缓存标准实践。4. 避坑指南中国象棋AI开发中90%开发者踩过的5个血泪坑4.1 坑1将帅照面判定逻辑错误 → AI主动送将被一步绝杀现象AI在优势局面下突然把帅走到与将同一直线且中间无子的位置下一回合被对方车将死。原因is_in_check函数只检查“当前将是否被攻击”但未在generate_moves中过滤“走完后导致将帅照面”的走法。将帅照面是独立于“被攻击”的特殊规则必须在走法生成后额外验证。解决在generate_moves末尾添加照面过滤def is_king_face_to_face(board): # 找红帅和黑将的列坐标 red_king_col black_king_col -1 for r in range(10): for c in range(9): if board[r][c] Piece.RED_KING: red_king_col c elif board[r][c] Piece.BLACK_KING: black_king_col c if red_king_col ! black_king_col: return False # 检查两王之间是否无子 min_r, max_r min(find_king_row(board, True), find_king_row(board, False)), \ max(find_king_row(board, True), find_king_row(board, False)) for r in range(min_r 1, max_r): if board[r][red_king_col] ! Piece.EMPTY: return False return True # 在generate_moves的后验过滤中加入 if is_king_face_to_face(new_board): continue # 跳过导致照面的走法4.2 坑2马腿别子判断坐标错位 → 马在(0,0)能走(2,1)实际应被别现象AI让马从红方左下角(0,0)跳到(2,1)但(1,0)位置有子马腿被别此走法非法。原因“马腿”坐标计算错误。马走“日”字从(r,c)到(nr,nc)马腿在(r (nr-r)//2, c (nc-c)//2)。若此处有子则别腿。常见错误是直接用(r1,c)硬编码忽略方向。解决统一用向量中点公式def _gen_horse_moves(board, r, c, is_red_turn): moves [] # 马的8个日字落点 offsets [(2,1),(2,-1),(-2,1),(-2,-1),(1,2),(1,-2),(-1,2),(-1,-2)] for dr, dc in offsets: nr, nc r dr, c dc if not (0 nr 10 and 0 nc 9): continue # 计算马腿位置中点 leg_r, leg_c r dr//2, c dc//2 if not (0 leg_r 10 and 0 leg_c 9): continue if board[leg_r][leg_c] ! Piece.EMPTY: # 马腿被别 continue # 后续吃子/空位判断...4.3 坑3炮的“隔山打牛”逻辑混淆“隔一子”与“有子可吃” → 炮在空线乱吃子现象炮在空直线上如第0行AI认为可吃掉第9行的黑将实际因中间无子而不能吃。原因_gen_cannon_moves中将“移动”和“吃子”混为一谈。炮移动时无需隔子吃子时才需隔一子。错误代码常写成“遇子就停再遇子就吃”导致空线误判。解决分开处理移动与吃子def _gen_cannon_moves(board, r, c, is_red_turn): moves [] directions [(0,1),(0,-1),(1,0),(-1,0)] for dr, dc in directions: nr, nc r dr, c dc has_mount False # 是否已遇到“山” while 0 nr 10 and 0 nc 9: p board[nr][nc] if p Piece.EMPTY: if not has_mount: # 无山时可移动 moves.append((r, c, nr, nc)) else: if not has_mount: has_mount True # 遇到第一子设为山 else: # 已有山再遇子可吃 if (is_red_turn and p 0) or (not is_red_turn and p 0): moves.append((r, c, nr, nc)) break # 吃完或遇到己方子停止 nr dr nc dc return moves4.4 坑4局面哈希未考虑“将帅照面”状态 → 同一棋盘不同照面状态被当同一局面缓存现象AI在局面A将帅未照面搜索得高分缓存后局面B将帅照面哈希相同直接返回高分导致误判。原因Zobrist哈希只编码棋子位置未编码“将帅是否照面”这一全局状态。照面是规则状态非棋子属性。解决在哈希中加入照面标志位def hash_board(self, board): h self._base_hash(board) # 原棋子哈希 if is_king_face_to_face(board): h ^ 0xdeadbeefcafe1234 # 加入照面专属随机数 return h4.5 坑5评估函数未处理“长将”和“逼和” → AI陷入循环将被判和棋却不知现象AI连续3次用同一炮将军裁判判和但AI评估函数仍给高分继续长将。原因evaluate_board只看静态局面未接入“历史走法”信息。长将和逼和需检测连续重复局面国际象棋是3次中国象棋规则为“长将”直接判负但需识别。解决在搜索前维护一个history_set存储最近10步的Zobrist哈希在is_game_over中检测def is_long_check(history_hashes): # 若最后3个哈希相同且都是将军局面则为长将 if len(history_hashes) 3: return False last3 history_hashes[-3:] return last3[0] last3[1] last3[2] and is_in_check_by_cannon_or_chariot(last3[0])5. 工程化落地打包为跨平台可执行文件、接入Web界面与性能调优技巧5.1 用PyInstaller打包为单文件exe/dmg消除Python环境依赖用户不需要装Python、pip、numpy——这是产品化底线。PyInstaller是唯一成熟方案但象棋AI常因numpy和PIL若加GUI导致打包失败# 安装PyInstaller推荐用虚拟环境 pip install pyinstaller # 打包命令关键参数说明 pyinstaller --onefile \ --name ChineseChessAI \ --add-data chess;chess \ # 包含chess包所有.py文件 --hidden-import numpy \ --hidden-import PIL \ --exclude-module tkinter \ # 若不用tkinter GUI排除减小体积 main.py逻辑说明--add-data确保chess/目录完整复制到打包后目录--hidden-import显式声明动态导入模块numpy在评估函数中可能被间接调用--exclude-module排除无用GUI库。打包后生成dist/ChineseChessAIWindows为exemacOS为dmgLinux为可执行文件。参数说明main.py是入口文件内容为# main.py from chess.game import ChessGame if __name__ __main__: game ChessGame() game.run_cli() # 或 game.run_web() 启动Flask服务5.2 接入轻量Web界面用FlaskHTML实现网页版中国象棋小游戏无需React/Vue纯HTMLJSFlask即可实现。核心是/move接口接收走法返回AI响应# web/app.py from flask import Flask, render_template, request, jsonify from chess.game import ChessGame app Flask(__name__) game ChessGame() app.route(/) def index(): return render_template(chess.html) app.route(/move, methods[POST]) def make_move(): data request.json from_pos data[from] # [r, c] to_pos data[to] # [r, c] # 执行玩家走法 if not game.make_player_move(from_pos[0], from_pos[1], to_pos[0], to_pos[1]): return jsonify({error: Invalid move}), 400 # AI思考并走棋 ai_move game.search_best_move(max_depth3, time_limit0.8) if ai_move: game.make_ai_move(*ai_move) return jsonify({ board: game.get_board_state(), status: game.get_game_status(), ai_move: ai_move }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse) # 生产环境用gunicorn前端chess.html只需一个canvas画棋盘用fetch(/move)发请求。关键技巧AI思考时前端显示“Thinking...”用CSS动画避免页面假死而非阻塞JS线程。5.3 性能调优三板斧从1.2秒到0.3秒的实测优化路径在i5-8250U笔记本上初始版本搜索3层需1.2秒。经三次优化降至0.3秒优化项操作效果注意事项1. 用array.array替代list[list[int]]board array.array(b, [0]*90)按行优先展平-35%耗时需重写坐标计算board[r*9c]array(b)为有符号字节覆盖-128~127足够象棋编码2. 缓存generate_moves结果对每个局面哈希缓存走法列表make_move后清空-25%耗时仅缓存max_depth1的走法避免内存爆炸用lru_cache(maxsize10000)3. 评估函数向量化NumPyboard本文还有配套的精品资源点击获取
返回列表