每日算法 — 使用java实现黄金矿工:摆线运动模拟与贪心抓取策略

黄金矿工是经典的休闲益智小游戏,玩家通过控制摆动的钩子抓取地下的金块、钻石和障碍物,在有限时间内获取最高分数。看似简单的操作背后,隐藏着简谐运动模拟射线投射碰撞检测贪心最优目标选择三大算法核心。本文将用Java完整实现黄金矿工的AI决策引擎,从钩子摆动到抓取策略逐层剖析。

一、游戏物理模型与坐标系

1.1 为什么用简谐运动建模钩子摆动

黄金矿工的钩子从固定支点出发,在重力作用下做周期性左右摆动。这种运动天然适合用简谐运动(Simple Harmonic Motion, SHM)建模:

  • 摆角 θ:钩子与竖直方向的夹角,随时间正弦变化
  • 角振幅 θ_max:最大摆角,决定摆动范围
  • 角频率 ω:控制摆动速度
  • 摆长 L:支点到钩子的距离

钩子末端的坐标可通过三角函数精确计算:

x = pivotX + L * sin(θ)
y = pivotY + L * cos(θ)

1.2 核心实体类定义

/**
 * 二维向量,用于表示位置、速度和方向
 */
class Vec2 {
    double x, y;

    Vec2(double x, double y) {
        this.x = x;
        this.y = y;
    }

    Vec2 add(Vec2 other) {
        return new Vec2(this.x + other.x, this.y + other.y);
    }

    Vec2 sub(Vec2 other) {
        return new Vec2(this.x - other.x, this.y - other.y);
    }

    double length() {
        return Math.sqrt(x * x + y * y);
    }

    Vec2 normalize() {
        double len = length();
        if (len < 1e-9) return new Vec2(0, 0);
        return new Vec2(x / len, y / len);
    }

    Vec2 scale(double s) {
        return new Vec2(x * s, y * s);
    }

    @Override
    public String toString() {
        return String.format("(%.2f, %.2f)", x, y);
    }
}

/**
 * 可抓取物品类型枚举
 */
enum ItemType {
    GOLD_SMALL(50, 15, 2.0),    // 小金块:价值50,半径15,重量2.0
    GOLD_MEDIUM(100, 25, 3.5),  // 中金块:价值100,半径25,重量3.5
    GOLD_LARGE(250, 40, 5.0),   // 大金块:价值250,半径40,重量5.0
    DIAMOND(600, 12, 1.0),      // 钻石:价值600,半径12,重量轻
    STONE(20, 35, 6.0),         // 石头:价值低,重量大
    MYSTERY_BAG(0, 20, 2.5);    // 神秘袋:随机价值

    final int baseValue;   // 基础价值
    final int radius;      // 碰撞半径
    final double weight;   // 重量系数(影响拉拽速度)

    ItemType(int baseValue, int radius, double weight) {
        this.baseValue = baseValue;
        this.radius = radius;
        this.weight = weight;
    }

    /**
     * 获取实际价值,神秘袋随机生成
     */
    int getValue(Random rand) {
        if (this == MYSTERY_BAG) {
            int[] mysteryValues = {10, 50, 100, 300, 500, 800};
            return mysteryValues[rand.nextInt(mysteryValues.length)];
        }
        return baseValue;
    }
}

/**
 * 地下物品实体
 */
class Item {
    Vec2 position;      // 中心位置
    ItemType type;      // 物品类型
    int value;          // 实际价值
    boolean grabbed;    // 是否已被抓取

    Item(Vec2 position, ItemType type, Random rand) {
        this.position = position;
        this.type = type;
        this.value = type.getValue(rand);
        this.grabbed = false;
    }

    /**
     * 计算钩子末端到物品中心的距离
     */
    double distanceToHook(Vec2 hookPos) {
        return hookPos.sub(position).length();
    }

