引言
《红色警戒》作为经典的即时战略游戏,其核心策略在于如何高效地管理基地建设与资源采集。建造高级单位往往需要前置建筑,形成复杂的依赖链;而采矿车如何在多个矿区间选择最优路线,直接决定了经济优势。本文用Java实现拓扑排序解析建造依赖树与贪心策略调度资源采集,构建一个高效的基地运营决策引擎。
核心算法设计
1. 建造依赖的拓扑排序
游戏中的建筑之间存在严格的依赖关系:建造兵营需要先建发电厂,建造战车工厂需要先建兵营。这构成了一张有向无环图(DAG)。使用Kahn算法进行拓扑排序,可在O(V+E)时间内计算出合法的建造顺序,并检测循环依赖错误。
2. 资源采集贪心调度
采矿车面对多个矿区时,需要综合考虑:矿区剩余资源量、距离远近、当前排队车辆数。贪心策略每次选择”单位时间内期望收益最高”的矿区,实现资源采集效率最大化。
完整Java实现
import java.util.*;
/**
* 红色警戒风格基地运营算法
* 包含拓扑排序建造依赖解析与资源采集贪心调度
*/
public class RedAlertBase {
// 建筑定义
static class Building {
String name;
int cost; // 造价
int buildTime; // 建造时间(秒)
int power; // 耗电量(负值表示发电)
List<String> dependencies; // 前置建筑名称列表
Building(String name, int cost, int time, int power, List<String> deps) {
this.name = name;
this.cost = cost;
this.buildTime = time;
this.power = power;
this.dependencies = deps != null ? deps : new ArrayList<>();
}
}
// 矿区定义
static class OreField {
String name;
Vec2 pos; // 位置
int totalOre; // 总储量
int remainingOre; // 剩余储量
double miningRate; // 每秒开采速度
List<MiningTruck> queue; // 当前排队车辆
OreField(String name, Vec2 pos, int ore, double rate) {
this.name = name;
this.pos = pos;
this.totalOre = ore;
this.remainingOre = ore;
this.miningRate = rate;
this.queue = new ArrayList<>();
}
// 计算当前排队等待时间
double queueWaitTime() {
return queue.size() * 5.0; // 假设每车装载5秒
}
// 预计单位时间收益 = 矿区价值 / (往返时间 + 排队时间)
double expectedYield(Vec2 basePos, double truckSpeed) {
double distance = pos.dist(basePos);
double travelTime = distance / truckSpeed * 2; // 往返
double waitTime = queueWaitTime();
double effectiveRate = Math.min(miningRate, remainingOre / 100.0);
return effectiveRate / (travelTime + waitTime + 1.0);
}
}
static class Vec2 {
double x, y;
Vec2(double x, double y) { this.x = x; this.y = y; }
double dist(Vec2 v) {
return Math.sqrt((x-v.x)*(x-v.x) + (y-v.y)*(y-v.y));
}
}
static class MiningTruck {
String id;
Vec2 pos;
int capacity; // 运载容量
int load; // 当前装载量
OreField target; // 目标矿区
MiningTruck(String id, Vec2 pos, int cap) {
this.id = id;
this.pos = pos;
this.capacity = cap;
this.load = 0;
}
}
private Map<String, Building> buildingDB;
public RedAlertBase() {
buildingDB = new HashMap<>();
initBuildings();
}
private void initBuildings() {
// 发电厂不依赖任何建筑
buildingDB.put("发电厂", new Building("发电厂", 800, 10, 200, null));
// 兵营需要发电厂
buildingDB.put("兵营", new Building("兵营", 500, 15, -50,
Arrays.asList("发电厂")));
// 矿石精炼厂需要发电厂
buildingDB.put("矿石精炼厂", new Building("矿石精炼厂", 2000, 20, -75,
Arrays.asList("发电厂")));
// 战车工厂需要兵营和矿石精炼厂
buildingDB.put("战车工厂", new Building("战车工厂", 2000, 25, -100,
Arrays.asList("兵营", "矿石精炼厂")));
// 雷达站需要矿石精炼厂
buildingDB.put("雷达站", new Building("雷达站", 1000, 20, -150,
Arrays.asList("矿石精炼厂")));
// 作战实验室需要雷达站和战车工厂
buildingDB.put("作战实验室", new Building("作战实验室", 2000, 30, -200,
Arrays.asList("雷达站", "战车工厂")));
// 空指部需要作战实验室
buildingDB.put("空指部", new Building("空指部", 1500, 25, -100,
Arrays.asList("作战实验室")));
// 核弹发射井需要作战实验室
buildingDB.put("核弹发射井", new Building("核弹发射井", 5000, 60, -300,
Arrays.asList("作战实验室")));
}
/**
* Kahn算法拓扑排序:计算建造目标建筑所需的完整建造顺序
* 返回从基础到目标的合法建造序列
*/
public List<String> buildOrder(String target) {
if (!buildingDB.containsKey(target)) {
throw new IllegalArgumentException("未知建筑: " + target);
}
// 收集所有相关建筑(目标及其所有依赖)
Set<String> relevant = new HashSet<>();
collectDependencies(target, relevant);
// 构建子图的入度表
Map<String, Integer> inDegree = new HashMap<>();
Map<String, List<String>> adj = new HashMap<>();
for (String b : relevant) {
inDegree.put(b, 0);
adj.put(b, new ArrayList<>());
}
for (String b : relevant) {
Building building = buildingDB.get(b);
for (String dep : building.dependencies) {
if (relevant.contains(dep)) {
adj.get(dep).add(b); // dep -> b
inDegree.put(b, inDegree.get(b) + 1);
}
}
}
// Kahn算法
Queue<String> queue = new LinkedList<>();
for (String b : relevant) {
if (inDegree.get(b) == 0) queue.offer(b);
}
List<String> order = new ArrayList<>();
while (!queue.isEmpty()) {
String cur = queue.poll();
order.add(cur);
for (String next : adj.get(cur)) {
inDegree.put(next, inDegree.get(next) - 1);
if (inDegree.get(next) == 0) queue.offer(next);
}
}
if (order.size() != relevant.size()) {
throw new RuntimeException("存在循环依赖,无法建造");
}
return order;
}
private void collectDependencies(String building, Set<String> collected) {
if (collected.contains(building)) return;
collected.add(building);
Building b = buildingDB.get(building);
if (b != null) {
for (String dep : b.dependencies) {
collectDependencies(dep, collected);
}
}
}
/**
* 计算建造总成本与总时间
*/
public BuildPlan analyzeBuildPlan(String target) {
List<String> order = buildOrder(target);
int totalCost = 0;
int totalTime = 0;
int powerBalance = 0;
for (String name : order) {
Building b = buildingDB.get(name);
totalCost += b.cost;
totalTime += b.buildTime;
powerBalance += b.power;
}
return new BuildPlan(order, totalCost, totalTime, powerBalance);
}
static class BuildPlan {
List<String> order;
int totalCost, totalTime, powerBalance;
BuildPlan(List<String> o, int c, int t, int p) {
this.order = o; this.totalCost = c; this.totalTime = t; this.powerBalance = p;
}
}
/**
* 贪心调度:为每辆采矿车分配最优矿区
* 每次选择使全局收益增量最大的分配方案
*/
public void greedyAssignTrucks(List<MiningTruck> trucks, List<OreField> fields,
Vec2 basePos, double truckSpeed) {
// 清空旧队列
for (OreField f : fields) f.queue.clear();
for (MiningTruck truck : trucks) {
OreField bestField = null;
double bestYield = -1;
for (OreField field : fields) {
if (field.remainingOre <= 0) continue;
// 临时加入队列,计算边际收益
field.queue.add(truck);
double yield = field.expectedYield(basePos, truckSpeed);
field.queue.remove(truck);
if (yield > bestYield) {
bestYield = yield;
bestField = field;
}
}
if (bestField != null) {
truck.target = bestField;
bestField.queue.add(truck);
}
}
}
/**
* 模拟一帧资源采集更新
*/
public void simulateMining(List<MiningTruck> trucks, List<OreField> fields,
Vec2 basePos, double truckSpeed, double dt) {
for (MiningTruck truck : trucks) {
if (truck.target == null) continue;
OreField target = truck.target;
double distToField = truck.pos.dist(target.pos);
double distToBase = truck.pos.dist(basePos);
if (truck.load < truck.capacity && distToField > 1.0) {
// 前往矿区
Vec2 dir = new Vec2(
(target.pos.x - truck.pos.x) / distToField,
(target.pos.y - truck.pos.y) / distToField
);
truck.pos = new Vec2(
truck.pos.x + dir.x * truckSpeed * dt,
truck.pos.y + dir.y * truckSpeed * dt
);
} else if (truck.load < truck.capacity && distToField <= 1.0) {
// 在矿区装载
int mined = (int)(target.miningRate * dt);
mined = Math.min(mined, target.remainingOre);
mined = Math.min(mined, truck.capacity - truck.load);
truck.load += mined;
target.remainingOre -= mined;
} else if (truck.load >= truck.capacity && distToBase > 1.0) {
// 返回基地
Vec2 dir = new Vec2(
(basePos.x - truck.pos.x) / distToBase,
(basePos.y - truck.pos.y) / distToBase
);
truck.pos = new Vec2(
truck.pos.x + dir.x * truckSpeed * dt,
truck.pos.y + dir.y * truckSpeed * dt
);
} else if (truck.load >= truck.capacity && distToBase <= 1.0) {
// 卸货
truck.load = 0;
}
}
}
// 演示主方法
public static void main(String[] args) {
RedAlertBase game = new RedAlertBase();
// 测试拓扑排序
System.out.println("=== 建造核弹发射井的依赖分析 ===");
BuildPlan plan = game.analyzeBuildPlan("核弹发射井");
System.out.println("建造顺序: " + String.join(" -> ", plan.order));
System.out.printf("总造价: $%d, 总时间: %ds, 电力盈余: %d%n",
plan.totalCost, plan.totalTime, plan.powerBalance);
// 测试资源采集调度
System.out.println("\n=== 矿区采集贪心调度 ===");
Vec2 basePos = new Vec2(50, 50);
List<OreField> fields = Arrays.asList(
new OreField("富矿区A", new Vec2(20, 20), 5000, 10.0),
new OreField("贫矿区B", new Vec2(80, 30), 2000, 5.0),
new OreField("富矿区C", new Vec2(30, 80), 6000, 12.0)
);
List<MiningTruck> trucks = new ArrayList<>();
for (int i = 0; i < 5; i++) {
trucks.add(new MiningTruck("T" + i, new Vec2(50, 50), 100));
}
// 贪心分配
game.greedyAssignTrucks(trucks, fields, basePos, 10.0);
System.out.println("采矿车分配结果:");
for (MiningTruck t : trucks) {
System.out.printf(" %s -> %s%n", t.id, t.target != null ? t.target.name : "无");
}
// 模拟采集
System.out.println("\n模拟采集过程:");
for (int i = 0; i < 10; i++) {
game.simulateMining(trucks, fields, basePos, 10.0, 1.0);
if (i % 3 == 0) {
System.out.printf(" 第%ds: A=%d, B=%d, C=%d%n", i,
fields.get(0).remainingOre, fields.get(1).remainingOre, fields.get(2).remainingOre);
}
}
}
}
算法复杂度分析
| 算法模块 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| Kahn拓扑排序 | O(V+E) | O(V+E) |
| 建造计划分析 | O(V+E) | O(V) |
| 贪心矿车分配 | O(T×F) | O(F) |
| 单帧采集模拟 | O(T) | O(1) |
其中V为建筑种类数,E为依赖边数,T为矿车数量,F为矿区数量。
扩展思考
- 关键路径优化:在拓扑排序基础上,结合建造时间和资源约束,使用关键路径法(CPM)压缩总建造时间。
- 动态矿区重分配:当某个矿区枯竭或发现新矿区时,使用匈牙利算法进行全局最优重分配。
- 多基地协同:多个基地共享资源时,引入网络流模型求解全局最优运输方案。