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.
Four buckets for game data — each lives in a different place and has different visibility rules.
class Card extends PieceDef<{ tapped: boolean }, { faceDown: boolean }, { cost: number }> {
readonly kind = "card";
readonly width = 100;
readonly height = 140;
readonly orientations = [{ imageSrc: "CardFront" }];
readonly static = { cost: 3 }; // class constant
protected publicDefaults() { return { tapped: false }; }
protected privateDefaults() { return { faceDown: true }; }
}
const card = new Card({ order: 1 });
card.publicState.tapped; // false — visible to all viewers
card.privateState.faceDown; // true on owner's view; null on others'
card.def.static.cost; // 3 — same for every Card instance
| Bucket | Lives on | Mutable in | Sent on the wire? | Per-viewer scrubbing? |
|---|---|---|---|---|
static |
The Def class | never (declared at class-definition time) | no | n/a |
publicState |
The instance (Piece / Space / Player) |
applyActions, preGameInitialization |
yes — to every viewer | no |
privateState |
The instance | applyActions, preGameInitialization |
yes — scrubbed per viewer | yes (see below) |
metaData |
GameState |
applyActions, preGameInitialization |
yes — to every viewer | no |
static is read via piece.def.static.foo / space.def.static.foo / piece.static.foo. Use it for class-level constants (card cost, ability text) that an instance never overrides.
publicState and privateState are merged from publicDefaults() / privateDefaults() and the constructor's positional / named params (see pieces and spaces).
metaData is per-game ambient data. Type it via GameState<MetaData>:
type MyMeta = { phase: "draw" | "play" | "combat"; turn: number };
export const applyActions: ApplyActionsFn<MyMeta> = (state, action) => {
state.metaData = { ...state.metaData, phase: "combat" };
};
privateStateBefore each viewer receives a state update, the framework runs scrubStateForViewer:
Player.privateState → null for every viewer except that player.Space.privateState → null for every viewer except space.playerId's owner.Space with isPrivate: true has its privateState → null for every viewer except the space's owner.Read sites always see PrivateState | null. Handle the null branch:
const render: PieceRenderFn<{}, { value: number }> = (piece) => {
if (piece.privateState === null) {
return { type: "Image", src: "CardBack" }; // unauthorized viewer
}
return {
type: "Box",
children: [String(piece.privateState.value)],
};
};
piece.isPrivate / space.isPrivate / player.isPrivate returns true only when the bucket has been scrubbed (i.e. data.privateState === null).
static.publicState.privateState + put the piece in an isPrivate space owned by the player. Or store it on the Player directly.metaData.A player's hand:
class Hand extends SpaceDef {
readonly kind = "hand";
readonly type = "Stack" as const;
readonly width = 600;
readonly height = 160;
readonly x = 0;
readonly y = 600;
readonly isPrivate = true; // pieces' privateState scrubbed for others
}
// In gameConfig:
player: { spaces: { hand: new Hand() } };
Per-player private bookkeeping (no piece needed):
gameConfig: () => ({
player: {
privateState: { secretGoal: null as string | null },
},
// ...
});
// In applyActions:
state.currentPlayer.privateState = {
...state.currentPlayer.privateState,
secretGoal: "domination",
};
privateState is null for unauthorized viewers, not undefined or an empty object. Branch on === null.isPrivate space has its privateState sent to everyone — isPrivate is the privacy switch, not the existence of a privateState field.Player.privateState = null directly hides it from everyone including the owner — usually a bug. Mutate the contents instead.static is on the Def, not the instance. piece.static is a shortcut for piece.def.static. Reassigning piece.static = ... doesn't work; it's a getter.metaData is public. Don't stash secrets there — use Player.privateState or piece privateState in an isPrivate space.undefined values you need preserved, no circular refs). Strings, numbers, booleans, plain arrays/objects, null.