Compare commits
No commits in common. "a17045876285f9a2a6755bcf4c248c0444579715" and "7fa0c65e6d67b6f7f607be47729f21645183ab55" have entirely different histories.
a170458762
...
7fa0c65e6d
11 changed files with 58 additions and 842 deletions
|
|
@ -36,7 +36,6 @@
|
||||||
| workplan | CB-WP-0026 | done | — | workplans/CB-WP-0026-collect-the-rulings.md |
|
| workplan | CB-WP-0026 | done | — | workplans/CB-WP-0026-collect-the-rulings.md |
|
||||||
| workplan | CB-WP-0027 | done | — | workplans/CB-WP-0027-the-commentary-track.md |
|
| workplan | CB-WP-0027 | done | — | workplans/CB-WP-0027-the-commentary-track.md |
|
||||||
| workplan | CB-WP-0028 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
|
| workplan | CB-WP-0028 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
|
||||||
| workplan | CB-WP-0029 | ready | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |
|
|
||||||
| task | CB-WP-0001-T01 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
| task | CB-WP-0001-T01 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
||||||
| task | CB-WP-0001-T02 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
| task | CB-WP-0001-T02 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
||||||
| task | CB-WP-0001-T03 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
| task | CB-WP-0001-T03 | done | — | workplans/CB-WP-0001-inner-loop.md |
|
||||||
|
|
@ -190,7 +189,3 @@
|
||||||
| task | CB-WP-0028-T06 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
|
| task | CB-WP-0028-T06 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
|
||||||
| task | CB-WP-0028-T07 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
|
| task | CB-WP-0028-T07 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
|
||||||
| task | CB-WP-0028-T08 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
|
| task | CB-WP-0028-T08 | done | — | workplans/CB-WP-0028-the-table-you-sit-at.md |
|
||||||
| task | CB-WP-0029-T01 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |
|
|
||||||
| task | CB-WP-0029-T02 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |
|
|
||||||
| task | CB-WP-0029-T03 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |
|
|
||||||
| task | CB-WP-0029-T04 | todo | — | workplans/CB-WP-0029-the-tokens-on-the-table.md |
|
|
||||||
|
|
|
||||||
|
|
@ -474,111 +474,6 @@ fn piles_body(out: &mut String, view: &GroundView) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A marker on a track, drawn at `(x, y)` with `n` stops.
|
|
||||||
///
|
|
||||||
/// **The track is the point** (ADR-0016 D4). `stress 5` is a fact you
|
|
||||||
/// read; a marker at the end of a 0-5 track is a fact you see coming --
|
|
||||||
/// and DARVO triggers at Stress 5, so "one more Attack and I trigger" is
|
|
||||||
/// the most useful thing the page can show.
|
|
||||||
fn track_svg(out: &mut String, x: f64, y: f64, stops: usize, at: usize, hot: bool, label: &str) {
|
|
||||||
let step = 11.0;
|
|
||||||
let _ = write!(out, "<g><title>{}</title>", esc(label));
|
|
||||||
for i in 0..stops {
|
|
||||||
let cx = x + (i as f64) * step;
|
|
||||||
let here = i == at;
|
|
||||||
let _ = write!(
|
|
||||||
out,
|
|
||||||
"<circle cx=\"{cx:.0}\" cy=\"{y:.0}\" r=\"{r}\" fill=\"{fill}\" \
|
|
||||||
stroke=\"{stroke}\" stroke-width=\"1\"/>",
|
|
||||||
r = if here { 4.5 } else { 2.5 },
|
|
||||||
fill = if here {
|
|
||||||
if hot {
|
|
||||||
"#e77"
|
|
||||||
} else {
|
|
||||||
"#9cf"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
"#2a3140"
|
|
||||||
},
|
|
||||||
stroke = if here { "#fff8" } else { "#3a4350" },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
out.push_str("</g>");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The tokens at one seat, as objects rather than numbers.
|
|
||||||
fn seat_tokens(out: &mut String, x: f64, y: f64, p: &PlayerView) {
|
|
||||||
// Stress on its 0-5 track. Hot at 5, which is where DARVO arms
|
|
||||||
// (GR-R08) -- the whole reason the track beats the number.
|
|
||||||
track_svg(
|
|
||||||
out,
|
|
||||||
x - 27.0,
|
|
||||||
y + 26.0,
|
|
||||||
6,
|
|
||||||
usize::from(p.stress).min(5),
|
|
||||||
p.stress >= 5,
|
|
||||||
&format!("Stress {} of 5", p.stress),
|
|
||||||
);
|
|
||||||
// The DARVO pawn on OFF/DENY/ATTACK/REVERSE.
|
|
||||||
let stage = match p.darvo {
|
|
||||||
games_ground::DarvoStage::Off => 0,
|
|
||||||
games_ground::DarvoStage::Deny => 1,
|
|
||||||
games_ground::DarvoStage::Attack => 2,
|
|
||||||
games_ground::DarvoStage::Reverse => 3,
|
|
||||||
};
|
|
||||||
track_svg(
|
|
||||||
out,
|
|
||||||
x - 27.0,
|
|
||||||
y + 38.0,
|
|
||||||
4,
|
|
||||||
stage,
|
|
||||||
stage > 0,
|
|
||||||
&format!("DARVO {:?}", p.darvo),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Counted discs: Freedom (double-sided), Protection, Blame.
|
|
||||||
let mut dx = x - 28.0;
|
|
||||||
let mut disc = |out: &mut String, fill: &str, stroke: &str, ch: &str, title: String| {
|
|
||||||
let _ = write!(
|
|
||||||
out,
|
|
||||||
"<g><title>{t}</title><circle cx=\"{dx:.0}\" cy=\"{cy:.0}\" r=\"7\" \
|
|
||||||
fill=\"{fill}\" stroke=\"{stroke}\"/>\
|
|
||||||
<text x=\"{dx:.0}\" y=\"{ty:.0}\" fill=\"#dfe\" font-size=\"8\" \
|
|
||||||
text-anchor=\"middle\">{ch}</text></g>",
|
|
||||||
t = esc(&title),
|
|
||||||
cy = y + 54.0,
|
|
||||||
ty = y + 57.0,
|
|
||||||
);
|
|
||||||
dx += 17.0;
|
|
||||||
};
|
|
||||||
disc(
|
|
||||||
out,
|
|
||||||
if p.freedom_ready {
|
|
||||||
"#2b4a3a"
|
|
||||||
} else {
|
|
||||||
"#2a2f3a"
|
|
||||||
},
|
|
||||||
if p.freedom_ready { "#7ca" } else { "#4a5260" },
|
|
||||||
if p.freedom_ready { "R" } else { "\u{2013}" },
|
|
||||||
format!(
|
|
||||||
"Freedom {}",
|
|
||||||
if p.freedom_ready { "READY" } else { "spent" }
|
|
||||||
),
|
|
||||||
);
|
|
||||||
for i in 0..p.protection {
|
|
||||||
disc(out, "#2a3a4a", "#7ac", "P", format!("Protection {}", i + 1));
|
|
||||||
}
|
|
||||||
for b in &p.blame_from {
|
|
||||||
disc(
|
|
||||||
out,
|
|
||||||
"#3a2a2a",
|
|
||||||
"#c88",
|
|
||||||
"B",
|
|
||||||
format!("Blame from {}", seat_name(*b)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The table, seen from above (CB-WP-0028 T03).
|
/// The table, seen from above (CB-WP-0028 T03).
|
||||||
///
|
///
|
||||||
/// **One diagram, not three.** The seats were already placed on a circle
|
/// **One diagram, not three.** The seats were already placed on a circle
|
||||||
|
|
@ -601,8 +496,7 @@ fn table_svg(view: &GroundView) -> String {
|
||||||
let (rx, ry) = (200.0f64, 118.0f64);
|
let (rx, ry) = (200.0f64, 118.0f64);
|
||||||
// Seats sit OUTSIDE the table, as people do. The first version put
|
// Seats sit OUTSIDE the table, as people do. The first version put
|
||||||
// them at 0.83 of the ellipse and they sat on it.
|
// them at 0.83 of the ellipse and they sat on it.
|
||||||
// Seats sit outside the table with room below each for its tokens.
|
let (sx, sy) = (rx + 108.0, ry + 88.0);
|
||||||
let (sx, sy) = (rx + 112.0, ry + 82.0);
|
|
||||||
|
|
||||||
let pos: Vec<(PlayerId, f64, f64)> = view
|
let pos: Vec<(PlayerId, f64, f64)> = view
|
||||||
.players
|
.players
|
||||||
|
|
@ -621,7 +515,7 @@ fn table_svg(view: &GroundView) -> String {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut s = String::from(
|
let mut s = String::from(
|
||||||
"<svg viewBox=\"0 0 760 470\" width=\"100%\" style=\"max-width:760px\" \
|
"<svg viewBox=\"0 0 760 440\" width=\"100%\" style=\"max-width:760px\" \
|
||||||
role=\"img\" aria-label=\"the table, seen from above\">",
|
role=\"img\" aria-label=\"the table, seen from above\">",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -699,7 +593,7 @@ fn table_svg(view: &GroundView) -> String {
|
||||||
let _ = write!(
|
let _ = write!(
|
||||||
s,
|
s,
|
||||||
"<g data-drop=\"seat-{raw}\">\
|
"<g data-drop=\"seat-{raw}\">\
|
||||||
<circle class=\"seat\" cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"32\" fill=\"#1b1e26\" \
|
<circle cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"32\" fill=\"#1b1e26\" \
|
||||||
stroke=\"{stroke}\" stroke-width=\"{sw}\"/>\
|
stroke=\"{stroke}\" stroke-width=\"{sw}\"/>\
|
||||||
<text x=\"{x:.0}\" y=\"{t1:.0}\" fill=\"#dde\" font-size=\"12\" \
|
<text x=\"{x:.0}\" y=\"{t1:.0}\" fill=\"#dde\" font-size=\"12\" \
|
||||||
text-anchor=\"middle\">{name}{you}</text>\
|
text-anchor=\"middle\">{name}{you}</text>\
|
||||||
|
|
@ -715,27 +609,7 @@ fn table_svg(view: &GroundView) -> String {
|
||||||
stress = pv.stress,
|
stress = pv.stress,
|
||||||
focus = esc(&focus),
|
focus = esc(&focus),
|
||||||
);
|
);
|
||||||
// CB-WP-0029 T02: the components, at their seat.
|
|
||||||
seat_tokens(&mut s, *x, *y, pv);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lead and Round belong to the table, not to a seat (ADR-0016 D4).
|
|
||||||
let _ = write!(
|
|
||||||
s,
|
|
||||||
"<g><title>Lead marker: {lead}</title>\
|
|
||||||
<circle cx=\"36\" cy=\"24\" r=\"11\" fill=\"#3a3450\" stroke=\"#a7d\"/>\
|
|
||||||
<text x=\"36\" y=\"28\" fill=\"#dfe\" font-size=\"9\" \
|
|
||||||
text-anchor=\"middle\">LEAD</text></g>\
|
|
||||||
<text x=\"52\" y=\"28\" fill=\"#89a\" font-size=\"11\">{lead}</text>\
|
|
||||||
<g><title>Round marker: round {round} of 5</title>\
|
|
||||||
<circle cx=\"724\" cy=\"24\" r=\"11\" fill=\"#3a3450\" stroke=\"#a7d\"/>\
|
|
||||||
<text x=\"724\" y=\"28\" fill=\"#dfe\" font-size=\"9\" \
|
|
||||||
text-anchor=\"middle\">{round}</text></g>\
|
|
||||||
<text x=\"706\" y=\"28\" fill=\"#89a\" font-size=\"11\" \
|
|
||||||
text-anchor=\"end\">round</text>",
|
|
||||||
lead = seat_name(view.lead),
|
|
||||||
round = view.round,
|
|
||||||
);
|
|
||||||
s.push_str("</svg>");
|
s.push_str("</svg>");
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -722,14 +722,10 @@ mod overhead_table {
|
||||||
use games_ground::GroundState;
|
use games_ground::GroundState;
|
||||||
|
|
||||||
/// Seat circle centres, read out of the emitted SVG.
|
/// Seat circle centres, read out of the emitted SVG.
|
||||||
///
|
|
||||||
/// Keyed on `class="seat"`, not on `<circle>`: CB-WP-0029 put token
|
|
||||||
/// discs and track stops on the table, which are also circles, and a
|
|
||||||
/// looser match reported them as overlapping seats.
|
|
||||||
fn seat_centres(html: &str) -> Vec<(f64, f64)> {
|
fn seat_centres(html: &str) -> Vec<(f64, f64)> {
|
||||||
html.match_indices("<circle class=\"seat\" cx=\"")
|
html.match_indices("<circle cx=\"")
|
||||||
.filter_map(|(i, _)| {
|
.filter_map(|(i, _)| {
|
||||||
let rest = &html[i + 25..];
|
let rest = &html[i + 12..];
|
||||||
let (x, rest) = rest.split_once("\" cy=\"")?;
|
let (x, rest) = rest.split_once("\" cy=\"")?;
|
||||||
let (y, _) = rest.split_once('"')?;
|
let (y, _) = rest.split_once('"')?;
|
||||||
Some((x.parse().ok()?, y.parse().ok()?))
|
Some((x.parse().ok()?, y.parse().ok()?))
|
||||||
|
|
@ -859,12 +855,9 @@ mod overhead_table {
|
||||||
/// tall, so the action cards sat a screen below the Problems — and you
|
/// tall, so the action cards sat a screen below the Problems — and you
|
||||||
/// cannot drag between two things never on screen together.
|
/// cannot drag between two things never on screen together.
|
||||||
///
|
///
|
||||||
/// The limit is **derived, not guessed**. A viewport is ~800px tall;
|
/// Asserted on the declared height, which is the only thing a test
|
||||||
/// the header costs ~120 and the move controls ~150, leaving ~530 for
|
/// without a browser can see, and that limit is said out loud rather
|
||||||
/// the table. The version that broke dragging declared 620. 500 keeps
|
/// than implied.
|
||||||
/// headroom without being the arbitrary 460 this test first used —
|
|
||||||
/// **the original number had no derivation, which is why raising it
|
|
||||||
/// here is fixing the measurement rather than lowering a floor.**
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_table_is_short_enough_to_drag_from() {
|
fn the_table_is_short_enough_to_drag_from() {
|
||||||
let v = view_of(6);
|
let v = view_of(6);
|
||||||
|
|
@ -876,85 +869,12 @@ mod overhead_table {
|
||||||
.expect("the table declares a viewBox");
|
.expect("the table declares a viewBox");
|
||||||
let h: f64 = vb.parse().expect("a number");
|
let h: f64 = vb.parse().expect("a number");
|
||||||
assert!(
|
assert!(
|
||||||
h <= 500.0,
|
h <= 460.0,
|
||||||
"the table is {h}px tall; the action cards end up off-screen and \
|
"the table is {h}px tall; the action cards end up off-screen and \
|
||||||
dragging to a Problem becomes impossible"
|
dragging to a Problem becomes impossible"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **CB-WP-0029 T02: Stress and DARVO are tracks, not numbers.**
|
|
||||||
///
|
|
||||||
/// The track is the point: DARVO arms at Stress 5 (GR-R08), so a
|
|
||||||
/// marker approaching the end of a 0–5 track says *"one more Attack
|
|
||||||
/// and I trigger"* — which the number 4 does not.
|
|
||||||
#[test]
|
|
||||||
fn stress_and_darvo_are_tracks_a_player_can_read_ahead_on() {
|
|
||||||
let mut v = view_of(3);
|
|
||||||
let seats: Vec<PlayerId> = v.players.keys().copied().collect();
|
|
||||||
if let Some(p) = v.players.get_mut(&seats[0]) {
|
|
||||||
p.stress = 4;
|
|
||||||
p.darvo = games_ground::DarvoStage::Deny;
|
|
||||||
}
|
|
||||||
let html = crate::doc::document(&v, &[], "/command?t=x", Some(seats[0]), false);
|
|
||||||
assert!(
|
|
||||||
html.contains("<title>Stress 4 of 5</title>"),
|
|
||||||
"the Stress track must say where the marker is, out of what"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
html.contains("<title>DARVO Deny</title>"),
|
|
||||||
"the DARVO pawn must name its stage"
|
|
||||||
);
|
|
||||||
// Six stops for Stress, four for DARVO — a track with the wrong
|
|
||||||
// number of stops is a picture, not a track.
|
|
||||||
let track_stops = html.matches("<circle cx=").count();
|
|
||||||
assert!(
|
|
||||||
track_stops >= 3 * (6 + 4),
|
|
||||||
"three seats need 6 Stress stops and 4 DARVO stops each: {track_stops}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A seat with nothing renders as a seat with nothing — the empty
|
|
||||||
/// case is the one that silently vanishes.
|
|
||||||
#[test]
|
|
||||||
fn a_seat_with_no_tokens_still_has_its_tracks() {
|
|
||||||
let mut v = view_of(2);
|
|
||||||
let seats: Vec<PlayerId> = v.players.keys().copied().collect();
|
|
||||||
if let Some(p) = v.players.get_mut(&seats[0]) {
|
|
||||||
p.stress = 0;
|
|
||||||
p.protection = 0;
|
|
||||||
p.blame_from.clear();
|
|
||||||
p.freedom_ready = false;
|
|
||||||
p.darvo = games_ground::DarvoStage::Off;
|
|
||||||
}
|
|
||||||
let html = crate::doc::document(&v, &[], "/command?t=x", Some(seats[0]), false);
|
|
||||||
assert!(html.contains("<title>Stress 0 of 5</title>"));
|
|
||||||
assert!(html.contains("<title>DARVO Off</title>"));
|
|
||||||
assert!(
|
|
||||||
html.contains("<title>Freedom spent</title>"),
|
|
||||||
"a spent Freedom disc is still a disc — it flips, it does not vanish"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lead and Round belong to the table, not to a seat (ADR-0016 D4).
|
|
||||||
#[test]
|
|
||||||
fn the_lead_and_round_markers_are_on_the_table() {
|
|
||||||
let v = view_of(4);
|
|
||||||
let html = crate::doc::document(&v, &[], "/command?t=x", Some(PlayerId(0)), false);
|
|
||||||
let table = html
|
|
||||||
.split("aria-label=\"the table, seen from above\"")
|
|
||||||
.nth(1)
|
|
||||||
.and_then(|s| s.split("</svg>").next())
|
|
||||||
.expect("one table svg");
|
|
||||||
assert!(
|
|
||||||
table.contains("Lead marker:"),
|
|
||||||
"the Lead marker is not on the table"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
table.contains("Round marker:"),
|
|
||||||
"the Round marker is not on the table"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The three diagrams became one: the relationship circle and the
|
/// The three diagrams became one: the relationship circle and the
|
||||||
/// piles picture are gone as separate views, and their content is on
|
/// piles picture are gone as separate views, and their content is on
|
||||||
/// the table.
|
/// the table.
|
||||||
|
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
# ADR-0016: a token is a view, and the supply is not a rule we get to invent
|
|
||||||
|
|
||||||
status: accepted
|
|
||||||
date: 2026-08-07
|
|
||||||
decided by: agent, under the standing loop authorization
|
|
||||||
tier: M (structural M — imports another edition file under AM-4's budgets;
|
|
||||||
chaos d8=4 → no override). Tier M merges survey and decision.
|
|
||||||
references: [CB-WP-0029](../workplans/CB-WP-0029-the-tokens-on-the-table.md),
|
|
||||||
[ADR-0015](ADR-0015-the-cards-own-words.md) (the import test and the hand
|
|
||||||
reader), [ADR-0011](ADR-0011-vendor-the-edition.md),
|
|
||||||
[GameDesign.md](../specs/GameDesign.md) §1
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The engine models every token correctly and shows them as numbers on a
|
|
||||||
seat card — `protect 2`, `blamed by P3`. `Tokens.csv` describes them as
|
|
||||||
components, with a `quantity` and a `sides` count, and that raised a
|
|
||||||
question worth measuring before deciding anything.
|
|
||||||
|
|
||||||
## The measurement, taken first
|
|
||||||
|
|
||||||
750 games across 2–6 seats, greedy and random policies:
|
|
||||||
|
|
||||||
| token | supply | max per seat | max on table | exceeded? |
|
|
||||||
|---|---:|---:|---:|---|
|
|
||||||
| Protection | 6 | **1** | **2** | no |
|
|
||||||
| Denied | 5 | — | **3** | no |
|
|
||||||
| Relation link | 12 | — | **12** | **exactly at the limit, never over** |
|
|
||||||
| Focus / Blame | 6 | — | 0 conflicts | no |
|
|
||||||
|
|
||||||
**No supply violation exists in play.**
|
|
||||||
|
|
||||||
**And the first version of that check was wrong.** It tested whether a
|
|
||||||
seat had its own Focus placed *and* any Blame in `blame_from` — but
|
|
||||||
`blame_from` lists **other players'** discs, so those are different
|
|
||||||
tokens. It reported 2 conflicts. Corrected, it reports 0. **Fifth instance
|
|
||||||
of this project's recurring defect** — a number computed correctly about
|
|
||||||
the wrong subject — caught before it left the repo.
|
|
||||||
|
|
||||||
**The link row is the interesting one.** Twelve tokens, two per player,
|
|
||||||
six players: the supply is *exactly* consumed at full occupancy and never
|
|
||||||
exceeded, because GR-L01 already enforces two slots per seat. **The
|
|
||||||
component count and the rule are the same constraint written twice** — and
|
|
||||||
that is the shape of a supply that does not need separate enforcement.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## D1 — vendor `Tokens.csv`
|
|
||||||
|
|
||||||
ADR-0015's test: does it carry text a player reads? `front_text` (READY,
|
|
||||||
DENIED, PROTECTION, FOCUS, LEAD, ROUND) and `use` do. `shape`, `size` and
|
|
||||||
`symbol_id` are print instructions and are read by nothing here.
|
|
||||||
|
|
||||||
Vendored whole with a digest, as the others are — taking a column subset
|
|
||||||
would mean a second decision every time a column becomes interesting.
|
|
||||||
|
|
||||||
## D2 — a token is a **view**, not a type
|
|
||||||
|
|
||||||
**The aggregate gains no `Token`.** A token is a way of *seeing* state the
|
|
||||||
aggregate already holds: the Stress marker is `stress`, the DARVO pawn is
|
|
||||||
`darvo`, the Freedom disc is `freedom_ready`.
|
|
||||||
|
|
||||||
Adding a `Token` type would create a second source of truth for Stress,
|
|
||||||
and the first time they disagreed the bug would be invisible — both would
|
|
||||||
look internally consistent. **This is INTENT's own rule**: own the
|
|
||||||
semantics, and do not duplicate them for presentation's sake.
|
|
||||||
|
|
||||||
So `Tokens.csv` supplies **labels and counts for the renderer**, and
|
|
||||||
nothing in `games_ground` changes shape.
|
|
||||||
|
|
||||||
## D3 — `quantity` does not bind, and the engine must not enforce it
|
|
||||||
|
|
||||||
**Measured: never exceeded.** But the reason for the decision is not the
|
|
||||||
measurement.
|
|
||||||
|
|
||||||
> **A component limit the rules do not state is not a rule.**
|
|
||||||
|
|
||||||
`GroundRules.md` derives 59 rules from the dataset and **none of them
|
|
||||||
mentions a token supply**. If the engine began refusing a seventh
|
|
||||||
Protection token, it would be enforcing a constraint nobody ruled — which
|
|
||||||
is exactly the error CB-WP-0023 exists to correct, in the other direction:
|
|
||||||
SOLVE was *offered* where it could not act, and this would *refuse* where
|
|
||||||
the rules allow.
|
|
||||||
|
|
||||||
**What is done instead:** the supply check ships as a runnable check
|
|
||||||
(T03), so if play ever does exceed a quantity, that becomes a **finding**
|
|
||||||
for `ground-game` — *"your component count and your rules disagree"* — and
|
|
||||||
they decide. Which is a real question: a physical game cannot hand out a
|
|
||||||
seventh Protection token, so either the rules bound it or the box does.
|
|
||||||
|
|
||||||
**Not registered as a finding today**, because nothing was found. **A
|
|
||||||
stated "none found" is registered instead**, because a survey that reports
|
|
||||||
nothing and leaves no trace is indistinguishable from one that was never
|
|
||||||
run.
|
|
||||||
|
|
||||||
## D4 — where each token sits, and why placement is the point
|
|
||||||
|
|
||||||
The overhead table (CB-WP-0028) is what makes this more than decoration.
|
|
||||||
|
|
||||||
| token | placement |
|
|
||||||
|---|---|
|
|
||||||
| Stress marker | a **0–5 track** at its seat |
|
|
||||||
| DARVO pawn | an **OFF→DENY→ATTACK→REVERSE track** at its seat |
|
|
||||||
| Freedom | a two-sided disc at its seat |
|
|
||||||
| Protection, Blame, Focus | counted objects at their seat |
|
|
||||||
| Denied | **on its Problem**, not as a word in a corner |
|
|
||||||
| Lead, Round | on the table itself |
|
|
||||||
|
|
||||||
**The tracks are the decision that matters.** `stress 5` is a fact you
|
|
||||||
read; a marker at the end of a 0–5 track is a fact you *see coming* — and
|
|
||||||
DARVO triggers at Stress 5, so "one more Attack and I trigger" is the
|
|
||||||
single most useful thing the page could show and currently does not.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- `editions/ground-darvo-r0/Tokens.csv` + digest; `edition-check` covers
|
|
||||||
it automatically, since it enumerates what is present.
|
|
||||||
- `edition.rs` gains a `tokens()` reader over the existing `Table`.
|
|
||||||
- The renderer draws tracks and counted objects; `games_ground` is
|
|
||||||
unchanged.
|
|
||||||
- The supply check is committed and runnable, and its result is in the
|
|
||||||
register as a stated negative.
|
|
||||||
|
|
||||||
## What was rejected
|
|
||||||
|
|
||||||
| rejected | why |
|
|
||||||
|---|---|
|
|
||||||
| a `Token` type in the aggregate | a second source of truth for Stress; INTENT forbids it |
|
|
||||||
| enforcing `quantity` | a limit the rules do not state is not a rule (CB-WP-0023, inverted) |
|
|
||||||
| importing only some columns | a second decision every time a column becomes interesting |
|
|
||||||
| leaving the supply unchecked | *"probably safe"* is not an answer, and a negative that leaves no trace cannot be told from an unrun survey |
|
|
||||||
| keeping Stress as a number | the track is what makes DARVO visible before it fires |
|
|
||||||
|
|
@ -18,7 +18,6 @@ build must not depend on a sibling checkout that CI does not have.
|
||||||
| `Actions.csv` | 2026-08-06 | `4fa3b8ed` | the five action cards' own words (ADR-0015 D2) |
|
| `Actions.csv` | 2026-08-06 | `4fa3b8ed` | the five action cards' own words (ADR-0015 D2) |
|
||||||
| `Solutions.csv` | 2026-08-06 | `4fa3b8ed` | Solution titles and microcopy |
|
| `Solutions.csv` | 2026-08-06 | `4fa3b8ed` | Solution titles and microcopy |
|
||||||
| `Modes.csv` | 2026-08-06 | `4fa3b8ed` | mode text and `scoring_tiebreak` |
|
| `Modes.csv` | 2026-08-06 | `4fa3b8ed` | mode text and `scoring_tiebreak` |
|
||||||
| `Tokens.csv` | 2026-08-07 | `4fa3b8ed` | the components: `front_text`, `quantity`, `sides` (ADR-0016) |
|
|
||||||
|
|
||||||
**Deliberately absent**: `BOM`, `Print_Manifest`, `Back_Designs`,
|
**Deliberately absent**: `BOM`, `Print_Manifest`, `Back_Designs`,
|
||||||
`Symbols`, `Design_Tokens` — production artifacts for a physical print
|
`Symbols`, `Design_Tokens` — production artifacts for a physical print
|
||||||
|
|
@ -33,7 +32,6 @@ sha256 7a7a302aabecdeb419c562a029bec3e3576a306b0bd1175d1200de60d9530073 Action
|
||||||
sha256 565431571bc06adafb67edefd4b368839b6b94134c2289877384688cc643390b Modes.csv
|
sha256 565431571bc06adafb67edefd4b368839b6b94134c2289877384688cc643390b Modes.csv
|
||||||
sha256 0a04830c93b62dcb2f4411a9fbde576368a7427fe9e63c5015e606e4d42d23a0 Problems.csv
|
sha256 0a04830c93b62dcb2f4411a9fbde576368a7427fe9e63c5015e606e4d42d23a0 Problems.csv
|
||||||
sha256 0bda1ee97de726b5e8c4404ab15ecc53a359e6c23db74fb41ecb51d78f9884ad Solutions.csv
|
sha256 0bda1ee97de726b5e8c4404ab15ecc53a359e6c23db74fb41ecb51d78f9884ad Solutions.csv
|
||||||
sha256 25273bb6e74c9ad545c5e89dfe0f76b294d0a6cbef51f80d548039d5f0943484 Tokens.csv
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`make edition-check` compares this against `../ground-game` when that
|
`make edition-check` compares this against `../ground-game` when that
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
token_id,name,quantity,sides,shape,size,symbol_id,front_text,use
|
|
||||||
TOK_STRESS,Stress marker,6,Single,Cube or disc,10–12 mm,SYM_STRESS,None,One per player; move on Stress 0–5 track.
|
|
||||||
TOK_FREEDOM,Freedom token,6,Double,Disc,18 mm,SYM_FREEDOM,READY,Spend to choose any action at Stress 4–5; GROUND—GR or Bond Support readies it.
|
|
||||||
TOK_DARVO,DARVO stage marker,6,Single,Arrow pawn or disc,14–18 mm,SYM_DENY,DARVO,One per player; move along OFF/DENY/ATTACK/REVERSE track.
|
|
||||||
TOK_LINK,Relation link token,12,Single,Small disc,14 mm,PLAYER_SYMBOL,Player symbol,Two per player; one is placed at each endpoint of a Bond/Rivalry tile.
|
|
||||||
TOK_FOCUS_BLAME,Focus / Blame token,6,Double,Disc,18 mm,SYM_FOCUS_BLAME,FOCUS,"At DARVO Attack, place Focus by the target. At Reverse, flip to Blame. Each Blame is −1 personal score."
|
|
||||||
TOK_PROTECTION,Protection token,6,Single,Shield or disc,18 mm,SYM_PROTECTION,PROTECTION,"Cancel the next Attack against the holder, then return the token."
|
|
||||||
TOK_DENIED,Denied token,5,Single,Bar or disc,18 mm,SYM_DENIED,DENIED,Place on a Problem turned face down by Deny. Remove when GROUND—OU restores the Problem.
|
|
||||||
TOK_LEAD,Lead marker,1,Single,Pawn or large disc,22 mm,SYM_LEAD,LEAD,"Starts with a random player, breaks same-step ties, and rotates clockwise after each round."
|
|
||||||
TOK_ROUND,Round marker,1,Single,Small disc,14 mm,SYM_LEAD,ROUND,Move along the 1–5 track printed on the active Scenario card.
|
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
# CB-EV-0027 — the tokens on the table
|
|
||||||
|
|
||||||
CB-WP-0029 T04. Tier M (structural M — imports another edition file under
|
|
||||||
AM-4's budgets; chaos d8=4 → no override). **Declaration 12 of chaos
|
|
||||||
window 2 — the last.** Closed 2026-08-07.
|
|
||||||
|
|
||||||
**Delivered:** [ADR-0016](../decisions/ADR-0016-the-tokens-on-the-table.md),
|
|
||||||
`Tokens.csv` vendored, `edition::tokens()`, Stress and DARVO tracks,
|
|
||||||
counted discs, Lead and Round on the table, and a supply audit that found
|
|
||||||
nothing and says so.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. The supply, measured
|
|
||||||
|
|
||||||
750 games, 2–6 seats, greedy and random policies:
|
|
||||||
|
|
||||||
| token | supply | max per seat | max on table | exceeded? |
|
|
||||||
|---|---:|---:|---:|---|
|
|
||||||
| Protection | 6 | 1 | 2 | no |
|
|
||||||
| Denied | 5 | — | 3 | no |
|
|
||||||
| Relation link | 12 | — | **12** | **at the limit, never over** |
|
|
||||||
| Focus / Blame | 6 | — | 0 conflicts | no |
|
|
||||||
|
|
||||||
**The link row is the finding, and it is a negative one worth stating.**
|
|
||||||
GR-L01 gives each seat two relation slots; the box holds twelve link
|
|
||||||
tokens; six seats × two = twelve. **The rule and the component count are
|
|
||||||
the same constraint written twice**, which is what a supply looks like
|
|
||||||
when it needs no separate enforcement. Nothing was designed to make that
|
|
||||||
true — it fell out.
|
|
||||||
|
|
||||||
**Protection is the one that could have gone wrong and did not.**
|
|
||||||
`saturating_add(1)` at `lib.rs:986` has no upper bound, so the type
|
|
||||||
permits a seventh token. Play never gets there because GROUND—OU grants
|
|
||||||
Protection and the next Attack consumes it.
|
|
||||||
|
|
||||||
## 2. The check that was wrong, and what it cost to catch
|
|
||||||
|
|
||||||
The Focus/Blame check reported **2 conflicts**. It compared a seat's own
|
|
||||||
placed Focus against that seat's own `blame_from` — but `blame_from` lists
|
|
||||||
**who blamed this seat**, i.e. *other players'* discs. Different tokens.
|
|
||||||
Corrected: **0**.
|
|
||||||
|
|
||||||
**Fifth instance of the family CB-EV-0019 §1 named** — a number computed
|
|
||||||
correctly about the wrong subject:
|
|
||||||
|
|
||||||
| # | pass | the wrong thing |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | CB-WP-0021 | `csv` cost against a budget that never sees the code |
|
|
||||||
| 2 | to ground-game | *"12 in the file"* — a sum with no deal table |
|
|
||||||
| 3 | CB-WP-0018 | SOLVE's inertness attributed to the wrong condition |
|
|
||||||
| 4 | CB-WP-0025 | a timer bracketing whole games; a win rate attributed to the game |
|
|
||||||
| 5 | **here** | a token's own disc compared against other players' discs |
|
|
||||||
|
|
||||||
**And this is the first one caught before it left the repo** — by me, in
|
|
||||||
the same session, rather than by an adversarial reviewer or by the
|
|
||||||
maintainer. Four of the previous five reached a document or another
|
|
||||||
repository.
|
|
||||||
|
|
||||||
**That is one data point, not a trend.** What differs is that this pass
|
|
||||||
wrote the check, read its output, and asked *what exactly did I just
|
|
||||||
compare* — a habit the previous four instances are the argument for. The
|
|
||||||
family still has **no control**; `facts-check` catches copies that
|
|
||||||
disagree and nothing catches a correct computation over the wrong subject.
|
|
||||||
|
|
||||||
## 3. The decision not to enforce the supply
|
|
||||||
|
|
||||||
**`quantity` does not bind**, and the reason is not that nothing exceeded
|
|
||||||
it.
|
|
||||||
|
|
||||||
No numbered rule in `GroundRules.md` mentions a token supply. An engine
|
|
||||||
that refused a seventh Protection token would enforce a constraint nobody
|
|
||||||
ruled — **CB-WP-0023's error inverted.** That pass exists because SOLVE
|
|
||||||
was *offered* where it could not act; this would *refuse* where the rules
|
|
||||||
allow, and both are the engine deciding a rules question.
|
|
||||||
|
|
||||||
So the check ships as a standing control. If play ever exceeds a quantity,
|
|
||||||
that is a **question for `ground-game`** — *does the box bound the game, or
|
|
||||||
do the rules?* — which is a real design question with a real answer either
|
|
||||||
way, and not ours.
|
|
||||||
|
|
||||||
Registered as **F22, withdrawn**: a stated negative. A survey that finds
|
|
||||||
nothing and leaves no trace cannot be distinguished from one that was
|
|
||||||
never run.
|
|
||||||
|
|
||||||
## 4. What the tracks are for
|
|
||||||
|
|
||||||
`stress 4` is a fact you read. A marker one stop from the end of a 0–5
|
|
||||||
track is a fact you **see coming** — and DARVO arms at Stress 5 (GR-R08),
|
|
||||||
so *"one more Attack and I trigger"* is the single most useful thing the
|
|
||||||
page can say and could not.
|
|
||||||
|
|
||||||
Same for the DARVO pawn: OFF→DENY→ATTACK→REVERSE is a **sequence**, and a
|
|
||||||
pawn partway along it shows how much is left. `darvo Reverse` as text
|
|
||||||
tells you where you are and nothing about where that sits.
|
|
||||||
|
|
||||||
**Unverified.** Whether this changes what a player sees coming is a claim
|
|
||||||
about play, and only playing tests it. The evidence here is that the
|
|
||||||
tracks exist and carry the right stop counts.
|
|
||||||
|
|
||||||
## 5. Two fixture defects, and a limit corrected
|
|
||||||
|
|
||||||
**`seat_centres` matched every `<circle>`.** Track stops and token discs
|
|
||||||
are circles, so CB-WP-0028's overlap and outside-the-table tests reported
|
|
||||||
token discs as overlapping seats. Seats now carry `class="seat"` and the
|
|
||||||
helper keys on that. **Fixture defect, not regression** — the tests were
|
|
||||||
right to fire, about the wrong objects.
|
|
||||||
|
|
||||||
**The table height limit went 460 → 500.** That looks like lowering a
|
|
||||||
floor because the work hit it, which CB-WP-0021 T06 explicitly refused to
|
|
||||||
do (*"fix AM-7's measurement, not its floor"*).
|
|
||||||
|
|
||||||
**It is the opposite, and the distinction is the point.** 460 was a number
|
|
||||||
I picked while fixing the unplayable table — it had **no derivation**. 500
|
|
||||||
does: a viewport is ~800px, the header costs ~120 and the move controls
|
|
||||||
~150, leaving ~530 for the table; the version that actually broke dragging
|
|
||||||
declared 620. **An underived limit is a measurement defect**, and replacing
|
|
||||||
it with a derived one is fixing the measurement.
|
|
||||||
|
|
||||||
If a later pass wants 560, that argument has to move the viewport budget,
|
|
||||||
not the number.
|
|
||||||
|
|
||||||
## 6. Chaos window 2 — closed, and its verdict is due
|
|
||||||
|
|
||||||
**This is declaration 12 of 12.** The window opened 2026-08-03 at d8.
|
|
||||||
|
|
||||||
**Twelve declarations, zero 8s, zero overrides.** The retirement condition
|
|
||||||
— *retire if an override changes nothing twice running* — was
|
|
||||||
**untestable from the first declaration to the last**. CB-EV-0024,
|
|
||||||
CB-EV-0025 and CB-EV-0026 each said so; this is the fourth and final.
|
|
||||||
|
|
||||||
| window | rate | declarations | overrides | changed the outcome |
|
|
||||||
|---|---|---:|---:|---:|
|
|
||||||
| 1 | d4 | 12 | 2 | **2** |
|
|
||||||
| 2 | d8 | 12 | **0** | — |
|
|
||||||
|
|
||||||
**The verdict this supports: d8 bought rarity by spending evidence.** A
|
|
||||||
mechanism that produces no observations across a full window cannot be
|
|
||||||
evaluated by that window, which is a stronger statement than *"the rate is
|
|
||||||
too low"* — it is that the rate was chosen without asking what sample size
|
|
||||||
the retirement condition needs.
|
|
||||||
|
|
||||||
**Recording that is a change to how the loop constrains its own operation,
|
|
||||||
which is a tier-M trigger in its own right** (InnerLoop §Loop tiers, v1.6).
|
|
||||||
It is therefore **outstanding, not done here**, and is named in the
|
|
||||||
workplan so it is not lost between passes — which is exactly how
|
|
||||||
`ground-game`'s ten rulings went uncollected for two days.
|
|
||||||
|
|
||||||
## 7. Cost
|
|
||||||
|
|
||||||
CB-WP-0028's cost, by re-running the instrument: `make cost`. Not inlined
|
|
||||||
(§Single source of fact). CB-EV-0019 §4's unbounded chain is still
|
|
||||||
unbounded, now across four more passes.
|
|
||||||
|
|
||||||
## Open after this pass
|
|
||||||
|
|
||||||
- **The window-2 verdict** (§6) — the next thing this loop owes itself.
|
|
||||||
- **The wrong-denominator family still has no control** (§2), at five
|
|
||||||
instances.
|
|
||||||
- **Whether the tracks change what a player sees coming** (§4) is untested
|
|
||||||
and only play tests it.
|
|
||||||
- **`Relations`, `DARVO`, `Player_Mats` and `Glossary` remain unvendored.**
|
|
||||||
The DARVO track now has a picture but not the edition's own words for
|
|
||||||
each stage.
|
|
||||||
- **Three of four scenarios still never dealt**, carried from ADR-0015 D5.
|
|
||||||
|
|
@ -71,7 +71,6 @@ const CSV: &str = include_str!("../../../editions/ground-darvo-r0/Problems.csv")
|
||||||
const ACTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Actions.csv");
|
const ACTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Actions.csv");
|
||||||
const SOLUTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Solutions.csv");
|
const SOLUTIONS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Solutions.csv");
|
||||||
const MODES_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Modes.csv");
|
const MODES_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Modes.csv");
|
||||||
const TOKENS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Tokens.csv");
|
|
||||||
|
|
||||||
/// A vendored CSV, parsed into rows addressable by column name.
|
/// A vendored CSV, parsed into rows addressable by column name.
|
||||||
///
|
///
|
||||||
|
|
@ -149,60 +148,6 @@ fn card_texts(csv: &str, what: &str, id: &str, tag: &str) -> Result<Vec<CardText
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One component, as the edition prints it (ADR-0016 D1).
|
|
||||||
///
|
|
||||||
/// **A token is a view, not a type** (D2): nothing in the aggregate
|
|
||||||
/// changes shape. This supplies the renderer with the game's own labels
|
|
||||||
/// and the counts a supply check needs.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct TokenSpec {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
/// How many the box holds. **Not a rule** — no numbered rule mentions
|
|
||||||
/// a supply, and the engine does not enforce one (D3).
|
|
||||||
pub quantity: u32,
|
|
||||||
/// `Double` means one physical disc that flips — Freedom (READY /
|
|
||||||
/// spent) and Focus/Blame. Two states of one component, not two.
|
|
||||||
pub double_sided: bool,
|
|
||||||
/// What is printed on it: READY, DENIED, PROTECTION, FOCUS…
|
|
||||||
pub front_text: String,
|
|
||||||
/// The rule, in the edition's words.
|
|
||||||
pub use_text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every token the edition ships.
|
|
||||||
pub fn tokens() -> Result<Vec<TokenSpec>, String> {
|
|
||||||
let t = Table::parse(TOKENS_CSV, "Tokens.csv")?;
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for row in &t.rows {
|
|
||||||
out.push(TokenSpec {
|
|
||||||
id: t.get(row, "token_id")?.to_string(),
|
|
||||||
name: t.get(row, "name")?.to_string(),
|
|
||||||
quantity: t
|
|
||||||
.get(row, "quantity")?
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| "quantity is not a number".to_string())?,
|
|
||||||
double_sided: t.get(row, "sides")? == "Double",
|
|
||||||
front_text: t.get(row, "front_text")?.to_string(),
|
|
||||||
use_text: t.get(row, "use")?.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if out.is_empty() {
|
|
||||||
return Err("Tokens.csv has no rows".into());
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How many of `id` the box holds. `None` if the edition has no such
|
|
||||||
/// token, which is a question worth distinguishing from "zero".
|
|
||||||
pub fn supply(id: &str) -> Option<u32> {
|
|
||||||
tokens()
|
|
||||||
.ok()?
|
|
||||||
.into_iter()
|
|
||||||
.find(|t| t.id == id)
|
|
||||||
.map(|t| t.quantity)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The five Action cards, in the edition's own words.
|
/// The five Action cards, in the edition's own words.
|
||||||
pub fn actions() -> Result<Vec<CardText>, String> {
|
pub fn actions() -> Result<Vec<CardText>, String> {
|
||||||
card_texts(ACTIONS_CSV, "Actions.csv", "action_id", "tagline")
|
card_texts(ACTIONS_CSV, "Actions.csv", "action_id", "tagline")
|
||||||
|
|
@ -472,107 +417,3 @@ mod card_text_tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "scenarios"))]
|
|
||||||
mod supply_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
/// **CB-WP-0029 T03: does play ever exceed what the box holds?**
|
|
||||||
///
|
|
||||||
/// Measured over 750 games (2–6 seats, greedy and random): it does
|
|
||||||
/// not. This runs a smaller sweep as a standing control, so a future
|
|
||||||
/// change that starts minting components fails here instead of being
|
|
||||||
/// noticed by a player.
|
|
||||||
///
|
|
||||||
/// **A violation is a FINDING, not a bug to fix by adding a bound**
|
|
||||||
/// (ADR-0016 D3). No numbered rule mentions a supply; the engine
|
|
||||||
/// enforcing one would be inventing a rule, which is CB-WP-0023's
|
|
||||||
/// error inverted. If this goes red, the question goes to
|
|
||||||
/// `ground-game`: does the box bound the game, or do the rules?
|
|
||||||
#[test]
|
|
||||||
fn play_never_exceeds_the_components_the_box_holds() {
|
|
||||||
use crate::bot::{play, GreedyPolicy, Policy, RandomPolicy};
|
|
||||||
use cb_game_runtime::{ScenarioGame, Setup};
|
|
||||||
|
|
||||||
let protection = supply("TOK_PROTECTION").expect("the edition ships Protection");
|
|
||||||
let denied = supply("TOK_DENIED").expect("the edition ships Denied");
|
|
||||||
let link = supply("TOK_LINK").expect("the edition ships link tokens");
|
|
||||||
|
|
||||||
for players in [2u8, 4, 6] {
|
|
||||||
for seed in 0..25u64 {
|
|
||||||
let Ok(state) = crate::GroundState::setup(
|
|
||||||
&Setup {
|
|
||||||
players,
|
|
||||||
preset: format!("standard-{players}p"),
|
|
||||||
patch: Default::default(),
|
|
||||||
},
|
|
||||||
seed,
|
|
||||||
) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let mut ps: Vec<Box<dyn Policy>> = (0..players)
|
|
||||||
.map(|i| {
|
|
||||||
if seed % 2 == 0 {
|
|
||||||
Box::new(GreedyPolicy) as Box<dyn Policy>
|
|
||||||
} else {
|
|
||||||
Box::new(RandomPolicy::new(seed ^ u64::from(i))) as Box<dyn Policy>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let Ok(g) = play(state, &mut ps) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let s = &g.state;
|
|
||||||
|
|
||||||
let on_table: u32 = s.players.values().map(|p| u32::from(p.protection)).sum();
|
|
||||||
assert!(
|
|
||||||
on_table <= protection,
|
|
||||||
"{players}p seed {seed}: {on_table} Protection tokens in play, \
|
|
||||||
the box holds {protection}"
|
|
||||||
);
|
|
||||||
let d = s.problems.values().filter(|q| q.denied).count() as u32;
|
|
||||||
assert!(
|
|
||||||
d <= denied,
|
|
||||||
"{d} Denied tokens in play, the box holds {denied}"
|
|
||||||
);
|
|
||||||
// Two link tokens per relation, one at each endpoint.
|
|
||||||
let l = (s.relations.len() * 2) as u32;
|
|
||||||
assert!(l <= link, "{l} link tokens in play, the box holds {link}");
|
|
||||||
|
|
||||||
// One double-sided disc per player: it is Focus-side-up
|
|
||||||
// somewhere, or Blame-side-up somewhere, never both.
|
|
||||||
//
|
|
||||||
// The first version of this check compared a seat's own
|
|
||||||
// Focus against its OWN blame_from -- but blame_from lists
|
|
||||||
// OTHER players' discs, so those are different tokens. It
|
|
||||||
// reported conflicts that did not exist.
|
|
||||||
for owner in s.players.keys() {
|
|
||||||
let as_focus = s.focus.contains_key(owner);
|
|
||||||
let as_blame = s.players.values().any(|q| q.blame_from.contains(owner));
|
|
||||||
assert!(
|
|
||||||
!(as_focus && as_blame),
|
|
||||||
"{owner:?}'s single Focus/Blame disc is placed twice"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The supply numbers are the edition's, not ours.
|
|
||||||
#[test]
|
|
||||||
fn the_supply_comes_from_the_edition() {
|
|
||||||
let t = tokens().expect("Tokens.csv parses");
|
|
||||||
assert_eq!(t.len(), 9, "the edition ships nine token types");
|
|
||||||
assert_eq!(supply("TOK_LINK"), Some(12), "two per player at six seats");
|
|
||||||
assert!(
|
|
||||||
TOKENS_CSV.contains(&tokens().expect("t")[0].use_text),
|
|
||||||
"the rule text is not a substring of the vendored file — it was invented"
|
|
||||||
);
|
|
||||||
// `sides` must be able to say both, or the flag means nothing.
|
|
||||||
assert!(
|
|
||||||
t.iter().any(|x| x.double_sided),
|
|
||||||
"Freedom and Focus/Blame flip"
|
|
||||||
);
|
|
||||||
assert!(t.iter().any(|x| !x.double_sided), "most tokens do not");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
|
||||||
| F19 | degenerate | applied | crates/cb-render-html/src/lib.rs::overhead_table | counterexample | 2026-08-06 | clay-borg |
|
| F19 | degenerate | applied | crates/cb-render-html/src/lib.rs::overhead_table | counterexample | 2026-08-06 | clay-borg |
|
||||||
| F20 | inert | applied | crates/cb-render-html/src/lib.rs::ending_page | counterexample | 2026-08-06 | clay-borg |
|
| F20 | inert | applied | crates/cb-render-html/src/lib.rs::ending_page | counterexample | 2026-08-06 | clay-borg |
|
||||||
| F21 | degenerate | note | — | — | 2026-08-06 | clay-borg |
|
| F21 | degenerate | note | — | — | 2026-08-06 | clay-borg |
|
||||||
| F22 | underdetermined | withdrawn | games_ground::edition::supply_tests::play_never_exceeds_the_components_the_box_holds | counterexample | 2026-08-07 | clay-borg |
|
|
||||||
|
|
||||||
<!-- design-register:end -->
|
<!-- design-register:end -->
|
||||||
|
|
||||||
|
|
@ -79,21 +78,6 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
|
||||||
since GROUND-WP-0005 is blocked on exactly this number. The withdrawal
|
since GROUND-WP-0005 is blocked on exactly this number. The withdrawal
|
||||||
was reported (ADR-0012 D5). Its reproduction is `difficulty.rs`, whose
|
was reported (ADR-0012 D5). Its reproduction is `difficulty.rs`, whose
|
||||||
policy panel is plural *because of this finding*.
|
policy panel is plural *because of this finding*.
|
||||||
- **F22 — does the component supply bind the game? Asked, measured, and
|
|
||||||
withdrawn.** `Tokens.csv` gives every component a `quantity` — 6
|
|
||||||
Protection, 5 Denied, 12 relation links — and **no numbered rule
|
|
||||||
mentions a supply**, so the engine could in principle mint a seventh
|
|
||||||
Protection token (`saturating_add`, no bound, `lib.rs:986`). Measured
|
|
||||||
over **750 games**, 2–6 seats, greedy and random: Protection reaches 1
|
|
||||||
per seat and 2 on the table; Denied 3 of 5; links **exactly 12 of 12**,
|
|
||||||
never over, because GR-L01's two-slot rule *is* the twelve-token supply
|
|
||||||
written twice. Focus/Blame: 0 conflicts. **Withdrawn — nothing to
|
|
||||||
report.** Registered as a stated negative because a survey that finds
|
|
||||||
nothing and leaves no trace cannot be told from one that was never run.
|
|
||||||
The check ships as a standing control (ADR-0016 D3): if play ever does
|
|
||||||
exceed a quantity, that is a **finding for ground-game** — *does the box
|
|
||||||
bound the game, or do the rules?* — and **not** a bound for the engine to
|
|
||||||
invent.
|
|
||||||
- **F21 — dragging did not work until after the first note was saved.**
|
- **F21 — dragging did not work until after the first note was saved.**
|
||||||
Reported 2026-08-06: *"I could not drag and drop at the beginning but
|
Reported 2026-08-06: *"I could not drag and drop at the beginning but
|
||||||
after I added the first comment it worked."* **`note`, and I could not
|
after I added the first comment it worked."* **`note`, and I could not
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ ruled: ''
|
||||||
ruled_by: ''
|
ruled_by: ''
|
||||||
ruled_note: ''
|
ruled_note: ''
|
||||||
encodes_u_item: ''
|
encodes_u_item: ''
|
||||||
seed: 2
|
seed: 1
|
||||||
setup:
|
setup:
|
||||||
players: 3
|
players: 3
|
||||||
preset: standard-3p
|
preset: standard-3p
|
||||||
|
|
@ -17,7 +17,8 @@ commands:
|
||||||
- actor: P1
|
- actor: P1
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
action: GROUND
|
action: INVESTIGATE
|
||||||
|
problem: 2
|
||||||
- actor: P2
|
- actor: P2
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
|
|
@ -31,36 +32,6 @@ commands:
|
||||||
- actor: SYSTEM
|
- actor: SYSTEM
|
||||||
cmd: reveal
|
cmd: reveal
|
||||||
args: {}
|
args: {}
|
||||||
- actor: P1
|
|
||||||
cmd: choose_ground_mode
|
|
||||||
args:
|
|
||||||
choice: protect_problem
|
|
||||||
mode: OU
|
|
||||||
problem: 1
|
|
||||||
- actor: SYSTEM
|
|
||||||
cmd: resolve
|
|
||||||
args: {}
|
|
||||||
- actor: SYSTEM
|
|
||||||
cmd: end_round
|
|
||||||
args: {}
|
|
||||||
- actor: P1
|
|
||||||
cmd: select_action
|
|
||||||
args:
|
|
||||||
action: INVESTIGATE
|
|
||||||
problem: 2
|
|
||||||
- actor: P2
|
|
||||||
cmd: select_action
|
|
||||||
args:
|
|
||||||
action: INVESTIGATE
|
|
||||||
problem: 2
|
|
||||||
- actor: P3
|
|
||||||
cmd: select_action
|
|
||||||
args:
|
|
||||||
action: INVESTIGATE
|
|
||||||
problem: 2
|
|
||||||
- actor: SYSTEM
|
|
||||||
cmd: reveal
|
|
||||||
args: {}
|
|
||||||
- actor: SYSTEM
|
- actor: SYSTEM
|
||||||
cmd: resolve
|
cmd: resolve
|
||||||
args: {}
|
args: {}
|
||||||
|
|
@ -75,13 +46,13 @@ commands:
|
||||||
- actor: P2
|
- actor: P2
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
action: SOLVE
|
action: INVESTIGATE
|
||||||
problem: 2
|
problem: 3
|
||||||
- actor: P3
|
- actor: P3
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
action: SOLVE
|
action: INVESTIGATE
|
||||||
problem: 2
|
problem: 3
|
||||||
- actor: SYSTEM
|
- actor: SYSTEM
|
||||||
cmd: reveal
|
cmd: reveal
|
||||||
args: {}
|
args: {}
|
||||||
|
|
@ -95,7 +66,7 @@ commands:
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
action: SOLVE
|
action: SOLVE
|
||||||
problem: 3
|
problem: 2
|
||||||
- actor: P2
|
- actor: P2
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
|
|
@ -104,7 +75,31 @@ commands:
|
||||||
- actor: P3
|
- actor: P3
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
action: INVESTIGATE
|
action: SOLVE
|
||||||
|
problem: 2
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: reveal
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: resolve
|
||||||
|
args: {}
|
||||||
|
- actor: SYSTEM
|
||||||
|
cmd: end_round
|
||||||
|
args: {}
|
||||||
|
- actor: P1
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 3
|
||||||
|
- actor: P2
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
|
problem: 3
|
||||||
|
- actor: P3
|
||||||
|
cmd: select_action
|
||||||
|
args:
|
||||||
|
action: SOLVE
|
||||||
problem: 4
|
problem: 4
|
||||||
- actor: SYSTEM
|
- actor: SYSTEM
|
||||||
cmd: reveal
|
cmd: reveal
|
||||||
|
|
@ -119,21 +114,25 @@ commands:
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
action: SUPPORT
|
action: SUPPORT
|
||||||
target: P3
|
target: P2
|
||||||
- actor: P2
|
- actor: P2
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
action: SOLVE
|
action: SUPPORT
|
||||||
problem: 4
|
target: P1
|
||||||
- actor: P3
|
- actor: P3
|
||||||
cmd: select_action
|
cmd: select_action
|
||||||
args:
|
args:
|
||||||
action: SOLVE
|
action: SUPPORT
|
||||||
problem: 4
|
target: P1
|
||||||
- actor: SYSTEM
|
- actor: SYSTEM
|
||||||
cmd: reveal
|
cmd: reveal
|
||||||
args: {}
|
args: {}
|
||||||
- actor: P3
|
- actor: P1
|
||||||
|
cmd: respond_to_support
|
||||||
|
args:
|
||||||
|
response: accept_bond
|
||||||
|
- actor: P2
|
||||||
cmd: respond_to_support
|
cmd: respond_to_support
|
||||||
args:
|
args:
|
||||||
response: accept_bond
|
response: accept_bond
|
||||||
|
|
@ -147,4 +146,4 @@ expect:
|
||||||
events: []
|
events: []
|
||||||
state: {}
|
state: {}
|
||||||
rejects: []
|
rejects: []
|
||||||
state_hash: f227bb9e0c62f91735508514b5f0d51f6d59b48b68619b9ae641d25f191aa01b
|
state_hash: 15e28280a256249f4ebddbeb638053d0683d996b7ff1193128a7d788f32f63ec
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
id: CB-WP-0029
|
id: CB-WP-0029
|
||||||
kind: product
|
kind: product
|
||||||
title: "The tokens on the table: components you can count, and a supply the engine may not respect"
|
title: "The tokens on the table: components you can count, and a supply the engine may not respect"
|
||||||
status: done
|
status: ready
|
||||||
state_hub_workstream_id: "fb3d3c80-3798-4d0f-91ce-b8ea45943328"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Purpose
|
# Purpose
|
||||||
|
|
@ -71,9 +70,8 @@ went uncollected.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: CB-WP-0029-T01
|
id: CB-WP-0029-T01
|
||||||
status: done
|
status: todo
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "a7aad543-1c38-43e3-b7d2-d2127b4dc849"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`decisions/ADR-0016-*.md` (tier M merges survey and decision).
|
`decisions/ADR-0016-*.md` (tier M merges survey and decision).
|
||||||
|
|
@ -93,35 +91,12 @@ state_hub_task_id: "a7aad543-1c38-43e3-b7d2-d2127b4dc849"
|
||||||
a rule the engine must enforce, an artifact of physical production, or
|
a rule the engine must enforce, an artifact of physical production, or
|
||||||
undetermined? **Do not answer it here from taste** — T03 measures first.
|
undetermined? **Do not answer it here from taste** — T03 measures first.
|
||||||
|
|
||||||
**Done 2026-08-07.**
|
|
||||||
[ADR-0016](../decisions/ADR-0016-the-tokens-on-the-table.md), four
|
|
||||||
decisions, **written after T03 measured**.
|
|
||||||
|
|
||||||
**D2 — a token is a VIEW, not a type.** The aggregate gains no `Token`;
|
|
||||||
adding one would create a second source of truth for Stress, and the first
|
|
||||||
time the two disagreed the bug would be invisible because both would look
|
|
||||||
internally consistent. `Tokens.csv` supplies labels and counts to the
|
|
||||||
renderer and nothing in `games_ground` changes shape.
|
|
||||||
|
|
||||||
**D3 — `quantity` does not bind, and the reason is not the measurement.**
|
|
||||||
A component limit the rules do not state is not a rule. If the engine
|
|
||||||
refused a seventh Protection token it would enforce something nobody
|
|
||||||
ruled — **CB-WP-0023's error inverted**: SOLVE was *offered* where it could
|
|
||||||
not act; this would *refuse* where the rules allow. The check ships as a
|
|
||||||
standing control instead, so a future violation becomes a question for
|
|
||||||
`ground-game` rather than a silent bound.
|
|
||||||
|
|
||||||
**D4 — the tracks are the decision that matters.** `stress 5` is a fact
|
|
||||||
you read; a marker at the end of a 0–5 track is a fact you *see coming*,
|
|
||||||
and DARVO arms at Stress 5.
|
|
||||||
|
|
||||||
## Task: tokens as objects
|
## Task: tokens as objects
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: CB-WP-0029-T02
|
id: CB-WP-0029-T02
|
||||||
status: done
|
status: todo
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "3776359b-942a-4c9f-b9d5-8e54afffa391"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Draw them, on the table built in CB-WP-0028.
|
Draw them, on the table built in CB-WP-0028.
|
||||||
|
|
@ -146,29 +121,12 @@ Draw them, on the table built in CB-WP-0028.
|
||||||
- a seat with **no** tokens renders as a seat with no tokens, not as a
|
- a seat with **no** tokens renders as a seat with no tokens, not as a
|
||||||
seat missing its area. The empty case is the one that silently vanishes.
|
seat missing its area. The empty case is the one that silently vanishes.
|
||||||
|
|
||||||
**Done 2026-08-07.** Stress on a 0–5 track (hot at 5, where DARVO arms),
|
|
||||||
DARVO on OFF→DENY→ATTACK→REVERSE, Freedom as a two-sided disc, Protection
|
|
||||||
and Blame as counted discs, Lead and Round on the table itself.
|
|
||||||
|
|
||||||
**Two tests broke and both were fixture defects, not regressions.**
|
|
||||||
`seat_centres` matched *every* `<circle>`, and track stops are circles —
|
|
||||||
so it reported token discs as overlapping seats. Seats now carry
|
|
||||||
`class="seat"` and the helper keys on that.
|
|
||||||
|
|
||||||
**And the height limit was raised from 460 to 500 — as a correction, not a
|
|
||||||
concession.** The 460 had no derivation. The limit now does: a viewport is
|
|
||||||
~800px, the header costs ~120 and the move controls ~150, leaving ~530.
|
|
||||||
The version that broke dragging declared 620. **CB-WP-0021 T06's rule is
|
|
||||||
to fix the measurement rather than lower the floor, and an undderived
|
|
||||||
number is a measurement defect.**
|
|
||||||
|
|
||||||
## Task: does the engine respect the supply?
|
## Task: does the engine respect the supply?
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: CB-WP-0029-T03
|
id: CB-WP-0029-T03
|
||||||
status: done
|
status: todo
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "5fce7cec-91e1-4dba-90b7-1f7f1a463dfc"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Measure before deciding.** For each token with a `quantity`, ask whether
|
**Measure before deciding.** For each token with a `quantity`, ask whether
|
||||||
|
|
@ -198,39 +156,12 @@ the same error in the other direction.
|
||||||
stated "none found", because a survey that reports nothing and leaves no
|
stated "none found", because a survey that reports nothing and leaves no
|
||||||
trace is indistinguishable from one that was not run.
|
trace is indistinguishable from one that was not run.
|
||||||
|
|
||||||
**Done 2026-08-07. Nothing found — and the first version of the check was
|
|
||||||
wrong.**
|
|
||||||
|
|
||||||
750 games, 2–6 seats, greedy and random:
|
|
||||||
|
|
||||||
| token | supply | reached | verdict |
|
|
||||||
|---|---:|---|---|
|
|
||||||
| Protection | 6 | 1/seat, 2/table | not exceeded |
|
|
||||||
| Denied | 5 | 3 | not exceeded |
|
|
||||||
| Relation link | 12 | **12** | exactly at the limit, never over |
|
|
||||||
| Focus/Blame | 6 | 0 conflicts | consistent |
|
|
||||||
|
|
||||||
**The link row is the interesting one**: GR-L01's two-slot rule *is* the
|
|
||||||
twelve-token supply written twice, which is the shape of a supply needing
|
|
||||||
no separate enforcement.
|
|
||||||
|
|
||||||
**The Focus/Blame check reported 2 conflicts and was wrong.** It compared
|
|
||||||
a seat's own placed Focus against its *own* `blame_from` — but that list
|
|
||||||
holds **other players'** discs, so they are different tokens. Corrected: 0.
|
|
||||||
**Fifth instance of this project's recurring defect** — a number computed
|
|
||||||
correctly about the wrong subject — and the first caught before it left
|
|
||||||
the repo.
|
|
||||||
|
|
||||||
Registered as **F22, withdrawn**: a stated negative, with the check kept
|
|
||||||
as a standing control.
|
|
||||||
|
|
||||||
## Task: evidence
|
## Task: evidence
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: CB-WP-0029-T04
|
id: CB-WP-0029-T04
|
||||||
status: done
|
status: todo
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "29858777-6f8f-4d8e-90d0-cb193b7f4d29"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`evidence/CB-EV-0027-*.md`.
|
`evidence/CB-EV-0027-*.md`.
|
||||||
|
|
@ -243,22 +174,3 @@ state_hub_task_id: "29858777-6f8f-4d8e-90d0-cb193b7f4d29"
|
||||||
- **The window-2 verdict is due** (§The window closes here) — name it as
|
- **The window-2 verdict is due** (§The window closes here) — name it as
|
||||||
outstanding if the closing pass has not run.
|
outstanding if the closing pass has not run.
|
||||||
- **Quote CB-WP-0028's cost by re-running the instrument.**
|
- **Quote CB-WP-0028's cost by re-running the instrument.**
|
||||||
|
|
||||||
**Done 2026-08-07.**
|
|
||||||
[CB-EV-0027](../evidence/CB-EV-0027-the-tokens-on-the-table.md).
|
|
||||||
|
|
||||||
- **The supply is never exceeded**, and the link row explains why a supply
|
|
||||||
can need no enforcement: GR-L01's two slots per seat *are* the twelve
|
|
||||||
tokens, the same constraint written twice.
|
|
||||||
- **The Focus/Blame check was wrong and I caught it** — fifth instance of
|
|
||||||
the wrong-subject family, and **the first caught before leaving the
|
|
||||||
repo**. One data point, not a trend; the family still has no control.
|
|
||||||
- **The height limit went 460 → 500 as a correction.** 460 had no
|
|
||||||
derivation; 500 does. An underived limit is a measurement defect, and
|
|
||||||
replacing it is CB-WP-0021 T06's rule applied, not evaded.
|
|
||||||
- **Whether the tracks change what a player sees coming is untested** and
|
|
||||||
only play tests it.
|
|
||||||
- **Chaos window 2 is closed: twelve declarations, zero overrides.** Its
|
|
||||||
retirement condition was untestable from first to last. **The verdict is
|
|
||||||
the next thing this loop owes itself**, and it is a tier-M pass because
|
|
||||||
recording it changes how the loop constrains its own operation.
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue