Documentation

Game logic

v2 · Stable

Players and turns

Top-level

Property Type Mutable
currentPlayer Player<MetaData> no : server-set, the viewer this call runs for
activePlayerIds number[] yes : this is whose turn it is
metaData MetaData yes : free-form game-wide scratch state
scoreLabels string[] | undefined yes

scoreLabels names each entry of the points arrays your getPlayerScores returns, in order. The labels head the columns of the final scores table that players see at the end of a match and in their Recently played history, and of the score table on the studio Matches page. Studio Stats also uses them for "Points by category", which compares how many points winners and everyone else score in each label. Points are matched to labels by name, so keep a label's name stable across versions to compare it over time. An entry with no label is shown as "Score 1", "Score 2", and so on.

The end-of-match panel shows results and lets players start another match or return to games.
The end-of-match panel shows results and lets players start another match or return to games. Example data from the local development UI. Select the image to enlarge.

Scoped lookups

Player has the same piece / pieces / space / spaces / *OfKind methods, scoped to spaces that player owns and the pieces inside them:

const me = state.currentPlayer;
me.pieces("card", (c) => c.isSelected);
me.ensureSpaceOfKind("hand");

Per-player spaces get synthesized ids like ${playerId}/space/${configKey}, so player.space("hand") (an id match) usually misses : use player.spaceOfKind("hand").

Space scopes the same way over its contents:

space.pieces("card");
space.ensurePiece("p1");

And you can walk upward:

piece.space;              // Space | undefined
piece.ensureSpace();      // throws if unattached
piece.player;             // Player | undefined
space.player;             // Player | undefined

gameConfig and the config object

gameConfig({ numPlayers }) runs once and returns the table's starting layout. It must be deterministic : same params, same output : because reconnects and replays re-run it. Randomized setup (shuffling, dealing) belongs in preGameInitialization.

export const gameConfig: GameStateConfigFn = () => {
  const config: GameStateConfigObject = {
    startingPlayerStrategy: "Random",
    scoreLabels: ["Points"],
    spaces: {
      "0": new GridCell({ x: 0, y: 0 }),
      "1": new GridCell({ x: 135, y: 0 }),
    },
    pieces: {},
    pieceDefs: {
      "x-token": new XToken({ order: 1 }),
    },
  };
  return config;
};
Field Holds
spaces Starting spaces, keyed by spaceId.
pieces Starting pieces, keyed by pieceId. May be empty if you create pieces during play.
pieceDefs Every piece kind the game can ever create, keyed by kind. Registers the class so state.addPiece can resolve it at runtime.
scoreLabels Names the entries in each player's publicPoints array, in order.
startingPlayerStrategy How the first active player is chosen, e.g. "Random".
player Optional per-player setup : spaces each player owns (a hand, a board side).

A kind you only create at runtime must still appear in pieceDefs. state.addPiece throws if the kind isn't registered.

Need a hand with this?

Join our Discord