☰
Java五子棋源码实战:面向对象、Swing界面与简单AI设计
2026/10/24 14:24:33 网站建设 项目流程

简介:本资源是一份基于Java语言实现的轻量级五子棋游戏源码包,面向Java初学者与GUI编程入门者,旨在通过完整可运行的小型项目,帮助学习者掌握Swing界面开发、事件驱动编程、二维数组建模及胜负逻辑算法等核心实践技能。压缩包共27个文件,包含24个Java源文件(涵盖Main启动类、Board棋盘管理、GameController游戏逻辑控制、Piece棋子封装等核心模块)、1个Maven配置文件pom.xml用于依赖与构建管理、1个readme.txt提供运行说明,以及1个.gitignore规范版本控制行为;整体仅40KB,结构清晰、无冗余资源。目前已有269人学习下载。读者可直接导入IDE运行体验双人本地对弈,深入理解MVC思想在小型游戏中的落地,复用Stone、Player、PieceColor等设计良好的类结构,并参考其中简洁的胜负判定算法(如五连检测)与图形绘制逻辑,是巩固面向对象编程与GUI实战能力的优质教学范例。 做这个项目前,我先把需求边界划清楚了

最近学习群里又有人在找五子棋源码,说是课程设计要交作业。我翻了一下手头的东西,正好有一份很多年前用Java写的五子棋,当时为了练手,前后重构了三个版本:控制台版、Swing界面版,后来又加了简单的AI对战。这次干脆把这份源码的完整设计思路、核心逻辑和踩坑过程都整理出来,给正在学Java的朋友做个参考。

先说结论:这份源码虽然叫“简单五子棋”,但代码里该有的结构一点都不含糊。它不需要任何第三方依赖,纯JDK环境就能编译运行,非常适合Java初学者拿来理解面向对象设计、二维数组操作、事件监听和基础算法。如果你是刚学完Java语法,想找一个能写到简历上的小项目,或者正在做Java课程设计,这篇内容可以直接帮你省掉大量的摸索时间。

整个项目从完成度上分成三个层次:第一层是控制台版本,能让两个人在命令行里轮流下棋,胜负判断完整;第二层是Swing版本,支持鼠标点击落子,有清晰的棋盘绘制;第三层是简单的电脑AI,谈不上多聪明,但足够让新手看懂“电脑是怎么思考该下哪一步的”。

我先把这份源码的项目结构和数据模型讲清楚,然后拆解落子逻辑、输赢判断、AI决策这几个核心模块,最后把我在测试过程中撞过的坑和性能优化心得一并放出来。每段代码我都会解释为什么要这样写,而不是只丢一个能跑的结果了事。

1. 五子棋项目的类结构设计与职责划分

写任何Java项目,第一步不是急着敲代码,而是把类结构定好。这份源码的包名是gobang,一共拆了五个类,每个类的职责非常单一,这也是我后来能轻松给项目加AI功能的基础。

类的划分如下表所示:

类名职责关键属性和方法
GobangGame程序入口,负责初始化游戏循环main()方法,控制台与Swing两种启动模式
ChessBoard棋盘数据模型,保存所有落子状态int[][] board,initBoard(),isValidMove()
Player管理玩家信息和落子动作name,color,move()
Rule静态规则类,判断输赢和合法性checkWin(),checkFiveInLine()
AI简单电脑玩家,计算最佳落子点getBestMove(),evaluatePoint()

这个设计里最核心的决策是:把棋盘状态和规则判断分成了两个独立的类(ChessBoard和Rule)。很多初学者喜欢把规则判断的方法直接写进棋盘类里,短时间看确实方便,但如果后面要做悔棋、复盘、AI评估,棋盘类和规则逻辑缠在一起会非常痛苦。分开之后,棋盘类只负责“我这个位置现在是空还是有子”,规则类只负责“当前这步下在这个位置后,是否达成了胜利条件”,各管各的,测试也好写。

ChessBoard类内部用了一个int[][]二维数组保存棋盘状态,15×15的棋盘就是int[15][15],0表示空位,1表示黑子,2表示白子。这里用int而不是boolean或枚举的原因很简单:这个棋盘状态不仅要表示“有没有子”,还要表示“是黑是白”,int直接做角色区分最小成本。

入口类GobangGame里做了一件比较有意思的事情:启动时读取命令行参数,如果带-console参数就走控制台交互,否则启动Swing界面。这个设计让同一份核心逻辑可以在两个界面形态下复用,也方便我在命令行下写自动化测试。启动入口的代码如下:

