Docs / API Reference

API Reference

All public methods on the MCE global namespace.

Game Lifecycle

MCE.createGame(variant)

Creates a new game object with the board set to the starting position for the given variant.

const game = MCE.createGame('standard');
const capablanca = MCE.createGame('capablanca'); // 10×8 board

Returns a game object with properties: board, rows, cols, turn, castling, enPassant, halfmove, fullmove, history, variant.

MCE.loadFEN(game, fen)

Loads a FEN string into an existing game object. Supports multi-digit empty square counts for boards wider than 9.

MCE.toFEN(game)

Exports the current game state as a FEN string.

MCE.positionKey(game)

Returns a string key representing the current position for repetition detection and transposition table lookups. Excludes halfmove and fullmove counters.

const key = MCE.positionKey(game);
// e.g. "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3"

When ownershipMode === 'pieceData', automatically includes pieceData keys and owners in the hash (prevents TT collisions for custom-piece games).

Variants can override with a custom positionKey hook:

MCE.registerVariant('myGame', {
  ownershipMode: 'pieceData',
  positionKey: function(g) {
    var key = '';
    for (var i = 0; i < g.rows * g.cols; i++) {
      var pd = g.pieceData[i];
      key += pd ? pd.key + pd.owner[0] : '.';
    }
    return key + ' ' + g.turn;
  }
});

Move Generation

MCE.legalMoves(game)

Returns an array of all legal moves for the current side. Each move is an object:

{ from: 52, to: 36, flag: 'double', promo: null }

Flags: null (quiet), 'capture', 'double' (pawn double push), 'ep' (en passant), 'promo', 'castle-k', 'castle-q'.

MCE.pseudoLegalMoves(game)

Returns moves without checking for pins or self-check. Faster but may include illegal moves.

MCE.variantLegalMoves(game)

Returns legal moves with variant-specific filtering (e.g., forced captures in Antichess, no-check filter in Racing Kings).

Move Execution

MCE.makeMove(game, move)

Executes a move on the game. Mutates the game state. Returns an undo object for unmakeMove.

const undo = MCE.makeMove(game, move);
// game.turn has now flipped

MCE.unmakeMove(game, undo)

Reverses a move. Restores the game to its previous state.

Status Detection

MCE.getStatus(game)

Returns: 'active', 'check', 'checkmate', 'stalemate', 'draw-repetition', 'draw-material', or 'draw-50'.

Draw conditions:

Game object properties

In addition to the properties returned by createGame, the game object tracks:

MCE.getVariantStatus(game)

Calls the active variant's winCondition hook. Returns null if no variant win, or a status string like 'koth-w', 'race-b', 'antichess-w'.

MCE.inCheck(game, side)

Returns true if the given side's king is in check. side is 'w' or 'b'.

Coordinates

MCE.sq(row, col, game)

Converts row/col to a board index. Pass game for non-8×8 boards.

MCE.rc(index, game)

Converts a board index to [row, col].

MCE.onBoard(row, col, game)

Returns true if the coordinates are within the board boundaries. Applies wrap-around normalisation first if game.wrapFiles or game.wrapRanks is set.

MCE.wrapCoords(row, col, game)

Normalises coordinates according to the game's wrap settings. If game.wrapFiles is true, columns wrap modulo game.cols. If game.wrapRanks is true, rows wrap modulo game.rows. Returns [row, col].

var [r, c] = MCE.wrapCoords(row, col, game);
// For cylinder chess (wrapFiles): col -1 becomes col 7
// For toroidal chess (both): row -1 wraps to row 7 too

MCE.sqToAlgebraic(index, game)

Converts an index to algebraic notation (e.g., 'e4').

MCE.algebraicToSq(str, game)

Converts algebraic notation to an index.

Rendering

MCE.renderBoard(container, game, opts)

Renders an SVG chess board into a DOM element.

MCE.renderBoard(el, game, {
  size: 480,             // width in px (height auto from rows/cols)
  flipped: false,        // flip board for black's perspective
  selected: null,        // highlighted square index
  legalMoves: [],        // array of moves to show indicators for
  onSquareClick: fn,     // callback(squareIndex)
  fogMask: null,         // Set of visible square indices (Fog of War)
  duckSq: null,          // duck position index (Duck Chess)
  lastMove: null,        // { from, to } — highlights last move squares
  animate: false,        // enable piece movement animation
  animStyle: 'slide',    // 'slide' | 'arc' | 'bounce' | 'warp'
  animDuration: 200,     // animation duration in ms
  animEasing: 'ease-out',// CSS easing (slide mode only)
  animArcHeight: 0.25,   // arc peak as fraction of distance (arc mode)
  animCaptureBurst: false,// show particle burst on captures
  // Extension points (v0.7.0):
  tilePainter: null,     // custom tile rendering function
  pieceProvider: null,   // custom piece rendering function
  afterRender: null,     // post-render callback for overlays
  surroundRenderer: null,// render around the board (before SVG creation)
  effectOverlay: null,   // per-effect overlay renderer
  legalMoveRenderer: null,// custom legal move indicator renderer
  suppressHighlights: false, // skip built-in selection/lastMove highlights
  excludePiece: -1,      // single square index to hide piece (animation)
  excludePieces: null,   // array of square indices to hide (multi-animation)
});

Animation styles

animStyle: 'slide' — Piece slides smoothly from origin to destination using a CSS transform transition. Clean and minimal.

animStyle: 'arc' — Piece lifts in a parabolic arc, scaling up mid-flight then landing on the target square. Suitable for dungeon/fantasy themes.

animStyle: 'bounce' — Piece accelerates toward the target, overshoots slightly, then settles back. Gives a tactile, snappy feel.

