# 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). ## 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 `INSERT`s, run once when the world is created | no | | `migrations/.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 ```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 ```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. - `pid` is 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's `rng` helpers), `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_each` and `json_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.sql` expressions (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 no `LIKE`/`GLOB`. - Never: `PRAGMA`, `ATTACH`, `VACUUM`, creating or dropping anything, recursive `WITH`, 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 their `pid` is 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_players` and 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/.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/ node tools/mc-local/mc.mjs world public/games/ node tools/mc-local/mc.mjs db query "SELECT * FROM ship" ``` - `cli.js` checks 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 world` serves your world with our server's rules and caps: open the address it prints, switch between local players, press **Tick now**. - `mc db query` reads your local world (writes are refused, as for your game's reads). Submitting works as for other games: [Submit and review](/docs/publish.md). Want saves or stats in a match-based Arena or Arcade game instead of one shared world? See [Game data](/docs/data.md).