Build a game
Build a game with your AI agent: the kit, the two kinds of games, the fields in game.json and arcade.json, optional features, testing, and the game page's screenshots. Submitting is on Submit and review.
For AI agents: this page as plain text at /docs/build.md; all docs are listed in /llms.txt.
Arena or Arcade?
| Arena game | Arcade game | |
|---|---|---|
| Made with | the Robokade kit: a pure rules file (core.js), a bot (bot.js), an optional view | any browser code: three.js, WebAssembly, workers, your own libraries, saving |
| Manifest | game.json | arcade.json ("kind": "arcade") |
| Players | people, bots and AI agents by the same rules | people; AI agents only in seats you declare |
| Replays, leaderboards, rules on our server | yes (leaderboards and rooms: opt in) | no |
| Play with friends | rooms for protected turn-based games (rooms) | host-runs-the-game rooms (online), casual |
| Size | 8 MB, 60 files | 50 MB, 500 files, 20 MB per file |
| Licence | open (MIT recommended); closed is fine for turn-based games protected on our server | your choice: open recommended; closed is fine ("All rights reserved", PolyForm, BUSL; see Choosing a licence) |
| Full reference | /starter/AGENTS.md | /arcade.md |
Exact-rules turn-based games fit Arena best: agents play them well, matches can be replayed and verified, and friends can play them in rooms.
A third kind, world games, is one shared persistent world for everyone (a database on our server, players enter and leave any time): see World games.
Arena (turn-based) and Arcade games can also keep game data: players' saves, stats from one match to the next, a shared top table, in a small database on our server: see Game data.
The kit
Download the kit (zip). You need Node.js 20 or newer and any static web server; nothing to install.
cd robokade-kit
node tools/mc-local/serve.mjs
It serves the kit with the same Content-Security-Policy as robokade.com (see Styles and scripts in your view); a plain static server such as python3 -m http.server 8080 -d public works too, but without the CSP a view can work on your machine and break on ours. Open http://localhost:8080/ to try the example game (Nim). The kit has the play page, the test suite, the builder guide (public/starter/AGENTS.md), the example, three.js, a local match server with an MCP adapter and the mc command line tool. Your kit's version is in public/kit/version.json; node tools/mc-local/mc.mjs news (or What's new) tells you what changed. Download the kit again when a feature you need is newer than yours; your saved tokens keep working.
Build an Arena game
Open the kit folder in Claude Code, Codex or any coding agent and say:
Read public/starter/AGENTS.md and build a game called <game-id>.
Run node public/kit/test/cli.js public/games/<game-id> until every check passes.
Start small, play it, then grow it one rule at a time. Tests prove the game is correct; only playing shows if it is fun.
The game folder public/games/<id>/ (the folder name equals the id):
| File | Needed | What |
|---|---|---|
game.json | yes | the manifest (fields below) |
spec.md | yes | the rules in plain words, and what makes it fun |
core.js | yes | pure rules: setup, observe, legalActions, act, step, result, describe |
bot.js | yes | decide(observation, legal, rng, { level }): a bot with the same rules |
view.js | no | your own graphics (DOM, canvas, or three.js from our hosted copy); without it a default view is used |
img/card.png | no | 16:9 card art for the game list (PNG, WebP or JPG) |
media/… | screenshots yes | 1 to 6 screenshots and an optional video for the game page |
models/*.glb, sounds/*.ogg, *.mp3, CSS, images | no | assets (no SVG); free CC0 models and how to load them: 3D models |
3D games: free CC0 models (Kenney, Quaternius, Poly Haven, Poly Pizza), the licence rule and a loader that keeps their textures are on 3D models.
The rules in short (the checks enforce them): core.js is pure (no Math.random, dates, timers, page, network or storage; randomness comes from the kit's rng stored in the state), the state is plain JSON, the game is deterministic, each player's observation shows only what that player may see, every game ends, and describe() is plain text of at most 600 characters, facts only, with no URLs or HTML. Details and the exact interface: /starter/AGENTS.md.
game.json fields
| Field | Rule |
|---|---|
id | 2 to 32 lowercase letters, digits and -; equals the folder name. The first account whose game is approved owns it. |
name | 1 to 32 characters |
version | x.y.z; raise it for every update |
kit | "0.3" (the game interface version) |
description | up to 200 characters (the card text) |
license | required: an open licence such as MIT (recommended). A turn-based game may use a closed licence (All rights reserved, PolyForm-Strict-1.0.0, ...) if its rules run on our server (protected); realtime games need an open licence |
mode | "turn" or "realtime" (realtime also needs tickRate, and botEveryTicks with bots) |
players | { "min": 2, "max": 2 }, from 1 to 8 |
playerTypes | which of human, bot, agent may sit |
agentModes | how agents play: ["turn"] for turn games; realtime games use commander (with orders) or sliced |
bots | bot level names, for example ["easy", "normal", "hard"] |
actions | every action type and parameter with its widest range (int, number, enum, bool) |
scoring | { "kind": "winner" }, "highscore" or "fastest" |
limits | { "maxTicks": 0, "maxActions": 40 }: every game must end |
hiddenInfo | true for cards, fog of war: other players' actions are hidden in move lists; observe() must hide everything else |
author | optional { "name": "...", "url": "https://..." } (the link on the card) |
tests | optional: sims (default 200), fairness.p0Range (default 0.4 to 0.6) |
screenshots, video, about | the game page (see Screenshots, video and About) |
teams, leaderboard, rooms, watch, agentGuide, agentRoles, seatsUi, publicReplays | optional features (next section); none of them changes the game's rules hash |
Optional features for Arena games
- Teams:
"teams": [{ "name": "White", "seats": ["p0", "p2"] }, { "name": "Black", "seats": ["p1", "p3"] }]andresult()returnswinners(the whole winning side). The end screen, replays and records show the team. - Leaderboard:
"leaderboard": { "rank": "wins" }(or"score", with"lower": truefor lower is better, or"fastest"; optionallabel). Boards exist only for turn-based games whose rules run on our server, and only results the server played and signed count. You store nothing. - Play with friends:
"rooms": { "clock": 60, "agentClock": 120 }(seconds per move, 10 to 600;"rooms": trueuses 60;agentClockis for players' AI agents in a room, default 120). Only for turn-based games whose rules run on our server. A slow or gone player's move is played by your bot, and so is an agent seat until its owner accepts the agent. - Seats:
seatsUinames and draws your seats on the room page and the play page. It is declarative and reviewed; it never reaches your rules:
``json "seatsUi": { "layout": "cross", "partnersShareViews": false, "seats": [{ "name": "Red", "color": "#d33" }, { "name": "Yellow", "color": "#e0b000" }, { "name": "Blue", "color": "#36c" }, { "name": "Green", "color": "#2a2" }], "presets": [{ "name": "Me + agent vs 2 bots", "seats": ["me", "bot:easy", "agent", "bot:easy"] }], "allow": { "default": ["human", "agent", "bot"], "botLevel": "normal", "seats": { "p2": { "allow": ["bot", "human"] } } } } ``
seats: exactly players.max entries (at most 8), a name of 1 to 16 characters (plain text, no links) and an optional colour #rgb or #rrggbb (pages darken a colour with less than 3:1 contrast; the check warns). layout: row, versus, or for 4-player games table4 or cross (p0 bottom, p1 left, p2 top, p3 right). presets: up to 6 ready setups of me, coach, agent, open and bot or bot:<level>, each with players.min to players.max words; every preset must work as a solo game (open becomes a bot there) and in a room, where presets are the Quick setup buttons. allow: the kinds a seat takes (from playerTypes), and botLevel for "bot", "open" in a solo game and an agent seat's bot. With rooms, at least one seat must take a person. partnersShareViews: true (needs teams) lets one person hold several seats of one team in rooms of a hidden-information game, so partners see each other's hands: only if that fits your game; the reviewer checks it. Team names stay in teams.
- Watching:
"watch": { "view": "public" }(the default),{ "view": "all", "delay": 2 }(every hand, up to 10 moves late), orfalse. For hidden-information games exportobservePublic(state)fromcore.js: what everyone at the table sees. Preview withplay.html?game=<id>&spectate=public. - Hints for players' agents:
"agentGuide": "...", plain text up to 3000 characters: rules and strategy, never instructions about the agent's tools. It goes into the prompt players copy, marked as game text. It is public and reviewed. - Agent roles:
"agentRoles": ["play", "opponent"]to offer only some ofplay,opponent,coach(see roles). - Recorded games: every game played on our server is recorded for 180 days and listed on your game's page (players nameless unless they chose otherwise). A hidden-information game is listed only with
"publicReplays": true, because its replay may show every hand at the end. Nothing else to do.
Styles and scripts in your view (our CSP)
An Arena or world game's view.js runs inside our pages (play, room, watch and world), which send this Content-Security-Policy:
default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self' blob: data:; worker-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
What it means in the browser:
| Blocked by the CSP (styles and handlers are dropped silently) | Use instead |
|---|---|
el.setAttribute("style", ...), style="..." in HTML text | el.style.left = "3px", el.style.cssText = "...", el.style.setProperty(...), Object.assign(el.style, {...}) |
<style> elements, <link> to a data: or blob: stylesheet | a view.css next to view.js, loaded with a <link rel="stylesheet"> whose href is new URL("view.css", import.meta.url).href; or new CSSStyleSheet() with replaceSync and document.adoptedStyleSheets |
onclick= and other event attributes, javascript: links | el.addEventListener("click", fn) or el.onclick = fn |
eval, new Function, setTimeout("text"), WebAssembly, blob: workers | plain functions in your files |
The kit's csp check FAILs on the patterns that are sure to break (setAttribute("style"), setAttribute("on..."), createElement("style") or h("style", ...) when h is your own helper around document.createElement(tag) (in the view or a file it imports; other calls with "style" only warn), javascript: URLs, string timers, WebAssembly) and warns on HTML text holding style=, on...= or <style>. The kit's servers (node tools/mc-local/serve.mjs, protected.mjs, mc world) send the same CSP, so what breaks here breaks on your machine first; the browser console says Refused to apply inline style. --no-csp turns it off for debugging.
Arcade games run on their own site with a looser policy (next sections).
Test an Arena game
Run from the kit folder:
node public/kit/test/cli.js public/games/<id> --write-golden # first time: creates golden.json
node public/kit/test/cli.js public/games/<id> # every check must PASS
node public/kit/test/cli.js public/games/<id> --register # lists it on your local copy of the site
Then play it at http://localhost:8080/play.html?game=<id>&seats=human,bot:normal (served by node tools/mc-local/serve.mjs), and let an agent play it with seats=agent,bot:normal.
- The checks cover the manifest, files, 200 bot games without errors, JSON state, determinism, fairness,
describe()text (at most 600 characters, no URLs or HTML), speed, purity, the view scan, a security scan, junk moves (fuzz), the golden replay and more. The full table is in /starter/AGENTS.md. - Our server runs only the file checks (
--static) on a submission, and only their FAIL blocks it. The reviewer runs the full checks; WARN lines are for the reviewer to judge. - Changed the rules after the golden exists? Raise
versionand run--write-golden --force. - Rules on the server: the
separationcheck (Node 20.6 or newer; it is skipped on older Node) plays your game as our game server would.node tools/mc-local/protected.mjsserves the real play page with your rules in a worker the page can't reach: if the page asks for a rule file, the console printsPAGE ASKED FOR RULE FILE. Keep helpers your view needs in their own file (a view that importscore.jsgets arules-publicwarning). - The kit's tests run your game's code with your own rights: only keep games you trust in your kit.
Build an Arcade game
Put your game and an arcade.json in a folder. The main fields:
| Field | Rule |
|---|---|
kind | "arcade" |
id | 3 to 40 lowercase letters, digits and -; becomes arcade-<id>.robokade.com. Reserved: www, api, admin, preview, test, static, cdn, assets, metachromia, robokade, play, arcade, id, dev |
name, version | the name; x.y.z, raised for every update |
entry | the .html file that starts the game; use relative paths everywhere |
card | a 16:9 picture for the game list |
description, controls | the card text (up to 400 characters); how to play |
contentNotes | needed for anything violent or scary; harming civilians must be off by default |
desktopOnly | true if it needs keyboard and mouse |
license, author, thirdParty, assets | your licence; your name and link; every library with version and licence; a file listing where models, sounds and textures come from |
screenshots, video, about | the game page (below) |
agentGuide | optional hints for players' agents (up to 3000 characters) |
online | optional: play with friends (below) |
How it runs: on its own address, only inside our frame, under a bar that says it is made by you, not Robokade. Allowed: inline scripts, WebAssembly, workers, your own files, saving in the browser, WebGL, sound after the first click, pointer lock, gamepads. Blocked: any request to another address, popups, forms, cookies, camera, microphone and location, the browser's own fullscreen (our page has Full window). Allowed file types, limits and the full rules: /arcade.md.
The Arcade game's own site (arcade-<id>.robokade.com) sends this Content-Security-Policy:
default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob:; worker-src 'self' blob:; connect-src 'self' data: blob:; img-src 'self' data: blob:; media-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors <our Arcade page>; sandbox allow-scripts allow-same-origin allow-pointer-lock
(frame-ancestors names our Arcade page's hosts; the exact string is ARCADE_GAME_CSP in the kit's public/kit/test/csp.js.)
So inline <style>, style= attributes, inline scripts and event attributes work there, and so do WebAssembly and blob: workers. Still blocked: eval, new Function and setTimeout("text") (no 'unsafe-eval'), stylesheets from data: or blob: URLs, anything from another address. Load CSS as a file of your game (<link rel="stylesheet" href="style.css">) or inline. mc online-test and mc data serve your game with this policy (frame-ancestors naming the local test page).
Agent seats (optional): declare up to 4 seats with mc.agentSeats([...]) from our SDK (/__mc/v1/sdk.js): a description, actions with numbers, true/false or a short list of choices (never free text), and observations you send. Choose per seat pace (realtime or turns), coaching and approve. Design for one agent decision every 5 to 20 seconds. Details: /arcade.md.
Online play with friends (Arcade)
Opt in with online in arcade.json:
"online": { "min": 2, "max": 4, "pace": "turns", "words": ["red", "blue", "pass", "pick"] }
| Field | Rule |
|---|---|
min, max | players in a room, 2 to 8 (the host is one of them) |
pace | "turns" (about 1 to 1.5 s per move) or "live" (slow real-time, about 0.5 to 1 s delay) |
words | up to 200 short words (a-z 0-9 _ -): the only strings your messages may hold when a player aged 13 to 17 is in the room |
The host's page runs your game; friends' pages are guests. In the SDK: mc.onlineSeats(...) tells your game its role (solo, host or guest), the host sends mc.state(...) (to everyone or one seat) and mc.send(...), guests draw from mc.onState(...) and send their inputs, and mc.online.end() ends the match. Your game still has no network access: every message goes through our page and server, which check it.
- Review gate (refused if broken): no player-typed text and no text one player can make another player's game show, in any online message, not even hidden in numbers. Room chat is ours, in our panel.
- A fresh state must be enough to draw the game. Check every guest input on the host. Hidden information only in per-seat states.
- Short polls only: good for turn-based, cards, board, party and slow real-time games, not for racing, shooters or fighting games.
- Rooms are private (invite link) or, for adult hosts, public: adults the host doesn't know can join from the Live page, so treat every guest as a stranger. Casual only: no results or rankings.
- Test locally:
node tools/mc-local/mc.mjs online-test path/to/your-gameopens your game as host and guests side by side, each from its own local address, with the same limits, a 13-17 switch, a delay setting, and Reload and Leaves buttons.
Exact limits for messages and states: /arcade.md.
Screenshots, video and About
Every game has a page at /game/<id> that players see before Play. Add to game.json or arcade.json:
"screenshots": ["media/1.webp", { "path": "media/2.webp", "alt": "Late game, red is winning" }],
"video": "media/trailer.mp4",
"about": "What the game is, what makes it fun, how a match goes."
- Screenshots: 1 to 6, needed for new games. 16:9 or 9:16, at least 1280×720, at most 3840 px, up to 2 MB each. Real pictures of this game: misleading ones are refused. The kit's play page has Save screenshot (1280×720 of the game canvas; WebGL views need
preserveDrawingBuffer: true). - Video (optional): one
.mp4, H.264, up to 30 seconds and 8 MB, saved with "fast start" (ffmpeg -i in.mov -c:v libx264 -crf 28 -movflags +faststart -an media/trailer.mp4). It plays muted until the player turns the sound on. - About (optional): plain text up to 3000 characters, no links, no HTML; a blank line starts a new paragraph.
- Media have their own budget (up to 14 MB) beside the game's size limit.
mc submituploads them for you.
Translations (optional)
A Traditional Chinese (Taiwan) version of Robokade's own pages is coming. Your game's text is yours: it shows in English unless you add your own translation. Add to game.json or arcade.json:
"i18n": { "zh-TW": { "name": "四子棋", "description": "…", "about": "…", "seats": ["紅", "藍"], "presets": ["…"] } }
- Every field is optional and falls back to your English. Arcade games may also translate
controlsandcontentNotes. - Same limits as the English fields;
seatsandpresetslist one name perseatsUiseat or preset, in the same order. Plain text only: no links, no rude words (the check reads Chinese too), no invisible characters. Onlyzh-TWfor now. - Your view learns the reader's language: an Arena
view.jsgetslang("en" or "zh-TW") increateView(el, { send, lang }); an Arcade game readsmc.langandmc.onLang((lang) => …)from the SDK ("en" until our page tells it). - A translation is part of the game: a change needs a new version. The reviewer checks that it matches the game.
Done?
- Every check passes, the golden is written, you have played it, and an agent can play it.
spec.mdexplains the rules and what makes it fun.- 1 to 6 screenshots and an About text.
Next: Submit and review.