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.

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

  • orientations and render are mutually exclusive. Defining both throws "PieceDef must define exactly one of orientations or render" at game start.
  • kind must be unique across all PieceDef classes registered for the game. Building the def registry throws on collision.
  • order is required in every constructor call. Pieces in the same space are sorted ascending by order.
  • render() MUST be deterministic in (piece, gameState, ctx, clientState) — the framework caches and re-runs it; side effects belong in applyActions or onMouseEnter / onMouseLeave.
  • piece.privateState is PrivateState | null inside render()null means the viewer isn't authorized. Render fns that read privateState MUST handle null (e.g. render a card back) or the piece draws nothing.
  • A kind you only state.addPiece at runtime must still be registered in gameConfig().pieceDefs or it throws when added.