引言
套圈游戏是庙会、嘉年华中广受欢迎的娱乐项目:玩家手持圆环,站在指定距离外,试图将圆环套中远处的奖品立柱。看似简单的一抛,实际上涉及抛体运动学、空气阻力、投掷角度和力度的精确配合。本文将用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 套中判定
圆环套中立柱的充分必要条件:
- 圆环在飞行过程中,水平位置与某立柱的水平距离小于圆环半径 $R$
- 圆环在穿越立柱高度时,其竖直方向的速度向下(即已经过了轨迹最高点)
- 圆环在套中立柱时仍处于空中($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策略更具鲁棒性
读者可以在此基础上添加风力影响、旋转动力学(圆环的自旋效应)、或设计在线对战模式,将这个物理模拟扩展为完整的嘉年华游戏体验。