Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 33

Castling: Logic and Conditions

Recommended resources

Hello! Welcome back to our journey of building a chess game in Rust.

In our last lesson, we laid the crucial groundwork for handling special chess moves by extending our Board struct to include castling_rights and an en_passant_square. This "memory" is essential because these rules depend on the history of the game, not just the current board state.

Today, we'll put that new state to work. Our goal is to implement the logic for castling, including all necessary conditions. We will write the code that reads the castling_rights, checks that all rules are met, and generates the appropriate castling moves.

1. Understanding the Rules of Castling

Castling is a unique move where the king and a rook move simultaneously. It's the only time in chess you can move two pieces in one turn. Before we write any code, we need to be crystal clear on the conditions that allow it.

Visual Guide to Castling in Chess
This image illustrates the king-side and queen-side castling moves for both White and Black. The king always moves two squares, and the rook moves to the square the king crossed.

There are five key conditions that must all be true for castling to be legal. To see a great explanation of these, please watch the first couple of minutes of the following video.

Programming a Chess Game in C# | Part 13 - Castling

This video, although in C#, provides a clear and concise explanation of the five rules of castling. Focus on understanding these conditions, as they are the requirements we'll translate into Rust code.

Watch from the beginning until 02:33. Pay close attention to the five enumerated rules for when castling is allowed.

To summarize, here are the five rules for castling:

  1. The king must not have previously moved.
  2. The chosen rook must not have previously moved.
  3. The squares between the king and the rook must be empty.
  4. The king cannot be in check.
  5. The king cannot pass through or end up on a square that is attacked by an enemy piece.

Our task is to check these five conditions in our Rust code.


2. Strategy for Implementation

We will add a method to your Board implementation that generates valid castling moves for the current player. A good approach is to create a method, for example generate_castling_moves, which can be called from your main move generation logic.

This method will:

  1. Check which player is to move.
  2. Check the castling_rights bitfield we created last lesson to see if the king and rooks are eligible.
  3. Verify that the squares between them are empty by checking the board.squares array.
  4. Use the is_square_attacked logic you built in Module 6 to ensure the king isn't in check and doesn't pass through an attacked square.
  5. If all checks pass, it will add a Move to a list of legal moves.

For now, we will represent a castling move simply as the king moving two squares (e.g., from e1 to g1). Later, when we implement the execute_move function, we'll add a special case: if a king moves two squares, we'll know it's a castling move and will also move the rook accordingly.


3. Hands-On: Generating Castling Moves

Let's get to the code. We'll build the logic for White's castling moves first. You can then use that as a template to implement Black's moves.

Open the file where your impl Board is located (likely src/main.rs). We'll add our checks inside a new function.

Step 1: Checking Castling Rights and Empty Squares

First, we use the castling_rights field. The bitwise AND operator (&) is perfect for checking if a specific flag is set in our u8.

Let's start by checking White's kingside castling. The conditions we can check right away are:

  • Does White have kingside castling rights?
  • Are the f1 and g1 squares empty?
// Inside your `impl Board` block

// We assume you have a function to generate all moves, which can call this.
// For now, let's create it. If you have a different structure, adapt it.
pub fn generate_legal_moves(&self) -> Vec<Move> {
    let mut moves = Vec::new();
    // ... here you would call generators for pawns, knights, etc. ...

    self.add_castling_moves(&mut moves);

    moves
}


// This new function will contain our castling logic.
fn add_castling_moves(&self, moves: &mut Vec<Move>) {
    if self.player_to_move == PieceColor::White {
        // --- White Kingside Castling ---
        // 1. Check if white has kingside castling rights
        // 2. Check if the squares between king and rook are empty
        if (self.castling_rights.0 & castling::W_KINGSIDE != 0) &&
           self.squares[7][5].is_none() && // f1
           self.squares[7][6].is_none()    // g1
        {
            // If these conditions are met, we then check the attack rules.
            // We'll add that code here in the next step.
        }

        // --- White Queenside Castling ---
        // 1. Check if white has queenside castling rights
        // 2. Check if the squares between king and rook are empty
        if (self.castling_rights.0 & castling::W_QUEENSIDE != 0) &&
           self.squares[7][1].is_none() && // b1
           self.squares[7][2].is_none() && // c1
           self.squares[7][3].is_none()    // d1
        {
            // If these conditions are met, we then check the attack rules.
            // We'll add that code here too.
        }
    } else {
        // TODO: Implement for Black
    }
}

