CB-WP-0048 T02/T03/T04: Configuration replaces Variant in the state
Some checks failed
ci / check (push) Has been cancelled
Some checks failed
ci / check (push) Has been cancelled
The three-armed enum could not express a configuration carrying two
modules. It does now: --variant h1 expands to problem_stress.flat_any_open
AND attack_relief.self_soothe_ge4, and scoped_plus_attack_soothe plays.
Legacy names alias forever via serde(alias="variant") plus a
scalar-or-map deserialiser. serde(default) alone would have been a silent
migration bug -- every H1/H2 recording would have come back as baseline.
All 26 scenarios pass unchanged; none pins a state hash.
Three things shipped broken first, all caught by gates rather than by
reading:
1. `profile` was in the state hash. with_config(select("ground-darvo-r0"))
sets profile=Some("baseline") where setup alone leaves None, so two
states at THE SAME POINT IN ASPECT SPACE hashed differently and a
replay bundle stopped reproducing its own initial state. Now
serde(skip): a hash covers what determines play. This was the open
judgement from T01 and it did not survive contact with the replay path.
2. A YAML parse inside the event loop. rules() -> resolve() -> catalog()
re-parsed catalog.yaml per rule check; AM-6 fell to 9,345 events/s
against a 100,000 target. OnceLock, and aspect validation moved to
where a configuration is BUILT.
Then I nearly optimised a phantom: 470k still looked like a 3x
regression against the "~1.7M on bnt-lap001" reference in the gate's
own message. Making rules() free measured 491k -- this machine's
ceiling. Before optimising against a reference, measure the ceiling
with the suspect code removed.
3. The refusal did not fire on the path a player takes.
`--module problem_deal.pressure_deck` played a full baseline game and
reported success, because with_config is a builder and fell back to
the printed rules -- the silent no-op ADR-0022 exists to refuse. Every
unit test of resolve() passed. The helper was tested and the driver
was not, which is CB-WP-0033's finding verbatim. The new test asserts
refusal BY NAME and that no game was played.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
704b99975b
commit
48a7bce04d
12 changed files with 538 additions and 67 deletions
|
|
@ -1089,7 +1089,10 @@ pub fn document_with_log(
|
|||
// not, so a player who ran the default and was told the layout had
|
||||
// changed reported "no changes" — correctly, because the baseline
|
||||
// is deliberately unchanged and nothing on screen said so.
|
||||
variant = esc(view.variant.id()),
|
||||
// CB-WP-0048: the page names every LIVE MODULE, not one package
|
||||
// id. A configuration is a point in aspect space and can carry
|
||||
// two modules at once; `h2` was never able to say that.
|
||||
variant = esc(&config_label(&view.config)),
|
||||
who = match view.viewer {
|
||||
Some(p) => format!("{} (their hand only)", seat_name(p)),
|
||||
None => "a spectator (no hands)".to_string(),
|
||||
|
|
@ -1572,6 +1575,19 @@ fn stress_scope_rule_text() -> Option<String> {
|
|||
games_ground::edition::stress_scope_rule()
|
||||
}
|
||||
|
||||
/// Which modules are live, in words (CB-WP-0048, ADR-0022).
|
||||
///
|
||||
/// The baseline says so by name rather than showing an empty list: "no
|
||||
/// modules" and "we could not read the catalog" must not look the same
|
||||
/// on a page a player uses to tell one rules set from another.
|
||||
fn config_label(c: &games_ground::config::Configuration) -> String {
|
||||
let live = c.active_modules();
|
||||
if live.is_empty() {
|
||||
return c.baseline.clone();
|
||||
}
|
||||
live.join(" + ")
|
||||
}
|
||||
|
||||
/// 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 —
|
||||
|
|
|
|||
|
|
@ -192,7 +192,10 @@ mod coverage {
|
|||
// that could only be satisfied by geometry would have to be
|
||||
// OMITTED, and the page would be illegible to `text_of` and to a
|
||||
// screen reader alike.
|
||||
("variant", "ground-darvo-r0"),
|
||||
// The page shows the LIVE modules, or the baseline id when none
|
||||
// are — a player is asking "which rules am I playing", not "what
|
||||
// is every aspect set to". The fixture is the baseline.
|
||||
("config.baseline", "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
|
||||
|
|
@ -211,10 +214,37 @@ mod coverage {
|
|||
];
|
||||
|
||||
/// Deliberate omissions, each with a reason.
|
||||
const OMITTED: &[(&str, &str)] = &[(
|
||||
"players.*.hand",
|
||||
"null for a non-viewer seat; the absence is rendered as a count",
|
||||
)];
|
||||
const OMITTED: &[(&str, &str)] = &[
|
||||
(
|
||||
"players.*.hand",
|
||||
"null for a non-viewer seat; the absence is rendered as a count",
|
||||
),
|
||||
// An aspect sitting at its default IS the printed game, and
|
||||
// naming all four on every page is the wall of rules a player
|
||||
// stops reading — the same reasoning that keeps the GROUND-mode
|
||||
// explanation to the moment a mode is being chosen.
|
||||
//
|
||||
// **A LIVE module is a different matter and is not omitted**:
|
||||
// `the_table_names_every_live_module` asserts it by name, so
|
||||
// this omission covers only the case where there is nothing to
|
||||
// say. The machine-facing `cb-play inspect` prints all four.
|
||||
(
|
||||
"config.modules.problem_stress",
|
||||
"shown only when it differs from the printed rules; see the_table_names_every_live_module",
|
||||
),
|
||||
(
|
||||
"config.modules.attack_relief",
|
||||
"shown only when it differs from the printed rules; see the_table_names_every_live_module",
|
||||
),
|
||||
(
|
||||
"config.modules.end_condition",
|
||||
"shown only when it differs from the printed rules; see the_table_names_every_live_module",
|
||||
),
|
||||
(
|
||||
"config.modules.problem_deal",
|
||||
"shown only when it differs from the printed rules; see the_table_names_every_live_module",
|
||||
),
|
||||
];
|
||||
|
||||
fn fixture() -> GroundView {
|
||||
crate::testfix::view(Some(PlayerId(0)))
|
||||
|
|
@ -780,6 +810,52 @@ mod gamelog {
|
|||
}
|
||||
}
|
||||
|
||||
/// **The table names every live module** (CB-WP-0048 T04).
|
||||
///
|
||||
/// CB-WP-0044's finding, generalised: a rules change the page cannot
|
||||
/// name is reported as "no changes". A configuration can now carry
|
||||
/// two modules at once, so naming one package id would be the wrong
|
||||
/// shape — `h2` could never have said
|
||||
/// `problem_stress.scoped + attack_relief.self_soothe_ge4`.
|
||||
#[test]
|
||||
fn the_table_names_every_live_module() {
|
||||
let combo = games_ground::config::Configuration::select("scoped_plus_attack_soothe")
|
||||
.expect("the catalog ships this profile");
|
||||
let mut v = crate::testfix::view(Some(PlayerId(0)));
|
||||
v.config = combo.clone();
|
||||
let text = crate::text_of(&crate::doc::document(
|
||||
&v,
|
||||
&[],
|
||||
"/c",
|
||||
Some(PlayerId(0)),
|
||||
false,
|
||||
));
|
||||
for m in combo.active_modules() {
|
||||
assert!(
|
||||
text.contains(&m),
|
||||
"the page does not name the live module {m}"
|
||||
);
|
||||
}
|
||||
|
||||
// And the baseline says so by NAME rather than showing nothing:
|
||||
// "no modules" and "we could not read the catalog" must not look
|
||||
// the same on the line a player uses to tell rules sets apart.
|
||||
let mut base = crate::testfix::view(Some(PlayerId(0)));
|
||||
base.config = games_ground::config::Configuration::default();
|
||||
let plain = crate::text_of(&crate::doc::document(
|
||||
&base,
|
||||
&[],
|
||||
"/c",
|
||||
Some(PlayerId(0)),
|
||||
false,
|
||||
));
|
||||
assert!(plain.contains("ground-darvo-r0"), "the baseline is unnamed");
|
||||
assert!(
|
||||
!plain.contains("problem_stress."),
|
||||
"the baseline page names a module that is not live"
|
||||
);
|
||||
}
|
||||
|
||||
/// **A scope says what it does** (CB-WP-0046).
|
||||
///
|
||||
/// CB-WP-0045 shipped the placement and recorded the gap with the
|
||||
|
|
@ -790,7 +866,7 @@ mod gamelog {
|
|||
#[test]
|
||||
fn a_scoped_table_says_what_a_scope_does() {
|
||||
let mut v = crate::testfix::view(Some(PlayerId(0)));
|
||||
v.variant = games_ground::Variant::H2ScopedProblemStress;
|
||||
v.config = games_ground::Variant::H2ScopedProblemStress.configuration();
|
||||
// One card that is not everyone's — the condition for the rule
|
||||
// to be worth stating at all.
|
||||
if let Some(m) = v.problem_markers.values_mut().next() {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ pub fn view(viewer: Option<PlayerId>) -> GroundView {
|
|||
(Pair::new(p2, p3), Relation::Bond),
|
||||
]),
|
||||
problems,
|
||||
variant: games_ground::Variant::Baseline,
|
||||
config: games_ground::config::Configuration::default(),
|
||||
focus: BTreeMap::from([(p1, p3)]),
|
||||
selections: BTreeMap::from([
|
||||
(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue