RMHbox โ€” Codebase Reference for Coding Agentsยถ

Maintenance Requirement: This document must be kept up-to-date whenever changes are made to the core design, structure, patterns, or architecture of the RMHbox codebase. Any agent working on RMHbox should verify that this document still accurately reflects the codebase after their changes, and update it if anything has changed.


Table of Contentsยถ

  1. Overview

  2. Architecture Summary

  3. Directory Structure

  4. Technology Stack

  5. Shared Type System

  6. WebSocket Protocol

  7. Server Architecture

  8. Client Architecture

  9. Minigame System

  10. Adding a New Minigame โ€” Checklist

  11. Game Settings System

  12. Spectator System

  13. History & Leaderboard System

  14. Database Models

  15. Theming & Styling

  16. Sound System

  17. Testing Conventions

  18. Code Standards & Maintainability Guidelines


Overviewยถ

RMHbox is a real-time multiplayer party game platform built on Next.js (frontend) and a standalone Socket.io WebSocket server (backend). Players create or join lobbies via room codes, vote on or select minigames, and play together in real time. The platform supports 7 implemented minigames, spectator modes, host controls, match history, leaderboards, and reconnection.

The seven currently implemented minigames are:

  • Rhyme Time โ€” Speed-based vocabulary rhyming game

  • Undercover Agent โ€” Team-based word-association deduction (like Codenames)

  • Category Crash โ€” Brainstorming showdown with peer review. Phases: REVEAL โ†’ INPUT โ†’ PEER_REVIEW โ†’ ROUND_RESULTS. Voting is crash/safe; points come only from unique safe answers (no voting bonuses).

  • Wiki-Race โ€” Navigate Wikipedia from a start article to a target

  • Minimalist Masterpiece โ€” Draw with limited strokes, then art auction. Drawing phase supports auto-save (SAVE_DRAWING) and explicit submit (SUBMIT_DRAWING). Auto-save preserves drawings for reconnection and spectator live updates without locking. Explicit submit locks the drawing and ends the phase early if all players submit.

  • Emoji Cinema โ€” Describe movies with emojis, others guess

  • Wit-War โ€” Battle of wits with head-to-head voting


Architecture Summaryยถ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     Next.js Frontend                         โ”‚
โ”‚  app/rmhbox/          โ€” Pages (landing, lobby, minigames)   โ”‚
โ”‚  components/rmhbox/   โ€” React components                     โ”‚
โ”‚  lib/rmhbox/          โ€” Shared types, store, socket, utils   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                  WebSocket (Socket.io)                        โ”‚
โ”‚            ws://host/rmhbox-ws (port 7676)                   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚               Standalone RMHbox Server                       โ”‚
โ”‚  server/rmhbox/       โ€” Server logic, lobby, game, auth      โ”‚
โ”‚  server/rmhbox/minigames/ โ€” Server-side minigame handlers   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                   PostgreSQL (Prisma)                         โ”‚
โ”‚  RMHboxProfile, RMHboxMatch, RMHboxMatchPlayer              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The client and server are separate processes. The RMHbox WebSocket server runs independently on port 7676 (configurable via RMHBOX_PORT), separate from the main Next.js server. Communication is exclusively via Socket.io WebSocket events.


Directory Structureยถ

Core Platform Filesยถ

lib/rmhbox/
โ”œโ”€โ”€ types.ts                    # Shared type definitions (client + server)
โ”œโ”€โ”€ events.ts                   # WebSocket event name constants (C2S, S2C)
โ”œโ”€โ”€ constants.ts                # Shared tuning constants (timers, scoring, limits)
โ”œโ”€โ”€ schemas.ts                  # Shared Zod validation schemas for event payloads
โ”œโ”€โ”€ store.ts                    # Client-side Zustand store
โ”œโ”€โ”€ socket.ts                   # Client-side Socket.io connection manager
โ”œโ”€โ”€ minigame-client.ts          # Shared hooks/utils for minigame components
โ”œโ”€โ”€ minigame-registry.ts        # Minigame metadata definitions + settings schemas
โ”œโ”€โ”€ game-settings.ts            # Game settings validation & defaults
โ”œโ”€โ”€ audio.ts                    # Sound effect manager (Howler.js)
โ”œโ”€โ”€ toast-store.ts              # Toast notification store
โ”œโ”€โ”€ utils.ts                    # Shared utilities (sanitize, room code gen)
โ”œโ”€โ”€ history-display-registry.ts # History detail component registry
โ”œโ”€โ”€ history-display-registrations.ts # All minigame history registrations
โ””โ”€โ”€ <minigame-id>/              # Per-minigame shared code (schemas, data loaders)
    โ”œโ”€โ”€ schemas.ts
    โ””โ”€โ”€ data-loader.ts
server/rmhbox/
โ”œโ”€โ”€ index.ts                    # Server entry point โ€” bootstraps all services
โ”œโ”€โ”€ config.ts                   # Server configuration (env vars, defaults)
โ”œโ”€โ”€ auth.ts                     # WebSocket auth middleware (Better Auth tokens)
โ”œโ”€โ”€ lobby-manager.ts            # Lobby CRUD, joins, leaves, host controls, GC
โ”œโ”€โ”€ game-coordinator.ts         # Game lifecycle state machine orchestration
โ”œโ”€โ”€ vote-manager.ts             # Minigame voting system
โ”œโ”€โ”€ state-sync.ts               # Heartbeat, phase sync, timer broadcasting
โ”œโ”€โ”€ reconnection.ts             # Player reconnection & duplicate session handling
โ”œโ”€โ”€ chat.ts                     # Chat message handling
โ”œโ”€โ”€ leaderboard.ts              # Match persistence & leaderboard queries
โ”œโ”€โ”€ rate-limit.ts               # Per-socket rate limiting
โ”œโ”€โ”€ schemas.ts                  # Re-exports shared schemas + `validated()` wrapper
โ”œโ”€โ”€ logger.ts                   # Structured JSON logger
โ”œโ”€โ”€ prisma-client.ts            # Prisma client singleton
โ”œโ”€โ”€ types.ts                    # Server-only type definitions
โ””โ”€โ”€ minigames/
    โ”œโ”€โ”€ base-minigame.ts        # Abstract base class for all minigame handlers
    โ””โ”€โ”€ <minigame-id>/
        โ”œโ”€โ”€ index.ts            # Barrel export
        โ”œโ”€โ”€ handler.ts          # Minigame server handler (extends BaseMinigame)
        โ””โ”€โ”€ types.ts            # Minigame-specific server types