Note: Your board coordinates might be different (e.g., squares[0][..] for rank 1). Adjust the indices [7][..] to match your board representation for rank 1.

Step 2: Checking for Attacks

Now for the tricky part: rules #4 and #5. The king cannot be in check, nor can it pass through an attacked square. Fortunately, you should already have a function from Module 6 that can tell you if a square is attacked by the opponent. Let's call it is_square_attacked.

We need to check three squares for kingside castling:

  • e1 (current king square): Must not be attacked.
  • f1 (traversal square): Must not be attacked.
  • g1 (destination square): Must not be attacked.

Let's integrate this into our logic.

// Inside the `add_castling_moves` method, for White

// --- White Kingside Castling ---
if (self.castling_rights.0 & castling::W_KINGSIDE != 0) &&
   self.squares[7][5].is_none() && // f1
   self.squares[7][6].is_none()    // g1
{
    // Check if the king's path is clear of attacks
    let king_pos = Position { rank: 7, file: 4 }; // e1
    if !self.is_square_attacked(king_pos, PieceColor::Black) &&
       !self.is_square_attacked(Position { rank: 7, file: 5 }, PieceColor::Black) && // f1
       !self.is_square_attacked(Position { rank: 7, file: 6 }, PieceColor::Black)    // g1
    {
        // ALL CHECKS PASSED! Add the move.
        moves.push(Move {
            from: king_pos,
            to: Position { rank: 7, file: 6 }, // e1 to g1
            // You might have other fields like `promotion`, set them to None/default
        });
    }
}

// --- White Queenside Castling ---
if (self.castling_rights.0 & castling::W_QUEENSIDE != 0) &&
   self.squares[7][1].is_none() && // b1
   self.squares[7][2].is_none() && // c1
   self.squares[7][3].is_none()    // d1
{
    // Check if the king's path is clear of attacks
    let king_pos = Position { rank: 7, file: 4 }; // e1
    if !self.is_square_attacked(king_pos, PieceColor::Black) &&
       !self.is_square_attacked(Position { rank: 7, file: 3 }, PieceColor::Black) && // d1
       !self.is_square_attacked(Position { rank: 7, file: 2 }, PieceColor::Black)    // c1
    {
        // ALL CHECKS PASSED! Add the move.
        moves.push(Move {
            from: king_pos,
            to: Position { rank: 7, file: 2 }, // e1 to c1
        });
    }
}

Reminder: is_square_attacked is a function you should have from a previous lesson. If you named it differently, use your name. It should take a position and the attacking color and return true if any piece of that color attacks the square.

Your Turn: Implement for Black

Now, it's your turn to apply what we just did. Inside the else block for PieceColor::Black, implement the logic for both kingside and queenside castling for Black. Remember:

  • Black's king starts at e8.
  • The castling rights are B_KINGSIDE and B_QUEENSIDE.
  • The ranks and squares will be different (e.g., rank 0 if you're using a 0-indexed array from the top).
  • You will be checking for attacks from PieceColor::White.

Take your time to adapt the logic. This is a great exercise to solidify your understanding.


Conclusion

Fantastic work! You have now translated the complex rules of castling into concrete Rust code. By combining the castling_rights state with your existing is_square_attacked logic, you've created a robust system for determining when this powerful move is legal. This is a huge step forward for your chess engine.

Key Takeaways:

  • Implementing a complex rule like castling can be broken down into a series of smaller, verifiable checks.
  • The five main conditions for castling are: king/rook haven't moved, path is clear, king is not in check, and king doesn't pass through or land in check.
  • We used the castling_rights bitfield from the last lesson to efficiently check the movement history of the king and rooks.
  • Reusing existing functions like is_square_attacked is key to building complex logic without duplicating code.

Preview of the Next Lesson

We've now tackled one of the two special moves we prepared for. In the next lesson, we will implement the logic for the en passant capture. We'll use the en_passant_square field from our Board struct to determine when this unique pawn capture is possible.