caputchin
All docs
View raw .md

Game SDK

The contract a game author writes against. Distributed as @caputchin/game-sdk — a tiny package containing the register() helper and TypeScript types for the game contract. The customer-facing widget is published separately as @caputchin/widget; the split keeps games from transitively bundling the widget runtime — see ADR-0008. First-party games use the same public API — no private contract.

Games execute inside a sandboxed iframe mounted by the widget; the register() call writes into the iframe's own window.Caputchin.games, not the host page's. See ADR-0015. Authors do not have to change anything about how they write register() — the realm difference is invisible at the SDK level.

Registration — helper form

import { register } from '@caputchin/game-sdk';

register('bubble-pop', (container, bridge) => {
  // render UI into container
  bridge.complete({ score: 0.847, durationMs: 4200 });
});

The helper logs a console warning when an ID is already registered. Last write wins. There is no platform enforcement.

Function signature

(container: HTMLElement, bridge: Bridge) => (() => void) | void

interface Bridge {
  complete(result: { score: number; durationMs?: number }): void
  error(err: { code: string; message?: string }): void
}
Parameter Notes
container Element inside the game's sandboxed iframe. Render here. Style is naturally scoped — the iframe is its own document.
bridge Push-only interface to the widget. The game emits events upward; it does not subscribe. The widget controls game start via mode; the game does not need a start signal. Nickname collection is widget-owned. Widget-side errors are not surfaced to the game.
Return Optional cleanup function. Called by the widget if the game is unmounted.

score is normalized 0.0–1.0. There is no maxScore. The score is gameplay metadata, not a security signal — see principles.

Bridge methods

Method Effect
bridge.complete({ score, durationMs }) Round finished. May fire multiple times (replay, multi-round). The widget releases the held Cap redeem on the first call and locks the wrapped token; subsequent calls record additional rounds for scoreboard purposes only. durationMs is optional.
bridge.error({ code, message }) The game failed in a way the widget should surface. Triggers the widget's error event to the customer. The game is expected to release resources via its returned cleanup function.

The bridge is push-only at MVP. Game-load customization (theme, language, container size hints) will arrive as a third parameter at factory invocation in a later release — not as an event. See roadmap.

What is intentionally absent

  • No sessionId parameter. The game does not see Cap-side or platform-side identifiers. The widget owns that.
  • No start listener on the bridge. The widget controls when the game starts via the mode attribute. The factory call is itself the start signal — when the SDK invokes your factory, you may begin rendering.
  • No widget-side error listener on the bridge. Errors in the widget realm are not surfaced into the iframe. The game does not need to react; its own cleanup function handles unmount.
  • No unmount method. Return a cleanup function from your render call instead — closes over the right state without forcing a method shape.
  • No lifecycle hooks (mount, pause, resume). Render once; clean up via the returned function.
  • No maxScore. Normalize to 0.0–1.0 at the source.
  • No nickname / player-handle parameter. Scoreboard handles (when scoreboards ship — Post-MVP) are collected by the widget after the game completes, never by the game itself. Game authors do not see or set player identity in any form. See ADR-0014.

The contract is intentionally tiny so it can be stable across many years of game catalogue growth.

Multi-round and replay

bridge.complete(...) may fire multiple times. Common patterns:

  • "Play again" button after a round
  • Multi-round games where each round emits a score

The platform handles dedup of verification: the first bridge.complete(...) redeems Cap and locks the wrapped token; subsequent calls feed scoreboard data only. See api. Game authors do not need to think about this — call bridge.complete(...) whenever a round finishes, in whatever the natural cadence is for the game.

Defaults expected of marketplace games

These are author-declared in the marketplace manifest, never enforced. First-party games should ship responsive: true, touch: true, and accessible: true to set the bar.

Bundle requirements

The iframe loads exactly one script URL per game. Everything the game needs at runtime must be inside that bundle: sprites, sounds, fonts, WASM. Configure your bundler for a single self-contained output with assets inlined as data URLs and no code splitting. Path-relative fetch, dynamic import(), URL-spawned Worker, and external CDN fetches do not work inside the sandbox. See game-distribution.

Distribution

Two paths, both equivalent at runtime — see game-distribution. The SDK doesn't know or care which path delivered the script.