每日算法 — 使用java实现黄金矿工:物理抛物线与贪心最优目标选择

黄金矿工是经典街机游戏的代表,玩家控制一个摇摆的钩子抓取地下的金矿、钻石和障碍物。本文用Java实现黄金矿工游戏的核心算法,重点讲解物理抛物线运动模拟钩子的抓取轨迹,以及贪心策略在多目标场景下的最优选择决策。

游戏机制与物理建模

黄金矿工的核心算法挑战包括:

  1. 钩子摆动:钩子以固定点为圆心做简谐摆动
  2. 发射轨迹:按下按钮后钩子沿当前角度直线发射
  3. 物理抓取:抓到物品后根据重量调整收回速度
  4. 目标选择:多个物品同时可见时,选择最有价值的目标

物理抛物线与直线运动模拟

钩子的运动分为两个阶段:摆动阶段和发射阶段。摆动使用简谐运动模型,发射后沿直线运动直到碰到边界或物品。

import java.util.*;

/**
 * 黄金矿工 - 物理引擎核心
 * 模拟钩子摆动、发射、抓取和回收的完整物理过程
 */
public class MinerPhysics {

    // 钩子状态
    static class Hook {
        double originX, originY;  // 旋转中心(固定)
        double angle;             // 当前角度(弧度)
        double length;            // 当前绳长
        double maxLength;         // 最大绳长
        double swingSpeed;        // 摆动角速度
        boolean extending;        // 是否正在发射
        boolean retracting;       // 是否正在回收
        double extendSpeed;       // 发射速度
        double retractBaseSpeed;  // 基础回收速度
        Item caughtItem;          // 抓到的物品

        Hook(double ox, double oy, double maxLen) {
            this.originX = ox; this.originY = oy;
            this.maxLength = maxLen;
            this.length = 0;
            this.angle = 0;
            this.swingSpeed = 1.5; // 弧度/秒
            this.extendSpeed = 300; // 像素/秒
            this.retractBaseSpeed = 150;
        }

        /**
         * 更新钩子摆动
         */
        void updateSwing(double dt) {
            if (extending || retracting) return;
            angle += swingSpeed * dt;
            // 限制摆动范围在 -60度 到 60度
            if (angle > Math.PI / 3 || angle < -Math.PI / 3) {
                swingSpeed = -swingSpeed;
            }
        }

        /**
         * 获取钩子末端坐标
         */
        double getTipX() { return originX + Math.sin(angle) * length; }
        double getTipY() { return originY + Math.cos(angle) * length; }

        /**
         * 发射钩子
         */
        void extend(double dt) {
            if (!extending) return;
            length += extendSpeed * dt;
            if (length >= maxLength) {
                length = maxLength;
                extending = false;
                retracting = true; // 到达边界自动回收
            }
        }

        /**
         * 回收钩子,重量越大速度越慢
         */
        void retract(double dt) {
            if (!retracting) return;
            double weight = (caughtItem != null) ? caughtItem.weight : 1.0;
            double speed = retractBaseSpeed / Math.sqrt(weight);
            length -= speed * dt;
            if (length <= 0) {
                length = 0;
                retracting = false;
            }
        }
    }

    static class Item {
        double x, y;
        int value;
        double weight;
        double radius;
        String type;

        Item(double x, double y, int value, double weight, double radius, String type) {
            this.x = x; this.y = y; this.value = value;
            this.weight = weight; this.radius = radius; this.type = type;
        }

        /**
         * 计算物品的"性价比":价值/重量比
         */
        double getValueDensity() {
            return value / weight;
        }
    }
}

贪心最优目标选择算法

当钩子沿直线运动时,可能穿过多个物品的碰撞范围。贪心策略选择当前角度下”性价比”最高的物品进行抓取。

/**
 * 黄金矿工 - 贪心目标选择器
 * 在钩子发射路径上选择最优目标
 */
public class MinerTargetSelector {

    /**
     * 贪心策略:选择发射路径上性价比最高的物品
     * 性价比 = 价值 / (重量 * 距离)
     * 同时考虑物品大小(大物品更容易被抓到)
     */
    public MinerPhysics.Item selectBestTarget(
            MinerPhysics.Hook hook, 
            List<MinerPhysics.Item> items) {

        MinerPhysics.Item best = null;
        double bestScore = -1;

        for (MinerPhysics.Item item : items) {
            // 计算钩子到物品的距离
            double dx = item.x - hook.originX;
            double dy = item.y - hook.originY;
            double distance = Math.sqrt(dx * dx + dy * dy);

            // 计算物品相对于钩子的角度
            double itemAngle = Math.atan2(dx, dy);
            double angleDiff = Math.abs(itemAngle - hook.angle);

            // 如果角度差太大,钩子不会经过该物品
            if (angleDiff > 0.1) continue; // 约5度容差

            // 计算钩子尖端到物品中心的最近距离(垂直距离)
            double hookTipX = hook.getTipX();
            double hookTipY = hook.getTipY();
            double distToLine = pointToLineDistance(
                item.x, item.y, 
                hook.originX, hook.originY,
                hookTipX, hookTipY
            );

            // 如果距离大于物品半径,抓不到
            if (distToLine > item.radius) continue;

            // 贪心评分:价值高、重量轻、距离近、体积大的物品优先
            double score = item.value / (item.weight * 0.5 + distance * 0.01 + 1);

            if (score > bestScore) {
                bestScore = score;
                best = item;
            }
        }

        return best;
    }

    /**
     * 计算点到线段的距离
     */
    private double pointToLineDistance(double px, double py,
                                       double x1, double y1, double x2, double y2) {
        double dx = x2 - x1;
        double dy = y2 - y1;
        double len2 = dx * dx + dy * dy;
        if (len2 == 0) return Math.sqrt((px-x1)*(px-x1) + (py-y1)*(py-y1));

        double t = Math.max(0, Math.min(1, ((px-x1)*dx + (py-y1)*dy) / len2));
        double projX = x1 + t * dx;
        double projY = y1 + t * dy;
        return Math.sqrt((px-projX)*(px-projX) + (py-projY)*(py-projY));
    }

    /**
     * 多目标场景下的贪心策略:预测下一钩的最佳发射角度
     * 枚举多个角度,选择能抓到最高分物品的角度
     */
    public double findBestAngle(
            MinerPhysics.Hook hook,
            List<MinerPhysics.Item> items,
            double angleRange, // 搜索角度范围
            double step) {     // 角度步长

        double bestAngle = hook.angle;
        double bestScore = -1;
        double originalAngle = hook.angle;

        for (double a = -angleRange; a <= angleRange; a += step) {
            hook.angle = originalAngle + a;
            MinerPhysics.Item target = selectBestTarget(hook, items);
            if (target != null) {
                double score = target.value / (target.weight + 1);
                if (score > bestScore) {
                    bestScore = score;
                    bestAngle = hook.angle;
                }
            }
        }

        hook.angle = originalAngle; // 恢复原角度
        return bestAngle;
    }
}

完整可运行项目

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;

/**
 * 黄金矿工完整游戏实现
 */
public class GoldMinerGame extends JPanel implements ActionListener, KeyListener {

    private static final int WIDTH = 800;
    private static final int HEIGHT = 600;
    private static final int HOOK_ORIGIN_X = WIDTH / 2;
    private static final int HOOK_ORIGIN_Y = 80;

    private Timer timer;
    private Hook hook = new Hook();
    private List<Item> items = new ArrayList<>();
    private Random rand = new Random();
    private int score = 0;
    private int targetScore = 650;
    private int level = 1;
    private boolean gameOver = false;

    // 钩子状态
    class Hook {
        double angle = 0;
        double length = 0;
        double maxLength = 450;
        double swingSpeed = 1.2;
        boolean extending = false;
        boolean retracting = false;
        Item caught = null;

