
简介这份资源面向具备一定Java基础、希望以项目实战方式巩固面向对象设计与算法思维的开发者围绕五子棋这一规则简单却策略深奥的智力游戏展开。内容从棋盘类、棋子类、玩家类的职责划分入手讲解落子校验、五子连珠判定与合法移动验证并延伸到基于Minimax与Alpha-Beta剪枝的电脑玩家实现以及借助Swing或JavaFX构建事件驱动的图形界面与游戏主循环同时涉及悔棋、存档读档、难度设置与人机/人人对战等扩展功能。压缩包共65个文件约2.73MB以h头文件、cpp源文件与obj编译产物为主体辅以wav音效、bmp位图、ico图标及ini配置等素材另含工程与说明文档便于对照源码理解模块组织。已有205人学习下载适合作为课程设计或入门AI博弈的练手项目参考。1. 从一份 Java 五子棋源码说起它到底能帮你解决什么很多人第一次搜「java 五子棋游戏」心里想的其实不是「我要学 Java」而是「我要一个能跑起来、能看懂、能改得动的小项目」。这份 Java 实现的五子棋游戏教程正好卡在这个位置上它不是那种把 Swing 界面、事件监听、胜负判断全塞进一个类里的教学玩具而是把棋盘数据结构、落子合法性校验、五连判定、悔棋与重开这些模块拆开来讲的完整源码包。你拿到手之后最直接的用途有三个一是当作 Java 图形界面编程的练手项目二是当作课程设计或期末大作业的底稿三是把它当成一个「可改」的骨架往里面加 AI 对战、加网络联机、加棋谱回放。适合的人群也很明确——学过 Java 基础语法、知道类和对象是怎么回事、但还没独立写过完整桌面应用的人。如果你已经能熟练写 Swing 或 JavaFX这份东西对你来说偏简单但它的判定逻辑和状态管理仍然值得扫一眼。2. 拆开源码看结构棋盘、落子与胜负判定怎么落地2.1 棋盘的数据结构选型二维数组还是稀疏存储五子棋棋盘最常见的表示就是二维数组15×15 或 19×19元素用 0、1、2 三个值分别表示空位、黑子、白子。这个选型看起来朴素但它是这份源码里最不该被改掉的部分。原因在于五连判定需要频繁做「沿某个方向连续取五个格子」的操作二维数组的随机访问是 O(1)下标直接算没有哈希开销也没有链表遍历。常见做法是定义一个常量BOARD_SIZE 15然后用int[][] board new int[BOARD_SIZE][BOARD_SIZE]初始化落子时写board[row][col] currentPlayer。有人会问能不能用MapPoint, Integer来存理论上可以落子稀疏时省内存但五子棋的判定逻辑会变得很难写——你要判断某个方向上的连续棋子就得反复查 Map代码可读性直线下降而且 15×15 一共才 225 个格子内存根本不是瓶颈。所以这份源码坚持用二维数组是对的我一般也会这么选。真正需要注意的是数组下标和界面坐标的换算Swing 里鼠标事件的坐标是像素棋盘数组的下标是格子序号中间要除以格子边长再取整这一步写错就会出现「点这里下那里」的玄学现象。public class Board { public static final int SIZE 15; public static final int EMPTY 0; public static final int BLACK 1; public static final int WHITE 2; private final int[][] grid new int[SIZE][SIZE]; public boolean place(int row, int col, int player) { // 边界检查必须放在最前面否则数组越界直接抛异常 if (row 0 || row SIZE || col 0 || col SIZE) { return false; } if (grid[row][col] ! EMPTY) { return false; // 已有棋子落子非法 } grid[row][col] player; return true; } public int get(int row, int col) { return grid[row][col]; } }上面这段是棋盘类的核心place方法把「边界校验」和「占位校验」合并成一个布尔返回值调用方拿到 false 就知道这步不能下不需要抛异常。参数player传 BLACK 或 WHITE不要传布尔值因为后面加 AI 或者加观战模式时三态比两态好扩展。get方法留给判定逻辑用注意它不做边界检查调用前必须保证下标合法这是这份源码里一个隐含约定改代码时要留意。2.2 五连判定的四个方向与提前终止胜负判定是整个项目里最容易写错的地方。新手常犯的错是「只往一个方向数」比如只数右边和下边结果左边或上边的连子被漏掉。正确做法是以刚落下的那颗棋子为中心向四个轴向各数一次水平、垂直、主对角线、副对角线。每个轴向要往正反两个方向都数加起来等于或超过五就算赢。这份源码里的判定函数大致长这样对每个方向定义一对增量(dr, dc)比如水平是(0,1)垂直是(1,0)主对角线是(1,1)副对角线是(1,-1)。然后从落子点出发先往正方向累加同色棋子再往反方向累加最后把两个方向的计数相加再减一因为中心棋子被数了两次。这里有个细节累加过程中一旦遇到不同颜色或越界就立刻停不要继续数否则会数穿棋盘。private static final int[][] DIRS {{0,1},{1,0},{1,1},{1,-1}}; public boolean isWin(int row, int col, int player) { 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 grid[r][c] player) { n; r dr; c dc; } return n; }DIRS数组把四个方向抽象成增量对countDirection负责单方向计数isWin负责合并。参数dr、dc的正负决定了数的是哪一侧所以调用时传-d[0]、-d[1]就是反方向。这个写法的好处是四个方向共用一套逻辑加「禁手判定」或者改棋盘大小时不用动判定主体。注意count 5而不是 5因为长连六子及以上在无禁手规则下也算赢如果你的规则要求恰好五子这里要改成 5并额外处理长连。2.3 界面刷新与事件分发Swing 的重绘时机界面部分这份源码用的是 Swing棋盘画在JPanel的paintComponent里鼠标点击走MouseListener。这里有一个新手很容易翻车的点落子之后只改了数组忘了调repaint()结果界面不动以为逻辑错了其实是没触发重绘。正确顺序是「改数据 → 调 repaint → 在 paintComponent 里根据数组重画整个棋盘」。不要试图只画那一颗新棋子因为悔棋、重开、窗口遮挡恢复都会让局部绘制出问题全量重绘在 15×15 的规模下开销可以忽略。事件分发这边mouseClicked里拿到e.getX()、e.getY()之后先减去棋盘绘制区域的偏移量再除以格子边长得到行列下标。偏移量和格子边长建议定义成常量绘制和点击共用同一套否则改窗口大小时两边对不上。还有一个细节点击落在格子边缘时取整方向要统一一般用整数除法向下取整就行但如果你画棋子时是以格子中心为圆心点击判定最好也以中心为准偏差控制在半个格子内手感才正常。3. 把项目跑起来环境、编译与第一次落子3.1 JDK 版本与目录约定这份源码没有用任何第三方依赖纯 JDK 自带库所以环境要求很低。JDK 8 及以上都能编译如果你本机装的是 JDK 17 或 21Swing 依然在标准库里不需要额外配置模块。目录结构一般是src/下按包名分层比如game/放棋盘和判定ui/放窗口和面板Main.java作为入口。拿到源码后先别急着改按原结构建一个普通 Java 项目把src设为源码根目录确认能编译通过再动手。如果你用命令行编译和运行的命令如下。注意-d指定输出目录-cp指定类路径包名要和目录结构对应否则会报「找不到主类」。# 在项目根目录执行假设源码在 src 下包名为 com.example.gomoku javac -encoding UTF-8 -d out $(find src -name *.java) java -cp out com.example.gomoku.Main-encoding UTF-8不能省源码里有中文注释或中文字符串时Windows 默认编码可能不是 UTF-8编译出来会乱码。find src -name *.java是把所有源文件一次性交给编译器文件多了也不会漏。运行时的-cp out指向编译输出目录主类名要带完整包名。如果你用 IDE这些它都会帮你做但建议至少手动跑一次命令行能暴露「包名和目录不一致」这类 IDE 帮你掩盖的问题。3.2 第一次落子要验证的三件事程序起来之后别急着下满整盘先做三个最小验证。第一点棋盘空白处棋子是否出现在点击位置这是坐标换算对不对的直接体现。第二点已有棋子的位置是否被拒绝且没有出现重叠绘制这是占位校验在起作用。第三黑白是否交替这是当前玩家状态有没有正确切换。这三件事任何一件不对都说明基础链路有问题先修好再往下走。常见现象是「棋子画出来了但位置偏了半个格子」原因通常是绘制时用了格子左上角坐标而点击判定用了格子中心两边基准不一致。解决办法是把绘制和判定统一到「格子中心」绘制时圆心取col * cellSize cellSize / 2判定时用(x - offsetX) / cellSize取整。改完这两处手感立刻正常。另一个现象是「下了一颗之后再也下不了」多半是当前玩家变量没切换或者切换了但判定函数里用的还是旧值顺着currentPlayer这个变量从头到尾捋一遍就能找到。3.3 胜负提示与重开流程赢棋之后这份源码会给一个弹窗提示然后提供「重开」按钮。重开的实现要注意不能只把界面清空棋盘数组也必须重置为全 EMPTY当前玩家重置为黑方否则下一局第一手可能下不进去或者判定错乱。常见做法是写一个reset()方法把数组逐行填充 EMPTY再把状态变量归位界面那边调repaint()。弹窗建议用JOptionPane.showMessageDialog简单直接不要自己画一个模态窗口容易出焦点问题。还有一个容易被忽略的点赢棋之后如果还能继续落子就会出现「已经赢了还在下」的尴尬。所以判定胜利后要设一个gameOver标志place方法开头检查这个标志为 true 直接返回 false。这个标志在reset()里清掉。别小看这一行判断少了它用户手快多点几下棋盘状态就乱了后面加悔棋功能时会更麻烦。4. 避坑与排查五子棋项目里最容易翻车的五件事4.1 现象连成五子却不判赢或者斜线漏判原因通常有两个。一是判定方向只写了水平和垂直忘了两条对角线或者对角线增量写成了(1,1)和(1,1)重复。二是计数时正反方向只数了一边比如只往右下数左上方向的棋子没算进去。解决方法是把四个方向的增量对写死成常量数组正反两个方向都调用同一个计数函数并且用「中心算一次、两边各数一次再相加减一」的方式合并。改完之后拿几个固定棋型手动摆一遍横五、竖五、主对角五、副对角五四个都要过。4.2 现象点击位置和落子位置对不上越往右下偏得越多这是坐标换算的经典翻车。原因一般是绘制时用了col * cellSize作为左上角而点击判定时又减了一个偏移量两边基准不同或者格子边长在绘制和判定里用了两个不同的变量改窗口大小时只改了一处。解决办法是把「棋盘左上角偏移」和「格子边长」定义成类级常量或共享字段绘制和点击都引用同一份并且明确约定「格子中心」为基准。改完在棋盘四个角和中心各点一次位置都对才算过。4.3 现象悔棋之后界面没变或者再落子时判定错乱原因多半是悔棋只改了数组没调repaint()或者只改了界面没改数组。更隐蔽的一种是悔棋把「当前玩家」也回退了但回退逻辑写反导致下一手颜色不对。解决方法是把悔棋做成一个原子操作从历史栈里弹出最后一步把对应格子置 EMPTY切换当前玩家然后repaint()。历史栈里存的是(row, col, player)三元组不要只存坐标否则回退玩家时没依据。另外gameOver为 true 时是否允许悔棋要明确一般赢棋后不允许悔棋避免状态混乱。4.4 现象编译报「找不到符号」或运行时「找不到主类」前者通常是包名和目录结构不一致比如源码里写的是package game;但文件放在src/com/example/game/下。后者是运行时-cp指错了目录或者主类名没带包名。排查顺序是先看每个源文件第一行的 package 声明再对照它在磁盘上的相对路径两者必须严格对应然后确认javac -d的输出目录和java -cp指向的是同一个。命令行下可以用javac -verbose看它实际在找哪些文件比瞎猜快。4.5 现象中文注释或提示乱码原因是编译时没指定编码或者运行环境的默认字符集和源码编码不一致。源码如果是 UTF-8 保存的编译时加-encoding UTF-8如果是在 Windows 控制台运行且出现乱码还要确认控制台代码页。更稳妥的做法是源码里尽量少用中文做界面文案或者把文案抽到一个资源文件里统一管理。这一条看起来是小问题但课程设计交上去乱码观感很差血泪经验是编译命令里永远带上编码参数。5. 从能跑到能改加一个简单 AI 与棋谱回放的思路项目跑通之后大多数人会想往里加东西最常见的就是「人机对战」。这里给一个不依赖任何算法库的最小思路评估函数 一层搜索。对棋盘上每个空位假设当前玩家下在这里算一个分数分数由「这个点四个方向上能形成多少连子」加权求和活三、冲四给高权重死四给低权重然后选分数最高的点落子。这个做法很粗糙但代码量小几十行就能跑适合作为第一版 AI 的骨架。// 简化版评分只统计四个方向的连子长度实际项目里要区分活/死 private int scorePoint(int row, int col, int player) { int total 0; for (int[] d : DIRS) { int len 1 countDirection(row, col, d[0], d[1], player) countDirection(row, col, -d[0], -d[1], player); total weight(len); // weight 里对 2、3、4、5 连给不同分值 } return total; }scorePoint复用了前面判定逻辑里的countDirection这就是把判定写成独立函数的好处——AI 直接拿来用不用重写。weight函数是调参的地方连子越长分越高但要注意「活三」和「眠三」的区别简化版可以先不区分跑起来之后再补。搜索层数先做一层也就是「我下哪里最好」不做「对手会怎么应」等一层跑顺了再加极小化极大否则一上来就写递归搜索调试成本很高。棋谱回放是另一个性价比很高的扩展。思路是在每次落子时把(row, col, player)追加到一个List里回放时用一个定时器每隔几百毫秒从列表里取一步调place再repaint。注意回放期间要禁用鼠标落子否则用户点一下就和回放打架。这个功能对课程设计答辩很加分实现成本却很低本质就是把「历史栈」正向遍历一遍。我自己的习惯是每次改完判定或坐标相关的代码都强制走一遍「四方向五连 四角点击 悔棋三步」的固定回归不图快。这套动作花不了两分钟但能挡住绝大多数低级错误。希望帮到你。本文还有配套的精品资源点击获取