🚧 This page is still under construction. Check back soon for updates! 🚧

Building a Magic Bitboard Chess Engine in JavaScript

In this tutorial, I’ll walk you through how I created a high-performance chess engine using magic bitboards and other techniques for efficient move generation.

Table of Contents

Preface and Acknowledgements

Placeholder: This section will include a preface and acknowledgements.

Computer Chess Overview

Placeholder: Introduce computer chess concepts and history.


/*
Bit Map With Coordinates

a8:56  b8:57  c8:58  d8:59  e8:60  f8:61  g8:62  h8:63
a7:48  b7:49  c7:50  d7:51  e7:52  f7:53  g7:54  h7:55
a6:40  b6:41  c6:42  d6:43  e6:44  f6:45  g6:46  h6:47
a5:32  b5:33  c5:34  d5:35  e5:36  f5:37  g5:38  h5:39
a4:24  b4:25  c4:26  d4:27  e4:28  f4:29  g4:30  h4:31
a3:16  b3:17  c3:18  d3:19  e3:20  f3:21  g3:22  h3:23
a2:8   b2:9   c2:10  d2:11  e2:12  f2:13  g2:14  h2:15
a1:0   b1:1   c1:2   d1:3   e1:4   f1:5   g1:6   h1:7


Bit Map Clean

56  57  58  59  60  61  62  63
48  49  50  51  52  53  54  55
40  41  42  43  44  45  46  47
32  33  34  35  36  37  38  39
24  25  26  27  28  29  30  31
16  17  18  19  20  21  22  23
8   9   10  11  12  13  14  15
0   1   2   3   4   5   6   7


*/
      

Computer Chess Challenges

Placeholder: Discuss the main challenges in building a chess engine.


export const NOT_A = (FILE[1]|FILE[2]|FILE[3]|FILE[4]|FILE[5]|FILE[6]|FILE[7]);
export const NOT_AB = (FILE[2]|FILE[3]|FILE[4]|FILE[5]|FILE[6]|FILE[7]);
export const NOT_B = (FILE[0]|FILE[2]|FILE[3]|FILE[4]|FILE[5]|FILE[6]|FILE[7]);
export const NOT_C = (FILE[0]|FILE[1]|FILE[3]|FILE[4]|FILE[5]|FILE[6]|FILE[7]);
export const NOT_D = (FILE[0]|FILE[1]|FILE[2]|FILE[4]|FILE[5]|FILE[6]|FILE[7]);
export const NOT_E = (FILE[0]|FILE[1]|FILE[2]|FILE[3]|FILE[5]|FILE[6]|FILE[7]);
export const NOT_F = (FILE[0]|FILE[1]|FILE[2]|FILE[3]|FILE[4]|FILE[6]|FILE[7]);
export const NOT_G = (FILE[0]|FILE[1]|FILE[2]|FILE[3]|FILE[4]|FILE[5]|FILE[7]);
export const NOT_H = (FILE[0]|FILE[1]|FILE[2]|FILE[3]|FILE[4]|FILE[5]|FILE[6]);
export const NOT_GH = (FILE[0]|FILE[1]|FILE[2]|FILE[3]|FILE[4]|FILE[5]);

export const NOT_1 = ( RANK[1]| RANK[2]| RANK[3]| RANK[4]| RANK[5]| RANK[6]| RANK[7]);
export const NOT_1_2 = ( RANK[2]| RANK[3]| RANK[4]| RANK[5]| RANK[6]| RANK[7]);
export const NOT_8 = ( RANK[0]| RANK[1]| RANK[2]| RANK[3]| RANK[4]| RANK[5]| RANK[6]);
export const NOT_7_8 = ( RANK[0]| RANK[1]| RANK[2]| RANK[3]| RANK[4]| RANK[5]);
      

export function getWhitePawnThreatMap(pawnMap){
    return ((pawnMap & BoardState.NOT_A) << 7n) | ((pawnMap & BoardState.NOT_H) << 9n); 
}

export function getBlackPawnThreatMap(pawnMap){
    return ((pawnMap & BoardState.NOT_A) >> 9n) | ((pawnMap & BoardState.NOT_H) >> 7n);  
}
      

Simple Approaches To Computer Chess

Placeholder: Overview of simple algorithms for chess AI.


export const PIECE_TYPE = {
    pawn: 0,
    knight: 1,
    bishop: 2,
    rook: 3,
    queen: 4,
    king: 5,
};

export const PIECE_COLOR = {
    white : 0,
    black : 1,
};

export const MOVE_TYPE = {
    normal: 0,
    castleKingside : 1,
    castleQueenside: 2,
    promotion: 3,
    enPassant: 4,
};
      

Computational Issues With Simple Approaches

Placeholder: Explain why simple methods are computationally expensive.


function generateChessRays(rookRays){
    const rayBuffer = new ArrayBuffer(4096*8);
    let rays = new BigUint64Array(rayBuffer).fill(0n);    
    
    for(let start = 0; start < 64; start++){
        for(let end = 0; end < 64; end++){
            let index = (start * 64) + end;
            
            if (start == end) {
                rays[index] = 0n;
                continue;
            }

            let sign = 1;
            if (end < start) {sign = -1;}

            let success = false;
            let newRay = 0n;

            
            if (rookRays){
                //Try horizontal ray
                [newRay, success] = tryCreateRayInclusive(start, end, 1, sign, false);
                if (success){
                    rays[index] = newRay;
                    //console.log("horizontal ray created from: ", start, " to ", end);
                    continue;
                }

                //Try vertical ray
                [newRay, success] = tryCreateRayInclusive(start, end, 8, sign, true);
                if (success){
                    rays[index] = newRay;
                    //console.log("vertical ray created from: ", start, " to ", end);
                    continue;
                }
            } else {            
                //Try up-right diagonal ray
                [newRay, success] = tryCreateRayInclusive(start, end, 9, sign, false);
                if (success){
                    rays[index] = newRay;
                    //console.log("diagonal ray created from: ", start, " to ", end);
                    continue;
                }

                //Try down-left diagonal ray
                [newRay, success] = tryCreateRayInclusive(start, end, 7,sign, false);
                if (success){
                    rays[index] = newRay;
                    //console.log("diagonal ray created from: ", start, " to ", end);
                    continue;
                }
            }

            //No ray was able to be created
            //console.log("no ray created from: ", start, " to ", end);
            rays[index] = 0n;
        }           
    }

    return rays;
}
      

Bitwise Computations

Placeholder: Introduce bitwise operations used in chess programming.


export function isBlackPawnAtMask(board, mask){
    return (board.blackPawns & mask) !== 0n;
}

//Black Knight
export function isBlackKnightAtMask(board, mask){
    return (board.blackKnights & mask) !== 0n;
}

//Black Bishop
export function isBlackBishopAtMask(board, mask){
    return (board.blackBishops & mask) !== 0n;
}

//Black Rook
export function isBlackRookAtMask(board, mask){
    return (board.blackRooks & mask) !== 0n;
}

//Black Queen
export function isBlackQueenAtMask(board, mask){
    return (board.blackQueens & mask) !== 0n;
}

//Black King
export function isBlackKingAtMask(board, mask){
    return (board.blackKing & mask) !== 0n;
}
      

//All Pieces
export function isAnyPieceAtMask(board, mask){
    return (board.allPieces & mask) !== 0n;
}

//White Pieces
export function isWhitePieceAtMask(board, mask){
    return (board.whitePieces & mask) !== 0n;
}

//Black Pieces
export function isBlackPieceAtMask(board, mask){
    return (board.blackPieces & mask) !== 0n;
}
      

Overview of Bitboards

Placeholder: Explain bitboards as 64-bit integers representing chessboards.


export class ChessBoard {
// Starting Positions
    constructor(){
        this.whiteRooks = 0b0000000000000000000000000000000000000000000000000000000010000001n; // bits 0 and 7
        this.blackRooks = 0b1000000100000000000000000000000000000000000000000000000000000000n; // bits 56 and 63 

        this.whiteKnights = 0b0000000000000000000000000000000000000000000000000000000001000010n; // bits 1 and 6
        this.blackKnights = 0b0100001000000000000000000000000000000000000000000000000000000000n; // bits 57 and 62 

        this.blackBishops = 0b0010010000000000000000000000000000000000000000000000000000000000n; // bits 58 and 61
        this.whiteBishops = 0b0000000000000000000000000000000000000000000000000000000000100100n; // bits 2 and 5

        this.blackQueens = 0b0000100000000000000000000000000000000000000000000000000000000000n; // bit 59
        this.whiteQueens = 0b0000000000000000000000000000000000000000000000000000000000001000n; // bit 5

        this.blackKing = 0b0001000000000000000000000000000000000000000000000000000000000000n; // bit 60
        this.whiteKing = 0b0000000000000000000000000000000000000000000000000000000000010000n; // bit 4

        this.blackPawns = 0b0000000011111111000000000000000000000000000000000000000000000000n; // bits 48 through 55
        this.whitePawns = 0b0000000000000000000000000000000000000000000000001111111100000000n; // bits 8 through 15

        this.whitePieces = 0b0000000000000000000000000000000000000000000000000000000000000000n; // no bits to start
        this.blackPieces = 0b0000000000000000000000000000000000000000000000000000000000000000n; // no bits to start

        this.allPiecesNoWhiteKing = 0b0000000000000000000000000000000000000000000000000000000000000000n; // no bits to start
        this.allPiecesNoBlackKing = 0b0000000000000000000000000000000000000000000000000000000000000000n; // no bits to start

        this.allPieces = 0b0000000000000000000000000000000000000000000000000000000000000000n; // no bits to start

        this.notWhitePieces = ALL_ONES; // inverse of whitePieces
        this.notBlackPieces = ALL_ONES; // inverse of blackPieces
        this.emptySquares = ALL_ONES; // Set default to all Squares
        this.checkMask = ALL_ONES; //Set default to all squares.

        this.whiteAttackMap = 0b0000000000000000000000000000000000000000000000000000000000000000n; // no bits to start
        this.blackAttackMap = 0b0000000000000000000000000000000000000000000000000000000000000000n; // no bits to start

        this.pinMasks = [
            0b0000000000000000000000000000000000000000000000000000000000000000n, // no bits to start
            0b0000000000000000000000000000000000000000000000000000000000000000n, // no bits to start
            0b0000000000000000000000000000000000000000000000000000000000000000n, // no bits to start
            0b0000000000000000000000000000000000000000000000000000000000000000n, // no bits to start
            0b0000000000000000000000000000000000000000000000000000000000000000n, // no bits to start
            0b0000000000000000000000000000000000000000000000000000000000000000n, // no bits to start
            0b0000000000000000000000000000000000000000000000000000000000000000n, // no bits to start
            0b0000000000000000000000000000000000000000000000000000000000000000n, // no bits to start
        ];
        this.pinMapFull = 0b0000000000000000000000000000000000000000000000000000000000000000n; // no bits to start
        this.totalPins = 0;
        

        this.whiteToMove = 1;
        this.whiteKingIndex = 4;
        this.blackKingIndex = 60;
        this.whiteKingIndexBig = 4n;
        this.blackKingIndexBig = 60n;
        this.totalChecks = 0;
        this.checkmate = 1; //0 is checkmate, 1 is not checkmate
        this.whiteCanCastleKingside = 1;
        this.whiteCanCastleQueenside = 1;
        this.blackCanCastleKingside = 1;
        this.blackCanCastleQueenside = 1;
        this.enPassantTarget = 0b0000000000000000000000000000000000000000000000000000000000000000n; // no bits to start
        }
}
      

