简介面向Java学习者与游戏开发初学者的五子棋项目实现教程围绕棋盘、棋子、玩家与AI算法展开完整演示十五路或十九路棋盘的落子、胜负判定、人机对战等核心流程。压缩包共65个文件大小仅2.73MB内含14个头文件、12个源文件及多个工程配置与资源素材如wav音效、bmp与ico图标等可满足编译运行与二次修改需要。教程涵盖棋盘类、棋子类与玩家类的设计思路重点解析Minimax算法与Alpha-Beta剪枝在AI落子中的应用同时介绍基于Swing或JavaFX的事件驱动界面、合法移动验证及边界处理并附带悔棋、存档、难度调节等扩展功能的实现说明。目前已有205人学习下载借助该包可系统掌握Java面向对象设计、图形界面构建和基础博弈算法适合作为课程设计或自学练手项目。1. Java 五子棋游戏项目完整拆解从棋盘渲染到 AI 落子这份资源能省下你一晚上的排错时间拿到一份标着“Java 五子棋游戏教程”的资源包别急着双击 jar 乱点棋盘。我复盘过好几版同类项目真正让人卡住的问题从来不在五子棋规则本身而在三个细节坐标换算偏移导致棋子总落不到交叉点上、胜负判定写成全盘扫描导致代码又慢又难维护、AI 在事件线程里跑评估把窗口直接卡到“假死”。这份教程资源恰好把这三块都拆平了棋盘用 Swing 绘制15×15 二维数组作数据模型胜负判定走增量检测只查落子点附近的四个方向AI 不做暴力搜索用评分表加候选点剪枝防守意识已经能打赢不少乱点的玩家。适合三类人拿 Java 做课程设计或期末项目的在校生、想系统练一遍 Swing 图形界面和算法配合的开发者以及想快速接手别人棋局代码的维护者。2. 棋盘模型与绘制先把二维数组和像素坐标的换算焊死五子棋界面看着简单实际上一动手就会发现问题棋盘是网格程序内部是数组鼠标点下去拿到的是像素坐标三者之间得有一套统一换算规则。这一章先把地基打好后面所有逻辑都围绕它转。2.1 为什么用 Swing 而不是 JavaFX选型与实际约束我拆过的几个五子棋教程包界面部分不约而同都选 Swing原因很实际JDK 自带的 javax.swing 包不需要任何第三方依赖装好 JDK 就能直接编译运行。JavaFX 在新版本 JDK 里已经和标准包分离要额外配模块路径对初学者来说光是跑通环境就多一道坎。作为课程设计或练习项目“能直接跑”比“界面更现代”重要得多。Swing 的绘图机制也够用JPanel 上重写 paintComponent棋盘网格、棋子、坐标标记全在这里画。棋盘不需要精细的贴图资源画线和画圆就能完成所有渲染。落子后的刷新靠 repaint() 触发系统会把重绘请求合并到事件队列里批量处理不会一帧帧地闪烁。资源包里通常还附了运行脚本双击就能看到棋盘窗口弹出来这一点对判断“代码有没有跑通”非常友好。2.2 棋盘数据模型15×15 数组存三态常量别散落各处棋盘的逻辑核心是一个二维数组每个格子有三种状态空、黑子、白子。用 int 还是 char 都行关键是状态值必须用常量不能写魔法数字。我看过有人直接在代码里写 board[3][5] 1过两天自己都不记得 1 是黑还是白。这份教程资源里的做法是定义三个常量界面逻辑和判断逻辑共用同一套取值。public class GameBoard { public static final int EMPTY 0; public static final int BLACK 1; public static final int WHITE 2; public static final int SIZE 15; private int[][] grid new int[SIZE][SIZE]; public int get(int row, int col) { return grid[row][col]; } public void place(int row, int col, int role) { grid[row][col] role; } public boolean isEmpty(int row, int col) { return grid[row][col] EMPTY; } }这段代码把棋盘数据从界面里剥离出来AI、胜负判断、悔棋都只跟 GameBoard 打交道不碰任何 Swing 组件。SIZE 固定为 15这是标准五子棋棋盘规格15 条线交叉形成 225 个落子点。place 方法直接在数组上赋值不需要做边界检查因为调用方在落子前已经过滤过坐标和状态。2.3 落子坐标换算鼠标点上去的过程误差要控制在半个格子内棋盘界面里有一个经常翻车的地方棋盘面板通常有留白网格不是从 (0,0) 开始的。如果忽略 PADDING换算出来的行列会整体偏移越靠右下角偏移越明显。正确做法是建立两组互逆的方法像素转行列、行列转像素都走同一套公式。public class BoardRenderer { private static final int CELL 40; // 每个格子的像素宽 private static final int PADDING 30; // 棋盘四周留白 public int toPixel(int index) { return PADDING index * CELL; } public int toIndex(int pixel) { return (pixel - PADDING CELL / 2) / CELL; } public boolean isValidPoint(int px, int py) { int row toIndex(py); int col toIndex(px); return row 0 row 15 col 0 col 15; } }换算公式的核心是 toIndex 里的“加半格偏移”。鼠标点下去的位置不总是正好落在交叉点上加 CELL/2 之后再整除等效于四舍五入到最近的交叉点误差不超过半个格子。这个细节新手经常遗漏结果就是棋子像喝醉了一样歪歪扭扭地排成一斜线。isValidPoint 负责拦截越界点击比如点到留白区或窗口边缘直接忽略处理避免落到数组的非法下标上。3. 胜负判定增量检测代替全盘扫描四方向延伸最多四步五子棋的胜负判定是很多人写崩的第一块。直观想法是每次落子后把整个棋盘扫一遍统计每个位置的横向、纵向、斜向连续棋子数超过五个就算赢。这种“每次全盘扫描”的写法不是不能跑而是做了大量和本轮落子无关的计算——棋盘上大部分区域根本没变化又得重新数一遍。3.1 为什么用增量检测胜负只可能围绕刚落的这颗子展开新落下的棋子是棋局最新的变化点胜负条件成立时这颗棋子必须参与其中。换句话说检查以落子位置为中心、四个方向上的连续同色棋子数数量达到五个就判胜。这比全盘扫描少了一个数量级的遍历15×15 的棋盘上也就是每个方向延伸四格总共几十次数组访问。判定方法还能顺带知道是哪一方赢了因为刚落子的角色就是赢家。这种设计还有一个隐藏优点判断逻辑不依赖棋盘全貌只依赖落子点和当前局面天然适合做单元测试。你可以不启动界面直接构造一个数组状态调用判定方法验证结果。后面第六章的验证流程还会用到这个特性。3.2 四方向延伸实现从落子点出发朝两边数到边界为止每个方向的检查逻辑是固定的从落子点朝某个方向走数一数多少颗连续同色棋子再掉头朝反方向数两次数值相加再加上落子本身这一颗就是该方向上的连续棋子总数。代码里用两个循环分别处理两个方向比一个循环里来回切换方向清晰得多。private static final int[][] DIRECTIONS { {0, 1}, // 水平列方向 {1, 0}, // 垂直行方向 {1, 1}, // 主对角线 {1, -1} // 副对角线 }; public boolean checkWin(int row, int col, int role) { for (int[] dir : DIRECTIONS) { int count 1; count countDirection(row, col, role, dir[0], dir[1]); count countDirection(row, col, role, -dir[0], -dir[1]); if (count 5) { return true; } } return false; } private int countDirection(int row, int col, int role, int dr, int dc) { int count 0; for (int step 1; step 5; step) { int nr row dr * step; int nc col dc * step; if (nr 0 || nr 15 || nc 0 || nc 15) { break; } if (board.get(nr, nc) ! role) { break; } count; } return count; }count 初始值为 1代表刚落下的这颗棋子自身。每次沿一个方向走一格遇到同色棋子就 count 加一遇到空位、对方棋子或棋盘边界立刻停止。走四步是刻意控制的因为五子棋只需要连续五颗从落子点出发走到四格以外还没形成五连这一路就不可能再凑出五颗了。DIRECTIONS 数组里四个方向互不重叠每个方向单独统计后再汇总互不影响。这个实现还有一个值得注意的参数细节step 从 1 开始而不是从 0 开始。如果从 0 开始等于把落子点本身算了两遍count 会出现虚高四连会被误判成五连。这种错误在代码审查阶段很难发现但一旦出现对战表现为“明明只连了四颗却判我赢”非常打击信任度。教程资源里把 countDirection 和 checkWin 分开封装目的就是让这种基础逻辑单独可测。3.3 平局判定与长连规则5 和 5 的背后是规则分歧胜负判定写完还得处理两个边界情况。第一个是棋盘下满但仍未分出胜负这就是平局需要在落子循环里检查剩余空位数归零时停止对局。第二个是长连规则如果双方约定“五子连珠即为胜”那么连续六颗甚至七颗也应该算赢判定条件要写成 count 5 而不是 count 5。有些带禁手规则的对局会禁止黑棋形成长连但普通对局教程不会做这么深的规则限制。我在资源里看到的惯例是判定方法返回 boolean调用方在落子后立即检查。如果赢弹窗显示赢家并禁用棋盘如果棋盘已满提示平局。这两个分支分别处理避免赢棋提示被平局提示覆盖。曾有开发者把两个判断写成 else 关系结果最后一子既触发胜利又触发平局弹了两个对话框。解决方式很简单先判胜胜局拿到结果后直接 return不再走平局分支。4. 人机对战 AI评分表加候选点剪枝零搜索也能有章法五子棋的人机 AI 是这份资源里最让人好奇、也最容易写跑偏的部分。有人一上来就想上极大极小搜索写着写着递归深度太深算到天荒地老。这个教程包的做法务实得多不做搜索树只用单层评分表也能让 AI 具备堵活三、占活四的基本对战能力。4.1 先算清楚账单层评估为什么够用15×15 棋盘上空位超过两百个MINIMAX 哪怕只搜两层每次也要评估几百个落子点每个点还要算四个方向的棋型计算量瞬间爆炸。Swing 事件线程又承担着界面刷新稍不留神窗口就“假死”。这份资源把 AI 的定位设成“让玩家有对抗感”而不是“虐杀所有人类”单层评分表完全够用。单层评估的逻辑是对每个候选空位假设 AI 在这里落子计算它形成的棋型价值同时假设对手在这里落子计算对手形成的棋型价值两者按权重相加选总分最高的点。进攻价值高了会导致 AI 只顾自己连子不管对手防守权重高了又会变成纯堵截打不死人。参数调校是这章最后要说的重点。4.2 棋型评分表把连续棋子数和开放端转成分数评估的核心是计算“如果某方在此落子四个方向上分别能形成什么棋型”。一个方向的棋型质量由两个参数决定连续同色棋子的数量以及该方向两端是否仍为空位——也就是开放端的数量。开放端是活的棋型两端都被堵的死棋型价值大打折扣。public int evaluatePoint(int row, int col, int role) { if (!board.isEmpty(row, col)) { return -1; } board.place(row, col, role); int score 0; for (int[] dir : DIRECTIONS) { int[] line buildLine(row, col, dir); score lineScore(line, role); } board.place(row, col, EMPTY); // 评估完必须还原棋盘 return score; } private int[] buildLine(int row, int col, int[] dir) { int[] line new int[9]; int center 4; line[center] board.get(row, col); for (int step 1; step 4; step) { line[center step] getSafely(row dir[0] * step, col dir[1] * step); line[center - step] getSafely(row - dir[0] * step, col - dir[1] * step); } return line; } private int lineScore(int[] line, int role) { int center 4; int count 1; int openEnds 0; for (int step 1; step 5; step) { if (line[center step] role) { count; } else { if (line[center step] EMPTY) { openEnds; } break; } } for (int step 1; step 5; step) { if (line[center - step] role) { count; } else { if (line[center - step] EMPTY) { openEnds; } break; } } return patternScore(count, openEnds); } private int patternScore(int count, int openEnds) { if (count 5) return 100000; if (count 4 openEnds 2) return 10000; if (count 4 openEnds 1) return 8000; if (count 3 openEnds 2) return 5000; if (count 3 openEnds 1) return 800; if (count 2 openEnds 2) return 300; if (count 2 openEnds 1) return 50; return 0; }buildLine 取出以落子点为中心、左右各四格的一条线长度 9越出棋盘边界的位置用 EMPTY 填充这样后续统计时边界当作开放端处理不会因为下标越界报错。这个“边界视作开放端”的细节要留意靠近棋盘边缘的三四连和下在正中间的活三是同等价值实战中边缘棋型往往更难被堵死评分略微偏高没有大问题。lineScore 统计中心点两边的连续同色棋子数遇到第一种非同类棋子就停下并记录这端是开放还是被堵。这里的 count 没有把两步之间断开的情况算进去比如“黑黑白黑”这种断层棋型它只会计最靠近中心的连续段。对单层评分来说足够了真要支持跳子棋型会让实现复杂两个量级收益却很低。4.3 候选点剪枝不扫全盘只看有棋子的邻域评分函数写完后如果对 225 个位置全部跑一遍评估虽然不至于卡死但明显浪费。实战中没人会把棋子下到离战场十万八千里的位置候选点只需要围绕棋盘上已有的棋子展开。资源包里的做法是只检查已有棋子周围一小圈未落子的位置棋盘越下到中盘这个剪枝节省的计算量越大。public int[] findBestMove(int aiRole) { int bestScore Integer.MIN_VALUE; int bestRow -1, bestCol -1; for (int row 0; row SIZE; row) { for (int col 0; col SIZE; col) { if (!board.isEmpty(row, col) || !nearPiece(row, col)) { continue; } int attackScore evaluatePoint(row, col, aiRole); int defendScore evaluatePoint(row, col, opposite(aiRole)); int totalScore (int)(attackScore * 1.1 defendScore * 0.9); if (totalScore bestScore) { bestScore totalScore; bestRow row; bestCol col; } } } return new int[]{bestRow, bestCol}; } private boolean nearPiece(int row, int col) { for (int dr -2; dr 2; dr) { for (int dc -2; dc 2; dc) { int nr row dr; int nc col dc; if (nr 0 nr SIZE nc 0 nc SIZE !board.isEmpty(nr, nc)) { return true; } } } return false; }nearPiece 检查以当前空位为中心、5×5 范围里是否存在任意棋子。这个范围是合理的折中比 3×3 更能捕捉到稍远的连续性棋型比 7×7 又不会把太多无关空位拉进候选集合。棋盘刚落第一颗黑子时白方候选点只有那一点周围一小圈开局响应速度非常快。4.4 权重调参进攻 1.1、防守 0.9先保证不输再谈赢AI 的落子口味由 attackScore 和 defendScore 的系数决定这是典型的“调参玄学”环节。进攻权重高于防守权重的意思是当某个点对 AI 形成活三的威胁比对对手形成的威胁更严重时AI 会选择抢先进攻而不是去堵别人。这个配比在普通对局里表现是“有攻有守”大多数玩家能明显感受到压力。但有个情况必须要防如果进攻权重太高AI 可能会无视对手已经活四的点只顾自己冲四结果对手下一步直接五连带走。我的习惯是在 findBestMove 开头加一个紧急拦截判断如果对手在任何一个方向已经形成四连AI 必须第一时间下在对应的堵截点不再走评分流程。教程资源里的基础版本没加这个逻辑你拿到后可以自行补充——它本质上是把“冲四威胁”的优先级强行拉到最高。5. 常见问题排查线程假死、坐标偏移与悔棋翻车实录任何代码写成后都会撞上各种怪问题五子棋项目里最典型的就是这一章这几个。每个坑我都标了现象、原因和解决手段照着排查能省不少时间。5.1 点击落子后界面卡死AI 在事件线程里跑评分现象点下鼠标棋子没显示窗口变成“白板”过几秒或十几秒才恢复。连点几次后系统提示“程序未响应”。根子Swing 的鼠标事件和 repaint 都在事件分发线程EDT里排队执行。AI 评分是 CPU 密集运算如果直接在 mouseClicked 回调里调用 findBestMove整个界面的重绘事件全被堵在后面窗口自然像死掉一样。解决常见做法是把 AI 计算扔到后台线程计算完成后通过 SwingUtilities.invokeLater 回到 EDT 更新界面。代码上不复杂重点是要形成习惯凡是超过几十毫秒的计算一律不直接放在事件回调里。教程资源里用了一个最朴素的 Thread 包一层算完后调 boardPanel.repaint()。5.2 棋子总是画偏PADDING 参与了换算却没人统一现象鼠标点在网格交叉点上画出来的棋子圆心偏离交叉点几个像素越往棋盘右下角偏得越厉害。根子像素转行列的公式和行列转像素的公式用了不同的 PADDING 值或者其中一个忘了加半格偏移。最常见的是 toIndex 里减了 PADDINGtoPixel 里却用的是另一个留白变量两边各差几个像素累积下来就是十几像素的偏移。解决把 PADDING 定义成同一个常量两个方向的计算都用它。toIndex 保留“加半格再整除”的取整方式toPixel 直接乘格子数加留白。改完画一条参考线验证鼠标移到棋盘四个角落的交叉点落子位置必须和交叉点完全重合。5.3 悔棋之后 AI 不落子历史栈只记了一半现象玩家悔一步棋后棋盘上棋子消失但 AI 不动了。再点一步AI 才开始响应。根子悔棋逻辑只弹出了玩家上一次落子没同时处理 AI 的上一步棋。五子棋是双方轮流落子悔棋要撤销两步才能回到“玩家行动前”的状态。历史栈如果只压入玩家落子、AI 落子直接改棋盘数组悔棋时栈里已经没有 AI 的位置信息了。解决每次落子都往栈里压入包含行、列、角色三个字段的记录AI 落子也不例外。悔棋时连续弹出两步把对应的数组位置恢复为 EMPTY。这个记录结构在后面做“重新开局”也能复用——把栈清空并重置数组比一遍遍遍历恢复残局优雅得多。5.4 双方各成四连时误判胜局方向循环没在对方棋子处停下现象对局中出现离奇胜利——玩家右上角明明只有三连系统却提示赢了。根子写 countDirection 时少了对“对方棋子”的处理直接循环取下一个位置判断遇到对方棋子没有 break而是继续往后数。棋盘数组被割裂开的两段同色棋子被算进了同一个 count三连加两连被拼成五连。解决循环里判定顺序必须是先检查越界再看当前格是否等于目标角色不等于目标角色一律 break。这里也暴露了一个测试盲区胜利判定要多写几个反例用例专门构造“己方棋子中间夹着对手棋子”的棋型验证系统不会乱判。这是所有五子棋项目里最应该补的测试。5.5 “再来一局”后棋盘上还有残留数组与界面没有同步重置现象点了“新游戏”界面还有上局留下的棋子再点落子AI 的行棋逻辑开始出现很奇怪的选点。根子重置逻辑只调用了 repaint()没清理底层二维数组。界面画出来的数据还在只是之前被新对局盖住了一旦有新棋子落下来旧棋子参与评分和判定整个局面就乱套了。解决重置顺序必须是先清数组、再清历史栈、最后调 repaint()。清数组用双层循环重新赋 EMPTY不要 new 一个新数组再替换因为界面里可能有人持有了旧数组的引用。把这套顺序写进 startNewGame 方法里按钮、菜单项、快捷键都调用同一个入口避免重置逻辑分散成多处互相覆盖。6. 落地验证与打包判定逻辑先测过再封装成双击可跑的 jar教程资源拿到手最后要做两件事验证核心逻辑没被界面代码带偏以及把工程打成可直接运行的 jar。这两个步骤做完这份源码才算真正落地到你自己手上。先写一个不依赖窗口的单元测试把胜负判定单独拉出来验证。由于 GameBoard 和 checkWin 都不碰 Swing 组件测试可以直接构造棋局状态public class GameCoreTest { Test public void testHorizontalWin() { GameBoard board new GameBoard(); for (int col 2; col 5; col) { board.place(7, col, GameBoard.BLACK); } assertTrue(GameCore.checkWin(board, 7, 5, GameBoard.BLACK)); } Test public void testNoWinWhenBroken() { GameBoard board new GameBoard(); board.place(7, 2, GameBoard.BLACK); board.place(7, 3, GameBoard.BLACK); board.place(7, 4, GameBoard.WHITE); board.place(7, 5, GameBoard.BLACK); assertFalse(GameCore.checkWin(board, 7, 5, GameBoard.BLACK)); } }只要这种测试能跑通判定代码就不会出现“夹心棋型乱判胜”这种低级漏洞。打包阶段工程不依赖第三方库的话用 JDK 自带命令就可以完成不需要引入 Maven 或 Gradlejavac -encoding UTF-8 -d out src/com/course/gobang/*.java jar cfe gobang.jar com.course.gobang.GameMain -C out . java -jar gobang.jarjavac 指定输出目录到 out把所有源码编译进去。jar 的 cfe 参数里“e”指定主类入口主类必须是包含 main 方法的那个类写错会在双击运行时直接报“找不到主类”。-C out . 表示把 out 目录下的内容作为 jar 根目录。打包完成后把 gobang.jar 和一份运行说明放在同一层目录整个项目就算交付了。最后说一个我的习惯任何五子棋相关改动做完都会先跑一遍那组判定测试再打开界面手动走几手。有一次为了调 AI 攻防系数硬是把一个四连误判成五连的 bug 放进了发布包玩家玩到一半“提前获胜”那叫一个尴尬。从那以后我每次改完都要把测试和手工验证强制走一遍确认无误再整理打包。希望你把这份资源拿到手后也能先跑测试再玩界面这样能避开我踩过的坑直接看到棋盘和 AI 都正常工作起来。本文还有配套的精品资源点击获取