黄金矿工是经典的休闲益智小游戏,玩家通过控制摆动的钩子抓取地下的金块、钻石和障碍物,在有限时间内获取最高分数。看似简单的操作背后,隐藏着简谐运动模拟、射线投射碰撞检测与贪心最优目标选择三大算法核心。本文将用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决策引擎:
- 简谐运动建模:用正弦函数精确描述钩子摆动,通过相位累加实现平滑动画。
- 射线-圆碰撞检测:将伸长路径抽象为射线,通过点到射线距离公式判断碰撞,避免了复杂的每帧全量碰撞检测。
- 贪心最优策略:以”价值/耗时”为评估指标,在离散化的摆角空间中搜索全局最优释放时机。
可扩展方向:
– 动态障碍物:引入移动的动物或炸弹,需要预测其未来位置,升级为带预测的碰撞检测。
– 多目标规划:当前每次只抓一个物品,可引入动态规划规划剩余时间内的最优抓取序列。
– 强化学习AI:用Q-Learning或策略梯度训练神经网络,让AI在大量对局中学习超越贪心策略的复杂决策。