//! **H2 measured against ground-game's own criteria** (CB-WP-0042 T05). //! //! Their §3 targets, not H1's reused from memory: //! //! 1. group success (greedy SHARED, 3–4p) well above H1's 0 //! 2. Stress **variance** — max−min across seats — higher than baseline //! and H1-greedy //! 3. DARVO non-zero **for some policy that still sometimes wins** //! 4. bond cards show an elevated SOLVE rate versus personal //! 5. control: force every scope global and H1-like collapse returns //! //! **Criterion 4 is the one this instrument probably cannot answer**, and //! that is said up front rather than discovered in the numbers: no policy //! here models another seat, or knows what a scope is. If bond and //! personal rates come out equal it is because nothing in the panel could //! have made them differ — not because the incentive fails at a table. use std::collections::BTreeMap; use cb_game_runtime::{ScenarioGame, Setup}; use cb_kernel::PlayerId; use games_ground::bot::{play, Choice, GreedyPolicy, Policy}; use games_ground::edition::StressScope; use games_ground::{Action, GroundCommand, GroundState, ScoringMode, Variant}; const GAMES: u32 = 200; /// Greedy with GROUND demoted below ATTACK — the unregulated seat from /// CB-EV-0031, delegating so exactly one preference differs. struct Reactive; impl Policy for Reactive { fn name(&self) -> &'static str { "reactive" } fn choose( &mut self, state: &GroundState, seat: PlayerId, legal: &[GroundCommand], _may_pass: bool, ) -> Choice { let gated = state .players .get(&seat) .is_some_and(|p| p.stress >= 4 && !p.freedom_gate_lifted); let rank = |c: &GroundCommand| -> i32 { match c { GroundCommand::SelectAction { action: Action::Ground, .. } if gated => 5, other => GreedyPolicy::rank(state, seat, other), } }; let mut best = 0; for (i, c) in legal.iter().enumerate() { if rank(c) > rank(&legal[best]) { best = i; } } Choice::Command(best) } } #[derive(Default)] struct Cell { games: u32, played: u32, won: u32, darvo: u32, /// Summed max−min final Stress, for criterion 2. spread: u32, /// Claimed / total, by scope, for criterion 4. claimed: BTreeMap<&'static str, (u32, u32)>, } fn sweep(variant: Variant, players: u8, reactive: bool, force_global: bool) -> Cell { let mut c = Cell::default(); for seed in 0..GAMES as u64 { let Ok(st) = GroundState::setup( &Setup { players, preset: format!("standard-{players}p"), patch: Default::default(), }, seed, ) else { continue; }; let mut st = st.with_variant(variant); st.mode = ScoringMode::SharedGround; // Criterion 5's control: every scope forced global. if force_global { for p in st.problems.values_mut() { if p.scope.is_some() { p.scope = Some(StressScope::Global); p.owner = None; } } } let mut ps: Vec> = (0..players) .map(|_| { if reactive { Box::new(Reactive) as Box } else { Box::new(GreedyPolicy) as Box } }) .collect(); let Ok(g) = play(st, &mut ps) else { continue }; c.games += 1; if g.state.outcome.is_some() && g.rounds == 5 { c.played += 1; } if g.state.outcome.as_ref().is_some_and(|o| o.group_success) { c.won += 1; } for e in &g.events { if matches!(e, games_ground::GroundEvent::DarvoTriggered { .. }) { c.darvo += 1; } } let stresses: Vec = g.state.players.values().map(|p| p.stress).collect(); let (lo, hi) = ( stresses.iter().copied().min().unwrap_or(0), stresses.iter().copied().max().unwrap_or(0), ); c.spread += u32::from(hi - lo); for p in g.state.problems.values() { let name = match p.scope { Some(StressScope::Global) => "global", Some(StressScope::Personal) => "personal", Some(StressScope::Bond) => "bond", None => continue, }; let e = c.claimed.entry(name).or_default(); e.1 += 1; if p.claimed_by.is_some() { e.0 += 1; } } } assert_eq!(c.games, GAMES, "{players}p {variant:?}: short cell"); assert_eq!( c.played, GAMES, "{players}p {variant:?}: games did not finish" ); c } fn main() { println!("H2 — against ground-game's §3 criteria (CB-WP-0042 T05)\n"); println!("SHARED GROUND, {GAMES} games per cell. `spread` is the mean"); println!("max−min final Stress across seats — their criterion 2.\n"); for (label, reactive) in [("greedy", false), ("reactive", true)] { println!("{label}"); println!("seats variant won darvo spread"); for players in [2u8, 3, 4, 6] { for (vname, v) in [ ("baseline", Variant::Baseline), ("H1 ", Variant::H1ProblemStress), ("H2 ", Variant::H2ScopedProblemStress), ] { let c = sweep(v, players, reactive, false); println!( " {players}p {vname} {:>4} {:>5} {:>5.2}", c.won, c.darvo, f64::from(c.spread) / f64::from(GAMES) ); } } println!(); } println!("criterion 4 — claim rate by scope (H2, greedy)"); println!("seats global personal bond"); for players in [2u8, 3, 4, 6] { let c = sweep(Variant::H2ScopedProblemStress, players, false, false); let rate = |k: &str| match c.claimed.get(k) { Some((a, b)) if *b > 0 => format!("{:>6.1}%", 100.0 * f64::from(*a) / f64::from(*b)), _ => " —".to_string(), }; println!( " {players}p {} {} {}", rate("global"), rate("personal"), rate("bond") ); } println!("\ncriterion 5 — control: every scope forced global (H2, greedy)"); println!("seats won (scoped) won (all global)"); for players in [3u8, 4] { let scoped = sweep(Variant::H2ScopedProblemStress, players, false, false); let global = sweep(Variant::H2ScopedProblemStress, players, false, true); println!( " {players}p {:>4} {:>4}", scoped.won, global.won ); } }