All public methods on the MCE global namespace.
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.
Loads a FEN string into an existing game object. Supports multi-digit empty square counts for boards wider than 9.
Exports the current game state as a FEN string.
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;
}
});
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'.
Returns moves without checking for pins or self-check. Faster but may include illegal moves.
Returns legal moves with variant-specific filtering (e.g., forced captures in Antichess, no-check filter in Racing Kings).
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
Reverses a move. Restores the game to its previous state.
Returns: 'active', 'check', 'checkmate', 'stalemate', 'draw-repetition', 'draw-material', or 'draw-50'.
Draw conditions:
'draw-repetition' — threefold repetition detected (same position reached three times)'draw-material' — insufficient material (K vs K, K+B vs K, K+N vs K, K+B vs K+B same colour)'draw-50' — 50-move rule (100 half-moves without a pawn move or capture)In addition to the properties returned by createGame, the game object tracks:
positionHistory — array of position keys (one per half-move), used for threefold repetition detection. Updated automatically by makeMove.Calls the active variant's winCondition hook. Returns null if no variant win, or a status string like 'koth-w', 'race-b', 'antichess-w'.
Returns true if the given side's king is in check. side is 'w' or 'b'.
Converts row/col to a board index. Pass game for non-8×8 boards.
Converts a board index to [row, col].
Returns true if the coordinates are within the board boundaries. Applies wrap-around normalisation first if game.wrapFiles or game.wrapRanks is set.
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
Converts an index to algebraic notation (e.g., 'e4').
Converts algebraic notation to an index.
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)
});
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.
Triggers a capture burst animation at the given SVG coordinates. Can be called independently for custom capture effects.
Consumer games can extend the board renderer without replacing it. All callbacks are optional — returning null/undefined falls through to default rendering.
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
}
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.
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);
});
}
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
Returns the current theme object with colour values:
const t = MCE.getTheme();
// { light, dark, highlight, lastMove, dot, ring, border, label }
Object containing all available theme definitions. Keys: classic, cosmic, wood, marble, neon, minimal, transparent.
| Theme | Light sq | Dark sq | Style |
|---|---|---|---|
classic | #f0d9b5 | #b58863 | Standard tan/brown (default) |
cosmic | #2d3760 | #141c37 | Deep navy, inspired by the OG card |
wood | #deb887 | #8b5e3c | Warm brown wood tones |
marble | #f2f0ec | #b8b5af | Grey/white stone |
neon | #1a1a2e | #0f0f1a | Dark with bright move indicators |
minimal | #fafafa | #e8e8e8 | Near-white, very subtle contrast |
transparent | rgba(255,255,255,0.12) | rgba(255,255,255,0.22) | See-through for embedding on any background |
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');
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)
Returns the current piece style key string.
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).
| Key | Label | Dark fill | Description |
|---|---|---|---|
auto | Auto | #000 (or tinted on dark boards) | Default. Adjusts black pieces automatically on dark themes for visibility |
gold | White & Gold | #b58863 | Warm amber/gold pieces (chess.com style) |
charcoal | Cream & Charcoal | #3a3a3a | Off-white vs dark grey with light detail lines |
burgundy | White & Burgundy | #6b1a2a | Classic red set style |
navy | White & Navy | #1a3a5c | Dark blue with light blue details |
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.
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.
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';
When the renderer needs a piece symbol, the resolver checks in order:
overrides[char] — per-piece set override (e.g. use a specific set for knights)set valuefallback value (covers missing pieces)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');
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');
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
});
Returns a copy of the active resolver configuration.
const cfg = PieceSetResolver.getConfig();
// { set: 'staunty', fallback: 'classic', overrides: { n: 'alpha' } }
Returns an array of all loaded piece set descriptors.
const sets = PieceSetResolver.getAvailableSets();
// [{ id: 'classic', name: 'Classic', recolorable: true }, ...]
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.
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');
}
Piece set manifests come in two forms:
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" }
}
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" } }
}
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';
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']
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: '...', ... }
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'
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', ...] }
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';
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]]);
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
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'));
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
Named offset sets available as string shortcuts:
| Name | Offsets | Description |
|---|---|---|
'knight' | (2,1) in all orientations | Standard knight jumps |
'king' | All 8 adjacent squares | One step any direction |
'bishop' | 4 diagonal steps | One diagonal step (Fers) |
'rook' | 4 orthogonal steps | One orthogonal step (Wazir) |
'queen' | All 8 directions | One step any direction |
'elephant' | (2,2) in all orientations | Alfil/Elephant leap |
'camel' | (3,1) in all orientations | Extended knight |
'dabbaba' | (2,0) in all orientations | Orthogonal 2-leap |
'zebra' | (3,2) in all orientations | Extended knight variant |
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,
});
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 });
Available difficulty presets:
| Key | Time | Behaviour |
|---|---|---|
beginner | 200ms | 30% random blunders |
easy | 400ms | 15% blunders, picks from top 5 moves |
medium | 800ms | Picks from top 3 moves |
hard | 1.5s | Always picks best move |
expert | 3s | Always picks best move, deepest search |
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).
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.
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.
Temporary status effects on board squares. Effects auto-decrement each turn and are fully undo-aware for AI search.
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
});
Returns true if the square has an active effect of the given type.
Returns array of all effects on a square.
Removes an effect by square and type.
Decrements all effect durations by 1 and removes expired effects. Called automatically after each turn (unless variant has custom turnLogic).
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
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.
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.
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.
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'
};
}
}
g._pendingAction is set, the turn does NOT advanceMCE.legalMoves() restricts moves to: only from pendingAction.from, and only those passing pendingAction.filterafterMove does NOT set a new pending action, so the turn advances normally| Property | Type | Description |
|---|---|---|
from | number | Square index the follow-up move must originate from |
filter | function(move, game) → boolean | Optional filter for which targets are valid |
label | string | Optional UI label describing the required action |
See the live SDK demo for interactive examples of everything below.
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);
},
});
| Method | Description |
|---|---|
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 });
});
| Option | Signature | Description |
|---|---|---|
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) → boolean | Intercepts 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 }. |
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
}
});
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);
}
});
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
}
});
The players object maps colour keys to 'human' or 'ai'. For multi-player games:
players: { w: 'human', b: 'ai', r: 'ai', g: 'ai' }
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(); };
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.
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)
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', ...]
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).
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.
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' }
});
String specs are composed with colons: 'style:dirs:modifier'. Order does not matter.
| Token | Type | Meaning |
|---|---|---|
rook | Directions | Orthogonal (N/S/E/W) |
bishop | Directions | Diagonal (NE/NW/SE/SW) |
queen | Directions | All 8 directions |
knight | Directions + style | L-shaped jumps (also sets style to jump) |
king | Directions + style | Single-step in all 8 (queen dirs, jump style) |
jump | Style | Single-square jumps (no sliding) |
gapped | Style | Slide that must skip exactly one empty square before landing |
waterBlock | Modifier | Movement blocked by water terrain (terrainSkip predicate) |
| Property | Type | Description |
|---|---|---|
move | string | string[] | function | Movement spec (non-capturing moves). Array for multiple movement styles. |
attack | string | string[] | function | Attack 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). |
genMoves | function(g, sq, side) | Escape hatch: full custom move generation (overrides move/attack). |
attacks | function(g, from, target) | Escape hatch: full custom attack detection. |
// 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;
}
});
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' });
Returns the registered config object for a unit, or null if not found.
var dragonConfig = MCE.getUnit('dragon');
var handler = MCE.buildUnitHandler(dragonConfig);
Returns the full unit registry object. Keys are unit names, values are config objects.
Terrain predicates control how terrain values affect move generation globally for a variant.
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; },
// ...
});
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.
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'; } }
});
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"
Returns 'w' or 'b' based on case.
Returns the lowercase piece letter.
Advances to the next player's turn. Used in turnLogic hooks to end a player's turn.
| Constant | Value |
|---|---|
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_FEN | Standard 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().
The play page supports URL parameters for embedding in iframes. Combine parameters to control appearance and behaviour.
| Parameter | Values | Effect |
|---|---|---|
variant | Any variant key | Start with a specific variant (e.g. atomic, fogOfWar) |
embed | 1 | Hides nav, sidebar, and footer. Adds embed-mode class to body |
boardonly | 1 | Requires embed=1. Hides all UI except the board. Board fills container width |
mode | solo | pass | fullscreen | Solo plays vs AI; pass-and-play is two humans; fullscreen shows only the board scaled to viewport |
theme | Any theme key | Sets the board colour theme (persists across refresh) |
pieces | Any piece style key | Sets the piece colour style (persists across refresh) |
difficulty | beginner ... expert | AI difficulty level (default: medium) |
animStyle | slide | arc | bounce | warp | Piece movement animation style (default: slide) |
animSpeed | slow | normal | fast | instant | Animation speed: 400ms, 200ms, 100ms, or disabled (default: normal) |
p1 | Any string | White player name (default: "White") |
p2 | Any string | Black player name (default: "Black") |
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
These only apply when embed=1 is set:
| Parameter | Values | Effect |
|---|---|---|
bg | CSS colour | Override background colour (e.g. %23f0f0f0 for #f0f0f0) |
accent | CSS colour | Override accent colour for selection highlights and borders |
radius | CSS value | Border radius on the board container (e.g. 12px) |
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>
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>
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.
# Run standalone
node mcp/server.js
# Add to Claude Code
claude mcp add --transport stdio moddable-chess node mcp/server.js
| Tool | Parameters | Description |
|---|---|---|
chess_list_variants | group? | All 70 variants with board sizes, win conditions, descriptions. Optional group filter (Classic, Tactical, Alternate Rules, Asymmetric, Small Boards, Large Boards) |
chess_get_legal_moves | variant?, fen? | All legal moves for a position in algebraic notation with annotations (capture, promotion, castling) |
chess_analyze_position | variant?, fen?, depth? | AI evaluation: score in centipawns, best move, principal variation. Depth 1-6 (default 4) |
chess_validate_move | move, variant?, fen? | Check if a specific move is legal. Returns resulting position FEN if valid |
chess_make_moves | moves, variant?, fen? | Play a sequence of moves, return final FEN and game status |
chess_get_opening_book | variant?, fen? | Opening book continuations for the current position (where available) |
chess_generate_puzzle | variant?, type? | Generate tactical puzzles. Types: mate-in-1, mate-in-2, tactics |
chess_render_svg | variant?, 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) |
// 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 } }
// 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" }
// chess_generate_puzzle
{ "variant": "standard", "type": "mate-in-2" }
// Returns: { fen: "2r3k1/5ppp/...", solution: ["d1d8", "c8d8", "e1e8"], sideToMove: "w" }