Skip to the content
Georgi DimitrovdaTuzzo

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
A PlayBelote table mid-hand under an All-Trumps contract. Three bots hold fanned hands at the top, left and right, the player's cards are fanned along the bottom, four cards lie in the centre, and the bidding history, score and announcements sit in the corners.
A trick under an All-Trumps contract, with the bidding history and score in the corners.

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.

The bidding step at a PlayBelote table: a panel offers four suits, No Trumps, All Trumps and Pass while three bots and the player hold their dealt hands around green felt.
The bidding step.

How it works

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

A trick of clubs in the middle of a PlayBelote table, with the three bots' hands around it, the player's hand along the bottom and a Quarte announcement at the lower left.
A club trick with a Quarte announcement.

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.