每日算法 — 使用java实现马里奥医生:贪心落点评估与连锁消除预测

引言

马里奥医生(Dr. Mario)是任天堂于1990年推出的经典下落式消除游戏。与俄罗斯方块不同,游戏中下落的是由两种颜色胶囊组成的双格方块,玩家需要将四个及以上同色胶囊横向或纵向连成一线来消除病毒。本文用 Java 实现马里奥医生的 AI 决策系统,重点讲解贪心落点评估算法与连锁消除深度预测,让 AI 能够自主选择最优下落位置与旋转角度。

游戏规则建模

游戏场景为一个 8 行 × 16 列的垂直 bottle(药瓶):
– 病毒(Virus):红、黄、蓝三色,散布在药瓶底部区域
– 胶囊(Capsule):由两个相连的半胶囊组成,有 6 种颜色组合(RR, YY, BB, RY, RB, YB)
– 消除规则:4 个及以上同色半胶囊横向或纵向连续时消除
– 连锁(Chain):消除后上方胶囊下落,可能形成新的消除,产生连锁反应

AI 的决策空间:对于每一个下落的胶囊,选择{旋转角度} × {水平位置} × {是否加速下落},共约 40 种可能动作。

核心算法设计

贪心落点评估

AI 需要对每一个可能的落点进行评分,选择分数最高的动作。评估函数包含多个维度:

  1. 消除贡献(Elimination Score):落点直接消除的病毒数量 × 100
  2. 连通潜力(Connectivity Bonus):与已有同色半胶囊相邻的数量 × 10
  3. 高度惩罚(Height Penalty):落点越高(越接近瓶口)扣分越多,防止堆叠过高
  4. 空洞惩罚(Hole Penalty):落点下方有空隙时扣分,避免制造难以消除的深洞
  5. 连锁预测(Chain Prediction):预测消除后引发的连锁反应深度,深度越大加分越多

综合评分公式:

Score = 消除贡献 + 连通潜力 - 高度惩罚 - 空洞惩罚 + 连锁加分

连锁消除深度预测

连锁是马里奥医生高分的关键。AI 需要模拟”放置胶囊 → 检测消除 → 胶囊下落 → 再次检测”的连锁过程,计算最大连锁深度。

我们使用迭代消除模拟:
1. 在当前棋盘状态下放置胶囊
2. 执行 Flood Fill 检测所有 4 连以上的同色区域
3. 标记消除,让上方胶囊下落(重力模拟)
4. 重复步骤 2-3,直到没有新的消除产生
5. 记录连锁次数和总消除病毒数

Java 实现

import java.util.*;

/**
 * 马里奥医生 AI:贪心落点评估与连锁消除预测
 */
public class DrMarioAI {

    // 颜色定义
    static final int EMPTY = 0;
    static final int RED = 1;
    static final int YELLOW = 2;
    static final int BLUE = 3;
    static final int HALF_RED = 4;     // 胶囊的左/上半部分
    static final int HALF_YELLOW = 5;
    static final int HALF_BLUE = 6;
    static final int VIRUS_RED = 7;
    static final int VIRUS_YELLOW = 8;
    static final int VIRUS_BLUE = 9;

    static final int ROWS = 16;
    static final int COLS = 8;

    // 方向数组:上下左右
    static final int[] DX = {0, 0, -1, 1};
    static final int[] DY = {-1, 1, 0, 0};

    int[][] board; // 游戏棋盘

    public DrMarioAI() {
        this.board = new int[ROWS][COLS];
    }

    public DrMarioAI(int[][] board) {
        this.board = new int[ROWS][COLS];
        for (int i = 0; i < ROWS; i++) {
            System.arraycopy(board[i], 0, this.board[i], 0, COLS);
        }
    }

    /**
     * 胶囊定义:(color1, color2),水平或垂直放置
     */
    static class Capsule {
        int c1, c2; // 两个半胶囊的颜色
        int x, y;   // 中心位置(水平时c1在左,垂直时c1在上)
        boolean horizontal; // true=水平, false=垂直

        Capsule(int c1, int c2) {
            this.c1 = c1; this.c2 = c2;
        }

        // 获取两个半胶囊的坐标
        int[][] getPositions() {
            if (horizontal) {
                return new int[][]{{y, x}, {y, x + 1}};
            } else {
                return new int[][]{{y, x}, {y + 1, x}};
            }
        }
    }

    /**
     * 落点动作:包含位置和旋转状态
     */
    static class Action {
        int x, rotation; // x为水平位置,rotation为旋转角度(0=水平,1=垂直,2=水平翻转,3=垂直翻转)
        double score;
        int chainDepth;
        int virusesCleared;

        Action(int x, int rotation) {
            this.x = x; this.rotation = rotation;
        }
    }

    /**
     * AI 主决策函数:对所有可能落点进行贪心评估
     */
    public Action decideBestMove(int[][] currentBoard, int c1, int c2) {
        this.board = currentBoard;
        List<Action> candidates = new ArrayList<>();

        // 遍历所有可能的旋转和水平位置
        for (int rot = 0; rot < 4; rot++) {
            Capsule cap = new Capsule(c1, c2);
            cap.horizontal = (rot == 0 || rot == 2);
            // 根据旋转确定两个颜色位置
            if (rot == 2) { int tmp = cap.c1; cap.c1 = cap.c2; cap.c2 = tmp; } // 水平翻转
            if (rot == 3) { int tmp = cap.c1; cap.c1 = cap.c2; cap.c2 = tmp; } // 垂直翻转

            int width = cap.horizontal ? 2 : 1;
            for (int x = 0; x <= COLS - width; x++) {
                cap.x = x;
                cap.y = 0; // 从顶部开始下落

                Action action = evaluateDrop(cap);
                if (action != null) {
                    action.x = x;
                    action.rotation = rot;
                    candidates.add(action);
                }
            }
        }

        if (candidates.isEmpty()) return null;

        // 选择评分最高的动作
        candidates.sort((a, b) -> Double.compare(b.score, a.score));
        return candidates.get(0);
    }

    /**
     * 评估一个胶囊在特定位置的落点效果
     */
    Action evaluateDrop(Capsule cap) {
        // 模拟下落到最底部
        int[][] positions = cap.getPositions();
        int finalY = findDropPosition(positions[0][1], positions[0][0], 
                                       positions[1][1], positions[1][0]);
        if (finalY < 0) return null; // 无法放置(游戏结束)

        // 在模拟棋盘上放置胶囊
        int[][] simBoard = cloneBoard();
        int[][] finalPos = new int[][]{
            {finalY, positions[0][1]},
            {finalY + (positions[1][0] - positions[0][0]), positions[1][1]}
        };

        // 放置两个半胶囊
        simBoard[finalPos[0][0]][finalPos[0][1]] = HALF_RED + cap.c1 - 1;
        simBoard[finalPos[1][0]][finalPos[1][1]] = HALF_RED + cap.c2 - 1;

        // 计算各项评分指标
        Action result = new Action(cap.x, cap.horizontal ? 0 : 1);

        // 1. 预测连锁消除
        ChainResult chain = simulateChainElimination(simBoard);
        result.chainDepth = chain.depth;
        result.virusesCleared = chain.virusesCleared;

        // 2. 计算综合评分
        result.score = computeScore(simBoard, finalPos, chain);

        return result;
    }

    /**
     * 查找胶囊下落后的最终位置
     */
    int findDropPosition(int x1, int y1, int x2, int y2) {
        int dy = 0;
        while (true) {
            int ny1 = y1 + dy + 1;
            int ny2 = y2 + dy + 1;
            if (ny1 >= ROWS || ny2 >= ROWS) break;
            if (board[ny1][x1] != EMPTY || board[ny2][x2] != EMPTY) break;
            dy++;
        }
        return y1 + dy;
    }

    /**
     * 模拟连锁消除过程
     */
    ChainResult simulateChainElimination(int[][] simBoard) {
        int depth = 0;
        int totalViruses = 0;
        boolean changed;

        do {
            changed = false;
            boolean[][] toEliminate = new boolean[ROWS][COLS];

            // 检测所有 4 连以上的同色区域
            for (int y = 0; y < ROWS; y++) {
                for (int x = 0; x < COLS; x++) {
                    if (simBoard[y][x] == EMPTY) continue;
                    int color = normalizeColor(simBoard[y][x]);

                    // 水平检测
                    int countH = 1;
                    for (int dx = 1; x + dx < COLS && normalizeColor(simBoard[y][x + dx]) == color; dx++) countH++;
                    for (int dx = 1; x - dx >= 0 && normalizeColor(simBoard[y][x - dx]) == color; dx++) countH++;
                    if (countH >= 4) {
                        for (int dx = 0; x + dx < COLS && normalizeColor(simBoard[y][x + dx]) == color; dx++) toEliminate[y][x + dx] = true;
                        for (int dx = 1; x - dx >= 0 && normalizeColor(simBoard[y][x - dx]) == color; dx++) toEliminate[y][x - dx] = true;
                    }

                    // 垂直检测
                    int countV = 1;
                    for (int dy = 1; y + dy < ROWS && normalizeColor(simBoard[y + dy][x]) == color; dy++) countV++;
                    for (int dy = 1; y - dy >= 0 && normalizeColor(simBoard[y - dy][x]) == color; dy++) countV++;
                    if (countV >= 4) {
                        for (int dy = 0; y + dy < ROWS && normalizeColor(simBoard[y + dy][x]) == color; dy++) toEliminate[y + dy][x] = true;
                        for (int dy = 1; y - dy >= 0 && normalizeColor(simBoard[y - dy][x]) == color; dy++) toEliminate[y - dy][x] = true;
                    }
                }
            }

            // 统计消除的病毒数
            for (int y = 0; y < ROWS; y++) {
                for (int x = 0; x < COLS; x++) {
                    if (toEliminate[y][x]) {
                        if (isVirus(simBoard[y][x])) totalViruses++;
                        simBoard[y][x] = EMPTY;
                        changed = true;
                    }
                }
            }

            // 重力下落:让上方胶囊落下
            if (changed) {
                applyGravity(simBoard);
                depth++;
            }
        } while (changed);

        return new ChainResult(depth, totalViruses);
    }

    /**
     * 应用重力:让悬浮的胶囊下落
     */
    void applyGravity(int[][] simBoard) {
        for (int x = 0; x < COLS; x++) {
            int writePos = ROWS - 1;
            for (int y = ROWS - 1; y >= 0; y--) {
                if (simBoard[y][x] != EMPTY) {
                    if (writePos != y) {
                        simBoard[writePos][x] = simBoard[y][x];
                        simBoard[y][x] = EMPTY;
                    }
                    writePos--;
                }
            }
        }
    }

    /**
     * 综合评分函数
     */
    double computeScore(int[][] simBoard, int[][] placedPos, ChainResult chain) {
        double score = 0;

        // 1. 消除病毒得分(最重要)
        score += chain.virusesCleared * 100;

        // 2. 连锁深度加分
        score += chain.depth * chain.depth * 50; // 连锁深度平方加分

        // 3. 连通潜力:放置的胶囊与已有同色半胶囊相邻的数量
        for (int[] pos : placedPos) {
            int y = pos[0], x = pos[1];
            int color = normalizeColor(simBoard[y][x]);
            for (int d = 0; d < 4; d++) {
                int nx = x + DX[d], ny = y + DY[d];
                if (inBounds(ny, nx) && normalizeColor(simBoard[ny][nx]) == color) {
                    score += 10;
                }
            }
        }

        // 4. 高度惩罚:落点越高越危险
        int maxHeight = Math.min(placedPos[0][0], placedPos[1][0]);
        score -= maxHeight * 2;

        // 5. 空洞惩罚:放置后下方是否有空隙
        for (int[] pos : placedPos) {
            int y = pos[0], x = pos[1];
            for (int dy = y + 1; dy < ROWS; dy++) {
                if (simBoard[dy][x] == EMPTY) {
                    score -= 15; // 下方有空洞,扣分
                    break;
                }
            }
        }

        // 6. 平整度奖励:同一行填满的格子越多越好
        for (int y = 0; y < ROWS; y++) {
            int filled = 0;
            for (int x = 0; x < COLS; x++) {
                if (simBoard[y][x] != EMPTY) filled++;
            }
            if (filled == COLS) score += 20; // 整行填满奖励
        }

        return score;
    }

    int normalizeColor(int cell) {
        if (cell == EMPTY) return EMPTY;
        if (cell == HALF_RED || cell == VIRUS_RED) return RED;
        if (cell == HALF_YELLOW || cell == VIRUS_YELLOW) return YELLOW;
        if (cell == HALF_BLUE || cell == VIRUS_BLUE) return BLUE;
        return cell;
    }

    boolean isVirus(int cell) {
        return cell == VIRUS_RED || cell == VIRUS_YELLOW || cell == VIRUS_BLUE;
    }

    boolean inBounds(int y, int x) {
        return y >= 0 && y < ROWS && x >= 0 && x < COLS;
    }

    int[][] cloneBoard() {
        int[][] clone = new int[ROWS][COLS];
        for (int i = 0; i < ROWS; i++) {
            System.arraycopy(board[i], 0, clone[i], 0, COLS);
        }
        return clone;
    }

