每日算法 — 使用java实现三消游戏:BFS连通检测与级联消除策略

三消游戏(Match-3)是休闲益智游戏的经典类型,代表作包括《宝石迷阵》《糖果传奇》等。玩家通过交换相邻元素,使三个或以上相同元素连成一线即可消除,并触发级联反应。本文用Java实现三消游戏的核心算法引擎。

核心规则建模

游戏面板通常是一个 M × N 的二维网格,每个格子包含一种颜色/类型的宝石。玩家可以交换水平或垂直相邻的两个宝石,如果交换后形成三个或以上同色连线,则触发消除。

public class Match3Engine {
    // 宝石类型枚举
    enum GemType {
        EMPTY(0), RED(1), BLUE(2), GREEN(3), YELLOW(4), PURPLE(5), ORANGE(6);
        final int code;
        GemType(int c) { this.code = c; }
    }

    private final int rows, cols;
    private GemType[][] board;
    private final Random random = new Random();

    public Match3Engine(int rows, int cols) {
        this.rows = rows;
        this.cols = cols;
        this.board = new GemType[rows][cols];
        initializeBoard();
    }

    /**
     * 初始化面板,确保没有初始匹配
     */
    private void initializeBoard() {
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                GemType gem;
                do {
                    gem = randomGem();
                } while (wouldFormMatch(r, c, gem));
                board[r][c] = gem;
            }
        }
    }

    private GemType randomGem() {
        return GemType.values()[1 + random.nextInt(6)]; // 排除EMPTY
    }

    /**
     * 检查在(r,c)放置gem是否会形成匹配
     */
    private boolean wouldFormMatch(int r, int c, GemType gem) {
        // 临时放置检查
        GemType original = board[r][c];
        board[r][c] = gem;
        boolean match = hasMatchAt(r, c);
        board[r][c] = original;
        return match;
    }
}

BFS连通检测算法

消除的核心是检测所有满足匹配条件的连通区域。我们使用BFS从每个宝石出发,搜索水平或垂直方向的同色连续序列。

public class Match3Engine {

    /**
     * 使用BFS查找所有可消除的连通组
     * @return 所有匹配组的坐标集合
     */
    public List<List<int[]>> findAllMatches() {
        boolean[][] visited = new boolean[rows][cols];
        List<List<int[]>> allMatches = new ArrayList<>();

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (visited[r][c] || board[r][c] == GemType.EMPTY) continue;

                // BFS搜索水平方向连续同色
                List<int[]> horizontal = new ArrayList<>();
                horizontal.add(new int[]{r, c});

                // 向右扩展
                for (int nc = c + 1; nc < cols && board[r][nc] == board[r][c]; nc++) {
                    horizontal.add(new int[]{r, nc});
                }
                // 向左扩展
                for (int nc = c - 1; nc >= 0 && board[r][nc] == board[r][c]; nc--) {
                    horizontal.add(new int[]{r, nc});
                }

                // BFS搜索垂直方向连续同色
                List<int[]> vertical = new ArrayList<>();
                vertical.add(new int[]{r, c});

                // 向下扩展
                for (int nr = r + 1; nr < rows && board[nr][c] == board[r][c]; nr++) {
                    vertical.add(new int[]{nr, c});
                }
                // 向上扩展
                for (int nr = r - 1; nr >= 0 && board[nr][c] == board[r][c]; nr--) {
                    vertical.add(new int[]{nr, c});
                }

                // 如果任一方向达到3个或以上,记录匹配
                if (horizontal.size() >= 3) {
                    allMatches.add(horizontal);
                    for (int[] pos : horizontal) visited[pos[0]][pos[1]] = true;
                }
                if (vertical.size() >= 3) {
                    allMatches.add(vertical);
                    for (int[] pos : vertical) visited[pos[0]][pos[1]] = true;
                }
            }
        }

        return allMatches;
    }

    /**
     * 检查特定位置是否有匹配(用于交换合法性判断)
     */
    private boolean hasMatchAt(int r, int c) {
        GemType gem = board[r][c];
        if (gem == GemType.EMPTY) return false;

        // 水平计数
        int hCount = 1;
        for (int nc = c + 1; nc < cols && board[r][nc] == gem; nc++) hCount++;
        for (int nc = c - 1; nc >= 0 && board[r][nc] == gem; nc--) hCount++;

        // 垂直计数
        int vCount = 1;
        for (int nr = r + 1; nr < rows && board[nr][c] == gem; nr++) vCount++;
        for (int nr = r - 1; nr >= 0 && board[nr][c] == gem; nr--) vCount++;

        return hCount >= 3 || vCount >= 3;
    }
}