board.whitePieces = board.whitePawns | board.whiteKnights | board.whiteBishops | board.whiteRooks | board.whiteQueens;
board.blackPieces = board.blackPawns | board.blackKnights | board.blackBishops | board.blackRooks | board.blackQueens;
board.allPieces = board.whitePieces | board.blackPieces;
board.allPiecesNoWhiteKing = board.allPieces | board.blackKing;
board.allPiecesNoBlackKing = board.allPieces | board.whiteKing;
board.whitePieces |= board.whiteKing;
board.blackPieces |= board.blackKing;
board.allPieces |= board.whiteKing | board.blackKing;

// Update precomputed inverse masks
board.notWhitePieces = ALL_ONES ^ board.whitePieces;
board.notBlackPieces = ALL_ONES ^ board.blackPieces;
board.emptySquares = ALL_ONES ^ board.allPieces;
      

function moveWhiteKnight(board, fromMask, toMask){
    board.whiteKnights &= ~fromMask; //Remove the old location
    board.whiteKnights |= toMask;  //Add to the new location
    captureBlackPiece(board, toMask);
}
      

Fancy Magic Numbers

Placeholder: Explain magic numbers for fast move generation.


export function getBishopAttacks(occupancyMask, locationIndex){
    const occupancy = BishopMove.blockerMaskTable[locationIndex] & occupancyMask;
    const attackIndex = BigInt.asUintN(64, occupancy * BishopMagics.bishopMagics[locationIndex])>> BishopMagics.bishopShifts[locationIndex];
    const attackIndexOffset = attackIndex + BishopMagics.bishopOffsets[locationIndex];
    return BishopMagics.bishopAttackData[Number(attackIndexOffset)];
 }
        

function generateAllMagicLookups(isRook, isXrayTable) {
    const occupancyMasks = isRook ? generateRookBlockerMaskTable() : generateBishopBlockerMaskTable();
    let magics = [];
    let shifts = [];
    let indexOffsets = [];
    currentOffset = 0;

    for (let squareIndex = 0; squareIndex < 64; squareIndex++) {
        console.log("Looking for magics for: " + squareIndex);
        indexOffsets.push(BigInt(currentOffset));
        let { magic, shift } = findMagic(squareIndex, occupancyMasks[squareIndex], isRook, isXrayTable);        
        magics.push(magic);
        shifts.push(shift);        
    }

    if(isXrayTable){
        if (isRook){
        RookXRayMagics.setAttackData(rookXRayFlatAttackTable);
        RookXRayMagics.setOffsets(indexOffsets);
        RookXRayMagics.setShifts(shifts);
        RookXRayMagics.setMagics(magics);
    } else {
        BishopXRayMagics.setAttackData(bishopXRayFlatAttackTable);
        BishopXRayMagics.setOffsets(indexOffsets);
        BishopXRayMagics.setShifts(shifts);
        BishopXRayMagics.setMagics(magics);
    }

    } else {
        if (isRook){
        RookMagics.setAttackData(rookFlatAttackTable);
        RookMagics.setOffsets(indexOffsets);
        RookMagics.setShifts(shifts);
        RookMagics.setMagics(magics);
    } else {
        BishopMagics.setAttackData(bishopFlatAttackTable);
        BishopMagics.setOffsets(indexOffsets);
        BishopMagics.setShifts(shifts);
        BishopMagics.setMagics(magics);
    }

    }
    return { magics, shifts, offsets: indexOffsets};
}
      

Non-Sliding Pieces

Placeholder: Handling knights and pawns efficiently.


export function getKnightAttacks(locationIndex){
    return KnightMove.moveTable[locationIndex];
}

//Move Table Snippet 
moveTable[0] = 0b0000000000000000000000000000000000000000000000100000010000000000n;
moveTable[1] = 0b0000000000000000000000000000000000000000000001010000100000000000n;
moveTable[2] = 0b0000000000000000000000000000000000000000000010100001000100000000n;
moveTable[3] = 0b0000000000000000000000000000000000000000000101000010001000000000n;
moveTable[4] = 0b0000000000000000000000000000000000000000001010000100010000000000n;
moveTable[5] = 0b0000000000000000000000000000000000000000010100001000100000000000n;
//etc...
        

Handling Checks

Placeholder: Explain check detection in the engine.


export function countChecksOnWhiteKing(board){
    let checkingPieces = (PawnMove.captureTableWhite[board.whiteKingIndex] & board.blackPawns);
    checkingPieces |= getKnightAttacks(board.whiteKingIndex) & board.blackKnights;
    let lsb = checkingPieces & -checkingPieces;
    let locationIndex = index64n[Number(((lsb * deBruijn64) >> 58n)& 63n)];
    detectKnightPawnCheckFunctions[Number(checkingPieces >> locationIndex)](board, checkingPieces);

    checkingPieces = getBishopAttacks(board.allPieces, board.whiteKingIndex) & (board.blackBishops | board.blackQueens);
    checkingPieces |= getRookAttacks(board.allPieces, board.whiteKingIndex) & (board.blackRooks | board.blackQueens);
    while(checkingPieces){
        lsb = checkingPieces & -checkingPieces;
        locationIndex = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
        board.totalChecks += 1;
        board.checkMask = ChessRays.allInclusiveRays[(board.whiteKingIndex*64)+locationIndex];
        checkingPieces ^= lsb;
    }

    detectDoubleCheckFunctions[board.totalChecks](board);
}
        

export function setWhiteAttackMap(board){
    let whiteKnightBits = board.whiteKnights;
    let whiteBishopQueenBits = board.whiteBishops | board.whiteQueens;
    let whiteRookQueenBits = board.whiteRooks | board.whiteQueens;
    board.whiteAttackMap = ((board.whitePawns & BoardState.NOT_A) << 7n) | ((board.whitePawns & BoardState.NOT_H) << 9n);
    board.whiteAttackMap |= KingMove.moveTable[board.whiteKingIndex]; 

    let lsb = 0
    let locationIndex = 0;
    
    while(whiteKnightBits){
        lsb = whiteKnightBits & -whiteKnightBits;
        locationIndex = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
        board.whiteAttackMap |= KnightMove.moveTable[locationIndex];
        whiteKnightBits ^= lsb;
    }
    
    
    while(whiteBishopQueenBits){
        lsb = whiteBishopQueenBits & -whiteBishopQueenBits;
        locationIndex = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
        board.whiteAttackMap |= getBishopAttacks(board.allPiecesNoBlackKing, locationIndex);
        whiteBishopQueenBits ^= lsb;
    }

    while(whiteRookQueenBits){
        lsb = whiteRookQueenBits & -whiteRookQueenBits;
        locationIndex = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
        board.whiteAttackMap |= getRookAttacks(board.allPiecesNoBlackKing, locationIndex);
        whiteRookQueenBits ^= lsb;
    }    
}
      

