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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue