diff --git a/Makefile b/Makefile index 54150bc..0d2db3a 100644 --- a/Makefile +++ b/Makefile @@ -35,11 +35,6 @@ PLAYERS ?= 3 # a session that could not say which variant it was is what produced # the report "I did a session and noted no changes". VARIANT ?= ground-darvo-r0 -# CB-WP-0047: the other three boards and the other two modes were -# unreachable from the way anyone actually plays. Defaults are what was -# always played, so `make ground` is unchanged. -SCENARIO ?= 1 -MODE ?= shared PORT ?= 0 # Every cargo recipe runs at the repo root; the shell does not persist cd. @@ -77,14 +72,13 @@ help: ## type in the notes panel is bound to the position you typed it at. ## `make ground PLAYERS=2 SLUG=darvo-confusion` ## Read the notes back afterwards with `make trials`. -## play a session (VARIANT=ground-darvo-r0|h1|h2, SCENARIO=1|2|3|4, MODE=shared|common|coalitions) +## play a session (VARIANT=ground-darvo-r0|h1|h2) ground: - @echo " rules: $(VARIANT) scenario: $(SCENARIO) scoring: $(MODE)" + @echo " rules: $(VARIANT)" @mkdir -p $(REPO)/trials @echo " trial: trials/$(TRIAL_NAME).md (notes) + .yaml (the game)" $(IN_REPO) $(CARGO) run -q -p cb-play -- \ --players $(PLAYERS) --serve $(PORT) --variant $(VARIANT) \ - --scenario $(SCENARIO) --mode $(MODE) \ --record trials/$(TRIAL_NAME).yaml \ --trial trials/$(TRIAL_NAME).md $(ARGS) @@ -251,7 +245,6 @@ panels: @cargo run --release -q -p games-ground --example regulation @cargo run --release -q -p games-ground --example perfect-recall @cargo run --release -q -p games-ground --example h2-panel - @cargo run --release -q -p games-ground --example scenario-panel # CB-WP-0022 T05: the design-finding register, reported over # specs/GroundRules.md. Shows the QUEUE by default; the log of closed diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 3337f23..6002ba4 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -53,7 +53,6 @@ | workplan | CB-WP-0043 | done | — | workplans/CB-WP-0043-a-problem-sits-where-its-stress-lands.md | | workplan | CB-WP-0044 | done | — | workplans/CB-WP-0044-the-page-says-which-rules-it-plays.md | | workplan | CB-WP-0045 | done | — | workplans/CB-WP-0045-the-table-names-what-it-shows.md | -| workplan | CB-WP-0046 | done | — | workplans/CB-WP-0046-the-rule-the-placement-encodes.md | | task | CB-WP-0001-T01 | done | — | workplans/CB-WP-0001-inner-loop.md | | task | CB-WP-0001-T02 | done | — | workplans/CB-WP-0001-inner-loop.md | | task | CB-WP-0001-T03 | done | — | workplans/CB-WP-0001-inner-loop.md | @@ -246,4 +245,3 @@ | task | CB-WP-0043-T01 | done | — | workplans/CB-WP-0043-a-problem-sits-where-its-stress-lands.md | | task | CB-WP-0044-T01 | done | — | workplans/CB-WP-0044-the-page-says-which-rules-it-plays.md | | task | CB-WP-0045-T01 | done | — | workplans/CB-WP-0045-the-table-names-what-it-shows.md | -| task | CB-WP-0046-T01 | done | — | workplans/CB-WP-0046-the-rule-the-placement-encodes.md | diff --git a/crates/cb-render-html/src/doc.rs b/crates/cb-render-html/src/doc.rs index d5d3456..bb168b0 100644 --- a/crates/cb-render-html/src/doc.rs +++ b/crates/cb-render-html/src/doc.rs @@ -1069,22 +1069,13 @@ pub fn document_with_log( s, "

GROUND \u{2014} round {round}, step {step:?}

