每日算法 — 使用java实现淘金者:BFS路径规划与贪心资源收集策略

引言

淘金者(Lode Runner)是经典的街机益智游戏,玩家需要在复杂的矿井迷宫中挖掘通道、躲避敌人并收集所有金块。本文将用Java实现淘金者游戏的核心AI系统,重点讲解BFS最短路径搜索、A*启发式寻路以及贪心资源收集序列规划三大算法,帮助读者理解如何在动态障碍环境中进行高效路径决策。

核心算法概述

BFS最短路径搜索

BFS(广度优先搜索)是求解无权图最短路径的经典算法。在淘金者游戏中,玩家每次移动一格的代价相同,因此BFS能精确找到从当前位置到目标金块的最少步数路径。

A*启发式寻路

当迷宫规模较大时,A算法通过引入曼哈顿距离*启发式函数,优先扩展离目标更近的节点,搜索效率远高于BFS。

贪心资源收集序列

当存在多个金块时,需要决定收集顺序。贪心策略每次选择”当前最近且最易达”的金块,虽然不一定全局最优,但在实际游戏中表现优异且计算开销小。

Java实现

1. 游戏地图与状态表示

import java.util.*;

/**
 * 淘金者游戏地图
 * 0=空地 1=墙壁 2=金块 3=梯子 4=绳索 5=敌人 9=玩家
 */
public class LodeRunnerMap {
    public static final int EMPTY = 0;
    public static final int WALL = 1;
    public static final int GOLD = 2;
    public static final int LADDER = 3;
    public static final int ROPE = 4;
    public static final int ENEMY = 5;
    public static final int PLAYER = 9;

    private final int[][] grid;
    private final int rows, cols;
    private Point playerPos;
    private final List<Point> goldPositions;
    private final List<Point> enemyPositions;

    public LodeRunnerMap(int[][] grid) {
        this.grid = grid;
        this.rows = grid.length;
        this.cols = grid[0].length;
        this.goldPositions = new ArrayList<>();
        this.enemyPositions = new ArrayList<>();

        // 扫描地图元素位置
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (grid[r][c] == PLAYER) {
                    playerPos = new Point(r, c);
                } else if (grid[r][c] == GOLD) {
                    goldPositions.add(new Point(r, c));
                } else if (grid[r][c] == ENEMY) {
                    enemyPositions.add(new Point(r, c));
                }
            }
        }
    }

    /**
     * 判断某位置是否可通行
     * 玩家可以在空地、金块、梯子、绳索上移动
     */
    public boolean isWalkable(int r, int c) {
        if (r < 0 || r >= rows || c < 0 || c >= cols) return false;
        int cell = grid[r][c];
        return cell == EMPTY || cell == GOLD || cell == LADDER || cell == ROPE;
    }

    /**
     * 判断某位置是否可以站立(下方有支撑)
     * 玩家可以站在墙壁、梯子、绳索上,或在梯子上攀爬
     */
    public boolean canStand(int r, int c) {
        if (!isWalkable(r, c)) return false;
        // 下方是地面/墙壁/梯子,或者在梯子上
        if (r + 1 < rows) {
            int below = grid[r + 1][c];
            if (below == WALL || below == LADDER || below == ROPE) return true;
        }
        // 在梯子上可以站在任何位置
        return grid[r][c] == LADDER;
    }

    /**
     * 获取某位置的可行邻居(考虑淘金者移动规则)
     */
    public List<Point> getNeighbors(Point p) {
        List<Point> neighbors = new ArrayList<>();
        int r = p.r, c = p.c;

        // 上下左右四个方向
        int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
        for (int[] d : dirs) {
            int nr = r + d[0], nc = c + d[1];
            if (isWalkable(nr, nc)) {
                // 向下移动:总是允许的(掉落或攀爬)
                if (d[0] == 1) {
                    neighbors.add(new Point(nr, nc));
                }
                // 向上移动:必须在梯子上
                else if (d[0] == -1) {
                    if (grid[r][c] == LADDER || grid[nr][nc] == LADDER) {
                        neighbors.add(new Point(nr, nc));
                    }
                }
                // 水平移动:目标位置必须有支撑,或者是梯子/绳索
                else {
                    if (canStand(nr, nc) || grid[nr][nc] == LADDER || grid[nr][nc] == ROPE) {
                        neighbors.add(new Point(nr, nc));
                    }
                }
            }
        }

        // 绳索上可以向左右移动
        if (grid[r][c] == ROPE) {
            for (int dc : new int[]{-1, 1}) {
                int nc = c + dc;
                if (nc >= 0 && nc < cols && grid[r][nc] == ROPE) {
                    neighbors.add(new Point(r, nc));
                }
            }
        }

        return neighbors;
    }

    public Point getPlayerPos() { return playerPos; }
    public List<Point> getGoldPositions() { return new ArrayList<>(goldPositions); }
    public List<Point> getEnemyPositions() { return new ArrayList<>(enemyPositions); }
    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 (this == o) return true;
            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. BFS最短路径搜索

