# Showdown — room protocol Contract between the frontend and the backend (Rust, in `backend/`). The design goal is **no database**: each room lives entirely in server memory and disappears when its last player disconnects. ## Transport - One WebSocket per player: `GET /ws/room/{roomId}` (upgrade). - All frames are JSON text messages. - The server serves the built frontend (`frontend/dist`) for every other route, so deep links like `/room/lucky-coyote-07` load the SPA. ## Room lifecycle - A room is created in memory the first time someone connects to its id. - Presence **is** the connection: when a socket closes, that player is removed and a new state snapshot is broadcast (this is why friends vanish when they close the tab). - When the last socket closes, the room is deleted. Watchers count as connections (a TV keeps the room alive) but never as players. - `playerId` is generated by the browser and persisted in localStorage. A reconnect with the same `playerId` replaces the old seat (refresh keeps your identity and your vote). ## Client → server messages ```jsonc { "type": "join", "playerId": "uuid", "name": "Billy", "emoji": "🤠" } // first message on every (re)connect { "type": "watch" } // alternative first message: spectator (TV display) { "type": "vote", "value": "5" } // value: deck card, or null to retract { "type": "reveal" } // anyone may reveal; ignored if no votes yet { "type": "reset" } // anyone may start a new round { "type": "deck", "cards": ["1","2","3"] } // anyone may swap the deck; implies reset ``` A socket that opens with `watch` instead of `join` is a **watcher**: it receives every state snapshot but never appears in `players`, and any other message it sends is ignored. The frontend's TV view (`/room/{id}/tv`) connects this way. ## Server → client messages The server replies to **every** change (join, leave, vote, reveal, reset, deck) by broadcasting one full-state snapshot to every socket in the room. Snapshots are small (≤ ~16 players), so no deltas are needed. ```jsonc { "type": "state", "state": { "roomId": "lucky-coyote-07", "deck": ["0","1","2","3","5","8","13","21","34","?","☕"], "revealed": false, "round": 3, "players": [ { "id": "uuid", "name": "Billy", "emoji": "🤠", "voted": true, "vote": null } ] } } ``` Rules the server must enforce: - `vote` is hidden (`null`) in snapshots until `revealed` is true — only `voted` leaks before the showdown. - Votes are rejected while `revealed` is true. - `reset` and `deck` increment `round`, clear all votes and set `revealed = false`. Clients use the `round` counter to clear local UI state. - `reveal` with zero votes is a no-op. ## Server shape (implemented in `backend/`) All rooms in one `Mutex>`. Each socket is a single async task: it registers (join/watch), subscribes to the room's broadcast channel, then loops — relaying snapshots out and applying client messages in. Disconnect cleanup removes the seat (and the room, when its socket count hits zero). No persistence anywhere.