资讯动态

Java五子棋对战系统源码详解:AI算法与Socket网络对战实现

发布时间:2026/10/14 15:37:47 来源:尧图企业网站定制
简介一份基于Java实现的五子棋对战系统课程设计源码适合需要完成课设或希望借助完整项目进阶Java开发的读者覆盖棋盘绘制、落子交互、胜负判断等核心功能。资源包共290个文件约18.26MB以265个GIF图像和17个Java源文件为主GIF用于棋盘棋子及动画展示Java源文件实现游戏初始化、玩家操作处理与胜负判断另有XML配置界面布局、WAV提供音效iml与readme便于导入项目并快速上手。资源内还包含JPG封面与Git忽略文件方便项目演示与版本管理。目前已有283人学习下载。整套源码模块层次清晰既展现单机对战完整逻辑也提供了可扩展的界面组织方式对照源码可深入理解事件监听、状态管理、资源加载等工程化写法同时借助图片、音频与配置文件的整合方式能快速搭建一个可演示、可拓展的五子棋程序是课程设计答辩与Java实战练习的实用参考。1. 基于Java五子棋对战系统的设计实现源码从课程设计到完整对战项目差的不是代码量基于Java五子棋对战系统的设计实现源码这类标题在课程设计和毕业设计选题里反复出现但同一个标题下大家交出来的东西质量能差出好几档。有人交单机双人版几百行数组加一个Swing面板就完事有人交的是带AI、带局域网对战、带悔棋复盘的三层结构光网络模块就上千行。两者代码量差十倍面试官追问的技术点也完全不同。五子棋这个题材好在它足够小却把Java桌面开发最常见的几块内容全串起来了棋盘数据结构与胜负判定算法、图形界面的事件模型、AI的评估与搜索、Socket网络通信与多线程。这篇文章就顺着标题往下拆每一块怎么设计、代码怎么写、参数怎么调、哪里最容易翻车一次讲清楚。2. 五子棋核心模型棋盘数据结构、悔棋栈与四方向胜负判定2.1 棋盘数据结构选型二维数组是默认解BitSet什么时候才值得用15行15列225个交叉点。这个规模决定了数据结构的选择空间非常大但我一般建议默认就用二维数组别为了炫技去上复杂结构。二维数组直观、调试方便getStone(x, y) 的坐标与棋盘坐标完全对应打印棋盘也简单。public class Board { private static final int SIZE 15; private final byte[][] grid new byte[SIZE][SIZE]; // 0 表示空位1 表示黑子2 表示白子 public synchronized boolean placeStone(int x, int y, int player) { if (!inBound(x, y) || grid[x][y] ! 0) { return false; } grid[x][y] (byte) player; return true; } public byte getStone(int x, int y) { return grid[x][y]; } public void clearStone(int x, int y) { grid[x][y] 0; } private boolean inBound(int x, int y) { return x 0 x SIZE y 0 y SIZE; } }这段代码里有几个参数选择值得说。player 传 1 或 2而不是 0因为 0 被保留给空位这样 int 转 byte 时不会产生歧义。placeStone 加了 synchronized单机版里没感觉一旦后面接上网络对战、AI 线程同时读写棋盘少了它就会出现两个线程同时落子到同一个点的竞态这个坑我见过不少属于血泪经验。选型上二维数组的优点是直观缺点仅是占 225 字节内存毫无压力。那为什么仍有人用 BitSet常见理由是序列化传输数据量小以及位运算做模式匹配快——用 long 的位移和掩码可以一次性比较 8 个位置比循环读数组快。但代价是坐标换算那层很容易写错x * 15 y 的位索引稍有疏忽就越界。对这套系统来说胜负判定和 AI 评估都只扫局部区域二维数组的开销完全可以忽略。我的结论是二维数组是默认解BitSet 是加分项别为了它牺牲可读性。2.2 悔棋与复盘为什么必须用状态栈而不是只记最后一步悔棋看起来简单实际上它决定了对局状态怎么设计。如果只在棋盘里记一个 lastMove那只能悔一步连续悔棋、网络对战里对方悔棋、以及后续做复盘回放都需要完整的状态栈。public class Move { public final int x; public final int y; public final int player; public Move(int x, int y, int player) { this.x x; this.y y; this.player player; } } public class GameState { private final Board board new Board(); private final DequeMove history new ArrayDeque(); private int currentPlayer 1; public boolean move(int x, int y) { if (!board.placeStone(x, y, currentPlayer)) { return false; } history.push(new Move(x, y, currentPlayer)); currentPlayer 3 - currentPlayer; // 1 和 2 之间切换 return true; } public boolean undo() { Move last history.pollFirst(); if (last null) { return false; } board.clearStone(last.x, last.y); currentPlayer last.player; // 把行棋权还给刚才悔掉的那一手 return true; } }两个关键细节。第一是 currentPlayer 3 - currentPlayer这个写法依赖玩家编号是 1 和 2如果你改用 0 和 1就得换成异或。第二是 undo 时 currentPlayer last.player意思是要把出棋权还给悔棋的人这个细节经常被漏掉漏掉的结果是悔棋后轮到对手下逻辑全乱。栈结构还有一个隐藏好处做跳转到任意一手的复盘时直接从 history 重建局面即可。设计上把 history 独立成 GameState 的对象而不是塞进 Board这样网络模块和 AI 模块只依赖 GameState不碰底层棋盘细节后面的扩展会顺畅很多。2.3 胜负判定四方向扫描的实现与两个边界坑胜负判定是这套系统里最容易被测出来有问题的函数。正常对局触发判负的频率低很多人写完就再没验证过边界。public boolean checkWin(int x, int y) { byte p board.getStone(x, y); if (p 0) { return false; } // 四个方向向量水平、垂直、主对角线、副对角线 int[][] dirs { {1, 0}, {0, 1}, {1, 1}, {1, -1} }; for (int[] d : dirs) { int count 1; count countDir(x, y, d[0], d[1], p); count countDir(x, y, -d[0], -d[1], p); if (count 5) { return true; } } return false; } private int countDir(int x, int y, int dx, int dy, byte p) { int count 0; int nx x dx; int ny y dy; while (nx 0 nx 15 ny 0 ny 15 board.getStone(nx, ny) p) { count; nx dx; ny dy; } return count; }这里有两个边界坑必须讲。第一个是方向向量只定义四个而不是八个因为每个方向要往两端扫正向加反向各统计一次才是一个完整方向如果定义八个方向又只往单侧扫统计会重复。第二个坑是 count 5 而不是 count 5标准五子棋规则里长连六子以上连续在很多民间规则下也算赢用 5 的话用户连成六子时会弹未分胜负体验非常诡异。只有做禁手规则时才需要精确判断恰为 5。性能上这个判定只在每次落子后沿四方向扫描最坏情况每个方向扫 14 格几十次数组访问完全不用优化。真正需要优化的是下一章要说的 AI 评估函数那里才是性能黑洞。提示checkWin 只扫描当前落子的四条线不要写全盘扫描那样会把每次落子的判定复杂度从 O(n) 抬到 O(n²)对 15 路棋盘虽然影响不大但属于一开始就该避免的坏习惯。3. 把源码跑起来Maven工程、Swing界面与自带AI的搭建路线3.1 Maven工程结构与启动入口我一般建议用 Maven 而不是直接把 .java 拖进 IDE因为后续要引单元测试依赖、要打可执行 jarMaven 让这些都是标准操作。目录结构如下gomoku/ ├── pom.xml └── src/main/java/com/example/gomoku/ ├── App.java ├── model/Board.java ├── model/Move.java ├── model/GameState.java ├── ui/BoardPanel.java ├── ai/AiEngine.java └── network/ ├── Protocol.java ├── GameServer.java └── GameClient.javapom.xml 里只需要一个单元测试依赖Swing 是 JDK 自带的不需要第三方 GUI 库project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdgomoku/artifactId version1.0-SNAPSHOT/version properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target /properties dependencies dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version5.9.2/version scopetest/scope /dependency /dependencies /project启动入口放在 App 里注意把 Swing 初始化丢进事件调度线程public class App { public static void main(String[] args) { SwingUtilities.invokeLater(() - { GameState state new GameState(); BoardPanel panel new BoardPanel(state); JFrame frame new JFrame(Java五子棋对战系统); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(680, 700); frame.add(panel); frame.setVisible(true); }); } }SwingUtilities.invokeLater 不是玄学它的作用是保证所有 UI 操作都在事件分发线程EDT上执行。不这样做程序大概率也能跑但一旦 AI 计算或网络消息在别的线程里触发 repaint界面就会出现偶发闪烁甚至卡死这类问题定位起来相当费劲。3.2 Swing棋盘绘制与鼠标交互BoardPanel 继承 JPanel重写 paintComponent 画网格和棋子鼠标点击时把像素坐标换算成棋盘坐标再交给 GameState。坐标换算是整个 UI 层最值得写的部分public class BoardPanel extends JPanel { private static final int CELL_SIZE 40; private static final int MARGIN 30; private static final int BOARD_SIZE 15; private final GameState gameState; public BoardPanel(GameState gameState) { this.gameState gameState; addMouseListener(new MouseAdapter() { Override public void mouseClicked(MouseEvent e) { int x Math.round((e.getX() - MARGIN) / (float) CELL_SIZE); int y Math.round((e.getY() - MARGIN) / (float) CELL_SIZE); if (x 0 x BOARD_SIZE y 0 y BOARD_SIZE) { if (gameState.move(x, y)) { repaint(); } } } }); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(new Color(210, 180, 140)); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(Color.BLACK); for (int i 0; i BOARD_SIZE; i) { g.drawLine(MARGIN, MARGIN i * CELL_SIZE, MARGIN (BOARD_SIZE - 1) * CELL_SIZE, MARGIN i * CELL_SIZE); g.drawLine(MARGIN i * CELL_SIZE, MARGIN, MARGIN i * CELL_SIZE, MARGIN (BOARD_SIZE - 1) * CELL_SIZE); } for (int x 0; x BOARD_SIZE; x) { for (int y 0; y BOARD_SIZE; y) { int stone gameState.getStoneAt(x, y); if (stone 0) { continue; } int cx MARGIN x * CELL_SIZE; int cy MARGIN y * CELL_SIZE; g.setColor(stone 1 ? Color.BLACK : Color.WHITE); g.fillOval(cx - CELL_SIZE / 2 2, cy - CELL_SIZE / 2 2, CELL_SIZE - 4, CELL_SIZE - 4); g.setColor(Color.GRAY); g.drawOval(cx - CELL_SIZE / 2 2, cy - CELL_SIZE / 2 2, CELL_SIZE - 4, CELL_SIZE - 4); } } } }Math.round((e.getX() - MARGIN) / (float) CELL_SIZE) 这里必须除以浮点数。如果直接除以整数 40鼠标落在格子中线左侧一点时会被截断到前一个坐标落子位置整体偏移半格这是 UI 层最常见的坐标坑。参数上CELL_SIZE 调成 4015 路棋盘加边距后窗口约 660 像素宽适配大多数笔记本屏幕如果要支持缩放就把这些值统一定义成常量别散落在代码里。3.3 简单AI评估函数与候选点剪枝入门版 AI 不需要搜索整棵博弈树用一个评估函数给每个候选点打分即可。思路是在某个空位假设下了 AI 的棋子然后统计这个点四个方向上能形成的连子模式数量按权重累计。public class AiEngine { // 权重成五 活四 活三 活二 private static final int SCORE_FIVE 100000; private static final int SCORE_LIVE_FOUR 10000; private static final int SCORE_LIVE_THREE 5000; private static final int SCORE_LIVE_TWO 1000; public int[] nextMove(GameState state, int aiPlayer) { int bestScore Integer.MIN_VALUE; int[] bestMove null; for (int x 0; x 15; x) { for (int y 0; y 15; y) { if (state.getStoneAt(x, y) ! 0 || !hasNeighbor(state, x, y)) { continue; } int score evaluate(state, x, y, aiPlayer); score evaluate(state, x, y, 3 - aiPlayer) / 2; // 防守权重减半 if (score bestScore) { bestScore score; bestMove new int[]{x, y}; } } } return bestMove; } private boolean hasNeighbor(GameState state, int x, int y) { for (int dx -2; dx 2; dx) { for (int dy -2; dy 2; dy) { if (dx 0 dy 0) { continue; } int nx x dx; int ny y dy; if (nx 0 nx 15 ny 0 ny 15 state.getStoneAt(nx, ny) ! 0) { return true; } } } return false; } private int evaluate(GameState state, int x, int y, int player) { int score 0; int[][] dirs {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (int[] d : dirs) { int count 1; count countDir(state, x, y, d[0], d[1], player); count countDir(state, x, y, -d[0], -d[1], player); boolean openLeft isOpen(state, x - d[0], y - d[1], player); boolean openRight isOpen(state, x d[0], y d[1], player); score patternWeight(count, openLeft, openRight); } return score; } }这个评估函数有个明显局限只看了这一手棋形成的局面没考虑对手下一手会怎么应对所以挡不住对手的连续进攻。第 6 章会把它嵌进极小化极大搜索里同样的评估逻辑能发挥完全不同的棋力。参数上防守权重 /2 可以调调成 /3 时 AI 更激进调成 1 时更偏防守看你想让 AI 呈现什么风格这个值就是你控制棋风最直接的旋钮。4. 网络对战怎么接Socket文本协议与服务端线程模型4.1 协议设计为什么要用文本行而不是对象流网络对战有两种常见做法中心服务器模式一台服务器同时接两个客户端服务器只负责转发或者点对点模式一方监听端口另一方主动连接。对课程设计和中小型项目中心服务器模式更好写断线重连、观战、战绩统计这些扩展都挂在服务器上。协议层我强烈建议用文本行协议而不是 Java 对象流。原因有两个对象流要求两端类版本完全一致改一个字段就全崩而粘包半包问题在文本行协议里好处理得多。踩坑踩多了你会发现网络模块里越简单的东西越可靠。public class Protocol { // 统一格式TYPE|payload以换行符结尾 public static final String HELLO HELLO; public static final String MOVE MOVE; public static final String UNDO UNDO; public static final String RESET RESET; public static final String QUIT QUIT; public static String move(int x, int y, int player) { return MOVE | x | y | player; } public static String clean(String line) { return line.replace(\r, ).replace(\n, ); } }消息里 x、y、player 用 | 分隔解析时用 line.split(\|) 效率足够一局棋最多几百条消息不需要考虑性能。真正要注意的是消息末尾的 \r\n不同平台的换行不一致BufferedReader 的 readLine() 能自动处理但如果你自己写 byte 流解析就一定要把 \r 去掉否则 MOVE|7|7|1\r 会把 player 字段撑坏。这个坑我踩过不止一次。4.2 服务端线程模型一个连接一个线程还是固定线程池服务端同时服务两个客户端不是高并发系统最简单的模型是主线程 accept每来一个连接丢给固定线程池处理public class GameServer { private final ServerSocket serverSocket; private final ExecutorService pool Executors.newFixedThreadPool(2); private final ListPrintWriter clients new CopyOnWriteArrayList(); public GameServer(int port) throws IOException { this.serverSocket new ServerSocket(port); } public void start() throws IOException { System.out.println(服务器已启动等待玩家加入...); while (true) { Socket socket serverSocket.accept(); pool.submit(() - handleClient(socket)); } } private void handleClient(Socket socket) { try (BufferedReader in new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out new PrintWriter(socket.getOutputStream(), true)) { clients.add(out); String line; while ((line in.readLine()) ! null) { String msg Protocol.clean(line.trim()); if (msg.startsWith(MOVE)) { broadcast(msg); } } } catch (IOException e) { // 客户端断开在 finally 里清理连接 } finally { // 这里做 clients.remove(out) 的清理 } } private void broadcast(String message) { for (PrintWriter out : clients) { out.println(message); } } }两个细节值得展开。第一是 PrintWriter 的 autoFlush 参数设为 trueprintln 之后立即 flush避免消息堆积在缓冲区里造成对局卡住的假象。缓冲区慢一秒双方看到的落子就慢一秒实战中很容易被误判成网络故障。第二是 clients 用了 CopyOnWriteArrayList因为广播时可能正好有客户端断开遍历的同时有其他线程在 remove普通 ArrayList 会抛 ConcurrentModificationException。客户端代码和服务端对称区别是客户端只维护一个连接并且要区分自己的落子回显和对方落子广播自己落子时 UI 里要即时更新对方落子到达时才走 repaint。如果两端都只在收到广播后刷新自己下棋会有半秒延迟体验像在玩高延迟网游这个在下一章的 5.3 里还会展开。提示端口号选 1024 以上的高位端口避免占用系统保留端口同一台机器上做双开测试时两个客户端连同一个端口服务器会分别给两个 socket 建立连接不需要额外配置。5. 五子棋开发避坑指南五个高频故障的现象、原因与修复5.1 悔棋后棋盘状态与界面状态不一致现象悔棋后棋盘上棋子消失了但轮到下棋的一方还是对方或者连按两次悔棋第二次没有反应。原因悔棋逻辑里只调用了 board.clearStone忘记把 currentPlayer 还给悔棋方或者 history 用的是 ArrayList 的 remove(index)删的不是栈顶而是最早的落子。解决悔棋只允许操作栈顶undo() 里必须执行 currentPlayer last.player并发场景下给 undo() 加 synchronized防止 AI 线程计算一半时棋盘被改掉。正确实现直接参照 2.2 节的 GameState别自己另写一套。5.2 斜线五连漏判现象横竖方向的五连能正确弹出胜利斜着连成五子却没反应。原因checkWin 里的方向向量只写了 {1,0} 和 {0,1}漏了 {1,1} 和 {1,-1}或者副对角线的 y 方向写成了 {1,1}被重复统计成主对角线。解决方向向量补齐为四个之后写一个专项测试把横、竖、两条斜线、边界上的五连全部构造出来跑一遍。很多同学觉得写测试小题大做但五子棋的胜负判断就是那种看着简单、边界极多、回归必翻车的典型。5.3 网络对战时落子消失或重复落子现象双方都显示自己落子成功但对端界面上看不到子或者同一手棋出现两次。原因消息没按行结束readLine() 读到半条消息重复落子更常见——客户端本地落子一次收到服务器广播又落一次没有做幂等处理。解决发送端统一用 out.println(message)接收端统一用 in.readLine()两端都严格按行处理本地落子和广播落子做消息去重最简单是消息里带事件序号收到 MOVE 时检查序号是否大于本地已处理的序号。5.4 AI 思考时界面卡死现象点击人机对战后AI 思考两秒期间窗口变白无法拖动松开后界面恢复。原因AI 的 nextMove 跑在事件分发线程里。鼠标点击事件回调在 EDT 上执行如果在这个线程里做重计算Swing 的事件队列就被堵住repaint、鼠标移动全部排队表现就是界面假死。解决把 AI 计算丢到独立线程计算完再回 EDT 刷新new Thread(() - { int[] mv ai.nextMove(state, aiPlayer); SwingUtilities.invokeLater(() - { state.move(mv[0], mv[1]); panel.repaint(); }); }).start();这条模式建议从一开始就用。第 6 章的 Alpha-Beta 搜索一旦加上去单步计算轻松超过几十毫秒不扔到后台线程的话窗口会直接卡到让人想强杀进程。5.5 AI 落子速度过慢现象简单评估函数版 AI 下棋很快加了搜索后每步要算十几秒。原因候选点没有剪枝开局阶段几乎每个空位都要评估搜索深度每加一层节点数按分支因子指数增长五子棋开局的分支因子接近 225。解决三层剪枝。候选点只取有棋子邻居的位置评估后按分数排序先搜高分节点Alpha-Beta 剪枝的效率非常依赖节点排序搜索深度控制在 2 到 4 层评估函数不要过度复杂。这里我给出一个可复现的做法nextMove 里先对候选点快速打分降序排列后再进递归搜索单步耗时通常能下降 60% 以上。6. 强化AI棋力Alpha-Beta剪枝的落地参数与调校习惯前面第 3 章的评估函数只能看到一步棋。实战中它的棋力大概在能挡住明显的活三水平遇到会做双三进攻的对手基本招架不住。让 AI 变强的标准做法是极小化极大搜索交替模拟双方下棋若干层之后在叶子节点用评估函数打分再从底向上取最大最小值。当前玩家取最大对手取最小。暴力实现会指数爆炸所以必须加 Alpha-Beta 剪枝。核心一句话当一个节点的分值已经超过父亲节点的下界时剩下的兄弟节点不用再搜。public class SearchEngine { private int bestMoveX -1; private int bestMoveY -1; public int[] search(GameState state, int aiPlayer, int depth) { bestMoveX -1; bestMoveY -1; int score alphaBeta(state, depth, Integer.MIN_VALUE, Integer.MAX_VALUE, aiPlayer, aiPlayer); return bestMoveX 0 ? null : new int[]{bestMoveX, bestMoveY}; } private int alphaBeta(GameState state, int depth, int alpha, int beta, int currentPlayer, int aiPlayer) { if (depth 0 || state.isGameOver()) { return evaluate(state, aiPlayer); } Listint[] candidates orderCandidates(state, currentPlayer); if (currentPlayer aiPlayer) { // 最大化节点 int best Integer.MIN_VALUE; for (int[] mv : candidates) { state.move(mv[0], mv[1]); int val alphaBeta(state, depth - 1, alpha, beta, 3 - currentPlayer, aiPlayer); state.undo(); if (val best) { best val; bestMoveX mv[0]; bestMoveY mv[1]; } alpha Math.max(alpha, best); if (beta alpha) { break; // 剪枝 } } return best; } else { // 最小化节点 int best Integer.MAX_VALUE; for (int[] mv : candidates) { state.move(mv[0], mv[1]); int val alphaBeta(state, depth - 1, alpha, beta, 3 - currentPlayer, aiPlayer); state.undo(); best Math.min(best, val); beta Math.min(beta, best); if (beta alpha) { break; // 剪枝 } } return best; } } }三个工程细节必须说明。第一depth 参数depth2 时 AI 能看出下一步要被对手成五的威胁depth4 时棋力明显变强但耗时从几十毫秒涨到几秒必须配合候选点剪枝和节点排序。第二bestMoveX 只应该在顶层更新——如果你在递归的每一层都记录位置内层节点会覆盖掉真正要下的那手棋这是写递归搜索最容易犯的错。第三state.undo() 必须在递归返回后立即执行否则状态栈会被搜索过程污染连正式对局的悔棋功能一起报废。搜索和真实对局共用同一个 GameState 时这一点尤其要小心。节点排序是剪枝效率的生命线。我的习惯是先对候选点做一次快速打分按分数降序排列再进递归。同样的 depth4排序后的搜索时间能降到未排序的三分之一这不是玄学而是 Alpha-Beta 剪枝对搜索顺序高度敏感的理论特性。调参我给一个能直接上手的起点depth2评估函数只用连子权重不加搜索跑通后 depth 提到 3配合排序如果单步耗时超过 3 秒优先减候选点数而不是降 depth既能保住棋力又能把延迟压下来。这套组合在我自己维护的版本上单步在 0.5 到 1.5 秒之间能挡住活三和大多数冲四。最后把 bestMove 为空的情况处理成随机落子毕竟棋局总有边界情况让人机对战在角落也能正常玩下去。我接手过好几个五子棋项目发现棋力弱的版本九成不是评估函数写得差而是搜索顺序和剪枝没做对白白算了大量无用的分支。如果你的目标只是让 AI 别太蠢depth3 加排序就够了想再往上走把评估函数从连子权重换成模式库打分那又是一层新的东西。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