Catan Duel
A browser table for a two-player card game I play with a friend. Both players see one shared board with sounds and a soundtrack, the app does the maths, dice and shuffling, and nothing enforces the rules: the players decide, exactly like the cards in front of them.
What this is
A digital table for the two-player Catan card game, for people who own the physical game and want to play it across two screens. Both principalities and the central row sit on one screen. You drag a card onto the board to place it, rotate your own region discs to pay, and tap an active card to play it, and every card type and event has a matching sound with a fitting half-hour soundtrack per expansion era. All nine eras are in, and you can mix as many as you like.
It is a personal project and not affiliated with the publisher. I built it with my friend Ziad; I wrote about two thirds of the commits and the code lives in our shared repository.
The main decision: a table, not a referee
Most digital board games enforce the rules for you. We went the other way on purpose. The app tracks state, rolls dice, shuffles and does the arithmetic, but it never refuses a move. That matches how the physical game actually gets played (house rules, take-backs, a quick “wait, I forgot”) and it removed a whole class of edge-case arguments from the code, so we could spend the time on feel instead: drag and drop that lands where you expect, the top bar folding away to a corner so nothing covers the board, and a browser for looking through any stack.
Keeping two browsers on the same board
Online play runs through a small websocket relay, and it also works over a LAN or a free VPN with no cloud at all. Every action is applied identically on both sides from a shared seed, so dice and shuffles agree without sending the results around. When we hit a case where the two boards drifted apart in a real game, the fix was structural rather than a patch: the room owns the seed, and a reconvergence step brings a client that has fallen behind back to the agreed state instead of leaving it to argue.
Testing
240 unit and fuzz tests run on every change. The fuzz suite throws long random sequences of actions at the game state and checks it never becomes inconsistent, which is how the sync bugs were caught after the first one. A Playwright suite drives the real interface, including a test that the audio actually plays.
Stack
React and TypeScript with Vite, Zustand for state, PartyKit for the relay, Vitest and Playwright for tests.