CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
//! cb-play — play GROUND headless, against bots (CB-WP-0008 T02;
|
|
|
|
|
|
//! precursor of `cb play`).
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! INTENT stage 0 asks for a **CLI player**. Everything else on that list
|
|
|
|
|
|
//! shipped in CB-WP-0001..0006; until this binary existed, the engine was
|
|
|
|
|
|
//! correct, measured and replayable, and nothing could play it.
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! Exit codes: 0 the game finished, 1 it did not. There is no partial
|
|
|
|
|
|
//! success — a session that ends without an outcome is a failure, and
|
|
|
|
|
|
//! saying otherwise is how a stalled game passes for a played one.
|
|
|
|
|
|
|
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit
Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
|
|
|
|
mod hotseat;
|
CB-WP-0011-T01: the inspector shows everything, and a gate says so
The renderer moves out of the play loop into inspect.rs and grows from
24 to 42 of the 43 leaf paths a populated GroundView carries. What it
had been dropping was the whole DARVO state machine, the whole GROUND
practice, the scoring mode, Focus tokens, the discard pile, per-seat
protection, and every part of the outcome except the headline.
The load-bearing half is every_view_field_is_classified, which walks
the serialized view for leaf paths and requires each to be listed as
rendered (with a token the output must contain) or omitted (with a
reason). Paths rather than keys: 'problem' occurs under a DARVO target,
a GROUND choice and a Selection, and a key-set walk would let one of
the three vouch for the other two.
Four M-D1-MUT controls, each red for its stated reason. The
unclassified-field control fired for real on the first run --
players.*.hand, a field the gate's own author had missed.
2026-08-02 02:38:38 +02:00
|
|
|
|
mod inspect;
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
mod table;
|
|
|
|
|
|
|
|
|
|
|
|
use table::Config;
|
|
|
|
|
|
|
|
|
|
|
|
const USAGE: &str = "\
|
|
|
|
|
|
usage: cb-play [--seed N] [--players N] [--seat N]... [--bot greedy|random]
|
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit
Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
|
|
|
|
[--replay DIR] [--all-bots] [--record FILE] [--serve PORT]
|
2026-08-02 02:45:55 +02:00
|
|
|
|
cb-play --inspect PATH [--as SEAT|spectator]
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
|
2026-08-02 02:45:55 +02:00
|
|
|
|
play:
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
--seed N game seed (default 1); the same seed replays identically
|
|
|
|
|
|
--players N 2..6 seats (default 3)
|
|
|
|
|
|
--seat N a seat you play, 0-based, repeatable (default 0)
|
|
|
|
|
|
--all-bots no human seats; watch a game play itself
|
|
|
|
|
|
--bot KIND policy for every other seat: greedy (default) or random
|
|
|
|
|
|
--replay DIR write a .cbreplay bundle of the finished game to DIR
|
|
|
|
|
|
--record FILE write the finished game as a scenario YAML
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
|
--trial FILE write a trial log: what the player said, bound to where
|
CB-WP-0038: variant selection, H1 implemented, and H1 measured
ground-game packages hypotheses as selectable rules variants — a catalog,
a rules_delta.yaml, and prose — and their note is explicit that CSV text
alone is not executable here. So the kernel gains a Variant in game state:
in the state, therefore in the hash, therefore in the recording, because a
scenario replayed under a different variant would diverge silently.
Baseline is bit-for-bit what it was, asserted across seat counts and
seeds. A variant system that perturbs the baseline invalidates every
measurement this repo has.
H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on
their own defects: "unclaimed" misread as face-up-and-unsolved, and the
attacker's Stress read after the attack's effects. Their `unchanged:` list
is asserted rather than trusted — that list is their claim about their own
experiment.
Measured, and three of their four criteria fail. DARVO arm rate is still
0 under greedy; ATTACK selection does not rise and falls for the rank-75
policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats.
The mechanism is not the assumed one: greedy answers the pressure by
regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the
arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under
competent play.
A harness defect was caught before the claim: sweep discarded refused
games silently and never reported its count, so "nobody won" and "nothing
played" printed identically. Reporting H1 as unwinnable on that basis
would have been the ADR-0018 family aimed at another repo's design. All
200 games ran in every cell; the zeros are real.
Chaos d8 = 8 — the window's first override, redrew L against a structural
L, so it changed nothing. Window 3 recorded in ChaosRollHistory.
NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1
result may reach ground-game until it has run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:50:08 +02:00
|
|
|
|
--variant V rules package: ground-darvo-r0 (default) or
|
|
|
|
|
|
h1-problem-stress (experimental; ground-game H1)
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
--pace P speed (default) or interactive: how much ornamentation
|
|
|
|
|
|
is performed. Never changes the game -- the recording is
|
|
|
|
|
|
byte-identical either way (specs/Ornamentation.md)
|
GR-E03 and GR-E04 played to the end — F14 closed, and the reason they were
unplayed was ours
Tier S (a fix and a measurement inside a boundary; chaos d8=4 from
CB-WP-0029's roll, no override).
cb-play built EVERY game with ScoringMode::SharedGround and passed an
empty patch. The mode was settable in scenarios and not from the driver,
so two of the three shipped modes were unreachable from the only way
anyone actually plays. F14 sat open for a week because nobody could reach
the thing it was about.
--mode added. All three now play out and give DIFFERENT WINNERS FROM
IDENTICAL PLAY: shared -> all four seats (mastery 4), common -> P3 alone
(top personal scorer), coalitions -> P1+P2 (best Bond network, 4>3>2).
Same 37 commands, three answers.
AND THEY ANSWER F17'S OPEN QUESTION. I had flagged that ATTACK might earn
its place where Blame costs personal score. It does not, in any mode:
SHARED GROUND 132/165/190/200 -> identical free but pointless
COMMON PROBLEM 59/52/48/44 -> 59/52/48/34 a cost at six seats
BONDED COALITIONS 131/134/132/116 -> 59/52/48/34 roughly halved
The coalitions row has a mechanism and the data confirms it unprompted.
GR-A07 flips a Bond to a Rivalry on Attack, and GR-E04 scores Bond
NETWORKS -- so attacking destroys the thing that scores. And the attacking
numbers in E04 are IDENTICAL to E03's, which is exactly what that
predicts: break every Bond and each seat is a coalition of one, so GR-E04
degenerates into GR-E03. That check was not designed; it fell out.
F14 -> applied. F17 strengthened and no longer bounded to co-op: ATTACK
has no mode in which it helps, and one where it actively destroys your
score.
Still framed as a question rather than a verdict. DARVO is the pattern the
game is about not falling into, so a self-destructive ATTACK may be the
design. What ground-game has to decide is whether the namesake mechanic
being unreachable in competent play -- in all three modes -- is intended.
make all: exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:38:27 +02:00
|
|
|
|
--mode M scoring mode: shared (GR-E02), common (GR-E03),
|
|
|
|
|
|
coalitions (GR-E04). Default shared.
|
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit
Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
|
|
|
|
--serve PORT play human seats in a browser on 127.0.0.1:PORT instead
|
|
|
|
|
|
of on the terminal; 0 lets the OS pick. Prints a URL
|
|
|
|
|
|
carrying a per-process token — without it the page is
|
|
|
|
|
|
refused (ADR-0007).
|
2026-08-02 02:45:55 +02:00
|
|
|
|
|
|
|
|
|
|
inspect a recorded game — a .cbreplay bundle directory or a scenario YAML:
|
|
|
|
|
|
--inspect PATH render the table after every recorded step
|
|
|
|
|
|
--as WHO whose projection to render: a 0-based seat, or
|
|
|
|
|
|
spectator (default). A seat sees only its own hand.
|
|
|
|
|
|
|
|
|
|
|
|
The two modes take disjoint flags: inspecting reads a recording, it does
|
|
|
|
|
|
not deal one, so a seed or a seat count would be silently ignored.
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
";
|
|
|
|
|
|
|
2026-08-02 02:45:55 +02:00
|
|
|
|
/// The two things this binary does. Separate rather than one `Config`
|
|
|
|
|
|
/// with optional halves: `--inspect --seed 9` is not a request anyone
|
|
|
|
|
|
/// can mean, and a flag that is accepted and ignored is worse than one
|
|
|
|
|
|
/// that is refused.
|
|
|
|
|
|
enum Mode {
|
|
|
|
|
|
Play(Config),
|
|
|
|
|
|
Inspect {
|
|
|
|
|
|
source: std::path::PathBuf,
|
|
|
|
|
|
eyes: inspect::Eyes,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
CB-WP-0047: all four boards, and every mode named on the page
The modes were already implemented; nothing had ever COMPARED them. The
scenarios were not implemented at all: edition::deal has taken a
scenario_id since it was written and the only caller passed the literal
"SCN_01", so 15 of 20 Problem cards had never been dealt by anything.
The seam was the whole mechanism and it sat unused, with nothing red
because nothing asked.
Scenario is now state (serde default SCN_01, so all 26 recordings replay
unchanged), selected by preset `scn-03-4p` with `standard-Np` still
meaning SCN_01, and by --scenario/SCENARIO= accepting ids, numbers or
titles, validated against the edition rather than a pattern.
The threshold now comes off the Scenario card, closing F25's hardcoded
5/7/9. The first version of that control was worthless and mutation said
so: all four scenarios print 5/7/9, so reverting to the bands left it
green. Split threshold_from() so it can be handed a card that disagrees.
The header read `scoring CommonProblem` where the Mode card is titled
COMMON PROBLEM, PERSONAL EDGE -- the defect CB-WP-0034 deleted from the
move buttons, still standing on the line that says what winning means.
The coverage probe was matching that Debug output and went red when it
was fixed: third instance (CB-WP-0024, CB-WP-0034). Page now carries the
premise, the mode's rules text, and the tiebreak.
scenario-panel plays 4x3x3. Findings: SCN_01 and SCN_02 are the same
board (identical cells, pinned by a characterisation test); SCN_04 is
the hard board at 2p (52% vs 67/73%, the only deck needing two Repair);
and group success is EXACTLY equal across all three modes in all 36
cells, because greedy never reads state.mode -- filed F27, the two
competitive modes are scoring lenses over cooperative play.
F28: SHARED GROUND's mastery subtracts penalties from the claimed COUNT
where the mode card's shared score is claimed VALUE. Raised, not fixed;
scoring is ground-game's to rule on.
Also fixes design.py reporting a backticked path as no reproduction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:51:46 +02:00
|
|
|
|
/// A scenario as the player names it, as the edition names it.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Accepts `2`, `02`, `scn-02`, `SCN_02` and the title's first word, so
|
|
|
|
|
|
/// that `--scenario confidence` works — the ids are the edition's
|
|
|
|
|
|
/// vocabulary and *"Broken Confidence"* is the player's.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// **Validated against the edition, never against a pattern.** `SCN_09`
|
|
|
|
|
|
/// looks exactly like an id; the error names what the edition has.
|
|
|
|
|
|
fn normalise_scenario(v: &str) -> Result<String, String> {
|
|
|
|
|
|
let known = games_ground::edition::scenarios()?;
|
|
|
|
|
|
let want = v.trim().to_ascii_lowercase();
|
|
|
|
|
|
let digits: String = want.chars().filter(|c| c.is_ascii_digit()).collect();
|
|
|
|
|
|
let by_id = |s: &games_ground::edition::ScenarioText| {
|
|
|
|
|
|
s.id.to_ascii_lowercase() == want
|
|
|
|
|
|
|| (!digits.is_empty()
|
|
|
|
|
|
&& s.id
|
|
|
|
|
|
.rsplit('_')
|
|
|
|
|
|
.next()
|
|
|
|
|
|
.is_some_and(|n| n.parse::<u32>().ok() == digits.parse::<u32>().ok()))
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(s) = known.iter().find(|s| by_id(s)) {
|
|
|
|
|
|
return Ok(s.id.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
// By title, so the four cards can be named by what is printed on them.
|
|
|
|
|
|
if let Some(s) = known
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|s| s.title.to_ascii_lowercase().contains(&want) && !want.is_empty())
|
|
|
|
|
|
{
|
|
|
|
|
|
return Ok(s.id.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(format!(
|
|
|
|
|
|
"unknown --scenario {v:?} (the edition has {})",
|
|
|
|
|
|
known
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|s| format!("{} {:?}", s.id, s.title))
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
.join(", ")
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// `scn-02-4p`, or `standard-4p` for the scenario every recording plays.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// **`SCN_01` keeps the old preset string.** Twenty-six recorded
|
|
|
|
|
|
/// scenarios name `standard-Np`; emitting `scn-01-Np` for the same board
|
|
|
|
|
|
/// would have made every one of them unreplayable to no purpose.
|
|
|
|
|
|
pub fn scenario_preset(scenario: &str, players: u8) -> String {
|
|
|
|
|
|
if scenario == "SCN_01" {
|
|
|
|
|
|
return format!("standard-{players}p");
|
|
|
|
|
|
}
|
|
|
|
|
|
format!(
|
|
|
|
|
|
"scn-{}-{players}p",
|
|
|
|
|
|
scenario.rsplit('_').next().unwrap_or(scenario)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-02 02:45:55 +02:00
|
|
|
|
fn parse_args(argv: &[String]) -> Result<Mode, String> {
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
let mut config = Config::default();
|
2026-08-02 02:45:55 +02:00
|
|
|
|
let mut source: Option<std::path::PathBuf> = None;
|
|
|
|
|
|
let mut eyes = inspect::Eyes::Spectator;
|
|
|
|
|
|
let mut play_flags: Vec<String> = Vec::new();
|
|
|
|
|
|
let mut inspect_flags: Vec<String> = Vec::new();
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
let mut seats: Vec<u8> = Vec::new();
|
|
|
|
|
|
let mut all_bots = false;
|
|
|
|
|
|
let mut i = 0;
|
|
|
|
|
|
let value = |i: usize, argv: &[String], flag: &str| -> Result<String, String> {
|
|
|
|
|
|
argv.get(i + 1)
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.ok_or_else(|| format!("{flag} needs a value"))
|
|
|
|
|
|
};
|
|
|
|
|
|
while i < argv.len() {
|
|
|
|
|
|
let flag = argv[i].as_str();
|
|
|
|
|
|
match flag {
|
|
|
|
|
|
"--seed" => {
|
2026-08-02 02:45:55 +02:00
|
|
|
|
play_flags.push(flag.into());
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
config.seed = value(i, argv, flag)?
|
|
|
|
|
|
.parse()
|
|
|
|
|
|
.map_err(|e| format!("--seed: {e}"))?;
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
"--players" => {
|
2026-08-02 02:45:55 +02:00
|
|
|
|
play_flags.push(flag.into());
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
config.players = value(i, argv, flag)?
|
|
|
|
|
|
.parse()
|
|
|
|
|
|
.map_err(|e| format!("--players: {e}"))?;
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
"--seat" => {
|
2026-08-02 02:45:55 +02:00
|
|
|
|
play_flags.push(flag.into());
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
seats.push(
|
|
|
|
|
|
value(i, argv, flag)?
|
|
|
|
|
|
.parse()
|
|
|
|
|
|
.map_err(|e| format!("--seat: {e}"))?,
|
|
|
|
|
|
);
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
"--bot" => {
|
2026-08-02 02:45:55 +02:00
|
|
|
|
play_flags.push(flag.into());
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
config.bot = value(i, argv, flag)?;
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
"--record" => {
|
2026-08-02 02:45:55 +02:00
|
|
|
|
play_flags.push(flag.into());
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
config.record = Some(value(i, argv, flag)?.into());
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
|
// CB-WP-0027: a trial is a recorded session PLUS what the
|
|
|
|
|
|
// player said while playing (GameDesign §5, ADR-0014).
|
GR-E03 and GR-E04 played to the end — F14 closed, and the reason they were
unplayed was ours
Tier S (a fix and a measurement inside a boundary; chaos d8=4 from
CB-WP-0029's roll, no override).
cb-play built EVERY game with ScoringMode::SharedGround and passed an
empty patch. The mode was settable in scenarios and not from the driver,
so two of the three shipped modes were unreachable from the only way
anyone actually plays. F14 sat open for a week because nobody could reach
the thing it was about.
--mode added. All three now play out and give DIFFERENT WINNERS FROM
IDENTICAL PLAY: shared -> all four seats (mastery 4), common -> P3 alone
(top personal scorer), coalitions -> P1+P2 (best Bond network, 4>3>2).
Same 37 commands, three answers.
AND THEY ANSWER F17'S OPEN QUESTION. I had flagged that ATTACK might earn
its place where Blame costs personal score. It does not, in any mode:
SHARED GROUND 132/165/190/200 -> identical free but pointless
COMMON PROBLEM 59/52/48/44 -> 59/52/48/34 a cost at six seats
BONDED COALITIONS 131/134/132/116 -> 59/52/48/34 roughly halved
The coalitions row has a mechanism and the data confirms it unprompted.
GR-A07 flips a Bond to a Rivalry on Attack, and GR-E04 scores Bond
NETWORKS -- so attacking destroys the thing that scores. And the attacking
numbers in E04 are IDENTICAL to E03's, which is exactly what that
predicts: break every Bond and each seat is a coalition of one, so GR-E04
degenerates into GR-E03. That check was not designed; it fell out.
F14 -> applied. F17 strengthened and no longer bounded to co-op: ATTACK
has no mode in which it helps, and one where it actively destroys your
score.
Still framed as a question rather than a verdict. DARVO is the pattern the
game is about not falling into, so a self-destructive ATTACK may be the
design. What ground-game has to decide is whether the namesake mechanic
being unreachable in competent play -- in all three modes -- is intended.
make all: exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:38:27 +02:00
|
|
|
|
// GR-E02..E04, so the other two shipped modes are reachable.
|
|
|
|
|
|
"--mode" => {
|
|
|
|
|
|
play_flags.push(flag.into());
|
|
|
|
|
|
let v = value(i, argv, flag)?;
|
|
|
|
|
|
config.mode = match v.to_ascii_lowercase().as_str() {
|
|
|
|
|
|
"shared" | "sharedground" | "coop" => games_ground::ScoringMode::SharedGround,
|
|
|
|
|
|
"common" | "commonproblem" | "semi" => games_ground::ScoringMode::CommonProblem,
|
|
|
|
|
|
"coalitions" | "bondedcoalitions" | "coalition" => {
|
|
|
|
|
|
games_ground::ScoringMode::BondedCoalitions
|
|
|
|
|
|
}
|
|
|
|
|
|
other => {
|
|
|
|
|
|
return Err(format!(
|
|
|
|
|
|
"unknown --mode {other:?} (shared, common, coalitions)"
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
// CB-WP-0036 T02. A SEPARATE axis from --mode: that one is
|
|
|
|
|
|
// ScoringMode, a rule of the game; this is how much of what
|
|
|
|
|
|
// the rules cannot see gets performed (Ornamentation §4).
|
|
|
|
|
|
//
|
|
|
|
|
|
// **The flag exists before anything reads it, deliberately.**
|
|
|
|
|
|
// It is the seam `clay-animate` attaches to, and a seam is
|
|
|
|
|
|
// cheap now where a retrofit would not be.
|
CB-WP-0038: variant selection, H1 implemented, and H1 measured
ground-game packages hypotheses as selectable rules variants — a catalog,
a rules_delta.yaml, and prose — and their note is explicit that CSV text
alone is not executable here. So the kernel gains a Variant in game state:
in the state, therefore in the hash, therefore in the recording, because a
scenario replayed under a different variant would diverge silently.
Baseline is bit-for-bit what it was, asserted across seat counts and
seeds. A variant system that perturbs the baseline invalidates every
measurement this repo has.
H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on
their own defects: "unclaimed" misread as face-up-and-unsolved, and the
attacker's Stress read after the attack's effects. Their `unchanged:` list
is asserted rather than trusted — that list is their claim about their own
experiment.
Measured, and three of their four criteria fail. DARVO arm rate is still
0 under greedy; ATTACK selection does not rise and falls for the rank-75
policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats.
The mechanism is not the assumed one: greedy answers the pressure by
regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the
arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under
competent play.
A harness defect was caught before the claim: sweep discarded refused
games silently and never reported its count, so "nobody won" and "nothing
played" printed identically. Reporting H1 as unwinnable on that basis
would have been the ADR-0018 family aimed at another repo's design. All
200 games ran in every cell; the zeros are real.
Chaos d8 = 8 — the window's first override, redrew L against a structural
L, so it changed nothing. Window 3 recorded in ChaosRollHistory.
NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1
result may reach ground-game until it has run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:50:08 +02:00
|
|
|
|
// CB-WP-0038: ground-game names these in
|
|
|
|
|
|
// `editions/catalog.yaml`; `--variant` takes that id.
|
CB-WP-0047: all four boards, and every mode named on the page
The modes were already implemented; nothing had ever COMPARED them. The
scenarios were not implemented at all: edition::deal has taken a
scenario_id since it was written and the only caller passed the literal
"SCN_01", so 15 of 20 Problem cards had never been dealt by anything.
The seam was the whole mechanism and it sat unused, with nothing red
because nothing asked.
Scenario is now state (serde default SCN_01, so all 26 recordings replay
unchanged), selected by preset `scn-03-4p` with `standard-Np` still
meaning SCN_01, and by --scenario/SCENARIO= accepting ids, numbers or
titles, validated against the edition rather than a pattern.
The threshold now comes off the Scenario card, closing F25's hardcoded
5/7/9. The first version of that control was worthless and mutation said
so: all four scenarios print 5/7/9, so reverting to the bands left it
green. Split threshold_from() so it can be handed a card that disagrees.
The header read `scoring CommonProblem` where the Mode card is titled
COMMON PROBLEM, PERSONAL EDGE -- the defect CB-WP-0034 deleted from the
move buttons, still standing on the line that says what winning means.
The coverage probe was matching that Debug output and went red when it
was fixed: third instance (CB-WP-0024, CB-WP-0034). Page now carries the
premise, the mode's rules text, and the tiebreak.
scenario-panel plays 4x3x3. Findings: SCN_01 and SCN_02 are the same
board (identical cells, pinned by a characterisation test); SCN_04 is
the hard board at 2p (52% vs 67/73%, the only deck needing two Repair);
and group success is EXACTLY equal across all three modes in all 36
cells, because greedy never reads state.mode -- filed F27, the two
competitive modes are scoring lenses over cooperative play.
F28: SHARED GROUND's mastery subtracts penalties from the claimed COUNT
where the mode card's shared score is claimed VALUE. Raised, not fixed;
scoring is ground-game's to rule on.
Also fixes design.py reporting a backticked path as no reproduction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:51:46 +02:00
|
|
|
|
// CB-WP-0047: WHICH SCENARIO. `deal` has taken a scenario id
|
|
|
|
|
|
// since it was written; the call site passed the literal
|
|
|
|
|
|
// "SCN_01", so three of four decks were unreachable from the
|
|
|
|
|
|
// driver — the same shape as `--mode` before F14.
|
|
|
|
|
|
"--scenario" => {
|
|
|
|
|
|
play_flags.push(flag.into());
|
|
|
|
|
|
let v = value(i, argv, flag)?;
|
|
|
|
|
|
config.scenario = normalise_scenario(&v)?;
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
CB-WP-0038: variant selection, H1 implemented, and H1 measured
ground-game packages hypotheses as selectable rules variants — a catalog,
a rules_delta.yaml, and prose — and their note is explicit that CSV text
alone is not executable here. So the kernel gains a Variant in game state:
in the state, therefore in the hash, therefore in the recording, because a
scenario replayed under a different variant would diverge silently.
Baseline is bit-for-bit what it was, asserted across seat counts and
seeds. A variant system that perturbs the baseline invalidates every
measurement this repo has.
H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on
their own defects: "unclaimed" misread as face-up-and-unsolved, and the
attacker's Stress read after the attack's effects. Their `unchanged:` list
is asserted rather than trusted — that list is their claim about their own
experiment.
Measured, and three of their four criteria fail. DARVO arm rate is still
0 under greedy; ATTACK selection does not rise and falls for the rank-75
policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats.
The mechanism is not the assumed one: greedy answers the pressure by
regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the
arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under
competent play.
A harness defect was caught before the claim: sweep discarded refused
games silently and never reported its count, so "nobody won" and "nothing
played" printed identically. Reporting H1 as unwinnable on that basis
would have been the ADR-0018 family aimed at another repo's design. All
200 games ran in every cell; the zeros are real.
Chaos d8 = 8 — the window's first override, redrew L against a structural
L, so it changed nothing. Window 3 recorded in ChaosRollHistory.
NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1
result may reach ground-game until it has run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:50:08 +02:00
|
|
|
|
"--variant" => {
|
|
|
|
|
|
play_flags.push(flag.into());
|
|
|
|
|
|
config.variant = value(i, argv, flag)?.parse()?;
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
"--pace" => {
|
|
|
|
|
|
play_flags.push(flag.into());
|
|
|
|
|
|
config.pace = value(i, argv, flag)?.parse()?;
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
|
"--trial" => {
|
|
|
|
|
|
play_flags.push(flag.into());
|
|
|
|
|
|
config.trial = Some(value(i, argv, flag)?.into());
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
"--replay" => {
|
2026-08-02 02:45:55 +02:00
|
|
|
|
play_flags.push(flag.into());
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
config.replay_dir = Some(value(i, argv, flag)?.into());
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit
Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
|
|
|
|
"--serve" => {
|
|
|
|
|
|
play_flags.push(flag.into());
|
|
|
|
|
|
config.serve = Some(
|
|
|
|
|
|
value(i, argv, flag)?
|
|
|
|
|
|
.parse()
|
|
|
|
|
|
.map_err(|e| format!("--serve: {e}"))?,
|
|
|
|
|
|
);
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
"--all-bots" => {
|
|
|
|
|
|
all_bots = true;
|
2026-08-02 02:45:55 +02:00
|
|
|
|
play_flags.push(flag.into());
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
i += 1;
|
|
|
|
|
|
}
|
2026-08-02 02:45:55 +02:00
|
|
|
|
"--inspect" => {
|
|
|
|
|
|
source = Some(value(i, argv, flag)?.into());
|
|
|
|
|
|
inspect_flags.push(flag.into());
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
"--as" => {
|
|
|
|
|
|
eyes = inspect::Eyes::parse(&value(i, argv, flag)?)?;
|
|
|
|
|
|
inspect_flags.push(flag.into());
|
|
|
|
|
|
i += 2;
|
|
|
|
|
|
}
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
"-h" | "--help" => return Err(USAGE.into()),
|
|
|
|
|
|
other => return Err(format!("unknown flag {other:?}\n\n{USAGE}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-02 02:45:55 +02:00
|
|
|
|
if let Some(source) = source {
|
|
|
|
|
|
if !play_flags.is_empty() {
|
|
|
|
|
|
return Err(format!(
|
|
|
|
|
|
"--inspect reads a recording; it cannot also {}\n\n{USAGE}",
|
|
|
|
|
|
play_flags.join(", ")
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
return Ok(Mode::Inspect { source, eyes });
|
|
|
|
|
|
}
|
|
|
|
|
|
if !inspect_flags.is_empty() {
|
|
|
|
|
|
return Err(format!(
|
|
|
|
|
|
"{} only means something with --inspect\n\n{USAGE}",
|
|
|
|
|
|
inspect_flags.join(", ")
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
if !(2..=6).contains(&config.players) {
|
|
|
|
|
|
return Err(format!("GR-O01: {} players is outside 2–6", config.players));
|
|
|
|
|
|
}
|
|
|
|
|
|
config.human_seats = if all_bots {
|
|
|
|
|
|
vec![]
|
|
|
|
|
|
} else if seats.is_empty() {
|
|
|
|
|
|
vec![0]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
seats
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(bad) = config.human_seats.iter().find(|s| **s >= config.players) {
|
|
|
|
|
|
return Err(format!(
|
|
|
|
|
|
"--seat {bad} is not one of {} seats",
|
|
|
|
|
|
config.players
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-08-02 02:45:55 +02:00
|
|
|
|
Ok(Mode::Play(config))
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
|
let argv: Vec<String> = std::env::args().skip(1).collect();
|
2026-08-02 02:45:55 +02:00
|
|
|
|
let mode = match parse_args(&argv) {
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
Ok(c) => c,
|
|
|
|
|
|
Err(message) => {
|
|
|
|
|
|
eprintln!("{message}");
|
|
|
|
|
|
std::process::exit(64);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-02 02:45:55 +02:00
|
|
|
|
let config = match mode {
|
|
|
|
|
|
Mode::Play(c) => c,
|
|
|
|
|
|
Mode::Inspect { source, eyes } => {
|
|
|
|
|
|
let mut out = std::io::stdout().lock();
|
|
|
|
|
|
match inspect::walk(&source, eyes, &mut out) {
|
|
|
|
|
|
Ok(walk) => {
|
|
|
|
|
|
println!(
|
|
|
|
|
|
" {}: {} step(s), {} rejected, hash {}{}",
|
|
|
|
|
|
walk.source,
|
|
|
|
|
|
walk.steps,
|
|
|
|
|
|
walk.rejected,
|
|
|
|
|
|
walk.end_state_hash,
|
|
|
|
|
|
match walk.expected_hash {
|
|
|
|
|
|
Some(_) => " — reproduces the recording",
|
|
|
|
|
|
None => " — the source pins no hash to check",
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(message) => {
|
|
|
|
|
|
eprintln!("cb-play: {message}");
|
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
let stdin = std::io::stdin();
|
|
|
|
|
|
let stdout = std::io::stdout();
|
|
|
|
|
|
match table::play(&config, stdin.lock(), stdout.lock()) {
|
|
|
|
|
|
Ok(summary) => {
|
|
|
|
|
|
println!(
|
|
|
|
|
|
" {} round(s), {} commands, hash {}",
|
|
|
|
|
|
summary.rounds,
|
|
|
|
|
|
summary.scenario.commands.len(),
|
|
|
|
|
|
summary.end_state_hash
|
|
|
|
|
|
);
|
|
|
|
|
|
if let Some(path) = summary.bundle {
|
|
|
|
|
|
println!(" bundle {}", path.display());
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(path) = summary.recorded {
|
|
|
|
|
|
println!(" recorded {}", path.display());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(message) => {
|
|
|
|
|
|
eprintln!("cb-play: {message}");
|
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
use cb_game_runtime::{run, RunOutcome};
|
|
|
|
|
|
use games_ground::GroundState;
|
|
|
|
|
|
|
|
|
|
|
|
fn args(list: &[&str]) -> Vec<String> {
|
|
|
|
|
|
list.iter().map(|s| s.to_string()).collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
CB-WP-0008-T03: prove the 2-6 player range
GR-O01 states 2-6 players; every scenario in the corpus was 3-player.
Now all five counts play to GameEnded under both policies and reproduce
at the same seed, with scenarios at both boundaries and the CLI
transcript run at 2p, 3p and 6p.
Nothing broke — the rules are seat-count-generic. What the boundaries
exposed is arithmetic: with the standard preset's placeholder Problem
values (value = priority), the best total any game can reach is 3 at 2p,
6 at 3-4p, 10 at 5-6p, against GR-E01 thresholds of 5, 7 and 9. Group
success is unreachable below five seats regardless of play, and no
scenario noticed because none had played to scoring with everything
claimed.
GR-S01 calls the fixture a stand-in for scenario Problem data, so this
is evidence the stand-in is not neutral, not that GR-E01 is wrong. It is
pinned by a passing scenario, an arithmetic test, and a provisional
marker owned by ground-game so it ages in `make coverage`. The test
states its own delete-by: it is expected to fail when Problem values
become real data, and that failure is the signal to delete it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:20:04 +02:00
|
|
|
|
/// A scripted transcript plays a full game to `GameEnded`. "0"
|
|
|
|
|
|
/// always takes the first legal command, which is a real player input
|
|
|
|
|
|
/// and needs no knowledge of the board.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Run at **both GR-O01 boundaries and the middle** (T03): the CLI is
|
|
|
|
|
|
/// where a seat-count assumption would show up as a prompt nobody can
|
|
|
|
|
|
/// answer.
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn a_scripted_transcript_plays_a_full_game() {
|
CB-WP-0008-T03: prove the 2-6 player range
GR-O01 states 2-6 players; every scenario in the corpus was 3-player.
Now all five counts play to GameEnded under both policies and reproduce
at the same seed, with scenarios at both boundaries and the CLI
transcript run at 2p, 3p and 6p.
Nothing broke — the rules are seat-count-generic. What the boundaries
exposed is arithmetic: with the standard preset's placeholder Problem
values (value = priority), the best total any game can reach is 3 at 2p,
6 at 3-4p, 10 at 5-6p, against GR-E01 thresholds of 5, 7 and 9. Group
success is unreachable below five seats regardless of play, and no
scenario noticed because none had played to scoring with everything
claimed.
GR-S01 calls the fixture a stand-in for scenario Problem data, so this
is evidence the stand-in is not neutral, not that GR-E01 is wrong. It is
pinned by a passing scenario, an arithmetic test, and a provisional
marker owned by ground-game so it ages in `make coverage`. The test
states its own delete-by: it is expected to fail when Problem values
become real data, and that failure is the signal to delete it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:20:04 +02:00
|
|
|
|
for players in [2u8, 3, 6] {
|
|
|
|
|
|
let config = Config {
|
|
|
|
|
|
seed: 42,
|
|
|
|
|
|
players,
|
|
|
|
|
|
human_seats: vec![0],
|
|
|
|
|
|
bot: "greedy".into(),
|
|
|
|
|
|
replay_dir: None,
|
|
|
|
|
|
record: None,
|
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit
Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
|
|
|
|
serve: None,
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
|
trial: None,
|
GR-E03 and GR-E04 played to the end — F14 closed, and the reason they were
unplayed was ours
Tier S (a fix and a measurement inside a boundary; chaos d8=4 from
CB-WP-0029's roll, no override).
cb-play built EVERY game with ScoringMode::SharedGround and passed an
empty patch. The mode was settable in scenarios and not from the driver,
so two of the three shipped modes were unreachable from the only way
anyone actually plays. F14 sat open for a week because nobody could reach
the thing it was about.
--mode added. All three now play out and give DIFFERENT WINNERS FROM
IDENTICAL PLAY: shared -> all four seats (mastery 4), common -> P3 alone
(top personal scorer), coalitions -> P1+P2 (best Bond network, 4>3>2).
Same 37 commands, three answers.
AND THEY ANSWER F17'S OPEN QUESTION. I had flagged that ATTACK might earn
its place where Blame costs personal score. It does not, in any mode:
SHARED GROUND 132/165/190/200 -> identical free but pointless
COMMON PROBLEM 59/52/48/44 -> 59/52/48/34 a cost at six seats
BONDED COALITIONS 131/134/132/116 -> 59/52/48/34 roughly halved
The coalitions row has a mechanism and the data confirms it unprompted.
GR-A07 flips a Bond to a Rivalry on Attack, and GR-E04 scores Bond
NETWORKS -- so attacking destroys the thing that scores. And the attacking
numbers in E04 are IDENTICAL to E03's, which is exactly what that
predicts: break every Bond and each seat is a coalition of one, so GR-E04
degenerates into GR-E03. That check was not designed; it fell out.
F14 -> applied. F17 strengthened and no longer bounded to co-op: ATTACK
has no mode in which it helps, and one where it actively destroys your
score.
Still framed as a question rather than a verdict. DARVO is the pattern the
game is about not falling into, so a self-destructive ATTACK may be the
design. What ground-game has to decide is whether the namesake mechanic
being unreachable in competent play -- in all three modes -- is intended.
make all: exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:38:27 +02:00
|
|
|
|
mode: games_ground::ScoringMode::SharedGround,
|
CB-WP-0047: all four boards, and every mode named on the page
The modes were already implemented; nothing had ever COMPARED them. The
scenarios were not implemented at all: edition::deal has taken a
scenario_id since it was written and the only caller passed the literal
"SCN_01", so 15 of 20 Problem cards had never been dealt by anything.
The seam was the whole mechanism and it sat unused, with nothing red
because nothing asked.
Scenario is now state (serde default SCN_01, so all 26 recordings replay
unchanged), selected by preset `scn-03-4p` with `standard-Np` still
meaning SCN_01, and by --scenario/SCENARIO= accepting ids, numbers or
titles, validated against the edition rather than a pattern.
The threshold now comes off the Scenario card, closing F25's hardcoded
5/7/9. The first version of that control was worthless and mutation said
so: all four scenarios print 5/7/9, so reverting to the bands left it
green. Split threshold_from() so it can be handed a card that disagrees.
The header read `scoring CommonProblem` where the Mode card is titled
COMMON PROBLEM, PERSONAL EDGE -- the defect CB-WP-0034 deleted from the
move buttons, still standing on the line that says what winning means.
The coverage probe was matching that Debug output and went red when it
was fixed: third instance (CB-WP-0024, CB-WP-0034). Page now carries the
premise, the mode's rules text, and the tiebreak.
scenario-panel plays 4x3x3. Findings: SCN_01 and SCN_02 are the same
board (identical cells, pinned by a characterisation test); SCN_04 is
the hard board at 2p (52% vs 67/73%, the only deck needing two Repair);
and group success is EXACTLY equal across all three modes in all 36
cells, because greedy never reads state.mode -- filed F27, the two
competitive modes are scoring lenses over cooperative play.
F28: SHARED GROUND's mastery subtracts penalties from the claimed COUNT
where the mode card's shared score is claimed VALUE. Raised, not fixed;
scoring is ground-game's to rule on.
Also fixes design.py reporting a backticked path as no reproduction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:51:46 +02:00
|
|
|
|
scenario: "SCN_01".into(),
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
pace: table::Pace::Speed,
|
CB-WP-0038: variant selection, H1 implemented, and H1 measured
ground-game packages hypotheses as selectable rules variants — a catalog,
a rules_delta.yaml, and prose — and their note is explicit that CSV text
alone is not executable here. So the kernel gains a Variant in game state:
in the state, therefore in the hash, therefore in the recording, because a
scenario replayed under a different variant would diverge silently.
Baseline is bit-for-bit what it was, asserted across seat counts and
seeds. A variant system that perturbs the baseline invalidates every
measurement this repo has.
H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on
their own defects: "unclaimed" misread as face-up-and-unsolved, and the
attacker's Stress read after the attack's effects. Their `unchanged:` list
is asserted rather than trusted — that list is their claim about their own
experiment.
Measured, and three of their four criteria fail. DARVO arm rate is still
0 under greedy; ATTACK selection does not rise and falls for the rank-75
policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats.
The mechanism is not the assumed one: greedy answers the pressure by
regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the
arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under
competent play.
A harness defect was caught before the claim: sweep discarded refused
games silently and never reported its count, so "nobody won" and "nothing
played" printed identically. Reporting H1 as unwinnable on that basis
would have been the ADR-0018 family aimed at another repo's design. All
200 games ran in every cell; the zeros are real.
Chaos d8 = 8 — the window's first override, redrew L against a structural
L, so it changed nothing. Window 3 recorded in ChaosRollHistory.
NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1
result may reach ground-game until it has run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:50:08 +02:00
|
|
|
|
variant: games_ground::Variant::Baseline,
|
CB-WP-0008-T03: prove the 2-6 player range
GR-O01 states 2-6 players; every scenario in the corpus was 3-player.
Now all five counts play to GameEnded under both policies and reproduce
at the same seed, with scenarios at both boundaries and the CLI
transcript run at 2p, 3p and 6p.
Nothing broke — the rules are seat-count-generic. What the boundaries
exposed is arithmetic: with the standard preset's placeholder Problem
values (value = priority), the best total any game can reach is 3 at 2p,
6 at 3-4p, 10 at 5-6p, against GR-E01 thresholds of 5, 7 and 9. Group
success is unreachable below five seats regardless of play, and no
scenario noticed because none had played to scoring with everything
claimed.
GR-S01 calls the fixture a stand-in for scenario Problem data, so this
is evidence the stand-in is not neutral, not that GR-E01 is wrong. It is
pinned by a passing scenario, an arithmetic test, and a provisional
marker owned by ground-game so it ages in `make coverage`. The test
states its own delete-by: it is expected to fail when Problem values
become real data, and that failure is the signal to delete it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:20:04 +02:00
|
|
|
|
};
|
|
|
|
|
|
let script = "0\n".repeat(400);
|
|
|
|
|
|
let mut out: Vec<u8> = Vec::new();
|
|
|
|
|
|
let summary = table::play(&config, script.as_bytes(), &mut out)
|
|
|
|
|
|
.unwrap_or_else(|e| panic!("{players}p scripted game failed: {e}"));
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
|
CB-WP-0008-T03: prove the 2-6 player range
GR-O01 states 2-6 players; every scenario in the corpus was 3-player.
Now all five counts play to GameEnded under both policies and reproduce
at the same seed, with scenarios at both boundaries and the CLI
transcript run at 2p, 3p and 6p.
Nothing broke — the rules are seat-count-generic. What the boundaries
exposed is arithmetic: with the standard preset's placeholder Problem
values (value = priority), the best total any game can reach is 3 at 2p,
6 at 3-4p, 10 at 5-6p, against GR-E01 thresholds of 5, 7 and 9. Group
success is unreachable below five seats regardless of play, and no
scenario noticed because none had played to scoring with everything
claimed.
GR-S01 calls the fixture a stand-in for scenario Problem data, so this
is evidence the stand-in is not neutral, not that GR-E01 is wrong. It is
pinned by a passing scenario, an arithmetic test, and a provisional
marker owned by ground-game so it ages in `make coverage`. The test
states its own delete-by: it is expected to fail when Problem values
become real data, and that failure is the signal to delete it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:20:04 +02:00
|
|
|
|
assert_eq!(summary.rounds, 5, "{players}p: GR-R09 runs five rounds");
|
|
|
|
|
|
let text = String::from_utf8(out).expect("utf8");
|
|
|
|
|
|
assert!(text.contains("OUTCOME"), "{players}p: no outcome reported");
|
|
|
|
|
|
assert!(text.contains("you are P1"), "{players}p: no prompt");
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
|
CB-WP-0008-T03: prove the 2-6 player range
GR-O01 states 2-6 players; every scenario in the corpus was 3-player.
Now all five counts play to GameEnded under both policies and reproduce
at the same seed, with scenarios at both boundaries and the CLI
transcript run at 2p, 3p and 6p.
Nothing broke — the rules are seat-count-generic. What the boundaries
exposed is arithmetic: with the standard preset's placeholder Problem
values (value = priority), the best total any game can reach is 3 at 2p,
6 at 3-4p, 10 at 5-6p, against GR-E01 thresholds of 5, 7 and 9. Group
success is unreachable below five seats regardless of play, and no
scenario noticed because none had played to scoring with everything
claimed.
GR-S01 calls the fixture a stand-in for scenario Problem data, so this
is evidence the stand-in is not neutral, not that GR-E01 is wrong. It is
pinned by a passing scenario, an arithmetic test, and a provisional
marker owned by ground-game so it ages in `make coverage`. The test
states its own delete-by: it is expected to fail when Problem values
become real data, and that failure is the signal to delete it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:20:04 +02:00
|
|
|
|
// The transcript replays identically — through the scenario
|
|
|
|
|
|
// runner, not through a second call to the same driver.
|
|
|
|
|
|
match run::<GroundState>(&summary.scenario) {
|
|
|
|
|
|
RunOutcome::Passed { .. } => {}
|
|
|
|
|
|
RunOutcome::Failed { reason, .. } => {
|
|
|
|
|
|
panic!("{players}p replay failed: {reason}")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
/// **Ornamentation §5, I3** — the invariant the clay-borg /
|
|
|
|
|
|
/// clay-animate split rests on.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// > The same seed and the same decisions produce a **byte-identical
|
|
|
|
|
|
/// > recording** at any pace.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Asserted rather than intended. If this ever fails, something that
|
|
|
|
|
|
/// was called ornamentation has become mechanism, and the boundary in
|
|
|
|
|
|
/// `specs/Ornamentation.md` has stopped being real — which §7 names
|
|
|
|
|
|
/// as wrong at the root rather than patchable at the edges.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn pace_cannot_change_the_game() {
|
|
|
|
|
|
let at = |pace| {
|
|
|
|
|
|
let config = Config {
|
|
|
|
|
|
seed: 42,
|
|
|
|
|
|
players: 3,
|
|
|
|
|
|
human_seats: vec![0],
|
|
|
|
|
|
bot: "greedy".into(),
|
|
|
|
|
|
replay_dir: None,
|
|
|
|
|
|
record: None,
|
|
|
|
|
|
serve: None,
|
|
|
|
|
|
trial: None,
|
|
|
|
|
|
mode: games_ground::ScoringMode::SharedGround,
|
CB-WP-0047: all four boards, and every mode named on the page
The modes were already implemented; nothing had ever COMPARED them. The
scenarios were not implemented at all: edition::deal has taken a
scenario_id since it was written and the only caller passed the literal
"SCN_01", so 15 of 20 Problem cards had never been dealt by anything.
The seam was the whole mechanism and it sat unused, with nothing red
because nothing asked.
Scenario is now state (serde default SCN_01, so all 26 recordings replay
unchanged), selected by preset `scn-03-4p` with `standard-Np` still
meaning SCN_01, and by --scenario/SCENARIO= accepting ids, numbers or
titles, validated against the edition rather than a pattern.
The threshold now comes off the Scenario card, closing F25's hardcoded
5/7/9. The first version of that control was worthless and mutation said
so: all four scenarios print 5/7/9, so reverting to the bands left it
green. Split threshold_from() so it can be handed a card that disagrees.
The header read `scoring CommonProblem` where the Mode card is titled
COMMON PROBLEM, PERSONAL EDGE -- the defect CB-WP-0034 deleted from the
move buttons, still standing on the line that says what winning means.
The coverage probe was matching that Debug output and went red when it
was fixed: third instance (CB-WP-0024, CB-WP-0034). Page now carries the
premise, the mode's rules text, and the tiebreak.
scenario-panel plays 4x3x3. Findings: SCN_01 and SCN_02 are the same
board (identical cells, pinned by a characterisation test); SCN_04 is
the hard board at 2p (52% vs 67/73%, the only deck needing two Repair);
and group success is EXACTLY equal across all three modes in all 36
cells, because greedy never reads state.mode -- filed F27, the two
competitive modes are scoring lenses over cooperative play.
F28: SHARED GROUND's mastery subtracts penalties from the claimed COUNT
where the mode card's shared score is claimed VALUE. Raised, not fixed;
scoring is ground-game's to rule on.
Also fixes design.py reporting a backticked path as no reproduction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:51:46 +02:00
|
|
|
|
scenario: "SCN_01".into(),
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
pace,
|
CB-WP-0038: variant selection, H1 implemented, and H1 measured
ground-game packages hypotheses as selectable rules variants — a catalog,
a rules_delta.yaml, and prose — and their note is explicit that CSV text
alone is not executable here. So the kernel gains a Variant in game state:
in the state, therefore in the hash, therefore in the recording, because a
scenario replayed under a different variant would diverge silently.
Baseline is bit-for-bit what it was, asserted across seat counts and
seeds. A variant system that perturbs the baseline invalidates every
measurement this repo has.
H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on
their own defects: "unclaimed" misread as face-up-and-unsolved, and the
attacker's Stress read after the attack's effects. Their `unchanged:` list
is asserted rather than trusted — that list is their claim about their own
experiment.
Measured, and three of their four criteria fail. DARVO arm rate is still
0 under greedy; ATTACK selection does not rise and falls for the rank-75
policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats.
The mechanism is not the assumed one: greedy answers the pressure by
regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the
arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under
competent play.
A harness defect was caught before the claim: sweep discarded refused
games silently and never reported its count, so "nobody won" and "nothing
played" printed identically. Reporting H1 as unwinnable on that basis
would have been the ADR-0018 family aimed at another repo's design. All
200 games ran in every cell; the zeros are real.
Chaos d8 = 8 — the window's first override, redrew L against a structural
L, so it changed nothing. Window 3 recorded in ChaosRollHistory.
NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1
result may reach ground-game until it has run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:50:08 +02:00
|
|
|
|
variant: games_ground::Variant::Baseline,
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
};
|
|
|
|
|
|
let script = "0\n".repeat(400);
|
|
|
|
|
|
let mut out: Vec<u8> = Vec::new();
|
|
|
|
|
|
let s = table::play(&config, script.as_bytes(), &mut out).expect("game");
|
|
|
|
|
|
(
|
|
|
|
|
|
serde_yaml::to_string(&s.scenario).expect("yaml"),
|
|
|
|
|
|
s.end_state_hash,
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
let (speed_yaml, speed_hash) = at(table::Pace::Speed);
|
|
|
|
|
|
let (inter_yaml, inter_hash) = at(table::Pace::Interactive);
|
|
|
|
|
|
|
|
|
|
|
|
// The recording, byte for byte.
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
speed_yaml, inter_yaml,
|
|
|
|
|
|
"the recording differs by pace, so a renderer has become mechanism"
|
|
|
|
|
|
);
|
|
|
|
|
|
// And the state hash, which is what §1.1 uses to tell the two
|
|
|
|
|
|
// categories apart in the first place.
|
|
|
|
|
|
assert_eq!(speed_hash, inter_hash, "pace moved the state hash");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// `--pace` parses, defaults to speed, and refuses what it cannot do.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// **Speed is the default** because `sim`, `trials`, the benchmarks
|
|
|
|
|
|
/// and every bot game run at it (Ornamentation §4).
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn pace_parses_and_defaults_to_speed() {
|
|
|
|
|
|
assert_eq!(Config::default().pace, table::Pace::Speed);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
play_args(&["--pace", "interactive"]).expect("parse").pace,
|
|
|
|
|
|
table::Pace::Interactive
|
|
|
|
|
|
);
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
play_args(&["--pace", "speed"]).expect("parse").pace,
|
|
|
|
|
|
table::Pace::Speed
|
|
|
|
|
|
);
|
|
|
|
|
|
// A misspelling must not silently mean the default: pace is the
|
|
|
|
|
|
// seam clay-animate attaches to, and a typo that quietly selects
|
|
|
|
|
|
// Speed would look like the renderer being broken.
|
|
|
|
|
|
let e = match play_args(&["--pace", "cinematic"]) {
|
|
|
|
|
|
Err(e) => e,
|
|
|
|
|
|
Ok(_) => panic!("a misspelt pace was accepted"),
|
|
|
|
|
|
};
|
|
|
|
|
|
assert!(e.contains("cinematic"), "{e}");
|
|
|
|
|
|
assert!(e.contains("speed") && e.contains("interactive"), "{e}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
/// K13 at the boundary that matters: what the human is *shown* must
|
|
|
|
|
|
/// not contain another seat's face-down selection.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// The human seat is **P3**, deliberately. Seats are asked in order,
|
|
|
|
|
|
/// so a P1 human is always prompted before anyone has selected and
|
|
|
|
|
|
/// never sees a hidden selection at all — a test seated there passes
|
|
|
|
|
|
/// without exercising the rule.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn the_prompt_never_shows_a_hidden_selection() {
|
|
|
|
|
|
let config = Config {
|
|
|
|
|
|
seed: 42,
|
|
|
|
|
|
players: 3,
|
|
|
|
|
|
human_seats: vec![2],
|
|
|
|
|
|
bot: "greedy".into(),
|
|
|
|
|
|
replay_dir: None,
|
|
|
|
|
|
record: None,
|
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit
Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
|
|
|
|
serve: None,
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
|
trial: None,
|
GR-E03 and GR-E04 played to the end — F14 closed, and the reason they were
unplayed was ours
Tier S (a fix and a measurement inside a boundary; chaos d8=4 from
CB-WP-0029's roll, no override).
cb-play built EVERY game with ScoringMode::SharedGround and passed an
empty patch. The mode was settable in scenarios and not from the driver,
so two of the three shipped modes were unreachable from the only way
anyone actually plays. F14 sat open for a week because nobody could reach
the thing it was about.
--mode added. All three now play out and give DIFFERENT WINNERS FROM
IDENTICAL PLAY: shared -> all four seats (mastery 4), common -> P3 alone
(top personal scorer), coalitions -> P1+P2 (best Bond network, 4>3>2).
Same 37 commands, three answers.
AND THEY ANSWER F17'S OPEN QUESTION. I had flagged that ATTACK might earn
its place where Blame costs personal score. It does not, in any mode:
SHARED GROUND 132/165/190/200 -> identical free but pointless
COMMON PROBLEM 59/52/48/44 -> 59/52/48/34 a cost at six seats
BONDED COALITIONS 131/134/132/116 -> 59/52/48/34 roughly halved
The coalitions row has a mechanism and the data confirms it unprompted.
GR-A07 flips a Bond to a Rivalry on Attack, and GR-E04 scores Bond
NETWORKS -- so attacking destroys the thing that scores. And the attacking
numbers in E04 are IDENTICAL to E03's, which is exactly what that
predicts: break every Bond and each seat is a coalition of one, so GR-E04
degenerates into GR-E03. That check was not designed; it fell out.
F14 -> applied. F17 strengthened and no longer bounded to co-op: ATTACK
has no mode in which it helps, and one where it actively destroys your
score.
Still framed as a question rather than a verdict. DARVO is the pattern the
game is about not falling into, so a self-destructive ATTACK may be the
design. What ground-game has to decide is whether the namesake mechanic
being unreachable in competent play -- in all three modes -- is intended.
make all: exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:38:27 +02:00
|
|
|
|
mode: games_ground::ScoringMode::SharedGround,
|
CB-WP-0047: all four boards, and every mode named on the page
The modes were already implemented; nothing had ever COMPARED them. The
scenarios were not implemented at all: edition::deal has taken a
scenario_id since it was written and the only caller passed the literal
"SCN_01", so 15 of 20 Problem cards had never been dealt by anything.
The seam was the whole mechanism and it sat unused, with nothing red
because nothing asked.
Scenario is now state (serde default SCN_01, so all 26 recordings replay
unchanged), selected by preset `scn-03-4p` with `standard-Np` still
meaning SCN_01, and by --scenario/SCENARIO= accepting ids, numbers or
titles, validated against the edition rather than a pattern.
The threshold now comes off the Scenario card, closing F25's hardcoded
5/7/9. The first version of that control was worthless and mutation said
so: all four scenarios print 5/7/9, so reverting to the bands left it
green. Split threshold_from() so it can be handed a card that disagrees.
The header read `scoring CommonProblem` where the Mode card is titled
COMMON PROBLEM, PERSONAL EDGE -- the defect CB-WP-0034 deleted from the
move buttons, still standing on the line that says what winning means.
The coverage probe was matching that Debug output and went red when it
was fixed: third instance (CB-WP-0024, CB-WP-0034). Page now carries the
premise, the mode's rules text, and the tiebreak.
scenario-panel plays 4x3x3. Findings: SCN_01 and SCN_02 are the same
board (identical cells, pinned by a characterisation test); SCN_04 is
the hard board at 2p (52% vs 67/73%, the only deck needing two Repair);
and group success is EXACTLY equal across all three modes in all 36
cells, because greedy never reads state.mode -- filed F27, the two
competitive modes are scoring lenses over cooperative play.
F28: SHARED GROUND's mastery subtracts penalties from the claimed COUNT
where the mode card's shared score is claimed VALUE. Raised, not fixed;
scoring is ground-game's to rule on.
Also fixes design.py reporting a backticked path as no reproduction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:51:46 +02:00
|
|
|
|
scenario: "SCN_01".into(),
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
pace: table::Pace::Speed,
|
CB-WP-0038: variant selection, H1 implemented, and H1 measured
ground-game packages hypotheses as selectable rules variants — a catalog,
a rules_delta.yaml, and prose — and their note is explicit that CSV text
alone is not executable here. So the kernel gains a Variant in game state:
in the state, therefore in the hash, therefore in the recording, because a
scenario replayed under a different variant would diverge silently.
Baseline is bit-for-bit what it was, asserted across seat counts and
seeds. A variant system that perturbs the baseline invalidates every
measurement this repo has.
H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on
their own defects: "unclaimed" misread as face-up-and-unsolved, and the
attacker's Stress read after the attack's effects. Their `unchanged:` list
is asserted rather than trusted — that list is their claim about their own
experiment.
Measured, and three of their four criteria fail. DARVO arm rate is still
0 under greedy; ATTACK selection does not rise and falls for the rank-75
policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats.
The mechanism is not the assumed one: greedy answers the pressure by
regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the
arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under
competent play.
A harness defect was caught before the claim: sweep discarded refused
games silently and never reported its count, so "nobody won" and "nothing
played" printed identically. Reporting H1 as unwinnable on that basis
would have been the ADR-0018 family aimed at another repo's design. All
200 games ran in every cell; the zeros are real.
Chaos d8 = 8 — the window's first override, redrew L against a structural
L, so it changed nothing. Window 3 recorded in ChaosRollHistory.
NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1
result may reach ground-game until it has run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:50:08 +02:00
|
|
|
|
variant: games_ground::Variant::Baseline,
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
};
|
|
|
|
|
|
let mut out: Vec<u8> = Vec::new();
|
|
|
|
|
|
table::play(&config, "0\n".repeat(200).as_bytes(), &mut out).expect("game");
|
|
|
|
|
|
let text = String::from_utf8(out).expect("utf8");
|
|
|
|
|
|
|
|
|
|
|
|
// Every rendered Select-step block must show other seats as
|
|
|
|
|
|
// face-down. A block naming a seat's action before Reveal is a
|
|
|
|
|
|
// leak; `step Select` and `Shown` never co-occur.
|
|
|
|
|
|
let mut checked = 0;
|
|
|
|
|
|
for block in text.split("\nround ") {
|
|
|
|
|
|
if !block.contains("step Select") {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(line) = block
|
|
|
|
|
|
.lines()
|
|
|
|
|
|
.find(|l| l.trim_start().starts_with("selections:"))
|
|
|
|
|
|
{
|
|
|
|
|
|
let others: Vec<&str> = line
|
|
|
|
|
|
.split(" ")
|
|
|
|
|
|
.filter(|s| s.starts_with("P1") || s.starts_with("P2"))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
for seat in others {
|
|
|
|
|
|
checked += 1;
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
seat.contains("face-down"),
|
|
|
|
|
|
"a Select-step prompt showed {seat:?}: {line}"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// The loop above passes trivially if it never found a selections
|
|
|
|
|
|
// line to check — the harness-does-nothing class, which is why
|
|
|
|
|
|
// this counts what it inspected.
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
checked >= 4,
|
|
|
|
|
|
"only {checked} other-seat entries inspected across the session"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A session whose input runs out must say so, not finish the game
|
|
|
|
|
|
/// on the human's behalf.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn running_out_of_input_fails_loudly() {
|
|
|
|
|
|
let config = Config::default();
|
|
|
|
|
|
let mut out: Vec<u8> = Vec::new();
|
|
|
|
|
|
let err = table::play(&config, "0\n".as_bytes(), &mut out)
|
|
|
|
|
|
.expect_err("an exhausted transcript must not finish a game");
|
|
|
|
|
|
assert!(err.contains("input ended"), "got {err:?}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// `--all-bots` is the flag that makes a game watchable, and the
|
|
|
|
|
|
/// control that proves the human path is not the only path.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn all_bots_needs_no_input_at_all() {
|
|
|
|
|
|
let config = Config {
|
|
|
|
|
|
seed: 42,
|
|
|
|
|
|
players: 3,
|
|
|
|
|
|
human_seats: vec![],
|
|
|
|
|
|
bot: "random".into(),
|
|
|
|
|
|
replay_dir: None,
|
|
|
|
|
|
record: None,
|
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit
Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
|
|
|
|
serve: None,
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
|
trial: None,
|
GR-E03 and GR-E04 played to the end — F14 closed, and the reason they were
unplayed was ours
Tier S (a fix and a measurement inside a boundary; chaos d8=4 from
CB-WP-0029's roll, no override).
cb-play built EVERY game with ScoringMode::SharedGround and passed an
empty patch. The mode was settable in scenarios and not from the driver,
so two of the three shipped modes were unreachable from the only way
anyone actually plays. F14 sat open for a week because nobody could reach
the thing it was about.
--mode added. All three now play out and give DIFFERENT WINNERS FROM
IDENTICAL PLAY: shared -> all four seats (mastery 4), common -> P3 alone
(top personal scorer), coalitions -> P1+P2 (best Bond network, 4>3>2).
Same 37 commands, three answers.
AND THEY ANSWER F17'S OPEN QUESTION. I had flagged that ATTACK might earn
its place where Blame costs personal score. It does not, in any mode:
SHARED GROUND 132/165/190/200 -> identical free but pointless
COMMON PROBLEM 59/52/48/44 -> 59/52/48/34 a cost at six seats
BONDED COALITIONS 131/134/132/116 -> 59/52/48/34 roughly halved
The coalitions row has a mechanism and the data confirms it unprompted.
GR-A07 flips a Bond to a Rivalry on Attack, and GR-E04 scores Bond
NETWORKS -- so attacking destroys the thing that scores. And the attacking
numbers in E04 are IDENTICAL to E03's, which is exactly what that
predicts: break every Bond and each seat is a coalition of one, so GR-E04
degenerates into GR-E03. That check was not designed; it fell out.
F14 -> applied. F17 strengthened and no longer bounded to co-op: ATTACK
has no mode in which it helps, and one where it actively destroys your
score.
Still framed as a question rather than a verdict. DARVO is the pattern the
game is about not falling into, so a self-destructive ATTACK may be the
design. What ground-game has to decide is whether the namesake mechanic
being unreachable in competent play -- in all three modes -- is intended.
make all: exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:38:27 +02:00
|
|
|
|
mode: games_ground::ScoringMode::SharedGround,
|
CB-WP-0047: all four boards, and every mode named on the page
The modes were already implemented; nothing had ever COMPARED them. The
scenarios were not implemented at all: edition::deal has taken a
scenario_id since it was written and the only caller passed the literal
"SCN_01", so 15 of 20 Problem cards had never been dealt by anything.
The seam was the whole mechanism and it sat unused, with nothing red
because nothing asked.
Scenario is now state (serde default SCN_01, so all 26 recordings replay
unchanged), selected by preset `scn-03-4p` with `standard-Np` still
meaning SCN_01, and by --scenario/SCENARIO= accepting ids, numbers or
titles, validated against the edition rather than a pattern.
The threshold now comes off the Scenario card, closing F25's hardcoded
5/7/9. The first version of that control was worthless and mutation said
so: all four scenarios print 5/7/9, so reverting to the bands left it
green. Split threshold_from() so it can be handed a card that disagrees.
The header read `scoring CommonProblem` where the Mode card is titled
COMMON PROBLEM, PERSONAL EDGE -- the defect CB-WP-0034 deleted from the
move buttons, still standing on the line that says what winning means.
The coverage probe was matching that Debug output and went red when it
was fixed: third instance (CB-WP-0024, CB-WP-0034). Page now carries the
premise, the mode's rules text, and the tiebreak.
scenario-panel plays 4x3x3. Findings: SCN_01 and SCN_02 are the same
board (identical cells, pinned by a characterisation test); SCN_04 is
the hard board at 2p (52% vs 67/73%, the only deck needing two Repair);
and group success is EXACTLY equal across all three modes in all 36
cells, because greedy never reads state.mode -- filed F27, the two
competitive modes are scoring lenses over cooperative play.
F28: SHARED GROUND's mastery subtracts penalties from the claimed COUNT
where the mode card's shared score is claimed VALUE. Raised, not fixed;
scoring is ground-game's to rule on.
Also fixes design.py reporting a backticked path as no reproduction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:51:46 +02:00
|
|
|
|
scenario: "SCN_01".into(),
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
pace: table::Pace::Speed,
|
CB-WP-0038: variant selection, H1 implemented, and H1 measured
ground-game packages hypotheses as selectable rules variants — a catalog,
a rules_delta.yaml, and prose — and their note is explicit that CSV text
alone is not executable here. So the kernel gains a Variant in game state:
in the state, therefore in the hash, therefore in the recording, because a
scenario replayed under a different variant would diverge silently.
Baseline is bit-for-bit what it was, asserted across seat counts and
seeds. A variant system that perturbs the baseline invalidates every
measurement this repo has.
H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on
their own defects: "unclaimed" misread as face-up-and-unsolved, and the
attacker's Stress read after the attack's effects. Their `unchanged:` list
is asserted rather than trusted — that list is their claim about their own
experiment.
Measured, and three of their four criteria fail. DARVO arm rate is still
0 under greedy; ATTACK selection does not rise and falls for the rank-75
policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats.
The mechanism is not the assumed one: greedy answers the pressure by
regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the
arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under
competent play.
A harness defect was caught before the claim: sweep discarded refused
games silently and never reported its count, so "nobody won" and "nothing
played" printed identically. Reporting H1 as unwinnable on that basis
would have been the ADR-0018 family aimed at another repo's design. All
200 games ran in every cell; the zeros are real.
Chaos d8 = 8 — the window's first override, redrew L against a structural
L, so it changed nothing. Window 3 recorded in ChaosRollHistory.
NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1
result may reach ground-game until it has run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:50:08 +02:00
|
|
|
|
variant: games_ground::Variant::Baseline,
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
};
|
|
|
|
|
|
let mut out: Vec<u8> = Vec::new();
|
|
|
|
|
|
let summary = table::play(&config, "".as_bytes(), &mut out).expect("bot game");
|
|
|
|
|
|
assert_eq!(summary.rounds, 5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-02 02:45:55 +02:00
|
|
|
|
/// `parse_args` in play mode. Panics on an inspect-mode result, so a
|
|
|
|
|
|
/// flag that quietly switched modes could not pass for a play flag.
|
|
|
|
|
|
fn play_args(list: &[&str]) -> Result<Config, String> {
|
|
|
|
|
|
match parse_args(&args(list))? {
|
|
|
|
|
|
Mode::Play(c) => Ok(c),
|
|
|
|
|
|
Mode::Inspect { .. } => panic!("{list:?} parsed as inspect, not play"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
#[test]
|
|
|
|
|
|
fn flags_parse_and_bad_ones_are_refused() {
|
2026-08-02 02:45:55 +02:00
|
|
|
|
let c = play_args(&["--seed", "9", "--players", "4", "--seat", "2"]).unwrap();
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
assert_eq!((c.seed, c.players, c.human_seats.clone()), (9, 4, vec![2]));
|
2026-08-02 02:45:55 +02:00
|
|
|
|
assert_eq!(play_args(&[]).unwrap().human_seats, vec![0]);
|
|
|
|
|
|
assert!(play_args(&["--all-bots"]).unwrap().human_seats.is_empty());
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
// GR-O01's range, refused at the door rather than at setup.
|
|
|
|
|
|
assert!(parse_args(&args(&["--players", "7"])).is_err());
|
|
|
|
|
|
assert!(parse_args(&args(&["--players", "1"])).is_err());
|
|
|
|
|
|
// A seat nobody occupies would silently never be prompted.
|
|
|
|
|
|
assert!(parse_args(&args(&["--players", "3", "--seat", "3"])).is_err());
|
|
|
|
|
|
assert!(parse_args(&args(&["--seed"])).is_err());
|
2026-08-02 02:45:55 +02:00
|
|
|
|
// The two modes take disjoint flags in both directions. A flag
|
|
|
|
|
|
// accepted and ignored is how a user believes they inspected
|
|
|
|
|
|
// seed 9 when they inspected whatever the recording holds.
|
|
|
|
|
|
assert!(parse_args(&args(&["--inspect", "x", "--seed", "9"])).is_err());
|
|
|
|
|
|
assert!(parse_args(&args(&["--as", "2"])).is_err());
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
parse_args(&args(&["--inspect", "x", "--as", "2"])).unwrap(),
|
|
|
|
|
|
Mode::Inspect { eyes, .. } if eyes == inspect::Eyes::Seat(cb_kernel::PlayerId(2))
|
|
|
|
|
|
));
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
parse_args(&args(&["--inspect", "x"])).unwrap(),
|
|
|
|
|
|
Mode::Inspect {
|
|
|
|
|
|
eyes: inspect::Eyes::Spectator,
|
|
|
|
|
|
..
|
|
|
|
|
|
}
|
|
|
|
|
|
));
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
assert!(parse_args(&args(&["--nope"])).is_err());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A bundle is written only when asked, and it replays.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn a_bundle_is_written_and_replays() {
|
|
|
|
|
|
let dir = std::env::temp_dir().join(format!("cb-play-{}", std::process::id()));
|
|
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
|
|
|
|
std::fs::create_dir_all(&dir).expect("tmp");
|
|
|
|
|
|
let config = Config {
|
|
|
|
|
|
seed: 42,
|
|
|
|
|
|
players: 3,
|
|
|
|
|
|
human_seats: vec![],
|
|
|
|
|
|
bot: "greedy".into(),
|
|
|
|
|
|
replay_dir: Some(dir.clone()),
|
|
|
|
|
|
record: Some(dir.join("session.yaml")),
|
CB-WP-0012-T04: cb-render-html — stage 1 draws, and the browser is the toolkit
Delivers ADR-0007 Decision 1: visualization, drag-to-propose and hot-seat
play, at a measured marginal AM-4a cost of zero.
games-ground shipped: 23 third-party crates
cb-render-html: 23 third-party crates
new crates introduced: 0
Measured, not asserted — the survey's own lesson. AM-4a is unmoved at
246,250; own source is 7,636 -> 9,652.
What shipped:
crates/cb-render-html doc.rs (HTML/SVG emission, incl. the relationship
graph), input.rs (pointer facts -> commands),
serve.rs (Guard, Request, loopback bind)
tools/cb-play hotseat.rs + `--serve PORT`
Per ADR-0007 Decision 2 there is NO cb-render-api and NO cb-render-null.
The renderer targets the existing Project trait; the port waits for
stage 2's wgpu implementation to be its second use.
The six controls, all live, all mutation-checked (8 mutations, each red
for its stated reason):
1-3 token / Origin+Sec-Fetch-Site / explicit 127.0.0.1 bind
4 a token-less request is refused, in the unit AND over a real socket
5 JS may not construct commands — the page reports pointer facts, Rust
resolves them against the legal list the aggregate already offered,
and a test asserts the emitted script contains no game vocabulary
6 the coverage gate crosses the language boundary: it walks the
serialized view for leaf paths and requires each token to appear in
the PARSED emitted document, with a test that the parse really is a
parse (script/style contents must not count as rendered)
The gate fired on its author again, on its first run: ground_choices.*.
choice, ground_choices.*.problem and players.*.blame_from were in neither
list. The last is the one worth keeping — an EMPTY vector is a leaf path
of its own, and it now renders as an explicit absence.
Also, a mutation that did not go red: removing the Sec-Fetch-Site arm
alone left the cross-site test green, because the Origin check caught it
independently. Both had to be removed before the control bit. Recorded
because a control that passes for a reason you did not intend has not
been demonstrated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 04:27:25 +02:00
|
|
|
|
serve: None,
|
CB-WP-0027 T01-T04: the commentary track
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:37:46 +02:00
|
|
|
|
trial: None,
|
GR-E03 and GR-E04 played to the end — F14 closed, and the reason they were
unplayed was ours
Tier S (a fix and a measurement inside a boundary; chaos d8=4 from
CB-WP-0029's roll, no override).
cb-play built EVERY game with ScoringMode::SharedGround and passed an
empty patch. The mode was settable in scenarios and not from the driver,
so two of the three shipped modes were unreachable from the only way
anyone actually plays. F14 sat open for a week because nobody could reach
the thing it was about.
--mode added. All three now play out and give DIFFERENT WINNERS FROM
IDENTICAL PLAY: shared -> all four seats (mastery 4), common -> P3 alone
(top personal scorer), coalitions -> P1+P2 (best Bond network, 4>3>2).
Same 37 commands, three answers.
AND THEY ANSWER F17'S OPEN QUESTION. I had flagged that ATTACK might earn
its place where Blame costs personal score. It does not, in any mode:
SHARED GROUND 132/165/190/200 -> identical free but pointless
COMMON PROBLEM 59/52/48/44 -> 59/52/48/34 a cost at six seats
BONDED COALITIONS 131/134/132/116 -> 59/52/48/34 roughly halved
The coalitions row has a mechanism and the data confirms it unprompted.
GR-A07 flips a Bond to a Rivalry on Attack, and GR-E04 scores Bond
NETWORKS -- so attacking destroys the thing that scores. And the attacking
numbers in E04 are IDENTICAL to E03's, which is exactly what that
predicts: break every Bond and each seat is a coalition of one, so GR-E04
degenerates into GR-E03. That check was not designed; it fell out.
F14 -> applied. F17 strengthened and no longer bounded to co-op: ATTACK
has no mode in which it helps, and one where it actively destroys your
score.
Still framed as a question rather than a verdict. DARVO is the pattern the
game is about not falling into, so a self-destructive ATTACK may be the
design. What ground-game has to decide is whether the namesake mechanic
being unreachable in competent play -- in all three modes -- is intended.
make all: exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:38:27 +02:00
|
|
|
|
mode: games_ground::ScoringMode::SharedGround,
|
CB-WP-0047: all four boards, and every mode named on the page
The modes were already implemented; nothing had ever COMPARED them. The
scenarios were not implemented at all: edition::deal has taken a
scenario_id since it was written and the only caller passed the literal
"SCN_01", so 15 of 20 Problem cards had never been dealt by anything.
The seam was the whole mechanism and it sat unused, with nothing red
because nothing asked.
Scenario is now state (serde default SCN_01, so all 26 recordings replay
unchanged), selected by preset `scn-03-4p` with `standard-Np` still
meaning SCN_01, and by --scenario/SCENARIO= accepting ids, numbers or
titles, validated against the edition rather than a pattern.
The threshold now comes off the Scenario card, closing F25's hardcoded
5/7/9. The first version of that control was worthless and mutation said
so: all four scenarios print 5/7/9, so reverting to the bands left it
green. Split threshold_from() so it can be handed a card that disagrees.
The header read `scoring CommonProblem` where the Mode card is titled
COMMON PROBLEM, PERSONAL EDGE -- the defect CB-WP-0034 deleted from the
move buttons, still standing on the line that says what winning means.
The coverage probe was matching that Debug output and went red when it
was fixed: third instance (CB-WP-0024, CB-WP-0034). Page now carries the
premise, the mode's rules text, and the tiebreak.
scenario-panel plays 4x3x3. Findings: SCN_01 and SCN_02 are the same
board (identical cells, pinned by a characterisation test); SCN_04 is
the hard board at 2p (52% vs 67/73%, the only deck needing two Repair);
and group success is EXACTLY equal across all three modes in all 36
cells, because greedy never reads state.mode -- filed F27, the two
competitive modes are scoring lenses over cooperative play.
F28: SHARED GROUND's mastery subtracts penalties from the claimed COUNT
where the mode card's shared score is claimed VALUE. Raised, not fixed;
scoring is ground-game's to rule on.
Also fixes design.py reporting a backticked path as no reproduction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:51:46 +02:00
|
|
|
|
scenario: "SCN_01".into(),
|
CB-WP-0036 done: the pace flag and the first ornament declarations
--pace speed|interactive, defaulting to Speed. Nothing reads it yet, and
that is the point: it is the seam clay-animate attaches to, and a seam is
cheap now where a retrofit would not be. A misspelt pace is refused rather
than defaulting, because quietly falling back to Speed would look exactly
like the renderer being broken.
I3 is asserted rather than intended: the same scripted game at both paces
must produce a byte-identical serialised recording and the same end state
hash. Mutation-proven — leak the pace into the seed and it fails with "the
recording differs by pace, so a renderer has become mechanism".
specs/OrnamentRegister.md carries four declarations. This reverses the
reasoning written in T03 earlier, which said the first declarations would
come from F18's unvendored files: instances already existed. Hand order is
what prompted the category, and "who deals" was the maintainer's own
example. O3 is the interesting one — seat ORDER is mechanism because
GR-R08 rotates Lead, while where a seat is drawn is not.
I5 is executable: check_ornament_falsifier fails any row still declared
that names no falsifier, mutation-proven red on O1. Presence, never
adequacy, and the finding text says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:15:38 +02:00
|
|
|
|
pace: table::Pace::Speed,
|
CB-WP-0038: variant selection, H1 implemented, and H1 measured
ground-game packages hypotheses as selectable rules variants — a catalog,
a rules_delta.yaml, and prose — and their note is explicit that CSV text
alone is not executable here. So the kernel gains a Variant in game state:
in the state, therefore in the hash, therefore in the recording, because a
scenario replayed under a different variant would diverge silently.
Baseline is bit-for-bit what it was, asserted across seat counts and
seeds. A variant system that perturbs the baseline invalidates every
measurement this repo has.
H1-A and H1-B implemented from rules_delta.yaml and mutation-proven on
their own defects: "unclaimed" misread as face-up-and-unsolved, and the
attacker's Stress read after the attack's effects. Their `unchanged:` list
is asserted rather than trusted — that list is their claim about their own
experiment.
Measured, and three of their four criteria fail. DARVO arm rate is still
0 under greedy; ATTACK selection does not rise and falls for the rank-75
policy; group success collapses from 165/190/200 to 0 at 3/4/6 seats.
The mechanism is not the assumed one: greedy answers the pressure by
regulating, Stress plateaus at 3, so it never reaches the gate at 4 or the
arm at 5 — H1-A acts as a solve-rate tax and H1-B is unreachable under
competent play.
A harness defect was caught before the claim: sweep discarded refused
games silently and never reported its count, so "nobody won" and "nothing
played" printed identically. Reporting H1 as unwinnable on that basis
would have been the ADR-0018 family aimed at another repo's design. All
200 games ran in every cell; the zeros are real.
Chaos d8 = 8 — the window's first override, redrew L against a structural
L, so it changed nothing. Window 3 recorded in ChaosRollHistory.
NOT REVIEWED: tier L owes a separate-agent adversarial review, and no H1
result may reach ground-game until it has run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 00:50:08 +02:00
|
|
|
|
variant: games_ground::Variant::Baseline,
|
CB-WP-0008-T02: cb-play — INTENT stage 0's CLI player
A human seat is a Policy like any bot, so the CLI adds no second driver:
HumanPolicy renders the projection, lists the legal commands and reads an
index or `pass`. `make play` runs it; `--all-bots` watches one.
K13's Project trait gains its first implementor after six passes with
none. Hidden: other seats' face-down selections until Reveal, hands and
deck (counts only), a face-down Problem's suit and value, and the seed —
not secret content, but a seat holding it can compute the deck.
A played session becomes an artifact: --record writes it as a scenario
the runner executes, --replay writes a .cbreplay bundle. record.rs is the
inverse of parse_command and its warrant is a round-trip test over every
command shape.
The acceptance test for the projection passed vacuously twice. First it
asserted the text contained "face-down", which every render does because
of Problems. Counted, it then reported zero inspected entries: seats are
asked in order, so a human at P1 is prompted before anyone has selected.
Seated at P3 it inspects ten entries and dies when the projection is
mutated to reveal everything. Counting what the harness examined caught
both, which is the second time that remedy has worked where a stronger
predicate would not have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 14:43:53 +02:00
|
|
|
|
};
|
|
|
|
|
|
let mut out: Vec<u8> = Vec::new();
|
|
|
|
|
|
let summary = table::play(&config, "".as_bytes(), &mut out).expect("game");
|
|
|
|
|
|
let bundle = summary.bundle.expect("bundle path");
|
|
|
|
|
|
assert!(bundle.join("manifest.yaml").exists());
|
|
|
|
|
|
assert!(bundle.join("commands.log").exists());
|
|
|
|
|
|
let report = cb_game_runtime::replay::replay::<GroundState>(&bundle)
|
|
|
|
|
|
.unwrap_or_else(|e| panic!("bundle does not replay: {e}"));
|
|
|
|
|
|
assert_eq!(report.hash, summary.end_state_hash);
|
|
|
|
|
|
|
|
|
|
|
|
// The recorded scenario is a file the runner can execute, which
|
|
|
|
|
|
// is what makes a played session a regression test.
|
|
|
|
|
|
let text = std::fs::read_to_string(summary.recorded.expect("record path")).expect("read");
|
|
|
|
|
|
let parsed = cb_game_runtime::ScenarioFile::from_yaml(&text).expect("parse");
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
run::<GroundState>(&parsed),
|
|
|
|
|
|
RunOutcome::Passed { .. }
|
|
|
|
|
|
));
|
|
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|