From 67e7405252b7335a01ad9f4db45fb0e53993fa16 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 5 Sep 2026 14:18:49 +0700 Subject: [PATCH] docs(journal): record the phase 7 online play session --- ...and-a-room-that-had-to-outlive-its-game.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 plans/journals/2026-09-05-phase-7-online-play-and-a-room-that-had-to-outlive-its-game.md diff --git a/plans/journals/2026-09-05-phase-7-online-play-and-a-room-that-had-to-outlive-its-game.md b/plans/journals/2026-09-05-phase-7-online-play-and-a-room-that-had-to-outlive-its-game.md new file mode 100644 index 0000000..42d3fae --- /dev/null +++ b/plans/journals/2026-09-05-phase-7-online-play-and-a-room-that-had-to-outlive-its-game.md @@ -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.