1
Fork 0

plotter sim support via deepseek

This commit is contained in:
Andy Killorin 2026-11-10 09:34:19 -05:00
parent db234a0a53
commit c24a30ac31
Signed by: ank
GPG key ID: 23F9463ECB67FE8C

View file

@ -372,6 +372,16 @@ impl WeaponType {
},
}
}
/// The radius of the weapon's ball, if it has one. Used to report the
/// swept parameter in sequential-sweep JSON output.
pub fn radius(&self) -> Option<f32> {
match self {
WeaponType::Ball { radius } => Some(*radius),
WeaponType::ForwardBall { radius } => Some(*radius),
WeaponType::Lazer { .. } => None,
}
}
pub fn crater(&self, robot: &Robot) -> (f32, f32, f32, Color) {
match self {
WeaponType::Ball{radius} => {
@ -631,6 +641,149 @@ fn run_headless_simulation(seed: u64, config: &SimConfig) -> (i32, i32) {
}
}
/// Stopping parameters for [`run_sequential`].
#[derive(Debug, Clone, Copy)]
struct SeqOptions {
/// Stop once `|mean| / standard_error >= sigma`.
sigma: f64,
/// Never check the stopping rule before this many samples.
min_samples: u64,
/// Hard cap on samples, whether or not the rule fired.
max_samples: u64,
/// Seeds evaluated per parallel batch between stopping-rule checks.
batch: u64,
}
/// Outcome of a sequential sweep for a single configuration. `winner` is the
/// side the food-share difference favours; `stopped` records whether that
/// decision came from the sigma rule or from hitting `max_samples` (in which
/// case the match is effectively undecided).
#[derive(Debug, Clone, Copy)]
struct SeqResult {
n: u64,
mean_red: f64,
mean_blu: f64,
mean_diff: f64,
sd_diff: f64,
se_diff: f64,
sigma: f64,
winner: &'static str,
stopped: &'static str,
red_wins: u64,
blu_wins: u64,
ties: u64,
}
/// Sequentially evaluate a single configuration, stopping as soon as the
/// RED-BLU food-share difference is `sigma` standard errors away from zero (or
/// `max_samples` is reached). Seeds run in parallel batches; every batch
/// reuses the same seed range starting at 0, so all configurations share
/// common random numbers and remain directly comparable.
fn run_sequential(config: &SimConfig, opts: SeqOptions) -> SeqResult {
let denom = config.food_amount.max(1) as f64;
let mut red = Variance::default();
let mut blu = Variance::default();
let mut diff = Variance::default();
let (mut red_wins, mut blu_wins, mut ties) = (0u64, 0u64, 0u64);
let mut n = 0u64;
let mut stopped = "max_samples";
while n < opts.max_samples {
let take = opts.batch.max(1).min(opts.max_samples - n);
let batch: Vec<(i32, i32)> = (n..n + take)
.into_par_iter()
.map(|seed| run_headless_simulation(seed, config))
.collect();
for &(r, b) in &batch {
let r_share = r as f64 / denom;
let b_share = b as f64 / denom;
red.push(r_share);
blu.push(b_share);
diff.push(r_share - b_share);
match r.cmp(&b) {
std::cmp::Ordering::Greater => red_wins += 1,
std::cmp::Ordering::Less => blu_wins += 1,
std::cmp::Ordering::Equal => ties += 1,
}
}
n += take;
if n >= opts.min_samples {
if let Some(se) = diff.std_err() {
if se > 0.0 && (diff.mean.abs() / se) >= opts.sigma {
stopped = "sigma";
break;
}
}
}
}
let sd = diff.std_dev().unwrap_or(0.0);
let se = diff.std_err().unwrap_or(0.0);
let sigma = if se > 0.0 { diff.mean.abs() / se } else { 0.0 };
SeqResult {
n,
mean_red: red.mean,
mean_blu: blu.mean,
mean_diff: diff.mean,
sd_diff: sd,
se_diff: se,
sigma,
winner: if diff.mean >= 0.0 { "red" } else { "blu" },
stopped,
red_wins,
blu_wins,
ties,
}
}
/// Format a float as a JSON number, falling back to `null` for non-finite
/// values so downstream `json` parsers never see `NaN`/`inf`.
fn json_float(x: f64) -> String {
if x.is_finite() {
format!("{x}")
} else {
"null".to_string()
}
}
/// Emit one line of JSON describing the configuration and its sequential
/// result. Flat object, hand-rolled to avoid pulling in `serde_json`.
fn print_seq_json(config: &SimConfig, res: &SeqResult) {
let radius = match config.weapon.model.radius() {
Some(r) => json_float(r as f64),
None => "null".to_string(),
};
let fields: Vec<(&str, String)> = vec![
("view_dist", json_float(config.view_dist as f64)),
("weapon_radius", radius),
("weapon_kills_user", config.weapon.kills_user.to_string()),
("red_count", config.red_count.to_string()),
("blu_count", config.blu_count.to_string()),
("food_amount", config.food_amount.to_string()),
("n", res.n.to_string()),
("mean_red", json_float(res.mean_red)),
("mean_blu", json_float(res.mean_blu)),
("mean_diff", json_float(res.mean_diff)),
("sd_diff", json_float(res.sd_diff)),
("se_diff", json_float(res.se_diff)),
("sigma", json_float(res.sigma)),
("winner", format!("\"{}\"", res.winner)),
("stopped", format!("\"{}\"", res.stopped)),
("red_wins", res.red_wins.to_string()),
("blu_wins", res.blu_wins.to_string()),
("ties", res.ties.to_string()),
];
let body = fields
.iter()
.map(|(k, v)| format!("\"{k}\":{v}"))
.collect::<Vec<_>>()
.join(",");
println!("{{{body}}}");
}
#[derive(Parser)]
#[command(version, about, long_about = None)]
@ -651,6 +804,27 @@ struct Cli {
#[arg(short, long, value_name = "FILE")]
config: PathBuf,
/// Sequential mode: run seeds in batches, stop once the RED-BLU food-share
/// difference is `--sigma` standard errors from zero, print JSON, exit.
#[arg(long)]
sequential: bool,
/// Sigma threshold for `--sequential` stopping (default 3).
#[arg(long, default_value_t = 3.0)]
sigma: f64,
/// Minimum seeds before the `--sequential` stopping rule may fire (default 32).
#[arg(long, default_value_t = 32)]
min_samples: u64,
/// Hard cap on seeds for `--sequential` (default 2000).
#[arg(long, default_value_t = 2000)]
max_samples: u64,
/// Seeds per parallel batch for `--sequential` (default 64).
#[arg(long, default_value_t = 64)]
batch: u64,
/// Turn debugging information on
#[arg(short, long, action = clap::ArgAction::Count)]
debug: u8,
@ -750,6 +924,21 @@ fn print_average(red: i32, blu: i32, iters: u64) {
fn main() {
let cli = Cli::parse();
// Sequential mode is inherently headless: it never touches the GUI and
// prints a single JSON line per invocation.
if cli.sequential {
let config = load_config(&cli.config);
let opts = SeqOptions {
sigma: cli.sigma,
min_samples: cli.min_samples,
max_samples: cli.max_samples.max(1),
batch: cli.batch,
};
let result = run_sequential(&config, opts);
print_seq_json(&config, &result);
return;
}
// Headless runs never need a window. Going through macroquad's event loop
// would otherwise cost seconds of X11/window setup on every invocation,
// which dwarfs the actual simulation for parallel sweeps.