H2's ATTACK dynamics, and a construction hazard found while measuring them
Some checks failed
ci / check (push) Failing after 3s
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:
parent
d3c2dc1375
commit
47e7941057
7 changed files with 409 additions and 4 deletions
|
|
@ -96,7 +96,57 @@ the best evidence in this file: **the scoping is what saves it**, not any
|
||||||
other difference between the packages. Their optional A/B was worth
|
other difference between the packages. Their optional A/B was worth
|
||||||
running.
|
running.
|
||||||
|
|
||||||
## 5. What this does not say
|
## 5. ATTACK — affordable under H2, still not profitable
|
||||||
|
|
||||||
|
Added after the first report, because the H2 panel measured wins, variance
|
||||||
|
and DARVO and **not ATTACK selection**, which is F17's actual question.
|
||||||
|
|
||||||
|
`attack-value`'s rank-75 policy — *attack when convenient* — SHARED
|
||||||
|
GROUND, wins with attacks and DARVO arms in brackets:
|
||||||
|
|
||||||
|
| seats | baseline | H1 | H2 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2p | 132 (604 atk, 81 arms) | 66 (180, 59) | **65 (248, 102)** |
|
||||||
|
| 3p | 165 (592, 125) | **0** (0, 0) | **112 (250, 52)** |
|
||||||
|
| 4p | 190 (923, 218) | **0** (0, 0) | **173 (626, 165)** |
|
||||||
|
| 6p | 200 (389, 31) | **0** (0, 0) | **199 (299, 45)** |
|
||||||
|
|
||||||
|
**The shape is the finding.** Under H1 a seat that sometimes attacks loses
|
||||||
|
everything at 3p and above. Under H2 it wins **112 / 173 / 199** — against
|
||||||
|
greedy's 120 / 175 / 199 — *while* attacking and arming DARVO.
|
||||||
|
|
||||||
|
> **H2 makes occasional ATTACK affordable. It does not make it pay.**
|
||||||
|
|
||||||
|
rank-75 never beats greedy in any cell; at best it draws level (199 vs 199
|
||||||
|
at 6p). And **rank-95 — always attack — still wins 0 everywhere**, in all
|
||||||
|
three variants, so the *"not always-attack-optimal"* property 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, or whether ATTACK should actually reward, is
|
||||||
|
ground-game's judgement.
|
||||||
|
|
||||||
|
## 6. A construction hazard, found while measuring this
|
||||||
|
|
||||||
|
`GroundState::with_variant()` exists because H2 assigns Problem owners at
|
||||||
|
setup and `state.variant = v` leaves them unassigned — **scoped pressure
|
||||||
|
then 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.rs` 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 are 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. That is worth knowing before the next variant.
|
||||||
|
|
||||||
|
## 7. What this does not say
|
||||||
|
|
||||||
- **No felt-play.** H2's central claim is about *motivation* — a bonded
|
- **No felt-play.** H2's central claim is about *motivation* — a bonded
|
||||||
pair caring about each other's card — and §3 says as much. 200-game
|
pair caring about each other's card — and §3 says as much. 200-game
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,8 @@ fn sweep(
|
||||||
st.mode = mode;
|
st.mode = mode;
|
||||||
// CB-WP-0038 T03: the SAME instrument measures both rule sets in
|
// CB-WP-0038 T03: the SAME instrument measures both rule sets in
|
||||||
// the same run. Two harnesses would compare harnesses.
|
// the same run. Two harnesses would compare harnesses.
|
||||||
st.variant = variant;
|
// `with_variant`, never a field write (see regulation.rs).
|
||||||
|
let st = st.with_variant(variant);
|
||||||
let mut ps = mk(players);
|
let mut ps = mk(players);
|
||||||
// CB-WP-0038 T03: a game the engine REFUSES is not a game the
|
// CB-WP-0038 T03: a game the engine REFUSES is not a game the
|
||||||
// table lost, and `else { continue }` made the two identical.
|
// table lost, and `else { continue }` made the two identical.
|
||||||
|
|
@ -211,6 +212,10 @@ fn main() {
|
||||||
"H1 h1-problem-stress",
|
"H1 h1-problem-stress",
|
||||||
games_ground::Variant::H1ProblemStress,
|
games_ground::Variant::H1ProblemStress,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"H2 h2-scoped-problem-stress",
|
||||||
|
games_ground::Variant::H2ScopedProblemStress,
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
println!("================ {vlabel} ================\n");
|
println!("================ {vlabel} ================\n");
|
||||||
for (label, mode) in [
|
for (label, mode) in [
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,10 @@ fn sweep(mode: ScoringMode, variant: Variant, players: u8, reactive: bool) -> Ce
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
st.mode = mode;
|
st.mode = mode;
|
||||||
st.variant = variant;
|
// `with_variant`, never a field write: H2 assigns Problem
|
||||||
|
// owners at setup, and the bare write leaves them unassigned —
|
||||||
|
// scoped pressure then ticks nobody and H2 measures as inert.
|
||||||
|
let st = st.with_variant(variant);
|
||||||
// The dealt state, kept so the peak metric can read the real
|
// The dealt state, kept so the peak metric can read the real
|
||||||
// starting Stress rather than trust a constant.
|
// starting Stress rather than trust a constant.
|
||||||
let started = st.clone();
|
let started = st.clone();
|
||||||
|
|
|
||||||
|
|
@ -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).
|
/// **The baseline is untouched by H2's new state** (T02).
|
||||||
///
|
///
|
||||||
/// `owner` and `scope` are new fields on `ProblemState`. Both are
|
/// `owner` and `scope` are new fields on `ProblemState`. Both are
|
||||||
|
|
|
||||||
|
|
@ -408,7 +408,10 @@ fn run_game<'a, R: BufRead + 'a, W: Write + 'a>(
|
||||||
initial.mode = config.mode;
|
initial.mode = config.mode;
|
||||||
// CB-WP-0038: set beside `mode` and before the hash, so a recorded
|
// CB-WP-0038: set beside `mode` and before the hash, so a recorded
|
||||||
// session replays under the variant it was played under.
|
// session replays under the variant it was played under.
|
||||||
initial.variant = config.variant;
|
// `with_variant`, never a field write: H2 assigns Problem owners at
|
||||||
|
// setup, and the bare write would leave them unassigned — a silently
|
||||||
|
// wrong game rather than a failing one.
|
||||||
|
let initial = initial.with_variant(config.variant);
|
||||||
let initial_json = serde_json::to_value(&initial).map_err(|e| e.to_string())?;
|
let initial_json = serde_json::to_value(&initial).map_err(|e| e.to_string())?;
|
||||||
let initial_hash = cb_events::state_hash_hex(&initial);
|
let initial_hash = cb_events::state_hash_hex(&initial);
|
||||||
|
|
||||||
|
|
|
||||||
141
trials/2026-08-08-1648-2.yaml
Normal file
141
trials/2026-08-08-1648-2.yaml
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
scenario: ground/cb-play-session
|
||||||
|
description: recorded by cb-play (CB-WP-0008 T02)
|
||||||
|
covers: []
|
||||||
|
provisional: false
|
||||||
|
provisional_owner: ''
|
||||||
|
provisional_raised: ''
|
||||||
|
ruled: ''
|
||||||
|
ruled_by: ''
|
||||||
|
ruled_note: ''
|
||||||
|
encodes_u_item: ''
|
||||||
|
seed: 2
|
||||||
|
setup:
|
||||||
|
players: 3
|
||||||
|
preset: standard-3p
|
||||||
|
patch: {}
|
||||||
|
commands:
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 1
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 1
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 1
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 2
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 2
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 2
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 3
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 2
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 2
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 4
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 4
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 4
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 3
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 4
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 4
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
expect:
|
||||||
|
events: []
|
||||||
|
state: {}
|
||||||
|
rejects: []
|
||||||
|
state_hash: 71b243fdd585e678b6876a6bd8aec74f2490438db5ce852c8d0320f24550070f
|
||||||
149
trials/2026-08-08-1648.yaml
Normal file
149
trials/2026-08-08-1648.yaml
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
scenario: ground/cb-play-session
|
||||||
|
description: recorded by cb-play (CB-WP-0008 T02)
|
||||||
|
covers: []
|
||||||
|
provisional: false
|
||||||
|
provisional_owner: ''
|
||||||
|
provisional_raised: ''
|
||||||
|
ruled: ''
|
||||||
|
ruled_by: ''
|
||||||
|
ruled_note: ''
|
||||||
|
encodes_u_item: ''
|
||||||
|
seed: 1
|
||||||
|
setup:
|
||||||
|
players: 3
|
||||||
|
preset: standard-3p
|
||||||
|
patch: {}
|
||||||
|
commands:
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 2
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 1
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 1
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SUPPORT
|
||||||
|
target: P2
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 3
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 3
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: P2
|
||||||
|
cmd: respond_to_support
|
||||||
|
args:
|
||||||
|
response: accept_bond
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 4
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: INVESTIGATE
|
||||||
|
problem: 4
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 2
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 3
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 3
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 4
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SUPPORT
|
||||||
|
target: P3
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SUPPORT
|
||||||
|
target: P1
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SUPPORT
|
||||||
|
target: P1
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: P3
|
||||||
|
cmd: respond_to_support
|
||||||
|
args:
|
||||||
|
response: accept_bond
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
expect:
|
||||||
|
events: []
|
||||||
|
state: {}
|
||||||
|
rejects: []
|
||||||
|
state_hash: b15834bdf1b30ef0e3d84bdba10395d0fa4efd16086d24b59a6080d08b1804f2
|
||||||
Loading…
Add table
Add a link
Reference in a new issue