\
lead {lead} \ - scoring {mode} \ + scoring {mode:?} \ rules {variant} \ - scenario {scenario} \ viewing as {who}
", round = view.round, step = view.step, lead = seat_name(view.lead), - // 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)), + mode = view.mode, // 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 @@ -1315,46 +1306,6 @@ fn body(s: &mut String, view: &GroundView) { s.push_str("
no problems in play
"); } 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, - "
{} \u{2014} what happened\ - {}
", - esc(&scenario_title(&view.scenario)), - esc(&premise) - ); - } - if let Some((rules, tiebreak)) = mode_rules_text(view.mode) { - let _ = write!( - s, - "
{} \u{2014} how this game is won\ - {}", - 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, - "
tiebreak {}", - esc(&tiebreak) - ); - } - s.push_str("
"); - } // 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 @@ -1572,59 +1523,6 @@ fn stress_scope_rule_text() -> Option { 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 { - 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; diff --git a/crates/cb-render-html/src/lib.rs b/crates/cb-render-html/src/lib.rs index d5f92b2..7e316f4 100644 --- a/crates/cb-render-html/src/lib.rs +++ b/crates/cb-render-html/src/lib.rs @@ -135,12 +135,7 @@ mod coverage { ("round", "round 3"), ("lead", "lead P2"), ("step", "step Select"), - // 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"), + ("mode", "scoring BondedCoalitions"), ("viewer", "viewing as P1"), ("solution_deck_len", "draw pile: 17 remaining"), ("solution_discard.*.suit", "discard Repair Change"), @@ -193,11 +188,6 @@ 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"), @@ -707,79 +697,6 @@ 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 diff --git a/crates/cb-render-html/src/testfix.rs b/crates/cb-render-html/src/testfix.rs index ca6d8de..4610eab 100644 --- a/crates/cb-render-html/src/testfix.rs +++ b/crates/cb-render-html/src/testfix.rs @@ -76,7 +76,6 @@ pub fn view(viewer: Option) -> GroundView { lead: p2, step: RoundStep::Select, mode: ScoringMode::BondedCoalitions, - scenario: "SCN_03".into(), players, relations: BTreeMap::from([ (Pair::new(p1, p2), Relation::Rivalry), diff --git a/editions/catalog.yaml b/editions/catalog.yaml index c14c74a..4cfcf54 100644 --- a/editions/catalog.yaml +++ b/editions/catalog.yaml @@ -1,252 +1,98 @@ -# GROUND edition catalog — schema 2: composable modules -# Docs: CATALOG.md -# clay-borg: select baseline + 0..N modules (≤1 per axis), or a named profile. +# Selectable GROUND edition / rules packages. +# Schema docs: CATALOG.md +# clay-borg: pin by variant_id → path (+ optional git_pin). -schema_version: 2 +schema_version: 1 updated: "2026-08-08" -default_baseline: ground-darvo-r0 -default_profile: baseline +default_variant: ground-darvo-r0 +# H2 added; H1 remains selectable as reject-as-baseline control. -# --------------------------------------------------------------------------- -# Axes — orthogonal dimensions. At most one non-default module per axis. -# --------------------------------------------------------------------------- -axes: - - id: problem_stress - title: Problem → Stress routing - default_module: problem_stress.none - summary: > - Whether and how unclaimed Problems raise Stress at Round End. - - - id: attack_relief - title: ATTACK self-soothing - default_module: attack_relief.none - summary: > - Whether resolving ATTACK can reduce the attacker's Stress. - - - id: end_condition - title: How the game ends - default_module: end_condition.fixed_rounds_5 - summary: > - Fixed round clock vs clear-board / collapse / hybrid ends. - - - id: problem_deal - title: How Problems enter play - default_module: problem_deal.fixed_setup - summary: > - Fixed setup deal only vs mid-game influx (pressure deck, etc.). - -# Future axes (not yet registered): setup_difficulty, sequence_pacing, -# ground_as_sequence, status_stress, competence_track. - -# --------------------------------------------------------------------------- -# Baseline content package (CSV edition data) -# --------------------------------------------------------------------------- -baselines: - - baseline_id: ground-darvo-r0 +packages: + - variant_id: ground-darvo-r0 path: editions/ground-darvo-r0 + kind: baseline + base: null selectable: true status: baseline dataset_id: GROUND-DARVO-CORE-0.1 title: "GROUND DARVO Edition — core r0" summary: > - Print/playtest content. Modes, deal 6/9/12, thresholds 5/7/9. - Default modules on all axes = r0 printed behaviour. - utility_estimate: > - Ship-default content. Stress has no problem pressure until a - problem_stress module is selected. - decision: none - -# --------------------------------------------------------------------------- -# Modules — independent variations (one directory each) -# --------------------------------------------------------------------------- -modules: - # --- problem_stress --- - - module_id: problem_stress.none - axis: problem_stress - path: editions/modules/problem_stress/none - is_default: true - selectable: true - status: baseline-default + Playtest baseline. Surface + hidden 1..k deal; available 6/9/12; + thresholds 5/7/9. Stress ledger has no problem pressure; ATTACK + does not self-soothe. + hypothesis_ref: null + workplan_ref: null rules_delta: null - summary: Unclaimed Problems do not raise Stress (r0). + changed_files: [] + utility_estimate: > + Ship-default. RPT-0003 + CB-EV-0031: under non-attacking competent + policies, peak Stress held never exceeds start 2 — ATTACK is the + sole inbound pressure. Keep pinned until a successor is accepted. decision: none - - - module_id: problem_stress.flat_any_open - axis: problem_stress - path: editions/modules/problem_stress/flat_any_open - is_default: false - selectable: true - status: measured - rules_delta: editions/modules/problem_stress/flat_any_open/rules_delta.yaml - legacy_experiment_ids: [h1-problem-stress] - measurement_ref: reports/260808-clay-borg-h1-measured.md - summary: > - +1 Stress to every seat if any Problem unclaimed (former H1-A). - utility_estimate: > - Reject as sole pressure: greedy 3–4p wins → 0. Keep for A/B control. - decision: reject-as-baseline - clay_borg_notes: Former H1-A; implement without H1-B unless attack_relief also selected. - - - module_id: problem_stress.scoped - axis: problem_stress - path: editions/modules/problem_stress/scoped - is_default: false - selectable: true - status: measured - rules_delta: editions/modules/problem_stress/scoped/rules_delta.yaml - data_overlays: [Problems.csv, Rules_Text.csv] - legacy_experiment_ids: [h2-scoped-problem-stress] - measurement_ref: reports/260808-clay-borg-h2-measured.md - hypothesis_ref: history/260808-h2-scoped-problem-stress.md - summary: > - +1 Stress per unclaimed Problem to stress_scope (personal/bond/global). - utility_estimate: > - Scoping works (RPT-0005). Keep; candidate promote with other axes later. - decision: keep-as-experiment + git_pin: null clay_borg_notes: > - Prefer path editions/modules/problem_stress/scoped. Use with_variant()/owners. - legacy_experiment_id h2-scoped-problem-stress remains an alias profile. + Vendor CSVs as today (Problems, Actions, Solutions, Modes, Tokens). + Kernel implements printed r0 rules. - # --- attack_relief --- - - module_id: attack_relief.none - axis: attack_relief - path: editions/modules/attack_relief/none - is_default: true - selectable: true - status: baseline-default - rules_delta: null - summary: ATTACK does not self-soothe (r0). - decision: none - - - module_id: attack_relief.self_soothe_ge4 - axis: attack_relief - path: editions/modules/attack_relief/self_soothe_ge4 - is_default: false - selectable: true - status: measured-as-combo - rules_delta: editions/modules/attack_relief/self_soothe_ge4/rules_delta.yaml - legacy_experiment_ids: [h1-problem-stress] - measurement_ref: reports/260808-clay-borg-h1-measured.md - summary: Uncancelled ATTACK at Stress ≥4 → attacker −1 Stress (former H1-B). - utility_estimate: > - Alone unmeasured. With flat problem stress, dead for competent play. - Re-measure with problem_stress.scoped via profile scoped_plus_attack_soothe. - decision: none - clay_borg_notes: Independent of problem_stress; compose explicitly. - - # --- end_condition --- - - module_id: end_condition.fixed_rounds_5 - axis: end_condition - path: editions/modules/end_condition/fixed_rounds_5 - is_default: true - selectable: true - status: baseline-default - rules_delta: null - summary: Always 5 rounds then threshold scoring (r0). - decision: none - - - module_id: end_condition.hybrid_clear_collapse - axis: end_condition - path: editions/modules/end_condition/hybrid_clear_collapse - is_default: false - selectable: true - status: proposed - rules_delta: editions/modules/end_condition/hybrid_clear_collapse/rules_delta.yaml - hypothesis_ref: history/260808-deal-end-sequences-design.md - summary: > - End on board clear, group collapse, or round ceiling (draft rules). - utility_estimate: Unimplemented — draft only. - decision: none - clay_borg_notes: Kernel pending; do not claim measured until implemented. - - # --- problem_deal --- - - module_id: problem_deal.fixed_setup - axis: problem_deal - path: editions/modules/problem_deal/fixed_setup - is_default: true - selectable: true - status: baseline-default - rules_delta: null - summary: Surface + hidden 1..k at setup only (r0). - decision: none - - - module_id: problem_deal.pressure_deck - axis: problem_deal - path: editions/modules/problem_deal/pressure_deck - is_default: false - selectable: true - status: proposed - rules_delta: editions/modules/problem_deal/pressure_deck/rules_delta.yaml - hypothesis_ref: history/260808-deal-end-sequences-design.md - summary: > - Small start set + mid-game draws from Pressure deck (draft). - utility_estimate: Unimplemented — draft only. - decision: none - clay_borg_notes: > - Kernel pending. Recommended compose with problem_stress.scoped. - v0 draft uses 0-point drawn cards. - -# --------------------------------------------------------------------------- -# Profiles — named compositions (convenience; not a second rules source) -# Selection = baseline + modules list. Defaults fill missing axes. -# --------------------------------------------------------------------------- -profiles: - - profile_id: baseline - title: Pure r0 - modules: [] - summary: All axis defaults — printed ground-darvo-r0 behaviour. - - - profile_id: h1 - title: Legacy H1 (flat problem stress + attack soothe) - modules: - - problem_stress.flat_any_open - - attack_relief.self_soothe_ge4 - legacy_experiment_id: h1-problem-stress - summary: Equivalent to old monolithic experiment h1-problem-stress. - decision: reject-as-baseline - - - profile_id: h2 - title: Legacy H2 (scoped problem stress only) - modules: - - problem_stress.scoped - legacy_experiment_id: h2-scoped-problem-stress - summary: Equivalent to old monolithic experiment h2-scoped-problem-stress. - decision: keep-as-experiment - - - profile_id: scoped_plus_attack_soothe - title: Scoped stress + ATTACK self-soothe - modules: - - problem_stress.scoped - - attack_relief.self_soothe_ge4 - summary: First intentional multi-axis combo after modular catalog. - status: unmeasured - - - profile_id: scoped_plus_hybrid_end - title: Scoped stress + hybrid end (when end module ships) - modules: - - problem_stress.scoped - - end_condition.hybrid_clear_collapse - summary: Requires end_condition.hybrid_clear_collapse kernel support. - status: proposed - - - profile_id: scoped_plus_pressure_deck - title: Scoped stress + pressure deck (when deal module ships) - modules: - - problem_stress.scoped - - problem_deal.pressure_deck - summary: Requires problem_deal.pressure_deck kernel support. - status: proposed - -# --------------------------------------------------------------------------- -# Legacy experiment paths (still on disk; prefer modules + profiles) -# --------------------------------------------------------------------------- -legacy_experiments: - - experiment_id: h1-problem-stress + - variant_id: h1-problem-stress path: editions/experiments/h1-problem-stress - equivalent_profile: h1 - note: Prefer profile h1 or modules problem_stress.flat_any_open + attack_relief.self_soothe_ge4 + kind: experiment + base: ground-darvo-r0 + selectable: true + status: measured + dataset_id: GROUND-DARVO-EXP-H1-0.1 + title: "H1 — problem pressure + high-stress ATTACK self-soothe" + summary: > + Two deltas only: (A) +1 Stress each player at Round End if any + Problem remains unclaimed; (B) uncancelled ACT_ATTACK by a seat + at Stress ≥4 gives that seat −1 Stress. Thresholds/deal unchanged. + hypothesis_ref: history/260807-attack-darvo-stress-design.md + workplan_ref: workplans/GROUND-WP-0006-h1-problem-stress-experiment.md + measurement_ref: reports/260808-clay-borg-h1-measured.md + rules_delta: editions/experiments/h1-problem-stress/rules_delta.yaml + changed_files: + - Actions.csv + - Rules_Text.csv + - metadata.json + utility_estimate: > + Measured 2026-08-08 (CB-EV-0030/0031; instrument caveat applies). + Direction right, magnitude wrong: H1-A is a solve-rate tax that + competent seats absorb with GROUND (Stress ~3); H1-B never fires + for them. DARVO arms for unregulated seats; group wins collapse + to 0 at 3p+ under greedy SHARED GROUND. Do not promote as-is. + decision: reject-as-baseline + # Package stays selectable for regression compare; not a ship pin. + git_pin: null + clay_borg_notes: > + Kernel implements H1-A/H1-B. Keep for A/B against successors (H2…). - - experiment_id: h2-scoped-problem-stress + - variant_id: h2-scoped-problem-stress path: editions/experiments/h2-scoped-problem-stress - equivalent_profile: h2 - note: Prefer profile h2 or module problem_stress.scoped + kind: experiment + base: ground-darvo-r0 + selectable: true + status: experimental + dataset_id: GROUND-DARVO-EXP-H2-0.1 + title: "H2 — scoped problem stress (personal / bond / global)" + summary: > + Unclaimed Problems apply +1 End Stress only to stress_scope: + personal=owner, bond=owner's Bond network, global=all. Surface + global; priority 3 is bond (in play at 3p+). Anyone may SOLVE any + card. Not stacked on H1; no ATTACK self-soothe. + hypothesis_ref: history/260808-h2-scoped-problem-stress.md + workplan_ref: workplans/GROUND-WP-0007-h2-scoped-problem-stress.md + measurement_ref: null + rules_delta: editions/experiments/h2-scoped-problem-stress/rules_delta.yaml + changed_files: + - Problems.csv + - Rules_Text.csv + - metadata.json + utility_estimate: > + Unmeasured. Expected: stress variance up; group wins at 3–4p much + better than H1; bond cards create joint SOLVE incentive in networks. + decision: none + git_pin: null + clay_borg_notes: > + Implement rules_delta H2-SCOPE, H2-OWN, H2-A, H2-SOLVE on base r0. + Do not also apply H1 deltas. Problems.csv adds stress_scope column. + Report bond-card SOLVE rates and stress variance vs r0 and h1. diff --git a/editions/experiments/h1-problem-stress/VARIANT.md b/editions/experiments/h1-problem-stress/VARIANT.md index 01fc4f5..f4b4b02 100644 --- a/editions/experiments/h1-problem-stress/VARIANT.md +++ b/editions/experiments/h1-problem-stress/VARIANT.md @@ -2,8 +2,7 @@ | | | |---|---| -| **variant_id** | `h1-problem-stress` (**legacy** — prefer profile `h1` = `problem_stress.flat_any_open` + `attack_relief.self_soothe_ge4`) | -| **modules** | flat problem stress + attack self-soothe (see `editions/modules/`) | +| **variant_id** | `h1-problem-stress` | | **base** | `ground-darvo-r0` | | **status** | measured — reject as baseline (2026-08-08); keep for A/B | | **measurement** | [`../../../reports/260808-clay-borg-h1-measured.md`](../../../reports/260808-clay-borg-h1-measured.md) | diff --git a/editions/experiments/h2-scoped-problem-stress/VARIANT.md b/editions/experiments/h2-scoped-problem-stress/VARIANT.md index f96d5bf..ce5b9bd 100644 --- a/editions/experiments/h2-scoped-problem-stress/VARIANT.md +++ b/editions/experiments/h2-scoped-problem-stress/VARIANT.md @@ -2,10 +2,9 @@ | | | |---|---| -| **variant_id** | `h2-scoped-problem-stress` (**legacy** — prefer module `problem_stress.scoped` / profile `h2`) | -| **module** | [`../../modules/problem_stress/scoped/`](../../modules/problem_stress/scoped/) | +| **variant_id** | `h2-scoped-problem-stress` | | **base** | `ground-darvo-r0` (not stacked on H1) | -| **status** | measured — keep-as-experiment | +| **status** | experimental | | **catalog** | [`../../catalog.yaml`](../../catalog.yaml) | | **design note** | [`../../../history/260808-h2-scoped-problem-stress.md`](../../../history/260808-h2-scoped-problem-stress.md) | | **workplan** | [`../../../workplans/GROUND-WP-0007-h2-scoped-problem-stress.md`](../../../workplans/GROUND-WP-0007-h2-scoped-problem-stress.md) | diff --git a/editions/ground-darvo-r0/PROVENANCE.md b/editions/ground-darvo-r0/PROVENANCE.md index 72d1dee..e195650 100644 --- a/editions/ground-darvo-r0/PROVENANCE.md +++ b/editions/ground-darvo-r0/PROVENANCE.md @@ -58,16 +58,16 @@ the adversarial review ([CB-REV-0001](../../reviews/CB-REV-0001-h1.md)) reported it unverified and it was a real gap. ``` -sha256 cd0c0db9eef5e9f94df6c1b26837c5e800d9eb8c79ee113a537767aa88934c76 ../catalog.yaml +sha256 c469f984c3147861815e8fbd67cf1254de73368fad1654f4177f740c0c280499 ../catalog.yaml sha256 4c81bae21d2ecb70c7424fa17445246a9b551b10e258d448634816c564e28f09 ../experiments/h2-scoped-problem-stress/Problems.csv sha256 8ed8deb7ad142de1bda70dc11add339f742e8c12ac1f02d5f7c7ec8f17f77b1d ../experiments/h2-scoped-problem-stress/Rules_Text.csv -sha256 b0ffea41820ef35960d8c75ff0a734370a29a738e49fda74851fe5c2daafe53e ../experiments/h2-scoped-problem-stress/VARIANT.md +sha256 b2db91edc6798efca1ccf0b048bb0d7cb324e808e01d636ebca813527bcd0db2 ../experiments/h2-scoped-problem-stress/VARIANT.md sha256 abf994f585fdfa9b2822614a7961e98141cded6a91c446916ff7fd48642de0a7 ../experiments/h2-scoped-problem-stress/metadata.json sha256 8dc569b2ae62f88f7f64e282bc6bba3dbcc3ffcad276591baafbdd24cb6c16b7 ../experiments/h2-scoped-problem-stress/rules_delta.yaml sha256 f58e81f84ea2b0d16e39932261eb3f3d9890345cdf37ad6f0b3abc00636840be ../experiments/h1-problem-stress/rules_delta.yaml sha256 7b1cc0149122b855e827bc930576ed165bf7dd8d62707e845a9e514ce3521f8e ../experiments/h1-problem-stress/Actions.csv sha256 62785f5e7e245c60171624d15de2f40187a44fec54f93c7d9705cf52584b1078 ../experiments/h1-problem-stress/Rules_Text.csv -sha256 49897a68056643a8cfccff32c9e4a9811018b4e4689b90a85124a487f2318369 ../experiments/h1-problem-stress/VARIANT.md +sha256 b2714210c142f1d6d5bed8f9a795019e51829c627f9599ac0ebfa04ef60a37a5 ../experiments/h1-problem-stress/VARIANT.md sha256 443199db94601cc889557e5e86823f374dbfdf875962fc84f95c01865605101c ../experiments/h1-problem-stress/metadata.json ``` diff --git a/games/ground/examples/scenario-panel.rs b/games/ground/examples/scenario-panel.rs deleted file mode 100644 index ab8c0fa..0000000 --- a/games/ground/examples/scenario-panel.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! 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, 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, -} - -fn sweep(scenario: &str, mode: ScoringMode, players: u8) -> 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> = (0..players) - .map(|_| Box::new(GreedyPolicy) as Box) - .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} {:>7} {:>6} {:>9}", - "scenario", "mode", "won", "win/g", "pts", "threshold" - ); - for s in &scenarios { - for (label, mode) in &modes { - let c = sweep(&s.id, *mode, players); - println!( - " {:<32} {label} {:>5} {:>7.2} {:>6.1} {:>9}", - format!("{} {}", s.id, s.title), - c.group_success, - f64::from(c.winners) / f64::from(c.games), - f64::from(c.total) / f64::from(c.games), - c.threshold, - ); - } - } - println!(); - } - - 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."); -} diff --git a/games/ground/src/edition.rs b/games/ground/src/edition.rs index a487460..c942db7 100644 --- a/games/ground/src/edition.rs +++ b/games/ground/src/edition.rs @@ -474,11 +474,6 @@ pub fn darvo_stages() -> Result, String> { pub struct ScenarioText { pub id: String, pub title: String, - /// What the game is ABOUT, in the edition's words (CB-WP-0047). - /// - /// Vendored and unread until the four scenarios became selectable: - /// with one scenario hardcoded there was nothing to tell apart. - pub premise: String, pub surface_problem_id: String, pub hidden_problem_ids: Vec, /// GR-E01 by seat band, as the EDITION states it. The engine has its @@ -499,7 +494,6 @@ pub fn scenarios() -> Result, String> { out.push(ScenarioText { id: t.get(row, "scenario_id")?.to_string(), title: t.get(row, "title")?.to_string(), - premise: t.get(row, "premise")?.to_string(), surface_problem_id: t.get(row, "surface_problem_id")?.to_string(), hidden_problem_ids: t .get(row, "hidden_problem_ids")? @@ -1013,91 +1007,6 @@ mod card_text_tests { /// /// Same shape as F24's solution deck. `inert`, role `default`: green /// because they agree, red the moment either side moves. - /// **All four scenarios deal, and they are not the same board** - /// (CB-WP-0047). - /// - /// `setup` passed the literal `"SCN_01"`, so 15 of the 20 Problem - /// cards had never been dealt by anything. This deals every scenario - /// at every seat band and holds the deal to the Scenario card. - #[test] - fn every_scenario_deals_the_board_its_card_states() { - for s in scenarios().expect("Scenarios.csv") { - for (players, k) in [(2u8, 2usize), (4, 3), (6, 4)] { - let dealt = - deal(&s.id, players).unwrap_or_else(|e| panic!("{} at {players}p: {e}", s.id)); - assert_eq!( - dealt.len(), - k + 1, - "{} at {players}p: Surface + {k} hidden is {} cards", - s.id, - k + 1 - ); - assert_eq!( - dealt.iter().filter(|p| p.surface).count(), - 1, - "{}: exactly one Surface Problem is dealt face up", - s.id - ); - // The card states the available total; the deal must be - // able to reach the threshold or the board is unwinnable. - let available: u32 = dealt.iter().map(|p| u32::from(p.value)).sum(); - let threshold = match players { - 0..=2 => s.thresholds.0, - 3..=4 => s.thresholds.1, - _ => s.thresholds.2, - }; - assert!( - available >= threshold, - "{} at {players}p: {available} points available against a \ - threshold of {threshold} — the group cannot win in principle", - s.id - ); - } - } - } - - /// **Two of the four scenarios are the same board** (CB-WP-0047). - /// - /// SCN_01 and SCN_02 have identical suit-and-value profiles at every - /// priority, so they play identically and differ only in prose. Not - /// a defect — a designed reskin is a legitimate choice — but it is a - /// fact about what "four scenarios" buys, and measuring them as four - /// independent boards would be measuring two of them twice. - /// - /// This is a **characterisation** test: it pins what is true today so - /// that a change to either deck is a decision rather than a drift. - #[test] - fn which_scenarios_are_mechanically_distinct() { - let profile = |id: &str| -> Vec<(u8, String, u8)> { - let mut v: Vec<_> = problems_of(id) - .expect(id) - .into_iter() - .map(|p| (p.priority, format!("{:?}", p.suit), p.value)) - .collect(); - v.sort(); - v - }; - assert_eq!( - profile("SCN_01"), - profile("SCN_02"), - "SCN_01 and SCN_02 diverged — they were identical boards; \ - if this is intended, the panel now measures four real boards" - ); - for pair in [ - ("SCN_01", "SCN_03"), - ("SCN_01", "SCN_04"), - ("SCN_03", "SCN_04"), - ] { - assert_ne!( - profile(pair.0), - profile(pair.1), - "{} and {} became the same board", - pair.0, - pair.1 - ); - } - } - #[test] fn the_engine_agrees_with_the_editions_own_numbers() { for s in scenarios().expect("Scenarios.csv") { diff --git a/games/ground/src/lib.rs b/games/ground/src/lib.rs index 7a55309..d785e83 100644 --- a/games/ground/src/lib.rs +++ b/games/ground/src/lib.rs @@ -200,21 +200,6 @@ pub struct GroundState { /// variants existed still loads, as baseline — which is what it was. #[serde(default)] pub variant: Variant, - /// Which of the edition's four Scenarios is on the table - /// (CB-WP-0047). - /// - /// **The engine dealt `SCN_01` and only `SCN_01`** for the whole life - /// of this repo — the other three decks were vendored, gated, and - /// never played. Fifteen of the twenty Problem cards had never - /// reached a table. - /// - /// In the state for the same reason `variant` is: the threshold and - /// the whole board follow from it, so a recording that did not carry - /// it could not be replayed. `#[serde(default)]` returns `SCN_01`, - /// which is what every recording written before this field existed - /// actually played. - #[serde(default = "default_scenario")] - pub scenario: String, /// GR-R09: set once the game has ended and scoring has run. pub outcome: Option, /// GR-S04/U4: retained so a deck reshuffle stays a pure function of @@ -222,53 +207,6 @@ pub struct GroundState { pub seed: u64, } -/// GR-E01 by seat band, as the engine used to hardcode it. -/// -/// **Kept as the fallback, and only as the fallback.** All four -/// scenarios print 5/7/9, so this and the edition agree today — which is -/// exactly why a test comparing the two numbers proves nothing. See -/// `threshold_from`. -fn seat_band_threshold(seats: usize) -> u32 { - match seats { - 0..=2 => 5, - 3..=4 => 7, - _ => 9, - } -} - -/// The threshold a Scenario card states, or the seat band if the edition -/// does not have that card (CB-WP-0047). -/// -/// **Split out so it can be tested against a card that disagrees.** The -/// first version read the edition inline, and mutating it back to the -/// hardcoded bands left every test green — because all four scenarios -/// print 5/7/9 and the two paths are observationally identical on every -/// input the edition can supply. A control that cannot separate the -/// thing it is about from its fallback is not a control; taking a list -/// as an argument lets one be written. -#[cfg(feature = "scenarios")] -fn threshold_from(list: &[crate::edition::ScenarioText], scenario: &str, seats: usize) -> u32 { - match list.iter().find(|s| s.id == scenario) { - Some(s) => { - let (two, three_four, five_six) = s.thresholds; - match seats { - 0..=2 => two, - 3..=4 => three_four, - _ => five_six, - } - } - None => seat_band_threshold(seats), - } -} - -/// The scenario every recording written before CB-WP-0047 played. -/// -/// **Not "the first scenario" — the one that was actually dealt.** The -/// distinction matters if the edition ever reorders `Scenarios.csv`. -fn default_scenario() -> String { - "SCN_01".to_string() -} - /// GR-E02..E04: the three scoring modes. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ScoringMode { @@ -1793,31 +1731,13 @@ impl GroundState { events } - /// GR-E01: the Scenario threshold, **off the Scenario card**. - /// - /// This was `match seats { 0..=2 => 5, 3..=4 => 7, _ => 9 }` — the - /// engine's own copy of a number four cards already print, which is - /// exactly what F25 was raised about. It was right only because all - /// four scenarios happen to agree, and a fifth scenario with a - /// different threshold would have been scored against the old one - /// with nothing to say so. - /// - /// The bands survive as the fallback for a state whose scenario the - /// edition cannot supply. **That path is unreachable through - /// `setup`**, which refuses an unknown scenario before dealing, and - /// a test holds it unreachable — a fallback nothing can reach is the - /// only kind that cannot silently answer for the real one. - #[cfg(feature = "scenarios")] + /// GR-E01: the Scenario threshold by player count (dataset 0.1). fn threshold(&self) -> u32 { - let list = crate::edition::scenarios().unwrap_or_default(); - threshold_from(&list, &self.scenario, self.players.len()) - } - - /// Without the `scenarios` feature there is no edition to read, so - /// the seat bands are the whole answer rather than a fallback. - #[cfg(not(feature = "scenarios"))] - fn threshold(&self) -> u32 { - seat_band_threshold(self.players.len()) + match self.players.len() { + 0..=2 => 5, + 3..=4 => 7, + _ => 9, + } } /// GR-E01..E04: final scoring for the configured mode. @@ -2135,61 +2055,6 @@ impl GroundState { // GR-S04's deck now lives in `edition::solution_deck` (ADR-0011), // beside the Problem data it is dealt against. -/// Which Scenario a `Setup` preset names, and for how many seats -/// (CB-WP-0047). -/// -/// | preset | scenario | -/// |---|---| -/// | `standard-4p` | `SCN_01` | -/// | `scn-03-4p` | `SCN_03` | -/// -/// **`standard-Np` keeps meaning exactly what it meant**, which is not a -/// convenience: twenty-six recorded scenarios name it, and a grammar -/// that redefined it would have moved every one of their boards while -/// their hashes still claimed to pin them (ADR-0019's discipline, and -/// the reason `serde(default)` on the field returns `SCN_01`). -/// -/// The seat count is checked here rather than after the deal, because -/// `deal` refuses an unknown scenario and a preset naming the wrong seat -/// count would otherwise be reported as a scenario problem. -#[cfg(feature = "scenarios")] -fn parse_preset(preset: &str, seats: u8) -> Result { - let suffix = format!("-{seats}p"); - let Some(head) = preset.strip_suffix(&suffix) else { - return Err(format!( - "preset {preset:?} does not match {seats} players \ - (expected {:?} or e.g. {:?})", - format!("standard{suffix}"), - format!("scn-02{suffix}"), - )); - }; - let id = match head { - "standard" => default_scenario(), - other => { - let n = other.strip_prefix("scn-").ok_or_else(|| { - format!("preset {preset:?}: expected \"standard\" or \"scn-0N\", got {other:?}") - })?; - format!("SCN_{n}") - } - }; - // **Checked against the edition, not against a pattern.** `SCN_09` - // matches the shape and is not a scenario; dealing it would fail - // later with a message about Problems rather than about the preset. - let known = crate::edition::scenarios()?; - if !known.iter().any(|s| s.id == id) { - return Err(format!( - "preset {preset:?} names {id}, which the edition does not have \ - (it has {})", - known - .iter() - .map(|s| s.id.as_str()) - .collect::>() - .join(", ") - )); - } - Ok(id) -} - #[cfg(feature = "scenarios")] impl ScenarioGame for GroundState { /// GR-S01..S04. The `standard-Np` presets differ only in seat count; @@ -2198,13 +2063,17 @@ impl ScenarioGame for GroundState { /// modelled. fn setup(setup: &Setup, seed: u64) -> Result { let seats = setup.players; - let scenario = parse_preset(&setup.preset, seats)?; + let expected = format!("standard-{seats}p"); + if setup.preset != expected { + return Err(format!( + "preset {:?} does not match {seats} players (expected {expected:?})", + setup.preset + )); + } // GR-S01 as ruled 2026-08-04: Surface always, plus hidden // priorities 1..=k. The values and suits come from the edition - // (ADR-0011); the engine used to invent both. **And now the - // scenario does too** — this argument was the literal `"SCN_01"` - // for the whole life of the repo (CB-WP-0047). - let dealt = crate::edition::deal(&scenario, seats)?; + // (ADR-0011); the engine used to invent both. + let dealt = crate::edition::deal("SCN_01", seats)?; let mut rng = ChaChaRng::from_seed(Seed(seed)); // GR-S04: shuffle first, then deal, so the deal is seed-derived. @@ -2270,7 +2139,6 @@ impl ScenarioGame for GroundState { support_responses: BTreeMap::new(), darvo_targets: BTreeMap::new(), mode: ScoringMode::SharedGround, - scenario, // Baseline. The driver overwrites this after setup and // before the hash is taken, which is the route `mode` uses // (`table.rs`) — so a recorded session replays under the @@ -3475,7 +3343,6 @@ mod tests { darvo_targets: BTreeMap::new(), mode: ScoringMode::SharedGround, variant: Variant::Baseline, - scenario: default_scenario(), outcome: None, seed: 0, } @@ -3513,204 +3380,6 @@ mod tests { .unwrap() } - /// **The mastery rating counts cards; the Mode card counts points** - /// (F28, CB-WP-0047). - /// - /// MODE_COOP reads: *"All claimed Problem cards form one shared - /// score. … For a mastery rating, subtract 1 for each Blame token - /// still in play and 1 for each Denied Problem."* - /// - /// The shared score is the claimed **value** — `total`, which is what - /// the threshold is compared against two lines earlier. `mastery` - /// subtracts the same penalties from the claimed **count**. Both - /// readings fit the sentence; they disagree on every game where a - /// 3-point Problem is claimed, which is most of them. - /// - /// **A characterisation test, not a correction.** Scoring is - /// `ground-game`'s to rule on (ADR-0015). This pins what the engine - /// does today and shows the size of the disagreement, so the ruling - /// has a number in front of it. - #[test] - fn the_mastery_rating_and_the_shared_score_count_different_things() { - let mut s = setup_3p(11); - s.mode = ScoringMode::SharedGround; - // Two claimed Problems worth 2 and 3 — five points, two cards. - let seats: Vec = s.players.keys().copied().collect(); - s.problems.insert( - 1, - ProblemState { - suit: Suit::Repair, - value: 2, - face_up: true, - denied: false, - claimed_by: Some(seats[0]), - protected_this_round: false, - owner: None, - scope: None, - }, - ); - s.problems.insert( - 2, - ProblemState { - suit: Suit::Change, - value: 3, - face_up: true, - denied: false, - claimed_by: Some(seats[1]), - protected_this_round: false, - owner: None, - scope: None, - }, - ); - let o = s.score(); - assert_eq!(o.total, 5, "the shared score is claimed VALUE"); - assert_eq!( - o.mastery, - Some(2), - "mastery is the claimed COUNT, with no penalties in play" - ); - // The disagreement, stated as a number rather than as a worry. - assert_ne!( - i32::try_from(o.total).unwrap(), - o.mastery.unwrap(), - "the two readings agree on this board, so F28 has no bite here \ - and the example needs replacing" - ); - } - - /// **Every scenario is reachable through `setup`** (CB-WP-0047). - /// - /// `setup` passed the literal `"SCN_01"`. `deal` had taken a - /// scenario id since the day it was written and no caller ever - /// passed a different one — the parameter was the whole seam and it - /// sat unused, which is why three decks went unplayed without any - /// gate noticing. - #[test] - fn every_scenario_can_be_set_up_and_carries_its_own_board() { - use cb_game_runtime::{ScenarioGame, Setup}; - let mut boards = std::collections::BTreeSet::new(); - for s in crate::edition::scenarios().expect("Scenarios.csv") { - let preset = if s.id == "SCN_01" { - "standard-4p".to_string() - } else { - format!("scn-{}-4p", s.id.rsplit('_').next().unwrap()) - }; - let state = GroundState::setup( - &Setup { - players: 4, - preset: preset.clone(), - patch: Default::default(), - }, - 7, - ) - .unwrap_or_else(|e| panic!("{preset}: {e}")); - assert_eq!(state.scenario, s.id, "{preset} dealt the wrong scenario"); - boards.insert( - state - .problems - .values() - .map(|p| (format!("{:?}", p.suit), p.value)) - .collect::>(), - ); - } - // SCN_01 and SCN_02 are the same board by design, so four - // scenarios yield THREE distinct boards. Asserting 4 here would - // be asserting a fact about the edition that is not true. - assert_eq!( - boards.len(), - 3, - "the four scenarios yield {} distinct boards; SCN_01 and SCN_02 \ - were identical and the other two differ", - boards.len() - ); - } - - /// The preset grammar, including what it must keep meaning. - #[test] - fn the_preset_names_a_scenario_and_a_seat_count() { - assert_eq!(parse_preset("standard-4p", 4).unwrap(), "SCN_01"); - assert_eq!(parse_preset("scn-03-6p", 6).unwrap(), "SCN_03"); - // Twenty-six recordings name `standard-Np`; if this ever stopped - // meaning SCN_01 every one of them would replay a different board - // while its hash still claimed to pin the old one. - assert_eq!(parse_preset("standard-2p", 2).unwrap(), "SCN_01"); - // A preset that looks like an id but names no card is refused - // HERE, so the error is about the preset and not about Problems. - assert!(parse_preset("scn-09-4p", 4).is_err()); - assert!(parse_preset("standard-4p", 3).is_err()); - assert!(parse_preset("nonsense-4p", 4).is_err()); - } - - /// **The threshold comes off the Scenario card** (CB-WP-0047, F25). - #[test] - fn the_threshold_is_the_editions_and_the_fallback_is_unreachable() { - use cb_game_runtime::{ScenarioGame, Setup}; - for s in crate::edition::scenarios().expect("Scenarios.csv") { - for (players, want) in [ - (2u8, s.thresholds.0), - (4, s.thresholds.1), - (6, s.thresholds.2), - ] { - let preset = if s.id == "SCN_01" { - format!("standard-{players}p") - } else { - format!("scn-{}-{players}p", s.id.rsplit('_').next().unwrap()) - }; - let state = GroundState::setup( - &Setup { - players, - preset, - patch: Default::default(), - }, - 3, - ) - .unwrap(); - assert_eq!( - state.threshold(), - want, - "{} at {players}p: the engine scored against its own number", - s.id - ); - } - } - // **The real control.** Every shipped scenario prints 5/7/9, so - // the loop above passes whether the number came off the card or - // off the old hardcoded bands — verified by mutation: reverting - // to the bands left it green. This asks a card that DISAGREES. - let odd = crate::edition::ScenarioText { - id: "SCN_XX".into(), - title: "a card that disagrees".into(), - premise: String::new(), - surface_problem_id: String::new(), - hidden_problem_ids: vec![], - thresholds: (4, 6, 8), - starting_stress: String::new(), - round_track: String::new(), - }; - for (seats, want) in [(2usize, 4), (4, 6), (6, 8)] { - assert_eq!( - threshold_from(std::slice::from_ref(&odd), "SCN_XX", seats), - want, - "the threshold did not come off the Scenario card" - ); - } - // And the fallback is what answers for a card the edition lacks. - assert_eq!(threshold_from(&[], "SCN_XX", 4), 7); - - // That fallback is unreachable through `setup`, which refuses an - // unknown scenario before dealing. A fallback nothing can reach - // is the only kind that cannot silently answer for the real one. - assert!(GroundState::setup( - &Setup { - players: 4, - preset: "scn-09-4p".into(), - patch: Default::default(), - }, - 3 - ) - .is_err()); - } - /// GR-S02/S04: every seat starts at Stress 2 with two dealt cards, /// and the deck loses exactly what was dealt. #[test] diff --git a/games/ground/src/view.rs b/games/ground/src/view.rs index b8ea886..e5e8ac5 100644 --- a/games/ground/src/view.rs +++ b/games/ground/src/view.rs @@ -48,14 +48,6 @@ pub struct GroundView { /// change as "no changes" after playing the default. #[serde(default)] pub variant: crate::Variant, - /// Which Scenario is on the table (CB-WP-0047). - /// - /// The player was told the premise by nothing: the same four boards - /// would have arrived unannounced. Same reasoning as `variant` — - /// CB-WP-0044 shipped a rules change the page could not name and the - /// maintainer reported it as "no changes". - #[serde(default = "crate::default_scenario")] - pub scenario: String, pub players: BTreeMap, pub relations: BTreeMap, pub problems: BTreeMap, @@ -186,7 +178,6 @@ impl Project for GroundState { step: self.step, mode: self.mode, variant: self.variant, - scenario: self.scenario.clone(), players: self .players .iter() diff --git a/specs/FindingRegister.md b/specs/FindingRegister.md index e1bd41a..7e5c81c 100644 --- a/specs/FindingRegister.md +++ b/specs/FindingRegister.md @@ -54,36 +54,9 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by | F23 | inconsistent | applied | decisions/ADR-0017-chaos-window-2-verdict.md | counterexample | 2026-08-07 | clay-borg | | F22 | underdetermined | withdrawn | games_ground::edition::supply_tests::play_never_exceeds_the_components_the_box_holds | counterexample | 2026-08-07 | clay-borg | | F26 | inert | raised | `crates/cb-render-html/src/lib.rs::a_scoped_table_says_what_a_scope_does` | counterexample | 2026-08-08 | ground-game | -| F27 | unplayed | raised | `games/ground/examples/scenario-panel.rs` | counterexample | 2026-08-08 | clay-borg | -| F28 | underdetermined | raised | `games_ground::tests::the_mastery_rating_and_the_shared_score_count_different_things` | counterexample | 2026-08-08 | ground-game | -- **F27 — the two competitive modes are scoring lenses over cooperative - play.** `scenario-panel` finds group success *exactly* equal across - SHARED GROUND, COMMON PROBLEM and BONDED COALITIONS in all 36 cells. - Correct arithmetic: `GreedyPolicy` maximises the group outcome and - never reads `state.mode`, so the same games are played and only the - winner set is carved differently. **Whether a mode changes how GROUND - is played is therefore untested**, and cannot be tested by any panel - built from the current policies — it needs one that plays for personal - score. `unplayed` rather than `inert`: the modes score correctly, they - have simply never faced a seat that wanted to win alone. - -- **F28 — SHARED GROUND's mastery rating counts cards where the mode - card counts points.** *"All claimed Problem cards form one shared score. - … For a mastery rating, subtract 1 for each Blame token still in play - and 1 for each Denied Problem."* The shared score is claimed **value** - (`total`); `mastery` subtracts the same penalties from the claimed - **count**. Both readings fit the sentence and they differ on every game - where a 3-point Problem is claimed. Ours to report, theirs to rule. - - **Sensitivity:** vary only the point value of the claimed Problems and - the gap moves with `sum(value) - count`. The two readings agree exactly - when every claimed Problem is worth 1 — no Problem in the edition is — - or when nothing is claimed, which is a loss. Everything else is held - fixed: same board, same penalties, same threshold. - - **F26 — a package that adds a FILE is invisible, where a package that adds a column is not.** `h2-scoped-problem-stress` ships `Rules_Text.csv` — twenty-two passages of player-facing rules, including diff --git a/tools/cb-play/src/hotseat.rs b/tools/cb-play/src/hotseat.rs index 4f6160e..029e216 100644 --- a/tools/cb-play/src/hotseat.rs +++ b/tools/cb-play/src/hotseat.rs @@ -1288,7 +1288,6 @@ mod tests { serve: Some(0), trial: None, mode: games_ground::ScoringMode::SharedGround, - scenario: "SCN_01".into(), pace: crate::table::Pace::Speed, variant: games_ground::Variant::Baseline, }, @@ -1480,7 +1479,6 @@ mod tests { serve: Some(0), trial: None, mode: games_ground::ScoringMode::SharedGround, - scenario: "SCN_01".into(), pace: crate::table::Pace::Speed, variant: games_ground::Variant::Baseline, }, diff --git a/tools/cb-play/src/inspect.rs b/tools/cb-play/src/inspect.rs index a05da88..827d465 100644 --- a/tools/cb-play/src/inspect.rs +++ b/tools/cb-play/src/inspect.rs @@ -140,18 +140,12 @@ fn render_player(id: PlayerId, p: &PlayerView, is_viewer: bool) -> String { pub fn render(view: &GroundView) -> String { let mut out = String::new(); out.push_str(&format!( - "\nround {} step {:?} lead {} mode {:?} rules {} scenario {} \ - deck {} discard [{}]\n", + "\nround {} step {:?} lead {} mode {:?} rules {} deck {} discard [{}]\n", view.round, view.step, seat_name(view.lead), view.mode, view.variant.id(), - // CB-WP-0047: WHICH of the four boards. The id, not the title, - // because this is the machine-facing view — `cb-play inspect` is - // read by a maintainer diffing states, where the HTML page is - // read by a player and shows the printed title. - view.scenario, view.solution_deck_len, cards(&view.solution_discard), )); @@ -503,7 +497,6 @@ mod tests { // CB-WP-0044: the inspector must say which rules it is replaying, // for the same reason the page must. ("variant", "rules ground-darvo-r0"), - ("scenario", "scenario SCN_"), ("viewer", "(you)"), ("solution_deck_len", "deck 11"), ("solution_discard.*.suit", "discard [Repair, Change]"), @@ -621,7 +614,6 @@ mod tests { lead: p2, step: RoundStep::Resolve, mode: ScoringMode::BondedCoalitions, - scenario: "SCN_01".into(), players, relations: BTreeMap::from([ (Pair::new(p1, p2), Relation::Bond), @@ -778,7 +770,6 @@ mod tests { serve: None, trial: None, mode: games_ground::ScoringMode::SharedGround, - scenario: "SCN_01".into(), pace: crate::table::Pace::Speed, variant: games_ground::Variant::Baseline, }; diff --git a/tools/cb-play/src/main.rs b/tools/cb-play/src/main.rs index 83a8dd8..2f4e0d3 100644 --- a/tools/cb-play/src/main.rs +++ b/tools/cb-play/src/main.rs @@ -62,61 +62,6 @@ enum Mode { }, } -/// A scenario as the player names it, as the edition names it. -/// -/// Accepts `2`, `02`, `scn-02`, `SCN_02` and the title's first word, so -/// that `--scenario confidence` works — the ids are the edition's -/// vocabulary and *"Broken Confidence"* is the player's. -/// -/// **Validated against the edition, never against a pattern.** `SCN_09` -/// looks exactly like an id; the error names what the edition has. -fn normalise_scenario(v: &str) -> Result { - let known = games_ground::edition::scenarios()?; - let want = v.trim().to_ascii_lowercase(); - let digits: String = want.chars().filter(|c| c.is_ascii_digit()).collect(); - let by_id = |s: &games_ground::edition::ScenarioText| { - s.id.to_ascii_lowercase() == want - || (!digits.is_empty() - && s.id - .rsplit('_') - .next() - .is_some_and(|n| n.parse::().ok() == digits.parse::().ok())) - }; - if let Some(s) = known.iter().find(|s| by_id(s)) { - return Ok(s.id.clone()); - } - // By title, so the four cards can be named by what is printed on them. - if let Some(s) = known - .iter() - .find(|s| s.title.to_ascii_lowercase().contains(&want) && !want.is_empty()) - { - return Ok(s.id.clone()); - } - Err(format!( - "unknown --scenario {v:?} (the edition has {})", - known - .iter() - .map(|s| format!("{} {:?}", s.id, s.title)) - .collect::>() - .join(", ") - )) -} - -/// `scn-02-4p`, or `standard-4p` for the scenario every recording plays. -/// -/// **`SCN_01` keeps the old preset string.** Twenty-six recorded -/// scenarios name `standard-Np`; emitting `scn-01-Np` for the same board -/// would have made every one of them unreplayable to no purpose. -pub fn scenario_preset(scenario: &str, players: u8) -> String { - if scenario == "SCN_01" { - return format!("standard-{players}p"); - } - format!( - "scn-{}-{players}p", - scenario.rsplit('_').next().unwrap_or(scenario) - ) -} - fn parse_args(argv: &[String]) -> Result { let mut config = Config::default(); let mut source: Option = None; @@ -196,16 +141,6 @@ fn parse_args(argv: &[String]) -> Result { // cheap now where a retrofit would not be. // CB-WP-0038: ground-game names these in // `editions/catalog.yaml`; `--variant` takes that id. - // CB-WP-0047: WHICH SCENARIO. `deal` has taken a scenario id - // since it was written; the call site passed the literal - // "SCN_01", so three of four decks were unreachable from the - // driver — the same shape as `--mode` before F14. - "--scenario" => { - play_flags.push(flag.into()); - let v = value(i, argv, flag)?; - config.scenario = normalise_scenario(&v)?; - i += 2; - } "--variant" => { play_flags.push(flag.into()); config.variant = value(i, argv, flag)?.parse()?; @@ -379,7 +314,6 @@ mod tests { serve: None, trial: None, mode: games_ground::ScoringMode::SharedGround, - scenario: "SCN_01".into(), pace: table::Pace::Speed, variant: games_ground::Variant::Baseline, }; @@ -427,7 +361,6 @@ mod tests { serve: None, trial: None, mode: games_ground::ScoringMode::SharedGround, - scenario: "SCN_01".into(), pace, variant: games_ground::Variant::Baseline, }; @@ -497,7 +430,6 @@ mod tests { serve: None, trial: None, mode: games_ground::ScoringMode::SharedGround, - scenario: "SCN_01".into(), pace: table::Pace::Speed, variant: games_ground::Variant::Baseline, }; @@ -564,7 +496,6 @@ mod tests { serve: None, trial: None, mode: games_ground::ScoringMode::SharedGround, - scenario: "SCN_01".into(), pace: table::Pace::Speed, variant: games_ground::Variant::Baseline, }; @@ -629,7 +560,6 @@ mod tests { serve: None, trial: None, mode: games_ground::ScoringMode::SharedGround, - scenario: "SCN_01".into(), pace: table::Pace::Speed, variant: games_ground::Variant::Baseline, }; diff --git a/tools/cb-play/src/table.rs b/tools/cb-play/src/table.rs index dfefde6..83239c4 100644 --- a/tools/cb-play/src/table.rs +++ b/tools/cb-play/src/table.rs @@ -47,14 +47,6 @@ pub struct Config { /// patch, so two of the three shipped modes were unreachable from the /// only way anyone actually plays. pub mode: games_ground::ScoringMode, - /// Which of the edition's four Scenarios to deal (CB-WP-0047). - /// - /// **The same shape as `mode` and the same history.** `setup` dealt - /// the literal `"SCN_01"`, so three of the four vendored decks — 15 - /// of the 20 Problem cards — had never reached a table through the - /// only way anyone actually plays. Stored as the preset's scenario - /// id so the kernel does the validating. - pub scenario: String, /// How much ornamentation is performed (CB-WP-0036, /// [`specs/Ornamentation.md`]). /// @@ -111,7 +103,6 @@ impl Default for Config { serve: None, trial: None, mode: games_ground::ScoringMode::SharedGround, - scenario: "SCN_01".into(), pace: Pace::Speed, variant: games_ground::Variant::Baseline, } @@ -408,7 +399,7 @@ fn run_game<'a, R: BufRead + 'a, W: Write + 'a>( ) -> Result<(Summary, crate::hotseat::EndChoice), String> { let setup = Setup { players: config.players, - preset: crate::scenario_preset(&config.scenario, config.players), + preset: format!("standard-{}p", config.players), patch: Default::default(), }; let mut initial = ::setup(&setup, config.seed)?; diff --git a/tools/design.py b/tools/design.py index f705015..4ef04b3 100644 --- a/tools/design.py +++ b/tools/design.py @@ -53,16 +53,7 @@ def reproduced(row, root=ROOT): """GameDesign §1.1: the artifact must resolve. A named test is admitted by its `crate::module::name` shape; anything else must be a real path on disk, and that is checked by stat, not by prefix.""" - # **Backticks are markdown, not part of the path** (CB-WP-0047). - # - # The register writes code spans, and every NAMED TEST survived that - # because `::` short-circuits before the stat. A backticked FILE path - # did not: os.path.exists("`games/.../x.rs`") is False, so a - # reproduction sitting on disk was reported as absent and its finding - # counted as debt against a target of zero. Same shape as ADR-0018: - # the computation was right and the string was not what it looked - # like. Found by F27, whose panel existed and did not count. - p = row["repro"].strip().strip("`").strip() + p = row["repro"] if p in ("", "—", "-"): return False if "::" in p: # a named test @@ -178,17 +169,6 @@ def self_test(): check("a named test counts", reproduced({"repro": "games_ground::view::tests::a_spectator_sees_no_hands"})) check("an em-dash does not count", not reproduced({"repro": "—"})) - # CB-WP-0047: the register writes CODE SPANS, so this is fed markdown. - check("a BACKTICKED path that exists counts", - reproduced({"repro": "`scenarios/ground/gr-p05-solve-legality.yaml`"}), - "a reproduction on disk was reported as absent, and the finding " - "counted as debt against a target of zero") - check("a backticked path that does NOT exist still does not count", - not reproduced({"repro": "`scenarios/ground/nope.yaml`"}), - "stripping the span must not turn the check off") - check("a backticked em-dash does not count", - not reproduced({"repro": "`—`"})) - # The distinction the backfill discovered: a green DEFAULT is expected, # a green COUNTEREXAMPLE is the alarm. Without this the report cried # wolf over U2, whose scenario is green precisely because the diff --git a/workplans/CB-WP-0046-the-rule-the-placement-encodes.md b/workplans/CB-WP-0046-the-rule-the-placement-encodes.md index ecc60e7..837bad8 100644 --- a/workplans/CB-WP-0046-the-rule-the-placement-encodes.md +++ b/workplans/CB-WP-0046-the-rule-the-placement-encodes.md @@ -3,7 +3,6 @@ id: CB-WP-0046 kind: product title: "The rule the placement encodes" status: done -state_hub_workstream_id: "169242ac-024d-4001-aae2-d9bbf710fe2a" --- # Purpose @@ -81,7 +80,6 @@ also why an unstamped log reports `unrecorded` and not `ground-darvo-r0`. id: CB-WP-0046-T01 status: done priority: high -state_hub_task_id: "64b9e9bb-bcaa-45e5-975f-3527e4c95c05" ``` **Controls, all mutation-proven:** diff --git a/workplans/CB-WP-0047-all-four-boards-and-all-three-modes.md b/workplans/CB-WP-0047-all-four-boards-and-all-three-modes.md deleted file mode 100644 index bc89a7e..0000000 --- a/workplans/CB-WP-0047-all-four-boards-and-all-three-modes.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -id: CB-WP-0047 -kind: product -title: "All four boards and all three modes" -status: done ---- - -# Purpose - -``` -structural tier M (changes the state and therefore the recording, and - moves a reported number -- the threshold -- from the - engine to the edition) -declared tier M -``` - -> *"lets have a look at the cards and the rules to those cards again and -> implement all the scenarios and modes"* - -## What was actually missing - -The two halves turned out to be very different sizes. - -**The modes were already implemented.** `ScoringMode`'s three arms all -score, `--mode` reaches them, F14 fixed the driver. What was missing was -that **nothing had ever compared them**, and the page never named them — -see below. - -**The scenarios were not implemented at all.** `edition::deal` has taken a -`scenario_id` since the day it was written, and the only caller passed the -literal `"SCN_01"`: - -```rust -let dealt = crate::edition::deal("SCN_01", seats)?; -``` - -**The seam was the whole mechanism and it sat unused.** 15 of the 20 -Problem cards had never been dealt by anything — not a gate, not a panel, -not a player. Nothing was red, because nothing asked. - -## Task: deal every board, name every mode - -```task -id: CB-WP-0047-T01 -status: done -priority: high -``` - -### The scenario is state - -`GroundState.scenario`, `#[serde(default = "default_scenario")]` → `SCN_01`. -In the state for the same reason `variant` is (CB-WP-0038): the board and -the threshold follow from it, so a recording that did not carry it could -not be replayed. - -**`standard-Np` keeps meaning `SCN_01`.** Twenty-six recorded scenarios -name that preset; a grammar that redefined it would have moved every one -of their boards while their hashes still claimed to pin them. `scn-03-4p` -names the rest. `--scenario` accepts `3`, `scn-03`, `SCN_03` or -`confidence` — ids are the edition's vocabulary, *"Broken Confidence"* is -the player's — and is validated against the edition, never a pattern, so -`SCN_09` is refused by name. - -### The threshold now comes off the Scenario card - -`threshold()` was `match seats { 0..=2 => 5, 3..=4 => 7, _ => 9 }` — the -engine's own copy of a number four cards already print, which is what F25 -was raised about. - -**The first version of this control was worthless and mutation said so.** -Reverting to the hardcoded bands left every test green, because all four -scenarios print 5/7/9 and the two paths are observationally identical on -every input the edition can supply. A control that cannot separate the -thing it is about from its fallback is not a control. - -Fixed by splitting `threshold_from(list, scenario, seats)`, which can be -handed a card that **disagrees** (4/6/8). That test goes red under the -mutation; the loop over real scenarios never could. - -### The page names the mode, the board, and what winning means - -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. - -**And the coverage probe was holding it in place.** `("mode", "scoring -BondedCoalitions")` — a probe matching `Debug` output, which went red the -moment the defect was fixed. That is the **third** time this shape has -been found (CB-WP-0024, CB-WP-0034, here), and it is now specific enough -to state as a rule: *a probe that names a rendering pins that rendering; -probes name facts.* - -The page now carries the Scenario's premise (*what happened*) and the Mode -card's rules text (*how this game is won*) — including the **tiebreak**, -which is a rule a player can play for: "lower Stress, then more Bonds" -changes what a losing seat should do in round five, and it lived in a -column nothing read. - -### Mutations - -| mutation | what went red | -|---|---| -| `setup` deals `"SCN_01"` regardless of preset | *"the four scenarios yield 1 distinct board"* | -| `threshold_from` ignores the card | *"the threshold did not come off the Scenario card"* | -| header back to `{mode:?}` | *"the page shows the Rust variant's name"* | - -**Done 2026-08-08.** `make all` green. Verified live on -`make ground SCENARIO=4 MODE=coalitions`. - -## What the panel found - -`make panels` gained `scenario-panel`: 4 scenarios × 3 modes × 3 seat -bands, greedy throughout, 100 games per cell. - -**1. Two of the four scenarios are the same board.** SCN_01 and SCN_02 -have identical suits and values at every priority; every cell matches -exactly. Not a defect — a designed reskin is a legitimate choice — but -"four scenarios" buys **three** boards, and a panel that measured them as -four independent boards would be measuring one of them twice. Pinned by a -characterisation test so a future divergence is a decision, not a drift. - -**2. SCN_04 is the hard board at 2 players** — 52% group success against -67% and 73%. It is the only deck that needs **two Repair** solutions in -the 2p deal, and the 2p Solution draw cannot always supply them. The seat -band the thresholds treat as uniform is not uniform across boards. - -**3. The three modes produce identical play.** Group success is *exactly* -equal across all three modes in every cell. That is correct arithmetic and -a real finding: `GreedyPolicy` maximises the group outcome and never -consults `state.mode`, so **the two competitive modes are scoring lenses -over cooperative play**. `win/g` differs only by how the winner set is -carved from the same games. Whether COMMON PROBLEM and BONDED COALITIONS -change how the game is *played* is untested and cannot be tested by this -panel — it needs a policy that plays for personal score. Filed as F27. - -**4. Every board is a formality at 6 players** — 100% group success in all -twelve cells. Consistent with F17's shape and with H2's motivation; noted, -not acted on. - -## Not done here - -- **No policy plays for personal score**, so F27 stands open and the two - competitive modes remain unexercised as *incentives*. This is the - largest remaining gap and it is the natural next pass. -- **SHARED GROUND's mastery rating counts CLAIMED CARDS, not points.** - The Mode card says *"All claimed Problem cards form one shared score … - subtract 1 for each Blame token still in play and 1 for each Denied - Problem"* — and the shared score is the claimed **value**, while - `mastery` subtracts from the claimed **count**. Underdetermined rather - than plainly wrong; raised for `ground-game` rather than changed here, - because scoring is theirs to rule on. -- **The trial log stamps the variant (CB-WP-0046) but not the scenario or - the mode.** A note from SCN_04 coalitions is indistinguishable from a - baseline SHARED GROUND note, which is the same defect one pass later in - two new axes. The marker already carries attributes, so this is small. -- **`design.py` reported an existing reproduction as absent.** It stats - `row["repro"]` directly, and the register writes **code spans** — so a - backticked file path was `os.path.exists("`games/…/x.rs`")` = False. - Named tests survived because `::` short-circuits before the stat, which - is why it had never shown. F27's panel existed and counted as debt - against a target of zero. ADR-0018's shape again: right computation, - wrong string. Fixed with three checks, including that stripping the - span must not turn the check off. - -- **`catalog.yaml` and both `VARIANT.md`s were re-vendored in this pass**: `ground-game` moved H2 - from `experimental` to `measured`, `decision: keep-as-experiment`, - citing our CB-EV-0032. That is their ruling on the previous pass, not - work done here.