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