ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java五子棋毕业设计:核心数据结构、判胜算法与AI权重评估全解析

Java五子棋毕业设计:核心数据结构、判胜算法与AI权重评估全解析 简介基于Java实现的五子棋游戏毕业设计项目主要面向计算机相关专业学生以及正在准备课程设计、毕业设计的Java开发者。项目通过完整可运行的代码展示了双人对战与网络联机机制包含棋盘绘制、落子交互、胜负判定、消息传递、多客户端接入等核心模块涉及Swing图形界面、鼠标事件、套接字编程、多线程处理等关键技能能够帮助学习者将面向对象设计思想与实战编码结合起来。压缩包中共有32个文件以7个Java源文件与19个class编译文件为主体另配3个bat启动脚本和2份doc加1份docx文档涵盖开题报告与设计论文资源整体仅1.06MB轻量但五脏俱全。目前已有2854人学习下载说明它对毕业设计选题有较高的参考价值。使用时可先阅读文档明确整体设计再运行脚本启动服务器与客户端体验对战流程最后对照源码理解网络通信与逻辑实现是一份少见的从界面、通信到文档一应俱全的实训资料。1. 基于 java 的五子棋游戏毕业设计真正该花力气的地方在哪很多同学拿到这个题目第一反应是“先把棋盘画出来”然后在一个星期里把按钮、鼠标监听、悔棋全堆上去最后发现工作量看起来很大但答辩时老师一句“交换双发规则怎么判”“AI 是怎么评估局势的”就卡住了。实际上基于 java 的五子棋游戏核心从来不是绘图 API而是三件事数据结构怎么设计、胜负判定怎么做到高效且不出错、AI 落子逻辑有没有一个能讲清楚的思路。源代码能跑只是及格线论文里能写清楚“为什么这么设计”才是拿高分的关键。这篇博文就从这三条主线展开把棋盘模型、判定算法、AI 权重评估、界面事件分发、打包发布从头到尾捋一遍直接用可复现的代码和参数说明带你走完整套方案适合用 Java Swing 或 JavaFX 做课程设计、毕设以及准备 java 面试八股文时拿“五子棋判胜算法”当实战案例的读者。2. 棋盘数据模型一局的胜负从你选的数据结构开始2.1 为什么用二维数组而不是用坐标集合常见做法是用一个int[15][15]二维数组表示 15 路棋盘0 表示空1 表示黑子2 表示白子。这个选择有两个直接理由第一五子棋的棋盘尺寸固定二维数组的随机访问是 O(1)判胜时需要频繁按坐标取棋子颜色用ListPoint存落子再遍历判断效率差且代码绕第二Swing 的JButton[][]或者Canvas绘图都天然按行列索引数组下标和界面坐标直接对应省去一层映射。但二维数组有一个隐藏缺点你无法快速知道某一方当前活三、冲四的数量而 AI 评估需要这些特征。所以我的建议是“数组存状态列表存落子历史”再加一个哈希表辅助缓存。用一个LinkedListMove history记录每一步悔棋时从数组清除该位置同时从历史弹出这样简单直接。public class Board { 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 final int[][] grid new int[SIZE][SIZE]; private final ListMove history new LinkedList(); public boolean place(int row, int col, int player) { if (row 0 || row SIZE || col 0 || col SIZE || grid[row][col] ! EMPTY) { return false; } grid[row][col] player; history.add(new Move(row, col, player)); return true; } public boolean undo() { if (history.isEmpty()) return false; Move last history.removeLast(); grid[last.row][last.col] EMPTY; return true; } public int get(int row, int col) { return grid[row][col]; } }这段代码里place返回boolean而不是直接 void是为了让 UI 层知道“这次点击是否合法”非法落子越界或已有子直接不刷新界面。history用LinkedList是因为悔棋只操作尾部插入删除都在 O(1)。不用Stack是因为 Java 官方已建议用Deque替代但LinkedList在源码阅读上更直观答辩时也更好讲。2.2 坐标体系到底用行号列号还是像素坐标这是 Swing 版本最容易踩的坑。鼠标监听器返回的是像素坐标e.getX()和e.getY()你必须在面板的paintComponent里先定义棋盘起点和格子间距然后在监听器里做反向换算。常见做法是设置棋盘左边距MARGIN 30格子间距CELL_SIZE 36那么行列计算为int row (int) Math.round((e.getY() - MARGIN) / (double) CELL_SIZE); int col (int) Math.round((e.getX() - MARGIN) / (double) CELL_SIZE);这里有一个精度问题Math.round之前必须用double除法如果你写成(e.getY() - MARGIN) / CELL_SIZE两个 int 相除会截断取整靠近格子边缘的点击会被判定到错误的格子。画棋子时则反过来中心点是MARGIN col * CELL_SIZE半径取CELL_SIZE / 2 - 4留出边距这样视觉效果不会粘连。还有一点值得注意判胜时数组是grid[row][col]绘图时是grid[row][col]两者必须用同一套 row/col不要在绘图时又按像素算一次。否则会出现“界面看起来下在这里实际记录在另一格”的错位问题这也是毕业设计答辩中老师常挑的实现瑕疵。2.3 用enum还是int表示棋子颜色很多源码用1和2两个魔数甚至连空位用-1。我建议用public static final int常量或者 Java 5 之后的enum。为什么因为五子棋逻辑里到处是if (grid[r][c] player)这种判断用魔数你写论文时很难措辞而且一旦把黑子从 1 改成 2所有地方都要跟着改。enum还有一个额外好处可以挂属性。public enum Player { BLACK(1, Color.BLACK), WHITE(2, Color.WHITE); public final int value; public final Color uiColor; Player(int value, Color uiColor) { this.value value; this.uiColor uiColor; } }把 UI 颜色直接挂在枚举里Swing 绘图时player.uiColor拿来就用不用在绘制方法里写if (player BLACK) g.setColor(Color.BLACK)这种重复分支。如果你的论文里要画类图这个枚举也能作为数据模型的一部分体现面向对象设计而不是把所有东西都塞进一个GamePanel类。3. 胜负判定连五检测的四种写法与时间复杂度对比3.1 最直观的连五扫描八方向四条线五子棋判胜的输入是“落子位置 (row, col)”不需要全局遍历棋盘。理由很简单一局棋的胜负只可能由刚落下这颗子造成所以只需要检查经过该点的横、竖、左斜、右斜四个方向。每个方向从落子点出发向两侧延伸数同色棋子连续个数达到 5 就赢。public static boolean isWin(int[][] grid, int row, int col) { int[][] directions {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; int player grid[row][col]; for (int[] dir : directions) { int count 1; int dr dir[0], dc dir[1]; for (int i 1; i 5; i) { int r row dr * i, c col dc * i; if (inBounds(r, c) grid[r][c] player) count; else break; } for (int i 1; i 5; i) { int r row - dr * i, c col - dc * i; if (inBounds(r, c) grid[r][c] player) count; else break; } if (count 5) return true; } return false; }这段代码的关键在于directions只定义四个基向量反向靠-i实现而不是单独写八个方向。另一个细节是循环边界写成i 5而不是i grid.length——因为一旦某个方向连续同色数达到 5就已经赢了不需要再往前数如果四个方向都没到 5那超过 5 的距离也不可能是连五。时间复杂度最坏 O(4×4)常数级。但是这里有一个规则歧义白棋连了六个算不算赢上面的代码按count 5处理也就是长连也算胜。传统无禁手规则下这个判定没问题但如果你论文里写的是“实现民间无禁手规则”要在论文里明确说明这一点。如果做的是有禁手版本黑棋三三禁手、四四禁手、长连禁手判定逻辑要单独写一组不能复用这个连五判断。3.2 更“高级”的判胜写法滑动窗口全局扫描有些毕设论文会写“使用滑动窗口算法判定棋盘状态”实现方式是按行、按列、按对角线扫描维护一个长度 5 的计数窗口。这种做法在 15×15 小棋盘上并没有性能优势但它有一个独特价值你可以在一次扫描里同时统计活三、冲四、眠三等特征这些数据恰好是第 4 章 AI 评估要用的。所以我的建议是双轨制落子后的即时判胜用上面 3.1 的局部检测因为它的语义是“这一步是否结束比赛”而 AI 决策时需要的是“整个棋盘当前谁占优”这时用全局扫描生成特征统计。两者用途不同不要混在一个方法里。public static MapString, Integer evaluateBoard(int[][] grid) { MapString, Integer features new HashMap(); features.put(BLACK_LIVE_THREE, 0); features.put(WHITE_LIVE_THREE, 0); // 按行扫描统计三连且两端为空——活三 for (int r 0; r grid.length; r) { for (int c 0; c grid.length - 3; c) { if (grid[r][c] ! 0 grid[r][c] grid[r][c1] grid[r][c] grid[r][c2] c-1 0 grid[r][c-1] 0 c3 grid.length grid[r][c3] 0) { String key (grid[r][c] 1) ? BLACK_LIVE_THREE : WHITE_LIVE_THREE; features.put(key, features.get(key) 1); } } } return features; }这段代码只展示了按行统计活三实际完整实现还要写按列、按两条对角线共计四个方向的相同逻辑。参数c grid.length - 3是为了防止c2越界而两端开空检查c-1和c3时必须先判断索引是否在棋盘内否则ArrayIndexOutOfBoundsException会直接打断评估。这种全局扫描的时间复杂度是 O(4×N²)N15 时毫无压力。3.3 判胜之后赢家弹窗与对局重置的边界处理判胜后不要直接JOptionPane.showMessageDialog然后什么都不管后面还要考虑“再来一局”时棋盘状态完全归零。很多源代码在重置时只清了grid忘了清history结果点悔棋按钮把上一局的子又放回来了。重置方法要同时清空数组和历史记录public void reset() { for (int[] row : grid) Arrays.fill(row, EMPTY); history.clear(); currentPlayer Player.BLACK; gameOver false; }一个容易被忽略的细节history.clear()和Arrays.fill要在同一线程执行Swing 的事件分发线程EDT内点按钮触发 reset 没问题但如果你额外开了定时器或网络对战线程这里会出现竞态。毕设阶段不会做多线程但你论文的“系统设计”章节如果画了线程模型图就要考虑到这处共享资源的同步问题。4. AI 落子逻辑从随机落子到权重评估的实现路径4.1 评估函数怎么把“局势”变成可计算的数字人机对战是五子棋毕设拉开差距的地方。最简单的 AI 是“空位随机落子”代码只有三行但论文没有什么可写。往上一个台阶是“攻击型权重评估”对棋盘每个空位分别计算黑棋和白棋如果在这个位置落子后形成的棋型得分取两者之和作为该位置的总分最后选分最高的点落子。棋型打分表建议直接写成常量数组不要写一堆 if-else。常见的分数设定五连 100000活四 50000冲四 10000活三 8000眠三 1000活二 800眠二 100。这个分值表不是唯一的但你要保证“五连 活四 活三”的大小关系否则 AI 可能为了一个冲四而放弃已经赢定的棋。用代码表示private static final int FIVE 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 1000; private static final int LIVE_TWO 800; private static final int SLEEP_TWO 100;落点评估的核心思路对每个空位(r, c)先模拟黑棋落在这里沿四个方向统计棋型得分attackScore再模拟白棋落在这里得到defendScore总积分attackScore defendScore * 1.2。防守分乘以 1.2 是因为后手方需要更激进地堵截先手这种“防守优先半格”的经验值是老棋手常用的调参起点你也可以调成 1.0 或 1.5 对比效果并在论文里给出你的测试结论。4.2 棋型识别从“数连续同色”到“数两端状态”单纯数连子数量是不够的OOO_和_OOO_对局势的判断完全不同。前者是眠三下一步对方堵住一端就废了后者是活三两端都能成四对手无法同时封住。所以棋型识别必须看“连子串 两端状态”。这里介绍一个通用方法沿某个方向从落子位置向两端扩展收集同色连续段然后记录左右两侧第一格是空、异色还是越界。public static Pattern scanPattern(int[][] grid, int row, int col, int dr, int dc, int player) { int count 1; int leftOpen 0, rightOpen 0; int r row dr, c col dc; while (inBounds(r, c) grid[r][c] player) { count; r dr; c dc; } if (inBounds(r, c) grid[r][c] EMPTY) rightOpen 1; r row - dr; c col - dc; while (inBounds(r, c) grid[r][c] player) { count; r - dr; c - dc; } if (inBounds(r, c) grid[r][c] EMPTY) leftOpen 1; return new Pattern(count, leftOpen, rightOpen); }Pattern类里包含三个字段count连子数、leftOpen和rightOpen是否端空。后续查表逻辑可以写成count 5返回FIVEcount 4 leftOpen 1 rightOpen 1返回LIVE_FOURcount 4且只有一端开返回RUSH_FOUR。这里有一层关键设计计分时要把连子段连带“假如落子后产生的延伸”一起算不是只看当前棋盘上已有的子。4.3 搜索深度一步贪心够用两步要小心“赢了不杀”的问题纯贪心 AI 有一个经典死穴它只会评估一步之后的最大分数但可能出现“我下一步冲四对方必须堵然后我第三步活三必胜”的组合贪心看不见第二步所以可能在第一步选择了价值稍低的活三而错过了直接冲四定胜。如果你写论文想突出一点可以加一个两层搜索我方落子后遍历对方所有可能落点把对方最高分作为我这步的“风险系数”最终得分 我的攻击分 - 对方反应分。public Move bestMove(int[][] grid, int player) { Move best null; int bestScore Integer.MIN_VALUE; for (int r 0; r SIZE; r) { for (int c 0; c SIZE; c) { if (grid[r][c] ! EMPTY) continue; grid[r][c] player; int attack evaluatePoint(grid, r, c, player); int opponentThreat findOpponentBestResponse(grid, r, c, 3 - player); grid[r][c] EMPTY; int total attack (int)(opponentThreat * 0.8); if (total bestScore) { bestScore total; best new Move(r, c, player); } } } return best; }这段代码的成本是 O(225×225)因为是 15×15 棋盘每个空位都遍历对方所有空位约 5 万次棋型扫描Swing 单线程下可能会卡 100300 毫秒。缓解办法有三个只搜索以已有棋子为中心的距离 2 以内的空位这种候选集通常只有几十个点加一个提前终止条件如果发现某一步已经形成五连直接返回或者用“逐步扩大搜索范围”的方式第一次只搜紧邻棋子的一层空位没有好棋再扩大。这些细节非常适合写进论文的“系统优化”章节。4.4 先手后手难度调整让 AI 可变强变弱毕设答辩时老师会问“你这个 AI 难度能不能调”所以建议在界面上放一个“AI 难度”下拉框而不是固定死。难度调整不需要改评估函数只需要在候选点选择上加一个扰动简单模式有 30% 概率避开最高分前三名普通模式在最高分前 5 个候选里随机选困难模式总是取最高分。这个方案比你重新写一个低深度搜索更直观代码量只需要一个Random和一个判断分支。难度分档要注意一个边界困难模式如果总是取最高分在缺少多层搜索时会显得很蠢因为它会死攻一个方向不防守。所以更合适的设置是困难模式同时看攻击分和防守分也就是 4.1 节里的attackScore defendScore * 1.2普通模式把防守系数降到 0.8。这样不必增加任何算法复杂度AI 风格差异就出来了。5. Swing 界面与交互事件棋盘绘制、鼠标落子与状态联动5.1 JPanel 自绘棋盘还是 JButton 网格选哪个更适合毕设网上大量源代码用JButton[][]做 225 个按钮每个按钮有背景色和图标。这个方案优点是点击事件天然绑定坐标但缺点也很明显绘制棋盘的横竖线时要用setBorder或者自定义ButtonUI视觉上很难做到格子线连续同时 225 个按钮在布局管理器里重绘开销也不小。更推荐的做法是继承一个JPanel在paintComponent里用Graphics2D画棋盘线用鼠标点击坐标换算行列。后者的绘制逻辑更接近“游戏”本身论文里也能配效果图说明棋盘渲染管线。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 * CELL_SIZE, MARGIN (SIZE - 1) * CELL_SIZE, MARGIN i * CELL_SIZE); g2.drawLine(MARGIN i * CELL_SIZE, MARGIN, MARGIN i * CELL_SIZE, MARGIN (SIZE - 1) * CELL_SIZE); } for (int row 0; row SIZE; row) { for (int col 0; col SIZE; col) { int piece board.get(row, col); if (piece ! Board.EMPTY) { g2.setColor(piece Board.BLACK ? Color.BLACK : Color.WHITE); g2.fillOval(MARGIN col * CELL_SIZE - PIECE_RADIUS, MARGIN row * CELL_SIZE - PIECE_RADIUS, PIECE_RADIUS * 2, PIECE_RADIUS * 2); } } } }绘制棋盘线时注意横线宽度是(SIZE - 1) * CELL_SIZE而不是SIZE * CELL_SIZE。因为 15 条线跨越 14 个格子间距如果把棋子画在交叉点上坐标MARGIN i * CELL_SIZE其中i从 0 到 14这是网格线的位置而不是格子的中心。棋子中心点用MARGIN col * CELL_SIZE也就是交叉点坐标和前面鼠标监听的换算公式一致。5.2 鼠标事件监听器里只改数据重绘交给 repaint()很多新手会在mousePressed里直接写绘制代码这是错误的。Swing 的绘制模型是“数据变化 - 调repaint()- 系统统一回调paintComponent”。在监听器里手动画图会因为重绘时机不确定而被系统清掉出现棋子闪烁或消失。正确的流程是先判断是否轮到你落子合法则调用board.place()然后repaint()触发重绘再调判胜方法。public void mousePressed(MouseEvent e) { if (board.isGameOver()) return; int row (int) Math.round((e.getY() - MARGIN) / (double) CELL_SIZE); int col (int) Math.round((e.getX() - MARGIN) / (double) CELL_SIZE); if (!board.place(row, col, currentPlayer)) return; repaint(); if (WinChecker.isWin(board.getGrid(), row, col)) { board.setGameOver(true); JOptionPane.showMessageDialog(this, currentPlayer.getLabel() 胜出); return; } switchPlayer(); if (gameMode PlayerMode.AI currentPlayer aiPlayer) { aiMove(); } }repaint()不是立即重绘它只是给 Swing 的事件分发线程发一个重绘请求但这个延迟对人眼不可感知。另一个关键点是switchPlayer()和aiMove()不要放在paintComponent里否则 AI 下棋会触发新一轮重绘造成递归。你可以在aiMove()内部计算好 AI 的落子并直接更新 board再调一次repaint()不要在紧邻的循环里连续落子。5.3 游戏状态机用枚举管住“等待落子 / AI思考 / 已结束”五子棋界面的状态切换比想象中容易出错。比如玩家落子后AI 还没来得及下你又点了棋盘这时候应该忽略输入再比如游戏已经结束点击棋盘不应该弹任何东西。一个干净的做法是引入GameState枚举所有事件处理入口先检查状态。public enum GameState { PLAYER_TURN, AI_TURN, GAME_OVER }AI_TURN状态时鼠标监听器第一行就return不需要额外的布尔开关。AI 计算可以放在后台线程SwingWorker里计算完成后在done()方法中更新 UI避免界面在大棋盘搜索时冻结。但毕设为了简化也可以不做多线程直接把aiMove()放在 MouseListener 里同步执行——当你只有一层贪心搜索时计算耗时低于 100ms用户感知不明显。如果论文里想写“引入 SwingWorker 提升响应性”代码量也就多十几行性价比很高。5.4 悔棋、重新开始和落子记录三种操作的联动悔棋在双人对战里很简单board.undo()一次换回上一个玩家repaint()。但在人机对战里AI 刚刚落子后你点悔棋应该一次悔两步棋你的和 AI 的否则你悔棋后 AI 的棋子还在棋盘上你又落在原位附近产生逻辑混乱。判断方式是看当前currentPlayer如果是玩家回合说明 AI 已经走了一步需要undo两次如果是 AI 回合说明刚才是玩家落的子AI 还没来得及走只需要undo一次。重新开始按钮需要重置棋盘数组、落子历史、当前玩家、游戏状态。还要考虑一个问题如果 AI 难度选了“先手”重新开始后应该黑棋直接走第一步。这些状态全部合并在resetGame(boolean aiFirst)里避免在按钮监听器里散落好几段初始化代码。落子记录功能可以放一个JTextArea每次落子追加一行“黑棋 (7, 7)”悔棋时从文本尾部删除对应行这个功能虽小但很能说明你的工程意识。6. 打包发布与论文写作从能运行到能答辩的关键一步6.1 用jar命令打出带主类的可执行包很多人把自己写的程序在 IDE 里跑通就完事了但毕业设计需要交“源代码 可执行文件”。常见做法是打包成可运行的 JAR 文件。如果是纯 JDK 环境没有引入第三方依赖用jar命令就能完成如果引入了 Maven 依赖用mvn package生成带依赖的 fat-jar。这里给出一条完整命令流程cd 项目根目录 mkdir -p out/production javac -encoding UTF-8 -d out/production src/main/java/com/example/gobang/*.java echo Main-Class: com.example.gobang.Main manifest.txt jar cfm gobang.jar manifest.txt -C out/production . java -jar gobang.jarMain-Class指定的类必须包含public static void main(String[] args)方法且类所在的包路径要写全。-C out/production .的意思是切换目录到编译输出目录把整个包结构打进 JAR。这里最容易犯的错是没有先进入out/production就执行jar结果包结构里多了一层目录运行时提示找不到主类。另外-encoding UTF-8是为了你的源码注释里包含中文时不乱码Windows 命令行默认编码可能是 GBK不加这个参数 JUnit 或编译阶段会报编码错误。如果你的界面图标、音效文件放在resources目录记住通过getClass().getResource(/images/icon.png)获取而不是new File(src/resources/icon.png)。后者在 IDE 里能跑但 JAR 包内文件系统路径不同会直接找不到资源。毕设答辩时如果现场换了一台电脑双击 JAR 崩溃大概率就是这个原因。6.2 论文结构建议把“五子棋解题框架”做成图表不要罗列代码基于标题后缀“源代码论文”论文才是重要载体。我的建议是论文不要写“系统实现”章节里大段贴代码截图而是画三张图系统模块结构图、判胜算法流程图、AI 落子时序图。三张图比三千字代码更能体现设计能力。文字部分重点是 2.1 节的数组选型、4.1 节的评估函数打分表、4.3 节的候选点剪枝策略。尤其是“为什么给防守分乘一个 1.2 系数”这个点答辩老师一定会问你要说明这个系数是实验得出的最优值而不是拍脑袋写的。6.3 答辩高频提问的预演清单答辩提问通常围绕判胜算法是局部判断还是全局判断AI 能不能保证走出必胜棋为什么选 15×15 而不是 19×19棋子坐标换算发生浮点误差怎么修正悔棋时历史记录和界面如何保持一致。这里我建议你也要准备好一个“加速功能”即按空格键显示当前局面的 AI 推荐落点这个功能几乎无需改动原有 AI 逻辑只是在evaluatePoint高分位置画一个半透明圆圈标记代码量不多但现场演示效果很好。一个很现实的问题很多人用了现成源码但不知道源码里 AI 的缺陷在哪。我建议你主动跑三个测试用例单方向连续五个子判胜先手活三对方是否堵两头AI 面对对方双活三时会不会拆掉一个。这三个用例的结果直接决定你答辩时敢不敢打开自己的项目演示。如果第三个用例 AI 失败优先调整 4.1 节防守系数或增加 4.3 节的两层搜索不要通过把 AI 难度调低来回避问题因为老师会让你当场切回困难模式验证。本文还有配套的精品资源点击获取
返回列表