docs(journal): record the phase 7 online play session

This commit is contained in:
tiennm99 committed 2026-09-05 14:18:49 +07:00
1 parent b2cad42b0c
commit 67e7405252
1 file changed
+70
@@ -0,0 +1,70 @@
---
title: "Phase 7: online play, and a room that had to outlive its game"
date: 2026-09-05
summary: "Shipped online 1v1, browser E2E and the container image; review found two defects caused by keeping a room alive past game over, plus a pre-existing room leak."
---
# Phase 7: online play, and a room that had to outlive its game
## What happened
Phase 7 in one pass: a rematch protocol, online 1v1 in the browser, 21 Playwright
specs, a container image, CI, and deployment docs.
The one real design change was on the server. Rooms used to die the instant
`engine.Over()` was true, so a rematch needed the room to survive its own game.
It does now, but only while both seats hold live connections — a bot room still
closes immediately, because there is nothing to negotiate with a bot and keeping
it alive leaks a goroutine and an engine per finished game.
## What that new state broke
Two of the three defects review found were direct consequences of it:
- **A game ending on the turn clock never offered a rematch.** The offer was
opened from the message arm of the room loop, and the timer branch `return`ed
before reaching it. Resign and no-legal-move endings worked; the most common
natural ending did not, and the UI still showed the button. The end-of-game
decision now sits after the whole `select`.
- **A second connection with the same resume token killed the room.**
`resumeFrom` retired the old connection as soon as `r.send` succeeded, but the
room decides asynchronously and refuses a resume into a finished game — which
is exactly the rematch window. A duplicated tab was enough. The room now
retires the old connection only once it has agreed to the swap.
The third predated the phase: `attach` overwrote the session's room pointer and
nothing told the old room, so one connection creating four rooms stranded three
of them forever. Fixed here because this is the phase that puts the server on
the internet.
## What building the browser suite found
Four more, none of which the unit tests could reach: a refreshed tab could not
rejoin its game, a restored opponent never cleared the other player's disconnect
banner, a socket that failed without closing was invisible to the client, and
the nickname typed on the online screen never reached the server because the
handshake had already gone out.
## Decision
Two testing tools do not work here, and both are now documented in
`e2e/socket-cut.js` rather than left to be rediscovered. Offline emulation does
not cut a loopback socket. A routed socket's close does not reliably reach the
server, so the grace window never starts — those tests take the page away
instead.
## A duplicate I wrote and removed
I added a bot difficulty-ladder test to satisfy a plan criterion, then found
`simulate_test.go` had covered it since phase 3 with better thresholds and a
real corpus variant. I had grepped one file and concluded the coverage was
missing. Deleted mine.
## Next steps
- CI has never run on GitHub; every step passes locally.
- The Dockerfile's upstream-downloading branch is unexercised — only the fixture
branch has been built.
> Historical work record — not durable authority. Prefer docs/specs/ADRs for current decisions.