components/rmhbox/
โ”œโ”€โ”€ RMHboxShell.tsx             # Theme wrapper + toast container
โ”œโ”€โ”€ RMHboxHeader.tsx            # Shared header with timer ring, settings, controls
โ”œโ”€โ”€ GameShell.tsx               # Game layout wrapper (footer with score/round/players)
โ”œโ”€โ”€ LobbyView.tsx               # WAITING state lobby view
โ”œโ”€โ”€ GameVoting.tsx              # VOTING state minigame selection
โ”œโ”€โ”€ GameSettingsPhase.tsx       # GAME_SETTINGS state host settings
โ”œโ”€โ”€ InstructionsScreen.tsx      # INSTRUCTIONS state game instructions
โ”œโ”€โ”€ PreloadScreen.tsx           # PRELOADING state asset preload progress
โ”œโ”€โ”€ ResultsScreen.tsx           # ROUND_RESULTS state results display
โ”œโ”€โ”€ PlayerList.tsx              # Player list sidebar
โ”œโ”€โ”€ ChatOverlay.tsx             # Chat overlay
โ”œโ”€โ”€ SpectatorBanner.tsx         # Spectator mode banner with player selector
โ”œโ”€โ”€ HostControls.tsx / HostControlModal.tsx  # Host management UI
โ”œโ”€โ”€ GameSettingsForm.tsx / GameSettingsModal.tsx  # Settings configuration UI
โ”œโ”€โ”€ GamePickerModal.tsx         # Minigame selection modal
โ”œโ”€โ”€ LeaderboardPanel.tsx        # Leaderboard display
โ”œโ”€โ”€ MinigameLeaderboardModal.tsx # Per-minigame leaderboard modal
โ”œโ”€โ”€ ReadyButton.tsx             # Ready-up button
โ”œโ”€โ”€ RoomCodeDisplay.tsx         # Room code display with copy
โ”œโ”€โ”€ ToastContainer.tsx          # Toast notification renderer
โ”œโ”€โ”€ SettingsMenu.tsx            # User settings (volume, theme)
โ””โ”€โ”€ minigames/
    โ”œโ”€โ”€ MinigameRenderer.tsx    # Dynamic lazy-loading of minigame components
    โ””โ”€โ”€ <minigame-id>/
        โ”œโ”€โ”€ <MinigameName>Game.tsx        # Main minigame component
        โ”œโ”€โ”€ <MinigameName>HistoryDetail.tsx # History detail view component
        โ””โ”€โ”€ <SubComponent>.tsx             # Game-specific sub-components
app/rmhbox/
โ”œโ”€โ”€ layout.tsx                  # Auth gate + RMHboxShell wrapper
โ”œโ”€โ”€ page.tsx                    # Landing page (create/join/browse/leaderboard)
โ”œโ”€โ”€ rmhbox.css                  # Theme stylesheet (CSS custom properties)
โ”œโ”€โ”€ [lobbyId]/page.tsx          # Lobby page (state machine view renderer)
โ””โ”€โ”€ minigames/
    โ”œโ”€โ”€ page.tsx                # Minigames catalog page
    โ””โ”€โ”€ [minigameId]/history/page.tsx  # Per-minigame match history
app/api/rmhbox/
โ”œโ”€โ”€ history/route.ts            # Match history REST API
โ”œโ”€โ”€ stats/route.ts              # Player stats REST API
โ””โ”€โ”€ leaderboard/route.ts        # Leaderboard REST API
data/rmhbox/<minigame-id>/     # Static JSON data files for minigames
testing/rmhbox/                 # Test suites organized by implementation phase
public/music/rmhbox/sfx/       # Sound effect files

Technology Stackยถ

Layer

Technology

Frontend Framework

Next.js (App Router, React 18+)

State Management

Zustand (with persist middleware for user settings)

Real-time Communication

Socket.io (client + server)

Validation

Zod (shared schemas, both client and server)

Database

PostgreSQL via Prisma ORM

Authentication

Better Auth (session tokens validated against DB)

Styling

Tailwind CSS + CSS custom properties (design tokens)

Sound

Howler.js

Testing

Vitest

Package Manager

pnpm

Language

TypeScript (strict mode)


Shared Type Systemยถ

Types are split between shared (client + server) and server-only:

lib/rmhbox/types.ts โ€” Shared Typesยถ

