函数化编程重构五子棋AI:从面条代码到模块化引擎的实践
2026/8/22 2:48:46 网站建设 项目流程

1. 从“硬编码”到“函数模拟”:一次五子棋AI算法的重构之旅

最近在社区里看到不少朋友在讨论用Java Swing实现五子棋,或者尝试挑战“地狱难度”的AI。这让我想起了几年前自己折腾五子棋算法时的一段经历。当时,我写了一个能下棋的程序,但核心的胜负判断和搜索逻辑写得一团糟——满屏的if-else和嵌套循环,代码又长又难维护,想加个“禁手”规则或者优化搜索深度都无从下手。后来,我痛定思痛,决定推倒重来,这次的目标很明确:用函数化的思想,模拟一个可迭代、可配置的“解法引擎”。这就是“五子棋(Version.2 函数模拟迭代解法)”这个项目的由来。它不是一个简单的游戏实现,而是一次关于如何将复杂的棋盘逻辑,拆解成一个个职责单一、可组合的“函数单元”,并通过迭代调用这些单元来模拟完整对弈过程的实践。如果你也受够了面条式的棋类游戏代码,或者对如何设计一个清晰、可扩展的AI核心感兴趣,那么这次的重构思路或许能给你带来一些启发。

2. 核心困境:为什么传统的五子棋代码难以维护?

在动手重构之前,我们得先搞清楚老代码到底“烂”在哪里。以最常见的胜负判断为例,很多初学者的写法是遍历整个15x15的棋盘,对每个点,再向四个方向(横、竖、左斜、右斜)分别检查是否有连续五个同色棋子。

2.1 “面条代码”的典型症状

这种写法的代码通常会膨胀成一个巨大的函数,里面充斥着坐标计算和条件判断。比如,检查横向时,你需要判断board[x][y]board[x+1][y]……board[x+4][y]是否都等于当前玩家颜色,并且还要确保x+4没有超出棋盘边界。四个方向就是四套几乎重复但又略有不同的逻辑。当你想加入“长连禁手”(超过五子不算赢)或者“四四禁手”等专业规则时,就不得不在这团乱麻中插入更多的if语句,代码的复杂度呈指数级上升。

2.2 函数化思维带来的转机

函数化编程的核心思想之一,是将程序分解为一系列接受输入、产生输出,且没有副作用的纯函数。应用到五子棋上,我们可以这样思考:

  • 胜负判断不应该是一个庞然大物,而应该是一个纯函数:checkWin(board, x, y, player)。它只关心在给定棋盘、给定落子点、给定玩家的情况下,返回truefalse
  • 方向检查可以被进一步抽象:checkDirection(board, x, y, dx, dy, player)(dx, dy)表示方向向量,比如(1,0)是横向,(0,1)是竖向。这个函数只负责沿一个方向计数连续的同色棋子。
  • 棋盘评估(为AI服务)可以是另一个函数:evaluatePosition(board, player),它扫描棋盘,为当前玩家计算一个分数。

这样一来,复杂的全局逻辑被拆解成了一个个乐高积木似的函数块。checkWin函数内部只需要调用四次checkDirection,分别传入四个方向向量即可。代码立刻变得清晰、可测试,并且修改一个规则(比如禁手)只需要修改或替换对应的那个“积木”,而不会牵一发而动全身

3. 构建五子棋的“函数模拟”核心引擎

基于上面的思路,我们来搭建Version.2的核心。我们不再关注Swing的界面如何画(那是另一个模块的事情),而是聚焦于棋盘数据模型和核心算法函数。

3.1 数据模型设计:简单的就是最好的

棋盘本质上是一个二维数组。我们用0表示空位,1表示黑棋,2表示白棋。

