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.
applyActions receives a single Action per call. The union is discriminated on type:
type Action =
| { type: "SpaceClick"; space: Space }
| { type: "PieceClick"; piece: Piece }
| { type: "ButtonClick"; button: Button };
Branch on action.type:
import { ApplyActionsFn } from "boardweaver";
export const applyActions: ApplyActionsFn = (state, action) => {
if (action.type === "SpaceClick") {
action.space.addPiece(action.space.spaceId, new XToken({ order: 1 }));
} else if (action.type === "PieceClick") {
action.piece.isSelected = !action.piece.isSelected;
} else if (action.type === "ButtonClick") {
if (action.button.id === "pass") advanceTurn(state);
}
};
type |
Payload | Fires when |
|---|---|---|
"SpaceClick" |
{ space: Space } |
The viewer clicks a space whose id is in getSelectableItems. |
"PieceClick" |
{ piece: Piece } |
The viewer clicks a piece whose id is in getSelectableItems. |
"ButtonClick" |
{ button: Button } |
The viewer clicks a button returned by getButtons. |
Space / Piece here are the live Space / Piece instances from gameState — see game-state. Button is { id: string; label: string; location: "PlayerHeader" \| "GameHeader"; disabled?: boolean }.
An action only fires if its target was selectable for the viewer. getSelectableItems returns:
type SelectableItem = { itemType: "Space" | "Piece"; itemId: string };
Buttons are gated separately via button.disabled returned from getButtons. See game-logic.
metaDataStash intermediate per-turn state in gameState.metaData. It survives between actions, lives on the wire (visible to everyone), and is yours to shape.
// Step 1: player clicks a piece to declare an attacker. Stash its id.
// Step 2: next SpaceClick targets the defender's space.
export const applyActions: ApplyActionsFn = (state, action) => {
if (action.type === "PieceClick" && action.piece.kind === "unit") {
state.metaData = { ...state.metaData, attackerId: action.piece.pieceId };
return;
}
if (action.type === "SpaceClick" && state.metaData.attackerId) {
resolveAttack(state, state.metaData.attackerId as string, action.space);
state.metaData = { ...state.metaData, attackerId: undefined };
}
};
export const getSelectableItems: GetSelectableItemsFn = (state) => {
if (state.metaData.attackerId) {
return state.spaces("enemy-territory").map((s) => ({
itemType: "Space", itemId: s.spaceId,
}));
}
return state.pieces("unit").map((p) => ({
itemType: "Piece", itemId: p.pieceId,
}));
};
gameState.metaData is typed via the optional MetaData generic on GameState<MetaData>. Hooks share it.
getSelectableItems (or whose button is disabled) is rejected by the host before applyActions runs — don't re-check inside the hook.applyActions MUST mutate state in place. The return type is void.render, getSelectableItems, getPlayerScores, isGameOver) may read metaData, but must not write it expecting persistence — only applyActions and preGameInitialization can mutate it.metaData is public — sensitive intermediate state belongs in a player's privateState (see state-data).applyActions and broadcasts the new state.