每日算法 — 使用java实现套圈游戏:抛体运动模拟与蒙特卡洛最优投掷策略

引言

套圈游戏是庙会、嘉年华中广受欢迎的娱乐项目:玩家手持圆环,站在指定距离外,试图将圆环套中远处的奖品立柱。看似简单的一抛,实际上涉及抛体运动学、空气阻力、投掷角度和力度的精确配合。本文将用Java实现一个物理精确的套圈游戏模拟器,并讲解如何运用蒙特卡洛方法搜索最优投掷策略,让AI以最高概率套中目标。

一、游戏规则与物理建模

1.1 基本规则

  • 场地中分布若干个立柱(目标),每个立柱有不同的分值
  • 玩家站在投掷线后,距离最近的立柱 $D$ 米
  • 每次投掷需要指定:投掷角度(仰角)$\theta$、初速度 $v_0$、横向偏移角 $\phi$
  • 圆环以抛物线轨迹飞行,若圆环中心穿过立柱且在落地前套中,则得分

1.2 抛体运动模型

忽略空气阻力时,圆环的运动轨迹由以下参数方程描述:

$$
x(t) = v_0 \cos\theta \cos\phi \cdot t
$$

$$
y(t) = v_0 \cos\theta \sin\phi \cdot t
$$

$$
z(t) = v_0 \sin\theta \cdot t – \frac{1}{2} g t^2
$$

其中 $g = 9.8 \, \text{m/s}^2$ 为重力加速度。

1.3 套中判定

圆环套中立柱的充分必要条件:

  1. 圆环在飞行过程中,水平位置与某立柱的水平距离小于圆环半径 $R$
  2. 圆环在穿越立柱高度时,其竖直方向的速度向下(即已经过了轨迹最高点)
  3. 圆环在套中立柱时仍处于空中($z > 0$)

二、核心算法设计

2.1 轨迹计算

给定投掷参数,计算圆环在任意时刻的位置,并判断是否套中目标。

2.2 蒙特卡洛最优策略搜索

由于投掷涉及多个连续参数(角度、速度、横向偏移),解析求解最优参数非常困难。蒙特卡洛方法通过大量随机采样,找到使套中概率最高的参数组合。

2.3 带噪声的投掷模型

模拟人类投掷的不确定性:实际投掷参数 = 目标参数 + 随机噪声(服从正态分布)。AI需要在”噪声容忍度”和”套中概率”之间找到平衡。

三、Java完整实现

import java.util.*;

/**
 * 套圈游戏 — 抛体运动模拟与蒙特卡洛最优投掷策略
 * 
 * 核心设计:
 * 1. 抛体运动物理引擎(三维轨迹计算)
 * 2. 套中判定几何检测
 * 3. 蒙特卡洛参数搜索
 * 4. 带噪声的现实投掷模拟
 */
public class RingTossGame {

    // 物理常数
    private static final double G = 9.8;           // 重力加速度 (m/s^2)
    private static final double TIME_STEP = 0.01;  // 模拟时间步长 (s)

    // 圆环参数
    private static final double RING_RADIUS = 0.15; // 圆环半径 (m)
    private static final double RING_MASS = 0.1;    // 圆环质量 (kg)

    // 投掷参数范围
    private static final double MIN_VELOCITY = 3.0;   // 最小初速度 (m/s)
    private static final double MAX_VELOCITY = 12.0;  // 最大初速度 (m/s)
    private static final double MIN_ANGLE = Math.toRadians(10);   // 最小仰角
    private static final double MAX_ANGLE = Math.toRadians(80);   // 最大仰角
    private static final double MAX_PHI = Math.toRadians(15);     // 最大横向偏移

    // 投掷噪声(模拟人类不稳定性)
    private static final double VELOCITY_SIGMA = 0.3;  // 速度标准差
    private static final double ANGLE_SIGMA = Math.toRadians(3);  // 角度标准差

    private List<Peg> pegs;      // 立柱列表
    private Random random;
    private double throwLineX;   // 投掷线X坐标

    // 立柱类
    static class Peg {
        double x, y;      // 水平位置
        double height;    // 立柱高度
        double radius;    // 立柱半径
        int score;        // 分值
        String name;

        Peg(String name, double x, double y, double height, double radius, int score) {
            this.name = name;
            this.x = x;
            this.y = y;
            this.height = height;
            this.radius = radius;
            this.score = score;
        }

        double distanceTo(double px, double py) {
            return Math.hypot(x - px, y - py);
        }
    }

    // 投掷参数类
    static class ThrowParams {
        double velocity;   // 初速度
        double theta;      // 仰角(与水平面夹角)
        double phi;        // 横向偏移角

        ThrowParams(double v, double t, double p) {
            this.velocity = v;
            this.theta = t;
            this.phi = p;
        }