animStyle: 'warp' — Piece shrinks and fades out at the origin, then grows and fades in at the destination. A teleportation effect that pairs well with cosmic/neon themes.

animCaptureBurst: true — On captures, triggers an expanding ring + spark particles effect at the capture square after the move animation completes.

MCE.captureBurst(svg, cx, cy, tileSize)

Triggers a capture burst animation at the given SVG coordinates. Can be called independently for custom capture effects.

Renderer Extension Points

Consumer games can extend the board renderer without replacing it. All callbacks are optional — returning null/undefined falls through to default rendering.

tilePainter(svg, sqIdx, row, col, tileSize, isLight, game)

Called per-square instead of the default flat colour fill. Return an SVG element to replace the default tile, or null to use default.

tilePainter: function(svg, sq, r, c, tileSize, isLight, game) {
  var terrain = game.terrain && game.terrain[sq];
  if (terrain === 'water') {
    var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect.setAttribute('width', tileSize);
    rect.setAttribute('height', tileSize);
    rect.setAttribute('fill', '#2196f3');
    return rect;
  }
  return null; // use default for non-water tiles
}

pieceProvider(game, sqIdx, tileSize)

Returns a custom SVG element for the piece at a square. Useful for non-standard sprites (faction colours, custom symbols). Return null to use default chess pieces.

pieceProvider: function(game, sq, tileSize) {
  var pd = game.pieceData && game.pieceData[sq];
  if (!pd) return null;
  var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  var use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
  use.setAttribute('href', '#sprite-' + pd.key);
  use.setAttribute('width', tileSize * 0.9);
  use.setAttribute('height', tileSize * 0.9);
  g.appendChild(use);
  return g;
}

Custom pieces work with the animation system (slide and arc). The renderer sets x, y, and transform attributes on the returned element.

afterRender(svg, game, tileSize, opts)

Called after all squares, pieces, and overlays are drawn. Use for custom overlays (selection rings, terrain markers, hex indicators).

afterRender: function(svg, game, tileSize, opts) {
  // Draw a red border around poisoned squares
  game.effects.forEach(function(e) {
    if (e.type !== 'poison') return;
    var [r, c] = MCE.rc(e.sq, game);
    var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect.setAttribute('x', c * tileSize + 2);
    rect.setAttribute('y', r * tileSize + 2);
    rect.setAttribute('width', tileSize - 4);
    rect.setAttribute('height', tileSize - 4);
    rect.setAttribute('fill', 'none');
    rect.setAttribute('stroke', 'rgba(255,0,0,0.6)');
    rect.setAttribute('stroke-width', '2');
    svg.appendChild(rect);
  });
}

Board Themes

MCE.setTheme(name)

Sets the active board colour theme. Affects all subsequent renderBoard calls.

MCE.setTheme('cosmic');  // deep navy squares
MCE.setTheme('wood');    // warm brown tones
MCE.setTheme('marble');  // grey/white stone
MCE.setTheme('neon');    // dark bg, bright accents
MCE.setTheme('minimal'); // near-white, subtle contrast
MCE.setTheme('classic'); // default tan/brown

MCE.getTheme()

Returns the current theme object with colour values:

const t = MCE.getTheme();
// { light, dark, highlight, lastMove, dot, ring, border, label }

MCE.THEMES

Object containing all available theme definitions. Keys: classic, cosmic, wood, marble, neon, minimal, transparent.

ThemeLight sqDark sqStyle
classic#f0d9b5#b58863Standard tan/brown (default)
cosmic#2d3760#141c37Deep navy, inspired by the OG card
wood#deb887#8b5e3cWarm brown wood tones
marble#f2f0ec#b8b5afGrey/white stone
neon#1a1a2e#0f0f1aDark with bright move indicators
minimal#fafafa#e8e8e8Near-white, very subtle contrast
transparentrgba(255,255,255,0.12)rgba(255,255,255,0.22)See-through for embedding on any background

Custom themes

Add your own theme by extending MCE.THEMES before calling setTheme:

MCE.THEMES.forest = {
  light: '#4a7c59', dark: '#2d5a3a',
  highlight: 'rgba(255,215,0,0.35)',
  lastMove: 'rgba(144,238,144,0.25)',
  dot: 'rgba(255,255,255,0.2)',
  ring: 'rgba(255,255,255,0.3)',
  border: '#1a3d2a',
  label: 'Forest'
};
MCE.setTheme('forest');

MCE.setPieceStyle(name)

Sets the active piece colour style. Recolours pieces on the next renderBoard call by cloning SVG symbol content and replacing fill/stroke values.

MCE.setPieceStyle('gold');      // white & amber/gold
MCE.setPieceStyle('charcoal');  // cream & dark grey
MCE.setPieceStyle('burgundy');  // white & deep red
MCE.setPieceStyle('navy');      // white & dark blue
MCE.setPieceStyle('auto');      // default (adapts on dark themes)

MCE.getPieceStyle()

Returns the current piece style key string.

MCE.PIECE_STYLES

Object containing all available piece style definitions. Each has a label, light (fill/stroke for white pieces), and dark (fill/stroke/detail for black pieces).

KeyLabelDark fillDescription
autoAuto#000 (or tinted on dark boards)Default. Adjusts black pieces automatically on dark themes for visibility
goldWhite & Gold#b58863Warm amber/gold pieces (chess.com style)
charcoalCream & Charcoal#3a3a3aOff-white vs dark grey with light detail lines
burgundyWhite & Burgundy#6b1a2aClassic red set style
navyWhite & Navy#1a3a5cDark blue with light blue details

MCE.DARK_THEMES

