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. Only applyActions and preGameInitialization can mutate it. A write from a read-only hook throws TypeError, and while the server is validating a submitted action that refuses the action.metaData is public — sensitive intermediate state belongs in a player's privateState (see state-data).applyActions and broadcasts the new state.