Room Code API โ€” Generate, Validate & Manage Game Room Codes for Ludo Multiplayer

Room codes are the bridge between human-friendly game invitations and machine-optimized backend infrastructure. When a player wants to invite a friend to a private Ludo match, they shouldn't need an account ID or a QR scan โ€” a 6-character alphanumeric string shared via any messaging app is all it takes. This guide covers the complete design of a room code API layer: generation algorithms with collision detection, case-sensitivity trade-offs, Redis-backed TTL expiration, room lifecycle state machines, spectator and privacy modes, and full production-ready implementations in Python and Node.js.

The Room Code Lifecycle โ€” From Creation to Expiration

Every room progresses through a well-defined lifecycle. Understanding this lifecycle is essential before writing any code, because every API endpoint and WebSocket event exists to serve a specific stage. The typical lifecycle for a Ludo room follows this sequence:

1. Active (or "waiting") โ€” The room has been created and is open for players to join. The host can adjust settings, and the room code is discoverable by anyone who has it. This is the lobby phase where friends enter the code and stream in.

2. Starting (or "countdown") โ€” The host has initiated the game start, or the minimum player threshold has been met. New players can no longer join. The room transitions from an open lobby to a locked session.

3. Playing โ€” The actual game is underway. The room is locked, game state is synchronized via WebSocket, and no structural changes (adding/removing players) are permitted. This phase can last anywhere from 5 minutes for a quick game to 45 minutes for a full-board match.

4. Ended โ€” The game has concluded. Results are recorded, leaderboards updated, and the room is marked as finished. At this stage, the room is read-only โ€” it exists for result retrieval and post-game chat, but no game actions are accepted.

5. Expired (or "archived") โ€” The TTL has elapsed or an admin has manually closed the room. The room data is removed from active storage and moved to an archive or deleted entirely. The code string becomes available for reuse.

Designing your API to respect these states explicitly prevents a wide range of bugs. For instance, a player attempting to join a "playing" room should receive a clear GAME_IN_PROGRESS error rather than a generic 400. A player requesting the room state during the "expired" phase should get a 404 with a helpful message suggesting the code may have expired.

Room Code Generation โ€” Algorithms, Collision Detection & Format Options

Room code generation is deceptively simple: pick random characters from an alphabet and check for uniqueness. In practice, several design decisions compound into meaningful consequences for security, usability, and performance.

Choosing a Code Format

The format you choose affects three things: the number of possible combinations, how easy the code is to read and type, and the attack surface for brute-force enumeration.

Format Comparison Table

Format Alphabet Combinations Best For
4-char alphanumeric A-Z + 0-9 (32 chars) ~1M (32โด) Very short sessions, fast expiry
6-char alphanumeric A-Z + 0-9 (32 chars) ~1B (32โถ) Standard casual games (recommended)
6-char mixed case A-Z + a-z + 0-9 (62 chars) ~56B (62โถ) Higher security, longer sessions
8-char alphanumeric A-Z + 0-9 (32 chars) ~33B (32โธ) Tournament modes, extended sessions
Emoji grid 16 emoji from ๐ŸŽฒ๐ŸŸข๐Ÿ”ด๐ŸŸกโฌœโšซ ~43M (6โด grid) Visual sharing, social games

For most casual Ludo implementations, 6-character uppercase alphanumeric codes with confusing character removal (stripping 0/O, 1/I/L) offer the best balance. This gives a 32-character alphabet, yielding approximately 1 billion combinations โ€” enough that brute-force attacks are computationally infeasible at normal traffic volumes.

Collision Detection โ€” The Critical Implementation Detail

Collision detection is the process of ensuring a newly generated code doesn't already exist in active use. Without it, your system could create duplicate rooms, leading to players accidentally joining the wrong game or being rejected when the code they received belongs to someone else's session.

The naive approach โ€” generate a code, check if it exists in the database, regenerate if it does โ€” works for low-traffic applications but breaks under concurrent load. Two simultaneous requests can generate the same code, both find it "available" (because neither has written yet), and both create rooms with identical codes.

The correct approach uses atomic operations. With Redis, the SET key value NX EX ttl command sets a key only if it doesn't exist, atomically. This eliminates the race window entirely. Our Python implementation below demonstrates this pattern.

Case Sensitivity โ€” What to Normalize and When

Should "AbCd12" and "abcd12" be treated as the same room? For most use cases, yes โ€” forcing players to match exact case is a poor user experience, especially on mobile keyboards where caps lock is easy to accidentally toggle. Normalize all codes to uppercase server-side before any lookup or comparison.

There is one exception: if you implement a two-part code system where the first 4 characters identify the room and the last 2 are a player-specific token, case sensitivity may be intentional for the token portion. In this hybrid model, the room identifier is case-insensitive (normalized to uppercase) while the player token is case-sensitive (preserving original casing). This design is useful for "join links" where the URL encodes both room and player identity.

Python Implementation โ€” Complete Room Code Generator with Redis

This implementation covers all the concepts discussed above: cryptographically secure generation using the secrets module, atomic Redis collision checks with SET NX EX, TTL-based expiration, and a full room state machine with player management.

Python
import secrets
import string
import json
import redis
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional, Dict, Any, List

# โ”€โ”€ Alphabet Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Remove visually confusing characters to reduce mistyping.
BASE_ALPHABET = string.ascii_uppercase + string.digits
CLEAN_ALPHABET = (
    BASE_ALPHABET
    .replace('0', '')
    .replace('O', '')
    .replace('1', '')
    .replace('I', '')
    .replace('L', '')
)

# โ”€โ”€ Room State Enum โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class RoomState(Enum):
    WAITING   = "waiting"
    STARTING  = "starting"
    PLAYING   = "playing"
    ENDED     = "ended"
    EXPIRED   = "expired"

# โ”€โ”€ Redis Client โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

# โ”€โ”€ Room Code Generator โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def generate_room_code(
    length: int = 6,
    alphabet: str = CLEAN_ALPHABET,
    ttl_hours: float = 4,
    max_attempts: int = 10
) -> str:
    """
    Generate a unique room code using atomic Redis SET NX EX.

    The SET NX EX combination ensures that even under concurrent load,
    no two requests can claim the same code. SET returns True only if
    the key did not exist โ€” making the check-and-insert atomic.
    """
    ttl_seconds = int(ttl_hours * 3600)

    for _ in range(max_attempts):
        code = ''.join(secrets.choice(alphabet) for _ in range(length))
        room_key = f"room:{code}"

        room_data = {
            "code": code,
            "status": RoomState.WAITING.value,
            "players": [],
            "max_players": 4,
            "host_id": None,
            "created_at": datetime.utcnow().isoformat(),
            "started_at": None,
            "ended_at": None,
            "privacy": "public",   # public | private | invite_only
            "spectator_code": None,
            "game_config": {
                "board_size": "standard",
                "time_limit_per_turn": None,
                "allow_bots": True
            }
        }

        # SET key json NX EX ttl โ€” atomic set-if-not-exists with expiry
        was_set = r.set(
            room_key,
            json.dumps(room_data),
            nx=True,
            ex=ttl_seconds
        )
        if was_set:
            return code

    raise RuntimeError(
        f"Failed to generate unique room code after {max_attempts} attempts. "
        "Consider increasing the alphabet or reducing TTL."
    )

# โ”€โ”€ Room Retrieval โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def get_room(code: str) -> Optional[Dict[str, Any]]:
    """Retrieve a room by code, normalized to uppercase. Returns None if expired."""
    room_key = f"room:{code.upper().strip()}"
    raw = r.get(room_key)
    return json.loads(raw) if raw else None

# โ”€โ”€ Player Join with Atomic Capacity Check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def join_room(
    code: str,
    player_id: str,
    player_name: str,
    is_host: bool = False
) -> Dict[str, Any]:
    """Add a player to a room with atomic capacity enforcement."""
    room = get_room(code)
    if not room:
        return {"success": False, "error": "ROOM_NOT_FOUND",
                "message": "Room not found or has expired."}

    state = room["status"]
    if state == RoomState.PLAYING.value:
        return {"success": False, "error": "GAME_IN_PROGRESS",
                "message": "This game is already in progress."}
    if state == RoomState.ENDED.value:
        return {"success": False, "error": "GAME_ENDED",
                "message": "This game has already ended."}
    if state == RoomState.EXPIRED.value:
        return {"success": False, "error": "ROOM_EXPIRED",
                "message": "This room has expired."}

    if len(room["players"]) >= room["max_players"]:
        return {"success": False, "error": "ROOM_FULL",
                "message": "This room is full."}

    # Atomic capacity check using Redis Lua script
    lua_script = """
    local room_key = KEYS[1]
    local max_players = tonumber(ARGV[1])
    local player_json = ARGV[2]
    local ttl = tonumber(ARGV[3])
    local room = redis.call('GET', room_key)
    if not room then return {-1, 'ROOM_NOT_FOUND'} end
    local data = cjson.decode(room)
    if #data.players >= max_players then return {0, 'ROOM_FULL'} end
    table.insert(data.players, cjson.decode(player_json))
    if data.status == 'waiting' and #data.players == data.max_players then
        data.status = 'starting'
    end
    redis.call('SET', room_key, cjson.encode(data), 'EX', ttl)
    return {1, 'OK'}
    """

    player = {
        "id": player_id,
        "name": player_name,
        "is_host": is_host,
        "joined_at": datetime.utcnow().isoformat()
    }
    remaining_ttl = r.ttl(f"room:{code.upper()}")
    result = r.eval(lua_script, 1,
                    f"room:{code.upper()}",
                    room["max_players"],
                    json.dumps(player),
                    remaining_ttl)

    if result[0] == 1:
        updated_room = get_room(code)
        return {"success": True, "room": updated_room,
                "state": updated_room["status"]}
    else:
        return {"success": False, "error": result[1],
                "message": result[1]}

# โ”€โ”€ State Transition โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def transition_room_state(code: str, new_state: RoomState) -> Dict[str, Any]:
    """Transition a room to a new state, enforcing valid transitions."""
    valid_transitions = {
        RoomState.WAITING:  {RoomState.STARTING, RoomState.EXPIRED},
        RoomState.STARTING: {RoomState.PLAYING,  RoomState.WAITING, RoomState.EXPIRED},
        RoomState.PLAYING:  {RoomState.ENDED,    RoomState.EXPIRED},
        RoomState.ENDED:    {RoomState.EXPIRED},
        RoomState.EXPIRED:   {RoomState.EXPIRED},
    }

    room = get_room(code)
    if not room:
        return {"success": False, "error": "ROOM_NOT_FOUND"}

    current = RoomState(room["status"])
    if new_state not in valid_transitions.get(current, set()):
        return {"success": False,
                "error": "INVALID_TRANSITION",
                "message": f"Cannot transition from {current.value} to {new_state.value}."}

    room["status"] = new_state.value
    if new_state == RoomState.PLAYING:
        room["started_at"] = datetime.utcnow().isoformat()
    elif new_state in {RoomState.ENDED, RoomState.EXPIRED}:
        room["ended_at"] = datetime.utcnow().isoformat()

    remaining_ttl = r.ttl(f"room:{code.upper()}")
    r.setex(f"room:{code.upper()}", remaining_ttl, json.dumps(room))
    return {"success": True, "room": room}

# โ”€โ”€ Spectator Code Generator โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def generate_spectator_code(room_code: str) -> Optional[str]:
    """Generate a read-only spectator code for a room."""
    room = get_room(room_code)
    if not room:
        return None
    # Spectator codes use a different alphabet (lower case) to visually distinguish
    spec_alphabet = string.ascii_lowercase + string.digits
    spec_code = ''.join(secrets.choice(spec_alphabet) for _ in range(4))
    room["spectator_code"] = spec_code
    remaining_ttl = r.ttl(f"room:{room_code.upper()}")
    r.setex(f"room:{room_code.upper()}", remaining_ttl, json.dumps(room))
    return spec_code

# โ”€โ”€ Demo Usage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if __name__ == "__main__":
    code = generate_room_code(length=6, ttl_hours=4)
    print(f"Created room: {code}")

    result = join_room(code, "player_001", "Rahul", is_host=True)
    print(f"Join result: {result}")

    result = transition_room_state(code, RoomState.STARTING)
    print(f"State transition: {result}")

    spec = generate_spectator_code(code)
    print(f"Spectator code: {spec}")

Node.js / Express REST API โ€” Complete Room Endpoints

The REST layer handles all room lifecycle operations: creation, retrieval, joining, state transitions, and cleanup. This Express implementation includes input validation with express-validator, JWT-based authentication, rate limiting with express-rate-limit, and structured error responses.

Node.js
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const rateLimit = require('express-rate-limit');
const router = express.Router();

// โ”€โ”€ Rate Limiter for Room Operations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const roomLimiter = rateLimit({
  windowMs: 60 * 1000,   // 1 minute
  max: 30,               // 30 requests per minute per IP
  message: { success: false, error: 'RATE_LIMIT_EXCEEDED',
             message: 'Too many room requests. Slow down.' },
  standardHeaders: true,
  legacyHeaders: false,
});

// โ”€โ”€ Validation Middleware โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const validateRoomCode = [
  param('code')
    .trim()
    .toUpperCase()
    .isLength({ min: 4, max: 8 })
    .matches(/^[A-Z0-9]+$/)
    .withMessage('Room code must be 4-8 alphanumeric characters')
];

// โ”€โ”€ POST /api/rooms โ€” Create a New Room โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
router.post('/', roomLimiter, async (req, res) => {
  const { maxPlayers = 4, privacy = 'public', gameConfig = {} } = req.body;

  try {
    const code = await RoomService.generateRoomCode({
      length: 6,
      ttlHours: 4,
      maxPlayers,
      privacy,
      gameConfig
    });

    res.status(201).json({
      success: true,
      room: { code, status: 'waiting', expiresIn: 4 * 3600 },
      joinUrl: `ludogame://join/{code}`
    });
  } catch (err) {
    console.error('Room creation failed:', err);
    res.status(500).json({ success: false, error: 'INTERNAL_ERROR',
                              message: 'Failed to create room.' });
  }
});

// โ”€โ”€ GET /api/rooms/:code โ€” Get Room Info โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
router.get('/:code', validateRoomCode, async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ success: false, errors: errors.array() });
  }

  const { code } = req.params;
  const room = await RoomService.getRoom(code);

  if (!room) {
    return res.status(404).json({
      success: false, error: 'ROOM_NOT_FOUND',
      message: 'Room not found or has expired. Check the code and try again.'
    });
  }

  // Sanitized response โ€” hide internal IDs
  res.json({
    success: true,
    room: {
      code: room.code,
      status: room.status,
      playerCount: room.players.length,
      maxPlayers: room.maxPlayers,
      privacy: room.privacy,
      gameConfig: room.gameConfig,
      createdAt: room.createdAt,
      hasSpectatorCode: Boolean(room.spectatorCode)
    }
  });
});

// โ”€โ”€ POST /api/rooms/:code/join โ€” Join a Room โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
router.post('/:code/join', roomLimiter, [
  validateRoomCode,
  body('playerId').isString().notEmpty().isLength({ max: 64 }),
  body('playerName').isString().isLength({ min: 1, max: 20 })
], async (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ success: false, errors: errors.array() });
  }

  const { code } = req.params;
  const { playerId, playerName } = req.body;

  try {
    const result = await RoomService.joinRoom(code, playerId, playerName);
    if (!result.success) {
      const statusMap = {
        'ROOM_NOT_FOUND': 404,
        'ROOM_FULL': 409,
        'GAME_IN_PROGRESS': 409,
        'GAME_ENDED': 410,
        'ROOM_EXPIRED': 410
      };
      return res.status(statusMap[result.error] || 400).json(result);
    }
    res.json(result);
  } catch (err) {
    console.error('Join room failed:', err);
    res.status(500json({ success: false, error: 'INTERNAL_ERROR' });
  }
});

// โ”€โ”€ POST /api/rooms/:code/start โ€” Start the Game โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
router.post('/:code/start', [
  validateRoomCode
], async (req, res) => {
  const { code } = req.params;
  const result = await RoomService.transitionRoomState(code, 'playing');
  if (!result.success) {
    return res.status(400).json(result);
  }
  // Emit WebSocket event to notify all connected clients
  io.to(code).emit('room:game-start', { roomCode: code, state: 'playing' });
  res.json(result);
});

// โ”€โ”€ DELETE /api/rooms/:code โ€” Close a Room (Host Only) โ”€โ”€โ”€โ”€โ”€โ”€
router.delete('/:code', validateRoomCode, async (req, res) => {
  const { code } = req.params;
  const result = await RoomService.transitionRoomState(code, 'expired');
  if (!result.success) {
    return res.status(400).json(result);
  }
  io.to(code).emit('room:closed', { roomCode: code, reason: 'host_closed' });
  res.json({ success: true, message: 'Room closed successfully.' });
});

module.exports = router;

Redis TTL-Based Room Expiration โ€” How It Works

Redis EXPIRE (and its sibling SET ... EX) is the most efficient way to manage room lifetimes. When you set a key with EX, Redis attaches a countdown timer. Once the timer hits zero, Redis automatically deletes the key โ€” no cron jobs, no background tasks, no manual cleanup code required.

The critical implementation detail is preserving TTL on updates. When you modify room data (adding a player, changing state), you must call TTL to get the remaining lifetime, then reapply it with SETEX. Forgetting this step resets the TTL to the full duration, effectively extending the room's life every time it gets modified. A room that was supposed to expire in 10 minutes will live forever if a player joins every 9 minutes.

