Files
2026-06-12 21:55:03 +07:00

79 lines
3.1 KiB
Markdown

# 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<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.