级联消除与重力下落

消除后上方的宝石需要下落填补空缺,新的宝石从顶部生成。这个过程中可能再次形成匹配,触发级联反应。

public class Match3Engine {

    /**
     * 执行完整的消除-下落-级联流程
     * @return 每次消除的得分记录
     */
    public List<CascadeResult> executeCascades() {
        List<CascadeResult> cascades = new ArrayList<>();
        int round = 0;

        while (true) {
            List<List<int[]>> matches = findAllMatches();
            if (matches.isEmpty()) break;

            round++;
            int gemsCleared = 0;
            Set<int[]> uniquePositions = new HashSet<>();

            // 计算得分:基础分 × Combo加成
            int baseScore = 0;
            for (List<int[]> group : matches) {
                gemsCleared += group.size();
                uniquePositions.addAll(group);
                // 匹配长度奖励:3个=30分, 4个=60分, 5个=100分
                baseScore += group.size() * 10 + (group.size() - 3) * 20;
            }

            int comboMultiplier = Math.min(round, 8); // Combo上限8倍
            int totalScore = baseScore * comboMultiplier;

            cascades.add(new CascadeResult(round, gemsCleared, totalScore, 
                new ArrayList<>(uniquePositions)));

            // 标记消除位置
            for (int[] pos : uniquePositions) {
                board[pos[0]][pos[1]] = GemType.EMPTY;
            }

            // 重力下落
            applyGravity();

            // 顶部生成新宝石
            fillTop();
        }

        return cascades;
    }

    /**
     * 重力下落:每列从下往上检查,非空宝石下落到最低可用位置
     */
    private void applyGravity() {
        for (int c = 0; c < cols; c++) {
            int writePos = rows - 1; // 从底部开始写入

            for (int r = rows - 1; r >= 0; r--) {
                if (board[r][c] != GemType.EMPTY) {
                    if (r != writePos) {
                        board[writePos][c] = board[r][c];
                        board[r][c] = GemType.EMPTY;
                    }
                    writePos--;
                }
            }
        }
    }

    /**
     * 填充顶部空位
     */
    private void fillTop() {
        for (int c = 0; c < cols; c++) {
            for (int r = 0; r < rows; r++) {
                if (board[r][c] == GemType.EMPTY) {
                    board[r][c] = randomGem();
                } else {
                    break; // 遇到非空停止,因为重力已保证连续性
                }
            }
        }
    }

    static class CascadeResult {
        int round;
        int gemsCleared;
        int score;
        List<int[]> positions;

        CascadeResult(int r, int g, int s, List<int[]> p) {
            this.round = r;
            this.gemsCleared = g;
            this.score = s;
            this.positions = p;
        }

        @Override
        public String toString() {
            return String.format("第%d轮消除: %d个宝石, 得分%d", round, gemsCleared, score);
        }
    }
}

最优交换搜索策略

为了构建AI玩家,需要评估所有可能的交换并选择最优的一步。这涉及到对每次交换后的级联结果进行预测。

public class Match3Engine {

    /**
     * 评估所有合法交换,返回最优动作
     * 使用贪心策略评估未来1-2步的级联收益
     */
    public SwapMove findBestMove() {
        SwapMove bestMove = null;
        int bestScore = -1;

        // 遍历所有可能的交换
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                // 尝试右交换
                if (c + 1 < cols) {
                    int score = evaluateSwap(r, c, r, c + 1);
                    if (score > bestScore) {
                        bestScore = score;
                        bestMove = new SwapMove(r, c, r, c + 1, score);
                    }
                }
                // 尝试下交换
                if (r + 1 < rows) {
                    int score = evaluateSwap(r, c, r + 1, c);
                    if (score > bestScore) {
                        bestScore = score;
                        bestMove = new SwapMove(r, c, r + 1, c, score);
                    }
                }
            }
        }

        return bestMove;
    }

    /**
     * 评估一次交换的预期收益
     * 方法:模拟交换并执行级联,计算总得分
     */
    private int evaluateSwap(int r1, int c1, int r2, int c2) {
        // 保存当前状态
        GemType[][] backup = deepCopy(board);

        // 执行交换
        GemType temp = board[r1][c1];
        board[r1][c1] = board[r2][c2];
        board[r2][c2] = temp;

        // 检查是否形成匹配
        if (!hasMatchAt(r1, c1) && !hasMatchAt(r2, c2)) {
            // 无匹配,撤销交换
            board = backup;
            return -1; // 非法移动
        }

        // 执行级联并计算总分
        List<CascadeResult> cascades = executeCascades();
        int totalScore = cascades.stream().mapToInt(c -> c.score).sum();

        // 额外奖励:创造特殊宝石的机会
        totalScore += evaluateSpecialGemPotential(r1, c1, r2, c2);

        // 恢复状态
        board = backup;
        return totalScore;
    }

    /**
     * 评估创造特殊宝石的潜力(4连、5连、T型、L型)
     */
    private int evaluateSpecialGemPotential(int r1, int c1, int r2, int c2) {
        int potential = 0;
        // 4连奖励
        potential += countDirection(r1, c1, 0, 1) + countDirection(r1, c1, 1, 0) >= 4 ? 50 : 0;
        potential += countDirection(r2, c2, 0, 1) + countDirection(r2, c2, 1, 0) >= 4 ? 50 : 0;
        // T型和L型检测(简化版)
        if (hasLShape(r1, c1) || hasLShape(r2, c2)) potential += 100;
        return potential;
    }

    private int countDirection(int r, int c, int dr, int dc) {
        GemType gem = board[r][c];
        int count = 1;
        int nr = r + dr, nc = c + dc;
        while (inBounds(nr, nc) && board[nr][nc] == gem) {
            count++;
            nr += dr;
            nc += dc;
        }
        nr = r - dr; nc = c - dc;
        while (inBounds(nr, nc) && board[nr][nc] == gem) {
            count++;
            nr -= dr;
            nc -= dc;
        }
        return count;
    }

    private boolean hasLShape(int r, int c) {
        int h = countDirection(r, c, 0, 1);
        int v = countDirection(r, c, 1, 0);
        return h >= 3 && v >= 3;
    }

    private boolean inBounds(int r, int c) {
        return r >= 0 && r < rows && c >= 0 && c < cols;
    }

    private GemType[][] deepCopy(GemType[][] src) {
        GemType[][] copy = new GemType[src.length][];
        for (int i = 0; i < src.length; i++) {
            copy[i] = src[i].clone();
        }
        return copy;
    }

    static class SwapMove {
        int r1, c1, r2, c2;
        int expectedScore;

        SwapMove(int r1, int c1, int r2, int c2, int score) {
            this.r1 = r1; this.c1 = c1;
            this.r2 = r2; this.c2 = c2;
            this.expectedScore = score;
        }

        @Override
        public String toString() {
            return String.format("交换(%d,%d)和(%d,%d), 预期得分:%d", r1, c1, r2, c2, expectedScore);
        }
    }
}

运行演示

public class Match3Demo {
    public static void main(String[] args) {
        Match3Engine engine = new Match3Engine(8, 8);

        System.out.println("=== 初始面板 ===");
        engine.printBoard();

        // AI寻找最优移动
        Match3Engine.SwapMove best = engine.findBestMove();
        System.out.println("\nAI推荐移动: " + best);

        // 执行移动并触发级联
        if (best != null) {
            engine.swap(best.r1, best.c1, best.r2, best.c2);
            List<Match3Engine.CascadeResult> cascades = engine.executeCascades();

            System.out.println("\n=== 级联消除结果 ===");
            int total = 0;
            for (Match3Engine.CascadeResult c : cascades) {
                System.out.println(c);
                total += c.score;
            }
            System.out.println("总得分: " + total);
        }
    }
}

算法复杂度分析

  • BFS匹配检测:遍历每个格子,对每个格子进行四方向的线性扫描,时间复杂度 O(rows × cols × max(rows, cols))。对于常见的 8×8 面板,完全可接受。
  • 级联消除:每轮消除后重力和填充操作是 O(rows × cols),级联轮数通常不超过5轮。
  • 最优交换搜索:需要评估 2 × rows × cols 种交换,每种评估需要一次完整的级联模拟,最坏情况 O(rows² × cols² × max(rows, cols))。对于小面板可用,大面板需要引入剪枝或蒙特卡洛采样。

扩展方向

  1. 特殊宝石系统:实现炸弹、激光、彩虹球等特殊宝石的生成和爆炸效果
  2. 蒙特卡洛树搜索:用MCTS评估多步后的最优策略,而非单步贪心
  3. 模式识别:预计算常见有利图案(如”可形成4连的预备形状”)
  4. 关卡生成:使用约束满足算法生成有解且有趣的初始面板