Hello! Welcome back to building your chess game in Rust.
In the last lesson, we created the "heartbeat" of our game: a main loop that alternates turns between players. Your program can now understand move notation like e2e4 and switch from White's turn to Black's. However, it doesn't yet know if e2e4 is a valid move, or if a player is trying to do something illegal, like moving a rook diagonally.
Today, we will connect the move logic you built in previous modules to our game loop. Our goal is to validate that a player's requested move is legal for the current turn. By the end of this lesson, your game will prevent players from making any moves that violate the rules of chess.
1. From Pseudo-Legal to Legal
The core rule we need to enforce is this: a player cannot make a move that leaves their own king in check.
Your move generation functions from Module 5 and 7 produce what we call pseudo-legal moves—moves that follow a piece's basic movement rules. For example, they know a bishop moves diagonally. However, they don't account for the context of the entire board, such as whether a move would expose the king to an attack.
A legal move is a pseudo-legal move that also passes this critical safety check.
Programming a Chess Game in C# | Part 8 - Detect Check & Legal Moves
To see this distinction in action, let's start with a short video that explains the concept of illegal moves and the three ways a player can get out of check. This provides the foundation for the logic we're about to build.
Watch the first two minutes (00:00 - 02:09). The video gives clear visual examples of moves that seem valid but are illegal because they place the king in check.
2. The Strategy: A "Look Ahead" Simulation
So, how can we know if a move will result in our king being in check before we actually make the move on the main board? The most common and reliable method is a simple simulation:
- Create a temporary copy of the game board.
- Execute the potential move on this temporary copy.
- On this modified temporary board, check if the current player's king is now under attack.
- If the king is not under attack, the move is legal.
- If the king is under attack, the move is illegal. Discard the temporary board and reject the move.
This "clone, execute, check" strategy is a cornerstone of chess programming.
Let's watch two different developers explain this exact strategy. Seeing it presented in a couple of ways will solidify the concept.
First, watch from 06:00 to 07:11. The developer gives a very quick and clear summary of taking a pseudo-legal move, playing it on the board, and then checking the opponent's responses to see if the king can be captured.
Programming a Chess Game in C# | Part 8 - Detect Check & Legal Moves
Now, let's see a more detailed walkthrough of the implementation. This video, although in C#, perfectly illustrates the code structure for our strategy.
Watch from 10:05 to 13:02. Pay close attention to how a copy of the board is made, the move is executed on the copy, and an is in check function is called to determine the move's legality. This is exactly what we will implement in Rust.
3. Hands-On: Implementing Move Validation
Excellent! You now have the theory and the strategy. The great news is that from your work in Module 6, you already have the key functions we need: execute_move and is_in_check. Let's put them to work.
Step 1: Making the Board Clone-able
Our strategy relies on creating a temporary copy of the board. To do this in Rust, our Board struct needs to implement the Clone trait.
Open the file containing your Board struct and add #[derive(Clone)] above it. You will also need to ensure that any structs contained within Board (like your Piece struct and Color enum) derive Clone and Copy.
// In your board representation file, e.g., src/board.rs
// Add Clone and Copy to your enums and piece struct
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Color { /* ... */ }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PieceType { /* ... */ }
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Piece {
pub piece_type: PieceType,
pub color: Color,
}
// And finally, derive Clone for the board itself
#[derive(Debug, Clone)] // No Copy for Board, it's too large
pub struct Board {
// your 8x8 array of Option<Piece>
// ...
}
Note: We use Copy for small, simple data like enums and the Piece struct, but only Clone for the Board itself, as copying an entire 8x8 array is not a trivial operation.
Step 2: Creating a Validation Function
Let's create a single, clean function to check if a move is legal. A good place for this is in your Game struct's implementation block, as it involves the context of the current_turn.
// In src/main.rs, inside the `impl Game` block
/// Checks if a given move is legal in the current game state.
pub fn is_move_legal(&self, mv: &input::Move) -> bool {
// First, generate all pseudo-legal moves for the current player.
// You should have a function for this from modules 5 & 7.
// Let's assume it's called `generate_pseudo_legal_moves`.
let pseudo_legal_moves = self.board.generate_pseudo_legal_moves(self.current_turn);
// 1. Is the requested move even pseudo-legal?
if !pseudo_legal_moves.contains(mv) {
return false;
}
// 2. If it is, perform the "look ahead" check.
let mut temp_board = self.board.clone();
// You wrote execute_move in Module 6!
temp_board.execute_move(mv);
// You wrote is_in_check in Module 6!
// The move is legal if, after making it, the current player is NOT in check.
!temp_board.is_in_check(self.current_turn)
}
This function does two things:
- It quickly rejects any move that doesn't follow the basic piece movement rules.
- For moves that are pseudo-legal, it performs our simulation to ensure they don't result in self-check.
Step 3: Integrating into the Game Loop
Now for the final step. Let's update the main function to use our new is_move_legal validator.
// In src/main.rs, in your main function's loop
// ... inside your loop ...
match input::parse_move(user_input.trim()) {
Ok(parsed_move) => {
// Use our new validation function!
if game.is_move_legal(&parsed_move) {
println!("Executing legal move: {:?}", parsed_move);
game.board.execute_move(&parsed_move); // Apply to the *real* board
game.switch_turn();
} else {
println!("Illegal move: {}. This move is not allowed.", user_input.trim());
// Do not switch turns, let the player try again.
}
}
Err(e) => {
println!("Error parsing move: {}. Please try again.", e);
}
}
// ...
And that's it! Your game loop no longer blindly accepts any formatted string. It now consults your game's logic to determine if a move is valid before applying it and switching turns.
For a great example of this pattern in a real Rust crate, you can look at the dsaiko/chessgen repository.
dsaiko/chessgen: Chess moves generator - GitHub
This GitHub repository provides a chess move generator in Rust. Look at the validate_and_apply_move function in the README.
Scroll to the "Applying a move" section. The example shows a function validate_and_apply_move. This is a common pattern that combines the logic we just built (is_move_legal and execute_move) into a single, convenient function. You could consider refactoring your code this way if you like!
Conclusion
Congratulations! You've just made a massive leap forward. Your chess engine is no longer just a board and pieces; it's now a referee that understands and enforces one of the most fundamental rules of chess.
Key Takeaways:
- A legal move is a pseudo-legal move that does not leave the moving player's king in check.
- The "clone, execute, check" strategy is a robust way to validate move legality without altering the actual game state.
- Rust's trait system (
Clone) allows for easy implementation of this pattern. - By integrating this logic into the game loop, you ensure that only valid moves are ever played.
Preview of the Next Lesson
Your game now correctly handles turn-by-turn play. But what happens when the game is over? In the next lesson, we will build on today's logic to integrate checkmate and stalemate detection to end the game appropriately.