Wires the Matchmaking menu section's Find Match button to the real
queue:join/queue:leave flow: a searching state with cancel, the
queue:banned countdown (banBanner.ts) with Find Match hidden until it
expires, a brief assigned-mode reveal on match:found, and the handoff
into MultiplayerMatch's two-phase init once round:start is known.
After a match ends (round:reveal + result dismissal, local quit, or
opponent quit via room:closed) the flow returns to the matchmaking
menu without re-queuing.
Also adds a shared GameClient singleton (net/client.ts) so every UI
flow that talks to the server reuses one connection.
Note: backend/src/matchmaking/queue.js actually sends round:start
before match:found (the reverse of PROTOCOL.md's documented order),
discovered via live two-tab testing against the real backend;
matchmakingFlow.ts tracks both messages independently so either
arrival order works.
- index.html/style.css: destructive-styled #match-quit-btn in the in-match
HUD row, two-click arm/confirm (no heavyweight modal).
- multiplayerMatch.ts: wires the button (send player:quit, tear down
timer/listeners/opponent pane immediately, no server reply awaited),
adds an optional onQuit constructor callback for a future host
(#14/#15) to route back to the main menu -- nothing mounts
MultiplayerMatch yet, so this ticket's own quit-button wiring is what
exercises it.
- ui/banBanner.ts (new): standalone renderBanBanner(container,
bannedUntil) countdown, ready for #14 to mount into #menu-matchmaking.
Not wired anywhere yet: per session.js/queue.js, quitting a
matchmaking match pushes nothing to the quitter directly -- the only
wire message carrying ban info is queue:banned, sent in reply to a
later queue:join while banned.
Verified against a real backend + real browser tab driving
MultiplayerMatch through an actual matchmaking match (paired against a
raw ws opponent): quit sends player:quit, opponent gets
room:playerLeft + room:closed(reason:quit) with no ban message; a
follow-up queue:join from the quitter correctly receives queue:banned
(bannedUntil ~120s out) and renderBanBanner counts it down live.
Repeated via room:create/room:join (private room): identical
teardown/onQuit behavior, opponent gets zero ban-related messages, and
the quitter's earlier matchmaking ban is left completely unchanged.
New multiplayerMatch.ts (MultiplayerMatch class) drives one round: syncs
ThemeGuessGame/OpponentView to a round:start payload, relays local
category assignments as round:progress, forwards opponent round:progress
to OpponentView, drives timerHud's countdown (submitting the local color
map as round:submit at expiry), and renders a two-column scoreboard on
round:reveal via computeMatchResult.
ThemeGuessGame gains an optional onCategoryAssigned?(id, hex) constructor
callback, invoked as the last step of applyColor.
timerHud's TimerHudOptions gains an optional onExpire?() callback on the
countdown variant, fired once when endsAt passes.
resultView.ts gains renderMultiplayerResult() for the two-column
you/opponent scoreboard + winner banner (equal overall -> explicit draw).
index.html/style.css: additive #mp-scoreboard/#mp-winner-banner/#mp-you-*/
#mp-opponent-* markup and .mp-* styles; existing single-player result
markup untouched aside from adding id="solo-score-meter-wrap" so it can
be toggled off during a multiplayer reveal.
Implements PROTOCOL.md's private-room flow: room:create/room:join
with a collision-checked numeric code, up to a 5-match repeating
series per room via game/session.js, random-theme selection that
excludes the previous match's theme, and room:closed for matchLimit
or quit (rooms never touch matchmaking's ban store). themeMode:
'chosen' gets a narrow resolveChosenTheme() stand-in pending #7's
real theme-voting flow.
Also adds a minimal opponentName lookup to registry.js
(registerSocket's new optional name arg + getPlayerName) since
room:joined needs it, mirroring the same addition landing on #5's
branch.
Registers queue:join/queue:leave handlers that FIFO-pair waiting players,
randomly assign timeMode (1 or 2, uniform) and theme via themeIds.js, and
hand off to game/session.js's createSession. Tracks a 2-minute
matchmaking-only quit ban (in-memory, resets on restart) via onEnd's
'quit' reason, scoped to sessions this module produced. Exports
isBanned() and registerMatchmakingHandlers(), wired into index.js at
boot.
Also extends ws/registry.js with a playerId -> display name store
(registerSocket's new optional third arg, getPlayerName()) so
match:found.opponentName (and future room:joined.opponentName) can be
populated; connectionHandler.js's identify handler now passes name
through.
Replace the single pick-a-theme-and-start intro modal with a three-section
menu (Solo/Private/Matchmaking). Solo keeps the existing gameplay reachable
via a direct Play button; Private (Enter Code/Create Room) and Matchmaking
(Find Match) are stubbed with console.log no-ops pending tickets #17/#15/#14.
main.ts gains a small showView(name) helper that toggles .hidden across the
menu/result overlay screens, keeping it a thin composition root.
Splits the single game.js god-class into layered modules (types, data,
engine, game, preview, ui, main) and converts every source file to
TypeScript, built and served by Vite instead of a plain static server.
No behavior change: verified against the previous JS version via a full
play-through (theme select, preview countdown, coloring, reveal) in a
real browser.