Playground — disposable chain, test WCHI, wiped without warning
Builder docs

Make it yours

Turn the template into your own game — the full rename surface, one field at a time.

The template is a working game. Turning it into yours is a checklist, not a rewrite — the SDK, the channel protocol, and the platform plumbing do not change. There are two starting points, and the first is a complete, registerable game on its own.

Path A — re-identify only (a reskin)

Keep the template's rules, blob, and channel wiring exactly as they are; give it a new name, game key, and storage prefix. Keeping the template rules is a valid finished state. The facts you keep (and register): seat range 2..3 and cfgSuffix 0200000001000000 — the suffix is PROD_CFG_SUFFIX in e2e/match-cfg.ts, the single file that owns the channel-config contract, pinned by e2e/onchain-scenarios.test.ts, and both are written up in the template's README.md (Make your own game from this) and blob/MANIFEST.md.

A game's identity is single-sourced, so the core of this is a small, closed edit — but the surface is larger than that section's checklist: a full re-identification reaches every place the template's own name is baked in. The nine edits below are the ones that fail silently or weirdly if you miss them, which is why they get a table; the rest fail loudly the first time you run the ladder (see Testing). Expect the ladder — not this list — to be what tells you when you are done.

EditWhat to change
src/app-identity.tsGAME_KEY, TITLE, STORAGE_PREFIXthe one source of every id. The adapter, configureApp(), the page <title>, the whole e2e suite, the on-chain smoke gate, and the wager fixture all import from here. MOVE_NS derives itself; leave it.
package.json"name" (plus description/repository for tidiness).
package.json's arcadeGameLibDirDeclares your game's lib dir — "arcadeGameLibDir": "src/lib/xayaman" in the template. Rename it in the same commit as the directory below: e2e/imports.test.ts reads this field and uses it as its sweep root, and a stale or missing value hard-FAILs that gate. (Before SDK 0.7.0 this slot was a #sdk-proto imports map pointing at a two-file sdk-proto{,.node}.ts twin; the isomorphic @xayaarcade/sdk/core entry removed the need for both, and they are deleted.)
src/lib/<game>/The template's game directory is src/lib/xayaman/ — channel, packed codec, board render, converter, bot. Rename it and sweep every import of it.
tests/repo/copyright-guard.test.tsThe self-identity pin block at the top — three literals (pkg.name, GAME_KEY, STORAGE_PREFIX). It is deliberately an independent copy that catches a half-finished rename: update it to your values. Everything below it — the forbidden-title sweep — stays untouched.
blob/tests/run-tests.shThe test image tag. The script hardcodes xayaman-testenv:local and only rebuilds when docker image inspect fails — so two forks on one machine silently share one stale image and you debug a battery that is not testing your code. Derive the tag from your GAME_KEY.
package-lock.jsonThe package name appears twice — the root name and the packages[""] entry. Edit both by hand; no reinstall and no registry access is needed.
docker/Dockerfile.frontend, docker/frontend.env.example, scripts/build-frontend-image.shThe NEXT_PUBLIC_GAME_ID=xbm default (the g/ namespace the deployment posts to). Set it to your game key or always pass it explicitly. The image tag auto-derives from the package name — no edit there.

The gate that reads it

e2e/imports.test.ts does not hardcode the template's game directory. It reads package.json's arcadeGameLibDir and uses that directory as its sweep root — so a fork that renames src/lib/<game>/ and that field together needs no edit here. Its ALLOW set contains only the gate file itself: no source file is exempt, because the /core entry needs no browser-twin exception.

It fails closed: a sweep root with no tracked .ts/.tsx files is a hard FAIL that names the root and tells you to update package.json's arcadeGameLibDir. Rename only the directory and this gate stops you instead of quietly shrinking to the e2e/ files and printing a green PASS. Still glance at the file count in its PASS line (N shared/e2e files): a number that drops after a rename means something left the sweep.

What else the ladder will tell you

  • Only tracked files exist to the gates. The import gate and the copyright guard both read git ls-files, so a new directory that is not yet git added reports as "no tracked files under <dir>" rather than as a rename. Stage new files before running the ladder.
  • The template's name is refused anywhere. Once your three pins differ from the template's, the guard's template-identity sweep fails any tracked file outside vendor/, .claude/skills/, rules/ and blob/ that still contains the template's name or uses its game key as a word — a README link to the template repository included. Credit it without spelling its name, or not at all.
  • Include every header you use in rules/. The wasm build and the native test build resolve transitive includes differently, so a type that arrives for free under wasi-sdk (size_t without <cstddef>) fails the native battery; a green build-blob.sh does not prove the C++ tests compile.
  • The first blob/tests/run-tests.sh needs the network — it builds the test image, and that build downloads the pinned wasmtime C API. The image derives from the libxayagame image for the heritage oracle's dependencies; a fork that has deleted rules/heritage/ needs only a C++ toolchain, libjsoncpp-dev and wasmtime, and can base it on a slim Debian instead.

Then rebuild the frontend image and hand over under your new key — see Submitting your game.

Path B — new rules (a different game)

Do everything in A, then replace the game itself:

  1. Replace rules/ with your C++ core. Keep the arcade_* ABI and the zero-import discipline (see The rules blob). rules/heritage/ leaves when and only when rules/ does — it is the independent oracle for two blob-battery tests, meaningful only against the rules it was ported from. Delete both together (plus the board/packed-core Makefile targets); on a reskin, touch neither.
  2. Decide your channel config and therefore your cfgSuffix (or none). Record it in blob/MANIFEST.md, in the register command, and in e2e's PROD_CFG_SUFFIX (pinned by a test).
  3. Build the blob in the pinned container (bash blob/build-blob.sh) and commit it with its .sha256. The builder image (arcade-blob-builder:local) is a game-agnostic pinned toolchain, built for you on first run.
  4. Write the adapter (rules + channel + renderer + input) and the app — see The SDK. Keep the root rules in src/app/globals.css (100dvh, overflow: hidden, overscroll-behavior: contain): they are what makes the game fit the frame the arcade gives it instead of scrolling inside it, and the SDK's squareBoard frame sizes a square board to that box for you. The game shape is spread wider than the adapter: src/lib/games/xayaman-adapter.ts and its exported symbol; the registerAdapter(GAME_KEY, …) call in src/bootstrap.ts (the registry key is GAME_KEY, not the move namespace); the board components and their CSS module names; src/hooks/use-xayaman-input.ts; the presentation constant in src/lib/arcade/presentation.ts; the game-shaped unit tests under tests/lib/, tests/hooks/, tests/components/, which rung 1 of the ladder runs; and a second game directory, src/game/ (types.ts, sudden-death.ts), which is separate from src/lib/<game>/ and just as game-shaped — e2e/harness.ts imports from both (its @/lib/<game>/… and @/game/types import lines sit side by side).
  5. Rewrite e2e/, which carries far more game shape than harness.ts alone. e2e/suite.ts has a scenario built on the template's mechanics and board fields (blast, bombs) your game will not have; e2e/match-cfg.ts owns PROD_CFG_SUFFIX and the cfg builder — this is the file to edit for a new cfg, and the rest import from it; e2e/onchain-scenarios.test.ts pins the literal cfg hex; e2e/onchain-scenarios.smoke.test.ts imports prodCfg and feeds the resulting ChannelConfig straight to judge.initialState(2, cfg) — since SDK 0.6.0 initialState takes a ChannelConfig value, so cfg bytes are never hand-assembled and a hand-rolled Uint8Array does not compile; e2e/packed-determinism.ts carries its own cfg builder, field names and trace filenames; e2e/selfplay.ts drives the template's heuristic bot through the harness. e2e/app-config.ts usually needs no edit — it reads @/app-identity like everything else.
  6. Keep the guards: the copyright guard (with your pins from A), the three determinism legs — native, wasmtime and V8, run by blob/tests/run-tests.sh and npm run e2e:determinism — and the five CI jobs (guard, blob, cpp, app, e2e in .github/workflows/ci.yml).

The identity fields

export const GAME_KEY = 'xbm';       // GSP game-type / registry key — frozen wire constant
export const MOVE_NS = process.env.NEXT_PUBLIC_GAME_ID?.trim() || GAME_KEY;
export const TITLE = 'Xayaman';      // display name in shared screens and the page title
export const STORAGE_PREFIX = 'xayaman';  // localStorage prefix (also gates <prefix>_debug)

MOVE_NS auto-derives from GAME_KEY; only a namespace-isolated fork deployment overrides it via NEXT_PUBLIC_GAME_ID, which must then match the GSP's --game_id.

STORAGE_PREFIX must be yours, and this one can break someone else's game. Games are served at <games-origin>/g/<slug>/ — one origin, separate from the shell's but shared with every other game — and localStorage is scoped to the origin, not the path. The prefix is the only thing namespacing the keys the SDK owns: the session keys, _dev_wallet, _name, _active_game, _channel_proof_<id>, _left_channels, _debug, all built from appConfig().storagePrefix in the vendored SDK and fed from src/bootstrap.ts. Ship the template's xayaman and you are writing over a live game's keys, not just your own.

The one rule that trips everyone: adapter.gameId must equal appConfig().moveNamespace. Both come from app-identity.ts for exactly this reason — if they drift, lifecycle moves and gameplay moves land in two different g/ namespaces and the channel silently splits in half. A bootstrap test pins the invariant; keep it.

The serving slug and the library listing are not in your repo — you supply them in the upload form, which today is the playground's /attach — submissions on arcade.xaya.io are closed during the curated phase. The serving path is not yours either: this arcade runs games-host in path mode (GAMES_MODE=path), so every game is mounted at /g/<slug>/ on the plane's games origin — on both public planes the shell's own hostname, under the operator's first-party declaration described in Overview — baked in from the /__arcade_base__ placeholder at acceptance. You never choose, see or hardcode it. (The manifest still records a port per game — that is the other serving mode, and nothing a builder on this arcade ever uses; see Hosting & registration.)

Toolchain notes (either path)

  • next build rewrites tsconfig.json and next-env.d.ts (the Next TypeScript plugin reflows both). That churn is not your edit — run git checkout -- tsconfig.json next-env.d.ts before committing.
  • npm run selfplay -- --games=100 is the one slow rung (15+ min, single-threaded; 100 is also the default, e2e/selfplay.ts). Everything else in the ladder runs in seconds. Run it deliberately, not in a tight loop.
  • Work in a real git repository. Two rungs shell out to git ls-files — the copyright guard, whose scope is the tracked file list (tracked() in tests/repo/copyright-guard.test.ts), and the imports gate (its ls helper in e2e/imports.test.ts) — and blob/check-blob.sh --rebuild builds from git archive HEAD, not your working tree (the rebuild-and-compare leg at the foot of blob/check-blob.sh), so an uncommitted file the build needs makes it fail. If you started from a downloaded copy rather than a clone, git init and commit once first. Outside a repository at all, git ls-files exits 128 and both gates die on the spot; inside one with nothing yet tracked it returns an empty list, and both gates fail closed on it — the copyright guard's first case asserts the tracked set is non-empty ("a guard over an empty set proves nothing", tests/repo/copyright-guard.test.ts), and the imports gate refuses an empty sweep root by name. Neither state is a green ladder.

The warning that saves a day

The seat range and the cfgSuffix in the registry are trusted as-is — nothing cross-checks them against your blob. A wrong maximum player count admits a channel your blob cannot seed; a wrong cfgSuffix wedges every channel part-filled. The machine check (requireRegistry()) catches a pin mismatch, not a bad suffix — so get these two right yourself. The rules blob explains the config format, and Hosting & registration covers the registry side.