十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python实现中国象棋:从规则引擎到AI对战完整源码解析

Python实现中国象棋:从规则引擎到AI对战完整源码解析 简介Python中国象棋源代码是一套面向Python初学者与游戏开发爱好者的完整小游戏项目以中国象棋为场景清晰展示了从界面绘制、棋子规则到人机对战的基础实现思路。压缩包共54个文件以5个Python源码文件为核心搭配大量png、gif、jpg图片素材并附有字节码缓存pyc整体仅299KB轻量易用。项目按职责拆分为主程序、数据常量、棋子走法、电脑计算、按钮定义五个模块界面图片与逻辑代码分离便于逐段阅读和二次开发尤其适合学习面向对象编程、事件驱动与简单AI算法。目前已有6640人学习使用电脑走法仍有较大优化空间读者可基于现有框架尝试升级搜索策略或评估函数在实战中提升编程能力。下载后即可运行体验也可作为课程设计或毕业设计的参考原型。 前几天整理代码仓库翻出了两年前写的一版Python中国象棋。当时是为了练手把整盘棋从棋盘绘制、规则判断到简单AI全部实现了一遍代码量大概两千行出头。说不上多优雅但胜在结构清晰注释也写着“给未来的自己看”今天重读还能顺畅读懂。这篇就把这套Python中国象棋源代码的核心思路、实现过程和踩过的坑拆开讲一遍希望能给正在做棋类游戏练手项目的朋友一点参考。1. 开局前的选择Python能扛住中国象棋的复杂度吗1.1 为什么拿中国象棋练手棋类游戏一直是学编程时很喜欢练的项目五子棋、黑白棋、国际象棋都很常见。但中国象棋有一套非常独特的规则体系马走日但有绊马腿象走田但怕塞象眼炮要隔子打将帅不能照面士和相还不能出九宫和过河。这些规则如果放在一个不带界面、纯逻辑层面去实现非常考验对状态、边界条件和数据结构的理解。相比国际象棋中国象棋的棋盘是10行9列共有90个交叉点棋子32枚逻辑上不算特别复杂但规则细节比五子棋丰富得多。用它来练手能覆盖到二维数组遍历、基于位置的走法搜索、以及“在各种限制条件下判断一个局面是否合法”这一类通用问题。这些能力做任何游戏项目都能用上。在整个项目过程中我刻意把“规则”和“界面”分开。棋盘渲染、鼠标点击这些都属于表现层规则引擎才是核心。只要规则层做对了换界面只是换层皮。实践证明这个决定很划算后来我不止一次把同一套规则模块搬到别的项目里。1.2 Pygame、Tkinter 还是纯命令行动手之前我先列出了三种可能的实现路线方案优点缺点适合场景纯命令行规则逻辑优先不需要管图形细节棋盘打印不直观交互体验差快速验证规则和AITkinter标准库自带无需额外安装界面样式相对简陋控件刷新不够顺滑简单开发环境下的可视化Pygame绘制自由度极高事件处理成熟社区教程多需要单独安装依赖字体渲染要额外处理做完整可玩的棋类游戏我最终选了Pygame。它本质上就是“画布事件循环”的结构很适合做回合制棋类游戏每一帧都可以根据当前棋盘状态把全部界面重新绘制一遍而不是像传统GUI那样维护一堆控件的同步状态。鼠标点击选中棋子、高亮合法落点、走子后刷新画面这套流程在Pygame里非常自然。1.3 项目结构怎么分模块整个项目没有用复杂的框架就是最朴素的多文件组织chess_board.py棋盘初始化、坐标转换、棋盘状态获取/修改chess_rules.py所有棋子的走法生成、将军判断、胜负判断chess_ai.py极小极大搜索、α-β剪枝、局面评估chess_gui.pyPygame界面、鼠标事件、绘制逻辑main.py启动入口组装上面所有模块这种拆分的好处是规则模块不依赖任何一个界面函数你可以单独写一个命令行测试脚本快速跑几百个局面来验证走法是否合法。如果一开始就把界面和规则全部揉在一个文件里后面排查问题的成本会成倍增长。2. 棋盘表示一套数据结构的取舍决定了后续所有代码怎么写2.1 坐标体系与二维数组中国象棋棋盘是10行9列红方在下方黑方在上方。我采用的坐标约定是board[row][col]其中row从0到9col从0到8。row越小越靠近黑方row越大越靠近红方col从左边0递增到右边8。用二维数组是最直观的做法读取某个位置的棋子就是一个下标索引遍历走法时也是逐个坐标判断。有人喜欢用一维数组配合位移表来做性能更好但对中国象棋这种90格的小棋盘来说二维数组可读性远大于那一点性能收益。2.2 棋子编码方案棋子的编码方式决定了代码的清晰度。我用数字表示棋子类型空位用-1EMPTY -1 RED_SHUAI 0 RED_SHI 1 RED_XIANG 2 RED_MA 3 RED_CHE 4 RED_PAO 5 RED_BING 6 BLACK_JIANG 10 BLACK_SHI 11 BLACK_XIANG 12 BLACK_MA 13 BLACK_CHE 14 BLACK_PAO 15 BLACK_ZU 16红方和黑方的编码相差10这样判断棋子归属只需要一句def is_red(piece): return 0 piece 6 def is_black(piece): return piece 10 def is_same_side(piece_a, piece_b): return (0 piece_a 6 and 0 piece_b 6) or (piece_a 10 and piece_b 10)用一个整数而不是字符串存棋子的好处是判断逻辑简单、比较快并且可以通过简单的整除运算映射到棋子名称字典上绘制界面时用字典转成中文即可。2.3 初始化棋局初始化函数就是把阵型摆上去def init_board(): board [[EMPTY] * 9 for _ in range(10)] for c in range(9): board[3][c] RED_BING board[6][c] BLACK_ZU board[0][0] board[0][8] BLACK_CHE board[0][1] board[0][7] BLACK_MA board[0][2] board[0][6] BLACK_XIANG board[0][3] board[0][5] BLACK_SHI board[0][4] BLACK_JIANG board[2][1] board[2][7] BLACK_PAO board[9][0] board[9][8] RED_CHE board[9][1] board[9][7] RED_MA board[9][2] board[9][6] RED_XIANG board[9][3] board[9][5] RED_SHI board[9][4] RED_SHUAI board[7][1] board[7][7] RED_PAO return board代码没有任何技巧性就是把每个棋子的起始位置写清楚。这种“表面无聊”的代码恰恰是规则引擎的地基一旦写错一个位置后面所有测试都会错得莫名其妙。3. 走法生成中国象棋规则里的“魔鬼细节”全在这一层3.1 车、兵、将的基础走法车的走法最简单从一个点出发沿上下左右四个方向直线延伸碰到任意棋子就停下如果碰到的是对方棋子可以吃掉。兵的走法要分过河前和过河后。红兵初始在row3当row 5时表示过河了过了楚河汉界黑卒初始在row6当row 4时表示过河了。过河前只能向前走一格过河后才能向左、向右走但永远不能后退。将和帅的走法是只能在九宫内移动一格四个方向都要先判断目标位置是否在九宫范围内。九宫的坐标范围对红方是将帅在row 7..9, col 3..5黑方是row 0..2, col 3..5这个判断必须严谨。3.2 绊马腿与塞象眼校验前置位置的逻辑马走日是8个方向每个方向需要判定马腿是否有子。比如马从(r, c)要跳到(r-2, c-1)那马腿的位置就是(r-1, c)。所谓“绊马腿”就是马腿位置不是空位时这个方向不能走。我最初写这部分时犯过一个低级错误把马腿方向写错了导致马能跳过棋子看上去像是在飞。后来才发现问题出在我用“目标位置与当前位置的差值”反推马腿时正负号搞反了。这里我直接把完整的方向表列出来MA_DIRECTIONS [ (-2, -1, -1, 0), # 上偏左马腿在正上方 (-2, 1, -1, 0), # 上偏右马腿在正上方 (2, -1, 1, 0), # 下偏左马腿在正下方 (2, 1, 1, 0), # 下偏右马腿在正下方 (-1, -2, 0, -1), # 左偏上马腿在正左侧 (1, -2, 0, -1), # 左偏下马腿在正左侧 (-1, 2, 0, 1), # 右偏上马腿在正右侧 (1, 2, 0, 1), # 右偏下马腿在正右侧 ]表中前两个值是目标相对偏移后两个值是马腿相对偏移。有了这个表以后写马走法只需要一个循环。象走田的规则也是同理四个大象眼位置就是目标位置和当前位置的中点。象的额外限制是不能过河红相不能去到row 5的区域黑象不能去到row 4的区域。3.3 炮的“越子吃法”是唯一特殊逻辑炮在空走时和车一样沿直线走不能跨越任何棋子。但是吃子时必须且只能跨越一个棋子这个被跨越的棋子叫“炮架”。也就是说炮的走法要分两步判断找从当前位置沿某个方向往前的所有格子从第一个非空格子开始后面的第一个非空格子如果是敌方棋子就是可以吃的位置。中间所有格子都必须有且只有一个棋子作为炮架。这个逻辑我一开始写成了一个单独的get_pao_moves函数后来为了统一所有棋子的行为我把它整合进一个通用的“沿方向扫描”函数里用count_blocks计数中间棋子数。吃子判断就从“计数1且目标为敌方”落在代码上。3.4 将帅不能照面这是全局规则不是单子规则将帅不能照面的规则是指两个将帅如果在同一条竖线上并且中间没有任何棋子则该局面不合法。这条规则的本质是把“将帅”看作是可以沿竖线吃子的“假想车”只不过吃子对象只能是对方的将帅。我选择在每次执行走子后调用一个is_check函数时顺带检查这条规则。具体实现是找到红帅和黑将的位置如果列号相同就统计它们之间所有棋子数量如果为0则当前走子不合法。这样就把“飞将”这种特殊情况也覆盖了。4. 将军与胜负让游戏知道什么时候该结束4.1 将军检测的实现路径判断一个局面中红方是否被将军思路是找到红帅的位置遍历黑方所有棋子的所有合法走法看是否存在一个走法的目标位置恰好是红帅的位置。如果有说明红帅正被将军。这里的关键点是要走“完整规则”的合法走法而不是简化的走法。如果将军检测只用了简化走法而没检查绊马腿、炮架就会出现明明马被别住腿却报将军的荒唐情况。4.2 将死、困毙与胜负判定将死的判断当前局面下己方被将军且己方所有棋子的所有合法走法都仍然会让己方将帅处于被将军的状态那么就是被将死对方胜。困毙的判断当前局面下己方没有被将军但所有合法走法走完以后都会把自己送进被将军的局面也就是无子可动。传统规则下中国象棋的困毙算作输棋这一点跟国际象棋的逼和不一样实战中很容易被忽视。我的判定逻辑是统一的def has_any_legal_move(board, turn): for each_piece in board: if piece belongs to turn: for each_move in get_moves(...): if is_move_legal(...): return True return False然后综合将军状态得出胜负结果。实现上不算复杂但需要注意不能把“被将军”和“无合法走法”这两个状态混为一谈。4.3 和棋局面重复与自然限着中国象棋里有很多和棋情况最常见的是一将一杀、长捉、以及60回合自然限着。完整实现这些规则需要记录历史局面和步数。我在这个版本里做了简化只记录过去若干步内是否出现三次相同局面如果出现则判和。history [] def is_draw(history): if len(history) 6: return False last history[-1] count 0 for item in history: if item last: count 1 return count 3为了做局面重复检测我把棋盘状态编码成一个字符串逐行逐列拼接棋子编号中间加一个分隔符表示当前走子方。这样比较的时候只需要做字符串相等判断简单可靠。5. 简单AI用极小球搜索加剪枝让电脑跟你下棋5.1 从“纯随机走法”到“只看一步”的跳跃最开始我的AI特别笨只是从所有合法走法里随机挑一个。后来改成“贪心算法”也就是对当前局面的所有合法走法都模拟执行一次用评估函数计算走完后的分值选分值最高的那步。贪心算法能应付很弱的对手但它看不见“被将军”和“丢大子”这类后续变化。比如你走出一步吃掉对方一个炮但对方下一步能直接将军抽车贪心算法完全意识不到。这就引出了极小极大搜索。5.2 极小极大搜索和α-β剪枝极小极大搜索的思想是假设双方都会走出对自己最有利的棋。到我的回合我会选择让评估值最大的走法到对方的回合他会选择让我评估值最小的走法。交替递归若干层就是国际象棋和象棋AI的基本盘。加上α-β剪枝之后效率提升非常明显。剪枝的思路是如果当前分支已经能够推断出对手不会让这个局面发生因为有一个更好的选择那就不用再深入搜索这个分支了。def minimax(board, depth, alpha, beta, maximizing): if depth 0: return evaluate(board) if maximizing: best -INF for move in legal_moves: new_board make_move(...) val minimax(new_board, depth - 1, alpha, beta, False) best max(best, val) alpha max(alpha, val) if beta alpha: break return best else: best INF for move in legal_moves: new_board make_move(...) val minimax(new_board, depth - 1, alpha, beta, True) best min(best, val) beta min(beta, val) if beta alpha: break return best在我那台普通笔记本上不加剪枝的4层搜索有时就要等好几秒加上剪枝之后基本能在1秒内出结果。这个差别实战体验非常明显。5.3 局面评估函数给每种棋子的价值定个价评估函数是最能体现个人风格的地方。我这个版本用的是最经典的“棋子分值累加法”棋子分值将/帅10000车600马400炮300相/象200士/仕200兵/卒未过河100兵/卒已过河200评估方法是遍历整个棋盘红方棋子加分黑方棋子减分最后返回一个总分。AI作为红方时最大化这个分数作为黑方时最小化这个分数。另外我又给兵加了一点“过河奖励”让AI更愿意把兵往前推进。这种评估函数非常朴素但配合3到4层的搜索已经能下出有模有样的棋至少不会比刚学会规则的新手差。6. Pygame界面把命令行棋盘搬到屏幕上6.1 画棋盘与画棋子画棋盘时要注意中国象棋棋盘的中间是楚河汉界没有纵向直线贯穿。所以绘制竖线时要分成上下两段中间留空。我先把10条横线画完整再将左右两侧的竖线从第1条画到第4条、从第6条画到第9条正中间的第5条竖线整体不画。然后补上九宫的两条斜线棋盘就成型了。棋子是一个实心圆加上中文字。文字渲染在Pygame里坑最大默认字体不支持中文直接渲染会显示成方块。解决方法是加载一个中文字体文件font_path /usr/share/fonts/truetype/wqy/wqy-microhei.ttc font pygame.font.Font(font_path, 30)这是一个很典型的环境坑。如果没提前准备字体文件程序在其他机器上跑起来就是一片方框。后来我干脆在代码里写了一个字体自动查找函数优先用常见字体路径找不到再降级到默认字体。6.2 鼠标点选与合法落点提示交互逻辑用状态机来组织SELECT状态等待玩家点击。点击到己方棋子记录选中棋子进入HIGHLIGHT状态。HIGHLIGHT状态计算选中棋子的所有合法走法在棋盘上画小圆点标记。点击合法落点执行走子进入WAIT_AI状态点击其他己方棋子切换选中点击空白处或者对方棋子且不是合法落点取消选择回到SELECT状态。WAIT_AI状态调用AI函数返回一步棋执行后回到SELECT状态。这个状态机虽然简单但很重要。如果没有状态管理鼠标事件会出现各种奇怪的冲突。比如你选中一个兵又点了一下自己的车结果反而把兵走到车的位置上去了这种bug都是状态混乱导致的。6.3 翻转子棋盘解决红黑视角问题还有一个细节是红方在下、黑方在上的视角问题。如果玩家选择执黑棋盘需要整体翻转180度。直接在绘制时对坐标做一次变换即可def transform_pos(row, col, is_flipped): if not is_flipped: return row, col return 9 - row, 8 - col规则引擎始终按红方视角存储棋盘只是界面绘制和鼠标点击坐标在进入/离开引擎时做一次变换。这个方案简单可靠不会把规则逻辑搞乱。7. 排错实录棋类程序最容易藏bug的几个地方7.1 走法生成器的验证方法我验证走法生成正确性的笨办法是写一个play_both_sides脚本让AI自己跟自己下每走一步就把棋盘打印出来人眼盯着看。这种方法虽然原始但非常有效。连续跑几十盘之后哪种棋子的走法哪里有问题基本都能暴露出来。更有系统性的做法是设计一些“固定局面测试”每个测试只摆少数几个棋子验证特定规则。比如棋盘中只放一个马周围摆上各种障碍断言马的合法走法集合是否等于预期。7.2 我踩过的三个真实bug第一个是象棋的“绊马腿”判断没写好导致马可以跳过棋子。原因是方向表里的马腿坐标写错修复成本不大但排查花了一个晚上。第二个是炮吃子判断把“隔着棋子”和“隔着多个棋子”搞混了。我发现电脑的炮经常会穿过两个棋子来吃子。修复方案是数出路径上的全部棋子数量吃子时要求严格等于1空走时要求严格执行0缺一个条件都会出错。第三个是最隐蔽的AI走出一步后自己的帅暴露在对方车的攻击线上也就是说AI没有检查“走子后自己是否处于被将军状态”。这个问题在只有一层贪心时不容易暴露因为评估函数分不出“被将军”和“没被将军”的区别。后来我在is_move_legal里强制加上了“模拟走子后检查己方将帅是否安全”这一步问题才彻底解决。7.3 性能优化别让Python慢到不能玩Python的执行速度不算快但象棋搜索的优化空间很大。我做的第一个优化是走法生成时避免重复分配大量列表尽量复用对象。第二个优化是评估函数只在搜索到叶子节点时才计算而不是在每个节点都全盘扫一遍。第三个优化是深度设置为4层最多5层超过5层等待时间就不太能接受了。另外我注意到一个有意思的现象kào剪枝之后搜索速度跟走法顺序有相当大的关系。如果先搜索看上去比较好的走法比如吃大子的走法剪枝效率会明显提高。我在代码里对走法列表按“棋子分值从大到小”排了个序据说这个技巧在更专业的引擎里叫“杀手走法启发”简单实现一下就有可感知的提速效果。8. 拿到源代码之后还能往哪些方向改8.1 棋谱保存与复盘功能目前AI和人对弈结束后我只会打印一句“红方胜”之类的文字。改进方向是做一个走法记录列表每步记录一个类似于马8进7的字符串存储到文件里。在对弈结束后可以像看棋谱一样一步步回放。关键还是坐标转换。我的内部坐标是(row, col)要转成“马8进7”这样的中国象棋记法需要按照棋子的移动方向和起始列号来推算这个转换逻辑本身也是一个值得写的小模块。8.2 更聪明的AI位置估值与开局库我的评估函数只算了棋子分值没有考虑位置因素。比如车占中路、马在河沿、炮在底线位置的好坏其实差异很大。改进方案是给每种棋子做一张“位置分值表”遍历棋子时把位置分也加起来这样AI会更有大局观。更进一步是加入开局库。把前几步的常见开局写在文件里AI开局阶段直接从开局库里查着走不仅能避开自己薄弱的中局还能省下搜索时间留给中后盘。8.3 网络对战与自定残局还有一个团队朋友建议过的玩法用socket通信把走法序列透传给对端做一个简单的网络对战版本。两边各自维护棋盘只传输走法的起点和终点坐标界面层自己负责渲染不需要同步整个棋盘状态。这样做的好处是网络只传输轻量数据不容易出现状态不一致的问题。自定残局功能则在规则引擎基础上加一个“从指定局面开始”的入口测试某个残局下AI能不能找出杀棋这是锻炼评估函数和搜索深度的好场景。说实话这套Python中国象棋源代码的整体难度并不高但它非常完整地覆盖了规则、搜索、界面、交互、调试这几个经典模块。如果你已经掌握Python基础语法想找一个稍有点挑战的练手项目这个方向很值得一试。个人体会是把走法生成器耐心写完、把测试用例补上后面做AI和界面都会顺利很多。等你跑通了自己写的AI对战之后再回头看那些看似烦琐的规则实现会发现它们其实都是最值回票价的代码。本文还有配套的精品资源点击获取
返回列表