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