For tournament or ranked modes, consider using Redis sorted sets with timestamps as scores to manage batch expiration and archiving. When a room expires from Redis (detected via keyspace notifications), push its final state to a PostgreSQL archive table for replay storage and analytics.

Privacy Modes and Spectator Codes

Privacy modes control discoverability and access. Three standard modes cover the full range of use cases:

Public โ€” The room appears in a public lobby or matchmaking list. Any player can find and join it without a code. Useful for open play, quick matches, and drop-in-drop-out games.

Private โ€” The room does not appear in any public listing. Access requires the room code. This is the standard mode for friend games and private matches โ€” the default for casual Ludo play.

Invite Only โ€” The room is private and additionally restricted to a pre-approved player list. The host generates invite tokens that can only be redeemed once, and only by specific player IDs. This is the right mode for tournament brackets and scheduled matches.

Spectator codes are a separate code that grants read-only access to a room's game stream. Spectators can watch the game unfold in real-time but cannot interact โ€” they cannot roll dice, move pieces, or send chat messages. The spectator code uses a distinct format (lowercase, shorter length) to make it visually distinguishable from the player code.

WebSocket Real-Time Room Synchronization

The REST API handles room lifecycle โ€” creation, joining, state transitions. WebSocket handles everything that happens inside the game: dice rolls, piece movements, turn order, chat messages, and disconnect notifications. These two layers complement each other perfectly: REST for commands, WebSocket for high-frequency state streams.

After a successful POST /api/rooms/:code/join, the client should establish a WebSocket connection to receive real-time updates. The client subscribes to a channel namespaced by the room code, and the server pushes all game events to that channel.

JavaScript
// โ”€โ”€ WebSocket Room Subscription (Client-Side) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const socket = new WebSocket('wss://api.ludokingapi.site/v1/game');

socket.onopen = () => {
  // Authenticate and subscribe to the room channel
  socket.send(JSON.stringify({
    type: 'room:subscribe',
    roomCode: 'ABCD12',
    playerId: 'player_001',
    token: jwtToken
  }));
};

socket.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  switch (msg.type) {
    case 'room:player-joined':
      appendLog(`{msg.playerName} joined the room ({msg.playerCount}/{msg.maxPlayers})`);
      break;
    case 'room:player-left':
      appendLog(`{msg.playerName} left the room`);
      break;
    case 'room:game-start':
      appendLog('Game started!');
      setGamePhase('playing');
      break;
    case 'game:dice-roll':
      animateDiceRoll(msg.value, msg.playerId);
      break;
    case 'game:piece-move':
      animatePieceMove(msg.pieceId, msg.from, msg.to);
      break;
    case 'game:turn-update':
      highlightCurrentPlayer(msg.currentPlayerId);
      startTurnTimer(msg.turnTimeoutSeconds);
      break;
    case 'room:closed':
      showGameOver(msg.reason, msg.results);
      socket.close();
      break;
    default:
      console.log('Unknown event type:', msg.type);
  }
};

socket.onclose = (event) => {
  if (event.code !== 1000) {
    console.warn('WebSocket disconnected unexpectedly. Reconnecting...');
    setTimeout(reconnect, 2000);
  }
};

Security Checklist โ€” Production Deployment

Rate limiting โ€” Apply per-IP limits of 30 requests/minute on room creation and lookup endpoints. Use Redis-backed rate limiting for distributed environments where multiple server instances handle traffic.

Code enumeration protection โ€” Beyond rate limiting, implement a 5-second server-side cooldown after 3 consecutive failed lookups from the same IP. This makes automated enumeration take centuries.

JWT authentication on join โ€” Require a valid JWT token when joining a room. Player names alone are spoofable. The token proves the player has an authenticated session.

Host-only actions โ€” Only the player who created the room should be able to start the game, kick players, or close the room. Validate the host ID in the JWT against the room's stored host.

TTL enforcement โ€” Never allow TTL extension beyond the original expiry. Once a room is marked for expiration, resist pressure to "refresh" it โ€” this accumulates stale rooms.

Input sanitization โ€” Strip whitespace, normalize to uppercase, reject anything outside the known alphabet. Never pass a raw room code into a Redis key or SQL query without validation.

Frequently Asked Questions

Build Your Room Code System Today

Get the complete room code implementation package with Redis integration, WebSocket sync, state machine, spectator codes, and production security hardening.

Chat on WhatsApp