Town of Salem is a social deduction game. Players are secretly assigned roles — Town, Mafia, or Neutral — and take turns investigating, accusing, and eliminating each other. The Town wins by finding and executing all Mafia members. The Mafia wins by reaching parity with the Town. Neutrals have their own win conditions.
We rebuilt it from scratch in 3D. The hard part wasn’t the graphics. It was the information architecture.
The information problem
In most multiplayer games, every player sees the same game state. Chess, card games with open hands, trivia — the state is public. You can send the full game state to every client and render it.
Social deduction games break this assumption. Every player has a different view of the same game. The Mafia knows who the other Mafia members are. The Town doesn’t. The Investigator learns one player’s role per night. The Godfather appears innocent to investigations.
This means the server can never send the full game state to any client. Every message to every player must be filtered through their role’s information access rules.
State machines with access control
We model the game as a state machine where each phase (night, day discussion, voting, defense, judgment, execution, role reveal) is a state with defined transitions. The state machine is deterministic — given the current state and an action, the next state is always the same.
The interesting layer is on top: an access control function that takes the full game state and a player ID, and returns the subset of state that player is allowed to see. This function encodes all the hidden information rules:
- Mafia members can see each other’s roles
- The Investigator sees investigation results
- Dead players see everything (they’re spectating)
- The Jailor sees who they’ve jailed
- Nobody sees the Godfather’s true role through investigation
The server runs the full state machine. Clients receive filtered views. Actions are validated against the full state but submitted through the filtered view.
What we learned about hidden information
Role reveal is a UI problem, not a logic problem. When a player dies, their role is revealed. This is a simple state transition in the logic — flip isAlive to false and add the player to the revealed list. But in the UI, this moment needs ceremony: a pause, an animation, atmosphere. The gap between “state change” and “player experience” is widest at role reveal.
Night actions are concurrent, not sequential. Multiple roles act simultaneously at night: the Mafia chooses a target, the Doctor chooses someone to protect, the Investigator chooses someone to investigate. These are simultaneous submissions that resolve together when the night phase ends. This is the same pattern as GameStack’s SubmitTracker — collect all submissions, then resolve.
Dead players are the best spectators. Once you die, you can see everything. This turns death from a negative experience (“I’m out of the game”) into an information experience (“now I know everything and can watch the drama unfold”). The spectator mode we built for Town of Salem became a platform feature we brought back to GameStack.
The game is live at town-of-salem-3d.pages.dev. The server runs on a single Cloudflare Durable Object.