
简介面向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×15225个格子的绘制量对现代电脑来说几乎可以忽略不计根本不存在性能问题。还有一个小坑要提醒重绘时棋子画成实心圆可以直接用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 ArrayListint[][] 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把两人对战搬到局域网里告别一台电脑轮着抢鼠标的尴尬。这三个方向任何一个做完这个项目的技术深度都能再上一个台阶。最后再说一个我个人的体会做了这么多年技术、看了这么多简历真正能体现水平的往往不是你用了多牛的技术栈而是你对自己做的东西理解得到不到位。五子棋这个项目看起来很朴素但如果你能把二维数组建模、数据与界面分离、方向向量扫描、深拷贝快照这些点讲清楚比堆十个人云亦云的微服务项目更有说服力。不要急着追新框架先把这个小东西吃透你后面再学什么都会顺很多。本文还有配套的精品资源点击获取