H2's ATTACK dynamics, and a construction hazard found while measuring them
Some checks failed
ci / check (push) Failing after 3s

The H2 panel measured wins, variance and DARVO but not ATTACK selection —
which is F17's actual question. attack-value now runs H2 too.

The shape is the finding. Under H1 a seat that sometimes attacks loses
everything at 3p and above. Under H2 rank-75 wins 112/173/199 against
greedy's 120/175/199, while attacking and arming DARVO. So H2 makes
occasional ATTACK affordable — it does not make it pay. rank-75 never
beats greedy in any cell, and rank-95 (always attack) still wins 0
everywhere in all three variants, so "not always-attack-optimal" holds.

F17 therefore stands: ATTACK earns its place in no mode. What changed is
that choosing it is no longer catastrophic. Whether affordable is what the
design wants is ground-game's judgement.

And a hazard: with_variant() exists because H2 assigns Problem owners at
setup and `state.variant = v` leaves them unassigned, so scoped pressure
ticks nobody and H2 measures as INERT. Three call sites had the bare
write, including cb-play's driver.

No published figure is affected, and that was checked rather than assumed:
h2-panel used the builder, and the two harnesses with the bare write had
only ever run baseline and H1, neither of which has a setup step; the
driver has never played H2. All three fixed, and
a_bare_variant_write_leaves_h2_inert now states the difference so a
regression is caught by a named test rather than by a reader wondering why
H2 did nothing.

The builder was not enough — the field is public, so the old form still
compiles. Worth knowing before the next variant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-08 16:56:27 +02:00
parent d3c2dc1375
commit 47e7941057
7 changed files with 409 additions and 4 deletions

View file

@ -2288,6 +2288,60 @@ mod tests {
}
}
/// **The hazard `with_variant` exists for, made visible.**
///
/// Setting `state.variant = H2` directly compiles and produces a
/// game where **no Problem has a scope**, so scoped pressure ticks
/// nobody and H2 measures as inert — a silently wrong result, not
/// a failure.
///
/// Three call sites had the bare write when this was found, one of
/// them the driver. This test states the difference so a
/// regression is caught by a name rather than by a puzzled reader
/// wondering why H2 did nothing.
#[test]
fn a_bare_variant_write_leaves_h2_inert() {
let base = GroundState::setup(
&Setup {
players: 4,
preset: "standard-4p".into(),
patch: Default::default(),
},
5,
)
.expect("setup");
// The wrong way.
let mut bare = base.clone();
bare.variant = Variant::H2ScopedProblemStress;
assert!(
bare.problems.values().all(|p| p.scope.is_none()),
"a bare write should leave scopes unassigned — if it does not, \
`with_variant` is no longer load-bearing and this test is stale"
);
assert!(
!bare
.end_round_events()
.iter()
.any(|e| matches!(e, GroundEvent::StressSet { .. })),
"a bare-write H2 produced pressure, so the hazard has changed shape"
);
// The right way.
let built = base.with_variant(Variant::H2ScopedProblemStress);
assert!(
built.problems.values().all(|p| p.scope.is_some()),
"`with_variant` did not assign scopes"
);
assert!(
built
.end_round_events()
.iter()
.any(|e| matches!(e, GroundEvent::StressSet { .. })),
"`with_variant` H2 produced no pressure with Problems unclaimed"
);
}
/// **The baseline is untouched by H2's new state** (T02).
///
/// `owner` and `scope` are new fields on `ProblemState`. Both are