Hello! In our last lesson, we built a way to print our chessboard to the console, giving us a clear visual for debugging. This is a perfect foundation for today's task.
Our goal is to generate valid non-capturing and capturing moves for pawns. We'll translate the unique movement rules of the pawn into Rust code. This involves handling its standard forward march, its special two-square opening move, and its diagonal captures. By the end of this lesson, you'll have a function that can take a pawn's position and tell you all the squares it can legally move to.
This is our first step into the core logic of the chess engine: understanding the board state and determining what's possible.
1. Understanding the Pawn's Moves
Before we write any code, let's solidify our understanding of how a pawn moves. Unlike other pieces, its move for capturing is different from its move for advancing.
There are three basic moves we need to implement today:
- Single-Square Advance: A pawn moves one square forward into an empty square.
- Two-Square Advance: On its very first move, a pawn has the option to move two squares forward, but only if both squares in its path are empty.
- Diagonal Capture: A pawn captures an opponent's piece by moving one square diagonally forward.
The following video from ChessKid provides a clear and simple visual explanation of these rules.
How to Move the Pawn | Chess Pieces | ChessKid
This short video demonstrates the three fundamental pawn moves. Pay attention to the distinction between moving forward and capturing diagonally.
Please watch from the beginning to 01:55 to see the standard one-square move and diagonal capture, and then from 03:03 to 03:55 for the initial two-square move.
To summarize, here is a visual guide to these three move types:

Note that we are intentionally not covering the special rules of en passant and pawn promotion today. These have their own complexities and are covered in a future lesson.
2. Planning Our Implementation
Now, let's translate these rules into a plan for our Rust code. Our goal is to create a function that generates a list of all valid moves for a given pawn.
A good function signature might look something like this:fn generate_pawn_moves(position: (u8, u8), color: PieceColor, board: &Board) -> Vec<Move>
To implement this, we'll need a way to represent a move. Let's create a simple struct for that.
Step 1: Create a Move Struct
In your src directory, create a new file named moves.rs. Inside, define a Move struct that holds the starting and ending squares of a move. We'll derive Debug, Clone, Copy, and PartialEq for convenience.
// In src/moves.rs
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Move {
pub from: (u8, u8),
pub to: (u8, u8),
}
Now, make this new module available to the rest of the project by adding pub mod moves; to your src/main.rs file.
Step 2: Set up the Move Generation Logic
We'll organize our move generation logic in its own module.
- Create a new directory named
move_generationinsidesrc. - Inside
src/move_generation, create amod.rsfile. - Add
pub mod move_generation;tosrc/main.rs.
Inside src/move_generation/mod.rs, we'll write our generate_pawn_moves function. A great reference for the logic we need is found in a Bevy chess tutorial, which implements a function to validate if a given pawn move is legal. We can adapt this logic to generate a list of all legal moves.
Chess game in Rust using Bevy - lmao
This blog post contains a fantastic code snippet for validating piece moves in Rust. We will focus on the PieceType::Pawn part of the is_move_valid function and adapt its logic for our move generator.
Scroll down to the impl Piece block and find the is_move_valid function. Inside the match self.piece_type expression, carefully read the code for PieceType::Pawn. Notice how it handles white and black pawns separately and checks conditions for the three move types: normal move, two-square move, and taking a piece.
3. Hands-On: Coding the Pawn Moves
Let's write the code. Open src/move_generation/mod.rs and let's get started. We will need access to our other modules.
// In src/move_generation/mod.rs
use crate::board::Board;
use crate::moves::Move;
use crate::pieces::{Piece, PieceColor};
pub fn generate_pawn_moves(piece: &Piece, position: (u8, u8), board: &Board) -> Vec<Move> {
let mut moves = Vec::new();
let (rank, file) = position;
// TODO: Implement pawn move logic here
moves
}
Now, we'll fill in the TODO. The logic is different for White and Black pawns due to their direction of movement.
Logic for White Pawns
A white pawn at (rank, file) moves "up" the board, meaning its rank increases.
- Starting rank: For us, this is
rank == 1. - Forward direction:
rank + 1.
Let's add the logic for white pawns inside our function:
// Inside generate_pawn_moves, after `let (rank, file) = position;`
if piece.color == PieceColor::White {
// 1. Single-square advance
// The square must be on the board (rank < 7) and empty.
if rank < 7 && board.pieces[rank as usize + 1][file as usize].is_none() {
moves.push(Move { from: position, to: (rank + 1, file) });
}
// 2. Two-square advance
// The pawn must be on its starting rank (1), and the two squares ahead must be empty.
if rank == 1 && board.pieces[2][file as usize].is_none() && board.pieces[3][file as usize].is_none() {
moves.push(Move { from: position, to: (rank + 2, file) });
}
// 3. Diagonal captures
// Must be on the board (rank < 7)
if rank < 7 {
// Capture left (file > 0 to stay on board)
if file > 0 {
if let Some(target_piece) = &board.pieces[rank as usize + 1][file as usize - 1] {
if target_piece.color == PieceColor::Black {
moves.push(Move { from: position, to: (rank + 1, file - 1) });
}
}
}
// Capture right (file < 7 to stay on board)
if file < 7 {
if let Some(target_piece) = &board.pieces[rank as usize + 1][file as usize + 1] {
if target_piece.color == PieceColor::Black {
moves.push(Move { from: position, to: (rank + 1, file + 1) });
}
}
}
}
}
A quick note on coordinates: The resource LINK uses (x, y) where x is the rank and y is the file. Our (rank, file) system matches this, but we use more descriptive names.
Logic for Black Pawns
Now, your turn. Based on the white pawn logic above, add an else block to handle PieceColor::Black. Remember:
- A black pawn moves "down" the board, meaning its rank decreases.
- Its starting rank is
rank == 6. - It captures pieces of
PieceColor::White.
Take a moment to write this logic yourself. The structure will be almost identical to the white pawn's logic, just with the rank arithmetic reversed.
Click here to see the solution for black pawns.
// Inside generate_pawn_moves, add this else block.
else { // Black Pawns
// 1. Single-square advance
if rank > 0 && board.pieces[rank as usize - 1][file as usize].is_none() {
moves.push(Move { from: position, to: (rank - 1, file) });
}
// 2. Two-square advance
if rank == 6 && board.pieces[5][file as usize].is_none() && board.pieces[4][file as usize].is_none() {
moves.push(Move { from: position, to: (rank - 2, file) });
}
// 3. Diagonal captures
if rank > 0 {
// Capture left (file > 0)
if file > 0 {
if let Some(target_piece) = &board.pieces[rank as usize - 1][file as usize - 1] {
if target_piece.color == PieceColor::White {
moves.push(Move { from: position, to: (rank - 1, file - 1) });
}
}
}
// Capture right (file < 7)
if file < 7 {
if let Some(target_piece) = &board.pieces[rank as usize - 1][file as usize + 1] {
if target_piece.color == PieceColor::White {
moves.push(Move { from: position, to: (rank - 1, file + 1) });
}
}
}
}
}
4. Testing Your Move Generator
With the logic complete, let's test it! Go to src/main.rs. We'll pick a few pawns from our starting board and see what moves our function generates for them.
First, make sure you import the necessary items at the top of src/main.rs:
pub mod board;
pub mod pieces;
pub mod moves; // Add this
pub mod move_generation; // Add this
use board::Board;
use move_generation::generate_pawn_moves; // Add this
Now, at the end of your main function, add the following test code:
// In src/main.rs, at the end of main()
println!("Testing Pawn Move Generation:");
// Test the pawn at e2 (rank 1, file 4)
let e2_pos = (1, 4);
if let Some(pawn) = &board.pieces[e2_pos.0 as usize][e2_pos.1 as usize] {
let white_pawn_moves = generate_pawn_moves(pawn, e2_pos, &board);
println!("Moves for white pawn at e2: {:?}", white_pawn_moves);
}
// Test the pawn at d7 (rank 6, file 3)
let d7_pos = (6, 3);
if let Some(pawn) = &board.pieces[d7_pos.0 as usize][d7_pos.1 as usize] {
let black_pawn_moves = generate_pawn_moves(pawn, d7_pos, &board);
println!("Moves for black pawn at d7: {:?}", black_pawn_moves);
}
Run cargo run. You should see output like this:
Testing Pawn Move Generation:
Moves for white pawn at e2: [Move { from: (1, 4), to: (2, 4) }, Move { from: (1, 4), to: (3, 4) }]
Moves for black pawn at d7: [Move { from: (6, 3), to: (5, 3) }, Move { from: (6, 3), to: (4, 3) }]
This confirms our function correctly generates the one-square and two-square advance for pawns on their starting squares. The capture logic isn't tested yet because there are no pieces to capture. As a challenge, you could try manually modifying the board in main.rs to place an enemy piece diagonally in front of a pawn and re-run the test to see the capture move appear!
Conclusion
Fantastic work! You have just implemented a core piece of your chess engine's "brain." You've successfully translated the pawn's complex, context-dependent movement rules into clear, testable Rust code.
Key Takeaways:
- Pawn movement logic is conditional, depending on the pawn's color, its position (starting rank or not), and the state of surrounding squares (empty or occupied by an enemy).
- We can adapt logic from a move validation function to create a move generation function, which is often more useful for game AI and user feedback.
- Organizing code into modules like
movesandmove_generationkeeps the project clean and scalable. - Immediate testing of new logic is crucial for catching bugs early and verifying correctness.
In the next lesson, we will move on to the "sliding" pieces. We'll start with the rook, which will introduce a new challenge: generating moves along a line until it hits another piece or the edge of the board.