        /**
         * 添加随机噪声,模拟实际投掷偏差
         */
        ThrowParams withNoise(Random rand) {
            double v = velocity + rand.nextGaussian() * VELOCITY_SIGMA;
            double t = theta + rand.nextGaussian() * ANGLE_SIGMA;
            double p = phi + rand.nextGaussian() * ANGLE_SIGMA;
            return new ThrowParams(
                Math.max(MIN_VELOCITY, Math.min(MAX_VELOCITY, v)),
                Math.max(MIN_ANGLE, Math.min(MAX_ANGLE, t)),
                Math.max(-MAX_PHI, Math.min(MAX_PHI, p))
            );
        }

        @Override
        public String toString() {
            return String.format("v=%.2fm/s, theta=%.1f°, phi=%.1f°",
                velocity, Math.toDegrees(theta), Math.toDegrees(phi));
        }
    }

    // 轨迹点
    static class TrajectoryPoint {
        double t, x, y, z, vx, vy, vz;
        TrajectoryPoint(double t, double x, double y, double z, double vx, double vy, double vz) {
            this.t = t; this.x = x; this.y = y; this.z = z;
            this.vx = vx; this.vy = vy; this.vz = vz;
        }
    }

    public RingTossGame() {
        this.random = new Random();
        this.pegs = new ArrayList<>();
        this.throwLineX = 0;
        initializeField();
    }

    /**
     * 初始化场地:设置若干立柱
     */
    void initializeField() {
        // 近处低分立柱
        pegs.add(new Peg("近左", 3.0, -0.5, 0.3, 0.02, 10));
        pegs.add(new Peg("近中", 3.0, 0.0, 0.3, 0.02, 10));
        pegs.add(new Peg("近右", 3.0, 0.5, 0.3, 0.02, 10));

        // 中距离中分立柱
        pegs.add(new Peg("中左", 5.0, -0.8, 0.4, 0.015, 30));
        pegs.add(new Peg("中中", 5.0, 0.0, 0.4, 0.015, 30));
        pegs.add(new Peg("中右", 5.0, 0.8, 0.4, 0.015, 30));

        // 远处高分立柱
        pegs.add(new Peg("远左", 7.0, -1.0, 0.5, 0.01, 50));
        pegs.add(new Peg("远中", 7.0, 0.0, 0.5, 0.01, 50));
        pegs.add(new Peg("远右", 7.0, 1.0, 0.5, 0.01, 50));

        // 超远距离大奖立柱
        pegs.add(new Peg("大奖", 9.0, 0.0, 0.6, 0.008, 100));
    }

    /**
     * 计算抛体轨迹
     * 
     * 参数:
     * - velocity: 初速度 (m/s)
     * - theta: 仰角 (rad)
     * - phi: 横向偏移角 (rad)
     * 
     * 返回轨迹上的所有采样点
     */
    public List<TrajectoryPoint> calculateTrajectory(double velocity, double theta, double phi) {
        List<TrajectoryPoint> points = new ArrayList<>();

        double vx = velocity * Math.cos(theta) * Math.cos(phi);
        double vy = velocity * Math.cos(theta) * Math.sin(phi);
        double vz = velocity * Math.sin(theta);

        double x = throwLineX, y = 1.5, z = 1.5; // 投掷高度约1.5米
        double t = 0;

        points.add(new TrajectoryPoint(t, x, y, z, vx, vy, vz));

        // 数值积分模拟运动,直到落地
        while (z >= 0 && t < 5.0) { // 最多模拟5秒
            t += TIME_STEP;

            // 更新速度(仅重力影响z方向)
            vz -= G * TIME_STEP;

            // 更新位置
            x += vx * TIME_STEP;
            y += vy * TIME_STEP;
            z += vz * TIME_STEP;

            points.add(new TrajectoryPoint(t, x, y, z, vx, vy, vz));
        }

        return points;
    }

    /**
     * 检查轨迹是否套中某个立柱
     * 
     * 套中条件:
     * 1. 圆环中心与立柱的水平距离 < 圆环半径
     * 2. 圆环在立柱高度处时,z方向速度向下(已经过了最高点)
     * 3. 套中时圆环仍在空中
     */
    public Peg checkHit(List<TrajectoryPoint> trajectory) {
        for (int i = 1; i < trajectory.size(); i++) {
            TrajectoryPoint p = trajectory.get(i);
            if (p.z <= 0) break; // 已落地

            // 只检查下降阶段(vz < 0 表示正在下降)
            if (p.vz >= 0) continue;

            for (Peg peg : pegs) {
                double hDist = peg.distanceTo(p.x, p.y);

                // 水平距离在圆环半径内,且高度接近立柱顶部
                if (hDist < RING_RADIUS && Math.abs(p.z - peg.height) < 0.1) {
                    return peg;
                }
            }
        }
        return null;
    }

