realtime-multiplayer

Real-Time Multiplayer Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "realtime-multiplayer" with this command: npx skills add fil512/upship/fil512-upship-realtime-multiplayer

Real-Time Multiplayer Skill

Overview

This skill provides expertise for building real-time multiplayer games using WebSockets and Socket.io. It covers connection management, state synchronization, latency handling, and the specific challenges of turn-based games with real-time updates.

Core Architecture

Client-Server Model for Games

┌─────────────┐ WebSocket ┌─────────────┐ │ Client │◄──────────────────►│ Server │ │ (Browser) │ │ (Node.js) │ └─────────────┘ └─────────────┘ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Local UI │ │ Game State │ │ State │ │ (Source │ │ (Optimistic) │ of Truth) │ └─────────────┘ └─────────────┘

Key Principle: The server is the authoritative source of truth. Clients can have optimistic local state for responsiveness, but server state always wins on conflict.

Socket.io Setup Pattern

// Server setup const io = require('socket.io')(server, { cors: { origin: process.env.CLIENT_URL }, pingTimeout: 60000, pingInterval: 25000 });

io.on('connection', (socket) => { // Join game room socket.on('join-game', ({ gameId, playerId }) => { socket.join(game:${gameId}); socket.gameId = gameId; socket.playerId = playerId; });

// Handle game actions socket.on('game-action', async (action) => { const result = await processAction(socket.gameId, socket.playerId, action); if (result.success) { // Broadcast to all players in game io.to(game:${socket.gameId}).emit('state-update', result.newState); } else { // Send error only to acting player socket.emit('action-error', result.error); } });

// Handle disconnection socket.on('disconnect', () => { handlePlayerDisconnect(socket.gameId, socket.playerId); }); });

Room Management

Game Rooms Pattern

Each game instance should be a Socket.io room:

// Room naming convention const roomName = game:${gameId};

// Player joins game socket.join(roomName);

// Broadcast to all players in game io.to(roomName).emit('event', data);

// Send to specific player io.to(playerSocketId).emit('private-event', data);

// Send to all except sender socket.to(roomName).emit('event', data);

Player Presence Tracking

const gamePresence = new Map(); // gameId -> Set of playerIds

function trackPresence(gameId, playerId, isOnline) { if (!gamePresence.has(gameId)) { gamePresence.set(gameId, new Set()); }

const players = gamePresence.get(gameId); if (isOnline) { players.add(playerId); } else { players.delete(playerId); }

// Notify other players io.to(game:${gameId}).emit('presence-update', { playerId, isOnline, onlinePlayers: Array.from(players) }); }

State Synchronization

Event Types

Define clear event categories:

// Server -> Client events const ServerEvents = { STATE_SYNC: 'state-sync', // Full state (on join/reconnect) STATE_UPDATE: 'state-update', // Partial state change ACTION_RESULT: 'action-result', // Response to player action PLAYER_JOINED: 'player-joined', PLAYER_LEFT: 'player-left', GAME_STARTED: 'game-started', TURN_CHANGED: 'turn-changed', GAME_ENDED: 'game-ended' };

// Client -> Server events const ClientEvents = { JOIN_GAME: 'join-game', LEAVE_GAME: 'leave-game', GAME_ACTION: 'game-action', REQUEST_SYNC: 'request-sync', PING: 'ping' };

Delta Updates vs Full Sync

// Send delta updates for efficiency function sendDelta(gameId, changes) { io.to(game:${gameId}).emit('state-update', { type: 'delta', changes, version: gameState.version }); }

// Send full state on reconnect or desync function sendFullSync(socket, gameState) { socket.emit('state-sync', { type: 'full', state: gameState, version: gameState.version }); }

Version Vectors for Consistency

// Track state version to detect desync let stateVersion = 0;

function applyAction(action) { // Validate and apply const newState = reducer(currentState, action); stateVersion++;

return { state: newState, version: stateVersion }; }

// Client requests sync if versions mismatch socket.on('state-update', ({ version, changes }) => { if (version !== localVersion + 1) { socket.emit('request-sync'); // Ask for full state } });

Handling Disconnections

Reconnection Strategy

// Client-side reconnection const socket = io(SERVER_URL, { reconnection: true, reconnectionAttempts: 10, reconnectionDelay: 1000, reconnectionDelayMax: 5000 });

socket.on('connect', () => { if (currentGameId) { // Rejoin game room after reconnect socket.emit('join-game', { gameId: currentGameId, playerId: myPlayerId, lastVersion: localStateVersion // For delta sync }); } });

socket.on('disconnect', () => { showReconnectingUI(); });

Grace Period for Disconnects

// Server-side: Don't immediately remove disconnected players const disconnectTimers = new Map();

function handlePlayerDisconnect(gameId, playerId) { // Mark as disconnected but give grace period updatePresence(gameId, playerId, false);

const timer = setTimeout(() => { // After grace period, handle as true disconnect handlePlayerTimeout(gameId, playerId); }, 60000); // 60 second grace period

disconnectTimers.set(${gameId}:${playerId}, timer); }

function handlePlayerReconnect(gameId, playerId) { // Cancel timeout if player reconnects const key = ${gameId}:${playerId}; if (disconnectTimers.has(key)) { clearTimeout(disconnectTimers.get(key)); disconnectTimers.delete(key); } updatePresence(gameId, playerId, true); }

Turn-Based Game Patterns

Turn Timer Implementation

class TurnTimer { constructor(gameId, onTimeout) { this.gameId = gameId; this.onTimeout = onTimeout; this.timer = null; }

start(playerId, durationMs) { this.clear(); const endTime = Date.now() + durationMs;

// Broadcast timer start to all clients
io.to(`game:${this.gameId}`).emit('turn-timer', {
  playerId,
  endTime,
  durationMs
});

this.timer = setTimeout(() => {
  this.onTimeout(playerId);
}, durationMs);

}

clear() { if (this.timer) { clearTimeout(this.timer); this.timer = null; } } }

Action Validation

// Always validate on server async function processAction(gameId, playerId, action) { const game = await getGame(gameId);

// Validate it's player's turn if (game.currentPlayer !== playerId) { return { success: false, error: 'Not your turn' }; }

// Validate action is legal const validationResult = validateAction(game.state, action); if (!validationResult.valid) { return { success: false, error: validationResult.reason }; }

// Apply action const newState = applyAction(game.state, action); await saveGame(gameId, newState);

return { success: true, newState }; }

Optimistic Updates

Client-Side Pattern

// For responsive UI, apply optimistically then reconcile function handlePlayerAction(action) { // 1. Optimistically apply locally const optimisticState = reducer(localState, action); renderUI(optimisticState);

// 2. Send to server socket.emit('game-action', action, (response) => { if (response.success) { // 3a. Server confirmed - update to authoritative state localState = response.state; } else { // 3b. Server rejected - rollback localState = previousState; showError(response.error); } renderUI(localState); }); }

Security Considerations

Never Trust the Client

// BAD: Client sends new state socket.on('update-state', (newState) => { gameState = newState; // Never do this! });

// GOOD: Client sends action, server validates and applies socket.on('game-action', (action) => { if (isValidAction(gameState, action, socket.playerId)) { gameState = applyAction(gameState, action); broadcast(gameState); } });

Rate Limiting

const rateLimit = require('socket.io-rate-limit');

io.use(rateLimit({ windowMs: 1000, max: 10 // Max 10 messages per second per client }));

Testing Multiplayer

Simulating Multiple Clients

// Test helper for multiple socket connections async function createTestClients(count, gameId) { const clients = []; for (let i = 0; i < count; i++) { const socket = io(SERVER_URL); await new Promise(resolve => socket.on('connect', resolve)); socket.emit('join-game', { gameId, playerId: player-${i} }); clients.push(socket); } return clients; }

Testing Reconnection

it('should handle reconnection gracefully', async () => { const client = await createTestClient(gameId);

// Force disconnect client.disconnect();

// Wait and reconnect await sleep(1000); client.connect();

// Should receive full state sync const state = await waitForEvent(client, 'state-sync'); expect(state).toBeDefined(); });

When This Skill Activates

Use this skill when:

  • Setting up WebSocket/Socket.io connections

  • Implementing game room management

  • Building state synchronization

  • Handling player disconnection/reconnection

  • Implementing turn timers

  • Adding optimistic updates

  • Securing multiplayer communications

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

ui-design-expert

No summary provided by upstream source.

Repository SourceNeeds Review
General

rulebook-writing

No summary provided by upstream source.

Repository SourceNeeds Review
General

game-state

No summary provided by upstream source.

Repository SourceNeeds Review
General

svelte

No summary provided by upstream source.

Repository SourceNeeds Review