Key interfaces available to both client and server:

  • LobbyState โ€” Union type of all lobby state machine states: 'WAITING' | 'VOTING' | 'GAME_SETTINGS' | 'INSTRUCTIONS' | 'PRELOADING' | 'COUNTDOWN' | 'PLAYING' | 'ROUND_RESULTS' | 'SESSION_RESULTS' | 'DISBANDED'

  • LobbySettings โ€” Configurable lobby properties (isPublic, maxPlayers, etc.)

  • ClientLobbyState โ€” The complete state snapshot sent to clients (lobby info, players, spectators, current game, chat, match history)

  • ClientPlayerInfo / ClientSpectatorInfo โ€” Player/spectator data for the client

  • ClientGameInfo โ€” Active game state sent to client (minigameId, phase, timeRemaining, publicState, privateState, spectatorMode)

  • GameAction โ€” Sequenced game action with type, payload, seq, timestamp

  • MinigameDefinition โ€” Full minigame metadata (id, displayName, description, category, icon, player limits, settings schema, join-in-progress policy, preload assets, etc.)

  • GameSettingDef / GameSettingsSchema / GameSettingValues โ€” Host-configurable game settings types

  • SpectatorMode โ€” 'shared-privileged' | 'competitive-individual'

  • RoundResultsPayload / PlayerRanking / Award / SessionStanding โ€” Results types

  • VoteCandidate / VoteStartedPayload / VoteCastPayload / VoteResultPayload โ€” Voting types

  • LeaderboardEntry / MatchSummary โ€” Leaderboard and history types

  • RMHboxError / RMHboxErrorCode โ€” Structured error types

  • MinigameCategory โ€” 'word' | 'trivia' | 'action' | 'creative'

  • JoinInProgressPolicy โ€” 'spectate_only' | 'join_next_subround' | 'join_immediately'

server/rmhbox/types.ts โ€” Server-Only Typesยถ

  • RMHboxPlayer โ€” Full server-side player state (includes socketId, joinedAt, lastSeenAt, role)

  • RMHboxSpectator โ€” Full server-side spectator state

  • RMHboxLobby โ€” Complete lobby state (Map-based players/spectators, active game handler, match history, pending settings)

  • ActiveGame โ€” Contains the minigame handler instance, minigameId, and start timestamp

  • ServerMatchSummary โ€” Internal match result tracking

  • MinigameContext / MinigameResults โ€” Re-exported from base-minigame.ts


WebSocket Protocolยถ

Event Naming Conventionยถ

All events use the rmhbox: prefix for namespacing. Events are defined in lib/rmhbox/events.ts as two constant objects:

  • C2S โ€” Client-to-Server events (e.g., rmhbox:lobby:create, rmhbox:game:input)

  • S2C โ€” Server-to-Client events (e.g., rmhbox:lobby:state_snapshot, rmhbox:game:action)

Key Eventsยถ

Lobby Management (C2S):

  • LOBBY_CREATE, LOBBY_JOIN, LOBBY_LEAVE, LOBBY_KICK, LOBBY_TRANSFER_HOST

  • LOBBY_UPDATE_SETTINGS, LOBBY_TOGGLE_READY, LOBBY_CHAT, LOBBY_BROWSE

  • LOBBY_REQUEST_PROMOTION, LOBBY_PROMOTE_SPECTATOR, LOBBY_END_SESSION

Game Flow (C2S):

  • GAME_PICK โ€” Host selects a game (or __vote__ for vote mode)

  • GAME_SELECT โ€” Host selects a game directly

  • GAME_START_VOTE โ€” Host initiates voting

  • GAME_CAST_VOTE โ€” Player votes for a game

  • GAME_FORCE_SKIP โ€” Host skips current phase

  • GAME_FORCE_END โ€” Host force-ends the game

  • GAME_PAUSE_TIMER โ€” Host pauses/resumes timer

  • GAME_READY_TO_RENDER โ€” Client signals preload complete

  • GAME_INPUT โ€” Player sends game-specific input

State Sync (S2C):

  • LOBBY_STATE_SNAPSHOT โ€” Full client state (sent on join, reconnect, heartbeat)

  • GAME_ACTION โ€” Sequenced incremental action (reducer-applied on client)

  • GAME_STATE_SNAPSHOT โ€” Full game-specific state snapshot

  • GAME_INSTRUCTIONS, GAME_PRELOAD_START, GAME_COUNTDOWN, GAME_STARTED

  • GAME_ROUND_RESULTS, GAME_SESSION_RESULTS

  • GAME_VOTE_STARTED, GAME_VOTE_UPDATE, GAME_VOTE_RESULT

  • GAME_SETTINGS_OPENED, GAME_SETTINGS_UPDATED

  • SPECTATOR_TARGET_STATE โ€” Spectator target player info

  • ERROR โ€” Structured error with code and message

Payload Validationยถ

All C2S event payloads are validated using Zod schemas defined in lib/rmhbox/schemas.ts. The server uses the validated() wrapper from server/rmhbox/schemas.ts which combines:

  1. Rate limiting (per-socket, per-event)

  2. Zod schema validation

  3. Error emission on failure

  4. Try-catch wrapping for handler execution

Usage pattern:

socket.on(C2S.SOME_EVENT, validated(socket, C2S.SOME_EVENT, SomeSchema, (s, data) => {
  this.handleSomeEvent(s, data);
}));

State Synchronization Modelยถ

The client receives state via two mechanisms:

  1. Full snapshots (LOBBY_STATE_SNAPSHOT) โ€” Complete ClientLobbyState sent on join, reconnect, and periodic heartbeat during gameplay

  2. Incremental actions (GAME_ACTION) โ€” Sequenced GameAction objects applied by reducer functions in the Zustand store

Actions have a monotonically increasing seq number. The clientโ€™s applyAction() skips any action with seq <= lastSeq to prevent out-of-order duplicates.

The store has two reducer functions:

  • applyLobbyAction() โ€” Handles lobby-level actions (PLAYER_JOINED, STATE_CHANGED, TIMER_TICK, etc.)

  • applyGameAction() โ€” Stores game-specific actions as raw payloads keyed by action type under gameState


Server Architectureยถ

