5f07027faa
Preserve the adapter facade and move adapter-only checks out of domain tests. Late tracks do not reveal an unseen launch origin or its timestamp. Co-Authored-By: Codex GPT-6.1 Terra <noreply@openai.com>
157 lines
6.9 KiB
Rust
157 lines
6.9 KiB
Rust
//! Native headless runner for the simulation.
|
|
//!
|
|
//! cargo run --release --example sim_run -- [options]
|
|
//!
|
|
//! Options (all optional; a preset gives the defaults, the others override it):
|
|
//! --scenario NAME big-smart | logistics | ew-adaptive (default big-smart)
|
|
//! --seed N random seed
|
|
//! --layout FILE systems from a text file, replacing the preset's; one per line:
|
|
//! `KIND X Z [HEADING_RAD] [empty]` (`#` starts a comment)
|
|
//! KIND is a system key (patriot, tor, radar, depot, tzm, ew, ...)
|
|
//! --raid SPEC replaces the preset's raids; repeatable. SPEC is
|
|
//! `kind=count,kind=count[;dist=500][;aim=smart|objects|sams][;approach=sector|two|all|waves][;at=TICK]`
|
|
//! --terrain T flat[:HEIGHT] | hills[:BASE:AMP:PERIOD]
|
|
//! --ticks N tick limit (20 a second)
|
|
//! --run-on do not stop when the raids have ended
|
|
//! --ew / --no-ew EW stations on or off
|
|
//! --json one JSON object instead of text
|
|
//! --list list scenarios, system and drone kinds
|
|
//! Exit code 1 when the run has problems (NaN positions, stuck trucks, a raid that never ended).
|
|
//! Native timings are for comparing runs on one machine, not an estimate of WASM/GPU cost.
|
|
mod harness;
|
|
use airdef::sim::{Aim, Approach, DroneKind, SamKind};
|
|
use glam::DVec3;
|
|
use harness::*;
|
|
|
|
fn die(msg: &str) -> ! {
|
|
eprintln!("sim_run: {msg}");
|
|
std::process::exit(2);
|
|
}
|
|
|
|
fn parse_raid(spec: &str) -> RaidSpec {
|
|
let mut r = RaidSpec { kinds: Vec::new(), approach: Approach::Sector, aim: Aim::Smart, dist: 400.0, at: 1 };
|
|
for (i, part) in spec.split(';').enumerate() {
|
|
if i == 0 {
|
|
for kc in part.split(',').filter(|s| !s.is_empty()) {
|
|
let (k, c) = kc.split_once('=').unwrap_or_else(|| die(&format!("raid kind `{kc}` needs =count")));
|
|
let kind = DroneKind::from_key(k).unwrap_or_else(|| die(&format!("unknown drone kind `{k}` (see --list)")));
|
|
r.kinds.push((kind, c.parse().unwrap_or_else(|_| die(&format!("bad count `{c}`")))));
|
|
}
|
|
continue;
|
|
}
|
|
let (k, v) = part.split_once('=').unwrap_or_else(|| die(&format!("bad raid option `{part}`")));
|
|
match k {
|
|
"dist" => r.dist = v.parse().unwrap_or_else(|_| die("bad dist")),
|
|
"at" => r.at = v.parse().unwrap_or_else(|_| die("bad at")),
|
|
"aim" => {
|
|
r.aim = match v {
|
|
"smart" => Aim::Smart,
|
|
"objects" => Aim::Objects,
|
|
"sams" => Aim::ObjectsAndSams,
|
|
_ => die("aim: smart | objects | sams"),
|
|
}
|
|
}
|
|
"approach" => {
|
|
r.approach = match v {
|
|
"sector" => Approach::Sector,
|
|
"two" => Approach::TwoSectors,
|
|
"all" => Approach::AllSides,
|
|
"waves" => Approach::Waves,
|
|
_ => die("approach: sector | two | all | waves"),
|
|
}
|
|
}
|
|
_ => die(&format!("unknown raid option `{k}`")),
|
|
}
|
|
}
|
|
r
|
|
}
|
|
|
|
fn parse_terrain(t: &str) -> Terrain {
|
|
let n: Vec<&str> = t.split(':').collect();
|
|
let num = |i: usize, d: f64| n.get(i).map_or(d, |s| s.parse().unwrap_or_else(|_| die("bad terrain number")));
|
|
match n[0] {
|
|
"flat" => Terrain::Flat(num(1, 70.0) as i32),
|
|
"hills" => Terrain::Hills { base: num(1, 70.0) as i32, amp: num(2, 14.0), period: num(3, 90.0) },
|
|
_ => die("terrain: flat[:H] | hills[:BASE:AMP:PERIOD]"),
|
|
}
|
|
}
|
|
|
|
fn parse_layout(path: &str) -> Vec<Unit> {
|
|
let text = std::fs::read_to_string(path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
|
|
let mut units = Vec::new();
|
|
for (n, line) in text.lines().enumerate() {
|
|
let line = line.split('#').next().unwrap_or("").trim();
|
|
if line.is_empty() {
|
|
continue;
|
|
}
|
|
let w: Vec<&str> = line.split_whitespace().collect();
|
|
let bad = || die(&format!("{path}:{}: expected `KIND X Z [HEADING] [empty]`", n + 1));
|
|
if w.len() < 3 {
|
|
bad();
|
|
}
|
|
let kind = SamKind::from_key(w[0]).unwrap_or_else(|| die(&format!("{path}:{}: unknown system `{}`", n + 1, w[0])));
|
|
let f = |s: &str| s.parse::<f64>().unwrap_or_else(|_| bad());
|
|
let heading = w.get(3).filter(|s| **s != "empty").map_or(0.0, |s| f(s));
|
|
units.push(Unit { kind, pos: DVec3::new(f(w[1]), 70.0, f(w[2])), heading, empty: w.contains(&"empty") });
|
|
}
|
|
units
|
|
}
|
|
|
|
pub fn main() {
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
let mut name = "big-smart".to_string();
|
|
let mut i = 0;
|
|
// The scenario first, so the other options override it whatever their order.
|
|
while i < args.len() {
|
|
if args[i] == "--scenario" {
|
|
name = args.get(i + 1).cloned().unwrap_or_else(|| die("--scenario needs a name"));
|
|
}
|
|
i += 1;
|
|
}
|
|
let mut sc = preset(&name).unwrap_or_else(|| die(&format!("unknown scenario `{name}`; one of {PRESETS:?}")));
|
|
let (mut json, mut raids) = (false, Vec::new());
|
|
let mut it = args.iter();
|
|
while let Some(arg) = it.next() {
|
|
let mut value = || it.next().cloned().unwrap_or_else(|| die(&format!("{arg} needs a value")));
|
|
match arg.as_str() {
|
|
"--scenario" => {
|
|
value();
|
|
}
|
|
"--seed" => sc.seed = value().parse().unwrap_or_else(|_| die("bad seed")),
|
|
"--ticks" => sc.ticks = value().parse().unwrap_or_else(|_| die("bad ticks")),
|
|
"--layout" => sc.units = parse_layout(&value()),
|
|
"--raid" => raids.push(parse_raid(&value())),
|
|
"--terrain" => sc.terrain = parse_terrain(&value()),
|
|
"--run-on" => sc.stop_at_end = false,
|
|
"--ew" => sc.ew = true,
|
|
"--no-ew" => sc.ew = false,
|
|
"--json" => json = true,
|
|
"--list" => {
|
|
println!("scenarios: {PRESETS:?}");
|
|
println!("systems: {:?}", SamKind::ALL.iter().map(|k| k.key()).collect::<Vec<_>>());
|
|
println!("drones: {:?}", DroneKind::ALL.iter().map(|k| k.spec().key).collect::<Vec<_>>());
|
|
return;
|
|
}
|
|
"-h" | "--help" => {
|
|
println!("{}", include_str!("main.rs").lines().take_while(|l| l.starts_with("//")).map(|l| l.trim_start_matches("//!").trim_start_matches("//").to_string() + "\n").collect::<String>());
|
|
return;
|
|
}
|
|
other => die(&format!("unknown option `{other}` (try --help)")),
|
|
}
|
|
}
|
|
if !raids.is_empty() {
|
|
sc.raids = raids;
|
|
}
|
|
let r = run(&sc);
|
|
if json {
|
|
println!("{}", r.json());
|
|
} else {
|
|
print!("{}", r.text());
|
|
}
|
|
let problems = r.problems();
|
|
if !problems.is_empty() {
|
|
eprintln!("PROBLEMS: {}", problems.join("; "));
|
|
std::process::exit(1);
|
|
}
|
|
}
|