Playground: disposable chain, test WCHI, wiped without warning
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 public playground, because the template has no chain of its own: anything with a second player in it happens there.

Clone and run

The template is private during the curated phase: /repos says how to ask for access.

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.

Optional: connect to a playground

PLAYGROUND_ORIGIN=https://test-arcade.xaya.io 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. The first three are built from PLAYGROUND_ORIGIN: https://test-arcade.xaya.io is the public playground. A match needs your game registered there first: attach it to the playground (Testing covers how).

ValueWhat it does
WS_PROXY_DESTINATION=$PLAYGROUND_ORIGIN/relaysame-origin /ws → the playground's relay
CHAIN_PROXY_DESTINATION=$PLAYGROUND_ORIGIN/chainsame-origin /chain → the playground's chain RPC
GSP_PROXY_DESTINATION=$PLAYGROUND_ORIGIN/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 SDK's four test wallets. A playground funds and names its shell's own instead, so matches are played on its /play/<slug>.

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. Your game is on that plane because you attached it: the playground bakes the bundle you uploaded, registers the blob and the row, and the game is live twice over: bare at /g/<slug>/ on the plane's games origin, and inside the plane's arcade shell, 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.

A playground is a disposable test plane: it is reset without warning. It reserves nothing: a name that is free there tells you nothing about the real chain. Quickstart Part 3 walks it end to end; Submitting your game is the route in.

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 a 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.