Entry Point (server/rmhbox/index.ts)ยถ

The server creates an HTTP server + Socket.io instance, applies auth middleware, and bootstraps these services:

  • LobbyManager โ€” Lobby CRUD, player management, GC

  • StateSyncService โ€” Heartbeat broadcasting, phase sync, timer broadcasts

  • LeaderboardService โ€” Match persistence, leaderboard queries

  • GameCoordinator โ€” Game lifecycle orchestration

  • VoteManager โ€” Voting system

  • ChatHandler โ€” Chat message handling

  • ReconnectionHandler โ€” Reconnection protocol

Each service implements a handleConnection(socket) method that registers its Socket.io event listeners on the socket.

Lobby Managerยถ

Manages all lobbies in memory using Map<string, RMHboxLobby>. Key features:

  • Room code generation โ€” 6-character codes using a custom alphabet (excludes ambiguous chars)

  • Player/spectator tracking โ€” Map<string, RMHboxPlayer> and Map<string, RMHboxSpectator>

  • Userโ†’Lobby index โ€” Map<string, string> for O(1) userIdโ†’lobbyId lookup

  • Sequence counters โ€” Per-lobby incrementing seq for GameActions

  • Grace timers โ€” For handling temporary disconnects

  • Garbage collection โ€” Periodic cleanup of idle/empty lobbies

  • Join-in-progress โ€” Pending join queue for join_next_subround policy

  • buildClientState() โ€” The ONLY function that constructs ClientLobbyState from internal state, ensuring data masking/scoping

  • broadcastAction() โ€” Emits sequenced GameAction to all lobby members

Game Coordinatorยถ

Orchestrates the game lifecycle state machine:

WAITING โ†’ VOTING โ†’ GAME_SETTINGS โ†’ INSTRUCTIONS โ†’ PRELOADING โ†’ COUNTDOWN โ†’ PLAYING โ†’ ROUND_RESULTS โ†’ WAITING

Key responsibilities:

  • Phase transitions โ€” Drives state transitions with timed phases

  • Minigame instantiation โ€” Uses MINIGAME_SERVER_REGISTRY to create handler instances

  • Input routing โ€” Routes GAME_INPUT to the active handlerโ€™s handleInput()

  • Timer management โ€” Delegates to StateSyncService.startTimerBroadcast() for timed phases, or the minigame handler manages its own timers via BaseMinigame

  • Disconnect handling โ€” Grace timers for in-game disconnects

  • Spectator target tracking โ€” Manages which player each spectator is following

  • Results processing โ€” Calls computeResults() and persists via LeaderboardService

MINIGAME_SERVER_REGISTRY is a Map<string, Constructor> in game-coordinator.ts that maps minigame IDs to their server handler class constructors. New minigames must be registered here.

State Sync Serviceยถ

  • Heartbeat โ€” Periodic full state snapshots to all in-game lobbies (every 10s by default)

  • Phase transition sync โ€” Immediate full sync on state changes

  • Timer broadcasting โ€” startTimerBroadcast() creates a 1-second countdown that emits TIMER_START then TIMER_TICK actions; returns a TimerHandle with cancel/pause/resume

Authenticationยถ

The authMiddleware validates Better Auth session tokens against PostgreSQL on every new connection. It attaches userId, userName, and avatarUrl to socket.data.

Reconnectionยถ

The ReconnectionHandler identifies returning users by userId (not socketId), re-associates their socket with the existing lobby slot, sends a full state snapshot to resync, and handles duplicate sessions (same user, different tab).


Client Architectureยถ

Zustand Store (lib/rmhbox/store.ts)ยถ

The useRMHboxStore is the central state for all RMHbox client-side data:

interface RMHboxStore {
  connectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error';
  lobby: ClientLobbyState | null;
  gameState: Record<string, unknown>;
  lastSeq: number;
  settings: RMHboxUserSettings;      // Persisted to localStorage
  timerInfo: TimerInfo | null;         // Header timer ring state
  minigameRound: MinigameRoundInfo | null;  // Footer round counter
  gameSettingsState: GameSettingsState | null;
  spectatorTarget: SpectatorTargetInfo | null;
  // ... actions
}

Key patterns:

  • Only settings is persisted to localStorage via Zustandโ€™s persist middleware

  • applyAction() enforces sequence ordering via lastSeq

  • applyFullSync() replaces the entire lobby state

  • leaveLobby() clears all lobby/game state

  • Timer and round state is written directly by store actions (not via reducers) for immediate reactivity

Socket Client (lib/rmhbox/socket.ts)ยถ

Module-level singleton socket with these exports:

  • connectToRMHbox() โ€” Creates connection with auth token, sets up global event listeners, returns socket

  • getSocket() โ€” Returns current socket instance (may be null)

  • disconnectFromRMHbox() โ€” Disconnects and resets store

  • emit() โ€” Null-safe emit helper

The socket uses dynamic auth callbacks to refresh the session token on every reconnection attempt.

Minigame Client Utilities (lib/rmhbox/minigame-client.ts)ยถ

Shared hooks and utilities used by all minigame components:

  • emitGameInput(action, data) โ€” Sends game input with lobbyId from store

  • useGameSocket(handlers) โ€” Subscribes to GAME_ACTION + GAME_STATE_SNAPSHOT events, hydrates from store on mount

  • extractTimerTick(data) โ€” Extracts timeRemaining from TIMER_TICK action payload

Page Structureยถ

  • Landing page (app/rmhbox/page.tsx) โ€” Connect, create/join lobby, browse public lobbies, view leaderboard

  • Lobby page (app/rmhbox/[lobbyId]/page.tsx) โ€” State machine renderer that shows the appropriate component based on lobby.state

  • Layout (app/rmhbox/layout.tsx) โ€” Auth gate (redirects unauthenticated users) + RMHboxShell theme wrapper

The lobby page renders different components based on lobby.state:

State

Component

WAITING

LobbyView

VOTING

GameVoting

GAME_SETTINGS

GameSettingsPhase

INSTRUCTIONS

InstructionsScreen

PRELOADING

PreloadScreen

COUNTDOWN

Countdown overlay

PLAYING

GameShell + MinigameRenderer

ROUND_RESULTS

ResultsScreen

SESSION_RESULTS

Session results view


Minigame Systemยถ

Server-Side Handler Patternยถ

Every minigame server handler extends BaseMinigame (in server/rmhbox/minigames/base-minigame.ts).

MinigameContext โ€” Provided to Every Handlerยถ

interface MinigameContext {
  lobbyId: string;
  players: Map<string, RMHboxPlayer>;
  settings: LobbySettings;
  gameSettings: GameSettingValues;           // Host-configured settings
  getHostId: () => string;
  broadcastToLobby: (event, data) => void;   // All members
  broadcastToPlayers: (event, data) => void; // Players only
  broadcastAction: (action) => void;          // Sequenced GameAction
  sendToPlayer: (userId, event, data) => void;
  sendToSpectators: (event, data) => void;
  sendToSpectatorFollowers: (targetPlayerId, event, data) => void;
  onComplete: (results) => void;              // Signal game end
  onError: (error) => void;
}

Abstract Methods to Implementยถ

Method

Description

start()

Called when PLAYING begins. Initialize game state and start game logic.

handleInput(userId, action, data)

Called when a player sends game input. Route by action string.

getStateForPlayer(userId)

Return scoped state for a specific player (reconnection, heartbeat).

getStateForSpectator()

Return omniscient/privileged state for spectators.

computeResults()

Compute final MinigameResults (rankings, awards, gameSpecificData, duration).

get spectatorMode

Return 'shared-privileged' or 'competitive-individual'.

Built-in Helpers from BaseMinigameยถ

  • Timer management:

    • setTimeout(fn, ms) โ€” Tracked timeout, auto-cleaned, pausable

    • clearTrackedTimeout(handle) โ€” Cancel a specific tracked timeout

    • setInterval(fn, ms) โ€” Tracked interval, auto-cleaned

    • startPhaseTimer(durationSeconds) โ€” Broadcasts TIMER_START + TIMER_TICK actions every second

    • clearPhaseTimer() โ€” Stops the phase timer

    • startInfinitePhaseTimer(showSkip?) โ€” Broadcasts infinite timer (no countdown, full ring + โˆž icon)

    • pausePhaseTimer() / resumePhaseTimer() โ€” Pause/resume all tracked timers + broadcast

  • State management:

    • getSetting<T>(key, fallback) โ€” Read host-configured game setting with type-safe fallback

    • broadcastRound(current, total) โ€” Broadcast minigame sub-round info to clients

    • broadcastInitialState(snapshotEvent) โ€” Send per-player scoped initial state to all players

    • broadcastGameAction(data) โ€” Convenience wrapper for broadcasting game actions

    • buildReconnectionSnapshot(userId, isSpectator, targetPlayerId?) โ€” Unified reconnection state

  • Lifecycle:

    • forceEnd(reason) โ€” Force-end with cleanup + partial results

    • cleanup() โ€” Clears all timers/intervals

    • handlePlayerJoin(userId) โ€” Override for join-in-progress support

    • handlePlayerDisconnect(userId) โ€” Override for disconnect handling

    • handlePlayerReconnect(userId) โ€” Override for reconnection side effects

  • Spectator:

    • getSpectatorSnapshot(targetPlayerId?) โ€” Dispatches to correct spectator state method

    • getStateForSpectatorViewingPlayer(targetPlayerId) โ€” Overridable per-player spectator view

    • getViewablePlayers() โ€” List of players available for spectator selection

Minigame Handler File Structureยถ

Each minigame lives in server/rmhbox/minigames/<minigame-id>/:

  • index.ts โ€” Barrel export (exports the handler class)

  • handler.ts โ€” The handler class extending BaseMinigame

  • types.ts โ€” Minigame-specific types (phase enums, state interfaces, submission types)

Client-Side Component Patternยถ

Each minigame has components in components/rmhbox/minigames/<minigame-id>/:

Main Game Component (<MinigameName>Game.tsx)ยถ

This is the root component loaded by MinigameRenderer. It receives MinigameProps:

interface MinigameProps {
  playerId: string;
  playerName: string;
}

Standard pattern used by all minigame components:

  1. Define local state for the gameโ€™s current phase, data, etc.

  2. Use useGameSocket() hook to subscribe to GAME_ACTION and GAME_STATE_SNAPSHOT events

  3. In the onGameAction callback, dispatch based on data.type to update local state

  4. In the onStateSnapshot callback, hydrate full local state from the snapshot

  5. Use emitGameInput(action, data) to send player input to the server

  6. Render different views based on the current phase

MinigameRenderer (components/rmhbox/minigames/MinigameRenderer.tsx)ยถ

Dynamically lazy-loads minigame components using React.lazy(). The MINIGAME_COMPONENTS map associates minigame IDs to their lazy-loaded component imports. New minigames must be added here.

Also exports two hooks for minigame components:

  • useHeaderTimer() โ€” Control the header timer ring (startTimer, tickTimer, clearTimer)

  • useMinigameRound() โ€” Control the footer round counter (setRound, clearRound)

History Detail Component (<MinigameName>HistoryDetail.tsx)ยถ

Renders the expanded view of a past matchโ€™s game log. Receives HistoryDetailProps:

