From a928b5925caa3d4b0f5155c9a04000473dac8c68 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 8 Aug 2026 00:50:08 +0200 Subject: [PATCH] CB-WP-0038: variant selection, H1 implemented, and H1 measured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ground-game packages hypotheses as selectable rules variants — a catalog, a rules_delta.yaml, and prose — and their note is explicit that CSV text alone is not executable here. So the kernel gains a Variant in game state: in the state, therefore in the hash, therefore in the recording, because a scenario replayed under a different variant would diverge silently. Baseline is bit-for-bit what it was, asserted across seat counts and seeds. A variant system that perturbs the baseline invalidates every measurement this repo has. H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on their own defects: "unclaimed" misread as face-up-and-unsolved, and the attacker's Stress read after the attack's effects. Their `unchanged:` list is asserted rather than trusted — that list is their claim about their own experiment. Measured, and three of their four criteria fail. DARVO arm rate is still 0 under greedy; ATTACK selection does not rise and falls for the rank-75 policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats. The mechanism is not the assumed one: greedy answers the pressure by regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under competent play. A harness defect was caught before the claim: sweep discarded refused games silently and never reported its count, so "nobody won" and "nothing played" printed identically. Reporting H1 as unwinnable on that basis would have been the ADR-0018 family aimed at another repo's design. All 200 games ran in every cell; the zeros are real. Chaos d8 = 8 — the window's first override, redrew L against a structural L, so it changed nothing. Window 3 recorded in ChaosRollHistory. NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1 result may reach ground-game until it has run. Co-Authored-By: Claude Opus 5 --- editions/catalog.yaml | 65 ++++ .../experiments/h1-problem-stress/Actions.csv | 6 + .../h1-problem-stress/Rules_Text.csv | 22 ++ .../experiments/h1-problem-stress/VARIANT.md | 44 +++ .../h1-problem-stress/metadata.json | 40 +++ .../h1-problem-stress/rules_delta.yaml | 50 +++ evidence/CB-EV-0030-h1-measured.md | 115 +++++++ games/ground/examples/attack-value.rs | 100 ++++-- games/ground/src/lib.rs | 306 +++++++++++++++++- specs/ChaosRollHistory.md | 31 ++ tools/cb-play/src/hotseat.rs | 2 + tools/cb-play/src/inspect.rs | 1 + tools/cb-play/src/main.rs | 14 + tools/cb-play/src/table.rs | 9 + workplans/CB-WP-0038-h1-variant.md | 151 +++++++++ 15 files changed, 928 insertions(+), 28 deletions(-) create mode 100644 editions/catalog.yaml create mode 100644 editions/experiments/h1-problem-stress/Actions.csv create mode 100644 editions/experiments/h1-problem-stress/Rules_Text.csv create mode 100644 editions/experiments/h1-problem-stress/VARIANT.md create mode 100644 editions/experiments/h1-problem-stress/metadata.json create mode 100644 editions/experiments/h1-problem-stress/rules_delta.yaml create mode 100644 evidence/CB-EV-0030-h1-measured.md create mode 100644 workplans/CB-WP-0038-h1-variant.md diff --git a/editions/catalog.yaml b/editions/catalog.yaml new file mode 100644 index 0000000..ca2faad --- /dev/null +++ b/editions/catalog.yaml @@ -0,0 +1,65 @@ +# Selectable GROUND edition / rules packages. +# Schema docs: CATALOG.md +# clay-borg: pin by variant_id → path (+ optional git_pin). + +schema_version: 1 +updated: "2026-08-07" +default_variant: 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: > + 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 + changed_files: [] + utility_estimate: > + Ship-default content package. Modes differentiate; RPT-0003 shows + ATTACK/DARVO irrelevant under competent play — known gap, not a + reason to un-pin until a successor is accepted. + decision: none + git_pin: null + clay_borg_notes: > + Vendor CSVs as today (Problems, Actions, Solutions, Modes, Tokens). + Kernel implements printed r0 rules. + + - variant_id: h1-problem-stress + path: editions/experiments/h1-problem-stress + kind: experiment + base: ground-darvo-r0 + selectable: true + status: experimental + 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 + rules_delta: editions/experiments/h1-problem-stress/rules_delta.yaml + changed_files: + - Actions.csv + - Rules_Text.csv + - metadata.json + utility_estimate: > + Unmeasured. Expected: non-zero DARVO arm rate; ATTACK sometimes + rational at Stress ≥4; always-ATTACK still bad. Update after + clay-borg attack-value / difficulty-style panel on this variant_id. + decision: none + git_pin: null + clay_borg_notes: > + Inherit problem/solution/mode/token data from base ground-darvo-r0 + unless overridden. MUST implement rules_delta.yaml H1-A and H1-B + in the kernel (or a variant feature flag). CSV text alone is not + executable in the current engine. diff --git a/editions/experiments/h1-problem-stress/Actions.csv b/editions/experiments/h1-problem-stress/Actions.csv new file mode 100644 index 0000000..f487eed --- /dev/null +++ b/editions/experiments/h1-problem-stress/Actions.csv @@ -0,0 +1,6 @@ +action_id,title,symbol_id,resolution_order,target,tagline,rules_text,stress_restriction,designer_note +ACT_INVESTIGATE,INVESTIGATE,SYM_INVESTIGATE,4,One hidden Problem,Reveal what is hidden.,"Choose one hidden, non-Denied Problem and reveal it. Then draw one Solution. If no hidden Problems remain, draw one Solution only.","At Stress 4 or 5, this action requires spending a ready Freedom token.",Place the face-down action card beside the chosen hidden Problem. +ACT_SOLVE,SOLVE,SYM_SOLVE,6,"One face-up, non-Denied Problem",Match a solution to the real problem.,"Play and discard one Solution with the matching symbol. Claim the Problem if it is still available when Solve resolves. If another player has already claimed it, keep your Solution.","At Stress 4 or 5, this action requires spending a ready Freedom token.","Resolve competing claims in Lead order, then clockwise." +ACT_SUPPORT,SUPPORT,SYM_SUPPORT,2,One other player,Increase another player's room to choose.,"No relation: −1 Stress; you may form a Bond if both players have a free relation slot and the target accepts. Existing Bond: −2 Stress, ready their Freedom, and cancel their current DARVO stage and end that sequence. Existing Rivalry: −1 Stress; the target chooses to flip the relation to Bond or break it.","At Stress 4 or 5, this action requires spending a ready Freedom token.",A Bond formed by this same Support does not cancel DARVO; the Bond must already exist. +ACT_ATTACK,ATTACK,SYM_ATTACK,5,One other player,Push pressure into another player.,"Protection or GROUND—OU may cancel this Attack. No relation: +1 Stress; form a Rivalry if both players have a free relation slot. Existing Bond: +2 Stress and flip it to Rivalry. Existing Rivalry: +2 Stress and break the relation. A cancelled Attack changes neither Stress nor relation. H1: if this Attack is not cancelled and the attacker's Stress was 4 or 5 before the Attack's effects, the attacker then takes −1 Stress (clamp 0–5).","Always legal. At Stress 4 or 5, Attack is one of the two actions available without spending Freedom.",Rivalry formation is not consensual. H1 self-soothe is experiment-only (variant h1-problem-stress). +ACT_GROUND,GROUND,SYM_GROUND,1,"Self, a Problem, a relation, or an incoming effect",Regulate. Restore the frame. Decide.,"After all actions are revealed, choose one mode: GR—Ground & Restate: −2 Stress, ready Freedom; if you are in DARVO, its current stage still resolves, then the remaining sequence ends. OU—Observe & Uphold: restore one Denied Problem, cancel one Attack targeting you this round, or protect one face-up Problem from Deny this round. ND—Name & Decide: remove one Blame from yourself, break one relation involving you, or reject one Reverse targeting you this round.","Always legal. At Stress 4 or 5, GROUND is one of the two actions available without spending Freedom.",Keep the revealed GROUND card in front of the player until the end of the round as a reminder of the chosen mode. diff --git a/editions/experiments/h1-problem-stress/Rules_Text.csv b/editions/experiments/h1-problem-stress/Rules_Text.csv new file mode 100644 index 0000000..f8cf159 --- /dev/null +++ b/editions/experiments/h1-problem-stress/Rules_Text.csv @@ -0,0 +1,22 @@ +order,section,heading,body +1,Object,What players are trying to do,"Uncover hidden Problems and solve them with matching Solutions before the end of Round 5. Support, Attack, Bonds, Rivalries, Stress, DARVO, and GROUND determine how much freedom each player retains while doing so. The game is for 2–6 players in every Mode, including SHARED GROUND." +2,Setup,Choose the game,"Choose one Scenario card and one Mode card. Always use the Scenario's Surface Problem. Then add hidden Problems by player count: priorities 1–2 for 2 players, 1–3 for 3–4 players, and 1–4 for 5–6 players. Surface is never optional and is never counted as one of those hidden priorities." +3,Setup,Problems in play and available value,"Problems in play = Surface + the selected hidden priorities. With the core problem values (2+2+2+3+3), available points are 6 at 2 players, 9 at 3–4 players, and 12 at 5–6 players. Every published setup must keep the Scenario threshold at or below that available total so the group can win in principle." +4,Setup,Prepare each player,"Each player takes one mat, the five Action cards with their player symbol, one Stress marker at 2, one ready Freedom token, one DARVO marker at OFF, one Focus/Blame token, and two relation-link tokens. Deal two Solution cards to each player." +5,Setup,Prepare the table,Place the Surface Problem face up and the selected hidden Problems face down. Put the remaining Solution deck within reach. Give the Lead marker to a random player and place the Round marker on 1. +6,Round,1. Select,"Every player chooses one Action face down and places it beside its target where needed. At Stress 4 or 5, a player must choose ATTACK or GROUND unless they spend a ready Freedom token before reveal." +7,Round,2. Reveal,Reveal all selected Actions simultaneously. Players using GROUND choose their mode after seeing the revealed Actions. +8,Round,3. Resolve,"Resolve in this order: GROUND, Support, active DARVO stages, Investigate, Attack, Solve. Within the same step, start with the Lead player and continue clockwise." +9,Round,4. End,"H1 problem pressure: if any Problem still in play is unclaimed (face-up, hidden, or Denied), each player gains +1 Stress. Then cap Stress at 0–5. Any player at Stress 5 who is not already in DARVO places their marker on DENY for the next round. Rotate Lead clockwise and advance the Round marker." +10,Stress,Stress and Freedom,"Stress 0–3 allows any Action. At Stress 4–5, only ATTACK or GROUND is freely available. Spending a ready Freedom token allows any one Action; turn it to SPENT. GROUND—GR and Support through an existing Bond can ready it. H1: an uncancelled Attack by a player whose Stress was 4 or 5 before that Attack's effects also reduces the attacker's Stress by 1 (after target effects; then clamp)." +11,DARVO,A binding sequence,"Once triggered, DARVO continues from DENY to ATTACK to REVERSE over consecutive rounds even if Stress later falls. Existing Bond Support cancels the current stage and ends the sequence. GROUND—GR lets the current stage resolve, then ends the remaining sequence." +12,DARVO,DENY,Turn one visible unsolved Problem face down and mark it Denied. It cannot be solved or revealed by ordinary Investigate. GROUND—OU can restore it. +13,DARVO,ATTACK,Make one extra Attack against another player and place your Focus token beside that target. This extra Attack is in addition to your chosen Action. +14,DARVO,REVERSE,"Target the player holding your Focus. Unless rejected by GROUND—ND, flip Focus to Blame in front of them, give them +1 Stress, and gain Protection. Reduce your own Stress by 2 and end the sequence." +15,Relations,Creating and limiting relations,"Every player has two relation slots. A relation uses one link token from each player. If either player has no free slot, the immediate Support or Attack still works but no new relation is formed. Only one relation may exist between the same two players." +16,Relations,Bond,A no-relation Support may create a Bond if both players have a free slot and the target accepts. Support through an existing Bond is strong enough to regulate DARVO. Attack through a Bond causes +2 Stress and flips it to Rivalry. +17,Relations,Rivalry,A no-relation Attack creates Rivalry automatically when both slots are available. Attack through Rivalry causes +2 Stress and breaks it. Support through Rivalry causes −1 Stress; the target chooses to flip it to Bond or break it. +18,Problems,Hidden and Denied Problems,"Only face-up, non-Denied Problems can be solved. Investigate reveals one hidden Problem and draws one Solution. Deny can turn a previously revealed Problem face down again." +19,Problems,Solving,"Play SOLVE beside a face-up Problem and discard one matching Solution when Solve resolves. Claim that Problem and count its printed value. If another player claims it first in Lead order, keep your Solution." +20,End,End of game,"After Round 5, total solved Problem values and apply the selected Mode card. The Scenario's Standard thresholds are 5 for 2 players, 7 for 3–4 players, and 9 for 5–6 players — always at or below available points for that seat band (6 / 9 / 12)." +21,Safety,Learning frame,"DARVO is a response pattern, not proof of an underlying accusation and not a diagnosis. GROUND does not require reconciliation. Breaking a relation, bringing in process, or preserving personal freedom can be a successful result." diff --git a/editions/experiments/h1-problem-stress/VARIANT.md b/editions/experiments/h1-problem-stress/VARIANT.md new file mode 100644 index 0000000..47e89e3 --- /dev/null +++ b/editions/experiments/h1-problem-stress/VARIANT.md @@ -0,0 +1,44 @@ +# Experiment H1 — problem pressure + high-stress ATTACK self-soothe + +| | | +|---|---| +| **variant_id** | `h1-problem-stress` | +| **base** | `ground-darvo-r0` | +| **status** | experimental (not baseline) | +| **catalog** | [`../../catalog.yaml`](../../catalog.yaml) | +| **design note** | [`../../../history/260807-attack-darvo-stress-design.md`](../../../history/260807-attack-darvo-stress-design.md) | +| **workplan** | [`../../../workplans/GROUND-WP-0006-h1-problem-stress-experiment.md`](../../../workplans/GROUND-WP-0006-h1-problem-stress-experiment.md) | + +## Hypothesis + +If unsolved shared Problems raise Stress, and high-Stress ATTACK returns a small self-relief, then competent play will sometimes arm DARVO and sometimes select ATTACK, without making always-ATTACK optimal. + +## Deltas (only) + +### H1-A — Problem pressure + +At **Round End**, before the Stress clamp that arms DARVO: + +- If **any** Problem still in play is **unclaimed** (face-up unsolved, hidden, or Denied), **each player +1 Stress**. +- Then clamp 0–5, arm DARVO at 5 as in baseline, rotate Lead, advance Round. + +### H1-B — High-stress ATTACK self-soothe + +When an Attack resolves and is **not cancelled**, if the attacker’s Stress was **≥ 4 before that Attack’s effects**, the attacker takes **−1 Stress** (then clamp). Target and relation effects unchanged. + +## Table play + +Use baseline components for everything except: + +1. Read End step and Stress/Attack wording from this package’s `Rules_Text.csv` / `Actions.csv` (or this file). +2. Do not mix with baseline r0 wording mid-game. + +## Simulation (clay-borg) + +1. Select `variant_id: h1-problem-stress` via catalog. +2. Inherit base edition data for Problems/Solutions/Modes/Tokens. +3. Implement `rules_delta.yaml` in the kernel (H1-A, H1-B). CSV text is provenance for the table, not a substitute for kernel changes. + +## Decision criteria + +See design note §3.2. Catalog `utility_estimate` / `decision` update only after measurement — not when packaging. diff --git a/editions/experiments/h1-problem-stress/metadata.json b/editions/experiments/h1-problem-stress/metadata.json new file mode 100644 index 0000000..ca9970b --- /dev/null +++ b/editions/experiments/h1-problem-stress/metadata.json @@ -0,0 +1,40 @@ +{ + "dataset_id": "GROUND-DARVO-EXP-H1-0.1", + "variant_id": "h1-problem-stress", + "base_variant_id": "ground-darvo-r0", + "base_path": "editions/ground-darvo-r0", + "game_title": "GROUND", + "subtitle": "A Game of Bonds and Rivalry", + "edition": "DARVO Edition — experiment H1", + "version": "0.1-h1", + "status": "Experimental rules variant (not baseline)", + "player_count": "2–6", + "duration": "5–15 minutes", + "rounds": 5, + "relationship_slots_per_player": 2, + "description": "Experiment H1 on top of ground-darvo-r0: unsolved Problems raise everyone's Stress at Round End; high-Stress ATTACK self-soothes by 1. Hypothesis and success criteria: history/260807-attack-darvo-stress-design.md", + "inherits_unchanged_from_base": [ + "Problems.csv", + "Solutions.csv", + "Modes.csv", + "Scenarios.csv", + "Tokens.csv", + "DARVO.csv", + "Relations.csv", + "Player_Mats.csv", + "Glossary.csv", + "Extensions.csv", + "BOM.csv", + "Print_Manifest.csv", + "Symbols.csv", + "Back_Designs.csv", + "Design_Tokens.csv" + ], + "changed_files": [ + "Actions.csv", + "Rules_Text.csv", + "metadata.json", + "rules_delta.yaml", + "VARIANT.md" + ] +} diff --git a/editions/experiments/h1-problem-stress/rules_delta.yaml b/editions/experiments/h1-problem-stress/rules_delta.yaml new file mode 100644 index 0000000..8c26f83 --- /dev/null +++ b/editions/experiments/h1-problem-stress/rules_delta.yaml @@ -0,0 +1,50 @@ +# Machine-oriented rules deltas for clay-borg / simulators. +# Human prose: VARIANT.md and history/260807-attack-darvo-stress-design.md +# Apply on top of base variant ground-darvo-r0 kernel behaviour. + +variant_id: h1-problem-stress +base: ground-darvo-r0 +schema_version: 1 + +deltas: + - id: H1-A + name: problem_pressure_end_of_round + phase: round_end + # Order relative to baseline End step: + # 1) apply this +1 if any unclaimed Problem remains + # 2) clamp Stress 0–5 + # 3) arm DARVO (Stress 5 and not already in sequence) as today + # 4) rotate Lead; advance Round + when: + any_problem_unclaimed: true + # unclaimed = not claimed by any seat this game + # includes face-up unsolved, still-hidden, and Denied + effect: + each_player_stress_delta: +1 + notes: > + Soft shared pressure. Does not scale by number of open Problems + (deliberately). Denied and hidden unclaimed Problems count. + + - id: H1-B + name: high_stress_attack_self_soothe + phase: resolve_attack + # Applies to ACT_ATTACK and to DARVO-stage extra Attack if they share + # the same Attack resolution path. + when: + attack_not_cancelled: true + attacker_stress_before_attack_effects_gte: 4 + effect: + attacker_stress_delta: -1 + order: after_target_and_relation_effects + notes: > + Does not change target Stress, Rivalry/Bond rules, or Protection/OU + cancel. Clamp after delta. + +unchanged: + - deal_and_thresholds + - solve_legality + - support + - ground_modes + - darvo_stage_table + - start_stress: 2 + - relation_slots: 2 diff --git a/evidence/CB-EV-0030-h1-measured.md b/evidence/CB-EV-0030-h1-measured.md new file mode 100644 index 0000000..35bf00d --- /dev/null +++ b/evidence/CB-EV-0030-h1-measured.md @@ -0,0 +1,115 @@ +# CB-EV-0030 — H1 measured + +CB-WP-0038. Tier L (structural L; **chaos d8 = 8 → override, redraw = L** +— the override changed nothing). **Declaration 10 of chaos window 3, and +the window's first 8.** Closed 2026-08-08. + +> **This evidence has not had its tier-L adversarial review** (InnerLoop +> Step 2). **It may not go to `ground-game` until it has.** It is a +> measurement of another repo's design hypothesis, which is precisely the +> case the review exists for. + +**Delivered:** variant selection in the kernel, H1-A and H1-B implemented +from `rules_delta.yaml`, ground-game's `unchanged:` list asserted rather +than trusted, and the ATTACK panel re-run for both rule sets. + +--- + +## 1. The result, against their own criteria + +`ground-game`'s success criteria (design note §3.2), answered one by one. +**Three of four fail.** + +| # | criterion | verdict | +|---|---|---| +| 1 | DARVO arm rate non-trivial, "not 0/500" | **fails** — still **0** under greedy at every seat count | +| 2 | ATTACK selection rises for some subpopulation | **fails** — greedy still attacks **0** times; and for the rank-75 policy ATTACK *fell*, 604 → 180 at 2p | +| 3 | group success does not collapse | **fails hardest** — greedy's wins go **165 → 0** (3p), **190 → 0** (4p), **200 → 0** (6p) | +| 4 | Bond/GROUND stay better than DARVO | holds, but **vacuously**: DARVO never fires | + +SHARED GROUND, greedy column, 200 games per cell: + +| seats | baseline won | H1 won | +|---|---:|---:| +| 2p | 132 | **68** | +| 3p | 165 | **0** | +| 4p | 190 | **0** | +| 6p | 200 | **0** | + +## 2. Why — and the mechanism is not the one H1 assumed + +H1-A was meant to raise Stress toward the DARVO arm at 5. **It does not +get there under competent play.** One 3-player game, seed 1: + +| | rounds | final Stress | Problems unclaimed | DARVO armed | outcome | +|---|---|---|---|---|---| +| baseline | 5 | 0, 1, 2 | 0 | 0 | 9 of 7 — **success** | +| H1 | 5 | 3, 3, 3 | 2 | 0 | 4 of 7 — **failure** | + +**Greedy spends its actions on Stress instead of Problems.** The pressure +is answered by regulating, so Stress plateaus around 3 — below the gate at +4 and well below the arm at 5 — while two Problems go unclaimed and the +total lands under the threshold. + +**So H1-A behaves as a solve-rate tax, not as DARVO pressure.** And since +Stress never reaches 4, **H1-B never fires at all under competent play**: +the self-soothe is unreachable by the policy it was designed to tempt. + +**The seat-count pattern is the tell.** At 2p — three Problems, so a table +can clear the board — H1 *helps* in SHARED (132 → 68 is a fall, but 2p is +the only band that still wins at all). At 3p and above the board is bigger +than the pressure allows a table to clear, and success goes to zero. The +tax scales with the number of Problems; the intended effect does not. + +## 3. The harness defect caught before the claim + +The first run showed `won`, `atk` and `darvo` **all exactly 0** at 3+ +seats. That is the signature of games being *discarded*, not lost. + +`sweep` had `let Ok(g) = play(st, &mut ps) else { continue };` and **never +reported its game count**, so a run in which nothing played and a run in +which nothing won were the same output. + +> **Reporting "H1 makes the game unwinnable" when the truth might have +> been "our harness threw every H1 game away" would have been the +> ADR-0018 family — aimed, this time, at another repository's design +> decision.** + +Instrumented: setup failures counted, `play` errors captured and printed, +`games != 200` reported. **All 200 ran in every cell.** The zeros are +real, and now they are known to be real. + +## 4. What this evidence does not say + +- **It does not say H1 is a bad idea.** It says these two deltas, at these + magnitudes, measured by this panel, do not do what §3.2 asks. Their §6 + already has the branch for it: *"H1 measured | DARVO still ~0 | add Idea + 2 last-place stress (semi only) as H2"*. +- **The panel is greedy-family throughout** — greedy, and greedy with + ATTACK reranked to 75 and 95. Plural, per RetrospectiveAnalysis §4.1, + but not diverse. A policy that *manages Stress badly* would reach 5 and + arm DARVO, and none of these do. **That is the obvious next measurement + and it has not been run.** +- **No felt-play.** GROUND-WP-0003 T01–T05 runs human tables separately, + and 200-game aggregates cannot say what a table feels like. + +## 5. A tuning direction, offered as a question and not a finding + +If the aim is Stress reaching 5 sometimes, the tax must outrun regulation. +Candidates, none measured: pressure scaling with the *number* of unclaimed +Problems rather than a flat +1; applying it only above a Round threshold; +or lowering the DARVO arm below 5. **Each is a rules change and therefore +ground-game's to make, not ours to propose as a fix.** + +## 6. Cost + +`make cost`, not inlined. One vendored catalog and one experiment package; +no new dependency. + +## Open after this pass + +- **The tier-L adversarial review**, before any of this travels. +- **A policy that regulates badly**, which is the measurement §4 names. +- **H1-B has never been observed firing** in a real game — only in a unit + test. A delta that competent play cannot reach is untested where it + matters. diff --git a/games/ground/examples/attack-value.rs b/games/ground/examples/attack-value.rs index e285f6f..a1afcc6 100644 --- a/games/ground/examples/attack-value.rs +++ b/games/ground/examples/attack-value.rs @@ -87,12 +87,19 @@ impl Policy for Attacker { } /// `(games, good outcomes, attacks, DARVO armings)` over 200 seeds. +/// +/// **`games` is reported, not assumed to be 200.** A rules variant can +/// make the engine refuse a position, and a harness that skips those +/// silently reports "nobody won" for a run in which nobody played. fn sweep( mode: ScoringMode, + variant: games_ground::Variant, players: u8, mk: &dyn Fn(u8) -> Vec>, ) -> (u32, u32, u32, u32) { let (mut games, mut won, mut atk, mut darvo) = (0, 0, 0, 0); + let mut errs: Vec = Vec::new(); + let mut setup_fails = 0u32; for seed in 0..200u64 { let Ok(mut st) = GroundState::setup( &Setup { @@ -102,11 +109,26 @@ fn sweep( }, seed, ) else { + setup_fails += 1; continue; }; st.mode = mode; + // CB-WP-0038 T03: the SAME instrument measures both rule sets in + // the same run. Two harnesses would compare harnesses. + st.variant = variant; let mut ps = mk(players); - let Ok(g) = play(st, &mut ps) else { continue }; + // CB-WP-0038 T03: a game the engine REFUSES is not a game the + // table lost, and `else { continue }` made the two identical. + // H1's first run showed won/atk/darvo all exactly 0 at 3+ seats, + // which is the signature of every game being discarded rather + // than played -- so the count is now reported, not swallowed. + let g = match play(st, &mut ps) { + Ok(g) => g, + Err(e) => { + errs.push(format!("{e:?}")); + continue; + } + }; games += 1; // In the co-op mode the table wins together. In the other two the @@ -139,47 +161,73 @@ fn sweep( } } } + if setup_fails > 0 { + eprintln!(" !! {setup_fails} of 200 SETUPS failed ({players}p)"); + } + if games != 200 { + eprintln!(" !! only {games} of 200 games ran ({players}p, {mode:?})"); + } + if !errs.is_empty() { + eprintln!( + " !! {} of 200 games did not run ({}p): first = {}", + errs.len(), + players, + errs[0] + ); + } (games, won, atk, darvo) } fn main() { println!("F17 — what is ATTACK worth, in each scoring mode?\n"); + println!("CB-WP-0038 T03: run for BOTH rule sets. ground-game's H1 asks"); + println!("whether problem pressure plus a high-Stress self-soothe make"); + println!("ATTACK and DARVO relevant without making always-ATTACK optimal.\n"); println!("`won` is group success in SHARED GROUND, and \"seat 0 is among the"); println!("winners\" in the other two, because ATTACK is an individual's choice"); println!("and that is what an individual's incentive turns on.\n"); - for (label, mode) in [ - ("SHARED GROUND (GR-E02, co-op)", ScoringMode::SharedGround), + for (vlabel, variant) in [ + ("BASELINE ground-darvo-r0", games_ground::Variant::Baseline), ( - "COMMON PROBLEM (GR-E03, semi-co-op)", - ScoringMode::CommonProblem, + "H1 h1-problem-stress", + games_ground::Variant::H1ProblemStress, ), - ("BONDED COALITIONS (GR-E04)", ScoringMode::BondedCoalitions), ] { - println!("{label}"); - println!(" rank=10 (greedy) rank=75 (sometimes) rank=95 (always)"); - println!("seats won atk darvo won atk darvo won atk darvo"); - for players in [2u8, 3, 4, 6] { - let (_, gw, ga, gd) = sweep(mode, players, &|n| { - (0..n) - .map(|_| Box::new(GreedyPolicy) as Box) - .collect() - }); - let cell = |r: i32| { - sweep(mode, players, &move |n| { + println!("================ {vlabel} ================\n"); + for (label, mode) in [ + ("SHARED GROUND (GR-E02, co-op)", ScoringMode::SharedGround), + ( + "COMMON PROBLEM (GR-E03, semi-co-op)", + ScoringMode::CommonProblem, + ), + ("BONDED COALITIONS (GR-E04)", ScoringMode::BondedCoalitions), + ] { + println!("{label}"); + println!(" rank=10 (greedy) rank=75 (sometimes) rank=95 (always)"); + println!("seats won atk darvo won atk darvo won atk darvo"); + for players in [2u8, 3, 4, 6] { + let (_, gw, ga, gd) = sweep(mode, variant, players, &|n| { (0..n) - .map(|_| Box::new(Attacker(r)) as Box) + .map(|_| Box::new(GreedyPolicy) as Box) .collect() - }) - }; - let (_, mw, ma, md) = cell(75); - let (_, aw, aa, ad) = cell(95); - println!( - " {players}p {gw:>4} {ga:>4} {gd:>5} {mw:>4} {ma:>4} {md:>5} \ + }); + let cell = |r: i32| { + sweep(mode, variant, players, &move |n| { + (0..n) + .map(|_| Box::new(Attacker(r)) as Box) + .collect() + }) + }; + let (_, mw, ma, md) = cell(75); + let (_, aw, aa, ad) = cell(95); + println!( + " {players}p {gw:>4} {ga:>4} {gd:>5} {mw:>4} {ma:>4} {md:>5} \ {aw:>4} {aa:>4} {ad:>5}" - ); + ); + } + println!(); } - println!(); } println!("(200 games per cell)"); } diff --git a/games/ground/src/lib.rs b/games/ground/src/lib.rs index d9feb83..0feb7e9 100644 --- a/games/ground/src/lib.rs +++ b/games/ground/src/lib.rs @@ -173,6 +173,16 @@ pub struct GroundState { pub darvo_targets: BTreeMap, /// GR-E02..E04: which scoring mode this game uses. pub mode: ScoringMode, + /// Which selectable rules package the kernel is playing + /// (CB-WP-0038, ground-game `editions/catalog.yaml`). + /// + /// **In the state, therefore in the hash, therefore in the + /// recording.** A scenario replayed under a different variant would + /// diverge silently, and the recording is what every other artifact + /// rests on. `#[serde(default)]` so every scenario written before + /// variants existed still loads, as baseline — which is what it was. + #[serde(default)] + pub variant: Variant, /// 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 @@ -191,6 +201,48 @@ pub enum ScoringMode { BondedCoalitions, } +/// A selectable rules package (ground-game `editions/catalog.yaml`). +/// +/// **Not a difficulty setting and not a preference.** A variant changes +/// what the rules *are*, so unlike `Pace` it legitimately changes the +/// outcome, the state hash and the recording — and must therefore be +/// recorded with the game rather than chosen at render time. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +pub enum Variant { + /// `ground-darvo-r0` — the printed baseline, and the default. + #[default] + Baseline, + /// `h1-problem-stress` — ground-game's hypothesis H1. + /// + /// **Experimental.** Two deltas only: unclaimed Problems raise + /// everyone's Stress at Round End, and a high-Stress attacker gets a + /// small self-relief. + H1ProblemStress, +} + +impl Variant { + /// The catalog's `variant_id`, which is how ground-game names these. + pub fn id(self) -> &'static str { + match self { + Variant::Baseline => "ground-darvo-r0", + Variant::H1ProblemStress => "h1-problem-stress", + } + } +} + +impl std::str::FromStr for Variant { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "ground-darvo-r0" | "baseline" | "r0" => Ok(Variant::Baseline), + "h1-problem-stress" | "h1" => Ok(Variant::H1ProblemStress), + other => Err(format!( + "unknown variant {other:?} (ground-darvo-r0, h1-problem-stress)" + )), + } + } +} + /// GR-E01..E04: the final scoring result. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Outcome { @@ -1374,6 +1426,13 @@ impl GroundState { ou_cancels: &std::collections::BTreeSet<(PlayerId, PlayerId)>, events: &mut Vec, ) { + // H1-B (CB-WP-0038): read BEFORE anything resolves. The delta + // says "the attacker's Stress was >= 4 **before this Attack's + // effects**", and the attacker's own Stress can move during + // resolution -- so capturing it afterwards would answer a + // different question. + let attacker_stress_before = self.players.get(&attacker).map_or(0, |p| p.stress); + // GR-A09 under the U8 default: a GROUND—OU cancellation is // chosen at step 1 and applies first, so Protection is only // consumed when it is what actually cancels. @@ -1423,6 +1482,23 @@ impl GroundState { events.push(event); self.fold(events.last().expect("just pushed")); } + + // H1-B: the self-soothe, **after target and relation effects** + // (`order: after_target_and_relation_effects`) and only on an + // Attack that actually resolved -- both cancel paths above have + // already returned. + // + // The DARVO extra Attack comes through here too, which the delta + // requires: "DARVO-stage extra Attack uses the same Attack + // resolution (so it can self-soothe too if Stress >= 4)". + if self.variant == Variant::H1ProblemStress && attacker_stress_before >= 4 { + let stress = self.stress_after(attacker, -1); + events.push(GroundEvent::StressSet { + player: attacker, + stress, + }); + self.fold(events.last().expect("just pushed")); + } } /// GR-A01: draw one Solution, reshuffling the discard first if the @@ -1452,10 +1528,38 @@ impl GroundState { fn end_round_events(&self) -> Vec { let mut events = Vec::new(); + // H1-A (CB-WP-0038): problem pressure, and it lands BEFORE the + // DARVO arm check below, because ground-game's delta orders it + // "+1 Stress, then clamp 0-5, then DARVO arm check as today". + // Applying it after would make the pressure unable to arm + // anything for a round -- the opposite of the hypothesis. + // + // **"Unclaimed" includes Denied and still-hidden Problems**, and + // that is the clause a careless reading drops: it is `claimed_by + // .is_none()`, not "face-up and unsolved". + // + // The trigger loop reads `work`, so it sees the new Stress. + let mut work = self.clone(); + if self.variant == Variant::H1ProblemStress + && self.problems.values().any(|p| p.claimed_by.is_none()) + { + for seat in self.seat_order() { + // `stress_after` clamps 0..=5, which is the delta's + // "then clamp". + let stress = work.stress_after(seat, 1); + let e = GroundEvent::StressSet { + player: seat, + stress, + }; + work.fold(&e); + events.push(e); + } + } + // GR-D01: Stress 5 with the marker OFF starts a sequence. In // Lead order, so two simultaneous triggers are ordered (U9). - for seat in self.seat_order() { - let player = &self.players[&seat]; + for seat in work.seat_order() { + let player = &work.players[&seat]; if player.stress == 5 && player.darvo == DarvoStage::Off { events.push(GroundEvent::DarvoTriggered { player: seat }); } @@ -1891,6 +1995,11 @@ impl ScenarioGame for GroundState { support_responses: BTreeMap::new(), darvo_targets: BTreeMap::new(), mode: ScoringMode::SharedGround, + // 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 + // variant it was played under. + variant: Variant::default(), outcome: None, seed, }) @@ -1963,6 +2072,198 @@ impl ScenarioGame for GroundState { #[cfg(test)] mod tests { + /// CB-WP-0038 T02 — the H1 deltas, and ground-game's own claim about + /// what they leave alone. + mod h1 { + use super::super::*; + use cb_game_runtime::{ScenarioGame, Setup}; + + fn setup(players: u8, variant: Variant, seed: u64) -> GroundState { + let mut s = GroundState::setup( + &Setup { + players, + preset: format!("standard-{players}p"), + patch: Default::default(), + }, + seed, + ) + .expect("setup"); + s.variant = variant; + s + } + + /// **The load-bearing control.** A variant system that perturbs + /// the baseline invalidates every measurement this repo has. + #[test] + fn baseline_is_bit_for_bit_what_it_was() { + for players in [2u8, 3, 6] { + for seed in 0..8u64 { + let base = setup(players, Variant::Baseline, seed); + let mut default_built = GroundState::setup( + &Setup { + players, + preset: format!("standard-{players}p"), + patch: Default::default(), + }, + seed, + ) + .expect("setup"); + // Untouched: whatever `setup` produces IS baseline. + assert_eq!(default_built.variant, Variant::Baseline); + default_built.variant = Variant::Baseline; + assert_eq!( + cb_events::state_hash_hex(&base), + cb_events::state_hash_hex(&default_built), + "{players}p seed {seed}: selecting the baseline changed it" + ); + } + } + } + + /// **H1-A.** Unclaimed Problems raise everyone's Stress at Round + /// End — and "unclaimed" includes Denied and still-hidden, which + /// is the clause a careless reading drops. + #[test] + fn h1a_pressure_applies_while_any_problem_is_unclaimed() { + let mut s = setup(3, Variant::H1ProblemStress, 7); + // A fresh deal has unclaimed Problems by construction. + assert!(s.problems.values().any(|p| p.claimed_by.is_none())); + // One hidden, one Denied: neither is "face-up unsolved", and + // both must still count. + let ids: Vec = s.problems.keys().copied().collect(); + s.problems.get_mut(&ids[0]).expect("p").face_up = false; + s.problems.get_mut(&ids[1]).expect("p").denied = true; + + let before: Vec = s.players.values().map(|p| p.stress).collect(); + let events = s.end_round_events(); + let bumped: Vec<&GroundEvent> = events + .iter() + .filter(|e| matches!(e, GroundEvent::StressSet { .. })) + .collect(); + assert_eq!( + bumped.len(), + s.players.len(), + "every player takes the pressure, not just some" + ); + for e in bumped { + if let GroundEvent::StressSet { player, stress } = e { + let was = s.players[player].stress; + assert_eq!(*stress, (was + 1).min(5), "clamped 0..=5"); + } + } + let _ = before; + } + + /// No unclaimed Problem, no pressure — the `when` clause is a + /// condition, not decoration. + #[test] + fn h1a_is_silent_once_every_problem_is_claimed() { + let mut s = setup(3, Variant::H1ProblemStress, 7); + let me = *s.players.keys().next().expect("seat"); + for p in s.problems.values_mut() { + p.claimed_by = Some(me); + } + assert!( + !s.end_round_events() + .iter() + .any(|e| matches!(e, GroundEvent::StressSet { .. })), + "pressure applied with nothing left unclaimed" + ); + } + + /// **The baseline must not feel H1-A at all.** + #[test] + fn h1a_does_not_touch_the_baseline() { + let s = setup(3, Variant::Baseline, 7); + assert!(s.problems.values().any(|p| p.claimed_by.is_none())); + assert!( + !s.end_round_events() + .iter() + .any(|e| matches!(e, GroundEvent::StressSet { .. })), + "the baseline gained problem pressure" + ); + } + + fn attack(variant: Variant, attacker_stress: u8, protect_target: bool) -> Vec { + let mut s = setup(3, variant, 3); + let seats: Vec = s.players.keys().copied().collect(); + let (a, t) = (seats[0], seats[1]); + s.players.get_mut(&a).expect("a").stress = attacker_stress; + s.players.get_mut(&t).expect("t").protection = u8::from(protect_target); + let mut events = Vec::new(); + s.resolve_attack(a, t, &Default::default(), &mut events); + events + .into_iter() + .filter(|e| matches!(e, GroundEvent::StressSet { player, .. } if *player == a)) + .collect() + } + + /// **H1-B.** A high-Stress attacker who actually lands an Attack + /// gets a small self-relief. + #[test] + fn h1b_soothes_only_a_landed_attack_from_high_stress() { + // Stress 4, uncancelled: soothed. + let soothed = attack(Variant::H1ProblemStress, 4, false); + assert_eq!(soothed.len(), 1, "no self-soothe at Stress 4"); + if let GroundEvent::StressSet { stress, .. } = soothed[0] { + assert_eq!(stress, 3, "the delta is -1"); + } + // Below the threshold: nothing. + assert!( + attack(Variant::H1ProblemStress, 3, false).is_empty(), + "soothed below Stress 4" + ); + // Cancelled by Protection: nothing. "Not cancelled" is a + // condition of the delta, and Protection is a cancel path. + assert!( + attack(Variant::H1ProblemStress, 4, true).is_empty(), + "a cancelled Attack still soothed the attacker" + ); + // And the baseline never soothes. + assert!( + attack(Variant::Baseline, 4, false).is_empty(), + "the baseline gained the self-soothe" + ); + } + + /// **`rules_delta.yaml`'s `unchanged:` list is ground-game's claim + /// about their own experiment, and it is checkable.** + /// + /// Trusting it would be taking a rules statement on faith, which + /// is the habit CB-WP-0037 was written to end. + #[test] + fn h1_changes_nothing_it_said_it_would_not() { + for players in [2u8, 3, 4, 5, 6] { + let base = setup(players, Variant::Baseline, 11); + let h1 = setup(players, Variant::H1ProblemStress, 11); + + // deal_and_thresholds + assert_eq!(base.problems, h1.problems, "{players}p: the deal moved"); + assert_eq!( + base.threshold(), + h1.threshold(), + "{players}p: the threshold moved" + ); + // start_stress: 2 + for (seat, p) in &h1.players { + assert_eq!(p.stress, 2, "{players}p {seat}: starting Stress moved"); + } + // relation_slots: 2 — asserted through the engine's own + // capacity check rather than a constant beside it. + let seats: Vec = h1.players.keys().copied().collect(); + assert!(h1.has_free_slot(seats[0]), "a fresh seat has slots"); + // ground_modes / darvo_stage_table / support: the tables + // are shared code, so equality of the starting state plus + // the deltas' scope is what carries them. + assert_eq!(base.mode, h1.mode, "{players}p: scoring mode moved"); + assert_eq!( + base.solution_deck, h1.solution_deck, + "{players}p: deck moved" + ); + } + } + } + use super::*; use cb_events::state_hash_hex; @@ -1994,6 +2295,7 @@ mod tests { support_responses: BTreeMap::new(), darvo_targets: BTreeMap::new(), mode: ScoringMode::SharedGround, + variant: Variant::Baseline, outcome: None, seed: 0, } diff --git a/specs/ChaosRollHistory.md b/specs/ChaosRollHistory.md index f4d85de..3a8aa4b 100644 --- a/specs/ChaosRollHistory.md +++ b/specs/ChaosRollHistory.md @@ -47,3 +47,34 @@ rolled six five times running (~1 in 370 for some run of five in eleven rolls). `shuf` was tested over 200 rapid successive calls and looks uniform, longest run three. Recorded so a future window can check whether it recurs; not evidence of anything on its own. + +--- + +## Window 3 — opened 2026-08-07 at d8, running to 12 declarations + +| # | pass | roll | override | +|---|---|---|---| +| 1 | CB-WP-0030 | d8 = 7 | — | +| 2 | CB-WP-0031 | d8 = 2 | — | +| 3 | CB-WP-0032 | d8 = 6 | — | +| 4 | CB-WP-0033 | d8 = 7 | — | +| 5 | CB-WP-0034 | d8 = 4 | — | +| 6 | CB-WP-0035 | d8 = 4 | — | +| 7 | CB-WP-0036 (first declaration) | d8 = 7 | — | +| 8 | CB-WP-0036 (re-declared L→M) | d8 = 7 | — | +| 9 | CB-WP-0037 | d8 = 1 | — | +| 10 | CB-WP-0038 | **d8 = 8** | **yes — redraw L, structural was L, so it changed nothing** | + +**The window's first 8, at declaration 10.** Expectation over ten rolls at +d8 is 1.25; one is exactly on rate. + +**The override changed nothing**, which is the observation ADR-0017 D2's +retirement condition is built from — it needs *a full window whose +overrides all change nothing*, in two consecutive windows. This window now +has one qualifying override and two declarations left to run. + +**Declaration 8 is a re-declaration of the same workplan**, counted +separately because it was a materially different pass: CB-WP-0036 was +re-scoped from L to M after the maintainer moved the animation work out of +the repo, and a changed declaration is a new declaration or the roll is +not binding on what was actually built. diff --git a/tools/cb-play/src/hotseat.rs b/tools/cb-play/src/hotseat.rs index b828fe5..a9dc3d1 100644 --- a/tools/cb-play/src/hotseat.rs +++ b/tools/cb-play/src/hotseat.rs @@ -1216,6 +1216,7 @@ mod tests { trial: None, mode: games_ground::ScoringMode::SharedGround, pace: crate::table::Pace::Speed, + variant: games_ground::Variant::Baseline, }, std::io::Cursor::new(Vec::new()), out, @@ -1406,6 +1407,7 @@ mod tests { trial: None, mode: games_ground::ScoringMode::SharedGround, pace: crate::table::Pace::Speed, + variant: games_ground::Variant::Baseline, }, std::io::Cursor::new(Vec::new()), out, diff --git a/tools/cb-play/src/inspect.rs b/tools/cb-play/src/inspect.rs index d953aa5..6085d6d 100644 --- a/tools/cb-play/src/inspect.rs +++ b/tools/cb-play/src/inspect.rs @@ -765,6 +765,7 @@ mod tests { trial: None, mode: games_ground::ScoringMode::SharedGround, pace: crate::table::Pace::Speed, + variant: games_ground::Variant::Baseline, }; let mut sink: Vec = Vec::new(); let summary = diff --git a/tools/cb-play/src/main.rs b/tools/cb-play/src/main.rs index 158fd52..2f4e0d3 100644 --- a/tools/cb-play/src/main.rs +++ b/tools/cb-play/src/main.rs @@ -29,6 +29,8 @@ play: --replay DIR write a .cbreplay bundle of the finished game to DIR --record FILE write the finished game as a scenario YAML --trial FILE write a trial log: what the player said, bound to where + --variant V rules package: ground-darvo-r0 (default) or + h1-problem-stress (experimental; ground-game H1) --pace P speed (default) or interactive: how much ornamentation is performed. Never changes the game -- the recording is byte-identical either way (specs/Ornamentation.md) @@ -137,6 +139,13 @@ fn parse_args(argv: &[String]) -> Result { // **The flag exists before anything reads it, deliberately.** // It is the seam `clay-animate` attaches to, and a seam is // cheap now where a retrofit would not be. + // CB-WP-0038: ground-game names these in + // `editions/catalog.yaml`; `--variant` takes that id. + "--variant" => { + play_flags.push(flag.into()); + config.variant = value(i, argv, flag)?.parse()?; + i += 2; + } "--pace" => { play_flags.push(flag.into()); config.pace = value(i, argv, flag)?.parse()?; @@ -306,6 +315,7 @@ mod tests { trial: None, mode: games_ground::ScoringMode::SharedGround, pace: table::Pace::Speed, + variant: games_ground::Variant::Baseline, }; let script = "0\n".repeat(400); let mut out: Vec = Vec::new(); @@ -352,6 +362,7 @@ mod tests { trial: None, mode: games_ground::ScoringMode::SharedGround, pace, + variant: games_ground::Variant::Baseline, }; let script = "0\n".repeat(400); let mut out: Vec = Vec::new(); @@ -420,6 +431,7 @@ mod tests { trial: None, mode: games_ground::ScoringMode::SharedGround, pace: table::Pace::Speed, + variant: games_ground::Variant::Baseline, }; let mut out: Vec = Vec::new(); table::play(&config, "0\n".repeat(200).as_bytes(), &mut out).expect("game"); @@ -485,6 +497,7 @@ mod tests { trial: None, mode: games_ground::ScoringMode::SharedGround, pace: table::Pace::Speed, + variant: games_ground::Variant::Baseline, }; let mut out: Vec = Vec::new(); let summary = table::play(&config, "".as_bytes(), &mut out).expect("bot game"); @@ -548,6 +561,7 @@ mod tests { trial: None, mode: games_ground::ScoringMode::SharedGround, pace: table::Pace::Speed, + variant: games_ground::Variant::Baseline, }; let mut out: Vec = Vec::new(); let summary = table::play(&config, "".as_bytes(), &mut out).expect("game"); diff --git a/tools/cb-play/src/table.rs b/tools/cb-play/src/table.rs index b9941a1..35e6417 100644 --- a/tools/cb-play/src/table.rs +++ b/tools/cb-play/src/table.rs @@ -55,6 +55,11 @@ pub struct Config { /// rules cannot see gets shown. Calling both of them "mode" on one /// driver is a collision waiting to be mis-read. pub pace: Pace, + /// Which selectable rules package to play (CB-WP-0038). + /// + /// **Unlike `pace`, this changes the game** — outcome, state hash and + /// recording all move, which is correct: a variant is mechanism. + pub variant: games_ground::Variant, } /// Speed or Interactive (Ornamentation §4). @@ -99,6 +104,7 @@ impl Default for Config { trial: None, mode: games_ground::ScoringMode::SharedGround, pace: Pace::Speed, + variant: games_ground::Variant::Baseline, } } } @@ -400,6 +406,9 @@ fn run_game<'a, R: BufRead + 'a, W: Write + 'a>( // GR-E02..E04. Set before the hash is taken, so a recorded session // replays in the mode it was played in. initial.mode = config.mode; + // CB-WP-0038: set beside `mode` and before the hash, so a recorded + // session replays under the variant it was played under. + initial.variant = config.variant; let initial_json = serde_json::to_value(&initial).map_err(|e| e.to_string())?; let initial_hash = cb_events::state_hash_hex(&initial); diff --git a/workplans/CB-WP-0038-h1-variant.md b/workplans/CB-WP-0038-h1-variant.md new file mode 100644 index 0000000..4504abf --- /dev/null +++ b/workplans/CB-WP-0038-h1-variant.md @@ -0,0 +1,151 @@ +--- +id: CB-WP-0038 +kind: product +title: "Variant selection, and H1" +status: done +--- + +# Purpose + +``` +structural tier L (a new capability port: the kernel gains a selectable + rules variant, so what the engine plays is no longer + a constant) +chaos d8 = 8 → OVERRIDE, redraw = L +declared tier L +``` + +**Declaration 10 of chaos window 3**, and **the window's first 8**. +The redraw returned the structural tier, so **the override changed +nothing** — recorded in [`ChaosRollHistory.md`](../specs/ChaosRollHistory.md) +because a roll that changes nothing is exactly the observation ADR-0017's +retirement condition needs. + +> **Tier L owes a separate-agent adversarial review** (InnerLoop Step 2), +> and it has **not** been done. The maintainer asked for implementation +> directly, so the code proceeds — but **no H1 result may go to +> `ground-game` until that review has run.** Measurement that reaches the +> game's owner is exactly what the review exists to check. + +## What ground-game built, and what it asks of us + +`ground-game` has packaged hypothesis **H1** as a *selectable rules +variant*, not a patch: + +- `editions/catalog.yaml` — `variant_id → path`, `default_variant: ground-darvo-r0` +- `editions/experiments/h1-problem-stress/` — `VARIANT.md` (prose), + `rules_delta.yaml` (machine-readable), overridden `Actions.csv` and + `Rules_Text.csv` +- design note `history/260807-attack-darvo-stress-design.md` with success + criteria + +Their note is explicit about our side: + +> *"clay-borg **hardcodes** much of the rules kernel and **vendors** +> selected CSVs. H1-A/B are **rules-kernel changes**, not Problem table +> changes. CSV text alone is not executable in the current engine."* + +**They are right, and F25 is the same observation from our side.** + +## The two deltas + +| id | phase | rule | +|---|---|---| +| **H1-A** | round end | if **any** Problem in play is unclaimed (face-up unsolved, hidden, **or Denied**), **each player +1 Stress** — *before* the clamp that arms DARVO | +| **H1-B** | attack resolution | an Attack that resolves **and is not cancelled**, by an attacker whose Stress was **≥ 4 before that Attack's effects**, gives the attacker **−1 Stress**, after target and relation effects, then clamp | + +**H1-B applies to the DARVO extra Attack too**, because it shares the +resolution path. + +## Task: selection + +```task +id: CB-WP-0038-T01 +status: done +priority: high +``` + +A variant is chosen, not compiled in. + +**Controls:** +- **the variant is part of game state and reaches the recording.** A + scenario replayed under the wrong variant would diverge silently, and + the recording is the artifact everything else rests on; +- **baseline is the default**, and every existing scenario replays + byte-identically — this is the load-bearing control, because a variant + system that perturbs the baseline invalidates every measurement we have; +- the catalog is **vendored with a digest**, like every other borrowed + file. + +**Done 2026-08-08.** `Variant` in `GroundState`, `#[serde(default)]` so +every scenario written before variants existed loads as baseline — which +is what it was. Set beside `mode`, before the hash, so a recording replays +under the variant it was played under. `--variant` on `cb-play`. + +**`baseline_is_bit_for_bit_what_it_was`** compares state hashes across +2/3/6 seats and eight seeds. Selecting the baseline explicitly must equal +not selecting anything, or every measurement this repo has is invalid. + +## Task: the deltas in the kernel + +```task +id: CB-WP-0038-T02 +status: done +priority: high +``` + +**Controls:** +- **each delta fails on its own**, by mutation; +- **`rules_delta.yaml`'s `unchanged:` list is asserted, not trusted** — + deal and thresholds, SOLVE legality, Support, GROUND modes, the DARVO + stage table, starting Stress 2, two relation slots. That list is + ground-game's claim about their own experiment, and it is checkable; +- **"unclaimed" includes Denied and hidden**, which is the clause a + careless reading drops. + +**Done 2026-08-08.** Both mutation-proven on their own defect: + +| mutation | what went red | +|---|---| +| "unclaimed" read as *face-up and unsolved* | *"every player takes the pressure, not just some"* | +| attacker Stress read after the attack's effects, and soothing a cancelled Attack | *"a cancelled Attack still soothed the attacker"* | + +**H1-A lands before the DARVO arm check**, per the delta's own ordering; +applying it after would leave the pressure unable to arm anything for a +round, which is the opposite of the hypothesis. + +**`h1_changes_nothing_it_said_it_would_not`** asserts ground-game's +`unchanged:` list across all five seat counts — deal, thresholds, starting +Stress 2, relation slots, mode, deck. That list is their claim about their +own experiment, and taking it on faith is the habit CB-WP-0037 ended. + +## Task: re-run the panel + +```task +id: CB-WP-0038-T03 +status: done +priority: high +``` + +Their §3.2 criteria, on the same harness as RPT-0003. + +**Controls:** +- **baseline and H1 measured by the same instrument in the same run**, or + the comparison is between two harnesses rather than two rule sets; +- **a single policy's numbers are not a fact about the game** + (RetrospectiveAnalysis §4.1) — the panel is plural or there is no result; +- **report what fails to move.** H1 under-delivering is a result their + §6 has a branch for, and reporting only the confirming half is how a + measurement becomes advocacy. + +**Done 2026-08-08, and the result is largely negative.** See +[CB-EV-0030](../evidence/CB-EV-0030-h1-measured.md). + +**A harness defect was caught first, and it mattered.** The first run +showed `won`, `atk` and `darvo` all exactly `0` at 3+ seats under H1 — +which is the signature of games being *discarded*, not lost, because +`sweep` had `let Ok(g) = play(..) else { continue }` and never reported +its game count. **Reporting "H1 makes the game unwinnable" when the truth +might have been "our harness threw every H1 game away" would have been the +ADR-0018 family aimed at another repo's design decision.** Instrumented: +all 200 games ran in every cell. The zeros are real.