This guide builds Orda Chess step by step, demonstrating custom pieces with registerPiece, divergent movement patterns, attack functions, asymmetric FEN, and variant registration.
You need a local copy of the moddable-chess repository and a browser. No build tools required.
git clone https://github.com/Moddable-Games/moddable-chess.git
cd moddable-chess
Open play/index.html in your browser to verify the engine loads. You should see the variant picker with existing variants.
Create js/variants/orda-chess.js. Every plugin starts with 'use strict' and ends with a call to MCE.registerVariant():
'use strict';
MCE.registerVariant('ordaChess', {
label: 'Orda Chess',
group: 'Asymmetric',
rows: 8,
cols: 8,
fen: 'rnbqkbnr/pppppppp/8/8/8/PPPPPPPP/8/RNBQKBNR w KQ - 0 1',
noCastling: false,
title: 'Orda Chess',
description: 'Asymmetric variant.',
rule: 'Board: 8×8 · Win: Checkmate',
});
Add a script tag in play/index.html before game-controller.js:
<script src="../js/variants/orda-chess.js?v=0.10.1"></script>
Reload the page. "Orda Chess" now appears in the picker under the Asymmetric group — but it plays like standard chess. The custom pieces don't exist yet.
Orda Chess introduces three custom pieces for Black's army. Each has a divergent movement pattern: they move one way but capture a different way. We start with the Yurt.
The Yurt moves one square diagonally (like a Ferz) but captures one square orthogonally (like a Wazir). This is the simplest divergent piece — no sliding, just single-step moves.
Register the piece before the registerVariant call in the same file:
MCE.registerPiece('y', {
genMoves: function(g, from, side) {
var moves = [];
var r = MCE.rc(from, g)[0], c = MCE.rc(from, g)[1];
// Non-capture moves: diagonal (Ferz pattern)
var moveDirs = [[-1,-1],[-1,1],[1,-1],[1,1]];
for (var i = 0; i < moveDirs.length; i++) {
var nr = r + moveDirs[i][0], nc = c + moveDirs[i][1];
if (!MCE.onBoard(nr, nc, g)) continue;
var target = MCE.sq(nr, nc, g);
if (g.board[target]) continue;
moves.push({ from: from, to: target, flag: null });
}
// Captures: orthogonal (Wazir pattern)
var capDirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (var i = 0; i < capDirs.length; i++) {
var nr = r + capDirs[i][0], nc = c + capDirs[i][1];
if (!MCE.onBoard(nr, nc, g)) continue;
var target = MCE.sq(nr, nc, g);
if (g.board[target] && MCE.isEnemy(target, side, g)) {
moves.push({ from: from, to: target, flag: 'capture' });
}
}
return moves;
},
attacks: function(g, from, target) {
var fr = MCE.rc(from, g)[0], fc = MCE.rc(from, g)[1];
var tr = MCE.rc(target, g)[0], tc = MCE.rc(target, g)[1];
var dr = Math.abs(tr - fr), dc = Math.abs(tc - fc);
return (dr + dc === 1);
}
});
Key concepts:
MCE.rc(sq, g) converts a square index to [row, col]MCE.sq(row, col, g) converts back to a square indexMCE.onBoard(r, c, g) checks boundsMCE.isEnemy(sq, side, g) checks if a square holds an opponent pieceattacks function only cares about the capture pattern — it's used for check detectionThe Lancer moves by sliding along ranks and files (like a Rook) but captures by jumping like a Knight. This demonstrates combining a sliding movement pattern with a leaping capture pattern.
MCE.registerPiece('l', {
genMoves: function(g, from, side) {
var moves = [];
var r = MCE.rc(from, g)[0], c = MCE.rc(from, g)[1];
// Non-capture moves: slide orthogonally (Rook pattern)
var moveDirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (var d = 0; d < moveDirs.length; d++) {
var nr = r + moveDirs[d][0], nc = c + moveDirs[d][1];
while (MCE.onBoard(nr, nc, g)) {
var target = MCE.sq(nr, nc, g);
if (g.board[target]) break;
moves.push({ from: from, to: target, flag: null });
nr += moveDirs[d][0]; nc += moveDirs[d][1];
}
}
// Captures: Knight jumps
var capOffsets = [[-2,-1],[-2,1],[-1,-2],[-1,2],
[1,-2],[1,2],[2,-1],[2,1]];
for (var i = 0; i < capOffsets.length; i++) {
var nr = r + capOffsets[i][0], nc = c + capOffsets[i][1];
if (!MCE.onBoard(nr, nc, g)) continue;
var target = MCE.sq(nr, nc, g);
if (g.board[target] && MCE.isEnemy(target, side, g)) {
moves.push({ from: from, to: target, flag: 'capture' });
}
}
return moves;
},
attacks: function(g, from, target) {
var fr = MCE.rc(from, g)[0], fc = MCE.rc(from, g)[1];
var tr = MCE.rc(target, g)[0], tc = MCE.rc(target, g)[1];
var dr = Math.abs(tr - fr), dc = Math.abs(tc - fc);
return (dr * dc === 2 && dr + dc === 3);
}
});
Notice the sliding pattern: use a while loop that advances in one direction until hitting a piece or the board edge. Only empty squares generate moves — the first occupied square stops the slide.
The Knight capture check uses the identity dr * dc === 2 && dr + dc === 3 — this matches exactly the eight L-shaped offsets without listing them individually.
The Archer is the diagonal counterpart of the Lancer: it moves by sliding diagonally (like a Bishop) but captures by jumping like a Knight. The structure is identical — just swap the slide directions.
MCE.registerPiece('h', {
genMoves: function(g, from, side) {
var moves = [];
var r = MCE.rc(from, g)[0], c = MCE.rc(from, g)[1];
// Non-capture moves: slide diagonally (Bishop pattern)
var moveDirs = [[-1,-1],[-1,1],[1,-1],[1,1]];
for (var d = 0; d < moveDirs.length; d++) {
var nr = r + moveDirs[d][0], nc = c + moveDirs[d][1];
while (MCE.onBoard(nr, nc, g)) {
var target = MCE.sq(nr, nc, g);
if (g.board[target]) break;
moves.push({ from: from, to: target, flag: null });
nr += moveDirs[d][0]; nc += moveDirs[d][1];
}
}
// Captures: Knight jumps (same as Lancer)
var capOffsets = [[-2,-1],[-2,1],[-1,-2],[-1,2],
[1,-2],[1,2],[2,-1],[2,1]];
for (var i = 0; i < capOffsets.length; i++) {
var nr = r + capOffsets[i][0], nc = c + capOffsets[i][1];
if (!MCE.onBoard(nr, nc, g)) continue;
var target = MCE.sq(nr, nc, g);
if (g.board[target] && MCE.isEnemy(target, side, g)) {
moves.push({ from: from, to: target, flag: 'capture' });
}
}
return moves;
},
attacks: function(g, from, target) {
var fr = MCE.rc(from, g)[0], fc = MCE.rc(from, g)[1];
var tr = MCE.rc(target, g)[0], tc = MCE.rc(target, g)[1];
var dr = Math.abs(tr - fr), dc = Math.abs(tc - fc);
return (dr * dc === 2 && dr + dc === 3);
}
});
Both the Lancer and Archer share the same attacks function — they both capture like a Knight. If you were building multiple pieces with the same capture pattern, you could extract this into a shared function, but keeping it inline keeps each piece self-contained.
Orda Chess is asymmetric: White plays standard chess pieces, Black commands the Horde. The FEN encodes this by using the custom piece characters for Black's back rank:
lhaykahl/8/pppppppp/8/8/PPPPPPPP/8/RNBQKBNR w KQ - 0 1
Breaking this down:
l — Black Lancer (slides like Rook, captures like Knight)h — Black Archer (slides like Bishop, captures like Knight)a — this is actually the standard Archbishop if registered, but here we use y for Yurt in the actual positionsy — Black Yurt (steps diagonal, captures orthogonal)k — Black Khan (identical to King — it's the royal piece)The corrected FEN for Orda is:
lhaykahl/8/pppppppp/8/8/PPPPPPPP/8/RNBQKBNR w KQ - 0 1
Uppercase letters are White (standard army), lowercase are Black (Horde). The engine automatically recognises registered piece characters in the FEN.
Notice KQ in the castling field — only White can castle. Black has no rooks on their starting squares, so no castling rights.
Now update the registerVariant call with the full description and the pieceRoles mapping that tells the engine which custom pieces exist:
MCE.registerVariant('ordaChess', {
label: 'Orda Chess',
group: 'Asymmetric',
rows: 8,
cols: 8,
fen: 'lhaykahl/8/pppppppp/8/8/PPPPPPPP/8/RNBQKBNR w KQ - 0 1',
noCastling: false,
title: 'Orda Chess',
description: 'Asymmetric: White plays standard chess. Black commands the Horde — Yurt (moves diagonal, captures orthogonal), Lancer and Archer (move like rook/bishop, capture like knight). Khan replaces king.',
rule: 'Board: 8×8 · Win: Checkmate',
pieceRoles: { y: 'y', l: 'l', h: 'h' },
});
The pieceRoles object maps piece characters to their registered handlers. This tells the engine "when you encounter a y on the board, use the registerPiece('y', ...) handler for move generation and attacks."
Add the script tag to play/index.html (if you haven't already) and reload. Verify:
Test the AI by switching to "vs Computer" mode. The engine's make/unmake cycle uses your genMoves and attacks functions automatically — no additional AI integration is needed.
A complete variant with custom pieces uses these components:
| Component | Purpose |
|---|---|
registerPiece(char, handler) | Define move generation and attack detection for a new piece type |
genMoves(g, from, side) | Return all pseudo-legal moves (both movement and captures) |
attacks(g, from, target) | Return true if the piece at from threatens target (used for check detection) |
registerVariant(key, config) | Register the variant with its FEN, metadata, and piece mappings |
pieceRoles | Map custom piece characters to their registered handlers |
| FEN string | Encode the starting position using standard + custom piece characters |
The complete file is approximately 100 lines. Every piece is self-contained: movement logic, capture logic, and attack detection live together. The variant registration ties them to a specific starting position.
You can also add an openingBook property to give the AI pre-programmed moves for known positions — see Crazyhouse Step 8 for the format.
See the Plugin Guide for the full config flag and hook reference, or the API Reference for all MCE namespace methods.