Most multiplayer games need a pre-game step: a lobby where players join, pick seats, optionally pick a bot for an empty seat, and wait for a quorum. Openturn ships aDocumentation Index
Fetch the complete documentation index at: https://openturn.io/docs/llms.txt
Use this file to discover all available pages before exploring further.
LobbyRuntime that handles this lifecycle; the unified @openturn/lobby package exposes it on the client (local single-device or hosted) without boilerplate.
If you want per-seat bot dropdowns (let players pick “Random” vs “Minimax · hard” before the match starts), see How-to: play against bots — the same <LobbyWithBots> component covers both lobbies-without-bots and lobbies-with-bots; you opt into bots by setting game.bots via attachBots(game, registry).
What the lobby does
- Track joined players by user ID.
- Enforce a player range —
[game.minPlayers, game.playerIDs.length]— declared on the game, with a host-mutabletargetCapacityinside that range. - Assign player IDs to users in a stable order.
- Broadcast lobby snapshots to everyone in the room.
- Sign game tokens and hand off to the match when ready.
Declaring a player range
SetmaxPlayers (or playerIDs for named seats) and optional minPlayers on your defineGame config. The framework generates default IDs "0",..,"N-1" from maxPlayers.
match.players is the seated subset (preserving seat order), so existing logic like match.players.includes(playerID) and roundRobin(match.players, ...) cycles only the players who actually joined. Author your setup against the runtime match parameter so per-player state matches the seated count.
Server side: LobbyRuntime
The openturn dev server and the generated Cloudflare Worker both instantiate a LobbyRuntime per room. You rarely touch this directly, but here is the shape:
takeSeat(userID, userName, seatIndex)/leaveSeat(userID)setReady(userID, ready)assignBot(hostUserID, seatIndex, botID)/clearSeat(hostUserID, seatIndex)(host-only)setTargetCapacity(hostUserID, targetCapacity)(host-only) — mutates within[minPlayers, maxPlayers]. Lowering evicts seats whoseseatIndex >= targetCapacity(humans become unseated; bots are removed).start(hostUserID)— succeeds when seated count is in[minPlayers, targetCapacity]and every human is ready. Bots count toward the seated total and skip the ready check.close(hostUserID)/dropUser(userID)
minPlayers, maxPlayers, targetCapacity, available bots. React binds to this via buildLobbyView.
Client side: two integration paths
The lobby UI lives in@openturn/lobby/react. @openturn/react re-exports it for back-compat, but new code should import from @openturn/lobby/react directly.
Path A — <OpenturnProvider> + useRoom (hosted multiplayer with built-in bridge)
room.phase walks idle → connecting → lobby → transitioning → game. room.lobby is a LobbyView; room.game is a HostedMatchState.
<LobbyWithBots /> is the standard round-table UI plus per-seat bot dropdowns. If your game has no BotRegistry, the dropdowns simply don’t appear — the component degrades to plain seat claim/leave/ready/start. Use plain <Lobby> (also from @openturn/lobby/react) if you want to skip bot affordances entirely.
If you want to build your own UI, room.lobby has all the fields you need: seats (a discriminated union of open | human | bot), playerID, isHost, canStart, availableBots, minPlayers/maxPlayers/targetCapacity, and the action callbacks (takeSeat, leaveSeat, setReady, assignBot, clearSeat, setTargetCapacity, start).
For variable-player games, <LobbyWithBots> automatically renders a host-only capacity picker (− / + buttons) when minPlayers < maxPlayers. The picker is hidden for fixed-size games.
Path B — useLobbyChannel directly (custom shells)
For custom lobby UIs without the hosted match wrapper, use useLobbyChannel:
Path C — useLocalLobbyChannel for single-device play
The same <LobbyWithBots> UI runs against an in-memory LobbyRuntime for single-device local play — no WebSocket, no token. Useful when you want one human to set up a match against bots before kicking off createLocalSession:
A complete reference implementation
examples/games/splendor/app is the canonical end-to-end lobby: a 2–4 player variable-capacity game wiring <OpenturnProvider> + useRoom + <LobbyWithBots> against hosted multiplayer, with the host’s capacity picker (because minPlayers=2, maxPlayers=4) and per-seat dropdowns for the three bots from splendor/bots. Run bun --filter @openturn/example-splendor-app dev and open the printed URL in 2–4 tabs to exercise every lobby affordance — seat claim, ready/start, capacity narrowing, bot picking — under a real WebSocket round-trip.
Related
- Tutorial: tic-tac-toe multiplayer uses
useRoom()end to end. - Reference: lobby — full
@openturn/lobbyAPI surface. - Reference: server for
LobbyRuntimeoptions. - Reference: protocol for the lobby wire protocol.
- How-to: play against bots — wire per-seat bot dropdowns.
- How-to: configure match settings — host-mutable settings form (turn timer, variant, etc.) that locks into
match.configat start. - How-to: enforce turn timeouts — server-authoritative deadline enforcement: declare a
phase.deadline, write anonTimeouthandler, the framework fires the trigger when wall-clock elapses.