CB-REV-0001: the adversarial review, and it was not approvable
Some checks failed
ci / check (push) Failing after 4s
Some checks failed
ci / check (push) Failing after 4s
Thirteen challenges, five FATAL, all five conceded. Nothing had reached ground-game, which is the only reason this is a correction and not a retraction. The worst: `Reactive` was not "greedy with one preference changed". It differed in five, including SpendFreedom — ranked 95 unconditionally against greedy's `95 if gated else 0` — so the seat burned its Freedom token in round one of every game. A second change to the exact mechanism under study, and every number in CB-EV-0031 was measuring it. The pass claimed ADR-0018's one-varying-parameter discipline in its own workplan while violating it. GreedyPolicy::rank is now public and the policy delegates, overriding one match arm, so the control is structurally true. Withdrawn entirely: "H1-B suppresses DARVO in the attacker". Disabling H1-B under the corrected policy changes the arm count by exactly zero. The pass hedged the wrong variable — it disclaimed "the number 2" and defended "the direction", and the direction is what failed. The supporting inference was invalid anyway: final Stress cannot show who armed, because DarvoEnded resets the stage and REVERSE gives its owner -2. Corrected: criterion 1 was failed on the greedy column while the pass's own printed table showed 31-1000 arms in the other columns — the selective-column move, in the file that names it. "Peak Stress was 1" was a maximum over StressSet payloads, not held state (true: 2); the baseline game count was 1,600 not 3,200; and "a reckless policy plays identically to a careful one" is refuted by this repo's own rank-95 policy. Inert controls replaced, each verified red against the reviewer's own mutation: the baseline hash test compared two identically-constructed states (serde(skip) on variant left 57/57 green); the `unchanged:` test checked 3 of 7 entries and passed with SOLVE made illegal; H1-A's ordering and H1-B's OU-cancel path had no test at all. edition-check now covers catalog.yaml and rules_delta.yaml, whose digests CB-WP-0038 claimed and never recorded — the review found it and reported it unverified rather than absent, which was the right call. Still open: H1-B on the DARVO extra-Attack path is untested, regulation.rs still skips setup failures silently, and round-5 arms are counted though they can never act. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
38106791a6
commit
041c0e7c3e
9 changed files with 489 additions and 101 deletions
|
|
@ -323,7 +323,20 @@ impl Policy for RandomPolicy {
|
|||
pub struct GreedyPolicy;
|
||||
|
||||
impl GreedyPolicy {
|
||||
fn rank(state: &GroundState, seat: PlayerId, cmd: &GroundCommand) -> i32 {
|
||||
/// The ranking, **public so a variant policy can override exactly one
|
||||
/// arm and inherit the rest** (CB-WP-0039, after review).
|
||||
///
|
||||
/// It was private, so `regulation.rs` re-typed an abridged copy and
|
||||
/// called it "greedy with one preference changed". It differed in
|
||||
/// five places — including `SpendFreedom`, which the copy ranked 95
|
||||
/// unconditionally where this ranks it 0 unless the gate is biting,
|
||||
/// so the "reactive" seat burned its Freedom token in round one of
|
||||
/// every game. **A second change to the exact mechanism the pass was
|
||||
/// studying**, and every number in CB-EV-0031 was measuring it.
|
||||
///
|
||||
/// Making this callable removes the possibility rather than testing
|
||||
/// for it: a caller that delegates cannot drift.
|
||||
pub fn rank(state: &GroundState, seat: PlayerId, cmd: &GroundCommand) -> i32 {
|
||||
let gated = state
|
||||
.players
|
||||
.get(&seat)
|
||||
|
|
|
|||
|
|
@ -2092,14 +2092,40 @@ mod tests {
|
|||
s
|
||||
}
|
||||
|
||||
/// **The load-bearing control.** A variant system that perturbs
|
||||
/// the baseline invalidates every measurement this repo has.
|
||||
/// **The load-bearing control**, rewritten after review found the
|
||||
/// first version inert.
|
||||
///
|
||||
/// It compared two states built by the *same* `setup` call, both
|
||||
/// then assigned `Variant::Baseline` — bitwise identical by
|
||||
/// construction, so the assertion could only fail if hashing were
|
||||
/// nondeterministic. It carried no information about variants at
|
||||
/// all. Adding `#[serde(skip)]` to `variant` — which is exactly
|
||||
/// the "a scenario replayed under the wrong variant diverges
|
||||
/// silently" failure the workplan named — left all 57 tests green.
|
||||
///
|
||||
/// This asserts the two properties that were claimed:
|
||||
/// **the variant reaches the hash**, and **selecting the baseline
|
||||
/// is not a change**.
|
||||
#[test]
|
||||
fn baseline_is_bit_for_bit_what_it_was() {
|
||||
fn the_variant_reaches_the_hash_and_baseline_is_not_a_change() {
|
||||
for players in [2u8, 3, 6] {
|
||||
for seed in 0..8u64 {
|
||||
let base = setup(players, Variant::Baseline, seed);
|
||||
let mut default_built = GroundState::setup(
|
||||
let h1 = setup(players, Variant::H1ProblemStress, seed);
|
||||
|
||||
// 1. The variant is IN the hash. `#[serde(skip)]` on
|
||||
// the field makes these equal, which is the whole
|
||||
// silent-divergence failure.
|
||||
assert_ne!(
|
||||
cb_events::state_hash_hex(&base),
|
||||
cb_events::state_hash_hex(&h1),
|
||||
"{players}p seed {seed}: the variant does not reach the state hash, \
|
||||
so a recording cannot say which rules it was played under"
|
||||
);
|
||||
|
||||
// 2. And selecting the baseline explicitly is not a
|
||||
// change from selecting nothing.
|
||||
let untouched = GroundState::setup(
|
||||
&Setup {
|
||||
players,
|
||||
preset: format!("standard-{players}p"),
|
||||
|
|
@ -2108,12 +2134,10 @@ mod tests {
|
|||
seed,
|
||||
)
|
||||
.expect("setup");
|
||||
// Untouched: whatever `setup` produces IS baseline.
|
||||
assert_eq!(default_built.variant, Variant::Baseline);
|
||||
default_built.variant = Variant::Baseline;
|
||||
assert_eq!(untouched.variant, Variant::Baseline, "the default moved");
|
||||
assert_eq!(
|
||||
cb_events::state_hash_hex(&base),
|
||||
cb_events::state_hash_hex(&default_built),
|
||||
cb_events::state_hash_hex(&untouched),
|
||||
"{players}p seed {seed}: selecting the baseline changed it"
|
||||
);
|
||||
}
|
||||
|
|
@ -2226,6 +2250,68 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// **H1-A lands before the DARVO arm check** (review M5).
|
||||
///
|
||||
/// The delta orders it "+1 Stress, then clamp, then DARVO arm
|
||||
/// check as today". CB-WP-0038 claimed this was got right and
|
||||
/// nothing tested it: moving the pressure after the arm check
|
||||
/// left every test green.
|
||||
///
|
||||
/// A seat at 4 with a Problem unclaimed must arm **in the same
|
||||
/// round end** — pressure takes it to 5, and the check sees it.
|
||||
#[test]
|
||||
fn h1a_pressure_arms_darvo_in_the_same_round_end() {
|
||||
let mut s = setup(3, Variant::H1ProblemStress, 7);
|
||||
for p in s.players.values_mut() {
|
||||
p.stress = 4;
|
||||
p.darvo = DarvoStage::Off;
|
||||
}
|
||||
assert!(s.problems.values().any(|p| p.claimed_by.is_none()));
|
||||
let events = s.end_round_events();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|e| matches!(e, GroundEvent::DarvoTriggered { .. })),
|
||||
"pressure took the seat to 5 but the arm check did not see it, \
|
||||
so H1-A is ordered after it"
|
||||
);
|
||||
// And the order is visible in the event stream: the Stress
|
||||
// must be set before the trigger, not after.
|
||||
let first_stress = events
|
||||
.iter()
|
||||
.position(|e| matches!(e, GroundEvent::StressSet { .. }));
|
||||
let first_trigger = events
|
||||
.iter()
|
||||
.position(|e| matches!(e, GroundEvent::DarvoTriggered { .. }));
|
||||
assert!(
|
||||
first_stress < first_trigger,
|
||||
"the arm check precedes the pressure in the event stream"
|
||||
);
|
||||
}
|
||||
|
||||
/// **H1-B must not fire on a GROUND—OU cancellation** (review M11).
|
||||
///
|
||||
/// `resolve_attack` has two cancel paths and the existing test
|
||||
/// exercised only Protection — it passed `&Default::default()`
|
||||
/// for `ou_cancels`, so the other path was never reached.
|
||||
#[test]
|
||||
fn h1b_does_not_soothe_an_ou_cancelled_attack() {
|
||||
let mut s = setup(3, Variant::H1ProblemStress, 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 = 4;
|
||||
let mut ou = std::collections::BTreeSet::new();
|
||||
ou.insert((a, t));
|
||||
let mut events = Vec::new();
|
||||
s.resolve_attack(a, t, &ou, &mut events);
|
||||
assert!(
|
||||
!events
|
||||
.iter()
|
||||
.any(|e| matches!(e, GroundEvent::StressSet { player, .. } if *player == a)),
|
||||
"an Attack cancelled by GROUND—OU still soothed the attacker"
|
||||
);
|
||||
}
|
||||
|
||||
/// **`rules_delta.yaml`'s `unchanged:` list is ground-game's claim
|
||||
/// about their own experiment, and it is checkable.**
|
||||
///
|
||||
|
|
@ -2260,6 +2346,61 @@ mod tests {
|
|||
base.solution_deck, h1.solution_deck,
|
||||
"{players}p: deck moved"
|
||||
);
|
||||
|
||||
// solve_legality / support / ground_modes: the SAME menu
|
||||
// in the SAME position. Comparing setup fields could not
|
||||
// see a delta that forbids an action -- the review made
|
||||
// SOLVE illegal under H1 and this test stayed green.
|
||||
let seats: Vec<PlayerId> = h1.players.keys().copied().collect();
|
||||
for seat in &seats {
|
||||
let mut lb: Vec<String> = crate::bot::legal_commands(&base, *seat)
|
||||
.iter()
|
||||
.map(|c| format!("{c:?}"))
|
||||
.collect();
|
||||
let mut lh: Vec<String> = crate::bot::legal_commands(&h1, *seat)
|
||||
.iter()
|
||||
.map(|c| format!("{c:?}"))
|
||||
.collect();
|
||||
lb.sort();
|
||||
lh.sort();
|
||||
assert_eq!(
|
||||
lb, lh,
|
||||
"{players}p {seat}: H1 changed which commands are legal"
|
||||
);
|
||||
}
|
||||
|
||||
// relation_slots: 2, asserted at the BOUNDARY. The old
|
||||
// check asked `has_free_slot` of a seat with no relations,
|
||||
// which holds for any capacity >= 1.
|
||||
if seats.len() >= 3 {
|
||||
let mut s = h1.clone();
|
||||
s.relations
|
||||
.insert(Pair::new(seats[0], seats[1]), Relation::Bond);
|
||||
assert!(s.has_free_slot(seats[0]), "one relation leaves a slot");
|
||||
s.relations
|
||||
.insert(Pair::new(seats[0], seats[2]), Relation::Bond);
|
||||
assert!(
|
||||
!s.has_free_slot(seats[0]),
|
||||
"{players}p: two relations must fill both slots -- capacity moved"
|
||||
);
|
||||
}
|
||||
|
||||
// darvo_stage_table: the arm sits at Stress 5. Every
|
||||
// Problem claimed, so H1-A cannot add pressure and the
|
||||
// only question is where the threshold is.
|
||||
let mut s = h1.clone();
|
||||
for p in s.players.values_mut() {
|
||||
p.stress = 4;
|
||||
}
|
||||
for p in s.problems.values_mut() {
|
||||
p.claimed_by = Some(seats[0]);
|
||||
}
|
||||
assert!(
|
||||
!s.end_round_events()
|
||||
.iter()
|
||||
.any(|e| matches!(e, GroundEvent::DarvoTriggered { .. })),
|
||||
"{players}p: DARVO armed below Stress 5 -- the stage table moved"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue