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.
Use @openturn/core directly when you want full control over the state graph or when gamekit’s move-first shape does not fit.
Skeleton
import { defineEvent, defineGame } from "@openturn/core";
export const game = defineGame({
maxPlayers: 2,
events: {
place_mark: defineEvent<{ row: number; col: number }>(),
},
initial: "play",
setup: () => ({ board: createEmptyBoard() }),
selectors: {
winnerMark: ({ G }) => getWinner(G.board),
},
states: {
play: {
activePlayers: ({ match, position }) =>
[match.players[(position.turn - 1) % match.players.length]!],
label: "Take your turn",
control: () => ({ status: "playing" }),
},
won: {
activePlayers: () => [],
label: "Winner",
control: () => ({ status: "won" }),
},
},
transitions: ({ transition }) => [
transition("place_mark", {
from: "play",
to: "won",
turn: "increment",
label: "place_mark:win",
resolve: ({ G, event, playerID }) => {
const board = placeMark(G.board, event.payload.row, event.payload.col, playerID);
if (board === null || getWinner(board) === null) return null;
return { G: { board }, result: { winner: playerID } };
},
}),
// more branches: draw, continue...
],
views: {
public: ({ G }) => ({ board: G.board }),
},
});
Declare events
events: {
place_mark: defineEvent<{ row: number; col: number }>(),
concede: defineEvent(),
},
defineEvent<T>() declares an event with a typed payload; defineEvent() (no type argument) is a no-payload event.
Declare states
Each state is a node of the game graph:
states: {
play: {
activePlayers: ({ match, position }) => [currentPlayerID(match, position)],
label: ({ match, position }) => `Player ${currentPlayerID(match, position)}`,
control: () => ({ status: "playing" }),
metadata: ({ G }) => [{ key: "moves_made", value: countMoves(G) }],
},
},
initial: "play",
activePlayers gates who can dispatch.
label, control, metadata surface in snapshot.derived for tooling.
Declare transitions
Each transition branch declares from, event, and resolve:
transitions: ({ transition }) => [
transition("place_mark", {
from: "play",
to: "play",
label: "place_mark:continue",
turn: "increment",
resolve: ({ G, event, playerID }) => {
const board = placeMark(G.board, event.payload.row, event.payload.col, playerID);
if (board === null || getWinner(board) !== null) return null;
return { G: { board } };
},
}),
],
Resolver returns:
null | false | undefined — branch does not match.
rejectTransition(code, details) — event is invalid; reject it.
{ G?, enqueue?, result?, turn? } — branch matches; commit.
Every (from, event) pair can have multiple branches as long as exactly one matches at any time.
Queue internal events
Need a chain reaction after committing? return { G: next, enqueue: [{ kind: "resolveEffects", payload: ... }] }. See reducers and queued events.
Selectors and views
Same as gamekit:
selectors: {
isBoardFull: ({ G }) => G.board.every((row) => row.every((cell) => cell !== null)),
},
views: {
public: ({ G }) => ({ board: G.board }),
player: ({ G, match }, playerID) => ({ board: G.board, myID: playerID }),
},
Why bother when gamekit exists
- You need more than one event per move.
- Your state graph has nodes that are not phase-shaped.
- You want to render the graph yourself and need stable transition labels.
- You are writing a library that mixes authored games (gamekit would force you to pick moves up front).