Array of theme keys considered "dark" for auto piece style adjustment: ['cosmic', 'neon', 'transparent']. When piece style is auto and the active board theme is in this list, black pieces are tinted to a visible blue-grey.

Custom piece styles

Add your own piece style by extending MCE.PIECE_STYLES:

MCE.PIECE_STYLES.forest = {
  label: 'White & Forest',
  light: { fill: '#fff', stroke: '#000' },
  dark: { fill: '#2d5a3a', stroke: '#1a3d2a', detail: '#a8e0b4' }
};
MCE.setPieceStyle('forest');

The detail colour is used for inner decorative lines on black pieces (cross on bishops, lines on rooks/kings). If omitted, falls back to the stroke colour.

Piece Set Resolver

The PieceSetResolver manages loading and switching between piece SVG sets. It resolves which SVG symbol to use for each piece character based on a priority chain: per-piece overrides, primary set, then fallback set.

import PieceSetResolver from './piece-set-resolver.js';

Resolution order

When the renderer needs a piece symbol, the resolver checks in order:

  1. overrides[char] — per-piece set override (e.g. use a specific set for knights)
  2. Primary set — the configured set value
  3. Fallback set — the configured fallback value (covers missing pieces)

PieceSetResolver.loadAllManifests()

Fetches index.json and loads all piece set manifests. Call once at startup to make all sets available.

await PieceSetResolver.loadAllManifests();
const sets = PieceSetResolver.getAvailableSets();
console.log(sets.length + ' piece sets loaded');

PieceSetResolver.loadForVariant(variantKey, config?)

Loads only the piece SVGs needed for a specific variant. Returns the count of pieces loaded. More efficient than loadAllManifests() when you know which variant will be played.

const count = await PieceSetResolver.loadForVariant('capablanca');
console.log(count + ' piece symbols injected');

PieceSetResolver.setConfig(config)

Merges the given config into the active resolver configuration. Config shape:

PieceSetResolver.setConfig({
  set: 'staunty',           // primary piece set id
  fallback: 'classic',      // fallback for missing pieces
  overrides: { n: 'alpha' } // use 'alpha' set for knights
});

PieceSetResolver.getConfig()

Returns a copy of the active resolver configuration.

const cfg = PieceSetResolver.getConfig();
// { set: 'staunty', fallback: 'classic', overrides: { n: 'alpha' } }

PieceSetResolver.getAvailableSets()

Returns an array of all loaded piece set descriptors.

const sets = PieceSetResolver.getAvailableSets();
// [{ id: 'classic', name: 'Classic', recolorable: true }, ...]

PieceSetResolver.getSetsForVariant(variantKey)

Returns only the sets that cover all pieces needed by a given variant. Useful for building a set picker that only shows compatible options.

const compatible = PieceSetResolver.getSetsForVariant('orda');
// Only sets that include yurt, lancer, etc.

PieceSetResolver.isLoaded(char)

Returns true if the SVG symbol for the given piece character has already been injected into the document.

if (!PieceSetResolver.isLoaded('y')) {
  await PieceSetResolver.loadForVariant('orda');
}

Manifest format

Piece set manifests come in two forms:

Simple (single-source)

All pieces come from a single directory. The path field specifies the base path, and piece values are filename strings. Keys use color prefix + uppercase char format (e.g. wK, bQ).

{
  "id": "classic",
  "name": "Classic",
  "recolorable": true,
  "path": "assets/pieces/classic/",
  "pieces": { "wK": "wK.svg", "bK": "bK.svg", "wQ": "wQ.svg" }
}

Composite (multi-source)

Pieces are drawn from multiple source directories. Set composite: true and provide a sources map. Piece values are objects with source and file properties.

{
  "id": "mixed-fantasy",
  "name": "Mixed Fantasy",
  "composite": true,
  "sources": { "base": "assets/pieces/classic/", "fairy": "assets/pieces/fairy/" },
  "pieces": { "wK": { "source": "base", "file": "wK.svg" }, "wY": { "source": "fairy", "file": "wY.svg" } }
}

Piece Registry

All 18 piece types are registered via MCE.registerPiece() in js/pieces/. Every piece provides its own move generation and attack detection. The core engine has no hardcoded knowledge of any piece type.

import { PIECES, PIECE_NAMES, getPieceInfo } from './pieces/index.js';

MCE.getPieceRegistry()

Returns the full piece handler registry: move generators and attack detectors for all 18 registered piece types.

const registry = MCE.getPieceRegistry();
console.log(Object.keys(registry).sort());
// ['a','b','c','e','f','g','h','i','k','l','m','n','p','q','r','s','w','y']

PIECES

Dictionary of all 18 piece types with full metadata. Built from the registry at import time.

import { PIECES } from './pieces/index.js';
const queen = PIECES['q'];
// { char: 'q', name: 'Queen', category: 'standard', movement: '...', ... }

PIECE_NAMES

Simple map from piece character to human-readable display name.

import { PIECE_NAMES } from './pieces/index.js';
PIECE_NAMES['a']; // 'Archbishop'
PIECE_NAMES['c']; // 'Chancellor'
PIECE_NAMES['m']; // 'Maharaja'

getPieceInfo(char)

Returns a structured info object for a piece character.

import { getPieceInfo } from './pieces/index.js';
const info = getPieceInfo('a');
// { char: 'a', name: 'Archbishop', category: 'fairy',
//   movement: 'knight + bishop', capture: 'knight + bishop',
//   variants: ['capablanca', 'gothic', ...] }

Movement Primitives

Pieces are composed from reusable movement primitives defined in js/movement.js. This enables config-driven piece creation without writing custom genMoves/attacks functions.

import { Leaper, Rider, compose, divergent, OFFSETS } from './movement.js';

MCE.Leaper(offsets)

Creates a jump primitive. The piece can reach any square at the given offsets from its current position.

const knight = Leaper('knight');    // L-shaped jumps
const fers = Leaper('bishop');      // one diagonal step
const elephant = Leaper('elephant'); // (2,2) diagonal leap
const camel = Leaper('camel');      // (3,1) extended knight

// Custom offsets:
const dabbaba = Leaper([[0,2],[0,-2],[2,0],[-2,0]]);

MCE.Rider(dirs)

Creates a slide primitive. The piece moves any number of squares along the given directions until blocked.

const bishop = Rider('bishop');  // diagonal slides
const rook = Rider('rook');      // orthogonal slides
const queen = Rider('queen');    // all 8 directions

MCE.compose(...primitives)

Merges multiple movement types into one piece. The piece can use any of the provided primitives.

const archbishop = compose(Rider('bishop'), Leaper('knight'));
const chancellor = compose(Rider('rook'), Leaper('knight'));
const maharaja = compose(Rider('queen'), Leaper('knight'));

MCE.divergent(movePrimitive, capturePrimitive)

Creates a piece that moves differently from how it captures (like Orda pieces).

const yurt = divergent(Leaper('bishop'), Leaper('rook'));
// Moves diagonally, captures orthogonally

const lancer = divergent(Rider('rook'), Leaper('knight'));
// Moves like a rook, captures like a knight

MCE.OFFSETS

Named offset sets available as string shortcuts:

NameOffsetsDescription
'knight'(2,1) in all orientationsStandard knight jumps
'king'All 8 adjacent squaresOne step any direction
'bishop'4 diagonal stepsOne diagonal step (Fers)
'rook'4 orthogonal stepsOne orthogonal step (Wazir)
'queen'All 8 directionsOne step any direction
'elephant'(2,2) in all orientationsAlfil/Elephant leap
'camel'(3,1) in all orientationsExtended knight
'dabbaba'(2,0) in all orientationsOrthogonal 2-leap
'zebra'(3,2) in all orientationsExtended knight variant

Registering a piece from primitives

import { compose, Rider, Leaper } from './movement.js';

const movement = compose(Rider('bishop'), Leaper('knight'));

MCE.registerPiece('a', {
  name: 'Archbishop',
  category: 'compound',
  movement: 'Slides diagonally + L-shaped jump',
  primitives: [{ type: 'rider', dirs: 'bishop' }, { type: 'leaper', offsets: 'knight' }],
  genMoves: movement.genMoves,
  attacks: movement.attacks,
});

AI System

MCE.aiPickMove(game, depth, opts)

Returns the best move found by the AI. Uses iterative deepening with alpha-beta pruning, quiescence search, transposition table, and move ordering.

// Use difficulty preset (recommended):
var move = MCE.aiPickMove(game, null, { difficulty: 'hard' });

// Or specify time budget directly:
var move = MCE.aiPickMove(game, null, { timeMs: 2000 });

MCE.AI_DIFFICULTIES

Available difficulty presets:

KeyTimeBehaviour
beginner200ms30% random blunders
easy400ms15% blunders, picks from top 5 moves
medium800msPicks from top 3 moves
hard1.5sAlways picks best move
expert3sAlways picks best move, deepest search

Opening books

The AI probes for book moves before searching. If the current position has a book entry, it plays instantly. Books are stored as the openingBook property in variant configs:

MCE.registerVariant('myVariant', {
  openingBook: {
    "rnbqkbnr/.../RNBQKBNR w KQkq -": ["e2e4", "d2d4", "g1f3"],
    "...position key...": ["move1", "move2"]
  },
  // ...
});

Keys are position keys (first 4 FEN fields: board, turn, castling, en passant). Values are arrays of moves in coordinate notation (e2e4, g1f3, e7e8q for promotion). Multiple moves per position enables variety — one is chosen randomly.

26 variants ship with built-in opening books. The AI checks variantConfig.openingBook first, then falls back to data/openings.json (legacy).

MCE.loadOpeningBook(basePath)

Loads the legacy JSON book from basePath + 'data/openings.json'. Called automatically on page load. This is a fallback — prefer adding openingBook directly to your variant config.

Variant evaluators

Variants can provide custom position evaluation via the evaluate hook:

MCE.registerVariant('myVariant', {
  evaluate: function(g, defaultEval) {
    var material = defaultEval(g);
    // Add custom scoring logic
    return material + customBonus;
  }
});

The AI automatically uses the variant's evaluator if defined. The defaultEval parameter provides material + piece-square table scoring as a baseline.

Effects System

Temporary status effects on board squares. Effects auto-decrement each turn and are fully undo-aware for AI search.

MCE.addEffect(game, undo, effect)

Adds an effect to the game. Call from afterMove or beforeMove hooks.

MCE.addEffect(g, undo, {
  sq: targetSquare,
  type: 'poison',
  duration: 3,    // turns until expiry (null = permanent)
  owner: g.turn
});

MCE.hasEffect(game, sq, type)

Returns true if the square has an active effect of the given type.

MCE.getEffects(game, sq)

Returns array of all effects on a square.

MCE.removeEffect(game, undo, sq, type)

Removes an effect by square and type.

MCE.tickEffects(game, undo)

Decrements all effect durations by 1 and removes expired effects. Called automatically after each turn (unless variant has custom turnLogic).

Board Mutations

MCE.mutateBoard(game, undo, mutations)

Batch-modifies board squares with automatic undo tracking. Use in afterMove or beforeMove hooks instead of manually tracking changes.

// In afterMove: transform a piece and clear a square
MCE.mutateBoard(g, undo, [
  { sq: 27, piece: 'Q' },  // place white queen on d5
  { sq: 35, piece: null }  // clear e4
]);
// Automatically restored on unmakeMove — no restoreState needed

