中国象棋源码实战:走法生成与Alpha-Beta剪枝实现人机对弈
2026/9/17 1:11:45 网站建设 项目流程

简介:中国象棋程序源码,适合想通过实际项目学习棋盘类游戏开发、象棋规则与联网对战实现的读者。这份代码定位为教学示例,作者对类、函数与变量重新命名,并补充了必要注释,整体可读性比常见的课程设计源码更好。压缩包共361个文件,约5.78MB,主要包含Java源文件、编译class文件、xml配置、wav音效与少量png资源,覆盖代码、配置、音频与界面素材,目录结构便于对照阅读。功能上实现了走子规则、悔棋、联网对战等核心模块,读者可以借此理解棋盘坐标映射、规则判断流程、悔棋状态回退,以及联网交互的数据收发思路。需要说明的是,原作者也提示该版本功能较弱、界面体验一般、存在一些bug,因此更适合作为学习模板而非直接上线的成品。目前已有767人学习查看过该资源,用作课程设计参考或二次开发起点均比较合适。

1. 我为什么突然想写一版中国象棋源码

如果你在搜索引擎里敲下“中国象棋 源码”,大概率会看到一堆要么只有残局演示、要么注释稀疏到没法改的代码片段。我这版源码的起点,其实是想给老父亲做一个人机对弈的小程序,结果越做越深,最后把棋盘建模、走法生成、Alpha-Beta搜索和图形界面全走了一遍。这篇博文不聊天马行空的架构,只记录我在写这份中国象棋源码时踩过的坑、定下来的取舍,以及你拿到代码后最先应该看哪几个文件。

1.1 从一盘残局说起

当时我爸拿着一盘路边摊残局照片问我:“这个软件能不能摆出来?”我随口说能,然后发现现成的开源中国象棋项目要么是十年前的版本,要么只支持国际象棋。真正动手写源码之后才明白,中国象棋的规则细节比想象中多:象眼、蹩马腿、将帅不能照面、士不能出九宫、过河兵才能横走……任何一个没处理好,AI就会走出“诡异棋”。

所以我先做了一个决定:项目第一版只做控制台程序,把规则跑通,再谈界面。这个决定帮我省了大量时间,因为象棋源码的复杂度全都集中在规则和搜索上,图形界面反而是最不重要的部分。

1.2 这版源码要解决的三个问题

写之前我问自己三个问题,这三个问题也成了这个项目的骨架:

  • 棋盘和棋子用什么数据结构,能让走法生成既直观又高效?
  • 走法生成怎么处理中国象棋特有的“行动受限”规则?
  • 人机对战的AI该如何设计,才不至于让程序只会送子?

前两个问题决定代码能不能跑,第三个问题决定这个源码值不值得被人下载。下文我就按这三个问题展开,顺便把调试过程中那些让人挠头的细节也一并写出来。

2. 棋盘与棋子的建模:10×9数组是我最推荐的方式

中国象棋棋盘是9列10行,红黑两侧各有五个兵卒、两炮、两车、两马、两相(象)、两仕(士)、一帅(将),一共32颗棋子。数据结构的选择会直接影响所有后续代码的复杂度。

2.1 用二维数组加位置编码

我使用的是最朴素的board[row][col]二维数组,行从0到9,列从0到8。虽然很多高性能源码会用一维数组甚至位棋盘,但做中国象棋源码,二维数组的可读性最高,也最容易验证规则。

每个格子存一个整数:

  • 0表示空
  • 正数表示红方棋子
  • 负数表示黑方棋子

我给每类棋子分配了固定编号:车=1,马=2,相=3,仕=4,帅=5,炮=6,兵=7。这样判断红黑很方便:value > 0是红方,value < 0是黑方。至于为什么用正负而不是用颜色字段,纯粹是为了让评估函数和走法生成少写几个if。

EMPTY = 0 RED = 1 BLACK = -1 # 一个示例局面:红车在(0,0),黑将在(9,4) board = [[0 for _ in range(9)] for _ in range(10)] board[0][0] = 1 board[9][4] = -5

2.2 棋子对象到底该不该建类

我最初用class Piece写了一套面向对象版本,后来发现性能瓶颈不在于棋子对象,而在于搜索时需要反复复制整个棋盘。如果每层搜索都创建32个棋子对象,Python的GC压力会直接把棋力拖垮。

最终方案是:不建棋子类,只用整数编码。用整数数组描述局面,用独立的规则函数判断移动合法性。这样不仅搜索时深拷贝数组的开销小,而且整个源码更像一份规则说明,新手读起来也没有负担。

如果你打算用C++或Java实现,完全可以保留棋子对象;但如果用Python,请务必把“对象”交给字典或者轻量结构,不要把32个棋子全部实例化后放进列表遍历。

2.3 为什么没有用位棋盘