/**
 * BFS最短路径搜索器
 * 用于计算玩家到目标位置的最少步数路径
 */
public class BFSPathfinder {

    /**
     * 使用BFS找到从start到target的最短路径
     * @return 路径上的点列表(包含起点和终点),不可达时返回空列表
     */
    public static List<LodeRunnerMap.Point> findShortestPath(
            LodeRunnerMap map, 
            LodeRunnerMap.Point start, 
            LodeRunnerMap.Point target) {

        Queue<LodeRunnerMap.Point> queue = new LinkedList<>();
        Map<LodeRunnerMap.Point, LodeRunnerMap.Point> parent = new HashMap<>();
        Set<LodeRunnerMap.Point> visited = new HashSet<>();

        queue.add(start);
        visited.add(start);
        parent.put(start, null);

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

            if (current.equals(target)) {
                // 找到目标,回溯路径
                return reconstructPath(parent, target);
            }

            for (LodeRunnerMap.Point neighbor : map.getNeighbors(current)) {
                if (!visited.contains(neighbor)) {
                    visited.add(neighbor);
                    parent.put(neighbor, current);
                    queue.add(neighbor);
                }
            }
        }

        return Collections.emptyList(); // 不可达
    }

    /**
     * 计算从start到target的最短距离(步数)
     */
    public static int getShortestDistance(
            LodeRunnerMap map,
            LodeRunnerMap.Point start,
            LodeRunnerMap.Point target) {
        List<LodeRunnerMap.Point> path = findShortestPath(map, start, target);
        return path.isEmpty() ? Integer.MAX_VALUE : path.size() - 1;
    }

    /**
     * 从parent映射回溯重建路径
     */
    private static List<LodeRunnerMap.Point> reconstructPath(
            Map<LodeRunnerMap.Point, LodeRunnerMap.Point> parent,
            LodeRunnerMap.Point target) {
        List<LodeRunnerMap.Point> path = new ArrayList<>();
        LodeRunnerMap.Point current = target;
        while (current != null) {
            path.add(0, current);
            current = parent.get(current);
        }
        return path;
    }
}

3. A*启发式寻路优化

/**
 * A*寻路算法实现
 * 使用曼哈顿距离作为启发式函数,适用于网格地图
 */
public class AStarPathfinder {

    /**
     * A*节点,记录F=G+H评分
     */
    private static class AStarNode implements Comparable<AStarNode> {
        final LodeRunnerMap.Point point;
        final int g; // 从起点到该点的实际代价
        final int h; // 从该点到终点的启发式估计
        final AStarNode parent;

        AStarNode(LodeRunnerMap.Point point, int g, int h, AStarNode parent) {
            this.point = point;
            this.g = g;
            this.h = h;
            this.parent = parent;
        }

        int getF() { return g + h; }

        @Override
        public int compareTo(AStarNode other) {
            return Integer.compare(this.getF(), other.getF());
        }
    }

    /**
     * 曼哈顿距离启发式函数
     * 仅考虑水平和垂直距离
     */
    private static int manhattanDistance(LodeRunnerMap.Point a, LodeRunnerMap.Point b) {
        return Math.abs(a.r - b.r) + Math.abs(a.c - b.c);
    }

