mirror of
https://github.com/ThisTine/Showdown.git
synced 2026-08-18 23:18:48 +07:00
79 lines
3.1 KiB
Markdown
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.
|