The server owns the game
Every action is checked for turn, phase, path and card ownership, inside a database transaction that locks the game's state, so two simultaneous requests can't both act on one turn.
// Personal Project · Game · Case Study
A real-time multiplayer murder-mystery game for the browser. A snowstorm has trapped six guests in a mountain lodge, the host is dead, and one sealed envelope holds the answer. The rules all live on the server, every player sees only what they're allowed to, and a dropped connection never loses a clue.
Work in progress. The engineering is the stable part. The visuals, cards and characters are still changing, and the card art in these screenshots is placeholder.
A secret envelope holds one suspect, one weapon and one room, and the rest of the 21 cards are dealt to the players. On your turn you roll two dice, walk the lodge's tile grid and, inside a room, suggest a suspect and weapon. The next player holding one of those cards must privately show you one. When you think you know the answer you make a final accusation: get it right and you win, get it wrong and you're out, but you still have to answer everyone else's suggestions.
Around the core game there's a scripted tutorial against two bot guests, an animated story intro, lobby and in-game chat, a detective's notebook that ticks off the cards you've seen, badges and game history, English and Albanian translations, an accessibility pass and a mobile layout.
Hidden information is the whole game, so the browser can't be trusted with any of it. The Laravel API owns every rule, and the Vue app only ever sends intentions: roll, move, suggest, show a card, accuse.
Every action is checked for turn, phase, path and card ownership, inside a database transaction that locks the game's state, so two simultaneous requests can't both act on one turn.
The server builds a separate snapshot per player: the public board plus only their hand and the cards shown to them. Other hands and the envelope are encrypted at rest and never leave the server mid-game.
14 event types go out over Laravel Reverb: a presence channel per game and a private channel per player for secret prompts. Clients still re-fetch the snapshot after events, with a polling fallback, so a missed frame never costs anything.
Every phase change schedules a delayed job to auto-end the turn. Each phase gets a fresh token, so a timer left over from an earlier phase sees the mismatch and quietly does nothing.
The card someone showed you used to arrive as a one-off WebSocket message that vanished after a few seconds, so a reload or a dropped connection lost the most important fact in the game. Now every snapshot rebuilds each player's private clues from the game's append-only event log, and the socket only makes things arrive faster.
125 backend tests (424 assertions) cover the full game flow, the turn engine, suggestions, the notebook and the reconnect fix above. On top of that, 21 Playwright tests drive three real browsers through whole games against the real stack: the API, Reverb WebSockets and the queue worker. Dice and the deal are made deterministic by test-only endpoints that are blocked in production. GitHub Actions runs code style and the backend suite, and the end-to-end suite in its own workflow.
Fonts: Playfair Display and Crimson Pro (SIL Open Font License). Icons: Tabler Icons (MIT). Dice sound from Pixabay; the other sound effects are generated in the browser.