Action Moves

Moves that consume a turn without physical board movement. Generate them in moveFilter and handle them in afterMove.

// In moveFilter — generate action moves:
moves.push({
  from: pieceSq,
  to: targetSq,
  flag: 'action',
  action: 'hex'
});

// In afterMove — apply the action's effect:
if (move.flag === 'action' && move.action === 'hex') {
  MCE.addEffect(g, undo, { sq: move.to, type: 'hex', duration: 2 });
}

Action moves skip all standard board mutation (no piece movement, en passant, castling, or promotion). The AI evaluates them alongside normal moves in the search tree.

Capture Interception

The beforeMove hook can return a signal to modify capture behaviour:

beforeMove: function(g, move, undo) {
  if (MCE.hasEffect(g, move.to, 'shield')) {
    // Move attacker but don't remove target
    g.board[move.to] = g.board[move.from];
    g.board[move.from] = null;
    MCE.removeEffect(g, undo, move.to, 'shield');
    return { cancelCapture: true };
  }
  // Normal move
  g.board[move.to] = g.board[move.from];
  g.board[move.from] = null;
}

Return values: { cancelCapture: true } or { redirectCapture: squareIndex }. Existing variants that return nothing are unaffected.

Multi-Step Turns

Some variants require multiple moves per turn conditionally (e.g., a piece must retreat after capturing). The pendingAction pattern enables this without custom turn logic.

Setting a pending action

In your variant's afterMove hook, set g._pendingAction to signal that the current player must make a follow-up move before the turn ends:

afterMove: function(g, move, undo) {
  var pd = g.pieceData && g.pieceData[move.to];
  if (pd && pd.key === 'salamander' && undo.captured) {
    g._pendingAction = {
      from: move.to,
      filter: function(m, g) {
        // Only allow moves to adjacent empty squares
        return isAdjacent(m.to, move.to, g);
      },
      label: 'Salamander must retreat'
    };
  }
}

How it works

pendingAction object

PropertyTypeDescription
fromnumberSquare index the follow-up move must originate from
filterfunction(move, game) → booleanOptional filter for which targets are valid
labelstringOptional UI label describing the required action

Game Controller

See the live SDK demo for interactive examples of everything below.

MCE.createGameController(container, game, opts)

Mounts a full game interaction loop (select → move → animate → AI → repeat) on a DOM container. Consumer games use this instead of reimplementing click handling, AI scheduling, and undo logic.

var ctrl = MCE.createGameController(boardEl, game, {
  players: { w: 'human', b: 'ai' },
  aiDifficulty: 'medium',
  workerPath: 'lib/mce/js/ai-worker.js',
  variantPaths: ['lib/mce/js/variants/standard.js'],
  renderOpts: {
    size: 480,
    animate: true,
    animStyle: 'arc',
    tilePainter: myTilePainter,
    pieceProvider: myPieceProvider,
    afterRender: myOverlays,
  },
  onMove: function(move, undo, captured) {
    console.log('Move:', move.from, '->', move.to);
  },
  onGameEnd: function(result) {
    console.log('Game over:', result);
  },
  onTurnChange: function(turn, turnIndex) {
    updateTurnPanel(turn);
  },
  onPendingAction: function(action) {
    showMessage(action.label);
  },
  onPromotionNeeded: function(candidates, side, callback) {
    showPromotionUI(candidates, callback);
  },
});

Controller methods

MethodDescription
ctrl.undo()Undo last move (or pair if playing vs AI)
ctrl.redo()Redo an undone move
ctrl.forfeit()End the game immediately
ctrl.setDifficulty(level)Change AI difficulty mid-game
ctrl.setFlipped(bool)Flip the board perspective
ctrl.getGame()Returns the game object
ctrl.isThinking()Returns true if AI is currently searching
ctrl.render()Force a re-render
ctrl.destroy()Cleanup: terminates AI worker, removes listeners
ctrl.setRenderOpts(newOpts)Merges new render options into the active config and re-renders. Use for responsive resize or dynamic style changes
// Responsive resize on window change
window.addEventListener('resize', function() {
  ctrl.setRenderOpts({ size: boardEl.clientWidth });
});

Controller callbacks

OptionSignatureDescription
onMove(move, undo, captured)Called after every executed move (human or AI). Use for sound, logging, UI updates.
onGameEnd(result)Called when the game ends (checkmate, stalemate, or custom win condition).
onTurnChange(turn, turnIndex)Called when the active player changes. turn is the colour key, turnIndex the index into the players array.
onSelect(sq, piece, legalMoves)Called when a piece is selected. Receives the square index, piece character, and array of legal moves from that square.
onPendingAction(action, legalMoves)Called when a multi-step turn starts. Show retreat/follow-up UI.
onPendingActionEnd()Called when the pending action completes (follow-up move made).
onRender(game, renderOpts)Called after every board render. Use for syncing external UI with board state.
onAnimateMove(move, game, execute)Intercepts move execution. Call execute() when ready — lets you add pre-animation delays or effects.
onCaptureEffect(sq, capturedPiece)Called when a capture occurs. Use for particle effects, screen shake, sounds.
onSquareClick(sq, game, api) → booleanIntercepts square clicks before default handling. Return true to prevent the default select/move behaviour. The api object exposes { selected, executeMove, getLegalMoves, render }.
onPromotionNeeded(candidates, side, callback)Custom promotion UI. Call callback(pieceChar) with the chosen piece.
customRender(game, state)Replaces the default render entirely. state includes { selected, lastMove, flipped, aiThinking, gameOver, getLegalMoves }.

