Some checks failed
ci / check (push) Failing after 4s
Measured against ground-game's own §3 criteria, read from their design note rather than reused from H1. Criterion 1 met with room: greedy SHARED wins 120 at 3p and 175 at 4p, against H1's 0 and 0, restoring 73% and 92% of baseline. Criterion 3 met, and it was H1's clearest failure. Under H1 the unregulated seat armed DARVO constantly and never won; under H2 it arms and wins 13/13/57. "Non-zero for some policy that still sometimes wins" is exactly the shape H1 could not produce. Criterion 2 met at 2p/4p/6p and missed at 3p — 1.50 against baseline's 1.57 — reported as a miss because that is what this sample says. The mechanism is visible: H1-greedy's spread is 0.00 at 3p+, because a flat tax on every seat creates no variance at all. That is the clearest statement of why scoping was the right correction. Criterion 5 is the best evidence in the pass. Forcing every scope to global and changing nothing else reproduces H1's collapse exactly — 120 to 0 at 3p, 175 to 0 at 4p — so the scoping is what saves it, not any other difference between the packages. Criterion 4 came out backwards and the prediction held. The workplan said this panel might be unable to test it, because no policy here models another seat or knows what a scope is; bond claim rates are LOWER than personal at 3p and 4p, driven by suit availability rather than incentive. Reported as untested with an incidental figure pointing the wrong way, not as a refutation. Wired into make panels. First pass declared after ADR-0021, so no chaos roll is recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
198 lines
6.9 KiB
Rust
198 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);
|
||
}
|
||
}
|