游戏介绍
《火星地平线》(Mars Horizon )是一款太空探索 + 管理 + 策略模拟游戏 ,由英国独立工作室 Auroch Digital 开发,并由 The Irregular Corporation 发行。该作得到 欧洲航天局(ESA)和英国航天局的支持与咨询 ,目的在于提供真实感强、策略深度高的航天探索体验。
规则介绍

在《火星地平线》中,整个太空探索过程以回合制任务规划 的方式进行。
每一个回合,玩家需要在有限的行动次数内,合理选择并执行不同的指令,通过消耗已有资源来换取新的关键资源,从而逐步推进航天任务的完成。
开发辅助
1.Launcher.java
由于每次游戏命令列表是随机生成的,所以我们不能生搬硬套,必须写一个暴力求解程序。首先我们需要写一个接收输入变量的执行入口,使用的是Scanner。输入6~12条命令,这次任务是9条命令,再加上最后固定的充电命令。
java
import java.util.*;
public class Launcher {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
MarsSolver solver = new MarsSolver();
State init = new State();
List<Command> userCommands = new ArrayList<>();
// =====================================================
// 1. 输入命令
// =====================================================
while (true) {
String name;
while (true) {
System.out.print("\n请输入命令名称(或输入 0 结束命令输入): ");
name = scanner.nextLine().trim();
if ("0".equals(name)) break;
if (name.isEmpty()) {
System.out.println("❌ 命令名称不能为空");
continue;
}
break;
}
if ("0".equals(name)) break;
Map<String, Integer> consume = new HashMap<>();
Map<String, Integer> produce = new HashMap<>();
System.out.println(
"请输入【消耗】资源(格式:类型 数量)\n" +
"1-电力 2-磁力 3-数据 4-信号 5-动力 6-倾斜 7-温度 8-宇航员\n" +
"输入 0 结束:"
);
while (true) {
System.out.print("消耗> ");
String line = scanner.nextLine().trim();
if ("0".equals(line)) break;
try {
String[] p = line.split("\s+");
String key = typeToName(Integer.parseInt(p[0]));
int v = Integer.parseInt(p[1]);
consume.put(key, consume.getOrDefault(key, 0) + v);
} catch (Exception e) {
System.out.println("❌ 格式错误");
}
}
System.out.println(
"请输入【获得】资源(格式:类型 数量)\n" +
"1-电力 2-磁力 3-数据 4-信号 5-动力 6-倾斜 7-温度\n" +
"输入 0 结束:"
);
while (true) {
System.out.print("获得> ");
String line = scanner.nextLine().trim();
if ("0".equals(line)) break;
try {
String[] p = line.split("\s+");
String key = typeToName(Integer.parseInt(p[0]));
int v = Integer.parseInt(p[1]);
produce.put(key, produce.getOrDefault(key, 0) + v);
} catch (Exception e) {
System.out.println("❌ 格式错误");
}
}
userCommands.add(new Command(name, 1, consume, produce));
System.out.println("✅ 已添加命令:" + name);
}
// 固定充电命令
Map<String, Integer> charge = new HashMap<>();
charge.put("power", 1);
userCommands.add(new Command("Charge", 0, new HashMap<>(), charge));
MarsSolver.ALL = userCommands;
// =====================
// 命令校对 & 编辑阶段
// =====================
editCommands(scanner, userCommands);
MarsSolver.ALL = userCommands;
System.out.println("\n校对完成的命令列表:");
for (Command c : MarsSolver.ALL) {
System.out.println(c.getName() + " | 消耗: " + c.getConsume() + " | 获得: " + c.getProduce());
}
// =====================================================
// 2. 搜索参数
// =====================================================
MarsSolver.ROUNDS = readInt(scanner, "请输入回合数 ROUNDS: ");
MarsSolver.STEPS = readInt(scanner, "请输入每回合命令数 STEPS: ");
// =====================================================
// 3. 初始状态
// =====================================================
init.power = readInt(scanner, "请输入初始电力: ");
init.craw = readInt(scanner, "请输入宇航员数: ");
MarsSolver.crawPerRound = init.craw;
// 温度
MarsSolver.useTempLimit = readBool01(scanner, "是否启用【温度判定】?(1=是 0=否): ");
if (MarsSolver.useTempLimit) {
init.temp = readInt(scanner, "请输入初始温度: ");
if (readBool01(scanner, "是否设置温度下限?1是 0否: ")) {
MarsSolver.tempMin = readInt(scanner, "请输入温度下限: ");
}
if (readBool01(scanner, "是否设置温度上限?1是 0否: ")) {
MarsSolver.tempMax = readInt(scanner, "请输入温度上限: ");
}
MarsSolver.tempDeltaMin = readInt(scanner, "回合温度变化最小值: ");
MarsSolver.tempDeltaMax = readInt(scanner, "回合温度变化最大值: ");
}
// 倾斜
MarsSolver.useTiltLimit = readBool01(scanner, "是否启用【倾斜判定】?(1=是 0=否): ");
if (MarsSolver.useTiltLimit) {
init.tilt = readInt(scanner, "请输入初始倾斜: ");
MarsSolver.tiltMin = readInt(scanner, "倾斜下限: ");
MarsSolver.tiltMax = readInt(scanner, "倾斜上限: ");
MarsSolver.tiltResetEachRound =
readBool01(scanner, "每回合结束是否【倾斜归零】?(1=是 0=否): ");
MarsSolver.tiltDeltaMin = readInt(scanner, "回合倾斜变化最小值: ");
MarsSolver.tiltDeltaMax = readInt(scanner, "回合倾斜变化最大值: ");
}
// =====================================================
// 4. 目标
// =====================================================
MarsSolver.signalGoal = readInt(scanner, "信号目标: ");
MarsSolver.dataGoal = readInt(scanner, "数据目标: ");
MarsSolver.magGoal = readInt(scanner, "磁力目标: ");
MarsSolver.planeGoal = readInt(scanner, "飞机目标: ");
MarsSolver.planeConsumePerRound = readInt(scanner, "请输入每回合消耗的飞机数量: ");
// =====================================================
// 5. DFS + 重新输入目标循环
// =====================================================
while (true) {
System.out.println("\n==============================");
System.out.println("当前目标:");
System.out.println(" data=" + MarsSolver.dataGoal +
" mag=" + MarsSolver.magGoal +
" signal=" + MarsSolver.signalGoal +
" plane=" + MarsSolver.planeGoal);
System.out.println("==============================");
boolean found = solver.dfs(0, 0, init.copy(), new ArrayList<>());
if (found) {
System.out.println("\n✅ 已完成");
break;
}
System.out.println("\n❌ 当前条件无解,请重新设置目标(回车保持)");
MarsSolver.signalGoal = readIntOrKeep(scanner,
"信号目标(当前=" + MarsSolver.signalGoal + "): ", MarsSolver.signalGoal);
MarsSolver.dataGoal = readIntOrKeep(scanner,
"数据目标(当前=" + MarsSolver.dataGoal + "): ", MarsSolver.dataGoal);
MarsSolver.magGoal = readIntOrKeep(scanner,
"磁力目标(当前=" + MarsSolver.magGoal + "): ", MarsSolver.magGoal);
MarsSolver.planeGoal = readIntOrKeep(scanner,
"飞机目标(当前=" + MarsSolver.planeGoal + "): ", MarsSolver.planeGoal);
MarsSolver.reset();
}
}
static void editCommands(Scanner sc, List<Command> cmds) {
while (true) {
System.out.println("\n========== 输入完成的命令列表 ==========");
for (int i = 0; i < cmds.size(); i++) {
Command c = cmds.get(i);
System.out.println(
c.getName() +
" | 消耗 " + c.getConsume() +
" | 获得 " + c.getProduce());
}
System.out.println("================================");
System.out.println(
"\n操作:\n" +
"1 = 删除命令\n" +
"2 = 新增命令\n" +
"0 = 完成命令并继续\n");
System.out.print("请选择: ");
String op = sc.nextLine().trim();
if ("0".equals(op)) {
return;
}
else if ("1".equals(op)) {
System.out.print("输入要删除的命令名称: ");
String name = sc.nextLine().trim();
boolean removed = cmds.removeIf(c -> c.getName().equalsIgnoreCase(name));
if (removed) System.out.println("✅ 已删除 " + name);
else System.out.println("❌ 未找到命令 " + name);
}
else if ("2".equals(op)) {
addCommand(sc, cmds);
}
else {
System.out.println("❌ 无效操作");
}
}
}
static void addCommand(Scanner scanner, List<Command> cmds) {
String name;
while (true) {
System.out.print("\n请输入新命令名称: ");
name = scanner.nextLine().trim();
if ("0".equals(name)) break;
if (name.isEmpty()) {
System.out.println("❌ 命令名称不能为空");
continue;
}
break;
}
Map<String, Integer> consume = new HashMap<>();
Map<String, Integer> produce = new HashMap<>();
System.out.println("输入【消耗】(类型 数量),0结束");
while (true) {
System.out.print("消耗> ");
String line = scanner.nextLine().trim();
if ("0".equals(line)) break;
try {
String[] p = line.split("\s+");
String key = typeToName(Integer.parseInt(p[0]));
int v = Integer.parseInt(p[1]);
consume.put(key, consume.getOrDefault(key, 0) + v);
} catch (Exception e) {
System.out.println("❌ 格式错误");
}
}
System.out.println("输入【获得】(类型 数量),0结束");
while (true) {
System.out.print("获得> ");
String line = scanner.nextLine().trim();
if ("0".equals(line)) break;
try {
String[] p = line.split("\s+");
String key = typeToName(Integer.parseInt(p[0]));
int v = Integer.parseInt(p[1]);
produce.put(key, produce.getOrDefault(key, 0) + v);
} catch (Exception e) {
System.out.println("❌ 格式错误");
}
}
cmds.add(new Command(name, 1, consume, produce));
System.out.println("✅ 已新增命令: " + name);
}
// ================= 工具方法 =================
static int readInt(Scanner sc, String tip) {
while (true) {
System.out.print(tip);
try {
return Integer.parseInt(sc.nextLine().trim());
} catch (Exception e) {
System.out.println("❌ 输入错误");
}
}
}
static boolean readBool01(Scanner sc, String tip) {
while (true) {
System.out.print(tip);
String s = sc.nextLine().trim();
if ("1".equals(s)) return true;
if ("0".equals(s)) return false;
System.out.println("❌ 请输入 1 或 0");
}
}
static String typeToName(int type) {
switch (type) {
case 1:
return "power";
case 2:
return "mag";
case 3:
return "data";
case 4:
return "signal";
case 5:
return "plane";
case 6:
return "tilt";
case 7:
return "temp";
case 8:
return "craw";
default:
throw new IllegalArgumentException("未知类型: " + type);
}
}
static int readIntOrKeep(Scanner sc, String tip, int oldVal) {
while (true) {
System.out.print(tip);
String line = sc.nextLine().trim();
if (line.isEmpty()) return oldVal;
try {
return Integer.parseInt(line);
} catch (Exception e) {
System.out.println("❌ 请输入整数,或直接回车保持不变");
}
}
}
}
最后执行DFS(Depth-First Search,深度优先搜索)算法,也是我们整个程序的灵魂。
2.State
java
class State {
int craw;
int power;
int signal;
int mag;
int data;
int plane;
int temp;
int tilt;
// int rad;
State copy() {
State s = new State();
s.craw = craw;
s.power = power;
s.signal = signal;
s.mag = mag;
s.data = data;
s.plane = plane;
s.temp = temp;
s.tilt = tilt;
// s.rad = rad;
return s;
}
}
状态类,定义当前的资源状态。辐射☢️暂时先不考虑,降低命令成功率,不作为任务成功的判定标准。
3.Command
java
import java.util.Map;
class Command {
String name;
int costPower;
Map<String, Integer> consume;
Map<String, Integer> produce;
Command(String name, int costPower,
Map<String, Integer> consume,
Map<String, Integer> produce) {
this.name = name;
this.costPower = costPower;
this.consume = consume;
this.produce = produce;
}
Command(String name) {
this.name = name;
}
public int getCostPower() {
return costPower;
}
public Map<String, Integer> getConsume() {
return consume;
}
public Map<String, Integer> getProduce() {
return produce;
}
public String getName() {
return name;
}
}
命令类用于记录每条命令的资源消耗和生产。
4.MarsSolver
这里就进入主菜了,由于每回合结束环境会发生变化,所以要考虑进去。每回合结束温度都不能碰那个边界,步骤中间可以短期碰,但每一回合结束一定要拉回来,否则任务直接失败。有些任务温度的警戒线不是上限,而是下限,比如去冰巨星,天王星和海王星,还有柯伊伯带边界的冥王星,每回合都要升温,无论升多高直接将为0,所以升到1是最佳策略。倾斜虽然也作为一个限制,但是只在所有回合完成的最后达成目标即可,回合中随便偏移方向。倾斜可为负,而温度最低降为0。有些任务每回合结束也会有环境随机变化,比如温度+2,倾斜-2等。所以这样就不一定能搜索出解了,运气不好就是无解。
java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
public class MarsSolver {
// ========= 搜索参数 =========
public static int ROUNDS;
public static int STEPS;
// ========= 搜索统计 =========
static long visited = 0;
static final long PRINT_INTERVAL = 1_000_000;
static long lastPrint = 0;
static final long START_TIME = System.currentTimeMillis();
// ========= 目标 =========
public static int magGoal = 0;
public static int dataGoal = 0;
public static int signalGoal = 0;
public static int planeGoal = 0;
// ========= 回合规则 =========
public static int crawPerRound;
public static int planeConsumePerRound;
public static boolean tiltResetEachRound = false;
// ========= 回合环境变化 =========
public static int tempDeltaMin = 0;
public static int tempDeltaMax = 0;
public static int tiltDeltaMin = 0;
public static int tiltDeltaMax = 0;
// ========= 判定 =========
public static boolean useTempLimit = false;
public static Integer tempMin = null;
public static Integer tempMax = null;
public static boolean useTiltLimit = false;
public static int tiltMin;
public static int tiltMax;
// ========= 命令池 =========
public static List<Command> ALL = new ArrayList<>();
public static void reset() {
visited = 0;
lastPrint = 0;
}
static int maxProducePerRound(String key) {
int best = 0;
for (Command c : ALL) {
int v = c.produce.getOrDefault(key, 0);
if (v > best) best = v;
}
return best * STEPS;
}
// =====================================================
// DFS
// =====================================================
public boolean dfs(int round, int step, State state, List<String> path) {
int remainRounds = ROUNDS - round;
// ✂ 剪枝:剩余回合最多能生产的资源上限
int maxData = state.data + remainRounds * maxProducePerRound("data");
int maxMag = state.mag + remainRounds * maxProducePerRound("mag");
int maxSig = state.signal + remainRounds * maxProducePerRound("signal");
int maxPlane = state.plane + remainRounds * maxProducePerRound("plane");
if (maxData < dataGoal) return false;
if (maxMag < magGoal) return false;
if (maxSig < signalGoal) return false;
if (maxPlane < planeGoal) return false;
visited++;
if (visited - lastPrint >= PRINT_INTERVAL) {
lastPrint = visited;
long cost = (System.currentTimeMillis() - START_TIME) / 1000;
System.out.println("[PROGRESS] visited=" + visited +
" time=" + cost + "s R=" + round + " S=" + step);
}
// ===== 成功判定 =====
if (round == ROUNDS) {
// 最终倾斜判定
if (useTiltLimit) {
if (state.tilt < tiltMin || state.tilt > tiltMax) {
return false;
}
}
if (state.mag >= magGoal &&
state.data >= dataGoal &&
state.signal >= signalGoal &&
state.plane >= planeGoal) {
System.out.println("\n===== FOUND SOLUTION =====");
printSolutionByRound(path);
System.exit(0);
return true;
}
return false;
}
// ===== 回合结束 =====
if (step == STEPS) {
// 判定
if (useTempLimit) {
if (tempMin != null && state.temp < tempMin) return false;
if (tempMax != null && state.temp > tempMax) return false;
}
// 环境枚举
for (int dt = tempDeltaMin; dt <= tempDeltaMax; dt++) {
for (int dk = tiltDeltaMin; dk <= tiltDeltaMax; dk++) {
State next = state.copy();
// 环境变化
next.temp += dt;
if (MarsSolver.tiltResetEachRound) {
next.tilt = 0;
}
next.tilt += dk;
boolean planeConsumed = false;
boolean crawRecovered = false;
if (next.plane > 0) {
if (next.plane < MarsSolver.planeConsumePerRound) {
continue; // 飞机不够,剪枝
}
next.plane -= MarsSolver.planeConsumePerRound;
planeConsumed = true;
}
if (next.craw != crawPerRound) {
next.craw = crawPerRound;
crawRecovered = true;
}
StringBuilder log = new StringBuilder();
log.append("R").append(round + 1)
.append(" ENV CHANGE:")
.append(" temp").append(dt >= 0 ? "+" : "").append(dt)
.append(" tilt").append(dk >= 0 ? "+" : "").append(dk);
if (planeConsumed) {
log.append(" plane-").append(MarsSolver.planeConsumePerRound);
}
if (crawRecovered) log.append(" craw=").append(crawPerRound);
path.add(log.toString());
dfs(round + 1, 0, next, path);
path.remove(path.size() - 1);
}
}
return false;
}
// ===== 执行命令 =====
for (Command cmd : ALL) {
if (!canExecute(cmd, state)) continue;
State next = state.copy();
apply(cmd, next);
if (next.power < 0) continue;
path.add("R" + (round + 1) + "S" + (step + 1) + ":" + cmd.name);
dfs(round, step + 1, next, path);
path.remove(path.size() - 1);
}
return false;
}
// =====================================================
// apply
// =====================================================
static void apply(Command cmd, State s) {
s.power -= cmd.costPower;
for (Map.Entry<String, Integer> e : cmd.consume.entrySet()) adjust(s, e.getKey(), -e.getValue());
for (Map.Entry<String, Integer> e : cmd.produce.entrySet()) adjust(s, e.getKey(), e.getValue());
}
static void adjust(State s, String k, int v) {
if ("power".equals(k)) s.power += v;
else if ("mag".equals(k)) s.mag += v;
else if ("data".equals(k)) s.data += v;
else if ("signal".equals(k)) s.signal += v;
else if ("plane".equals(k)) s.plane += v;
else if ("tilt".equals(k)) s.tilt += v;
else if ("temp".equals(k)) s.temp += v;
else if ("craw".equals(k)) s.craw += v;
}
// =====================================================
// canExecute
// =====================================================
static boolean canExecute(Command cmd, State s) {
if (cmd.costPower > s.power) return false;
for (Map.Entry<String, Integer> e : cmd.consume.entrySet()) {
int need = e.getValue();
int cur;
String key = e.getKey();
switch (key) {
case "power":
cur = s.power;
break;
case "mag":
cur = s.mag;
break;
case "data":
cur = s.data;
break;
case "signal":
cur = s.signal;
break;
case "plane":
cur = s.plane;
break;
case "craw":
cur = s.craw;
break;
case "temp":
cur = s.temp;
break;
case "tilt":
continue;
default:
continue;
}
if (cur < need) return false;
}
return true;
}
// =====================================================
// 打印解
// =====================================================
static void printSolutionByRound(List<String> path) {
int cur = -1;
for (String s : path) {
if (s.matches("R\d+S\d+:.*")) {
int r = Integer.parseInt(s.substring(1, s.indexOf('S')));
if (r != cur) {
cur = r;
System.out.println("\n--- Round " + r + " ---");
}
System.out.println(s);
} else if (s.contains("ENV CHANGE")) {
System.out.println(s.replaceFirst("R\d+\s*", ""));
}
}
}
}
共分为两个维度,round指当前第几回合,step指当前回合第几步。所以我们设计出
java
boolean dfs(round, step, state, path)
这样的递归结构。使用canExecute函数检测某个命令当前是否可以执行,即需要的资源够不够。apply函数去执行一条命令。通过round == ROUNDS进入所有命令执行结束的判定阶段,step == STEPS进入回合结束的环境变化。使用path集合来保存已搜索过的路径。其中的亮点是还带有剪枝功能,大大降低搜索复杂度,降低时间消耗。这里我们称它为乐观上界剪枝。意思就是就算以后每一回合都用最强命令,我最多也只能产这么多。如果还不够,那这条路必死。