interface HistoryDetailProps {
  gameLog: GameLog;
  currentUserId: string;
  players: Array<{ userId: string; userName: string; rank: number; score: number }>;
}

Shared Minigame Code in lib/rmhbox/ยถ

Per-minigame shared code lives in lib/rmhbox/<minigame-id>/:

  • schemas.ts โ€” Zod validation schemas for game-specific inputs

  • data-loader.ts โ€” Data loading functions (reading from data/rmhbox/<minigame-id>/)

Static data files live in data/rmhbox/<minigame-id>/ as JSON files.


Adding a New Minigame โ€” Checklistยถ

When adding a new minigame, the following files/registrations are required:

1. Shared Types & Constantsยถ

  • Add game-specific constants to lib/rmhbox/constants.ts (use a consistent prefix, e.g., XY_ for โ€œXY Gameโ€)

  • If the minigame has player input that needs validation, create lib/rmhbox/<minigame-id>/schemas.ts with Zod schemas

  • If the minigame needs static data, create data/rmhbox/<minigame-id>/ with JSON files and lib/rmhbox/<minigame-id>/data-loader.ts

2. Minigame Registryยถ

  • Add the MinigameDefinition entry to MINIGAME_REGISTRY in lib/rmhbox/minigame-registry.ts

    • Define id, displayName, description, category, icon (Lucide icon name), minPlayers, maxPlayers, estimatedDurationSeconds, supportsTeams, instructionDurationSeconds, preloadAssets, joinInProgressPolicy, tags

    • If the minigame has host-configurable settings, define a GameSettingsSchema array and assign it to settingsSchema

3. Server Handlerยถ

  • Create server/rmhbox/minigames/<minigame-id>/types.ts โ€” Phase enum, state interfaces, submission types

  • Create server/rmhbox/minigames/<minigame-id>/handler.ts โ€” Handler class extending BaseMinigame

    • Implement all abstract methods: start(), handleInput(), getStateForPlayer(), getStateForSpectator(), computeResults(), get spectatorMode

    • Use this.getSetting(key, FALLBACK_CONSTANT) for host-configurable settings

    • Use this.setTimeout() / this.setInterval() for tracked timers

    • Use this.startPhaseTimer() / this.clearPhaseTimer() for countdown phases

    • Use this.broadcastRound() for sub-round display

    • Use this.context.broadcastAction() for sequenced state updates

    • Use this.context.sendToPlayer() for per-player private state

    • Use this.context.onComplete() to signal game end with results

  • Create server/rmhbox/minigames/<minigame-id>/index.ts โ€” Barrel export

  • Register the handler class in MINIGAME_SERVER_REGISTRY in server/rmhbox/game-coordinator.ts

    • Add import and Map entry: ['<minigame-id>', MinigameHandlerClass]

4. Client Componentsยถ

  • Create components/rmhbox/minigames/<minigame-id>/<MinigameName>Game.tsx โ€” Main game component

    • Accept MinigameProps (playerId, playerName)

    • Use useGameSocket() for event subscription

    • Use emitGameInput() for sending input

    • Export as default for lazy loading

  • Create game-specific sub-components as needed in the same directory

  • Register the lazy import in MINIGAME_COMPONENTS in components/rmhbox/minigames/MinigameRenderer.tsx

5. History Displayยถ

  • Create components/rmhbox/minigames/<minigame-id>/<MinigameName>HistoryDetail.tsx

  • Register in lib/rmhbox/history-display-registrations.ts using registerHistoryDisplay()

    • Define DetailComponent, searchableFields, filterableFields, and getSummary

6. Game Settings (if applicable)ยถ

  • Define a GameSettingsSchema array in lib/rmhbox/minigame-registry.ts (alongside the existing ones)

  • Assign it to the settingsSchema field in the minigameโ€™s MinigameDefinition

  • Use this.getSetting(key, fallback) in the handler to read settings

7. Testingยถ

  • Add tests following the existing patterns in testing/rmhbox/

  • Test the handler in isolation (start, input, results, edge cases)

8. Verify Integrationยถ

  • The game appears in the voting candidate pool (based on MINIGAME_REGISTRY)

  • The host can pick the game directly

  • Host-configured settings are passed to the handler

  • The game lifecycle (instructions โ†’ preloading โ†’ countdown โ†’ playing โ†’ results) works

  • Spectator mode works correctly

  • Reconnection provides correct state

  • Results and rankings display properly

  • Match history and leaderboard persist correctly


Game Settings Systemยถ

The game settings system (ยง12A) allows hosts to configure minigame parameters before starting.

Schema Definitionยถ

Settings are defined as GameSettingsSchema arrays (in lib/rmhbox/minigame-registry.ts):

const EXAMPLE_SETTINGS: GameSettingsSchema = [
  { key: 'totalRounds', type: 'integer', label: 'Number of Rounds', description: '...', default: 3, min: 1, max: 5, step: 1 },
  { key: 'enableFeature', type: 'boolean', label: 'Feature Toggle', description: '...', default: true },
  { key: 'difficulty', type: 'select', label: 'Difficulty', description: '...', default: 'medium', options: ['easy', 'medium', 'hard'] },
];

Flowยถ

  1. Host picks a game โ†’ GAME_PICKED action โ†’ client initializes gameSettingsState in store

  2. Host opens settings modal โ†’ modifies values โ†’ emits GAME_UPDATE_SETTINGS

  3. Server validates with validateGameSettings() โ†’ broadcasts GAME_SETTINGS_UPDATED

  4. Host confirms โ†’ emits GAME_CONFIRM_SETTINGS โ†’ server resolves settings and starts game

  5. Handler reads settings via this.getSetting(key, fallback)

Validationยถ

