Some checks failed
ci / check (push) Failing after 4s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
204 lines
6.9 KiB
Rust
204 lines
6.9 KiB
Rust
//! **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<Box<dyn Policy>> = (0..players)
|
||
.map(|_| {
|
||
if reactive {
|
||
Box::new(Reactive) as Box<dyn Policy>
|
||
} else {
|
||
Box::new(GreedyPolicy) as Box<dyn Policy>
|
||
}
|
||
})
|
||
.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<u8> = 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
|
||
);
|
||
}
|
||
}
|