The Issue of Pins

Placeholder: How pins affect legal move generation.


function setAllPinsOnBlackKing(board){
    let occupancy = BishopMove.blockerMaskTable[board.blackKingIndex] & board.allPieces;
    let xRayIndex = BigInt.asUintN(64, occupancy * BishopXRayMagics.bishopXRayMagics[board.blackKingIndex])>> BishopXRayMagics.bishopXRayShifts[board.blackKingIndex];
    let xRayIndexOffset = xRayIndex + BishopXRayMagics.bishopXRayOffsets[board.blackKingIndex];
    let xRayMap = BishopXRayMagics.bishopXRayAttackData[Number(xRayIndexOffset)]& (board.whiteBishops | board.whiteQueens);

    occupancy = RookMove.blockerMaskTable[board.blackKingIndex] & board.allPieces;
    xRayIndex = BigInt.asUintN(64, occupancy * RookXRayMagics.rookXRayMagics[board.blackKingIndex])>> RookXRayMagics.rookXRayShifts[board.blackKingIndex];
    xRayIndexOffset = xRayIndex + RookXRayMagics.rookXRayOffsets[board.blackKingIndex];
    xRayMap |= RookXRayMagics.rookXRayAttackData[Number(xRayIndexOffset)] & (board.whiteRooks | board.whiteQueens);

    let pinCount = 0;
    while (xRayMap){
        const lsb = xRayMap & -xRayMap;
        const pinIndex = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
        xRayMap ^= lsb;
        board.pinMasks[pinCount] = ChessRays.allInclusiveRays[(board.blackKingIndex*64)+pinIndex];
        board.pinMapFull |= board.pinMasks[pinCount];
        pinCount++;
    }

    board.totalPins = pinCount;

    const enIndex = index64n[Number(((board.enPassantTarget * deBruijn64) >> 58n)& 63n)];
    checkBlackEnPassantLegalityFunctions[Number(board.enPassantTarget >> enIndex)](board);
}
      

Fancy X-Ray Magic Numbers

Placeholder: Advanced techniques for sliding pieces and x-ray attacks.


// Code Snippet
export function generateRookXRayTable(square, occupancyMask) {
    const { file, rank } = indexToCoord(square);
    let xRayTargets = 0n;
    let potentialPinnedPieces = 0;

    // Right (file++)
    for (let f = file + 1; f < 8; f++) {
        const idx = coordToIndex(f, rank);
        if (bitAt(idx, occupancyMask)) {
            if (potentialPinnedPieces == 1){
                xRayTargets |= 1n << BigInt(idx);
                break;
            }
            potentialPinnedPieces++;
            continue;
        }  
        
        if ((f == 7) && (potentialPinnedPieces == 1)){
            xRayTargets |= 1n << BigInt(idx);
            break;
        }        
    }

    potentialPinnedPieces = 0;
    // Left (file--)
    for (let f = file - 1; f >= 0; f--) {
        const idx = coordToIndex(f, rank);
        if (bitAt(idx, occupancyMask)) {
            if (potentialPinnedPieces == 1){
                xRayTargets |= 1n << BigInt(idx);
                break;
            }
            potentialPinnedPieces++;
            continue;
        }    

        if ((f == 0) && (potentialPinnedPieces == 1)){
            xRayTargets |= 1n << BigInt(idx);
            break;
        }  
    }

    potentialPinnedPieces = 0;
    // Up (rank++)
    for (let r = rank + 1; r < 8; r++) {
        const idx = coordToIndex(file, r);
        if (bitAt(idx, occupancyMask)) {
            if (potentialPinnedPieces == 1){
                xRayTargets |= 1n << BigInt(idx);
                break;
            }
            potentialPinnedPieces++;
            continue;
        }    
        if ((r == 7) && (potentialPinnedPieces == 1)){
            xRayTargets |= 1n << BigInt(idx);
            break;
        }  
    }

    potentialPinnedPieces = 0;
    // Down (rank--)
    for (let r = rank - 1; r >= 0; r--) {
        const idx = coordToIndex(file, r);
        if (bitAt(idx, occupancyMask)) {
            if (potentialPinnedPieces == 1){
                xRayTargets |= 1n << BigInt(idx);
                break;
            }
            potentialPinnedPieces++;
            continue;
        }
        if ((r == 0) && (potentialPinnedPieces == 1)){
            xRayTargets |= 1n << BigInt(idx);
            break;
        }      
    }
    return xRayTargets;
}
      

Detecting Checkmate

Placeholder: Logic for detecting checkmate positions.


