Robokade

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 gameArcade game
Made withthe Robokade kit: a pure rules file (core.js), a bot (bot.js), an optional viewany browser code: three.js, WebAssembly, workers, your own libraries, saving
Manifestgame.jsonarcade.json ("kind": "arcade")
Playerspeople, bots and AI agents by the same rulespeople; AI agents only in seats you declare
Replays, leaderboards, rules on our serveryes (leaderboards and rooms: opt in)no
Play with friendsrooms for protected turn-based games (rooms)host-runs-the-game rooms (online), casual
Size8 MB, 60 files50 MB, 500 files, 20 MB per file
Licenceopen (MIT recommended); closed is fine for turn-based games protected on our serveryour 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):

FileNeededWhat
game.jsonyesthe manifest (fields below)
spec.mdyesthe rules in plain words, and what makes it fun
core.jsyespure rules: setup, observe, legalActions, act, step, result, describe
bot.jsyesdecide(observation, legal, rng, { level }): a bot with the same rules
view.jsnoyour own graphics (DOM, canvas, or three.js from our hosted copy); without it a default view is used
img/card.pngno16:9 card art for the game list (PNG, WebP or JPG)
media/…screenshots yes1 to 6 screenshots and an optional video for the game page
models/*.glb, sounds/*.ogg, *.mp3, CSS, imagesnoassets (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

FieldRule
id2 to 32 lowercase letters, digits and -; equals the folder name. The first account whose game is approved owns it.
name1 to 32 characters
versionx.y.z; raise it for every update
kit"0.3" (the game interface version)
descriptionup to 200 characters (the card text)
licenserequired: 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
playerTypeswhich of human, bot, agent may sit
agentModeshow agents play: ["turn"] for turn games; realtime games use commander (with orders) or sliced
botsbot level names, for example ["easy", "normal", "hard"]
actionsevery 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
hiddenInfotrue for cards, fog of war: other players' actions are hidden in move lists; observe() must hide everything else
authoroptional { "name": "...", "url": "https://..." } (the link on the card)
testsoptional: sims (default 200), fairness.p0Range (default 0.4 to 0.6)
screenshots, video, aboutthe game page (see Screenshots, video and About)
teams, leaderboard, rooms, watch, agentGuide, agentRoles, seatsUi, publicReplaysoptional 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"] }] and result() returns winners (the whole winning side). The end screen, replays and records show the team.
  • Leaderboard: "leaderboard": { "rank": "wins" } (or "score", with "lower": true for lower is better, or "fastest"; optional label). 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": true uses 60; agentClock is 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: seatsUi names 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), or false. For hidden-information games export observePublic(state) from core.js: what everyone at the table sees. Preview with play.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 of play, 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 textel.style.left = "3px", el.style.cssText = "...", el.style.setProperty(...), Object.assign(el.style, {...})
<style> elements, <link> to a data: or blob: stylesheeta 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: linksel.addEventListener("click", fn) or el.onclick = fn
eval, new Function, setTimeout("text"), WebAssembly, blob: workersplain 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 version and run --write-golden --force.
  • Rules on the server: the separation check (Node 20.6 or newer; it is skipped on older Node) plays your game as our game server would. node tools/mc-local/protected.mjs serves 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 prints PAGE ASKED FOR RULE FILE. Keep helpers your view needs in their own file (a view that imports core.js gets a rules-public warning).
  • 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:

FieldRule
kind"arcade"
id3 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, versionthe name; x.y.z, raised for every update
entrythe .html file that starts the game; use relative paths everywhere
carda 16:9 picture for the game list
description, controlsthe card text (up to 400 characters); how to play
contentNotesneeded for anything violent or scary; harming civilians must be off by default
desktopOnlytrue if it needs keyboard and mouse
license, author, thirdParty, assetsyour licence; your name and link; every library with version and licence; a file listing where models, sounds and textures come from
screenshots, video, aboutthe game page (below)
agentGuideoptional hints for players' agents (up to 3000 characters)
onlineoptional: 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"] }
FieldRule
min, maxplayers 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)
wordsup 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-game opens 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 submit uploads 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 controls and contentNotes.
  • Same limits as the English fields; seats and presets list one name per seatsUi seat or preset, in the same order. Plain text only: no links, no rude words (the check reads Chinese too), no invisible characters. Only zh-TW for now.
  • Your view learns the reader's language: an Arena view.js gets lang ("en" or "zh-TW") in createView(el, { send, lang }); an Arcade game reads mc.lang and mc.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.md explains the rules and what makes it fun.
  • 1 to 6 screenshots and an About text.

Next: Submit and review.