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.

API reference

No-prose dump of every public signature. See the topic-specific sections for behavior and gotchas.

Hook signatures

type GameStateConfigFn = (params: { numPlayers: number }) => GameStateConfigObject;

type ApplyActionsFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>, action: Action) => void;

type GetSelectableItemsFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>) => SelectableItem[];

type GetPlayerScoresFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>) => Scores;

type IsGameOverFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>, scores: Scores) => boolean;

type GetButtonsFn<MetaData = Record<string, unknown>> =
  (gameState: GameState<MetaData>) => Button[];

type PreGameInitializationFn<MetaData = Record<string, unknown>> =
  ((gameState: GameState<MetaData>) => void) | undefined;

type GetLayoutFn<MetaData = Record<string, unknown>> = (
  gameState: GameState<MetaData>,
  viewingPlayerId: number,
  viewport: { width: number; height: number },
) => Layout;

type RenderOverlayFn<MetaData = Record<string, unknown>> = (
  gameState: GameState<MetaData>,
  viewingPlayerId: number,
  clientState: GlobalClientStateReadView,
  overlay: Overlay,
) => OverlayBwssNode[];

Action / Button / SelectableItem

type Action =
  | { type: "ButtonClick"; button: Button }
  | { type: "PieceClick";  piece: Piece }
  | { type: "SpaceClick";  space: Space };

type Button = {
  id: string;
  label: string;
  location: "PlayerHeader" | "GameHeader";
  disabled?: boolean;
};

type SelectableItem = { itemType: "Space" | "Piece"; itemId: string };

Layout

type RadialLayout = { type: "radial" };

type GridLayout = {
  type: "grid";
  columns: string[];
  rows: string[];
  areas: (string | null)[][];
  gap?: number;
  areaOptions?: Record<string, { rotation?: 0 | 90 | 180 | 270 }>;
};

type Layout = RadialLayout | GridLayout;

Overlay

type OverlayHandle =
  | { piece: string }
  | { space: string }
  | { player: number };

type OverlayRect = { x: number; y: number; width: number; height: number };
type OverlayClip = OverlayRect | null;
type ResolvedTarget = { rect: OverlayRect; clip: OverlayClip; visibility: "visible" | "clipped" };

type Overlay = {
  rectOf(handle: OverlayHandle): ResolvedTarget | null;
  clampToClip(point: { x: number; y: number }, clip: OverlayClip): { x: number; y: number };
};

type OverlayBwssNode = PieceBwssNode;                  // Box | Image | Svg

Scores

type Score  = { publicPoints: number[]; privatePoints: number[] };
type Scores = Record<string /* playerId */, Score>;

PieceDef

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> = {},
> {
  abstract readonly kind: string;
  abstract readonly width: number;
  abstract readonly height: number;

  readonly orientations?: PieceOrientation[];          // XOR with render
  readonly render?: PieceRenderFn<PublicState, PrivateState, /* MetaData */ {}, ClientState, GlobalClientState>;

  readonly static?: Static;
  readonly onMouseEnter?: (event: BwMouseEvent, piece: Piece, state: GameState, clientState: PieceClientStateView<ClientState, GlobalClientState>) => void;
  readonly onMouseLeave?: (event: BwMouseEvent, piece: Piece, state: GameState, clientState: PieceClientStateView<ClientState, GlobalClientState>) => void;

  publicState:  PieceDefBaseState & PublicState;
  privateState: PrivateState;
  readonly clientStateDefault: Partial<ClientState>;

  protected publicDefaults():  Partial<PieceDefBaseState & PublicState>;
  protected privateDefaults(): Partial<PrivateState>;
  protected clientStateDefaults(): Partial<ClientState>;

  constructor(params: PieceDefConstructorParams<PublicState, PrivateState>);
}

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

type PieceOrientation = {
  imageSrc: GameImageKey | null;
  width?: number;
  height?: number;
};

SpaceDef

abstract class SpaceDef<
  PublicState  extends Record<string, unknown> = {},
  PrivateState extends Record<string, unknown> = {},
  Static       extends Record<string, unknown> = {},
  Flags        extends { render?: boolean }    = {},
  ClientState  extends Record<string, unknown> = {},
  GlobalClientState extends Record<string, unknown> = {},
> {
  abstract readonly kind: string;
  abstract readonly type: "Stack" | "Horizontal" | "Vertical";
  abstract readonly x: number;
  abstract readonly y: number;
  abstract readonly width: number;
  abstract readonly height: number;

  readonly playerId?: number;
  readonly isPrivate?: boolean;
  readonly isHidden?: boolean;
  readonly scrollable?: boolean | false | undefined;   // narrowed to false|undefined when Flags={render:true}

  readonly render?: SpaceRenderFn</* MetaData */ {}, ClientState, GlobalClientState>;
  readonly static?: Static;
  readonly pieces?: Record<string, PieceDef>;
  readonly onMouseEnter?: (event: BwMouseEvent, space: Space, state: GameState, clientState: SpaceClientStateView<ClientState, GlobalClientState>) => void;
  readonly onMouseLeave?: (event: BwMouseEvent, space: Space, state: GameState, clientState: SpaceClientStateView<ClientState, GlobalClientState>) => void;

  publicState:  PublicState;
  privateState: PrivateState;
  readonly clientStateDefault: Partial<ClientState>;

  protected publicDefaults():  Partial<PublicState>;
  protected privateDefaults(): Partial<PrivateState>;
  protected clientStateDefaults(): Partial<ClientState>;

  constructor(params?: SpaceDefConstructorParams<PublicState, PrivateState>);
}

PlayerDef

abstract class PlayerDef<
  PublicState  extends Record<string, unknown> = {},
  PrivateState extends Record<string, unknown> = {},
  Static       extends Record<string, unknown> = {},
> {
  readonly static?: Static;
  publicState:  PublicState;
  privateState: PrivateState;

  protected publicDefaults():  Partial<PublicState>;
  protected privateDefaults(): Partial<PrivateState>;

  constructor(params?: PlayerDefConstructorParams<PublicState, PrivateState>);
}

GameStateConfigObject

type GameStateConfigObject = {
  startingPlayerStrategy: "Random" | "All";
  pieces: Record<string, PieceDef>;
  spaces: Record<string, SpaceDef>;
  player?: {
    spaces?: Record<string, SpaceDef>;
    publicState?: Record<string, unknown>;
    privateState?: Record<string, unknown>;
    def?: PlayerDef;
  };
  pieceDefs?: Record<string, PieceDef>;
  spaceDefs?: Record<string, SpaceDef>;
  metaData?: Record<string, unknown>;
  scoreLabels?: string[];
};

GameState

class GameState<MetaData = Record<string, unknown>> {
  state: BaseGameStateWithIds;
  defRegistry: DefRegistry;

  get metaData(): MetaData;
  set metaData(value: Record<string, unknown>);

  get scoreLabels(): string[] | undefined;
  set scoreLabels(value: string[] | undefined);

  get currentPlayer(): Player<MetaData>;
  get activePlayerIds(): number[];
  set activePlayerIds(value: number[]);

  // Pieces
  pieces(): Piece<MetaData>[];
  pieces(selector: (p: Piece<MetaData>) => unknown): Piece<MetaData>[];
  pieces<K extends keyof PieceKindRegistry>(kind: K): NarrowedPiece<MetaData, K>[];
  pieces<K extends keyof PieceKindRegistry>(kind: K, selector: (p: NarrowedPiece<MetaData, K>) => unknown): NarrowedPiece<MetaData, K>[];

  piece(selector: string | ((p: Piece<MetaData>) => unknown)): Piece<MetaData> | undefined;
  piece<K extends keyof PieceKindRegistry>(kind: K, selector: string | ((p: NarrowedPiece<MetaData, K>) => unknown)): NarrowedPiece<MetaData, K> | undefined;

  ensurePiece(selector: string | ((p: Piece<MetaData>) => unknown)): Piece<MetaData>;
  ensurePiece<K extends keyof PieceKindRegistry>(kind: K, selector: string | ((p: NarrowedPiece<MetaData, K>) => unknown)): NarrowedPiece<MetaData, K>;

  // Spaces
  spaces(): Space<MetaData>[];
  spaces(selector: (s: Space<MetaData>) => unknown): Space<MetaData>[];
  spaces<K extends keyof SpaceKindRegistry>(kind: K): NarrowedSpace<MetaData, K>[];
  spaces<K extends keyof SpaceKindRegistry>(kind: K, selector: string | ((s: NarrowedSpace<MetaData, K>) => unknown)): NarrowedSpace<MetaData, K>[];

  space(selector: string | ((s: Space<MetaData>) => unknown)): Space<MetaData> | undefined;
  space<K extends keyof SpaceKindRegistry>(kind: K, selector: string | ((s: NarrowedSpace<MetaData, K>) => unknown)): NarrowedSpace<MetaData, K> | undefined;

  ensureSpace(selector: string | ((s: Space<MetaData>) => unknown)): Space<MetaData>;
  ensureSpace<K extends keyof SpaceKindRegistry>(kind: K, selector: string | ((s: NarrowedSpace<MetaData, K>) => unknown)): NarrowedSpace<MetaData, K>;

  spaceOfKind<K extends keyof SpaceKindRegistry>(kind: K): NarrowedSpace<MetaData, K> | undefined;
  ensureSpaceOfKind<K extends keyof SpaceKindRegistry>(kind: K): NarrowedSpace<MetaData, K>;
  spacesOfKind<K extends keyof SpaceKindRegistry>(kind: K): NarrowedSpace<MetaData, K>[];

  // Players
  players(selector?: (p: Player<MetaData>) => unknown): Player<MetaData>[];
  player(selector?: number | ((p: Player<MetaData>) => unknown)): Player<MetaData> | undefined;
  ensurePlayer(selector?: number | ((p: Player<MetaData>) => unknown)): Player<MetaData>;

  // Mutation
  addPiece(pieceId: string, def: PieceDef): Piece<MetaData>;
  addSpace(spaceId: string, def: SpaceDef): Space<MetaData>;
}

Player

class Player<MetaData = Record<string, unknown>> {
  get baseData(): BasePlayerWithId;
  get playerId(): number;
  get color(): string;
  get username(): string;
  get avatarUrl(): string | null;

  get notification(): Notification | undefined;
  set notification(value: Notification | undefined);

  get publicState():  Record<string, unknown>;
  set publicState(value: Record<string, unknown>);

  get privateState(): Record<string, unknown> | null;
  set privateState(value: Record<string, unknown> | null);

  get def():    PlayerDef | undefined;
  get static(): Record<string, unknown>;
  get isPrivate(): boolean;

  // Scoped lookups (same signatures as GameState but limited to this player's spaces / pieces)
  piece(...), ensurePiece(...), pieces(...);
  space(...), ensureSpace(...), spaces(...);
  spaceOfKind(...), ensureSpaceOfKind(...), spacesOfKind(...);

  addSpace(spaceId: string, def: SpaceDef): Space<MetaData>;
}

type Notification = {
  title: string;
  intent: "success" | "warning" | "danger" | "info";
  description?: string;
  duration?: number;
};

Space

class Space<MetaData = Record<string, unknown>> {
  get baseData(): BaseSpaceWithId;
  get spaceId(): string;
  get kind(): keyof SpaceKindRegistry;
  get def(): SpaceDef;                                  // throws if unregistered
  get static(): Record<string, unknown>;

  get type(): "Stack" | "Horizontal" | "Vertical";
  set type(value: "Stack" | "Horizontal" | "Vertical");

  get x(): number;     set x(value: number);
  get y(): number;     set y(value: number);
  get width():  number;                                 // resolved from def
  get height(): number;

  get playerId(): number | undefined;
  set playerId(value: number | undefined);

  get isPrivate(): boolean | undefined;
  set isPrivate(value: boolean);

  get isHidden(): boolean; set isHidden(value: boolean);
  get isSelected(): boolean; set isSelected(value: boolean);
  get scrollable(): boolean; set scrollable(value: boolean);

  get publicState():  Record<string, unknown>;
  set publicState(value: Record<string, unknown>);
  get privateState(): Record<string, unknown> | null;
  set privateState(value: Record<string, unknown> | null);

  get player(): Player<MetaData> | undefined;
  ensurePlayer(): Player<MetaData>;

  // Pieces inside this space
  piece(...), ensurePiece(...), pieces(...);

  addPiece(pieceId: string, def: PieceDef): Piece<MetaData>;
}

Piece

class Piece<
  MetaData     = Record<string, unknown>,
  PublicState  = Record<string, unknown>,
  PrivateState = Record<string, unknown>,
> {
  get baseData(): BasePieceWithId;
  get pieceId(): string;
  get kind(): keyof PieceKindRegistry;
  get def(): PieceDef;                                  // throws if unregistered
  get static(): Record<string, unknown>;
  get isPrivate(): boolean;

  get publicState(): PublicState;
  set publicState(value: PublicState);
  get privateState(): PrivateState | null;
  set privateState(value: PrivateState | null);

  get isSelected(): boolean; set isSelected(value: boolean);

  get orientations(): PieceOrientation[];
  get usesRender(): boolean;
  get currentOrientationIndex(): number; set currentOrientationIndex(value: number);
  get currentOrientation(): PieceOrientation;          // undefined for render-mode pieces

  get width():  number;                                 // orientation override > def default
  get height(): number;

  get order(): number; set order(value: number);

  get spaceId(): string | undefined; set spaceId(value: string | undefined);
  get space(): Space<MetaData> | undefined;
  ensureSpace(): Space<MetaData>;
  ensureSpace<K extends keyof SpaceKindRegistry>(kind: K): NarrowedSpace<MetaData, K>;

  get player(): Player<MetaData> | undefined;
}

BWSS node types

type PieceBwssNode = PieceBoxNode | ImageNode | SvgNode;
type SpaceBwssNode = SpaceBoxNode | ImageNode | SvgNode | PiecesNode;

interface PieceBoxNode { type: "Box"; style?: BwssStyle; children?: (PieceBwssNode | string)[] }
interface SpaceBoxNode { type: "Box"; style?: BwssStyle; children?: (SpaceBwssNode | string)[] }
interface ImageNode    { type: "Image"; src: GameImageKey; alt?: string; style?: BwssStyle }
interface PiecesNode   { type: "Pieces"; style?: BwssStyle }
interface SvgNode      {
  type: "Svg";
  viewBox: { width: number; height: number };
  paths: SvgPathChild[];
  style?: BwssStyle;
}
interface SvgPathChild {
  d: string;
  fill?: string; stroke?: string;
  strokeWidth?: number | string;
  fillRule?: "nonzero" | "evenodd";
  strokeLinecap?: "butt" | "round" | "square";
  strokeLinejoin?: "miter" | "round" | "bevel";
  opacity?: number | string;
}

Render fns

type RenderContext =
  | { mode: "board"; isSelectable: boolean }
  | { mode: "tooltip" };

type PieceTooltipPlacement = "top" | "bottom" | "left" | "right";

type PieceRenderResult =
  | PieceBwssNode
  | { node: PieceBwssNode; width?: number; height?: number; placement?: PieceTooltipPlacement }
  | null;

type PieceRenderFn<
  PublicState  = unknown,
  PrivateState = unknown,
  MetaData     = Record<string, unknown>,
  ClientState  extends Record<string, unknown> = {},
  GlobalClientState extends Record<string, unknown> = {},
> = (
  piece: Piece<MetaData, PublicState, PrivateState>,
  gameState: GameState<MetaData>,
  ctx: RenderContext,
  clientState: PieceClientStateReadView<ClientState, GlobalClientState>,
) => PieceRenderResult;

type SpaceRenderContext = { mode: "board"; isSelectable: boolean };

type SpaceRenderResult =
  | SpaceBwssNode
  | { node: SpaceBwssNode; width?: number; height?: number }
  | null;

type SpaceRenderFn<
  MetaData     = Record<string, unknown>,
  ClientState  extends Record<string, unknown> = {},
  GlobalClientState extends Record<string, unknown> = {},
> = (
  space: Space<MetaData>,
  gameState: GameState<MetaData>,
  ctx: SpaceRenderContext,
  clientState: SpaceClientStateReadView<ClientState, GlobalClientState>,
) => SpaceRenderResult;

Client state views

type BwMouseEvent = {
  clientX: number; clientY: number;
  button: number; buttons: number;
  shiftKey: boolean; ctrlKey: boolean; metaKey: boolean; altKey: boolean;
};

type PieceClientStateView<C extends Record<string, unknown> = {}, G extends Record<string, unknown> = {}> = C & { global: G };
type SpaceClientStateView<C extends Record<string, unknown> = {}, G extends Record<string, unknown> = {}> = C & { global: G };

type PieceClientStateReadView<C extends Record<string, unknown> = {}, G extends Record<string, unknown> = {}> = Readonly<C> & { readonly global: Readonly<G> };
type SpaceClientStateReadView<C extends Record<string, unknown> = {}, G extends Record<string, unknown> = {}> = Readonly<C> & { readonly global: Readonly<G> };

type GlobalClientStateReadView<G extends Record<string, unknown> = {}> = Readonly<G>;