CB-WP-0047: all four boards, and every mode named on the page
Some checks failed
ci / check (push) Failing after 3s

The modes were already implemented; nothing had ever COMPARED them. The
scenarios were not implemented at all: edition::deal has taken a
scenario_id since it was written and the only caller passed the literal
"SCN_01", so 15 of 20 Problem cards had never been dealt by anything.
The seam was the whole mechanism and it sat unused, with nothing red
because nothing asked.

Scenario is now state (serde default SCN_01, so all 26 recordings replay
unchanged), selected by preset `scn-03-4p` with `standard-Np` still
meaning SCN_01, and by --scenario/SCENARIO= accepting ids, numbers or
titles, validated against the edition rather than a pattern.

The threshold now comes off the Scenario card, closing F25's hardcoded
5/7/9. The first version of that control was worthless and mutation said
so: all four scenarios print 5/7/9, so reverting to the bands left it
green. Split threshold_from() so it can be handed a card that disagrees.

The header read `scoring CommonProblem` where the Mode card is titled
COMMON PROBLEM, PERSONAL EDGE -- the defect CB-WP-0034 deleted from the
move buttons, still standing on the line that says what winning means.
The coverage probe was matching that Debug output and went red when it
was fixed: third instance (CB-WP-0024, CB-WP-0034). Page now carries the
premise, the mode's rules text, and the tiebreak.

scenario-panel plays 4x3x3. Findings: SCN_01 and SCN_02 are the same
board (identical cells, pinned by a characterisation test); SCN_04 is
the hard board at 2p (52% vs 67/73%, the only deck needing two Repair);
and group success is EXACTLY equal across all three modes in all 36
cells, because greedy never reads state.mode -- filed F27, the two
competitive modes are scoring lenses over cooperative play.

F28: SHARED GROUND's mastery subtracts penalties from the claimed COUNT
where the mode card's shared score is claimed VALUE. Raised, not fixed;
scoring is ground-game's to rule on.

Also fixes design.py reporting a backticked path as no reproduction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-08 20:51:46 +02:00
parent 1d3f1bfe60
commit d30938b259
19 changed files with 1351 additions and 109 deletions

View file

@ -1069,13 +1069,22 @@ pub fn document_with_log(
s,
"<h1>GROUND \u{2014} round {round}, step {step:?}</h1>\
<div><span class=\"k\">lead</span> {lead} \
<span class=\"k\">scoring</span> {mode:?} \
<span class=\"k\">scoring</span> {mode} \
<span class=\"k\">rules</span> {variant} \
<span class=\"k\">scenario</span> {scenario} \
<span class=\"k\">viewing as</span> {who}</div>",
round = view.round,
step = view.step,
lead = seat_name(view.lead),
mode = view.mode,
// CB-WP-0047: this was `{mode:?}` — `CommonProblem` where the
// Mode card is titled "COMMON PROBLEM, PERSONAL EDGE". The exact
// defect CB-WP-0034 fixed on the move buttons, still standing in
// the header, on the one line that says what winning means.
mode = esc(&mode_title(view.mode)),
// CB-WP-0047: and WHICH of the four boards this is. Four
// scenarios became selectable in this pass; before it, a page
// that never named one was at least never wrong.
scenario = esc(&scenario_title(&view.scenario)),
// CB-WP-0044: the page must say which rules it is playing. It did
// not, so a player who ran the default and was told the layout had
// changed reported "no changes" — correctly, because the baseline
@ -1306,6 +1315,46 @@ fn body(s: &mut String, view: &GroundView) {
s.push_str("<div class=\"card\">no problems in play</div>");
}
s.push_str(&table_svg(view));
// CB-WP-0047: WHAT THIS GAME IS ABOUT, and WHAT WINNING MEANS.
//
// The premise was vendored and unread while one scenario was
// hardcoded — with a single board there was nothing to tell apart.
// Selecting among four makes an unnamed board a real defect.
//
// The Mode card is the sharper one: it is the only statement of the
// win condition, three of them differ completely, and the page had
// never shown any of them. A player in BONDED COALITIONS was not
// told that Bonds decide the sides at the end.
if let Some(premise) = scenario_premise(&view.scenario) {
let _ = write!(
s,
"<details class=\"card\"><summary>{} \u{2014} what happened\
</summary>{}</details>",
esc(&scenario_title(&view.scenario)),
esc(&premise)
);
}
if let Some((rules, tiebreak)) = mode_rules_text(view.mode) {
let _ = write!(
s,
"<details class=\"card\"><summary>{} \u{2014} how this game is won\
</summary>{}",
esc(&mode_title(view.mode)),
esc(&rules)
);
// The tiebreak is a rule the player can PLAY FOR — "lower Stress,
// then more Bonds" changes what a losing seat should do in round
// five — and it lives in its own column, so it renders as its own
// sentence rather than being silently dropped.
if !tiebreak.trim().is_empty() && !tiebreak.eq_ignore_ascii_case("Not applicable.") {
let _ = write!(
s,
"<br><span class=\"k\">tiebreak</span> {}",
esc(&tiebreak)
);
}
s.push_str("</details>");
}
// CB-WP-0046: the placement is legible, the RULE it encodes was not.
//
// Shown only when a non-global scope is actually on the table — under
@ -1523,6 +1572,59 @@ fn stress_scope_rule_text() -> Option<String> {
games_ground::edition::stress_scope_rule()
}
/// The Mode card's printed title, not the Rust variant's name.
///
/// Falls back to the id, which is at least a thing the edition says —
/// never to `Debug`, which is a thing only the compiler says.
fn mode_title(mode: games_ground::ScoringMode) -> String {
use games_ground::ScoringMode as M;
let want = match mode {
M::SharedGround => "MODE_COOP",
M::CommonProblem => "MODE_SEMI",
M::BondedCoalitions => "MODE_COALITION",
};
games_ground::edition::modes()
.ok()
.and_then(|ms| {
ms.into_iter()
.find(|(c, _)| c.id == want)
.map(|(c, _)| c.title)
})
.unwrap_or_else(|| want.to_string())
}
/// What the Mode card says about how this game is won (CB-WP-0047).
fn mode_rules_text(mode: games_ground::ScoringMode) -> Option<(String, String)> {
use games_ground::ScoringMode as M;
let want = match mode {
M::SharedGround => "MODE_COOP",
M::CommonProblem => "MODE_SEMI",
M::BondedCoalitions => "MODE_COALITION",
};
games_ground::edition::modes().ok().and_then(|ms| {
ms.into_iter()
.find(|(c, _)| c.id == want)
.map(|(c, tie)| (c.rules_text, tie))
})
}
/// The Scenario card's printed title.
fn scenario_title(id: &str) -> String {
games_ground::edition::scenarios()
.ok()
.and_then(|ss| ss.into_iter().find(|s| s.id == id).map(|s| s.title))
.unwrap_or_else(|| id.to_string())
}
/// The Scenario card's premise — what the table is arguing about.
fn scenario_premise(id: &str) -> Option<String> {
games_ground::edition::scenarios()
.ok()?
.into_iter()
.find(|s| s.id == id)
.map(|s| s.premise)
}
/// A GROUND sub-choice, in words, naming whoever it touches.
fn ground_choice_label(c: &games_ground::GroundChoice) -> String {
use games_ground::GroundChoice as G;

View file

@ -135,7 +135,12 @@ mod coverage {
("round", "round 3"),
("lead", "lead P2"),
("step", "step Select"),
("mode", "scoring BondedCoalitions"),
// Was "scoring BondedCoalitions" — the Rust variant's name.
// **The probe was certifying the defect**, and went red the
// moment the defect was fixed. Third time this exact shape has
// been found (CB-WP-0024, CB-WP-0034): a probe that names a
// rendering holds that rendering in place, so probes name FACTS.
("mode", "BONDED COALITIONS"),
("viewer", "viewing as P1"),
("solution_deck_len", "draw pile: 17 remaining"),
("solution_discard.*.suit", "discard Repair Change"),
@ -188,6 +193,11 @@ mod coverage {
// OMITTED, and the page would be illegible to `text_of` and to a
// screen reader alike.
("variant", "ground-darvo-r0"),
// The fixture plays SCN_03, so the token is that card's TITLE.
// Not "SCN_03": a probe that matches an id would go green
// against a page showing the id, which is the machine's name for
// the board and not the player's (CB-WP-0034's finding).
("scenario", "Decision Without Consent"),
("problem_markers.*.owner", "P1\u{2019}s alone"),
("problem_markers.*.scope", "P2\u{2019}s Bond network"),
("outcome.total", "total 9"),
@ -697,6 +707,79 @@ mod gamelog {
}
}
/// **The page names the Mode by its printed title** (CB-WP-0047).
///
/// The header read `scoring CommonProblem` — the Rust variant's
/// name, where the Mode card is titled *"COMMON PROBLEM, PERSONAL
/// EDGE"*. CB-WP-0034 deleted this exact defect from the move
/// buttons and it was still standing on the one line that says what
/// winning means.
#[test]
fn every_mode_is_named_and_explained_in_the_editions_words() {
use games_ground::ScoringMode as M;
for mode in [M::SharedGround, M::CommonProblem, M::BondedCoalitions] {
let mut v = crate::testfix::view(Some(PlayerId(0)));
v.mode = mode;
let text = crate::text_of(&crate::doc::document(
&v,
&[],
"/c",
Some(PlayerId(0)),
false,
));
assert!(
!text.contains(&format!("{mode:?}")),
"{mode:?}: the page shows the Rust variant's name"
);
let (card, _) = games_ground::edition::modes()
.expect("Modes.csv")
.into_iter()
.find(|(c, _)| {
c.id == match mode {
M::SharedGround => "MODE_COOP",
M::CommonProblem => "MODE_SEMI",
M::BondedCoalitions => "MODE_COALITION",
}
})
.expect("the mode card");
assert!(
text.contains(&card.title),
"{mode:?}: the Mode card's title {:?} is not on the page",
card.title
);
// **And what it MEANS.** Three modes define winning three
// different ways and the page had never stated any of them.
let head: String = card.rules_text.chars().take(50).collect();
assert!(
text.contains(&head),
"{mode:?}: nothing on the page says how this game is won"
);
}
}
/// **The page names which of the four boards this is** (CB-WP-0047).
#[test]
fn every_scenario_is_named_with_its_premise() {
for s in games_ground::edition::scenarios().expect("Scenarios.csv") {
let mut v = crate::testfix::view(Some(PlayerId(0)));
v.scenario = s.id.clone();
let text = crate::text_of(&crate::doc::document(
&v,
&[],
"/c",
Some(PlayerId(0)),
false,
));
assert!(text.contains(&s.title), "{}: the board is unnamed", s.id);
let head: String = s.premise.chars().take(50).collect();
assert!(
text.contains(&head),
"{}: the page does not say what the table is arguing about",
s.id
);
}
}
/// **A scope says what it does** (CB-WP-0046).
///
/// CB-WP-0045 shipped the placement and recorded the gap with the

View file

@ -76,6 +76,7 @@ pub fn view(viewer: Option<PlayerId>) -> GroundView {
lead: p2,
step: RoundStep::Select,
mode: ScoringMode::BondedCoalitions,
scenario: "SCN_03".into(),
players,
relations: BTreeMap::from([
(Pair::new(p1, p2), Relation::Rivalry),