简介:Java实现的经典五子棋小游戏完整源码,主要面向Java初学者、课程设计学生以及棋类游戏开发爱好者,能够解决从零搭建一个可交互对弈程序的需求。正在准备Java期末项目或想尝试游戏编程的读者,可以从中获得一个包含界面和AI对手的完整小游戏范例。压缩包共24个文件,包括8个Java源文件、8个已编译class、3个XML工程配置、2张界面效果图和1份README文档,整体仅90KB,在Java环境下可直接运行对比效果。源码覆盖棋盘绘制、鼠标落子、胜负判定等完整流程,并在EasyAI与DifficultAI中实现了不同难度的对战算法,有助于理解二维数组棋盘建模、事件监听机制与基础AI策略。已有84人学习下载,资源兼具可运行性与可扩展性,适合作为Java课程设计、毕业设计或游戏开发入门练习的参考。
1. 从“Java实现五子棋游戏.zip”说起:这份源码到底在练什么
很多按 java 学习路线自学的人,容易卡在同一个位置:语法懂了、集合会用了、面向对象编程 java 也算上手,可真要独立写一个“能给别人演示”的完整项目,脑袋一片空白。五子棋游戏正好补上这段缺口——它不依赖数据库、不依赖 Spring Boot,只用 java 基础里的二维数组、循环判定、事件监听和 Swing 就能做完整。这份 zip 不是让你下载后跑一遍就完,而是要亲手拆成棋盘建模、胜负判定、人机 AI、界面交互、打包发布五个环节。适合三类人:刷 java 面试题前缺项目的人,蓝桥杯备赛想练数组算法的人,以及刚学完 java 基础想验证自己能不能独立交付的人。下面按我实际写这个项目时的顺序展开,你跟着复现即可。
2. 棋盘与落子模型:用 byte[][] 建模 15×15,先把数组越界异常挡在入口
2.1 棋盘数据结构:为什么固定棋盘选二维数组而不是 List
五子棋棋盘是固定的 15×15,最直接的数据结构就是二维数组。我选byte[][]而不是int[][],原因是棋盘每个格子只有三种状态:空、黑、白,用 byte 足够,内存占用小,遍历时缓存友好。为什么不选ArrayList<ArrayList<Byte>>?因为胜负判定和 AI 评估要频繁按下标读格子,二维数组的随机访问是 O(1),而集合的装箱拆箱和双重 get 会带来无谓开销。这个选择本身不算什么高深优化,但能帮新手复习 java 数据类型的默认值:new byte[15][15]所有元素自动是 0,正好对应“空”。
// Board.java —— 棋盘核心类(面向对象封装) public class Board { public static final int SIZE = 15; // 标准五子棋棋盘 public static final byte EMPTY = 0; public static final byte BLACK = 1; public static final byte WHITE = 2; private final byte[][] grid = new byte[SIZE][SIZE]; public boolean inBoard(int row, int col) { return row >= 0 && row < SIZE && col >= 0 && col < SIZE; } public boolean canPlace(int row, int col) { return inBoard(row, col) && grid[row][col] == EMPTY; } public boolean place(int row, int col, byte color) { if (!canPlace(row, col)) { return false; } grid[row][col] = color; return true; } public byte at(int row, int col) { return grid[row][col]; } public boolean isFull() { for (int r = 0; r < SIZE; r++) { for (int c = 0; c < SIZE; c++) { if (grid[r][c] == EMPTY) { return false; } } } return true; } }逻辑说明:inBoard是所有坐标判断的入口,只要后续代码先过它,就不会出现数组越界异常。place返回 boolean 而不是抛异常,是因为“落子失败”在游戏里是常态——位置被占、坐标在界外,程序应当继续提示玩家重下,而不是崩掉。at方法供判定逻辑读取棋盘,外部无法直接操作内部数组。
参数说明:SIZE=15是标准五子棋棋盘规格,想改成 19 路棋盘只要改这一处;EMPTY/BLACK/WHITE三个常量用 byte 的 0/1/2 表示,后续所有逻辑都基于这三个值判断,不要到处写魔法数字。
2.2 落子合法性校验:格式、范围、占用三层检查
控制台输入是最早要面对的坑:玩家输入“8,8”“8 8”甚至“abc”都要处理。我习惯把所有输入解析收敛到一个静态方法里,返回null表示非法输入,主循环只要判断空引用就行,不需要到处 try-catch。
// 输入解析:把用户输入转成 0 下标坐标 public static int[] parseMove(String input, int size) { String[] parts = input.trim().split("[,\\s]+"); if (parts.length != 2) { return null; // 格式不对:必须是“8,8”或“8 8” } int row, col; try { row = Integer.parseInt(parts[0]); col = Integer.parseInt(parts[1]); } catch (NumberFormatException e) { return null; // 不是数字 } if (row < 1 || row > size || col < 1 || col > size) { return null; // 超出棋盘范围 } return new int[]{row - 1, col - 1}; // 转成数组下标 }逻辑说明:这里把三种失败场景——格式错误、非数字、越界——统一归成null,调用方只需要输出一行“输入不合法”。注意下标转换是在校验之后做的,用户看到的是 1 到 15,内部数组是 0 到 14,漏掉row - 1会在落子时出现经典的数组越界异常。还有一种玄学情况:用户在行和列之间输入了中文逗号“,”,split("[,\\s]+")匹配不到,这里我建议在解析前把输入里的中文逗号替换成英文逗号,属于血泪经验。
参数说明:size参数从调用方传入,而不是方法内部写死 15,这样解析逻辑和棋盘尺寸解耦;返回数组的第一个元素是行、第二个是列,后续所有调用方都按这个约定取数。
2.3 五子连珠判定:四方向偏移数组与双向计数,不留给漏判死角
胜负判定是整个项目的核心算法。常见错误是只检查水平、垂直两个方向,或者只从落子点向一个方向数,导致斜着连成五子不判赢。正确做法是用一个偏移数组保存四个方向,对每个方向分别向正、反两侧连续统计。
// 胜负判定:只检查落子点所在的四条线 public boolean checkWinAt(int row, int col, byte color) { int[][] directions = { {0, 1}, // 水平:向右 {1, 0}, // 垂直:向下 {1, 1}, // 主对角线:右下 {1, -1} // 副对角线:左下 }; 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, byte color) { int count = 0; int r = row + dRow; int c = col + dCol; while (inBoard(r, c) && at(r, c) == color) { count++; r += dRow; c += dCol; } return count; }逻辑说明:为什么只检查落子点而不是全盘扫描?因为每局只落一颗子,如果出现五连,当前这颗子必然在赢线上,只查它所在四条线就够了。每条线向正反两个方向数,中间用count = 1把落子本身算进去,四个方向只要有一个方向的连续数达到 5 就判赢。while循环里先判inBoard再读at,顺序不能反——否则在边界线上会直接触发数组越界异常,这是新手最容易翻车的地方。
参数说明:directions里的{1, -1}表示向左下方向,正方向的反向是{-1, 1},也就是向右上,两者合成一条完整的副对角线。如果以后想改成四子棋或六子棋,把count >= 5改成 4 或 6 即可,SIZE不变也能跑。
3. 控制台版双人对战:先跑通主循环和状态机,再谈界面
3.1 主循环:读坐标、落子、判赢、换手的正确顺序
我强烈建议先用控制台把完整游戏跑通,再写 Swing 界面。原因是界面会引入坐标换算、重绘、事件排队这些干扰项,如果逻辑和界面耦合在一起,出了问题很难定位。控制台版的主循环只需要Scanner和循环,能验证的却是整套规则。
// ConsoleGame.java —— 双人控制台版主循环 public class ConsoleGame { private final Board board = new Board(); private byte current = Board.BLACK; private GameState state = GameState.PLAYING; private final Deque<int[][]> snapshots = new ArrayDeque<>(); public void start() { Scanner scanner = new Scanner(System.in); while (state == GameState.PLAYING) { System.out.print((current == Board.BLACK ? "黑方" : "白方") + " 请输入行列(如 8,8,输入 q 退出):"); String line = scanner.nextLine().trim(); if ("q".equalsIgnoreCase(line)) { return; } int[] move = parseMove(line, Board.SIZE); if (move == null) { System.out.println("输入不合法,请重新输入"); continue; } saveSnapshotForUndo(); if (!board.place(move[0], move[1], current)) { snapshots.pop(); // 落子失败要撤销刚才的快照 System.out.println("该位置已有棋子,请重新落子"); continue; } printBoard(); if (board.checkWinAt(move[0], move[1], current)) { state = (current == Board.BLACK) ? GameState.BLACK_WIN : GameState.WHITE_WIN; System.out.println((current == Board.BLACK ? "黑方" : "白方") + " 获胜"); return; } if (board.isFull()) { state = GameState.DRAW; System.out.println("棋盘已满,平局"); return; } current = (current == Board.BLACK) ? Board.WHITE : Board.BLACK; } } }逻辑说明:这里有一个顺序问题——必须先落子、再判赢、最后换手,如果判赢前就把current换了,提示信息会变成对方获胜。saveSnapshotForUndo()放在place之前,是因为悔棋要回滚到落子前;如果place失败,刚才压入栈的快照必须立刻弹出,否则悔棋会跳过一颗真实落子。
参数说明:current表示当前轮到谁,用board.BLACK和board.WHITE两个常量轮换而不是布尔值,因为后续 AI 和人机切换时,颜色直接传给 AI 评估函数,避免再转换一层。GameState是下一节要讲的枚举,这里已经用上了。
3.2 游戏状态机:用枚举替代散装 boolean,堵住“赢棋还能落子”
早期版本我用两个 boolean 变量分别表示“游戏是否结束”和“谁赢了”,结果平局时忘记置结束标志,界面版出现赢棋后还能继续落子的诡异现象。改用枚举后,所有分支只认一个state,逻辑清晰得多。
public enum GameState { PLAYING, // 对局中 BLACK_WIN, // 黑方胜 WHITE_WIN, // 白方胜 DRAW // 平局 }状态迁移只有四种:PLAYING -> BLACK_WIN / WHITE_WIN / DRAW,以及重开时回到PLAYING。主循环进入前检查state == PLAYING,落子入口也检查一次,双保险。这样无论控制台还是界面,都不会出现“死局还能落子”的问题。后面加悔棋功能时还有个边界——悔棋后如果状态是BLACK_WIN,应当把状态回退到PLAYING,因为棋盘已经变了,胜负结果不再成立。
3.3 悔棋功能:二维数组 clone 是假深拷贝,逐行复制才是后悔药
悔棋最常见的实现是保存落子前的棋盘快照。这里有一个 java 基础的经典陷阱:int[][] copy = grid.clone()只复制了外层数组,内层一维数组仍然是同一份引用,修改copy[0][0]会直接改动原棋盘,悔棋自然不生效。
private void saveSnapshotForUndo() { int[][] copy = new int[Board.SIZE][Board.SIZE]; for (int r = 0; r < Board.SIZE; r++) { System.arraycopy(board.atRow(r), 0, copy[r], 0, Board.SIZE); } snapshots.push(copy); } public boolean undo() { if (snapshots.isEmpty()) { return false; } int[][] last = snapshots.pop(); board.restore(last); current = (current == Board.BLACK) ? Board.WHITE : Board.BLACK; state = GameState.PLAYING; return true; }逻辑说明:System.arraycopy逐行复制才是真正的深拷贝,悔棋之后棋盘恢复到上一手落子之前。恢复后current要换回对手:假设黑方刚落完第 5 手、轮到白方,悔棋撤销这一手后应当还是黑方走,所以把current翻转一次。state强制设回PLAYING,避免从胜局状态悔棋后卡死在赢棋分支。
参数说明:Deque<int[][]>用ArrayDeque而不是Stack,因为Stack继承自Vector带同步开销,单线程项目用ArrayDeque是更正确的栈实现。快照数量上限是 225(15×15 全部落满),内存占用约 100KB,不需要额外限制。
4. 人机对战:用评分函数让 AI 学会堵活三和冲四
4.1 为什么先写评估函数而不是直接上搜索树
很多初学者一听人机对战就想到极大极小搜索、Alpha-beta 剪枝。实际做的时候会发现,搜索深度一旦超过 4 层,每层要评估的局面数量指数爆炸,而评估函数本身写得不准,搜索再深也是白搭。我建议第一步先做“贪心 AI”:对每个空位打分,永远选分数最高的位置落子。这个 AI 的棋力大约相当于刚学会规则的新手,但它能正确堵冲四、做活三,已经足够当练习对手。
// AiBrain.java —— 贪心评分 AI 的对外接口 public class AiBrain { private static final int WIN = 100000; private static final int LIVE_FOUR = 50000; private static final int RUSH_FOUR = 10000; private static final int LIVE_THREE = 8000; private static final int SLEEP_THREE = 2000; private static final int LIVE_TWO = 600; private static final int SLEEP_TWO = 200; }逻辑说明:这些常量构成了一张评分权重表,AI 对每个空位计算“我下这里能形成多好的棋型”和“对手下这里能形成多好的棋型”,加权求和。权重之间的大小关系决定了 AI 的攻防倾向:LIVE_FOUR远大于RUSH_FOUR,AI 会优先摆出活四而不是急着冲四;RUSH_FOUR大于LIVE_THREE,意味着对手下一步要冲四时 AI 会优先堵而不是自己去做活三。
参数说明:这些不是标准答案,而是调试起点。如果你希望 AI 更激进,把LIVE_THREE调到/10000以上,它会更愿意做活三;如果你希望它更稳健,把SLEEP_THREE调高,它会提前封堵对手的眠三。后面 4.3 的攻防权重是第二层调节旋钮。
4.2 单点棋型评分:四方向连子数与两端状态
评估一个空位价值,我用的方法是“试放”:先把当前位置放上己方棋子,沿着横、竖、两条斜线数一数连续有几颗同类棋子,再看两端分别是被对手挡住、被边界挡住还是空着。两端都空是“活”,一端被堵是“眠/冲”。
public int evaluatePoint(Board board, int row, int col, byte me, byte rival) { if (!board.canPlace(row, col)) { return -1; } int score = 0; score += lineScore(board, row, col, 0, 1, me, rival) * 2; // 横线 score += lineScore(board, row, col, 1, 0, me, rival) * 2; // 竖线 score += lineScore(board, row, col, 1, 1, me, rival); // 主对角线 score += lineScore(board, row, col, 1, -1, me, rival); // 副对角线 return score; } private int lineScore(Board board, int row, int col, int dr, int dc, byte me, byte rival) { board.place(row, col, me); int count = 1; boolean open1 = true, open2 = true; int r = row + dr, c = col + dc; while (board.inBoard(r, c) && board.at(r, c) == me) { count++; r += dr; c += dc; } if (!board.inBoard(r, c) || board.at(r, c) == rival) { open1 = false; } r = row - dr; c = col - dc; while (board.inBoard(r, c) && board.at(r, c) == me) { count++; r -= dr; c -= dc; } if (!board.inBoard(r, c) || board.at(r, c) == rival) { open2 = false; } board.place(row, col, Board.EMPTY); // 恢复现场,重要! if (count >= 5) return WIN; if (open1 && open2) { if (count == 4) return LIVE_FOUR; if (count == 3) return LIVE_THREE; if (count == 2) return LIVE_TWO; } else if (open1 || open2) { if (count == 4) return RUSH_FOUR; if (count == 3) return SLEEP_THREE; if (count == 2) return SLEEP_TWO; } return 0; }逻辑说明:board.place(row, col, me)是临时落子,函数结束时必须board.place(row, col, Board.EMPTY)恢复现场,否则评估一个位置之后棋盘就被污染了,后续所有空位判断全错。这个“试放再恢复”的模式是 AI 评估的核心手法,后面接搜索树时也完全复用。两个方向的连续计数方式和checkWinAt一致,区别在于这里要额外记录两端状态。
参数说明:横线和竖线乘以 2、斜线不乘,是因为五子棋里横竖线比斜线更容易被人眼关注,实际对局中横竖方向的威胁通常更需要响应,这个系数是经验值,可以调。评估对手时调用方式是evaluatePoint(board, r, c, human, ai),human变成me,这样对手的棋型也会按同一张权重表打分。
4.3 选点策略:进攻权重、防守权重与随机扰动的搭配
有了单点评分,AI 的策略就是在所有空位里找总分最高的位置。总分由两部分组成:自己下这里的进攻价值,加上对手下这里的防守价值。因为同一个空位对手下了也会形成棋型,AI 需要“攻击的同时堵住对方最好的点”。
public int[] bestMove(Board board, byte ai, byte human) { int[] best = null; long bestScore = Long.MIN_VALUE; for (int r = 0; r < Board.SIZE; r++) { for (int c = 0; c < Board.SIZE; c++) { if (!board.canPlace(r, c)) { continue; } long attack = evaluatePoint(board, r, c, ai, human); long defend = evaluatePoint(board, r, c, human, ai); long score = attack * ATTACK_WEIGHT + defend * DEFEND_WEIGHT + random.nextInt(3); if (score > bestScore) { bestScore = score; best = new int[]{r, c}; } } } return best; }逻辑说明:attack是 AI 自己形成棋型的价值,defend是同一位置被对手占住会形成的价值。两者用权重相加,谁权重大 AI 就更偏向哪一边。bestScore初始用Long.MIN_VALUE而不是 0,是因为某些空位的评分可能为负数,用 0 初始化会导致“所有空位都不如第一个位置”的 bug,AI 永远落在棋盘左上角。
参数说明:ATTACK_WEIGHT和DEFEND_WEIGHT默认一个是 110、一个是 100,相当于 AI 在攻守均衡下略微偏进攻。想做一个稳健防守型 AI,把DEFEND_WEIGHT调到 160、ATTACK_WEIGHT降到 80。random.nextInt(3)是 0 到 2 的随机扰动,目的是避免同一种棋局下 AI 永远走同一个点,实操中这个扰动极有用——不然你会发现 AI 前五手每次都一样。
5. Swing 界面搭建与常见问题排查:坐标换算、中文乱码、AI 不落子怎么定位
5.1 为什么用 JPanel 自绘棋盘而不是 JButton 网格
界面层最常见的诱惑是铺 225 个 JButton 当棋盘,每个格子一个按钮。这样做的缺点很明显:棋子是圆形且要叠加在格线上,按钮背景画圆形要走自定义 ButtonUI,鼠标悬停效果还会干扰棋局判断。我选择用一个 JPanel 重写paintComponent,在画布上先画 15×15 的格线,再根据 Board 数组画黑白棋子。
public class GomokuPanel extends JPanel { private final Board board; @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; // 画棋盘线 for (int i = 0; i < Board.SIZE; i++) { g2.drawLine(MARGIN + i * CELL_SIZE, MARGIN, MARGIN + i * CELL_SIZE, MARGIN + (Board.SIZE - 1) * CELL_SIZE); g2.drawLine(MARGIN, MARGIN + i * CELL_SIZE, MARGIN + (Board.SIZE - 1) * CELL_SIZE, MARGIN + i * CELL_SIZE); } // 画棋子 for (int r = 0; r < Board.SIZE; r++) { for (int c = 0; c < Board.SIZE; c++) { if (board.at(r, c) != Board.EMPTY) { int x = MARGIN + c * CELL_SIZE - PIECE_RADIUS; int y = MARGIN + r * CELL_SIZE - PIECE_RADIUS; g2.setColor(board.at(r, c) == Board.BLACK ? Color.BLACK : Color.WHITE); g2.fillOval(x, y, PIECE_RADIUS * 2, PIECE_RADIUS * 2); } } } } }逻辑说明:每次repaint()都会根据board数组重新绘制全部内容。15×15 的棋盘总共 225 个格子,现代电脑一帧画完毫无压力,完全不需要自己写双缓冲——Swing 的 JPanel 默认就是双缓冲的。这里是“整体重绘”而非“局部脏矩形”,原因是棋盘小、绘制简单,追求局部优化反而增加出错概率。
参数说明:MARGIN=30是棋盘边缘留白,CELL_SIZE=36是格子边长,PIECE_RADIUS=15是棋子半径。三个参数共同决定了窗口实际大小:宽高约等于MARGIN * 2 + 14 * CELL_SIZE。设置setPreferredSize时要用这个算式,而不是随便填 500,否则右边和下边的棋盘线会被裁掉。
5.2 鼠标坐标换算:MARGIN 与 CELL_SIZE 的乘除关系
现象:点击棋盘交叉点时,棋子总落在偏离半个格子的位置,或者落在左上角。原因:鼠标事件返回的是像素坐标,需要换算成行列下标,换算公式里少了“先减 MARGIN、再加半格”的步骤。
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; if (!board.inBoard(row, col)) { return; // 点在棋盘外,直接忽略 } // 后续交给统一的落子入口处理 } });逻辑说明:e.getX() - MARGIN把像素坐标转成相对棋盘左上角的偏移,+ CELL_SIZE / 2实现四舍五入——点击“第 0 格和第 1 格之间偏右”时算成第 1 格,而不是靠整除直接截断。换算完成后必须再用inBoard二次校验,因为玩家可能点在棋盘外边缘,此时col可能是 -1 或 15。
症状里还有一种翻车现场:点击后棋子和鼠标十字线错位一个格子,那是row和col的换算搞反了——注意x对应列、y对应行,写反之后横竖方向都偏移。这个错位看起来“挺像那么回事”,但斜着下几步就会露馅。
5.3 点击棋盘外与已有棋子:落子入口的统一拦截
现象:点击棋盘外区域时程序报数组越界异常,或者点击已经有棋子的位置时把原棋子覆盖成新颜色。原因:鼠标事件拿到坐标后没有经过合法性校验,直接写入了 Board,而 Board 里同一位置的旧值被覆盖了。解决的办法是把“界面点击”和“控制台输入”都收敛到同一个落子动作。
public boolean tryPlace(int row, int col, byte color) { if (state != GameState.PLAYING) { return false; // 对局已结束,拒绝落子 } if (!board.place(row, col, color)) { return false; // 棋子越界或该位置已有棋子 } repaint(); return true; }逻辑说明:tryPlace是所有落子路径的唯一入口,无论来自鼠标还是控制台,都先检查状态是否为PLAYING,再检查棋盘是否接受这次落子。这样既堵住了重复占位,也堵住了“赢棋后还能点上去”的问题。repaint()放在place成功之后,重绘整个棋盘。
注意:不要在mouseClicked里既调tryPlace又自己调repaint,界面层只负责“把坐标翻译成行列并请求落子”,绘制由统一入口触发。职责分层清楚之后,后面加悔棋、加 AI 自动落子,都只需要调用同一个逻辑层方法。
5.4 中文乱码和 AI 不动:编码设置与日志定位
中文乱码是 Swing 项目的经典问题。现象:控制台和窗口标题全是“口口”,连按钮文字都看不清。这里有两个独立原因:一是源文件编译时没有指定 UTF-8 编码,Windows 默认 GBK 编译导致中文字符串直接损坏;二是 Swing 默认字体不支持中文字符。解决方法是编译时加-encoding UTF-8,并在主窗口里显式设置字体。
Font font = new Font("Dialog", Font.PLAIN, 14); // Dialog 是跨平台逻辑字体 UIManager.put("Label.font", font); UIManager.put("Button.font", font);AI 不落子的排查概率最高的是 4.3 里那个bestScore = Long.MIN_VALUE的初始化问题,但还有一个隐蔽原因:bestMove里调evaluatePoint后忘了恢复现场,导致第一个空位被试放后变成非空,后续canPlace全部判断失败,最后返回 null。我自己的习惯是在bestMove入口和出口各打一行日志:
System.out.println("AI bestMove: " + Arrays.toString(best) + " score=" + bestScore);这个输出能直接看出 AI 是在正常选点还是遍历了 0 个候选位置。如果best是 null,优先检查恢复现场;如果best一直停在同一个点,再看随机扰动和权重。日志是定位 AI 黑匣子的唯一后悔药,别靠肉眼盯棋盘猜。
6. 回归验证与打包:让“Java实现五子棋游戏.zip”开箱即跑
6.1 五子棋五项回归用例:边界、四种连线、悔棋
做完功能后,我一般先跑这五条用例,再考虑打包发布。它们覆盖了全部核心路径,手点一遍不超过三分钟。
| 用例 | 操作 | 预期结果 |
|---|---|---|
| 横连 | 黑方依次落 (7,7) 到 (7,11) | 第 5 手提示黑方获胜,之后无法继续落子 |
| 竖连 | 白方依次落 (8,3) 到 (8,7) | 提示白方获胜 |
| 右斜 | 黑方落 (7,2),(8,3),(9,4),(10,5),(11,6) | 提示黑方获胜 |
| 左斜 | 白方落 (7,8),(8,7),(9,6),(10,5),(11,4) | 提示白方获胜 |
| 越界与悔棋 | 输入 0,0 和 16,16,再执行悔棋 | 输入被拒绝且不崩;悔棋后轮到被撤销方落子 |
如果表里的某一条不过,回去查对应的章节:连线问题看 2.3 的directions,越界问题看 2.2 和 5.2,悔棋问题看 3.3 的深浅拷贝。
6.2 javac 与 jar 手动打包:入口类、编码、JRE 三者对齐
我习惯用命令行手动打包而不是依赖 IDE 的导出按钮。原因是手动打包能看清整个链路,遇到“双击 jar 没反应”时知道往哪查。
javac -encoding UTF-8 -d out src/game/*.java jar cfe gomoku.jar game.GomokuMain -C out . java -jar gomoku.jar逻辑说明:-encoding UTF-8是中文不乱码的前提;cfe里的e指定入口类,game.GomokuMain必须写成带包名的全限定类名,而且要确认这个类里有public static void main(String[] args)。最后一步java -jar gomoku.jar如果启动失败,先执行java -version确认 JDK 装好了——win11 系统 java 环境配置不对时,双击 jar 通常没有任何反应,这时在命令行跑一次就能看到具体报错,比双击黑匣子高效得多。
6.3 README 该写什么:让解压的人三步跑起来
交付 zip 时我总会放一个 README,包含四件事:项目一句话简介、运行环境要求(JDK 8+)、启动命令java -jar gomoku.jar、操作说明(双人模式怎么下、人机模式怎么切换、悔棋按哪个键)。源码目录、编译好的 jar、README 三者放在同一级目录再压缩,命名成“Java实现五子棋游戏.zip”,这样别人拿到手不需要看源码也能先跑起来。
我自己最早写五子棋时,先做界面后写判定,结果坐标对不上、赢了还能下,改了两天才理顺。后来换了顺序:控制台逻辑、AI 评估、界面包装、回归验证,一天就完成了整个项目。这个顺序比任何一段代码都值得记住——先把规则跑对,再考虑好不好看。希望帮到你。
本文还有配套的精品资源,点击获取