Docs / Guides / Action Moves and State

Building Crazyhouse

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.

What We Are Building

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 creation
  • moveFilter — generate drop moves alongside normal moves
  • afterMove — place drops via mutateBoard; capture pieces into hand
  • restoreState — restore hand when AI undoes moves during search
  • evaluate — value pieces in hand for AI scoring
  • statusText — show hand contents to the player

Step 1: Create the Plugin File

Create 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.

Step 2: Initialize Hand State

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.

Step 3: Generate Drop Moves

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 physically
  • from: sq, to: sq — drops target the same square (the piece appears there)
  • Pawns cannot be dropped on rank 0 or rank 7 (promotion ranks)
  • We deduplicate piece types — if you have 3 pawns in hand, you only generate pawn drops once per empty square

Step 4: Handle Drops and Captures

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.

Step 5: Restore Hand State

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.

Step 6: Custom Evaluation

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.

Step 7: Status Text

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.).

Step 8: Opening Book

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.

Summary

Hooks used and their purpose:

HookPurpose in Crazyhouse
initCreate g.hand state for both sides
moveFilterGenerate drop action moves for pieces in hand
afterMovePlace drops via mutateBoard; add captures to hand
restoreStateRestore hand array during AI search undo
evaluateScore hand pieces at 70% board value
statusTextDisplay hand contents to the player
openingBookPre-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.

Back to all guides | Next: Poison Chess (Effects System)