Real-Time Ludo API — WebSocket Events & Game State Synchronization

Real-time gameplay is what separates a Ludo app from a static website. When a player rolls a dice, every other player in the room needs to see that result within milliseconds — not after a page refresh or a polling interval. This guide covers the complete WebSocket implementation for real-time Ludo: connection lifecycle, authentication handshake, event taxonomy, state synchronization strategies, and a production-ready JavaScript client implementation.

Why WebSocket for Ludo?

HTTP is request-response by design — a client sends a request, the server responds, and the connection closes. For turn-based Ludo, this creates a dilemma: poll the server frequently to catch updates (wasteful), or wait for the next player action to discover what happened (unresponsive). WebSocket solves this by establishing a persistent, bidirectional connection. The server can push events to clients the moment something happens — dice rolls, piece movements, turn changes — with sub-100ms latency in the same connection.

For Ludo specifically, WebSocket is ideal because: game events are discrete and infrequent (not high-frequency like an FPS), every player in a room needs to see the same events simultaneously, and the state is authoritative on the server (prevents desync and cheating).

Connection Lifecycle

A WebSocket connection for Ludo goes through four phases: establishment, authentication, room subscription, and active gameplay. The connection remains open throughout the game session and closes when the game ends or a player disconnects.

Phase 1 — Connection Establishment

The client initiates a WebSocket handshake to the server URL. This is a standard HTTP upgrade request. Most libraries handle this automatically — you call new WebSocket(url) or io.connect(url) and the handshake happens behind the scenes.

Phase 2 — Authentication

Immediately after connecting, the client must authenticate. Pass your session token as part of the handshake auth object (Socket.IO) or in the first message after connection. The server validates the token and associates the WebSocket connection with the player identity. An unauthenticated connection should be closed after a short timeout (5–10 seconds) to prevent resource exhaustion.

Phase 3 — Room Subscription

Once authenticated, the client emits a room:join event with the room code. The server validates the room exists and has space, adds the player to the room's subscriber list, and responds with the full room state. The client then renders the board and UI based on this initial state.

Phase 4 — Active Gameplay

The client listens for game events and emits player actions. This is the bulk of the WebSocket's lifetime — typically 10–20 minutes for a casual Ludo game.

WebSocket Client — Complete Implementation

This is a production-ready WebSocket client for Ludo using Socket.IO v4. It handles connection, reconnection, authentication, room joining, and all game events. You can adapt this directly into your frontend codebase.

JavaScript
class LudoGameClient {
  constructor({ serverUrl, sessionToken, roomCode }) {
    this.serverUrl = serverUrl;
    this.sessionToken = sessionToken;
    this.roomCode = roomCode;
    this.socket = null;
    this.gameState = null;
    this.listeners = new Map();
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 5;
  }

  connect() {
    this.socket = io(this.serverUrl, {
      auth: { token: this.sessionToken },
      reconnection: true,
      reconnectionDelay: 1000,
      reconnectionDelayMax: 5000,
      reconnectionAttempts: this.maxReconnectAttempts,
      timeout: 10000
    });

    this.socket.on('connect', () => {
      console.log('Connected to game server');
      this.reconnectAttempts = 0;
      this.socket.emit('room:join', { roomCode: this.roomCode });
    });

    this.socket.on('connect_error', (err) => {
      console.error('Connection error:', err.message);
      if (err.message === 'Invalid token') {
        alert('Session expired. Please log in again.');
        this.disconnect();
      }
    });

    this.socket.on('disconnect', (reason) => {
      console.warn('Disconnected:', reason);
      if (reason === 'io server disconnect') {
        // Server initiated disconnect — attempt manual reconnect
        this.socket.connect();
      }
    });

    this.socket.on('reconnect_attempt', (attempt) => {
      this.reconnectAttempts = attempt;
      console.log(`Reconnection attempt {attempt}/{this.maxReconnectAttempts}`);
    });

    // Register game event handlers
    this._registerGameEvents();
  }

