Java五子棋对战小游戏从零实现:控制台+Swing+AI完整教程

📅 发布时间:2026/10/10 3:53:21
Java五子棋对战小游戏从零实现:控制台+Swing+AI完整教程
很多学Java的朋友都会卡在一个尴尬的节点语法看完了面向对象也懂了Swing也刷过几个视频但要真动手做点东西不知道从哪下手。我这些年带过不少新人最常推荐的练手项目就是五子棋。它不依赖框架不碰网络和数据库逻辑链完整从数据结构到算法到界面一环扣一环做完以后你对Java的理解会上一个台阶。这篇就把我从零实现一个Java五子棋对战小游戏的完整过程拆开讲控制台版和图形界面版都有代码可以直接跑适合零基础入门也适合想找完整项目练手的朋友建议收藏后跟着一步步写不要光看。1. 为什么说五子棋是练手性价比最高的Java项目先说一个很多人想不通的问题网上Java项目一大把为什么偏偏选五子棋我见过太多新手第一个项目选“学生管理系统”“图书管理系统”结果写出来的代码就是一堆增删改查学了半天最核心的编程思维——状态管理、算法逻辑、程序流程控制——一点没练到。五子棋不一样它看着小五脏俱全。1.1 一个项目覆盖Java基础知识的八成五子棋这个项目最大的价值在于它能把Java基础语法和面向对象思想全部串起来。二维数组用来存棋盘数据这是Java里最常用的数据结构之一四方向扫描判输赢练的是循环嵌套和边界条件判断类封装把棋盘、玩家、AI拆开练的是面向对象设计到了图形界面阶段你会接触Swing组件、事件监听和重绘机制。一个项目下来数组、循环、方法、类、接口、异常处理、事件监听全都能碰到而且每个知识点都有具体的应用场景。比单纯看语法书好理解得多也扎实得多。1.2 完成度高做完能真正拿得出手另一个容易被忽略的点是“完成度”。很多练手项目做一半就烂尾因为没有成就感。五子棋不一样它从控制台版到GUI版再到AI对战是一步步升级的每走一步都能看到实际效果。你写完棋盘打印那一刻就能在控制台里下一盘棋加上AI后能跟电脑对弈最后套上Swing界面一个像模像样的桌面小游戏就诞生了。这个项目的边界也很清晰标准15x15棋盘、黑白双方交替落子、五子连珠判胜。规则简单但实现起来有小门槛。如果纯零基础只要掌握数组、for循环、方法调用和基本的类概念就可以开始这个项目。过程中遇到不懂的查询或debug反复几次理解会非常深刻。2. 动手之前先把这三件事想明白很多人写代码翻车不是语法不懂而是设计没想清楚就硬写。五子棋虽然简单但动手之前有三个决策点值得先想明白想清楚了后面代码一路顺。2.1 先做控制台版还是直接上图形界面我强烈建议分两步走先做控制台版再做GUI版。为什么因为控制台版的核心是“纯逻辑”测试方便、问题定位快。你可以在没有图形界面的情况下用System.out.println把棋盘一行行打出来用Scanner读用户的横纵坐标这样就完成了整个游戏规则的核心。等逻辑完全跑通了再把同一套逻辑接入Swing界面替换掉的只是“输入输出层”。这样做的隐藏价值更大锻炼“逻辑与界面分离”的意识。很多初学者习惯把业务逻辑和界面代码写在同一个类里写完一个项目提取不出来代码复用性极差。分两步走你会自然意识到——棋盘判赢这些代码在控制台版和GUI版中完全一样根本不用改。这个经验比项目本身值钱。2.2 棋盘用什么数据结构棋盘毫无疑问用二维数组。标准五子棋棋盘15x15定义一个int[][] board new int[15][15]就够了。数组中每个位置的值表示该格的状态0表示空1表示黑子2表示白子。为什么不用boolean数组因为boolean只有两种状态棋盘实际有三种状态空、黑、白不够用。为什么不用枚举对于新手来说int加常量也可以枚举虽然更安全但先用int能把注意力集中在逻辑上。项目做到后期自然会产生换枚举优化可读性的意识。这里也要提前预告一个关键点数组的行列索引从0开始但我们人习惯从1到15编号坐标。所以在做输入映射时要小心这个后面会专门讲。2.3 胜负判断的思路落子法不是全盘扫描判断五子棋胜负新手最容易想到的方法是每下一步棋就遍历整个棋盘检查所有连子情况。这个思路能实现但代码复杂、效率又低。实际上有一个更聪明的经典解法不用管全盘只看最后落下的那颗棋子。因为五子棋的胜负必然包含最后一颗落子所以胜负一定存在经过该点的连线。我们以最后落点为中心沿水平、垂直、左上到右下斜线、左下到右上斜线这四个方向从落点向两头延伸统计同色棋子的连续数量。只要有一个方向上连续棋子数大于等于5当前玩家就赢了。这个方法的代码量小、无重复扫描是绝大多数五子棋实现采用的做法。3. 核心逻辑的实现这四段代码是地基设计想清楚之后就可以写代码了。这里先实现纯逻辑部分是整个项目最重要的代码段。后续所有版本都建立在这个基础上建议亲手敲一遍不要直接复制。3.1 棋盘类封装状态和核心操作为了后面GUI扩展方便从一开始就把棋盘逻辑封装成一个独立的类。先定义常量和成员变量import java.util.Stack; public class GobangBoard { 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 int[][] board new int[SIZE][SIZE]; private Stackint[] history new Stack(); // 记录落子历史方便做悔棋history这个栈是额外加的。新手阶段可以先不管悔棋但加上它之后就给后续扩展留了口子。每次成功落子就把坐标数组压入栈中后面悔棋时直接弹出即可。3.2 落子与合法性校验落子方法要做三件事判断坐标越界、判断该位置是否已有棋子、占位并记录历史。返回boolean是为了让调用方知道是否成功不成功时玩家需要重新输入。public boolean placePiece(int row, int col, int player) { if (row 0 || row SIZE || col 0 || col SIZE) { return false; } if (board[row][col] ! EMPTY) { return false; } board[row][col] player; history.push(new int[]{row, col}); return true; }坐标边界判断的顺序很有讲究必须先判断越界再判断该位置是否为空。有次一个学员把顺序写反用户输入非法坐标后程序在访问数组时直接抛出ArrayIndexOutOfBoundsException游戏崩溃。先判边界再判内容这属于数组操作的基本功。3.3 胜负判定四方向双向扫描这是整个项目的核心算法。按之前说的“落子法”把最后落点传入检查四个方向public boolean checkWin(int row, int col, int player) { // 四个方向水平、垂直、主对角线、副对角线 int[][] directions { {0, 1}, // 水平方向行不变列递增 {1, 0}, // 垂直方向行递增列不变 {1, 1}, // 主对角线行和列同时递增 {1, -1} // 副对角线行递增列递减 }; for (int[] dir : directions) { int count 1; // 落子本身算一颗 // 沿正方向延伸 for (int step 1; step 5; step) { int nr row dir[0] * step; int nc col dir[1] * step; if (nr 0 nr SIZE nc 0 nc SIZE board[nr][nc] player) { count; } else { break; } } // 沿反方向延伸 for (int step 1; step 5; step) { int nr row - dir[0] * step; int nc col - dir[1] * step; if (nr 0 nr SIZE nc 0 nc SIZE board[nr][nc] player) { count; } else { break; } } if (count 5) { return true; } } return false; }这段代码的细节值得多看几遍。step 5不是随便写的只需要检查离落点4步内的棋子就够了因为带上落子本身count达到5就意味着出现了五连。超过4步之后count不可能超过5再扫是白费。同时每个方向的循环里一旦遇到空位或对方棋子要立刻break——因为统计的必须是“连续”棋子不能跳着数。这个连续性判断新手最容易搞错。3.4 打印棋盘让逻辑看得见控制台版需要一个可视化函数把二维数组转成带坐标的棋盘public void printBoard() { System.out.print( ); for (int i 0; i SIZE; i) { System.out.printf(%2d , i 1); } System.out.println(); for (int i 0; i SIZE; i) { System.out.printf(%2d , i 1); for (int j 0; j SIZE; j) { if (board[i][j] EMPTY) { System.out.print( . ); } else if (board[i][j] BLACK) { System.out.print( ● ); } else { System.out.print( ○ ); } } System.out.println(); } }如果你在Windows控制台里发现●显示成乱码不用慌把输出字符换成“X”和“O”就够了。控制台版的目的是看清逻辑而不是把界面做得好看。4. 把核心逻辑串成游戏控制台版完整流程有了以上四个方法整个游戏的主流程就很清晰了。它基本是一个死循环显示当前棋盘当前玩家输入坐标尝试落子失败就重新输入成功后判断胜负赢了结束否则切换玩家继续下一轮。4.1 主流程代码下面给出一个可以直接运行的完整控制台版精简了包结构方便复制import java.util.Scanner; public class ConsoleGobang { public static void main(String[] args) { GobangBoard game new GobangBoard(); Scanner scanner new Scanner(System.in); int currentPlayer GobangBoard.BLACK; int moveCount 0; System.out.println(欢迎玩五子棋输入行列坐标1-15例如7 7); while (true) { game.printBoard(); String playerName (currentPlayer GobangBoard.BLACK) ? 黑方 : 白方; System.out.print(playerName 请落子行 列); int row, col; try { row scanner.nextInt() - 1; // 用户输入从1开始数组索引从0开始 col scanner.nextInt() - 1; } catch (Exception e) { System.out.println(输入格式不对请输入两个数字用空格隔开); scanner.nextLine(); continue; } if (!game.placePiece(row, col, currentPlayer)) { System.out.println(该位置不合法要么越界要么已有棋子请重新输入。); continue; } moveCount; if (game.checkWin(row, col, currentPlayer)) { game.printBoard(); System.out.println(恭喜 playerName 获胜); break; } if (moveCount GobangBoard.SIZE * GobangBoard.SIZE) { System.out.println(棋盘已满平局); break; } currentPlayer (currentPlayer GobangBoard.BLACK) ? GobangBoard.WHITE : GobangBoard.BLACK; } scanner.close(); } }4.2 输入处理的几个细节很多新手卡在“输入坐标”这一环其实就两个坑。第一个是1到15与0到14的转换。用户输入7 7直觉上想下在棋盘中央但数组索引从0开始必须把输入值减1得到6 6才是真正的索引位置。忘了减1棋子位置整体偏移到边界时还会越界。第二个坑是Scanner的nextInt在用户输入非数字时会抛InputMismatchException。如果不做异常处理程序直接崩。上面代码里用try-catch包了一下并在catch中调用scanner.nextLine()清掉错误输入避免死循环。这个细节一开始可能觉得麻烦但实际对战过程中手滑输错的情况屡屡发生。4.3 悔棋与平局两个锦上添花的扩展平局判断在主循环里已经写好了逻辑很简单每成功落一次子moveCount加1当落子数等于22515x15个交叉点时说明棋盘填满但没人赢就是平局。悔棋实现也不复杂要悔棋时从history栈中弹出上一次落子坐标把board上该位置重置为EMPTY并把当前玩家切换回去。栈的特点是后进先出完美匹配“撤销上一步”的需求。控制台版加悔棋只需要在输入命令环节加一个分支比如输入-1 -1表示悔棋调用下面的方法public boolean undoMove() { if (history.isEmpty()) { return false; } int[] last history.pop(); board[last[0]][last[1]] EMPTY; return true; }注意如果悔棋时对方已经落子那么应该连续撤回两步保证落子权不被打乱。这个细节不少人做过项目都漏了导致悔完棋轮到对方连下两子。5. 让电脑陪你下棋三种AI思路由浅入深控制台双人对战跑通五子棋就完成了一半。但自己跟自己下终究有点寂寞接下来给程序加个AI对手。这里从最简单到稍复杂给出三种思路按需选择。5.1 随机AI让程序先“会走”最简单的AI就是瞎走从所有空位里随机选一个落子。实现毫无策略可言但适合验证整个“人机对战”流程是否通畅。public int[] randomMove() { Listint[] emptyCells new ArrayList(); for (int i 0; i SIZE; i) { for (int j 0; j SIZE; j) { if (board[i][j] EMPTY) { emptyCells.add(new int[]{i, j}); } } } if (emptyCells.isEmpty()) { return null; } return emptyCells.get(new Random().nextInt(emptyCells.size())); }随机AI的验证价值是一眼能看出程序在按棋规运行同时验证了棋盘状态管理没写崩。不过跟它对战你会觉得它像一个三岁小孩该堵的棋不堵该赢的棋不赢。但没关系先让它跑起来再做聪明的AI。5.2 攻防一体AI先看自己能赢再看对手要不要赢比随机强一个档次的思路是“堵胜截击”。轮到电脑走的时候先遍历所有空位看看有没有哪个位置放上电脑的棋子后能直接形成五连有就立刻赢如果没有再检查有没有哪个位置放上对手的棋子后会形成五连有就马上去堵。这个AI已经能跟新手打得有来有回了。实现的关键是复用checkWin方法模拟在空位落子然后调用checkWin判断判断完再把该位置恢复成EMPTY。注意这里必须要恢复现场不然棋盘状态就乱了public int[] findWinningMove(int player) { for (int i 0; i SIZE; i) { for (int j 0; j SIZE; j) { if (board[i][j] ! EMPTY) continue; board[i][j] player; if (checkWin(i, j, player)) { board[i][j] EMPTY; // 恢复现场 return new int[]{i, j}; } board[i][j] EMPTY; } } return null; }AI落子的完整逻辑是先找自己的必赢点再找对手的必赢点都找不到就随便挑一个靠近中心的空位下。这个思路的缺点也很明显——它只看得见一步棋不会防守“冲三”“活三”这类需要提前一步的威胁。5.3 权值评分AI给每个位置打分的“下棋大脑”真正让AI有“棋感”的做法是给棋盘上每个空位打分。打分依据是如果这个位置落下某方棋子在水平、垂直、两条斜线四个方向上能形成多少种有威胁的棋型。棋型分很多常见的有成五、活四、冲四、活三、眠三、活二、眠二等。每种棋型对应一个分数权重例如棋型分数含义成五1000000已形成五连活四100000两头都是空的四连冲四10000一端被堵的四连再走一步必成五活三5000两头空的连三眠三1000一端被堵的连三活二500两头空的连二AI对每个空位分别站在“自己视角”和“堵对手视角”打分自己下在这里能形成高分棋型加分对手下在这里会形成高分棋型也要加分。最后找总分最高的位置落子。实测下来权值AI下得过“堵胜截击”AI偶尔也会给新手制造惊喜。评分函数的实现核心是扫描每个空位四个方向上的“连续同色棋子数”和“两端开口情况”然后查表给分。下面给一个能直接跑通的评分思路public int scorePosition(int row, int col, int player) { int totalScore 0; int[][] dirs {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; for (int[] dir : dirs) { int count 1; // 假设此处已下player的棋子 int openEnds 0; // 两端开放的数量 // 正方向 int nr row dir[0], nc col dir[1]; while (nr 0 nr SIZE nc 0 nc SIZE board[nr][nc] player) { count; nr dir[0]; nc dir[1]; } if (nr 0 nr SIZE nc 0 nc SIZE board[nr][nc] EMPTY) { openEnds; } // 反方向 nr row - dir[0]; nc col - dir[1]; while (nr 0 nr SIZE nc 0 nc SIZE board[nr][nc] player) { count; nr - dir[0]; nc - dir[1]; } if (nr 0 nr SIZE nc 0 nc SIZE board[nr][nc] EMPTY) { openEnds; } totalScore evaluateLine(count, openEnds); } return totalScore; }evaluateLine把count和openEnds映射成分数映射规则就是上面那张表。这段代码还能继续优化比如把“堵对手”分数加权重再算一次但作为入门AI这个版本已经非常够用了。6. 从黑框框到图形界面Swing画出可视化战场控制台版玩久了会嫌它丑接下来用Swing做一个看得见的窗口版。这里不展开Swing的基础概念直接讲怎么把五子棋界面搭起来重点放在绘图和事件处理这两个核心问题上。6.1 整体界面结构界面分两层上层是一个画棋盘的面板BoardPanel负责绘制网格和棋子下层是一个状态栏JLabel显示当前轮到谁谁赢了。整体放进JFrame里窗口大小按棋盘计算。public class GobangUI extends JFrame { private GobangBoard game; private BoardPanel boardPanel; private JLabel statusLabel; public GobangUI(GobangBoard game) { this.game game; setTitle(Java五子棋对战); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); boardPanel new BoardPanel(); statusLabel new JLabel(黑方执子); add(boardPanel, BorderLayout.CENTER); add(statusLabel, BorderLayout.SOUTH); pack(); setLocationRelativeTo(null); setVisible(true); } }窗口创建本身不复杂复杂的是把棋盘画清楚。6.2 绘制棋盘与棋子棋盘面板继承JPanel重写paintComponent方法。每次系统刷新界面时这个方法会被自动调用。它的任务就是“把当前棋盘状态完整画出来”。先定义几个常量格子大小CELL_SIZE 40像素棋盘边距MARGIN 40像素。画网格线用drawLine画棋子用fillOval实心圆。class BoardPanel extends JPanel { Override protected void paintComponent(Graphics g) { super.paintComponent(g); int size GobangBoard.SIZE; // 画网格 for (int i 0; i size; i) { g.drawLine(MARGIN, MARGIN i * CELL_SIZE, MARGIN (size - 1) * CELL_SIZE, MARGIN i * CELL_SIZE); g.drawLine(MARGIN i * CELL_SIZE, MARGIN, MARGIN i * CELL_SIZE, MARGIN (size - 1) * CELL_SIZE); } // 画棋子 for (int i 0; i size; i) { for (int j 0; j size; j) { int x MARGIN j * CELL_SIZE - CELL_SIZE / 2; int y MARGIN i * CELL_SIZE - CELL_SIZE / 2; if (game.board[i][j] GobangBoard.BLACK) { g.setColor(Color.BLACK); g.fillOval(x, y, CELL_SIZE - 4, CELL_SIZE - 4); } else if (game.board[i][j] GobangBoard.WHITE) { g.setColor(Color.WHITE); g.fillOval(x, y, CELL_SIZE - 4, CELL_SIZE - 4); g.setColor(Color.BLACK); g.drawOval(x, y, CELL_SIZE - 4, CELL_SIZE - 4); } } } } }画棋子坐标上减去CELL_SIZE / 2目的是让棋子的圆心正好落在交叉点上。这个偏移量是画棋盘最容易忽略的细节。6.3 鼠标点击与坐标换算给BoardPanel注册鼠标监听器。用户点击的像素坐标要先换算成棋盘坐标boardPanel.addMouseListener(new MouseAdapter() { Override public void mouseClicked(MouseEvent e) { int col (e.getX() - MARGIN CELL_SIZE / 2) / CELL_SIZE; int row (e.getY() - MARGIN CELL_SIZE / 2) / CELL_SIZE; // 落子、判赢、刷新状态... } });换算公式要理解不要只抄。e.getX() - MARGIN把像素坐标变成相对于棋盘左上角的坐标加CELL_SIZE / 2是“就近取整”——鼠标点在第一格左半边也算第一格最后除以CELL_SIZE得到格子编号。少了加半格这步鼠标会偏差半格才能选中正确位置体验非常差。拿到坐标后调用游戏核心逻辑——判断合法性、落子、判赢——这些代码和控制台版完全一样。这就是前面强调“逻辑分离”的好处if (game.placePiece(row, col, currentPlayer)) { boardPanel.repaint(); if (game.checkWin(row, col, currentPlayer)) { statusLabel.setText(XX方胜出); } else { currentPlayer (currentPlayer GobangBoard.BLACK) ? GobangBoard.WHITE : GobangBoard.BLACK; statusLabel.setText(...); } }6.4 重绘问题为什么我的界面不刷新Swing界面不会自动感知数据变化必须在数据更新后手动调用repaint()。很多新手改了game里的数组界面却纹丝不动最后发现是忘了重绘。另外注意不要在paintComponent里做耗时操作。Swing的绘制在事件分发线程执行如果绘制逻辑里出现大量循环或复杂计算界面会卡顿。五子棋15x15规模完全不用担心但如果以后做更复杂的画布应用就要格外注意。7. 实测常踩的坑和我的调优笔记最后这部分算个人经验浓缩。上面代码你自己写一遍大概率会遇到下面几个问题提前帮你排雷。7.1 行列颠倒最容易犯的低级错误五子棋棋盘是正方形行列颠倒经常不会立刻报错但会导致棋谱错乱。比如用board[row][col]存数据画图时却用x对应行、y对应列整个棋盘像是被镜像翻转了。排查方法很简单在任意位置落一颗子打印棋盘与界面对比看哪边的棋子位置不一致。建议统一命名row是行纵坐标col是列横坐标界面中x对应coly对应row全程保持一致。7.2 判赢算法的“断头”问题如果把checkWin里扩展方向的循环条件错写成step 5会造成边界误判如果只扫正方向不扫反方向就会出现漏判。最典型的问题是落点右侧已经有4颗连子落点左侧还有一颗同色棋子正确情况下count应该是6必须判赢但如果代码只扫了反方向count只有1赢不了。这种逻辑bug很难一次定位建议在判赢方法里临时加个调试输出把每个方向的count打出来对比排查起来会快很多。7.3 鼠标坐标与棋子中心的偏差前面说过加半格的问题。还有一个细节鼠标点得偏到棋盘外时算出的row或col可能小于0或大于14代码里必须做范围过滤否则又双叒叕数组越界。稳妥的写法是先判断row 0 row SIZE col 0 col SIZE再调placePiece。7.4 面向对象层面的优化如果代码写到最后已经能跑我还想多说一句试着把GobangBoard、AI、UI拆成三层让GobangBoard完全不依赖Swing。你会发现以后想改成JavaFX版本、想加网络对战、想换更强的AI算法都不需要动棋盘逻辑。这其实就是“高内聚低耦合”思想的雏形。当年我项目做完第一版也是一个类堆到底后来重构一遍思路清晰太多了。最后分享一个我实际用下来体验很好的扩展方向给AI加难度选择。把随机AI、堵胜截击AI、权值评分AI做成三个档位开局时让玩家自己选。这样一个五子棋项目从简单到困难全覆盖发给朋友试玩反馈特别好。核心代码都是现成的组织到AiPlayer类里也就是几个if-else的事。我自己做这个项目时最满意的不是某个炫技算法而是“全部自己搞定、每一步都清楚”的踏实感。如果你能完整跟着写下来不做任何删减最后一定也会收获这种感觉。