Agent skill
realtime-multiplayer
Real-time multiplayer game networking with Socket.io. Use when implementing WebSocket connections, game state synchronization, room management, reconnection handling, or optimistic updates. Covers latency compensation and conflict resolution.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/realtime-multiplayer
SKILL.md
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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?