public class GomokuBoard { private int[][] board; // 15x15 的棋盘 private int currentPlayer; // 当前行棋方:1 或 2 // ... 构造函数,获取器,设置器 }

这个类只负责存储状态和提供基本的查询、落子方法(如makeMove(x, y)会校验位置是否为空)。它不应该包含任何复杂的游戏逻辑。

3.2 核心算法函数库:职责分离

我们将所有算法剥离到独立的工具类或一组静态函数中。这是本次重构的精华所在。

3.2.1 基础胜负判断函数

public class GomokuLogic { // 方向向量:横、竖、左斜(左上-右下)、右斜(左下-右上) private static final int[][] DIRECTIONS = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; /** * 检查在(x,y)处落子后,玩家player是否获胜 * @param board 棋盘 * @param x 落子横坐标 * @param y 落子纵坐标 * @param player 玩家编号 * @return 是否构成五连 */ public static boolean checkWin(int[][] board, int x, int y, int player) { for (int[] dir : DIRECTIONS) { int count = 1; // 刚落下的这颗子 // 向正方向检查 count += countDirection(board, x, y, dir[0], dir[1], player); // 向反方向检查 count += countDirection(board, x, y, -dir[0], -dir[1], player); if (count >= 5) { return true; } } return false; } /** * 沿某个方向(dx, dy)统计连续的同色棋子数 */ private static int countDirection(int[][] board, int startX, int startY, int dx, int dy, int player) { int count = 0; int x = startX + dx; int y = startY + dy; while (x >= 0 && x < board.length && y >= 0 && y < board[0].length && board[x][y] == player) { count++; x += dx; y += dy; } return count; } }

注意:这里将方向向量定义为常量,并使用循环遍历,彻底消除了重复代码。countDirection是一个纯净的、可复用的函数单元。

3.2.2 引入“迭代”概念:棋盘状态生成器“迭代”在此处有两层含义。一是指AI搜索算法中的迭代加深(Iterative Deepening)。二是指我们可以设计一个函数,来“迭代”地生成所有可能的下一步棋盘状态,供AI评估。

public class BoardUtils { /** * 生成当前棋盘所有合法落子点的列表 * 为了性能,可以只生成棋盘上已有棋子周围一圈的空位(启发式) */ public static List<Move> generateLegalMoves(int[][] board, int currentPlayer) { List<Move> moves = new ArrayList<>(); Set<String> considered = new HashSet<>(); // 用于去重 int size = board.length; // 首先,遍历整个棋盘,找到所有已有棋子的位置 for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { if (board[i][j] != 0) { // 在该棋子周围3x3范围内寻找空位 for (int dx = -2; dx <= 2; dx++) { // 范围可以调整 for (int dy = -2; dy <= 2; dy++) { int nx = i + dx; int ny = j + dy; if (nx >= 0 && nx < size && ny >= 0 && ny < size && board[nx][ny] == 0) { String key = nx + "," + ny; if (!considered.contains(key)) { moves.add(new Move(nx, ny, currentPlayer)); considered.add(key); } } } } } } } // 如果棋盘为空,返回中心点 if (moves.isEmpty() && board[size/2][size/2] == 0) { moves.add(new Move(size/2, size/2, currentPlayer)); } return moves; } }

这个generateLegalMoves函数就是一个典型的“迭代器”,它基于当前状态,产生一系列新的可能状态。这是后续AI搜索的基石。

4. 实现“可迭代的”AI决策引擎

有了核心函数库,我们就可以构建一个更像“引擎”的AI。这个AI不再是一堆写死的逻辑,而是通过组合和调用这些函数,进行可配置深度的搜索。

4.1 评估函数:给棋盘局面打分

AI需要知道哪个局面更好。我们实现一个简单的评估函数。这个函数本身也是由更小的“模式识别”函数组合而成。

public class Evaluator { // 定义棋型分数(非常简化的版本) private static final int SCORE_FIVE = 100000; private static final int SCORE_FOUR = 10000; private static final int SCORE_THREE = 1000; // ... 其他棋型 /** * 评估当前棋盘对指定玩家的有利程度 */ public static int evaluate(int[][] board, int player) { int score = 0; int size = board.length; // 扫描整个棋盘,识别棋型 for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { if (board[i][j] == player) { score += evaluatePoint(board, i, j, player); } else if (board[i][j] != 0) { score -= evaluatePoint(board, i, j, 3 - player); // 对手的棋子,减分 } } } return score; } /** * 评估单个棋子在其四个方向上形成的潜在棋型 */ private static int evaluatePoint(int[][] board, int x, int y, int player) { int pointScore = 0; for (int[] dir : GomokuLogic.DIRECTIONS) { // 这里可以调用更精细的模式检查函数,例如: // Pattern pattern = detectPattern(board, x, y, dir[0], dir[1], player); // pointScore += getPatternScore(pattern); // 简化版:只计算连续棋子数 int count = GomokuLogic.countDirection(board, x, y, dir[0], dir[1], player) + 1; // +1是自身 pointScore += getScoreByCount(count); } return pointScore; } private static int getScoreByCount(int count) { switch (count) { case 5: return SCORE_FIVE; case 4: return SCORE_FOUR; case 3: return SCORE_THREE; default: return count; // 连续子数越多,基础分越高 } } }

评估函数是AI的“眼睛”,它的好坏直接决定AI的强弱。这里只是一个示例,真正的强AI(比如“地狱难度”)会使用更复杂的模式库和更精细的分数计算。

4.2 极小化极大算法与Alpha-Beta剪枝的函数化实现

这是AI的“大脑”。我们将搜索算法也实现为一系列函数。

public class AISearchEngine { private int maxDepth; // 搜索深度 public AISearchEngine(int maxDepth) { this.maxDepth = maxDepth; } /** * 主入口:寻找当前最佳落子点 */ public Move findBestMove(int[][] board, int currentPlayer) { List<Move> moves = BoardUtils.generateLegalMoves(board, currentPlayer); Move bestMove = null; int bestValue = Integer.MIN_VALUE; for (Move move : moves) { // 模拟落子 board[move.x][move.y] = move.player; // 调用递归搜索函数,评估这个走法后的局面 int moveValue = alphaBeta(board, maxDepth, Integer.MIN_VALUE, Integer.MAX_VALUE, false, 3 - currentPlayer); // 撤销落子 board[move.x][move.y] = 0; if (moveValue > bestValue) { bestValue = moveValue; bestMove = move; } } return bestMove != null ? bestMove : moves.get(0); // 保底返回第一个合法走法 } /** * Alpha-Beta 剪枝搜索核心函数 * @param depth 剩余搜索深度 * @param alpha 当前层已知的最好值(对MAX方) * @param beta 当前层已知的最差值(对MIN方) * @param isMaximizing 当前是否是MAX方(AI)在决策 * @param player 当前要下棋的玩家 * @return 当前节点的评估值 */ private int alphaBeta(int[][] board, int depth, int alpha, int beta, boolean isMaximizing, int player) { // 终止条件:达到深度限制或游戏结束 if (depth == 0 || isTerminal(board)) { return Evaluator.evaluate(board, this.maximizingPlayer); // 假设AI是 maximizingPlayer } List<Move> moves = BoardUtils.generateLegalMoves(board, player); if (isMaximizing) { int value = Integer.MIN_VALUE; for (Move move : moves) { board[move.x][move.y] = player; value = Math.max(value, alphaBeta(board, depth - 1, alpha, beta, false, 3 - player)); board[move.x][move.y] = 0; alpha = Math.max(alpha, value); if (value >= beta) { break; // Beta 剪枝 } } return value; } else { int value = Integer.MAX_VALUE; for (Move move : moves) { board[move.x][move.y] = player; value = Math.min(value, alphaBeta(board, depth - 1, alpha, beta, true, 3 - player)); board[move.x][move.y] = 0; beta = Math.min(beta, value); if (value <= alpha) { break; // Alpha 剪枝 } } return value; } } private boolean isTerminal(int[][] board) { // 这里可以调用 GomokuLogic.checkWin 遍历检查,但更高效的做法是在递归过程中判断。 // 简化处理,假设深度够了就评估。 return false; } }

这个AISearchEngine类就是一个标准的“函数模拟迭代解法”的集大成者。它通过迭代调用generateLegalMoves(生成状态)、递归调用自身(模拟未来)、调用Evaluator.evaluate(评估叶节点)来完成整个决策过程。alphaBeta函数是核心,它清晰地展示了“迭代”(遍历走法)和“递归”(模拟未来步骤)的过程。

5. 版本迭代与性能优化实战

第一版的函数化引擎跑起来后,你会发现随着搜索深度增加,速度会急剧下降。这就是“迭代”需要优化的地方。

5.1 迭代加深搜索(Iterative Deepening Search, IDS)

与其一开始就进行深度为5的搜索,不如先搜深度1,再搜深度2,依次加深。这样做好处很多:可以在固定时间内返回一个尽可能好的结果(时间控制),并且浅层搜索产生的“最佳走法”排序信息,可以为深一层搜索的Alpha-Beta剪枝提供更好的节点顺序,极大提升剪枝效率。

public Move findBestMoveWithTimeLimit(int[][] board, int currentPlayer, long timeLimitMillis) { Move bestMove = null; long startTime = System.currentTimeMillis(); for (int depth = 1; depth <= MAX_POSSIBLE_DEPTH; depth++) { this.maxDepth = depth; Move currentBest = findBestMove(board, currentPlayer); // 调用之前的函数 if (currentBest != null) { bestMove = currentBest; } // 检查是否超时 if (System.currentTimeMillis() - startTime > timeLimitMillis * 0.9) { // 留10%余量 break; } } return bestMove; }

5.2 启发式排序与置换表

generateLegalMoves中,我们返回的走法列表是乱序的。Alpha-Beta剪枝的效率极度依赖节点顺序。我们应该把“看起来更好”的走法(比如成四、活三的点)排在前面。

// 在生成走法后,进行排序 moves.sort((m1, m2) -> { // 简单启发:靠近棋盘中心、靠近已有棋子的位置优先 int score1 = heuristicScore(m1.x, m1.y, board); int score2 = heuristicScore(m2.x, m2.y, board); return Integer.compare(score2, score1); // 降序 });

更进一步,可以使用“置换表”(Transposition Table)来缓存已经搜索过的棋盘局面的结果。棋盘局面可以通过Zobrist哈希转换成一个唯一的long型键值。当再次遇到相同的局面时,直接查表返回结果,避免重复搜索。这是提升博弈树搜索性能的经典手段。

5.3 多线程并行搜索

现代CPU都是多核的。我们可以将第一层生成的多个走法(即不同的“根节点”)分配给不同的线程并行进行Alpha-Beta搜索,最后汇总结果。这是从“函数迭代”到“并行迭代”的升级。

ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); List<Future<MoveResult>> futures = new ArrayList<>(); for (Move move : firstLevelMoves) { futures.add(executor.submit(() -> { // 复制棋盘,模拟落子 int[][] newBoard = copyBoard(board); newBoard[move.x][move.y] = currentPlayer; // 在新的棋盘上执行搜索 int score = alphaBeta(newBoard, maxDepth-1, ...); return new MoveResult(move, score); })); } // ... 收集所有结果,选择分数最高的

6. 从理论到实践:集成与调试心得

将这套函数化的引擎集成到Swing GUI中,就完成了整个五子棋游戏。这里分享几个关键的集成点和调试技巧。

6.1 引擎与界面的松耦合

GUI(Swing)部分只负责三件事:1. 绘制棋盘和棋子;2. 接收玩家鼠标点击,转换为坐标;3. 在玩家落子后,调用GomokuBoard.makeMove()GomokuLogic.checkWin(),然后调用AISearchEngine.findBestMove()获取AI落子,再重复这个过程。 它们之间通过定义清晰的接口(比如一个GameController)来通信,引擎完全不知道界面的存在。这使得你可以轻松替换GUI(比如换成JavaFX或控制台),或者替换AI引擎(比如换成一个神经网络模型)。

6.2 调试复杂递归函数的技巧

Alpha-Beta搜索递归深,状态多,出错了很难调试。

  • 日志输出:在递归函数入口打印深度和当前主要参数(如alpha, beta),但要注意日志量巨大,可以只对特定搜索路径或深度小于3时开启。
  • 单元测试:为每一个基础函数(checkWin,countDirection,evaluatePoint)编写详尽的单元测试。确保这些“积木”本身是牢固的。
  • 可视化调试:写一个简单的函数,将搜索过程中AI考虑过的前几个最佳走法在控制台用字符画出来,直观感受AI的“思考”过程。
  • 使用断言:在关键位置使用assert语句,例如在递归函数中确保depth >= 0,在评估函数中确保棋子坐标有效。

6.3 性能分析与瓶颈定位

使用JProfiler或VisualVM等工具监控游戏运行时,你会发现90%的时间可能都花在了evaluatePointgenerateLegalMoves上。

  • 评估函数优化:将棋型模式预计算成查表。例如,事先计算好所有可能的五元组(一行5个点)对应的棋型分数,评估时直接拼接查表,避免实时分析。
  • 走法生成优化:维护一个“空位热点列表”,只记录棋盘上所有空位,并在每次落子后只更新这个列表(移除被占用的点,加入新棋子周围新增的空位),而不是每次都全盘扫描。
  • 剪枝优化:除了Alpha-Beta,实现更激进的剪枝,如“空步裁剪”(Null-move pruning),在特定情况下假设自己停一手,如果局面仍然大优,则直接剪枝。

通过这样一轮从“面条代码”到“函数模拟迭代引擎”的重构,你得到的不仅仅是一个能运行的五子棋程序,而是一个清晰、模块化、可测试、可扩展的算法框架。你可以很方便地在这个框架上实验新的评估函数、尝试蒙特卡洛树搜索(MCTS)、或者加入更复杂的禁手规则。这种用函数组合来模拟复杂过程、用迭代和递归来探索解空间的思想,其价值远远超出了五子棋这个具体的项目,它是解决许多计算问题的通用利器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询