    /**
     * 检查钩子是否与物品发生碰撞
     */
    boolean intersects(Vec2 hookPos) {
        return distanceToHook(hookPos) <= type.radius;
    }

    @Override
    public String toString() {
        return String.format("%s[value=%d, pos=%s, r=%d]",
                type.name(), value, position, type.radius);
    }
}

1.3 钩子摆动模拟器

/**
 * 钩子摆动模拟器
 * 使用简谐运动建模摆角变化
 */
class HookSwing {
    // 支点坐标(屏幕顶部中央)
    Vec2 pivot;
    // 摆长(初始长度,抓取后会伸长)
    double ropeLength;
    // 当前摆角(弧度,0表示竖直向下,正数向右偏)
    double angle;
    // 角振幅(最大摆角)
    double maxAngle;
    // 角频率(控制摆动速度)
    double omega;
    // 时间相位
    double phase;
    // 摆动状态
    boolean isExtending = false;   // 是否正在向下伸长
    boolean isRetracting = false;  // 是否正在回拉
    Vec2 hookPos;                  // 钩子末端实时坐标

    HookSwing(Vec2 pivot, double ropeLength, double maxAngleDeg, double omega) {
        this.pivot = pivot;
        this.ropeLength = ropeLength;
        this.maxAngle = Math.toRadians(maxAngleDeg);
        this.omega = omega;
        this.phase = 0;
        this.hookPos = calculatePosition();
    }

    /**
     * 更新摆动相位,计算当前钩子位置
     * @param deltaTime 时间步长(秒)
     */
    void update(double deltaTime) {
        if (!isExtending && !isRetracting) {
            // 自由摆动阶段:θ(t) = θ_max * sin(ω*t + φ)
            phase += omega * deltaTime;
            angle = maxAngle * Math.sin(phase);
        }
        // 更新钩子末端坐标
        hookPos = calculatePosition();
    }

    /**
     * 根据当前摆角和绳长计算钩子末端坐标
     */
    Vec2 calculatePosition() {
        double x = pivot.x + ropeLength * Math.sin(angle);
        double y = pivot.y + ropeLength * Math.cos(angle);
        return new Vec2(x, y);
    }

    /**
     * 向下伸长钩子
     * @param speed 伸长速度(像素/秒)
     * @param deltaTime 时间步长
     * @return 伸长后的新位置
     */
    Vec2 extend(double speed, double deltaTime) {
        ropeLength += speed * deltaTime;
        hookPos = calculatePosition();
        return hookPos;
    }

    /**
     * 向上回拉钩子
     * @param speed 回拉速度(受重量影响)
     * @param deltaTime 时间步长
     * @return 回拉后的新位置
     */
    Vec2 retract(double speed, double deltaTime) {
        ropeLength = Math.max(ropeLength - speed * deltaTime, baseRopeLength);
        hookPos = calculatePosition();
        return hookPos;
    }

    double baseRopeLength = 60; // 基础绳长

    /**
     * 获取钩子运动方向向量(沿绳子向外)
     */
    Vec2 getDirection() {
        return new Vec2(Math.sin(angle), Math.cos(angle));
    }

    void resetRope() {
        this.ropeLength = baseRopeLength;
    }
}

二、核心算法一:射线投射碰撞检测

2.1 为什么需要射线碰撞检测

当钩子向下伸长时,我们需要实时检测它是否碰到了地下物品。由于钩子沿直线运动,可以使用射线-圆碰撞检测算法:

  • 射线起点:钩子当前位置
  • 射线方向:沿绳子向下的单位向量
  • 碰撞目标:每个物品是一个圆形区域

2.2 射线与圆的距离计算

/**
 * 射线投射碰撞检测器
 * 计算射线与圆形物品的最短距离和碰撞点
 */
class RaycastDetector {

