资讯动态

Java五子棋胜负判定算法:四个方向增量扫描与课程设计避坑指南

发布时间:2026/10/27 19:05:56 来源:尧图企业网站定制
简介这是一份面向高校计算机专业学生的Java五子棋课程设计完整源码包适合正在做程序设计实践、需要理解博弈算法与GUI开发的初学者和进阶者。项目以面向对象思想实现双人对弈与简单AI对战重点解析了水平、垂直及两条对角线共四个方向的胜负判断算法并涉及Minimax与α-β剪枝等搜索评估技术同时使用Swing或JavaFX构建可视化棋盘界面。压缩包共56个文件约5.16MB包含12个java源文件、19个class编译文件、5个xml配置、6个png与5个jpg界面素材以及jar依赖、dat数据文件和README说明结构完整可直接导入运行。目前已有46人学习下载。通过研读源码读者能掌握棋盘数组遍历、连子检测、界面事件响应与AI落子评估的完整实现思路并借鉴代码分层与注释规范为课程设计答辩和后续软件开发打下基础。1. Java 五子棋带算法解析课程设计里最容易被追问的四个方向到底是什么很多同学做 Java 五子棋课程设计棋盘画完、落子能下、胜负也能判但一到答辩就被问住你这个赢棋判断是怎么写的四个方向具体指哪四个为什么不是八个时间复杂度多少这时候如果只会说“我用了循环遍历”基本就凉了。所谓“带算法解析分四个方向”核心就是把五子棋的胜负判定从“看起来能跑”做到“讲得清楚、改得动、测得住”。它解决的不是界面好不好看而是落子之后如何在水平、垂直、主对角线、副对角线这四个方向上快速判断是否形成连续五子。适合正在做 Java 课程设计、需要提交源码和文档、还要面对老师追问算法细节的本科生。下面我按自己带课程设计的经验把选型、实现、参数和踩坑一次讲透。2. 四个方向判定算法从暴力遍历到增量扫描怎么选2.1 为什么是四个方向而不是八个棋盘上任意一个落子点理论上周围有八个相邻格子。但判断五子连线时方向是成对出现的水平方向向左和向右本质上是同一条直线垂直方向向上和向下也是同一条直线主对角线左上和右下是一对副对角线右上和左下是一对。所以只需要四个方向每个方向同时向正负两侧延伸统计就能覆盖全部八邻域。这个结论看起来简单但很多课程设计文档里写“八个方向判断”代码里却写了八次循环每次只往一个方向数最后还要去重。不是不能跑而是代码冗余、答辩时解释成本高。我一般会直接告诉学生四个方向是标准做法八个方向是没想清楚对称性。四个方向用坐标增量表示方向行增量 dr列增量 dc说明水平01左右扫描垂直10上下扫描主对角线11左上到右下副对角线1-1右上到左下有了这张表后面所有代码都围绕它展开。2.2 暴力遍历法的实现与复杂度最直观的做法是每次落子后遍历整个棋盘对每个已落子的位置检查四个方向是否形成五连。这种方法在 15×15 棋盘上完全能跑因为总共 225 个格子每个格子最多检查 4 个方向、每个方向最多延伸 4 格计算量很小。// 暴力遍历每次落子后全盘扫描 public boolean checkWinByFullScan(int[][] board, int player) { int size board.length; // 四个方向右、下、右下、左下 int[][] dirs {{0,1},{1,0},{1,1},{1,-1}}; for (int r 0; r size; r) { for (int c 0; c size; c) { if (board[r][c] ! player) continue; for (int[] d : dirs) { int count 1; // 只往正方向数避免重复 for (int step 1; step 5; step) { int nr r d[0] * step; int nc c d[1] * step; if (nr 0 || nr size || nc 0 || nc size) break; if (board[nr][nc] ! player) break; count; } if (count 5) return true; } } } return false; }这段代码的逻辑是对每个属于当前玩家的格子沿四个方向之一正向延伸统计连续同色棋子数量。注意这里只往正方向数因为反向的情况会在另一个起点的正向扫描中被覆盖。比如水平方向从最左边的棋子往右数就能覆盖整条线。参数说明board是二维数组0 表示空1 和 2 表示两个玩家dirs是方向增量数组step控制延伸步数最大到 4 是因为加上起点本身最多 5 个。时间复杂度是 O(N²)N 是棋盘边长15×15 时约 225 次外层循环实际运行时间在毫秒级。但这种方法有个问题每次落子都要全盘扫描虽然 15×15 无所谓但如果课程设计要求支持 19×19 甚至更大棋盘或者要求高频落子就会显得笨。更重要的是答辩时老师可能会问“你能不能只检查落子点周围”这时候就需要增量扫描。2.3 增量扫描只检查落子点周围增量扫描的思路是既然只有新落子的位置可能产生新的五连那就只以这个点为中心向四个方向的正负两侧延伸统计。这样每次落子只需要检查 4 个方向、每个方向最多 8 个格子计算量恒定。// 增量扫描只检查最新落子点 public boolean checkWinByIncremental(int[][] board, int row, int col, int player) { int size board.length; // 四个方向水平、垂直、主对角线、副对角线 int[][] dirs {{0,1},{1,0},{1,1},{1,-1}}; for (int[] d : dirs) { int count 1; // 当前落子本身 // 正方向延伸 for (int step 1; step 5; step) { int nr row d[0] * step; int nc col d[1] * step; if (nr 0 || nr size || nc 0 || nc size) break; if (board[nr][nc] ! player) break; count; } // 负方向延伸 for (int step 1; step 5; step) { int nr row - d[0] * step; int nc col - d[1] * step; if (nr 0 || nr size || nc 0 || nc size) break; if (board[nr][nc] ! player) break; count; } if (count 5) return true; } return false; }逻辑说明以落子点(row, col)为中心对每个方向分别向正负两个方向延伸。正方向用d负方向用-d。每次延伸前先做边界检查再判断棋子颜色。如果某个方向累计数量达到 5立即返回 true。参数说明row和col是最新落子的坐标player是当前玩家编号dirs同样是四个方向。注意这里的count初始为 1因为落子点本身已经占据一格。正负方向各最多延伸 4 步所以单个方向最多检查 8 个格子四个方向共 32 次判断比全盘扫描快一个数量级。实际课程设计中我建议直接用增量扫描。它代码更短、逻辑更清晰、答辩时更容易解释。全盘扫描可以作为对比方案写在文档里体现你思考过不同实现。2.4 方向数组的边界处理与常见误用方向数组虽然简单但边界处理是翻车重灾区。最常见的错误是在负方向延伸时忘记检查nr 0或nc 0导致数组越界。Java 会抛出ArrayIndexOutOfBoundsException如果没加异常处理程序直接崩溃。另一个常见误用是把四个方向写成八个方向然后每个方向只往一个方向数最后统计时发现同一条线被数了两次导致计数翻倍。比如水平方向从左往右数一次从右往左又数一次如果两条线都达到 5就会误判。正确做法是四个方向每个方向双向延伸。还有一个坑是棋盘边界和棋子边界的区别。棋盘边界是0到size-1但棋子边界还要考虑连续五子的最大延伸范围。比如在(0,0)落子主对角线正方向最多延伸到(4,4)负方向直接越界。所以边界检查必须放在每一步延伸之前不能只检查起点。提示写方向数组时建议把dr和dc分开写不要用int[][] dirs {{0,1},{1,0},{1,1},{1,-1}}这种紧凑写法。分开写虽然多几行但调试时一眼就能看出哪个方向出了问题。3. 棋盘数据结构与落子逻辑二维数组够不够用3.1 二维数组 vs 一维数组 vs 稀疏矩阵课程设计里最常用的棋盘数据结构是二维数组int[][] board new int[15][15]。优点是直观、索引方便、内存连续。15×15 的 int 数组只占 900 字节左右完全不需要考虑内存优化。有些同学会问能不能用一维数组int[] board new int[225]可以但没必要。一维数组需要手动计算索引index row * size col代码可读性下降而且五子棋的四个方向判断天然适合二维坐标。除非课程设计明确要求“不能用二维数组”否则我建议直接用二维数组。稀疏矩阵适合棋盘很大但落子很少的场景比如 100×100 棋盘只下了 20 步。但课程设计通常固定 15×15稀疏矩阵的额外复杂度不值得。如果老师追问“大数据量怎么办”可以提一句“可以用 HashMap 存储已落子位置”但不要真的在课程设计里实现除非你时间充裕。3.2 落子、悔棋与状态维护落子逻辑本身很简单检查坐标是否在棋盘内、是否为空、然后写入玩家编号。但课程设计通常要求悔棋功能这就需要维护落子历史。// 落子历史记录 private Dequeint[] history new ArrayDeque(); public boolean placeStone(int row, int col, int player) { if (row 0 || row size || col 0 || col size) return false; if (board[row][col] ! 0) return false; board[row][col] player; history.push(new int[]{row, col, player}); return true; } public boolean undo() { if (history.isEmpty()) return false; int[] last history.pop(); board[last[0]][last[1]] 0; return true; }逻辑说明placeStone先做边界和空位检查然后写入棋盘并把落子信息压入栈。undo从栈顶弹出最近一步把对应格子清空。用ArrayDeque而不是Stack因为Stack是遗留类性能较差。参数说明history存储{row, col, player}三元组push和pop是栈操作保证后进先出。注意悔棋后要切换当前玩家否则会出现连续下两步的问题。3.3 棋盘重置与多局连下课程设计通常要求支持“重新开始”。最简单的做法是重新new一个二维数组但这样会丢失历史记录。更稳妥的做法是写一个reset方法把棋盘所有格子清零同时清空历史栈。public void reset() { for (int r 0; r size; r) { Arrays.fill(board[r], 0); } history.clear(); currentPlayer 1; }逻辑说明逐行用Arrays.fill清零比双重循环更简洁。history.clear()清空落子历史currentPlayer重置为玩家 1。参数说明size是棋盘边长board是二维数组。注意Arrays.fill对二维数组要逐行调用不能直接Arrays.fill(board, 0)因为那样只会把每一行引用替换掉不会清空内部元素。注意如果课程设计要求“多局连下”并统计胜率reset时不要重置胜率统计变量只重置棋盘和当前玩家。4. 胜负判定与四个方向的代码落地从伪代码到可运行类4.1 完整胜负判定类把前面讲的增量扫描封装成一个独立的类方便在项目里复用也方便答辩时展示模块化设计。public class WinChecker { private final int size; private static final int[][] DIRS { {0, 1}, // 水平 {1, 0}, // 垂直 {1, 1}, // 主对角线 {1, -1} // 副对角线 }; public WinChecker(int size) { this.size size; } public boolean check(int[][] board, int row, int col, int player) { for (int[] d : DIRS) { int count 1; count countDirection(board, row, col, d[0], d[1], player); count countDirection(board, row, col, -d[0], -d[1], player); if (count 5) return true; } return false; } private int countDirection(int[][] board, int row, int col, int dr, int dc, int player) { int count 0; for (int step 1; step 5; step) { int nr row dr * step; int nc col dc * step; if (nr 0 || nr size || nc 0 || nc size) break; if (board[nr][nc] ! player) break; count; } return count; } }逻辑说明check方法遍历四个方向对每个方向分别调用countDirection统计正负两侧的连续棋子数。countDirection接收方向增量dr和dc从落子点出发每次走一步直到越界或遇到不同颜色。最终count加上落子点本身如果大于等于 5 就判定胜利。参数说明DIRS是静态常量避免每次调用都创建数组size是棋盘边长board是二维数组row和col是落子坐标player是玩家编号。countDirection的step从 1 到 4因为加上落子点最多 5 个。4.2 四个方向的测试用例设计写完代码不算完课程设计答辩时老师很可能让你现场跑测试。我一般会准备一组边界测试用例覆盖四个方向的正常和异常情况。测试场景落子序列预期结果水平五连(7,3)(7,4)(7,5)(7,6)(7,7)玩家1胜垂直五连(3,7)(4,7)(5,7)(6,7)(7,7)玩家1胜主对角线五连(3,3)(4,4)(5,5)(6,6)(7,7)玩家1胜副对角线五连(3,11)(4,10)(5,9)(6,8)(7,7)玩家1胜边界水平(0,0)(0,1)(0,2)(0,3)(0,4)玩家1胜边界垂直(0,0)(1,0)(2,0)(3,0)(4,0)玩家1胜四连不胜(7,3)(7,4)(7,5)(7,6)继续游戏中间断开(7,3)(7,4)(7,6)(7,7)(7,8)不胜这些用例可以直接写成 JUnit 测试也可以在主程序里加一个调试模式手动触发。关键是答辩时能说清楚我测了四个方向测了边界测了四连不胜。4.3 把胜负判定接入主流程胜负判定不能孤立存在必须接入落子流程。典型的主循环是这样的public class Game { private int[][] board; private WinChecker checker; private int currentPlayer 1; private boolean gameOver false; public void onStonePlaced(int row, int col) { if (gameOver) return; if (!placeStone(row, col, currentPlayer)) return; if (checker.check(board, row, col, currentPlayer)) { gameOver true; System.out.println(玩家 currentPlayer 获胜); return; } currentPlayer 3 - currentPlayer; // 1变22变1 } }逻辑说明onStonePlaced是落子入口先检查游戏是否结束再尝试落子。落子成功后调用checker.check判断胜负。如果胜利设置gameOver标志并输出结果否则切换玩家。3 - currentPlayer是一个小技巧因为玩家编号只有 1 和 23-123-21。参数说明gameOver防止胜利后继续落子currentPlayer初始为 1checker在构造函数里初始化传入棋盘边长。提示如果课程设计要求 AI 对战胜负判定要在 AI 落子后也调用一次不能只检查人类玩家。5. 课程设计避坑四个方向判定最容易翻车的五个地方5.1 现象程序在边界落子时崩溃报 ArrayIndexOutOfBoundsException原因负方向延伸时没有检查nr 0或nc 0。比如在(0,0)落子副对角线负方向是(-1,1)直接越界。解决在每次计算nr和nc后立即检查nr 0 || nr size || nc 0 || nc size不满足就break。不要先访问数组再检查Java 的数组访问越界会直接抛异常。5.2 现象明明没有五连程序却判胜利原因方向数组写成了八个方向每个方向只往一个方向数导致同一条线被重复统计。比如水平方向从左往右数一次从右往左又数一次两次都达到 5计数变成 10。解决坚持四个方向每个方向双向延伸。如果已经写了八个方向改成四个方向加正负延伸。检查代码里是否有dirs长度为 8 的情况。5.3 现象悔棋后胜负判定错乱已经赢了的棋局还能继续下原因悔棋只清空了棋盘格子没有重置gameOver标志。或者悔棋后没有切换玩家导致连续落子。解决悔棋时除了清空格子还要把gameOver设为false并根据历史记录恢复currentPlayer。如果历史栈为空不允许悔棋。5.4 现象AI 对战模式下AI 落子后不判断胜负人类继续下原因胜负判定只写在了人类落子的回调里AI 落子走了另一条代码路径。解决把胜负判定封装成独立方法人类和 AI 落子后都调用同一个方法。不要在两处重复写判定逻辑否则迟早不一致。5.5 现象棋盘重置后历史记录还在悔棋能悔到上一局原因reset方法只清了棋盘没有清空history栈。解决reset里同时调用history.clear()并把currentPlayer重置为 1。如果有多局统计胜率变量不要重置。6. 进阶技巧用位运算加速四个方向判定与答辩演示脚本前面讲的增量扫描已经够用但如果想在课程设计里加一点亮点可以用位运算把四个方向的判定压缩成几个整数操作。思路是把每一行、每一列、每条对角线表示成一个位棋盘落子时更新对应位判断五连时用位运算检查连续 5 位是否为 1。// 位运算示例检查水平方向是否有连续5个1 private boolean hasFiveInRow(int bits) { int mask 0b11111; for (int i 0; i 10; i) { if ((bits (mask i)) (mask i)) return true; } return false; }逻辑说明bits是一个整数每一位代表一个格子是否有当前玩家的棋子。mask是低 5 位全 1 的掩码。循环左移掩码检查是否与bits完全匹配。如果匹配说明有连续 5 个 1。参数说明bits最多 15 位用int足够mask是0b11111即 31循环范围0到10因为 15 位棋盘上连续 5 位的起始位置最多到第 10 位。这个技巧的优点是快缺点是代码可读性下降答辩时如果老师追问你要能解释清楚位运算的含义。我一般建议主判定用增量扫描位运算作为“优化方案”写在文档里不一定要真的替换。答辩演示脚本我习惯准备三个场景第一正常下棋到五连展示胜负判定第二悔棋后重新下展示状态恢复第三边界落子展示边界处理。每个场景控制在 30 秒内不要现场写代码提前准备好测试用例。最后说一个我自己的习惯每次写完胜负判定我都会手动在棋盘四个角各下一组五连再在中间下一组确认四个方向都覆盖到。这个习惯帮我省了很多后悔药因为边界问题往往在答辩现场才暴露。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