3.1 KiB
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-07load 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.
playerIdis generated by the browser and persisted in localStorage. A reconnect with the sameplayerIdreplaces the old seat (refresh keeps your identity and your vote).
Client → server messages
{ "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.
{
"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:
voteis hidden (null) in snapshots untilrevealedis true — onlyvotedleaks before the showdown.- Votes are rejected while
revealedis true. resetanddeckincrementround, clear all votes and setrevealed = false. Clients use theroundcounter to clear local UI state.revealwith zero votes is a no-op.
Server shape (implemented in backend/)
All rooms in one Mutex<HashMap<roomId, Room>>. 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.