public class GobangGame { public static void main(String[] args) { boolean consoleMode = false; for (String arg : args) { if ("-console".equals(arg)) { consoleMode = true; break; } } if (consoleMode) { new ConsoleRunner(new ChessBoard()).start(); } else { SwingUtilities.invokeLater(() -> new GameFrame(new ChessBoard()).setVisible(true)); } } }

这段代码里SwingUtilities.invokeLater()不是可有可无的装饰,Swing组件的创建和更新必须在Event Dispatch Thread也就是EDT线程上执行,否则在高分屏或快速点击时可能出现界面假死。控制台模式走的是普通线程,两者互不干扰。

2. 棋盘初始化与落子逻辑:二维数组的正确打开方式

五子棋的整个游戏状态就是一张棋盘,所以数据模型的设计直接决定了后面所有代码的复杂度。我用的是最简单的int[][],但在这个“简单”的背后有几个关键细节值得展开说。

2.1 棋盘尺寸与坐标体系的约定

标准五子棋棋盘是15×15,这份源码里在ChessBoard类开头定义了两个常量:

public static final int BOARD_SIZE = 15; public static final int EMPTY = 0; public static final int BLACK = 1; public static final int WHITE = 2;

坐标体系的约定是board[row][col],row表示行号(从上往下0到14),col表示列号(从左往右0到14)。这个约定虽然在程序内部自洽,但在后面做Swing界面时,鼠标坐标换算成棋盘坐标需要在X轴和Y轴的处理上高度小心,我在这里踩过很深的坑,后面专门开一节来讲。

初始化棋盘的方法特别简单,就是遍历二维数组全部置0:

public void initBoard() { for (int i = 0; i < BOARD_SIZE; i++) { for (int j = 0; j < BOARD_SIZE; j++) { board[i][j] = EMPTY; } } }

2.2 落子前必须做的合法性校验

落子的合法性校验是五子棋里最琐碎也最不能出错的逻辑。玩家点击一个位置,必须先判断是不是越界了,再判断这个位置是不是已经被占用了,跳过一个都会出Bug。

我的isValidMove()实现如下:

public boolean isValidMove(int row, int col) { if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { return false; } return board[row][col] == EMPTY; }

为什么要把越界检查放在数组访问之前?因为Java数组越界会抛出ArrayIndexOutOfBoundsException,如果这个异常没被捕获,整个程序就崩了。先做范围判断,再访问数组元素,是这段代码能稳跑的前提。虽然题目叫“简单五子棋”,但这种边界防护一定是刻在骨子里的习惯。

落子操作本身也很直接:

public boolean placePiece(int row, int col, int color) { if (!isValidMove(row, col)) { return false; } board[row][col] = color; moveCount++; return true; }

这里记录moveCount落子计数不是多余的,后面AI判断“是不是该我下”“棋盘满没满”都能用上,也可以用来实现简单的超时判断。我见过很多初学版本把落子记录省略了,后面做悔棋功能时又得重新加,不如一开始就留好扩展位。

2.3 棋盘满局的判断不可忽略

五子棋看起来棋盘很大,15×15有225个位置,看似永远不会下满。但实际测试中如果两个玩家水平都一般,下到一百多手还没分出胜负是常态,一旦没有空位,程序必须给出“平局”而不是卡死。

判断平局的逻辑我写在isBoardFull()里:

public boolean isBoardFull() { return moveCount >= BOARD_SIZE * BOARD_SIZE; }

因为每次落子都会记录moveCount,满局判断就是一个简单的数值比较,时间复杂度O(1),不用遍历整个棋盘。这个细节虽然小,但在实际游戏循环中每下一步都要调用一次,效率上的差别在性能敏感的AI评估场景里会被放大。

3. 输赢判断的核心算法:从坐标映射到边界防护

很多人写五子棋最头疼的就是输赢判断。初学者习惯在每次落子后遍历整个棋盘,看有没有任何一个位置的横、竖、两个斜方向能连成五子。这种思路确实简单,但问题是每次落子后都要检查225个位置乘以4个方向,效率低且代码冗长。

这份源码里的思路是:只需要检查最后一步落子位置的四个方向即可。因为胜利的五子连线必然包含刚才落下的那一颗棋子,否则这一手棋就不产生胜负。这个方法把每次判断的计算量从棋盘面积降到了固定常数,不仅代码清晰,算法效率也高出一个数量级。

3.1 四方向检查的框架