    /**
     * 计算点到线段的最短距离
     * 用于判断钩子伸长路径是否穿过物品
     *
     * @param point  圆心(物品中心)
     * @param rayOrigin 射线起点
     * @param rayDir    射线方向(单位向量)
     * @return 点到射线的最短距离
     */
    static double pointToRayDistance(Vec2 point, Vec2 rayOrigin, Vec2 rayDir) {
        Vec2 op = point.sub(rayOrigin);
        double projection = op.x * rayDir.x + op.y * rayDir.y;

        // 如果投影为负,说明点在射线反方向,取到起点的距离
        if (projection < 0) {
            return op.length();
        }

        // 计算垂足坐标
        Vec2 closest = rayOrigin.add(rayDir.scale(projection));
        return point.sub(closest).length();
    }

    /**
     * 在射线方向上找到第一个碰撞的物品
     *
     * @param origin    射线起点(钩子位置)
     * @param direction 射线方向
     * @param items     所有物品列表
     * @return 第一个碰撞的物品,无碰撞返回null
     */
    static Item findFirstCollision(Vec2 origin, Vec2 direction, java.util.List<Item> items) {
        Item best = null;
        double bestDist = Double.MAX_VALUE;

        for (Item item : items) {
            if (item.grabbed) continue; // 跳过已抓取的

            double dist = pointToRayDistance(item.position, origin, direction);
            if (dist <= item.type.radius) {
                // 计算物品到射线起点的实际距离,取最近的
                double actualDist = item.position.sub(origin).length();
                if (actualDist < bestDist) {
                    bestDist = actualDist;
                    best = item;
                }
            }
        }
        return best;
    }
}

三、核心算法二:贪心最优目标选择策略

3.1 评估函数设计

在黄金矿工中,玩家需要在有限时间内获取最大分数。当钩子摆动时,AI需要判断何时释放钩子才能抓到最有价值的物品。贪心策略的核心是价值-时间比评估:

score(item) = value / (extendTime + retractTime)

其中伸长时间和回拉时间与距离和物品重量相关。

3.2 最优释放时机预测

/**
 * 贪心最优策略计算器
 * 在钩子摆动过程中,实时计算当前角度下能抓到的最佳物品
 */
class GreedyStrategy {
    // 钩子基础伸长速度
    static final double BASE_EXTEND_SPEED = 200.0;
    // 基础回拉速度
    static final double BASE_RETRACT_SPEED = 150.0;

    /**
     * 评估某个角度下释放钩子能获得的"效率分"
     *
     * @param angle         当前摆角(弧度)
     * @param pivot         支点坐标
     * @param baseLength    基础绳长
     * @param items         所有物品
     * @return 最优评估结果 {目标物品, 预期得分, 总耗时}
     */
    static java.util.Map<String, Object> evaluateAngle(
            double angle, Vec2 pivot, double baseLength, java.util.List<Item> items) {

        // 计算该角度下的射线方向
        Vec2 direction = new Vec2(Math.sin(angle), Math.cos(angle));
        Vec2 origin = new Vec2(
                pivot.x + baseLength * Math.sin(angle),
                pivot.y + baseLength * Math.cos(angle)
        );

        Item bestItem = null;
        double bestScore = -1;
        double bestTime = 0;

        for (Item item : items) {
            if (item.grabbed) continue;

            // 计算物品到射线的距离,判断是否在该角度下可达
            double perpDist = RaycastDetector.pointToRayDistance(
                    item.position, origin, direction);
            if (perpDist > item.type.radius) continue;

            // 计算物品到射线起点的距离(沿射线方向)
            Vec2 toItem = item.position.sub(origin);
            double alongDist = toItem.x * direction.x + toItem.y * direction.y;
            if (alongDist < 0) continue; // 在反方向,够不到

            // 计算伸长时间(距离 / 速度)
            double extendTime = alongDist / BASE_EXTEND_SPEED;
            // 回拉时间受物品重量影响:越重越慢
            double retractTime = alongDist / (BASE_RETRACT_SPEED / item.type.weight);
            double totalTime = extendTime + retractTime;

            // 效率分 = 价值 / 总耗时
            double score = item.value / totalTime;

            if (score > bestScore) {
                bestScore = score;
                bestItem = item;
                bestTime = totalTime;
            }
        }

        java.util.Map<String, Object> result = new java.util.HashMap<>();
        result.put("item", bestItem);
        result.put("score", bestScore);
        result.put("time", bestTime);
        return result;
    }

