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.
No-prose dump of every public signature. See the topic-specific sections for behavior and gotchas.
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[];
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 };
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;
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
type Score = { publicPoints: number[]; privatePoints: number[] };
type Scores = Record<string /* playerId */, Score>;
PieceDefabstract 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;
};
SpaceDefabstract 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>);
}
PlayerDefabstract 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>);
}
GameStateConfigObjecttype 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[];
};
GameStateclass 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>;
}
Playerclass 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;
};
Spaceclass 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>;
}
Piececlass 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;
}
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;
}
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;
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>;