打地鼠(Whack-a-Mole)是一款考验反应速度的经典街机游戏,地鼠随机从多个洞穴中冒出,玩家需要在限定时间内尽可能多地击中它们。本文用Java实现一个带AI决策的打地鼠系统,核心讲解贪心策略如何决定最优击打顺序,以及基于滑动窗口的反应时间统计模型。
游戏规则与算法建模
假设游戏界面有N个洞穴排成一行,地鼠按批次随机出现,每只地鼠有存活时间T。玩家每次移动锤子需要时间cost,必须在目标消失前完成击打。AI需要解决的核心问题是:当多只地鼠同时出现时,如何规划击打顺序以最大化得分?
这本质上是一个带时间窗口的调度问题,可建模为:
– 每个地鼠是一个任务,有出现时间start、存活截止时间deadline、位置pos、分值score
– 玩家从位置p移动到位置q需要时间|p-q|×moveCost
– 在deadline前到达并完成击打才能获得score
贪心策略设计
最优击打策略采用贪心选择:每次从当前可到达的地鼠中,选择单位时间收益最高且剩余时间最紧迫的目标。具体规则:
- 可到达性筛选:计算从当前位置到达每只地鼠所需时间,筛选出deadline允许到达的目标。
- 紧急度排序:优先选择deadline最早的地鼠,避免错过。
- 得分密度权衡:在紧急度相近时,优先选择分值高、距离近的目标。
Java 实现
import java.util.*;
public class WhackAMole {
static class Mole {
int id, pos, score;
long appearTime, deadline; // 毫秒时间戳
boolean hit = false;
Mole(int id, int pos, int score, long appear, long aliveMs) {
this.id = id; this.pos = pos; this.score = score;
this.appearTime = appear; this.deadline = appear + aliveMs;
}
}
static class GameResult {
int totalScore, hitCount, missCount;
double avgReactionMs;
List<Long> reactionTimes = new ArrayList<>();
}
private final int moveCostPerUnit; // 每单位距离移动耗时(ms)
private final int hitCost; // 击打动作耗时(ms)
private int currentPos = 0;
private long currentTime = 0;
public WhackAMole(int moveCost, int hitCost) {
this.moveCostPerUnit = moveCost;
this.hitCost = hitCost;
}
// 贪心策略:选择最优目标
public Mole selectTarget(List<Mole> activeMoles) {
Mole best = null;
double bestPriority = Double.NEGATIVE_INFINITY;
for (Mole m : activeMoles) {
if (m.hit) continue;
int dist = Math.abs(m.pos - currentPos);
long travelTime = (long) dist * moveCostPerUnit + hitCost;
long arriveTime = currentTime + travelTime;
if (arriveTime > m.deadline) continue; // 来不及
long slack = m.deadline - arriveTime; // 剩余缓冲时间
// 优先级 = 得分 / (距离+1) + 紧急度奖励(缓冲越小越紧急)
double priority = (double) m.score / (dist + 1) + 1000.0 / (slack + 1);
if (priority > bestPriority) {
bestPriority = priority;
best = m;
}
}
return best;
}
// 模拟完整游戏流程
public GameResult playGame(List<Mole> moles, long gameDurationMs) {
GameResult result = new GameResult();
PriorityQueue<Mole> appearQueue = new PriorityQueue<>(Comparator.comparingLong(m -> m.appearTime));
appearQueue.addAll(moles);
List<Mole> active = new ArrayList<>();
while (currentTime < gameDurationMs) {
// 将新出现的地鼠加入活动列表
while (!appearQueue.isEmpty() && appearQueue.peek().appearTime <= currentTime) {
active.add(appearQueue.poll());
}
// 清理已过期的地鼠
Iterator<Mole> it = active.iterator();
while (it.hasNext()) {
Mole m = it.next();
if (m.deadline < currentTime && !m.hit) {
it.remove();
result.missCount++;
}
}
Mole target = selectTarget(active);
if (target == null) {
// 没有可击打目标,快进时间到下一个地鼠出现
currentTime = appearQueue.isEmpty() ? gameDurationMs : appearQueue.peek().appearTime;
continue;
}
// 执行移动和击打
int dist = Math.abs(target.pos - currentPos);
long travelTime = (long) dist * moveCostPerUnit;
currentTime += travelTime + hitCost;
currentPos = target.pos;
target.hit = true;
long reaction = currentTime - target.appearTime;
result.reactionTimes.add(reaction);
result.totalScore += target.score;
result.hitCount++;
active.remove(target);
}
// 统计反应时间
if (!result.reactionTimes.isEmpty()) {
long sum = 0;
for (long rt : result.reactionTimes) sum += rt;
result.avgReactionMs = (double) sum / result.reactionTimes.size();
}
return result;
}
// 滑动窗口反应时间统计:分析玩家/AI的反应能力变化趋势
public static double[] slidingWindowAvg(List<Long> reactionTimes, int windowSize) {
int n = reactionTimes.size();
double[] avgs = new double[n];
long windowSum = 0;
LinkedList<Long> window = new LinkedList<>();
for (int i = 0; i < n; i++) {
window.addLast(reactionTimes.get(i));
windowSum += reactionTimes.get(i);
if (window.size() > windowSize) {
windowSum -= window.pollFirst();
}
avgs[i] = (double) windowSum / window.size();
}
return avgs;
}
public static void main(String[] args) {
Random rand = new Random(42);
List<Mole> moles = new ArrayList<>();
long gameDuration = 30000; // 30秒游戏
// 随机生成50只地鼠
for (int i = 0; i < 50; i++) {
long appear = rand.nextInt((int) gameDuration);
int pos = rand.nextInt(10); // 10个洞穴
int score = 10 + rand.nextInt(90); // 10-100分
long alive = 800 + rand.nextInt(1200); // 存活0.8-2秒
moles.add(new Mole(i, pos, score, appear, alive));
}
WhackAMole game = new WhackAMole(50, 100); // 每格50ms,击打100ms
GameResult result = game.playGame(moles, gameDuration);
System.out.println("Hits: " + result.hitCount + " | Misses: " + result.missCount);
System.out.println("Total Score: " + result.totalScore);
System.out.println("Avg Reaction: " + String.format("%.1f", result.avgReactionMs) + "ms");
// 滑动窗口分析
double[] trend = slidingWindowAvg(result.reactionTimes, 5);
System.out.println("Reaction trend (last 5 avg): " +
String.format("%.1f", trend[trend.length - 1]) + "ms");
}
}
复杂度分析
- 时间复杂度:每次选择目标需要O(M)遍历活跃地鼠列表,M为同时出现的地鼠数。总复杂度O(K×M),K为地鼠总数。
- 空间复杂度:O(M)存储活跃地鼠列表,以及O(K)存储反应时间记录。
策略对比与扩展
| 策略 | 平均得分 | 漏击率 | 适用场景 |
|---|---|---|---|
| 随机选择 | 低 | 高 | 无AI |
| 最近优先 | 中 | 中 | 简单模式 |
| 贪心(本文) | 高 | 低 | 标准模式 |
| 动态规划 | 最优 | 最低 | 理论分析 |
对于更复杂的场景(如连击奖励、炸弹惩罚),贪心策略可扩展为带权调度,或引入强化学习训练最优策略网络。