    /**
     * 在整个摆动周期内寻找全局最优释放角度
     *
     * @param hook          钩子模拟器
     * @param items         所有物品
     * @param sampleCount   采样点数(将摆角范围离散化采样)
     * @return 最优释放角度和对应目标
     */
    static java.util.Map<String, Object> findOptimalRelease(
            HookSwing hook, java.util.List<Item> items, int sampleCount) {

        double bestAngle = 0;
        Item bestItem = null;
        double bestScore = -1;

        // 在 [-maxAngle, +maxAngle] 范围内均匀采样
        double step = 2 * hook.maxAngle / sampleCount;
        for (int i = 0; i <= sampleCount; i++) {
            double angle = -hook.maxAngle + i * step;
            java.util.Map<String, Object> eval = evaluateAngle(
                    angle, hook.pivot, hook.baseRopeLength, items);
            double score = (Double) eval.get("score");
            if (score > bestScore) {
                bestScore = score;
                bestAngle = angle;
                bestItem = (Item) eval.get("item");
            }
        }

        java.util.Map<String, Object> result = new java.util.HashMap<>();
        result.put("angle", bestAngle);
        result.put("item", bestItem);
        result.put("score", bestScore);
        return result;
    }
}

四、核心算法三:拉拽物理模拟

4.1 抓取后的运动学计算

当钩子抓到物品后,回拉速度会受到物品重量影响。根据规则,重量越大回拉越慢:

/**
 * 拉拽物理模拟器
 */
class PullPhysics {
    /**
     * 计算抓取物品后的实际回拉速度
     * @param baseSpeed 基础回拉速度
     * @param weight    物品重量系数
     * @return 实际回拉速度
     */
    static double calculateRetractSpeed(double baseSpeed, double weight) {
        return baseSpeed / weight;
    }

    /**
     * 模拟一次完整的抓取流程
     *
     * @param hook      钩子
     * @param item      目标物品
     * @param items     所有物品列表(用于更新状态)
     * @param timeLimit 剩余时间限制
     * @return 抓取结果 {成功?, 耗时, 获得价值}
     */
    static java.util.Map<String, Object> simulateGrab(
            HookSwing hook, Item item, java.util.List<Item> items, double timeLimit) {

        double elapsed = 0;
        double timeStep = 0.05; // 50ms模拟步长
        boolean success = false;

        // 阶段1:向下伸长直到碰到目标
        hook.isExtending = true;
        Vec2 direction = hook.getDirection();
        while (elapsed < timeLimit) {
            hook.extend(GreedyStrategy.BASE_EXTEND_SPEED, timeStep);
            elapsed += timeStep;

            if (item.intersects(hook.hookPos)) {
                success = true;
                item.grabbed = true;
                break;
            }
            // 超出边界检测(简化:绳长不超过500)
            if (hook.ropeLength > 500) break;
        }
        hook.isExtending = false;

        if (!success) {
            // 没抓到,回拉空钩
            hook.isRetracting = true;
            while (hook.ropeLength > hook.baseRopeLength && elapsed < timeLimit) {
                hook.retract(GreedyStrategy.BASE_RETRACT_SPEED, timeStep);
                elapsed += timeStep;
            }
            hook.isRetracting = false;
            hook.resetRope();

            java.util.Map<String, Object> fail = new java.util.HashMap<>();
            fail.put("success", false);
            fail.put("time", elapsed);
            fail.put("value", 0);
            return fail;
        }

        // 阶段2:带着物品回拉
        hook.isRetracting = true;
        double retractSpeed = calculateRetractSpeed(
                GreedyStrategy.BASE_RETRACT_SPEED, item.type.weight);
        while (hook.ropeLength > hook.baseRopeLength && elapsed < timeLimit) {
            hook.retract(retractSpeed, timeStep);
            // 物品跟随钩子移动
            item.position = hook.hookPos;
            elapsed += timeStep;
        }
        hook.isRetracting = false;
        hook.resetRope();

        java.util.Map<String, Object> result = new java.util.HashMap<>();
        result.put("success", true);
        result.put("time", elapsed);
        result.put("value", item.value);
        return result;
    }
}