onSquareClick example — hex targeting UI

var ctrl = MCE.createGameController(boardEl, game, {
  onSquareClick: function(sq, game, api) {
    if (hexTargetMode) {
      api.executeMove({ from: shamanSq, to: sq, flag: 'action' });
      hexTargetMode = false;
      return true; // prevent default
    }
    return false; // let default handling proceed
  }
});

onAnimateMove example — pre-move preview

var ctrl = MCE.createGameController(boardEl, game, {
  onAnimateMove: function(move, game, execute) {
    showArrow(move.from, move.to);
    setTimeout(function() {
      hideArrow();
      execute(); // now actually make the move
    }, 200);
  }
});

getLegalMovesOverride

Pass a getLegalMovesOverride function in the controller options to replace or filter legal moves before they reach the interaction loop. Return an array of moves to override, or null to use the default.

var ctrl = MCE.createGameController(boardEl, game, {
  getLegalMovesOverride: function(game, state) {
    // Only allow pawn moves on the first turn
    if (game.fullmove === 1) {
      return MCE.legalMoves(game).filter(function(m) {
        return game.board[m.from] && game.board[m.from].type === 'p';
      });
    }
    return null; // use default
  }
});

Player configuration

The players object maps colour keys to 'human' or 'ai'. For multi-player games:

players: { w: 'human', b: 'ai', r: 'ai', g: 'ai' }

Replay

MCE.createReplay(completedGame)

Creates a replay controller for stepping through a completed game's move history.

var replay = MCE.createReplay(completedGame);

replay.stepForward();  // advance one move
replay.stepBack();     // go back one move
replay.goToMove(10);   // jump to move 10
replay.isAtEnd();      // true if at last move
replay.isAtStart();    // true if at initial position
replay.currentMove();  // current move index (0-based)
replay.totalMoves();   // total moves in history
replay.getGame();      // game object at current state
replay.getMove(n);     // get move object at index n

The replay manages its own internal game state. Use replay.getGame() with MCE.renderBoard() to display the board at each step.

// Render the replay board at each step:
function renderReplay() {
  MCE.renderBoard(container, replay.getGame(), {
    size: 480,
    animate: true,
    animStyle: 'slide'
  });
}

nextBtn.onclick = function() { replay.stepForward(); renderReplay(); };
prevBtn.onclick = function() { replay.stepBack(); renderReplay(); };

Plugin Registration

MCE.registerVariant(key, config)

Registers a variant plugin. All variant-specific logic is defined in the config object — no core engine code is modified.

MCE.registerVariant('myVariant', {
  label: 'My Variant',       // picker display name
  group: 'Tactical',        // picker group
  rows: 8, cols: 8,         // board dimensions
  fen: null,                // null = standard position
  title: 'My Variant',
  description: 'How this variant works.',
  rule: 'Board: 8×8 · Win: Checkmate',
  // Config flags (all optional):
  noCastling: false,
  noCheck: false,
  torpedo: false,
  // Hook functions (all optional):
  init: function(g) { },
  moveFilter: function(g, moves) { return moves; },
  beforeMove: function(g, move, undo) { },
  afterMove: function(g, move, undo) { },
  turnLogic: function(g, undo) { },
  restoreState: function(g, undo) { },
  winCondition: function(g) { return null; },
  evaluate: function(g, defaultEval) { return defaultEval(g); },
  visibility: function(g, side) { return new Set(); },
  statusText: function(g, helpers) { return null; },
  explosionCaptures: function(g, move) { return []; },
  positionKey: function(g) { return customHash(g); },
  openingBook: { "posKey": ["e2e4", "d2d4"] },

  // Config flags
  noCheck: true,
  noRepetitionDraw: true,
  promotionPieces: ['q', 'r', 'b', 'n'],
});

statusText receives helpers: { nameFor, nameForOpp, gameOver, variantStatus }. When gameOver is true, variantStatus contains the value returned by winCondition. Return a string to display as the game status.

explosionCaptures returns an array of { piece, capturedBy } objects for pieces destroyed as collateral (e.g. Atomic explosions). Used by the UI to track captured pieces.

noRepetitionDraw disables threefold repetition and insufficient material detection for variants where position repetition is normal (fog, duck, atomic).

See the Plugin Guide for full documentation of all config flags and hooks.

MCE.getVariantConfig(key)

Returns the registered config object for a variant, with inheritance resolved if extends was used. Returns null if not registered.

const vc = MCE.getVariantConfig('atomic');
console.log(vc.label);      // 'Atomic'
console.log(vc.noCheck);    // undefined (uses standard check rules)
console.log(vc.beforeMove); // [Function] (explosion logic)

MCE.variantRegistry

The raw registry object. Keys are variant camelCase IDs, values are config objects. Used by the game controller to build the picker.

Object.keys(MCE.variantRegistry); // ['standard', 'chess960', 'atomic', ...]

MCE.registerPiece(typeChar, handlers)

Registers a custom piece type for move generation and attack detection.

MCE.registerPiece('m', {
  genMoves: function(g, from, side) {
    // Return array of { from, to, flag } move objects
  },
  attacks: function(g, from, target) {
    // Return true if piece at 'from' attacks 'target' square
  }
});

Use the registered character in FEN strings (uppercase = white, lowercase = black).

Unit Template System

For games with many custom unit types (like Dungeon Chess's 22 units), the template system lets you define movement declaratively instead of writing genMoves/attacks by hand.

MCE.buildUnitHandler(config)

Returns a { genMoves, attacks } handler object from a declarative config. Use it with registerPiece or as a dispatch target.

// Simple: slides like a rook, blocked by water
var dragon = MCE.buildUnitHandler({ move: 'rook:waterBlock' });

// Divergent: moves like a king, attacks like a bishop (gapped)
var archer = MCE.buildUnitHandler({
  move: 'king:waterBlock',
  attack: 'gapped:bishop:waterBlock'
});

// Knight jumps
var cavalry = MCE.buildUnitHandler({ move: 'knight:waterBlock' });

// Cannon: slides to move, needs a screen piece to capture over
var cannon = MCE.buildUnitHandler({
  move: 'rook:waterBlock',
  cannon: { dirs: 'rook' }
});

Move spec string format

String specs are composed with colons: 'style:dirs:modifier'. Order does not matter.

TokenTypeMeaning
rookDirectionsOrthogonal (N/S/E/W)
bishopDirectionsDiagonal (NE/NW/SE/SW)
queenDirectionsAll 8 directions
knightDirections + styleL-shaped jumps (also sets style to jump)
kingDirections + styleSingle-step in all 8 (queen dirs, jump style)
jumpStyleSingle-square jumps (no sliding)
gappedStyleSlide that must skip exactly one empty square before landing
waterBlockModifierMovement blocked by water terrain (terrainSkip predicate)

Config object properties

PropertyTypeDescription
movestring | string[] | functionMovement spec (non-capturing moves). Array for multiple movement styles.
attackstring | string[] | functionAttack spec (capture-only moves). If omitted, move is used for captures too.
cannon{ dirs: string }Cannon-style capture (needs a screen piece to jump over).
genMovesfunction(g, sq, side)Escape hatch: full custom move generation (overrides move/attack).
attacksfunction(g, from, target)Escape hatch: full custom attack detection.

Using with registerPiece

// Direct: one handler per piece character
MCE.registerPiece('d', MCE.buildUnitHandler({ move: 'rook:waterBlock' }));

// Dispatch: many unit types via a single character + pieceData
var unitHandlers = {
  dragon: MCE.buildUnitHandler({ move: 'rook:waterBlock' }),
  archer: MCE.buildUnitHandler({ move: 'king', attack: 'gapped:bishop' }),
  knight: MCE.buildUnitHandler({ move: 'knight:waterBlock' }),
};
MCE.registerPiece('x', {
  genMoves: function(g, sq, side) {
    var pd = g.pieceData[sq];
    return pd && unitHandlers[pd.key] ? unitHandlers[pd.key].genMoves(g, sq, side) : [];
  },
  attacks: function(g, from, target) {
    var pd = g.pieceData[from];
    return pd && unitHandlers[pd.key] ? unitHandlers[pd.key].attacks(g, from, target) : false;
  }
});

MCE.registerUnit(key, config)

Registers a unit in the global unit registry by key. Retrieval via getUnit(). Useful for shared unit definitions across multiple variants.

MCE.registerUnit('dragon', { move: 'rook:waterBlock' });
MCE.registerUnit('archer', { move: 'king', attack: 'gapped:bishop' });

MCE.getUnit(key)

Returns the registered config object for a unit, or null if not found.

var dragonConfig = MCE.getUnit('dragon');
var handler = MCE.buildUnitHandler(dragonConfig);

MCE.getUnitRegistry()

Returns the full unit registry object. Keys are unit names, values are config objects.

Terrain Predicates

Terrain predicates control how terrain values affect move generation globally for a variant.

terrainSkip (variant config)

A function registered in registerVariant() that returns true for terrain values that block movement. All slide/jump/cannon generators respect it automatically.

MCE.registerVariant('dungeon-chess', {
  terrainSkip: function(t) { return t === 'w' || t === 2; },
  // ...
});

waterBlock (move spec modifier)

Per-unit terrain blocking. When waterBlock is in a move spec string, movement stops at squares where the variant's terrainSkip predicate returns true.

// This unit IS blocked by water:
var soldier = MCE.buildUnitHandler({ move: 'rook:waterBlock' });

// This unit is NOT blocked (e.g. a flying unit):
var wraith = MCE.buildUnitHandler({ move: 'queen' });

This lets you have units that respect terrain alongside units that ignore it — all using the same global terrain array.

terrainBlock (move spec modifier)

A custom terrain blocking predicate, more specific than waterBlock. Takes a terrain value, returns true to block.

// Block only lava, not water
var fireVulnerable = MCE.buildUnitHandler({
  move: { dirs: 'queen', terrainBlock: function(t) { return t === 'lava'; } }
});

Utilities

MCE.randomFEN960()

Generates a random Chess960 starting position FEN. Bishops on opposite colours, king between rooks.

const fen = MCE.randomFEN960();
// e.g. "brnqknrb/pppppppp/8/8/8/8/PPPPPPPP/BRNQKNRB w KQkq - 0 1"

MCE.pieceColor(char)

Returns 'w' or 'b' based on case.

MCE.pieceType(char)

Returns the lowercase piece letter.

MCE.advanceTurn(game)

Advances to the next player's turn. Used in turnLogic hooks to end a player's turn.

Constants

ConstantValue
MCE.WHITE'w'
MCE.BLACK'b'
MCE.PIECE{ P:'p', N:'n', B:'b', R:'r', Q:'q', K:'k', A:'a', C:'c', S:'s', M:'m' }
MCE.INITIAL_FENStandard starting position

MCE.PIECE is a convenience enum for common piece chars. The full set of 18 registered piece types is available via MCE.getPieceRegistry().

Embedding

The play page supports URL parameters for embedding in iframes. Combine parameters to control appearance and behaviour.

Basic parameters

ParameterValuesEffect
variantAny variant keyStart with a specific variant (e.g. atomic, fogOfWar)
embed1Hides nav, sidebar, and footer. Adds embed-mode class to body
boardonly1Requires embed=1. Hides all UI except the board. Board fills container width
modesolo | pass | fullscreenSolo plays vs AI; pass-and-play is two humans; fullscreen shows only the board scaled to viewport
themeAny theme keySets the board colour theme (persists across refresh)
piecesAny piece style keySets the piece colour style (persists across refresh)
difficultybeginner ... expertAI difficulty level (default: medium)
animStyleslide | arc | bounce | warpPiece movement animation style (default: slide)
animSpeedslow | normal | fast | instantAnimation speed: 400ms, 200ms, 100ms, or disabled (default: normal)
p1Any stringWhite player name (default: "White")
p2Any stringBlack player name (default: "Black")

Fullscreen mode

Displays only the board at maximum viewport size with an exit button. Triggered by ?mode=fullscreen or the Fullscreen button in the toolbar. Press Escape or click Exit to return to normal view.

// Direct URL:
https://chess.moddable.games/play/?mode=fullscreen&variant=atomic&pieces=gold

// Shareable with all preferences:
https://chess.moddable.games/play/?variant=fogOfWar&theme=cosmic&pieces=navy

Embed-only styling parameters

These only apply when embed=1 is set:

ParameterValuesEffect
bgCSS colourOverride background colour (e.g. %23f0f0f0 for #f0f0f0)
accentCSS colourOverride accent colour for selection highlights and borders
radiusCSS valueBorder radius on the board container (e.g. 12px)

Examples

Minimal embed — board only, atomic variant:

<iframe src="https://chess.moddable.games/play/?embed=1&boardonly=1&variant=atomic"
  width="400" height="400" frameborder="0"></iframe>

Full game UI in light theme with custom names:

<iframe src="https://chess.moddable.games/play/?embed=1&theme=light&p1=Alice&p2=Bob&variant=standard"
  width="800" height="600" frameborder="0"></iframe>

Custom styling — green accent, rounded corners:

<iframe src="https://chess.moddable.games/play/?embed=1&accent=%233a9928&radius=16px&variant=kingOfTheHill"
  width="600" height="500" frameborder="0"></iframe>

Pass-and-play with player names (no AI):

<iframe src="https://chess.moddable.games/play/?embed=1&mode=pass&p1=Magnus&p2=Hikaru"
  width="800" height="600" frameborder="0"></iframe>

postMessage API

Control embedded iframes without reloading. Send messages to the iframe's contentWindow:

// Switch variant (resets board):
iframe.contentWindow.postMessage({ type: 'chess:setVariant', variant: 'atomic' }, '*');

// Switch theme (preserves game state):
iframe.contentWindow.postMessage({ type: 'chess:setTheme', theme: 'cosmic' }, '*');

// Change background colour:
iframe.contentWindow.postMessage({ type: 'chess:setBg', bg: '#1a1a2e' }, '*');

// Reset current variant to starting position:
iframe.contentWindow.postMessage({ type: 'chess:newGame' }, '*');

Cosmic dark board theme with Capablanca variant:

<iframe src="https://chess.moddable.games/play/?embed=1&theme=cosmic&variant=capablanca"
  width="800" height="600" frameborder="0"></iframe>

Neon theme, board only:

<iframe src="https://chess.moddable.games/play/?embed=1&boardonly=1&theme=neon&variant=standard"
  width="400" height="400" frameborder="0"></iframe>

MCP Server

The engine is available as an MCP (Model Context Protocol) server for use with Claude Code, Claude Desktop, or any MCP-compatible client. Eight tools expose the full game engine for position analysis, move validation, board rendering, and puzzle generation.

Setup

# Run standalone
node mcp/server.js

# Add to Claude Code
claude mcp add --transport stdio moddable-chess node mcp/server.js

Available tools

ToolParametersDescription
chess_list_variantsgroup?All 70 variants with board sizes, win conditions, descriptions. Optional group filter (Classic, Tactical, Alternate Rules, Asymmetric, Small Boards, Large Boards)
chess_get_legal_movesvariant?, fen?All legal moves for a position in algebraic notation with annotations (capture, promotion, castling)
chess_analyze_positionvariant?, fen?, depth?AI evaluation: score in centipawns, best move, principal variation. Depth 1-6 (default 4)
chess_validate_movemove, variant?, fen?Check if a specific move is legal. Returns resulting position FEN if valid
chess_make_movesmoves, variant?, fen?Play a sequence of moves, return final FEN and game status
chess_get_opening_bookvariant?, fen?Opening book continuations for the current position (where available)
chess_generate_puzzlevariant?, type?Generate tactical puzzles. Types: mate-in-1, mate-in-2, tactics
chess_render_svgvariant?, fen?, theme?, highlights?, size?Render a board position as a self-contained SVG string. Supports all board sizes and 6 themes. No DOM required (works in Cloudflare Workers)

Example: position analysis

// chess_analyze_position
{ "variant": "standard", "fen": "r1bqkbnr/pppppppp/2n5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq -", "depth": 5 }
// Returns: { score: 35, bestMove: "d2d4", pv: ["d2d4", "d7d5", "e4d5"], material: { w: 39, b: 39 } }

Example: move validation

// chess_validate_move
{ "variant": "standard", "move": "e2e4" }
// Returns: { legal: true, resultFen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", notation: "e4" }

// chess_validate_move (illegal)
{ "variant": "standard", "move": "e2e5" }
// Returns: { legal: false, reason: "No legal move from e2 to e5" }

Example: puzzle generation

// chess_generate_puzzle
{ "variant": "standard", "type": "mate-in-2" }
// Returns: { fen: "2r3k1/5ppp/...", solution: ["d1d8", "c8d8", "e1e8"], sideToMove: "w" }