plotter sim support via deepseek
This commit is contained in:
parent
db234a0a53
commit
c24a30ac31
1 changed files with 189 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue