Have more questions? Join our

Pieces, spaces, and state buckets

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.

PieceDef

import { 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().

Registering kinds

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

SpaceDef

import { 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.

The three state buckets

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.

Framework fields

piece.publicState always carries these, and your PublicState generic must not shadow them:

type PieceDefBaseState = {
  order: number;
  currentOrientationIndex: number;
  isSelected?: boolean;
  spaceId?: string;
};

Reading privateState

piece.privateState is PrivateState | nullnull 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.

Gotchas

  • kind collisions throw when the def registry is built.
  • A runtime-only kind still has to be in gameConfig().pieceDefs.
  • Defining both orientations and render throws. Define only orientations.
  • privateState is null for non-owners in every context, including your React client.