☰
Java Swing象棋AI源码:极大极小值与AlphaBeta剪枝
2026/10/18 12:14:10 网站建设 项目流程

简介:一套基于 Java Swing 的象棋游戏源码,采用极大极小值搜索算法与 Alpha-Beta 剪枝实现人机对弈,并附带文档说明,适合计算机专业学生用于课程设计、毕业设计,也适合希望通过实战项目提升 Swing 与算法能力的开发者。压缩包共 150 个文件,约 3.03MB,以 74 个 Java 源文件为核心,配合 JPG、PNG 界面素材、WAV 音效以及 Markdown、XML、properties 等配置与说明文档,其中图片用于棋盘界面展示,音频提供对弈反馈,文档辅助理解设计思路,便于快速掌握项目结构并按需替换资源。核心代码覆盖棋局状态表示、走法生成、搜索评估与界面交互,模块划分清晰,既能学习经典博弈算法的实际落地,也能参考完整桌面应用的工程组织。已有 242 人学习下载,项目上传前已在本机运行验证,可直接导入 IDE 运行,也可在源码基础上二次开发,用于课程设计作业或答辩展示。

1. 基于 Java Swing 的象棋源码:极大极小值算法如何让电脑真正会下棋

做 Java 课程设计的人,十个里有八个想选象棋;写完界面能走子的能留下三个,真正让电脑会进攻、会防守的,基本都卡在搜索算法这一步。这份基于 Java Swing 的象棋游戏源码解决的就是最后这一层:不靠预置套路,而是用极大极小值搜索算法驱动人机对弈,把“走子、评估、搜索”拆成了 Situation、AnalysisBean、AlphaBeta 三个独立类。对正在做毕业设计的人来说,它的价值在于给你一条能跑的完整基线;对只想练手的人,最值得抄的是 AlphaBeta.java 里那段剪枝逻辑,比网上多数拼凑的象棋 Demo 干净得多。它适合两类人:一类是课程设计、毕业设计需要快速落地的学生,另一类是想弄懂棋类 AI 怎么在 Java 里落地的学习者。

2. 拆开棋盘与走法:Situation.java 如何把“车走直线”变成可搜索的代码

象棋 AI 和乱走棋的最大区别,是它每一步都在一个完整的局面模型上做决策。这个模型不复杂,但少了它,后面的搜索和评估全是空中楼阁。源码里 Situation.java 负责的就是这件事。我先说结论:只要把棋盘、走法生成和“走完不能送将”这个约束实现好,极大极小值部分反而是一段很短的递归。下面按角色定义、候选走法、递归框架三块拆开。

2.1 棋子建模:Role.java 里的类型与红黑归属

源码打开第一时间应该读的不是 AlphaBeta,而是 Role.java。这个文件只做一件事:给每种棋子一个稳定编号。常见做法是棋盘用 int[][] 表示,90 个交叉点,空位填 0,红方棋子为正数,黑方棋子为负数,绝对值代表棋子类型。这样设计的好处是:判断归属只看正负号,取类型只看绝对值,一次循环就能完成整张棋盘的扫描。

public class Role { public static final int EMPTY = 0; public static final int ROOK = 1; public static final int KNIGHT = 2; public static final int BISHOP = 3; public static final int ADVISOR = 4; public static final int PAWN = 5; public static final int CANNON = 6; public static final int KING = 7; }

这段代码只是示意写法,但它代表了一个关键约定:棋盘上任意一个格子,用一次 board[row][col] 就能同时拿到“有没有子、是谁的子、是什么子”三份信息。后续 generateMoves 遍历棋盘时,不需要额外的红黑两套数据结构,全部收敛到数值判断上。Role.java 里可能还包含将帅的“九宫”范围判断,比如将只能落在 3 列到 5 列、己方底线三行内,这个约束会在 2.2 的走法生成子方法里用到。

2.2 走法生成:Situation.java 的 generateMoves 核心循环

Situation.java 里最值得抄的方法是 generateMoves(int[][] board, int side),它返回当前棋手所有走法。做法很简单:逐行逐列扫描棋盘,遇到属于当前方的棋子,就按类型展开它的所有合法终点。车走直线、马走日、象走田、炮翻山,规则看着零散,实际都能收敛成“给一个起点,返回一串终点坐标”的小方法。

public List<int[]> generateMoves(int[][] board, int side) { List<int[]> moves = new ArrayList<>(); for (int row = 0; row < 10; row++) { for (int col = 0; col < 9; col++) { int piece = board[row][col]; if (piece == Role.EMPTY || !belongsTo(piece, side)) { continue; } collectPieceMoves(board, row, col, piece, moves); } } return moves; }

这个方法的逻辑是:外层双层循环负责遍历棋盘,内层 collectPieceMoves 负责把指定棋子能走的终点全部展开,走法用一个 int[] 存四元组——起点行、起点列、终点行、终点列。注意这里没有把“走完是否被将军”直接过滤掉,而是把检查放到 makeMove 之后的统一入口。原因是将军判断要被反复执行,散落在每个子方法里会重复计算几十次,集中在 makeMove 后判断,代码维护更简单。代价是生成结果里会混入少量送将的无效走法,搜索时多走一小截,但影响可以忽略。

这里有个容易踩的细节:象棋里“将帅不能照面”是炮和车走直线之外的另一个硬约束,单独写在某个棋子的 move 方法里很容易漏。更稳的做法是统一在 canMoveAfterCheck 里检查:走完这步后,自己一方的将帅是否暴露在对方火力下。源码里 Situation.java 的 makeMove 实现如果做得够完整,会直接拦截这种情况。

2.3 递归搜索框架:极大极小值如何把胜负判断交给叶子局面

走法生成好了,剩下的问题是:这么多走法里选哪一个。没有背景知识的做法是随机选,但随机走几盘就会原形毕露。极大极小值的思路是向前看:自己走一步,对方会怎么应对,自己再如何回击,最后走到一个不能再深入的叶子局面,用评估函数打一个分,这个分决定了当前这步棋的好坏。

