Skip to main content
Use @openturn/gamekit when your game is naturally “a player picks one of a few moves on their turn.” It is the shortest path from design to a playable reducer.

Skeleton

Declare moves

Each move declares:
  • args? — the payload type. Use move<T> to type it explicitly.
  • phases? — restrict the move to specific phases.
  • run — the pure state transition. Must return an outcome from move.stay | endTurn | goto | finish | invalid.
queue(kind, payload?) is a helper for building internal events to enqueue from run.

Turn gating

With a round-robin turn policy, only the current player is in activePlayers, and core’s dispatch gate handles “wrong seat” rejections automatically — no per-move predicate required. When more than one seat is allowed in a phase (simultaneous play, plugin moves), enforce game-specific rules inline in run and reject with a reason:
See gamekit turn gating for the full pattern.

Phases

Phases group moves by game stage. Each phase can override activePlayers (for simultaneous play) and label:
Transition phases with move.goto("phaseName").

Computed values

Expose derived facts as selectors. They are available as C inside moves, permissions, and views, and also show up in snapshot.derived.selectors:

Views

views.public is the default; views.player runs once per seat. Both must return JSON.

When to reach for core

  • You need more than one event per move (rare).
  • You want states that are not phase-shaped (e.g. “waiting for reconnect”).
  • You need custom state labels or control metadata that doesn’t fit phases.
Drop to core via the core field or rewrite the game with @openturn/core directly. See how-to: author with core.