        double getTipX() { return HOOK_ORIGIN_X + Math.sin(angle) * length; }
        double getTipY() { return HOOK_ORIGIN_Y + Math.cos(angle) * length; }
    }

    class Item {
        double x, y;
        int value;
        double weight;
        double radius;
        Color color;
        String name;

        Item(double x, double y, int value, double weight, double radius, Color color, String name) {
            this.x = x; this.y = y; this.value = value;
            this.weight = weight; this.radius = radius;
            this.color = color; this.name = name;
        }
    }

    public GoldMinerGame() {
        setPreferredSize(new Dimension(WIDTH, HEIGHT));
        setBackground(new Color(135, 206, 235)); // 天空蓝
        addKeyListener(this);
        setFocusable(true);
        timer = new Timer(20, this);

        generateLevel();
    }

    /**
     * 生成关卡物品
     * 贪心策略确保物品分布均匀且有价值梯度
     */
    private void generateLevel() {
        items.clear();
        hook.caught = null;
        hook.length = 0;
        hook.extending = false;
        hook.retracting = false;

        // 大金块(高价值高重量)
        for (int i = 0; i < 2 + level; i++) {
            items.add(new Item(
                rand.nextInt(WIDTH - 100) + 50,
                rand.nextInt(300) + 200,
                50 * (rand.nextInt(3) + 1), 8, 25,
                Color.YELLOW, "大金块"
            ));
        }

        // 小金块(中等价值)
        for (int i = 0; i < 5 + level * 2; i++) {
            items.add(new Item(
                rand.nextInt(WIDTH - 100) + 50,
                rand.nextInt(300) + 200,
                15 * (rand.nextInt(3) + 1), 3, 12,
                new Color(255, 215, 0), "小金块"
            ));
        }

        // 钻石(高价值轻重量,最优目标)
        for (int i = 0; i < 2 + level; i++) {
            items.add(new Item(
                rand.nextInt(WIDTH - 100) + 50,
                rand.nextInt(300) + 200,
                100 + rand.nextInt(100), 1, 8,
                Color.CYAN, "钻石"
            ));
        }

        // 石头(障碍物,低价值高重量)
        for (int i = 0; i < 3 + level; i++) {
            items.add(new Item(
                rand.nextInt(WIDTH - 100) + 50,
                rand.nextInt(300) + 200,
                5 + rand.nextInt(10), 15, 20,
                Color.GRAY, "石头"
            ));
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (gameOver) return;
        double dt = 0.02;

        // 摆动阶段
        if (!hook.extending && !hook.retracting) {
            hook.angle += hook.swingSpeed * dt;
            if (hook.angle > Math.PI / 3 || hook.angle < -Math.PI / 3) {
                hook.swingSpeed = -hook.swingSpeed;
            }
        }

        // 发射阶段
        if (hook.extending) {
            hook.length += 250 * dt;

            // 检测碰撞
            double tipX = hook.getTipX();
            double tipY = hook.getTipY();

            for (Item item : items) {
                double dist = Math.sqrt((tipX - item.x) * (tipX - item.x) 
                                      + (tipY - item.y) * (tipY - item.y));
                if (dist < item.radius + 5) {
                    hook.caught = item;
                    hook.extending = false;
                    hook.retracting = true;
                    break;
                }
            }

            // 到达边界
            if (hook.length >= hook.maxLength) {
                hook.extending = false;
                hook.retracting = true;
            }
        }

        // 回收阶段
        if (hook.retracting) {
            double weight = (hook.caught != null) ? hook.caught.weight : 1;
            double speed = 120 / Math.sqrt(weight);
            hook.length -= speed * dt;

            if (hook.length <= 0) {
                hook.length = 0;
                hook.retracting = false;
                if (hook.caught != null) {
                    score += hook.caught.value;
                    items.remove(hook.caught);
                    hook.caught = null;

                    // 检查过关
                    if (score >= targetScore) {
                        level++;
                        targetScore += 200;
                        generateLevel();
                    }

                    // 检查是否还有物品
                    if (items.isEmpty()) {
                        generateLevel();
                    }
                }
            }
        }

        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        // 绘制地面背景
        g.setColor(new Color(194, 178, 128));
        g.fillRect(0, 180, WIDTH, HEIGHT - 180);

        // 绘制绳索
        g.setColor(Color.BLACK);
        int tipX = (int) hook.getTipX();
        int tipY = (int) hook.getTipY();
        g.drawLine(HOOK_ORIGIN_X, HOOK_ORIGIN_Y, tipX, tipY);

        // 绘制钩子
        g.setColor(Color.DARK_GRAY);
        g.fillOval(tipX - 5, tipY - 5, 10, 10);

        // 绘制被抓物品(跟随钩子)
        if (hook.caught != null) {
            Item item = hook.caught;
            item.x = tipX;
            item.y = tipY + item.radius;
            drawItem(g, item);
        }

        // 绘制所有物品
        for (Item item : items) {
            if (item != hook.caught) {
                drawItem(g, item);
            }
        }

        // UI
        g.setColor(Color.BLACK);
        g.setFont(new Font("Microsoft YaHei", Font.BOLD, 18));
        g.drawString("分数: " + score + " / 目标: " + targetScore, 20, 30);
        g.drawString("关卡: " + level, 20, 55);
        g.drawString("剩余物品: " + items.size(), 20, 80);

        // 提示信息
        if (!hook.extending && !hook.retracting) {
            g.setColor(Color.RED);
            g.drawString("按空格键发射钩子", WIDTH/2 - 80, 30);
        }
    }

    private void drawItem(Graphics g, Item item) {
        g.setColor(item.color);
        g.fillOval((int)(item.x - item.radius), (int)(item.y - item.radius), 
                   (int)(item.radius * 2), (int)(item.radius * 2));
        g.setColor(Color.BLACK);
        g.drawOval((int)(item.x - item.radius), (int)(item.y - item.radius), 
                   (int)(item.radius * 2), (int)(item.radius * 2));
        g.setColor(Color.WHITE);
        g.setFont(new Font("Arial", Font.BOLD, 10));
        g.drawString(String.valueOf(item.value), (int)item.x - 5, (int)item.y + 4);
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_SPACE) {
            if (!hook.extending && !hook.retracting) {
                hook.extending = true;
            }
        }
    }

    public void start() { timer.start(); }

    @Override public void keyReleased(KeyEvent e) {}
    @Override public void keyTyped(KeyEvent e) {}

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("黄金矿工 - 物理抛物线与贪心策略");
            GoldMinerGame game = new GoldMinerGame();
            frame.add(game);
            frame.pack();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
            game.start();
        });
    }
}

算法核心要点总结

物理模拟关键公式:

  1. 钩子末端坐标:x = originX + sin(angle) * length,y = originY + cos(angle) * length
  2. 简谐摆动:angle += swingSpeed * dt,到达边界反弹
  3. 重量影响回收:retractSpeed = baseSpeed / sqrt(weight),重物品大幅减速

贪心策略应用:

  1. 目标选择贪心:在钩子发射路径上,选择 value / weight 比率最高的物品
  2. 角度预测贪心:枚举小范围角度,预测能抓到最高价值物品的发射时机
  3. 关卡生成贪心:确保每关都有足够的高性价比物品(钻石)作为策略奖励

时间复杂度分析:
– 碰撞检测:O(N),N为场上物品数
– 最优角度搜索:O(M × N),M为角度枚举数
– 摆动更新:O(1)

扩展方向

  1. 弹道预测线:绘制虚线预览钩子轨迹,帮助玩家决策
  2. 道具系统:炸弹(清除周围物品)、大力水(加速回收)等
  3. 多钩子协同:两个钩子同时工作,需要协调避免碰撞