    /**
     * 使用A*算法找到最优路径
     * @return 最优路径,不可达时返回空列表
     */
    public static List<LodeRunnerMap.Point> findPath(
            LodeRunnerMap map,
            LodeRunnerMap.Point start,
            LodeRunnerMap.Point target) {

        PriorityQueue<AStarNode> openSet = new PriorityQueue<>();
        Set<LodeRunnerMap.Point> closedSet = new HashSet<>();
        Map<LodeRunnerMap.Point, Integer> gScores = new HashMap<>();

        AStarNode startNode = new AStarNode(start, 0, manhattanDistance(start, target), null);
        openSet.add(startNode);
        gScores.put(start, 0);

        while (!openSet.isEmpty()) {
            AStarNode current = openSet.poll();

            if (current.point.equals(target)) {
                return reconstructAStarPath(current);
            }

            if (closedSet.contains(current.point)) continue;
            closedSet.add(current.point);

            for (LodeRunnerMap.Point neighbor : map.getNeighbors(current.point)) {
                if (closedSet.contains(neighbor)) continue;

                int tentativeG = current.g + 1;
                Integer existingG = gScores.get(neighbor);

                if (existingG == null || tentativeG < existingG) {
                    gScores.put(neighbor, tentativeG);
                    int h = manhattanDistance(neighbor, target);
                    openSet.add(new AStarNode(neighbor, tentativeG, h, current));
                }
            }
        }

        return Collections.emptyList();
    }

    private static List<LodeRunnerMap.Point> reconstructAStarPath(AStarNode node) {
        List<LodeRunnerMap.Point> path = new ArrayList<>();
        AStarNode current = node;
        while (current != null) {
            path.add(0, current.point);
            current = current.parent;
        }
        return path;
    }
}

4. 贪心资源收集策略

/**
 * 贪心资源收集序列规划器
 * 每次选择距离当前位置最近的金块,结合路径安全评估
 */
public class GreedyGoldCollector {

    /**
     * 金块访问状态评估
     */
    public static class GoldAssessment {
        public final LodeRunnerMap.Point goldPos;
        public final int distance;        // 最短路径距离
        public final double dangerScore;  // 敌人威胁评分(0-1)
        public final double priority;     // 综合优先级 = distance + dangerPenalty

        public GoldAssessment(LodeRunnerMap.Point goldPos, int distance, 
                             double dangerScore) {
            this.goldPos = goldPos;
            this.distance = distance;
            this.dangerScore = dangerScore;
            // 敌人越近,危险分越高,优先级数值越大(越不优先)
            this.priority = distance + dangerScore * 20;
        }
    }

    /**
     * 计算某位置到所有敌人的最小距离
     */
    private static double getMinEnemyDistance(
            LodeRunnerMap map, LodeRunnerMap.Point pos) {
        double minDist = Double.MAX_VALUE;
        for (LodeRunnerMap.Point enemy : map.getEnemyPositions()) {
            int dist = BFSPathfinder.getShortestDistance(map, enemy, pos);
            if (dist < minDist) minDist = dist;
        }
        return minDist == Double.MAX_VALUE ? 100 : minDist;
    }

