PlayBelote
Live multiplayer belote: a deterministic engine, Monte Carlo bots and a replay format
- Role
- Solo, directing agent fleets
- Status
- Live
- Source
- Private repository
- Stack
- TypeScriptNext.js 16React 19Phaser 3ColyseusPrismaPostgreSQL 16PgBouncerNginxPM2VercelHetznerVitestPlaywrightfast-check

In numbers
13,123
bot games across 20 championships of 128 teams
10/ 10
seeded game pairings at visual parity between the new table client and the Phaser baseline
1.4
current version of BGN, the replay format modelled on chess PGN; the viewer switches perspective between seats
12
bot types rated by ELO in the 20-championship run
The problem
Belote has hidden hands, bidding, announcements and a scoring system with many exceptions. An online table needs a rules engine that client and server both trust and bots strong enough to fill empty seats, and the game has to survive reconnects and tab switches. I do not write the code by hand: I specify the work, and agents build it while I check the result against numbers.
The approach
The code has three layers: shared/ is a pure deterministic engine (seedable RNG, no I/O) that both sides import, server/ a Colyseus room server split into an orchestrator and six managers, and src/ the Next.js app with a Phaser canvas. Verification happens in long runs with a contract kept in the repo: a 20-tournament bot evaluation, Codex unattended runs, a 120-agent review sweep in which an adversarial verifier checked each finding before it became an issue, and a later full sweep.

How it works
Bots that sample the hidden hands
The Monte Carlo player deals the unseen cards at random, consistent with what the table has revealed: cards already played and suits a player has shown void in. It plays out 60 simulations per card decision and 40 per bid, and opens the bidding only above a 55% simulated win rate. Tournaments also field twelve heuristic personalities, seeded as 128 permanent bot teams.
13,123 games across 20 championships
The production server ran 20 consecutive 128-team championships: a 9-round Swiss phase, the top 32 into single elimination, best of three with a best-of-five final, first to 151 points. That came to 13,123 games and 180,362 rounds, rated by ELO per bot type. The top two types tied at 1531, and first to twelfth spans 55 ELO points (58.9% to 42.1% win rate). The run also found a leak: finished all-AI rooms were never disposed, and the server ran out of memory at 1.8 GB after tournament 11. All-AI rooms now disconnect when the game ends.
BGN, a notation for belote
Belote Game Notation is modelled on chess PGN and versioned, currently 1.4. The server records each game; players download the .bgn file and step through it in a viewer that switches perspective between seats. The review sweep found that recorded files carried a wrong scoring breakdown, and version 1.4 fixed the format.
The alt-tab bug, written down as seven invariants
At one point returning to the tab made the game unplayable: instant deals, a lost hand, a crash inside the scene. The chain: next-auth refetched the session on window focus and returned a new object with the same data; the game page's connect effect listed that object as a dependency, re-ran and set a loading flag; the loading gate unmounted the Phaser canvas. Each tab return tore the game down and rebuilt it. The e2e suite stayed green because its helper created a detached room that bypassed the page's connect effect. The fix ships with seven written invariants (the canvas stays mounted for the session, the connect effect depends only on stable primitives, reconnection is an overlay, each await in a scene handler is a teardown boundary) and a required check through the real lobby: a scripted alt-tab once, five times and 90 times, with zero teardown lines in the console.
Unattended runs judged by vision
The deeper fix was a second table client that renders from state. Codex built it in unattended runs under a contract kept in the repo: a baseline gate, deterministic tests, live browser runs, then a panel of vision judges comparing the same seeded game in the Phaser client and the new client frame by frame, then an adversary panel, then a written report. The contract's rules: verify every number yourself, leave the engine, server and database alone, fix forward, and keep Phaser as the default. Run 2 ended with 2,499 tests passed and 10 of 10 seeded pairings at parity. Run 3 split the QA list by file cluster, because two agents editing one file collide.
Production setup
The client is on Vercel. The game server, Postgres 16, PgBouncer, Nginx and PM2 run on a Hetzner VPS, migrated from Render and Neon. Deploys arrive through a webhook, the database is dumped nightly with 31 days kept, and a rate limiter plus invalid-action tracking disconnects clients that keep sending illegal moves.

What I chose, and what lost
Chose
Render the table from state, a snapshot plus the events missed, as a second client next to Phaser
Over
More patches to the event-driven Phaser client
It assumed each event arrives once and in order. Reconnects and tab resumes break that assumption.
Chose
Phaser stays the default until I switch it, and the new client has to match it frame by frame on seeded games
Over
Switching renderer once the tests are green
Passing correctness tests was not enough: the alt-tab bug had shipped through a green e2e suite.
Chose
Build v2 in a clean room: agents may read the rules, the brief and fresh research, and nothing from v1
Over
Evolving v1, or letting the v2 agents read its code and specs
Reuse the rules and leave v1's assumptions behind.
Outcome
Live at playbelote.vercel.app. The last recorded full run: 2,838 tests passing across 98 files. A later sweep filed 31 issues (3 critical, 10 high); two critical server-side findings from it are still open. There is no mobile app: Capacitor is scaffolded and the Android project was never added. v2 exists and is unreleased. It is a clean-room, single-player 3D rebuild with two themes, a premium card room and a tavern parody, and bots that run determinised Monte Carlo over up to 48 sampled worlds with an exact minimax once two cards remain. Phases 1 to 3 passed their gates with 717 tests; the no-lag performance gate passes 6 of its 9 criteria. Nothing is pushed, and at handoff I had not played the finished build.
What comes next
v2 is built to take multiplayer later: a table actor in a Web Worker owns state and clocks, clients speak a zod-validated protocol with sequence numbers and per-seat redaction, and only the transport changes, from postMessage to WebSocket.
Gallery
The public landing page.
The bid is called, the bots pass, and two tricks are played.
A trick in the new table renderer, with the bid and announcements visible.
Games export to a plain-text replay format and can be played back.
Deal sequence in the table renderer, the candidate in a parity check.
The same deal in the Phaser baseline.