CB-WP-0018-T01: the browser sees the end of its own game
Server::serve_end plus doc::ending, wired into both of run_game's exits. Where the browser used to get Connection refused it now gets the ending page with the result and the final table. It serves until the page posts 'done' (the page carries a close control), with a 600s linger so an abandoned tab cannot hold the process open. document() split into body() and move_section() so the ending shows the same table rather than a second rendering of it. The control had to be built twice and the first was worthless: the_end_of_the_game_reaches_the_browser calls serve_end directly, and deleting the call from run_game left it GREEN -- it tested the link and not the chain, which is CB-EV-0012's finding recurring. a_real_game_played_to_its_end_leaves_the_ending_on_screen runs the real play() with a browser seat, drives a real game to its end over a real socket, and goes red under that mutation printing an empty page -- the reported symptom exactly. A weak assertion of mine caught by itself: the first draft grepped the page for location.reload, which would have forced a second script to satisfy a test rather than a requirement. It now asserts the ending endpoint cannot answer 'ok'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
78e82497ee
commit
57639623da
4 changed files with 433 additions and 17 deletions
|
|
@ -431,6 +431,24 @@ pub fn document(
|
|||
},
|
||||
);
|
||||
|
||||
body(&mut s, view);
|
||||
move_section(&mut s, legal, seat, may_pass);
|
||||
let _ = write!(
|
||||
s,
|
||||
"<div id=\"cb-status\">ready</div>\
|
||||
<script>window.CB_ENDPOINT={endpoint}</script><script>{SCRIPT}</script>\
|
||||
</body></html>",
|
||||
endpoint = json_string(endpoint),
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
/// The table itself: problems, relationships, seats, solutions, outcome.
|
||||
///
|
||||
/// Factored out of [`document`] so [`ending`] shows the SAME table rather
|
||||
/// than a second rendering of it — two renderings of one state is how
|
||||
/// they drift.
|
||||
fn body(s: &mut String, view: &GroundView) {
|
||||
s.push_str(
|
||||
"<h2>problems</h2><svg width=\"760\" height=\"100\" role=\"img\" aria-label=\"problems\">",
|
||||
);
|
||||
|
|
@ -440,7 +458,7 @@ pub fn document(
|
|||
);
|
||||
}
|
||||
for (i, (priority, p)) in view.problems.iter().enumerate() {
|
||||
problem_svg(&mut s, *priority, p, 10 + (i as i32) * 130);
|
||||
problem_svg(s, *priority, p, 10 + (i as i32) * 130);
|
||||
}
|
||||
s.push_str("</svg>");
|
||||
|
||||
|
|
@ -449,7 +467,7 @@ pub fn document(
|
|||
|
||||
s.push_str("<h2>seats</h2><div class=\"row\">");
|
||||
for (id, p) in &view.players {
|
||||
player_card(&mut s, *id, p, view);
|
||||
player_card(s, *id, p, view);
|
||||
}
|
||||
s.push_str("</div>");
|
||||
|
||||
|
|
@ -512,6 +530,16 @@ pub fn document(
|
|||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// The "your move" section: action cards, numbered fallbacks, the table,
|
||||
/// and pass. Emits nothing when the seat has nothing legal to do.
|
||||
fn move_section(
|
||||
s: &mut String,
|
||||
legal: &[games_ground::GroundCommand],
|
||||
seat: Option<PlayerId>,
|
||||
may_pass: bool,
|
||||
) {
|
||||
if !legal.is_empty() {
|
||||
s.push_str("<h2>your move</h2><div class=\"row\">");
|
||||
for a in [
|
||||
|
|
@ -572,14 +600,6 @@ pub fn document(
|
|||
"<div class=\"card btn pick\" data-drop=\"pass\">pass \u{2014} decline to act</div>",
|
||||
);
|
||||
}
|
||||
|
||||
let _ = write!(
|
||||
s,
|
||||
"<div id=\"cb-status\">ready</div>\
|
||||
<script>window.CB_ENDPOINT={};</script><script>{SCRIPT}</script></body></html>",
|
||||
json_string(endpoint),
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
/// Quote a string as a JSON literal, so a token can never end the script.
|
||||
|
|
@ -622,6 +642,44 @@ fn json_string(s: &str) -> String {
|
|||
/// element could ever be `seat-0` — the relationship-graph circle took it
|
||||
/// and the seat card the instruction text points at went without. A seat
|
||||
/// is drawn twice and both drawings are the seat.
|
||||
/// The page a finished game leaves behind (CB-WP-0018 T01).
|
||||
///
|
||||
/// `view` is `None` when the game ended badly: then there is no result to
|
||||
/// draw and saying so is the whole point. Drawing a table for a game that
|
||||
/// crashed would be the same lie the empty page told, dressed up.
|
||||
///
|
||||
/// **This page does not auto-reload.** The old one reloaded on `ok` and
|
||||
/// the reload was refused, which is how a completed game became a blank
|
||||
/// tab.
|
||||
pub fn ending(view: Option<&GroundView>, message: &str, endpoint: &str) -> String {
|
||||
let mut s = String::with_capacity(4096);
|
||||
let _ = write!(
|
||||
s,
|
||||
"<!doctype html><meta charset=\"utf-8\">\
|
||||
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\
|
||||
<title>GROUND — game over</title><style>{STYLE}</style>\
|
||||
<h1>game over</h1><div class=\"card\">{msg}</div>",
|
||||
msg = esc(message),
|
||||
);
|
||||
match view {
|
||||
Some(v) => body(&mut s, v),
|
||||
None => {
|
||||
s.push_str(
|
||||
"<div class=\"card\">The game ended without a result, so there \
|
||||
is no final table to show. The reason is above.</div>",
|
||||
);
|
||||
}
|
||||
}
|
||||
let _ = write!(
|
||||
s,
|
||||
"<div class=\"card btn pick\" data-drop=\"done\">close — I have read this</div>\
|
||||
<div id=\"cb-status\">the game is over</div>\
|
||||
<script>window.CB_ENDPOINT={endpoint}</script><script>{SCRIPT}</script>",
|
||||
endpoint = json_string(endpoint),
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
pub fn drop_keys(html: &str) -> std::collections::BTreeSet<String> {
|
||||
let mut out = std::collections::BTreeSet::new();
|
||||
let mut rest = html;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue