← Back to projects

// Personal Project · Game · Case Study

A Blackpeak Lodge game in progress: the lodge's rooms on a tile board, the reachable tiles lit up after a roll of seven, the turn timer, dice and a privately shown clue

Blackpeak Lodge

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.

Role
Design & development
Type
Multiplayer browser game
Stack
Laravel 13 · Vue 3 · Reverb
Status
In development

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.

01. How it plays

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.

A lodge waiting room: three guests connected, a suspect picker, a room code to invite friends, and a live chat
Guests gather in a lodge, pick a suspect and chat until the host starts the evening.

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.

02. How it's built

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.

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.

Each player sees their own game

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.

Real time, with a safety net

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.

Timers as queued jobs

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.

Two players' screens at the same moment: Fox waits while Owl is privately asked to show one of three cards
The same moment on two screens: Fox waits while Owl, and only Owl, is asked to show a card.

03. What I didn't see coming

Clues lost on a reload

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.

The same game before and after a hard page reload: board, hand, dice, timer and the private clue all come back
Before and after a hard reload mid-turn: board, hand, phase, timer and the private clue all come back from the server.

04. Tested end to end

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.

Terminal output: 125 PHPUnit tests passing on the left, 21 Playwright end-to-end tests passing on the right
Real output from both suites: 125 backend tests in about 5 seconds, 21 end-to-end tests in 3.5 minutes.

05. Where it stands

  • Playable end to end on my machine: from the lobby through suggestions and accusations to a winner.
  • Not deployed yet. Production setup for Reverb, Redis and Octane is the last open item before it goes public.
  • Still being designed: the card illustrations, the deck size and possibly a larger cast of suspects.
End of a game: the envelope opened, showing the suspect, room and weapon, and the player who solved it
The envelope is opened and the mystery is solved.

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.