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.

Actions

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);
  }
};

Action types

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 }.

Selectability

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.

Multi-step flows: metaData

Stash 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.

Gotchas

  • An action whose target is not in 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.
  • Read-only hooks (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).
  • Action handling is server-authoritative. The client emits a candidate action; the server runs applyActions and broadcasts the new state.