25 test games, no faults — and F17 gets the artifact that changes what it
Some checks failed
ci / check (push) Failing after 4s

says

Five games per seat count, 2-6 players. NO ANOMALIES: every game reaches
5 rounds with an outcome, no stalls, no stress above the cap, no
over-claimed Problems.

But the series showed something a crash never would. DARVO NEVER FIRED IN
25 GAMES and stress never exceeded 2. Measured wider: GreedyPolicy plays
ATTACK exactly ZERO times in 10,000 selections across 500 games.

THAT NUMBER IS ABOUT OUR BOT, NOT THE GAME. bot.rs ranks
`Action::Attack => 10`, below everything. Reporting "the game gives no
incentive to attack" from a policy we programmed to rank attack last would
have been CB-WP-0025's C4 error committed again -- a single policy's
behaviour presented as the game's.

So the artifact varies exactly one number: ATTACK's rank in an otherwise
identical policy, 200 games per cell.

  rank 10 (below all):   132/165/190/200/200 wins,    0 attacks,    0 DARVO
  rank 75 (above SUPPORT): 132/165/190/200/200 wins, 315-923,      13-218
  rank 95 (above SOLVE):     0/0/0/0/0 wins,        1400-5170,   400-1000

THE MIDDLE ROW IS THE FINDING. Identical win counts at every seat count,
while attacking hundreds of times and arming DARVO repeatedly. Attacking
is not punished -- it is INERT with respect to the goal. Group success is
a function of SOLVE alone, and ATTACK costs anything only when it ranks
above SOLVE and displaces it.

The maintainer was right and the reason is sharper than his phrasing:
there is no incentive because there is no PATH. ATTACK's effects (Stress,
Rivalry, DARVO) feed nothing that decides group_success.

Bounded honestly to SHARED GROUND. Blame costs PERSONAL score, so ATTACK
may earn its place in GR-E03 and GR-E04 -- which have never been played to
the end (F14), and that is where to ask next. And this is NOT a claim the
game is broken: DARVO is the pattern the game is about not falling into,
so a self-destructive ATTACK may be the design. The question for
ground-game is whether the namesake mechanic being unreachable in
competent co-op play is intended.

F17 promoted from note to raised, with games/ground/examples/attack-value.rs
as its reproduction. Register: 18 findings, 8 with a resolving
reproduction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-07 10:08:47 +02:00
parent d5b3d27b43
commit f4eeddd726
2 changed files with 176 additions and 11 deletions

View file

@ -0,0 +1,143 @@
//! F17's reproduction: what is ATTACK worth? (CB-WP-0029 follow-up.)
//!
//! The maintainer reported *"there is no incentive to play attacks as long
//! as I have positive cards"*. F17 sat as a note because nothing
//! demonstrated it. This is the artifact.
//!
//! GreedyPolicy ranks Attack at 10, below everything (bot.rs). So its zero
//! attacks measure OUR HEURISTIC, not the game. The test is a policy
//! identical in every other respect with Attack promoted: if it wins as
//! often, attacking is neutral; much less, avoiding it is correct play;
//! more, and greedy is simply wrong.
use cb_game_runtime::{ScenarioGame, Setup};
use cb_kernel::PlayerId;
use games_ground::bot::{play, Choice, GreedyPolicy, Policy};
use games_ground::{Action, GroundCommand, GroundState};
/// Greedy's ordering with ATTACK promoted to the top of the action list.
/// A copy of the ranking, not a call into it: `GreedyPolicy::rank` is
/// private, and the point is to vary exactly one number.
struct Attacker(i32);
impl Policy for Attacker {
fn name(&self) -> &'static str {
"attacker"
}
fn choose(
&mut self,
state: &GroundState,
seat: PlayerId,
legal: &[GroundCommand],
_p: bool,
) -> Choice {
let gated = state
.players
.get(&seat)
.is_some_and(|p| p.stress >= 4 && !p.freedom_gate_lifted);
let rank = |c: &GroundCommand| -> i32 {
match c {
GroundCommand::SelectAction {
action, problem, ..
} => match action {
Action::Ground if gated => 100,
Action::Attack => self.0, // <-- the ONE varying number (greedy: 10)
Action::Solve
if problem
.and_then(|p| state.problems.get(&p))
.is_some_and(|p| p.claimed_by.is_some()) =>
{
5
}
Action::Solve => 90,
Action::Investigate => 80,
Action::Support => 70,
Action::Ground => 60,
},
GroundCommand::SpendFreedom => {
if gated {
96
} else {
0
}
}
GroundCommand::Reveal | GroundCommand::Resolve | GroundCommand::EndRound => -1,
_ => 50,
}
};
let mut best = 0usize;
for (i, c) in legal.iter().enumerate() {
if rank(c) > rank(&legal[best]) {
best = i;
}
}
Choice::Command(best)
}
}
fn sweep(
name: &str,
players: u8,
mk: &dyn Fn(u64, u8) -> Vec<Box<dyn Policy>>,
) -> (u32, u32, u32, u32) {
let (mut games, mut won, mut atk, mut darvo) = (0, 0, 0, 0);
for seed in 0..200u64 {
let Ok(st) = GroundState::setup(
&Setup {
players,
preset: format!("standard-{players}p"),
patch: Default::default(),
},
seed,
) else {
continue;
};
let mut ps = mk(seed, players);
let Ok(g) = play(st, &mut ps) else { continue };
games += 1;
if g.state.outcome.as_ref().is_some_and(|o| o.group_success) {
won += 1;
}
for (_, c) in &g.steps {
if let GroundCommand::SelectAction {
action: Action::Attack,
..
} = c
{
atk += 1;
}
}
for e in &g.events {
if matches!(e, games_ground::GroundEvent::DarvoTriggered { .. }) {
darvo += 1;
}
}
}
let _ = name;
(games, won, atk, darvo)
}
fn main() {
println!("F17: does ATTACK cost you the game, or does our bot just avoid it?\n");
// The gradient, not just the extremes: greedy ranks Attack at 10,
// below everything. 75 puts it above SUPPORT and below INVESTIGATE --
// "attack when convenient". 95 is "attack whenever legal".
println!(" rank=10 (greedy) rank=75 (sometimes) rank=95 (always)");
println!("seats won atk darvo won atk darvo won atk darvo");
for players in [2u8, 3, 4, 5, 6] {
let cell = |r: i32, p: u8| {
sweep("x", p, &move |_s, n| {
(0..n)
.map(|_| Box::new(Attacker(r)) as Box<dyn Policy>)
.collect()
})
};
let (_, gw, ga, gd) = sweep("greedy", players, &|_s, p| {
(0..p)
.map(|_| Box::new(GreedyPolicy) as Box<dyn Policy>)
.collect()
});
let (_, mw, ma, md) = cell(75, players);
let (_, aw, aa, ad) = cell(95, players);
println!(" {players}p {gw:>4} {ga:>4} {gd:>5} {mw:>4} {ma:>4} {md:>5} {aw:>4} {aa:>4} {ad:>5}");
}
println!("\n(200 games per cell; greedy shown as the rank=10 column)");
}

View file

@ -44,7 +44,7 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
| F14 | unplayed | note | — | — | 2026-08-01 | clay-borg |
| F15 | underdetermined | note | — | — | 2026-08-05 | clay-borg |
| F16 | inconsistent | withdrawn | games/ground/examples/difficulty.rs | counterexample | 2026-08-05 | clay-borg |
| F17 | degenerate | note | — | — | 2026-08-06 | ground-game |
| F17 | degenerate | raised | games/ground/examples/attack-value.rs | counterexample | 2026-08-06 | ground-game |
| F18 | inert | raised | — | — | 2026-08-06 | clay-borg |
| F19 | degenerate | applied | crates/cb-render-html/src/lib.rs::overhead_table | counterexample | 2026-08-06 | clay-borg |
| F20 | inert | applied | crates/cb-render-html/src/lib.rs::ending_page | counterexample | 2026-08-06 | clay-borg |
@ -128,16 +128,38 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
seat-position and single-drop-zone proxies added. **They are proxies.**
Nothing here lays out a browser, and the gap CB-EV-0026 named — that no
gate measures whether a player can play — is unclosed.
- **F17 — no incentive to ATTACK while holding useful Solutions.**
Reported from play, 2026-08-06: *"there is no incentive to play attacks
as long as I have positive cards."* If true, ATTACK is a dead branch for
most of a game, which would make GR-A06/A07/A08 and the whole Rivalry
half of the relation system reachable only when a player is out of good
options. **`note`, not a finding**: no artifact demonstrates it yet. One
is cheap — count ATTACK selections across the policy panel and compare
against hand quality — and until it exists this may not go to
`ground-game` (GameDesign §3.1). **Owner is ground-game if it survives**;
it would be a design finding, not an engine one.
- **F17 — ATTACK cannot affect whether the table succeeds.** Reported
from play as *"there is no incentive to play attacks as long as I have
positive cards"*, and **the artifact now exists**:
`games/ground/examples/attack-value.rs`, 200 games per cell, one number
varied — ATTACK's rank in an otherwise identical policy.
| ATTACK ranked | wins (2/3/4/5/6p) | attacks | DARVO armed |
|---|---|---:|---:|
| 10 (below all) | 132 / 165 / 190 / 200 / 200 | 0 | 0 |
| 75 (above SUPPORT) | **132 / 165 / 190 / 200 / 200** | 315923 | 13218 |
| 95 (above SOLVE) | **0 / 0 / 0 / 0 / 0** | 14005170 | 4001000 |
**The middle row is the finding: identical win counts at every seat
count**, while attacking hundreds of times and arming DARVO. Attacking
is not punished — it is **inert with respect to the goal**. Group success
is a function of SOLVE alone, and ATTACK only costs anything when it
ranks above SOLVE and displaces it.
**So the maintainer was right and the reason is sharper than the
phrasing.** There is no incentive because there is no *path*: ATTACK's
effects (Stress, Rivalry, DARVO) feed nothing that decides
`group_success`.
**Bounded to SHARED GROUND.** These are co-op games. Blame costs
*personal* score, so ATTACK may well earn its place in GR-E03 and
GR-E04 — **which have never been played to the end (F14)**, and that is
where this question should be asked next.
**Not a claim that the game is broken.** DARVO is the pattern the game
is *about* not falling into; a self-destructive ATTACK may be the
design. The question for `ground-game` is whether the namesake mechanic
being unreachable in competent co-op play is intended.
- **F18 — the engine imports one of nineteen edition files, so the cards
cannot say what they do.** Reported as *"I don't understand the GROUND
card"* — which is not a design gap: `Actions.csv` carries that card's