Skip to main content

Welcome to a Grasp lesson

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

Lesson 27

Implementing Piece Movement

Recommended resources

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:

  1. Remove the piece from the from square. The Option<Piece> at this position becomes None.
  2. Place that piece on the to square. The Option<Piece> at this position becomes Some(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.

  1. Open the file where your Board struct is defined.

  2. Add the following impl block:

    // 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 the execute_move method needs to borrow the Board instance mutably, allowing it to change its data (the pieces array).
  • .take(): This is a very handy method on Option<T>. It moves the value out of the Option, returning the value inside Some, but leaving None behind 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 Piece struct 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 Move can be represented as a simple struct containing from and to coordinates.
  • Executing a move on the board involves changing the Option<Piece> values in the pieces array.
  • 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 Piece struct stores its own coordinates.
  • Our function is the apply_move part of a fundamental apply/undo pattern 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.