Files
airdef/airdef-mod/examples/sim_run/main.rs
T
chexit 5f07027faa Build canonical simulation without Horizon SDK and remove hidden launch fixes (#87 #107)
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>
2026-11-02 01:17:03 +03:00

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);
}
}