Most games have settings that should be agreed before play begins: turn time limit, score target, variant rules, difficulty. Declaring them as 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.
config schema on your game gets you a host-mutable settings form in the lobby, validation at three layers, full TypeScript inference for game code, and persistence-safe round-trip — without writing a config form yourself.
If your game has no match-shape settings, skip this entirely. match.config is opt-in.
1. Declare the schema
Addconfig to your defineGame(...) call. Each field declares a type, a default, a label, and any type-specific bounds.
as const satisfies ConfigSchema: without as const on the literal, enum option tuples widen to string and you lose narrow inference (variant would type as string instead of "classic" | "advanced"). The satisfies clause keeps the schema typed without needing an explicit annotation that would also widen literals.
Supported field types
| Type | Knobs | Default UI |
|---|---|---|
"number" | default, optional min, max, step | Slider when both min and max are set, otherwise numeric stepper |
"boolean" | default | Checkbox |
"enum" | options (literal tuple), default, optional per-option labels | Radio group when options.length <= 4, dropdown otherwise |
label (required) and description (optional). step on numbers is purely a UI hint — the engine doesn’t validate against it.
2. Read values from game code
match.config is typed from your schema. Read it in state configs, transition resolvers, view functions, or anywhere ctx.match is in scope:
deadline: (ctx) => deadline.after(ctx, 5_000) for that state. The schema is a source of values, not a directive.
3. Render the lobby UI
<Lobby> and <LobbyWithBots> accept a configUI opt-in prop. When "auto", they render a collapsible “Settings” section above the seat list, default-expanded for the host with disabled inputs for non-host viewers.
configUI defaults to "none" for back-compat — existing lobbies don’t grow a config section unless you explicitly opt in.
Custom field rendering
For fields that need bespoke UI (a slider with custom tick labels, a swatch picker for a color enum, etc.), pass aconfigRenderers map. The generic helper ConfigRenderers<TSchema> enforces that each renderer’s value type matches its field’s declared type at construction time:
value, an onChange(next) callback, the field’s defaultValue and schema, plus disabled and an optional error (set when the server rejects a mutation). Other fields fall back to the built-in renderer.
4. Validation, errors, and lifecycle
Three validation layers protect the runtime:- Wire —
LobbyRuntime.setConfigrejects invalid values withreason: "invalid_config_value"andconfigKey/configDetail("below_min: 5000","not_in_options: foo", etc.). The auto-rendered form surfaces these next to the offending field. - Lock —
lobby.start()re-validates the whole config before snapshotting it intomatch.config. - Engine —
normalizeMatchInput(called automatically at session creation) validates against the schema and fills any missing keys with their declared defaults. This catches malformed input from non-lobby callers (saved replays, hand-rolled tests, externalcreateRoomRuntimeinvocations).
- Mutable until
lobby:start, frozen afterward. Once the lobby starts,match.configis immutable for the duration of the match — same model as the rest ofmatch(replays are deterministic). - Host-only mutations. Non-host viewers can read but not write.
- Successful changes un-ready every human seat. Players have to re-confirm before the host can start, so nobody gets surprised by a last-second timer change.
- Schema evolution between deploys. Lobby state preserves persisted values that still pass validation; fields removed from the schema are dropped silently, fields newly added pick up their default. No migrations.
When to reach for this vs. game state
Use the config schema for match-shape settings — values that everyone in the match needs to agree on before play, that affect the rules, and that are read-only after start: turn timer, point target, variant rules, difficulty. Use normal game state (with a host-only setup state, if you have a host) for in-match decisions — drafts, variant picks tied to who’s seated, per-player choices, anything that depends on the actual match starting first. The two compose: a “draft” state can readctx.match.config.draftStyle (config) and present different choices for that draft (game state).
See also
- Build a lobby for the lobby surface this plugs into.
- Concepts: turns, phases, and control for the per-state
deadlinefield that pairs naturally withconfig.turnTimeoutMs. - Enforce turn timeouts — the canonical use case for
turnTimeoutMs: server-authoritative deadline enforcement with game-author-defined responses.