World games
A world game is one shared, persistent world for everyone who plays it: players enter and leave any time, and what they do stays. No matches, rooms or seats. The world's data lives in one SQLite database on our server; your game's code reads and writes it with SQL. Small example: Lantern Square, in the kit as public/games/lantern-square (try it with mc world, below).
For AI agents: this page as plain text at /docs/worlds.md; all docs are listed in /llms.txt.
When to build one
- Good fits: a shared town, a galaxy to trade in, a garden that grows while you are away, a guild board, a daily-visit game.
- Not for: real-time action (ticks are at least 5 minutes apart), chat (there is none; messages are preset lines), real money or trading of anything with real value (never), casino-style games or paid random rewards (never).
The files
| File | What | Public? |
|---|---|---|
game.json | "mode": "world" and the world field (below) | yes |
world.js | your world's code: join, observe, legal, act and optionally tick, check, forget, upgrade | no: runs on our server only |
schema.sql | the database's tables and indexes (CREATE TABLE ... STRICT, CREATE INDEX) | yes |
seed.sql | optional INSERTs, run once when the world is created | no |
migrations/<fromVersion>.sql | how a world of an older version becomes this version's schema (ALTER TABLE ... ADD COLUMN, CREATE TABLE/INDEX, DROP INDEX) | no |
view.js | optional: draws the world in the player's browser | yes |
spec.md | the rules in words (players and their agents read it) | yes |
World games always run on our server, so view.js may not import world.js.
game.json
"mode": "world",
"players": { "min": 1, "max": 10000 },
"playerTypes": ["human", "agent"],
"world": { "db": true, "tickMinutes": 60, "textFields": ["shipName"], "phrases": ["Hello!", "Good luck!"] }
tickMinutes: 5 to 1440, or leave it out (no ticks).textFields: action parameters that may carry free text (names of ships, crews, lanterns). They pass the same rule as display names: at most 24 plain letters, digits, spaces and. _ -, no links, rude or reserved words, for every player.phrases: up to 30 preset lines players can send or post (notes, greetings, bounties). Free-text messages are not allowed.agentPlayers: 0 for now (a player's own agent plays their character).- Not for world games:
rooms,watch,leaderboard,tableChat,teams,seatsUi.
world.js
export function join(db, pid, ctx) { db.run("INSERT INTO ship (pid, gold) VALUES (?, 10)", pid); }
export function observe(db, pid, ctx) { return { view: db.get("SELECT * FROM ship WHERE pid = ?", pid), text: "Your ship is in port." }; }
export function legal(db, pid, ctx) { return { sail: { to: { type: "enum", values: ["north", "south"] } }, name: { shipName: { type: "text" } } }; }
export function act(db, pid, action, ctx) { /* ... */ return { ok: true, msg: "Sailed north." }; }
export function tick(db, ctx) { /* every tickMinutes: markets move, crops grow */ }
export function check(db) { return ""; } // "" or a problem: a problem cancels the change
export function forget(db, pid) { db.run("DELETE FROM ship WHERE pid = ?", pid); }
db.get(sql, ...params)(one row or null),db.all(sql, ...params)(rows),db.run(sql, ...params)({ changes }). Synchronous. Always pass values as?parameters.pidis the player's id in your world: the same each visit, different in every game, and it never tells you who the player is. Store it as it is.ctx:now(the minute),since(the last tick),rng(a seeded random generator for the kit'srnghelpers),agent(true when a player's agent acts).mc.textOk(s): true when text your game builds itself passes the display-name rule.- One transaction per call. Every call runs in its own transaction;
check()runs after every write, inside it. If anything goes wrong (an error, a cap,check()says no), nothing of that call is kept. - No memory between calls. Keep everything in the database.
- The usual rules for game code apply: no clock, no randomness except
ctx.rng, no network.
What SQL may do
- Read and write your own tables; read the platform table
_mc_players(pid,kind,joinedAt,lastSeen);json_eachandjson_tree. - Functions:
abs coalesce ifnull iif nullif length lower upper trim ltrim rtrim substr instr replace round min max sum total count avg typeof printf format like glob json json_extract json_object json_array json_array_length json_valid json_group_array json_group_object row_number rank. - In
schema.sqlexpressions (DEFAULT, CHECK, generated columns, indexes) a shorter list:abs coalesce ifnull iif nullif length lower upper trim ltrim rtrim substr round min max typeof json_valid json_extract json_array_length, and noLIKE/GLOB. - Never:
PRAGMA,ATTACH,VACUUM, creating or dropping anything, recursiveWITH, triggers, views,WITHOUT ROWID,AUTOINCREMENT. - Every table is
STRICT. Values up to 256 KB; integers within ±2^53; no blobs in results.
Caps per call
| act, join, forget | tick | observe, legal, check | |
|---|---|---|---|
| CPU | 100 ms (world.actCpu up to 250) | 2 s | 100 ms |
| One statement / whole call | 250 ms / 1 s | 2 s / 10 s | 250 ms / 1 s |
| Statements | 200 | 5000 | 200 |
| Rows per statement | 1000 | 10 000 | 1000 |
| Rows changed | 500 | 50 000 | none |
| Data returned | 1 MB | 8 MB | 1 MB |
Database size: 100 MB to start. A tick catches up from ctx.since itself: after a long quiet time there is one tick, never many in a row.
Players' data
- Leave and delete my data, and account deletion, call your
forget(db, pid), then Robokade checks every table again: cells equal to the player's names go blank, and every copy of theirpidis replaced by an anonymous id. So keep pids as they are (never encoded or hashed) and don't copy players' names into other places. - You can download a copy of your world: account page → Games → Download world data (3 a day; not while a player's data is being deleted). It holds your tables,
_mc_playersand the action log_mc_log(with the names players typed), under pids only, never accounts: don't try to find out who players are, don't share it, and delete old copies (a fresh download shows who left).
New versions
A new version is stored next to the live one. After review we test it on a copy of the live world (your migration, your upgrade(db, fromVersion, ctx) in batches: return { more: true } to be called again, then check()), then switch, with a backup taken just before. We can roll back for 7 days. schema.sql always describes the full current schema; migrations/<fromVersion>.sql takes a world of that version to it.
Test it in the kit
You need Node.js 22.5 or newer for world games (the kit uses Node's built-in SQLite with our rules).
node public/kit/test/cli.js public/games/<id>
node tools/mc-local/mc.mjs world public/games/<id>
node tools/mc-local/mc.mjs db query "SELECT * FROM ship"
cli.jschecks the files, then plays: three players join, 20 random legal actions, 3 ticks,check()after every write, one player forgets and the check finds them nowhere.mc worldserves your world with our server's rules and caps: open the address it prints, switch between local players, press Tick now.mc db queryreads your local world (writes are refused, as for your game's reads).
Submitting works as for other games: Submit and review.
Want saves or stats in a match-based Arena or Arcade game instead of one shared world? See Game data.