lib/rmhbox/game-settings.ts provides:

  • getDefaultSettings(schema) โ€” Build defaults object from schema

  • validateGameSettings(schema, values) โ€” Validate and sanitize all values

  • mergeGameSettings(schema, current, updates) โ€” Merge partial updates


Spectator Systemยถ

RMHbox supports two spectator viewing modes:

shared-privilegedยถ

Spectators see an omniscient/privileged view (e.g., the spymasterโ€™s full grid in Undercover Agent). All spectators see the same state from getStateForSpectator().

competitive-individualยถ

All players compete equally; spectators select a player to โ€œfollowโ€ and see that playerโ€™s individual state. Used for games like Rhyme Time and Wiki-Race where showing everything would be unfair.

Key API:

  • SPECTATOR_SELECT_PLAYER (C2S) โ€” Spectator chooses a player to follow

  • SPECTATOR_TARGET_STATE (S2C) โ€” Server sends target player info

  • sendToSpectatorFollowers(targetPlayerId, event, data) โ€” Forward per-player events to spectators following that player

  • getSpectatorSnapshot(targetPlayerId?) โ€” Dispatches to the correct state method based on mode

  • getPhaseTimerSnapshot() โ€” Returns current phase timer state; sent to the spectator on target switch so the timer immediately reflects the correct remaining time


History & Leaderboard Systemยถ

Match Persistenceยถ

After each game completes, the LeaderboardService persists:

  • RMHboxMatch โ€” Match metadata (minigameId, lobbyId, duration, results, gameLog)

  • RMHboxMatchPlayer โ€” Per-player results (rank, score, wasWinner)

  • RMHboxProfile โ€” Updated aggregate stats (totalGamesPlayed, totalWins, totalScore, minigameStats)

History Display Registryยถ

lib/rmhbox/history-display-registry.ts defines the HistoryDisplayConfig interface:

  • DetailComponent โ€” React component for rendering game log details

  • searchableFields โ€” Fields extractable from game logs for search

  • filterableFields โ€” Fields for filtering (select, range, boolean)

  • getSummary() โ€” One-line summary from a game log

All minigames register their history display in lib/rmhbox/history-display-registrations.ts.

History Page UI (app/rmhbox/minigames/[minigameId]/history/page.tsx)ยถ

The history page consumes the registered HistoryDisplayConfig to provide:

  • Search bar โ€” Searches player names, dates, and all searchableFields extracted from each matchโ€™s gameLog. Placeholder text lists available searchable fields.

  • Filter controls โ€” Renders select-type filterableFields as dropdowns, with options aggregated from all loaded match game logs.

  • Summary display โ€” Each match entry row shows the getSummary() result from the game log for quick recall.

  • Detail expansion โ€” Clicking a match loads the full gameLog and renders the minigameโ€™s DetailComponent.

The list API (GET /api/rmhbox/history) includes gameLog in list responses to support client-side search, filtering, and summary computation.

REST APIsยถ

  • GET /api/rmhbox/history โ€” Paginated match history with optional match detail

  • GET /api/rmhbox/stats โ€” Player statistics

  • GET /api/rmhbox/leaderboard โ€” Leaderboard data


Database Modelsยถ

Defined in prisma/schema.prisma:

model RMHboxProfile {
  id, userId (unique), totalGamesPlayed, totalWins, totalScore,
  minigameStats (Json), currentWinStreak, bestWinStreak, createdAt, updatedAt
  โ†’ matches: RMHboxMatchPlayer[]
}

model RMHboxMatch {
  id, minigameId, lobbyId, startedAt, endedAt, durationMs,
  winnerUserId, playerCount, gameLog (Json), results (Json)
  โ†’ players: RMHboxMatchPlayer[]
}

model RMHboxMatchPlayer {
  id, matchId, profileId, userId, userName, rank, score, wasWinner
}

Theming & Stylingยถ

RMHbox uses CSS custom properties (design tokens) for a fully isolated visual identity. The theme stylesheet is app/rmhbox/rmhbox.css.

Design Token Patternยถ

All RMHbox-specific colors, fonts, and spacing use the --rmhbox- prefix:

.rmhbox-theme {
  --rmhbox-bg: #1a1b1e;
  --rmhbox-surface: #27282c;
  --rmhbox-text: #e8e8ec;
  --rmhbox-accent: #6ea8d9;
  --rmhbox-success: #7bc88a;
  --rmhbox-danger: #d98a8a;
  --rmhbox-warning: #d9c36e;
  /* ... */
}

Dark/Light Themeยถ

  • Default is dark theme (.rmhbox-theme)

  • Light theme activated by adding .rmhbox-light class (.rmhbox-theme.rmhbox-light)

  • Theme toggle is in user settings, persisted via Zustand

Usage in Componentsยถ

Reference tokens via Tailwind arbitrary values:

<div className="bg-(--rmhbox-surface) text-(--rmhbox-text) border-(--rmhbox-border)">

Iconsยถ

The codebase uses Lucide React icons. Minigame icons in MinigameDefinition reference Lucide icon names (e.g., 'mic-vocal', 'shield-check', 'globe', 'brush', 'clapperboard').


Sound Systemยถ

lib/rmhbox/audio.ts provides a simple API using Howler.js:

  • playSound(name: SoundName) โ€” Play a sound effect at current volume settings

  • preloadSounds() โ€” Preload all sound files

Available sounds: chime, click, countdownBeep, goFanfare, scoreDing, buzzer, victoryFanfare, swoosh

Sound files are stored in public/music/rmhbox/sfx/. Volume respects the userโ€™s masterVolume ร— sfxVolume settings from the Zustand store.


Testing Conventionsยถ

