Build a game in one file.
Depend only on @anysync/game-sdk. Export a
defineGame() with a TV present and a phone
remote. We lazy-load it as its own chunk and pair phone↔TV over the
fastest link. No fail states, big targets, instant feedback — made for
ages 2–5.
1 · Getting started
- Copy the template folder
packages/games/_template/topackages/games/<your-id>/. - Edit
src/index.js— setmeta, and writepresent(TV) andremote(phone). - Put any images/sounds in
assets/. - Validate:
validateGame(default).ok === true. Send us the folder — we add one catalog row and rebuild.
import { defineGame, makeCanvas, loop, ui, SND, emojiAt, addButtons, rTitle }
from "@anysync/game-sdk";
export default defineGame({
meta: { id: "my-game", title: "My Game", emoji: "🎮",
control: "Buttons", group: "A", mode: "presenter", api: 1 },
present(host, ch) { /* draw TV; ch.on(...) */ return () => {}; },
remote(host, ch) { /* phone control; ch.send(...) */ return () => {}; },
});
2 · The contract
A game is { meta, present, remote }. Each half receives a
channel and returns a cleanup.
| Field | Type | Notes |
|---|---|---|
meta.id | slug | lowercase; chunk folder + pairing topic |
meta.title | string | card name |
meta.emoji | string | card thumbnail |
meta.control | enum | Buttons · Roller · Wheel · Slider · Tilt · Pad · Custom |
meta.group | A · B | A physical, B gyroscope |
meta.mode | presenter · single | phone→TV, or one device |
present(host, ch) | fn → cleanup | TV/iPad output |
remote(host, ch) | fn → cleanup | phone control |
The channel
ch.send(msg) | send a message to the other half |
ch.on(fn) | subscribe; returns unsubscribe |
ch.which?() | "direct" | "ws" | null — live transport |
Messages are tiny JSON objects with a type t and
normalized coordinates (positions 0..1, axes −1..1). Never send
pixels — the presenter de-normalizes to its own canvas.
3 · SDK API reference
Loading the live API surface from the SDK…
Everything above is imported from @anysync/game-sdk.
The list is read live from the deployed SDK, so it can never drift from
the docs.
4 · Assets
Ship files in your package's assets/. The build copies them
to /js/games/<id>/assets/…. Load them relative to your
module — no game needs to know its id or the origin:
import { assetUrl, loadImage } from "@anysync/game-sdk";
const url = assetUrl(import.meta.url, "sprite.png");
const img = await loadImage(import.meta.url, "sprite.png"); // decoded, or null
Always fall back if an asset is missing (draw an emoji). No game may ever fail.
5 · Interactive storybook
These run the real SDK, live. Poke them.
Tilt control
Full game — present + remote wired over one channel
Exactly how a paired session behaves, but on one page.
6 · Handing it over
- Confirm your default export passes
validateGame(). - Zip
packages/games/<your-id>/(src + assets). - Send it. We review, drop it into
packages/games/, add one catalog row, and rebuild — it lazy-loads in the APPS grid.
Backend optional: add a sibling
<your-id>-worker/ exporting
handle(request, env, ctx, sub) for /games/<id>/*.