Skip to main content

Welcome to a Grasp lesson

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

Lesson 28

Detecting Check for a Specific Player's King

Recommended resources

Hello! In our last lesson, you built the execute_move function, which is the engine that changes the state of our game board. It was a crucial step in making our chess game interactive.

Today, we'll build upon that foundation to implement one of the most fundamental rules of chess: detecting if a king is in check. A king is in check if it is under attack by one or more of the opponent's pieces. This logic is the cornerstone for determining legal moves, checkmate, and stalemate, which we'll tackle in upcoming lessons.

Our goal is to create a method on our Board struct, let's call it is_in_check, that will tell us whether a specific player's king is currently threatened.

Chessboard with King in Check
This image shows a classic check scenario. The white queen on g5 is attacking the black king on h7. Our code will learn to detect such threats.

1. The Logic of Detecting a Check

So, how do we determine if a king is in check? The logic is straightforward:

  1. Find the position of the king for the player we're checking.
  2. Look at every piece belonging to the opponent.
  3. For each opponent piece, generate all its possible moves (the "pseudo-legal" moves you worked on in Module 5).
  4. If any of those moves land on the king's square, the king is in check.

This approach essentially asks the question: "If it were the opponent's turn, could any of their pieces capture the king?"

To make our code clean and reusable, we can break this down into a more general-purpose helper function: one that can tell us if any square on the board is attacked by a given player. Then, checking if the king is in check simply becomes a matter of asking if the king's specific square is attacked.

Let's look at a resource that follows this exact pattern.

Building a Chess AI, Part 2 – Move Generation - Tom Cant

The article 'Building a Chess AI, Part 2' provides an excellent guide on move generation and check detection in Rust. It perfectly illustrates the modular approach we're about to take.

Please scroll down to the subsection '3. The square the king will pass through is not attacked' and read it, along with the following subsection '4. The colour-to-move is not in check'. Focus on the structure of the Rust code snippets for is_attacked and is_in_check. Notice how the is_in_check function becomes very simple by relying on the more general is_attacked function. We will implement this same powerful and clean design.


2. Hands-On: Building the Check Detector

Inspired by the article, we will implement two new methods inside the impl Board block:

  1. is_square_attacked_by(&self, square: (u8, u8), attacker_color: PieceColor) -> bool
  2. is_in_check(&self, player_color: PieceColor) -> bool

Let's start with the helper function.

Step 2.1: Is a Square Attacked?

This function will be the core of our logic. It will iterate through all of the attacker_color's pieces and use their move-generation logic to see if they can attack the given square.

// Inside your `impl Board` block

/// Checks if a given square is attacked by any piece of the specified color.
pub fn is_square_attacked_by(&self, square: (u8, u8), attacker_color: PieceColor) -> bool {
    // We need to check for attacks from all of the opponent's pieces.
    for y in 0..8 {
        for x in 0..8 {
            if let Some(piece) = self.get_piece_at((x, y)) {
                if piece.color == attacker_color {
                    // We found a piece belonging to the attacker.
                    // Now, let's see if it can move to the target square.
                    // NOTE: This assumes you have your move generation functions from Module 5.
                    // We'll use a placeholder `generate_pseudo_legal_moves` function here.
                    // You should replace this with calls to your actual functions (generate_pawn_moves, etc.).
                    
                    let moves = self.generate_pseudo_legal_moves_for_piece(piece, (x, y));
                    for mv in moves {
                        if mv.to == square {
                            // An opponent's piece can move to this square, so it's attacked.
                            return true;
                        }
                    }
                }
            }
        }
    }

    // No piece can attack the square.
    false
}

Integrating Your Move Generation:

In the code above, generate_pseudo_legal_moves_for_piece is a placeholder. You need to call the specific move generation functions you've already written. Your implementation should look something like this:

// This is a helper method you can add inside `impl Board`
fn generate_pseudo_legal_moves_for_piece(&self, piece: Piece, pos: (u8, u8)) -> Vec<Move> {
    match piece.piece_type {
        PieceType::Pawn => self.generate_pawn_moves(pos, piece.color),
        PieceType::Rook => self.generate_rook_moves(pos, piece.color),
        PieceType::Knight => self.generate_knight_moves(pos, piece.color),
        PieceType::Bishop => self.generate_bishop_moves(pos, piece.color),
        PieceType::Queen => self.generate_queen_moves(pos, piece.color),
        PieceType::King => self.generate_king_moves(pos, piece.color),
    }
}

Make sure you add this helper or integrate the match statement directly into is_square_attacked_by. This is a fantastic example of reusing the code you've already built!

Step 2.2: Is the King in Check?

Now that we have our powerful helper function, implementing is_in_check is surprisingly simple. We just need to find the king and then ask if its square is attacked by the opponent.

First, let's create a helper to find the king.

// Inside your `impl Board` block

/// Finds the position of the king for a given color.
/// Returns `None` if the king is not on the board (which implies an invalid state).
fn find_king_pos(&self, color: PieceColor) -> Option<(u8, u8)> {
    for y in 0..8 {
        for x in 0..8 {
            if let Some(piece) = self.get_piece_at((x, y)) {
                if piece.piece_type == PieceType::King && piece.color == color {
                    return Some((x, y));
                }
            }
        }
    }
    None
}

With that in place, is_in_check is trivial:

// Inside your `impl Board` block

/// Checks if the specified player is in check.
pub fn is_in_check(&self, player_color: PieceColor) -> bool {
    // 1. Find the player's king.
    if let Some(king_pos) = self.find_king_pos(player_color) {
        // 2. Check if that square is attacked by the opponent.
        let opponent_color = if player_color == PieceColor::White {
            PieceColor::Black
        } else {
            PieceColor::White
        };
        return self.is_square_attacked_by(king_pos, opponent_color);
    }
    
    // If there's no king on the board, we can't be in check.
    false
}

Look at how clean that is! By building a reusable component (is_square_attacked_by), our main is_in_check function becomes very readable and clearly expresses its intent.


3. Testing Your Logic

As always, we need to test our new feature. Let's add some scenarios to your main function to verify it works correctly.

Add this code to the end of main.rs.

// In src/main.rs, at the end of the main() function

println!("\n--- Testing Check Detection ---");

// Scenario 1: White King in check by a Black Rook
let mut check_board = Board::new(); // An empty board
check_board.pieces[0][4] = Some(Piece { piece_type: PieceType::King, color: PieceColor::White }); // White King on e1
check_board.pieces[7][4] = Some(Piece { piece_type: PieceType::Rook, color: PieceColor::Black }); // Black Rook on e8

println!("\nScenario 1: White King on e1, Black Rook on e8");
check_board.print_board();
assert!(check_board.is_in_check(PieceColor::White), "Test Failed: White King should be in check!");
println!("Test Passed: White King is correctly detected as in check.");


// Scenario 2: King is safe (blocked by its own piece)
let mut safe_board = Board::new();
safe_board.pieces[0][4] = Some(Piece { piece_type: PieceType::King, color: PieceColor::White }); // White King on e1
safe_board.pieces[1][4] = Some(Piece { piece_type: PieceType::Pawn, color: PieceColor::White }); // White Pawn on e2
safe_board.pieces[7][4] = Some(Piece { piece_type: PieceType::Rook, color: PieceColor::Black }); // Black Rook on e8

println!("\nScenario 2: Rook attack blocked by a friendly pawn");
safe_board.print_board();
assert!(!safe_board.is_in_check(PieceColor::White), "Test Failed: White King should NOT be in check!");
println!("Test Passed: White King is correctly detected as safe.");

// Scenario 3: Black King in check by a White Knight
let mut knight_check_board = Board::new();
knight_check_board.pieces[7][6] = Some(Piece { piece_type: PieceType::King, color: PieceColor::Black }); // Black King on g8
knight_check_board.pieces[5][5] = Some(Piece { piece_type: PieceType::Knight, color: PieceColor::White }); // White Knight on f6

println!("\nScenario 3: Black King on g8, White Knight on f6");
knight_check_board.print_board();
assert!(knight_check_board.is_in_check(PieceColor::Black), "Test Failed: Black King should be in check!");
println!("Test Passed: Black King is correctly detected as in check by the knight.");

Run cargo run. If all your tests pass, you have successfully implemented check detection!


Conclusion

Fantastic work today. You've implemented a critical piece of chess logic that moves us significantly closer to a playable game. By breaking the problem down into smaller, reusable functions like is_square_attacked_by, you've not only solved the immediate problem but also practiced writing clean, modular Rust code.

Key Takeaways:

  • A king is in check if its square is attacked by any opposing piece.
  • A powerful way to implement check detection is to first create a general is_square_attacked_by function.
  • This modular approach makes the final is_in_check function simple, readable, and robust.
  • This lesson successfully integrated the move generation logic from previous modules, showing how different parts of your project are starting to work together.

In our next lesson, we will put this new function to immediate use. We will learn how to filter a list of potential moves to generate only legal moves that do not leave the king in check. This will be our first step in enforcing the most important rule in chess