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.
230 lines
6.4 KiB
TypeScript
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;
|