Hello! In our last lesson, we successfully implemented pseudo-legal move generation for every piece, including the king. You now have the logic to determine all the possible moves a piece can make based on its movement rules. That's a huge milestone!
Today, we'll bridge the gap between planning moves and actually making them. This lesson focuses on a fundamental part of the game engine: writing a function to execute a move by updating the piece positions on the board struct. This function will become the heart of our game loop, driving the game forward one turn at a time.
1. The Anatomy of a Move
Before we can execute a move, we need a clear, structured way to represent one. In previous lessons, we've implicitly used pairs of coordinates. Let's formalize this.
A move, at its most basic, needs to know two things: the square the piece is coming from and the square it's going to. We can define a simple struct for this:
#[derive(Debug, Clone, Copy, PartialEq)] // We'll talk more about `derive` soon!
pub struct Move {
pub from: (u8, u8),
pub to: (u8, u8),
}
You can add this struct to a relevant file, like src/main.rs or a new src/game.rs if you've started organizing your code.
With this Move struct, "executing a move" on our Board struct (which we've defined as an 8x8 array of Option<Piece>) involves two main steps:
- Remove the piece from the
fromsquare. TheOption<Piece>at this position becomesNone. - Place that piece on the
tosquare. TheOption<Piece>at this position becomesSome(piece).
This process naturally handles captures: if there was a piece on the to square, it simply gets replaced.
2. Hands-On: Implementing execute_move
Let's add a method to our Board struct to perform this operation. Because this function will change the board's state, it needs to take a mutable reference to the board, which we write as &mut self.
-
Open the file where your
Boardstruct is defined. -
Add the following
implblock:// In the file with your Board struct // ... your other structs (Board, Piece, etc.) impl Board { // ... your other methods like `new()` and `get_piece_at()` pub fn execute_move(&mut self, mv: &Move) { // Use .take() to move the piece out of the `from` square, // leaving `None` in its place. if let Some(mut piece) = self.pieces[mv.from.1 as usize][mv.from.0 as usize].take() { // If your Piece struct stores its own position, update it! // This is a crucial step for state consistency. // We'll assume your Piece has `x` and `y` fields for this example. // If it doesn't, you can ignore these two lines. // piece.x = mv.to.0; // piece.y = mv.to.1; // Place the piece on the `to` square. // This will overwrite any piece that was there (a capture). self.pieces[mv.to.1 as usize][mv.to.0 as usize] = Some(piece); } } }
Key Concepts in this Code:
&mut self: This tells Rust that theexecute_movemethod needs to borrow theBoardinstance mutably, allowing it to change its data (thepiecesarray)..take(): This is a very handy method onOption<T>. It moves the value out of theOption, returning the value insideSome, but leavingNonebehind in its original spot. It's the perfect tool for "picking up" a piece from a square.- State Consistency: Many chess engine designs include the piece's coordinates within the
Piecestruct itself, as seen in some tutorials.
Chess game in Rust using Bevy - lmao
For context, have a quick look at the Piece struct definition in the "Chess game in Rust using Bevy" tutorial. You'll see it includes x and y coordinates.
Scroll down to the Rust code block that defines the PieceColor, PieceType, and Piece structs. Notice that the Piece struct contains x: u8 and y: u8 fields. The author explains this is to know the piece's 'true' square, even during a graphical animation. If you adopt a similar design, updating these fields during a move is essential.
If your Piece struct has x and y fields, it's critical to update them when the piece moves. Forgetting this step can lead to confusing bugs where the board's array and the piece's internal data are out of sync.
3. Testing Your Implementation
Now, let's verify that our new function works. We'll set up a simple scenario in main.rs, execute a move, and check that the board state has changed as expected.
Add the following code to the end of your main function:
// In src/main.rs, at the end of the main() function
println!("\n--- Testing Move Execution ---");
// 1. Create a board and place a white pawn at e2 (4, 1)
let mut move_test_board = Board::new();
let pawn_pos = (4, 1);
move_test_board.pieces[pawn_pos.1 as usize][pawn_pos.0 as usize] = Some(Piece {
piece_type: PieceType::Pawn,
color: PieceColor::White,
});
println!("Board before move:");
move_test_board.print_board(); // Assuming you have a print_board() method
// 2. Define a move from e2 to e4
let pawn_move = Move { from: (4, 1), to: (4, 3) };
// 3. Execute the move
move_test_board.execute_move(&pawn_move);
println!("\nBoard after moving pawn e2 to e4:");
move_test_board.print_board();
// 4. Verify the change
assert!(move_test_board.get_piece_at((4, 1)).is_none());
assert!(move_test_board.get_piece_at((4, 3)).is_some());
if let Some(pawn) = move_test_board.get_piece_at((4, 3)) {
assert_eq!(pawn.piece_type, PieceType::Pawn);
}
println!("\nMove execution test passed!");
Run your project with cargo run. You should see the board printed before and after, showing the pawn moving from its starting square to two squares forward. The assertions will confirm the logic is correct.
4. The Bigger Picture: apply_move and undo_move
Our execute_move function is a crucial building block. In the world of chess engines, this kind of function is often called apply_move. It's typically paired with an undo_move function. This pair is essential for an AI to explore different future possibilities and for us to implement check detection.
Pleco: Creating A Chess Engine With Rust
The article 'Pleco: Creating A Chess Engine With Rust' gives a great high-level view of why applying and undoing moves are central to a chess engine's design.
Read the two bullet points under the heading 'Mile-High Chess Board Design'. The author outlines the core requirements for Board::apply_move() and Board::undo_move(). Our execute_move function fulfills the basic need of apply_move: it changes the board state to reflect a move being played.
The article hints at why undo_move is complex: you have to restore the board to its exact previous state. This includes putting back a captured piece, resetting castling rights, or handling en passant captures. We won't implement undo_move right now, but it's valuable to understand that our execute_move is the first half of this powerful pattern. We will use it on temporary copies of the board to check for illegal moves.
Conclusion
Excellent work! You've just built the engine that powers your chess game. While it seems simple, this execute_move function is the mechanism that will bring your game to life.
Key Takeaways:
- A
Movecan be represented as a simple struct containingfromandtocoordinates. - Executing a move on the board involves changing the
Option<Piece>values in thepiecesarray. - The
Option::take()method is an idiomatic and effective way to "pick up" a piece from a square. - Maintaining consistent state is crucial, especially if your
Piecestruct stores its own coordinates. - Our function is the
apply_movepart of a fundamentalapply/undopattern used in all chess engines.
In our next lesson, we will start building on this foundation. Now that we can alter the board's state, we can begin to answer more complex questions. We'll start with a big one: implementing the logic to detect if a player's king is currently in check.