CB-WP-0008-T03: prove the 2-6 player range

GR-O01 states 2-6 players; every scenario in the corpus was 3-player.
Now all five counts play to GameEnded under both policies and reproduce
at the same seed, with scenarios at both boundaries and the CLI
transcript run at 2p, 3p and 6p.

Nothing broke — the rules are seat-count-generic. What the boundaries
exposed is arithmetic: with the standard preset's placeholder Problem
values (value = priority), the best total any game can reach is 3 at 2p,
6 at 3-4p, 10 at 5-6p, against GR-E01 thresholds of 5, 7 and 9. Group
success is unreachable below five seats regardless of play, and no
scenario noticed because none had played to scoring with everything
claimed.

GR-S01 calls the fixture a stand-in for scenario Problem data, so this
is evidence the stand-in is not neutral, not that GR-E01 is wrong. It is
pinned by a passing scenario, an arithmetic test, and a provisional
marker owned by ground-game so it ages in `make coverage`. The test
states its own delete-by: it is expected to fail when Problem values
become real data, and that failure is the signal to delete it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-01 15:20:04 +02:00
parent 25531e9d01
commit d97e8e825d
7 changed files with 289 additions and 28 deletions

View file

@ -142,37 +142,42 @@ mod tests {
list.iter().map(|s| s.to_string()).collect()
}
/// A scripted transcript plays a full 3-player game to `GameEnded`.
/// "0" always takes the first legal command, which is a real player
/// input and needs no knowledge of the board.
/// A scripted transcript plays a full game to `GameEnded`. "0"
/// always takes the first legal command, which is a real player input
/// and needs no knowledge of the board.
///
/// Run at **both GR-O01 boundaries and the middle** (T03): the CLI is
/// where a seat-count assumption would show up as a prompt nobody can
/// answer.
#[test]
fn a_scripted_transcript_plays_a_full_game() {
let config = Config {
seed: 42,
players: 3,
human_seats: vec![0],
bot: "greedy".into(),
replay_dir: None,
record: None,
};
let script = "0\n".repeat(200);
let mut out: Vec<u8> = Vec::new();
let summary = table::play(&config, script.as_bytes(), &mut out)
.unwrap_or_else(|e| panic!("scripted game failed: {e}"));
for players in [2u8, 3, 6] {
let config = Config {
seed: 42,
players,
human_seats: vec![0],
bot: "greedy".into(),
replay_dir: None,
record: None,
};
let script = "0\n".repeat(400);
let mut out: Vec<u8> = Vec::new();
let summary = table::play(&config, script.as_bytes(), &mut out)
.unwrap_or_else(|e| panic!("{players}p scripted game failed: {e}"));
assert_eq!(summary.rounds, 5, "GR-R09 runs five rounds");
let text = String::from_utf8(out).expect("utf8");
assert!(text.contains("OUTCOME"), "the game must report an outcome");
assert!(
text.contains("you are P1"),
"the human seat must be prompted"
);
assert_eq!(summary.rounds, 5, "{players}p: GR-R09 runs five rounds");
let text = String::from_utf8(out).expect("utf8");
assert!(text.contains("OUTCOME"), "{players}p: no outcome reported");
assert!(text.contains("you are P1"), "{players}p: no prompt");
// The transcript replays identically — through the scenario
// runner, not through a second call to the same driver.
match run::<GroundState>(&summary.scenario) {
RunOutcome::Passed { .. } => {}
RunOutcome::Failed { reason, .. } => panic!("replay failed: {reason}"),
// The transcript replays identically — through the scenario
// runner, not through a second call to the same driver.
match run::<GroundState>(&summary.scenario) {
RunOutcome::Passed { .. } => {}
RunOutcome::Failed { reason, .. } => {
panic!("{players}p replay failed: {reason}")
}
}
}
}