Authoring a Variant

A variant is one markdown file. If the rules you want are already expressible, you will not write any JavaScript.

Setting up

The engine is inert on its own. Every game definition, every board and every starting position lives in a second repository, moddable-rules. Clone both as siblings:

git clone https://github.com/Moddable-Games/moddable-engine.git
git clone https://github.com/Moddable-Games/moddable-rules.git
cd moddable-engine && npm ci

The browser resolves rules at ../../moddable-rules/ relative to the engine, so the two checkouts must sit under a common parent. Node tooling and tests read the MODDABLE_RULES_DIR environment variable instead:

MODDABLE_RULES_DIR=../moddable-rules/games npm test

Without that variable the suite fails wholesale, because the corpus it is testing against is not there. That is expected, not a broken checkout.

Where the file goes

Variants are discovered by path. A chess variant called Zebra Chess lives at:

moddable-rules/games/chess/content/variants/zebra-chess.md

The filename is the slug. It is how the variant is addressed in URLs (?family=chess&variant=zebra-chess), in the SDK, and in every API response. Use lowercase and hyphens.

The six families that can currently be extended this way are chess, shogi, go, draughts, xiangqi and reversi. Adding a seventh family is not yet a markdown-only operation.

A minimal variant

This is a complete, playable file. Nothing has been elided.

---
title: Zebra Chess
slug: zebra-chess
parent: chess
playable: true
engine:
  topology:
    type: grid
    rows: 8
    cols: 8
  players: [white, black]
  setup: "rzbqkbzr/pppppppp/8/8/8/8/PPPPPPPP/RZBQKBZR"
  pieces:
    set: wikimedia-standard
  vocabulary:
    zebra:
      symbols: { 0: Z, 1: z }
  plugins:
    chess:
      castling: false
      pieces:
        zebra:
          type: leaper
          offsets: [[2,3],[3,2],[-2,3],[-3,2],[2,-3],[3,-2],[-2,-3],[-3,-2]]
---

## Zebra Chess

Knights are replaced by zebras, which leap (2,3) instead of (1,2).

Four frontmatter fields outside engine: are load-bearing:

FieldRequiredWhat it does
titleyesDisplay name in the variant picker
slugyesMust match the filename. Addressed everywhere by this.
parentyesThe family. Schema validation rejects the file without it.
playableyestrue puts it on the play page. Omit or set false and the variant renders but cannot be played.

The engine block

Everything the engine reads lives under engine:. The keys divide into two groups, and the distinction matters more than it looks.

Structural keys describe the board and its presentation. They are consumed by the topology and render layers: topology, players, surface, render, pieces, components, meta, plugins.

Rule keys describe how the game is played. They live under engine.plugins.<family> and are passed to that family's plugin as its config. Each family page documents its own set: see Chess, Draughts, Xiangqi and Reversi.

pieces means two different things. engine.pieces selects the artwork: { set: wikimedia-standard }. engine.plugins.<family>.pieces defines movement. They are unrelated, and putting a movement definition in the first one silently does nothing.

Defining a new piece

A piece is a movement primitive plus its parameters, declared under engine.plugins.<family>.pieces. The available primitives:

typeParametersBehaviour
riderdirs, maxSteps, minStepsSlides until blocked. A rook is dirs: orthogonal; a king is dirs: all, maxSteps: 1.
leaperoffsetsJumps to fixed offsets, ignoring anything between.
hopperdirsMust jump exactly one piece to move or capture. The xiangqi cannon.
locustdirsCaptures the piece it jumps over and lands beyond it.
bentfirst, firstSteps, minSecondLegMoves in one direction then turns. Bent riders and crooked bishops.
divergentmove, captureDifferent movement and capture patterns, each itself a primitive.
composepartsThe union of several primitives. An amazon is a queen composed with a knight.

dirs accepts orthogonal, diagonal or all. offsets accepts either an explicit [[row, col], ...] list or one of the named presets:

knight  camel  elephant  dabbaba  zebra  king  hex-knight

Offsets are [row, col], row first, with row increasing down the board as printed in the setup string.

Symbols and vocabulary

The setup string is FEN-like: one rank per /, digits for runs of empty squares, a character per piece. vocabulary maps piece types to the characters that represent them, keyed by owner index:

  vocabulary:
    zebra:
      symbols: { 0: Z, 1: z }

Owner 0 is the first entry in players. For boards with more than two armies, declare all of them:

  vocabulary:
    rook:
      symbols: { 0: rR, 1: yR, 2: gR, 3: bR }

Symbols may be more than one character. When they are, the setup string is comma-separated per rank rather than character-per-square. Artwork is resolved from the symbol by convention (Z looks for wZ in the chosen set), so a piece with no matching file renders as an empty square.

Precedence

A non-structural key written at engine.<key> is copied into the plugin config, and it overwrites the same key written at engine.plugins.<family>.<key>. The less specific location wins. This is the opposite of what most authors expect, so prefer to write rule keys in the plugin block only, and never in both places.

Making it appear

The play page reads a generated manifest. After adding or changing a variant:

MODDABLE_RULES_DIR=../moddable-rules/games node scripts/gen-playability-manifest.mjs

Commit the resulting play/playability-manifest.json alongside your variant. CI checks that it is fresh.

Common mistakes

SymptomCause
Unknown variant "x" for family "y"The file is not at games/<family>/content/variants/<slug>.md, or MODDABLE_RULES_DIR is not set.
Unmapped FEN symbol "Z"A character in setup has no vocabulary entry.
Vocabulary declares "x" but no matching entry in piecesA symbol was declared without a movement definition under plugins.<family>.pieces.
FEN has N ranks but topology has M rowsThe setup string does not match the declared board.
[family] Unknown config keys: xA rule key is misspelled. The plugin ignored it and used its default.
Board renders, no piecespieces.set names a set that does not exist, or the symbols have no matching artwork in it.
Variant plays as the family defaultRule keys are under engine: rather than engine.plugins.<family>:, or extends names a parent that does not resolve.

The existing corpus is the best reference. Every variant on the play page is one of these files, and reading a nearby one is usually faster than reading this page twice.