每日算法 — 使用java实现2048:Expectimax博弈树搜索与多维度启发式评估

2048是一款经典的数字益智游戏,玩家通过上下左右滑动方块,使相同数字碰撞合并,最终目标是合成2048甚至更高的数字。由于每次移动后新方块会在随机空位生成2或4,游戏中存在”玩家决策”与”随机生成”两种交替进行的节点,这为算法设计带来了独特的挑战。本文将用Java实现一个带AI的2048游戏,核心采用Expectimax博弈树搜索处理随机性,配合多维度启发式评估函数进行状态评分,包含完整的游戏引擎与AI决策模块。

一、Expectimax算法原理

Expectimax是Minimax算法的扩展,专门适用于存在随机因素的对弈或决策场景。在2048中,游戏树包含两类节点:

  • Max节点(玩家回合):玩家选择上下左右四个方向中评分最高的动作,取子节点评估值的最大值。
  • Chance节点(随机回合):系统在随机空位生成2(概率90%)或4(概率10%),对所有可能的结果取期望值。

搜索深度通常受限于计算资源,实际应用中可通过迭代加深或设置时间上限来控制。Expectimax的递归公式如下:

$$
V(s) = \begin{cases}
\text{evaluate}(s) & \text{if } \text{depth} = 0 \text{ or terminal} \
\max_{a \in \text{actions}} V(s_a) & \text{if Max node} \
\sum_{r \in \text{random outcomes}} P(r) \cdot V(s_r) & \text{if Chance node}
\
\end{cases}
$$

二、多维度启发式评估函数

由于2048的搜索空间巨大,搜索深度受限时评估函数的质量直接决定AI水平。我们设计一个融合多个维度的线性加权评估函数:

评估维度 说明 权重方向
单调性(Monotonicity) 鼓励行列呈单调递增或递减趋势,便于高数值聚集 正向
平滑性(Smoothness) 相邻格子差值越小越好,减少”孤岛” 正向
空格数(Empty Tiles) 保留更多操作空间,避免死局 正向
最大值位置(Max Tile Position) 将最大数字固定在角落,利于后续合并 正向
合并潜力(Merge Potential) 统计相邻相等数字对,评估近期合并机会 正向

各维度加权求和后得到状态评分,引导AI向”角落聚集、平滑过渡、保留空格”的理想格局发展。

三、核心数据结构

游戏板使用4×4的二维整数数组表示,0代表空格。状态类封装棋盘与当前得分:

public class Board {
    public static final int SIZE = 4;
    private int[][] grid;
    private int score;

    public Board() {
        grid = new int[SIZE][SIZE];
        score = 0;
    }

    public Board(int[][] grid, int score) {
        this.grid = new int[SIZE][SIZE];
        for (int i = 0; i < SIZE; i++) {
            System.arraycopy(grid[i], 0, this.grid[i], 0, SIZE);
        }
        this.score = score;
    }

    public int[][] getGrid() { return grid; }
    public int getScore() { return score; }
    public void setScore(int score) { this.score = score; }

    /** 获取所有空格坐标 */
    public List<int[]> getEmptyCells() {
        List<int[]> cells = new ArrayList<>();
        for (int r = 0; r < SIZE; r++) {
            for (int c = 0; c < SIZE; c++) {
                if (grid[r][c] == 0) cells.add(new int[]{r, c});
            }
        }
        return cells;
    }

    /** 在指定位置插入数字 */
    public void insertTile(int row, int col, int value) {
        grid[row][col] = value;
    }

    /** 获取最大值 */
    public int getMaxTile() {
        int max = 0;
        for (int[] row : grid) {
            for (int v : row) max = Math.max(max, v);
        }
        return max;
    }

    /** 深拷贝 */
    public Board copy() {
        return new Board(grid, score);
    }
}

四、游戏引擎:移动与合并逻辑

移动操作需要处理滑动、合并和计分。以向左移动为例,对每一行执行”压缩-合并-压缩”三步:

public class GameEngine {
    public static final int UP = 0, DOWN = 1, LEFT = 2, RIGHT = 3;

    /** 执行指定方向的移动,返回是否发生变化 */
    public static boolean move(Board board, int direction) {
        int[][] g = board.getGrid();
        boolean moved = false;
        int newScore = board.getScore();

        // 统一通过旋转矩阵简化四个方向的处理
        int rotations = direction == UP ? 3 : direction == DOWN ? 1 : direction == RIGHT ? 2 : 0;
        int[][] working = rotate(g, rotations);

        for (int r = 0; r < Board.SIZE; r++) {
            int[] result = processLine(working[r]);
            if (!Arrays.equals(working[r], result)) moved = true;
            working[r] = result;
            // 累加合并得分
            for (int i = 0; i < Board.SIZE - 1; i++) {
                if (result[i] != 0 && result[i] == result[i + 1]) {
                    newScore += result[i] * 2;
                }
            }
        }

        int[][] restored = rotate(working, (4 - rotations) % 4);
        for (int i = 0; i < Board.SIZE; i++) {
            System.arraycopy(restored[i], 0, g[i], 0, Board.SIZE);
        }
        board.setScore(newScore);
        return moved;
    }

    /** 处理单行:压缩零值、合并相邻相等元素、再次压缩 */
    private static int[] processLine(int[] line) {
        int[] temp = new int[Board.SIZE];
        int idx = 0;
        for (int v : line) {
            if (v != 0) temp[idx++] = v;
        }
        int[] result = new int[Board.SIZE];
        idx = 0;
        for (int i = 0; i < Board.SIZE; i++) {
            if (temp[i] == 0) break;
            if (i + 1 < Board.SIZE && temp[i] == temp[i + 1]) {
                result[idx++] = temp[i] * 2;
                i++; // 跳过已被合并的元素
            } else {
                result[idx++] = temp[i];
            }
        }
        return result;
    }

    /** 矩阵顺时针旋转n次 */
    private static int[][] rotate(int[][] matrix, int times) {
        int[][] result = new int[Board.SIZE][Board.SIZE];
        for (int i = 0; i < Board.SIZE; i++) {
            System.arraycopy(matrix[i], 0, result[i], 0, Board.SIZE);
        }
        for (int t = 0; t < times; t++) {
            int[][] next = new int[Board.SIZE][Board.SIZE];
            for (int r = 0; r < Board.SIZE; r++) {
                for (int c = 0; c < Board.SIZE; c++) {
                    next[c][Board.SIZE - 1 - r] = result[r][c];
                }
            }
            result = next;
        }
        return result;
    }

    /** 随机在空格生成新方块(90%概率为2,10%为4) */
    public static void spawnTile(Board board) {
        List<int[]> empty = board.getEmptyCells();
        if (empty.isEmpty()) return;
        int[] cell = empty.get((int)(Math.random() * empty.size()));
        int value = Math.random() < 0.9 ? 2 : 4;
        board.insertTile(cell[0], cell[1], value);
    }

    /** 检查是否还能移动 */
    public static boolean canMove(Board board) {
        int[][] g = board.getGrid();
        for (int r = 0; r < Board.SIZE; r++) {
            for (int c = 0; c < Board.SIZE; c++) {
                if (g[r][c] == 0) return true;
                if (c + 1 < Board.SIZE && g[r][c] == g[r][c + 1]) return true;
                if (r + 1 < Board.SIZE && g[r][c] == g[r + 1][c]) return true;
            }
        }
        return false;
    }
}

五、Expectimax AI决策引擎

AI引擎的核心是递归搜索与状态评估。由于Chance节点的分支因子等于空格数(最多16个),直接搜索深度有限。我们采用”一次生成一个随机方块”的简化模型(每次Chance节点只考虑在一个随机位置生成2或4),将分支控制在合理范围:

public class ExpectimaxAI {
    // 各评估维度权重,通过大量对局调优
    private static final double WEIGHT_MONOTONICITY = 47.0;
    private static final double WEIGHT_SMOOTHNESS = 11.0;
    private static final double WEIGHT_EMPTY = 270.0;
    private static final double WEIGHT_MAX_CORNER = 100.0;
    private static final double WEIGHT_MERGE = 70.0;

