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.
GameState<MetaData> is the live, mutable view of the game passed to every hook. It exposes query methods (returning Player / Piece / Space instances) and mutation methods (addPiece, addSpace, settable props).
import { ApplyActionsFn } from "boardweaver";
export const applyActions: ApplyActionsFn = (state, action) => {
const me = state.currentPlayer; // Player
const myHand = me.ensureSpaceOfKind("hand"); // Space
const top = myHand.pieces("card")[0]; // Piece | undefined
if (top) top.spaceId = state.ensureSpace("discard").spaceId;
state.activePlayerIds = [otherPlayerId(state)];
};
| Property | Type | Mutable? |
|---|---|---|
currentPlayer |
Player<MetaData> |
no (derived from session) |
activePlayerIds |
number[] |
yes — controls whose turn it is |
metaData |
MetaData (defaults to Record<string, unknown>) |
yes |
scoreLabels |
string[] | undefined |
yes |
defRegistry |
DefRegistry |
no (framework-built) |
Every query method accepts a selector:
string — meaning depends on the method. On the singular lookups (piece/space/player, ensurePiece/ensureSpace/ensurePlayer) a bare string matches by id (pieceId / spaceId / playerId). On the plural lookups (pieces/spaces/players) a bare string matches by kind — state.pieces("x-token") returns every piece whose kind is "x-token", not a piece with id "x-token".(item) => unknown — truthy keeps it.Kind-typed overloads take a kind first, then a selector (string id or predicate) that sees the narrowed item. Predicates that return undefined are treated as false.
state.pieces(); // Piece[]
state.pieces((p) => p.spaceId === "0"); // Piece[] (by predicate)
state.pieces("x-token"); // Piece[] narrowed to XToken
state.pieces("x-token", (p) => !p.isSelected); // Piece[] narrowed
state.piece("p1"); // Piece | undefined (by id)
state.piece("x-token", (p) => p.order === 1); // narrowed | undefined
state.ensurePiece("p1"); // throws if missing
state.ensurePiece("x-token", (p) => p.order === 1); // throws if missing
All pieces() results are sorted ascending by piece.order.
state.spaces();
state.spaces((s) => s.x < 200);
state.spaces("grid-cell");
state.spaces("grid-cell", (s) => s.pieces().length === 0);
state.space("0");
state.space("grid-cell", (s) => s.x === 0 && s.y === 0);
state.ensureSpace("0");
state.ensureSpace("grid-cell", (s) => s.x === 0 && s.y === 0);
// Kind-only helpers (no selector) for the common "one of this kind per scope" case:
state.spaceOfKind("grid-cell"); // first match, or undefined
state.spacesOfKind("grid-cell"); // all matches
state.ensureSpaceOfKind("grid-cell"); // throws if missing
state.players(); // Player[]
state.players((p) => p.playerId !== state.currentPlayer.playerId);
state.player(0); // by playerId (number)
state.player((p) => p.username === "alice");
state.ensurePlayer(0); // throws if missing
There are no kind-typed overloads for players (no PlayerKindRegistry).
addPiece(pieceId, def)const piece = state.addPiece("p123", new XToken({ order: 1 }));
piece.spaceId = "0"; // attach to a space
Throws if pieceId collides or def.kind isn't registered (see pieces).
space.addPiece(pieceId, def) is shorthand that sets piece.spaceId = space.spaceId.
addSpace(spaceId, def)const space = state.addSpace("s123", new GridCell({ x: 0, y: 0 }));
space.playerId = state.currentPlayer.playerId;
Throws if spaceId collides or def.kind isn't registered (see spaces).
player.addSpace(spaceId, def) is shorthand that sets space.playerId = player.playerId.
Most fields on Piece, Space, Player, GameState have setters — see api-reference for the full list. Common mutations:
piece.spaceId = newSpace.spaceId;
piece.order = 5;
piece.isSelected = true;
piece.currentOrientationIndex = 1;
piece.publicState = { ...piece.publicState, tapped: true };
space.isHidden = true;
space.type = "Horizontal";
player.notification = { title: "Your turn", intent: "info" };
state.activePlayerIds = [nextPlayerId];
state.metaData = { ...state.metaData, phase: "combat" };
publicState and privateState setters take the full object — mutating an individual key in place works too, but the setter is the safe form when you need to pin a shape.
Player has the same piece / pieces / space / spaces / *OfKind methods, scoped to that player's owned spaces and the pieces inside them.
const me = state.currentPlayer;
me.pieces(); // pieces in spaces I own, sorted by order
me.pieces("card", (c) => c.isSelected);
me.spaceOfKind("hand"); // first space I own with kind "hand"
me.ensureSpaceOfKind("hand"); // throws if missing
me.addSpace("scratch", new GridCell({ x: 0, y: 0 })); // adds + sets playerId
Per-player spaces use synthesized ids like ${playerId}/space/${configKey}, so player.space("hand") (matching spaceId) usually won't hit. Use player.spaceOfKind("hand") instead.
Space.pieces / piece / ensurePiece are scoped to that space:
space.pieces(); // pieces with spaceId === space.spaceId
space.pieces("card");
space.piece((p) => p.isSelected);
space.ensurePiece("p1");
space.addPiece("p2", new XToken({ order: 1 })); // adds + sets spaceId
piece.space; // Space | undefined
piece.ensureSpace(); // throws if no space
piece.ensureSpace("grid-cell"); // narrows the returned Space
piece.player; // Player | undefined (via piece.space.player)
space.player; // Player | undefined
space.ensurePlayer(); // throws if unowned
currentPlayer is "the player this hook is being run on behalf of," which is server-set. Don't override it.activePlayerIds is the source of truth for whose turn it is. Set it in applyActions to advance turns.addPiece / addSpace throw on duplicate ids — generate stable ids you can derive (e.g. ${space.spaceId}/${turn}), not random UUIDs in render-time logic.piece.publicState includes the framework's PieceDefBaseState (order, currentOrientationIndex, isSelected, spaceId). Don't shadow those keys in your own PublicState generic.piece.privateState is PrivateState | null after scrubbing — read-side hooks must handle null (you'll see your own pieces normal, opponents' as null).state.pieces() returns objects sorted by order; if you mutate order, subsequent reads in the same applyActions call already see the new order.