// Code Snippet
function detectSingleCheckmateWhite(board){
    let blockOnly = board.checkMask & board.emptySquares;
    const captureOnly = board.checkMask & board.blackPieces;
    //All pawn moves can be grabbed in one batch
    let pawnBlocks = (board.whitePawns << 8n) & board.emptySquares;
    pawnBlocks |= ((pawnBlocks & BoardState.RANK[2]) << 8n) & board.emptySquares;
    let legalMoveMask = pawnBlocks & blockOnly;

    //Chek for knight and sliding piece blockers
    while(blockOnly){
        const lsb = blockOnly & -blockOnly;
        const squareBlock = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
        legalMoveMask |= getKnightAttacks(squareBlock) & board.whiteKnights; //Knight block
        legalMoveMask |= getBishopAttacks(board.allPieces, squareBlock) & (board.whiteBishops | board.whiteQueens); //Bishop/Queen Block
        legalMoveMask |= getRookAttacks(board.allPieces, squareBlock) & (board.whiteRooks | board.whiteQueens); //Rook/Queen Block
        blockOnly ^= lsb; //Pop the bit
    }

    const lsb = captureOnly & -captureOnly;
    const squareBlock = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
    legalMoveMask |= getKnightAttacks(squareBlock) & board.whiteKnights; //Knight Capture
    legalMoveMask |= getBishopAttacks(board.allPieces, squareBlock) & (board.whiteBishops | board.whiteQueens); //Bishop/Queen Capture
    legalMoveMask |= getRookAttacks(board.allPieces, squareBlock) & (board.whiteRooks | board.whiteQueens); //Rook/Queen Capture
    
    let legalPawnCaptureMask = ((board.enPassantTarget >> 8n) & captureOnly) << 8n;
    legalPawnCaptureMask |= captureOnly;    
    legalMoveMask |= getBlackPawnThreatMap(legalPawnCaptureMask) & board.whitePawns; //Pawn Capture stops mate

    //Get rid of any pinned pieces
    legalMoveMask &= ~board.pinMapFull;
    
    //Add any legal king moves
    legalMoveMask |= getLegalWhiteKingMoves(board, board.whiteKingIndex);

    const lsbC = legalMoveMask & -legalMoveMask;
    //Set the checkmate
    board.checkmate = lsbC >> index64n[Number(((lsbC * deBruijn64) >> 58n)& 63n)];
}
        

function detectDoubleCheckmateWhite(board){
    const legalMoves = getLegalWhiteKingMoves(board, board.whiteKingIndex);
    const lsb = legalMoves & -legalMoves;
    board.checkmate = lsb >> index64n[Number(((lsb * deBruijn64) >> 58n)& 63n)];
}
      

Chess and Edge Cases

Placeholder: Discuss the challenges that the special rules of chess pose upon efficient computation.


function checkEnPassantPinsOnBlackKing(board){
    //Ensure en passant cannot be done if king is horizontal to the potential pin
    const ignoreEnPassantPawn = (board.enPassantTarget << 8n); 
    const occupancy = RookMove.blockerMaskTable[board.blackKingIndex] & (board.allPieces ^ ignoreEnPassantPawn) & BoardState.RANK_OF_INDEX[board.blackKingIndex]; //Only horizontal pieces matter
    const xRayIndex = BigInt.asUintN(64, occupancy * RookXRayMagics.rookXRayMagics[board.blackKingIndex])>> RookXRayMagics.rookXRayShifts[board.blackKingIndex];
    const xRayIndexOffset = xRayIndex + RookXRayMagics.rookXRayOffsets[board.blackKingIndex];
    let xRayMap = RookXRayMagics.rookXRayAttackData[Number(xRayIndexOffset)] & (board.whiteRooks | board.whiteQueens);

    let enPassantPinMap = 0n;
    while (xRayMap){
        const lsb = xRayMap & -xRayMap;
        const pinIndex = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
        xRayMap ^= lsb;
        enPassantPinMap |= ignoreEnPassantPawn & ChessRays.allInclusiveRays[(board.blackKingIndex*64)+pinIndex];
    }
    
    //Remove any en passant capture that would result in a check
    let enPassantPinIndex = index64n[Number(((enPassantPinMap * deBruijn64) >> 58n)& 63n)];
    board.enPassantTarget = board.enPassantTarget * (1n - (enPassantPinMap>>enPassantPinIndex));
}
      

export function getDisambiguationTypeFromLocationArray(possibleLocations, start){
    let rankDisambiguation = true;
    let fileDisambiguation = true;

    for(let i = 0; i < possibleLocations.length; i++){
        if (sameRank(start, possibleLocations[i])){ rankDisambiguation = false;}
        if (sameFile(start, possibleLocations[i])){ fileDisambiguation = false;}
    }

    if (!rankDisambiguation && !fileDisambiguation){return 3;}
    if (fileDisambiguation) {return 2;}
    if (rankDisambiguation) {return 1;}    
    return 0;
}

export function getDisambiguationTypeFromPossiblePiecesMask(possiblePiecesMask, start){
    let possibleLocations = [];
    let foundStart = false;
    while (possiblePiecesMask){
        const lsb = possiblePiecesMask & -possiblePiecesMask;
        const pieceIndex = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
        possiblePiecesMask ^= lsb;
        if (pieceIndex == start){
                foundStart = true;
        } else {
            possibleLocations.push(pieceIndex);
        }
    }

    if (!foundStart){return 0;}

    if (possibleLocations){        
        if (possibleLocations.length >= 1){
            return getDisambiguationTypeFromLocationArray(possibleLocations, start);
        }
    } 

    return 0;
}
      

Castling

Placeholder: Rules and implementation of castling in the engine.


export const whiteKingsideRookStart = 0b0000000000000000000000000000000000000000000000000000000010000000n; // bit 7
export const whiteQueensideRookStart = 0b0000000000000000000000000000000000000000000000000000000000000001n; // bit 0
export const blackKingsideRookStart = 0b1000000000000000000000000000000000000000000000000000000000000000n; // bits 63 
export const blackQueensideRookStart = 0b0000000100000000000000000000000000000000000000000000000000000000n; // bits 56 

export const whiteKingsideCastleTarget = 0b0000000000000000000000000000000000000000000000000000000001000000n; // bit 6
export const whiteQueensideCastleTarget = 0b0000000000000000000000000000000000000000000000000000000000000100n; // bit 2
export const blackKingsideCastleTarget = 0b0100000000000000000000000000000000000000000000000000000000000000n; // bit 62
export const blackQueensideCastleTarget = 0b0000010000000000000000000000000000000000000000000000000000000000n; // bits 58