    static class ChainResult {
        int depth;
        int virusesCleared;
        ChainResult(int d, int v) { depth = d; virusesCleared = v; }
    }

    // 主程序:演示 AI 决策
    public static void main(String[] args) {
        int[][] board = new int[ROWS][COLS];

        // 初始化一些病毒
        board[14][2] = VIRUS_RED;
        board[14][3] = VIRUS_RED;
        board[13][2] = VIRUS_YELLOW;
        board[15][5] = VIRUS_BLUE;
        board[14][5] = VIRUS_BLUE;
        board[13][5] = VIRUS_BLUE;

        // 放置一些已有胶囊
        board[15][2] = HALF_RED;
        board[15][3] = HALF_RED;
        board[14][2] = VIRUS_RED; // 被覆盖,重新设置
        board[14][3] = VIRUS_RED;

        // 重新设置病毒(避免被覆盖)
        board[14][2] = VIRUS_RED;
        board[14][3] = VIRUS_RED;
        board[13][2] = VIRUS_YELLOW;
        board[15][5] = VIRUS_BLUE;
        board[14][5] = VIRUS_BLUE;
        board[13][5] = VIRUS_BLUE;

        DrMarioAI ai = new DrMarioAI(board);

        // AI 决策:当前下落的是红色-蓝色胶囊
        Action best = ai.decideBestMove(board, RED, BLUE);

        System.out.println("=== 马里奥医生 AI 决策结果 ===");
        if (best != null) {
            System.out.println("最佳动作:水平位置=" + best.x + ", 旋转=" + best.rotation);
            System.out.println("预测连锁深度:" + best.chainDepth);
            System.out.println("预测消除病毒数:" + best.virusesCleared);
            System.out.println("综合评分:" + String.format("%.2f", best.score));
        } else {
            System.out.println("无可用动作,游戏结束!");
        }
    }
}

算法解析

贪心评估 vs 全局最优

马里奥医生的状态空间极其庞大(每步约 40 种动作,病毒分布千变万化),精确求解全局最优在计算上不可行。贪心评估策略在局部选择评分最高的落点,虽然不能保证全局最优,但在实际游戏中表现优异:
– 优先消除病毒,减少失败风险
– 追求连锁反应,获得高分
– 维持盘面平整,为后续操作留有余地

连锁预测的关键

连锁预测是 AI 的核心难点。我们的 simulateChainElimination 方法通过迭代消除+重力模拟准确预测连锁深度。关键在于:
1. 消除后要正确处理胶囊的拆分(一个胶囊的两个半部分可能分别属于不同消除组)
2. 重力下落时要保持胶囊的相对位置关系
3. 连锁评分采用平方加权(深度² × 50),鼓励 AI 追求高连锁而非浅层多次消除

评估函数的调参艺术

评估函数中各项权重的设置直接影响 AI 风格:
– 提高病毒消除权重 → AI 更保守,优先清盘
– 提高连锁深度权重 → AI 更激进,追求高分但风险更大
– 提高高度惩罚 → AI 更谨慎,避免堆叠过高

通过调整权重,可以训练出不同风格的 AI:速攻型、保守型、高分型。

复杂度分析

操作 时间复杂度 空间复杂度
生成候选动作 O(4 × COLS) = O(COLS) O(COLS)
单次落点模拟 O(ROWS × COLS) O(ROWS × COLS)
连锁消除检测 O(K × ROWS × COLS),K 为连锁次数 O(ROWS × COLS)
完整决策 O(COLS × K × ROWS × COLS) O(ROWS × COLS)

对于 16×8 的棋盘,单次决策约需评估 30-40 个落点,每个落点模拟 1-3 次连锁,总计算量在数万级别,现代 CPU 可在毫秒级完成。

扩展思路

  1. 蒙特卡洛树搜索(MCTS):用 MCTS 替代贪心评估,考虑未来多步的期望收益
  2. 胶囊序列预测:根据已出现的胶囊序列,用马尔可夫链预测下一个胶囊颜色
  3. 深度学习评估:用 CNN 训练一个局面评估网络,替代手工设计的评分函数

总结

本文实现了一个完整的马里奥医生 AI,核心亮点在于:
– 多维度贪心评估:综合考虑消除、连锁、高度、空洞、平整度五个维度
– 精确连锁模拟:通过迭代消除+重力模拟准确预测连锁深度
– 物理下落模拟:正确处理胶囊重力下落和位置关系

读者可以从中学习到:贪心算法的评估函数设计、消除类游戏的连锁检测机制、以及游戏 AI 的决策框架。