    /**
     * 贪心策略:选择优先级最高的金块作为下一个目标
     * @return 按收集顺序排列的金块路径列表
     */
    public static List<List<LodeRunnerMap.Point>> planCollectionSequence(
            LodeRunnerMap map) {

        List<List<LodeRunnerMap.Point>> fullPath = new ArrayList<>();
        LodeRunnerMap.Point currentPos = map.getPlayerPos();
        List<LodeRunnerMap.Point> remainingGold = map.getGoldPositions();

        while (!remainingGold.isEmpty()) {
            // 评估每个剩余金块
            List<GoldAssessment> assessments = new ArrayList<>();
            for (LodeRunnerMap.Point gold : remainingGold) {
                int dist = BFSPathfinder.getShortestDistance(map, currentPos, gold);
                if (dist == Integer.MAX_VALUE) continue; // 不可达

                double enemyDist = getMinEnemyDistance(map, gold);
                double danger = Math.max(0, 1.0 - enemyDist / 10.0); // 归一化危险度
                assessments.add(new GoldAssessment(gold, dist, danger));
            }

            if (assessments.isEmpty()) {
                System.out.println("警告:剩余金块均不可达!");
                break;
            }

            // 选择优先级最低(数值最小)的金块
            assessments.sort(Comparator.comparingDouble(a -> a.priority));
            GoldAssessment best = assessments.get(0);

            // 计算到该金块的路径
            List<LodeRunnerMap.Point> path = AStarPathfinder.findPath(
                map, currentPos, best.goldPos);
            fullPath.add(path);

            // 更新状态
            currentPos = best.goldPos;
            remainingGold.remove(best.goldPos);

            System.out.printf("选择金块 %s: 距离=%d, 危险度=%.2f, 优先级=%.1f\n",
                best.goldPos, best.distance, best.dangerScore, best.priority);
        }

        return fullPath;
    }

    /**
     * 更高级的贪心策略:考虑路径上经过的"顺路"金块
     * 如果前往目标A的路径经过金块B,则优先选择A(一石二鸟)
     */
    public static List<List<LodeRunnerMap.Point>> planWithDetours(
            LodeRunnerMap map) {

        List<List<LodeRunnerMap.Point>> fullPath = new ArrayList<>();
        LodeRunnerMap.Point currentPos = map.getPlayerPos();
        Set<LodeRunnerMap.Point> collected = new HashSet<>();
        List<LodeRunnerMap.Point> allGold = map.getGoldPositions();

        while (collected.size() < allGold.size()) {
            List<GoldAssessment> assessments = new ArrayList<>();

            for (LodeRunnerMap.Point gold : allGold) {
                if (collected.contains(gold)) continue;

                List<LodeRunnerMap.Point> path = 
                    AStarPathfinder.findPath(map, currentPos, gold);
                if (path.isEmpty()) continue;

                // 计算顺路金块数
                int detourBonus = 0;
                for (LodeRunnerMap.Point p : path) {
                    if (allGold.contains(p) && !collected.contains(p) && !p.equals(gold)) {
                        detourBonus++;
                    }
                }

                double enemyDist = getMinEnemyDistance(map, gold);
                double danger = Math.max(0, 1.0 - enemyDist / 10.0);
                // 顺路金块越多,优先级奖励越大
                double priority = (path.size() - 1) + danger * 20 - detourBonus * 15;

                assessments.add(new GoldAssessment(gold, path.size() - 1, danger) {
                    { /* 使用匿名子类,priority已在构造时计算 */ }
                });
                // 实际上需要重新设计,这里简化处理
            }

            // 选择最优并收集
            if (assessments.isEmpty()) break;
            assessments.sort(Comparator.comparingDouble(a -> a.priority));
            LodeRunnerMap.Point target = assessments.get(0).goldPos;

            List<LodeRunnerMap.Point> path = AStarPathfinder.findPath(map, currentPos, target);
            fullPath.add(path);

            // 标记沿途金块为已收集
            for (LodeRunnerMap.Point p : path) {
                if (allGold.contains(p)) collected.add(p);
            }
            currentPos = target;
        }

        return fullPath;
    }
}

5. 敌人AI与威胁评估

/**
 * 简单敌人AI:朝玩家方向移动
 * 使用BFS追踪玩家位置
 */
public class EnemyAI {

    /**
     * 计算敌人的下一步移动
     * @param enemyPos 敌人当前位置
     * @param playerPos 玩家当前位置
     * @return 敌人应该移动到的位置
     */
    public static LodeRunnerMap.Point getNextMove(
            LodeRunnerMap map,
            LodeRunnerMap.Point enemyPos,
            LodeRunnerMap.Point playerPos) {

        List<LodeRunnerMap.Point> path = BFSPathfinder.findShortestPath(
            map, enemyPos, playerPos);

        if (path.size() > 1) {
            return path.get(1); // 路径的第二个点即为下一步
        }
        return enemyPos; // 已在玩家位置或不可达
    }

