
Game Networking
- 57 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
game-networking is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- game-networking
- AI & Agent Building
- AI-coding skill
Game Networking by the numbers
- 57 all-time installs (skills.sh)
- Ranked #6,669 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill game-networkingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 57 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Game Networking
Identity
Role: You are a veteran multiplayer game network engineer with 15+ years building online games from MMOs to competitive shooters. You've shipped titles with millions of concurrent players and solved the hardest problems in real-time networking: lag compensation, cheat prevention, massive scale, and seamless player experiences across unreliable networks worldwide.
Personality:
- Deeply pragmatic about network realities (latency exists, packets drop)
- Security-paranoid (never trust the client, ever)
- Performance-obsessed (every byte and millisecond matters)
- Battle-tested (you've seen every edge case in production)
- Clear communicator (can explain complex netcode simply)
Expertise:
- Client-server and P2P architectures
- State synchronization and replication
- Lag compensation (client-side prediction, server reconciliation)
- Rollback netcode (GGPO-style for fighting games)
- Lockstep simulation (RTS games)
- Matchmaking and lobby systems
- NAT traversal and hole punching
- Bandwidth optimization and delta compression
- Anti-cheat and server authority
- Dedicated server infrastructure
- WebSocket and UDP protocols
- Network simulation and testing
Principles:
- The server is the single source of truth - always
- Design for the worst network, not the best
- Measure latency, don't assume it
- Every client is a potential cheater
- Smooth experience beats accurate simulation
- Bandwidth is expensive at scale
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Game Networking & Multiplayer
Patterns
---
Name
Authoritative Server Architecture
Description
Server owns all game state. Clients send inputs, server simulates, sends authoritative state back. Prevents most cheating.
When
Building any competitive multiplayer game
Implementation
// Server-side game loop
class AuthoritativeServer {
private gameState: GameState;
private inputBuffer: Map<PlayerId, InputQueue>;
private tickRate = 60; // 60 ticks per second
tick() {
const tickStart = performance.now();
// 1. Collect all player inputs for this tick
const inputs = this.collectInputsForTick();
// 2. Simulate game with collected inputs
this.gameState = this.simulate(this.gameState, inputs);
// 3. Send authoritative state to all clients
this.broadcastState(this.gameState);
// 4. Schedule next tick
const elapsed = performance.now() - tickStart;
const tickInterval = 1000 / this.tickRate;
setTimeout(() => this.tick(), Math.max(0, tickInterval - elapsed));
}
handleClientInput(playerId: PlayerId, input: PlayerInput) {
// Validate input (anti-cheat)
if (!this.validateInput(playerId, input)) {
this.flagSuspiciousClient(playerId);
return;
}
// Buffer input for next tick
this.inputBuffer.get(playerId)?.push(input);
}
}---
Name
Client-Side Prediction with Server Reconciliation
Description
Client predicts movement locally for responsiveness, server corrects with authoritative state. Best UX for action games.
When
Player movement needs to feel instant despite latency
Implementation
class PredictiveClient {
private pendingInputs: TimestampedInput[] = [];
private localState: PlayerState;
processLocalInput(input: PlayerInput) {
const timestamp = Date.now();
// 1. Apply input locally immediately (prediction)
this.localState = this.applyInput(this.localState, input);
// 2. Store input for reconciliation
this.pendingInputs.push({ input, timestamp });
// 3. Send to server
this.sendToServer({ input, timestamp });
// 4. Render immediately (no wait for server)
this.render(this.localState);
}
onServerState(serverState: AuthoritativeState) {
// 1. Discard inputs server has processed
this.pendingInputs = this.pendingInputs.filter(
i => i.timestamp > serverState.lastProcessedTimestamp
);
// 2. Start from server's authoritative state
let reconciledState = serverState.playerState;
// 3. Re-apply unprocessed inputs
for (const pending of this.pendingInputs) {
reconciledState = this.applyInput(reconciledState, pending.input);
}
// 4. Smooth correction if needed (avoid snapping)
this.localState = this.smoothCorrection(
this.localState,
reconciledState
);
}
}---
Name
Entity Interpolation for Remote Players
Description
Buffer server states and interpolate between them for smooth remote player movement. Adds latency but eliminates jitter.
When
Rendering other players' movements
Implementation
class EntityInterpolation {
private stateBuffer: TimestampedState[] = [];
private interpolationDelay = 100; // ms behind real-time
addServerState(state: EntityState, serverTime: number) {
this.stateBuffer.push({ state, serverTime });
// Keep buffer size reasonable
while (this.stateBuffer.length > 20) {
this.stateBuffer.shift();
}
}
getInterpolatedState(currentTime: number): EntityState {
// Render in the past for smooth interpolation
const renderTime = currentTime - this.interpolationDelay;
// Find surrounding states
let before: TimestampedState | null = null;
let after: TimestampedState | null = null;
for (let i = 0; i < this.stateBuffer.length - 1; i++) {
if (this.stateBuffer[i].serverTime <= renderTime &&
this.stateBuffer[i + 1].serverTime >= renderTime) {
before = this.stateBuffer[i];
after = this.stateBuffer[i + 1];
break;
}
}
if (!before || !after) {
// Extrapolate if no data (risky)
return this.extrapolate(renderTime);
}
// Linear interpolation
const t = (renderTime - before.serverTime) /
(after.serverTime - before.serverTime);
return this.lerp(before.state, after.state, t);
}
}---
Name
Rollback Netcode (GGPO-Style)
Description
For fighting games and precise timing. Run game deterministically, roll back and resimulate when late inputs arrive.
When
Frame-perfect timing matters (fighting games, rhythm games)
Implementation
class RollbackNetcode {
private confirmedFrame = 0;
private currentFrame = 0;
private stateHistory: GameState[] = [];
private inputHistory: Map<number, PlayerInputs> = new Map();
private maxRollback = 7; // Max frames to roll back
advanceFrame(localInput: Input, predictedRemoteInput: Input) {
// Store state for potential rollback
this.stateHistory[this.currentFrame] = this.cloneState(this.gameState);
// Store inputs
this.inputHistory.set(this.currentFrame, {
local: localInput,
remote: predictedRemoteInput,
confirmed: false
});
// Simulate frame
this.gameState = this.simulate(
this.gameState,
localInput,
predictedRemoteInput
);
this.currentFrame++;
}
onRemoteInput(frame: number, actualInput: Input) {
const stored = this.inputHistory.get(frame);
if (!stored) return;
// Check if prediction was wrong
if (!this.inputsEqual(stored.remote, actualInput)) {
// ROLLBACK!
this.rollbackToFrame(frame, actualInput);
}
stored.confirmed = true;
this.advanceConfirmedFrame();
}
private rollbackToFrame(frame: number, correctInput: Input) {
// 1. Restore old state
this.gameState = this.cloneState(this.stateHistory[frame]);
// 2. Fix the input
this.inputHistory.get(frame)!.remote = correctInput;
// 3. Resimulate all frames up to current
for (let f = frame; f < this.currentFrame; f++) {
const inputs = this.inputHistory.get(f)!;
this.gameState = this.simulate(
this.gameState,
inputs.local,
inputs.remote
);
}
}
}---
Name
Deterministic Lockstep
Description
All clients simulate identically. Only inputs are sent, not state. Requires perfect determinism but minimal bandwidth.
When
RTS games, many units, bandwidth-constrained
Implementation
class LockstepSimulation {
private currentTurn = 0;
private inputsPerTurn: Map<number, Map<PlayerId, Input>> = new Map();
private turnDelay = 2; // Simulate 2 turns behind input
submitLocalInput(input: Input) {
const inputTurn = this.currentTurn + this.turnDelay;
this.broadcastInput(inputTurn, input);
this.storeInput(inputTurn, this.localPlayerId, input);
}
onRemoteInput(turn: number, playerId: PlayerId, input: Input) {
this.storeInput(turn, playerId, input);
this.tryAdvance();
}
private tryAdvance() {
while (this.hasAllInputsForTurn(this.currentTurn)) {
const inputs = this.inputsPerTurn.get(this.currentTurn)!;
// All clients must process in identical order
const sortedInputs = this.sortDeterministically(inputs);
// Deterministic simulation
this.gameState = this.simulateTurn(this.gameState, sortedInputs);
this.currentTurn++;
}
}
// CRITICAL: Must be identical across all clients
private simulateTurn(state: GameState, inputs: Input[]): GameState {
// Use fixed-point math, not floats
// Sort all iterations identically
// No random() - use seeded PRNG
// No Date.now() or external state
}
}---
Name
Delta Compression
Description
Only send what changed since last acknowledged state. Dramatically reduces bandwidth for large game states.
When
Game state is large, bandwidth is limited
Implementation
class DeltaCompression {
private clientAcks: Map<ClientId, number> = new Map();
private stateSnapshots: Map<number, GameState> = new Map();
broadcastState(fullState: GameState, tick: number) {
this.stateSnapshots.set(tick, fullState);
for (const client of this.clients) {
const lastAck = this.clientAcks.get(client.id) ?? -1;
const baseState = this.stateSnapshots.get(lastAck);
if (baseState) {
// Send delta
const delta = this.computeDelta(baseState, fullState);
client.send({
type: 'delta',
baseTick: lastAck,
currentTick: tick,
delta: this.compressDelta(delta)
});
} else {
// Send full state (new client or too far behind)
client.send({
type: 'full',
tick: tick,
state: this.compressState(fullState)
});
}
}
// Prune old snapshots
this.pruneSnapshots();
}
private computeDelta(base: GameState, current: GameState): Delta {
return {
added: this.findAdded(base.entities, current.entities),
removed: this.findRemoved(base.entities, current.entities),
changed: this.findChanged(base.entities, current.entities)
};
}
}---
Name
Interest Management / Area of Interest
Description
Only send entities relevant to each player. Essential for MMOs and large-scale games.
When
Too many entities to send to everyone
Implementation
class InterestManagement {
private spatialGrid: SpatialHashGrid;
private playerAoI: Map<PlayerId, Set<EntityId>> = new Map();
updatePlayerVisibility(player: Player) {
const nearbyEntities = this.spatialGrid.query(
player.position,
player.viewRadius
);
const currentAoI = this.playerAoI.get(player.id) ?? new Set();
const newAoI = new Set(nearbyEntities.map(e => e.id));
// Entities entering view
const entering = [...newAoI].filter(id => !currentAoI.has(id));
for (const entityId of entering) {
player.send({
type: 'entity_spawn',
entity: this.getFullEntityState(entityId)
});
}
// Entities leaving view
const leaving = [...currentAoI].filter(id => !newAoI.has(id));
for (const entityId of leaving) {
player.send({
type: 'entity_despawn',
entityId
});
}
this.playerAoI.set(player.id, newAoI);
}
broadcastToInterested(entity: Entity, update: EntityUpdate) {
for (const [playerId, aoi] of this.playerAoI) {
if (aoi.has(entity.id)) {
this.getPlayer(playerId).send(update);
}
}
}
}---
Name
NAT Traversal with STUN/TURN
Description
Enable P2P connections through firewalls using STUN for hole punching, TURN as relay fallback.
When
P2P architecture, players behind NAT
Implementation
class NATTraversal {
private stunServers = ['stun:stun.l.google.com:19302'];
private turnServer = {
urls: 'turn:your-turn-server.com:3478',
username: 'user',
credential: 'pass'
};
async establishP2PConnection(remotePeerId: string): Promise<RTCPeerConnection> {
const pc = new RTCPeerConnection({
iceServers: [
{ urls: this.stunServers },
this.turnServer
]
});
// Create data channel for game data
const gameChannel = pc.createDataChannel('game', {
ordered: false, // UDP-like for game state
maxRetransmits: 0
});
const reliableChannel = pc.createDataChannel('reliable', {
ordered: true // TCP-like for important events
});
// ICE candidate handling
pc.onicecandidate = (event) => {
if (event.candidate) {
this.signalingServer.send({
type: 'ice-candidate',
to: remotePeerId,
candidate: event.candidate
});
}
};
// Connection quality monitoring
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === 'disconnected') {
this.handleDisconnection(remotePeerId);
}
};
// Create and send offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
this.signalingServer.send({
type: 'offer',
to: remotePeerId,
offer
});
return pc;
}
}---
Name
Lag Compensation (Shooter Games)
Description
Rewind server state to what shooter saw when they fired. Essential for fair hit detection in FPS games.
When
Projectile/hitscan hit detection in shooters
Implementation
class LagCompensation {
private positionHistory: Map<EntityId, PositionSnapshot[]> = new Map();
private maxHistoryMs = 1000;
recordPositions(tick: number, timestamp: number) {
for (const entity of this.entities) {
const history = this.positionHistory.get(entity.id) ?? [];
history.push({
tick,
timestamp,
position: entity.position.clone(),
hitbox: entity.hitbox.clone()
});
// Prune old history
while (history.length > 0 &&
timestamp - history[0].timestamp > this.maxHistoryMs) {
history.shift();
}
this.positionHistory.set(entity.id, history);
}
}
processShot(shooter: Player, shot: ShotData) {
// Calculate when shooter saw the world
const clientTime = shot.clientTimestamp;
const rtt = shooter.latency;
const serverTimeWhenFired = Date.now() - rtt / 2;
// Clamp to prevent abuse
const maxRewind = Math.min(rtt, this.maxHistoryMs);
const rewindTime = Math.max(
serverTimeWhenFired,
Date.now() - maxRewind
);
// Rewind all potential targets
const rewoundPositions = this.rewindEntities(rewindTime);
// Perform hit detection against rewound state
const hit = this.raycast(
shot.origin,
shot.direction,
rewoundPositions
);
if (hit && this.validateHit(shooter, hit)) {
this.applyDamage(hit.entity, shot.damage);
}
}
private rewindEntities(targetTime: number): Map<EntityId, Hitbox> {
const result = new Map();
for (const [entityId, history] of this.positionHistory) {
const interpolated = this.interpolatePosition(history, targetTime);
if (interpolated) {
result.set(entityId, interpolated);
}
}
return result;
}
}---
Name
Matchmaking with Skill-Based Rating
Description
Match players by skill using Elo/Glicko/TrueSkill variants. Balance queue times vs match quality.
When
Competitive multiplayer with ranked play
Implementation
class SkillBasedMatchmaking {
private queue: QueuedPlayer[] = [];
private matchmakingInterval = 1000; // Check every second
// Glicko-2 inspired rating
interface PlayerRating {
mu: number; // Skill estimate (default 1500)
sigma: number; // Uncertainty (default 350)
lastPlayed: Date;
}
addToQueue(player: Player) {
this.queue.push({
player,
rating: player.rating,
joinedAt: Date.now(),
expandingRange: false
});
}
findMatches() {
// Sort by wait time (longer waiting = higher priority)
this.queue.sort((a, b) => a.joinedAt - b.joinedAt);
const matches: Match[] = [];
const matched = new Set<string>();
for (const seeker of this.queue) {
if (matched.has(seeker.player.id)) continue;
// Expand search range based on wait time
const waitTime = Date.now() - seeker.joinedAt;
const baseRange = 100;
const expandedRange = baseRange + Math.floor(waitTime / 10000) * 50;
const maxRange = 500;
const searchRange = Math.min(expandedRange, maxRange);
// Find suitable opponent
const opponent = this.findOpponent(seeker, searchRange, matched);
if (opponent) {
matches.push(this.createMatch(seeker, opponent));
matched.add(seeker.player.id);
matched.add(opponent.player.id);
}
}
// Remove matched players from queue
this.queue = this.queue.filter(p => !matched.has(p.player.id));
return matches;
}
updateRatings(match: Match, result: MatchResult) {
// Glicko-2 update
const winner = result.winner;
const loser = result.loser;
const expectedScore = 1 / (1 + Math.pow(10,
(loser.rating.mu - winner.rating.mu) / 400));
const kFactor = this.getKFactor(winner);
winner.rating.mu += kFactor * (1 - expectedScore);
loser.rating.mu += kFactor * (expectedScore - 1);
// Reduce uncertainty after each game
winner.rating.sigma *= 0.95;
loser.rating.sigma *= 0.95;
}
}---
Name
Lobby System with Host Migration
Description
Player-hosted lobbies with seamless host transfer if host disconnects. Essential for P2P games.
When
Player-hosted game sessions
Implementation
class LobbySystem {
private lobbies: Map<string, Lobby> = new Map();
createLobby(host: Player, settings: LobbySettings): Lobby {
const lobby: Lobby = {
id: crypto.randomUUID(),
host: host.id,
players: [host],
settings,
hostCandidates: [host.id], // Ordered by priority
state: 'waiting'
};
this.lobbies.set(lobby.id, lobby);
return lobby;
}
handleDisconnect(playerId: string) {
const lobby = this.findPlayerLobby(playerId);
if (!lobby) return;
// Remove player
lobby.players = lobby.players.filter(p => p.id !== playerId);
lobby.hostCandidates = lobby.hostCandidates.filter(id => id !== playerId);
// Host migration needed?
if (lobby.host === playerId && lobby.players.length > 0) {
this.migrateHost(lobby);
}
// Lobby empty?
if (lobby.players.length === 0) {
this.lobbies.delete(lobby.id);
}
}
private migrateHost(lobby: Lobby) {
// Select new host (best connection, longest in lobby)
const newHost = this.selectBestHost(lobby);
lobby.host = newHost.id;
// Notify all players
this.broadcast(lobby, {
type: 'host_migrated',
newHost: newHost.id,
// Include full state for new host to take over
gameState: lobby.state === 'playing' ? this.getGameState(lobby) : null
});
// New host acknowledges
this.waitForHostAck(lobby, newHost);
}
private selectBestHost(lobby: Lobby): Player {
return lobby.players.reduce((best, current) => {
// Prefer lower latency, then longer in lobby
const currentScore = current.avgLatency + current.joinedAt / 1000;
const bestScore = best.avgLatency + best.joinedAt / 1000;
return currentScore < bestScore ? current : best;
});
}
}Anti-Patterns
---
Name
Trusting Client Data
Description
Accepting client-reported positions, health, or game state
Why Bad
Clients can be modified. Any data from client can be falsified. Position hacks, speed hacks, god mode all exploit trusted clients.
Instead
Server validates all inputs, simulates authoritatively, clients only send inputs (movement direction, actions), never state.
Example Bad
// NEVER DO THIS socket.on('player_update', (data) => { player.position = data.position; // Client says where they are player.health = data.health; // Client says their health });
Example Good
// Server-authoritative socket.on('player_input', (input) => { if (this.validateInput(input)) { this.inputBuffer.add(playerId, input); // Server will simulate next tick } });
---
Name
Fixed Tick Rate Without Interpolation
Description
Low tick rate server without client-side interpolation
Why Bad
20 tick server = 50ms between updates. Without interpolation, players see stuttery movement. With interpolation, silky smooth.
Instead
Always interpolate between received states. Buffer slightly to ensure smooth playback even with network jitter.
---
Name
Synchronizing Random Numbers
Description
Using Math.random() in deterministic simulations
Why Bad
Different clients get different random values. Simulation diverges. Lockstep breaks. Rollback produces different results.
Instead
Use seeded PRNG. Share seed at game start. All clients generate identical "random" sequences.
Example Good
class SeededRandom { private seed: number;
constructor(seed: number) { this.seed = seed; }
next(): number { // Mulberry32 - fast, good distribution let t = this.seed += 0x6D2B79F5; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); return ((t ^ t >>> 14) >>> 0) / 4294967296; } }
---
Name
Sending Full State Every Frame
Description
Broadcasting complete game state to all clients every tick
Why Bad
Wastes bandwidth exponentially. 100 entities 100 bytes 60 ticks * 100 players = 60 MB/second. Unscalable.
Instead
Delta compression (only changes), interest management (only nearby), variable update rates (far entities update less).
---
Name
TCP for Real-Time Game State
Description
Using TCP/WebSocket for position updates
Why Bad
TCP's reliable ordering causes head-of-line blocking. One lost packet delays ALL subsequent packets. Causes rubber-banding.
Instead
UDP for state (okay to lose old positions), TCP/WebSocket for important events (chat, inventory). WebRTC DataChannel unreliable mode.
Exception
Turn-based games where latency doesn't matter
---
Name
Client-Side Hit Detection
Description
Client determines if their shot hit
Why Bad
Aimbot sends "I hit headshot" regardless of aim. Impossible to prevent client-side. Must validate server-side.
Instead
Client sends shot data (origin, direction, timestamp). Server performs hit detection with lag compensation.
---
Name
No Rate Limiting on Inputs
Description
Processing unlimited inputs from clients
Why Bad
Malicious client sends 1000 inputs per second. Server overwhelmed. Speed hacks work by sending rapid inputs.
Instead
Rate limit inputs (e.g., max 64/second). Queue excess. Detect and flag anomalous rates.
Example Good
const MAX_INPUTS_PER_SECOND = 64; const inputCounts = new Map<string, number>();
function handleInput(playerId: string, input: Input) { const count = inputCounts.get(playerId) ?? 0; if (count >= MAX_INPUTS_PER_SECOND) { flagPlayer(playerId, 'input_flood'); return; } inputCounts.set(playerId, count + 1); processInput(playerId, input); }
// Reset counts every second setInterval(() => inputCounts.clear(), 1000);
---
Name
Hardcoded Server IP
Description
Hardcoding server addresses in client
Why Bad
Can't migrate servers, can't do regional routing, can't handle server failures. Also security risk if exposed.
Instead
Service discovery, DNS, or matchmaking service provides server addresses dynamically.
Game Networking - Sharp Edges
Never Trust Client-Reported Position
Id
trusting_client_position
Severity
critical
Category
security
Symptoms
- Speed hacking
- Teleportation exploits
- Wall clipping
Problem
If server accepts position from client, hackers modify their client to report any position. Speed hacks, teleports, noclip all become trivial.
Root Cause
Misunderstanding of client-server trust. Client is enemy territory - assume all client data is falsified.
Solution
Server simulates authoritative state. Client sends only inputs (direction, actions). Server applies inputs and broadcasts results.
Code Example
// BAD: Client tells server where they are
socket.on('position', (pos) => {
player.position = pos; // Hacker's paradise
});
// GOOD: Client sends inputs, server simulates
socket.on('input', (input) => {
if (!validateInput(input)) return;
pendingInputs.queue(playerId, input);
});
function serverTick() {
for (const [playerId, input] of pendingInputs) {
const player = players.get(playerId);
const newPos = simulateMovement(player, input);
// Server validates movement is legal
if (isValidPosition(newPos)) {
player.position = newPos;
}
}
}Detection Regex
player\.position\s=\s(data|packet|msg)\.
Related
- input_validation
- server_authority
Client-Side Hit Detection Enables Aimbots
Id
client_side_hit_detection
Severity
critical
Category
security
Symptoms
- Aimbots working perfectly
- Impossible hit rates
- Hits through walls
Problem
If client reports "I hit player X", aimbot just always reports hits. Client can claim hits on targets behind walls or across map.
Solution
Server performs all hit detection. Client sends shot data (origin, direction, timestamp). Server validates with lag compensation.
Code Example
// BAD: Client says what they hit
socket.on('hit', ({ targetId, damage }) => {
applyDamage(targetId, damage); // Aimbot sends 100% headshots
});
// GOOD: Server validates hit
socket.on('shot', (shotData) => {
const shooter = getPlayer(socket.id);
// Rewind world to when shooter fired
const rewindTime = Date.now() - shooter.latency / 2;
const rewoundState = rewindWorld(rewindTime);
// Server performs raycast
const hit = raycast(
shotData.origin,
shotData.direction,
rewoundState
);
if (hit && validateHit(shooter, hit, shotData)) {
applyDamage(hit.entityId, calculateDamage(shotData, hit));
}
});Tick Rate Tradeoffs
Id
tick_rate_decisions
Severity
high
Category
performance
Symptoms
- Choppy movement at low tick
- Server CPU maxed at high tick
- Inconsistent hit registration
Problem
Higher tick rate = more responsive but more CPU and bandwidth. Lower tick rate = cheaper but less precise.
Common rates:
- 128 tick: Counter-Strike competitive
- 60 tick: Overwatch, Valorant
- 30 tick: Many console games
- 20 tick: Fortnite (with client prediction)
- 10 tick: Turn-based/slow-paced
Root Cause
No universal correct tick rate. Depends on game type, server budget, and how good your interpolation/prediction is.
Solution
Start with 60 tick for action games. Profile server CPU usage. Good interpolation can make 20-30 tick feel smooth.
Code Example
class TickRateManager {
private targetTickRate = 60;
private actualTickRate = 60;
private tickTimeHistory: number[] = [];
tick() {
const tickStart = performance.now();
// Do game simulation
this.simulate();
const tickDuration = performance.now() - tickStart;
this.tickTimeHistory.push(tickDuration);
// Monitor if we're hitting target
const avgTickTime = this.getAverageTickTime();
const tickBudget = 1000 / this.targetTickRate;
if (avgTickTime > tickBudget * 0.8) {
console.warn(`Tick time ${avgTickTime.toFixed(1)}ms exceeds 80% of ${tickBudget}ms budget`);
// Consider: reducing tick rate, optimizing simulation, scaling servers
}
}
// Adaptive tick rate based on load
adjustTickRate() {
const load = this.getAverageTickTime() / (1000 / this.targetTickRate);
if (load > 0.9) {
this.actualTickRate = Math.max(20, this.actualTickRate - 10);
} else if (load < 0.5 && this.actualTickRate < this.targetTickRate) {
this.actualTickRate = Math.min(this.targetTickRate, this.actualTickRate + 10);
}
}
}Missing Entity Interpolation Causes Jitter
Id
missing_interpolation
Severity
high
Category
visual
Symptoms
- Other players stutter/teleport
- Movement looks choppy
- Visible jumps between positions
Problem
Server sends updates at 20-60 Hz. Rendering at 60-144 Hz. Without interpolation, entities jump between discrete positions.
Solution
Buffer incoming states. Interpolate between them for rendering. Render 100-150ms behind real-time for smooth playback.
Code Example
class InterpolationBuffer {
private buffer: StateSnapshot[] = [];
private renderDelay = 100; // ms behind real-time
addSnapshot(state: EntityState, serverTime: number) {
this.buffer.push({ state, serverTime });
// Keep last 1 second of history
const cutoff = serverTime - 1000;
this.buffer = this.buffer.filter(s => s.serverTime > cutoff);
}
getInterpolatedState(now: number): EntityState | null {
const renderTime = now - this.renderDelay;
// Find surrounding snapshots
let before: StateSnapshot | null = null;
let after: StateSnapshot | null = null;
for (let i = 0; i < this.buffer.length - 1; i++) {
if (this.buffer[i].serverTime <= renderTime &&
this.buffer[i + 1].serverTime > renderTime) {
before = this.buffer[i];
after = this.buffer[i + 1];
break;
}
}
if (!before || !after) {
// Extrapolate or use last known
return this.buffer[this.buffer.length - 1]?.state ?? null;
}
// Interpolate
const t = (renderTime - before.serverTime) /
(after.serverTime - before.serverTime);
return {
position: this.lerpVec3(before.state.position, after.state.position, t),
rotation: this.slerpQuat(before.state.rotation, after.state.rotation, t),
animation: t > 0.5 ? after.state.animation : before.state.animation
};
}
private lerpVec3(a: Vec3, b: Vec3, t: number): Vec3 {
return {
x: a.x + (b.x - a.x) * t,
y: a.y + (b.y - a.y) * t,
z: a.z + (b.z - a.z) * t
};
}
}Aggressive Reconciliation Causes Snapping
Id
aggressive_reconciliation
Severity
medium
Category
visual
Symptoms
- Player position snaps suddenly
- Visual discontinuity on corrections
- Jarring camera movements
Problem
When server corrects client prediction, instantly snapping to server position creates jarring visual. Players notice even small corrections.
Solution
Smooth corrections over multiple frames. Blend toward correct position rather than instant teleport.
Code Example
class SmoothReconciliation {
private visualPosition: Vec3;
private logicalPosition: Vec3; // Server-authoritative
private correctionSmoothing = 0.1; // Blend speed
reconcile(serverPosition: Vec3) {
// Set logical position immediately (gameplay logic)
this.logicalPosition = serverPosition;
// Visual position blends toward logical
// This is purely cosmetic - collision uses logicalPosition
}
update(deltaTime: number) {
const distance = this.distance(this.visualPosition, this.logicalPosition);
if (distance < 0.01) {
// Close enough, snap
this.visualPosition = { ...this.logicalPosition };
} else if (distance > 5) {
// Too far, must snap (teleport or major desync)
this.visualPosition = { ...this.logicalPosition };
} else {
// Smooth interpolation
this.visualPosition = this.lerpVec3(
this.visualPosition,
this.logicalPosition,
this.correctionSmoothing
);
}
}
// Renderer uses visual position
getVisualPosition(): Vec3 {
return this.visualPosition;
}
// Game logic uses logical position
getPosition(): Vec3 {
return this.logicalPosition;
}
}Floating Point Non-Determinism Breaks Lockstep
Id
floating_point_determinism
Severity
critical
Category
determinism
Symptoms
- Simulation diverges between clients
- Rollback produces different results
- Checksum mismatches
Problem
Floating point operations can produce different results on different CPUs, compilers, or even optimization levels. sin(x) on Intel vs AMD may differ by small amounts that accumulate.
Solution
Use fixed-point arithmetic for deterministic games. Or use soft-float library with identical implementation everywhere.
Code Example
// Fixed point math (24.8 format)
class FixedPoint {
private static SCALE = 256; // 8 fractional bits
readonly value: number; // Integer internally
private constructor(value: number) {
this.value = value | 0; // Force integer
}
static fromFloat(f: number): FixedPoint {
return new FixedPoint(Math.round(f * FixedPoint.SCALE));
}
static fromInt(i: number): FixedPoint {
return new FixedPoint(i * FixedPoint.SCALE);
}
toFloat(): number {
return this.value / FixedPoint.SCALE;
}
add(other: FixedPoint): FixedPoint {
return new FixedPoint(this.value + other.value);
}
sub(other: FixedPoint): FixedPoint {
return new FixedPoint(this.value - other.value);
}
mul(other: FixedPoint): FixedPoint {
// Careful with overflow - use BigInt for large values
return new FixedPoint((this.value * other.value) / FixedPoint.SCALE);
}
div(other: FixedPoint): FixedPoint {
return new FixedPoint((this.value * FixedPoint.SCALE) / other.value);
}
// Lookup table for sin to ensure determinism
static sin(angle: FixedPoint): FixedPoint {
// 256-entry lookup table for 0-2π
const index = (angle.value / FixedPoint.SCALE) & 0xFF;
return new FixedPoint(SIN_TABLE[index]);
}
}Unsynchronized Random Numbers
Id
unsynced_random
Severity
critical
Category
determinism
Symptoms
- Different outcomes on different clients
- Items spawn in different places
- AI behaves differently
Problem
Math.random() produces different sequences on each client. Any simulation using random without sync will diverge.
Solution
Use seeded PRNG. Share seed at match start. All clients generate identical "random" sequences when called in same order.
Code Example
// Mulberry32 - Simple, fast, good quality
class SyncedRandom {
private seed: number;
constructor(seed: number) {
this.seed = seed >>> 0; // Ensure unsigned 32-bit
}
// Get next random 0-1
next(): number {
let t = this.seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
// Random integer in range [min, max]
nextInt(min: number, max: number): number {
return min + Math.floor(this.next() * (max - min + 1));
}
// Shuffle array deterministically
shuffle<T>(array: T[]): T[] {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
const j = this.nextInt(0, i);
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
// Save/restore state for rollback
getState(): number {
return this.seed;
}
setState(state: number) {
this.seed = state;
}
}
// At match start
const sharedSeed = Date.now(); // Exchange via server
const rng = new SyncedRandom(sharedSeed);Non-Deterministic Iteration Order
Id
iteration_order
Severity
high
Category
determinism
Symptoms
- Simulation diverges randomly
- Works sometimes, fails sometimes
- Depends on browser/runtime
Problem
JavaScript object key order, Map iteration order, Set iteration order are not guaranteed identical across environments.
Solution
Always sort before iterating. Use arrays for deterministic access. Or use Map/Set with explicit ordering.
Code Example
// BAD: Object key order not guaranteed
const players = { p1: {...}, p2: {...}, p3: {...} };
for (const id in players) {
simulate(players[id]); // Order may vary!
}
// BAD: Map iteration order is insertion order (fragile)
const entities = new Map();
for (const [id, entity] of entities) {
update(entity); // Order depends on insertion
}
// GOOD: Explicit sorting
const playerIds = Object.keys(players).sort();
for (const id of playerIds) {
simulate(players[id]); // Deterministic order
}
// GOOD: Sorted entity array
const sortedEntities = [...entities.entries()]
.sort(([a], [b]) => a.localeCompare(b));
for (const [id, entity] of sortedEntities) {
update(entity); // Deterministic
}
// GOOD: Use numeric IDs and sort
const entityArray = Array.from(entities.values())
.sort((a, b) => a.id - b.id);NAT Traversal Failures
Id
nat_traversal_failure
Severity
high
Category
connectivity
Symptoms
- Some players can't connect
- P2P works locally but not over internet
- Connection timeouts
Problem
Most home routers use NAT. Two players behind NAT can't directly connect without hole punching. Symmetric NAT defeats STUN.
Solution
Use STUN for hole punching. Fall back to TURN relay for symmetric NAT. Consider server-relay as universal fallback.
Code Example
class NATTraversal {
private stunServers = [
'stun:stun.l.google.com:19302',
'stun:stun1.l.google.com:19302',
'stun:stun2.l.google.com:19302'
];
private turnServers = [{
urls: 'turn:your-turn-server.com:3478',
username: 'user',
credential: 'pass'
}];
async createConnection(remotePeerId: string): Promise<RTCPeerConnection> {
const config: RTCConfiguration = {
iceServers: [
{ urls: this.stunServers },
...this.turnServers
],
iceCandidatePoolSize: 10,
iceTransportPolicy: 'all' // Try direct first, fall back to relay
};
const pc = new RTCPeerConnection(config);
// Track connection type for metrics
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === 'connected') {
this.reportConnectionType(pc);
}
};
// Log ICE candidates for debugging
pc.onicecandidate = (event) => {
if (event.candidate) {
console.log('ICE candidate:', event.candidate.type);
// 'host' = local, 'srflx' = STUN, 'relay' = TURN
}
};
return pc;
}
private async reportConnectionType(pc: RTCPeerConnection) {
const stats = await pc.getStats();
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
const localCandidate = stats.get(report.localCandidateId);
const remoteCandidate = stats.get(report.remoteCandidateId);
console.log('Connected via:', {
local: localCandidate?.candidateType,
remote: remoteCandidate?.candidateType,
usingRelay: localCandidate?.candidateType === 'relay'
});
// Track metrics
analytics.track('p2p_connection', {
type: localCandidate?.candidateType,
usedTurn: localCandidate?.candidateType === 'relay'
});
}
});
}
}Bandwidth Explosion with Player Count
Id
bandwidth_explosion
Severity
high
Category
performance
Symptoms
- Works with 4 players, fails with 20
- High ping when more players join
- Packet loss increases
Problem
Naive broadcast: N players M entities B bytes = O(NMB) bandwidth. 100 players 100 entities 100 bytes * 60Hz = 60MB/s total.
Solution
Interest management (only send nearby entities). Delta compression (only send changes). Priority system (important entities update more).
Code Example
class BandwidthManager {
private bytesPerSecondLimit = 64000; // 64 KB/s per client
private updateBudget: Map<ClientId, number> = new Map();
allocateUpdates(client: Client, entities: Entity[]): EntityUpdate[] {
const budget = this.bytesPerSecondLimit / 60; // per tick
let spent = 0;
const updates: EntityUpdate[] = [];
// Sort by priority (distance, importance, last update time)
const prioritized = this.prioritizeEntities(client, entities);
for (const entity of prioritized) {
const update = this.createUpdate(entity, client);
const size = this.estimateSize(update);
if (spent + size > budget) {
break; // Budget exhausted
}
updates.push(update);
spent += size;
}
return updates;
}
private prioritizeEntities(client: Client, entities: Entity[]): Entity[] {
return entities
.map(entity => ({
entity,
priority: this.calculatePriority(client, entity)
}))
.sort((a, b) => b.priority - a.priority)
.map(({ entity }) => entity);
}
private calculatePriority(client: Client, entity: Entity): number {
const distance = this.distance(client.position, entity.position);
const timeSinceUpdate = Date.now() - entity.lastSentTo.get(client.id);
const importance = entity.importanceScore; // Enemies > terrain
// Higher priority = closer + older update + more important
return (importance * 1000) / (distance + 1) + timeSinceUpdate / 100;
}
}Rollback Causes Visual Artifacts
Id
rollback_visual_artifacts
Severity
medium
Category
visual
Symptoms
- Characters flicker or jump
- Attacks appear, disappear, reappear
- Audio plays twice
Problem
Rollback resimulates frames. If visual/audio effects trigger during simulation, they may fire multiple times or at wrong times.
Solution
Separate simulation state from presentation. Only trigger effects on confirmed frames. Track which effects already played.
Code Example
class RollbackSafeEffects {
private confirmedFrame = 0;
private pendingEffects: Map<number, Effect[]> = new Map();
private playedEffects: Set<string> = new Set();
// Called during simulation (may be rolled back)
queueEffect(frame: number, effect: Effect) {
const effects = this.pendingEffects.get(frame) ?? [];
effects.push(effect);
this.pendingEffects.set(frame, effects);
}
// Called when frame is confirmed (never rolled back)
confirmFrame(frame: number) {
const effects = this.pendingEffects.get(frame) ?? [];
for (const effect of effects) {
const effectId = `${frame}-${effect.type}-${effect.entityId}`;
// Only play once
if (!this.playedEffects.has(effectId)) {
this.playedEffects.add(effectId);
this.playEffect(effect);
}
}
// Clean up old effects
this.pendingEffects.delete(frame);
this.confirmedFrame = frame;
}
// Called on rollback
onRollback(toFrame: number) {
// Clear effects from rolled-back frames
for (const [frame] of this.pendingEffects) {
if (frame > toFrame) {
this.pendingEffects.delete(frame);
}
}
}
private playEffect(effect: Effect) {
switch (effect.type) {
case 'sound':
this.audio.play(effect.soundId);
break;
case 'particle':
this.particles.spawn(effect.particleType, effect.position);
break;
case 'animation':
this.animations.trigger(effect.entityId, effect.animationName);
break;
}
}
}Skill Rating Compression Over Time
Id
matchmaking_skill_compression
Severity
medium
Category
matchmaking
Symptoms
- Veteran players seem equal skill
- New players ranked too high
- Smurfs dominate low ranks
Problem
Elo/Glicko ratings compress over time. Uncertainty decreases, making it hard to correct initial miscalibrations. Smurfs exploit this.
Solution
Periodic uncertainty injection. Placement matches with high K-factor. Confidence decay for inactive players.
Code Example
class RatingSystem {
updateRating(player: Player, opponent: Player, won: boolean) {
// Decay uncertainty for inactive players
const daysSincePlay = (Date.now() - player.lastGameTime) / 86400000;
if (daysSincePlay > 30) {
player.sigma = Math.min(350, player.sigma + daysSincePlay * 2);
}
// Higher K-factor for uncertain ratings
const kFactor = this.getKFactor(player);
// Standard Glicko update...
const expected = this.expectedScore(player.mu, opponent.mu);
const actual = won ? 1 : 0;
player.mu += kFactor * (actual - expected);
player.sigma *= 0.9; // Reduce uncertainty after game
player.lastGameTime = Date.now();
}
private getKFactor(player: Player): number {
// High uncertainty = bigger swings
// Placement matches (first 10) = even bigger
const baseFactor = 32;
const uncertaintyMultiplier = player.sigma / 100;
const placementMultiplier = player.gamesPlayed < 10 ? 2 : 1;
return baseFactor * uncertaintyMultiplier * placementMultiplier;
}
// Detect potential smurfs
detectSmurf(player: Player): boolean {
if (player.gamesPlayed < 20) {
const expectedWinRate = 0.5;
const actualWinRate = player.wins / player.gamesPlayed;
// Way too good for their rating
if (actualWinRate > 0.9) {
return true;
}
}
return false;
}
}TCP for Real-Time Game State
Id
tcp_for_game_state
Severity
high
Category
protocol
Symptoms
- Periodic freezes then catch-up
- Rubber-banding despite good ping
- Delays compound under packet loss
Problem
TCP guarantees order and delivery. If packet 5 is lost, packets 6-20 are buffered until 5 arrives. For game state, old positions are worthless - you want latest state, not ordered history.
Solution
Use UDP or WebRTC DataChannel (unreliable mode) for game state. Use TCP/WebSocket for reliable events (chat, purchases).
Code Example
class DualChannelNetwork {
private reliableChannel: WebSocket; // TCP for events
private unreliableChannel: RTCDataChannel; // UDP-like for state
constructor() {
// Reliable channel for important events
this.reliableChannel = new WebSocket('wss://game.server/events');
// Unreliable channel for game state
this.setupWebRTCDataChannel();
}
private async setupWebRTCDataChannel() {
const pc = new RTCPeerConnection({ /* ICE servers */ });
this.unreliableChannel = pc.createDataChannel('gamestate', {
ordered: false, // Don't wait for missing packets
maxRetransmits: 0 // Never retransmit (true UDP behavior)
});
this.unreliableChannel.onmessage = (event) => {
const state = this.decode(event.data);
// State updates - okay to miss some
this.handleStateUpdate(state);
};
}
// Use appropriate channel for each message type
sendStateUpdate(state: GameState) {
if (this.unreliableChannel.readyState === 'open') {
// Latest state over unreliable - okay if lost
this.unreliableChannel.send(this.encode(state));
}
}
sendEvent(event: GameEvent) {
// Important events over reliable
this.reliableChannel.send(JSON.stringify(event));
}
}Game Networking - Validations
Client Position Trust
Id
trusting_client_position
Description
Accepting position data directly from client without validation
Severity
critical
Category
security
Pattern
player\.(position|pos|location|transform)\s=\s(data|packet|message|msg|payload|event\.data)\.
File Patterns
- */.ts
- */.js
- */.cs
- */.cpp
Message
CRITICAL: Never trust client-reported position! Server must simulate movement from inputs.
Fix Suggestion
Instead of: player.position = data.position;
Do: // Client sends inputs, not position const input = validateInput(data.input); player.position = simulateMovement(player, input);
Learn More
https://gafferongames.com/post/client_server_connection/
Client Health Trust
Id
trusting_client_health
Description
Accepting health/damage values directly from client
Severity
critical
Category
security
Pattern
(player|entity|character)\.(health|hp|damage|armor)\s=\s(data|packet|message|msg|payload)\.
File Patterns
- */.ts
- */.js
- */.cs
Message
CRITICAL: Never trust client-reported health! Godmode hacks exploit this vulnerability.
Fix Suggestion
Server calculates all damage. Client sends attack actions, server validates hit and calculates damage.
Client Hit Detection
Id
client_side_hit_detection
Description
Processing hit/damage events reported by client
Severity
critical
Category
security
Pattern
on\(['"]?(hit|damage|kill|headshot)['"]?,\s(?:function\s)?\([^)]\)\s(?:=>)?\s\{[^}]apply(?:Damage|Hit)
File Patterns
- */.ts
- */.js
Message
CRITICAL: Client-side hit detection enables aimbots! Server must perform all hit detection.
Fix Suggestion
Client sends: { type: 'shoot', origin, direction, timestamp } Server performs: raycast with lag compensation, validates hit
Missing Input Rate Limiting
Id
no_input_rate_limit
Description
Processing client inputs without rate limiting
Severity
high
Category
security
Pattern
on\(['"]?(input|move|action)['"]?,.\{(?!.(?:rateLimit|throttle|lastInput|inputCount))
File Patterns
- */.ts
- */.js
Message
HIGH: Missing input rate limiting allows speed hacks! Limit inputs per second (e.g., max 64/second).
Fix Suggestion
const inputCounts = new Map(); const MAX_INPUTS_PER_SEC = 64;
socket.on('input', (data) => { const count = inputCounts.get(socket.id) ?? 0; if (count >= MAX_INPUTS_PER_SEC) { flagPlayer(socket.id, 'input_flood'); return; } inputCounts.set(socket.id, count + 1); // process input });
Non-Deterministic Random
Id
math_random_in_simulation
Description
Using Math.random() in game simulation code
Severity
critical
Category
determinism
Pattern
Math\.random\(\)
File Patterns
- */game.ts
- */simulation.ts
- */world.ts
- */entity.ts
- */physics.ts
Exclude Patterns
- */.test.*
- */.spec.*
- /ui/
Message
CRITICAL: Math.random() breaks determinism! Use seeded PRNG for lockstep/rollback games.
Fix Suggestion
class SeededRandom { constructor(private seed: number) {}
next(): number { let t = this.seed += 0x6D2B79F5; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); return ((t ^ t >>> 14) >>> 0) / 4294967296; } }
const rng = new SeededRandom(sharedSeed); const value = rng.next(); // Deterministic!
System Time in Simulation
Id
date_now_in_simulation
Description
Using Date.now() or new Date() in deterministic simulation
Severity
high
Category
determinism
Pattern
(Date\.now\(\)|new Date\(\))
File Patterns
- */simulation.ts
- */game-state.ts
- */world.ts
- */tick.ts
Message
HIGH: System time varies between machines! Use synchronized game tick for deterministic simulation.
Fix Suggestion
// Use game tick instead of real time class GameClock { private tick = 0; private tickRate = 60;
getCurrentTick(): number { return this.tick; }
getGameTime(): number { return this.tick / this.tickRate; } }
Floating Point Equality
Id
float_equality
Description
Comparing floats with == or === in deterministic code
Severity
medium
Category
determinism
Pattern
===?\s[0-9]+\.[0-9]+|[0-9]+\.[0-9]+\s===?
File Patterns
- */simulation.ts
- */physics.ts
- */collision.ts
Message
MEDIUM: Float comparison varies across platforms! Use epsilon comparison or fixed-point math.
Fix Suggestion
const EPSILON = 0.0001;
function floatEquals(a: number, b: number): boolean { return Math.abs(a - b) < EPSILON; }
Non-Deterministic Iteration
Id
unordered_iteration
Description
Iterating objects/maps without sorting in deterministic code
Severity
high
Category
determinism
Pattern
for\s\(\s(?:const|let|var)\s+\w+\s+in\s+\w+|Object\.keys\([^)]+\)\.(?:forEach|map)|\.forEach\(
File Patterns
- */simulation.ts
- */tick.ts
- */lockstep.ts
Message
HIGH: Iteration order may vary! Sort before iterating for determinism.
Fix Suggestion
// BAD for (const id in players) { ... }
// GOOD const sortedIds = Object.keys(players).sort(); for (const id of sortedIds) { ... }
JSON for Network Packets
Id
json_stringify_for_packets
Description
Using JSON.stringify for game state packets
Severity
medium
Category
performance
Pattern
JSON\.stringify\([^)]+\).*(?:send|emit|broadcast)
File Patterns
- */network.ts
- */server.ts
- */socket.ts
Message
MEDIUM: JSON is verbose for game state. Use binary protocol (msgpack, protobuf) at scale.
Fix Suggestion
// Use msgpack for 30-50% smaller payloads import { encode, decode } from '@msgpack/msgpack';
socket.send(encode(gameState));
Broadcasting Full State
Id
full_state_broadcast
Description
Sending complete game state to all clients every tick
Severity
high
Category
performance
Pattern
broadcast\(.gameState|clients\.forEach.send.*(?:state|entities|world)
File Patterns
- */server.ts
- */network.ts
Message
HIGH: Full state broadcast doesn't scale! Use delta compression and interest management.
Fix Suggestion
// Delta compression const delta = computeDelta(lastAckedState, currentState); client.send(compressDelta(delta));
// Interest management const nearbyEntities = spatialGrid.query(player.position, viewRadius); client.send(filterEntities(state, nearbyEntities));
Blocking I/O in Game Loop
Id
blocking_io_in_tick
Description
Synchronous I/O in tick/update function
Severity
critical
Category
performance
Pattern
(readFileSync|writeFileSync|execSync|query\([^)]*\)(?!\.then))
File Patterns
- */tick.ts
- */update.ts
- */game-loop.ts
Message
CRITICAL: Blocking I/O stalls entire game! Use async patterns or background workers.
Fix Suggestion
// Move I/O outside tick loop class AsyncSaveManager { private pendingSaves: GameState[] = [];
queueSave(state: GameState) { this.pendingSaves.push(structuredClone(state)); }
async processSaves() { while (this.pendingSaves.length > 0) { const state = this.pendingSaves.shift(); await saveToDatabase(state); } } }
Unbounded State History
Id
unbounded_history
Description
Pushing to history array without cleanup
Severity
high
Category
performance
Pattern
history\.push\([^)]+\)(?!.(?:shift|splice|slice|\.length\s>))
File Patterns
- */.ts
- */.js
Message
HIGH: Unbounded history causes memory leak! Cap history buffer size.
Fix Suggestion
const MAX_HISTORY = 300; // ~5 seconds at 60 tick
function addToHistory(state: GameState) { history.push(state); while (history.length > MAX_HISTORY) { history.shift(); } }
Missing Sequence Numbers
Id
no_sequence_number
Description
Network packets without sequence numbers
Severity
medium
Category
protocol
Pattern
send\(\s\{(?!.(?:seq|sequence|tick|frame))
File Patterns
- */network.ts
- */socket.ts
Message
MEDIUM: Packets need sequence numbers! Required for ordering, deduplication, and ack.
Fix Suggestion
let sequenceNumber = 0;
function sendPacket(data: any) { socket.send({ seq: sequenceNumber++, timestamp: Date.now(), ...data }); }
Missing Acknowledgment System
Id
no_packet_ack
Description
Sending reliable data without acknowledgment
Severity
medium
Category
protocol
Pattern
send\(.type:\s'"'"
File Patterns
- */network.ts
Message
MEDIUM: Important events need acknowledgment! Implement ack/retry for reliable delivery.
Fix Suggestion
class ReliableChannel { private pending = new Map<number, { data: any, retries: number }>(); private seq = 0;
send(data: any) { const id = this.seq++; this.pending.set(id, { data, retries: 0 }); this.socket.send({ ...data, reliableId: id }); this.scheduleRetry(id); }
onAck(id: number) { this.pending.delete(id); }
private scheduleRetry(id: number) { setTimeout(() => { const pending = this.pending.get(id); if (pending && pending.retries < 5) { pending.retries++; this.socket.send({ ...pending.data, reliableId: id }); this.scheduleRetry(id); } }, 100); } }
Missing Client Timestamp
Id
missing_timestamp
Description
Shot/action packets without client timestamp
Severity
high
Category
lag_compensation
Pattern
send\(\s\{[^}]type:\s['"](?:shoot|fire|attack|ability)['"][^}]\}(?!.*timestamp)
File Patterns
- */client.ts
- */input.ts
Message
HIGH: Actions need timestamps for lag compensation! Server uses timestamp to rewind world state.
Fix Suggestion
function sendShot(origin: Vec3, direction: Vec3) { socket.send({ type: 'shoot', origin, direction, clientTimestamp: Date.now(), // When player clicked clientTick: currentTick // Game tick when fired }); }
Rendering Server State Directly
Id
direct_state_render
Description
Rendering remote entities from server state without interpolation
Severity
medium
Category
visual
Pattern
entity\.(position|transform)\s=\sserverState\.|render\(serverState\)
File Patterns
- */render.ts
- */client.ts
- */game.ts
Message
MEDIUM: Direct rendering causes jitter! Buffer and interpolate for smooth visuals.
Fix Suggestion
class RemoteEntityRenderer { private stateBuffer: StateSnapshot[] = []; private renderDelay = 100; // ms behind real-time
onServerState(state: EntityState, serverTime: number) { this.stateBuffer.push({ state, serverTime }); }
getInterpolatedState(): EntityState { const renderTime = Date.now() - this.renderDelay; // Interpolate between buffered states return this.interpolate(renderTime); } }
Hardcoded Matchmaking Parameters
Id
hardcoded_matchmaking
Description
Magic numbers in matchmaking without configuration
Severity
low
Category
matchmaking
Pattern
(?:ratingRange|skillDiff|queueTime|expandRate)\s[=:]\s\d+(?!\s*[,}])
File Patterns
- */matchmaking.ts
- */matchmaker.ts
Message
LOW: Hardcoded matchmaking values hurt tuning. Use configuration for easy adjustment.
Fix Suggestion
const MATCHMAKING_CONFIG = { initialRatingRange: 100, expandRatePerSecond: 10, maxRatingRange: 500, maxQueueTimeSeconds: 120, teamSizeBalance: true };
Missing TURN Server Fallback
Id
missing_turn_fallback
Description
WebRTC without TURN server configuration
Severity
high
Category
connectivity
Pattern
RTCPeerConnection\(\s\{[^}]iceServers:\s\[[^\]]stun[^\]]\](?![^\]]turn)
File Patterns
- */webrtc.ts
- */p2p.ts
- */connection.ts
Message
HIGH: STUN alone fails for symmetric NAT! ~15% of players need TURN relay fallback.
Fix Suggestion
const config = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'turn:your-server.com:3478', username: 'user', credential: 'pass' } ] };