This guide builds Crazyhouse from scratch, demonstrating action moves, moveFilter, afterMove, mutateBoard, restoreState, evaluate, and statusText. These are the Tier 3 hooks that enable ability-based and state-tracking variants.
Crazyhouse is the most popular chess variant online. When you capture a piece, it goes into your "hand." On your turn, instead of moving a piece on the board, you can "drop" a piece from your hand onto any empty square. This creates an ever-growing arsenal and deep tactical play.
Key engine features used:
init — set up hand state on game creationmoveFilter — generate drop moves alongside normal movesafterMove — place drops via mutateBoard; capture pieces into handrestoreState — restore hand when AI undoes moves during searchevaluate — value pieces in hand for AI scoringstatusText — show hand contents to the playerCreate js/variants/crazyhouse.js with the skeleton:
'use strict';
MCE.registerVariant('crazyhouse', {
label: 'Crazyhouse',
group: 'Alternate Rules',
rows: 8, cols: 8,
fen: null,
title: 'Crazyhouse',
description: 'Captured pieces switch sides and can be dropped back onto the board.',
rule: 'Board: 8x8 - Win: Checkmate',
init: function(g) { },
moveFilter: function(g, moves) { return moves; },
afterMove: function(g, move, undo) { },
restoreState: function(g, undo) { },
evaluate: function(g, defaultEval) { return defaultEval(g); },
statusText: function(g, helpers) { return null; },
});
Add the script tag to play/index.html before game-controller.js.
The init hook runs once when the game is created. We use it to add a hand for each side:
init: function(g) {
g.hand = { w: [], b: [] };
}
The hand is an array of piece type characters (e.g. ['p', 'n', 'b']). These are always lowercase — the engine determines colour from whose hand it is.
The moveFilter hook receives all legal moves and can add or remove from the list. We append drop moves for each unique piece type in the player's hand:
moveFilter: function(g, moves) {
var side = g.turn;
var hand = g.hand[side];
if (!hand || hand.length === 0) return moves;
var total = g.rows * g.cols;
var uniquePieces = {};
for (var h = 0; h < hand.length; h++) {
uniquePieces[hand[h]] = true;
}
var pieceTypes = Object.keys(uniquePieces);
for (var p = 0; p < pieceTypes.length; p++) {
var pt = pieceTypes[p];
for (var sq = 0; sq < total; sq++) {
if (g.board[sq]) continue;
if (pt === 'p') {
var rc = MCE.rc(sq, g);
if (rc[0] === 0 || rc[0] === g.rows - 1) continue;
}
moves.push({ from: sq, to: sq, flag: 'action',
action: 'drop', dropPiece: pt });
}
}
return moves;
}
Key points:
flag: 'action' tells the engine to skip normal board mutation — the piece does not move physicallyfrom: sq, to: sq — drops target the same square (the piece appears there)The afterMove hook fires after the piece has moved (or in our case, after the action move no-op). We handle two cases:
afterMove: function(g, move, undo) {
var moverSide = undo.turn;
if (move.flag === 'action' && move.action === 'drop') {
// Place piece from hand onto the board
var dropChar = move.dropPiece;
var placed = (moverSide === MCE.WHITE)
? dropChar.toUpperCase() : dropChar;
MCE.mutateBoard(g, undo, [{ sq: move.to, piece: placed }]);
// Remove from hand (save for undo)
undo._handBefore = g.hand[moverSide].slice();
var idx = g.hand[moverSide].indexOf(dropChar);
if (idx !== -1) g.hand[moverSide].splice(idx, 1);
} else if (undo.captured || move.flag === 'ep') {
// Capture: add piece type to hand
var capturedPiece = undo.captured;
if (move.flag === 'ep') capturedPiece = undo.epCaptured;
if (capturedPiece) {
undo._handBefore = g.hand[moverSide].slice();
g.hand[moverSide].push(MCE.pieceType(capturedPiece));
}
}
}
MCE.mutateBoard(g, undo, [...]) places the piece AND automatically tracks the change for undo — no manual restoration needed for board changes. We only need to manually track the hand state since it is not part of the board array.
The restoreState hook fires during unmakeMove. The AI calls make/unmake millions of times during search, so this must be correct:
restoreState: function(g, undo) {
if (undo._handBefore !== undefined) {
g.hand[undo.turn] = undo._handBefore;
}
}
We saved a snapshot of the hand array before modifying it. Restoration is a simple reassignment. The board changes made via mutateBoard are restored automatically — we do not need to handle those here.
The AI needs to know that pieces in hand have value. We score them at 70% of their board value:
evaluate: function(g, defaultEval) {
var material = defaultEval(g);
var score = material;
var pieceValues = { p: 70, n: 210, b: 220, r: 350, q: 630 };
var side = g.turn;
var opp = (side === MCE.WHITE) ? MCE.BLACK : MCE.WHITE;
var myHand = g.hand[side] || [];
var oppHand = g.hand[opp] || [];
for (var i = 0; i < myHand.length; i++)
score += (pieceValues[myHand[i]] || 0);
for (var j = 0; j < oppHand.length; j++)
score -= (pieceValues[oppHand[j]] || 0);
return score;
}
The defaultEval parameter gives material + piece-square table scoring for on-board pieces. We add hand value on top.
Show the current player's hand contents:
statusText: function(g, helpers) {
var hand = g.hand[g.turn] || [];
if (hand.length === 0) return null;
return helpers.nameFor(g.turn) + ' hand: ' + hand.slice().sort().join(', ');
}
Returning null falls through to the default status text ("White to move", etc.).
Give the AI known good openings so it plays instantly for the first few moves instead of searching. Add an openingBook object to your variant config:
openingBook: {
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -": ["e2e4", "d2d4", "g1f3"],
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3": ["e7e5", "d7d5", "g8f6"],
}
Keys are position keys (first 4 FEN fields). Values are arrays of candidate moves in coordinate notation — the AI picks one randomly for variety. Multiple entries let you cover a few moves deep from the starting position.
This is optional — the AI plays fine without it — but book moves eliminate the thinking delay in well-known positions and give the AI more human-like opening play.
Hooks used and their purpose:
| Hook | Purpose in Crazyhouse |
|---|---|
init | Create g.hand state for both sides |
moveFilter | Generate drop action moves for pieces in hand |
afterMove | Place drops via mutateBoard; add captures to hand |
restoreState | Restore hand array during AI search undo |
evaluate | Score hand pieces at 70% board value |
statusText | Display hand contents to the player |
openingBook | Pre-programmed moves for known positions |
The complete file is approximately 110 lines. Action moves and mutateBoard handle all the complexity of piece drops with automatic undo support.