Skip to content

Grandes Colonies

A board game for eight to twelve players, in the browser, on a 3D board. Everyone joins from their own phone, and nobody loses their seat when the wifi drops.

production2026 – presentTypeScript · React · three.js · WebSocket · Vite
The host screen: QR code, game code, seats and settings
The host screen: QR code, game code, seats and settings

8 to 12 players in a single game613 tests, including full games played end to endReconnects on its own, seat keptRuns on my own hardware

The problem

Board games that take twelve players exist, but they take an evening to set up and a referee to keep straight. Somebody always ends up counting cards for everyone else instead of playing.

The digital versions solve the counting and lose the table. They are built for four players, matched with strangers, and they assume everyone stays connected for two hours. On a home network with twelve phones, that assumption breaks in the first ten minutes.

What it does

One machine hosts. It shows a screen with a QR code and a game code that can be read out loud, and everyone else opens the link on their phone. No account, no install, no lobby full of strangers.

The board is rendered in 3D and resizes itself to the number of players, so twelve people are not fighting over a board built for four. Missing players can be handed to a bot, and the host can pause the game when somebody goes to get a drink.

Joining
A QR code and a code you can say out loud
Board
Archipelago or disc, sized to the player count
Opponents
Bots with levels and characters
Recovery
Every game is journalled and can be resumed

Architecture

  • Host screenlocal network only

    Game server

  • Players' phonesone per guest

    Game server

  • Botsown processes

    Game server

  • Game serverraw WebSocket

    Rules engine

  • Rules engineno network, no clock

    Game journal

  • Game journalone line per command
visitormy hardwarestorageeveryone comes in through the same door
01

Rules engine

The whole game, as pure functions with no network and no clock. It is the part that is tested, and it can play thousands of games against itself in a few seconds.

02

Game server

A raw WebSocket transport and nothing else. It routes commands into the engine and sends back a view per player, redacted so that nobody ever receives another player's hand.

03

Host screen

The referee: settings, seats, pause, extra time, and the panel that can adjust a game in progress. It answers on the local network only, and refuses anything arriving from the internet.

04

Bots

Separate processes that connect through the same WebSocket as everyone else. They see the same views and get the same refusals, so no shortcut into the game state can exist.

05

Game journal

Every command, written to disk as it happens. A game that crashes is exactly the moment you wish you had it, so it is written whether or not anyone asked.

Built with

Client

React
Screens, hands, and the trading interface
three.js
The 3D board, dice, and pieces
Vite
Build, and the dev server during a session

Server

Node
One process serving the page and the game
ws
Raw WebSocket transport, no framework above it
TypeScript
Shared protocol types, so client and server cannot drift

Testing

Vitest
613 tests over the engine, client and server
Simulation harness
Plays whole games, bots against bots, to check balance

Delivery

GitHub Actions
Tests, then builds the image
Container registry
Where the built image waits to be collected
Cloudflare Tunnel
Public access with no inbound port at home

Under the hood

01The framework that was the plan turned out not to exist

The design called for Colyseus. Its 0.16 release does not install: a workspace dependency was published by mistake. Its 0.18 has no JavaScript client. No single version offered both ends. The loss was smaller than it looked, because only the rooms and the transport were ever going to be used: the state synchronisation broadcasts to everyone, which would have meant filtering hands and secret objectives field by field, where one mistake reveals a hand. The transport was replaced by raw WebSocket in an afternoon.

02A reconnection is the normal case, not an exception

Twelve phones on a home network means a one second dropout is routine, and a locked screen is not a departure. The seat token is written to persistent storage rather than held in memory, and the client reconnects by itself. Nobody should have to reload a page to survive a lift, and nobody should lose a game because their phone went to sleep.

03The clock had to learn to wait

Resuming a saved game restarted the turn timer immediately, and every absent player had their turn played for them, cycle after cycle. Six hundred and sixty-nine cycles were eaten before the first human managed to reopen their link. The game now resumes paused. It is also the right human behaviour: after a power cut, the host waits for the room to reconnect, then starts.

04Bots come in through the front door

The automatic opponents are separate processes that connect over the same WebSocket as the guests. They receive the same redacted views and the same refusals, and declaring yourself a bot grants no privilege at all. It serves exactly one purpose: when the host shrinks the table, the bots are the ones who give up their seats. Keeping them outside the server also means no code path exists that could hand them the full game state.

Gallery

The 3D board during a twelve player game
The 3D board during a twelve player game
A player's hand and the trading panel, on a phone

Try it

Yes, but it is better with a room full of people.

The link opens the player view, and you can take a seat. What you cannot do from outside is host: the referee screen answers on the local network only, so a game has to be started from the house. The architecture below is what the host side looks like.

What broke

Would change

Publishing the game removed its only access control

The referee panel had no authentication and never needed any: it lived on a port that only the guests in the room knew. Putting the game on the internet deleted that protection without replacing it, and for a short window anyone could have granted themselves resources. The rule taken from it: a protection that lives in a dashboard is not a protection. The refusal now sits in the code, with a test holding it in place.

Would do again

Journalling every game before anyone asked for it

Every command is written to disk as it happens, without being switched on. It was not on any list of features. It is what makes a crashed evening recoverable, and it is also what the balance measurements are computed from. Two uses for something that was written on a hunch.

Where it stops

A twelve player game is still long

The simulation suite plays whole games and measures how many cycles they take. At twelve players it stays well above the target length, and the smaller disc board only shortens it so far. Making it faster is a rules problem, not a code problem, and it has not been solved.

Next projectDocStack