01. Overview
A full-stack, real-time multiplayer implementation of the classic dice game, built around a server-authoritative architecture rather than a trusting client. Every roll, hold, and scoring decision is validated and computed on the backend, with the server acting as the single source of truth for game state; the client never decides what happened, only what it's asking to happen next.
The app supports three modes from one shared engine: offline pass-and-play on a single device, live private lobbies where friends join over a room code and play in real time over WebSockets, and a ranked mode with a working Elo rating system laying the groundwork for competitive matchmaking.
02. Real-Time Multiplayer Engineering
The core challenge was keeping every connected player's view of the game consistent and cheat-proof without a client ever holding authority over the outcome.
- WebSocket Protocol Design: Designed a custom protocol split into client-sent "intents" (
ROLL_REQUEST, SCORE_REQUEST, etc.) and server-broadcast "facts" (DICE_ROLLED, TURN_CHANGED, etc.), with every intent re-validated against the current database state before being applied. Invalid actions are rejected with an explicit error, never silently ignored.
- Per-Recipient Score Masking: Built a mechanism to keep opponents' scorecards hidden until game over using a single broadcast payload per scoring event. Each Channels consumer independently decides whether to null out the score fields for its own connected player, rather than maintaining separate broadcast logic per viewer.
- Reconnection Handling: Implemented a 15-second reconnect grace period backed by an idempotent async task, so a dropped connection can rejoin mid-game without losing its seat, while a player who doesn't return in time is automatically skipped without stalling the rest of the match.
- Channel Layer, Not a Cache: Used Redis strictly as the Channels pub/sub transport for fanning messages out to every consumer in a room. Game state itself lives in a single PostgreSQL row read and written directly by the consumer, a deliberate simplification for a traffic pattern that doesn't need a caching layer.
03. Game Engine & Frontend
The frontend had to support two very different data sources, local state and a live server, behind one identical UI.
- Dual-Mode Engine: Authored a pure-function scoring and turn engine in TypeScript for local play, and a matching Python port enforcing the same rules server-side for online play, keeping upper-section bonuses, Yahtzee bonus stacking, and category validation identical across both.
- Mode-Aware State Management: Built a single Zustand store where local actions mutate state directly through the engine, and online actions dispatch WebSocket intents and reduce incoming server events back into that exact same shape, so every UI component works unmodified in both modes.
- Custom 3D Dice: Designed and animated the dice as real CSS 3D objects (
transform-style: preserve-3d, per-face rotateX/rotateY/translateZ), driven by requestAnimationFrame. Deliberately avoided a physics engine or WebGL renderer for a turn-based game that doesn't need one.
04. Data Model & Ranked Groundwork
Beyond gameplay, the project includes a schema and rating system designed to support competitive play without needing to be re-architected later.
- Unified Schema: Modeled all three game modes (local, online, ranked) through a single
GameSession/GamePlayer schema in PostgreSQL, with category scores as individually typed columns for database-level type enforcement over a looser JSON blob.
- Multiplayer Elo Rating: Implemented an Elo system generalized from head-to-head to N-player free-for-all matches: each player's result is their average pairwise outcome against every opponent, weighted against the average expected score their rating implied, with a higher K-factor during placement matches for faster convergence.
- Deployment-Ready Packaging: Built the app as an installable PWA and containerized the backend, with a single Docker image serving both REST and WebSocket traffic through one Daphne (ASGI) process.