199 lines
6.9 KiB
Rust
199 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);
|
|||
|
|
}
|
|||
|
|
}
|