CB-WP-0045: the table names what it shows
Some checks failed
ci / check (push) Failing after 4s

Three reports from the 2026-08-08 sessions. H2 lands ("that seemed more
interesting"), two legibility gaps do not.

The number was only a FALLBACK for a missing title, so it showed exactly
when it was least useful. That was survivable while Problems sat in one
ordered row -- position WAS the number. CB-WP-0043 scattered them by
scope and took the implicit index with it, with nothing going red because
no test named the number.

DARVO.csv's mandatory_effect and Actions.csv's GROUND rules_text were
both vendored, both used only as tripwires, and neither ever reached the
player -- F18's shape again. The modes now explain themselves at the
point of choice, and NOT when no mode is on offer.

Tests assert the edition's own words verbatim (ADR-0015), the number over
every key in view.problems, and both halves of the mode explanation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-08 18:38:09 +02:00
parent d9b88d3140
commit fd76db4f71
7 changed files with 809 additions and 0 deletions

View file

@ -401,6 +401,9 @@ fn problem_svg(out: &mut String, priority: u32, p: &ProblemView, x: i32) {
out,
"<g data-drop=\"problem-{priority}\"><rect x=\"{x}\" y=\"10\" width=\"120\" height=\"78\" rx=\"8\" \
fill=\"{fill}\" stroke=\"#5a6b7a\"/>\
<circle cx=\"{bx}\" cy=\"24\" r=\"11\" fill=\"#1b1e26\" stroke=\"#5a6b7a\"/>\
<text x=\"{bx}\" y=\"29\" fill=\"#9cf\" font-size=\"13\" text-anchor=\"middle\">\
{priority}</text>\
<text x=\"{tx}\" y=\"36\" fill=\"#dde\" font-size=\"13\">{label}</text>\
<text x=\"{tx}\" y=\"56\" fill=\"#89a\" font-size=\"11\">{name}</text>\
<text x=\"{tx}\" y=\"74\" fill=\"#fc9\" font-size=\"11\">{sub}</text></g>",
@ -410,6 +413,14 @@ fn problem_svg(out: &mut String, priority: u32, p: &ProblemView, x: i32) {
// not remove the only identifier the player had.
name = esc(&problem_title(priority).unwrap_or_else(|| format!("priority {priority}"))),
tx = x + 10,
// CB-WP-0045: the number, on the card.
//
// Every move label says "Problem 7" and nothing on the table said
// which card that was. In the old row the position was a hint;
// once Problems are placed by scope the row is gone and the hint
// with it. The number was only ever a FALLBACK for a missing
// title — visible exactly when it was least useful.
bx = x + 104,
label = esc(&label),
sub = esc(&sub),
);
@ -941,6 +952,21 @@ fn player_card(out: &mut String, id: PlayerId, p: &PlayerView, view: &GroundView
//
// The `support` line is the one the maintainer reported: it read
// `support AcceptBond`, which names the answer and not the person.
// CB-WP-0045: **what this DARVO stage will do**, in the edition's own
// words. `DARVO.csv` carries `mandatory_effect` per stage; we vendored
// it for tripwires and never showed it, so the page said "DARVO
// Reverse" and left the player to guess. Reported as *"the GROUND and
// DARVO logic is far from self explanatory"*.
if p.darvo != games_ground::DarvoStage::Off {
if let Some(text) = darvo_stage_text(p.darvo) {
let _ = write!(
out,
"<details><summary>what {:?} does</summary>{}</details>",
p.darvo,
esc(&text)
);
}
}
if let Some(m) = view.ground_modes.get(&id) {
let _ = write!(
out,
@ -1432,6 +1458,38 @@ fn support_words(r: &games_ground::SupportResponse, who: Option<PlayerId>) -> St
}
}
/// The edition's own words for what a DARVO stage does (CB-WP-0045).
///
/// From `DARVO.csv`'s `mandatory_effect`. **Not our paraphrase**: the game
/// says what its stages do and ADR-0015's discipline is to use that.
fn darvo_stage_text(stage: games_ground::DarvoStage) -> Option<String> {
use games_ground::DarvoStage;
let want = match stage {
DarvoStage::Off => return None,
DarvoStage::Deny => "DENY",
DarvoStage::Attack => "ATTACK",
DarvoStage::Reverse => "REVERSE",
};
games_ground::edition::darvo_stages()
.ok()?
.into_iter()
.find(|s| s.stage == want)
.map(|s| s.mandatory_effect)
}
/// The GROUND card's own account of its three modes (CB-WP-0045).
///
/// Shown where the mode is chosen. The card explains GR, OU and ND in one
/// passage; by the Reveal step the action card is no longer on screen, so
/// the explanation has to travel to the decision.
fn ground_modes_text() -> Option<String> {
games_ground::edition::actions()
.ok()?
.into_iter()
.find(|c| c.id.to_uppercase().contains("GROUND"))
.map(|c| c.rules_text)
}
/// 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;
@ -1605,6 +1663,23 @@ fn move_section(
);
}
}
// CB-WP-0045: when a GROUND mode is on offer, the card's own
// account of the three modes travels with the decision. By the
// Reveal step the action card has left the screen, so the player
// was choosing GR / OU / ND from three names alone.
if legal
.iter()
.any(|c| matches!(c, games_ground::GroundCommand::ChooseGroundMode { .. }))
{
if let Some(text) = ground_modes_text() {
let _ = write!(
s,
"<details class=\"card\" open><summary>what the GROUND modes do\
</summary>{}</details>",
esc(&text)
);
}
}
s.push_str("</div><div class=\"row\">");
for (i, c) in legal.iter().enumerate() {
let spatial = seat.is_some_and(|seat| crate::input::affordance(c, seat).is_some());

View file

@ -697,6 +697,114 @@ mod gamelog {
}
}
/// **The number is on the card** (CB-WP-0045).
///
/// Every move label says "Problem 7"; nothing on the table said which
/// card that was. In the old row, position was a hint — once
/// Problems are placed by scope the row is gone and the hint with it.
/// The number was only ever a fallback for a missing title, so it
/// appeared exactly when it was least useful.
#[test]
fn every_problem_card_shows_its_number() {
let v = crate::testfix::view(Some(PlayerId(0)));
let text = crate::text_of(&crate::doc::document(
&v,
&[],
"/c",
Some(PlayerId(0)),
false,
));
for key in v.problems.keys() {
assert!(
text.contains(&format!("{key}")),
"Problem {key} is on the table with no number, and the move \
labels call it by one"
);
}
}
/// **The game's own words for a DARVO stage** (CB-WP-0045).
///
/// Reported as *"the GROUND and DARVO logic is far from self
/// explanatory"*. `DARVO.csv` carries `mandatory_effect` per stage —
/// vendored for tripwires and never shown, so the page said "DARVO
/// Reverse" and left the player to guess. F18's shape again: the data
/// was present and could not fire.
#[test]
fn a_darvo_stage_says_what_it_does() {
let v = crate::testfix::view(Some(PlayerId(0)));
let text = crate::text_of(&crate::doc::document(
&v,
&[],
"/c",
Some(PlayerId(0)),
false,
));
// The fixture has a seat at Reverse.
assert!(
text.contains("what Reverse does"),
"a seat in a DARVO stage does not say what the stage does"
);
// And it is the EDITION's sentence, not a paraphrase of ours.
let edition = games_ground::edition::darvo_stages()
.expect("DARVO.csv")
.into_iter()
.find(|s| s.stage == "REVERSE")
.expect("REVERSE");
let head: String = edition.mandatory_effect.chars().take(40).collect();
assert!(
text.contains(&head),
"the stage text is not the edition's own words: {head:?}"
);
}
/// **The GROUND modes explain themselves at the decision** (CB-WP-0045).
///
/// By the Reveal step the action card has left the screen, so the
/// player was choosing GR / OU / ND from three names alone.
#[test]
fn the_ground_modes_explain_themselves_where_they_are_chosen() {
use games_ground::{GroundCommand, GroundMode};
let v = crate::testfix::view(Some(PlayerId(0)));
let legal = vec![GroundCommand::ChooseGroundMode {
mode: GroundMode::Gr,
choice: None,
}];
let with = crate::text_of(&document_with_log(
&v,
&legal,
crate::TEST_ENDPOINTS,
Some(PlayerId(0)),
false,
Account::of(&[]),
&[],
));
assert!(
with.contains("what the GROUND modes do"),
"the mode choice is offered with no account of the modes"
);
assert!(
with.contains("Ground & Restate"),
"the explanation is not the card's own text"
);
// Absent when no mode is on offer — an always-on wall of rules is
// how a player stops reading them.
let without = crate::text_of(&document_with_log(
&v,
&[],
crate::TEST_ENDPOINTS,
Some(PlayerId(0)),
false,
Account::of(&[]),
&[],
));
assert!(
!without.contains("what the GROUND modes do"),
"the mode explanation shows when no mode is being chosen"
);
}
/// **A Problem is drawn where its Stress lands** (CB-WP-0043).
///
/// H2 scopes End-of-Round Stress, so a row across the middle — true