简介:面向Java开发者的五子棋游戏实战教程资源包,覆盖棋盘设计、棋子逻辑、玩家交互、AI算法与图形界面构建等核心环节,适合希望通过完整项目入门游戏开发的编程学习者。压缩包共65个文件,包含工程源码、可执行程序、音频与位图素材、配置说明及文档等类型,整体大小仅2.73MB,结构清晰便于按需检索。目前已有204人学习,教程重点拆解了棋盘类、棋子类与玩家类的设计思路,并解释Swing/JavaFX事件驱动模式下的游戏循环流程;同时延伸到Minimax与Alpha-Beta剪枝算法,以及悔棋、存档、难度调节等扩展功能。读者可从中获得完整游戏框架的代码阅读路径、AI优化思路、界面交互细节与常见边界问题排错技巧,系统掌握五子棋从实现到测试优化的完整链路。配合包内说明文档,可对照工程逐步复现项目,尤其适合课程设计与自学练手。
1. 项目来了:为什么我建议你用Java写一个五子棋
做技术这些年,我陆陆续续带过不少新人,也帮很多准备面试的朋友看过项目。聊到练手项目,我每次都会推荐一个几乎人人都听过、但很少真正自己完整写过的程序:五子棋。
这句话放在这里,熟悉的朋友应该已经能猜到我要说什么了。五子棋听上去简单,但把Java的核心知识点几乎全部串上了,包括面向对象设计、二维数组建模、事件监听、图形界面绘制,甚至还能往深处延伸到AI博弈、评分算法、悔棋状态栈这些高级话题。单说性价比,同类练手项目里几乎没有比它更合适的了。
这篇文章不会只讲一个能跑起来的demo,我会把自己实际写这个项目时的完整思路、踩过的坑、调试技巧全部拆开讲。不管你是刚学完Java基础正在找第一个项目的初学者,还是想在简历上补一个"有技术含量的小游戏"的求职者,这套内容都可以直接照着做。
需要说明一下项目的基本定位:我们最终做出来的是一个基于Swing界面的桌面五子棋,支持双人对战,附带悔棋、重新开局、简单的AI落子评分功能。从零开始,完整代码加调试,按我的节奏大概需要一个周末的时间。
2. 动手之前:用二维数组建模棋盘,是整个项目的基石
2.1 棋盘的数据结构选型
写游戏的第一步不是画界面,而是想清楚棋盘在程序里怎么存。很多新手一上来就研究怎么画线、怎么贴图,结果写到胜负判断时发现根本无从下手,因为棋盘数据没有一个清晰的模型。
正确的顺序是先定数据结构,再做界面。
五子棋的棋盘是15×15的网格,最自然的存法就是一个二维数组。我定义成:
private int[][] board = new int[15][15];数组里每一个值代表一个交叉点的状态:0表示空位,1表示黑子,2表示白子。这套约定贯穿整个项目的所有模块,不管是落子、判断胜负还是AI算分,读写的都是同一个board。
为什么用int数组而不是别的结构?一个很直白的原因:int的默认值是0,天然对应"空位",不需要额外初始化。如果你想做带悔棋功能,二维数组也方便拷贝,深拷贝一行代码就能生成一个历史快照。另外,数组的下标就对应棋盘坐标,后续做范围判断、周围扫描都非常直观,省去了维护复杂坐标对象的成本。
2.2 界面层与数据层怎么分工
Swing界面只做两件事:把board画出来,把鼠标点击坐标换算成数组下标。真正决定游戏状态的永远是board中的数据,界面只是一个可视化外壳。
我用一个自定义的JPanel做棋盘绘制,在paintComponent方法里画网格、画棋子。注意一个细节:Swing的绘制是在事件分发线程(EDT)上执行的,所有对界面组件的修改都应在这个线程里做,否则可能出现界面错乱。实际操作中最稳妥的做法是所有交互逻辑放在监听器或SwingUtilities.invokeLater里跑。
这个"数据与界面分离"的思路,往大了说就是MVC的雏形。面试时如果被问到"你这个项目怎么设计的",能讲清楚这一层,已经比绝大多数只会复制代码的人强很多了。
3. 落子与绘制:事件监听的几个关键坑
3.1 鼠标坐标到棋盘坐标的换算
棋盘落在界面上,每个交叉点之间的间距我设置为30像素(后面统称gridSize),左上角留了30像素的边距(offset)。鼠标点一下,怎么知道点在哪一格?
int col = (e.getX() - offset + gridSize / 2) / gridSize; int row = (e.getY() - offset + gridSize / 2) / gridSize;这里有个细节很容易被忽略:为什么要加gridSize的一半再除?因为用户很少能精确点到交叉点上,落点只要落在某个格子附近,就应该归到离它最近的交叉点。加了半个格子宽度再整除,本质就是四舍五入,这样鼠标点下去的手感才不会让人觉得"明明点了却落在别处"。
换算完成后,必须做两个校验:一是坐标是否在棋盘合法范围内,二是该位置是否已经有棋子。我见过不少跑起来就崩的五子棋,基本都是漏了越界判断,点边缘就数组下标越界。
if (row < 0 || row >= 15 || col < 0 || col >= 15 || board[row][col] != 0) { return; }3.2 重绘机制:改了数据怎么让界面刷新
落子的核心动作本质上就三步:修改board数组、判断是否获胜、调用repaint()让界面重新绘制。
很多人第一次写Swing时会在paintComponent里写"画当前棋子"的逻辑,这是错的。正确的做法是paintComponent里无条件地把整个board重新遍历一遍,根据每个位置的值决定画黑子、白子还是不画。这样做的好处是逻辑单一、不会出现残影。每下一子就全量重绘一次,15×15=225个格子的绘制量对现代电脑来说几乎可以忽略不计,根本不存在性能问题。
还有一个小坑要提醒:重绘时棋子画成实心圆,可以直接用:
g.fillOval(x - pieceSize / 2, y - pieceSize / 2, pieceSize, pieceSize);注意fillOval的前两个参数是外接矩形的左上角坐标,不是圆心。想画在交叉点上就需要减去棋子半径。做黑白棋时记得画完黑子加一个细边框,不然深色背景下观感很差。
4. 胜负判断:五子棋程序的灵魂所在
4.1 为什么说判断逻辑决定整个项目的上限
可以说,任何一个五子棋程序,不管做得再花哨,最终玩家体验都落在胜负判断准不准、快不快上。判断慢半秒,或者出现"明明五颗连在一起却说没赢"的诡异局面,这个游戏基本就没法用了。
初学的时候我见过一个很"暴力"的写法:每次落子后,把整个棋盘所有位置遍历一遍,检查每个位置往四个方向能不能连成五个。这种做法不是不行,但在效率上和代码优雅度上都很差。而且它还有一个隐患:因为全盘扫,你可能在一个棋子落下后错误地判定出两个胜利方向。
正确的思路是:每次只在当前落子位置的基础上,向四个方向延伸检查。因为获胜的五连必然包含刚下的这颗棋子——如果下之前已经赢了,游戏早就结束了,所以只需要检查包含当前点的连线即可。
4.2 四方向检查模板
四个方向分别是水平、垂直、两条对角线。核心逻辑完全一样:从当前棋子出发,往一个方向数连续相同棋子的数量,再从当前点往反方向数,总数加起来大于等于5就赢了。
这里有个值得写的优化点:检查时不用递归也不用复杂算法,只要写一个按方向偏移的循环就行。我封装成下面这样:
private boolean checkWin(int row, int col, int player) { int[][] directions = {{1,0},{0,1},{1,1},{1,-1}}; 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 dx, int dy, int player) { int count = 0; int r = row + dx; int c = col + dy; while (r >= 0 && r < 15 && c >= 0 && c < 15 && board[r][c] == player) { count++; r += dx; c += dy; } return count; }这里directions数组里的四个向量,每个向量加上它的相反方向,就覆盖了游戏所需的全部八个相邻方向。认真看这个设计,你会发现它把问题抽象得非常干净:不管哪个方向,都是同一个逻辑。
边界条件我已经用while循环条件处理了,不需要额外写一堆if判断到底是棋盘上边缘还是下边缘。
4.3 为什么建议判断"当前点附近"而不是"全盘扫描"
除了性能上的考虑,全盘扫描最大的问题是容易误判。举个例子,棋盘上同时存在一条黑子四连和一条白子四连,如果你在全盘扫描后直接判断"有任意四连就赢",就会出现黑白同时获胜的荒谬结果。
所以判定时机也很重要:每落一子,只检查当前落子颜色是否获胜,另一方不做检查。这个时序逻辑听起来很简单,但很多人写着写着就忘了。
5. 进阶玩法:悔棋、重开与简单AI评分
5.1 悔棋功能:用ArrayList当历史栈
悔棋功能是我推荐一定要做的,因为它能培养"栈"这种数据结构在实际项目中的应用意识。
最简单的实现方式:每次落子时,把当前的board快照保存到一个ArrayList里,悔棋时弹出一个历史快照,恢复现场,再重绘。
private ArrayList<int[][]> history = new ArrayList<>(); private void recordHistory() { int[][] snapshot = new int[15][15]; for (int i = 0; i < 15; i++) { snapshot[i] = board[i].clone(); } history.add(snapshot); } private void undo() { if (history.isEmpty()) return; board = history.remove(history.size() - 1); repaint(); }这里有个让人肉疼的细节:不能直接history.add(board),因为数组存的是引用,后面改board的时候历史记录也会被改,悔棋就完全失效。必须clone每个一维数组,做一次真正的深拷贝。这个坑我当年踩过,第一次写悔棋翻来覆去调试了一晚上,最终恍然大悟是引用传递的问题。
类似的引用陷阱在Java里非常普遍,面试聊起这个项目时,如果你能主动讲清楚"为什么悔棋快照必须深拷贝",会是很大的加分项。
5.2 重新开始与轮次切换
重开功能没什么难点,把board重置为全0、清空历史、当前玩家设为黑棋、重绘即可。真正的坑在于判断游戏状态:因为加入了悔棋和重开,你必须有一个状态变量标明当前是否在对局中,否则就会出现"游戏已经结束还继续落子"的意外。
我常用的做法是加一个boolean gameOver标记,获胜或和棋时置为true,重开时重置为false,落子的监听器第一行就判断这个标记。
5.3 给AI一点面子:基于评分的贪心落子
很多人听到AI就头大,但其实五子棋的初级AI完全可以用很朴素的启发式评分实现,代码量不大,效果却足以"看起来会下棋"。
核心思路是:遍历所有空位,给每个位置打分,然后选最高分的落子。打分规则可以很直白:
- 某个位置周围有多少颗己方棋子,加分;
- 有多少颗对方棋子,也要加分,因为要堵;
- 能形成四连、三连的位置额外加权重。
我用的这套简单评分,虽然谈不上聪明,但对一个练手项目来说足够撑起"人机对战"的面子。更关键的是,写评分的过程能逼着你把"棋形"这个概念抽象成数据,对之后想做更复杂的博弈算法是很好的铺垫。
6. 常见问题排查:那些年我踩过的坑
6.1 问题速查表
| 现象 | 根源 | 对应解法 |
|---|---|---|
| 鼠标点击没有反应 | 坐标换算越界或落子合法性判断漏了 | 检查行列是否在0~14之间、当前位置是否为0 |
| 棋子重叠在同一位置 | 没有判断当前位置是否为空 | 落子前增加board[row][col] != 0判断 |
| 胜负判断延迟或漏判 | 方向向量没写全,只查了部分方向 | 检查四个方向是否都覆盖,注意反对角线方向 |
| 界面不刷新 | 忘记调用repaint(),或数据与绘制逻辑混杂 | 数据改动后统一调用repaint(),绘制只读取board |
| 悔棋后棋子消失 | 历史记录存了引用,没做深拷贝 | 用clone()逐行拷贝一维数组 |
| 游戏结束后还能落子 | 缺少gameOver状态标记 | 添加状态判断,对局结束后直接return |
| 窗口关闭但进程不退 | 没有设置默认关闭操作 | setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) |
6.2 调试经验:把棋盘状态打印出来
界面类程序出了问题,最忌讳的就是"肉眼盯着界面看"。我调试五子棋时最常用的手段是在控制台把board按二维数组格式打出来,用符号0/1/2区分空位、黑子、白子。这样能一眼看出数据结构到底对不对,完全不受绘制代码干扰。
这个方法听起来土,但真的能大幅缩小问题范围:如果控制台打印的board是正确的、逻辑也是正确的,那问题多半出在绘制;如果控制台打印本身就是乱的,那就老老实实回到数据结构层面查。
6.3 一个经常被低估的编译问题
开发环境偶尔会出现类找不到或者旧版本缓存导致的问题。项目运行时如果出现奇怪的类加载异常,先不要急着怀疑代码逻辑,试一下mvn clean或者IDE里的"Clear Project Cache"再重新编译,很多诡异问题都只是构建缓存惹的祸。Java项目里这个坑出现频率远比你想的高。
7. 从交作业到拿得出手:扩展方向与我的个人体会
代码跑通、能双人对战、能悔棋、能简单人机对战之后,这个项目已经可以从简历的"项目经历"栏里亮相了。但如果你想让它在一堆项目里显得更有含金量,还有几个方向可以继续加码。
一个是公平性相关的禁手规则,三三禁、四四禁、长连禁,这个做到位就能说明你对五子棋规则有深入理解。另一个是更聪明的AI,把贪心评分升级成极小化极大搜索加Alpha-Beta剪枝,这个就真正触及博弈论与搜索算法的核心了。再有一个是网络对战,用Socket或Netty把两人对战搬到局域网里,告别一台电脑轮着抢鼠标的尴尬。这三个方向任何一个做完,这个项目的技术深度都能再上一个台阶。
最后再说一个我个人的体会:做了这么多年技术、看了这么多简历,真正能体现水平的往往不是你用了多牛的技术栈,而是你对自己做的东西理解得到不到位。五子棋这个项目看起来很朴素,但如果你能把二维数组建模、数据与界面分离、方向向量扫描、深拷贝快照这些点讲清楚,比堆十个人云亦云的微服务项目更有说服力。不要急着追新框架,先把这个小东西吃透,你后面再学什么都会顺很多。
本文还有配套的精品资源,点击获取