Skip docs navigation
Runtime lifecycle, shell controls, shared state, and author-facing primitives for TPG games.

Game SDK Guide

Before choosing a game architecture, read the Game Capability Matrix. Browser rendering support is separate from transport, authority, persistence, and scale guarantees. The matrix also defines the exact reviewed manifest mapping to iframe sandbox and Permissions Policy grants.

For the full author workflow, including packaging, upload, review, and publish, see the TPG Game Authoring Guide.

TPG games are built around a strict runtime contract instead of arbitrary uploaded apps.

Runtime model

  • logic: authoritative game logic runtime
  • host-display: the main shared screen
  • controller: player-controlled surfaces
  • spectator: optional read-only or low-authority surfaces

In the current platform shell, logic runs on the host by default. The contract is intentionally shaped so the logic runtime can later move to a dedicated service without changing the game-facing API.

Lifecycle and surface readiness

Lifecycle and surface readiness are separate signals.

Lifecycle hooks describe the shell-owned game stage:

  • boot
  • ready
  • started
  • paused
  • ended
  • disposed

Surface-readiness hooks describe whether every required iframe surface can receive runtime events:

  • surfacesLoading(api, readiness) runs while at least one required surface is pending
  • surfacesReady(api, readiness) runs when readiness.allReady is true

The same game definition runs independently inside every mounted logic, host-display, controller, and spectator surface. Module variables are therefore local to one iframe; they are not room state.

The shell may replay lifecycle and readiness envelopes after iframe load, reconnect, participant changes, or readiness changes. Every hook can run more than once, including consecutive calls with the same value. Hook work must be idempotent. Guard authoritative mutations with api.context().isAuthority and check current durable state before initializing or advancing it.

Authoring with @tpgames/sdk

Shell controls

The runtime API now exposes:

  • reportLoading
  • endGame
  • returnToLobby
  • openSettings
  • reportAnalytics
  • getSettings
  • room
  • participants
  • me
  • getSharedState / setSharedState
  • getSharedStateSnapshot
  • getPlayerState / setPlayerState
  • getPlayerStateSnapshot
  • broadcast
  • sendToParticipant
  • subscribeSettings
  • subscribeLifecycle
  • subscribeLoading
  • subscribeContext
  • subscribeParticipants
  • subscribeSharedState
  • subscribePlayerState
  • subscribeMessages
  • context

subscribeContext(listener) immediately receives the current SurfaceContext, then receives each effective room, presentation, participant, or authority change. The runtime updates context() before calling the listener, so authority checks inside the callback always use the same snapshot. Keep the returned unsubscribe function when the subscription is shorter-lived than the surface; otherwise the iframe teardown disposes the runtime with the surface.

Controller identity can be provisional during bootstrap. Once participant state is available, use api.me()?.id ?? api.context().participantId as the current controller id. api.me() reflects the canonical live participant record, while context().participantId is the fallback before that record arrives. Recompute the value from subscribeParticipants and subscribeContext instead of caching the bootstrap fallback.

Game analytics

Games can report creator-facing milestones through reportAnalytics. These events are advisory telemetry: the runtime validates and forwards them through the bridge without changing room state, transport behavior, or session authority.

Allowed game-defined event classes:

  • milestone.reached
  • funnel.step
  • round.completed
  • outcome.recorded
  • mechanic.used

Use stable names such as tutorial-complete or prompt-vote. Dimensions must be string, number, or boolean values; metrics must be finite numbers. Do not include player-identifying fields such as player ids, names, email addresses, IP addresses, device ids, or precise locations.

State and message decision guide

Choose the narrowest channel whose delivery semantics match the data:

Do not use transient messages for facts that must survive reconnect. Do not use sendGameAction as a general game-to-game message bus; SDK-native games should prefer typed shared/player state and runtime messages.

Shared state is authority-only. Non-authority surfaces receive a typed not-authority rejection before the bridge is touched. Player state is participant-owned: controllers and spectators can update their own record, while authority may update any participant.

Player-state reads are filtered at the runtime boundary. Host-display and logic surfaces receive every participant record, each controller receives only its own record, and spectators receive none. Put room-wide public facts in shared state; do not copy private controller intent into shared state or broadcast messages.

State setters return a discriminated result:

  • applied means the authority committed the mutation at the returned revision.
  • accepted is the compatibility result when a legacy event-only bridge accepted the send but cannot acknowledge reduction.
  • rejected includes an actionable reason, current revision, and message.

getSharedStateSnapshot() and getPlayerStateSnapshot() return the last inbound value and revision. Pass that revision as expectedRevision for optimistic concurrency. A stale request is rejected without overwriting newer state. Getters remain unchanged for compatibility, and inbound subscription echoes remain the canonical local view.

Accessible canonical updates

Treat the inbound shared- or player-state subscription as the accessibility boundary for confirmed gameplay changes. Do not announce an optimistic controller intent before the authority echoes the canonical result.

  • Keep lifecycle and connection instructions in their own stable status region.
  • Render a second, initially empty role="status" region for concise gameplay results such as Count 12 or Pong received in 18 milliseconds.
  • Coalesce rapid canonical updates before changing that region so assistive technology receives the latest useful result instead of an unbounded queue of intermediate values.
  • For an ordered transcript, use a named role="log" with polite additions and hydrate existing history without replacing or re-announcing the whole list.
  • Leave focus on the control that initiated a successful action. Associate validation failures with their field through aria-invalid and aria-describedby.

Counter, Echo Lab, and Room Chat are the first-party reference implementations for these patterns.

Higher-level authoring primitives

@tpgames/sdk now also exposes a higher-level authoring layer for common party-game patterns:

  • defineSimpleGame<TSharedState, TPlayerState>() wraps the lower-level runtime API with typed state helpers
  • api.controllers() and api.controllerIds() expose controller collections directly
  • createDeadline() builds round and vote timers without hand-rolled arithmetic
  • setPhase() handles explicit phase transitions
  • syncPlayerValues() keeps per-player collections aligned with the active participant list

Dev harness

@tpgames/sdk-dev-kit provides an in-memory bridge and standalone harness for author testing without the full web shell.

The old browser-based /sdk-harness route is now internal-only and is not part of the public shell.

Use @tpgames/sdk-dev-kit as the default fast feedback loop before you boot a full room or run Playwright coverage.

Validation games in the shell

  • Room Chat verifies broadcast messaging through the host logic layer. Its authority policy trims surrounding whitespace, accepts at most 240 Unicode code points per message and five accepted messages per participant in ten seconds, retains at most 40 ordered messages, and evicts older entries before canonical state exceeds 48 KiB. Rejections remain bounded and identify the sending participant so controller surfaces can show the responsible player how to recover.
  • Echo Lab verifies targeted request/response behavior and ping/pong round trips. Echo messages trim surrounding whitespace and accept at most 240 Unicode code points at both the controller and authoritative boundaries.
  • Counter verifies authoritative shared state mutation.
  • Spotlight Vote is the first showcase package with lobby → prompt → vote → results phase logic and scoring.

Testing guidance

  • unit-test pure game reducers and derived state
  • browser-test host/controller communication across separate clients
  • use @tpgames/sdk-dev-kit to test lifecycle, participants, shared state, and transient messages without the browser shell
  • keep game logic transport-agnostic so the same game can run over local channels, WebRTC, or WebSocket adapters