CB-WP-0029 T01-T03: components you can count, and a supply that does not bind
ADR-0016, Tokens.csv vendored, tracks and discs on the table, and a supply audit that found nothing and says so. T03 MEASURED BEFORE T01 DECIDED. 750 games, 2-6 seats, greedy and random: Protection reaches 1 per seat and 2 on the table against a supply of 6; Denied 3 of 5; relation links EXACTLY 12 OF 12 and never more; Focus/Blame 0 conflicts. 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. AND THE FIRST VERSION OF THE FOCUS/BLAME CHECK 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. It reported 2 conflicts; corrected, it reports 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 rather than by a reviewer. 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 they disagreed the bug would be invisible because both would look internally consistent. D3: quantity does NOT bind, and the reason is not the measurement. A component limit the rules do not state is not a rule. Refusing a seventh Protection token 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, so a future violation becomes a question for ground-game (does the box bound the game, or do the rules?) rather than a bound the engine invented. Registered as F22, withdrawn: a stated negative, because a survey that finds nothing and leaves no trace cannot be told from one never run. D4: Stress on a 0-5 track that turns red at 5 where DARVO arms, DARVO on OFF/DENY/ATTACK/REVERSE, Freedom as the two-sided disc the edition says it is, Protection and Blame counted, Lead and Round on the table. Two tests broke on token discs and both were FIXTURE defects: seat_centres matched every <circle> and track stops are circles. Seats now carry class="seat". The table height limit went 460 -> 500 as a CORRECTION, not a concession. 460 had no derivation; 500 does -- ~800px viewport less ~120 header and ~150 controls leaves ~530, and the version that broke dragging was 620. CB-WP-0021 T06's rule is to fix the measurement rather than lower the floor, and an underived number is a measurement defect. make all: exit 0. 66 render tests, 26 cb-play. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7fa0c65e6d
commit
631fb41fc2
10 changed files with 657 additions and 57 deletions
|
|
@ -474,6 +474,111 @@ 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).
|
||||
///
|
||||
/// **One diagram, not three.** The seats were already placed on a circle
|
||||
|
|
@ -496,7 +601,8 @@ fn table_svg(view: &GroundView) -> String {
|
|||
let (rx, ry) = (200.0f64, 118.0f64);
|
||||
// Seats sit OUTSIDE the table, as people do. The first version put
|
||||
// them at 0.83 of the ellipse and they sat on it.
|
||||
let (sx, sy) = (rx + 108.0, ry + 88.0);
|
||||
// Seats sit outside the table with room below each for its tokens.
|
||||
let (sx, sy) = (rx + 112.0, ry + 82.0);
|
||||
|
||||
let pos: Vec<(PlayerId, f64, f64)> = view
|
||||
.players
|
||||
|
|
@ -515,7 +621,7 @@ fn table_svg(view: &GroundView) -> String {
|
|||
};
|
||||
|
||||
let mut s = String::from(
|
||||
"<svg viewBox=\"0 0 760 440\" width=\"100%\" style=\"max-width:760px\" \
|
||||
"<svg viewBox=\"0 0 760 470\" width=\"100%\" style=\"max-width:760px\" \
|
||||
role=\"img\" aria-label=\"the table, seen from above\">",
|
||||
);
|
||||
|
||||
|
|
@ -593,7 +699,7 @@ fn table_svg(view: &GroundView) -> String {
|
|||
let _ = write!(
|
||||
s,
|
||||
"<g data-drop=\"seat-{raw}\">\
|
||||
<circle cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"32\" fill=\"#1b1e26\" \
|
||||
<circle class=\"seat\" cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"32\" fill=\"#1b1e26\" \
|
||||
stroke=\"{stroke}\" stroke-width=\"{sw}\"/>\
|
||||
<text x=\"{x:.0}\" y=\"{t1:.0}\" fill=\"#dde\" font-size=\"12\" \
|
||||
text-anchor=\"middle\">{name}{you}</text>\
|
||||
|
|
@ -609,7 +715,27 @@ fn table_svg(view: &GroundView) -> String {
|
|||
stress = pv.stress,
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -722,10 +722,14 @@ mod overhead_table {
|
|||
use games_ground::GroundState;
|
||||
|
||||
/// 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)> {
|
||||
html.match_indices("<circle cx=\"")
|
||||
html.match_indices("<circle class=\"seat\" cx=\"")
|
||||
.filter_map(|(i, _)| {
|
||||
let rest = &html[i + 12..];
|
||||
let rest = &html[i + 25..];
|
||||
let (x, rest) = rest.split_once("\" cy=\"")?;
|
||||
let (y, _) = rest.split_once('"')?;
|
||||
Some((x.parse().ok()?, y.parse().ok()?))
|
||||
|
|
@ -855,9 +859,12 @@ mod overhead_table {
|
|||
/// tall, so the action cards sat a screen below the Problems — and you
|
||||
/// cannot drag between two things never on screen together.
|
||||
///
|
||||
/// Asserted on the declared height, which is the only thing a test
|
||||
/// without a browser can see, and that limit is said out loud rather
|
||||
/// than implied.
|
||||
/// The limit is **derived, not guessed**. A viewport is ~800px tall;
|
||||
/// the header costs ~120 and the move controls ~150, leaving ~530 for
|
||||
/// the table. The version that broke dragging declared 620. 500 keeps
|
||||
/// 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]
|
||||
fn the_table_is_short_enough_to_drag_from() {
|
||||
let v = view_of(6);
|
||||
|
|
@ -869,12 +876,85 @@ mod overhead_table {
|
|||
.expect("the table declares a viewBox");
|
||||
let h: f64 = vb.parse().expect("a number");
|
||||
assert!(
|
||||
h <= 460.0,
|
||||
h <= 500.0,
|
||||
"the table is {h}px tall; the action cards end up off-screen and \
|
||||
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
|
||||
/// piles picture are gone as separate views, and their content is on
|
||||
/// the table.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue