Step 1 — Register your bots
Build aBotRegistry once. Put it next to your game’s bot implementations:
defineBotRegistry validates botID uniqueness at definition time. Different difficulties are distinct bot instances — there is no runtime-tunable param.
The attachBots(game, registry) helper sidesteps a circular package dep: the bots package imports the game’s types, so the game can’t import the bots back. Apps import the bot-attached game from the bots package instead.
Step 2 — Render the lobby with bot dropdowns
<LobbyWithBots> is the same as <Lobby> but each seat is a <LobbySeatControl> with an “Assign bot ▾” dropdown for host viewers. The bot catalog comes from lobby.availableBots, which the server populates from the registry — apps don’t have to thread bots into the UI.
Local single-device
botMap is { "1": "minimax-hard" }. The game phase mounts and the bot starts playing.
Hosted multiplayer (dev server + cloud)
The hosted lobby uses the WebSocket-backeduseLobbyChannel instead of useLocalLobbyChannel, but the rest is identical:
openturn dev) and the cloud Durable Object both read game.bots and feed buildKnownBots(registry) into LobbyEnv.knownBots automatically — no app glue. The dropdowns appear as soon as attachBots(game, registry) is set on the deployed game.
Step 3 — Wire bots into the freshly-started session
The transition gives you the seat→bot map. You need to attachBot<TGame> instances to the corresponding seats so they dispatch moves.
Pattern A — useBotAttachOnTransition (simplest)
For apps that hold a raw LocalGameSession directly:
Pattern B — createLocalBotSupervisor (no React)
For non-React drivers, CLIs, or test harnesses:
Inspector-aware bot driver
If you usecreateOpenturnBindings({ runtime: "local" }) and want bot moves to flow through the same matchStore (so the inspector timeline tracks them alongside human moves), wire bots manually with a small effect keyed on snapshot:
useBotAttachOnTransition but every dispatch — human and bot — flows through the bindings’ matchStore, so the inspector shows the full timeline.
Step 4 — Verify
Smoke test in three modes:-
Local React app:
Open the page, pick “Bot · Minimax · hard” for seat 1, click Start. The bot plays.
-
OSS hosted dev:
In tab A, pick “Bot · Random” for seat 1. In tab B, take seat 0. Click Start in tab A. Both browsers see the bot play.
-
Cloud: deploy a build with
attachBots(game, registry)set and play a public room. The cloud Durable Object reads the manifest’savailableBotsand instantiates the in-DOBotDriverautomatically.
A larger worked example: Splendor
examples/games/splendor ships the same pattern at production polish — a 2–4 player hosted game with three difficulty-tiered bots in the lobby. See splendor/bots/src/index.ts for defineBotRegistry declaring random, greedy, and strategic, and splendor/app/ for the <LobbyWithBots> wiring against hosted multiplayer. Run it with bun --filter @openturn/example-splendor-app dev and open the printed URL in 2–4 tabs to see the per-seat dropdowns under load.
Common rejections
seat_has_human— you tried toassignBotto a seat someone is sitting in. UseclearSeatfirst or pick a different seat.seat_has_bot— a non-host viewer tried totakeSeaton a bot seat. Bot seats are host-controlled; the host has toclearSeatfirst.unknown_bot— the wirebotIDisn’t inLobbyEnv.knownBots. Usually means the deployed game wasn’t built withattachBots. Rebuild and redeploy.
Limitations
- Different difficulties are distinct bot instances. No runtime-tunable params yet. If you want a “depth slider”, instantiate one descriptor per slider value or extend
BotDescriptorwithparams?: Record<string, unknown>and pipe it throughlobby:assign_bot. - The local channel auto-seats the host. Single-device play assumes one human; the host takes seat 0 on mount. To skip auto-seat (rare), pass
autoSeatIndex: nulltouseLocalLobbyChannel. - No bot-vs-bot starting condition for hosted. The host has to take a seat for
start()to gate properly. If you want a “headless host” who only configures and starts, lowerminPlayersand don’t auto-seat.
Related
- Reference:
@openturn/lobby— full API surface. - How-to: build a lobby — the lobby itself, without bots.
- How-to: add an AI bot — author the underlying
Botinstances. - Concepts: AI bots — design rationale.