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.
Pieces
A piece is an instance of a PieceDef subclass. The subclass declares the piece's identity (kind), default dimensions, how it draws (orientations xor render), and optional state buckets and event handlers.
import { PieceDef } from "boardweaver";
class XToken extends PieceDef {
readonly kind = "x-token"; // class id; unique across all PieceDefs
readonly width = 110;
readonly height = 110;
readonly orientations = [{ imageSrc: "XPiece" }]; // image-based piece
}
// Instantiate in gameConfig().pieces OR at runtime via state.addPiece.
new XToken({ order: 1 });
Required fields
| Field | Type | Notes |
|---|---|---|
kind |
string |
Unique class id. Used by state.pieces("x-token"), the def registry, the wire kind field. Two PieceDef classes MUST NOT share a kind. |
width |
number |
Default px width. Orientation-mode pieces override per-orientation; render-mode pieces override per-call. |
height |
number |
Default px height. Same override rules. |
Drawing: orientations XOR render
Exactly one of these must be defined. Both → throws at game start. Neither → throws.
Orientation mode (image-based)
class Card extends PieceDef {
readonly kind = "card";
readonly width = 100;
readonly height = 140;
readonly orientations = [
{ imageSrc: "CardFront" }, // current when currentOrientationIndex === 0
{ imageSrc: "CardBack" }, // current when currentOrientationIndex === 1
{ imageSrc: null }, // null = blank tile
];
}
imageSrc is a GameImageKey (string) or null — see images. Per-orientation width/height overrides the class default for that orientation only.
Render mode (BWSS function)
import { PieceDef, type PieceRenderFn } from "boardweaver";
class CountToken extends PieceDef<{ count: number }> {
readonly kind = "count-token";
readonly width = 80;
readonly height = 80;
readonly render: PieceRenderFn<{ count: number }> = (piece, state, ctx) => ({
type: "Box",
style: {
width: piece.width,
height: piece.height,
backgroundColor: "$colors.surface.canvas",
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
},
children: [String(piece.publicState.count)],
});
}
Signature: (piece, gameState, ctx, clientState) => PieceRenderResult. Result is a PieceBwssNode, { node, width?, height?, placement? }, or null (renders nothing in this context). See styling for the BWSS node shapes; see layout-overlays for clientState.
ctx.mode is "board" (with ctx.isSelectable: boolean) or "tooltip". Return null from "tooltip" mode to suppress the hover popover. ctx.placement (when returned) hints the preferred tooltip side: "top" | "bottom" | "left" | "right".
Optional fields
| Field | Type | Default | Notes |
|---|---|---|---|
static |
Static (generic) |
{} |
Constant per-class data. Never on the wire. Read via piece.def.static. |
publicState |
PublicState & PieceDefBaseState |
merged from constructor + publicDefaults() |
See state-data. |
privateState |
PrivateState |
merged from constructor + privateDefaults() |
Scrubbed to null for unauthorized viewers. |
onMouseEnter |
(event, piece, state, clientState) => void |
not set | Viewer-local. May mutate clientState. state is deep-frozen. |
onMouseLeave |
same shape as onMouseEnter |
not set |
PieceDefBaseState (always present on piece.publicState):
type PieceDefBaseState = {
order: number; // required in constructor params
currentOrientationIndex: number; // default 0
isSelected?: boolean;
spaceId?: string; // which space contains this piece
};
Constructor
type PieceDefConstructorParams<PublicState, PrivateState> =
Partial<PieceDefBaseState> &
Partial<PublicState> & {
privateState?: Partial<PrivateState>;
} & Pick<PieceDefBaseState, "order">; // `order` is required
Subclasses override publicDefaults() / privateDefaults() for class-level defaults:
class Card extends PieceDef<{ tapped: boolean }, { victoryPoints: number }> {
readonly kind = "card";
readonly width = 100;
readonly height = 140;
readonly orientations = [{ imageSrc: "CardFront" }];
protected publicDefaults() {
return { tapped: false };
}
protected privateDefaults() {
return { victoryPoints: 0 };
}
}
new Card({ order: 1 }); // tapped=false, victoryPoints=0
new Card({ order: 1, tapped: true }); // override public
new Card({ order: 1, privateState: { victoryPoints: 3 } }); // override private
Generic parameters
abstract class PieceDef<
PublicState extends Record<string, unknown> = {},
PrivateState extends Record<string, unknown> = {},
Static extends Record<string, unknown> = {},
ClientState extends Record<string, unknown> = {},
GlobalClientState extends Record<string, unknown> = {},
>
ClientState is viewer-local UI state — see layout-overlays. The two state generics may not declare fields that collide with PieceDefBaseState keys (order, currentOrientationIndex, isSelected, spaceId) or with each other.
Gotchas
orientationsandrenderare mutually exclusive. Defining both throws"PieceDef must define exactly one of orientations or render"at game start.kindmust be unique across allPieceDefclasses registered for the game. Building the def registry throws on collision.orderis required in every constructor call. Pieces in the same space are sorted ascending byorder.render()MUST be deterministic in(piece, gameState, ctx, clientState)— the framework caches and re-runs it; side effects belong inapplyActionsoronMouseEnter/onMouseLeave.piece.privateStateisPrivateState | nullinsiderender()—nullmeans the viewer isn't authorized. Render fns that readprivateStateMUST handlenull(e.g. render a card back) or the piece draws nothing.- A
kindyou onlystate.addPieceat runtime must still be registered ingameConfig().pieceDefsor it throws when added.