  _registerGameEvents() {
    this.socket.on('room:joined', (data) => {
      this.gameState = data.room;
      this._emit('roomJoined', data);
    });

    this.socket.on('room:player-joined', (data) => {
      this.gameState.players.push(data.player);
      this._emit('playerJoined', data);
    });

    this.socket.on('room:player-left', (data) => {
      this.gameState.players = this.gameState.players
        .filter(p => p.id !== data.playerId);
      this._emit('playerLeft', data);
    });

    this.socket.on('game:dice-rolled', (data) => {
      this.gameState.lastDiceValue = data.value;
      this._emit('diceRolled', data);
    });

    this.socket.on('game:turn-change', (data) => {
      this.gameState.currentPlayerId = data.currentPlayerId;
      this.gameState.turnStartedAt = Date.now();
      this._emit('turnChange', data);
    });

    this.socket.on('game:piece-moved', (data) => {
      const piece = this.gameState.pieces.find(p => p.id === data.pieceId);
      if (piece) {
        piece.position = data.position;
        piece.status = data.status;
      }
      this._emit('pieceMoved', data);
    });

    this.socket.on('game:ended', (data) => {
      this.gameState.status = 'ended';
      this.gameState.winner = data.winnerId;
      this._emit('gameEnded', data);
    });

    this.socket.on('room:closed', () => {
      this._emit('roomClosed');
      this.disconnect();
    });
  }

  // Public action methods
  rollDice() {
    this.socket.emit('game:roll-dice', { roomCode: this.roomCode });
  }

  movePiece(pieceId, targetPosition) {
    this.socket.emit('game:move-piece', {
      roomCode: this.roomCode,
      pieceId,
      targetPosition
    });
  }

  sendChat(message) {
    this.socket.emit('room:chat', {
      roomCode: this.roomCode,
      message
    });
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event).add(callback);
  }

  _emit(event, data) {
    const callbacks = this.listeners.get(event);
    if (callbacks) callbacks.forEach(cb => cb(data));
  }

  disconnect() {
    if (this.socket) this.socket.disconnect();
  }
}

// Usage example
const client = new LudoGameClient({
  serverUrl: 'wss://api.ludokingapi.site/ws',
  sessionToken: 'eyJhbGci...',
  roomCode: 'ABCD12'
});

client.on('turnChange', ({ currentPlayerId }) => {
  const isMyTurn = currentPlayerId === client.gameState.playerId;
  document.getElementById('status').textContent = isMyTurn
    ? 'Your turn!'
    : "Opponent's turn";
});

client.on('diceRolled', ({ value, playerId }) => {
  animateDiceRoll(value);
  console.log(`Player {playerId} rolled {value}`);
});

client.connect();

Game State Synchronization Strategy

State sync is the hardest part of any multiplayer real-time game. The goal: every player sees the same board state, in the same order of events, even under network jitter and reconnection. There are three proven approaches:

1. Event Sourcing (Recommended for Ludo)

The server maintains an authoritative event log. Every game action (dice roll, piece move) is appended as an event with a monotonic sequence number. When a player connects mid-game, the server sends the full event log from the beginning, and the client replays it to reconstruct the current state. This is the most reliable approach for Ludo's turn-based model — events are few (a typical game has 200–400 events), so replay cost is negligible.

2. Full State Snapshots

The server periodically broadcasts the complete game state to all clients. Between snapshots, clients rely on incremental events. This reduces the reconnect reconstruction cost (just load the latest snapshot) but adds bandwidth overhead. For Ludo, a snapshot every 10–20 seconds is sufficient since the board doesn't change rapidly.

3. Client-Side Prediction (Advanced)

When it's your turn, the client predicts the result of your own actions immediately (optimistic UI) and reconciles with the server's authoritative response. If the server disagrees (e.g., the move was invalid), the client rolls back and applies the correction. This gives instant feedback but adds complexity. For casual Ludo games, event sourcing without prediction is simpler and sufficient.

Implementation tip: For Ludo, implement a 50ms reconnection grace period on the server. If a player's connection drops but reconnects within 50ms, they keep their seat. If the grace period expires, mark them as "disconnected" and notify the room. After reconnection, the client calls a room:sync event to get any missed events since disconnection.

Event Reference Table

Event Name Direction Payload Description
room:join Client → Server { roomCode } Join a game room by code
room:joined Server → Client { room, player } Confirmation with full room state
room:player-joined Server → Client { player } Another player joined the room
room:player-left Server → Client { playerId } A player left or disconnected
game:roll-dice Client → Server { roomCode } Request a dice roll for current player
game:dice-rolled Server → Client { playerId, value, isValid } Broadcast dice result to all players
game:move-piece Client → Server { roomCode, pieceId, targetPos } Submit a piece movement
game:piece-moved Server → Client { pieceId, from, to, status } Broadcast validated move to all players
game:turn-change Server → Client { currentPlayerId, turnNumber } Notify whose turn it is now
game:ended Server → Client { winnerId, scores, duration } Game completed with winner info

Frequently Asked Questions

Ready to Add Real-Time Multiplayer to Your Ludo Game?

Get a pre-built WebSocket server with room management, game logic, and state sync included.

Chat on WhatsApp