简介:这份资源面向具备一定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。
有人会问能不能用Map<Point, 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 或 21,Swing 依然在标准库里,不需要额外配置模块。目录结构一般是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。注意回放期间要禁用鼠标落子,否则用户点一下就和回放打架。这个功能对课程设计答辩很加分,实现成本却很低,本质就是把「历史栈」正向遍历一遍。
我自己的习惯是,每次改完判定或坐标相关的代码,都强制走一遍「四方向五连 + 四角点击 + 悔棋三步」的固定回归,不图快。这套动作花不了两分钟,但能挡住绝大多数低级错误。希望帮到你。
本文还有配套的精品资源,点击获取