theme_guess/frontend/src/net/messages.ts
Gabriel Franco 7a6df57ad0 fix(frontend,backend): online scoring bug, mutual reveal vote, theme preview timing
Scoring bug (priority fix): MultiplayerMatch/BotMatch add a `hidden`
class to the solo result panel (#solo-score-meter-wrap, #score-verdict,
#score-breakdown, #compare-toggle) once a networked/bot round's real
two-column scoreboard takes over, but no CSS rule ever paired `.hidden`
with those selectors (every other hideable element in this codebase has
its own scoped rule) -- the class was inert, so the solo panel stayed
visible forever, stacked above the mp-scoreboard, making both look like
they showed the same/wrong numbers. Added the missing scoped rules.

Reveal button (online play): clicking Reveal Match used to run
ThemeGuessGame's own instant local reveal() even in a live multiplayer
round -- unrelated to the server, and the direct trigger of the above
scoring bug. Added a revealOverride hook on ThemeGuessGame; while a
multiplayer round is live, MultiplayerMatch routes the click through a
new round:revealVote message instead. The round now only ends early
once *both* players vote (server tracks votes per session and reveals
immediately on a mutual vote); a single vote relays round:revealVoteUpdate
to the opponent, and the voter sees "Waiting for opponent to vote for
reveal..." below the button.

Theme preview timing: matchmaking and private-room rounds never ran the
10s theme-preview flash solo/bot rounds already have, so a round's real
colors never appeared before painting started. Added it to both flows
(ui/matchmakingFlow.ts, ui/privateRoomFlow.ts, including every match in
a private room's up-to-5-match series, not just the first). The
backend's round:start.endsAt now bakes in a matching 10s PREVIEW_MS
head start (session.js) so the preview never eats into actual painting
time.

Private room results view: a private room's next match used to start
within milliseconds of the previous one's reveal, cutting the result
modal off almost instantly. room.js now delays round:nextMatch (and the
series-ending room:closed) by RESULT_VIEW_MS (10s) after round:reveal so
both players actually get to look at a match's result.

Theme grid / vote UI: the theme-vote modal's grid was missing the
theme-grid class that actually applies the 2-column CSS grid (dead CSS
already anticipated the combination), so vote theme cards rendered in
whatever the browser's default block flow was. Added the class, and
added a distinct (pink, dashed outline) opponent-pick highlight to
ui/themeGrid.ts so the opponent's live vote pick is visually distinct
from the local player's own selection, keeping the existing 'Opponent
chose <theme>' text.

PROTOCOL.md updated for all wire-visible changes: endsAt formula,
round:revealVote/round:revealVoteUpdate, and the round:nextMatch/
room:closed timing.
2026-09-13 20:36:52 -03:00

230 lines
6.4 KiB
TypeScript

// Hand-maintained TypeScript twin of the WebSocket JSON contract
// described in `backend/src/PROTOCOL.md`. There is no shared
// package/codegen between frontend and backend (see AGENTS.md), so this
// file only exists to give the frontend compile-time safety around
// message shapes — `backend/src/PROTOCOL.md` is the source of truth;
// keep this in sync with it by hand whenever the protocol changes.
import type { CategoryId, ThemeId } from '../types';
export type TimeMode = 2 | 4;
export type ThemeMode = 'random' | 'chosen';
export type RoomErrorReason = 'not_found' | 'full';
export type RoomClosedReason = 'quit' | 'matchLimit' | 'finished';
// -- Identity -----------------------------------------------------------
/** Client -> server, always the first message on a connection. */
export interface IdentifyMessage {
type: 'identify';
playerId: string;
name: string;
}
/** Server -> client, acknowledges `identify`. */
export interface IdentifyAckMessage {
type: 'identify:ack';
}
// -- Round lifecycle (shared by matchmaking and rooms) -------------------
/** Server -> both players, start of a round. */
export interface RoundStartMessage {
type: 'round:start';
snippetIndex: number;
themeId: ThemeId;
timeMode: TimeMode;
endsAt: number;
}
/** Client -> server -> relayed to opponent; fired per category painted.
* Carries only which category changed, never the color value. */
export interface RoundProgressMessage {
type: 'round:progress';
categoryId: CategoryId;
}
/** Server -> both players, fired once, 20s before `endsAt`. */
export interface RoundTimeWarningMessage {
type: 'round:timeWarning';
}
/** Client -> server, a player's final full guess. */
export interface RoundSubmitMessage {
type: 'round:submit';
colors: Record<CategoryId, string>;
}
/** Server -> both players, sent once `endsAt` has passed. Keyed by both
* players' `playerId`; a player who never submitted may be omitted. */
export interface RoundRevealMessage {
type: 'round:reveal';
colors: Record<string, Record<CategoryId, string>>;
}
/** Client -> server, ask to end the round early — see PROTOCOL.md
* "round:revealVote". Carries the same full-guess shape as
* `round:submit` (the Reveal button is only enabled once every category
* is painted); the round only actually ends once both players have
* sent this. */
export interface RoundRevealVoteMessage {
type: 'round:revealVote';
colors: Record<CategoryId, string>;
}
/** Server -> the other player, live notice that this player voted to
* reveal early — lets that client's UI nudge them to do the same. */
export interface RoundRevealVoteUpdateMessage {
type: 'round:revealVoteUpdate';
}
/** Server -> both players, private rooms only: next match in the series
* is about to start. */
export interface RoundNextMatchMessage {
type: 'round:nextMatch';
match: number;
}
// -- Matchmaking ----------------------------------------------------------
/** Client -> server, join the matchmaking queue. */
export interface QueueJoinMessage {
type: 'queue:join';
}
/** Client -> server, leave the matchmaking queue. */
export interface QueueLeaveMessage {
type: 'queue:leave';
}
/** Server -> client, sent instead of queuing while matchmaking-banned. */
export interface QueueBannedMessage {
type: 'queue:banned';
bannedUntil: number;
}
/** Server -> both matched players, transition out of the queue. */
export interface MatchFoundMessage {
type: 'match:found';
opponentName: string;
timeMode: TimeMode;
}
// -- Private rooms ----------------------------------------------------------
/** Client -> server, create a private room. */
export interface RoomCreateMessage {
type: 'room:create';
timeMode: TimeMode;
themeMode: ThemeMode;
}
/** Server -> creator, with the room's shareable code. */
export interface RoomCreatedMessage {
type: 'room:created';
code: number;
}
/** Client -> server, attempt to join an existing room by code. */
export interface RoomJoinMessage {
type: 'room:join';
code: number;
}
/** Server -> both players, once a second player joins a room. */
export interface RoomJoinedMessage {
type: 'room:joined';
code: number;
opponentName: string;
timeMode: TimeMode;
themeMode: ThemeMode;
}
/** Server -> the joining client only, when `room:join` fails. */
export interface RoomErrorMessage {
type: 'room:error';
reason: RoomErrorReason;
}
/** Server -> the remaining player, opponent is gone. */
export interface RoomPlayerLeftMessage {
type: 'room:playerLeft';
}
/** Server -> whichever player(s) are still connected, terminal room
* message. */
export interface RoomClosedMessage {
type: 'room:closed';
reason: RoomClosedReason;
}
// -- Theme voting (private rooms with `themeMode: 'chosen'`) --------------
/** Server -> both players, a 10s countdown to pick a theme. */
export interface VoteStartMessage {
type: 'vote:start';
themeIds: ThemeId[];
endsAt: number;
}
/** Client -> server, cast (or re-cast) a vote. */
export interface VoteCastMessage {
type: 'vote:cast';
themeId: ThemeId;
}
/** Server -> the other player, live relay of an accepted vote. */
export interface VoteOpponentChoiceMessage {
type: 'vote:opponentChoice';
themeId: ThemeId;
}
/** Server -> both players, once voting's `endsAt` elapses. */
export interface VoteSettledMessage {
type: 'vote:settled';
themeId: ThemeId;
agreed: boolean;
}
// -- Quit -------------------------------------------------------------------
/** Client -> server, explicit leave (also synthesized server-side on an
* unexpected socket close). */
export interface PlayerQuitMessage {
type: 'player:quit';
}
// -- Unions -----------------------------------------------------------------
/** Messages this client ever sends to the server. */
export type ClientMessage =
| IdentifyMessage
| QueueJoinMessage
| QueueLeaveMessage
| RoomCreateMessage
| RoomJoinMessage
| RoundProgressMessage
| RoundSubmitMessage
| RoundRevealVoteMessage
| VoteCastMessage
| PlayerQuitMessage;
/** Messages this client ever receives from the server. */
export type ServerMessage =
| IdentifyAckMessage
| QueueBannedMessage
| MatchFoundMessage
| RoundStartMessage
| RoundProgressMessage
| RoundTimeWarningMessage
| RoundRevealMessage
| RoundRevealVoteUpdateMessage
| RoundNextMatchMessage
| RoomCreatedMessage
| RoomJoinedMessage
| RoomErrorMessage
| RoomPlayerLeftMessage
| RoomClosedMessage
| VoteStartMessage
| VoteOpponentChoiceMessage
| VoteSettledMessage;