Some checks failed
ci / check (push) Has been cancelled
objective() reads GroundState::score (now public) rather than restating
what winning is; a copy in the bot would disagree with the kernel the
first time ground-game rules on F28.
Working out WHERE the modes can differ was most of the task and it
bounds the result: SOLVE always claims for the actor, so own-score and
group-score want the same SOLVE nearly everywhere. That is a fact about
GROUND's action set, not a shortcoming of the bot. Two real divergences,
both readable off the table: SUPPORT regulates someone else (worth less
against a rival, worth MORE under coalitions where a Bond merges them
into my side), and SOLVE's value is the card's value, which greedy
ignores entirely.
THE RESULT — F27 splits in two:
group success UNCHANGED in 34 of 36 cells
who wins MOVES: BONDED COALITIONS at 4p goes 2.04 -> 2.98,
2.12 -> 3.29, 2.05 -> 3.01 winning seats per game
So "the competitive modes are scoring lenses over cooperative play" was
too strong and is withdrawn. The sharper claim: GROUND's scoring modes
change WHO WINS, not WHETHER THE GROUP SUCCEEDS. And the effect is
seat-band dependent -- 2p none, 4p largest, 6p none under coalitions;
two relation slots capping network growth is a candidate explanation and
is untested.
The panel now prints BOTH policies side by side. That was a correction
mid-task: the first version printed only the new one and I compared it
against a figure remembered from CB-WP-0047 -- a comparison against a
board nobody re-ran.
Control that makes the numbers mean anything: under SHARED GROUND the
two policies agree at all but <=2 decision points across 12 boards, so a
moving column is mode-awareness and not simply a different bot.
Also: two T01 tests keyed on `status: proposed`, which ground-game
renamed to `ready-for-implement` mid-session. They now find the module
by asking resolve() -- the structural property is ours and does not move
when another repo edits its vocabulary.
Also: `make vendor` replaces three hand re-vendors with a tool that
regenerates digests by walking editions/, and reports one-sided files
rather than resolving them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
181 lines
7 KiB
Rust
181 lines
7 KiB
Rust
//! Every board, in every mode, at every seat band (CB-WP-0047).
|
|
//!
|
|
//! The engine dealt `SCN_01` and only `SCN_01` for the whole life of this
|
|
//! repo: `deal` took a scenario id, `setup` passed a literal, and **15 of
|
|
//! the 20 Problem cards had never been dealt by anything**. The three
|
|
//! scoring modes were reachable from the driver but no measurement had
|
|
//! ever compared them.
|
|
//!
|
|
//! This plays all of it. It is a **characterisation panel**, not a
|
|
//! hypothesis test: nothing here predicts an outcome, and the numbers
|
|
//! exist so that a later change to a deck or a mode is visible as a
|
|
//! change rather than discovered by a player.
|
|
//!
|
|
//! **SCN_01 and SCN_02 are the same board.** Identical suits and values
|
|
//! at every priority; they differ only in prose. They are both played
|
|
//! anyway — a panel that dropped one would hide the day they diverge —
|
|
//! and the column is expected to match, which is itself a control.
|
|
|
|
use cb_game_runtime::{ScenarioGame, Setup};
|
|
use games_ground::bot::{play, GreedyPolicy, ObjectivePolicy, Policy};
|
|
use games_ground::{GroundState, ScoringMode};
|
|
|
|
/// Games per cell. Named once so the banner and the assertion cannot
|
|
/// disagree — a previous panel printed "200 games per cell" over
|
|
/// 196-game columns (CB-REV-0002 #1).
|
|
const GAMES: u32 = 100;
|
|
|
|
struct Cell {
|
|
games: u32,
|
|
/// Games that reached an outcome over the full five rounds. A game
|
|
/// that RAN is not a game that was PLAYED (CB-REV-0003 #2).
|
|
played: u32,
|
|
group_success: u32,
|
|
/// Seats named a winner. Under SHARED GROUND that is everyone or
|
|
/// nobody; under the other two it is the point of the mode.
|
|
winners: u32,
|
|
/// Claimed value summed over games, so the threshold has something
|
|
/// to be compared against.
|
|
total: u32,
|
|
threshold: u32,
|
|
setup_fails: u32,
|
|
}
|
|
|
|
/// Which bot fills every seat. CB-WP-0049 T03: F27 says the three modes
|
|
/// produce identical play because greedy never reads `state.mode`; the
|
|
/// only way to know whether that is a fact about GROUND or a fact about
|
|
/// our bot is to run the same panel with a seat that does read it.
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum Seat {
|
|
Greedy,
|
|
Objective,
|
|
}
|
|
|
|
fn sweep(scenario: &str, mode: ScoringMode, players: u8, who: Seat) -> Cell {
|
|
let mut c = Cell {
|
|
games: 0,
|
|
played: 0,
|
|
group_success: 0,
|
|
winners: 0,
|
|
total: 0,
|
|
threshold: 0,
|
|
setup_fails: 0,
|
|
};
|
|
let preset = if scenario == "SCN_01" {
|
|
format!("standard-{players}p")
|
|
} else {
|
|
format!("scn-{}-{players}p", scenario.rsplit('_').next().unwrap())
|
|
};
|
|
for seed in 0..GAMES as u64 {
|
|
let Ok(mut st) = GroundState::setup(
|
|
&Setup {
|
|
players,
|
|
preset: preset.clone(),
|
|
patch: Default::default(),
|
|
},
|
|
seed,
|
|
) else {
|
|
// Counted, not skipped (CB-REV-0001 #11).
|
|
c.setup_fails += 1;
|
|
continue;
|
|
};
|
|
st.mode = mode;
|
|
// The board actually dealt, so the panel cannot report a
|
|
// scenario it did not play.
|
|
assert_eq!(st.scenario, scenario, "{preset} dealt {}", st.scenario);
|
|
let mut ps: Vec<Box<dyn Policy>> = (0..players)
|
|
.map(|_| match who {
|
|
Seat::Greedy => Box::new(GreedyPolicy) as Box<dyn Policy>,
|
|
Seat::Objective => Box::new(ObjectivePolicy) as Box<dyn Policy>,
|
|
})
|
|
.collect();
|
|
let g = match play(st, &mut ps) {
|
|
Ok(g) => g,
|
|
Err(e) => {
|
|
eprintln!(" !! {scenario} {mode:?} {players}p seed {seed}: {e:?}");
|
|
continue;
|
|
}
|
|
};
|
|
c.games += 1;
|
|
if g.state.outcome.is_some() && g.rounds == 5 {
|
|
c.played += 1;
|
|
}
|
|
if let Some(o) = &g.state.outcome {
|
|
if o.group_success {
|
|
c.group_success += 1;
|
|
}
|
|
c.winners += o.winners.len() as u32;
|
|
c.total += o.total;
|
|
c.threshold = o.threshold;
|
|
}
|
|
}
|
|
assert_eq!(
|
|
c.games, GAMES,
|
|
"{scenario} {mode:?} {players}p: only {} of {GAMES} games ran \
|
|
({} setups refused) — every number in the cell is over a sample \
|
|
nobody chose",
|
|
c.games, c.setup_fails
|
|
);
|
|
assert_eq!(
|
|
c.played, GAMES,
|
|
"{scenario} {mode:?} {players}p: {} of {} games reached an outcome \
|
|
over five rounds",
|
|
c.played, c.games
|
|
);
|
|
c
|
|
}
|
|
|
|
fn main() {
|
|
println!("CB-WP-0047 — the four Scenarios, the three Modes\n");
|
|
println!("Greedy throughout; {GAMES} games per cell; seeds 0..{GAMES}.");
|
|
println!("`won` is group success; `win/g` is winning SEATS per game,");
|
|
println!("which is what separates the modes — SHARED GROUND names all");
|
|
println!("or none, the other two name a subset.\n");
|
|
|
|
let scenarios = games_ground::edition::scenarios().expect("Scenarios.csv");
|
|
// Every mode the kernel has. Adding a fourth without extending the
|
|
// panel is the omission this list exists to make loud.
|
|
let modes = [
|
|
("SHARED GROUND ", ScoringMode::SharedGround),
|
|
("COMMON PROBLEM ", ScoringMode::CommonProblem),
|
|
("BONDED COALITIONS ", ScoringMode::BondedCoalitions),
|
|
];
|
|
|
|
for players in [2u8, 4, 6] {
|
|
println!("{players} players");
|
|
println!(
|
|
" {:<32} {:<18} {:>5} {:>5} {:>7} {:>7} {:>6} {:>9}",
|
|
"scenario", "mode", "grdy", "obj", "win/g g", "win/g o", "pts", "threshold"
|
|
);
|
|
for s in &scenarios {
|
|
for (label, mode) in &modes {
|
|
let g = sweep(&s.id, *mode, players, Seat::Greedy);
|
|
let c = sweep(&s.id, *mode, players, Seat::Objective);
|
|
println!(
|
|
" {:<32} {label} {:>5} {:>5} {:>7.2} {:>7.2} {:>6.1} {:>9}",
|
|
format!("{} {}", s.id, s.title),
|
|
g.group_success,
|
|
c.group_success,
|
|
// **Both, side by side.** Reading one policy's number
|
|
// against a figure remembered from an earlier pass is
|
|
// how a comparison gets made against a board nobody
|
|
// re-ran.
|
|
f64::from(g.winners) / f64::from(g.games),
|
|
f64::from(c.winners) / f64::from(c.games),
|
|
f64::from(c.total) / f64::from(c.games),
|
|
c.threshold,
|
|
);
|
|
}
|
|
}
|
|
println!();
|
|
}
|
|
|
|
println!("`grdy` is GreedyPolicy, which never reads state.mode; `obj`");
|
|
println!("is ObjectivePolicy, which plays its seat's own objective.");
|
|
println!("If the obj column varies ACROSS modes where grdy does not,");
|
|
println!("F27 was a fact about our bot. If it does not, it is a fact");
|
|
println!("about GROUND.\n");
|
|
println!("SCN_01 and SCN_02 are the same board — identical suits and");
|
|
println!("values at every priority. Their rows are expected to MATCH,");
|
|
println!("and a divergence means one of the two decks was edited.");
|
|
}
|