Java Swing象棋AI源码:极大极小值与AlphaBeta剪枝

📅 发布时间:2026/10/18 12:14:43
Java Swing象棋AI源码:极大极小值与AlphaBeta剪枝
简介一套基于 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 Listint[] generateMoves(int[][] board, int side) { Listint[] 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(); // 叶子局面打分 } Listint[] 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 到 5ChessMenuBar 里通过菜单项给玩家选。在单线程 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); } Listint[] 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(Listint[] 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 里最省事的是 SwingWorkerexecute() 后 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 MapString, String OPENING new HashMap(); static { OPENING.put(初始局面key, 马二进三); OPENING.put(红马二进三后, 炮8平5); }key 存的是开局几步后的局面特征value 存要走的着法。开局库只生效前 4 步之后切回 AlphaBeta 搜索。这个做法不需要引入复杂数据库十几行代码就能让电脑开局有模有样比硬背棋谱实在得多。验证三种棋风有没有生效别靠肉眼多看几盘写一个固定局面的单测更快摆一个“中炮对屏风马”的常见局面分别用三个难度档位跑一步看输出的走法是不是不一样。如果三档结果完全一致就说明难度切换只改了深度或没切换成功。那次我调棋风把进攻权重拉到 0.9电脑果然喜欢反弃车轰士看起来很凶却连一个马后炮都防不住。后来我学乖了任何棋类 AI 资源拿到手先写一个固定局面的单测再改搜索参数。每次动手前都强制走一遍评估符号、深度上限、后台线程这三件事能省掉大半调试时间。希望帮到你。本文还有配套的精品资源点击获取