Hello! Welcome back to our chess project.
In the last lesson, you successfully built the is_in_check function, a crucial piece of logic that can tell us if a king is under attack. Today, we're going to put that function to work to solve one of the most important parts of the game's rules engine.
Our learning outcome for this lesson is to filter a list of potential moves to generate only legal moves that do not leave the king in check. This means we'll be distinguishing between pseudo-legal moves (all the moves a piece can mechanically make) and truly legal moves (moves that are allowed by the rules of chess, specifically the rule about not exposing your king to capture).
1. The Strategy: "Simulate and Check"
So, how do we know if moving a piece is legal? For example, if your rook is "pinned" to your king by an enemy queen, you can't move the rook sideways because it would expose your king to check.
The most straightforward and reliable way to figure this out is a simple, powerful strategy:
- Generate a potential (pseudo-legal) move.
- Pretend to make the move on a temporary, hypothetical copy of the board.
- On this temporary board, use our
is_in_checkfunction to see if our own king is now under attack. - If the king is not in check, the move is legal. If it is in check, the move was illegal.
- Discard the temporary board and repeat for the next potential move.
This "simulate and check" method correctly handles all complex situations like pins, discovered checks, and getting out of an existing check, without us needing to write special code for each scenario.
To get a clear visual of this idea, the following video explains this exact concept. While the code is in C#, the logic is universal and identical to what we will implement in Rust.
Programming a Chess Game in C# | Part 8 - Detect Check & Legal Moves
Watch this segment from the video 'Programming a Chess Game in C# | Part 8' by OttoBotCode. It provides an excellent explanation and visualization of the 'copy-execute-check' strategy for validating moves.
Watch the section from 10:05 to 13:48. Pay close attention to the diagram that illustrates making a move on a copy of the board and then checking the result. This is the core logic for our lesson today.
2. Hands-On: Implementing Legal Move Generation
Ready to translate that logic into Rust? Let's get started. We'll create a new function that takes all the pseudo-legal moves for a piece and filters them down to only the legal ones.
Step 2.1: Making the Board Clone-able
The "simulate and check" strategy requires us to make a copy of our Board. The easiest way to enable this in Rust is by using the clone method. To make clone available, we need to tell the compiler that our data structures are "cloneable" by deriving the Clone trait.
Open your src/main.rs file and find the definitions for your Board, Piece, PieceType, and PieceColor. Add Clone to the #[derive(...)] attribute for each of them. You probably already have Debug and PartialEq, so just add Clone to the list. If you don't have a derive attribute, add the whole line.
// In src/main.rs
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PieceColor {
White,
Black,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PieceType {
Pawn,
Rook,
Knight,
Bishop,
Queen,
King,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Piece {
pub piece_type: PieceType,
pub color: PieceColor,
}
#[derive(Debug, Clone, PartialEq)] // Board should not be Copy
pub struct Board {
// ... your fields ...
pub pieces: [[Option<Piece>; 8]; 8],
}
Note: We use Copy for the simple enums and Piece struct because they are small and simple to duplicate. The Board itself is larger and contains a Vec or array, so we only make it Clone, not Copy. This distinction is an important aspect of Rust's performance philosophy.
Step 2.2: Creating the Legal Move Filter
Now, let's build the function that ties everything together. We will create a new function within impl Board that generates moves for a piece and immediately filters them for legality.
Add the following function inside your impl Board block. It reuses the move generation logic you wrote in Module 5 and the check detection logic from our last lesson.
// Inside your `impl Board` block
/// Generates all legal moves for the piece at a given position.
pub fn generate_legal_moves_for_piece(&self, pos: (u8, u8)) -> Vec<Move> {
let (x, y) = pos;
// 1. Get the piece and its pseudo-legal moves.
// Ensure there is a piece at the position.
let piece = match self.get_piece_at(pos) {
Some(p) => p,
None => return vec![], // No piece, no moves.
};
// This helper should already exist from the previous lesson.
let pseudo_legal_moves = self.generate_pseudo_legal_moves_for_piece(piece, pos);
let mut legal_moves = Vec::new();
let player_color = piece.color;
// 2. For each pseudo-legal move, simulate it and check for legality.
for move_candidate in pseudo_legal_moves {
// Create a temporary board to simulate the move.
let mut temp_board = self.clone();
// Execute the move on the temporary board.
// This function is from lesson 6.1.
temp_board.execute_move(&move_candidate);
// Check if the current player's king is in check after the move.
// This function is from lesson 6.2.
if !temp_board.is_in_check(player_color) {
// If the king is NOT in check, the move is legal.
legal_moves.push(move_candidate);
}
}
// 3. Return the filtered list of legal moves.
legal_moves
}
Look at how our previous work pays off! This function elegantly combines generate_pseudo_legal_moves_for_piece, execute_move, and is_in_check to produce a correct result.
3. Testing Your Legal Move Logic
As always, testing is essential to verify our logic works in tricky situations. Let's test two classic scenarios: a pinned piece and a king that must move out of check.
Add these tests to the end of your main() function.
// In src/main.rs, at the end of the main() function
println!("\n--- Testing Legal Move Generation ---");
// Scenario 1: Pinned Piece
// A white rook on d1 is pinned to the king on e1 by a black rook on h1.
// The rook should only be able to move along the file it's on (to e1, f1, g1).
// It cannot move vertically (e.g., to d2) as that would expose the king.
let mut pinned_board = Board::new();
pinned_board.pieces[0][4] = Some(Piece { piece_type: PieceType::King, color: PieceColor::White }); // White King on e1
pinned_board.pieces[0][3] = Some(Piece { piece_type: PieceType::Rook, color: PieceColor::White }); // White Rook on d1
pinned_board.pieces[0][7] = Some(Piece { piece_type: PieceType::Rook, color: PieceColor::Black }); // Black Rook on h1
println!("\nScenario 1: Pinned Rook");
pinned_board.print_board();
let legal_moves = pinned_board.generate_legal_moves_for_piece((3, 0)); // Moves for rook on d1
println!("Legal moves for pinned rook on d1: {:?}", legal_moves.iter().map(|m| (m.to.0, m.to.1)).collect::<Vec<_>>());
// We assert that the rook cannot move to d2, but can move to f1.
let can_move_to_d2 = legal_moves.iter().any(|m| m.to == (3, 1));
let can_move_to_f1 = legal_moves.iter().any(|m| m.to == (5, 0));
assert!(!can_move_to_d2, "Test Failed: Pinned rook should not be able to move off the pin axis!");
assert!(can_move_to_f1, "Test Failed: Pinned rook should be able to move along the pin axis!");
println!("Test Passed: Pinned rook moves are correctly restricted.");
// Scenario 2: Getting out of check
// White king on e1 is in check from a black rook on e8.
// The only legal moves are to block the check (e.g., with a knight to e2)
// or move the king (e.g., to d1 or f1).
let mut check_evasion_board = Board::new();
check_evasion_board.pieces[0][4] = Some(Piece { piece_type: PieceType::King, color: PieceColor::White }); // King on e1
check_evasion_board.pieces[7][4] = Some(Piece { piece_type: PieceType::Rook, color: PieceColor::Black }); // Rook on e8
check_evasion_board.pieces[0][6] = Some(Piece { piece_type: PieceType::Knight, color: PieceColor::White }); // Knight on g1
println!("\nScenario 2: Evasion from Check");
check_evasion_board.print_board();
let king_moves = check_evasion_board.generate_legal_moves_for_piece((4, 0)); // Moves for king on e1
println!("Legal moves for checked king: {:?}", king_moves.iter().map(|m| (m.to.0, m.to.1)).collect::<Vec<_>>());
assert_eq!(king_moves.len(), 2, "Test Failed: King should have 2 escape squares (d1, f1)");
let knight_moves = check_evasion_board.generate_legal_moves_for_piece((6, 0)); // Moves for knight on g1
println!("Legal moves for knight to block check: {:?}", knight_moves.iter().map(|m| (m.to.0, m.to.1)).collect::<Vec<_>>());
assert_eq!(knight_moves.len(), 1, "Test Failed: Knight should only have 1 legal move to block the check (e2)");
assert!(knight_moves.iter().any(|m| m.to == (4, 1)), "Test Failed: Knight's only legal move is to e2!");
println!("Test Passed: Legal moves correctly limited to check evasions.");
Run cargo run and verify that all tests pass. If they do, congratulations! You have now implemented one of the trickiest parts of a chess engine's rule system.
Conclusion
This was a major step forward! By combining functions we've previously built, you've created a robust system for determining legal moves that correctly handles all sorts of complex chess scenarios automatically.
Key Takeaways:
- A move is only legal if it does not result in the player's own king being in check.
- The "simulate and check" strategy (copying the board, making a move, and checking for check) is a powerful and general way to enforce this rule.
- The
#[derive(Clone)]attribute is a convenient way to make your data structures copyable in Rust. - Your project is becoming more interconnected, with new features building directly on the foundation you've already laid.
In our next lesson, we will use this new generate_legal_moves function to detect the end of the game. We'll implement logic for checkmate, which occurs when a player is in check and has zero legal moves.