The web client now plays borealis (data/models.json), trained on the three-round match and taking the 501-dim match view rather than a bare round. Only the actor trunk is exported -- the critic exists to grade moves in training and never plays, so the graph physically cannot leak the opponent's hand or the deck, which beats promising not to call it. The TypeScript match layer and observation mirror match.py and match_obs.py. They have to agree to the bit: a mismatch throws nowhere, the ONNX policy just consumes a wrong vector and plays worse for reasons nobody can see. So the port is not trusted -- generate_match_parity_fixture.py emits 282 positions from real JAX play (mid-round, both seats, past a roll-over, with a live carry) and the TS output is checked against them to float32 round-off. Match mode is a menu toggle. A seed fixes all three deals and the coin flips, so a match stays a pure function of it. One-deal mode is unchanged from the player's side; borealis simply sees it as round one at a carry of zero, a position it has seen a great many times. Two bugs found by driving the built app in a browser, both silent: - The result card totalled the round, not the match. It read "-11 : 3" while the match stood at -96 : 66 -- it would have named the wrong winner. It now headlines the match total and breaks the round out beneath it. - Game records were being rejected. The client's schema went to v2 (it now records which model played; the old records stored the on-screen label, which stops identifying anything once there are two models) while serve_web_with_logs.py still only accepted v1, so every record would have 400'd into a console warning. v1 stays accepted -- the 111 existing games are altair. npm test and tsc were green through both. Hence web/.claude/skills/verify, which records the recipe and the selectors so the next session drives the app instead of re-deriving how. .gitignore excluded the new model, which would have shipped a 404: the deploy builds straight from the repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBQKgvBbxbheiTF1AVy1Sh
69 lines
2.6 KiB
Markdown
69 lines
2.6 KiB
Markdown
---
|
|
name: verify
|
|
description: Drive the Lost Cities web client in a real browser to observe a change working — model loading, a full match, the result card, records being written.
|
|
---
|
|
|
|
# Verifying the web client
|
|
|
|
The client is where a silent bug hides best. `npm test` and `tsc` were both green
|
|
while the app was announcing the wrong winner and dropping every game record on
|
|
the floor. Neither throws. Run it.
|
|
|
|
## Build and serve
|
|
|
|
```bash
|
|
cd web && npm run build
|
|
cd .. && tmux new-session -d -s websrv \
|
|
"uv run python scripts/serve_web_with_logs.py --host 127.0.0.1 --port 5199 \
|
|
--dist web/dist --output /tmp/verify-records.jsonl"
|
|
```
|
|
|
|
Serve the **built dist**, not the vite dev server — the deploy builds from dist,
|
|
and the model is a static asset whose path only resolves there. Point `--output`
|
|
at a scratch file so verification runs never touch `data/human-play/`.
|
|
|
|
## Drive it
|
|
|
|
Playwright is deliberately **not** a dependency: the `playwright` package downloads
|
|
~114MB of Chromium on install, and `npm ci` runs in the deploy workflow. Install it
|
|
for the run and uninstall after.
|
|
|
|
```bash
|
|
cd web
|
|
npm i -D playwright --no-fund --no-audit && npx playwright install chromium
|
|
# ... drive ...
|
|
npm uninstall playwright
|
|
```
|
|
|
|
## Selectors that actually work
|
|
|
|
Found by dumping the DOM; guessing at them wasted two runs.
|
|
|
|
| What | Selector |
|
|
|---|---|
|
|
| a hand card | `button.card:not([disabled])` |
|
|
| play onto an expedition | `button.lane__zone--mine.is-target` |
|
|
| discard | `button.lane__discard.is-target` |
|
|
| draw from deck | `button.deck-stack:not([disabled])` |
|
|
| which model loaded | `.score-plaque--rival small` |
|
|
| match progress | `.round-strip` |
|
|
| final scores | `.result-card` |
|
|
|
|
A turn is three clicks: pick a card, choose where it goes, then draw. The place and
|
|
draw targets only appear **after** the card is selected, and only the legal ones are
|
|
enabled — so click the card first, then query.
|
|
|
|
The rival answers on a 620ms timer plus inference; ~200ms of slack between plies is
|
|
enough. A full three-round match runs ~140 plies, so budget a few minutes.
|
|
|
|
## Worth driving
|
|
|
|
- **A whole match, not one round.** The round roll-over is where carry banks, and it
|
|
is where the result card got it wrong.
|
|
- **Check the record actually saved.** A schema bump on the client silently 400s
|
|
against `scripts/serve_web_with_logs.py` until its allowlist is updated too.
|
|
- **A stale save in localStorage.** Bump the key on a schema change; a half-migrated
|
|
save is worse than a fresh deal.
|
|
- **The same seed twice.** In match mode the seed fixes all three deals and the coin
|
|
flips, so a match is a pure function of it.
|