
简介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 足够内存占用小遍历时缓存友好。为什么不选ArrayListArrayListByte因为胜负判定和 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方法供判定逻辑读取棋盘外部无法直接操作内部数组。参数说明SIZE15是标准五子棋棋盘规格想改成 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 Dequeint[][] 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避免从胜局状态悔棋后卡死在赢棋分支。参数说明Dequeint[][]用ArrayDeque而不是Stack因为Stack继承自Vector带同步开销单线程项目用ArrayDeque是更正确的栈实现。快照数量上限是 22515×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_FOURAI 会优先摆出活四而不是急着冲四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 初始化会导致“所有空位都不如第一个位置”的 bugAI 永远落在棋盘左上角。参数说明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 默认就是双缓冲的。这里是“整体重绘”而非“局部脏矩形”原因是棋盘小、绘制简单追求局部优化反而增加出错概率。参数说明MARGIN30是棋盘边缘留白CELL_SIZE36是格子边长PIECE_RADIUS15是棋子半径。三个参数共同决定了窗口实际大小宽高约等于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 评估、界面包装、回归验证一天就完成了整个项目。这个顺序比任何一段代码都值得记住——先把规则跑对再考虑好不好看。希望帮到你。本文还有配套的精品资源点击获取