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.
| Value | What it does |
|---|---|
WS_PROXY_DESTINATION=https://test-arcade.xaya.io/relay | same-origin /ws → the playground's relay |
CHAIN_PROXY_DESTINATION=https://test-arcade.xaya.io/chain | same-origin /chain → the playground's chain RPC |
GSP_PROXY_DESTINATION=https://test-arcade.xaya.io/gsp | same-origin /gsp → the playground's GSP JSON-RPC |
NEXT_PUBLIC_POLYGON_RPC=/chain | tells 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=1 | turns 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
| Path | What 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.ts | The 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 sharedg/namespace (the GSP's own--game_id:xarcon the playground,arcbetaon arcade.xaya.io), not your game type; the SDK wraps every move withadapter.gameId, which must equalappConfig().moveNamespace. A channel's config isLE32(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.