@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
args?— the payload type. Usemove<T>to type it explicitly.phases?— restrict the move to specific phases.run— the pure state transition. Must return an outcome frommove.stay | endTurn | goto | finish | invalid.
queue(kind, payload?) is a helper for building internal events to enqueue from run.
Turn gating
With a round-robinturn 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:
Phases
Phases group moves by game stage. Each phase can overrideactivePlayers (for simultaneous play) and label:
move.goto("phaseName").
Computed values
Expose derived facts as selectors. They are available asC 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
controlmetadata that doesn’t fitphases.
core field or rewrite the game with @openturn/core directly. See how-to: author with core.
Related
- Concepts: gamekit is the conceptual deep dive.
- Reference: gamekit is the API reference.