fix: the game was unplayable, and the note redirect was refused
Some checks failed
ci / check (push) Failing after 3s
Some checks failed
ci / check (push) Failing after 3s
Tier S (fixes inside a boundary; chaos d8=7, no override). Four observations from play, three of them caused by CB-WP-0028 -- and make all was green for all of them. THE NOTE BUG, WHICH I GOT WRONG TWICE. The first fix put the token in the form's action, and that worked. But the 303 afterwards pointed at bare `/` with no token, so the note WAS SAVED and then the browser followed a redirect control 1 refuses. The player sees "no session token" for a note that already landed. A redirect is a request the browser makes on your behalf and is subject to every control the others are. I had tested the POST and stopped there -- the same mistake as the first fix, one step further along. Guard::page_path() now carries the token, and the test asserts the redirect target is ADMITTED rather than merely non-empty. WHY DRAGGING BROKE, WHICH WAS NOT THE DRAG. The gesture logic was fine: the JS harness posts correctly against the served page, and all 14 drop targets are present. The table was 620px tall, which pushed the action cards a full screen below the Problems -- and you cannot drag between two things that are never on screen together. Now 440, with a test asserting the declared height stays under 460 and saying why. That is a proxy for a browser layout, not the property itself, and the test says so. Seats sat ON the ellipse: they were placed at 0.83 of the table radius, which is inside it. Now outside, asserted numerically at 2 through 6 seats against the ellipse equation rather than eyeballed. And the `table` drop target was a separate CARD among the move buttons, which is exactly why a player looking at a picture of a table could not find anywhere to drop. The drawn ellipse is the drop zone now, and a test asserts there is EXACTLY ONE table target and that it is the drawn one -- two elements claiming to be the table is worse than none. The gap this exposes is the one CB-EV-0026 named a day earlier: every test asserted the DOM was correct, and it was. Nothing asserted the page was usable, and the drag test passes on a page you cannot physically drag on. What is added here are proxies a browser-less test can check. make all: exit 0. 61 render tests, 26 cb-play. Verified over real HTTP rather than by inspection: note POST 303, the redirect carries the token, following it returns 200, the table declares 440, one drop zone, seats at (380,421)/(113,112)/(647,112) against a table of rx=200 ry=118, and both notes reached the trial log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ab364a0319
commit
8eadb6c963
4 changed files with 175 additions and 46 deletions
|
|
@ -467,18 +467,24 @@ fn piles_body(out: &mut String, view: &GroundView) {
|
|||
/// and drawing the same thing twice is how the two drift.
|
||||
fn table_svg(view: &GroundView) -> String {
|
||||
let n = view.players.len().max(1);
|
||||
let (cx, cy) = (380.0f64, 300.0f64);
|
||||
let seat_r = 240.0f64;
|
||||
// Compact on purpose. The first version was 760x620, which pushed the
|
||||
// action cards a full screen below the Problems -- and you cannot drag
|
||||
// between two things that are never on screen together, which made the
|
||||
// game unplayable. Height is the constraint, not width.
|
||||
let (cx, cy) = (380.0f64, 215.0f64);
|
||||
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);
|
||||
|
||||
let pos: Vec<(PlayerId, f64, f64)> = view
|
||||
.players
|
||||
.keys()
|
||||
.enumerate()
|
||||
.map(|(i, p)| {
|
||||
// Start at the bottom, not the top: the viewer sits nearest
|
||||
// the reader, which is where you sit at a real table.
|
||||
// Start at the bottom: the viewer sits nearest the reader.
|
||||
let a = std::f64::consts::TAU * (i as f64) / (n as f64) + std::f64::consts::FRAC_PI_2;
|
||||
(*p, cx + seat_r * a.cos(), cy + seat_r * a.sin() * 0.72)
|
||||
(*p, cx + sx * a.cos(), cy + sy * a.sin())
|
||||
})
|
||||
.collect();
|
||||
let find = |p: PlayerId| {
|
||||
|
|
@ -488,12 +494,22 @@ fn table_svg(view: &GroundView) -> String {
|
|||
};
|
||||
|
||||
let mut s = String::from(
|
||||
"<svg viewBox=\"0 0 760 620\" width=\"100%\" style=\"max-width:760px\" \
|
||||
role=\"img\" aria-label=\"the table, seen from above\">\
|
||||
<ellipse cx=\"380\" cy=\"300\" rx=\"290\" ry=\"215\" fill=\"#171b23\" stroke=\"#2a3140\"/>",
|
||||
"<svg viewBox=\"0 0 760 440\" width=\"100%\" style=\"max-width:760px\" \
|
||||
role=\"img\" aria-label=\"the table, seen from above\">",
|
||||
);
|
||||
|
||||
// THE TABLE IS THE DROP ZONE. `table` was an abstract id with no
|
||||
// picture; the player looked for somewhere to drop GROUND and there
|
||||
// was nowhere. Now the surface you can see is the surface you drop on.
|
||||
let _ = write!(
|
||||
s,
|
||||
"<g data-drop=\"table\"><ellipse cx=\"{cx}\" cy=\"{cy}\" rx=\"{rx}\" ry=\"{ry}\" \
|
||||
fill=\"#171b23\" stroke=\"#2a3140\" stroke-width=\"2\"/>\
|
||||
<text x=\"{cx}\" y=\"{ty}\" fill=\"#556\" font-size=\"10\" \
|
||||
text-anchor=\"middle\">the table</text></g>",
|
||||
ty = cy + ry - 8.0,
|
||||
);
|
||||
|
||||
// Relations first, so seats draw over them.
|
||||
for (pair, rel) in &view.relations {
|
||||
if let (Some((x1, y1)), Some((x2, y2))) = (find(pair.0), find(pair.1)) {
|
||||
let colour = match rel {
|
||||
|
|
@ -503,7 +519,7 @@ fn table_svg(view: &GroundView) -> String {
|
|||
let _ = write!(
|
||||
s,
|
||||
"<line x1=\"{x1:.0}\" y1=\"{y1:.0}\" x2=\"{x2:.0}\" y2=\"{y2:.0}\" \
|
||||
stroke=\"{colour}\" stroke-width=\"2\"/>\
|
||||
stroke=\"{colour}\" stroke-width=\"2\" stroke-opacity=\"0.7\"/>\
|
||||
<text x=\"{mx:.0}\" y=\"{my:.0}\" fill=\"{colour}\" font-size=\"10\" \
|
||||
text-anchor=\"middle\">{rel:?}</text>",
|
||||
mx = (x1 + x2) / 2.0,
|
||||
|
|
@ -512,22 +528,28 @@ fn table_svg(view: &GroundView) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
// The middle of the table: Problems, then the two stacks under them.
|
||||
let span = (view.problems.len().max(1) as f64) * 130.0;
|
||||
// On the table: Problems across the middle, the two stacks under them.
|
||||
let count = view.problems.len().max(1) as f64;
|
||||
let scale = (2.0 * rx * 0.92 / (count * 130.0)).min(0.82);
|
||||
let _ = write!(
|
||||
s,
|
||||
"<g transform=\"translate({x:.0},170)\">",
|
||||
x = cx - span / 2.0,
|
||||
"<g transform=\"translate({x:.0},{y:.0}) scale({scale:.3})\">",
|
||||
x = cx - count * 130.0 * scale / 2.0,
|
||||
y = cy - ry + 24.0,
|
||||
);
|
||||
for (i, (priority, p)) in view.problems.iter().enumerate() {
|
||||
problem_svg(&mut s, *priority, p, (i as i32) * 130);
|
||||
}
|
||||
s.push_str("</g>");
|
||||
let _ = write!(s, "<g transform=\"translate({x:.0},290)\">", x = cx - 150.0);
|
||||
let _ = write!(
|
||||
s,
|
||||
"<g transform=\"translate({x:.0},{y:.0}) scale(0.62)\">",
|
||||
x = cx - 300.0 * 0.62 / 2.0,
|
||||
y = cy + 8.0,
|
||||
);
|
||||
piles_body(&mut s, view);
|
||||
s.push_str("</g>");
|
||||
|
||||
// Seats, each with what it played in front of it.
|
||||
for (p, x, y) in &pos {
|
||||
let is_viewer = view.viewer == Some(*p);
|
||||
let focus = view
|
||||
|
|
@ -536,34 +558,31 @@ fn table_svg(view: &GroundView) -> String {
|
|||
.map(|f| format!(" \u{2192}{}", seat_name(*f)))
|
||||
.unwrap_or_default();
|
||||
let pv = &view.players[p];
|
||||
// The played card sits between the seat and the centre.
|
||||
let (dx, dy) = ((cx - x) * 0.30, (cy - y) * 0.30);
|
||||
// The played card sits between the seat and the table.
|
||||
if let Some(sel) = view.selections.get(p) {
|
||||
let _ = write!(
|
||||
s,
|
||||
"<g transform=\"translate({px:.0},{py:.0}) scale(0.62)\">",
|
||||
px = x + dx - 26.0,
|
||||
py = y + dy - 17.0,
|
||||
"<g transform=\"translate({px:.0},{py:.0}) scale(0.55)\">",
|
||||
px = x + (cx - x) * 0.34 - 23.0,
|
||||
py = y + (cy - y) * 0.34 - 15.0,
|
||||
);
|
||||
played_inner(&mut s, sel);
|
||||
played_svg(&mut s, sel);
|
||||
s.push_str("</g>");
|
||||
}
|
||||
let _ = write!(
|
||||
s,
|
||||
"<g data-drop=\"seat-{raw}\">\
|
||||
<circle cx=\"{x:.0}\" cy=\"{y:.0}\" r=\"34\" fill=\"#1b1e26\" \
|
||||
<circle 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>\
|
||||
<text x=\"{x:.0}\" y=\"{t2:.0}\" fill=\"#89a\" font-size=\"10\" \
|
||||
text-anchor=\"middle\">stress {stress}{focus}</text></g>",
|
||||
raw = p.0,
|
||||
// The viewer's own seat is thicker and blue: a table where you
|
||||
// cannot find yourself is worse than a list.
|
||||
stroke = if is_viewer { "#9cf" } else { "#5a6b7a" },
|
||||
sw = if is_viewer { 3 } else { 2 },
|
||||
t1 = y - 2.0,
|
||||
t2 = y + 14.0,
|
||||
t2 = y + 13.0,
|
||||
name = seat_name(*p),
|
||||
you = if is_viewer { " (you)" } else { "" },
|
||||
stress = pv.stress,
|
||||
|
|
@ -610,11 +629,6 @@ const CARD_BACK: &str = "<svg viewBox=\"0 0 84 56\" width=\"84\" height=\"56\" c
|
|||
<rect x=\"2\" y=\"2\" width=\"80\" height=\"52\" rx=\"7\" fill=\"#2a2f3a\" stroke=\"#5a6b7a\"/>\
|
||||
<path d=\"M14 14 h56 M14 28 h56 M14 42 h56\" stroke=\"#3d4756\" stroke-width=\"3\"/></svg>";
|
||||
|
||||
/// The played card without its own `<svg>`, for placing on the table.
|
||||
fn played_inner(out: &mut String, sel: &SelectionView) {
|
||||
played_svg(out, sel);
|
||||
}
|
||||
|
||||
fn played_svg(out: &mut String, sel: &SelectionView) {
|
||||
let s = match sel {
|
||||
SelectionView::Hidden => {
|
||||
|
|
@ -1053,10 +1067,11 @@ fn move_section(
|
|||
);
|
||||
}
|
||||
}
|
||||
s.push_str(
|
||||
"</div><div class=\"card\" data-drop=\"table\">the table \u{2014} drop here for an \
|
||||
untargeted action</div>",
|
||||
);
|
||||
// CB-WP-0028: the table's drop zone is THE TABLE, drawn above.
|
||||
// This was a separate card labelled "the table" sitting among the
|
||||
// move buttons, which is why a player looking at a picture of a
|
||||
// table could not find anywhere to drop.
|
||||
s.push_str("</div>");
|
||||
}
|
||||
if may_pass {
|
||||
s.push_str(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue