Commit graph

31 commits

Author SHA1 Message Date
42d1732816 fix(frontend): scope player identity to a tab, not the browser
Two tabs of the same browser share localStorage, so both got the same
persisted playerId and stepped on each other: matchmaking's waiting
queue is keyed by playerId and dedupes a second queue:join as a no-op,
so two tabs searching never both count as waiting; and re-identifying
with the same playerId hijacks the ws registry's socket binding, tearing
down the first tab's connection (surfacing as room:join replying 'room
already has two players' once its creator's socket is dead).

Move playerId/name from localStorage to sessionStorage: still stable
across reloads within a tab (reconnect-by-refresh keeps the same
identity), but each new tab gets a fresh UUID, so two tabs of one
browser behave like two separate players. Not IP-related; no such
restriction existed.
2026-09-13 19:52:32 -03:00
16d36328ec fix(frontend): drop Change Theme button, stop pre-selecting last theme
- Theme-picker modal always opens with a fixed default highlighted
  card instead of the last-played theme.
- Removed the change-theme-btn entirely; Back to Menu is now the only
  result-modal button shown while a solo/bot round is active, and does
  what Change Theme used to (go to the main menu) plus solo cleanup.
2026-09-13 19:35:37 -03:00
6c01d7e4e0 feat(frontend): standalone theme-picker modal, offline anti-repeat, dimmer opponent green
- 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).
2026-09-13 18:07:04 -03:00
892235cdaf fix(frontend): extend theme-preview window to 10s and raise SFX volume 2026-09-13 17:42:45 -03:00
beccfacc22 docs: require agents to commit all changes before finishing a task 2026-09-13 17:36:52 -03:00
cbc4d8a2eb feat(frontend): rebalance time modes to 2/4 min and rework scoring weights
- 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.
2026-09-13 17:36:41 -03:00
36b2725498 fix(frontend): share one ThemeGuessGame instance across solo/matchmaking/rooms
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).
2026-09-10 14:31:59 -03:00
bf88a340d6 feat(frontend): add theme-voting modal (countdown, live picks, settle) 2026-09-10 14:23:44 -03:00
f0d08f3b50 feat(frontend): add private room UI flow (create/join/repeat)
- 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.
2026-09-10 14:07:20 -03:00
8bb0c79659 feat(frontend): add matchmaking UI flow (Find Match)
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.
2026-09-10 14:07:00 -03:00
0d1de48841 feat(frontend): wire bot engine into solo-vs-bot split-view match 2026-09-10 12:40:54 -03:00
12423cec1d feat(frontend): add quit button and matchmaking ban UX
- 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.
2026-09-10 12:40:50 -03:00
74a6ab471f feat(backend): add private-room theme-voting flow 2026-09-10 12:40:44 -03:00
5190ec19a3 feat(frontend): add in-match orchestrator and multiplayer scoreboard
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.
2026-09-10 12:16:15 -03:00
a7cc3e7d8c feat(backend): add private rooms with create/join and match repeat
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.
2026-09-10 12:16:13 -03:00
ea003453af feat(backend): add matchmaking queue with random mode and quit ban
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.
2026-09-10 12:15:52 -03:00
611b588db6 chore(backend): add shared theme id catalog for matchmaking/rooms 2026-09-10 12:04:33 -03:00
30a2d9f3dd feat(frontend): add in-match timer HUD with countdown and time warning 2026-09-10 12:03:42 -03:00
4b8c402e43 feat(frontend): add in-match split-view opponent progress pane 2026-09-10 12:03:42 -03:00
fea76e94cc feat(backend): add match/round session engine 2026-09-10 12:03:42 -03:00
5162f090cb feat(frontend): add solo mode config flow (time/theme/opponent) 2026-09-10 11:54:48 -03:00
906fe59add feat(frontend): add WebSocket client service with identity and typed messages 2026-09-10 11:54:48 -03:00
66db5b1931 feat(backend): add WS connection lifecycle and player identity registry 2026-09-10 11:54:48 -03:00
0047da5aab feat(frontend): add bot AI engine for pacing and accuracy 2026-09-10 11:45:28 -03:00
18cf6a00ca feat(frontend): restructure main menu into Solo/Private/Matchmaking
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.
2026-09-10 11:45:28 -03:00
d51113469f feat(frontend): expand code snippet catalog for random selection 2026-09-10 11:45:28 -03:00
629f52caa3 docs(backend): add multiplayer protocol spec 2026-09-10 11:45:28 -03:00
a58b1512fc docs: rename trunk branch references from master to main 2026-09-10 10:41:42 -03:00
970206fb99 feat(backend): scaffold matchmaking server project
Bare HTTP + WebSocket bootstrap (health check route, ws upgrade,
connection logging) with matchmaking/ and rooms/ reserved but
unimplemented. No matchmaking logic yet.

Also adds root README.md (project overview) and AGENTS.md (architecture
map, code style, git workflow).
2026-09-10 10:37:52 -03:00
6f9a298aef refactor(frontend): move to frontend/, port to Vite + TypeScript
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.
2026-09-10 10:37:44 -03:00
467447b51a initial commit (first version of game) 2026-09-10 10:15:02 -03:00