> ## Documentation Index
> Fetch the complete documentation index at: https://openturn.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Gamekit turn gating

> How gamekit decides which seat may dispatch a move.

Two layers decide whether a dispatched move is allowed to run: the engine's `activePlayers` set, and your move's own `run` body.

## `activePlayers` (engine layer)

Each phase derives an `activePlayers` list. Core's dispatch gate rejects any event whose `playerID` is not in that list with the reason `inactive_player`. This is automatic — you don't write it.

By default, with a round-robin `turn` policy, `activePlayers` is `[currentPlayer]`. That alone is enough turn-gating for a standard alternating game; no per-move check is required.

```ts theme={null}
import { defineGame, move, turn } from "@openturn/gamekit";

defineGame({
  maxPlayers: 2,
  setup: () => ({ value: 0 }),
  turn: turn.roundRobin(),
  moves: {
    increment: move({
      run({ G, move }) {
        return move.endTurn({ value: G.value + 1 });
      },
    }),
  },
  // ...
});
```

If a player whose turn it isn't tries to dispatch `increment`, core rejects with `inactive_player` before `run` is called.

## `phases` on a move

If a move is only valid in some phases, list them:

```ts theme={null}
moves: ({ move }) => ({
  fire: move<FireArgs>({
    phases: ["battle"],
    run: /* ... */,
  }),
  placeShip: move<PlaceShipArgs>({
    phases: ["planning"],
    run: /* ... */,
  }),
}),
```

If a move omits `phases`, it is valid in every phase.

## Custom rules in `run` (game layer)

When the engine layer is too coarse — phases that allow multiple seats, custom validity rules, "the player must own this card" — your `run` decides and returns `move.invalid(reason)`.

```ts theme={null}
move({
  run({ G, args, move: m, player, turn: t }) {
    if (player.id !== t.currentPlayer) return m.invalid("not_your_turn");
    if (G.hasVoted[player.id]) return m.invalid("already_voted");
    return m.endTurn({ /* ... */ });
  },
})
```

The `reason` string surfaces to the client in the dispatch result, so the UI can render a specific message rather than a generic rejection.

## Active players (per phase)

Override `activePlayers` for a phase when more than one seat may act in it — typically simultaneous phases:

```ts theme={null}
phases: {
  planning: {
    activePlayers: ({ G }) => PLAYERS.filter((id) => !G.players[id].ready),
    label: "Place your fleet",
  },
  battle: {
    label: "Fire!",
    // no override; round-robin gives a single currentPlayer
  },
},
```

When `activePlayers` returns a list, every seat on that list can dispatch moves. The move's `run` decides what's actually valid for the dispatching seat.

## Custom turn policies

Today gamekit ships one turn policy: `turn.roundRobin()`. If your game is strictly alternating, use it. If not, rely on `activePlayers` and inline `move.invalid(...)` checks to enforce ordering, and leave `turn` off.

For fully custom progression (skip turns, reversed order, elimination), author the game with `@openturn/core` directly. See [how-to: author with core](/docs/how-to/author-with-core).

## What to read next

* [Gamekit moves and outcomes](/docs/concepts/gamekit-moves-and-outcomes) is the outcome reference.
* [How-to: handle simultaneous moves](/docs/how-to/handle-simultaneous-moves) shows a concrete simultaneous pattern.
