CB-WP-0050 T02: F31 falsified — the module is unreachable, then real
Some checks failed
ci / check (push) Failing after 4s
Some checks failed
ci / check (push) Failing after 4s
GateAttackPolicy ranks ATTACK above GROUND at the stress gate and delegates everything else. A PROBE, not a better bot: it wins 12/100 where greedy wins 60/100 under the same module, and the panel's banner says so, because a column that looks like a policy comparison will be read as one. (a) THE MODULE IS UNREACHABLE IN THE PRINTED GAME. Selected alone it still never fires: peak Stress never exceeds 2, so the gate never bites and no ATTACK is ever gated. A module can be unreachable because of an aspect it does not name -- attack_relief needs a problem_stress module before it can act at all, and nothing in its own declaration says so. (b) ONCE REACHABLE IT IS REAL. Holding the probe fixed and varying only the module: group wins 12->21, 19->28, 50->57 at 3/4/6p, and DARVO arms 299->221, 294->213, 334->164. Under flat pressure the same shape appears in DARVO alone: h1 arms 200 against flat_any_open's 300/400/600. Stated as the conditional it is: GIVEN seats that attack at the gate. Not advice to attack, and not evidence against F17. AND IT CHANGES WHAT WE TOLD GROUND-GAME. CB-EV-0030 rejected H1 with policies that never attacked, so H1-B was inert for every game behind that verdict. The rejection stands on flat pressure alone -- wins are 0 either way -- but "H1-B does nothing" was never established and is now known to be false. We owe them that correction. Sensitivity: vary only the ATTACK rank. At 10 (greedy) and +55 (module-aware) the module never fires; at 110 it fires in every game. Nothing about the module changed -- only whether any seat gave it a turn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1b6a68fb39
commit
4a72420e01
4 changed files with 224 additions and 11 deletions
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
use cb_game_runtime::{ScenarioGame, Setup};
|
||||
use cb_kernel::PlayerId;
|
||||
use games_ground::bot::{play, GreedyPolicy, ModuleAwarePolicy, Policy};
|
||||
use games_ground::bot::{play, GateAttackPolicy, GreedyPolicy, ModuleAwarePolicy, Policy};
|
||||
use games_ground::config::Configuration;
|
||||
use games_ground::{GroundState, ScoringMode};
|
||||
|
||||
|
|
@ -94,6 +94,10 @@ fn fired(m: &str, g: &games_ground::bot::BotGame) -> bool {
|
|||
enum Seat {
|
||||
Greedy,
|
||||
ModuleAware,
|
||||
/// F31's falsifier: a seat that attacks at the stress gate, so
|
||||
/// `attack_relief.self_soothe_ge4` gets an opportunity at all. **A
|
||||
/// probe, not a recommendation** — its win rate is not advice.
|
||||
GateAttack,
|
||||
}
|
||||
|
||||
fn sweep(cfg: &Configuration, players: u8, who: Seat) -> Cell {
|
||||
|
|
@ -120,6 +124,7 @@ fn sweep(cfg: &Configuration, players: u8, who: Seat) -> Cell {
|
|||
.map(|_| match who {
|
||||
Seat::Greedy => Box::new(GreedyPolicy) as Box<dyn Policy>,
|
||||
Seat::ModuleAware => Box::new(ModuleAwarePolicy) as Box<dyn Policy>,
|
||||
Seat::GateAttack => Box::new(GateAttackPolicy) as Box<dyn Policy>,
|
||||
})
|
||||
.collect();
|
||||
let g = match play(st, &mut ps) {
|
||||
|
|
@ -227,6 +232,11 @@ fn main() {
|
|||
println!("ModuleAwarePolicy. A row flat under `grdy` and moving under");
|
||||
println!("`mod` is a module that needs a seat able to see it — not a");
|
||||
println!("module that does nothing (F27's distinction).\n");
|
||||
println!("`atk!` is GateAttackPolicy, which attacks at the stress gate so");
|
||||
println!("attack_relief gets an opportunity at all (F31). It is a PROBE:");
|
||||
println!("its win rate is not a recommendation. The peak/darvo/atk columns");
|
||||
println!("are its games, because they are the only ones where ATTACK");
|
||||
println!("happens.\n");
|
||||
println!("`never fired: UNMEASURED` means no seat created that module's");
|
||||
println!("precondition in any game, so its contribution to the row is not");
|
||||
println!("a null result — it is an absence of evidence. The other numbers");
|
||||
|
|
@ -235,8 +245,8 @@ fn main() {
|
|||
for players in [3u8, 4, 6] {
|
||||
println!("{players} players");
|
||||
println!(
|
||||
" {:<18} {:<38} {:>5} {:>5} {:>6} {:>6} {:>6}",
|
||||
"aspect", "module / profile", "grdy", "mod", "peak", "darvo", "atk"
|
||||
" {:<18} {:<38} {:>5} {:>5} {:>5} {:>6} {:>6} {:>6}",
|
||||
"aspect", "module / profile", "grdy", "mod", "atk!", "peak", "darvo", "atk"
|
||||
);
|
||||
for (aspect, name, cfg) in points() {
|
||||
let cfg = match cfg {
|
||||
|
|
@ -257,6 +267,7 @@ fn main() {
|
|||
}
|
||||
let g = sweep(&cfg, players, Seat::Greedy);
|
||||
let m = sweep(&cfg, players, Seat::ModuleAware);
|
||||
let a = sweep(&cfg, players, Seat::GateAttack);
|
||||
// **A module that never acted is UNMEASURED, not null.**
|
||||
// Printing win counts for it invites the reading that it does
|
||||
// nothing, when what happened is that no seat ever created
|
||||
|
|
@ -269,7 +280,10 @@ fn main() {
|
|||
// every game and drives group success to 0, which is a real
|
||||
// result about that module even though its ATTACK half never
|
||||
// gets an opportunity.
|
||||
let never: Vec<&String> = m
|
||||
// **The probe's row decides "never fired", not the others.**
|
||||
// A module is unreachable only if the seat built to reach it
|
||||
// could not either.
|
||||
let never: Vec<&String> = a
|
||||
.unfired
|
||||
.iter()
|
||||
.filter(|(_, n)| **n == GAMES)
|
||||
|
|
@ -288,8 +302,8 @@ fn main() {
|
|||
)
|
||||
};
|
||||
println!(
|
||||
" {aspect:<18} {name:<38} {:>5} {:>5} {:>6} {:>6} {:>6}{note}",
|
||||
g.won, m.won, m.peak_stress, m.darvo, m.attacks
|
||||
" {aspect:<18} {name:<38} {:>5} {:>5} {:>5} {:>6} {:>6} {:>6}{note}",
|
||||
g.won, m.won, a.won, a.peak_stress, a.darvo, a.attacks
|
||||
);
|
||||
}
|
||||
println!();
|
||||
|
|
|
|||
|
|
@ -1356,6 +1356,71 @@ impl Policy for ModuleAwarePolicy {
|
|||
}
|
||||
}
|
||||
|
||||
/// A seat that **attacks at the stress gate** — F31's falsifier.
|
||||
///
|
||||
/// ## This is a probe, not a better bot
|
||||
///
|
||||
/// `attack_relief.self_soothe_ge4` acts only on an uncancelled ATTACK by
|
||||
/// a seat at Stress >= 4. `module-panel` found **zero ATTACKs in every
|
||||
/// cell**, so the module has never had an opportunity and no claim about
|
||||
/// it is supported in either direction — including the one embedded in
|
||||
/// H1's rejection, whose other half is this module.
|
||||
///
|
||||
/// The obstacle is a ranking, not the game: `GreedyPolicy` ranks
|
||||
/// `Ground` at 100 when the gate bites against `Attack`'s 10, so at
|
||||
/// exactly the position where self-soothe would pay, GROUND wins.
|
||||
/// `ModuleAwarePolicy` adds 55 and still loses.
|
||||
///
|
||||
/// **So this exists to create the precondition, and nothing else.** It is
|
||||
/// not offered as good play and its win rate is not a recommendation.
|
||||
/// The question it answers is whether the module *does* anything once
|
||||
/// reachable — measured by holding this policy fixed and varying only
|
||||
/// the module, which is the only comparison that isolates it.
|
||||
///
|
||||
/// **It delegates**, overriding exactly one arm. CB-WP-0039 re-typed an
|
||||
/// abridged greedy and called it "one preference changed"; it differed
|
||||
/// in five, and every number measuring it was measuring the difference.
|
||||
pub struct GateAttackPolicy;
|
||||
|
||||
impl GateAttackPolicy {
|
||||
pub fn rank(state: &GroundState, seat: PlayerId, cmd: &GroundCommand) -> i32 {
|
||||
let gated = state
|
||||
.players
|
||||
.get(&seat)
|
||||
.is_some_and(|p| p.stress >= 4 && !p.freedom_gate_lifted);
|
||||
match cmd {
|
||||
// ONE ARM OVERRIDDEN. At the gate, ATTACK outranks GROUND's
|
||||
// 100 — which is the whole intervention.
|
||||
GroundCommand::SelectAction {
|
||||
action: Action::Attack,
|
||||
..
|
||||
} if gated => 110,
|
||||
other => ModuleAwarePolicy::rank(state, seat, other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Policy for GateAttackPolicy {
|
||||
fn name(&self) -> &'static str {
|
||||
"gate-attack"
|
||||
}
|
||||
fn choose(
|
||||
&mut self,
|
||||
state: &GroundState,
|
||||
seat: PlayerId,
|
||||
legal: &[GroundCommand],
|
||||
_may_pass: bool,
|
||||
) -> Choice {
|
||||
let mut best = 0;
|
||||
for (i, c) in legal.iter().enumerate() {
|
||||
if Self::rank(state, seat, c) > Self::rank(state, seat, &legal[best]) {
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
Choice::Command(best)
|
||||
}
|
||||
}
|
||||
|
||||
/// **A policy is bound by what its seat can see** ([ADR-0023]).
|
||||
///
|
||||
/// `Policy::choose` takes the whole `GroundState`, which carries every
|
||||
|
|
@ -1545,6 +1610,8 @@ mod blindness_tests {
|
|||
.unwrap_or_else(|e| panic!("{players}p {mode:?} seed {seed}: {e}"));
|
||||
is_blind(|| ModuleAwarePolicy, &st, seat)
|
||||
.unwrap_or_else(|e| panic!("{players}p {mode:?} seed {seed}: {e}"));
|
||||
is_blind(|| GateAttackPolicy, &st, seat)
|
||||
.unwrap_or_else(|e| panic!("{players}p {mode:?} seed {seed}: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1884,6 +1951,51 @@ mod blindness_tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// **The probe actually attacks** — F31's falsifier must create the
|
||||
/// precondition, or it falsifies nothing.
|
||||
///
|
||||
/// Asserted on real games rather than on the ranking, because a rank
|
||||
/// of 110 proves an intention and the panel needs an occurrence.
|
||||
#[test]
|
||||
fn the_gate_attack_probe_creates_the_precondition_it_exists_for() {
|
||||
let mut attacks = 0;
|
||||
let mut gated_attacks = 0;
|
||||
for seed in 0..40u64 {
|
||||
let st = deal(4, seed).with_config(crate::config::Configuration::select("h1").unwrap());
|
||||
let mut ps: Vec<Box<dyn Policy>> = (0..4)
|
||||
.map(|_| Box::new(GateAttackPolicy) as Box<dyn Policy>)
|
||||
.collect();
|
||||
let Ok(g) = play(st, &mut ps) else { continue };
|
||||
for (_, c) in &g.steps {
|
||||
if let GroundCommand::SelectAction {
|
||||
action: Action::Attack,
|
||||
..
|
||||
} = c
|
||||
{
|
||||
attacks += 1;
|
||||
}
|
||||
}
|
||||
// The module needs an ATTACK **at Stress >= 4**, which is a
|
||||
// stronger condition than "an ATTACK happened".
|
||||
if g.events
|
||||
.iter()
|
||||
.any(|e| matches!(e, crate::GroundEvent::StressSet { stress, .. } if *stress >= 4))
|
||||
&& attacks > 0
|
||||
{
|
||||
gated_attacks += 1;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
attacks > 0,
|
||||
"the probe never attacked, so it cannot falsify F31"
|
||||
);
|
||||
assert!(
|
||||
gated_attacks > 0,
|
||||
"the probe attacked but never with a seat at the gate, which is \
|
||||
the only position attack_relief.self_soothe_ge4 acts on"
|
||||
);
|
||||
}
|
||||
|
||||
/// And the shipped policies are blind.
|
||||
#[test]
|
||||
fn every_shipped_policy_is_blind_to_what_its_seat_cannot_see() {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
|
|||
| F26 | inert | ruled | `crates/cb-render-html/src/lib.rs::a_scoped_table_says_what_a_scope_does` | counterexample | 2026-08-08 | ground-game |
|
||||
| F27 | unplayed | reported | `games/ground/examples/scenario-panel.rs` | counterexample | 2026-08-08 | clay-borg |
|
||||
| F29 | inconsistent | applied | `games_ground::edition::card_text_tests::which_scenarios_are_mechanically_distinct` | counterexample | 2026-08-08 | ground-game |
|
||||
| F31 | unplayed | raised | `games/ground/examples/module-panel.rs` | counterexample | 2026-08-08 | clay-borg |
|
||||
| F31 | unplayed | reported | `games/ground/examples/module-panel.rs` | counterexample | 2026-08-08 | clay-borg |
|
||||
| F30 | degenerate | ruled | `games/ground/examples/scenario-panel.rs` | counterexample | 2026-08-08 | ground-game |
|
||||
| F28 | underdetermined | applied | `games_ground::tests::the_mastery_rating_and_the_shared_score_count_different_things` | counterexample | 2026-08-08 | ground-game |
|
||||
|
||||
|
|
@ -95,6 +95,49 @@ are the whole register and `make design` reads them from here.
|
|||
**no claim about this module is supported in either direction**, and
|
||||
CB-EV-0030's H1 verdict rests on its flat-pressure half alone.
|
||||
|
||||
**FALSIFIED, 2026-08-08, and the answer has two parts.**
|
||||
`GateAttackPolicy` — a probe that ranks ATTACK above GROUND at the
|
||||
stress gate, delegating everything else — was built for exactly this.
|
||||
|
||||
**(a) The module is unreachable in the printed game, not merely
|
||||
unchosen.** With `attack_relief.self_soothe_ge4` selected *alone* the
|
||||
probe still never attacks and the row still reads `never fired`:
|
||||
**peak Stress never exceeds 2**, so the gate never bites and no ATTACK
|
||||
is ever gated. The baseline has no inbound Stress (F17's observation
|
||||
from the other side), so this module needs a `problem_stress` module
|
||||
before it can act at all. **A module can be unreachable because of an
|
||||
aspect it does not name.**
|
||||
|
||||
**(b) Once reachable it is real.** Holding the probe fixed and varying
|
||||
only the module — the only comparison that isolates it:
|
||||
|
||||
| seats | `scoped` | `scoped_plus_attack_soothe` | group wins | DARVO arms |
|
||||
|---|---|---|---|---|
|
||||
| 3p | 12 | **21** | +9 | 299 → 221 |
|
||||
| 4p | 19 | **28** | +9 | 294 → 213 |
|
||||
| 6p | 50 | **57** | +7 | 334 → 164 |
|
||||
|
||||
Under flat pressure the same shape appears in DARVO alone: `h1` against
|
||||
`problem_stress.flat_any_open` arms 200 against 300 / 400 / 600 at
|
||||
3/4/6p, with group wins 0 in both.
|
||||
|
||||
**Stated as the conditional it is:** *given seats that attack at the
|
||||
gate*, self-soothe raises group success by ~7–9 per 100 and cuts DARVO
|
||||
arming by a quarter to a half. The probe is deliberately poor play — it
|
||||
wins 12/100 where greedy wins 60/100 under the same module — so this is
|
||||
**not** a recommendation to attack, and the row is not evidence against
|
||||
F17.
|
||||
|
||||
**What this changes about H1.** CB-EV-0030 rejected H1 with policies
|
||||
that never attacked, so H1-B was inert for every game behind that
|
||||
verdict. The rejection stands on flat pressure alone — group wins are 0
|
||||
with or without self-soothe — but *"H1-B does nothing"* was never
|
||||
established and is now known to be false.
|
||||
|
||||
**Sensitivity:** vary only the ATTACK rank. At `Attack = 10` (greedy)
|
||||
and at `+55` (module-aware) the module never fires; at `110` it fires
|
||||
in every game. Nothing about the module changed — only whether any seat
|
||||
ever gave it a turn.
|
||||
- **F30 — SCN_04 is materially harder at 2 players.** 52/100 group
|
||||
success against 67 and 73, with **every other parameter held by the
|
||||
edition itself**: same deal shape, same 6 available points, same
|
||||
|
|
|
|||
|
|
@ -84,16 +84,60 @@ The numbers in such a row are real for the modules that did fire. Saying
|
|||
which one did not is what stops the row being read as a verdict on all of
|
||||
them.
|
||||
|
||||
## Task: F31's falsifier
|
||||
|
||||
```task
|
||||
id: CB-WP-0050-T02
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
`GateAttackPolicy` ranks ATTACK above GROUND at the stress gate and
|
||||
delegates everything else. **A probe, not a better bot** — its win rate
|
||||
is not a recommendation, and the panel's banner says so, because a
|
||||
column that looks like a policy comparison will be read as one.
|
||||
|
||||
**Controls:**
|
||||
- **it actually attacks, and at the gate** — asserted on real games, not
|
||||
on the ranking: a rank of 110 proves an intention where the panel needs
|
||||
an occurrence;
|
||||
- **blind** (ADR-0023);
|
||||
- **the probe's row decides "never fired"**, not the others — a module is
|
||||
unreachable only if the seat built to reach it could not reach it
|
||||
either.
|
||||
|
||||
**Done 2026-08-08.** F31 answered, in two parts.
|
||||
|
||||
**(a) The module is unreachable in the printed game.** Selected *alone*
|
||||
it still never fires: peak Stress never exceeds 2, so the gate never
|
||||
bites and no ATTACK is ever gated. **A module can be unreachable because
|
||||
of an aspect it does not name** — `attack_relief` needs a
|
||||
`problem_stress` module before it can act at all, and nothing in its own
|
||||
declaration says so.
|
||||
|
||||
**(b) Once reachable it is real.** Holding the probe fixed and varying
|
||||
only the module: group wins +9 / +9 / +7 at 3/4/6p, and DARVO arming down
|
||||
by a quarter to a half.
|
||||
|
||||
Stated as the conditional it is: *given seats that attack at the gate*.
|
||||
The probe wins 12/100 where greedy wins 60/100 under the same module, so
|
||||
this is not advice to attack and not evidence against F17.
|
||||
|
||||
**And it changes what we told ground-game about H1.** CB-EV-0030 rejected
|
||||
H1 with policies that never attacked, so H1-B was inert for every game
|
||||
behind that verdict. The rejection stands on flat pressure alone — wins
|
||||
are 0 either way — but *"H1-B does nothing"* was never established and is
|
||||
now known to be false.
|
||||
|
||||
## Not done here
|
||||
|
||||
- **The panel measures SHARED GROUND only.** Mode × module is a
|
||||
three-dimensional sweep and nothing yet says the modules behave the
|
||||
same under the competitive modes — where, per CB-EV-0033, *who wins*
|
||||
moves even when group success does not.
|
||||
- **F31 has no falsifier running.** A seat that attacks at the gate would
|
||||
settle whether `attack_relief.self_soothe_ge4` is real or merely
|
||||
unreachable; until one exists, no claim about that module is supported
|
||||
in either direction.
|
||||
- **ground-game has not been told about F31.** It bears on a verdict we
|
||||
already reported to them (H1's rejection), and a correction we owe is
|
||||
worth more than one we volunteer.
|
||||
- **The scope term is inert at round-one positions** (CB-WP-0049 T04), so
|
||||
`problem_stress.scoped`'s measured effect is driven by later rounds.
|
||||
The panel does not report per-round effect, and that is where the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue