Documentation

Game logic

v1 · Deprecated

Framework v1 is deprecated for new games. Existing games remain playable and editable.

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.

Need a hand with this?

Join our Discord