Have more questions? Join our

This documents framework v1, which is deprecated. Games already on it keep running and their authors can keep editing them, but new games are created on the current framework — start with React frontends.

Game logic hooks

Each hook is a top-level export. The framework calls them in a fixed order — see core-concepts. All except applyActions and preGameInitialization are read-only on gameState; writes throw TypeError.

getLayout and renderOverlay are documented in layout-overlays.

gameConfig (required)

type GameStateConfigFn = (params: { numPlayers: number }) => GameStateConfigObject;
export const gameConfig: GameStateConfigFn = ({ numPlayers }) => ({ /* ... */ });

Called once per game. Returns the initial config object — see source-files for the field list. Deterministic: same params → same output (so reconnects and replays match).

applyActions (required)

type ApplyActionsFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>, action: Action) => void;

export const applyActions: ApplyActionsFn = (state, action) => {
  if (action.type === "SpaceClick") { /* mutate state */ }
};

Called once per legal action. MUST mutate gameState in place. See actions for the Action union; see game-state for the mutation API.

getSelectableItems (required)

type GetSelectableItemsFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>) => SelectableItem[];

type SelectableItem = { itemType: "Space" | "Piece"; itemId: string };

export const getSelectableItems: GetSelectableItemsFn = (state) => {
  return state.spaces("grid-cell", (s) => s.pieces().length === 0)
    .map((s) => ({ itemType: "Space", itemId: s.spaceId }));
};

Called on every render. Returns what the current viewer may click. The host gates input — only ids in the returned list dispatch a SpaceClick / PieceClick. Read-only.

getPlayerScores (required)

type GetPlayerScoresFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>) => Scores;

type Scores = Record<string /* playerId */, {
  publicPoints: number[];                   // one entry per scoreLabel
  privatePoints: number[];                  // hidden from non-owner
}>;

export const getPlayerScores: GetPlayerScoresFn = (state) => ({
  [state.players()[0].playerId]: { publicPoints: [3], privatePoints: [] },
  [state.players()[1].playerId]: { publicPoints: [1], privatePoints: [] },
});

Called after applyActions. The map key is playerId.toString(). Read-only.

publicPoints[i] corresponds to gameConfig().scoreLabels[i]. privatePoints are nulled on the wire for any viewer who isn't that player.

isGameOver (required)

type IsGameOverFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>, scores: Scores) => boolean;

export const isGameOver: IsGameOverFn = (state, scores) =>
  Object.values(scores).some((s) => s.publicPoints[0] >= 3);

Called after getPlayerScores. Return true to end the game. Read-only.

getButtons (optional)

type Button = {
  id: string;
  label: string;
  location: "PlayerHeader" | "GameHeader";
  disabled?: boolean;
};

type GetButtonsFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>) => Button[];

export const getButtons: GetButtonsFn = (state) => [
  { id: "pass", label: "Pass", location: "PlayerHeader" },
  { id: "concede", label: "Concede", location: "GameHeader", disabled: state.activePlayerIds.length === 0 },
];

Called on every render. A ButtonClick action delivers the clicked Button to applyActions. disabled: true greys the button and blocks dispatch. Read-only.

preGameInitialization (optional)

type PreGameInitializationFn<MetaData = Record<string, unknown>> =
  ((gameState: GameState<MetaData>) => void) | undefined;

export const preGameInitialization: PreGameInitializationFn = (state) => {
  state.activePlayerIds = [state.players()[0].playerId];
};

Called once, after gameConfig and before any action. May mutate gameState. Use for randomized setup (deck shuffle, hand draw) — gameConfig runs in a deterministic builder context, preGameInitialization runs in the same mutating context as applyActions.

Gotchas

  • The framework deep-freezes gameState before calling every hook except applyActions and preGameInitialization. Attempted writes throw TypeError, surfaced via console.error. Convert read-only-context "writes" to actions (the RemoteAction path) instead.
  • getSelectableItems runs per viewer — the gameState it receives already has privateState scrubbed for that viewer. Don't make selectability decisions on privateState you can't see.
  • Scores keys are strings (player ids stringified). Use String(playerId) or computed-property [playerId] syntax.
  • Don't depend on call order between getSelectableItems, getPlayerScores, getButtons, getLayout, renderOverlay — they're independent reads of the same state.