export const whiteRookKingsideCastleTarget = 0b0000000000000000000000000000000000000000000000000000000000100000n; // bit 5
export const whiteRookQueensideCastleTarget = 0b0000000000000000000000000000000000000000000000000000000000001000n; // bit 3
export const blackRookKingsideCastleTarget = 0b0010000000000000000000000000000000000000000000000000000000000000n; // bit 61
export const blackRookQueensideCastleTarget = 0b0000100000000000000000000000000000000000000000000000000000000000n; // bits 59

export const whiteKingSideCastleEmpty = 0b0000000000000000000000000000000000000000000000000000000001100000n;
export const whiteKingSideCastleSafe = 0b0000000000000000000000000000000000000000000000000000000001110000n;
export const whiteQueenSideCastleEmpty = 0b0000000000000000000000000000000000000000000000000000000000001110n;
export const whiteQueenSideCastleSafe = 0b0000000000000000000000000000000000000000000000000000000000011100n;

export const blackKingSideCastleEmpty = 0b0110000000000000000000000000000000000000000000000000000000000000n;
export const blackKingSideCastleSafe = 0b0111000000000000000000000000000000000000000000000000000000000000n;
export const blackQueenSideCastleEmpty = 0b0000111000000000000000000000000000000000000000000000000000000000n;
export const blackQueenSideCastleSafe = 0b0001110000000000000000000000000000000000000000000000000000000000n;
        

Pawn Promotion

Placeholder: Handling pawn promotion logic.


function promoteWhitePawnMove(board, fromMask, toMask){
    let pawnPromotionType = index64[Number(((toMask * deBruijn64) >> 58n)& 63n)];
    pawnPromotionType = ((pawnPromotionType - (pawnPromotionType % 8)-32) / 8);
    pawnPromotionType = 3-pawnPromotionType;
    invokeWhitePawnPromotionFunctions[pawnPromotionType](board,fromMask,toMask);
    board.enPassantTarget = 0n;
}

function promoteToWhiteQueen(board, fromMask, toMask){
    board.whitePawns &= ~fromMask; //Remove the old location
    board.whiteQueens |= toMask;  //Add to the new location
    captureBlackPiece(board, toMask);
}

function promoteToWhiteRook(board, fromMask, toMask){
    board.whitePawns &= ~fromMask; //Remove the old location
    board.whiteRooks |= (toMask << 8n);  //Add to the new location    
    captureBlackPiece(board, toMask << 8n);
}

function promoteToWhiteBishop(board, fromMask, toMask){
    board.whitePawns &= ~fromMask; //Remove the old location
    board.whiteBishops |= (toMask << 16n);  //Add to the new location 
    captureBlackPiece(board, toMask << 16n);
}

function promoteToWhiteKnight(board, fromMask, toMask){
    board.whitePawns &= ~fromMask; //Remove the old location
    board.whiteKnights |= (toMask << 24n);  //Add to the new location 
    captureBlackPiece(board, toMask << 24n);    
}
        

promotionSquares = [];
const file = promotionSquareSelected % 8n;
for (let i = 0n; i < 64n; i++) {
    if ((validMoves >> i) & 1n) {
        const { row, col } = indexToSquareCoord(Number(i));
        const square = squareGrid[row][col];
        if (square && (Number(i % 8n) == file )) {
            promotionSquares.push(i);
            square.classList.add('promotion-option');

            //Add the promotion piece render to the highlighted squares

            //Get the color
            let promotionColor = 'white';
            if (piece.color == 1){ promotionColor = 'black'; }

            //Get the piece
            let promotionPiece = 'queen';
            if ((i >= 48n && i <= 55n) || (i >= 8n  && i <= 15n)) {promotionPiece = 'rook';}
            if ((i >= 40n && i <= 47n) || (i >= 16n && i <= 23n)) {promotionPiece = 'bishop';}
            if ((i >= 32n && i <= 39n) || (i >= 24n && i <= 31n)) {promotionPiece = 'knight';}

            //get the image source
            const imgSrc = `images/${PIECE_IMAGE[promotionColor][promotionPiece]}`;

            //Create the element
            const promotionImg = document.createElement('img');
            promotionImg.src = imgSrc;
            promotionImg.alt = `${promotionColor} ${promotionPiece}`;
            square.appendChild(promotionImg);
        }
    }
}
      

En Passant

Placeholder: Implementing en passant captures.


export function getLegalWhitePawnMoves(board, locationIndex){
    let legalMask = (1n << BigInt(locationIndex+8)) & board.emptySquares;
    legalMask |= ((legalMask & BoardState.RANK[2]) << 8n) & board.emptySquares;
    legalMask |= getWhitePawnAttacks(locationIndex) & (board.blackPieces | board.enPassantTarget); 
    const enPassantCheckMask = ((board.enPassantTarget >> 8n) & board.checkMask) << 8n;
    legalMask = applyPins(legalMask, board, locationIndex) & (board.checkMask | enPassantCheckMask);
    const promotionSquares = legalMask & BoardState.RANK[7];
    legalMask |= (promotionSquares >> 8n) | (promotionSquares >> 16n) | (promotionSquares >> 24n);
    return legalMask;
}
        

function regularWhitePawnMove(board, fromMask, toMask){    
    board.whitePawns &= ~fromMask; //Remove the old location
    board.whitePawns |= toMask;  //Add to the new location

    //Check for en passant capture
    const enPassantCapture = (toMask & board.enPassantTarget) >> 8n;  //Capture the pawn 1 rank behind the target
    captureBlackPawn(board, enPassantCapture); 

    //Set en passant target
    board.enPassantTarget = (fromMask << 8n) & (toMask >> 8n) & getBlackPawnThreatMap(board.blackPawns); //This will be zero unless the pawn moved two squares and did not capture //It will also be zero if there are no potential captures
    captureBlackPiece(board, toMask);    
}
        

Limitations of JavaScript

Placeholder: Discuss performance or language limitations.