国际象棋源码里位棋盘几乎成了标配,因为64位刚好覆盖8×8棋盘。中国象棋有90个交叉点,两个64位整数才能放下一方棋子,处理“炮隔子打”这种跳跃规则时,位运算的收益并不明显。

我做过一个简单的对比实验:位棋盘版本的走法生成比二维数组版快大约15%,但代码复杂度翻了一倍。对于一份面向普通开发者和棋类爱好者的中国象棋源码来说,15%的性能提升不值得用可读性去换。如果你只是个人学习,二维数组足够跑出不错的棋力;如果要做棋力引擎,那就另当别论,后面可以再单独优化。

3. 走法生成是象棋源码的试金石

走法生成是中国象棋源码里最容易写错的部分。很多网上流传的源码,乍一看能走子,但走几步就会漏掉“将帅照面”“蹩马腿”这类隐藏规则。我建议按“先生成所有候选走法,再逐个过滤非法走法”的思路写,虽然多一层判断,但正确率远高于直接在候选生成时做一堆边界判断。

3.1 每种棋子的移动规则

  • 车:沿直线走,遇到己方棋子停,遇到对方棋子可以吃且停止。
  • 马:走“日”字,但要检查蹩马腿。
  • 相(象):走“田”字,不能过河,要检查塞象眼。
  • 士(仕):限九宫内走斜线,每步一格。
  • 将(帅):限九宫内走直线,每步一格,但可以与对方将帅直接照面(即不能形成将帅对脸)。
  • 炮:沿直线走,吃子时必须隔一个棋子(炮架),不吃子时不能越子。
  • 兵(卒):红方兵过河前只能前进,过河后可以前进和左右走;黑方卒反之。

这里最容易被当成“理所当然”的函数是马和炮的生成器。马需要判断目标坐标与起始坐标形成的位移,再反向推导马腿坐标;炮则要区分移动和吃子两种模式。

3.2 蹩马腿和塞象眼的处理细节

以马为例,马从(row, col)走到(row+2, col+1)。如果行差为2,那马腿在(row+1, col);如果列差为2,那马腿在(row, col+1)。千万别在生成走法时才想去算,而要在生成之前就把方向表固定下来。

# 马的四个方向:行差2、列差1 moves = [(2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2)] def gen_knight_moves(board, r, c): for dr, dc in moves: nr, nc = r + dr, c + dc if not (0 <= nr < 10 and 0 <= nc < 9): continue # 计算马腿位置 leg_r = r + (dr // 2 if abs(dr) == 2 else dr) leg_c = c + (dc // 2 if abs(dc) == 2 else dc) if board[leg_r][leg_c] != EMPTY: continue if board[nr][nc] * board[r][c] > 0: continue # 目标位置是己方棋子 yield (nr, nc)

这个函数里的“马腿位置”计算,是我调试时间最长的地方。原因是我一开始把dr // 2dr搞混,导致所有斜向跳的马都漏掉了马腿判断。

相(象)的塞象眼同理,从(r, c)走到(r+2, c+2),象眼在(r+1, c+1),同时还要检查目标位置是否在己方半场。

3.3 将帅照面和胜负判定

将帅照面是中国象棋独有的规则:双方将帅如果在同一条竖线上,且中间没有任何棋子,则走完这一步、让将帅直接对脸的一方判负。所以在检查合法性时,只要一步棋导致己方将帅与对方将帅“直接对视”,这步棋就不合法。

我实现了一个简单的is_check_after_move(board, move)函数:先模拟走子,然后找到双方将帅位置,检查是否同列且中间无子。因为棋盘列只有9列,这个检查非常快。

胜负判定在走法生成之后:如果某一方没有合法走法,那要么是被将死(输了),要么是无子可动(也输)。这比实时搜索整个棋盘更可靠,也更容易写单元测试。

4. 从暴力搜索到能下棋的AI

中国象棋源码的精华在于AI。这一版我用的方法是传统的极小化极大(Minimax)加Alpha-Beta剪枝。虽然现在深度学习已经很强,但在源码学习场景里,传统搜索树的思路才是最容易理解和复现的。

4.1 子力价值表设定

评估函数决定AI的“棋感”。我参考了常规开局棋力表,并做了一些调整:

棋子基础价值备注
帅/将10000被吃即输
900中国象棋里车最灵活
400配合位置价值浮动
450前期比马好用,残局略降
相/象200防守子力
士/仕200防守子力
兵/卒100过河后加到150

除了基础价值,我还给每个兵的位置加了额外奖励:过河兵加50,靠近九宫的兵再加30。马的位置则用“马踏八方”表微调,避免AI把马走到角落。

4.2 极小化极大与Alpha-Beta剪枝

这一版搜索树的伪代码如下:

