Skip to main content

Battle lifecycle

A battle is a state machine with a fixed number of seats. The recording, the live feed and the webhook all hang off a transition in that machine, so read this page before the API pages.

The six states

OPEN accepts players, RUNNING means seats are recording, and the remaining three states are terminal.

Transitions are deny-by-default in both directions: a state with no outgoing entry cannot transition at all, and an unlisted target is rejected. A rejected transition leaves the status unchanged rather than erroring into a half-applied state.

FromMay become
OPENSTARTING, CANCELLED
STARTINGRUNNING, COMPLETED, FAILED
RUNNINGCOMPLETED, FAILED
COMPLETED / FAILED / CANCELLEDnothing, terminal

STARTING reaches COMPLETED or FAILED without passing through RUNNING when a launch resolves or fails before any seat starts recording. Both edges are reachable.

Seats

A battle has between 2 and 8 seats, fixed at creation. Each seat runs its own lifecycle, independent of the battle's.

Seat statusMeaning
EMPTYNobody in it
FILLEDClaimed, but no game session yet, so it cannot play
READYClaimed with a game session URL, so it can play
PLAYINGRecording in progress
DONERecording finished, outcome known

FILLED is not READY. A seat becomes READY only once it has a game session URL, which is why the creator's own seat can land in either state.

The creator's seat

Creating a battle seats the creator at index 0. One field decides whether that seat lands FILLED or READY:

  • creator_game_url supplied: seat 0 is READY
  • creator_game_url omitted: seat 0 is FILLED, and the creator must supply a URL later

A two-seat battle created without creator_game_url therefore does not auto-start when the other player joins, because seat 0 is still FILLED.

Auto-start

There is no start endpoint. The lobby starts itself the moment every seat is READY, checked after each seat change. An empty seat list never satisfies the check, so a zero-seat battle can never auto-start.

The three ways to reach STARTING are therefore all seat changes:

  1. The last human player takes the last seat.
  2. The creator fills the remaining seats with bots.
  3. A player whose seat was FILLED upgrades it to READY by supplying a URL.

What happens in RUNNING

On the transition, SlotBattle dispatches one recorder per seat, each an isolated invocation running a headless browser against the real game. Seats coordinate through Redis barriers so they start spinning together rather than drifting apart by however long each browser took to boot.

Each recorder captures its seat to HLS video and uploads segments as they are produced, which is what lets the audience watch a seat while that seat is still spinning. Each recorder reports its seat's terminal result over a signed internal callback, and SlotBattle aggregates all of them into exactly one terminal battle event.

The control plane never touches a browser or a video encoder. See Architecture.

Reaching a terminal state

Terminal stateHow a battle gets there
COMPLETEDEvery seat reported back
FAILEDThe launch failed, a seat failed unrecoverably, or the run window expired
CANCELLEDThe creator cancelled, the creator left, or the lobby window expired while the battle was still OPEN

Cancelling is creator-only and OPEN-only. A non-creator asking to cancel is refused, and so is cancelling a battle that already started. A creator who leaves cancels the battle: leaving does not hand the lobby to someone else. A non-creator who leaves frees their seat back to EMPTY.

The three windows

Three TTLs bound how long a battle may sit in a non-terminal state. A background sweep reconciles anything that overstays.

WindowBoundsOverstay becomes
LobbyHow long an OPEN lobby accepts playersCANCELLED
StartingHow long a battle may sit in STARTINGFAILED
RunHow long a RUNNING battle may takeFAILED

These are the only tuning values changeable at runtime, from the admin console under platform-scoped lobby settings. They take effect on the next battle, not the next deploy.

Zero means "use the default", never "expire now"

An untouched form field submits zero, and reading that literally would close every lobby the instant it opened. The form, the resolver and the service each reject zero as a window independently.

Two rules that shape integrations

One active battle per player, per casino. A player already seated in a non-terminal battle cannot create or join another one in the same tenant. The attempt is rejected rather than granted as a second seat.

Game session URLs never leave the process. The URL that lets a browser play a real-money session is encrypted at rest and appears in no HTTP response, log line, WebSocket frame, pub/sub message or webhook. The API never exposes it. See Keys and credentials.

Next

  • Events: what each transition emits, and which events your front end can see
  • Architecture: what runs where during RUNNING
  • Battles API: the endpoints that drive this machine