Builder docs

Getting started

Fork the template, run it locally against the committed SDK, and tour the project.

The local journey is short: clone the template, install, and run it (the tooling you need first is What you need). The SDK needs no private registry — it ships as a committed tarball under vendor/; everything else installs from the public npm registry as usual.

There are two rungs, and you need both. The first is entirely offline — the rules, the channel logic and the UI, with no chain, no relay and no funds. The second points the same dev server at the hosted playground, because there is no local chain: anything with a second player in it happens there.

Clone and run

git clone https://github.com/xaya/arcade-xayaman.git && cd arcade-xayaman
npm ci

npm run e2e               # 12/12 channel scenarios — no chain, no relay, no funds
npm run e2e:determinism   # V8 matches the golden traces
npm run dev               # the UI at http://localhost:3001 — one client, no channel

npm run e2e runs the full off-chain channel suite (disputes, timeouts, forfeits, reconnects, out-of-order proofs) with no external services — twelve scenarios, listed in one array in e2e/suite.ts. Testing & determinism has the rest of the ladder.

npm run dev serves the frontend standalone on port 3001 (package.json). It works with no shell listening on the other end, because the arcade bridge is inert unless the app is actually embedded — but that is the whole of what it does, and it cannot open a channel. It sets no environment at all, so the browser's wagmi transports fall through to real Polygon (getChainRpcUrls in sdk/src/lib/config/service-urls.ts returns the public default when NEXT_PUBLIC_POLYGON_RPC is unset) and the test wallets stay off (DEV_WALLET_ENABLED in sdk/src/lib/chain/dev-wallet.ts needs NEXT_PUBLIC_DEV_WALLET=1). Use it for the board, the renderer and the input; for a lobby, a second player, or a single on-chain move, use the second rung.

One local-only difference to know before you write any asset path: the dev server runs with no basePath, while the arcade serves your bundle under /g/<slug>/ (next.config.ts — unset NEXT_PUBLIC_BASE_PATH means no basePath). So a root-absolute URL — fetch('/data.json'), <img src="/sprite.png">, url(/bg.png) — works here and breaks there. Keep every reference relative.

The second rung — point it at the playground

npm run dev:playground

That is a committed script in the template's package.json: the same next dev on the same port, with five values in front of it.

ValueWhat it does
WS_PROXY_DESTINATION=https://test-arcade.xaya.io/relaysame-origin /ws → the playground's relay
CHAIN_PROXY_DESTINATION=https://test-arcade.xaya.io/chainsame-origin /chain → the playground's chain RPC
GSP_PROXY_DESTINATION=https://test-arcade.xaya.io/gspsame-origin /gsp → the playground's GSP JSON-RPC
NEXT_PUBLIC_POLYGON_RPC=/chaintells the browser to use that proxy. Without it wagmi reads real Polygon while you believe you are on the fork, and the /chain rewrite sits there unused.
NEXT_PUBLIC_DEV_WALLET=1turns on the shared, pre-funded test wallets.

The first three are dev-server rewrites (next.config.ts, beforeFiles); only the last two reach the browser, and the test wallets need both of them — the gate is the flag and a non-empty NEXT_PUBLIC_POLYGON_RPC (DEV_WALLET_ENABLED, sdk/src/lib/chain/dev-wallet.ts), so the flag alone gives you no signable identity and one console error.

The recognition test. On the login screen you should see a row of Test Wallet 1–4 buttons above the usual connect button (sdk/src/components/WalletLogin.tsx) — a dev-wallet picker, not just a wallet-connect prompt. If all you get is the connect prompt, the environment never reached the browser; fix that before you debug anything else.

Do not move these into .env.local. Next auto-loads that file for next build as well as next dev, so a dotfile would bake dev-only endpoints and the dev-wallet flag into the very bundle you later upload — a game that plays perfectly against the playground and is broken on the real arcade. The inline prefix on one script is deliberate.

Pick a different Test Wallet in each of N separate browser profiles: the selection is a single per-origin localStorage key, so two tabs of one profile are one player. Attach your game at /attach and it goes live twice over: bare at https://test-arcade.xaya.io/g/<slug>/, and inside the plane's arcade shell — a card on its /games, framed at /play/<slug> with the header, the wallet picker and the brokered signing wrapped around it, exactly as an accepted game is presented on the real arcade.

The playground is a disposable public test plane: its own fork, auto-accepting uploads, wiped every Monday around 04:10 UTC and without notice in between, and it reserves nothing — a name that is free there tells you nothing about the real chain. Quickstart Part 3 walks the dry run end to end; Submitting your game is the real door.

Project tour

PathWhat it is
rules/The C++ game core — integer-only, no host imports. This is the game.
blob/The containerised deterministic build → rules.wasm + its .sha256, the checker, and the manifest.
blob/tests/The C++ battery: engine, golden vectors, packed-codec parity, the hostile-state corpus, native == wasmtime determinism, the fuel probe.
src/The Next frontend on the vendored SDK — the adapter, the board renderer, the input.
src/app-identity.tsThe one place your game's ids live (game type, move namespace, title, storage prefix).
scripts/The export/bundle build (build-export.sh --bundle) and the frontend-image build.
docker/Dockerfile.frontend — the standalone image the platform's compose runs.
e2e/The off-chain channel gate and the on-chain scenario runner.
vendor/The committed @xayaarcade/sdk tarball — the SDK is distributed this way, not from an npm registry.
tests/The frontend unit tests and the copyright guard.

The two seams to understand

  • The blob is opaque to the host. A move is an XayaAccounts.move() carrying {"g":{"<moveNamespace>":{…}}} — the plane's shared g/ namespace (the GSP's own --game_id: xarc on the playground, arcbeta on arcade.xaya.io), not your game type; the SDK wraps every move with adapter.gameId, which must equal appConfig().moveNamespace. A channel's config is LE32(seed) ++ cfgSuffix (or a 0-byte config when the registry carries no suffix). The host never parses your bytes — it hands them straight to the blob. See The rules blob.
  • The SDK is the only thing your UI imports. It plugs your game in through one GameAdapter. See The SDK.

When you are ready to make it your own, go to Make it yours.