def search(board, depth, alpha, beta, maximizing): if depth == 0: return evaluate(board) moves = generate_legal_moves(board) if not moves: return -99999 if maximizing else 99999 if maximizing: best = -99999 for move in moves: make_move(board, move) best = max(best, search(board, depth - 1, alpha, beta, False)) undo_move(board, move) alpha = max(alpha, best) if beta <= alpha: break return best else: best = 99999 for move in moves: make_move(board, move) best = min(best, search(board, depth - 1, alpha, beta, True)) undo_move(board, move) beta = min(beta, best) if beta <= alpha: break return best

Alpha-Beta剪枝的收益不是线性提升,而是指数级优化。在普通棋局里,4层搜索的用时和6层搜索的用时差很多,但效果也差很多。我测试下来,Python实现里4层搜索大概需要0.3秒,5层需要1秒左右,6层就有点卡了。

4.3 搜索深度的调节与AI强度

为了让AI适配不同水平的用户,我加了三档难度:

  • 初级:深度2,评估函数只用子力价值
  • 中级:深度4,启用位置价值
  • 高级:深度5,启用“杀手走法”排序

走法排序很重要,我一直对走法列表按吃子价值从高到低排序。这样Alpha-Beta剪枝能更早触发,搜索速度提升显著。其中最简单的策略是:如果某步能吃掉对方车,就把它排在最前面。这个策略看起来简单,但在搜索树前几层非常管用。

另外,我必须提醒:评估函数不要只算子力,还要判断将帅是否被将军。因为在深层的搜索里,一次将军如果没被处理,AI会误以为能白白吃掉对方棋子,然后走出很弱的棋。我的做法是:每层搜索开始前,先检查当前走子方是否被将军,如果被将军,就只保留能解除将军的走法,否则直接返回极差分数。

5. 图形界面与联调中的坑

控制台版跑通后,我开始做图形界面。这里踩的坑最多,尤其是坐标映射和悔棋逻辑,很多中国象棋源码的质量都差在这两个地方。

5.1 坐标换算与点击落子

中国象棋的棋盘交叉点是90个,而鼠标点击的坐标是连续的。我一开始直接拿鼠标像素坐标除以格子宽度,结果点击和落子位置总是错位。

后来我单独写了一个“坐标矫正函数”:先算出最接近的交叉点坐标,再用交叉点坐标反算是否在可接受范围。因为中国象棋的棋子放在交叉点上,所以点击范围的判定比国际象棋四格中心点判定更讲究。

def pixel_to_board(px, py, origin_x, origin_y, cell_size): col = round((px - origin_x) / cell_size) row = round((py - origin_y) / cell_size) if 0 <= col < 9 and 0 <= row < 10: return row, col return None

这个round就是关键。如果用了int,点击时总会向左上角偏移。

5.2 悔棋和复盘怎么实现

悔棋功能的本质不是“撤销一步”,而是“撤销两步”。因为是红黑双方轮流走,所以悔棋时要同时撤回对手的一步和己方的一步。我的做法是维护一个走法历史栈,每个元素保存走子前后的棋盘副本。

用Python实现时,直接copy.deepcopy(board)最简单,但搜索时不能这么干。悔棋和复盘时偶尔用一次,性能还算可以接受。复盘功能就是把历史栈里的走法按顺序重新播放,再配合一个“当前回放索引”就能实现。

5.3 单元测试与局面校验

中国象棋源码最怕什么?最怕改了一处规则函数,结果其他地方跟着崩。我建议把每个棋子的走法生成单独写成测试用例,例如:

  • 马在边界是否还能跳?
  • 炮隔两个子时能不能吃?
  • 将帅照面怎么判负?
  • 兵过河前后的移动方向变化?

这些测试不需要很多,十几条就够用。我每条规则至少配了一个测试,后续调整AI和界面时,只要测试通过,基本不会出大问题。

另外,我在联调时发现一个非常隐蔽的bug:AI在选择走法时,偶尔会把己方将帅和对方将帅“送对脸”。原因是搜索树里没有在生成走法时校验照面规则,只在最终局面评估时校验。所以我把“将帅照面”的检查提前到了走法合法性里,这比任何评估函数都重要。

写在源码之外

写这份中国象棋源码最大的收获,不是做出来一个能下棋的程序,而是彻底理解了“规则约束”如何影响程序结构。棋盘用二维数组、走法生成加合法性过滤、搜索用Alpha-Beta剪枝,这套组合虽然传统,但足够稳定。如果你想继续往上做,可以试试加入开局库、残局库,或者用置换表(Transposition Table)缓存已搜索局面。

最后分享一个调试小技巧:在搜索过程中给每个局面的棋盘状态算一个哈希值,如果发现两个相同哈希值的局面出现在搜索树的不同层级,就要警惕是否有无意义的循环走法。中国象棋里长将、长捉很容易导致搜索树膨胀,处理时可以先在走法列表里排除重复走法,或者限制同一局面的重复次数。这些细节不做进源码里,棋力就会差一大截。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询