# Complete Game Workflow Documentation **Version**: 2.0 **Last Updated**: October 30, 2025 **Document Type**: Complete System Reference --- ## Table of Contents 1. [System Overview](#system-overview) 2. [Game Flow Diagram](#game-flow-diagram) 3. [REST API Endpoints](#rest-api-endpoints) 4. [WebSocket Events](#websocket-events) 5. [Interfaces & Data Structures](#interfaces--data-structures) 6. [Domain Aggregates](#domain-aggregates) 7. [Complete Game Scenarios](#complete-game-scenarios) 8. [Turn Tracking System](#turn-tracking-system) 9. [Position Guessing Mechanic](#position-guessing-mechanic) 10. [Error Handling](#error-handling) --- ## System Overview The SerpentRace game system uses a **hybrid architecture**: - **REST API**: Game creation, joining, and initial setup - **WebSocket (Socket.IO)**: Real-time gameplay, card interactions, and game state updates - **Redis**: Session management, pending states, turn tracking - **PostgreSQL**: Persistent game data, user profiles, decks ### Technology Stack - **Backend**: Node.js + TypeScript + Express - **Real-time**: Socket.IO (WebSocket library) - **Database**: TypeORM + PostgreSQL - **Cache/Session**: Redis - **Authentication**: JWT tokens (Access + Refresh) --- ## Game Flow Diagram ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ GAME LIFECYCLE │ └──────────────────────────────────────────────────────────────────────────┘ 1. GAME CREATION (REST) │ ├─ POST /api/v1/game/start │ ├─ Gamemaster creates game with deck selection │ ├─ Game Code generated (6 characters) │ └─ Game state: "waiting" │ ▼ 2. PLAYER JOINING (REST + WebSocket) │ ├─ POST /api/v1/game/join │ ├─ Validate game code │ ├─ Check game type (PUBLIC/PRIVATE/ORGANIZATION) │ ├─ Add player to game.players[] │ └─ Return gameToken for WebSocket auth │ ├─ WebSocket Connection │ ├─ Connect to /game namespace │ ├─ Emit: game:join with gameToken │ ├─ Server validates & joins rooms │ └─ Broadcast: game:player-joined to all │ ├─ Emit: game:ready (player marks ready) └─ Broadcast: game:player-ready to all │ ▼ 3. GAME START (REST) │ ├─ POST /api/v1/game/:gameId/start │ ├─ Only gamemaster can start │ ├─ Check minimum players (2+) │ ├─ Generate board (100 fields with pattern) │ ├─ Shuffle and assign turn sequence │ ├─ Initialize player positions (all at 0) │ └─ Game state: "active" │ ├─ Broadcast: game:started │ ├─ Board data sent to all players │ ├─ Turn sequence revealed │ └─ First player notified │ ▼ 4. GAMEPLAY LOOP (WebSocket) │ ├─ DICE ROLL │ ├─ Emit: game:dice-roll { diceValue: 1-6 } │ ├─ Broadcast: game:dice-rolled │ ├─ Calculate new position │ ├─ Update player position │ ├─ Broadcast: game:player-moved │ └─ Check field type at new position │ ├─ SPECIAL FIELD LANDING (positive/negative/luck) │ ├─ Wait 2 seconds (frontend animation) │ ├─ Draw card from appropriate deck │ ├─ Broadcast: game:card-drawn │ └─ Branch based on card type: │ │ ┌─ QUESTION CARD (types 0-4) │ │ ├─ Emit to player: game:card-drawn-self (60s) │ │ ├─ Player answers │ │ ├─ Emit: game:card-answer │ │ ├─ Broadcast: game:answer-submitted │ │ ├─ Validate answer │ │ ├─ Broadcast: game:answer-validated │ │ ├─ Determine if guess required: │ │ │ ├─ Positive field + correct = GUESS │ │ │ ├─ Negative field + wrong = GUESS │ │ │ ├─ Positive field + wrong = NO MOVEMENT │ │ │ └─ Negative field + correct = NO MOVEMENT │ │ │ │ │ └─ IF GUESS REQUIRED: │ │ ├─ Emit: game:position-guess-request (30s) │ │ ├─ Show pattern modifier, dice, stepValue │ │ ├─ Player guesses position │ │ ├─ Emit: game:position-guess │ │ ├─ Broadcast: game:position-guess-broadcast │ │ ├─ Calculate with BoardGenerationService │ │ ├─ Apply -2 penalty if wrong │ │ ├─ Broadcast: game:guess-result │ │ ├─ Check win condition (position >= 100) │ │ └─ Check secondary landing │ │ ┌─ LUCK CARD (type 6) │ │ ├─ Process immediately (no answer) │ │ ├─ Broadcast: game:card-result │ │ ├─ Apply consequence: │ │ │ ├─ MOVE_FORWARD (0): Move X steps │ │ │ ├─ MOVE_BACKWARD (1): Move X steps back │ │ │ ├─ LOSE_TURN (2): Store X turns to lose │ │ │ ├─ EXTRA_TURN (3): Store X extra turns │ │ │ └─ GO_TO_START (5): Return to position 1 │ │ ├─ Broadcast: game:luck-consequence │ │ └─ Advance turn (with turn tracking) │ │ └─ JOKER CARD (type 5) - Secondary Landing Only │ ├─ Broadcast: game:joker-drawn │ ├─ Request gamemaster decision │ ├─ Emit to gamemaster: game:gamemaster-decision-request (120s) │ ├─ Gamemaster decides (approve/reject) │ ├─ Emit: game:gamemaster-decision │ ├─ Broadcast: game:gamemaster-decision-result │ ├─ Determine if guess required: │ │ ├─ Positive field + approved = GUESS │ │ ├─ Negative field + rejected = GUESS │ │ ├─ Positive field + rejected = NO MOVEMENT │ │ └─ Negative field + approved = NO MOVEMENT │ │ │ └─ IF GUESS REQUIRED: │ ├─ Emit: game:joker-position-guess-request (30s) │ ├─ Player guesses with dice=6 │ ├─ Emit: game:joker-position-guess │ ├─ Calculate with BoardGenerationService │ ├─ Apply -2 penalty if wrong │ ├─ Broadcast: game:joker-complete │ └─ Check secondary landing │ ├─ TURN ADVANCEMENT │ ├─ Check if current player has extra turns │ │ └─ IF YES: Same player continues, decrement counter │ ├─ Find next player in sequence │ ├─ Skip players with turns to lose │ │ └─ Broadcast: game:players-skipped │ ├─ Broadcast: game:turn-changed │ └─ Emit to next player: game:your-turn │ └─ WIN CONDITION ├─ Player reaches position >= 100 ├─ Broadcast: game:ended ├─ Update database (finished=true, winner) └─ Cleanup all Redis data and disconnect │ ▼ 5. GAME END & CLEANUP │ ├─ Clear all Redis keys: │ ├─ gameplay:{gameCode} │ ├─ game_positions:{gameCode} │ ├─ pending_card:{gameCode}:{playerId} │ ├─ pending_decision:{gameCode}:{requestId} │ ├─ player_extra_turns:{gameCode}:{playerId} │ └─ player_turns_to_lose:{gameCode}:{playerId} │ ├─ Force disconnect all players from rooms ├─ Emit: game:cleanup-complete └─ Database updated with final state ``` --- ## REST API Endpoints ### Authentication Headers All authenticated endpoints require: ``` Authorization: Bearer ``` ### 1. Create Game **Endpoint**: `POST /api/v1/game/start` **Auth**: Required **Description**: Create a new game session with selected decks **Request Body**: ```typescript { deckids: string[]; // Array of deck UUIDs (at least 1) maxplayers: number; // Maximum players (2-10) logintype: number; // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION } ``` **Response** (200 OK): ```typescript { id: string; // Game UUID gamecode: string; // 6-character game code maxplayers: number; logintype: number; boardsize: number; // Default 100 createdby: string; // Gamemaster user ID orgid: string | null; // Organization ID (for ORG games) gamedecks: GameDeck[]; // Array of decks with cards players: string[]; // Empty array initially startdate: Date | null; enddate: Date | null; finished: boolean; winner: string | null; totaltiles: number; } ``` **Errors**: - `400`: Invalid input (missing deckids, invalid maxplayers, etc.) - `401`: Not authenticated - `404`: Deck not found - `500`: Internal server error --- ### 2. Join Game **Endpoint**: `POST /api/v1/game/join` **Auth**: Optional (depends on game type) **Description**: Join an existing game using game code **Request Body**: ```typescript { gameCode: string; // 6-character game code playerName?: string; // Required for PUBLIC, optional for authenticated } ``` **Authentication Requirements by Game Type**: - **PUBLIC (0)**: No auth required, playerName mandatory - **PRIVATE (1)**: Auth required - **ORGANIZATION (2)**: Auth + organization membership required **Response** (200 OK): ```typescript { id: string; // Game UUID gamecode: string; // Game code playerName: string; // Player's display name playerCount: number; // Current number of players maxPlayers: number; gameType: string; // "PUBLIC" | "PRIVATE" | "ORGANIZATION" isAuthenticated: boolean; gameToken: string; // JWT token for WebSocket authentication } ``` **Game Token Payload**: ```typescript { gameId: string; gameCode: string; playerName: string; playerId?: string; // Only if authenticated iat: number; // Issued at exp: number; // Expires in 24 hours } ``` **Errors**: - `400`: Invalid gameCode format or missing playerName - `401`: Authentication required (PRIVATE/ORGANIZATION games) - `403`: Organization membership required or mismatch - `404`: Game not found - `409`: Game full or already started - `500`: Internal server error --- ### 3. Start Game Play **Endpoint**: `POST /api/v1/game/:gameId/start` **Auth**: Required (only gamemaster) **Description**: Start the actual gameplay after all players are ready **Path Parameters**: - `gameId`: UUID of the game **Request Body**: Empty `{}` **Response** (200 OK): ```typescript { message: string; // "Game started successfully" gameId: string; playerCount: number; game: GameAggregate; // Full game object boardData: { gameId: string; fields: GameField[]; // 100 fields with positions, types, stepValues generationComplete: boolean; generatedAt: Date; } } ``` **Board Generation**: - Generates 100 fields with pattern-based distribution - Field types: `regular`, `positive`, `negative`, `luck` - Each field has `stepValue` (0-3) for movement calculations - Pattern modifiers by zones: - Positions 1-20: +2 (easier start) - Positions 21-40: -1 (early challenge) - Positions 41-60: +1 (mid-game boost) - Positions 61-80: -2 (late challenge) - Positions 81-100: +3 (final stretch) **Game State Changes**: - `game.startdate` set to current date - `game.gamePhase` set to "active" - Turn sequence randomized - Player positions initialized to 0 **Errors**: - `400`: Invalid gameId - `403`: Only gamemaster can start game - `404`: Game not found - `409`: Game already started, not enough players, or players not ready - `500`: Board generation failed or internal error --- ## WebSocket Events ### Connection & Authentication **Namespace**: `/game` **Connection**: ```typescript const socket = io('http://localhost:3000/game', { auth: { token: gameToken // JWT token from /game/join endpoint } }); ``` **Authentication Flow**: 1. Client connects with `gameToken` in auth 2. Server validates token using `GameTokenService` 3. Server extracts: `gameId`, `gameCode`, `playerName`, `playerId` 4. Server joins socket to rooms: - `game_{gameCode}` (all players) - `game_{gameCode}:{playerName}` (individual player) 5. Server emits `authenticated` event **Events**: ```typescript // Client → Server: Initial join socket.emit('game:join', { gameToken: string }); // Server → Client: Authentication success socket.on('authenticated', { gameCode: string; playerName: string; message: string; }); // Server → All Players: Player joined socket.on('game:player-joined', { playerId: string; playerName: string; playerCount: number; timestamp: string; }); ``` --- ### Pre-Game Events #### Ready System ```typescript // Client → Server: Mark player as ready socket.emit('game:ready', { gameCode: string; ready: boolean; }); // Server → All Players: Player ready status changed socket.on('game:player-ready', { playerId: string; playerName: string; ready: boolean; readyCount: number; totalPlayers: number; allReady: boolean; timestamp: string; }); ``` #### Game Start Notification ```typescript // Server → All Players: Game started (after REST /start call) socket.on('game:started', { message: string; boardData: BoardData; turnSequence: string[]; // Array of player IDs in turn order currentPlayer: string; // First player ID currentPlayerName: string; timestamp: string; }); // Server → Current Player: Your turn notification socket.on('game:your-turn', { message: string; canRoll: boolean; isExtraTurn?: boolean; // True if using extra turn timestamp: string; }); ``` --- ### Gameplay Events #### Dice Roll ```typescript // Client → Server: Roll dice socket.emit('game:dice-roll', { gameCode: string; diceValue: number; // 1-6 }); // Server → All Players: Dice rolled socket.on('game:dice-rolled', { playerId: string; playerName: string; diceValue: number; timestamp: string; }); // Server → All Players: Player moved socket.on('game:player-moved', { playerId: string; playerName: string; oldPosition: number; newPosition: number; diceValue: number; timestamp: string; }); ``` --- #### Card Drawing & Question Cards ```typescript // Server → All Players: Card drawn (everyone sees question) socket.on('game:card-drawn', { playerName: string; playerId: string; cardType: string; // "QUIZ", "SENTENCE_PAIRING", etc. question: string; // The question text fieldType: string; // "positive" | "negative" | "luck" timestamp: string; }); // Server → Drawing Player: Interactive card details socket.on('game:card-drawn-self', { cardData: { cardid: string; question: string; type: CardType; timeLimit: number; // 60 seconds answerOptions?: QuizOption[]; // For QUIZ (multiple choice) sentencePairs?: SentencePair[]; // For SENTENCE_PAIRING (left-right matching) words?: string[]; // For SENTENCE_PAIRING (legacy scrambled words) // ... type-specific data }; timestamp: string; }); // SENTENCE_PAIRING Data Structures /** * Sentence pair for left-right matching */ interface SentencePair { id: string; // Unique identifier (e.g., "pair_0", "pair_1") left: string; // Left part to match (shown in order) right: string; // Right part (scrambled position) } /** * Player's answer for sentence pairing */ interface SentencePairingAnswer { pairId: string; // ID of the pair being matched leftText: string; // Left part text rightText: string; // Player's chosen right part } // Example: Matching fruits to colors // Card data sent to player: { sentencePairs: [ { id: "pair_0", left: "Apple", right: "Yellow" }, // Right parts { id: "pair_1", left: "Banana", right: "Orange" }, // are scrambled { id: "pair_2", left: "Orange", right: "Red" } ] } // Player's answer: { answer: [ { pairId: "pair_0", leftText: "Apple", rightText: "Red" }, { pairId: "pair_1", leftText: "Banana", rightText: "Yellow" }, { pairId: "pair_2", leftText: "Orange", rightText: "Orange" } ] } // Client → Server: Submit answer socket.emit('game:card-answer', { gameCode: string; answer: any; // Type depends on card type: // - QUIZ: string (A/B/C/D) // - SENTENCE_PAIRING: SentencePairingAnswer[] or string (legacy) // - OWN_ANSWER: string // - TRUE_FALSE: boolean or "true"/"false" // - CLOSER: number }); // Server → All Players: Answer submitted (before validation) socket.on('game:answer-submitted', { playerName: string; playerId: string; answer: any; message: string; timestamp: string; }); // Server → All Players: Answer validated socket.on('game:answer-validated', { playerName: string; playerId: string; isCorrect: boolean; correctAnswer: any; message: string; timestamp: string; }); ``` --- #### Position Guessing (Question Cards) ```typescript // Server → Player: Request position guess socket.on('game:position-guess-request', { message: string; currentPosition: number; // Starting position diceRoll: number; // Original dice value fieldStepValue: number; // Field's step value patternModifier: number; // Zone-based modifier timeLimit: number; // 30 seconds timestamp: string; }); // Client → Server: Submit position guess socket.emit('game:position-guess', { gameCode: string; guessedPosition: number; }); // Server → All Players: Player's guess broadcast socket.on('game:position-guess-broadcast', { playerId: string; playerName: string; guessedPosition: number; message: string; timestamp: string; }); // Server → All Players: Guess result with full calculation socket.on('game:guess-result', { playerId: string; playerName: string; guessedPosition: number; actualPosition: number; // Calculated position finalPosition: number; // After penalty (if wrong) guessCorrect: boolean; penaltyApplied: boolean; // -2 if wrong calculation: { startPosition: number; diceRoll: number; stepValue: number; patternModifier: number; calculatedPosition: number; penalty: number; // 0 or -2 }; message: string; timestamp: string; }); // Server → All Players: Player didn't move socket.on('game:no-movement', { playerId: string; playerName: string; reason: string; // "Wrong answer on positive field" message: string; timestamp: string; }); // Server → All Players: Penalty avoided socket.on('game:penalty-avoided', { playerId: string; playerName: string; message: string; // "Avoided penalty on negative field" timestamp: string; }); ``` --- #### Luck Cards ```typescript // Server → All Players: Luck card consequence applied socket.on('game:luck-consequence', { playerId: string; playerName: string; consequenceType: string; // "MOVE_FORWARD" | "MOVE_BACKWARD" | etc. value: number; // Magnitude of effect newPosition?: number; // For movement consequences turnsToLose?: number; // For LOSE_TURN extraTurns?: number; // For EXTRA_TURN message: string; timestamp: string; }); ``` **Consequence Types**: - `MOVE_FORWARD` (0): Move forward X steps - `MOVE_BACKWARD` (1): Move backward X steps - `LOSE_TURN` (2): Lose X turns - `EXTRA_TURN` (3): Gain X extra turns - `GO_TO_START` (5): Return to position 1 --- #### Joker Cards & Gamemaster Decisions ```typescript // Server → All Players: Joker card drawn socket.on('game:joker-drawn', { playerName: string; playerId: string; jokerCard: { question: string; consequence: Consequence; }; waitingForGamemaster: boolean; timestamp: string; }); // Server → Gamemaster: Decision request socket.on('game:gamemaster-decision-request', { requestId: string; playerName: string; playerId: string; jokerCard: { question: string; consequence: Consequence; }; timeLimit: number; // 120 seconds timestamp: string; }); // Client → Server: Gamemaster decision socket.emit('game:gamemaster-decision', { gameCode: string; requestId: string; decision: 'approve' | 'reject'; }); // Server → All Players: Decision result socket.on('game:gamemaster-decision-result', { playerName: string; playerId: string; gamemasterName: string; decision: string; // "approve" | "reject" approved: boolean; consequence: number | null; description: string; timestamp: string; }); // Server → Player: Joker position guess request socket.on('game:joker-position-guess-request', { message: string; currentPosition: number; diceRoll: number; // Always 6 for jokers fieldStepValue: number; patternModifier: number; timeLimit: number; // 30 seconds timestamp: string; }); // Client → Server: Joker position guess socket.emit('game:joker-position-guess', { gameCode: string; guessedPosition: number; }); // Server → All Players: Joker card complete socket.on('game:joker-complete', { playerId: string; playerName: string; guessedPosition: number; actualPosition: number; finalPosition: number; // Current position if didn't move guessCorrect: boolean; penaltyApplied: boolean; moved: boolean; // Based on field type + decision calculation: { startPosition: number; diceRoll: number; // 6 stepValue: number; patternModifier: number; calculatedPosition: number; penalty: number; }; message: string; timestamp: string; }); ``` **Joker Flow Logic**: - **Positive field + approved**: Player moves (with guess) - **Positive field + rejected**: Player doesn't move - **Negative field + approved**: Player doesn't move (avoided penalty) - **Negative field + rejected**: Player moves (with guess - penalty) --- #### Turn Advancement & Turn Tracking ```typescript // Server → All Players: Turn changed socket.on('game:turn-changed', { currentPlayer: string; // Player ID currentPlayerName: string; turnNumber: number; message: string; timestamp: string; }); // Server → All Players: Extra turn remaining socket.on('game:extra-turn-remaining', { playerId: string; playerName: string; remainingExtraTurns: number; // How many left after this one message: string; timestamp: string; }); // Server → All Players: Players skipped socket.on('game:players-skipped', { skippedPlayers: Array<{ playerId: string; playerName: string; remainingTurnsToLose: number; }>; message: string; timestamp: string; }); ``` --- #### Game End & Cleanup ```typescript // Server → All Players: Game ended socket.on('game:ended', { winner: string; // Player ID winnerName: string; message: string; finalPositions: PlayerPosition[]; timestamp: string; }); // Server → All Players: Cleanup complete socket.on('game:cleanup-complete', { gameCode: string; message: string; timestamp: string; }); ``` --- #### Error Events ```typescript // Server → Client: Error occurred socket.on('game:error', { message: string; code?: string; timestamp: string; }); // Server → All Players: Card error (no cards available) socket.on('game:card-error', { playerName: string; playerId: string; error: string; timestamp: string; }); // Server → All Players: Joker error socket.on('game:joker-error', { playerName: string; playerId: string; error: string; timestamp: string; }); ``` --- ## Interfaces & Data Structures ### WebSocket Interfaces Located in: `src/Application/Services/Interfaces/GameInterfaces.ts` ```typescript /** * Join game room data */ export interface JoinGameData { gameToken: string; // JWT token from REST /join endpoint } /** * Leave game data */ export interface LeaveGameData { gameCode: string; } /** * Dice roll data */ export interface DiceRollData { gameCode: string; diceValue: number; // 1-6 } /** * Player position tracking */ export interface PlayerPosition { playerId: string; playerName: string; boardPosition: number; turnOrder: number; } /** * Game chat message */ export interface GameChatData { gameCode: string; message: string; } /** * Card answer data */ export interface CardAnswerData { gameCode: string; answer: any; // Type depends on card type } /** * Gamemaster decision data */ export interface GamemasterDecisionData { gameCode: string; requestId: string; decision: 'approve' | 'reject'; } /** * Field effect calculation request */ export interface FieldEffectRequest { gameId: string; playerId: string; playerName: string; currentPosition: number; card: GameCard; field: GameField; dice: number; guessedPosition?: number; } /** * Field effect calculation result */ export interface FieldEffectResult { finalPosition: number; stepValue: number; dice: number; patternModifier: number; consequenceModifier: number; guessResult?: GuessResult; gamemasterResult?: GamemasterDecisionResult; description: string; effects: string[]; turnEffect?: TurnEffect; } /** * Turn effect (for multi-turn tracking) */ export interface TurnEffect { type: 'LOSE_TURN' | 'EXTRA_TURN'; playerId: string; value: number; // Number of turns } /** * Guess result details */ export interface GuessResult { guessedPosition: number; actualPosition: number; isCorrect: boolean; penaltyApplied: boolean; description: string; } ``` --- ### Pending State Interfaces Located in: `src/Application/Services/GameWebSocketService.ts` ```typescript /** * Pending card state (stored in Redis) * Used for question cards requiring answers */ interface PendingCardState { playerId: string; playerName: string; card: GameCard; field: GameField; // Field where card was drawn dice: number; // Original dice roll currentPosition: number; // Position before card effect drawnAt: number; // Timestamp answerGiven?: boolean; // Has player answered? answerCorrect?: boolean; // Was answer correct? requiresGuess?: boolean; // Does this require position guess? guessedPosition?: number; // Player's guessed position } /** * Pending gamemaster decision state (stored in Redis) * Used for joker cards */ interface PendingDecisionState { playerId: string; playerName: string; card: GameCard; field: GameField; // Field where joker was drawn dice: number; // Always 6 for jokers currentPosition: number; // Position before joker effect drawnAt: number; // Timestamp recursionDepth: number; // Prevent infinite secondary landings gamemasterDecided?: boolean; // Has gamemaster decided? gamemasterApproved?: boolean; // Was it approved? guessedPosition?: number; // Player's guessed position (if required) } ``` **Redis Keys**: ``` pending_card:{gameCode}:{playerId} → PendingCardState (TTL: 60s) pending_decision:{gameCode}:{requestId} → PendingDecisionState (TTL: 120s) player_extra_turns:{gameCode}:{playerId} → number (extra turns remaining) player_turns_to_lose:{gameCode}:{playerId} → number (turns to skip) ``` --- ## Domain Aggregates ### GameAggregate Located in: `src/Domain/Game/GameAggregate.ts` ```typescript /** * Main game entity */ @Entity('Games') export class GameAggregate { @PrimaryGeneratedColumn('uuid') id: string; @Column({ type: 'varchar', length: 10, unique: true }) gamecode: string; // 6-character unique code @Column({ type: 'int' }) maxplayers: number; @Column({ type: 'int', default: LoginType.PUBLIC }) logintype: LoginType; // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION @Column({ type: 'int', default: 50 }) boardsize: number; @Column({ type: 'uuid', nullable: false, name: 'createdBy' }) createdby: string; // Gamemaster user ID @Column({ type: 'uuid', nullable: true, name: 'organizationid' }) orgid: string | null; @Column({ type: 'jsonb', default: () => "'[]'", name: 'decks' }) gamedecks: GameDeck[]; // Decks with cards @Column({ type: 'simple-array', default: '' }) players: string[]; // Array of player IDs/names @Column({ type: 'timestamp', nullable: true }) startdate: Date | null; @Column({ type: 'timestamp', nullable: true }) enddate: Date | null; @Column({ type: 'boolean', default: false }) finished: boolean; @Column({ type: 'varchar', nullable: true }) winner: string | null; @Column({ type: 'int', default: 100 }) totaltiles: number; } /** * Login type enum */ export enum LoginType { PUBLIC = 0, // Anyone can join with playerName PRIVATE = 1, // Requires authentication ORGANIZATION = 2 // Requires auth + org membership } ``` --- ### GameField ```typescript /** * Individual board field/tile */ export interface GameField { position: number; // 0-100 type: 'regular' | 'positive' | 'negative' | 'luck'; stepValue?: number; // 0-3, used in movement calculation } /** * Complete board data */ export interface BoardData { gameId?: string; fields: GameField[]; // 100 fields generationComplete?: boolean; generatedAt?: Date; error?: string; } ``` **Field Type Distribution** (generated by `BoardGenerationService`): - **Regular**: ~40% (no special card) - **Positive**: ~25% (reward card) - **Negative**: ~25% (penalty card) - **Luck**: ~10% (instant consequence) **Important**: Fields have NO consequences. Consequences come only from **cards**. --- ### GameCard ```typescript /** * Card in a deck */ export interface GameCard { cardid: string; question?: string; answer?: any; // Can be string, number, object, array type?: CardType; consequence?: Consequence | null; // Only for LUCK and JOKER cards played?: boolean; playerid?: string; } /** * Card types */ export enum CardType { QUIZ = 0, // Multiple choice (A/B/C/D) SENTENCE_PAIRING = 1, // Match left parts to right parts OWN_ANSWER = 2, // Free text answer TRUE_FALSE = 3, // Boolean answer CLOSER = 4, // Closest number answer JOKER = 5, // Gamemaster decision (secondary landing only) LUCK = 6 // Instant consequence } ``` **Card Answer Formats by Type:** | Card Type | Answer Format in Database | Player Answer Format | Description | |-----------|---------------------------|----------------------|-------------| | **QUIZ** | `[{answer:"A", text:"...", correct:true}, ...]` | `"A"` (string) | Multiple choice with 4 options | | **SENTENCE_PAIRING** | `[{left:"Apple", right:"Red"}, ...]` | `[{pairId, leftText, rightText}, ...]` | Match left to right parts | | **SENTENCE_PAIRING (legacy)** | `"word1 word2 word3"` (string) | `["word1", "word2", "word3"]` or `"word1 word2 word3"` | Reconstruct scrambled sentence | | **OWN_ANSWER** | `["answer1", "answer2", ...]` (string[]) | `"player answer"` (string) | Free text with acceptable answers | | **TRUE_FALSE** | `true` or `false` (boolean) | `true/false` or `"true"/"false"` | True/False question | | **CLOSER** | `{correct: 42, percent: 10}` (object) | `40` (number) | Closest number within percentage | | **JOKER** | Not applicable | Not applicable | Gamemaster decides | | **LUCK** | Not applicable | Not applicable | Instant consequence | --- ### Consequence Located in: `src/Domain/Deck/DeckAggregate.ts` ```typescript /** * Card consequence structure */ export interface Consequence { type: ConsequenceType; value?: number; // Magnitude of effect (default: 1) } /** * Consequence types */ export enum ConsequenceType { MOVE_FORWARD = 0, // Move forward X steps MOVE_BACKWARD = 1, // Move backward X steps LOSE_TURN = 2, // Skip X turns EXTRA_TURN = 3, // Get X extra turns GO_TO_START = 5 // Return to position 1 } ``` **Multi-Turn Support**: - `LOSE_TURN` with `value=3`: Player skips next 3 turns - `EXTRA_TURN` with `value=2`: Player gets 2 additional turns after current --- ### GameDeck ```typescript /** * Deck of cards */ export interface GameDeck { deckid: string; decktype: DeckType; cards: GameCard[]; } /** * Deck types */ export enum DeckType { POSITIVE = 0, // For positive fields NEGATIVE = 1, // For negative fields LUCK = 2, // For luck fields JOKER = 3 // For secondary landings } ``` --- ## Complete Game Scenarios ### Scenario 1: Question Card on Positive Field (Correct Answer) ``` 1. Player at position 20, rolls dice = 4 2. Field at position 24 has stepValue = 2, type = "positive" 3. Player lands on position 24 4. Server waits 2 seconds (frontend animation) 5. Server draws POSITIVE deck card (type = QUIZ) 6. Server broadcasts: game:card-drawn (everyone sees question) 7. Server emits: game:card-drawn-self to player (60s timer) 8. Player answers correctly 9. Server broadcasts: game:answer-submitted 10. Server broadcasts: game:answer-validated { isCorrect: true } 11. Server determines: positive field + correct = GUESS REQUIRED 12. Server emits: game:position-guess-request to player (30s timer) - Shows: currentPosition=20, dice=4, stepValue=2, patternModifier=+2 13. Player calculates: 20 + 4 + 2 + 2 = 28, guesses 28 14. Server broadcasts: game:position-guess-broadcast { guessedPosition: 28 } 15. Server calculates: 20 + 4 + 2 + 2 = 28 (correct!) 16. Server broadcasts: game:guess-result { guessCorrect: true, finalPosition: 28, penaltyApplied: false } 17. Player position updated to 28 18. Server checks: position 28 is not special field 19. Server calls: advanceTurn() → next player's turn ``` --- ### Scenario 2: Question Card on Negative Field (Wrong Answer) ``` 1. Player at position 50, rolls dice = 5 2. Field at position 55 has stepValue = 1, type = "negative" 3. Player lands on position 55 4. Server waits 2 seconds 5. Server draws NEGATIVE deck card (type = TRUE_FALSE) 6. Server broadcasts: game:card-drawn 7. Server emits: game:card-drawn-self to player 8. Player answers incorrectly 9. Server broadcasts: game:answer-validated { isCorrect: false } 10. Server determines: negative field + wrong = GUESS REQUIRED (penalty test) 11. Server emits: game:position-guess-request - Shows: currentPosition=50, dice=5, stepValue=1, patternModifier=+1 12. Player guesses: 57, but actual is: 50 + 5 + 1 + 1 = 57 13. Server calculates actual: 57, player guessed: 57 (CORRECT!) 14. Server broadcasts: game:guess-result { guessCorrect: true, finalPosition: 57, penaltyApplied: false } 15. Player moves to 57 (avoided penalty despite wrong answer) 16. Server advances turn ``` --- ### Scenario 3: Luck Card with Multi-Turn EXTRA_TURN ``` 1. Player at position 30, rolls dice = 3 2. Field at position 33 has type = "luck" 3. Player lands on position 33 4. Server draws LUCK deck card 5. Card has: { type: EXTRA_TURN (3), value: 3 } 6. Server broadcasts: game:card-result 7. Server broadcasts: game:luck-consequence { consequenceType: "EXTRA_TURN", value: 3, extraTurns: 3 } 8. Server calls: setPlayerExtraTurns(gameCode, playerId, 3) - Redis: player_extra_turns:{gameCode}:{playerId} = 3 9. Server calls: advanceTurn() 10. advanceTurn() checks: getPlayerExtraTurns() = 3 > 0 11. Server emits: game:extra-turn-remaining { remainingExtraTurns: 2 } 12. Same player continues (no turn advance) 13. Player rolls again... 14. After turn ends, advanceTurn() checks again: extraTurns = 2 > 0 15. Process repeats until extraTurns = 0 16. Player gets 4 total turns (1 original + 3 extra) ``` --- ### Scenario 4: Luck Card with Multi-Turn LOSE_TURN ``` 1. Player A draws luck card: { type: LOSE_TURN (2), value: 2 } 2. Server calls: setPlayerTurnsToLose(gameCode, playerA, 2) - Redis: player_turns_to_lose:{gameCode}:{playerA} = 2 3. Server broadcasts: game:luck-consequence { consequenceType: "LOSE_TURN", turnsToLose: 2 } 4. Server calls: advanceTurn() → skip to Player B --- Later, when it's Player A's turn in sequence --- 5. advanceTurn() reaches Player A in turn sequence 6. Checks: getPlayerTurnsToLose(playerA) = 2 > 0 7. Decrements: 2 → 1 8. Adds to skippedPlayers array 9. Continues to next player in sequence (Player B) 10. Server broadcasts: game:players-skipped { skippedPlayers: [{ playerName: "Player A", remainingTurnsToLose: 1 }] } 11. Server broadcasts: game:turn-changed { currentPlayer: "Player B" } --- Next round --- 12. advanceTurn() reaches Player A again 13. Checks: getPlayerTurnsToLose(playerA) = 1 > 0 14. Decrements: 1 → 0, deletes Redis key 15. Skips Player A again 16. Player A skipped 2 times total ``` --- ### Scenario 5: Joker Card on Positive Field (Approved by Gamemaster) ``` 1. Player completes question card, lands on position 70 (positive field) 2. Server checks secondary landing 3. Server draws JOKER deck card 4. Server broadcasts: game:joker-drawn { waitingForGamemaster: true } 5. Server emits to gamemaster: game:gamemaster-decision-request (120s timer) 6. Gamemaster approves 7. Server broadcasts: game:gamemaster-decision-result { decision: "approve", approved: true } 8. Server determines: positive field + approved = GUESS REQUIRED 9. Server emits: game:joker-position-guess-request - Shows: currentPosition=70, dice=6, stepValue=1, patternModifier=-2 10. Player guesses: 75, actual: 70 + 6 + 1 - 2 = 75 (CORRECT!) 11. Server broadcasts: game:joker-complete { guessCorrect: true, moved: true, finalPosition: 75 } 12. Player moves to 75 13. Server checks for tertiary landing (not allowed - recursion depth) 14. Server advances turn ``` --- ### Scenario 6: Multiple Players with Turn Tracking ``` Turn Sequence: [PlayerA, PlayerB, PlayerC] --- Turn 1 --- PlayerA: Gets EXTRA_TURN (value=2) - Redis: player_extra_turns:game:PlayerA = 2 - PlayerA plays again --- Turn 2 (PlayerA extra turn 1) --- PlayerA: Gets LOSE_TURN (value=1) - Redis: player_turns_to_lose:game:PlayerA = 1 - Redis: player_extra_turns:game:PlayerA = 1 (still has 1 left) - advanceTurn() checks: extraTurns = 1 > 0 - PlayerA plays AGAIN (extra turn takes priority) --- Turn 3 (PlayerA extra turn 2) --- PlayerA: Normal turn - Redis: player_extra_turns:game:PlayerA = 0 (deleted) - advanceTurn() → next player --- Turn 4 --- Sequence reaches PlayerA - Checks: turnsToLose = 1 > 0 - Skip PlayerA, decrement to 0 - Server broadcasts: game:players-skipped - PlayerB plays --- Turn 5 --- PlayerB: Normal turn --- Turn 6 --- PlayerC: Normal turn --- Turn 7 --- Back to PlayerA (turnsToLose = 0) - PlayerA plays normally ``` --- ## Turn Tracking System ### Redis Storage ```typescript // Extra turns storage player_extra_turns:{gameCode}:{playerId} → "3" // Number as string TTL: None (cleared on cleanup or use) // Turns to lose storage player_turns_to_lose:{gameCode}:{playerId} → "2" // Number as string TTL: None (cleared on cleanup or use) ``` ### Turn Tracking Methods ```typescript class GameWebSocketService { // Extra turns private async setPlayerExtraTurns( gameCode: string, playerId: string, count: number ): Promise; private async getPlayerExtraTurns( gameCode: string, playerId: string ): Promise; // Returns 0 if not found private async decrementPlayerExtraTurns( gameCode: string, playerId: string ): Promise; // Decrements or deletes if reaches 0 // Turns to lose private async setPlayerTurnsToLose( gameCode: string, playerId: string, count: number ): Promise; private async getPlayerTurnsToLose( gameCode: string, playerId: string ): Promise; // Returns 0 if not found private async decrementPlayerTurnsToLose( gameCode: string, playerId: string ): Promise; // Decrements or deletes if reaches 0 // Cleanup private async clearPlayerTurnData( gameCode: string, playerId: string ): Promise; // Deletes both keys } ``` ### Enhanced advanceTurn() Logic ```typescript private async advanceTurn(gameCode: string): Promise { // PHASE 1: Check if current player has extra turns const extraTurns = await this.getPlayerExtraTurns(gameCode, currentPlayerId); if (extraTurns > 0) { await this.decrementPlayerExtraTurns(gameCode, currentPlayerId); emit('game:extra-turn-remaining', { remainingExtraTurns: extraTurns - 1 }); emit('game:your-turn', { isExtraTurn: true }); return; // Same player continues } // PHASE 2: Find next player, skipping those with lost turns let nextTurnIndex = (currentTurnIndex + 1) % turnSequence.length; const skippedPlayers = []; let loopGuard = 0; while (loopGuard < turnSequence.length) { const candidatePlayerId = turnSequence[nextTurnIndex]; const turnsToLose = await this.getPlayerTurnsToLose(gameCode, candidatePlayerId); if (turnsToLose > 0) { await this.decrementPlayerTurnsToLose(gameCode, candidatePlayerId); skippedPlayers.push({ playerId: candidatePlayerId, remainingTurnsToLose: turnsToLose - 1 }); nextTurnIndex = (nextTurnIndex + 1) % turnSequence.length; loopGuard++; } else { break; // Found valid player } } // PHASE 3: Update game state gameState.currentTurn = nextTurnIndex; gameState.currentPlayer = turnSequence[nextTurnIndex]; // PHASE 4: Notify about skipped players if (skippedPlayers.length > 0) { emit('game:players-skipped', { skippedPlayers }); } // PHASE 5: Notify about turn change emit('game:turn-changed', { currentPlayer, currentPlayerName }); } ``` --- ## Position Guessing Mechanic ### Pattern-Based Movement Calculation **Formula**: ``` finalPosition = currentPosition + dice + stepValue + patternModifier ``` **Pattern Modifiers by Zone**: ```typescript private getPatternModifier(position: number): number { if (position <= 20) return 2; // Positions 1-20 if (position <= 40) return -1; // Positions 21-40 if (position <= 60) return 1; // Positions 41-60 if (position <= 80) return -2; // Positions 61-80 return 3; // Positions 81-100 } ``` ### Guess Requirement Logic ```typescript private determineGuessRequirement( fieldType: 'regular' | 'positive' | 'negative' | 'luck', answerCorrect: boolean ): boolean { if (fieldType === 'positive') { return answerCorrect; // Correct = guess for REWARD } else if (fieldType === 'negative') { return !answerCorrect; // Wrong = guess for PENALTY } return false; // Regular and luck fields never require guess } ``` ### Question Card Guess Flow **Matrix**: | Field Type | Answer | Guess Required? | Reason | |------------|---------|-----------------|---------------------------| | Positive | Correct | **YES** | Reward scenario | | Positive | Wrong | NO | No movement | | Negative | Correct | NO | Avoided penalty | | Negative | Wrong | **YES** | Penalty test | | Regular | Any | NO | No special fields | | Luck | N/A | NO | Instant consequence | ### Joker Card Guess Flow **Matrix** (dice always = 6): | Field Type | Gamemaster | Guess Required? | Movement | |------------|------------|-----------------|---------------------| | Positive | Approved | **YES** | Move if guess | | Positive | Rejected | NO | No movement | | Negative | Approved | NO | No movement (avoid) | | Negative | Rejected | **YES** | Move if guess | ### Penalty System - **Wrong guess**: -2 steps from calculated position - **Minimum position**: 1 (can't go below start) - **Applied after calculation**: `finalPosition = max(1, calculatedPosition - 2)` ### Calculation Examples **Example 1**: Position 15, dice 4, stepValue 2 ``` patternModifier = 2 (position 15 is in zone 1-20) calculation = 15 + 4 + 2 + 2 = 23 ``` **Example 2**: Position 35, dice 6, stepValue 1 ``` patternModifier = -1 (position 35 is in zone 21-40) calculation = 35 + 6 + 1 - 1 = 41 ``` **Example 3**: Position 75 (joker), stepValue 3 ``` dice = 6 (always for jokers) patternModifier = -2 (position 75 is in zone 61-80) calculation = 75 + 6 + 3 - 2 = 82 if wrong guess: 82 - 2 = 80 ``` --- ## Error Handling ### REST API Error Responses **Format**: ```typescript { error: string; // Human-readable error message code?: string; // Optional error code } ``` **Status Codes**: - `400`: Bad Request (validation errors, missing fields) - `401`: Unauthorized (authentication required) - `403`: Forbidden (insufficient permissions, org mismatch) - `404`: Not Found (game, deck, user not found) - `409`: Conflict (game full, already started, player already in game) - `500`: Internal Server Error ### WebSocket Error Events ```typescript socket.on('game:error', { message: string; code?: string; timestamp: string; }); ``` **Common Errors**: - Authentication failed - Invalid game code - Game already started - Not your turn - Invalid answer format - Timeout expired - No cards available - Internal server error ### Timeout Handling **Card Answer Timeout** (60 seconds): ```typescript // Server auto-processes as wrong answer socket.on('game:card-timeout', { playerName: string; message: string; timestamp: string; }); ``` **Gamemaster Decision Timeout** (120 seconds): ```typescript // Server auto-rejects joker card socket.on('game:gamemaster-timeout', { requestId: string; playerName: string; message: string; timestamp: string; }); ``` **Position Guess Timeout** (30 seconds): ```typescript // Server treats as no movement socket.on('game:guess-timeout', { playerName: string; message: string; timestamp: string; }); ``` ### Redis Cleanup **Automatic Cleanup**: - Game end: All keys deleted - Player disconnect: Pending states cleared - Timeout: Keys expire naturally (TTL) **Manual Cleanup**: ```typescript // Called on game:ended or disconnect await cleanupGameData(gameCode, gameId); ``` **Keys Cleaned**: ``` gameplay:{gameCode} game_state:{gameCode} game_board_{gameCode} game_connections:{gameCode} game_ready:{gameCode} game_pending:{gameCode} game_positions:{gameCode} pending_card:{gameCode}:{playerId} pending_decision:{gameCode}:{requestId} player_extra_turns:{gameCode}:{playerId} player_turns_to_lose:{gameCode}:{playerId} ``` --- ## Card Type Implementation Details ### SENTENCE_PAIRING Card Type **Purpose**: Test player's ability to match related items (left parts to right parts) **Use Cases**: - Match words to definitions - Match questions to answers - Match countries to capitals - Match names to descriptions - Match terms to translations #### Database Format (NEW) ```typescript // In DeckAggregate Card.answer field { text: "Match each fruit to its color", type: CardType.SENTENCE_PAIRING, // 1 answer: [ { left: "Apple", right: "Red" }, { left: "Banana", right: "Yellow" }, { left: "Orange", right: "Orange" }, { left: "Grape", right: "Purple" } ] } ``` #### Client Preparation Server scrambles the **right parts** while keeping left parts in order: ```typescript // Sent to client in game:card-drawn-self { cardData: { question: "Match each fruit to its color", type: 1, // SENTENCE_PAIRING sentencePairs: [ { id: "pair_0", left: "Apple", right: "Purple" }, // Scrambled! { id: "pair_1", left: "Banana", right: "Orange" }, { id: "pair_2", left: "Orange", right: "Red" }, { id: "pair_3", left: "Grape", right: "Yellow" } ], timeLimit: 60 } } ``` #### Player Answer Format ```typescript // Player submits array of matches { answer: [ { pairId: "pair_0", leftText: "Apple", rightText: "Red" }, { pairId: "pair_1", leftText: "Banana", rightText: "Yellow" }, { pairId: "pair_2", leftText: "Orange", rightText: "Orange" }, { pairId: "pair_3", leftText: "Grape", rightText: "Purple" } ] } ``` #### Validation Logic ```typescript // For each correct pair in database: 1. Find player's match for the left part 2. Compare player's chosen right part to correct right part 3. Case-insensitive comparison with trimmed whitespace 4. ALL pairs must match correctly for answer to be correct // Example validation result: { isCorrect: true, // Only if ALL pairs match submittedAnswer: [...], correctAnswer: [...], explanation: "✅ Perfect! All 4 pairs matched correctly! ✓ \"Apple\" → \"Red\" ✓ \"Banana\" → \"Yellow\" ✓ \"Orange\" → \"Orange\" ✓ \"Grape\" → \"Purple\"" } ``` #### Partial Match Example ```typescript // Player gets 2 out of 4 correct: { isCorrect: false, explanation: "❌ Only 2/4 pairs correct: ✓ \"Apple\" → \"Red\" ✗ \"Banana\" → \"Purple\" (should be \"Yellow\") ✗ \"Orange\" → \"Yellow\" (should be \"Orange\") ✓ \"Grape\" → \"Purple\"" } ``` #### Legacy Format (Backward Compatibility) **Old Database Format**: ```typescript { text: "The quick brown fox jumps over the lazy dog", type: CardType.SENTENCE_PAIRING, answer: "The quick brown fox jumps over the lazy dog" // String } ``` **Client Preparation (Legacy)**: ```typescript { cardData: { question: "Arrange the words to form a sentence:", type: 1, words: ["lazy", "The", "dog", "fox", "over", "quick", "brown", "jumps", "the"], timeLimit: 60 } } ``` **Player Answer (Legacy)**: ```typescript { answer: ["The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"] // OR answer: "The quick brown fox jumps over the lazy dog" } ``` #### Frontend Implementation Example ```typescript // React/Vue component for SENTENCE_PAIRING interface Props { sentencePairs: SentencePair[]; onSubmit: (answer: SentencePairingAnswer[]) => void; } function SentencePairingCard({ sentencePairs, onSubmit }: Props) { const [matches, setMatches] = useState>(new Map()); const handleMatch = (pairId: string, leftText: string, rightText: string) => { setMatches(prev => new Map(prev).set(pairId, rightText)); }; const handleSubmit = () => { const answer: SentencePairingAnswer[] = sentencePairs.map(pair => ({ pairId: pair.id, leftText: pair.left, rightText: matches.get(pair.id) || '' })); onSubmit(answer); }; return (
{sentencePairs.map(pair => (
{pair.left}
))}
); } ``` #### Database Migration **No migration needed!** The implementation supports both formats: 1. **New cards**: Use array of `{left, right}` objects 2. **Existing cards**: Continue working with string format 3. Detection is automatic based on `answer` field type **Creating New Sentence Pairing Cards**: ```sql -- Example INSERT for new format INSERT INTO "Cards" (deck_id, text, type, answer) VALUES ( 'deck-uuid', 'Match programming languages to their creators', 1, -- SENTENCE_PAIRING '[ {"left": "Python", "right": "Guido van Rossum"}, {"left": "JavaScript", "right": "Brendan Eich"}, {"left": "C++", "right": "Bjarne Stroustrup"}, {"left": "Ruby", "right": "Yukihiro Matsumoto"} ]'::jsonb ); ``` --- ## Appendix: Complete Event Summary ### Client → Server Events | Event | Data | Description | |-------|------|-------------| | `game:join` | `{ gameToken: string }` | Join game room with auth token | | `game:ready` | `{ gameCode: string, ready: boolean }` | Mark player as ready/not ready | | `game:dice-roll` | `{ gameCode: string, diceValue: number }` | Roll dice (1-6) | | `game:card-answer` | `{ gameCode: string, answer: any }` | Submit card answer | | `game:gamemaster-decision` | `{ gameCode: string, requestId: string, decision: string }` | Gamemaster decision on joker | | `game:position-guess` | `{ gameCode: string, guessedPosition: number }` | Submit position guess (question) | | `game:joker-position-guess` | `{ gameCode: string, guessedPosition: number }` | Submit position guess (joker) | | `game:leave` | `{ gameCode: string }` | Leave game | ### Server → Client Events | Event | Audience | Description | |-------|----------|-------------| | `authenticated` | Individual | Auth success, joined rooms | | `game:player-joined` | All | Player joined game | | `game:player-ready` | All | Player ready status changed | | `game:started` | All | Game started, board generated | | `game:turn-changed` | All | Turn advanced to next player | | `game:your-turn` | Individual | Your turn notification | | `game:dice-rolled` | All | Dice rolled by player | | `game:player-moved` | All | Player moved to new position | | `game:card-drawn` | All | Card drawn (question shown) | | `game:card-drawn-self` | Individual | Interactive card data | | `game:answer-submitted` | All | Answer submitted (pre-validation) | | `game:answer-validated` | All | Answer validation result | | `game:position-guess-request` | Individual | Request position guess | | `game:position-guess-broadcast` | All | Player's guess shown | | `game:guess-result` | All | Guess result with calculation | | `game:no-movement` | All | Player didn't move | | `game:penalty-avoided` | All | Player avoided penalty | | `game:luck-consequence` | All | Luck card consequence applied | | `game:joker-drawn` | All | Joker card drawn | | `game:gamemaster-decision-request` | Gamemaster | Decision request | | `game:gamemaster-decision-result` | All | Decision result | | `game:joker-position-guess-request` | Individual | Joker position guess request | | `game:joker-complete` | All | Joker card processing complete | | `game:extra-turn-remaining` | All | Player using extra turn | | `game:players-skipped` | All | Players skipped (lost turns) | | `game:ended` | All | Game ended, winner declared | | `game:cleanup-complete` | All | Cleanup finished | | `game:error` | Individual | Error occurred | | `game:card-error` | All | Card drawing error | --- **End of Documentation** For additional details, see: - `FRONTEND_WEBSOCKET_EVENTS_REFERENCE.md` - Frontend event handling guide - `DATABASE_MANAGEMENT_GUIDE.md` - Database schema and queries - `BUILD.md` - Build and deployment instructions