五、游戏主循环与AI控制器

/**
 * 黄金矿工游戏主控制器
 */
class GoldMinerGame {
    HookSwing hook;
    java.util.List<Item> items;
    int totalScore;
    double remainingTime; // 游戏总时长(秒)
    Random rand;

    GoldMinerGame(int width, int height, int itemCount, double gameTime) {
        this.rand = new Random(42);
        this.totalScore = 0;
        this.remainingTime = gameTime;

        // 初始化钩子(支点在顶部中央)
        Vec2 pivot = new Vec2(width / 2.0, 0);
        this.hook = new HookSwing(pivot, 60, 60, 1.5);

        // 随机生成地下物品
        this.items = generateItems(width, height, itemCount);
    }

    /**
     * 随机生成物品,避开钩子摆动范围正下方
     */
    java.util.List<Item> generateItems(int width, int height, int count) {
        java.util.List<Item> list = new java.util.ArrayList<>();
        ItemType[] types = ItemType.values();

        for (int i = 0; i < count; i++) {
            double x = 30 + rand.nextDouble() * (width - 60);
            double y = 120 + rand.nextDouble() * (height - 150);
            ItemType type = types[rand.nextInt(types.length)];
            list.add(new Item(new Vec2(x, y), type, rand));
        }
        return list;
    }

    /**
     * AI自动运行一回合:寻找最优角度 -> 等待时机 -> 抓取 -> 计分
     */
    void aiPlayRound() {
        // 1. 计算全局最优释放角度
        java.util.Map<String, Object> optimal = GreedyStrategy.findOptimalRelease(
                hook, items, 120);
        double targetAngle = (Double) optimal.get("angle");
        Item target = (Item) optimal.get("item");

        if (target == null) {
            System.out.println("[AI] 场上无可用目标,跳过本轮");
            return;
        }

        System.out.printf("[AI] 选定目标: %s, 最优角度: %.2f°%n",
                target, Math.toDegrees(targetAngle));

        // 2. 模拟摆动,等待钩子到达目标角度(考虑角度方向)
        double timeStep = 0.02;
        double waitTime = 0;
        double maxWait = 10; // 最多等10秒

        while (waitTime < maxWait && remainingTime > 0) {
            hook.update(timeStep);
            waitTime += timeStep;
            remainingTime -= timeStep;

            // 判断当前角度是否接近目标角度(容差2度)
            double diff = Math.abs(hook.angle - targetAngle);
            if (diff < Math.toRadians(2.0)) {
                break;
            }
        }

        System.out.printf("[AI] 等待 %.2f 秒后释放钩子%n", waitTime);

        // 3. 执行抓取
        java.util.Map<String, Object> result = PullPhysics.simulateGrab(
                hook, target, items, remainingTime);
        boolean success = (Boolean) result.get("success");
        double usedTime = (Double) result.get("time");
        int value = (Integer) result.get("value");
        remainingTime -= usedTime;

        if (success) {
            totalScore += value;
            System.out.printf("[AI] 抓取成功! 价值=%d, 耗时=%.2fs, 总分=%d%n",
                    value, usedTime, totalScore);
        } else {
            System.out.printf("[AI] 抓取失败,耗时=%.2fs%n", usedTime);
        }
    }

    /**
     * 运行完整游戏,AI自动决策直到时间结束
     */
    void runAutoGame() {
        System.out.println("=== 黄金矿工 AI 自动模式 ===");
        System.out.println("初始物品:");
        for (Item item : items) {
            System.out.println("  " + item);
        }
        System.out.println();

        int round = 0;
        while (remainingTime > 2.0) {
            round++;
            System.out.printf("--- 第 %d 轮 (剩余时间: %.1fs) ---%n", round, remainingTime);
            aiPlayRound();

            // 检查是否还有未抓取物品
            boolean hasItems = false;
            for (Item item : items) {
                if (!item.grabbed) {
                    hasItems = true;
                    break;
                }
            }
            if (!hasItems) {
                System.out.println("所有物品已抓取完毕!");
                break;
            }
            System.out.println();
        }

        System.out.printf("%n=== 游戏结束 ===%n");
        System.out.printf("最终得分: %d%n", totalScore);
        System.out.printf("剩余时间: %.1fs%n", remainingTime);
    }
}

六、完整运行示例

public class GoldMinerDemo {
    public static void main(String[] args) {
        // 创建游戏:宽度400,深度300,10个物品,60秒时限
        GoldMinerGame game = new GoldMinerGame(400, 300, 10, 60.0);
        game.runAutoGame();
    }
}

典型输出示例:

=== 黄金矿工 AI 自动模式 ===
初始物品:
  GOLD_SMALL[value=50, pos=(85.34, 167.22), r=15]
  DIAMOND[value=600, pos=(312.56, 198.45), r=12]
  GOLD_LARGE[value=250, pos=(156.78, 245.30), r=40]
  STONE[value=20, pos=(234.12, 178.90), r=35]
  MYSTERY_BAG[value=300, pos=(78.90, 210.50), r=20]
  ...

--- 第 1 轮 (剩余时间: 60.0s) ---
[AI] 选定目标: DIAMOND[value=600, pos=(312.56, 198.45), r=12], 最优角度: 42.35°
[AI] 等待 1.23 秒后释放钩子
[AI] 抓取成功! 价值=600, 耗时=2.85s, 总分=600

--- 第 2 轮 (剩余时间: 55.9s) ---
[AI] 选定目标: GOLD_LARGE[value=250, pos=(156.78, 245.30), r=40], 最优角度: 18.60°
[AI] 等待 0.87 秒后释放钩子
[AI] 抓取成功! 价值=250, 耗时=4.12s, 总分=850
...

=== 游戏结束 ===
最终得分: 1470
剩余时间: 12.3s

七、复杂度分析

操作 时间复杂度 空间复杂度 说明
钩子摆动更新 O(1) O(1) 简谐运动三角函数计算
射线碰撞检测 O(n) O(1) n为物品数量,遍历检测
单角度贪心评估 O(n) O(1) 对每个物品计算效率分
全局最优搜索 O(k*n) O(1) k为摆角采样点数
物理模拟步进 O(m) O(1) m为时间步数,与距离相关

八、总结与扩展

本文从黄金矿工游戏的三个核心算法问题出发,用Java实现了完整的AI决策引擎:

  1. 简谐运动建模:用正弦函数精确描述钩子摆动,通过相位累加实现平滑动画。
  2. 射线-圆碰撞检测:将伸长路径抽象为射线,通过点到射线距离公式判断碰撞,避免了复杂的每帧全量碰撞检测。
  3. 贪心最优策略:以”价值/耗时”为评估指标,在离散化的摆角空间中搜索全局最优释放时机。

可扩展方向
动态障碍物:引入移动的动物或炸弹,需要预测其未来位置,升级为带预测的碰撞检测
多目标规划:当前每次只抓一个物品,可引入动态规划规划剩余时间内的最优抓取序列。
强化学习AI:用Q-Learning或策略梯度训练神经网络,让AI在大量对局中学习超越贪心策略的复杂决策。