我用了一个方向偏移量数组来统一处理水平、垂直和两个对角线的检查:

private static final int[][] DIRECTIONS = { {0, 1}, // 水平向右 {1, 0}, // 垂直向下 {1, 1}, // 主对角线方向 {1, -1} // 副对角线方向 };

checkWin(int row, int col, int color)的实现逻辑是:对每个方向,从当前落子点出发,沿正方向数连续同色棋子数,再沿反方向数连续同色棋子数,两者相加再减1(因为当前棋子被数了两次),如果大于等于5,就说明连成五子。

public boolean checkWin(int row, int col, int color) { for (int[] dir : DIRECTIONS) { int count = 1; count += countDirection(row, col, dir[0], dir[1], color); count += countDirection(row, col, -dir[0], -dir[1], color); if (count >= 5) { return true; } } return false; } private int countDirection(int row, int col, int dRow, int dCol, int color) { int count = 0; int r = row + dRow; int c = col + dCol; while (r >= 0 && r < ChessBoard.BOARD_SIZE && c >= 0 && c < ChessBoard.BOARD_SIZE && board[r][c] == color) { count++; r += dRow; c += dCol; } return count; }

这里的row + dRow和col + dCol就是沿某个方向逐步移动。while循环里必须先判断边界再访问数组,这一点和落子合法性校验的逻辑一致,都是防数组越界的标准姿势。

3.2 为什么不能用“扫描全局”来判断输赢

把一个需求实现出来容易,但实现得是否优雅决定了代码的后续可维护性。全盘扫描法在功能上完全正确,但存在三个问题:

第一,时间复杂度O(N²),N是棋盘边长,每手棋都要做225次×4方向的检查,虽然绝对耗时不大,但如果是AI在对弈中批量评估成百上千个候选点时,这个开销会被放大得很明显。第二,全盘扫描的输出不包含“是哪个位置连成了五子”的信息,做界面高亮胜利棋子的功能时又要再写一套代码。第三,从工程思维上看,“每次落子只影响局部区域的胜负状态”这个直觉才是正确模型,全局扫描是违背直觉的多余计算。

所以我在这份源码里采用的是增量检查:每次落子后只围绕落子点展开四个方向的连续性统计。这不仅让单次判断变得极快,还顺带得到了“哪几个位置的棋子构成了五连”的信息,为后续界面高亮和复盘功能铺好了路。

3.3 坐标体系不一致引发的Bug:一次真实排错记录

这里我必须专门讲讲坐标问题,因为这是我重构时踩过最深的一个坑,而且它非常隐蔽。

控制台模式下,玩家输入的是行 列,比如7 7,这在二维数组里对应的是board[6][6],因为数组索引从0开始,而人习惯从1开始数。我在控制台版本里做了row - 1的换算,一切正常。

但到了Swing版本,鼠标点击返回的是像素坐标,需要先换算成棋盘坐标。我的第一个版本写成了:

int row = (int) ((y - margin) / cellSize); int col = (int) ((x - margin) / cellSize);

看起来没问题?问题出在margin偏移量的定义上。我在绘制棋盘时,棋盘的左边界和上边界都留了30像素的边距,但绘制棋格线时用的cellSize是40像素。当鼠标点击在棋盘最左上角的交叉点上时,像素坐标大约是(30, 30),换算结果是(0, 0),正确。但当鼠标点击在棋盘最右下角交叉点上时,坐标大约是(30+14×40, 30+14×40) = (590, 590),换算结果是((590-30)/40, (590-30)/40) = (14, 14),也正确。

坑不在边界,而在棋盘大小不是整数倍的关系。15条线之间有14个间隔,每个间隔40像素,棋盘总宽14×40=560像素。如果我把cellSize理解成“格子边长”,那么第15条线的像素坐标应该是margin + 14 * cellSize;如果理解成“交叉点间距”,那15个交叉点之间的像素跨度是14 * cellSize。这两种理解差一个cellSize的距离,会导致最右边一列和最下边一行棋子永远画不在交叉点上。我一开始混合使用了这两种定义,结果棋盘上的棋子越下越歪,九段高手也救不回来。

正确的统一约定是:cellSize表示相邻交叉点的间距,交叉点的像素坐标是margin + i * cellSize(i从0到14),鼠标坐标换算成棋盘索引用int((x - margin) / cellSize + 0.5)四舍五入到最近的交叉点。我用一个统一的pointToIndex()方法封装了这个换算逻辑,后面再加任何坐标系转换都不慌了。

4. 简单AI的实现:从“就近堵截”到“打分评估”

五子棋的AI是这个项目里最有意思的部分,也是Java学习者最容易卡住的地方。网上能搜到很多“五子棋地狱难度ai”的视频,那些大多是实现了α-β剪枝搜索树的方案。但作为课程设计或练手项目,我建议先实现一个“会堵棋”的简单AI,能挡住一般的新手,等理解了评估思想再上搜索树不迟。

这份源码里的AI分为两个版本,第一个版本是“就近堵截型”,逻辑极其简单但效果意外地好。

4.1 版本一:敌人下一步要赢,我就堵住它

核心思路非常直观:遍历棋盘每一个空位,模拟把白子放在这里,如果这个位置能让白棋直接达成五连,那么这个位置就是“威胁点”,黑棋AI必须优先堵住。

public int[] getDefensiveMove() { for (int row = 0; row < BOARD_SIZE; row++) { for (int col = 0; col < BOARD_SIZE; col++) { if (board.isEmpty(row, col)) { board.placePiece(row, col, WHITE); boolean isThreat = rule.checkWin(row, col, WHITE); board.placePiece(row, col, EMPTY); // 模拟后恢复 if (isThreat) { return new int[]{row, col}; } } } } return null; }

注意这里的恢复技巧:board.placePiece(row, col, EMPTY)。很多人喜欢先备份原来的值再恢复,但在这个场景下空位本来就是为了模拟才填入的,恢复成空位就行。关键是下完必须恢复,否则AI每评估一个位置,棋盘就被污染一次,几步之后AI就“瞎”了。

这个版本能挡住所有“就差一步就赢”的威胁。不过它的局限也很明显:如果对手还没形成“差一步”的局面,AI就不知道该往哪下了,只会走一个默认位置。实际测试下来,这个水平大概能赢不会布局的新手,但遇到稍微懂一点“活三”“冲四”的人就会败下阵来。

4.2 版本二:基于位置打分的启发式评估

第二个版本把AI从“被动堵漏”升级为“主动评估”。核心思想是:对棋盘上的每一个空位,分别计算“如果我方下在这里的得分”和“如果对方下在这里的得分”,最终选择两者之和最高的位置。

得分依据是当前位置在四个方向上能形成的连续棋子数量和开放程度。例如“活三”比“眠三”更危险,“冲四”比“活三”更紧急。这里我简化成了棋子连子数量与两端开放状态的乘积,没有引入更复杂的棋型库,但效果已经能应付大多数业余玩家。

核心评估代码是:

public int evaluatePoint(int row, int col, int color) { int totalScore = 0; for (int[] dir : DIRECTIONS) { int count = 1; int openEnds = 0; count += countDirection(row, col, dir[0], dir[1], color); count += countDirection(row, col, -dir[0], -dir[1], color); if (isOpen(row, col, dir[0], dir[1], color)) openEnds++; if (isOpen(row, col, -dir[0], -dir[1], color)) openEnds++; int score = count * count * openEnds; totalScore += score; } return totalScore; }

count * count的设计是个经验值。连子数越多,威胁指数级增长,4个连子和3个连子的差距绝不是1个棋子的差距,而是能不能赢的本质区别。用平方的权重,能确保AI优先形成四连而不是去盲目围堵对方的三连。

AI最终决策的逻辑是:

public int[] getBestMove() { int bestScore = -1; int[] bestMove = {7, 7}; // 默认走天元 for (int row = 0; row < BOARD_SIZE; row++) { for (int col = 0; col < BOARD_SIZE; col++) { if (!board.isEmpty(row, col)) continue; int attackScore = evaluatePoint(row, col, BLACK); int defendScore = evaluatePoint(row, col, WHITE); int totalScore = attackScore + defendScore; if (totalScore > bestScore) { bestScore = totalScore; bestMove = new int[]{row, col}; } } } return bestMove; }

这个策略属于攻守兼备的贪心算法。attackScore是自己在某个位置落子的进攻价值,defendScore是如果对手下在这里的威胁价值,两者相加意味着AI既想积极进攻,又不会对对手的威胁视而不见。实际对局中,它会主动形成活三、冲四,同时能及时封堵对手的活三之路。

4.3 AI评估的边界条件与优化思路

上述评估逻辑有一个隐患:当棋盘上都是空位时,AI遍历225个空位每个都做4个方向的评估,总计算量是900次方向评估,人眼完全无感。但AI对战AI或者加上了搜索树之后,这一步会被调用成千上万次,性能就必须优化了。

一个常见优化是只看落子点附近8×8范围内的空位,因为距离当前所有棋子太远的位置,评估价值极低,多半是浪费时间。代码上可以做一个简单的isNearby(int row, int col)判断,只有既有棋子存在的周边才纳入评估集。

另一个优化点是评估方向的对称性。事实上countDirection(row, col, dRow, dCol)和countDirection(row, col, -dRow, -dCol)的结果有大量重复计算,可以用缓存或增量更新的方式减少计算,但在“简单五子棋”这个定位上,我选择了保持代码可读性优先,这些优化可以作为进阶练习留给读者。

5. 控制台版本与Swing版本:两种交互形态的演进路线

同一个核心逻辑,我做了两套交互界面,这段经验对Java学习者应该很有启发。控制台版本适合理解游戏循环逻辑,Swing版本适合理解Java GUI编程和事件模型。

5.1 控制台版的游戏循环设计

控制台版的核心逻辑是经典的“三方循环”:人下棋→判断胜负→电脑下棋→判断胜负→再来一轮。这里的关键是不要让游戏循环和界面渲染耦合在一起。

public void start() { chessBoard.initBoard(); while (true) { playerMove(); // 玩家落子 if (rule.checkWin(lastRow, lastCol, ChessBoard.BLACK)) { System.out.println("黑棋胜利!"); break; } if (chessBoard.isBoardFull()) { System.out.println("平局!"); break; } int[] aiMove = ai.getBestMove(); chessBoard.placePiece(aiMove[0], aiMove[1], ChessBoard.WHITE); System.out.println("电脑落子:" + (aiMove[0] + 1) + " " + (aiMove[1] + 1)); if (rule.checkWin(aiMove[0], aiMove[1], ChessBoard.WHITE)) { System.out.println("电脑胜利!"); break; } } }

这个循环结构非常标准,几乎所有回合制游戏都是这个骨架。玩家输入的部分做了异常处理,如果输入不是两个数字就提示重新输入,不会直接崩掉。有一点要提醒:控制台版本的输入用的是Scanner,在连续输入时要注意缓冲区问题,最好每次都用nextLine()读取整行再拆分,避免nextInt()和nextLine()混用导致的换行符残留。

5.2 Swing版本的绘制与事件绑定

Swing版本的核心是一个继承JPanel的自定义画板类。我在paintComponent()里绘制棋盘和棋子,在MouseListener的mouseClicked()里处理落子事件。

绘制棋盘线的代码逻辑清晰简单:

@Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setColor(new Color(210, 180, 140)); g2.fillRect(0, 0, getWidth(), getHeight()); g2.setColor(Color.BLACK); for (int i = 0; i < BOARD_SIZE; i++) { g2.drawLine(margin, margin + i * cellSize, margin + (BOARD_SIZE - 1) * cellSize, margin + i * cellSize); g2.drawLine(margin + i * cellSize, margin, margin + i * cellSize, margin + (BOARD_SIZE - 1) * cellSize); } }

这里开启抗锯齿ANTIALIASING是画棋子不毛边的关键。我见过很多朋友画的棋子边缘全是锯齿,以为是坐标算错了,其实只是没开抗锯齿。另外,Swing的paintComponent()方法在窗口最小化、拉伸时会被系统反复调用,所以绘制逻辑必须是无状态的,不能依赖上一次绘制的中间状态。我最初的版本把棋子列表存在ArrayList里,绘制时统一遍历,这样窗口重绘后棋子不会消失。

5.3 线程模型与界面卡顿的坑

Swing版在接入AI后出现过一个经典问题:点击鼠标落子后,AI计算需要几百毫秒,正常情况下可以接受,但如果后续升级评估逻辑,计算时间超过一秒钟,界面就会“卡死”。原因在于鼠标点击事件处理器是在EDT线程里执行的,如果在这个线程里做了耗时计算,界面就无法重绘。

解决方案是在点击事件里开一个新线程执行AI计算,算完后再通过SwingUtilities.invokeLater()把结果交回EDT线程更新界面。这个模式是Swing进阶必须掌握的,我先在这里点破,等你的AI真的变复杂了,思路就清晰了。

6. 代码验证、测试路径与常见Bug排查

写完了五子棋,最终要验证它是不是真的能用。这部分我分享一下我的测试路径和遇到的问题,每一件都是实际踩过的,不是纸上谈兵。

6.1 用命令行参数快速验证核心规则

核心规则类Rule不依赖界面,所以可以做纯逻辑测试。我推荐初学者养成一个习惯:写好规则后,不急着接界面,先写一段模拟落子的测试代码,直接在控制台跑。比如模拟一个四连和一个空位,验证AI能不能准确“看到”这个威胁。

一个简单的测试脚本如下:

public class RuleTest { public static void main(String[] args) { ChessBoard board = new ChessBoard(); board.placePiece(7, 7, BLACK); board.placePiece(7, 8, BLACK); board.placePiece(7, 9, BLACK); board.placePiece(7, 10, BLACK); System.out.println(new Rule(board).checkWin(7, 10, BLACK)); // 应输出 true System.out.println(new Rule(board).checkWin(7, 8, BLACK)); // 应输出 true } }

这种测试不需要任何GUI,跑一次就能确认输赢判断是否在边界情况下也正确。我在开发中发现,所有方向边界面(比如把棋子下在第0列、第14行等)最容易出错,因为方向扫描到棋盘边界就不得不停止,一旦边界判断顺序写错,就会抛出数组越界异常。建议读者在测试时专门覆盖四角和四条边的位置。

6.2 两个容易被忽略的经典Bug

Bug 1:数组越界但被try-catch吞掉。我最初写完countDirection时,边界判断写在数组访问之后,结果抛异常。当时图省事加了try-catch包围,程序“看起来”没崩,但连续计数永远只数到一个棋子,胜负判断全错。排查了一下午才发现是边界判断顺序的问题。这个教训让我深刻明白:异常处理是最后一道防线,不是解决逻辑错误的万能药。

Bug 2:Swing点击区域偏移。前面讲坐标系时提到过这个问题。如果你的棋子在棋盘上越画越歪,检查一下margin和cellSize两个常量的定义是否统一。我最终使用了一个getIndexByPixel(int pixel)方法,把“像素坐标→数组索引”的换算集中在一个方法里,界面各处都调用它,再也没出过坐标错乱。

6.3 运行效果与简单实测数据

我用黑棋AI对白棋普通玩家(我自己)实测了十局,胜率大概是7:3。AI前五步基本会走在天元附近,后续会主动布活三,同时能敏锐地堵住我的冲四。期间也有两局AI判断出现“假赢”,就是明明形成了四连但被自己堵了一端,导致后续进攻力下降。这个原因是评估函数里没有对“活四”和“冲四”做更细的区分,属于简化策略的已知局限。

如果你想让AI更聪明,可以做的升级方向是:引入棋型表(活四、冲四、活三、眠三、活二),给不同棋型赋予不同的权重,再用递归搜索配合α-β剪枝。但这已经是“五子棋地狱难度AI”的范畴了,跟这份“简单五子棋”源码的定位不一样,我建议先把基础版本吃透再进阶。

7. 源码使用方式、扩展方向与个人体会

如果你拿到这份源码,用javac GobangGame.java编译后,运行java GobangGame -console就能在命令行直接对弈,不带参数则启动图形界面。图形界面上默认玩家执黑先行,AI执白后手,这个颜色分配也可以改成玩家执白,在入口处调整一下棋子枚举值即可。

关于扩展方向,我建议三个最顺手的路径:第一,加上“悔棋”按钮,实现方式是在落子历史里加入栈结构,悔棋时从栈顶弹出一到两步并更新棋盘;第二,加上“谁先手”的菜单选项,这个改动只需在初始化时交换黑白赋值;第三,把AI的“只评估当前空位”升级成两层的极小极大搜索,每层各评估一次棋盘局面,代码量增加不大,但棋力会有质的飞跃。

我在做这个项目的过程中最大的体会是:五子棋看似简单,但“规则判断”“界面交互”“AI决策”三个模块的经验,几乎覆盖了Java游戏开发的所有核心知识点。不要抱着“网上有现成源码我抄一下”的心态去做,自己动手拆一遍、改一遍、踩一遍坑,收获会大得多。哪怕只是把小版本的代码逐行读一遍,把“为什么用二维数组”“为什么边界先判断”“为什么AI评估要分开计算进攻和防守分”这几个问题想清楚,你的Java功力都能往上走一层。

如果你在实际跑这份源码时遇到任何异常堆栈或逻辑问题,欢迎回来对照这篇内容的排查思路再过一遍。五子棋这个项目我已经写过三个版本,每次重构都有新理解,现在回头看,它的性价比比很多花哨的框架项目都高。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询