每日算法 — 使用java实现羊了个羊:栈结构管理与DFS可消除对检测

羊了个羊是一款爆火的益智消除游戏,核心机制是将多层堆叠的图案卡牌逐层暴露,玩家每次选择三张相同图案的卡牌放入下方槽位,凑齐三张即可消除。当槽位填满(通常7张)且无法消除时游戏结束。本文用Java实现其核心算法引擎。

游戏机制建模

游戏的核心数据结构是一个多层堆叠的卡牌系统,每张卡牌位于特定层,被上层的卡牌覆盖时不可点击。

public class SheepGameEngine {

    // 卡牌图案类型
    enum Pattern {
        CARROT, CORN, GRASS, BRUSH, GLOVE, SCISSORS, 
        BELL, FIRE, MILK, WOOL, HAY, FORK, BUCKET, PITCHFORK
    }

    static class Card {
        Pattern pattern;
        int layer;           // 所在层数(0为最底层)
        int x, y;            // 网格坐标
        boolean isRemoved;   // 是否已被消除

        Card(Pattern p, int layer, int x, int y) {
            this.pattern = p;
            this.layer = layer;
            this.x = x;
            this.y = y;
        }
    }

    // 游戏配置
    static final int SLOT_CAPACITY = 7;    // 槽位容量
    static final int MATCH_COUNT = 3;      // 消除所需相同卡牌数
    static final int BOARD_WIDTH = 8;
    static final int BOARD_HEIGHT = 10;
    static final int MAX_LAYERS = 5;

    private final List<Card> allCards;
    private final List<Card> slot;         // 下方槽位(用List模拟栈)
    private final boolean[][][] occupied;  // 三维占用标记 [layer][y][x]

    public SheepGameEngine() {
        this.allCards = new ArrayList<>();
        this.slot = new ArrayList<>();
        this.occupied = new boolean[MAX_LAYERS][BOARD_HEIGHT][BOARD_WIDTH];
    }
}

卡牌堆叠与覆盖检测

每张卡牌可能被同层或上层的其他卡牌部分覆盖。我们使用网格离散化方法检测覆盖关系。

public class SheepGameEngine {

    /**
     * 生成随机但可解的关卡布局
     * 规则:总卡牌数必须是3的倍数,每种图案出现次数也是3的倍数
     */
    public void generateLevel(int totalCards) {
        if (totalCards % 3 != 0) {
            throw new IllegalArgumentException("卡牌总数必须是3的倍数");
        }

        allCards.clear();
        slot.clear();

        // 确定每种图案的数量(每种至少3张,且为3的倍数)
        int patternCount = Pattern.values().length;
        int cardsPerPattern = totalCards / patternCount;
        if (cardsPerPattern % 3 != 0) {
            cardsPerPattern = (cardsPerPattern / 3) * 3;
        }

        List<Pattern> deck = new ArrayList<>();
        for (Pattern p : Pattern.values()) {
            for (int i = 0; i < cardsPerPattern; i++) {
                deck.add(p);
            }
        }
        Collections.shuffle(deck);

        // 分层放置卡牌
        int cardIndex = 0;
        Random rand = new Random();

        for (int layer = 0; layer < MAX_LAYERS && cardIndex < deck.size(); layer++) {
            // 每层卡牌数量递减,形成金字塔结构
            int cardsInLayer = Math.min(
                deck.size() - cardIndex,
                (MAX_LAYERS - layer) * 6 + 4
            );

            for (int i = 0; i < cardsInLayer; i++) {
                // 随机位置,确保不与同层卡牌完全重叠
                int x, y;
                int attempts = 0;
                do {
                    x = rand.nextInt(BOARD_WIDTH - 1);
                    y = rand.nextInt(BOARD_HEIGHT - 1);
                    attempts++;
                } while (isOccupied(layer, x, y) && attempts < 50);

                if (attempts >= 50) continue;

                Card card = new Card(deck.get(cardIndex++), layer, x, y);
                allCards.add(card);
                markOccupied(layer, x, y);
            }
        }

        // 确保有解性验证(简化版)
        if (!hasValidSolution()) {
            generateLevel(totalCards); // 重新生成
        }
    }

