
简介这份资源面向具备一定Java基础、希望以项目实战方式理解面向对象设计与博弈算法的开发者围绕五子棋这一规则简单却策略深奥的智力游戏展开。内容从棋盘类、棋子类、玩家类的职责划分入手讲解落子校验、五子连珠判定与合法移动验证并延伸到基于Minimax与Alpha-Beta剪枝的计算机玩家实现以及借助Swing或JavaFX构建事件驱动的图形界面与游戏主循环同时涉及悔棋、存档读档、难度设置与人机/人人对战等扩展功能。压缩包共65个文件约2.73MB以h头文件与cpp源文件为主体辅以obj、pdb、ilk等编译中间产物和exe可执行文件另有wav音效、bmp与ico图像资源及ini配置、doc文档便于对照源码理解工程组织。已有205人学习适合作为课程设计或Java综合练习的参考案例。1. 从一份 Java 五子棋源码说起为什么它值得你花一个下午拆完很多人第一次接触 Java 图形界面都是从五子棋开始的。原因很实在棋盘是二维数组落子是数组赋值胜负判断是四个方向的连续计数界面用 Swing 画格子加圆点就能跑起来。它不像电商系统那样要配数据库、搭框架、写一堆配置文件一个main方法就能看到窗口弹出来落子有反馈赢了会提示。这份 Java 实现的五子棋游戏教程核心价值就在这里——它把「二维数组建模 事件监听 胜负判定 界面重绘」这条完整链路压缩在一个能跑的小项目里适合刚学完 Java 语法、想找一个能动手改的练手项目的人也适合带课的老师拿去做课堂演示。下面我按自己拆这类源码的习惯从结构、落子逻辑、胜负算法一路讲到几个容易翻车的地方你照着走一遍基本就能把它改成自己的版本。2. 拆开源码看结构棋盘、落子、判胜三块怎么分工2.1 先认清这份代码的骨架拿到一份五子棋源码别急着从头读main。我一般的顺序是先看有几个类、每个类大概管什么再看棋盘数据存在哪最后才看落子和判胜。这类教学项目通常不会拆得太细常见的是三到四个类一个入口类负责启动窗口一个面板类继承JPanel管绘制和鼠标事件一个逻辑类或者干脆在面板类里放棋盘数组和判胜方法。有的版本会把「棋子」抽象成一个枚举或者常量0表示空、1表示黑、2表示白也有用-1和1的读的时候先把这个约定找出来后面所有判断都围绕它转。棋盘规模一般是 15×15这是五子棋的标准盘面。数组声明成int[][] board new int[15][15]索引board[row][col]对应第几行第几列。这里有个新手常忽略的点数组下标从 0 开始而界面上画格子往往也从 0 算起但鼠标坐标是像素值需要除以格子边长再取整才能映射回数组下标。这个换算关系是整个项目里最容易写错的一行后面避坑章节会专门说。2.2 三个核心模块的职责边界把骨架理清之后可以按职责把代码分成三块来看。第一块是状态管理也就是棋盘数组、当前轮到谁下、游戏是否结束这几个变量它们决定了程序「现在是什么局面」。第二块是输入处理鼠标点击落在哪个格子、这个格子是不是空的、能不能下属于这一块。第三块是判定与反馈落子之后要检查四个方向有没有连成五子有的话锁定棋盘并给出提示没有就切换执子方并重绘界面。这三块如果混在一个方法里代码会很难改。我见过不少教学版本把鼠标事件、落子、判胜、重绘全塞进mouseClicked里能跑但你想加一个「悔棋」或者「重新开始」就得动一大片。比较合理的做法是至少把判胜抽成一个独立方法入参是行、列、当前棋子值返回布尔值。这样判胜逻辑可以单独测试也方便你后面换成更高效的算法。下面这段是我按常见结构整理的一个最小骨架你可以对照自己手里的源码看它落在哪个类里public class GomokuPanel extends JPanel { private static final int SIZE 15; // 棋盘 15×15 private static final int EMPTY 0; private static final int BLACK 1; private static final int WHITE 2; private final int[][] board new int[SIZE][SIZE]; private int currentPlayer BLACK; // 黑先手 private boolean gameOver false; // 鼠标点击后调用把像素坐标换算成棋盘下标 private void handleClick(int pixelX, int pixelY, int cellSize, int margin) { int col (pixelX - margin) / cellSize; int row (pixelY - margin) / cellSize; if (row 0 || row SIZE || col 0 || col SIZE) return; if (board[row][col] ! EMPTY || gameOver) return; board[row][col] currentPlayer; if (checkWin(row, col, currentPlayer)) { gameOver true; } else { currentPlayer (currentPlayer BLACK) ? WHITE : BLACK; } repaint(); } }这段代码里几个参数值得说清楚。SIZE是棋盘边长改成 19 就是 19 路盘但判胜的「五连」阈值要跟着确认是不是还按 5 算。margin是棋盘左上角到面板边缘的留白画格子和坐标换算必须用同一个值否则点击位置会整体偏移。cellSize是每个格子的像素宽度通常由面板尺寸除以SIZE得到。handleClick里先做边界检查再做空位检查顺序不能反否则越界访问会直接抛异常。repaint()放在最后统一触发重绘而不是每改一个状态就画一次这是 Swing 的基本习惯。2.3 界面绘制与事件绑定的最小闭环界面这块教学项目基本都用paintComponent重写来画棋盘线和棋子。画线用Graphics.drawLine画棋子用fillOval黑子填黑、白子填白再描个边。事件绑定则是给面板addMouseListener在mouseClicked里拿到e.getX()和e.getY()传给上面的换算逻辑。这一套下来就是一个完整闭环点一下、数组变、重绘、看到棋子。需要提醒的是paintComponent里第一行要调用super.paintComponent(g)否则背景可能残留上一帧的内容出现棋子拖影。另外绘制顺序也有讲究先画棋盘线再画棋子最后画胜利提示顺序反了棋子会被线盖住。这些细节在源码里往往就一两行但改的时候漏掉就会出玄学问题。3. 落子与胜负判定四个方向扫描怎么写才不出错3.1 胜负判定的核心思路五子棋判胜的本质是以刚落下的棋子为中心分别沿横、竖、两条对角线四个方向看同色棋子连续出现了几个任意方向达到 5 个即胜。注意是「以落子点为中心向两侧延伸」而不是全盘扫描因为只有刚落下的那颗子可能形成新的五连全盘扫是浪费。这个思路决定了算法复杂度是常数级跟棋盘大小无关。实现上有两种常见写法。一种是计数法从落子点出发向某个方向的正反两侧分别数连续同色棋子两边数量相加再加 1落子本身判断是否大于等于 5。另一种是偏移量法定义四个方向向量(0,1)、(1,0)、(1,1)、(1,-1)对每个方向从-4到4的偏移逐个检查统计连续段长度。计数法代码更短偏移量法更好理解方向概念两种都行关键是边界处理要一致。3.2 计数法实现与参数说明下面是我常用的计数法实现四个方向用一个二维数组存方向向量循环里正反各数一次private boolean checkWin(int row, int col, int player) { // 四个方向横、竖、主对角线、副对角线 int[][] dirs {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; for (int[] d : dirs) { int count 1; // 落子本身算一个 // 正向延伸 count countDirection(row, col, d[0], d[1], player); // 反向延伸 count countDirection(row, col, -d[0], -d[1], player); if (count 5) return true; } return false; } private int countDirection(int row, int col, int dr, int dc, int player) { int n 0; int r row dr; int c col dc; while (r 0 r SIZE c 0 c SIZE board[r][c] player) { n; r dr; c dc; } return n; }dirs里四个向量分别对应向右、向下、右下、右上配合反向就是左下。countDirection的while条件顺序很重要先判边界再判棋子值利用短路求值避免越界。dr、dc传负值就实现了反方向延伸不用写两套循环。count初始为 1 代表落子点自己这个 1 不能漏漏了就会把「四连加一子」误判成四而不是五。这套写法对 15 路盘完全够用单次判定最多检查 4×8 个格子性能上没有任何压力。3.3 平局与非法落子的处理胜负之外还有两个状态要处理平局和非法落子。平局判断很简单落子后如果棋盘上没有空位且没人赢就是平局。常见做法是维护一个已落子计数等于SIZE * SIZE时判平比每次遍历数组找空位省事。非法落子则包括点到了已有棋子的格子、点到了棋盘外、以及游戏已经结束后继续点击这三种情况都应该直接return不改变任何状态。这里有个容易忽略的细节游戏结束后如果不锁定输入玩家还能继续落子棋盘会乱掉。所以gameOver标志位要在判胜或判平后立刻置位并且在handleClick开头就检查它。有的版本用弹窗提示胜负弹窗是模态的会阻塞事件看起来像是锁住了但关掉弹窗后如果标志位没设照样能继续下这就是个隐藏 bug。4. 避坑与排查这类五子棋源码最容易翻车的五个地方4.1 点击位置和棋子对不上现象点下去棋子出现在隔壁格子或者越往右下偏移越明显。原因像素坐标换算成数组下标时margin和cellSize跟绘制时用的值不一致或者忘了减去margin直接整除。解决把绘制和换算用的margin、cellSize抽成同一个常量或同一个方法返回值换算公式统一写成(pixel - margin) / cellSize并且整除前确认pixel margin否则负数除法会得到错误下标。4.2 边界方向扫描数组越界现象在棋盘边缘落子时抛ArrayIndexOutOfBoundsException。原因方向扫描的while里先访问了board[r][c]再判边界或者边界条件写成了 SIZE。解决把边界判断放在的最左边利用短路求值挡住越界访问边界统一用 SIZE和 0不要混用。改完在四个角和四条边上各点几颗子验证一遍。4.3 判胜漏掉反方向导致「四连不算赢」现象横着下满五颗却不提示胜利或者只在某个方向能判出来。原因只从一个方向延伸计数没有反向再数一遍导致落子点在连续段中间时统计不全。解决确认checkWin里每个方向都做了正反两次countDirection并且count初始值为 1。测试时专门在已有四颗子的中间落第五颗看能不能判出来。4.4 重绘拖影和界面卡顿现象棋子后面拖着残影或者连续点击时界面闪烁。原因paintComponent没调super.paintComponent(g)或者每次落子都new一个Graphics对象、重复加载图片资源。解决重写paintComponent第一行固定调super图片、字体等资源在构造时加载一次存成字段不要在绘制方法里反复创建。Swing 的单线程模型也要求界面更新都在事件分发线程里做别在别的线程直接改组件状态。4.5 重新开始后状态没清干净现象点「重新开始」后棋盘清了但轮到谁下、是否结束的标志还是旧值导致黑棋不能先手或者一点就提示结束。原因重置方法只清了数组漏了currentPlayer和gameOver。解决把重置逻辑写成一个方法数组清零、currentPlayer复位为黑、gameOver置 false、计数归零四件事一起做然后repaint()。以后加悔棋功能时也建议走同一套状态复位思路别东改一处西改一处。5. 从能跑到好用加一个悔棋栈和胜负高亮把基础版本跑通之后最值得加的功能是悔棋因为它逼着你把「状态」和「历史」分开管理是理解这类棋类程序架构的好练习。思路很简单每落一子把(row, col, player)压进一个Deque或者ArrayList当栈用悔棋时弹出栈顶把对应格子置回EMPTYcurrentPlayer切回被弹出那方的颜色gameOver复位再重绘。注意悔棋要能连续悔多步所以用栈结构比用单个变量记录上一步更合适。private final Dequeint[] history new ArrayDeque(); // 落子成功后压栈 history.push(new int[]{row, col, currentPlayer}); // 悔棋 private void undo() { if (history.isEmpty()) return; int[] last history.pop(); board[last[0]][last[1]] EMPTY; currentPlayer last[2]; // 回到落这子的一方 gameOver false; repaint(); }ArrayDeque做栈比Stack类更推荐后者是遗留类。压栈时机要放在判胜之前还是之后我一般放在落子赋值之后、判胜之前这样即使这一手直接赢了悔棋也能把它撤掉符合玩家直觉。胜负高亮则是判胜后记录获胜的五个坐标绘制时给这五颗子加个描边或者换个颜色让结果一目了然。实现上可以让checkWin顺便把连成的坐标收集到一个List里返回而不是只返回布尔值这样高亮和判定共用一次扫描不用重复计算。验证这套改动是否正确的办法很土但有效开一局黑下三子、白下两子悔两步看是不是回到黑该下且棋盘少了对应棋子再故意下出五连确认高亮出现后点悔棋高亮消失且能继续下。这几步走完状态管理基本就稳了。从那以后我每次改这类棋类项目都会先把「落子、悔棋、重开」三条路径各跑一遍再谈加功能因为血泪经验告诉我状态没清干净的程序功能加得越多翻车越狠。希望这份拆解帮到你拿到源码后先按第 2 章的骨架对一遍结构再照着第 4 章逐条排雷剩下的就是你自己往里加东西了。本文还有配套的精品资源点击获取