diff --git a/evidence/CB-EV-0031-a-seat-that-does-not-regulate.md b/evidence/CB-EV-0031-a-seat-that-does-not-regulate.md index c41d27e..7807651 100644 --- a/evidence/CB-EV-0031-a-seat-that-does-not-regulate.md +++ b/evidence/CB-EV-0031-a-seat-that-does-not-regulate.md @@ -124,7 +124,50 @@ failed. when a player feels cornered — that a human table answers and 3,200 simulated games do not. -## 5. Open +## 5. The three items the review left open, now closed + +**H1-B on the DARVO extra Attack** — the delta says the extra Attack +shares the Attack resolution *"so it can self-soothe too if Stress ≥ 4"*. +CB-WP-0038 asserted it because the code shares `resolve_attack`; nothing +tested it, and the withdrawn §3 ran straight through this path. +`h1b_soothes_the_darvo_stage_attack_too` now covers it, mutation-verified. + +**Round-5 pressure did not reach the score — a real defect, not a +reporting one.** `end_round_events` scored from `self` while H1-A's +pressure went into `work`, and `score` reads Stress for the GR-E03 and +GR-E04 tiebreaks. The final round's pressure was invisible to **the two +modes CB-EV-0030 reports on**. Fixed, and the test uses the case that +actually bites: uniform pressure preserves an ordering, so it takes the +**clamp** at 5 to collapse a gap and change who wins. + +**Inert arms, and a second wrong-subject error caught on the way.** A +DARVO arm at the End of Round 5 can never advance a stage — `GameEnded` +follows immediately — and ground-game's criterion 1 is about DARVO +*mattering*. Now reported separately: + +| seats | arms | of which inert | +|---|---:|---:| +| 2p | 363 | **29** (8%) | +| 3p | 600 | 0 | +| 4p | 800 | 0 | +| 6p | 1200 | 0 | + +**The first version of that metric was wrong and equalled `darvo` in +every cell**, because it tested `g.rounds >= 5` — a property of the +*game*, not of the *event*. Every arm in every completed game was marked +inert, which briefly looked like "criterion 1 is not met after all". An +arm is inert when no `RoundEnded` follows it; that figure matches the +reviewer's independent 29-of-363. + +**Criterion 1 stands as met**: 92% of arms at 2p and all of them above are +live. + +**`regulation.rs` no longer skips setup failures silently.** They are +counted, and a short cell now **fails an assertion** rather than printing +a number a reader has to notice — which is what CB-EV-0030 §3 claimed +credit for and only half did. + +## 6. Open - **The baseline's Stress economy being unreachable (§2) deserves its own finding**, and is not raised here because it wants the plural panel of diff --git a/games/ground/examples/regulation.rs b/games/ground/examples/regulation.rs index 3884227..fceecfb 100644 --- a/games/ground/examples/regulation.rs +++ b/games/ground/examples/regulation.rs @@ -78,6 +78,15 @@ struct Cell { atk: u32, darvo: u32, peak_stress: u8, + /// Setups the engine refused. **Counted, not skipped** (CB-REV-0001 + /// #11): CB-EV-0030 §3 credited this pass with fixing silent skips + /// and only the `play` path was instrumented. + setup_fails: u32, + /// DARVO arms at the End of Round 5. `GameEnded` is pushed + /// immediately after, so the sequence never advances a stage — the + /// arm is real and can do nothing. Reported separately, because + /// ground-game's criterion 1 is asking about DARVO *mattering*. + inert_arms: u32, } fn sweep(mode: ScoringMode, variant: Variant, players: u8, reactive: bool) -> Cell { @@ -87,6 +96,8 @@ fn sweep(mode: ScoringMode, variant: Variant, players: u8, reactive: bool) -> Ce atk: 0, darvo: 0, peak_stress: 0, + setup_fails: 0, + inert_arms: 0, }; for seed in 0..200u64 { let Ok(mut st) = GroundState::setup( @@ -97,6 +108,7 @@ fn sweep(mode: ScoringMode, variant: Variant, players: u8, reactive: bool) -> Ce }, seed, ) else { + c.setup_fails += 1; continue; }; st.mode = mode; @@ -140,12 +152,25 @@ fn sweep(mode: ScoringMode, variant: Variant, players: u8, reactive: bool) -> Ce // built its headline claim on that number. Wrong subject: the // metric answered "highest value ever assigned", the prose said // "highest Stress reached". + // An arm is inert when NO `RoundEnded` follows it: the game + // ended in the same End step, so the sequence never advances a + // stage. `g.rounds >= 5` is a property of the GAME, not of the + // event, and using it marked every arm in every completed game + // as inert — which is how a metric ends up equal to the thing it + // was supposed to be a subset of. + let last_round_ended = g + .events + .iter() + .rposition(|e| matches!(e, games_ground::GroundEvent::RoundEnded { .. })); let mut held: std::collections::BTreeMap = g.state.players.keys().map(|s| (*s, START_STRESS)).collect(); c.peak_stress = c.peak_stress.max(START_STRESS); - for e in &g.events { + for (i, e) in g.events.iter().enumerate() { if matches!(e, games_ground::GroundEvent::DarvoTriggered { .. }) { c.darvo += 1; + if last_round_ended.is_none_or(|last| i > last) { + c.inert_arms += 1; + } } if let games_ground::GroundEvent::StressSet { player, stress } = e { held.insert(*player, *stress); @@ -154,6 +179,22 @@ fn sweep(mode: ScoringMode, variant: Variant, players: u8, reactive: bool) -> Ce } let _ = held; } + // Reported as a REFUSAL, not left for a reader to notice a short + // column. CB-REV-0001 #11: `games != 200` was printed, never asserted. + assert_eq!( + c.games + c.setup_fails, + 200, + "{players}p {variant:?}: {} games ran and {} setups failed — the cell is short, \ + so every number in it is over a sample nobody chose", + c.games, + c.setup_fails + ); + if c.setup_fails > 0 { + eprintln!( + " !! {players}p {variant:?}: {} setups refused", + c.setup_fails + ); + } c } @@ -169,12 +210,12 @@ fn main() { ] { println!("{vlabel}"); println!(" greedy (regulates) reactive (does not)"); - println!("seats games won atk darvo peak games won atk darvo peak"); + println!("seats games won atk darvo peak games won atk darvo peak inert"); for players in [2u8, 3, 4, 6] { let g = sweep(ScoringMode::SharedGround, variant, players, false); let r = sweep(ScoringMode::SharedGround, variant, players, true); println!( - " {players}p {:>4} {:>4} {:>4} {:>5} {:>4} {:>4} {:>4} {:>4} {:>5} {:>4}", + " {players}p {:>4} {:>4} {:>4} {:>5} {:>4} {:>4} {:>4} {:>4} {:>5} {:>4} {:>6}", g.games, g.won, g.atk, @@ -184,7 +225,8 @@ fn main() { r.won, r.atk, r.darvo, - r.peak_stress + r.peak_stress, + r.inert_arms ); } println!(); diff --git a/games/ground/src/lib.rs b/games/ground/src/lib.rs index c6f2423..818de67 100644 --- a/games/ground/src/lib.rs +++ b/games/ground/src/lib.rs @@ -1572,9 +1572,15 @@ impl GroundState { .map_or(self.lead, |i| seats[(i + 1) % seats.len()]); // GR-R09: after Round 5's End the game ends and scoring applies. + // **Scored from `work`, not `self`** (CB-REV-0001 #13). H1-A's + // pressure goes into `work` above, and `score` reads Stress for + // the GR-E03 and GR-E04 tiebreaks — so scoring from `self` made + // the final round's pressure invisible to exactly the two modes + // CB-EV-0030 reports on. Under the baseline `work` is a clone and + // this is a no-op. if self.round >= 5 { events.push(GroundEvent::GameEnded { - outcome: self.score(), + outcome: work.score(), }); return events; } @@ -2312,6 +2318,106 @@ mod tests { ); } + /// **H1-B applies to the DARVO extra Attack** (review M12). + /// + /// The delta says so outright — *"DARVO-stage extra Attack uses + /// the same Attack resolution (so it can self-soothe too if + /// Stress >= 4)"* — and CB-WP-0038 asserted it on the strength of + /// the code sharing `resolve_attack`. **Nothing tested it**, and + /// CB-EV-0031's withdrawn mechanism story ran through this path. + #[test] + fn h1b_soothes_the_darvo_stage_attack_too() { + let soothe_count = |variant: Variant| { + let mut s = setup(3, variant, 5); + let seats: Vec = s.players.keys().copied().collect(); + let (owner, target) = (seats[0], seats[1]); + s.step = RoundStep::Resolve; + s.players.get_mut(&owner).expect("o").stress = 4; + s.players.get_mut(&owner).expect("o").darvo = DarvoStage::Attack; + s.darvo_targets.insert( + owner, + DarvoTarget { + problem: None, + player: Some(target), + }, + ); + s.resolution_events() + .iter() + .filter(|e| { + matches!(e, GroundEvent::StressSet { player, stress } + if *player == owner && *stress == 3) + }) + .count() + }; + assert_eq!( + soothe_count(Variant::H1ProblemStress), + 1, + "the DARVO extra Attack did not self-soothe, though the delta says it \ + shares the same Attack resolution" + ); + assert_eq!( + soothe_count(Variant::Baseline), + 0, + "the baseline soothed on a DARVO stage Attack" + ); + } + + /// **Round-5 pressure must reach the score** (review #13). + /// + /// `end_round_events` scored from `self` while H1-A's pressure + /// went into `work`, so the last round's Stress was invisible to + /// the GR-E03 and GR-E04 tiebreaks — the two modes CB-EV-0030 + /// reports on. + /// + /// Uniform pressure preserves an ordering, so the case that bites + /// is the **clamp**: two seats with equal claims, one at 5 and one + /// at 4, become equal once both are pushed to 5. + #[test] + fn the_last_rounds_pressure_reaches_the_tiebreak() { + let mut s = setup(3, Variant::H1ProblemStress, 5); + s.mode = ScoringMode::CommonProblem; + s.round = 5; + let seats: Vec = s.players.keys().copied().collect(); + + // A CONSTRUCTED board, not the deal: the tiebreak only + // decides among seats tied at the top, and no natural 3p deal + // splits evenly while leaving a Problem unclaimed. + // + // Two Problems worth 4 (one each to the two seats, so they + // tie on personal) and one worth 1 left unclaimed, so H1-A + // fires. Claimed = 8 against a threshold of 7. + let ids: Vec = s.problems.keys().copied().collect(); + for id in &ids { + s.problems.remove(id); + } + let mk = |value: u8, claimed_by: Option| ProblemState { + suit: Suit::Repair, + value, + face_up: true, + denied: false, + claimed_by, + protected_this_round: false, + }; + s.problems.insert(1, mk(4, Some(seats[0]))); + s.problems.insert(2, mk(4, Some(seats[1]))); + s.problems.insert(3, mk(1, None)); + s.players.get_mut(&seats[0]).expect("a").stress = 5; + s.players.get_mut(&seats[1]).expect("b").stress = 4; + + let events = s.end_round_events(); + let Some(GroundEvent::GameEnded { outcome }) = events.last() else { + panic!("round 5 did not end the game"); + }; + assert!(outcome.group_success, "the fixture must qualify"); + let pre = s.score(); + assert_ne!( + outcome.winners, pre.winners, + "the outcome ignores the final round's pressure — scored from `self`. \ + pre={:?} post={:?}", + pre.winners, outcome.winners + ); + } + /// **`rules_delta.yaml`'s `unchanged:` list is ground-game's claim /// about their own experiment, and it is checkable.** /// diff --git a/reviews/CB-REV-0001-h1.md b/reviews/CB-REV-0001-h1.md index a8051be..123e05f 100644 --- a/reviews/CB-REV-0001-h1.md +++ b/reviews/CB-REV-0001-h1.md @@ -4,7 +4,11 @@ The tier-L review CB-WP-0038 owed (InnerLoop Step 2). One round: challenge, then response. Run 2026-08-08 by a separate agent against CB-WP-0038/CB-EV-0030 and CB-WP-0039/CB-EV-0031. -> **Verdict: not approvable as submitted.** Thirteen challenges, five +> **Verdict: not approvable as submitted. All thirteen are now closed** — +> see the response under each. **Re-review is owed** before any of this +> travels: the corrections were made by the author of the errors. +> +> **Original verdict:** Thirteen challenges, five > rated FATAL. **Every FATAL is conceded.** Nothing from either evidence > file had reached `ground-game`, which is the only reason this is a > correction rather than a retraction. @@ -82,7 +86,7 @@ Stress is ever added. ATTACK is the sole inbound pressure. | 6 | `baseline_is_bit_for_bit_what_it_was` compared two identically-constructed states — inert against its own threat; `#[serde(skip)]` on `variant` left 57/57 green | **conceded.** Replaced by `the_variant_reaches_the_hash_and_baseline_is_not_a_change`, which asserts baseline and H1 hash **differently**. The reviewer's M6 now goes red | | 7 | the `unchanged:` test checked **3 of 7** entries; making SOLVE illegal under H1 left it green | **conceded.** Now compares `legal_commands` per seat (catches M9), checks relation slots at the **boundary** rather than on an empty seat, and asserts the DARVO arm is at 5 (catches M7). Both verified red | | 8 | "criteria met" rests on a forced move — `reactive` ranks ATTACK at 10 and picks it only when the gate leaves nothing else | **conceded as a limitation, recorded, not fixed.** It is true that the instrument cannot show a null result once H1-A reaches Stress 4. That is a real weakness of the measurement and is now stated in CB-EV-0031 §4 | -| 9 | three claimed properties had no failing test: H1-A's ordering, H1-B on the DARVO extra Attack, H1-B on an OU-cancelled Attack | **two fixed** — `h1a_pressure_arms_darvo_in_the_same_round_end` and `h1b_does_not_soothe_an_ou_cancelled_attack`, both mutation-verified. **The DARVO extra-Attack path remains untested** and is carried as open | +| 9 | three claimed properties had no failing test: H1-A's ordering, H1-B on the DARVO extra Attack, H1-B on an OU-cancelled Attack | **all three fixed and mutation-verified**: `h1a_pressure_arms_darvo_in_the_same_round_end`, `h1b_does_not_soothe_an_ou_cancelled_attack`, `h1b_soothes_the_darvo_stage_attack_too` | ## The rest @@ -90,14 +94,21 @@ Stress is ever added. ATTACK is the sole inbound pressure. here.** Conceded: nothing between the two positions touches the attacker's Stress, so the clause cannot be checked in this kernel. The honest statement replaces the claim that it was got right. -- **11 — `regulation.rs` still skips setup failures silently.** Conceded; - the credit CB-EV-0030 §3 took was half-earned. The `play` path was - instrumented, the `setup` path was not. -- **13 — round-5 DARVO arms are counted but can never act**, and - `end_round_events` scores from `self` rather than `work`, so round-5 H1-A - Stress is invisible to the CommonProblem tiebreak. **Accepted as a real - defect in the other two modes**, which CB-EV-0030 reports on. Carried - open. +- **11 — `regulation.rs` still skips setup failures silently.** Conceded + and **fixed**: counted, and a short cell now fails an assertion rather + than printing a number a reader must notice. +- **13 — round-5 arms, and scoring from `self`.** Both **fixed**, and the + second was a real defect rather than a reporting one: `score` reads + Stress for the GR-E03/GR-E04 tiebreaks, so the final round's pressure + was invisible to the two modes CB-EV-0030 reports on. Inert arms are now + reported separately — **29 of 363 at 2p, none above** — which matches the + reviewer's independent figure and leaves criterion 1 met. + + **Fixing it produced one more wrong-subject error**, caught before it + was reported: the first inert-arm metric tested `g.rounds >= 5`, a + property of the *game* rather than the *event*, so it marked every arm + in every completed game inert and briefly read as "criterion 1 fails + after all". - **12 — reproduction and sample robustness: no problem found.** Every number reproduced; no conclusion was seed-specific. **The failures were of construction and interpretation, not sampling.**