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.
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.
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.getSelectableItems, getPlayerScores, getButtons, getLayout, renderOverlay — they're independent reads of the same state.