Files
noitu/plans/journals/2026-09-05-phase-7-online-play-and-a-room-that-had-to-outlive-its-game.md
T

3.2 KiB

title, date, summary
title date summary
Phase 7: online play, and a room that had to outlive its game 2026-09-05 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 returned 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.