Java五子棋实战:Swing事件驱动与二维数组判胜逻辑详解
发布时间:2026/10/26 2:13:08 锦皓数字建站

简介这份资源面向具备一定Java基础、希望以完整项目练手的开发者围绕五子棋这一规则简单却策略深奥的智力游戏展开讲解如何用Java实现可跨平台运行的五子棋程序。内容涵盖棋盘类与棋子类的面向对象设计、落子与胜负判定、合法移动校验以及人类玩家与基于Minimax或Alpha-Beta剪枝的AI玩家实现并借助Swing或JavaFX构建事件驱动的游戏循环同时涉及悔棋、存档读档、难度设置与人机对战等扩展功能。压缩包共65个文件约2.73MB以h与cpp源文件、obj目标文件为主另含wav音效、bmp图像、ico图标及ini配置等素材并附有工程文件与说明文档便于直接编译运行和二次修改。已有205人学习下载适合想系统理解Java面向对象设计、GUI构建与博弈算法实践的读者参考。1. 从一张棋盘说起这个 Java 五子棋项目到底能干什么很多人第一次接触 Java 图形界面都是从一张棋盘开始的。这个项目就是一套用 Java 实现的五子棋游戏带完整落子逻辑、胜负判定和界面交互代码量不大但把「事件驱动 二维数组 状态机」这条主线走通了。它适合两类人一类是刚学完 Java 语法、想找个能跑起来的小项目练手的开发者另一类是想拿一个干净骨架做二次开发比如加 AI 对战、加悔棋、加网络对战的熟手。棋盘不大但五脏俱全落子、判胜、重开这些核心环节都在里面跑通之后你对 Swing 事件模型和游戏循环的理解会实打实上一个台阶。2. 项目结构与技术选型一个桌面棋局是怎么搭起来的2.1 技术栈与整体架构这个项目走的是纯 Java 桌面路线没有引入任何第三方框架核心依赖就是 JDK 自带的 Swing 和 AWT。选这套技术栈的原因很直接五子棋是典型的回合制、事件驱动型应用不需要高并发也不需要复杂的数据持久化Swing 的JPanel加MouseListener就能把「点击落子 → 重绘棋盘 → 判定胜负」这条链路串起来。常见做法是把整个工程拆成三层界面层负责画棋盘和响应鼠标逻辑层负责维护棋局状态和判胜入口层负责组装窗口。这样拆的好处是逻辑层不依赖 Swing后面想换成 JavaFX 或者命令行版本只改界面层就行。从工程组织上看一个合格的目录结构大致是这样gomoku/ ├── src/ │ └── com/example/gomoku/ │ ├── Main.java # 程序入口创建窗口 │ ├── ui/ │ │ ├── BoardPanel.java # 棋盘绘制与鼠标事件 │ │ └── GameFrame.java # 主窗口菜单与状态栏 │ ├── core/ │ │ ├── Board.java # 棋盘数据模型落子与判胜 │ │ └── Player.java # 玩家枚举黑/白 │ └── util/ │ └── Constants.java # 棋盘尺寸、格子像素等常量 └── README.md这个结构不是硬性规定但把「画」和「算」分开是这类项目最值得坚持的一条线。我见过不少新手把所有代码塞进一个JFrame子类里结果加个悔棋功能就要动界面代码改一处崩三处血泪经验就是别让绘制逻辑和棋局逻辑长在一起。2.2 棋盘数据模型二维数组怎么存、怎么判棋局的核心是一个二维数组通常定义成int[][] board new int[15][15]15 是标准五子棋棋盘线数。数组里 0 表示空位1 表示黑子2 表示白子。用 int 而不是 boolean 或枚举是为了后面扩展 AI 时方便做权值计算这一点在二次开发里很关键。落子的方法要同时做三件事检查坐标是否越界、检查该位置是否已有棋子、写入当前玩家标记。判胜则是从刚落下的那颗子出发沿四个方向横、竖、两条斜线各数一遍连续同色棋子任意方向达到 5 颗即胜。下面这段是核心逻辑的典型写法// Board.java 核心片段 public class Board { public static final int SIZE 15; public static final int EMPTY 0; 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; } // 判胜从 (row,col) 向四个方向统计 public boolean isWin(int row, int col) { int player grid[row][col]; int[][] dirs {{0,1},{1,0},{1,1},{1,-1}}; // 横、竖、主对角、副对角 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, r row dr, c col dc; while (r 0 r SIZE c 0 c SIZE grid[r][c] player) { n; r dr; c dc; } return n; } }place里的两次边界与占位检查是必须的少了任何一个都会出现「棋子叠在一起」或者数组越界的翻车现场。isWin用方向数组统一处理四个方向比写四段几乎一样的循环干净得多countDirection只朝一个方向数正反各调一次再减掉重复计算的起点就是完整连线长度。参数dr、dc是行和列的步进量横方向是(0,1)竖方向是(1,0)两条斜线分别是(1,1)和(1,-1)这个映射记住之后判胜逻辑基本不会写错。2.3 界面绘制与鼠标事件Swing 怎么把棋盘画出来界面层的职责是把二维数组翻译成屏幕上的线和圆。BoardPanel继承JPanel重写paintComponent(Graphics g)先画 15 条横线和 15 条竖线再遍历数组把非空位置画成实心圆。格子间距用一个常量控制比如CELL 40棋盘边距留MARGIN 30这样坐标换算就是x MARGIN col * CELL。鼠标事件通过实现MouseListener的mouseClicked拿到点击坐标再反算成行列号// BoardPanel.java 鼠标落子片段 Override public void mouseClicked(MouseEvent e) { int col Math.round((e.getX() - MARGIN) / (float) CELL); int row Math.round((e.getY() - MARGIN) / (float) CELL); if (row 0 || row Board.SIZE || col 0 || col Board.SIZE) return; if (board.place(row, col, currentPlayer)) { if (board.isWin(row, col)) { JOptionPane.showMessageDialog(this, (currentPlayer 1 ? 黑棋 : 白棋) 获胜); resetGame(); } else { currentPlayer (currentPlayer 1) ? 2 : 1; // 换手 } repaint(); // 触发重绘棋盘更新 } }坐标反算用Math.round而不是直接整除是因为玩家点击位置不会正好落在交叉点上四舍五入能把最近的交叉点吸附过来体验会好很多。repaint()是必须的Swing 不会自动感知数组变化不调它棋盘就一直是旧的这是新手最常见的「代码没错但界面不动」问题。换手逻辑用三元表达式在 1 和 2 之间切换简单直接后面要加 AI 时在这里插入一个分支即可。3. 跑起来从零编译到第一颗棋子落下3.1 环境准备与编译命令这个项目对 JDK 版本要求不高JDK 8 及以上都能跑因为只用到 Swing 和基础集合。确认环境的第一步是看版本java -version javac -version两条命令输出的版本号要一致不一致说明 PATH 里混了多个 JDK编译能过但运行报UnsupportedClassVersionError就是这个问题。确认之后进入源码根目录按包结构编译# 在项目根目录执行-d 指定输出目录 javac -encoding UTF-8 -d out src/com/example/gomoku/**/*.java-encoding UTF-8不能省源码里有中文提示语Windows 默认 GBK 编译会乱码甚至报错。-d out把所有 class 文件按包路径输出到out目录保持包结构完整。编译完成后运行入口类java -cp out com.example.gomoku.Main-cp out告诉 JVM 去哪里找 class 文件后面跟全限定类名。如果用的是 IDE直接把src标记为源码根、把Main设为启动类即可效果一样。3.2 第一次运行的验证路径程序启动后应该看到一个固定大小的窗口棋盘线完整、没有缺角。验证顺序建议这样走先在中心附近点一颗黑子确认圆画在交叉点上而不是格子中间再点同一个位置确认不会叠第二颗然后黑白交替连成五颗确认弹出胜负提示并且棋盘重置。这三步走完说明落子、判胜、重开三条主链路都是通的。如果窗口出来了但棋盘是空白先看paintComponent有没有被正确重写方法名拼错是最隐蔽的坑paintComponet这种拼写错误编译器不会报但永远不会被调用。如果点击没反应检查addMouseListener(this)有没有在构造器里调用监听器没注册上鼠标事件根本进不来。这两处是首次运行最高频的两个卡点按这个顺序排查基本十分钟内能定位。3.3 打包成可执行 jar想脱离 IDE 直接双击运行可以打一个带清单的 jar# 生成清单文件指定入口类 echo Main-Class: com.example.gomoku.Main manifest.txt jar cfm gomoku.jar manifest.txt -C out . java -jar gomoku.jarcfm三个字母分别是 create、file、manifest 的缩写-C out .表示切换到 out 目录并把其中所有内容打进 jar。清单文件末尾必须留一个空行否则Main-Class可能不被识别这是 jar 打包里一个很玄学的细节踩过一次就记住了。4. 核心模块拆解判胜、重开与状态管理的关键实现4.1 胜负判定的边界情况判胜逻辑看着简单边界情况却不少。第一类是「刚好五颗」和「超过五颗」标准五子棋里长连六颗及以上通常也算胜但如果你的规则要求严格五颗count 5就要改成count 5这个参数决定了规则口径改之前想清楚。第二类是棋盘边缘的棋子countDirection的 while 条件里r 0 r SIZE必须同时存在只写一边就会在贴边落子时数组越界。第三类是判胜时机必须在落子成功之后、换手之前调用顺序错了会出现「赢了还让对方下一步」的诡异现象。我一般会在判胜方法里加一行防御如果grid[row][col] EMPTY直接返回 false避免在空位上误判。这类防御性检查在游戏逻辑里不嫌多因为状态一旦错乱后面所有判定都是连锁反应。4.2 重开与状态复位重开功能本质是把所有可变状态归零二维数组全部置 0、当前玩家重置为黑棋、界面重绘。常见做法是封装一个reset()方法界面层和逻辑层各调各的// Board.java public void reset() { for (int[] row : grid) { java.util.Arrays.fill(row, EMPTY); } } // BoardPanel.java private void resetGame() { board.reset(); currentPlayer 1; // 黑棋先手 repaint(); }Arrays.fill比双重循环干净注意它填的是一维数组所以外层还是要遍历行。复位时最容易漏的是「当前玩家」这个变量如果它定义在界面层而reset只清了数组重开后会出现白棋先手的错乱。状态变量放在哪一层、由谁负责复位最好在写第一行代码时就定下来后面加功能才不会互相踩。4.3 用枚举替代魔法数字初版代码里 1 和 2 这种魔法数字到处都是读起来费劲改起来危险。把玩家定义成枚举是成本最低的重构public enum Player { BLACK(1), WHITE(2); private final int code; Player(int code) { this.code code; } public int code() { return code; } public Player next() { return this BLACK ? WHITE : BLACK; } }数组里仍然存 int但业务代码里用Player.BLACK.code()和player.next()换手逻辑从三元表达式变成方法调用语义清楚一大截。这个改动不影响任何现有功能却是后面接 AI 时最省事的一步因为 AI 需要频繁判断「轮到谁」枚举比裸数字可靠得多。5. 避坑与常见问题那些跑不通时最该先看的地方5.1 界面空白或棋盘不刷新现象是窗口正常弹出但棋盘区域一片灰白或者落子后看不到新棋子。原因通常有两个paintComponent方法签名写错参数类型不是Graphics或者方法名拼错导致重写没生效或者落子后忘了调repaint()。解决方式是先确认Override注解能编译通过编译不过就说明签名不对再在落子分支末尾补上repaint()。我习惯在paintComponent第一行加一句日志确认它确实被调用了比盲猜快得多。5.2 点击坐标偏移棋子画歪现象是棋子总是偏在交叉点右下方半个格子。原因是坐标反算时用了整除而不是四舍五入或者边距MARGIN和绘制时的边距不一致。解决方式是统一用Math.round((x - MARGIN) / (float) CELL)并且把MARGIN、CELL抽到常量类里绘制和反算引用同一个常量杜绝两处各写一个数的情况。5.3 中文乱码或编译报错现象是编译时提示「编码 GBK 的不可映射字符」或者运行时弹窗里的中文变成问号。原因是源码是 UTF-8 而javac默认按平台编码读。解决方式是编译命令固定加-encoding UTF-8IDE 里把项目编码和文件编码都设成 UTF-8。这个坑在跨平台协作时尤其常见Windows 和 macOS 默认编码不同同一份代码两边表现不一样。5.4 判胜在边缘位置数组越界现象是在棋盘最右列或最下行落子后抛ArrayIndexOutOfBoundsException。原因是countDirection的循环条件只写了上界没写下界或者方向数组里行列写反。解决方式是四个方向都走一遍边界测试四个角、四条边中点各落一子确认不崩。这个测试用例建议直接写进自测清单每次改判胜逻辑都跑一遍。5.5 jar 双击没反应现象是命令行java -jar能跑双击却毫无反应。原因多半是清单文件格式不对比如Main-Class后面多了空格、末尾缺空行或者打包时 class 文件的包路径没保留。解决方式是用jar tf gomoku.jar看一眼内部结构确认com/example/gomoku/Main.class路径完整再检查清单文件。命令行能跑说明代码没问题问题一定在打包环节。6. 二次开发从双人对战到 AI 对手的改造路径想在这个骨架上继续往前走最自然的方向是加一个 AI 对手。改造点集中在两处一是把「换手」逻辑从固定切换改成「人类落子后触发 AI 计算」二是在Board里补一个评估方法。最小可行的 AI 不需要多复杂用「打分选点」就够了遍历所有空位对每个位置分别计算自己和对手在该点的连子潜力取分值最高的位置落子。// 简易 AI对每个空位做启发式打分 public int[] suggestMove(int aiPlayer, int humanPlayer) { int bestScore -1, bestRow -1, bestCol -1; for (int r 0; r SIZE; r) { for (int c 0; c SIZE; c) { if (grid[r][c] ! EMPTY) continue; int attack lineScore(r, c, aiPlayer); // 自己连子潜力 int defend lineScore(r, c, humanPlayer); // 堵对手的权重 int score attack * 2 defend; // 攻守权重可调 if (score bestScore) { bestScore score; bestRow r; bestCol c; } } } return new int[]{bestRow, bestCol}; }lineScore可以复用判胜里的方向遍历统计某点四个方向上同色棋子的连续长度并映射成分值比如活三给高分、冲四给更高分。attack * 2 defend里的系数就是攻守倾向调大攻击系数 AI 更激进调大防守系数更保守这个参数值得反复试。改造时注意 AI 计算要放在落子成功之后、界面重绘之前并且用SwingUtilities.invokeLater包一层避免在事件线程里做重计算导致界面卡顿。验证改造是否成功我一般用固定棋谱回归摆几个已知的「活三必堵」「冲四必应」局面看 AI 是否做出正确选择再跑一遍完整对局确认不会自己堵自己。从那以后我每次给这类小项目加功能都强制先写三个可复现的测试局面再动代码不然改完看着能跑实际规则已经悄悄错了。希望这套拆解能帮你把这个五子棋骨架真正跑起来、改下去。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。