Skip to content

Socket Events

All real-time communication uses Socket.io. Events follow the pattern: noun-verb.

Client → Server Events

Connection & Lobby

EventPayloadDescription
join-public{ gameMode?: string }Join a public game (defaults to pixel-battle)
create-room{ password?: string; gameMode?: string }Create private room (optional password & mode)
join-room{ code: string; password?: string }Join private room by code
leave-lobby{}Leave current game
leave-queue{}Leave waiting queue
view-mode{ gameMode: string }Track viewing mode selection page
leave-mode{}Stop tracking mode page viewing

Host Functions (Private Rooms)

EventPayloadDescription
host-start-game{}Start game manually (host only)
host-kick-player{ playerId: string }Kick player from lobby
host-change-password{ password: string | null }Set or remove room password

Gameplay

EventPayloadDescription
submit-drawing{ pixels: string }Submit 64-char hex artwork
vote{ chosenId: string }Vote for image in matchup
finale-vote{ playerId: string }Vote for finalist
copycat-rematch-vote{ wantsRematch: boolean }CopyCat mode: vote for rematch
pixelguesser-draw{ pixels: string }PixelGuesser: artist sends drawing update
pixelguesser-guess{ guess: string }PixelGuesser: player submits a guess
zombie-move{ direction: Direction }ZombiePixel: move player (8 directions)

User Management

EventPayloadDescription
change-name{ name: string }Change display name (1-20 chars)
restore-user{ displayName: string }Restore username from localStorage (discriminator is always new)
restore-session{ sessionId: string }Restore session after reconnect
pingcallbackLatency check
activity-ping{}Lightweight ping to prevent idle timeout

Server → Client Events

Connection & Errors

EventPayloadDescription
connected{ socketId, serverTime, user, sessionId }Welcome event with session token
error{ code, message?, retryAfter? }Error notification
idle-warning{ timeLeft: number }Warning before idle disconnect
idle-disconnect{ reason: string }Disconnected due to inactivity

Lobby Events

EventPayloadDescription
lobby-joinedSee belowSuccessfully joined lobby
room-created{ code, instanceId }Private room created
player-joined{ user: User }New player in lobby
player-left{ playerId, user?, kicked? }Player left
player-updated{ playerId, user }Player info updated
player-disconnected{ playerId, user, timestamp }Player disconnected (grace period)
player-reconnected{ playerId, user, timestamp }Player reconnected
name-changed{ user: User }Name change confirmed
left-lobby{}Left lobby confirmed
kicked{ reason: string }You were kicked
lobby-timer-started{ duration, startsAt }Countdown to auto-start
lobby-timer-cancelled{}Timer cancelled (not enough players)
password-required{ code: string }Password needed to join
password-changed{ hasPassword: boolean }Password updated

lobby-joined payload:

{
instanceId: string;
type: 'public' | 'private';
code?: string;
isHost?: boolean;
hasPassword: boolean;
players: User[];
spectator: boolean;
gameMode: string;
phase?: GamePhase;
prompt?: Prompt;
timerEndsAt?: number;
votingRound?: number;
votingTotalRounds?: number;
}

Game State Events

EventPayloadDescription
phase-changedSee belowPhase transition
submission-received{ success, submissionCount }Drawing submitted
submission-count{ count, total }Submission progress

phase-changed payload:

{
phase: GamePhase;
prompt?: Prompt;
promptIndices?: PromptIndices;
duration?: number;
startsAt?: number;
endsAt?: number;
message?: string;
round?: number;
totalRounds?: number;
}

Voting Events

EventPayloadDescription
voting-roundSee belowNew voting round started
vote-received{ success, eloChange? }Vote registered
finale-startSee belowFinale phase started
finale-vote-received{ success }Finale vote registered

voting-round payload:

{
round: number;
totalRounds: number;
imageA: { playerId: string; pixels: string };
imageB: { playerId: string; pixels: string };
timeLimit: number;
endsAt: number;
}

finale-start payload:

{
finalists: Array<{
playerId: string;
pixels: string;
user: User;
elo: number;
}>;
timeLimit: number;
endsAt: number;
}

CopyCat Mode Events

CopyCat is a 1v1 memory-based game mode with unique phases.

EventPayloadDescription
copycat-image{ pixels: string }Reference image to memorize
copycat-resultSee belowRound results with accuracy
copycat-rematch-update{ votes, declined? }Rematch vote status

copycat-result payload:

{
yourPixels: string;
opponentPixels: string;
referencePixels: string;
yourAccuracy: number; // 0-100 percentage
opponentAccuracy: number;
winner: 'you' | 'opponent' | 'draw';
player: { user: User; accuracy: number };
opponent: { user: User; accuracy: number };
}

PixelGuesser Mode Events

PixelGuesser is a Pictionary-style game where one player draws while others guess.

EventPayloadDescription
pixelguesser-round-startSee belowNew round started
pixelguesser-drawing-update{ pixels: string }Artist’s canvas updated
pixelguesser-guess-result{ correct, guess, message? }Result of player’s guess
pixelguesser-correct-guessSee belowSomeone guessed correctly
pixelguesser-revealSee belowRound ended, answer revealed
pixelguesser-final-resultsSee belowGame over, final rankings

pixelguesser-round-start payload:

{
round: number;
totalRounds: number;
artistId: string;
artistUser: User;
isYouArtist: boolean;
secretPrompt?: string; // Only sent to artist
secretPromptIndices?: PromptIndices;
duration: number;
endsAt: number;
}

pixelguesser-correct-guess payload:

{
playerId: string;
user: User;
points: number;
timeMs: number; // How fast they guessed
position: number; // 1st, 2nd, 3rd...
remainingGuessers: number;
}

pixelguesser-reveal payload:

{
secretPrompt: string;
secretPromptIndices?: PromptIndices;
artistId: string;
artistUser: User;
artistPixels: string;
scores: PixelGuesserScoreEntry[];
duration: number;
endsAt: number;
}

pixelguesser-final-results payload:

{
rankings: PixelGuesserScoreEntry[];
totalRounds: number;
duration: number;
endsAt: number;
}

ZombiePixel Mode Events

ZombiePixel is a real-time infection game on a 32x32 grid with up to 100 players (bots fill empty slots).

EventPayloadDescription
zombie-game-stateSee belowGame state update (players, time, counts, items)
zombie-roles-assignedSee belowInitial role and position assignment
zombie-infection{ victimId, victimName, zombieId, zombieName, survivorsRemaining, timerExtendedBy }Player was infected (+1s timer)
zombie-healed{ healedId, healedName, healerId, healerName }Zombie was healed back to survivor
zombie-game-endSee belowGame ended with winner/stats
zombie-item-spawnedSee belowPower-up item spawned on the map
zombie-item-collected{ itemId, playerId, playerName, itemType, isZombie }Player collected an item
zombie-effect-startedSee belowEffect started (speed boost, healing touch)
zombie-effect-ended{ effectId, type, affectedId }Effect expired

zombie-game-state payload:

{
players: Array<{
id: string;
name: string;
x: number;
y: number;
isZombie: boolean;
isBot: boolean;
hasHealingItem: boolean;
}>;
timeRemaining: number;
survivorCount: number;
zombieCount: number;
items: ZombieItemClient[];
effects: ZombieEffectClient[];
zombieSpeedBoostActive: boolean;
zombieSpeedBoostRemaining: number;
playersWithHealingTouch: string[];
}

zombie-roles-assigned payload:

{
yourId: string;
yourRole: 'zombie' | 'survivor';
yourPosition: { x: number; y: number };
zombieCount: number;
survivorCount: number;
}

zombie-item-spawned payload:

{
id: string;
type: string; // 'speed-boost' | 'healing-touch'
x: number;
y: number;
icon: string;
color: string;
visibility: 'zombies' | 'survivors' | 'all';
}

zombie-effect-started payload:

{
effectId: string;
type: string;
affectedId: string; // Player ID or 'zombies'/'survivors'
expiresAt: number | null;
remainingUses: number | null;
sharedEffect: boolean;
icon: string;
color: string;
}

zombie-game-end payload:

{
winner: { id: string; name: string; isBot: boolean } | null;
zombiesWin: boolean;
duration: number;
stats: {
totalInfections: number;
gameDuration: number;
firstInfectionTime: number | null;
mostInfections: { playerId: string; name: string; count: number } | null;
longestSurvivor: { playerId: string; name: string; survivalTime: number } | null;
};
}
EventPayloadDescription
game-resultsSee belowFinal rankings

game-results payload:

{
prompt?: Prompt;
promptIndices?: PromptIndices;
rankings: Array<{
place: number;
playerId: string;
user: User;
pixels: string;
finalVotes: number;
elo: number;
}>;
compressedRankings?: string; // LZ-String if 50+ players
totalParticipants: number;
}

Queue & Server Status

EventPayloadDescription
queued{ position, estimatedWait }Added to queue
queue-update{ position, estimatedWait }Queue position changed
queue-ready{ message }Spot available
queue-removed{ reason }Removed from queue
server-status{ status, currentPlayers, maxPlayers }Server health
online-count{ count, total, byMode }Online players (total + per mode)

Session Events

EventPayloadDescription
session-restoredSee belowSession restored after reconnect
session-restore-failed{ reason }Restore failed
instance-closing{ reason }Game instance closing
game-modes{ available[], default }Available game modes

Rate Limits

EventLimit
Global50 requests/second per socket
submit-drawing5/minute
vote3/second
create-room3/minute
join-room5/10 seconds
change-name5/minute
copycat-rematch-vote2/10 seconds

Example Usage

import { io } from 'socket.io-client';
const socket = io('wss://spritebox.de');
// Handle connection
socket.on('connected', ({ user, sessionId }) => {
console.log('Connected as', user.fullName);
localStorage.setItem('sessionId', sessionId);
});
// Join public game (default mode)
socket.emit('join-public', {});
// Join specific game mode
socket.emit('join-public', { gameMode: 'copy-cat' });
// Handle phase changes
socket.on('phase-changed', ({ phase, promptIndices }) => {
if (phase === 'drawing') {
showCanvas();
}
});
// Submit artwork
socket.emit('submit-drawing', {
pixels: '0000000000000000000F0F00000F0F0000FFFF0000000000000000000000000'
});
// Handle voting
socket.on('voting-round', ({ imageA, imageB }) => {
displayMatchup(imageA, imageB);
});
socket.emit('vote', { chosenId: imageA.playerId });