A piece is an instance of a PieceDef subclass; a space is an instance of a SpaceDef subclass. The subclass is shared across every instance of that kind — it holds identity, dimensions, and constants, never per-instance data.
PieceDefimport { PieceDef } from "boardweaver";
class XToken extends PieceDef {
readonly kind = "x-token"; // unique across all PieceDefs
readonly width = 110;
readonly height = 110;
readonly orientations = [{ imageSrc: GameImage.XPiece }];
}
new XToken({ order: 1 });
| Field | Type | Notes |
|---|---|---|
kind |
string |
Unique class id. Drives state.pieces("x-token") and the def registry. Two classes must not share one. |
width / height |
number |
Default size in px. |
orientations is required even though your client does the drawing. A PieceDef must define exactly one of orientations or render, or building the game throws "PieceDef must define either orientations or render". In v2 the platform never draws from it — your React component decides what a piece looks like — so declare a single orientation naming the piece's image and move on:
readonly orientations = [{ imageSrc: GameImage.XPiece }];
imageSrc is a member of the per-game GameImage enum exported from "boardweaver" (or null for no image). Your client resolves the same key to a URL with useImage().
Every kind must appear in gameConfig().pieceDefs, including kinds you only create at runtime. state.addPiece throws on an unregistered kind.
Register the kind with the type-level registry so lookups narrow properly:
declare module "boardweaver" {
interface PieceKindRegistry {
"x-token": XToken;
}
}
SpaceDefimport { SpaceDef } from "boardweaver";
class GridCell extends SpaceDef {
readonly kind = "grid-cell";
readonly type = "Stack" as const;
readonly width = 130;
readonly height = 130;
readonly x: number;
readonly y: number;
constructor({ x, y }: { x: number; y: number }) {
super();
this.x = x;
this.y = y;
}
}
| Field | Type | Notes |
|---|---|---|
kind |
string |
Unique across all SpaceDefs. |
type |
"Stack" | "Horizontal" | "Vertical" |
How contained pieces are ordered. |
x / y |
number |
Position from the board origin. |
width / height |
number |
Size in px. |
Optional: playerId (marks ownership), isPrivate (scrubs contained pieces' privateState from other viewers), isHidden, and pieces (initial children).
Position and size are data, not layout. In v2 nothing places a space for you — x/y/width/height are values your React client may read and honor, or ignore entirely in favor of its own layout. Set them to something coherent so the numbers mean something, and lay the board out in your component.
SpaceDef also has a type-level registry, SpaceKindRegistry, declared the same way.
Where a value lives decides who can see it and whether it travels.
| Bucket | Scope | Visibility |
|---|---|---|
static |
Per class | Never on the wire. Constants: name, cost, ability text. Read via piece.def.static. |
publicState |
Per instance | Everyone. Tapped flags, counters, damage. |
privateState |
Per instance | Owner only. The server nulls it for every other viewer, so hidden information never leaves the server. |
class Card extends PieceDef<{ tapped: boolean }, { victoryPoints: number }> {
readonly kind = "card";
readonly width = 100;
readonly height = 140;
readonly orientations = [{ imageSrc: GameImage.CardFront }];
protected publicDefaults() {
return { tapped: false };
}
protected privateDefaults() {
return { victoryPoints: 0 };
}
}
new Card({ order: 1 }); // defaults
new Card({ order: 1, tapped: true }); // override public
new Card({ order: 1, privateState: { victoryPoints: 3 } }); // override private
order is required in every constructor call — pieces in a space sort ascending by it.
piece.publicState always carries these, and your PublicState generic must not shadow them:
type PieceDefBaseState = {
order: number;
currentOrientationIndex: number;
isSelected?: boolean;
spaceId?: string;
};
privateStatepiece.privateState is PrivateState | null — null means this viewer isn't the owner. Every read must handle null, in your hooks and in your client. That is the mechanism that keeps a hand hidden, so it is not optional.
kind collisions throw when the def registry is built.gameConfig().pieceDefs.orientations and render throws. Define only orientations.privateState is null for non-owners in every context, including your React client.