资讯动态

Java五子棋AI算法:四方向扫描与Minimax搜索实战

发布时间:2026/10/27 9:40:14 来源:尧图企业网站定制
简介这是一份面向高校计算机专业学生的Java五子棋课程设计完整项目适合正在学习面向对象编程、数据结构与算法、GUI开发及人工智能入门的人群可用于课程作业提交、项目实战练习或算法学习参考。压缩包共56个文件约5.16MB包含12个java源码、19个class编译文件、6个png与5个jpg界面素材、5个xml配置、2个jar依赖库及dat数据文件等覆盖源码、资源、配置与打包产物结构完整。项目重点在于胜负判断的算法解析围绕水平、垂直及两条对角线四个方向遍历棋盘数组检查落子后是否形成连续五子并鼓励引入Minimax与α-β剪枝实现简单AI对弈。界面部分采用Swing或JavaFX构建棋盘交互实时反馈轮次与胜负状态。目前已有46人学习读者可借此理解搜索策略、评估函数与代码结构优化为软件开发或人工智能方向积累实践基础。1. 五子棋课程设计里算法解析为什么要按四个方向拆很多同学做 Java 五子棋课程设计棋盘画完、落子能下、胜负也能判但一到答辩就被问住你的 AI 到底怎么算的为什么横着能赢、斜着就漏判我当年第一次交这个作业就是栽在“只判了一个方向”上老师随手摆了条斜线程序毫无反应那场面相当尴尬。这个标题里的“分四个方向”说的不是把棋盘转四圈而是胜负判定和 AI 估值都要沿横、竖、正斜、反斜四条线扫描。它解决的是两个核心问题一是落子后如何快速判断五连二是 AI 如何给每个空位打分选出最优点。适合正在做 Java 课程设计、想拿高分又不想只交个“能跑就行”版本的同学也适合想借五子棋练手搜索算法的初学者。下面我按自己踩过的路把选型、实现、参数和坑一次讲清。2. 棋盘数据结构与四方向扫描的底层逻辑2.1 为什么用二维数组而不是 List 存棋盘五子棋棋盘是标准的 15×15 网格最自然的存储就是int[][] board。我见过有人用ArrayListPoint存已落子位置判断胜负时遍历列表找相邻点代码写起来好像很灵活实际每次判定都要做距离计算和集合查找复杂度直接上去。二维数组的好处是索引即坐标board[row][col]一步到位四个方向的偏移量用常量数组就能统一处理。选型上还有一个细节用0表示空、1表示黑、2表示白比用char或枚举更省事因为后续估值函数要做数值累加整数直接参与运算不用来回转换。棋盘大小建议做成常量BOARD_SIZE 15别到处写死 15否则改尺寸时满屏找数字。public class Board { public static final int SIZE 15; public static final int EMPTY 0; public static final int BLACK 1; public static final int WHITE 2; private final int[][] grid new int[SIZE][SIZE]; public boolean isEmpty(int row, int col) { return grid[row][col] EMPTY; } public void place(int row, int col, int player) { grid[row][col] player; } public int get(int row, int col) { return grid[row][col]; } }这段代码把棋盘状态和基本操作封装起来SIZE、EMPTY、BLACK、WHITE都是常量后续所有方向扫描和估值都依赖它们。参数上唯一要留意的是行列范围落子前必须校验0 row SIZE否则数组越界直接抛异常AI 搜索时尤其容易在边界外试探。2.2 四个方向的偏移量到底怎么定义所谓四个方向指的是从当前落子点出发的四条轴线水平(0,1)、垂直(1,0)、正斜(1,1)、反斜(1,-1)。每条轴线要往正负两个方向同时延伸才能覆盖整条连线。很多人只写一个方向比如只往右和往下数结果就是开头说的斜线漏判。我一般把方向定义成二维数组配合一个“正反双向计数”的循环。这样胜负判定和 AI 估值可以共用同一套方向常量不用写四遍几乎一样的代码。这里的关键认知是五子棋的“五连”本质是某条轴线上连续同色棋子数达到 5所以任何判定都必须沿轴线双向展开。// 四个方向横、竖、正斜、反斜 public static final int[][] DIRECTIONS { {0, 1}, // 水平 {1, 0}, // 垂直 {1, 1}, // 正斜 {1, -1} // 反斜 };DIRECTIONS里每个元素是{行增量, 列增量}。注意反斜方向列增量是-1从左上往右下看是(1,1)从右上往左下看就是(1,-1)两者合起来才覆盖两条对角线。参数上不要随意改方向顺序因为后面估值函数里我会用方向索引来区分权重顺序乱了权重就对不上。2.3 胜负判定的双向扫描实现有了方向常量判定五连就变成对每个方向从落子点分别向正、负方向数同色棋子两边数量相加再加自己达到 5 即胜。这里有个容易翻车的点——计数时先走一步再判断还是先判断再走边界处理不一样。我习惯先向一个方向走越界或颜色不同就停这样不会把落子点自己重复计入。public boolean checkWin(int row, int col, int player) { for (int[] dir : DIRECTIONS) { int count 1; // 包含当前落子 // 正方向 count countDirection(row, col, dir[0], dir[1], player); // 负方向 count countDirection(row, col, -dir[0], -dir[1], player); if (count 5) { return true; } } return false; } private int countDirection(int row, int col, int dRow, int dCol, int player) { int count 0; int r row dRow; int c col dCol; while (r 0 r SIZE c 0 c SIZE grid[r][c] player) { count; r dRow; c dCol; } return count; }checkWin对四个方向各做一次双向计数count初始为 1 代表当前棋子。countDirection负责沿给定增量一直走直到越界或遇到异色。参数dRow、dCol可正可负所以正负方向调用同一个方法只是符号相反。这段逻辑跑通后横竖斜四种赢法都能判出来不会再出现斜线漏判。3. AI 估值函数把四个方向变成可比较的分数3.1 棋型识别活四、冲四、活三怎么区分胜负判定只回答“赢没赢”AI 要回答“哪步更好”所以需要给每个候选点打分。打分的依据是棋型连五最高活四次之冲四和活三再次活二、眠二递减。所谓活四是两边都空的四连下一手必成五冲四是一边被堵的四连只有一种成五方式。区分它们靠的还是四个方向的连续计数和两端空位判断。我一般把棋型抽象成“连续同色数 两端是否开放”的组合。比如连续 4 个、两端都空就是活四连续 4 个、一端被堵就是冲四。连续 3 个两端都空是活三一端被堵是眠三。这个判断必须四个方向都做因为一个点可能在横向上形成活三同时在斜向上形成冲四综合分要叠加。3.2 估值函数的代码结构与权重表估值函数的核心是对某个空位假设某方落子后沿四个方向统计形成的棋型查权重表累加得分。权重表是经验值不同实现会有差异但量级关系要拉开否则 AI 会分不清活四和活三哪个更急。// 棋型分值数值是经验值可按手感微调 public static final int FIVE 1000000; public static final int LIVE_FOUR 100000; public static final int RUSH_FOUR 10000; public static final int LIVE_THREE 8000; public static final int SLEEP_THREE 500; public static final int LIVE_TWO 300; public static final int SLEEP_TWO 50; public int evaluatePoint(int row, int col, int player) { int total 0; for (int[] dir : DIRECTIONS) { int count 1; int openEnds 0; // 统计正负方向连续数和开放端 // ... 具体统计逻辑见下节 total scoreOf(count, openEnds); } return total; }FIVE到SLEEP_TWO是棋型权重量级从百万到几十保证 AI 优先堵活四、再考虑活三。evaluatePoint对四个方向分别统计后累加这样横向活三加斜向活三会得到更高分符合“双三很危险”的直觉。参数上这些数值不是标准答案你可以按自己 AI 的表现微调但别把活三设得比冲四还高否则 AI 会放着必输的冲四不管去凑活三。3.3 攻防一体的打分为什么既要算自己也要算对手只算自己进攻分的 AI 是“莽夫”对手活四了它还在凑自己的活三。正确做法是对每个空位分别计算“我方落子得分”和“对方落子得分”然后综合。常见策略是myScore * 1.0 oppScore * 0.9让 AI 稍微偏向进攻但绝不忽视防守。public int evaluateMove(int row, int col, int me, int opponent) { int myScore evaluatePoint(row, col, me); int oppScore evaluatePoint(row, col, opponent); // 防守权重略低于进攻避免 AI 过于保守 return (int) (myScore * 1.0 oppScore * 0.9); }evaluateMove里me是当前 AI 执子方opponent是对手。myScore衡量进攻价值oppScore衡量如果对手占这个点有多危险两者加权相加。参数0.9是防守系数调高 AI 更爱堵调低更爱攻。我试过 0.8 到 1.0 之间0.9 在多数局面下比较均衡。注意这里假设了evaluatePoint内部会临时落子再撤销否则统计不到落子后的棋型。4. 搜索与落子决策从单层贪心到极小极大4.1 候选点生成别对 225 个格子全算一遍15×15 有 225 个空位每步都全盘估值虽然能跑但没必要。五子棋的有效落子通常集中在已有棋子附近我一般只取“距离任意已有棋子不超过 2 格”的空位作为候选。这样候选数从两百多降到几十搜索速度明显提升棋力几乎不受影响。public Listint[] generateCandidates(Board board) { Listint[] candidates new ArrayList(); for (int r 0; r Board.SIZE; r) { for (int c 0; c Board.SIZE; c) { if (!board.isEmpty(r, c)) continue; if (hasNeighbor(board, r, c, 2)) { candidates.add(new int[]{r, c}); } } } return candidates; } private boolean hasNeighbor(Board board, int row, int col, int range) { for (int r row - range; r row range; r) { for (int c col - range; c col range; c) { if (r 0 || r Board.SIZE || c 0 || c Board.SIZE) continue; if (!board.isEmpty(r, c)) return true; } } return false; }generateCandidates遍历全盘只保留hasNeighbor为真的空位。range 2表示周围两格内有棋子就算有效这个参数调大候选变多、棋力略升但变慢调小则相反。开局第一手棋盘全空hasNeighbor全为假需要特判直接下天元否则候选列表为空 AI 会卡住。4.2 极小极大搜索与深度控制单层贪心容易被对手的“双三”套路骗加一层搜索就稳很多。极小极大Minimax的思路是AI 假设自己选最优对手也选最优往前推若干层。五子棋分支多深度一般控制在 2 到 4 层再深就要上 Alpha-Beta 剪枝否则课程设计的机器上跑一步要好几秒。public int minimax(Board board, int depth, boolean isMax, int me, int opponent) { if (depth 0) { return evaluateBoard(board, me, opponent); } Listint[] candidates generateCandidates(board); if (isMax) { int best Integer.MIN_VALUE; for (int[] pos : candidates) { board.place(pos[0], pos[1], me); best Math.max(best, minimax(board, depth - 1, false, me, opponent)); board.place(pos[0], pos[1], Board.EMPTY); // 撤销 } return best; } else { int best Integer.MAX_VALUE; for (int[] pos : candidates) { board.place(pos[0], pos[1], opponent); best Math.min(best, minimax(board, depth - 1, true, me, opponent)); board.place(pos[0], pos[1], Board.EMPTY); } return best; } }minimax里isMax为真表示轮到 AI取最大值为假表示对手取最小值。每次落子后递归返回前必须撤销否则棋盘状态被污染后续分支全错。depth是剩余搜索深度到 0 就调evaluateBoard做全局估值。参数上深度每加 1耗时大约翻几倍课程设计里 3 层加剪枝通常够用。4.3 Alpha-Beta 剪枝的接入位置Alpha-Beta 剪枝是在 Minimax 基础上把已经能确定“不会选”的分支提前砍掉。接入位置就在取最大/最小的循环里维护alpha和beta两个边界一旦alpha beta就 break。它不改变搜索结果只减少计算量是性价比最高的优化。public int alphaBeta(Board board, int depth, int alpha, int beta, boolean isMax, int me, int opponent) { if (depth 0) return evaluateBoard(board, me, opponent); Listint[] candidates generateCandidates(board); if (isMax) { for (int[] pos : candidates) { board.place(pos[0], pos[1], me); alpha Math.max(alpha, alphaBeta(board, depth - 1, alpha, beta, false, me, opponent)); board.place(pos[0], pos[1], Board.EMPTY); if (alpha beta) break; // 剪枝 } return alpha; } else { for (int[] pos : candidates) { board.place(pos[0], pos[1], opponent); beta Math.min(beta, alphaBeta(board, depth - 1, alpha, beta, true, me, opponent)); board.place(pos[0], pos[1], Board.EMPTY); if (alpha beta) break; } return beta; } }alpha是 AI 已找到的最好分下界beta是对手能接受的最差分上界。alpha beta说明这个分支对手不会让它发生直接剪掉。参数初始调用时alpha Integer.MIN_VALUE、beta Integer.MAX_VALUE。注意候选点排序会影响剪枝效率先试估值高的点能剪更多这是进阶优化课程设计里不排也能跑。5. 避坑与排查四个方向实现里最容易翻车的地方5.1 斜线方向漏判横竖正常现象横着竖着五连都能判赢斜着连成五个程序没反应。原因方向数组只写了{0,1}和{1,0}漏了{1,1}和{1,-1}或者只写了正斜没写反斜。解决检查DIRECTIONS是否恰好四个元素并且反斜的列增量是-1。我建议写个单元测试手动在棋盘上摆四条五连线逐条验证checkWin返回 true。5.2 边界越界导致 AI 崩溃现象AI 思考时抛ArrayIndexOutOfBoundsException尤其在棋盘边缘落子后。原因方向扫描或候选生成时没做边界校验row dRow直接当索引用。解决所有沿方向走的地方循环条件必须先判r 0 r SIZE c 0 c SIZE再访问数组。countDirection和hasNeighbor里都要有这层保护顺序不能反。5.3 估值权重倒挂AI 行为反常现象AI 放着对手的活四不堵跑去连自己的活三一步就被将死。原因权重表里LIVE_THREE设得比RUSH_FOUR还高或者防守系数0.9调得太低。解决确保FIVE LIVE_FOUR RUSH_FOUR LIVE_THREE量级至少差几倍。防守系数先固定 0.9观察 AI 是否堵关键点再微调。血泪经验是别一次改好几个参数否则出了问题不知道是哪个引起的。5.4 递归落子没撤销棋盘状态污染现象AI 第一步正常第二步开始乱下或者同一局面每次算出的分不一样。原因Minimax 或 Alpha-Beta 里落子后忘记board.place(..., EMPTY)撤销递归返回后棋盘残留了假设棋子。解决落子和撤销必须成对出现建议用try-finally包住或者写个makeMove/undoMove辅助方法。这个坑很隐蔽因为单步测试看不出来多步搜索才暴露。5.5 候选点为空导致 AI 卡死现象开局第一手 AI 不落子程序无响应或抛异常。原因generateCandidates依赖周围有棋子空棋盘时返回空列表搜索循环一次没进。解决在落子决策入口特判如果棋盘为空直接下天元(7,7)如果候选为空但棋盘非空退化为全盘扫描空位。这个边界情况课程设计演示时经常被忽略答辩现场卡住就很尴尬。6. 让 AI 更聪明的两个实战技巧第一个技巧是候选点排序。Alpha-Beta 剪枝的效率极度依赖搜索顺序先试高分点能大幅减少节点数。做法很简单在generateCandidates之后对每个候选调一次evaluateMove按分数从高到低排序再进搜索。我实测在 3 层深度下排序后节点数能降一半左右AI 落子从明显卡顿变成基本即时。代价是每个候选多算一次估值但相比省下的递归开销完全值得。candidates.sort((a, b) - { int scoreA evaluateMove(a[0], a[1], me, opponent); int scoreB evaluateMove(b[0], b[1], me, opponent); return Integer.compare(scoreB, scoreA); // 降序 });这段排序放在alphaBeta取候选之后、循环之前。evaluateMove返回综合攻防分降序排列让最可能的好点先被搜索。注意排序本身有开销候选数几十个时没问题如果候选上百要权衡。第二个技巧是必胜/必堵的快速通道。在搜索之前先扫一遍候选如果某点让我方成五直接下如果某点让对手成五直接堵。这两类情况不需要搜索能省时间也能避免搜索深度不够导致的低级失误。判断方法就是临时落子调checkWin成五立即返回。场景处理方式判断依据我方有落子即成五直接落子临时落子后checkWin为真对手有落子即成五直接堵对手临时落子后checkWin为真我方有活四机会优先搜索估值分接近LIVE_FOUR普通局面进搜索走 Alpha-Beta这张表是我落子决策的顺序快速通道在前搜索在后。参数上“成五”判断用checkWin最稳别自己另写一套计数逻辑容易和胜负判定不一致。最后说个验证方法写一个简单的对弈测试让 AI 分别执黑执白自己跟自己下若干局观察有没有出现“该赢没赢”“该堵没堵”的局面。我一般会打印每步的候选数和搜索节点数节点数突然暴涨往往意味着剪枝失效或候选生成异常。这套东西调通后课程设计答辩被问算法细节基本都能接住。我自己的习惯是每改一个权重就存一版翻车了能回退别嫌麻烦。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