From game-dev
Implements skill-based player matchmaking with ELO/Glicko ratings, lobby state machines, priority queues via BullMQ, and search radius expansion for multiplayer games.
npx claudepluginhub fcsouza/agent-skills --plugin standalone-skillsThis skill uses the workspace's default tool permissions.
Rank-based and skill-based player matching, lobby lifecycle management, and queue orchestration for multiplayer games.
Game server architecture, scalability, matchmaking, and backend systems for online games. Build robust, scalable multiplayer infrastructure.
Provides expertise in real-time multiplayer game networking: lag compensation, state synchronization, authoritative servers, rollback netcode, matchmaking, and anti-cheat.
Defines ownership, authority, replication, prediction, rollback, and reconciliation strategies for multiplayer systems. Useful for networked state sync with lag or authority issues.
Share bugs, ideas, or general feedback.
Rank-based and skill-based player matching, lobby lifecycle management, and queue orchestration for multiplayer games.
Trigger: matchmaking, lobby, queue, ELO, Glicko, skill-based matchmaking, rank, match players, pairing, bracket, queue management, wait time, search radius, lobby state
bullmq-game-queues — priority queues, delayed jobs, worker patternsredis-game-patterns — Redis connection, pub/sub for lobby eventspostgres-game-schema — persisting match history, player ratingsRaph Koster: "Matchmaking is the invisible hand that shapes every player's experience. Get it wrong, and no amount of great game design can save you." Will Wright: "The best systems match complexity to the player — beginners face beginners, experts face experts, and everyone stays in flow."
bun add bullmq ioredis
bun add -d @types/node
Create dedicated BullMQ queues for matchmaking. Use the patterns from bullmq-game-queues.
import { Queue, Worker } from 'bullmq';
import type { Redis } from 'ioredis';
const matchmakingQueue = new Queue('matchmaking', {
connection: redis,
defaultJobOptions: {
removeOnComplete: 100,
removeOnFail: 500,
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
},
});
// Separate queue for search radius expansion
const radiusExpansionQueue = new Queue('matchmaking:radius-expand', {
connection: redis,
});
Choose ELO for simplicity or Glicko-2 for accuracy. Glicko-2 tracks rating deviation (confidence) and volatility.
interface PlayerRating {
rating: number; // ELO/Glicko-2 rating (default: 1500)
deviation: number; // Glicko-2 RD (default: 350, lower = more confident)
volatility: number; // Glicko-2 sigma (default: 0.06)
gamesPlayed: number;
}
// K-factor decreases as confidence increases
function getKFactor(gamesPlayed: number): number {
if (gamesPlayed < 10) return 40; // New players: high adjustment
if (gamesPlayed < 30) return 24; // Learning players
return 16; // Established players
}
See boilerplate/matchmaker.ts for the full MatchmakingEngine class. Players enter the queue with their rating, region, and game mode.
import { MatchmakingEngine } from './matchmaker';
const engine = new MatchmakingEngine(redis);
await engine.enqueue({
playerId: 'player_123',
rating: 1500,
deviation: 100,
gameMode: 'ranked-1v1',
region: 'us-east',
queuedAt: Date.now(),
searchRadius: 50,
});
The engine scores potential matches and picks the best one. Quality score considers rating difference, deviation overlap, wait time, and region latency.
// Inside BullMQ worker
const worker = new Worker('matchmaking', async (job) => {
const player = job.data as PlayerQueueEntry;
const match = await engine.findMatch(player);
if (match) {
await lobbyManager.createLobby(match);
} else {
// Schedule radius expansion after 10s
await radiusExpansionQueue.add('expand', {
playerId: player.playerId,
}, { delay: 10_000 });
}
}, { connection: redis, concurrency: 10 });
See boilerplate/lobby-manager.ts for the full LobbyManager class. Lobbies use Redis for state storage and pub/sub for real-time events.
import { LobbyManager } from './lobby-manager';
const lobbyManager = new LobbyManager(redis);
// Match found -> create lobby
const lobby = await lobbyManager.createLobby(match);
// Players ready up
await lobbyManager.readyUp(lobby.lobbyId, 'player_123');
// All ready -> countdown starts automatically
// Countdown complete -> IN_PROGRESS
// Game ends -> COMPLETE
// Player disconnects during countdown
await lobbyManager.leaveLobby(lobbyId, disconnectedPlayerId);
// Remaining players return to queue front with priority boost
// Player cancels queue
await engine.dequeue('player_123');
// Search radius expansion (called by delayed job)
engine.widenSearchRadius('player_123');
See boilerplate files:
boilerplate/matchmaker.ts — Core MatchmakingEngine class with enqueue, dequeue, match finding, and quality scoringboilerplate/lobby-manager.ts — LobbyManager class with state machine, Redis storage, and pub/sub eventstemplates/matchmaking-config.md — Configuration template for queue settings, rating parameters, and anti-abuse rulesRaph Koster: Matchmaking defines the difficulty curve more than any level designer ever could. A good matchmaker keeps players in Csikszentmihalyi's "flow channel" — challenged enough to be engaged, but never so outmatched that they feel helpless. The system is invisible when it works, infuriating when it doesn't.
Will Wright: The matchmaker is a simulation of social dynamics. It must model not just skill but confidence, frustration tolerance, and play patterns. A player on a losing streak needs an easier match, even if their rating says otherwise. Systems that serve the math instead of the human always fail.