Piece Rules: Pawn

Introduction

Now that we have a clear picture of how everything works at the top level, we can start with the fun part. That is, banging our heads against the keyboard while implementing the classes, methods, and interfaces we’ve defined in the architecture.

Pawns are interesting pieces because, at first glance, they seem really ordinary and simple to understand compared to the rest of the pieces. However, pawns have the most elaborated rules out of all chess. They can really give us a surprise if we underestimate them. And I’m talking both in the game and in the code.

Pawns have 4 basic moves: moving forward one square, moving forward two squares, capturing to the left, and capturing to the right. Let’s take a close look at each one.

Moving forward

The only rule for moving forward is that there must not be a piece in front of the pawn. This rule is pretty straightforward to code:

class Pawn extends Piece{
    
    getType(){
        return E_PieceType.Pawn;
    }

    /**
     * @param {BoardImplementation} board 
     * @returns Set of pawn moves
     */
    getMoves(board) {
        let oneSquareFront;

        //calculate destination squares based on color 
        switch (this.color) {
            case E_PieceColor.White:
                oneSquareFront = (this.position << 8n);
                break;
            case E_PieceColor.Black:
                oneSquareFront = (this.position >> 8n);
                break;
            case E_PieceColor.None:
                throw new Error("No color specified");
            default:
                throw new Error("No color specified");
        }

        //calculate front move
        let frontMove = oneSquareFront &
            board.getEmptySpaces(); //target square must be empty

        return frontJump;
    }
}

We first obtain the square in front of our current position. For that, we shift the bit that represents the pawn’s position to the left or right, depending on the color of the pawn that’s moving. Shifting to the left will move the position from left to right and top to bottom. Shifting to the right will move the position from right to left and top to bottom.

Then, we make sure the square is empty by retrieving the empty squares from Board and applying this filter. The ‘&’ operator [1] will return every square that is in front of the pawn and that is empty.

Captures

A pawn can capture if there’s a piece of the opposite color in the front and diagonally. We consider the same right direction regardless of the color, which is the right side from the point of view of the white pieces.

class Pawn extends Piece{

    getType(){
        return E_PieceType.Pawn;
    }

    /**
     * @param {BoardImplementation} board 
     * @returns Set of pawn moves
     */
    getMoves(board) {
        let oneSquareFront;
        let rightDiagonalSquare;
        let leftDiagonalSquare;

        //calculate destination squares based on color 
        switch (this.color) {
            case E_PieceColor.White:
                oneSquareFront = (this.position << 8n);
                rightDiagonalSquare = (this.position << 7n);
                leftDiagonalSquare = (this.position << 9n);
                break;
            case E_PieceColor.Black:
                oneSquareFront = (this.position >> 8n);
                rightDiagonalSquare = (this.position >> 9n);
                leftDiagonalSquare = (this.position >> 7n);
                break;
            case E_PieceColor.None:
                throw new Error("No color specified");
            default:
                throw new Error("No color specified");
        }

        //calculate front move
        let frontMove = oneSquareFront &
            board.getEmptySpaces(); //target square must be empty

        //calculate capturing moves
        let rightCapture = rightDiagonalSquare &
            board.getOccupied(OppositePieceColor(this.color)); //There's an enemy piece in that square

        let leftCapture = leftDiagonalSquare &
            board.getOccupied(OppositePieceColor(this.color)); //There's an enemy piece in that square

        return frontJump | rightCapture | leftCapture;
    }
}

We apply the same strategy as before to get the squares in the front right and front left part of the current position. Then, we ask the board to retrieve the squares occupied by enemy pieces, and we filter the square bitboard. The ‘&’ operator will return every square that is placed diagonally and that is occupied by an enemy piece.

Oops! My pawn teleports

Let’s see how the rules look so far:

Pretty cool, right? But, did you notice something strange? Check the pawns in the first and last file!

That’s right, these pawns are illegally making captures by teleporting from the first rank to the last, or the other way around. Dirty cheaters! Let’s fix that:

class Pawn extends Piece{

    getType(){
        return E_PieceType.Pawn;
    }

    /**
     * @param {BoardImplementation} board 
     * @returns Set of pawn moves
     */
    getMoves(board) {
        let oneSquareFront;
        let rightDiagonalSquare;
        let leftDiagonalSquare;

        //calculate destination squares based on color 
        switch (this.color) {
            case E_PieceColor.White:
                oneSquareFront = (this.position << 8n);
                rightDiagonalSquare = (this.position << 7n);
                leftDiagonalSquare = (this.position << 9n);
                break;
            case E_PieceColor.Black:
                oneSquareFront = (this.position >> 8n);
                rightDiagonalSquare = (this.position >> 9n);
                leftDiagonalSquare = (this.position >> 7n);
                break;
            case E_PieceColor.None:
                throw new Error("No color specified");
            default:
                throw new Error("No color specified");
        }

        //calculate front move
        let frontMove = oneSquareFront &
            board.getEmptySpaces(); //target square must be empty

        //calculate capturing moves
        let rightCapture = rightDiagonalSquare &
            board.getOccupied(OppositePieceColor(this.color)) & //There's an enemy piece in that square
            ~getFile(1); //remove right capture from 8th file to 1st file


        let leftCapture = leftDiagonalSquare &
            board.getOccupied(OppositePieceColor(this.color)) & //There's an enemy piece in that square
            ~getFile(8); //remove right capture from 1st file to 8th file

        return frontJump | rightCapture | leftCapture;
    }
}