    private int maxDepth;

    public ExpectimaxAI(int maxDepth) {
        this.maxDepth = maxDepth;
    }

    /** 为当前局面选择最优动作 */
    public int findBestMove(Board board) {
        int bestMove = -1;
        double bestValue = Double.NEGATIVE_INFINITY;

        for (int dir = 0; dir < 4; dir++) {
            Board next = board.copy();
            if (!GameEngine.move(next, dir)) continue; // 非法移动

            double value = expectimax(next, maxDepth - 1, false);
            if (value > bestValue) {
                bestValue = value;
                bestMove = dir;
            }
        }
        return bestMove;
    }

    /** Expectimax递归搜索
     * @param board 当前棋盘
     * @param depth 剩余深度
     * @param isMax true表示玩家回合(Max节点),false表示随机回合(Chance节点)
     */
    private double expectimax(Board board, int depth, boolean isMax) {
        if (depth == 0 || !GameEngine.canMove(board)) {
            return evaluate(board);
        }

        if (isMax) {
            // Max节点:玩家选择最佳动作
            double maxValue = Double.NEGATIVE_INFINITY;
            for (int dir = 0; dir < 4; dir++) {
                Board next = board.copy();
                if (!GameEngine.move(next, dir)) continue;
                double value = expectimax(next, depth - 1, false);
                maxValue = Math.max(maxValue, value);
            }
            return maxValue == Double.NEGATIVE_INFINITY ? evaluate(board) : maxValue;
        } else {
            // Chance节点:随机生成方块,取期望值
            List<int[]> empty = board.getEmptyCells();
            if (empty.isEmpty()) return evaluate(board);

            double totalValue = 0.0;
            int count = 0;
            // 为提高效率,只采样部分空格(如全部采样会导致分支爆炸)
            int sampleSize = Math.min(empty.size(), 8);
            for (int i = 0; i < sampleSize; i++) {
                int[] cell = empty.get(i);
                // 90%概率生成2
                Board with2 = board.copy();
                with2.insertTile(cell[0], cell[1], 2);
                totalValue += 0.9 * expectimax(with2, depth - 1, true);
                // 10%概率生成4
                Board with4 = board.copy();
                with4.insertTile(cell[0], cell[1], 4);
                totalValue += 0.1 * expectimax(with4, depth - 1, true);
                count++;
            }
            return totalValue / count;
        }
    }

    /** 多维度启发式评估函数 */
    public static double evaluate(Board board) {
        int[][] g = board.getGrid();
        double score = 0.0;

        // 1. 空格数评估
        int emptyCount = board.getEmptyCells().size();
        score += WEIGHT_EMPTY * emptyCount;

        // 2. 单调性评估(四个方向的单调性取最大值)
        double[] monoScores = new double[4];
        // 从左到右、从右到左
        for (int r = 0; r < Board.SIZE; r++) {
            for (int c = 0; c < Board.SIZE - 1; c++) {
                if (g[r][c] > g[r][c + 1]) monoScores[0] += g[r][c] - g[r][c + 1];
                else monoScores[1] += g[r][c + 1] - g[r][c];
            }
        }
        // 从上到下、从下到上
        for (int c = 0; c < Board.SIZE; c++) {
            for (int r = 0; r < Board.SIZE - 1; r++) {
                if (g[r][c] > g[r + 1][c]) monoScores[2] += g[r][c] - g[r + 1][c];
                else monoScores[3] += g[r + 1][c] - g[r][c];
            }
        }
        double mono = Math.max(
            Math.max(monoScores[0], monoScores[1]),
            Math.max(monoScores[2], monoScores[3])
        );
        score -= WEIGHT_MONOTONICITY * mono;

        // 3. 平滑性评估
        double smooth = 0.0;
        for (int r = 0; r < Board.SIZE; r++) {
            for (int c = 0; c < Board.SIZE; c++) {
                if (r + 1 < Board.SIZE) {
                    smooth -= Math.abs(g[r][c] - g[r + 1][c]);
                }
                if (c + 1 < Board.SIZE) {
                    smooth -= Math.abs(g[r][c] - g[r][c + 1]);
                }
            }
        }
        score += WEIGHT_SMOOTHNESS * smooth;

        // 4. 最大值位于角落的奖励
        int maxTile = board.getMaxTile();
        boolean inCorner = (g[0][0] == maxTile || g[0][Board.SIZE - 1] == maxTile
                         || g[Board.SIZE - 1][0] == maxTile || g[Board.SIZE - 1][Board.SIZE - 1] == maxTile);
        if (inCorner) score += WEIGHT_MAX_CORNER * maxTile;

        // 5. 合并潜力评估
        int mergePotential = 0;
        for (int r = 0; r < Board.SIZE; r++) {
            for (int c = 0; c < Board.SIZE; c++) {
                if (g[r][c] == 0) continue;
                if (c + 1 < Board.SIZE && g[r][c] == g[r][c + 1]) mergePotential += g[r][c];
                if (r + 1 < Board.SIZE && g[r][c] == g[r + 1][c]) mergePotential += g[r][c];
            }
        }
        score += WEIGHT_MERGE * mergePotential;

        return score;
    }
}

