Introduction
Now that the basic rules are implemented, we’ll continue with the special rules: castling, en-passant, and promotion. Luckily for us, these rules aren’t more complicated than the basic ones. We just have to make sure we truly understand the conditions to perform these moves. Once that’s done, the code will write itself.
Rules for Castling
Castling is allowed under these 4 commandments, as described by this article from Chess.com [1].
- Your king must NOT be in check!
- Your king and rook must not have moved!
- Your king must not pass through check!
- There must be no pieces between the king and rook!
Let’s go through the explanation and implementation of each rule. Our objective is to create the following routine:
class MoveGenerator {
/**
* @param {King} king
* @param {Rook[]} rooks
* @param {BoardImplementation} board
* @returns Array of castling moves
*/
generateCastlingMoves(king, rooks, board) {}
}
This routine receives the king, the rooks, and the board, and returns a set of possible castling moves.
You know the drill. Try to come up with your solution first before reading mine.
Exercise Implement the generateCastlingMoves function with the following signature:
/** * @param {King} king * @param {Rook[]} rooks * @param {BoardImplementation} board * @returns Array of castling moves */ generateCastlingMoves(king, rooks, board) {}
And in case you were wondering, it’s okay if your routine turns out to be completely different than mine. Up to this point, you might have followed an alternative path to implement the foundations of your chess program. Therefore, from now on it’ll be normal if your code deviates from what’s shown here.
No need to stress, though. Take what we do here as a reference and adapt whatever you need for your program.
What I tell myself after ChatGPT did my homework. Taken from [2] |
There must be kings and rooks left…
My own addition to the castling commandments. Obviously, if there is no king or no rooks on the board, castling is not possible.
class MoveGenerator {
generateCastlingMoves(king, rooks, board){
//if there are no rooks or no king, castling is not possible
if (king === undefined) return [];
if (rooks === undefined || rooks.length === 0) return [];
}
}
This one is easy. We return a set of empty moves if no king or rooks are passed to the function.
Your king must NOT be in check!
Another easy one. We can ask the board if the king is in check. If so, we return an empty set.
class MoveGenerator {
generateCastlingMoves(king, rooks, board){
//The king cannot be in check
if (board.isKingInCheck(king.color)) return [];
}
}
Your king and rook must not have moved!
This is a tricky one. How do we know if the king or the rooks have moved?
Well, we can begin by asking if they are on the same square as they would be in a starting position. If they are not, they had to have moved before.
class MoveGenerator {
generateCastlingMoves(king, rooks, board){
//if the king is not on its initial square, castling is not possible
if (!king.isOnInitialSquare()) return [];
let castlingMoves = [];
for (let rook of rooks) {
//if rook is not on its initial square, skip
if (!rook.isOnInitialSquare()) continue;
}
return castlingMoves;
}
}
class King {
isOnInitialSquare() {
return this.color === E_PieceColor.White ?
(this.rank === 1 && this.file === 5) :
(this.rank === 8 && this.file === 5);
}
}
class Rook {
isOnInitialSquare() {
return this.color === E_PieceColor.White ?
(this.rank === 1 && this.file === 1) | (this.rank === 1 && this.file === 8) :
(this.rank === 8 && this.file === 1) | (this.rank === 8 && this.file === 8);
}
}
However, this is not enough. A piece might end up on its initial square after several moves. We need a data structure that changes whenever a move is made for the first time and keeps that information persistently for the rest of the game.
The concept of castling rights is what we are looking for here, In the words of the Chess Programming Wiki (2021):
The Castling Rights specify whether both sides are principally able to castle king- or queen side, now or later during the game - whether the involved pieces have already moved or in case of the rooks, were captured. Castling rights do not specify, whether castling is actually possible, but are a pre-condition for both wing castlings. (para. 1) [3]
The Board class has us covered. As explained in the Move Generation architecture, this class should hold information that applies globally to the board. We’ll include a small object that holds castling rights for each side and each color.
class BoardImplementation {
#castlingRights = {
[E_PieceColor.White]: {
[E_CastlingSide.KingSide]: true,
[E_CastlingSide.QueenSide]: true
}, [E_PieceColor.Black]: {
[E_CastlingSide.KingSide]: true,
[E_CastlingSide.QueenSide]: true
}
}
}
When creating a new board, we’ll assume no moves have been made before. Then, we can set the castling rights for the king and the rooks by checking if they are in their initial square. This is what that would look like for the king:
class Board {
constructor(inputFen){
//...
//for each color
for (let color of Object.values(E_PieceColor)) {
//...
let kingKey = pieceColorTypeToKey(color, E_PieceType.King);
let kingSymbol = Quadrille.chessSymbols[kingKey];
let kingSquare = this.#board.search(createQuadrille([kingSymbol]), true)[0];
//if board has no king
if (kingSquare === undefined) {
//no castling is possible
this.#setCastlingRights(color, E_CastlingSide.KingSide, false);
this.#setCastlingRights(color, E_CastlingSide.QueenSide, false);
continue;
} else { //else if there's a king
let rank = kingSquare.rank;
let file = kingSquare.file;
let isKingOnInitialSquare = color === E_PieceColor.White ?
(rank === 1 && file === 5) :
(rank === 8 && file === 5);
//if king is not in its initial square
if (!isKingOnInitialSquare) {
//no castling is possible
this.#setCastlingRights(color, E_CastlingSide.KingSide, false);
this.#setCastlingRights(color, E_CastlingSide.QueenSide, false);
continue;
}
}
//...
}
}
}
We’ll take a look in a future devlog on how making moves changes this data structure. For now, we can use it to retrieve the information we need:
class MoveGenerator {
generateCastlingMoves(king, rooks, board){
//if the king is not on its initial square, castling is not possible
if (!king.isOnInitialSquare()) return [];
let castlingMoves = [];
for (let rook of rooks) {
//if rook is not on its initial square, skip
if (!rook.isOnInitialSquare()) continue;
//This side must have castling rights.
//That is, rooks cannot have moved or been captured and king cannot have moved.
let castlingSide = king.file > rook.file ? E_CastlingSide.QueenSide : E_CastlingSide.KingSide;
if (!board.hasCastlingRights(rook.color, castlingSide)) continue;
}
return castlingMoves;
}
}
class BoardImplementation {
#castlingRights = {
[E_PieceColor.White]: {
[E_CastlingSide.KingSide]: true,
[E_CastlingSide.QueenSide]: true
}, [E_PieceColor.Black]: {
[E_CastlingSide.KingSide]: true,
[E_CastlingSide.QueenSide]: true
}
}
/**
* @param {E_PieceColor} color
* @param {E_CastlingSide} castlingSide
* @returns Whether the given side has rights to castle (It does not necesarilly mean castling is possible).
*/
hasCastlingRights(color, castlingSide) {
return this.#castlingRights[color][castlingSide];
}
}
Code
//castling-devlog/castling-rights.js
let game;
const GAME_MARGIN = 10;
const EXTRA_SPACE = 75;
const FEN = 'r3k2r/8/8/R6r/8/8/8/R3K2R';
const TABLE = {
x: Chess.GAME_DIMENSIONS.WIDTH / 2 - 150,
y: 75,
horSpace: 100,
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, FEN);
game.setGameMode(Chess.E_GameMode.FREE);
}
function draw() {
background(255);
game.update();
textAlign(CENTER, TOP);
textStyle(BOLD);
textSize(15);
rectMode(CENTER);
text("Observe how castling rights are lost after moving the king, moving the rooks, or capturing a rook. But giving check does not remove castling rights.",
Chess.GAME_DIMENSIONS.WIDTH / 2,
25,
Chess.GAME_DIMENSIONS.WIDTH - GAME_MARGIN);
textStyle(NORMAL);
textAlign(LEFT, TOP);
text("White", TABLE.x + TABLE.horSpace, TABLE.y);
text("Black", TABLE.x + TABLE.horSpace * 2, TABLE.y);
text("Kingside", TABLE.x, TABLE.y + TABLE.verSpace);
text("QueenSide", TABLE.x, TABLE.y + TABLE.verSpace * 2);
text(getSymbol(E_PieceColor.White, E_CastlingSide.KingSide), TABLE.x + TABLE.horSpace, TABLE.y + TABLE.verSpace);
text(getSymbol(E_PieceColor.White, E_CastlingSide.QueenSide), TABLE.x + TABLE.horSpace, TABLE.y + TABLE.verSpace * 2);
text(getSymbol(E_PieceColor.Black, E_CastlingSide.KingSide), TABLE.x + TABLE.horSpace * 2, TABLE.y + TABLE.verSpace);
text(getSymbol(E_PieceColor.Black, E_CastlingSide.QueenSide), TABLE.x + TABLE.horSpace * 2, TABLE.y + TABLE.verSpace * 2);
}
function getSymbol(color, castlingSide) {
let symbol = game.board.hasCastlingRights(color, castlingSide) ? "✅" : "❌";
return symbol;
}
There must be no pieces between the king and rook!
Now that we have guaranteed that both the king and the rook haven’t moved from their initial squares, We can build a path from the king to the rook using bitboards.
class MoveGenerator {
generateCastlingMoves(king, rooks, board){
//There cannot be pieces between the rook and the king
let castlingPath = castlingSide === E_CastlingSide.QueenSide ?
king.position << 1n | king.position << 2n | king.position << 3n :
king.position >> 1n | king.position >> 2n;
let isCastlingPathObstructed = (board.getEmptySpaces() & castlingPath) !== castlingPath;
}
}
The castling side will determine the direction we take to create the path. If it’s a queenside castling, the rook is four squares to the left of the king. Hence, we shift the king’s position three times to the left and build a bitboard with each shift. For a kingside castling, the rook is three squares to the right of the king. We do the same in the other direction.
If we apply the ‘&’ operator to the castling path with a bitboard of empty squares, it’ll remain unaltered if there’s no piece between the king and the rook. Otherwise, the result will be different from the initial path we calculated, proving there is at least one piece in between. These applies to both white and black pieces.
Code
//castling-devlog/pieces-in-between.js
let board;
let king;
let path;
let empty;
let result;
let castlingSideSelector;
const FEN = '8/8/8/8/8/8/8/R1P1K2R';
const rows = 24;
const columns = 20;
const whitePawnSymbol = Quadrille.chessSymbols['P'];
const whiteRookSymbol = Quadrille.chessSymbols['R'];
const QUEEN_SIDE_OPTION = 'QueenSide';
const KING_SIDE_OPTION = 'KingSide';
function setup() {
Quadrille.cellLength = 25;
createCanvas(columns * Quadrille.cellLength, rows * Quadrille.cellLength);
board = createQuadrille(FEN);
king = createQuadrille(FEN).replace(whiteRookSymbol, null).replace(whitePawnSymbol, null);
castlingSideSelector = createSelect();
castlingSideSelector.position(3 * Quadrille.cellLength, 23 * Quadrille.cellLength);
castlingSideSelector.option(QUEEN_SIDE_OPTION);
castlingSideSelector.option(KING_SIDE_OPTION);
castlingSideSelector.selected(QUEEN_SIDE_OPTION);
castlingSideSelector.mouseClicked(calculateBitboards);
calculateBitboards();
}
function draw() {
background(255);
drawQuadrille(board, { row: 4, col: 1 })
drawBitboard(empty, 4, 10, 1);
drawBitboard(path, 14, 1, 1);
drawBitboard(result, 14, 10, 1);
textAlign(CENTER, TOP);
textStyle(BOLD);
textSize(15);
rectMode(CENTER);
text("Click 'Board' quadrille to change pawn's position. Click dropdown to change castling side. See results.",
columns * Quadrille.cellLength / 2,
25,
columns * Quadrille.cellLength);
text("Board", 5 * Quadrille.cellLength, 3 * Quadrille.cellLength);
text("Empty Bitboard", 14 * Quadrille.cellLength, 3 * Quadrille.cellLength);
text("Path", 5 * Quadrille.cellLength, 13 * Quadrille.cellLength);
text("Result", 14 * Quadrille.cellLength, 13 * Quadrille.cellLength);
rectMode(CORNER);
textStyle(NORMAL);
}
function mouseClicked() {
let clickedCell = board.read(board.mouseRow, board.mouseCol);
if (clickedCell !== undefined && clickedCell === null) {
board.replace(whitePawnSymbol, null);
board.fill(board.mouseRow, board.mouseCol, whitePawnSymbol);
calculateBitboards();
}
}
function calculatePath() {
let kingBitboard = king.toBigInt();
let pathBitboard = castlingSideSelector.selected() === QUEEN_SIDE_OPTION ?
kingBitboard << 1n | kingBitboard << 2n | kingBitboard << 3n :
kingBitboard >> 1n | kingBitboard >> 2n;
path = createBitboardQuadrille(pathBitboard);
}
function calculateBitboards() {
calculatePath();
empty = Quadrille.neg(board, 1);
result = Quadrille.and(empty, path);
}
function drawBitboard(bitboard, row, col, fill) {
drawQuadrille(bitboard, {
row: row,
col: col,
numberDisplay: ({ graphics, value, cellLength = Quadrille.cellLength } = {}) => {
graphics.fill(color(0));
graphics.textAlign(CENTER, CENTER);
graphics.textSize(cellLength * Quadrille.textZoom * 0.8);
graphics.text(fill, cellLength / 2, cellLength / 2);
}
});
}
function createBitboardQuadrille(bitboard) {
let quadrille = createQuadrille(8, bitboard, 1);
let quadrilleHeight = quadrille.height;
if (quadrilleHeight < 8) {
let rowsLeft = 8 - quadrilleHeight;
for (let i = 0; i < rowsLeft; i++) {
quadrille.insert(0);
}
}
return quadrille;
}
Your king must not pass through check!
When we castle the king, it always moves two squares in the direction of the rook. This rule states that, in order to castle, none of the squares the king traverses can be attacked by the opponent’s pieces.
Notice this rule only applies to the king. It doesn’t matter if the squares the rook traverses during castling are being attacked.
Here’s an example of this distinction:
We can follow the same approach as the last rule.
class MoveGenerator {
generateCastlingMoves(king, rooks, board){
//Your king can not pass through check
let attackedSquares = board.getAttackedSquares(OppositePieceColor(king.color));
let kingPathToCastle = castlingSide === E_CastlingSide.QueenSide ?
king.position << 1n | king.position << 2n :
king.position >> 1n | king.position >> 2n;
let isKingPathChecked = (kingPathToCastle & attackedSquares) > 0n;
}
}
This time, the path we calculate is the one the king takes when castling, not the space between the king and the rook. Then, we calculate the intersection between this path and a bitboard with squares attacked by the opponent. If the intersection is not empty, the king passes through the check.
Code
//castling-devlog/pass-through-check.js
let board;
let king;
let path;
let attacked;
let result;
let castlingSideSelector;
const FEN = '8/8/8/2r5/8/8/8/R3K2R';
const rows = 24;
const columns = 20;
const blackRookSymbol = Quadrille.chessSymbols['r'];
const whiteRookSymbol = Quadrille.chessSymbols['R'];
const QUEEN_SIDE_OPTION = 'QueenSide';
const KING_SIDE_OPTION = 'KingSide';
function setup() {
Quadrille.cellLength = 25;
createCanvas(columns * Quadrille.cellLength, rows * Quadrille.cellLength);
board = createQuadrille(FEN);
king = createQuadrille(FEN).replace(whiteRookSymbol, null).replace(blackRookSymbol, null);
castlingSideSelector = createSelect();
castlingSideSelector.position(3 * Quadrille.cellLength, 23 * Quadrille.cellLength);
castlingSideSelector.option(QUEEN_SIDE_OPTION);
castlingSideSelector.option(KING_SIDE_OPTION);
castlingSideSelector.selected(QUEEN_SIDE_OPTION);
castlingSideSelector.mouseClicked(calculateBitboards);
calculateBitboards();
}
function draw() {
background(255);
drawQuadrille(board, { row: 4, col: 1 })
drawBitboard(attacked, 4, 10, 1);
drawBitboard(path, 14, 1, 1);
drawBitboard(result, 14, 10, 1);
textAlign(CENTER, TOP);
textStyle(BOLD);
textSize(15);
rectMode(CENTER);
text("Click 'Board' quadrille to change rook's position. Click dropdown to change castling side. See results.",
columns * Quadrille.cellLength / 2,
25,
columns * Quadrille.cellLength);
text("Board", 5 * Quadrille.cellLength, 3 * Quadrille.cellLength);
text("Squares Attacked", 14 * Quadrille.cellLength, 3 * Quadrille.cellLength);
text("Path", 5 * Quadrille.cellLength, 13 * Quadrille.cellLength);
text("Intersection", 14 * Quadrille.cellLength, 13 * Quadrille.cellLength);
rectMode(CORNER);
textStyle(NORMAL);
}
function mouseClicked() {
let clickedCell = board.read(board.mouseRow, board.mouseCol);
if (clickedCell !== undefined && clickedCell === null) {
board.replace(blackRookSymbol, null);
board.fill(board.mouseRow, board.mouseCol, blackRookSymbol);
calculateBitboards();
}
}
function calculatePath() {
let kingBitboard = king.toBigInt();
let pathBitboard = castlingSideSelector.selected() === QUEEN_SIDE_OPTION ?
kingBitboard << 1n | kingBitboard << 2n :
kingBitboard >> 1n | kingBitboard >> 2n;
path = createBitboardQuadrille(pathBitboard);
}
function calculateBitboards() {
calculatePath();
calculateAttacked();
result = Quadrille.and(attacked, path);
}
function calculateAttacked() {
let boardImplementation = new Chess.BoardImplementation(board.toFEN());
let attackedBitboard = boardImplementation.getAttackedSquares(Chess.E_PieceColor.Black);
attacked = createBitboardQuadrille(attackedBitboard);
}
function drawBitboard(bitboard, row, col, fill) {
drawQuadrille(bitboard, {
row: row,
col: col,
numberDisplay: ({ graphics, value, cellLength = Quadrille.cellLength } = {}) => {
graphics.fill(color(0));
graphics.textAlign(CENTER, CENTER);
graphics.textSize(cellLength * Quadrille.textZoom * 0.8);
graphics.text(fill, cellLength / 2, cellLength / 2);
}
});
}
function createBitboardQuadrille(bitboard) {
let quadrille = createQuadrille(8, bitboard, 1);
let quadrilleHeight = quadrille.height;
if (quadrilleHeight < 8) {
let rowsLeft = 8 - quadrilleHeight;
for (let i = 0; i < rowsLeft; i++) {
quadrille.insert(0);
}
}
return quadrille;
}
Generating a Castling Move
The hard work is done. Now that we are positively and absolutely sure that castling is possible, we have to generate a new move.
For this, it’s important to remember that castling is a move that only the king can perform. Therefore, every castling move starts on the square of the king and ends two squares to the left or to the right, depending on whether it is queen-side or king-side.
We’ll use another small object that stores the files involved in a castling move. Remember the color does not matter. The files are the same.
class MoveGenerator {
generateCastlingMoves(king, rooks, board){
let kingTargetFile = CASTLING_FILES[castlingSide][E_PieceType.King].endFile;
let kingMove = new Move(king.rank, king.file, king.rank, kingTargetFile, E_MoveFlag.Castling);
castlingMoves.push(kingMove);
}
}
//--- ChessUtils.js ---
/**
* Files involved in castling. Provide castling side and piece which is moving (rook or king)
*/
const CASTLING_FILES = {
[E_CastlingSide.QueenSide]: {
[E_PieceType.King]: {
startFile: 5,
endFile: 3
},
[E_PieceType.Rook]: {
startFile: 1,
endFile: 4
},
},
[E_CastlingSide.KingSide]: {
[E_PieceType.King]: {
startFile: 5,
endFile: 7
},
[E_PieceType.Rook]: {
startFile: 8,
endFile: 6
}
}
}
Final Result
class MoveGenerator {
/**
* @param {King} king
* @param {Rook[]} rooks
* @param {BoardImplementation} board
* @returns Array of castling moves
*/
generateCastlingMoves(king, rooks, board) {
//if there are no rooks or no king, castling is not possible
if (king === undefined) return [];
if (rooks === undefined || rooks.length === 0) return [];
//The king cannot be in check
if (board.isKingInCheck(king.color)) return [];
let castlingMoves = [];
for (let rook of rooks) {
//if rook is not on its initial square, skip
if (!rook.isOnInitialSquare()) continue;
//This side must have castling rights.
//That is, rooks cannot have moved or been captured and king cannot have moved.
let castlingSide = king.file > rook.file ? E_CastlingSide.QueenSide : E_CastlingSide.KingSide;
if (!board.hasCastlingRights(rook.color, castlingSide)) continue;
//There cannot be any piece between the rook and the king
let castlingPath = castlingSide === E_CastlingSide.QueenSide ?
king.position << 1n | king.position << 2n | king.position << 3n :
king.position >> 1n | king.position >> 2n;
let isCastlingPathObstructed = (board.getEmptySpaces() & castlingPath) !== castlingPath;
//Your king can not pass through check
let attackedSquares = board.getAttackedSquares(OppositePieceColor(king.color));
let kingPathToCastle = castlingSide === E_CastlingSide.QueenSide ?
king.position << 1n | king.position << 2n :
king.position >> 1n | king.position >> 2n;
let isKingPathChecked = (kingPathToCastle & attackedSquares) > 0n;
if (!isCastlingPathObstructed && !isKingPathChecked) {
//castling move is possible!
let kingTargetFile = CASTLING_FILES[castlingSide][E_PieceType.King].endFile;
let kingMove = new Castling(king.rank, king.file, king.rank, kingTargetFile, castlingSide);
castlingMoves.push(kingMove);
}
}
return castlingMoves;
}
}
Conclusion
So that’s it! One thing I want to highlight is how after we fully understand the rules, the implementation comes out naturally. Castling rules are not rocket science, but there are little details one might miss that can lead to errors later. I hope you can catch some bugs in your code with this devlog.
References
- [1] How to castle in chess?. Chess.com. (2020, October 28). https://www.chess.com/article/view/how-to-castle-in-chess
- [2] Isaac Newton quote. AZQuotes. (n.d.). https://www.azquotes.com/quote/213687
- [3] Castling rights. Chessprogramming wiki. (2021, February 1). https://www.chessprogramming.org/Castling_Rights