Now, we prohibit left captures in the last file and right captures in the first file.

Code
//pawn-devlog/pawn-teleport.js

let game;
const GAME_MARGIN = 50;
const EXTRA_WIDTH_BUTTONS = 70;
const FEN = '8/8/p1p1p2P/7p/8/P2P1P2/8/8'
function setup() {
    createCanvas(500 + GAME_MARGIN + EXTRA_WIDTH_BUTTONS, Chess.GAME_DIMENSIONS.HEIGHT + GAME_MARGIN);
    game = new Chess.Game(GAME_MARGIN / 2, GAME_MARGIN / 2, FEN);
    game.setGameMode(Chess.E_GameMode.FREE);
}

function draw() {
    background(255);
    game.update();
}

That’s much better!

Front Jump ®

I came up with the great idea of calling front jump ® to a pawn moving two squares front. The rules to perform a front jump are:

  1. The square the pawn is jumping to must be empty.
  2. The square the pawn is jumping over must also be empty.
  3. The front jump is possible only if it’s the pawn’s first move.

Before showing the code, and considering what we’ve seen so far, try coming up with the implementation of these 3 rules yourself.

Exercise 

Implement the rules to perform a front jump.

See Solution
class Pawn extends Piece{

    getType(){
        return E_PieceType.Pawn;
    }

    /**
     * @param {BoardImplementation} board 
     * @returns Set of pawn moves
     */
    getMoves(board) {
        let oneSquareFront;
        let rightDiagonalSquare;
        let leftDiagonalSquare;
        let twoSquaresFront;
        let targetRankForJumping;

        //calculate destination squares based on color 
        switch (this.color) {
            case E_PieceColor.White:
                oneSquareFront = (this.position << 8n);
                rightDiagonalSquare = (this.position << 7n);
                leftDiagonalSquare = (this.position << 9n);
                twoSquaresFront = (this.position << 16n);
                targetRankForJumping = 4;
                break;
            case E_PieceColor.Black:
                oneSquareFront = (this.position >> 8n);
                rightDiagonalSquare = (this.position >> 9n);
                leftDiagonalSquare = (this.position >> 7n);
                twoSquaresFront = (this.position >> 16n);
                targetRankForJumping = 5;
                break;
            case E_PieceColor.None:
                throw new Error("No color specified");
            default:
                throw new Error("No color specified");
        }

        //calculate front move
        let frontMove = oneSquareFront &
            board.getEmptySpaces(); //target square must be empty

        //calculate front jump
        let frontJump = twoSquaresFront &
            board.getEmptySpaces() & //target square is empty 
            getBooleanBitboard(frontMove > 1) & //a front move is possible
            getRank(targetRankForJumping); //pawn can only jump from their initial rank

        //calculate capturing moves
        let rightCapture = rightDiagonalSquare &
            board.getOccupied(OppositePieceColor(this.color)); //There's an enemy piece in that square
            ~getFile(1); //remove right capture from 8th file to 1st file


        let leftCapture = leftDiagonalSquare &
            board.getOccupied(OppositePieceColor(this.color)); //There's an enemy piece in that square
            ~getFile(8); //remove right capture from 1st file to 8th file

        return frontJump | frontMove | leftCapture | rightCapture;
    }
}

For the first rule, the same approach is used to make sure the square the pawn is jumping to is empty.

Now, for the second rule, a front jump is possible if a front move is possible because the front square must be empty. Therefore, we use a little function called getBooleanBitboard. This function returns 0 if a front move is not possible (frontMove is equal to zero). Then, when applying ‘&’, frontJump turns to zero (that is, a front jump is not possible).

Otherwise, we return a bitboard full of ones which, when applying the ‘&’ operator, leaves the bitboard unaltered ( 1 & 1 equals 1, 1 & 0 equals 0).

For the last rule, we ensure that the jump lands two ranks in front of the starting rank of the pawn. That is the 4th rank for white, which starts at the 2nd rank, and the 5th rank for black, which starts at the 7th rank. In this case, we assume pawns will always start from the starting rank or beyond. A game where a pawn starts from the first or eighth rank is illegal anyway.

Final result

Code
//pawn-devlog/pawn-final.js

let game;
const GAME_MARGIN = 50;
const EXTRA_WIDTH_BUTTONS = 70;
const JUST_PAWNS_FEN = '8/pppppppp/8/8/8/8/PPPPPPPP/8';
function setup() {
    createCanvas(Chess.GAME_DIMENSIONS.WIDTH + GAME_MARGIN + EXTRA_WIDTH_BUTTONS, Chess.GAME_DIMENSIONS.HEIGHT + GAME_MARGIN);
    game = new Chess.Game(GAME_MARGIN / 2, GAME_MARGIN / 2, JUST_PAWNS_FEN);
    game.setGameMode(Chess.E_GameMode.FREE);
}

function draw() {
    background(255);
    game.update();
}

References