每日算法 — 使用java实现敲冰块:网格动态规划与最优破冰序列

引言

敲冰块(Ice Climber)是经典的街机平台游戏,玩家需要控制角色不断向上跳跃,敲碎上方的冰块开辟道路,同时躲避掉落的敌人和障碍物。本文将用Java实现敲冰块游戏的核心算法系统,重点讲解网格动态规划最优路径、冰块碎裂状态转移以及时间约束下的序列决策。通过状态空间建模与DP优化,让读者理解如何在动态破坏环境中规划最高效的上升路线。

核心算法概述

网格动态规划路径规划

将游戏地图建模为带权网格,每个格子的”通过代价”取决于冰块的厚度、敌人的威胁程度和是否已破碎。使用二维DP计算从起点到每一层的最低代价路径。

冰块状态机

每个冰块有三种状态:完整、裂纹、破碎。敲击一次从完整变裂纹,再敲一次从裂纹变破碎。状态转移形成马尔可夫链。

最优破冰序列

当存在多条可行上升路径时,需要权衡”绕路距离”与”破冰时间”,选择总耗时最短的方案。这等价于在扩展状态空间(位置+已破冰集合)上的最短路径问题。

Java实现

1. 游戏地图与冰块状态

import java.util.*;

/**
 * 敲冰块游戏地图
 * 地图由多层平台组成,每层有若干冰块
 */
public class IceClimberMap {
    public static final int EMPTY = 0;      // 空(无平台,会掉落)
    public static final int SOLID = 1;      // 不可破坏的实体平台
    public static final int ICE_FULL = 2;   // 完整冰块
    public static final int ICE_CRACKED = 3; // 裂纹冰块(再敲一次碎)
    public static final int LADDER = 4;     // 梯子(可直接攀爬)
    public static final int ENEMY = 5;      // 敌人
    public static final int BONUS = 6;      // 奖励物品

    /**
     * 冰块状态枚举
     */
    public enum IceState {
        FULL(2),      // 完整,需要敲2次
        CRACKED(3),   // 裂纹,需要敲1次
        BROKEN(0);    // 已破碎,可通过

        public final int mapCode;
        IceState(int mapCode) { this.mapCode = mapCode; }
    }

    private final int[][] grid;
    private final int rows, cols;
    private Point playerStart;
    private final List<Point> bonusPositions;
    private final List<Point> enemyPositions;

    // 动态状态:记录每个冰块被敲击后的状态
    private final IceState[][] iceStates;

