- Solo theme choice moves out of the settings menu into its own
post-Play modal; Play Again reopens it for chosen-theme rounds and
instantly rerolls (never repeating the last theme) for random rounds.
- Last solo/bot theme played persists in localStorage so a fresh
random pick or page reload doesn't immediately repeat it.
- Result modal swaps Change Theme for a Back to Menu button for as
long as a solo/bot round is active.
- Darkened the opponent-progress fill color (renderer.ts).
- Bump time modes from 1/2 to 2/4 minutes across backend protocol, bot,
timer HUD, and net message types (more room to read a snippet before
guessing).
- Extract per-category token counts/weights into a shared
engine/categoryStats.ts, replacing three independently-drifting
copies of the same formula in ThemeGuessGame/botMatch/multiplayerMatch.
- Switch scoring.ts from a plain weighted average to a power-mean
combine so whiffing a high-weight category can't be masked by acing
the rest; retune colorUtils' similarity falloff divisor to match.
- Update solo config flow, opponent view, and renderer for the new time
options and scoring feedback.
ThemeGuessGame's constructor permanently binds click/mousemove/document
listeners onto the single shared #code-canvas/#category-panel. Solo,
matchmaking, and private-room flows each independently constructed their
own instance, so using more than one mode in the same page load (e.g.
Solo then Find Match) left multiple instances double-bound to the same
DOM, corrupting whichever mode was used first.
Add game/sharedGame.ts: a single lazily-constructed instance, reused via
setTheme/setSnippet and now-mutable onCategoryAssigned across every
flow. main.ts, matchmakingFlow.ts, and privateRoomFlow.ts now call
getSharedGame() instead of `new ThemeGuessGame(...)`.
Verified via CDP DOMDebugger.getEventListeners against a live session:
exactly one click/mousemove listener on #code-canvas after playing a
Solo round followed by a real matched Matchmaking round in the same
page load (previously would have been two of each).
- frontend/src/ui/privateRoomFlow.ts: create-room sub-flow (time/theme
mode form, large shareable code, copy-to-clipboard with graceful
no-op fallback, waiting-for-opponent state) and enter-code sub-flow
(numeric input + inline room:error retry, no dead end). Handles
room:joined for both themeMode:'random' (straight into the live
match) and 'chosen' (vote-wait placeholder, TODO(#16) for the real
vote modal). Listens for round:nextMatch to show a brief 'Match N of
5' transition, and room:closed to show the reason and return to the
main menu. A room with no active session yet (waiting alone, or
mid-vote) is abandoned via a socket disconnect/reconnect cycle, since
player:quit is a no-op server-side without a session to attach to.
- frontend/src/game/multiplayerMatch.ts: MultiplayerMatch now owns
#split-view visibility (shown in startRound, hidden in
teardownRound), mirroring botMatch.ts's existing convention — nothing
previously made the opponent pane visible for a real multiplayer
match.
- frontend/src/net/client.ts: exports a shared `client` GameClient
singleton (connects on import) so private rooms and matchmaking use
one socket instead of each constructing their own.
- frontend/index.html: adds the #private-room-mount overlay container.
- frontend/src/style.css: private-room-* styles for the code display,
join form, and error/status text.
- frontend/src/main.ts: replaces only the enterCodeBtn/createRoomBtn
TODO stubs with real privateRoomFlow wiring; findMatchBtn line
untouched.
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.