    /**
     * 单次投掷模拟
     */
    public int throwRing(ThrowParams params) {
        List<TrajectoryPoint> traj = calculateTrajectory(params.velocity, params.theta, params.phi);
        Peg hit = checkHit(traj);
        return (hit != null) ? hit.score : 0;
    }

    /**
     * 带噪声的多次投掷模拟
     * 
     * 模拟同一个"目标参数"下,由于人体不稳定性导致的实际投掷偏差
     */
    public double simulateSuccessRate(ThrowParams targetParams, int trials) {
        int success = 0;
        int totalScore = 0;

        for (int i = 0; i < trials; i++) {
            ThrowParams actual = targetParams.withNoise(random);
            int score = throwRing(actual);
            if (score > 0) success++;
            totalScore += score;
        }

        return (double) success / trials;
    }

    /**
     * 蒙特卡洛搜索最优投掷参数
     * 
     * 策略:
     * 1. 在参数空间(速度、仰角、横向偏移)中随机采样
     * 2. 对每个采样点,进行带噪声的多次模拟
     * 3. 选择成功率最高的参数组合
     */
    public ThrowParams findOptimalParamsMC(int samples, int trialsPerSample) {
        ThrowParams best = null;
        double bestRate = -1;

        System.out.println("开始蒙特卡洛搜索最优参数...");

        for (int s = 0; s < samples; s++) {
            // 随机采样参数
            double v = MIN_VELOCITY + random.nextDouble() * (MAX_VELOCITY - MIN_VELOCITY);
            double theta = MIN_ANGLE + random.nextDouble() * (MAX_ANGLE - MIN_ANGLE);
            double phi = (random.nextDouble() * 2 - 1) * MAX_PHI;

            ThrowParams candidate = new ThrowParams(v, theta, phi);
            double rate = simulateSuccessRate(candidate, trialsPerSample);

            if (rate > bestRate) {
                bestRate = rate;
                best = candidate;
                System.out.printf("  样本 %d/%d: %s, 成功率=%.2f%%%n",
                    s + 1, samples, candidate, rate * 100);
            }
        }

        System.out.printf("\n最优参数: %s%n", best);
        System.out.printf("估计成功率: %.2f%%%n", bestRate * 100);
        return best;
    }

    /**
     * 精细搜索:在最优解附近进行局部细化
     */
    public ThrowParams fineTuneParams(ThrowParams roughBest, int samples, int trialsPerSample) {
        double vRange = 1.0;   // ±1 m/s
        double aRange = Math.toRadians(10); // ±10度
        double pRange = Math.toRadians(5);  // ±5度

        ThrowParams best = roughBest;
        double bestRate = simulateSuccessRate(roughBest, trialsPerSample * 2);

        System.out.println("\n开始局部精细搜索...");

        for (int s = 0; s < samples; s++) {
            double v = roughBest.velocity + (random.nextDouble() * 2 - 1) * vRange;
            double theta = roughBest.theta + (random.nextDouble() * 2 - 1) * aRange;
            double phi = roughBest.phi + (random.nextDouble() * 2 - 1) * pRange;

            v = Math.max(MIN_VELOCITY, Math.min(MAX_VELOCITY, v));
            theta = Math.max(MIN_ANGLE, Math.min(MAX_ANGLE, theta));
            phi = Math.max(-MAX_PHI, Math.min(MAX_PHI, phi));

            ThrowParams candidate = new ThrowParams(v, theta, phi);
            double rate = simulateSuccessRate(candidate, trialsPerSample);

            if (rate > bestRate) {
                bestRate = rate;
                best = candidate;
            }
        }

        System.out.printf("精细搜索后最优: %s, 成功率=%.2f%%%n", best, bestRate * 100);
        return best;
    }

    /**
     * AI完整决策流程
     */
    public ThrowParams aiDecision() {
        // 阶段1:粗搜索
        ThrowParams rough = findOptimalParamsMC(200, 50);

        // 阶段2:精细搜索
        ThrowParams fine = fineTuneParams(rough, 300, 100);

        return fine;
    }

    /**
     * 打印轨迹(文本可视化)
     */
    public void visualizeTrajectory(ThrowParams params) {
        List<TrajectoryPoint> traj = calculateTrajectory(params.velocity, params.theta, params.phi);
        Peg hit = checkHit(traj);

        System.out.println("\n=== 轨迹可视化 ===");
        System.out.printf("参数: %s%n", params);
        System.out.println("距离(m) | 高度(m) | 标记");
        System.out.println("-".repeat(40));

        for (TrajectoryPoint p : traj) {
            if (p.x < throwLineX) continue;
            if (p.x > 10) break;

            String marker = "  ";
            for (Peg peg : pegs) {
                if (Math.abs(p.x - peg.x) < 0.1 && Math.abs(p.z - peg.height) < 0.2) {
                    marker = peg.name.substring(0, Math.min(2, peg.name.length()));
                }
            }

            if (Math.abs(p.x - Math.round(p.x)) < 0.05 || !marker.equals("  ")) {
                int heightBar = (int) (p.z * 10);
                StringBuilder bar = new StringBuilder();
                for (int i = 0; i < heightBar; i++) bar.append("*");
                System.out.printf(" %6.2f | %6.2f | %s %s%n", p.x, p.z, bar, marker);
            }
        }

        if (hit != null) {
            System.out.println("结果: 套中 " + hit.name + "!得分: " + hit.score);
        } else {
            System.out.println("结果: 未套中。");
        }
    }

