Skip to content
Andy Colosimo edited this page Jun 12, 2026 · 1 revision

name: building-xaya-games description: Use when building or deploying a simple on-chain (non-channel) game on the Xaya platform — writing a GSP with libxayagame, running the XayaX Polygon bridge, porting the mover example to a new game or chain, registering Xaya names, or sending moves via the XayaAccounts contract. For real-time 1v1 channel games (off-chain signed states, disputes, relays), use xaya-game-skill instead.

Building Xaya Games (simple on-chain GSP)

Overview

A Xaya game = a GSP (Game State Processor: libxayagame + your C++ rules) that deterministically derives game state from moves stored on a blockchain, fed by the XayaX bridge. Everything below was verified live on Polygon mainnet (2026-06-12). Trust this file over memory — agents reconstructing these facts from memory invent XayaX flags, hedge on addresses, and get RPC semantics wrong.

Full verified file templates (patch, Dockerfile, compose, scripts, real outputs): REFERENCE.md in this skill directory. A complete worked tutorial lives in the Xaya tutorials wiki (Mover Parts 1–3).

Verified constants (Polygon mainnet)

Item Value
XayaAccounts (ERC-721 names + moves) 0x8C12253F71091b9582908C8a44F78870Ec6F304F
WCHI token (ERC-20, 8 decimals) 0xE79feAAA457ad7899357E8E2065a3267aC9eE601
Registration policy contract 0x997A8B19d200A453D77c3857E81Af31F680b3663
Name registration fee flat 1 WCHI (any length 1–10, verified via policy.checkRegistration)
Public Polygon RPC (Xaya team) https://polygon-node.xaya.io
Docker images bridge xaya/xayax (prebuilt, on Docker Hub), build base xaya/libxayagame
Move gas cost well under $0.01 in POL
Namespace for ALL player moves p

Choosing the architecture

Turn-based / async / MMO-style → simple on-chain moves (this skill). Real-time 1v1 with per-second interaction → game channels (xaya-game-skill).

Build recipe

  1. Pick a game id (e.g. gr). No on-chain registry — collision is by convention only. Moves arrive wrapped: a player sends {"g":{"gr":{...your move...}}}; your GSP receives only the inner object plus the name (no p/ prefix).
  2. Scaffold from mover (libxayagame/mover/): logic.cpp/hpp (GameLogic callbacks), moves.cpp/hpp (move parsing), main.cpp (DefaultMain wiring), proto/*.proto (protobuf state+undo — the verified-deterministic serialization; don't rely on JSON key ordering for state). pending.cpp optional — XayaX provides no pending feed.
  3. Add the chain case in GetInitialStateInternal: case Chain::POLYGON: height = <recent block ≤ current tip>; hashHex = ""; break; Empty hash = accept any block at that height (verified pattern). Check the live tip first (getblockchaininfo on XayaX, or polygonscan) — a future height breaks sync. The height is your game's genesis; moves before it are invisible. Never change it after launch.
  4. Handle the version check: XayaX reports version 1000000; libxayagame's default minimum is 1010200. Either set config.MinXayaVersion = 1000000; in your main.cpp (clean, field verified at defaultmain.hpp:180) or sed-patch the installed header before compiling (see REFERENCE.md — the verified tutorial path).
  5. Build with the xaya/libxayagame base image (libs preinstalled; minutes). Do NOT rebuild the whole libxayagame CMake tree (slow, needs jsonrpccpp/googletest/eth-utils from source). Do NOT use the image's prebuilt moverd — it's broken upstream (missing libmover.so). Full Dockerfile in REFERENCE.md.
  6. Run XayaX + GSP via compose. XayaX eth connector takes EXACTLY these flags (verified — do not invent others like --eth_ws_url, --genesis_height, --max_reorg_depth): eth --eth_rpc_url=<RPC> --accounts_contract=<XayaAccounts> --port=8000 --listen_locally=false --zmq_address=tcp://<service-name>:28555 --logtostderr. Fresh XayaX starts near the tip and serves within ~2 minutes; it backfills older blocks on demand.
  7. GSP flags: --xaya_rpc_url=http://<xayax-service>:8000 --xaya_rpc_protocol=2 --game_rpc_port=8600 --game_rpc_listen_locally=false --storage_type=sqlite --datadir=/xayagame --enable_pruning=1000 --pending_moves=false.
  8. Send moves: XayaAccounts.move('p', name, moveJson, MAX_UINT256, 0, address(0)) — nonce MAX_UINT256 skips the check; amount/receiver are for optional WCHI payments. Registration: policy.checkRegistrationWCHI.approve(XayaAccounts, fee)register('p', name). Verified scripts in REFERENCE.md.
  9. Verify with the GSP RPC: getnullstate until "state":"up-to-date", send a live move (sub-cent), getcurrentstate shows it. Expected log lines in REFERENCE.md.

Critical pitfalls

Pitfall Fix
Docker auto-assigns 192.168.0.0/20 to new networks when default pools are exhausted — shadows LAN routes, can cut off remote access (happened for real) ALWAYS pin the compose network subnet (e.g. 172.16.0.0/24) and check ip route for collisions
--zmq_address=tcp://localhost:28555 Must be the Docker service name — it's the address XayaX advertises; the GSP connects to it from another container
Default JSON-RPC protocol 1 XayaX speaks 2.0 — --xaya_rpc_protocol=2 or startup RPC errors
waitforchange("") assumed to return immediately Wrong: empty/unparseable hash WAITS for the next change or ~5 s internal timeout. Only a valid-but-stale hash returns immediately (libxayagame source)
Trusting any player JSON Invalid moves must be IGNORED (log + continue), never crash — anyone can send any JSON to your game id
First move semantics surprise (mover) A player's first move executes 1 step in its own block — {"d":"k","n":2} ends at (0,2), not (0,2)+1 block delay
C++ edits not picked up docker compose build --no-cache after source changes if in doubt
getpendingstate via XayaX Returns a "pending moves are not tracked" error; --pending_moves=true is harmless but useless (logs Not subscribing to pending moves)

Verification checklist

  • GSP log shows: Connected to RPC daemon with chain polygonDetected ZMQ blocks endpointGot genesis height from game: <N>Game did not specify genesis hash, retrieved <hash> → attach-step batches → done in ~2 min.
  • getnullstate{"chain":"polygon","gameid":"<id>","state":"up-to-date",...}.
  • Live round-trip: send a real move (costs <$0.01), watch Processed 1 moves forward in the log, confirm via getcurrentstate. Two independently-synced GSPs return byte-identical gamestate.

Clone this wiki locally