PlayBelote v2
A clean-room 3D belote with a tavern cast, a jazz salon and a table that lives in a worker
- Role
- Product owner, directing agent fleets
- Status
- Unreleased
- Source
- Private repository
- Stack
- TypeScriptVitethree.jsWeb WorkerzodBlenderVitestPlaywrightfast-check

In numbers
16/ 16
signature moments fired from real game events in each room, end to end
999/ 1,000
matches won by the baseline bot against a random-legal player
438,430
illegal moves thrown at the engine across 2,000 fuzzed games, 0 exceptions
136
logged design decisions, each with the alternatives it beat
144.1fps
tavern table at a human decision point, up from 37.5
The problem
PlayBelote v1 had grown through nearly a year of changes, and its client broke whenever an event arrived twice or out of order, as the alt-tab bug showed. I wanted a second game that kept the rules of the first and none of its assumptions, started as single-player, and put the cards in rooms worth looking at.
The approach
I wrote a brief with the stack decision, a clean-room rule and acceptance criteria frozen before each phase, then ran orchestrated agents against it. The brief rejected eight stacks in writing before anything was built, among them Colyseus, Phaser, React Three Fiber and Godot. A grader agent that had not written the code ran the commands and reported the results, and each decision went into a log with the alternatives it beat.

How it works
Two rooms, one renderer
The Salon is an art-deco card room after Sofia's old city casino: midnight-green felt with visible fibre, a brass rail, and one warm lamp whose pool of light follows the active player. The Mehana is a tavern table under a grapevine, with a checked cloth, a wood stove and the Rila lakes in the window. Cards are thin two-sided 3D objects that arc, flip and cast shadows under the lamp, and each one chases its target position on a damped spring, so a dealt card settles with a small overshoot. Every shader is written from scratch.
A cast with jobs
In the tavern a grandmother deals and comments, the grandmothers on the bench watch and scold real blunders that the bot flags for them, the neighbour looks on over the fence, and Bora, a Shar Pei, snores under the table and wakes for big moments. The goat is a pun: the Bulgarian for trump is koz and for goat koza, so on a no-trumps bid the goat bleats and walks off. A contre brings a kuker bursting in with bells, and a capot brings the full wedding band, a horo around the table and Bora howling along with the clarinet. Each room has 16 signature moments. The characters were flat cut-outs first and looked glitchy against the lit table, so mid-build they became Blender models, compressed with KTX2 and meshopt to 44 MB in total.
Music that keeps score
All music is synthesised in JavaScript by a look-ahead bar scheduler, with no samples, and it doubles as the streak meter: each consecutive win adds a layer. In the Salon an original modal, hard-bop tune adds walking bass, ride and brushes, then piano. In the Mehana an original rachenitsa in 7/8 adds tapan, gaida, clarinet and accordion up to the full band, the score odometer ticks in the same 7/8, and the deciding deal switches to the kopanitsa's 11/16. Character voices are formant synthesis in Bulgarian and English, and an intensity dial (calm, lively, wedding) scales particles, reactions and music.
A table actor in a worker
All game state and clocks live in a table actor inside a Web Worker. The client talks to it through a zod-validated protocol with sequence numbers, per-seat redacted events, and a snapshot plus missed events on reconnect. A game is its seed plus its action log, so the same table can later sit behind a socket. Hiding the tab and coming back, reloading, or restarting the browser mid-trick all resume to the actor's exact view.
Search bots with an exact endgame
Bots run determinised search over up to 48 sampled worlds per decision, with common random numbers across candidate moves and heuristic rollouts. Once the deciding seat holds two cards or fewer, minimax solves the hand exactly. I looked at ISMCTS in the stack research, and the determinised search is what the bots use. The baseline bot won 999 of 1,000 matches against a random-legal player, at a mean of 5.58 ms per decision.
Gates, a decision log and a frame budget
Acceptance criteria were frozen before each phase. All 112 spec ids were covered, a fuzz run threw 2,000 games and 438,430 illegal probes at the engine with 0 exceptions, and 56 layout shots covered 20 viewport sizes from 320 by 480 to 3840 by 2160. At a human decision point the tavern table rose from 37.5 to 144.1 fps, and its desktop download fell from 54.8 MB to 19.4 MB.

What I chose, and what lost
Chose
Single-player first, with a transport that can swap later
Over
Building multiplayer in the first pass
I wanted the single-player game finished before any networking. The table protocol moves from postMessage to a WebSocket later without touching the game.
Chose
Real 3D models
Over
Flat cut-out characters
The cut-outs looked glitchy and out of place against a lit 3D table.
Chose
Compressed models built from untouched Blender originals
Over
Compressing the shipped files in place, or Draco geometry compression
Meshopt decodes faster and three.js ships its decoder inline. The originals stay byte for byte, so a check can re-encode every model and confirm the shipped files match: 36 the same, 0 different.
Chose
A clean-room build from the written rules
Over
Porting the v1 code
Reusing v1 would carry its old assumptions into the second game.
Outcome
Phases one to three passed the gate in round three, with 717 tests across 63 files, lint clean and type checks passing in 8 of 8 projects. The no-lag performance gate sits on a separate branch and passed 6 of 9 criteria: the salon table still shows a 69.3 ms worst frame against a 50 ms budget, and the 6x CPU phone proxy fails. No real phone was tested; phone play is emulated. The game has no remote and no public build, and at handoff I had not yet played the finished version.
What comes next
Record a play-through or host a build, fix the two failing performance criteria, then add the WebSocket transport the protocol was designed for.
Gallery
The bidding bar.
The characters answer in speech bubbles.
The deal summary: trick points, declarations and totals.
The salon theme of the same build.
Bidding in the salon theme.
Fourteen seconds of play in the tavern theme.
The tavern room the match is played in.
The table in the room.
Mid-hand in the tavern theme, with the characters speaking in Bulgarian.
The portrait phone layout.
Three of the opponents at the table.
The grandmother, in nine of her poses.
The four haiduti, animated as one group.
Winning every trick, a capot, sends the grandmother into a wedding.