    /**
     * 运行完整游戏
     */
    public void runGame(int rounds) {
        System.out.println("===== 套圈游戏开始 =====");
        System.out.println("场地中有 " + pegs.size() + " 个立柱");
        for (Peg p : pegs) {
            System.out.printf("  %s: 距离=%.1fm, 高度=%.1fm, 分值=%d%n",
                p.name, p.x, p.height, p.score);
        }

        // AI学习最优参数
        ThrowParams optimal = aiDecision();

        // 使用最优参数进行多轮投掷
        System.out.println("\n===== 使用最优参数投掷 =====");
        int totalScore = 0;
        int hits = 0;

        for (int r = 1; r <= rounds; r++) {
            ThrowParams actual = optimal.withNoise(random);
            int score = throwRing(actual);
            totalScore += score;
            if (score > 0) hits++;

            System.out.printf("第%2d轮: %s -> ", r, actual);
            if (score > 0) {
                System.out.println("命中!得分 " + score);
            } else {
                System.out.println("未命中");
            }
        }

        System.out.println("\n===== 游戏结束 =====");
        System.out.printf("总轮数: %d, 命中次数: %d, 命中率: %.1f%%%n",
            rounds, hits, 100.0 * hits / rounds);
        System.out.printf("总得分: %d, 平均得分: %.1f%n",
            totalScore, (double) totalScore / rounds);

        // 可视化最优轨迹
        visualizeTrajectory(optimal);
    }

    public static void main(String[] args) {
        RingTossGame game = new RingTossGame();
        game.runGame(20);
    }
}

四、算法复杂度分析

模块 时间复杂度 空间复杂度 说明
轨迹计算 $O(T / \Delta t)$ $O(T / \Delta t)$ $T$为飞行时间,$\Delta t$为步长
套中检测 $O(P \times T / \Delta t)$ $O(1)$ $P$为立柱数量
单次投掷模拟 $O(P \times T / \Delta t)$ $O(T / \Delta t)$ 含轨迹存储
成功率估计 $O(trials \times P \times T / \Delta t)$ $O(T / \Delta t)$ 多次带噪声模拟
蒙特卡洛搜索 $O(samples \times trials \times P \times T / \Delta t)$ $O(T / \Delta t)$ 整体搜索

在实际参数下($T \approx 2$s,$\Delta t = 0.01$s),单次轨迹约200个点,计算极为轻量。

五、策略优化方向

5.1 解析解法

对于单立柱情况,可以通过求解抛体运动方程得到精确的解析解:给定目标距离 $D$ 和高度 $H$,求满足 $x(T) = D$ 且 $z(T) = H$ 的 $(v_0, \theta)$ 组合。这转化为求解二元非线性方程组,可用牛顿迭代法高效求解。

5.2 多目标优化

场地中有多个不同分值的立柱时,AI需要在”高分但难套中”和”低分但易套中”之间权衡。可建立期望得分函数:

$$
E[\text{score}] = \sum_{i} P(\text{hit}_i) \times \text{score}_i
$$

蒙特卡洛搜索直接以最大化期望得分为目标。

5.3 物理精细化

当前模型忽略了空气阻力。对于轻质的塑料圆环,空气阻力的影响不可忽略。可以引入阻力项:

$$
\vec{F}_d = -\frac{1}{2} \rho C_d A |\vec{v}| \vec{v}
$$

其中 $\rho$ 为空气密度,$C_d$ 为阻力系数,$A$ 为圆环截面积。

六、总结

本文实现了一个物理精确的套圈游戏模拟器,核心算法亮点包括:

  • 抛体运动引擎:三维轨迹的数值积分模拟
  • 几何套中判定:水平距离与高度双重检测
  • 蒙特卡洛参数搜索:两阶段搜索(粗搜索+精细搜索)找到最优投掷参数
  • 噪声模型:用高斯分布模拟人类投掷的不稳定性,使AI策略更具鲁棒性

读者可以在此基础上添加风力影响、旋转动力学(圆环的自旋效应)、或设计在线对战模式,将这个物理模拟扩展为完整的嘉年华游戏体验。