    public IceClimberMap(int[][] grid) {
        this.grid = grid;
        this.rows = grid.length;
        this.cols = grid[0].length;
        this.bonusPositions = new ArrayList<>();
        this.enemyPositions = new ArrayList<>();
        this.iceStates = new IceState[rows][cols];

        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (grid[r][c] == PLAYER_START) {
                    playerStart = new Point(r, c);
                    grid[r][c] = EMPTY;
                } else if (grid[r][c] == BONUS) {
                    bonusPositions.add(new Point(r, c));
                } else if (grid[r][c] == ENEMY) {
                    enemyPositions.add(new Point(r, c));
                }

                // 初始化冰块状态
                if (grid[r][c] == ICE_FULL) {
                    iceStates[r][c] = IceState.FULL;
                } else {
                    iceStates[r][c] = null;
                }
            }
        }
    }

    public static final int PLAYER_START = 9;

    /**
     * 判断某位置是否可站立
     */
    public boolean canStand(int r, int c) {
        if (r < 0 || r >= rows || c < 0 || c >= cols) return false;
        int cell = grid[r][c];
        IceState ice = iceStates[r][c];

        // 实体平台、梯子、已破碎冰块、空地上的支撑检查
        if (cell == SOLID || cell == LADDER) return true;
        if (ice == IceState.BROKEN || ice == null) {
            // 检查下方是否有支撑
            if (r + 1 < rows) {
                int below = grid[r + 1][c];
                IceState belowIce = iceStates[r + 1][c];
                if (below == SOLID || below == LADDER || 
                    (belowIce != null && belowIce != IceState.BROKEN)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 判断某位置是否可通行(站立或跳跃经过)
     */
    public boolean isPassable(int r, int c) {
        if (r < 0 || r >= rows || c < 0 || c >= cols) return false;
        int cell = grid[r][c];
        IceState ice = iceStates[r][c];
        return cell == EMPTY || cell == LADDER || cell == BONUS ||
               cell == ENEMY || (ice != null && ice != IceState.FULL) ||
               cell == SOLID;
    }

    /**
     * 敲击某位置的冰块
     * @return 敲击后该位置是否变为可通过
     */
    public boolean hitIce(int r, int c) {
        if (r < 0 || r >= rows || c < 0 || c >= cols) return false;
        IceState state = iceStates[r][c];
        if (state == null) return true; // 不是冰块

        switch (state) {
            case FULL:
                iceStates[r][c] = IceState.CRACKED;
                grid[r][c] = ICE_CRACKED;
                return false;
            case CRACKED:
                iceStates[r][c] = IceState.BROKEN;
                grid[r][c] = EMPTY;
                return true;
            case BROKEN:
                return true;
            default:
                return true;
        }
    }

    /**
     * 获取某位置的可行邻居(考虑跳跃和攀爬)
     */
    public List<Move> getPossibleMoves(Point p) {
        List<Move> moves = new ArrayList<>();
        int r = p.r, c = p.c;

        // 水平移动(左右)
        for (int dc : new int[]{-1, 1}) {
            int nc = c + dc;
            if (nc >= 0 && nc < cols && canStand(r, nc)) {
                moves.add(new Move(p, new Point(r, nc), MoveType.WALK, 1));
            }
        }

        // 向上跳跃(敲碎上方冰块或跳上平台)
        if (r > 0) {
            // 正上方跳跃
            if (isPassable(r - 1, c)) {
                boolean needHit = iceStates[r - 1][c] == IceState.FULL ||
                                 iceStates[r - 1][c] == IceState.CRACKED;
                moves.add(new Move(p, new Point(r - 1, c), 
                    needHit ? MoveType.JUMP_HIT : MoveType.JUMP, needHit ? 2 : 1));
            }

            // 斜上方跳跃(左上、右上)
            for (int dc : new int[]{-1, 1}) {
                int nr = r - 1, nc = c + dc;
                if (nr >= 0 && nc >= 0 && nc < cols && canStand(nr, nc)) {
                    moves.add(new Move(p, new Point(nr, nc), MoveType.JUMP_DIAGONAL, 2));
                }
            }
        }

        // 梯子攀爬
        if (grid[r][c] == LADDER && r > 0 && grid[r - 1][c] == LADDER) {
            moves.add(new Move(p, new Point(r - 1, c), MoveType.CLIMB, 1));
        }

        // 向下掉落(如果下方是空的)
        if (r + 1 < rows && !canStand(r, c) && isPassable(r + 1, c)) {
            moves.add(new Move(p, new Point(r + 1, c), MoveType.FALL, 0));
        }

        return moves;
    }

    /**
     * 移动类型枚举
     */
    public enum MoveType {
        WALK, JUMP, JUMP_HIT, JUMP_DIAGONAL, CLIMB, FALL
    }

    /**
     * 移动动作
     */
    public static class Move {
        public final Point from, to;
        public final MoveType type;
        public final int cost; // 时间代价

        public Move(Point from, Point to, MoveType type, int cost) {
            this.from = from;
            this.to = to;
            this.type = type;
            this.cost = cost;
        }

        @Override
        public String toString() {
            return String.format("%s %s->%s (cost=%d)", type, from, to, cost);
        }
    }

    public Point getPlayerStart() { return playerStart; }
    public int getRows() { return rows; }
    public int getCols() { return cols; }

    public static class Point {
        public final int r, c;
        public Point(int r, int c) { this.r = r; this.c = c; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point)) return false;
            Point p = (Point) o;
            return r == p.r && c == p.c;
        }
        @Override public int hashCode() { return r * 1000 + c; }
        @Override public String toString() { return "(" + r + "," + c + ")"; }
    }
}

2. 分层动态规划路径规划

/**
 * 分层动态规划路径规划器
 * 目标:从起点到达最高层,总代价(时间)最小
 * 状态:dp[r][c] = 到达(r,c)的最小代价
 */
public class LayeredDPPathfinder {

    /**
     * 使用分层DP计算最优上升路径
     * 按层(行)自底向上递推,每层使用Dijkstra-like松弛
     */
    public static Result findOptimalPath(IceClimberMap map) {
        int rows = map.getRows();
        int cols = map.getCols();

        // dp[r][c] = 到达(r,c)的最小代价
        int[][] dp = new int[rows][cols];
        // parent[r][c] = 前驱位置,用于回溯路径
        IceClimberMap.Point[][] parent = new IceClimberMap.Point[rows][cols];

        for (int[] row : dp) {
            Arrays.fill(row, Integer.MAX_VALUE);
        }

        IceClimberMap.Point start = map.getPlayerStart();
        dp[start.r][start.c] = 0;

        // 按层处理,从底层到顶层
        for (int targetRow = 0; targetRow < rows; targetRow++) {
            // 在当前层内使用BFS-like多源松弛
            boolean updated = true;
            int iterations = 0;

            while (updated && iterations < cols * 2) {
                updated = false;
                iterations++;

                for (int c = 0; c < cols; c++) {
                    IceClimberMap.Point current = new IceClimberMap.Point(targetRow, c);
                    if (dp[targetRow][c] == Integer.MAX_VALUE) continue;

                    // 尝试所有可能的移动
                    for (IceClimberMap.Move move : map.getPossibleMoves(current)) {
                        IceClimberMap.Point next = move.to;
                        int newCost = dp[current.r][current.c] + move.cost;

                        if (newCost < dp[next.r][next.c]) {
                            dp[next.r][next.c] = newCost;
                            parent[next.r][next.c] = current;
                            updated = true;
                        }
                    }
                }
            }
        }

        // 找到最高层可达位置中代价最小的
        int bestCost = Integer.MAX_VALUE;
        IceClimberMap.Point bestEnd = null;
        for (int c = 0; c < cols; c++) {
            if (dp[0][c] < bestCost) {
                bestCost = dp[0][c];
                bestEnd = new IceClimberMap.Point(0, c);
            }
        }

        List<IceClimberMap.Point> path = reconstructPath(parent, bestEnd);
        return new Result(path, bestCost, dp);
    }

    /**
     * 带冰块破坏状态的记忆化搜索
     * 由于冰块破坏会改变地图,需要记录已破坏的冰块集合
     */
    public static Result findPathWithIceBreaking(IceClimberMap map) {
        int rows = map.getRows();
        int cols = map.getCols();
        IceClimberMap.Point start = map.getPlayerStart();

        // 使用优先队列的Dijkstra算法
        // 状态:(位置, 已破坏冰块集合的位掩码)
        // 由于冰块可能很多,这里使用启发式简化:只记录每层的关键冰块

        PriorityQueue<State> pq = new PriorityQueue<>(Comparator.comparingInt(s -> s.cost));
        Map<String, Integer> visited = new HashMap<>();

        pq.add(new State(start, 0, 0, null));

        while (!pq.isEmpty()) {
            State current = pq.poll();
            String key = current.pos.r + "," + current.pos.c + "," + current.brokenMask;

            if (visited.containsKey(key) && visited.get(key) <= current.cost) {
                continue;
            }
            visited.put(key, current.cost);

            // 到达顶层即成功
            if (current.pos.r == 0) {
                return new Result(reconstructStatePath(current), current.cost, null);
            }

            // 尝试所有移动
            for (IceClimberMap.Move move : map.getPossibleMoves(current.pos)) {
                int newMask = current.brokenMask;
                int newCost = current.cost + move.cost;

                // 如果是敲碎冰块,更新状态掩码
                if (move.type == IceClimberMap.MoveType.JUMP_HIT) {
                    int iceId = move.to.r * cols + move.to.c;
                    newMask |= (1 << (iceId % 30)); // 简化位掩码
                }

                State next = new State(move.to, newCost, newMask, current);
                pq.add(next);
            }
        }

        return new Result(Collections.emptyList(), Integer.MAX_VALUE, null);
    }

    private static List<IceClimberMap.Point> reconstructPath(
            IceClimberMap.Point[][] parent, IceClimberMap.Point end) {
        List<IceClimberMap.Point> path = new ArrayList<>();
        IceClimberMap.Point current = end;
        while (current != null) {
            path.add(0, current);
            current = parent[current.r][current.c];
        }
        return path;
    }

    private static List<IceClimberMap.Point> reconstructStatePath(State end) {
        List<IceClimberMap.Point> path = new ArrayList<>();
        State current = end;
        while (current != null) {
            path.add(0, current.pos);
            current = current.parent;
        }
        return path;
    }

    private static class State {
        final IceClimberMap.Point pos;
        final int cost;
        final int brokenMask;
        final State parent;

        State(IceClimberMap.Point pos, int cost, int brokenMask, State parent) {
            this.pos = pos;
            this.cost = cost;
            this.brokenMask = brokenMask;
            this.parent = parent;
        }
    }

    public static class Result {
        public final List<IceClimberMap.Point> path;
        public final int totalCost;
        public final int[][] dpTable;

        public Result(List<IceClimberMap.Point> path, int totalCost, int[][] dpTable) {
            this.path = path;
            this.totalCost = totalCost;
            this.dpTable = dpTable;
        }
    }
}

3. 冰块破坏序列优化

/**
 * 冰块破坏序列优化器
 * 比较"绕路"和"破冰"两种策略的代价,选择总代价最小的方案
 */
public class IceBreakingOptimizer {

    /**
     * 路径策略评估
     */
    public static class PathStrategy {
        public final String name;
        public final List<IceClimberMap.Point> path;
        public final int iceHits;      // 需要敲击冰块的次数
        public final int walkSteps;    // 步行步数
        public final int totalTime;    // 总时间 = 敲击时间 + 步行时间

        public PathStrategy(String name, List<IceClimberMap.Point> path,
                           int iceHits, int walkSteps, int totalTime) {
            this.name = name;
            this.path = path;
            this.iceHits = iceHits;
            this.walkSteps = walkSteps;
            this.totalTime = totalTime;
        }
    }

    /**
     * 评估多条候选路径,选择最优策略
     * @param strategies 候选路径策略列表
     * @return 最优策略
     */
    public static PathStrategy selectOptimalStrategy(List<PathStrategy> strategies) {
        if (strategies.isEmpty()) return null;

        PathStrategy best = strategies.get(0);
        for (PathStrategy s : strategies) {
            if (s.totalTime < best.totalTime) {
                best = s;
            }
        }
        return best;
    }

    /**
     * 生成候选路径策略
     * 策略1:直接破冰上升(最短垂直距离)
     * 策略2:绕路寻找梯子或已破碎的通道
     * 策略3:混合策略:先绕路收集奖励,再破冰上升
     */
    public static List<PathStrategy> generateStrategies(IceClimberMap map) {
        List<PathStrategy> strategies = new ArrayList<>();

        // 策略1:使用DP直接计算最优路径
        LayeredDPPathfinder.Result dpResult = LayeredDPPathfinder.findOptimalPath(map);
        if (!dpResult.path.isEmpty()) {
            int hits = countIceHits(map, dpResult.path);
            strategies.add(new PathStrategy(
                "直接破冰上升", dpResult.path, hits, 
                dpResult.path.size() - hits, dpResult.totalCost));
        }

        // 策略2:绕路寻找梯子
        List<IceClimberMap.Point> ladderPath = findLadderPath(map);
        if (!ladderPath.isEmpty()) {
            int hits = countIceHits(map, ladderPath);
            int time = ladderPath.size() + hits; // 步行1步=1时间,敲冰=2时间
            strategies.add(new PathStrategy(
                "绕路找梯子", ladderPath, hits, ladderPath.size(), time));
        }

        return strategies;
    }

    /**
     * 统计路径上需要敲击冰块的次数
     */
    private static int countIceHits(IceClimberMap map, List<IceClimberMap.Point> path) {
        // 简化计算:统计经过的ICE_FULL和ICE_CRACKED格子数
        int hits = 0;
        for (IceClimberMap.Point p : path) {
            // 这里需要访问map的内部状态,简化处理
            hits += 0; // 实际实现需要检查grid状态
        }
        return hits;
    }

    /**
     * 寻找优先使用梯子的路径
     */
    private static List<IceClimberMap.Point> findLadderPath(IceClimberMap map) {
        // BFS,优先走梯子
        int rows = map.getRows();
        int cols = map.getCols();
        boolean[][] visited = new boolean[rows][cols];
        Queue<IceClimberMap.Point> queue = new LinkedList<>();
        Map<IceClimberMap.Point, IceClimberMap.Point> parent = new HashMap<>();

        IceClimberMap.Point start = map.getPlayerStart();
        queue.add(start);
        visited[start.r][start.c] = true;

        while (!queue.isEmpty()) {
            IceClimberMap.Point current = queue.poll();

            if (current.r == 0) {
                return reconstructBFSPath(parent, current);
            }

            for (IceClimberMap.Move move : map.getPossibleMoves(current)) {
                IceClimberMap.Point next = move.to;
                if (!visited[next.r][next.c]) {
                    visited[next.r][next.c] = true;
                    parent.put(next, current);
                    // 梯子移动优先入队
                    if (move.type == IceClimberMap.MoveType.CLIMB) {
                        // 使用deque实现优先
                        ((LinkedList<IceClimberMap.Point>) queue).addFirst(next);
                    } else {
                        queue.add(next);
                    }
                }
            }
        }

        return Collections.emptyList();
    }

    private static List<IceClimberMap.Point> reconstructBFSPath(
            Map<IceClimberMap.Point, IceClimberMap.Point> parent,
            IceClimberMap.Point end) {
        List<IceClimberMap.Point> path = new ArrayList<>();
        IceClimberMap.Point current = end;
        while (current != null) {
            path.add(0, current);
            current = parent.get(current);
        }
        return path;
    }
}

4. 主程序演示

/**
 * 敲冰块游戏AI系统演示
 */
public class IceClimberDemo {
    public static void main(String[] args) {
        System.out.println("=== 敲冰块网格动态规划与最优破冰序列系统 ===\n");

        // 创建示例关卡
        int[][] level = {
            {1, 1, 1, 1, 1, 1, 1, 1},
            {1, 2, 0, 0, 4, 0, 2, 1},
            {1, 1, 1, 2, 1, 1, 1, 1},
            {1, 2, 0, 0, 0, 0, 2, 1},
            {1, 1, 2, 1, 1, 2, 1, 1},
            {1, 0, 0, 0, 0, 0, 0, 1},
            {1, 1, 2, 1, 1, 2, 1, 1},
            {1, 9, 0, 0, 0, 0, 0, 1},
            {1, 1, 1, 1, 1, 1, 1, 1}
        };

        IceClimberMap map = new IceClimberMap(level);

        System.out.println("关卡地图:");
        System.out.println("1=实体墙 2=冰块 4=梯子 9=起点");
        System.out.println("玩家起始位置: " + map.getPlayerStart());
        System.out.println();

        // 计算最优路径
        System.out.println("--- 分层DP最优路径计算 ---\n");
        LayeredDPPathfinder.Result result = LayeredDPPathfinder.findOptimalPath(map);

        if (!result.path.isEmpty()) {
            System.out.println("最优上升路径:");
            for (int i = 0; i < result.path.size(); i++) {
                IceClimberMap.Point p = result.path.get(i);
                System.out.printf("步骤%d: %s\n", i, p);
            }
            System.out.printf("\n总代价(时间): %d\n", result.totalCost);
            System.out.printf("路径长度: %d 步\n", result.path.size());
        } else {
            System.out.println("未找到可行路径!");
        }

        // 策略对比
        System.out.println("\n--- 路径策略对比 ---\n");
        List<IceBreakingOptimizer.PathStrategy> strategies = 
            IceBreakingOptimizer.generateStrategies(map);

        for (IceBreakingOptimizer.PathStrategy s : strategies) {
            System.out.printf("策略: %s\n", s.name);
            System.out.printf("  敲击次数: %d, 步行步数: %d, 总时间: %d\n",
                s.iceHits, s.walkSteps, s.totalTime);
        }

        if (!strategies.isEmpty()) {
            IceBreakingOptimizer.PathStrategy best = 
                IceBreakingOptimizer.selectOptimalStrategy(strategies);
            System.out.printf("\n最优策略: %s (总时间 %d)\n", best.name, best.totalTime);
        }
    }
}

算法复杂度分析

模块 时间复杂度 空间复杂度 说明
分层DP路径规划 O(R × C²) O(R × C) R行C列,每层内多轮松弛
带状态Dijkstra O(2^k × R × C × log(RC)) O(2^k × R × C) k为关键冰块数
BFS梯子优先 O(R × C) O(R × C) 标准网格BFS
策略对比 O(S × path_length) O(RC) S为策略数

扩展方向

  1. 实时敌人规避:引入动态障碍物,使用A*增量重规划
  2. 多人协作模式:多玩家同时破冰的分工与路径协调
  3. 道具系统优化:弹簧鞋、锤子等道具对路径代价的重新评估
  4. 蒙特卡洛树搜索:处理更大规模地图中的长程规划

总结

本文将敲冰块游戏抽象为分层网格上的动态规划问题,通过状态递推计算最优上升路径,结合冰块破坏的代价模型进行策略选择。核心学习要点包括:分层DP在垂直路径规划中的应用、破坏型环境中的状态空间扩展方法、以及多策略评估与选择的决策框架。这些算法思想可推广到所有”在动态变化地形中寻求最优路径”的场景,如挖矿游戏、建筑拆除和灾难逃生路径规划等。