news 2026/10/14 15:37:42

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java五子棋对战系统源码详解:AI算法与Socket网络对战实现

简介:一份基于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 Deque<Move> 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,因为后续要引单元测试依赖、要打可执行 jar,Maven 让这些都是标准操作。目录结构如下:

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.java

pom.xml 里只需要一个单元测试依赖,Swing 是 JDK 自带的,不需要第三方 GUI 库:

<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>gomoku</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</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 调成 40,15 路棋盘加边距后窗口约 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 List<PrintWriter> 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 参数设为 true,println 之后立即 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); } List<int[]> 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 参数:depth=2 时 AI 能看出"下一步要被对手成五"的威胁;depth=4 时棋力明显变强,但耗时从几十毫秒涨到几秒,必须配合候选点剪枝和节点排序。第二,bestMoveX 只应该在顶层更新——如果你在递归的每一层都记录位置,内层节点会覆盖掉真正要下的那手棋,这是写递归搜索最容易犯的错。第三,state.undo() 必须在递归返回后立即执行,否则状态栈会被搜索过程污染,连正式对局的悔棋功能一起报废。搜索和真实对局共用同一个 GameState 时,这一点尤其要小心。

节点排序是剪枝效率的生命线。我的习惯是先对候选点做一次快速打分,按分数降序排列再进递归。同样的 depth=4,排序后的搜索时间能降到未排序的三分之一,这不是玄学,而是 Alpha-Beta 剪枝对搜索顺序高度敏感的理论特性。

调参我给一个能直接上手的起点:depth=2,评估函数只用连子权重不加搜索;跑通后 depth 提到 3,配合排序;如果单步耗时超过 3 秒,优先减候选点数而不是降 depth,既能保住棋力又能把延迟压下来。这套组合在我自己维护的版本上,单步在 0.5 到 1.5 秒之间,能挡住活三和大多数冲四。最后把 bestMove 为空的情况处理成随机落子,毕竟棋局总有边界情况,让人机对战在角落也能正常玩下去。

我接手过好几个五子棋项目,发现棋力弱的版本九成不是评估函数写得差,而是搜索顺序和剪枝没做对,白白算了大量无用的分支。如果你的目标只是让 AI 别太蠢,depth=3 加排序就够了;想再往上走,把评估函数从"连子权重"换成"模式库打分",那又是一层新的东西。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 15:37:15

学术图表绘制困境,硕词 AI 科研图表生成实操路径

图表是科研成果可视化表达的载体&#xff0c;图表质量直接影响论文可读性。很多研究生写作中遭遇图表难题&#xff1a;统计图表格式混乱&#xff0c;坐标轴、图例、图注不符合学术规范&#xff1b;机理示意图、工程原理图手绘效果差&#xff0c;配色杂乱&#xff1b;图表与正文…

作者头像 李华
网站建设 2026/10/14 15:37:10

STM32+SR8201F通用驱动包:百兆以太网PHY移植与调试指南

简介&#xff1a;针对SR8201F物联网芯片在STM32平台上的通用驱动代码&#xff0c;面向嵌入式开发与物联网终端设计工程师&#xff0c;解决SR8201F以太网控制器在STM32平台上的底层适配与快速移植问题。压缩包内共2个文件&#xff0c;采用1个头文件加1个源文件的精简结构&#x…

作者头像 李华
网站建设 2026/10/14 15:36:58

SSM框架图书借阅管理系统毕业设计全流程解析

简介&#xff1a;基于Java的图书馆借阅管理系统毕业设计资源&#xff0c;采用SSM框架&#xff0c;主要面向计算机专业毕业生、课程设计学习者&#xff0c;可完整解决图书借阅管理系统的需求分析、数据库设计与功能实现等问题&#xff0c;并配套毕业论文设计与答辩素材。压缩包共…

作者头像 李华
网站建设 2026/10/14 15:36:32

Python GUI 开发实战:Tkinter、PySide6 与 CustomTkinter 选型及打包交付

简介&#xff1a;这份资源面向希望将Python脚本转化为可视化操作界面的初学者与进阶开发者&#xff0c;围绕Tkinter标准库讲解GUI编程的核心思路&#xff0c;帮助非程序员也能通过窗口、按钮、菜单等控件与程序交互。压缩包共12个文件&#xff0c;约791KB&#xff0c;包含1个py…

作者头像 李华