Game Bots
Game Bots
Opponents that hold a seat - scripted and AI bots, models as data, hardware tiers, host migration, hidden information and what a trained model is worth.
On this page10 sections
A game bot is an opponent that holds a seat. The room's owner adds it in the lobby, it counts toward the player limit, it takes its turns like anybody else, and every player sees it in the seat list. What it does on its turn is code in your game's scene script.
(Not to be confused with the 3-D "player bots" people can wear as their presence at the table — those are cosmetic, and nothing on this page is about them.)
A game offers bots by declaring them in its manifest (bots), and plays
them by registering a handler for each in a scene script
(bots.register). Only a game pack can declare bots or
models.
Because a script plays every bot, a bot can only sit down in a variant whose
attached scripts register its handler. Publishing records, per variant, which handlers its scripts register; the
lobby and the table refuse a bot the chosen variant does not play (bot-variant-no-handler), and switching a
lobby to such a variant removes the bots already seated. A variant played by hand, which attaches no scripts,
seats no bots.
Bots also have a Score of their own in ranked games, one per bot and difficulty, starting at 1500 like everyone else.
Scripted bots and AI bots#
There are two kinds, and they share one contract:
| Scripted | AI | |
|---|---|---|
| What plays the turn | Your script | Your script, which also evaluates a model |
Needs an aiModels entry |
No | Yes — modelId, and optionally one per difficulty |
| Hardware requirement on the host | None | The minTier of the model its difficulty plays with |
| Listing tag it earns | bots-scripted |
bots-ai |
A scripted bot is not a weak bot. A scripted bot can search: write your rules as a
rules kernel with a heuristic, and call
ai.mcts without a model. Measured on Reversi, that model-less search
beats a greedy player 90% of the time at 64 simulations and plays level with a depth-4 minimax at 256 — while
costing about 1–5 ms a move on a desktop and running on any device. Start there.
An AI bot adds a trained model: an ONNX network that gives the same search a prior over moves and a value for each position. It is worth it only when the model plays better than the heuristic at the same number of simulations — see What to expect from a model.
A model is data, never code#
A model is an .onnx file in your repository, declared in aiModels
with its exact size and SHA-256. The publish scanner opens it at the pinned commit and refuses anything that is not
a plain feed-forward graph: a fixed list of standard operators, opset 11–21, no loops, branches or sub-graphs, no
custom operator domains, no functions, no external data files, no string tensors, and caps on its node count,
parameters, tensor sizes and metadata. (The full rules.)
An .onnx in your tree that no entry declares is refused too.
What that buys every player: a model cannot run code, cannot reach the network, and cannot loop forever. The host runs it in background workers, with a 2-second budget per call, and hands a script numbers back. Engines shipped as code (a chess engine compiled to WebAssembly, say) are not accepted.
Size limits: 64 MiB per model, 128 MiB for all of a game's models, at most 4 models. The in-app Publish sends at most 45 MiB per file; a larger model has to be pushed to the repository with git before publishing.
Everything runs on the host#
Table scripts run on the host only, so a bot's turn is played there: its script reads the table, decides, and makes ordinary script calls, which reach the other players as table changes. A model is evaluated on the host's own device and its numbers never leave it — nothing a model returns is replicated or saved. Other players do not download or run your model; the host does.
That has a cost: a bot thinks with the host's CPU. One bot thinks at a time, and a long search on a slow host is time everyone waits.
A model that crashes stays off. If a model takes down the worker running it — or the host's whole tab dies while the model is on its first call — the host switches that model off for the rest of the game and its bot stays paused. Pressing Resume does not bring it back: the Bots panel shows "This bot's AI model crashed while it was running, so it was switched off for the rest of this game to keep the table safe. Trying it again may crash this tab." with a Retry anyway button, and only that button tries the model again. In that host's browser, a model whose tab died mid-call is also switched off the next time a table loads it.
The table ends a bot's turn when its handler returns. A handler that throws, or runs past the turn's time budget
(30 seconds unless the game sets another), has its turn skipped with a notice; three failures in a row pause the
bot until the host resumes it. A table running no script at all pauses its bots, since nothing could play them.
An offline solo table has no turn order, so a bot there only moves if your own script drives it. And onTurn is
for games that use the table's turns: a game with its own order of play (poker betting, simultaneous moves, an
auction) decides for its bots wherever it decides for people, and registers each handler with an onTurn that
returns at once (a manifest bot must still be registered).
Each turn your handler is told which bot it plays (ctx.botId, ctx.peerId, ctx.seat), the model an AI bot
plays with at its difficulty (ctx.modelId), and the difficulty it plays at with the dials your manifest declares
for it (ctx.difficulty: simsPerMove, temperature and your own params from that model's difficulties). Read
them there instead of copying the numbers into your script.
One model per difficulty. An AI bot's modelId is its default model. When your Easy, Medium and Hard are
different networks, give each difficulty its own
modelId instead of choosing the model in your script.
The table then loads the right file ahead of time, checks the host's device against the model that difficulty
actually uses (Easy can run on a weaker device than Hard), hands it to your handler as ctx.modelId, and ranks
each difficulty on its own file — retraining only the Easy network starts Easy on a fresh score.
Hardware tiers#
Every device that might host an AI bot measures itself with a short hardware check — a fixed probe network, run once on the processor and, where WebGPU is available, once on the graphics card. The result is one of four tiers:
| Tier | What the device has | What it can host (one worker, per position) |
|---|---|---|
none |
No WebAssembly SIMD, fewer than 2 cores, a failed check — or not measured | No model. Scripted bots, and AI bots' model-less fallback, still work. |
cpu-basic |
SIMD and at least 2 cores | A Reversi-class model (≤ ~2 MiB) at 12 ms or more — playable with modest simulations |
cpu-fast |
4+ cores and the probe in ≤ 2.5 ms on one thread | ≤ ~2 MiB: ≤ 12 ms; ~2–12 MiB: up to ~44 ms — use fewer simulations |
gpu |
A WebGPU adapter that ran the probe with matching answers | 12–64 MiB models, batched: about 7 ms per batch call |
A model declares the weakest tier that should host it, as minTier:
up to about 2 MiB declare cpu-basic, about 2–12 MiB cpu-fast, above about 12 MiB gpu. The editor's import
measures your device and suggests one.
Measured on one fast desktop, single WebAssembly thread, per position: a tiny network (0.4 MB) 0.3 ms, small
(1.9 MB, Reversi-class) 1.4 ms, medium (7.2 MB) 5.1 ms, a 47 MB tower 32–42 ms. A graphics card costs a fixed
6–7 ms per call whatever the model's size, so it loses to the processor for one position at a time and wins only
when the search batches many positions per call. The cpu-fast threshold sits about five times slower than that
desktop, so most mid-range phones and older laptops land on cpu-fast and low-end devices on cpu-basic; the
bands will be revised as more devices are measured.
Tiers gate hosting, not joining. Anyone can join a table with an AI bot. The lobby refuses to Start one
whose owner's device is below a seated AI bot's model (bot-tier-unmet), and offers to re-test the device. A tier
is self-reported and advisory: it decides who may host and whom the server prefers as the next host, and grants
nothing — a device that claims more than it has gets a bot that cannot think on its own machine.
Host migration#
When the host leaves, another player becomes the host and the table carries on, bots included — the bots are part of the table's saved state. The server prefers a new host whose device meets the table's AI bots. When none does:
- The AI bots the new host cannot run are paused before any script asks for a move.
- The new host is asked "You are now the host", with three answers:
- Keep it paused (the default, and what Escape chooses) — its turns are skipped and a person can take the seat.
- Swap it for the scripted version in the same seat — offered only when the bot declares a
fallbackBotId. - End the table — with other players still present this makes the new host leave, and the table passes to the next player. Alone, it closes the table (a table with a named save stays in its owner's tables).
If that host leaves with the question still open, the next one is asked again. Declare a scripted fallback for every AI bot: it is the difference between "the game goes on" and "the bot sits there".
A script must never rely on two hosts making the same choice: a model's numbers differ slightly between devices, and the search is seeded per call.
Hidden information#
A bot's handler runs on the host, and the host's table is unredacted. A table script can read every card face,
every hand and every pile's order — a bot's code included. Table scripts have no capability for reading the table
(their only capability, read-cards, reads the public card catalogue), so nothing in the platform stops a bot from
peeking.
What players are told: because that is true of every bot, the lobby and the table's Bots panel say "Bots run on
the host and can see the whole table, including hidden cards." whenever your game declares bots AND its table
has anything that can be hidden — a card, a deck, a card holder, a hand zone or a hidden zone — or your manifest
declares read-hidden-information. The server works this out from your setup.json when the game is registered;
you do not declare anything for it. A game with nothing hideable (chess, Othello) shows no line.
So be honest:
- If your bot reads anything a player in its seat could not see, also declare
read-hidden-information, so a reviewer and the capability warning say so too. - If it should play fair, make it fair in code: build the state you search from only what that seat may see (its own hand, face-up cards, public counts), and never read the rest.
What "headless" means#
A rules kernel is pure game logic: given a position and a move, it says what position follows, with no table, no physics and no randomness. That is what lets a search imagine thousands of games in a few milliseconds, and what lets a model be trained without a table at all.
So a game whose outcomes come from physics — rolled dice, flicked discs, a tower that might fall — has no kernel,
and its bots are scripted by hand: they act, wait for the table to settle, and read the result. Games with a random
element (a shuffled draw) need care too, because a kernel's apply must give the same result every time.
The training tools find a kernel by name: declare it as const kernel in the outermost scope of a scene script,
with no export keyword, and keep it and everything it uses pure — no world, globalEvents, refObject, bots,
ai, declareVariables, no Math.random, no clock. See
RulesKernel.
What to expect from a model#
The measured Reversi ladder (200 games per row, built-in bots, one desktop thread):
| Player | vs greedy | vs minimax depth 2 | vs minimax depth 4 | vs minimax depth 6 |
|---|---|---|---|---|
| Heuristic search, 64 simulations | 90.5% | 56.5% | — | — |
| Heuristic search, 256 simulations | 97.5% | 96.0% | 52.0% | 17.5% |
| Minimax, depth 4 | 97.0% | 89.5% | — | — |
The model-less search is already strong. A model earns its place only if it beats the heuristic search at the same number of simulations — measure that, not "it beats greedy". Training a model that does is real work: a published Reversi-zero run took 8 hours and 72,000 games on a datacentre GPU. A 6-minute browser run on the smallest network beat random 70% of the time and roughly matched greedy, but won only 18% against the heuristic search. No overnight browser run has been measured. Training a bot model has the tools and the numbers.
Players, listings and rewards#
- Lobby. The room's owner adds bots from the game's list (each takes the next open seat; a difficulty can be picked), and they count toward the player limit. An AI bot's row shows the hardware it needs and its model's download size.
- A bot's deck. When your game ships a
gameDecksfile and a deck requirement lists the"game"source, the owner can Choose deck on a bot's row in an unranked lobby and hand it one of those decks — a bot has no library, so the game's own decks are the only ones it can be given. Ranked rooms refuse it and clear every bot's deck when ranked is switched on: in ranked play your script always chooses. A deck that would break the requirement'sdistinctBypairing is refused too. The platform does not deal the bot that deck — your scene script reads the choice asPlayerInfo.deckId(a mod script asModBotInfo.deckId), finds it in the decks file withworld.readData, and builds it.nullmeans nobody chose: pick one yourself, for example by the bot's difficulty and the deck'slevel. - At the table. Settings ▸ Bots lists the bots for every player — where they sit, how strong they play, which one is thinking, and why one is paused. The host can add, re-seat, change difficulty, resume and remove them.
- Catalogue. Declaring bots earns your listing the tags
bots, plusbots-aiand/orbots-scripted; they are derived from the manifest, so typing them intotagsyourself does nothing. The game's page gets a Bots section. - Chips and stats. Play earns at the full rate while at least two people are active at the table, at a quarter of the rate for one active person with an active bot seated, and nothing for a table nobody is playing at (a person counts as active within five minutes of their last action). A paused bot does not count.
See also#
- Bots and AI — the
botsandaiscript globals. manifest.botsandmanifest.aiModels.- Manifest ▸ Bots — declaring bots and importing a model in the editor.
- Training a bot model — browser and command-line training.
api.ai.evaluate— a mod script's access to the game's models.
