CB-WP-0020: the table you can read
Some checks failed
ci / check (push) Failing after 3s

Six of seven perceptual defects fixed; item 1 already passed.

T01, at the maintainer's instruction: a legal target restyles its
EXISTING border rather than drawing a new box. outline + outline-offset
drew a second rectangle, which an SVG viewport clips (the missing top and
left edges) and which made a seat's highlight card-sized. A border
already in the layout cannot move the layout.

T02: the ghost was a textContent copy of the card, which is why the line
break collapsed and it read as a second card, and why showing the
explanation destroyed the label. It is now a pill, the explanation is
appended beside the label, and the left-behind element is dimmed and
dashed. The stub grew innerHTML so a test can assert BOTH are present --
it could previously only see that something was displayed.

T03: NOT reproduced and recorded as not reproduced. The likeliest cause
is which element the browser reports -- for touch and pen the pointer is
captured to the pointerdown target, making every drop look like a
drop-on-itself, which is the other half of the report. elementFromPoint
is correct under both explanations. Separately the refusal was written in
element ids on the one surface a player reads when something goes wrong;
it now speaks the game's words and a test forbids id leakage.

T04: seat selections rendered as Debug. The coverage gate then failed my
first fix for dropping a field when target and problem were both set --
the aggregate does not produce that shape and the gate was right not to
care.

T05: the headline reads from group_success. 'Play again' is real, and its
first version was useless: run_game bound a fresh listener per game, so a
second game moved to a new port and left the tab pointing at a dead one.
One listener per session now, and the test asserts the second game is a
DIFFERENT deal.

Chaos d8=8 fired the first override at the new rate and drew S, changing
nothing -- one half of window 2's retirement condition.

CB-WP-0019 settled at $38.54/117 against $34.80/107. Eight for eight,
and the first under 20%.

make all exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-03 20:20:38 +02:00
parent 5e06a7d01e
commit bf24affa84
8 changed files with 584 additions and 55 deletions

View file

@ -21,6 +21,7 @@ use games_ground::{GroundCommand, GroundState};
use crate::inspect::{render, seat_name};
use std::io::{BufRead, Write};
#[derive(Clone)]
pub struct Config {
pub seed: u64,
pub players: u8,
@ -220,14 +221,39 @@ pub fn play<R: BufRead, W: Write>(config: &Config, input: R, out: W) -> Result<S
// borrow them, so ownership stays here and the game runs one frame in.
let input = std::cell::RefCell::new(input);
let out = std::cell::RefCell::new(out);
run_game(config, &input, &out)
// CB-WP-0020 T05: a browser session can deal a second game. The seed
// advances, because replaying the identical deal is not "again".
// CB-WP-0020 T05: ONE listener for the session, not one per game.
// The first version bound a fresh port inside `run_game`, so "play
// again" moved the game to a new URL and the player's tab was left
// pointing at a dead port. Caught by `play_again_deals_a_second_game`.
let server = match config.serve {
Some(port) => {
let s = std::rc::Rc::new(crate::hotseat::Server::bind(port)?);
let _ = writeln!(out.borrow_mut(), " open {}", s.url());
let _ = out.borrow_mut().flush();
Some(s)
}
None => None,
};
let mut cfg: Config = config.clone();
loop {
let (summary, choice) = run_game(&cfg, &input, &out, server.clone())?;
if choice != crate::hotseat::EndChoice::Again {
return Ok(summary);
}
cfg.seed = cfg.seed.wrapping_add(1);
}
}
fn run_game<'a, R: BufRead + 'a, W: Write + 'a>(
config: &Config,
input: &'a std::cell::RefCell<R>,
out: &'a std::cell::RefCell<W>,
) -> Result<Summary, String> {
server: Option<std::rc::Rc<crate::hotseat::Server>>,
) -> Result<(Summary, crate::hotseat::EndChoice), String> {
let setup = Setup {
players: config.players,
preset: format!("standard-{}p", config.players),
@ -243,15 +269,6 @@ fn run_game<'a, R: BufRead + 'a, W: Write + 'a>(
// ADR-0007: a browser seat and a CLI seat are both just a Policy, so
// the driver cannot tell them apart — which is the property that lets
// a browser game replay as a scenario like any other.
let server = match config.serve {
Some(port) => {
let s = std::rc::Rc::new(crate::hotseat::Server::bind(port)?);
let _ = writeln!(out.borrow_mut(), " open {}", s.url());
let _ = out.borrow_mut().flush();
Some(s)
}
None => None,
};
let mut policies: Vec<Box<dyn Policy + 'a>> = Vec::new();
for seat in 0..config.players {
if config.human_seats.contains(&seat) {
@ -322,10 +339,11 @@ fn run_game<'a, R: BufRead + 'a, W: Write + 'a>(
// CB-WP-0018 T01: the browser sees the end of its own game. Measured
// before this: a game ended at 5 rounds / 30 commands, the result went
// to stdout, and the page's post-`ok` reload got Connection refused.
let mut end_choice = crate::hotseat::EndChoice::Closed;
if let Some(srv) = &server {
let ended = game.state.project(Viewer::Spectator);
let msg = format!("{} commands, hash {}", game.commands, &end_hash[..12]);
let _ = srv.serve_end(Some(&ended), &msg, END_LINGER);
end_choice = srv.serve_end(Some(&ended), &msg, END_LINGER)?;
}
let scenario = games_ground::record::to_scenario(
@ -361,11 +379,14 @@ fn run_game<'a, R: BufRead + 'a, W: Write + 'a>(
}
};
Ok(Summary {
rounds: game.rounds,
end_state_hash: end_hash,
scenario,
bundle,
recorded,
})
Ok((
Summary {
rounds: game.rounds,
end_state_hash: end_hash,
scenario,
bundle,
recorded,
},
end_choice,
))
}