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
COOLRL Lost Cities Web
Browser-only Lost Cities client. The rules engine, observation builder, and PPO inference all run on the device; there is no application server.
The verified final JAX PPO policy is committed at
public/models/jax-ppo.onnx (3.1 MB). Vite copies it to the static build, and
the app resolves the asset relative to the deployed site so it works on GitHub
Pages, GitLab Pages, or a normal web root. Its size and SHA-256 are recorded in
public/models/jax-ppo.json.
Pushes to main build and publish the client through the repository's GitHub
Pages and GitLab Pages workflows. Each workflow supplies the correct base URL
for its host.
Seeded deals
Every game is dealt from a seed, shown in the menu and kept in the URL as
?seed=<seed>. Loading that URL — or typing the seed into the menu — replays
the exact same deal, so a game can be shared, replayed, or reported with a bug.
Seeds are arbitrary text; the deal is derived from a deterministic PRNG in
src/game/random.ts and is independent of the Python shuffle bank.
The seed shuffles the deck and nothing else. The policy is deterministic: it always plays its highest-logit legal action, so the same deal and the same moves reproduce the same game.
Hints, undo, and review
HINT asks the same policy that drives the rival what it would play in your
seat, and highlights the card, its destination, and the draw source (with the
policy's confidence when the ONNX model is loaded).
Undo and redo work per action, not per turn: choosing a card, choosing its
destination, and drawing are separate steps, and each is undone on its own. The
rival's moves are steps too, so a finished game can be stepped through from the
result screen (REVIEW GAME) all the way back to the deal. While undone moves
are pending the rival is suspended; PLAY FROM HERE drops them and resumes play
from the position on screen.
Setup
Install and run the checked-in final policy:
cd web
npm ci
npm run dev
Build the same static bundle used by a host:
npm run build
npm run preview
To replace the shipped policy, export a verified Orbax checkpoint from the repository root. The exporter writes both the ONNX file and its public metadata manifest:
uv run --with onnx scripts/export_jax_ppo_onnx.py \
--checkpoint /path/to/checkpoint \
--output web/public/models/jax-ppo.onnx
The policy tries WebGPU first and falls back to ONNX Runtime WebAssembly. If the model asset is absent, the UI remains playable using a simple local heuristic and reports that fallback in the header.
Checks
cd web
npm test
npm run build
The TypeScript engine follows src/lost_cities_jax/engine.py and its 96-action
atomic action space. Cross-runtime fixtures can be regenerated with:
uv run python scripts/generate_web_parity_fixture.py