Tests are organized in testing/rmhbox/ by implementation phase:

  • phase-1/ โ€” Core types, schemas, constants, utils, auth, rate limiting, registry

  • phase-2/ โ€” Lobby management (creation, join, leave, host controls, chat, ready-up)

  • phase-3/ โ€” Game coordination (voting, state sync, reconnection, spectators)

  • phase-4/ โ€” Client store, UI components, database, API routes, sound system

  • phase-5/ โ€” Minigame implementations (Rhyme Time, Undercover Agent, Category Crash, Wiki-Race)

  • phase-6/ โ€” Minigame implementations (Minimalist Masterpiece, Emoji Cinema)

Each phase has a setup.ts file for shared test utilities and mocking. Tests use Vitest as the test runner.

Security state masking tests (security-state-masking.test.ts) exist in each phase directory, verifying that sensitive server state is never leaked to clients.


Code Standards & Maintainability Guidelinesยถ

General Principlesยถ

  1. Modularity โ€” Every file should have a single, clear responsibility. The RMHbox codebase is organized by domain (lobby management, game coordination, per-minigame logic) with clean boundaries.

  2. Separation of Concerns

    • Shared code (client + server) lives in lib/rmhbox/

    • Server-only code lives in server/rmhbox/

    • UI components live in components/rmhbox/

    • Page routes live in app/rmhbox/

    • Each minigame is self-contained in its own subdirectory at each layer

  3. No Cross-Minigame Dependencies โ€” Minigame implementations must never import from other minigames. All shared functionality lives in the base class or shared utilities.

File Organizationยถ

  • Barrel exports โ€” Each minigame server handler directory must have an index.ts that exports the handler class

  • Consistent naming โ€” Files use kebab-case; React components use PascalCase; types/interfaces use PascalCase; constants use UPPER_SNAKE_CASE

  • File headers โ€” Every file begins with a JSDoc comment describing its purpose, responsibilities, and relevant reference sections

  • Section separators โ€” Use // โ”€โ”€โ”€ Section Name โ”€โ”€โ”€โ”€ comment dividers within files to organize logical sections

TypeScript Standardsยถ

  • Strict mode โ€” TypeScript strict mode is enabled

  • Explicit typing โ€” Use explicit types for function parameters, return types (on exported functions), and interface fields

  • unknown over any โ€” Always prefer unknown and narrow with type guards; never use any unless absolutely unavoidable

  • Readonly where possible โ€” Mark readonly fields and use as const for immutable constants

  • Import types with type keyword โ€” Use import type { ... } for type-only imports

Validationยถ

  • Zod everywhere โ€” All external inputs (WebSocket payloads, API request bodies, data files) must be validated with Zod schemas before use

  • Schema co-location โ€” Shared schemas go in lib/rmhbox/schemas.ts; minigame-specific schemas go in lib/rmhbox/<minigame-id>/schemas.ts

  • Transform in schemas โ€” Use Zod .transform() for normalization (trimming, lowercasing) at the schema level

Error Handlingยถ

  • Structured errors โ€” All errors emitted to clients use RMHboxError with a typed RMHboxErrorCode

  • Try-catch isolation โ€” All handler callbacks and timer callbacks are wrapped in try-catch. The validated() wrapper handles this for event handlers

  • BaseMinigame error isolation โ€” All setTimeout and setInterval callbacks in BaseMinigame are wrapped to catch errors and route them to context.onError()

  • Fire-and-forget persistence โ€” Database writes (match persistence) are async and must not affect game flow on failure

State Managementยถ

  • Single source of truth โ€” Server holds authoritative game state; client state is derived from server events

  • Sequence ordering โ€” All client state updates respect monotonic seq numbers

  • State masking โ€” buildClientState() in LobbyManager is the ONLY exit point for state data, ensuring internal fields are never leaked

  • Per-player scoping โ€” Game state sent to players is scoped via getStateForPlayer(userId) โ€” never send a player another playerโ€™s private state

React Component Patternsยถ

  • 'use client' โ€” All interactive components must have the 'use client' directive

  • Lazy loading โ€” Minigame components are loaded via React.lazy() in MinigameRenderer

  • Hooks for shared behavior โ€” Use custom hooks (useGameSocket, useHeaderTimer, useMinigameRound) for common patterns

  • Zustand selectors โ€” Use specific selectors like useRMHboxStore((s) => s.lobby) to minimize re-renders

  • Cleanup on unmount โ€” Hooks must clean up subscriptions and timers in their cleanup functions

  • No inline styles except for CSS variables โ€” Use Tailwind classes with CSS variable references; avoid hardcoded colors

Constantsยถ

  • Centralized โ€” All tuning constants (timers, scoring, limits) live in lib/rmhbox/constants.ts

  • Per-minigame prefix โ€” Minigame constants use a 2-letter prefix (e.g., RT_ for Rhyme Time, UA_ for Undercover Agent, CC_ for Category Crash, WR_ for Wiki-Race, MM_ for Minimalist Masterpiece, EC_ for Emoji Cinema, WW_ for Wit-War)

  • Server config โ€” Server-specific runtime configuration (env vars, ports) lives in server/rmhbox/config.ts

Adding New Featuresยถ

When adding new features to the RMHbox platform:

  1. Define types in lib/rmhbox/types.ts (shared) or server/rmhbox/types.ts (server-only)

  2. Add event names to C2S and S2C in lib/rmhbox/events.ts

  3. Create Zod schemas in lib/rmhbox/schemas.ts

  4. Implement server logic in the appropriate service

  5. Update the Zustand store reducers if new actions are needed

  6. Add socket event listeners in lib/rmhbox/socket.ts if new S2C events exist

  7. Create/update UI components

  8. Write tests

  9. Update this document if the change affects core architecture or patterns