v0.5.1 — multiplayer Phase 4: lobby, sessions, reconnection
A real server existed since v0.5.0 but nobody could reach it without a hand-built ?seat=&secret= URL. This is what makes it a game you can actually create or join. The server now hosts more than one game: src/server/lobby.ts (new) is pure logic — creating, joining, bot seats, host transfer, starting — same split session.ts already draws for a running game. persistence.ts gained one directory per gameId plus a top-level index so index.ts resumes every saved game on boot. /api/stream and /api/intent now authenticate by session token instead of ?seat=&secret= — the token alone proves identity (lobby-and-sessions.md §1), so the join secret's job ends at the lobby door. Bots fill empty seats at Lobby.Start only, never take over a disconnected human (D8): session.ts gained driveBots(), playing developerBot forward through consecutive bot seats after every accepted intent. Disconnect keeps the seat and says so — Push gained an optional presence field, built entirely by http.ts and never routed through the engine, since a disconnect is transport news, not a GameEvent. Host rights pass to the earliest-joined remaining player if the host drops before start. Client: src/web/lobby.ts adds create/join forms and a live seating screen; localStorage replaces ?seat= for reconnecting straight back into a game already joined. A Multiplayer button sits beside New game; the New Game dialog itself is untouched. Found only by the live smoke test, not by typechecking: /api/intent read its token from the JSON body while the client sends it in the query string (matching /api/stream) — every intent failed "no such game" until caught by curl-level verification. Doc fix: multiplayer.md's D18 said the player cap was 6; lobby-and-sessions.md §2 says 2-4 with the reasoning and the test coverage to back it. The two had drifted apart. D18 now reads 2-4. Not verified: an actual browser walking through the lobby screens — none available in this environment, same limitation Phase 2's RemoteSession shipped under. 656 tests, 0 failures. tools/jitsi-harness/ deliberately left untracked — unrelated side-project work, not part of this release.
This commit is contained in:
@@ -19,6 +19,72 @@ page as `v0.1.0 · <sha> · <date>`, so what is deployed can always be identifie
|
||||
|
||||
---
|
||||
|
||||
## 0.5.1 — 2026-08-21
|
||||
|
||||
Multiplayer Phase 4 — lobby, sessions, reconnection (`docs/architecture/multiplayer.md` §12 steps
|
||||
17-20, fully specified in `docs/architecture/lobby-and-sessions.md`). Phases 0-3 shipped in v0.4.0
|
||||
and v0.5.0; a real server existed but nobody could reach it without a hand-built URL. This is what
|
||||
makes it a game you can actually create or join.
|
||||
|
||||
### The server hosts more than one game, and knows who you are across a reconnect
|
||||
|
||||
`src/server/lobby.ts` is new: pure logic, no sockets, no filesystem, the same split `session.ts`
|
||||
draws for a running game. `createLobby`/`joinLobby`/`setBotSeat`/`reassignHost`/`startLobby`, plus a
|
||||
speakable game code (`RAIL-4471` style) and the player cap.
|
||||
|
||||
`persistence.ts` gained one directory per `gameId` and a top-level index, so `index.ts` resumes
|
||||
every saved game on boot, not just one. `/api/stream` and `/api/intent` now authenticate by session
|
||||
**token** instead of `?seat=&secret=` — `lobby-and-sessions.md` §1: the token alone proves identity,
|
||||
so the join secret's job ends at the lobby door (`/api/lobby/create`/`/api/lobby/join`).
|
||||
|
||||
### Bots fill empty seats, never take over a disconnected human (D8)
|
||||
|
||||
`session.ts` gained `driveBots()` — after any accepted intent, and once at construction for a
|
||||
resume that lands exactly on a bot's turn, it plays `developerBot` forward through every
|
||||
consecutive bot seat before the push goes out. Reuses `legalActions`/`developerBot` wholesale.
|
||||
`SavedGame` gained `botSeats` so a bot seat survives a restart. Bots are assigned once, at
|
||||
`Lobby.Start`, and never afterward — a disconnected human's seat waits, exactly as before.
|
||||
|
||||
### Disconnect keeps the seat and says so; reconnect gets a full view, not a tail
|
||||
|
||||
`Push` gained an optional `presence` field — connection news about another seat, built entirely by
|
||||
`http.ts` (which owns the connection table) and never routed through `session.ts` or the engine: a
|
||||
disconnect is transport news, not a `GameEvent`, and the engine must stay replayable from a seed.
|
||||
The page shows a small banner naming who has dropped and clears it the moment they reconnect.
|
||||
Host rights pass to the earliest-joined remaining player if the host's own connection drops before
|
||||
`Lobby.Start` — tracked by join order rather than seat, since a bot-filled seat never joined at all.
|
||||
|
||||
### The client: an actual lobby, not a URL you hand-build
|
||||
|
||||
`src/web/lobby.ts`, wired from `main.ts`: create-or-join forms, a live seating screen (host-only bot
|
||||
toggles and Start button, updated over its own SSE stream), and `localStorage` in place of `?seat=`
|
||||
for "was I already in a game" — found on load, it reconnects straight through and skips the lobby
|
||||
screen entirely. A `Multiplayer` button sits beside `New game`; the New Game dialog itself is
|
||||
untouched and still solitaire-only, its old "needs a server" note repointed at the new button.
|
||||
|
||||
### Found only by the live smoke test, not by typechecking
|
||||
|
||||
`/api/intent` read its token from the JSON body; `web/session.ts`'s `submit()` — unchanged since
|
||||
Phase 2 — sends it in the query string, the same as `/api/stream`. Every intent failed `no such
|
||||
game`. Both sides typecheck cleanly on their own (an HTTP body is `unknown` on the wire), which is
|
||||
exactly the gap a curl-level smoke test exists to catch: create a lobby, join a second player,
|
||||
start, submit from both seats (including a wrong-actor rejection and an idempotent resend),
|
||||
kill and restart the server and reconnect both tokens, start a bot-filled coop lobby and confirm it
|
||||
never stalls waiting on the bot, and watch a live stream receive a disconnect/reconnect presence
|
||||
notice for another seat.
|
||||
|
||||
### Doc fix
|
||||
|
||||
`multiplayer.md`'s decision table (D18) said the player cap was 6; `lobby-and-sessions.md` §2 —
|
||||
more detailed, and what `test/multiplayer.test.ts` actually exercises — says 2-4 with the reasoning
|
||||
for it. The two had quietly drifted apart; "6" was never implemented or tested anywhere. D18 now
|
||||
reads 2-4.
|
||||
|
||||
**Not verified: an actual browser** walking through the lobby screens — none is available in this
|
||||
environment, the same limitation Phase 2's `RemoteSession` shipped under. 656 tests, 0 failures.
|
||||
|
||||
---
|
||||
|
||||
## 0.5.0 — 2026-08-21
|
||||
|
||||
Multiplayer Phases 2 and 3 (`docs/architecture/multiplayer.md` §12): a real server exists now, one
|
||||
|
||||
Reference in New Issue
Block a user