六、主程序与自动对局

以下主程序启动一个AI自动运行的2048游戏,每步通过Expectimax搜索决策,控制台输出棋盘变化:

public class Game2048 {
    public static void main(String[] args) {
        Board board = new Board();
        GameEngine.spawnTile(board);
        GameEngine.spawnTile(board);
        ExpectimaxAI ai = new ExpectimaxAI(4); // 搜索深度4,平衡性能与效果

        System.out.println("=== 2048 AI 自动对局 ===");
        printBoard(board);

        int moves = 0;
        while (GameEngine.canMove(board)) {
            int move = ai.findBestMove(board);
            if (move == -1) break; // 无合法移动

            GameEngine.move(board, move);
            GameEngine.spawnTile(board);
            moves++;

            String[] dirNames = {"上", "下", "左", "右"};
            System.out.println("\n第" + moves + "步: " + dirNames[move] + " (得分: " + board.getScore() + ")");
            printBoard(board);
        }

        System.out.println("\n=== 游戏结束 ===");
        System.out.println("总步数: " + moves);
        System.out.println("最终得分: " + board.getScore());
        System.out.println("最大方块: " + board.getMaxTile());
    }

    private static void printBoard(Board board) {
        int[][] g = board.getGrid();
        for (int[] row : g) {
            for (int v : row) {
                System.out.printf("%6d", v);
            }
            System.out.println();
        }
    }
}

七、复杂度分析

指标 复杂度 说明
时间复杂度 $O(b^m \cdot c)$ $b$为玩家分支因子(≤4),$m$为搜索深度,$c$为Chance节点采样数
空间复杂度 $O(m)$ 递归栈深度等于搜索深度
评估函数 $O(1)$ 棋盘固定为4×4,评估为常数时间

实际运行中,深度4的Expectimax配合8个空格采样,每次决策约需评估数万个节点,在现代CPU上可在100ms内完成。如需提升性能,可采用迭代加深配合置换表(Transposition Table)缓存已评估状态,或引入多线程并行扩展Chance节点。

八、总结

本文用Java完整实现了2048游戏的AI决策系统,核心亮点包括:

  • Expectimax搜索:精准建模2048中”玩家决策-随机生成”的交替过程,通过Max节点取最优、Chance节点取期望的方式推导理论最优策略。
  • 多维度启发式评估:融合单调性、平滑性、空格数、角落最大化和合并潜力五个维度,引导AI形成”角落聚集、平滑过渡”的高分布局。
  • 可扩展架构:Board、GameEngine、ExpectimaxAI三层分离,便于后续引入置换表、迭代加深、神经网络评估等进阶优化。

Expectimax作为处理随机决策问题的经典算法,不仅适用于2048,也可推广到麻将、扑克等含隐藏信息与随机性的博弈场景。读者可尝试调整评估权重、增加搜索深度或引入蒙特卡洛采样,进一步挖掘AI的得分上限。