const getLegalMoveFunctions = [getLegalWhitePawnMoves, getLegalWhiteKnightMoves, getLegalWhiteBishopMoves, getLegalWhiteRookMoves, getLegalWhiteQueenMoves, getLegalWhiteKingMoves, getLegalBlackPawnMoves, getLegalBlackKnightMoves, getLegalBlackBishopMoves, getLegalBlackRookMoves, getLegalBlackQueenMoves, getLegalBlackKingMoves];
const moveWhitePieceFunctions = [moveWhitePawn, moveWhiteKnight, moveWhiteBishop, moveWhiteRook, moveWhiteQueen, moveWhiteKing];
const moveBlackPieceFunctions = [moveBlackPawn, moveBlackKnight, moveBlackBishop, moveBlackRook, moveBlackQueen, moveBlackKing];
const captureWhitePieceFunctions =[captureWhitePawn, captureWhiteKnight, captureWhiteBishop, captureWhiteRook, captureWhiteQueen, captureWhiteKing];
const captureBlackPieceFunctions = [captureBlackPawn, captureBlackKnight, captureBlackBishop, captureBlackRook, captureBlackQueen, captureBlackKing];
const capturePieceFunctions = [captureWhitePieceFunctions, captureBlackPieceFunctions];
const movePieceFunctions = [moveWhitePieceFunctions, moveBlackPieceFunctions];
const attackMapFunctions = [setWhiteAttackMap, setBlackAttackMap];
const setPinMapFunctions = [setAllPinsOnBlackKing, setAllPinsOnWhiteKing];
const applyPinFunctions = [noPin, applyPin];
const pinApplicationFunctions = [noPinsApplied, pinsApplied];
const detectKnightPawnCheckFunctions = [applyNoChecks, applyKnightPawnChecks];
const detectDoubleCheckFunctions = [noDoubleCheck, setSingleCheck, setDoubleCheck, setDoubleCheck, setDoubleCheck, setDoubleCheck, setDoubleCheck, setDoubleCheck, setDoubleCheck, setDoubleCheck, setDoubleCheck, setDoubleCheck];
const detectSingleCheckmateFunctions = [detectSingleCheckmateBlack, detectSingleCheckmateWhite];
const detectDoubleCheckmateFunctions = [detectDoubleCheckmateBlack, detectDoubleCheckmateWhite];
const detectCheckOnWhiteKingFunctions = [applyNoChecks, countChecksOnWhiteKing];
const detectCheckOnBlackKingFunctions = [applyNoChecks, countChecksOnBlackKing];
const detectCheckFunctions = [checkOnBlackKing, checkOnWhiteKing];
const flipWhiteToMove = [setWhiteToMove, setBlackToMove];
const revokeWhiteKingsideCastleFunctions = [noRevoke, revokeWhiteKingsideCastle];
const revokeWhiteQueensideCastleFunctions = [noRevoke, revokeWhiteQueensideCastle];
const revokeBlackKingsideCastleFunctions = [noRevoke, revokeBlackKingsideCastle];
const revokeBlackQueensideCastleFunctions = [noRevoke, revokeBlackQueensideCastle];
const invokeWhiteKingsideCastleFunctions = [noCastle, invokeWhiteKingsideCastle];
const invokeWhiteQueensideCastleFunctions = [noCastle, invokeWhiteQueensideCastle];
const invokeBlackKingsideCastleFunctions = [noCastle, invokeBlackKingsideCastle];
const invokeBlackQueensideCastleFunctions = [noCastle, invokeBlackQueensideCastle];

const checkWhiteKingsideCastlingMovesFunctions = [addNoCastlingMoves, checkWhiteKingsideCastling];
const checkWhiteQueensideCastlingMovesFunctions = [addNoCastlingMoves, checkWhiteQueensideCastling];
const checkBlackKingsideCastlingMovesFunctions = [addNoCastlingMoves, checkBlackKingsideCastling];
const checkBlackQueensideCastlingMovesFunctions = [addNoCastlingMoves, checkBlackQueensideCastling];

const addWhiteKingSideCastlingMoveFunctions = [addWhiteKingsideCastlingMove, castlingNotLegal];
const addWhiteQueenSideCastlingMoveFunctions = [addWhiteQueensideCastlingMove, castlingNotLegal];
const addBlackKingSideCastlingMoveFunctions = [addBlackKingsideCastlingMove, castlingNotLegal];
const addBlackQueenSideCastlingMoveFunctions = [addBlackQueensideCastlingMove, castlingNotLegal];

const checkWhiteEnPassantLegalityFunctions = [noEnPassantToCheck, checkEnPassantPinsOnWhiteKing];
const checkBlackEnPassantLegalityFunctions = [noEnPassantToCheck, checkEnPassantPinsOnBlackKing];

const checkWhitePawnPromotionFunctions = [regularWhitePawnMove, promoteWhitePawnMove];
const checkBlackPawnPromotionFunctions = [regularBlackPawnMove, promoteBlackPawnMove];

const invokeWhitePawnPromotionFunctions = [promoteToWhiteQueen, promoteToWhiteRook, promoteToWhiteBishop, promoteToWhiteKnight];
const invokeBlackPawnPromotionFunctions = [promoteToBlackQueen, promoteToBlackRook, promoteToBlackBishop, promoteToBlackKnight];
        

Chess Engine UI

Placeholder: Overview of user interface for the engine.


unction createDisplayBoard(){
    for (let row = 0; row < 8; row++) {
        for (let col = 0; col < 8; col++) {
            const square = document.createElement('div');
            square.classList.add('square');

            // Add light/dark class
            if ((row + col) % 2 === 0) {
            square.classList.add('light');
            } else {
            square.classList.add('dark');
            }

            // Add coordinate data
            square.dataset.row = row;
            square.dataset.col = col;
            square.dataset.coord = getSquareName(row,col);
            squareGrid[row][col] = square;
        }
    }
}
      

FEN Import and Export

Placeholder: Handling FEN notation for board positions.


