use fforge_core::match_engine::{StreamTelemetry, ai_pick_lineup, play_match};
use fforge_core::rng::derive_stream;
use fforge_core::{FIXTURE_STREAM_NS, WorldGenConfig, worldgen};
use fforge_domain::FORMATIONS;
struct CalibReport {
per_seed_gpm: Vec<f64>,
pooled: StreamTelemetry,
}
fn run_calibration(seeds: &[u64], cfg: &WorldGenConfig) -> CalibReport {
let mut pooled = StreamTelemetry::default();
let mut per_seed_gpm = Vec::with_capacity(seeds.len());
for &seed in seeds {
let (world, schedule, _start) = worldgen::generate(seed, cfg);
let mut seed_goals = 0u32;
let mut seed_matches = 0u32;
for fixture in &schedule {
let home_lineup = ai_pick_lineup(&world, fixture.home);
let away_lineup = ai_pick_lineup(&world, fixture.away);
let mut rng = derive_stream(seed, FIXTURE_STREAM_NS | fixture.id.0 as u64);
let outcome = play_match(&world, &home_lineup, &away_lineup, &mut rng);
seed_goals += outcome.home_goals as u32 + outcome.away_goals as u32;
seed_matches += 1;
pooled.record(&outcome, home_lineup.formation, away_lineup.formation);
}
per_seed_gpm.push(seed_goals as f64 / seed_matches as f64);
}
CalibReport {
per_seed_gpm,
pooled,
}
}
fn mean(xs: &[f64]) -> f64 {
xs.iter().sum::<f64>() / xs.len() as f64
}
fn stdev(xs: &[f64], mean: f64) -> f64 {
if xs.len() < 2 {
return 0.0;
}
let var = xs.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (xs.len() - 1) as f64;
var.sqrt()
}
fn print_report(report: &CalibReport) {
let p = &report.pooled;
let gpm_mean = mean(&report.per_seed_gpm);
let gpm_sd = stdev(&report.per_seed_gpm, gpm_mean);
let gpm_min = report
.per_seed_gpm
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
let gpm_max = report
.per_seed_gpm
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
println!(
"=== Calibration report ({} seeds pooled, {} matches) ===",
report.per_seed_gpm.len(),
p.matches
);
println!();
println!(
"goals/match : {gpm_mean:.2} (sd {gpm_sd:.2}, range {gpm_min:.2}-{gpm_max:.2} across seeds)"
);
println!(
"H / D / A : {:.1}% / {:.1}% / {:.1}%",
p.home_win_rate() * 100.0,
p.draw_rate() * 100.0,
p.away_win_rate() * 100.0
);
println!("shots/match : {:.2}", p.shots_per_match());
println!(
"shots on target : {:.1}%",
p.shot_on_target_rate() * 100.0
);
println!("conversion : {:.1}%", p.conversion_rate() * 100.0);
println!("headed goal share : {:.1}%", p.headed_goal_share() * 100.0);
println!(
"wide-origin share : {:.1}%",
p.wide_origin_goal_share() * 100.0
);
println!(
"home possession : {:.1}%",
p.home_possession_share() * 100.0
);
println!();
println!("=== Per-formation breakdown ===");
println!(
"{:<10} {:>10} {:>10} {:>14}",
"formation", "uses", "gpm", "shots/match"
);
for (idx, stats) in &p.by_formation {
let name = FORMATIONS.get(*idx as usize).map(|f| f.name).unwrap_or("?");
println!(
"{:<10} {:>10} {:>10.2} {:>14.2}",
name,
stats.uses,
stats.goals_per_match(),
stats.shots_per_match()
);
}
println!();
println!("=== Formation usage histogram ===");
let total_uses: u32 = p.by_formation.values().map(|s| s.uses).sum();
for (idx, stats) in &p.by_formation {
let name = FORMATIONS.get(*idx as usize).map(|f| f.name).unwrap_or("?");
let share = if total_uses == 0 {
0.0
} else {
stats.uses as f64 / total_uses as f64 * 100.0
};
println!("{name:<10} {:>6.1}% ({} uses)", share, stats.uses);
}
}
fn parse_seeds_arg(args: impl Iterator<Item = String>) -> u64 {
const DEFAULT_SEEDS: u64 = 8;
let args: Vec<String> = args.collect();
for i in 0..args.len() {
if args[i] == "--seeds"
&& let Some(v) = args.get(i + 1)
&& let Ok(n) = v.parse::<u64>()
{
return n;
}
}
DEFAULT_SEEDS
}
fn main() {
let num_seeds = parse_seeds_arg(std::env::args().skip(1));
let seeds: Vec<u64> = (0..num_seeds).collect();
let cfg = WorldGenConfig::default();
let report = run_calibration(&seeds, &cfg);
print_report(&report);
}