feat(online): make a room a lobby with an owner and a ready-up

A room used to be a wrapper around one game: joining started it, and the
room died with it unless both players accepted a rematch inside thirty
seconds. It is now a lobby that outlives its games.

Whoever created the room owns it and the other seat is the guest. The guest
readies and the owner starts; the owner has no readiness of their own,
because starting is the same statement. A finished game returns both to the
lobby, where the next one is agreed exactly as the last was — the readiness
that started a game is spent with it.

A guest takes their readiness back before leaving, which is deliberate
friction: a player the owner is waiting on should have to say so before
walking away. The owner can free the seat of a guest who is not ready, and
not of one who is — readiness is a commitment, not an inconvenience. An
owner who leaves hands the room to whoever is left, unreadied, because they
are the one who starts now.

Something has to bound a room that outlives its games: the last player out
closes it, as does ten minutes in a lobby nobody started a game in. A
dropped connection is still not a player leaving — the seat is held for the
reconnect window in the lobby as well as mid-game, so a refresh no longer
costs somebody their room, and a resume lands in the lobby it left.

The rematch handshake is retired, and RoomCreated and RoomJoined go with it.
All three described part of what RoomState now describes in full, and three
messages for one lobby is three ways for a client to hold a view of it the
server never had. One snapshot per recipient, broadcast from the one place
that knows an input is finished, so no handler can forget to send it.
This commit is contained in:
tiennm99 committed 2026-09-07 16:32:14 +07:00
1 parent ef638e52fd
commit d481a093ec
39 files changed
+2352 -1215

No files matched your search

+16 -4
View File
@@ -52,14 +52,26 @@ rather than against each other's assumptions. Regenerate the fixtures with
A player creates a room and gets a six-character code and an invite link. The
alphabet omits `0`/`O` and `1`/`I`/`L`, because these codes get read aloud. The
other player types the code or opens the link, which joins on arrival.
other player types the code or opens the link, which seats them in the room's
lobby.
The room is a lobby that outlives its games. Whoever created it owns it; the
other seat is the guest. The guest readies, the owner starts — the owner has no
readiness of their own, because starting is the same statement — and a finished
game returns both to the lobby, where the next one is agreed the same way. A
guest must take their readiness back before leaving, and the owner can free the
seat of a guest who is not ready. An owner who leaves hands the room to whoever
is left, and the last player out closes it, as does ten minutes with no game
started.
The whole lobby travels as one `RoomState` per recipient — roles, readiness,
presence — so a client that missed a frame is correct again from the next one
rather than from a stream of deltas it has to replay.
Both players see the other's server-sanitized nickname, never the raw input. A
disconnect holds the seat for a grace window and shows the opponent a countdown;
a return inside it resumes the same position, rebuilt from the engine rather
than from a recorded stream. When a game ends, either player may ask for a
rematch and the room restarts with a new opening word once both agree. Leaving
is how a rematch is declined — there is no separate message for it.
than from a recorded stream, or the lobby when no game is running.
### The frontend