    /**
     * 检查网格位置是否被占用
     * 每张卡牌占据2×2的网格区域
     */
    private boolean isOccupied(int layer, int x, int y) {
        for (int dy = 0; dy < 2; dy++) {
            for (int dx = 0; dx < 2; dx++) {
                if (y + dy >= BOARD_HEIGHT || x + dx >= BOARD_WIDTH) return true;
                if (occupied[layer][y + dy][x + dx]) return true;
            }
        }
        return false;
    }

    private void markOccupied(int layer, int x, int y) {
        for (int dy = 0; dy < 2; dy++) {
            for (int dx = 0; dx < 2; dx++) {
                if (y + dy < BOARD_HEIGHT && x + dx < BOARD_WIDTH) {
                    occupied[layer][y + dy][x + dx] = true;
                }
            }
        }
    }
}

DFS可消除对检测

判断一张卡牌是否可以被点击,需要检查它是否被上层的任何卡牌覆盖。然后使用DFS搜索当前状态下所有可能的消除序列。

public class SheepGameEngine {

    /**
     * 检查卡牌是否被上层覆盖
     */
    public boolean isCovered(Card card) {
        if (card.isRemoved) return false;

        // 检查更高层的卡牌是否与其位置重叠
        for (Card other : allCards) {
            if (other.isRemoved || other.layer <= card.layer) continue;

            // 2×2卡牌区域的碰撞检测
            if (Math.abs(other.x - card.x) < 2 && Math.abs(other.y - card.y) < 2) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取当前所有可点击的卡牌
     */
    public List<Card> getClickableCards() {
        return allCards.stream()
            .filter(c -> !c.isRemoved && !isCovered(c))
            .collect(Collectors.toList());
    }

    /**
     * 使用DFS搜索是否存在可行的消除路径
     * @return 是否存在从当前状态到清空所有卡牌的路径
     */
    public boolean hasValidSolution() {
        return dfsSolve(new ArrayList<>(slot), new HashSet<>(allCards));
    }

    /**
     * DFS求解:尝试所有可能的点击序列
     * 使用剪枝策略减少搜索空间
     */
    private boolean dfsSolve(List<Card> currentSlot, Set<Card> remaining) {
        // 终止条件:所有卡牌消除
        if (remaining.isEmpty()) return true;

        // 剪枝:槽位已满且无法消除
        if (currentSlot.size() >= SLOT_CAPACITY && !canEliminate(currentSlot)) {
            return false;
        }

        // 获取可点击的卡牌
        List<Card> clickable = remaining.stream()
            .filter(c -> !isCovered(c))
            .collect(Collectors.toList());

        if (clickable.isEmpty()) return false;

        // 按图案分组,优先选择能形成消除的卡牌
        Map<Pattern, List<Card>> grouped = new HashMap<>();
        for (Card c : clickable) {
            grouped.computeIfAbsent(c.pattern, k -> new ArrayList<>()).add(c);
        }

        // 优先尝试:1) 放入槽位能立即消除的 2) 槽位中已有同图案的
        List<Card> priorityOrder = new ArrayList<>();

        for (Card c : clickable) {
            long sameInSlot = currentSlot.stream().filter(s -> s.pattern == c.pattern).count();
            if (sameInSlot >= 2) {
                priorityOrder.add(0, c); // 能立即消除,最高优先级
            } else if (sameInSlot >= 1) {
                priorityOrder.add(c); // 有助于后续消除
            } else {
                priorityOrder.add(c);
            }
        }

        // 遍历尝试
        for (Card card : priorityOrder) {
            // 模拟点击
            currentSlot.add(card);
            remaining.remove(card);

            // 检查是否可以消除
            boolean eliminated = false;
            if (canEliminate(currentSlot)) {
                eliminatePattern(currentSlot, card.pattern);
                eliminated = true;
            }

            // 递归
            if (currentSlot.size() < SLOT_CAPACITY && dfsSolve(currentSlot, remaining)) {
                return true;
            }

            // 回溯
            if (eliminated) {
                // 恢复被消除的卡牌(简化处理:这里需要更精细的回溯)
                restoreEliminated(card.pattern);
            }
            currentSlot.remove(currentSlot.size() - 1);
            remaining.add(card);
        }

        return false;
    }

    /**
     * 检查槽位中是否有可消除的三张相同图案
     */
    private boolean canEliminate(List<Card> currentSlot) {
        Map<Pattern, Long> count = currentSlot.stream()
            .collect(Collectors.groupingBy(c -> c.pattern, Collectors.counting()));
        return count.values().stream().anyMatch(c -> c >= 3);
    }

    /**
     * 消除槽位中指定图案的三张卡牌
     */
    private void eliminatePattern(List<Card> currentSlot, Pattern pattern) {
        currentSlot.removeIf(c -> c.pattern == pattern);
    }

    private void restoreEliminated(Pattern pattern) {
        // 在实际实现中需要维护消除历史栈
    }
}

最优决策搜索

为了构建AI玩家,需要评估每次点击的长期价值,而不仅仅是短期收益。我们使用带启发函数的搜索策略。

public class SheepGameEngine {

    /**
     * 评估状态的优劣
     * 考虑因素:槽位占用率、可点击卡牌数、图案分布均衡性
     */
    private int evaluateState(List<Card> currentSlot, Set<Card> remaining) {
        int score = 0;

        // 1. 槽位空闲空间奖励
        score += (SLOT_CAPACITY - currentSlot.size()) * 10;

        // 2. 可点击卡牌数量奖励
        List<Card> clickable = remaining.stream()
            .filter(c -> !isCovered(c))
            .collect(Collectors.toList());
        score += clickable.size() * 5;

        // 3. 槽位中图案多样性惩罚(同类聚集不利)
        Map<Pattern, Long> slotCount = currentSlot.stream()
            .collect(Collectors.groupingBy(c -> c.pattern, Collectors.counting()));
        for (long count : slotCount.values()) {
            if (count == 2) score -= 15; // 两张相同较危险
            if (count >= 3) score += 50; // 可以消除很好
        }

        // 4. 剩余卡牌中各图案数量的均衡性
        Map<Pattern, Long> remainCount = remaining.stream()
            .collect(Collectors.groupingBy(c -> c.pattern, Collectors.counting()));
        for (long count : remainCount.values()) {
            if (count % 3 != 0) score -= 5; // 不能被3整除的图案难以处理
        }

        return score;
    }

    /**
     * 使用启发式搜索找到最优的下一步
     * 搜索深度为3,评估每种选择后的状态价值
     */
    public Card findBestMove() {
        List<Card> clickable = getClickableCards();
        if (clickable.isEmpty()) return null;

        Card bestCard = null;
        int bestValue = Integer.MIN_VALUE;

        for (Card card : clickable) {
            int value = minimaxEvaluate(card, 3, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
            if (value > bestValue) {
                bestValue = value;
                bestCard = card;
            }
        }

        return bestCard;
    }

    /**
     * 带Alpha-Beta剪枝的极小极大评估
     */
    private int minimaxEvaluate(Card move, int depth, int alpha, int beta, boolean isMax) {
        if (depth == 0 || allCards.stream().allMatch(c -> c.isRemoved)) {
            return evaluateState(slot, new HashSet<>(allCards));
        }

        // 模拟执行move
        slot.add(move);
        move.isRemoved = true;
        boolean eliminated = false;

        if (canEliminate(slot)) {
            eliminatePattern(slot, move.pattern);
            eliminated = true;
        }

        int value;
        if (isMax) {
            value = Integer.MIN_VALUE;
            List<Card> nextClickable = getClickableCards();
            for (Card next : nextClickable) {
                value = Math.max(value, minimaxEvaluate(next, depth - 1, alpha, beta, false));
                alpha = Math.max(alpha, value);
                if (beta <= alpha) break;
            }
            if (nextClickable.isEmpty()) {
                value = evaluateState(slot, new HashSet<>(allCards));
            }
        } else {
            value = Integer.MAX_VALUE;
            List<Card> nextClickable = getClickableCards();
            for (Card next : nextClickable) {
                value = Math.min(value, minimaxEvaluate(next, depth - 1, alpha, beta, true));
                beta = Math.min(beta, value);
                if (beta <= alpha) break;
            }
            if (nextClickable.isEmpty()) {
                value = evaluateState(slot, new HashSet<>(allCards));
            }
        }

        // 回溯
        if (eliminated) {
            // 恢复被消除的卡牌
            // 简化处理:实际需维护精确状态
        }
        move.isRemoved = false;
        slot.remove(move);

        return value;
    }

    /**
     * 执行玩家点击
     */
    public boolean clickCard(Card card) {
        if (card.isRemoved || isCovered(card)) return false;
        if (slot.size() >= SLOT_CAPACITY) return false;

        slot.add(card);
        card.isRemoved = true;

        // 检查消除
        Map<Pattern, Long> count = slot.stream()
            .collect(Collectors.groupingBy(c -> c.pattern, Collectors.counting()));

        for (Map.Entry<Pattern, Long> entry : count.entrySet()) {
            if (entry.getValue() >= 3) {
                final Pattern p = entry.getKey();
                slot.removeIf(c -> c.pattern == p);
                System.out.println("消除了图案: " + p);
            }
        }

        return true;
    }

    /**
     * 检查游戏是否结束
     */
    public boolean isGameOver() {
        if (slot.size() >= SLOT_CAPACITY) {
            // 槽位已满且无法消除
            Map<Pattern, Long> count = slot.stream()
                .collect(Collectors.groupingBy(c -> c.pattern, Collectors.counting()));
            return count.values().stream().noneMatch(c -> c >= 3);
        }
        return false;
    }

    /**
     * 检查是否获胜
     */
    public boolean isWin() {
        return allCards.stream().allMatch(c -> c.isRemoved);
    }
}

运行演示

public class SheepGameDemo {
    public static void main(String[] args) {
        SheepGameEngine engine = new SheepGameEngine();

        // 生成关卡:45张卡牌(15种图案 × 3张)
        engine.generateLevel(45);

        System.out.println("=== 羊了个羊AI演示 ===");
        int moves = 0;

        while (!engine.isGameOver() && !engine.isWin()) {
            SheepGameEngine.Card best = engine.findBestMove();
            if (best == null) break;

            System.out.printf("第%d步: 选择 %s (位置:层%d, x%d, y%d)%n",
                ++moves, best.pattern, best.layer, best.x, best.y);

            engine.clickCard(best);

            if (engine.isWin()) {
                System.out.println("恭喜通关!总步数: " + moves);
                break;
            }
        }

        if (engine.isGameOver()) {
            System.out.println("游戏结束,槽位已满!");
        }
    }
}

算法复杂度分析

  • 覆盖检测:对于每张卡牌,检查所有更高层卡牌的重叠情况,时间复杂度 O(L × C),L为层数,C为总卡牌数。
  • DFS可解性验证:最坏情况指数级 O(C!),但通过以下剪枝大幅优化:
  • 槽位满剪枝
  • 优先选择能立即消除的卡牌
  • 图案分组减少分支
  • Minimax搜索:深度为d的搜索,分支因子约为可点击卡牌数(通常5-15),使用Alpha-Beta剪枝后实际复杂度约为 O(b^(d/2))。

扩展方向

  1. 关卡生成优化:使用约束满足问题(CSP)求解器确保生成的关卡100%可解
  2. 概率分析:计算随机点击通关的概率,分析不同层数和卡牌分布对难度的影响
  3. 道具策略:实现洗牌、撤回、提示等道具的最优使用时机算法
  4. 多人对战:基于博弈论的实时策略调整,预测对手行为并做出最优响应