ScribblaZ Architecture

ScribblaZ keeps gameplay authority in one Nakama TypeScript match while a Svelte root coordinates platform events and a rune-backed controller owns local presentation state.

LayerCurrent source of truth
Plugin contractgames/scribblaz/funday-plugin.json
Root component and bridgesrc/ScribblaZGame.svelte
Client statesrc/lib/ScribblaZController.svelte.ts
Panel placementsrc/lib/PanelLayoutManager.svelte.ts
Authoritative game loopserver/match_handler.ts
Persistence RPCsserver/rpc.ts

Docs: Hub · Game design · Opcodes · Runbook · Verification · Nakama Multiplayer · Games Catalog

Component and state split

ScribblaZGame.svelte receives platform props, sends match data through platformSocket.sendMatchState, translates platform-bus events, and renders the game composition. Its handleMatchData decodes the bridge message before handing the opcode and payload to the controller (src/ScribblaZGame.svelte:126-170).

ScribblaZController holds rune state for identity, phase, players, canvas events, hint, timer, tools, MRU colors, trainer state, and terminal results (src/lib/ScribblaZController.svelte.ts:14-66). It derives roles and start eligibility from the received player list (:68-94) and applies incoming match opcodes (:101-269). PanelLayoutManager.svelte.ts owns panel elements, layouts, dragging, dock/attachment previews, persistence, and reflow (src/lib/PanelLayoutManager.svelte.ts:28-100) rather than authoritative gameplay.

The root sends draw, word, guess, ready, start, settings, and resync requests (ScribblaZGame.svelte:248-337, :426-464). The controller is therefore a client projection, not a rules engine.

Server authority

server/match_handler.ts imports shared DEFAULTS, OP, phases, and SETTINGS_OPCODE (:1-25) and is the configured production handler. It owns players, spectators, current word, opaque word-option mapping, strokes, score maps, timers, and creator identity (:28-100).

The authoritative transition functions are:

FunctionResponsibility
startWordPick (match_handler.ts:621-666)Advances the round, clears round state, rotates the drawer, makes three hashed options, and sends plaintext only to the drawer.
startDrawing (:668-698)Starts the configured drawing timer and sends drawer/non-drawer hint payloads.
endRound (:700-727)Builds score summary, broadcasts the word and scores, and clears retained strokes.
endGame (:729-756)Sorts final scores, broadcasts winner/top three, and closes the match label.
resetGameToWaiting (:758-791)Resets round state, scores and readiness, then broadcasts an authoritative waiting GAME_STATE.

matchLoop routes settings, draw operations, word choice, guesses, lobby readiness/start, kick, and resync opcodes (:961-1086) and advances timed phases from the 5 Hz server tick (:1089-1163). The browser’s 50 ms interpolation only smooths UI between authoritative updates (ScribblaZController.svelte.ts:291-317).

Join, spectator, and leave ownership

The server accepts a new player only in waiting while the player count is below the configured maximum. Outside waiting, a new arrival becomes a spectator only while fewer than 20 spectators exist (match_handler.ts:839-851, :853-910). Spectators can chat but cannot score guesses (:511-618).

On leave, the server marks the presence disconnected, reassigns the creator if needed, and—when fewer than DEFAULTS.MIN_PLAYERS remain in word_pick, drawing, or round_end—broadcasts a system explanation then resets to waiting (match_handler.ts:913-958). This quorum-reset behavior is visible in the current handler but still needs the manual evidence recorded as pending in Verification.

Reconnect and delta strokes

The root persists a joined match ID in localStorage, asks the platform to rejoin a stored match, and requests a sync after match join/state and socket reconnection (ScribblaZGame.svelte:431-464, :520-540). A match-state payload is applied by the controller; when it has isDelta and a strokes array, the controller appends those strokes rather than replacing the existing local canvas history (ScribblaZController.svelte.ts:104-140).

REQUEST_SYNC carries { lastStrokeIndex }. The server responds with GAME_STATE, and when that index is in range, returns state.strokes.slice(lastStrokeIndex) with isDelta: true; it separately sends shared settings (match_handler.ts:1060-1077). During drawing it also re-sends the drawer’s secret word in HINT_UPDATE (:1078-1084). The currently visible handler also re-sends drawer word options during word_pick (:1072-1077); treat its end-to-end reconnect behavior as pending until manually exercised.

Persistence boundary

scribblaz_save_drawing accepts { word, drawerName, lines, isPublic }, rejects missing word/lines, and writes structured drawing data to scribblaz_gallery; public work uses the system user ID and public read permission (server/rpc.ts:3-44). scribblaz_get_gallery lists that public collection with limit and cursor, returning { success, drawings, cursor } (:46-69). These RPCs store drawings; they do not decide match score or phase.

Ask Docs

AI assistant to help answer questions about the documentation. Answers are read-only and cite docs/source.

Hi! How can I help you with the documentation today? Answers are read-only and cite docs/source.

Ctrl+Enter to send