public int minimax(int depth, int side) { if (depth == 0) { return evaluator.evaluateBoard(); // 叶子局面打分 } List<int[]> moves = situation.generateMoves(board, side); int best = (side == RED) ? Integer.MIN_VALUE : Integer.MAX_VALUE; for (int[] move : moves) { situation.makeMove(board, move); int score = minimax(depth - 1, side ^ 1); situation.undoMove(board, move); if (side == RED) { best = Math.max(best, score); } else { best = Math.min(best, score); } } return best; }

这段代码看着玄学,实际就是一个带颜色的递归:红方走棋时从子节点里挑最大的分,黑方走棋时挑最小的分。因为同一盘棋,一方优势就是另一方劣势,所以谁走谁取极值。depth 每递归一层减 1,到 0 时停止,返回评估函数的分值。这就是极大极小值的完整骨架,AlphaBeta.java 本质是在这个框架上做剪枝,并没有改变评分逻辑。

顺便算一笔账:中国象棋平均每个局面大约有 40 个合法走法,残局会少一些。搜索 3 层就是 40^3 = 64000 个节点,6 层就是 40^6 = 40 多亿个节点。这就是为什么娱乐级象棋 AI 用 4 层,职业一点的开到 8 层已经能感受到长考。源码默认深度一般控制在 3 到 5,ChessMenuBar 里通过菜单项给玩家选。在单线程 Java 程序上盲目提深度到 6 以上,不会让棋力飞跃,只会在中局阶段卡到怀疑人生。

3. AlphaBeta.java 的剪枝实现:如何把数亿节点砍到能实时落子

纯极大极小值在 4 层深度下要走 250 万个节点,Java 单线程跑起来已经能感觉到卡顿。Alpha-Beta 剪枝的核心只有一句话:搜索过程中,某一支已经差到没救,而且对方也看到了这一点,那就不必把这条支路走完。下面从上下界传递、走法排序、参数设置三个角度讲清楚。

3.1 搜索主循环:alpha 和 beta 两个边界怎么传递

alpha 和 beta 代表一个搜索区间:alpha 是红方目前能保证的最大下限,beta 是黑方目前能接受的最小上限。红方节点只更新 alpha,黑方节点只更新 beta。一旦 alpha >= beta,说明当前分支对其中一个玩家已经明显不如兄弟分支,再往下搜也不会改变根节点的最终选择,直接 break。

private int alphaBeta(int depth, int side, int alpha, int beta) { if (depth == 0) { return evaluator.evaluateBoard(board, side); } List<int[]> moves = situation.generateMoves(board, side); if (moves.isEmpty()) { return side == RED ? Integer.MIN_VALUE + depth : Integer.MAX_VALUE - depth; } orderMoves(moves); if (side == RED) { int best = Integer.MIN_VALUE; for (int[] move : moves) { situation.makeMove(board, move); int score = alphaBeta(depth - 1, BLACK, alpha, beta); situation.undoMove(board, move); best = Math.max(best, score); alpha = Math.max(alpha, best); if (alpha >= beta) break; } return best; } int best = Integer.MAX_VALUE; for (int[] move : moves) { situation.makeMove(board, move); int score = alphaBeta(depth - 1, RED, alpha, beta); situation.undoMove(board, move); best = Math.min(best, score); beta = Math.min(beta, best); if (alpha >= beta) break; } return best; }

逻辑说明分三块:第一,moves.isEmpty() 表示当前方无棋可走,大概率是将死或困毙,用一个接近整型极值的分表达“输定”,depth 加进去是为了让程序在多个输法里优先选慢一点输的;第二,orderMoves 在搜索前把走法排序,让“看起来更好的走法”先被搜索,剪枝效率会成倍提升;第三,alpha 和 beta 在递归过程中逐层收窄,区间越小,剪掉的分支越多。这个剪枝是精确的——它不会漏掉最优走法,只砍掉那些确定不会影响根节点结论的分支。

3.2 走法排序:为什么“先吃子后走闲棋”能让搜索快一倍

剪枝剪得好不好,很大程度取决于先搜哪种走法。如果每次都先吃车,alpha 和 beta 会很快收拢,后面大量分支被剪掉;如果每次都先走边兵,上下界一直松垮,剪枝几乎不发生。这就是走法排序的意义,也是很多简化版 Alpha-Beta 实现效率差的直接原因。

private void orderMoves(List<int[]> moves) { moves.sort((a, b) -> { int loseA = Math.abs(board[a[2]][a[3]]); // 终点被吃子价值 int loseB = Math.abs(board[b[2]][b[3]]); return Integer.compare(loseB, loseA); // 吃越大的子越靠前 }); }

这段排序的逻辑很简单:走法四元组的后两位是终点坐标,终点如果有对方的子,说明这是一步吃子,被吃子价值越大,越值得先搜。实际工程里可以升级成 MVV-LVA 策略,也就是“先用小子吃大子、再用大子吃小子”的组合排序,但那是优化项,不急。先保证吃子优先,Alpha-Beta 的剪枝率就能从 30% 拉到 80% 以上,这一步对搜索速度的影响比把 Java 换成 C++ 还明显。

3.3 三个搜索参数怎么设:深度、时间上限与迭代加深

源码里的 AlphaBeta 类接受 depth 参数,但深度不该是唯一控制项。同一个 4 层深度,中局分支四十几个走法,和残局只剩十个走法,耗时差距可能是五倍。下面这张表是我在类似项目里常用的粗估参考,不同机器会有浮动:

搜索深度未剪枝节点数粗估单步耗时参考棋力表现
2 层约 160010ms 内刚会吃子,常走昏招
3 层约 64000100ms 左右不主动送大子
4 层约 250 万1 到 3 秒会兑子、会抽将
5 层约 1 亿需要剪枝支持残局有明显攻杀意识

固定深度有一个现实问题:残局时 4 层 0.5 秒就出来了,中局却要等 3 秒,体验不匀称。我一般会在根节点做迭代加深:先搜 1 层,能搜 2 层就搜 2 层,逐层加深,每层开始前记下时间,超时就返回上一层的结果。迭代加深多花的只是一层最浅搜索的时间,换来的是时间和棋力之间的平滑控制,和 Alpha-Beta 剪枝天然兼容,因为每一层深搜都能复用上一层的走法排序结果。

4. 评估函数决定棋风:AnalysisBean.java 如何告诉电脑“现在谁占优”

搜索负责把未来看穿,评估负责在当前局面打一个分。深度 0 时,极大极小值再往下没有信息,只能靠 AnalysisBean 的判断。如果评估只看棋子数量,电脑会乐于兑子,完全不理解“位置”这种东西;如果评估全是僵硬的死公式,电脑又会显得呆。下面分开拆子力价值、位置加成和难度联动。

4.1 子力价值表:车 900 vs 兵 100 的差距怎么来的

棋类评估最常见的是用“子力分值 + 位置加分”双通道。子力分值告诉电脑吃子划不划算,位置加分解释为什么“兵过河”比“兵在自家底线”值钱。源码 AnalysisBean.java 里大概率有一张基础价值表,常见标定是:

棋子基础分值说明
将/帅10000任何情况不能白送
车900直线长,控制力最强
炮450开局优于马,隔子打
马400受蹩腿限制,残局价值上升
象/相200守家棋子,位置固定
士/仕200九宫防守
兵/卒100过河后加成明显

这个表不是唯一的,有人把炮调到 480,把马调到 470,都能跑,差别在于电脑会倾向于多换还是多躲。重点在于车和将的比值要足够极端,否则搜索在残局可能放弃防守去吃一个卒。实际代码用两个数组就够:baseValue[8] 存类型基础分,for 循环遍历棋盘累加。

4.2 把“位置优势”量化:过河兵、边马、巡河车各加几分

位置加分是把棋力从“会吃子”拉到“会下棋”的关键。AnalysisBean.java 里如果只有子力分,电脑会傻乎乎把马跳到边线,因为边马和中心马在基础价值表里完全一样。加一个薄弱的位置权重,问题就解决了。

private int positionBonus(int pieceType, int row, int col) { if (pieceType == Role.PAWN) { // 红方在上方,row 越大越接近对方底线 return Math.max(0, (9 - row) * 10); } if (pieceType == Role.KNIGHT) { // 边马、角马减分,贴近中心区域正常 if (col == 0 || col == 8) { return -20; } } return 0; }

这段代码的含义很直白:兵每向对方底线前进一步多 10 分,过河之前因为被自家棋盘挡住,row 还没超过中线,加分是 0,过河之后越走越值钱;马落在边线直接扣 20,逼着电脑把马往纵深跳。实际工程里位置加成一般不是简单的几个 if,而是一张 8 行 10 列的 int[][] 位置表,每种棋子配一张,搜索时按 row 和 col 直接查表,性能比每次计算快得多。AnalysisBean 里如果保留的是基础表加少量规则,那也是够用的,重点不在于表有多精细,而在于有没有这个维度。

4.3 难度档位:浅深度加粗评估,还是深深度加细评估

ChessMenuBar 里的“简单、正常、困难”档位,不应该只改搜索深度。深度 2 加完整评估,和深度 4 加只看子力的评估,下出来的棋风格完全不同。我一般会建议源码的使用者把难度档位拆成三个开关:搜索深度、是否启用位置加成、是否启用将军优先项。

public void setDetailLevel(int level) { switch (level) { case 0: usePosition = false; // 简单:只看子力 maxDepth = 2; break; case 1: usePosition = true; // 正常:子力 + 位置 maxDepth = 4; break; case 2: usePosition = true; // 困难:全开 + 将军优先 useCheckBonus = true; maxDepth = 5; break; } }

这个设计是为了让不同难度输出差异化:简单难度反应快但贪吃,困难难度会为了位置优势放弃一些子力交换。还有一个约定必须强调:AnalysisBean 的返回值要保持“正数代表红方优势,负数代表黑方优势”,AlphaBeta 返回后,选择走法时统一按这个约定判断。一旦正负号约定混乱,电脑会疯狂送子。拿到源码第一件事,就是确认 evaluate() 是红正黑负,不是随着当前行动方翻转。两种写法规范上都能成立,但混用必翻车。

5. 避坑清单:搜索卡死、送大子、乱码与循环走子的五个排查方向

上面三章讲的是逻辑,实际下载跑起来,新手踩的坑往往不在算法本身,而在线程、编码、和评估约定上。下面整理五个排查方向,遇到问题按顺序看代码,能省掉大半时间。

提示:按 5.1 到 5.5 的顺序排查前,先把 IDE 的工作区编码统一成 UTF-8,能避免一半乱码问题。

5.1 现象一:人机对弈时界面假死,点完棋盘没反应

点下“电脑走棋”后窗口转圈,几秒后才恢复,是中局深度 4 以上的典型症状。原因是 alphaBeta 搜索直接跑在 Swing 事件线程里,递归期间整个窗口没法重绘,更没法接受鼠标事件。解决方式是把搜索丢到后台线程。Java 里最省事的是 SwingWorker,execute() 后 UI 立刻返回,搜索完成时通过 done() 回调刷新棋盘。如果源码里没有这一步,本地跑起来又卡,优先加这个,而不是降低搜索深度。

5.2 现象二:AI 明明能吃车却走一步闲棋

复盘发现,搜索树里其实出现过吃车的分支,但根节点没有选。这大概率是评估函数符号约定反了——AI 以为自己吃车后局面会变差,因为它在“帮对方”加分。排查方法是固定一个局面单元测试:红车放在能直接吃黑马的位置,强制让 AI 走深度 1,看返回走法是不是吃马。深度 1 不涉及复杂剪枝,能直接暴露评估符号和子力价值表的问题。

5.3 现象三:棋子可以走到棋盘外,或者吃掉自己的子

走子合法性校验不完整的表现。generateMoves 生成了候选走法,但 makeMove 后没有统一做“己方将帅是否暴露”的检测,就会把送将的走法也当成合法走法。解决时先看 Situation.java 里有没有一个 canMoveAfterCheck 或者 isKingInCheck 的入口。如果只有基本吃子规则没有将军过滤,补一个扫线判断:遍历对方所有棋子,看能不能在一步内吃到己方将帅。这一步补完,很多“走着走着突然棋子消失”的问题会一并消失。

5.4 现象四:残局阶段电脑反复走同一个循环

双方各剩两三个兵,电脑就来回拉锯,甚至在同一局面下重复同一招。原因是没有重复局面检测,也没有和棋判定。最简单的补救是加一个“过河拆桥”的评分扰动:给评估函数注入一个 ±5 的随机值,让相同局面每次搜索的得分略有差异,电脑就不会永远复读同一招。正规一点的可以做置换表加三次重复判和,但课程设计阶段通常不需要那么重,先把随机扰动加上,体验就能接受。

5.5 现象五:代码拖进 IDE 中文乱码,文档里的运行入口跑不起来

解压后直接双击源码文件,Windows 默认用 GBK 打开 UTF-8 文件,中文注释全变问号。解决方式是把工程源码编码统一设置成 UTF-8,或者反过来把文件转成 GBK,重点是一致的编码配置。运行入口找不到,多半是因为文档说明里的类的路径和实际目录结构略有出入。我的习惯是拿到压缩包先读文档说明,再看有没有带 main 方法的入口类,别一上来就在工程里全局搜 main。

6. 再进一步:把默认电脑调成“攻、守、稳”三种棋风

搜索深度和评估权重直接决定电脑的性格。默认参数是平衡型,对大多数课程设计够用,但答辩评审如果想看到你对算法的理解,调这三种棋风是成本最低的展示。思路还是改 AnalysisBean 的评分权重,不去动 AlphaBeta 的结构。

进攻型棋风,把车、马、炮的位置加分整体放大,尤其是车在对方二线、马在卧槽位置时给出额外奖励;同时降低防守类加分,让电脑更敢弃子换攻势。防守型反过来,给九宫边缘的位置大量加分,车的评分偏向退回本方埋伏,而不是急着过河。稳重型就是默认参数,子力和位置各占一半,搜索深度 4 到 5。调的时候不要直接删代码,给评估函数留一个权重系数,乘上去就行。

除此之外,可以加一个极简开局库,让前几步不像搜索出来的那么呆板。象棋开局套路非常明显,用一张 HashMap 就能撑住前四步:

private static final Map<String, String> OPENING = new HashMap<>(); static { OPENING.put("初始局面key", "马二进三"); OPENING.put("红马二进三后", "炮8平5"); }

key 存的是开局几步后的局面特征,value 存要走的着法。开局库只生效前 4 步,之后切回 AlphaBeta 搜索。这个做法不需要引入复杂数据库,十几行代码就能让电脑开局有模有样,比硬背棋谱实在得多。

验证三种棋风有没有生效,别靠肉眼多看几盘,写一个固定局面的单测更快:摆一个“中炮对屏风马”的常见局面,分别用三个难度档位跑一步,看输出的走法是不是不一样。如果三档结果完全一致,就说明难度切换只改了深度或没切换成功。那次我调棋风,把进攻权重拉到 0.9,电脑果然喜欢反弃车轰士,看起来很凶,却连一个马后炮都防不住。后来我学乖了,任何棋类 AI 资源拿到手,先写一个固定局面的单测,再改搜索参数。每次动手前都强制走一遍评估符号、深度上限、后台线程这三件事,能省掉大半调试时间。希望帮到你。

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

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

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

立即咨询