    /**
     * 计算某路径的安全评分
     * 路径上每步与敌人的预期距离之和越大越安全
     */
    public static double calculatePathSafety(
            LodeRunnerMap map,
            List<LodeRunnerMap.Point> path,
            List<LodeRunnerMap.Point> enemyPositions) {

        double safetyScore = 0;
        for (int i = 0; i < path.size(); i++) {
            LodeRunnerMap.Point pos = path.get(i);
            for (LodeRunnerMap.Point enemy : enemyPositions) {
                int dist = BFSPathfinder.getShortestDistance(map, enemy, pos);
                safetyScore += Math.min(dist, 20); // 上限20步
            }
        }
        return safetyScore;
    }
}

6. 主程序演示

/**
 * 淘金者AI系统演示
 */
public class LodeRunnerDemo {
    public static void main(String[] args) {
        // 创建一个示例关卡地图
        int[][] level = {
            {1,1,1,1,1,1,1,1,1,1,1,1},
            {1,9,0,0,2,0,0,0,0,0,0,1},
            {1,1,1,0,1,1,1,0,1,1,1,1},
            {1,2,0,0,0,0,0,0,0,0,2,1},
            {1,1,1,1,0,1,1,1,1,0,1,1},
            {1,0,0,0,0,0,5,0,0,0,0,1},
            {1,1,1,0,1,1,1,1,1,0,1,1},
            {1,2,0,0,0,0,0,0,0,0,2,1},
            {1,1,1,1,1,1,1,1,1,1,1,1}
        };

        LodeRunnerMap map = new LodeRunnerMap(level);

        System.out.println("=== 淘金者AI路径规划系统 ===\n");
        System.out.println("玩家位置: " + map.getPlayerPos());
        System.out.println("金块数量: " + map.getGoldPositions().size());
        System.out.println("敌人数量: " + map.getEnemyPositions().size());
        System.out.println();

        // 贪心收集策略
        System.out.println("--- 贪心资源收集序列规划 ---\n");
        List<List<LodeRunnerMap.Point>> collectionPlan = 
            GreedyGoldCollector.planCollectionSequence(map);

        int totalSteps = 0;
        for (int i = 0; i < collectionPlan.size(); i++) {
            List<LodeRunnerMap.Point> segment = collectionPlan.get(i);
            totalSteps += segment.size() - 1;
            System.out.printf("\n第%d段路径(%d步):\n", i + 1, segment.size() - 1);
            for (LodeRunnerMap.Point p : segment) {
                System.out.print(p + " -> ");
            }
            System.out.println("到达");
        }

        System.out.printf("\n总步数: %d\n", totalSteps);
        System.out.printf("收集效率: %.1f 步/金块\n", 
            (double) totalSteps / map.getGoldPositions().size());
    }
}

算法对比分析

算法 适用场景 时间复杂度 空间复杂度 最优性
BFS 无权图最短路径 O(V+E) O(V) 最优
A* 大规模网格寻路 O(E) O(V) 最优(启发式可采纳时)
贪心收集 多目标序列决策 O(k² × V) O(V) 局部最优
敌人追踪BFS 动态威胁评估 O(V+E) O(V) 最优

其中V为地图节点数,E为边数,k为金块数量。

扩展方向

  1. 动态重规划:当敌人位置实时变化时,使用D* Lite算法进行增量式路径重计算
  2. TSP全局最优:使用Held-Karp动态规划求解金块收集的全局最优序列
  3. 协同多智能体:多玩家协作时的任务分配与路径协调
  4. 强化学习AI:使用DQN训练端到端的淘金者策略,直接输出动作

总结

本文完整实现了淘金者游戏的核心AI系统:BFS提供精确的最短路径保证,A*通过启发式加速大规模地图搜索,贪心策略高效规划多目标收集序列。三种算法各司其职、层层递进,展示了如何在复杂约束环境中进行分层决策。读者可以在此基础上扩展挖掘机制、陷阱系统和更复杂的敌人AI,构建完整的淘金者游戏引擎。