Introduction

Oh boy, en-passant. A move created by some French guy who woke up one day and decided to make life a bit more miserable for everyone.

Alt text
En-Croissant (!!). Taken from [1]

Just kidding. En-Passant is one of the most recent additions to Chess (by recent I mean half a century ago). Apparently, it was introduced to counter double pawn pushes, or front jumps®, as we call them here. Although it’s been around for quite a long time, this move keeps surprising beginner chess players and chess programmers with how different it is from other moves. We are not scared of en-passant, so we’ll tackle it in this devlog.

We’ll follow a similar route as we did with castling. The first step will be to comprehend the rules for performing en-passant so we can flawlessly implement them.

Rules for En-Passant

En-Passant is allowed under these 3 commandments, as described by this article from Chess.com [2].

  1. The capturing pawn must have advanced exactly three ranks to perform this move.
  2. The captured pawn must have moved two squares in one move, landing right next to the capturing pawn.
  3. The en passant capture must be performed on the turn immediately after the pawn being captured moves. If the player does not capture en passant on that turn, they no longer can do it later

This time, we want to implement this routine:

class MoveGenerator {
    /**
     * @param {Pawn[]} pawns 
     * @param {BoardImplementation} board 
     * @returns Array of en passant moves
     */
    generateEnPassantMoves(pawns, board) {}
}

As always, see if you can build your own solution before continuing.

Exercise  Implement the generateEnPassantMoves function with the following signature:

    /**
     * @param {Pawn[]} pawns 
     * @param {BoardImplementation} board 
     * @returns Array of en passant moves
     */
    generateEnPassantMoves(king, rooks, board) {}

Capturing pawn

The capturing pawn must advance three ranks from its initial position. In the case of white pawns, that is the 5th rank. For black pawns, it is the 4th rank. We can store this information in a tiny data structure. Then, we can implement this rule by checking the pawn’s current rank against this data structure.

class MoveGenerator {
    /**
     * @param {Pawn[]} pawns 
     * @param {BoardImplementation} board 
     * @returns Array of en passant moves
     */
    generateEnPassantMoves(pawns, board) {
        let enPassantMoves = [];
        for (let pawn of pawns) {
            //The capturing pawn must have advanced exactly three ranks to perform this move.
            if (pawn.rank !== ENPASSANT_CAPTURING_RANKS[pawn.color]) continue;
        }
        return enPassantMoves;
    }
}

// --- ChessUtils.js ---
const ENPASSANT_CAPTURING_RANKS = {
    [E_PieceColor.White]: 5,
    [E_PieceColor.Black]: 4
}

Captured pawn

Let’s say we are given the following board and we are told that one of the black pawns just performed a front jump:

We have a problem here. How do we know which pawn performed the jump and can be captured via en-passant? There’s no way of telling just by looking at the current state of the board.

That’s exactly what sets castling and en-passant apart from regular moves. The rules for regular moves just depend on how pieces are currently arranged and not on the previous state of the board. In case of these special moves, we must have information about the past.

The Board class comes to the rescue again.

class Board{
    #enPassantInfo = {
        captureRank: null,
        captureFile: null
    }
}

This data structure will store the rank and file of the latest that performed a front jump in the last move. If no pawn made a jump in the last move, these values will default to null. With this information available, we can easily implement the rule.

class MoveGenerator {
    /**
     * @param {Pawn[]} pawns 
     * @param {BoardImplementation} board 
     * @returns Array of en passant moves
     */
    generateEnPassantMoves(pawns, board) {
        let enPassantMoves = [];
        let enPassantInfo = board.getEnPassantInfo();
        for (let pawn of pawns) {
            //The captured pawn must be right next to the capturing pawn.
            let rankDiff = Math.abs(enPassantInfo.captureRank - pawn.rank);
            let fileDiff = Math.abs(enPassantInfo.captureFile - pawn.file);
            if (fileDiff !== 1 || rankDiff !== 0) continue;
        }
        return enPassantMoves;
    }
}

If the captured pawn landed right next to our pawn, they’ll be in the same rank and there’ll be a 1-file difference between them.

En-Passant timing

The en-passant capture must be performed immediately after a front jump. We already accomplished this with the data structure added in the last rule. However, for the sake of being more explicit in the code, we’ll add a boolean flag to this structure.

class Board{
    #enPassantInfo = {
        rightToEnPassant: false,
        captureRank: null,
        captureFile: null
    }
}

Then, we can use it as follows:

class MoveGenerator {
    /**
     * @param {Pawn[]} pawns 
     * @param {BoardImplementation} board 
     * @returns Array of en passant moves
     */
    generateEnPassantMoves(pawns, board) {
        let enPassantMoves = [];
        let enPassantInfo = board.getEnPassantInfo();
        for (let pawn of pawns) {
             //The en passant capture must be performed on the turn immediately after the pawn being captured moves.
            if (enPassantInfo.rightToEnPassant === false) continue;

            //The capturing pawn must have advanced exactly three ranks to perform this move.
            if (pawn.rank !== ENPASSANT_CAPTURING_RANKS[pawn.color]) continue;

            //The captured pawn must be right next to the capturing pawn.
            let rankDiff = Math.abs(enPassantInfo.captureRank - pawn.rank);
            let fileDiff = Math.abs(enPassantInfo.captureFile - pawn.file);
            if (fileDiff !== 1 || rankDiff !== 0) continue;
        }

        return enPassantMoves;
    }
}

Code
//en-passant-devlog/en-passant-right.js

let game;
const GAME_MARGIN = 10;
const EXTRA_SPACE = 20;
const EN_PASSANT_INFO = {
    x: 50,
    y: 30,
    verSpace: 30
}

const E_PieceColor = Chess.E_PieceColor;
const E_CastlingSide = Chess.E_CastlingSide;

function setup() {
    createCanvas(Chess.GAME_DIMENSIONS.WIDTH + GAME_MARGIN, Chess.GAME_DIMENSIONS.HEIGHT + GAME_MARGIN + EXTRA_SPACE + 50);
    game = new Chess.Game(GAME_MARGIN / 2, GAME_MARGIN / 2 + EXTRA_SPACE);
    game.setGameMode(Chess.E_GameMode.FREE);
}

function draw() {
    background(255);
    game.update();
    textSize(15);
    rectMode(CENTER);
    textStyle(NORMAL);
    textAlign(LEFT, TOP);
    let enPassantInfo = game.board.getEnPassantInfo();
    let mark = enPassantInfo.rightToEnPassant ? "✅" : "❌";
    text("Right to EnPassant: " + mark, EN_PASSANT_INFO.x, EN_PASSANT_INFO.y);
    if (enPassantInfo.rightToEnPassant) {
        text("File: " + Chess.ChessUtils.FileToLetter(enPassantInfo.captureFile), EN_PASSANT_INFO.x, EN_PASSANT_INFO.y + EN_PASSANT_INFO.verSpace * 2);
        text("Rank: " + enPassantInfo.captureRank, EN_PASSANT_INFO.x, EN_PASSANT_INFO.y + EN_PASSANT_INFO.verSpace);
    }
}

Generating an En-Passant Move

Finally, after all conditions are met, we generate the en-passant move:

class MoveGenerator {
    /**
     * @param {Pawn[]} pawns 
     * @param {BoardImplementation} board 
     * @returns Array of en passant moves
     */
    generateEnPassantMoves(pawns, board) {
        let enPassantMoves = [];
        for (let pawn of pawns) {
            //You move your pawn diagonally to an adjacent square, one rank farther from where it had been, 
            //on the same file where the enemy's pawn is.
            let targetRank = pawn.color === E_PieceColor.White ? pawn.rank + 1 : pawn.rank - 1;
            let enPassant = new Move(pawn.rank, pawn.file, targetRank, enPassantInfo.captureFile, E_MoveFlag.EnPassant);
            enPassantMoves.push(enPassant);
        }

        return enPassantMoves;
    }
}

Final Result

class MoveGenerator {
    /**
     * @param {Pawn[]} pawns 
     * @param {BoardImplementation} board 
     * @returns Array of en passant moves
     */
    generateEnPassantMoves(pawns, board) {
        let enPassantMoves = [];
        let enPassantInfo = board.getEnPassantInfo();
        for (let pawn of pawns) {
            //The en passant capture must be performed on the turn immediately after the pawn being captured moves.
            if (enPassantInfo.rightToEnPassant === false) continue;

            //The capturing pawn must have advanced exactly three ranks to perform this move.
            if (pawn.rank !== ENPASSANT_CAPTURING_RANKS[pawn.color]) continue;

            //The captured pawn must be right next to the capturing pawn.
            let rankDiff = Math.abs(enPassantInfo.captureRank - pawn.rank);
            let fileDiff = Math.abs(enPassantInfo.captureFile - pawn.file);
            if (fileDiff !== 1 || rankDiff !== 0) continue;

            //You move your pawn diagonally to an adjacent square, one rank farther from where it had been, 
            //on the same file where the enemy's pawn is.
            let targetRank = pawn.color === E_PieceColor.White ? pawn.rank + 1 : pawn.rank - 1;
            let enPassant = new Move(pawn.rank, pawn.file, targetRank, enPassantInfo.captureFile, E_MoveFlag.EnPassant);
            enPassantMoves.push(enPassant);
        }
        return enPassantMoves;
    }
}

References