Skip to main content

Welcome to a Grasp lesson

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

Lesson 35

Pawn Promotion: Reaching the Opposite Rank

Recommended resources

Hello! Welcome back to our chess project.

In the last lesson, we tackled the tricky en passant rule by adding state to our board to remember the previous move. With that, we've covered almost every move in chess. There's just one more special rule to implement before our move generation logic is complete.

Today's goal is to implement pawn promotion when a pawn reaches the opposite rank. This is the exciting rule that lets a humble pawn become a much more powerful piece, often turning the tide of a game.

1. Understanding Pawn Promotion

When a pawn advances all the way to the other side of the board (the 8th rank for White, or the 1st rank for Black), it gets an immediate promotion. The player must replace the pawn with a new queen, rook, bishop, or knight of the same color.

Pawn Promotion to Queen Animation
This animation shows a white pawn advancing to the final rank and being promoted to a queen.

The player gets to choose which piece they want. As you can imagine, they almost always choose a queen, the most powerful piece.

Pawn Promotion Options on a Chess Board
When a pawn promotes, the player can choose between a Queen, Rook, Bishop, or Knight.

This presents a new challenge for our program. A normal move is just from a square to a square. But a promotion move is from a square to a square, plus a choice of what piece to become. How do we represent that?


2. Representing a Promotion Move

Let's think about how to encode this extra information. When a promotion happens, the move itself needs to carry the data about which piece was chosen.

ZuhaadRathore/Chess-Engine - GitHub

This GitHub repository for a chess engine (written in TypeScript) gives us a clue. Look at the make_move function signature to see how it handles this exact problem.

Find the make_move function definition. Notice the third parameter, promotion?. This shows that the function can take an optional argument specifying the promotion piece.

As you saw, the make_move function includes an optional promotion parameter. This is a great model. We need to do something similar in our Rust code. The most idiomatic way is to add a field to our Move struct.

Your Move struct probably looks something like this:




#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Move {
    pub from: Position,
    pub to: Position,
}

Let's add an Option<PieceType> to it. This field will be Some(PieceType) if the move is a promotion, and None otherwise.

// In your file where the Move struct is defined




#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Move {
    pub from: Position,
    pub to: Position,
    pub promotion: Option<PieceType>, // Add this field!
}

Go ahead and add this field to your Move struct. You will get compiler errors in places where you create a Move instance. You can fix them by adding promotion: None to each one. For example:
Move { from, to } becomes Move { from, to, promotion: None }.

This change is crucial because it allows our Move objects to carry all the information we need to both generate and execute a promotion.


3. Hands-On: Generating Promotion Moves

Now that our Move struct can represent a promotion, we need to update our pawn move generation logic.

The core idea is this: when a pawn's forward move would land it on the promotion rank, we don't generate one move. We generate four moves—one for each possible promotion piece.

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

The article 'Building a Chess AI, Part 2' describes this exact logic. It shows how to check if a pawn's destination is a promotion rank and then create the corresponding moves.

Read the section titled 'Pawn promotions'. It explains that if a pawn advance reaches the 1st or 8th rank, four moves are available.

Let's apply this to your code. Inside your generate_pawn_moves function (or wherever you generate moves for pawns), you'll need to modify the part that handles single-step forward advances.

Assuming your ranks are 0-indexed (where White starts on ranks 0 and 1, and Black on 6 and 7):

  • A White pawn on rank 6 moving to rank 7 is a promotion.
  • A Black pawn on rank 1 moving to rank 0 is a promotion.

Here is the logic to integrate into your pawn move generation.

// Inside your pawn move generation logic, where you calculate forward moves...

let current_pos = Position { rank: r, file: f };
let direction: i8 = if piece.color == PieceColor::White { 1 } else { -1 };

// Calculate the one-step forward move
let next_rank = (current_pos.rank as i8 + direction) as usize;
let destination = Position { rank: next_rank, file: current_pos.file };

// Check if the destination square is empty for a forward move
if is_on_board(destination) && self.squares[destination.rank][destination.file].is_none() {
    
    // --- PROMOTION LOGIC ---
    let promotion_rank = if piece.color == PieceColor::White { 7 } else { 0 };

    if destination.rank == promotion_rank {
        // This is a promotion move, generate all four possibilities
        moves.push(Move { from: current_pos, to: destination, promotion: Some(PieceType::Queen) });
        moves.push(Move { from: current_pos, to: destination, promotion: Some(PieceType::Rook) });
        moves.push(Move { from: current_pos, to: destination, promotion: Some(PieceType::Bishop) });
        moves.push(Move { from: current_pos, to: destination, promotion: Some(PieceType::Knight) });
    } else {
        // This is a normal one-step pawn move
        moves.push(Move { from: current_pos, to: destination, promotion: None });
    }

    // --- END PROMOTION LOGIC ---

    // ... continue with two-step move logic if applicable ...
}

// ... continue with pawn capture logic ...
// Note: Pawn captures can also be promotions! You'll need to add similar promotion
// logic for diagonal captures that land on the final rank.

Take a moment to integrate this. The key is to check the destination rank. If it's the final rank, create the four promotion moves. If not, create a single, normal move. Don't forget to apply this same logic for diagonal captures that land on the promotion rank!


4. Hands-On: Executing the Promotion

Just like with en passant, generating the move is only the first step. We also need to update execute_move to handle it correctly.

When your execute_move function sees a move with promotion: Some(piece_type), it needs to place the new piece on the board, not the pawn.

Here is how you can modify your execute_move function:

// In your `impl Board`, inside your `execute_move` function

pub fn execute_move(&mut self, mv: &Move) {
    // Take the piece from the 'from' square
    let moving_piece = self.squares[mv.from.rank][mv.from.file].take().unwrap();

    // Check if this is a promotion move
    if let Some(promotion_piece_type) = mv.promotion {
        // It's a promotion! Place the new piece on the 'to' square.
        // The color is the same as the moving pawn's color.
        self.squares[mv.to.rank][mv.to.file] = Some(Piece {
            piece_type: promotion_piece_type,
            color: moving_piece.color,
        });
    } else {
        // It's a normal move. Just move the piece.
        self.squares[mv.to.rank][mv.to.file] = Some(moving_piece);
    }
    
    // ... rest of your execute_move logic (resetting en_passant, updating castling rights, etc.) ...
}

This logic is straightforward: if the promotion field is Some, we create a brand new piece of the specified type. Otherwise, we just move the existing piece.


Conclusion

Congratulations! With pawn promotion implemented, you have now coded the logic for all legal moves in chess. This is a massive milestone in building your engine. Your Board struct and move generator now fully understand the rules of the game.

Key Takeaways:

  • Pawn promotion requires extra information (the chosen piece), which we encoded by adding promotion: Option<PieceType> to our Move struct.
  • When a pawn move or capture lands on the final rank, you must generate four separate Moves, one for each possible promotion piece.
  • The execute_move function must check for a promotion and place the new piece on the board instead of the pawn.

Preview of the Next Lesson

Our game logic is now complete! But so far, the only way to see what's happening is by printing debug information. In the next module, we'll start making this a real, playable game. We will begin by creating a main game loop and parsing user input from the console (e.g., "e2e4"), finally bringing your chess engine to life.