CB-WP-0038: variant selection, H1 implemented, and H1 measured
Some checks failed
ci / check (push) Failing after 3s

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 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-08 00:50:08 +02:00
parent 627992bc85
commit a928b5925c
15 changed files with 928 additions and 28 deletions

65
editions/catalog.yaml Normal file
View file

@ -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.

View file

@ -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 05).","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.
1 action_id title symbol_id resolution_order target tagline rules_text stress_restriction designer_note
2 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.
3 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.
4 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.
5 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).
6 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.

View file

@ -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 26 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 12 for 2 players, 13 for 34 players, and 14 for 56 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 34 players, and 12 at 56 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 05. 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 03 allows any Action. At Stress 45, 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 34 players, and 9 for 56 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."
1 order section heading body
2 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.
3 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.
4 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.
5 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.
6 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.
7 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.
8 7 Round 2. Reveal Reveal all selected Actions simultaneously. Players using GROUND choose their mode after seeing the revealed Actions.
9 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.
10 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.
11 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).
12 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.
13 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.
14 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.
15 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.
16 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.
17 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.
18 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.
19 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.
20 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.
21 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).
22 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.

View file

@ -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 05, 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 attackers Stress was **≥ 4 before that Attacks 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 packages `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.

View file

@ -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": "26",
"duration": "515 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"
]
}

View file

@ -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 05
# 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

View file

@ -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 T01T05 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.

View file

@ -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<Box<dyn Policy>>,
) -> (u32, u32, u32, u32) {
let (mut games, mut won, mut atk, mut darvo) = (0, 0, 0, 0);
let mut errs: Vec<String> = 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<dyn Policy>)
.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<dyn Policy>)
.map(|_| Box::new(GreedyPolicy) as Box<dyn Policy>)
.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<dyn Policy>)
.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)");
}

View file

@ -173,6 +173,16 @@ pub struct GroundState {
pub darvo_targets: BTreeMap<PlayerId, DarvoTarget>,
/// 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<Outcome>,
/// 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<Self, String> {
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<GroundEvent>,
) {
// 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<GroundEvent> {
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<u32> = 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<u8> = 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<GroundEvent> {
let mut s = setup(3, variant, 3);
let seats: Vec<PlayerId> = 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<PlayerId> = 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,
}

View file

@ -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.

View file

@ -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,

View file

@ -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<u8> = Vec::new();
let summary =

View file

@ -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<Mode, String> {
// **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<u8> = 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<u8> = 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<u8> = 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<u8> = 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<u8> = Vec::new();
let summary = table::play(&config, "".as_bytes(), &mut out).expect("game");

View file

@ -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);

View file

@ -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.