// Code Snippet
export function generateFEN(board){
    let FEN = '';
    //Generate the chars for the pieces for the FEN
    let emptyCounter = 0;

    //Loop through the FEN pieces string to extract the pieces
    for(let rank = 7; rank >= 0; rank--)
    {
        if (rank != 7) {FEN += '/';}
        
        for (let file = 0; file < 8; file++){
            const index = (rank * 8) + file;
            let currentPiece = BitBoardFunctions.getPieceAndTypeAtIndex(board, index);

            if (currentPiece.type == -1){
                emptyCounter++;
                continue;
            }

            if (emptyCounter != 0){
                FEN += emptyCounter;
                emptyCounter = 0;
            }

            if (currentPiece.color == 0){
                FEN += getPieceCharUpper(currentPiece.type);
            } else {
                FEN += getPieceCharLower(currentPiece.type);
            }            
        }
        if (emptyCounter != 0){
            FEN += emptyCounter;
            emptyCounter = 0;
        }    
    }

    //Add the white to move indicator
    FEN += ' ';
    FEN += (board.whiteToMove === 1 ? 'w':'b');

    //Add the castling rights
    FEN += ' ';

    let castlingRights = '';
    castlingRights += (board.whiteCanCastleKingside === 1 ? 'K':'');
    castlingRights += (board.whiteCanCastleQueenside === 1 ? 'Q':'');
    castlingRights += (board.blackCanCastleKingside === 1 ? 'k':'');
    castlingRights += (board.blackCanCastleQueenside === 1 ? 'q':'');

    if (castlingRights === ""){castlingRights = '-';}

    FEN += castlingRights;

    //Add the en passant target
    FEN += ' ';
    if (board.enPassantTarget == 0n){
        FEN += '-';
    } else {
        const lsb = board.enPassantTarget & -board.enPassantTarget;
        const enIndex = index64[Number(((lsb * deBruijn64) >> 58n)& 63n)];
        FEN += getSquareNameFromIndex(enIndex);
    }

    //Add the half move counter
    FEN += ' ';
    FEN += 0;

    //Add the full move counter
    FEN += ' ';
    FEN += fullMoveCounter;

    return FEN;
}
        

PGN Import and Export

Placeholder: Handling PGN notation and game variations.


// Code Snippet
export function getMovePGN(previousBoardState, currentBoardState, piece, from, to){
    let movePGN = '';
    let capture = false;
    let promotion = false;
    let disambiguation = 0;

    //Check for promotion first to fix capture square issues
    let promotionString = '';
    //Check for promotion 
    if (piece.type == 0){
        if (piece.color == 0){ //White Pawn
            if (from >= 48 && from <=55){ //Comes from 7th rank
                promotion = true;
                promotionString+= getFileFromIndex(to);
                promotionString+= '8=';
                
                if (to >= 56 && to <= 63){
                    promotionString+= 'Q';
                } else if (to >= 48 && to <= 55){
                    promotionString+= 'R';
                    to += 8;
                } else if (to >= 40 && to <= 47){
                    promotionString+= 'B';
                    to += 16;
                } else if (to >= 32 && to <= 39){
                    promotionString+= 'N';
                    to += 24;
                }

            }
        } else { //Black Pawn
            if (from >= 8 && from <= 15){ //Comes from 2nd rank
                promotion = true;
                promotionString+= getFileFromIndex(to);
                promotionString+= '1=';

                if (to >= 0 && to <= 7){
                    promotionString+= 'Q';
                } else if (to >= 8 && to <= 15){
                    promotionString+= 'R';
                    to -= 8;
                } else if (to >= 16 && to <= 23){
                    promotionString+= 'B';
                    to -= 16;
                } else if (to >= 24 && to <= 31){
                    promotionString+= 'N';
                    to -= 24;
                }
            }
        }
    }

    //Check if capture
    if (BitBoardFunctions.isAnyPieceAtIndex(previousBoardState, to)){
        capture = true;
    }

    //Check if en passant capture
    if (piece.type == 0){
        const moveMask = 1n << BigInt(to);
        const enPassantCapture = moveMask & previousBoardState.enPassantTarget;
        if (enPassantCapture != 0n){
            capture = true;
        }
    }

    //Get the movement piece type
    if (piece.type != 0){
        movePGN += getPieceCharUpper(piece.type);
    }

    //Check for disambiguation
    if (piece.type != 0 && piece.type != 5){
        disambiguation = BitBoardFunctions.getDisambiguationType(previousBoardState, piece, from, to);
    }

    switch(disambiguation){
        case 1: //Rank Disambiguation
            movePGN += getRankFromIndex(from);
            break;
        case 2: //File Disambiguation
            movePGN += getFileFromIndex(from);
            break;
        case 3: //Rank and file Disambiguation
            movePGN += getSquareNameFromIndex(from);
            break;
        default:
            break;
    }

    //Check if pawn capture
    if (piece.type == 0 && capture){
        movePGN += getFileFromIndex(from);
        movePGN += 'x';
    }

    //Check if regular capture
    if (piece.type != 0 && capture){
        movePGN += 'x';
    }    

    if (promotion){
        movePGN += promotionString;    
    } else {
        movePGN += getSquareNameFromIndex(to);
    }

    //Overide everything else if the move is a castling move

    //6, 2 // 62, 58 //Castling Target Indexes
    //4 //60 //King Start Pos

    //Check for castling
    if (piece.type == 5){ //Check if a king move
        if (piece.color == 0 && from == 4){ //White Castle
            if (to == 6){
                movePGN = 'O-O';
            } else if (to == 2){
                movePGN = 'O-O-O';
            }
        } else if (piece.color == 1 && from == 60){ //Black Castle
            if (to == 62){
                movePGN = 'O-O';
            } else if (to == 58){
                movePGN = 'O-O-O';
            }
        }
    }

    if (currentBoardState.checkmate == 0){
        movePGN += '#';
    } else if (currentBoardState.totalChecks > 0){ //Add the check if not checkmate
        //Add checkmate
        movePGN += '+';
    }

    return {rawPGN: movePGN, newTo: to};
}
        

On the Horizon

Placeholder: Future improvements or features for the engine.