A bot is a player whose moves come from code instead of a human. The bot layer exists so the rest of openturn doesn’t have to know that. The engine sees a dispatch from seatDocumentation Index
Fetch the complete documentation index at: https://openturn.io/docs/llms.txt
Use this file to discover all available pages before exploring further.
"1"; it doesn’t care whether a person clicked a square or a function returned an action. That separation is the whole design.
The contract
A bot is a function over views:decide receives what the seat is allowed to know — the same getPlayerView that a human client would render — plus pre-enumerated legal moves and a deterministic RNG. It returns a LegalAction. That is the entire bot.
The same shape supports three very different bots without changing:
- Random — pick uniformly from
legalActions. - Heuristic — score each
LegalAction, return the highest. - Search-based (MCTS / minimax) — call
simulate(action)to dry-run candidates, recurse on the resulting snapshot, return the best root move.
decide body changes shape across these three, you are doing more work than necessary.
Why bots are not plugins
Openturn already has@openturn/plugins. Plugins are in-engine state mutators: they declare a slice on G.plugins, expose moves, and execute synchronously inside the reducer. They are the right tool for chat, scoreboards, deck ownership, and any cross-cutting state.
Bots are different. A bot runs outside the reducer, after a snapshot exists, and produces an event to feed back in. It is closer to a player client than to a piece of game logic. Conflating the two would push turn ownership into the plugin layer and break determinism — decide may take seconds (a deep search), and a synchronous reducer cannot await that.
The bot package therefore sits next to plugins, not inside them. The only thing the engine knows about bots is one optional field on GameDefinition: legalActions?: (context, playerID) => readonly LegalAction[]. The engine never reads it. The bot runtime does.
Plug-and-play means three things
- No game changes are required. A game without
legalActionsstill works — the bot supplies its own enumerator. Authors who want to ship bots add the hook once per game (~5 lines for tic-tac-toe) and every bot benefits. - No engine changes are required. The runner consumes only the public session API:
applyEvent,getState,getPlayerView. Bots compose with replays, the inspector, hosted multiplayer, and React bindings without any of those packages knowing the bot exists. - Same bot, two transports. A
LocalSessionHostadapts aLocalGameSession; aHostedClientHostadapts aHostedClient. Both implement the sameBotHostinterface, soattachLocalBotandattachHostedBotdiffer only in the host they construct. The bot itself is unchanged.
The runner
attachLocalBot does three things:
- Wraps the session in a notification bus so dispatches from any seat — bot or human — fan out to every attached bot’s host.
- Creates a
BotHostover that bus pinned to one seat. - Subscribes the runner to the host. On every change, if the seat is now active and no decision is in flight, the runner fires
decidein a microtask.
decide is awaiting and a new snapshot arrives — typically because the opponent reconnected and a re-sync arrived — the runner aborts signal and queues a fresh decision once the in-flight one settles. After decide resolves, the runner re-checks position.turn against the snapshot it captured at the start; a stale decision is dropped before dispatch.
Invalid moves do not mutate state, so dispatch failure is recoverable: onError fires and the runner waits for the next change to retry. The simulate helper is just a clone-and-applyEvent that returns the resulting snapshot — search algorithms can run thousands of rollouts inside a single decide call without disturbing the live session.
Determinism
Bots inherit the engine’s determinism by default:context.rngis forked fromsnapshot.meta.rngplus a salt that includes the bot name, the seat, and the turn number. Two bots on the same snapshot get different streams; the same bot on the same snapshot always gets the same stream.simulaterehydrates a session from the snapshot, which resumes RNG frommeta.rng. MCTS rollouts that fork from a fixed seed produce the same trees every time.
decide is a pure function of (view, legalActions, rng) makes a match fully reproducible: rerun with the same seed and the same bots and you get bit-identical replays. Bots that consult external services or non-seeded randomness break this guarantee.
Cloud deployment
There are three places a cloud bot could live:| Where | Latency | Isolation | Recommended |
|---|---|---|---|
| Inside the room worker | Lowest | None — third-party code in a trusted process | No |
| External WebSocket client (sidecar) | One network hop | Full process boundary | Yes |
| Inside a human’s browser tab | Free CPU | Tied to a tab staying open | No |
HostedClient protocol a human browser uses. Failure modes reduce to “the bot disconnected” — the same recovery path the room already has for humans. Slow bot decisions don’t stall the room’s event loop. Bots scale independently of room workers.
A BotSupervisor service watches lobbies for matches with a botSeats config, spawns a HostedClient per (room, seat), and runs attachHostedBot against the room URL. The room worker is unaffected; only the lobby data model gains a botSeats field.
@openturn/lobby/supervisor ships both halves of this:
createLocalBotSupervisor— in-process, for single-device React apps and CLIs. WrapsattachLocalBotsand exposes agetSession()accessor for the bot-aware facade.createHostedBotSupervisor— opens aHostedClientper bot seat. The host (cloud Durable Object or OSS dev server) mints per-bot-seat tokens and feeds them in. Standard sidecar topology.
createHostedBotSupervisor directly inside the room DO so bot moves dispatch through the same authoritative path humans do — see Reference: @openturn/lobby for the full API and How-to: play against bots for the wiring.
Choosing a bot in the lobby
Before a match starts, players can pick which bot fills which empty seat — Random, Minimax · easy, Minimax · hard — from a typedBotRegistry. The same <LobbyWithBots> component renders the dropdown for both local single-device play and hosted multiplayer; the choice flows through lobby:assign_bot to the server, lands in LobbyStartAssignment.botID, and the supervisor wires the corresponding Bot<TGame> instance into the freshly-started session.
The registry is engine-inert metadata: defineGame({ bots }) (or attachBots(game, registry)) carries it through, but the engine never reads it. Only the lobby runtime, the deploy step, and the supervisor do. See How-to: play against bots for the registration + UI wiring.
The canonical multi-bot example ships in examples/games/splendor/bots: three difficulty-tiered bots (random, greedy, strategic) declared via defineBotRegistry and bound to the game with attachBots(splendor, splendorBotRegistry). The Splendor app ships them in the per-seat lobby dropdown for hosted 2–4 player matches.
Limitations
simulateis local-only. Hosted hosts do not see the full server-side snapshot. A bot connecting over the network must rely onview,legalActions, and pure-view heuristics. Search-based bots run server-side or as in-process bots in the CLI.legalActionsenumeration is the author’s responsibility. The framework cannot infer which(event, payload)combinations are valid because move-arg payloads are opaque types to the engine. Add the hook todefineGameonce and every bot benefits.- Per-decision deadline only. The runner enforces
thinkingBudgetMsvia aDeadlineTokenyou check insidedecide, but it does not kill a runawaydecide. A misbehaving bot stays in flight until it returns or the abort signal fires (which it will when the next snapshot lands). For untrusted code, run bots in a Worker or a separate process.
Related
- Reference:
@openturn/bot— full API surface. - Reference:
@openturn/lobby— bot registry + supervisors + lobby UI. - How-to: add an AI bot to a game — practical guide.
- How-to: play against bots in the lobby — per-seat bot dropdowns.
- Tutorial: tic-tac-toe with bots — end-to-end walkthrough including the example minimax bot.