一、背景介绍
爆破气球(Balloon Pop)是一款简单但富有策略性的休闲游戏。玩家在限定次数内按顺序刺破漂浮在不同高度的气球,每次爆破会产生连锁反应——爆开的气球可能触发相邻气球也爆破,触发额外的得分奖励。游戏目标是在最少次数内获得最高总分。
本游戏的核心算法包括:
- 物理模拟:气球在浮力与重力作用下的运动轨迹。
- 连锁反应:爆破波在气球网络中的传播。
- 贪心选择:选择爆破顺序以最大化连锁消除数。
- 图论建模:把气球抽象为图节点,连锁关系作为边。
二、整体设计
Balloon:包含位置、半径、颜色、HP、爆裂状态。Physics:浮力、重力、空气阻力的简单物理模型。ChainGraph:气球连锁反应的有向图,BFS 传播爆裂。GreedyStrategy:贪心选择每次爆破哪只气球以触发最长链。GameMain:演示一次完整的爆破过程与得分。
三、完整代码实现
3.1 气球 Balloon.java
/**
* 气球
* 状态:位置 (x, y)、半径 r、HP、是否已爆
*/
public class Balloon {
public final int id;
public double x, y; // 位置
public double vx, vy; // 速度
public final double radius;
public int hp; // 剩余血量
public boolean popped; // 是否已爆
public final int color; // 颜色(决定连锁时影响范围)
public Balloon(int id, double x, double y, double radius, int hp, int color) {
this.id = id;
this.x = x; this.y = y;
this.vx = 0; this.vy = 0;
this.radius = radius;
this.hp = hp;
this.popped = false;
this.color = color;
}
public boolean overlaps(Balloon other) {
double dx = x - other.x, dy = y - other.y;
return Math.hypot(dx, dy) < (radius + other.radius);
}
}
3.2 物理模拟 Physics.java
import java.util.*;
/**
* 物理模拟
* 浮力向上、重力向下、空气阻力与速度成正比
* ay = (buoyancy - gravity) / mass - drag * vy / mass
*/
public class Physics {
public static final double G = 9.8;
public static final double AIR_DENSITY = 1.225;
public final double buoyancy; // 单位体积浮力
public final double drag; // 阻力系数
public final double dt; // 时间步长
public Physics(double buoyancy, double drag, double dt) {
this.buoyancy = buoyancy;
this.drag = drag;
this.dt = dt;
}
public static Physics helium() {
// 氦气浮力约 1.1 kg/m^3
return new Physics(11.0, 0.05, 0.02);
}
/**
* 推进一个时间步
* 浮力 = buoyancy * volume
* 体积 = 4/3 * pi * r^3
* 质量 mass = volume * AIR_DENSITY
*/
public void step(Balloon b) {
double volume = (4.0 / 3.0) * Math.PI * Math.pow(b.radius, 3);
double mass = volume * AIR_DENSITY;
double netForce = (buoyancy - AIR_DENSITY * G) * volume;
double ay = netForce / mass - drag * b.vy / mass;
b.vy += ay * dt;
b.vx *= 0.99; // 水平轻微阻尼
b.x += b.vx * dt;
b.y += b.vy * dt;
// 地面反弹
if (b.y < b.radius) {
b.y = b.radius;
b.vy = -b.vy * 0.5; // 弹性碰撞
}
}
}
3.3 连锁图 ChainGraph.java
import java.util.*;
/**
* 连锁反应图
* 当气球 A 被刺破时,与 A 距离 <= 触发半径 r 的同色或近色气球也会被触发
* 使用 BFS 传播爆裂
*/
public class ChainGraph {
private final List<Balloon> balloons = new ArrayList<>();
private final double triggerRadius;
public ChainGraph(double triggerRadius) {
this.triggerRadius = triggerRadius;
}
public void addBalloon(Balloon b) { balloons.add(b); }
public List<Balloon> getBalloons() { return balloons; }
/**
* 计算邻居:距离 < triggerRadius 的其他气球
*/
public List<Balloon> neighbors(Balloon b) {
List<Balloon> result = new ArrayList<>();
for (Balloon other : balloons) {
if (other == b || other.popped) continue;
double dx = b.x - other.x, dy = b.y - other.y;
if (Math.hypot(dx, dy) < triggerRadius) result.add(other);
}
return result;
}
/**
* 触发爆裂链:从 balloon 出发 BFS 传播
* 返回被引爆的所有气球
*/
public List<Balloon> popChain(Balloon start) {
List<Balloon> popped = new ArrayList<>();
if (start.popped) return popped;
Queue<Balloon> queue = new ArrayDeque<>();
start.popped = true;
queue.add(start);
popped.add(start);
while (!queue.isEmpty()) {
Balloon cur = queue.poll();
for (Balloon nb : neighbors(cur)) {
if (nb.popped) continue;
// 同色一定触发;不同色概率触发
boolean trigger = (nb.color == cur.color) || Math.random() < 0.3;
if (trigger) {
nb.popped = true;
popped.add(nb);
queue.add(nb);
}
}
}
return popped;
}
}
3.4 贪心策略 GreedyStrategy.java
import java.util.*;
/**
* 贪心策略
* 给定一个 ChainGraph 与一个允许刺破次数 budget,
* 每次选择能触发最长连锁的气球优先爆破
* 目标:最大化总爆裂数
*/
public class GreedyStrategy {
private final ChainGraph graph;
private final int budget;
public GreedyStrategy(ChainGraph graph, int budget) {
this.graph = graph;
this.budget = budget;
}
/**
* 模拟贪心爆破
* 每一步:扫描未爆气球,对每个气球模拟链式反应,选取引爆数最大的
* 注意:模拟时需要临时回滚 popped 状态
*/
public SimulationResult simulate() {
int totalPopped = 0;
List<Integer> chosenIds = new ArrayList<>();
List<Integer> chainSizes = new ArrayList<>();
for (int step = 0; step < budget; step++) {
Balloon best = null;
int bestSize = -1;
// 扫描所有未爆气球
for (Balloon b : graph.getBalloons()) {
if (b.popped) continue;
// 模拟链式反应
Map<Balloon, Boolean> backup = new HashMap<>();
for (Balloon x : graph.getBalloons()) backup.put(x, x.popped);
List<Balloon> chain = graph.popChain(b);
int size = chain.size();
// 回滚
for (Map.Entry<Balloon, Boolean> e : backup.entrySet())
e.getKey().popped = e.getValue();
if (size > bestSize) { bestSize = size; best = b; }
}
if (best == null) break;
List<Balloon> chain = graph.popChain(best);
totalPopped += chain.size();
chosenIds.add(best.id);
chainSizes.add(chain.size());
}
return new SimulationResult(totalPopped, chosenIds, chainSizes);
}
public static class SimulationResult {
public final int totalPopped;
public final List<Integer> chosenIds;
public final List<Integer> chainSizes;
public SimulationResult(int total, List<Integer> ids, List<Integer> sizes) {
this.totalPopped = total;
this.chosenIds = ids;
this.chainSizes = sizes;
}
public void print() {
System.out.println("Total popped: " + totalPopped);
System.out.println("Choices (id, chainSize): " + chosenIds + " " + chainSizes);
}
}
}
3.5 主程序 GameMain.java
import java.util.*;
/**
* 爆破气球游戏主程序
* 模拟 30 个气球,玩家有 5 次刺破机会,贪心选择最大化总爆裂数
*/
public class GameMain {
public static void main(String[] args) {
Random rand = new Random(42);
ChainGraph graph = new ChainGraph(2.5);
// 随机生成 30 个气球
for (int i = 0; i < 30; i++) {
double x = rand.nextDouble() * 30.0;
double y = rand.nextDouble() * 15.0 + 1.0;
double r = 0.4 + rand.nextDouble() * 0.3;
int color = rand.nextInt(5);
graph.addBalloon(new Balloon(i, x, y, r, 1, color));
}
// 物理模拟若干步,让气球稳定
Physics physics = Physics.helium();
for (int t = 0; t < 100; t++) {
for (Balloon b : graph.getBalloons()) physics.step(b);
}
// 贪心爆破
GreedyStrategy strategy = new GreedyStrategy(graph, 5);
GreedyStrategy.SimulationResult res = strategy.simulate();
System.out.println("=== 爆破气球 - 贪心策略 ===");
res.print();
// 物理模拟后查看剩余气球
int remaining = 0;
for (Balloon b : graph.getBalloons()) if (!b.popped) remaining++;
System.out.println("Remaining balloons: " + remaining);
}
}
四、算法分析
4.1 物理模拟
- 每帧 O(N) 推进 N 个气球位置。
- 总模拟时间 100 帧,N=30 时约 3000 次运算。
4.2 连锁反应 BFS
- 单次 BFS 复杂度 O(N + E),E 为相邻对数。
- 由于 triggerRadius 较小,E << N^2。
4.3 贪心选择
- 每次扫描 N 个候选,每个候选模拟一次 BFS。
- 单次决策 O(N * (N + E))。
- 总决策 O(budget * N * (N + E)),budget=5、N=30 时约 4500 次运算。
4.4 贪心最优性
- 贪心算法在每一步选择局部最优,但不一定全局最优。例如可能存在先用一次机会触发小链,再用一次触发包含多个小链覆盖区的大链。
- 实际可使用动态规划精确求解,状态为 (剩余 budget, 已爆气球集合),但状态空间 2^N 太大。
五、可扩展方向
- 精确 DP:用 bitmask 表示已爆状态,求解最优爆破顺序。
- 蒙特卡洛搜索:用 MCTS 评估每步动作的长期收益。
- 多色连锁规则:同色必爆,相邻色概率爆,无关色不爆。
- 强化学习:用 PPO 训练自适应策略,应对不同关卡布局。
六、运行说明
将 Balloon.java、Physics.java、ChainGraph.java、GreedyStrategy.java、GameMain.java 放入同一目录,javac *.java && java GameMain,程序会输出贪心策略下的总爆裂数与每次选择的气球 ID。