Hello! In our last lesson, you successfully implemented the move generation logic for pawns, which have some of the most complex and conditional rules in chess. That was a significant step, and you now have a solid foundation for handling piece-specific logic.
Today, we move on to our first "sliding" piece: the rook. The learning outcome for this lesson is to generate valid moves for rooks along horizontal and vertical lines, stopping at blocking pieces. Unlike the pawn's fixed, one-step moves, the rook introduces the concept of generating moves along a continuous line or "ray" until it hits an obstacle. This pattern will be fundamental for the bishop and queen as well.
1. Understanding Rook Movement
First, let's refresh our memory on how a rook moves. It travels in straight lines—horizontally along ranks and vertically along files—for any number of squares. Its movement stops when it hits the edge of the board or another piece.
The rook in chess: How It Moves and Captures
This article from Chesscul provides a concise explanation of the rook's movement and capture rules. It's a great way to formalize the logic we're about to implement.
Please read the sections titled "Horizontal movement," "Vertical movement," and the bullet point under "Capture limitations" that starts with "Cannot jump." These clearly define the rules we need to code.
On an empty board, a rook has a wide range of movement.

The crucial rule for our logic is that the rook cannot jump over other pieces.
- If it encounters a piece of the same color, its path is blocked. It can move to the square just before that piece, but no further.
- If it encounters a piece of the opposite color, it can capture that piece by moving onto its square, but it cannot move any further in that direction.
This image perfectly illustrates a rook's path being blocked.

2. Planning the Implementation
Our goal is to write a function generate_rook_moves that takes a rook's position and returns a Vec<Move> containing all of its legal moves. We will add this function to our src/move_generation/mod.rs file, alongside the pawn logic from last time.
The signature will be consistent with our previous work:pub fn generate_rook_moves(piece: &Piece, position: (u8, u8), board: &Board) -> Vec<Move>
The strategy is to iterate outwards from the rook's position in four directions:
- Up (increasing rank)
- Down (decreasing rank)
- Right (increasing file)
- Left (decreasing file)
For each direction, we'll use a loop that moves one square at a time. In each iteration, we'll check the new square:
- Is it on the board? If not, the loop for this direction is done.
- Is it empty? If yes, it's a valid move. We add it to our list and continue to the next square in the same direction.
- Is it occupied?
- If by an enemy piece, it's a valid capture. We add it to our list but then stop the loop for this direction.
- If by a friendly piece, it's not a valid move. We simply stop the loop for this direction.
3. Hands-On: Coding the Rook Moves
Let's put this plan into action. Open src/move_generation/mod.rs and add the skeleton for our new function.
// In src/move_generation/mod.rs
use crate::board::Board;
use crate::moves::Move;
use crate::pieces::{Piece, PieceColor};
// ... (your existing generate_pawn_moves function)
pub fn generate_rook_moves(piece: &Piece, position: (u8, u8), board: &Board) -> Vec<Move> {
let mut moves = Vec::new();
let (rank, file) = position;
// TODO: Implement the four directions of movement
moves
}
Now, let's implement the logic for the four directions inside the function. I'll provide the code for the "up" direction as a template. A for loop is perfect for this.
// Inside generate_rook_moves, replace the TODO comment
// Direction 1: Up (increasing rank)
for r in (rank + 1)..8 {
let current_pos = (r, file);
if let Some(target_piece) = &board.pieces[r as usize][file as usize] {
if target_piece.color != piece.color {
moves.push(Move { from: position, to: current_pos }); // Capture
}
break; // Stop at the first piece encountered (friendly or enemy)
} else {
moves.push(Move { from: position, to: current_pos }); // Empty square
}
}
This code iterates from the square just above the rook (rank + 1) to the edge of the board (..8). Inside the loop, it checks the square. If it finds a piece, it adds a capture move (if it's an enemy) and then immediately breaks. If the square is empty, it adds the move and the loop continues.
Your Turn: Complete the Other Directions
Now it's your turn to apply this pattern to the other three directions. The logic is nearly identical, but the loops will be different.
- Down (decreasing rank): You'll need a loop that goes from
rank - 1down to0. A reversed range like(0..rank).rev()is perfect for this. - Right (increasing file): Similar to the "up" direction, but you'll iterate through files
(file + 1)..8while keeping the rank constant. - Left (decreasing file): Similar to the "down" direction, using
(0..file).rev()while keeping the rank constant.
Take a few minutes to implement these three loops. This exercise will solidify your understanding of range-based iteration and board coordinates.
Click here to see the complete solution.
pub fn generate_rook_moves(piece: &Piece, position: (u8, u8), board: &Board) -> Vec<Move> {
let mut moves = Vec::new();
let (rank, file) = position;
// Direction 1: Up (increasing rank)
for r in (rank + 1)..8 {
let current_pos = (r, file);
if let Some(target_piece) = &board.pieces[r as usize][file as usize] {
if target_piece.color != piece.color {
moves.push(Move { from: position, to: current_pos });
}
break;
} else {
moves.push(Move { from: position, to: current_pos });
}
}
// Direction 2: Down (decreasing rank)
for r in (0..rank).rev() {
let current_pos = (r, file);
if let Some(target_piece) = &board.pieces[r as usize][file as usize] {
if target_piece.color != piece.color {
moves.push(Move { from: position, to: current_pos });
}
break;
} else {
moves.push(Move { from: position, to: current_pos });
}
}
// Direction 3: Right (increasing file)
for f in (file + 1)..8 {
let current_pos = (rank, f);
if let Some(target_piece) = &board.pieces[rank as usize][f as usize] {
if target_piece.color != piece.color {
moves.push(Move { from: position, to: current_pos });
}
break;
} else {
moves.push(Move { from: position, to: current_pos });
}
}
// Direction 4: Left (decreasing file)
for f in (0..file).rev() {
let current_pos = (rank, f);
if let Some(target_piece) = &board.pieces[rank as usize][f as usize] {
if target_piece.color != piece.color {
moves.push(Move { from: position, to: current_pos });
}
break;
} else {
moves.push(Move { from: position, to: current_pos });
}
}
moves
}
For reference, another common way to implement this is with a function that checks if the path between two squares is empty. The Bevy chess tutorial you looked at in the last lesson does exactly this.
Chess game in Rust using Bevy - lmao
This article demonstrates an alternative approach. Instead of generating moves by iterating, it validates a given move by checking if the path is clear. It's useful to see different ways of solving the same problem.
Skim the function is_path_empty. Then, look at the PieceType::Rook case inside the is_move_valid function. You'll see it calls is_path_empty and then checks if the move is strictly horizontal or vertical. Our loop-based generator is generally more efficient for finding all moves at once.
4. Testing Your Rook Logic
Now for the most important part: testing. Go to your src/main.rs file.
First, import the new function:
// At the top of src/main.rs
use move_generation::{generate_pawn_moves, generate_rook_moves};
Then, at the end of your main function, let's test a rook. A rook at a1 (position (0,0)) on the starting board is completely blocked by its own pieces. This is a great first test case.
// In src/main.rs, at the end of main()
println!("\nTesting Rook Move Generation:");
// Test the rook at a1 (0, 0) on the starting board. It should have 0 moves.
let a1_pos = (0, 0);
if let Some(rook) = &board.pieces[a1_pos.0 as usize][a1_pos.1 as usize] {
let rook_moves = generate_rook_moves(rook, a1_pos, &board);
println!("Moves for white rook at a1: {:?}", rook_moves);
assert!(rook_moves.is_empty()); // This confirms it found no moves
}
Run cargo run. The program should run without panicking on the assert!, and you should see an empty list of moves for the rook.
Now, for a more interesting test. Let's create a custom board state. We can do this by clearing the default board and placing pieces manually. Add this code to main.rs to test a rook in the center of the board with a few obstacles.
// In src/main.rs, after the first rook test
// Create a new board for a custom test scenario
let mut test_board = Board::new(); // Clears the board
test_board.pieces[3][3] = Some(Piece {
piece_type: PieceType::Rook,
color: PieceColor::White,
});
test_board.pieces[3][6] = Some(Piece { // Enemy piece
piece_type: PieceType::Pawn,
color: PieceColor::Black,
});
test_board.pieces[1][3] = Some(Piece { // Friendly piece
piece_type: PieceType::Pawn,
color: PieceColor::White,
});
let d4_pos = (3, 3);
if let Some(rook) = &test_board.pieces[d4_pos.0 as usize][d4_pos.1 as usize] {
let rook_moves = generate_rook_moves(rook, d4_pos, &test_board);
println!("\nMoves for test rook at d4: {:?}", rook_moves);
}
When you run cargo run, you should see a list of moves for the rook at d4 ((3,3)). Verify the output:
- It should have moves going up (
(4,3), (5,3), ...). - It should have a move to
(2,3), but not(1,3)(blocked by friendly pawn) or(0,3). - It should have moves going left (
(3,2), (3,1), (3,0)). - It should have moves going right up to and including the capture at
(3,6), but not(3,7).
This confirms your logic for empty squares, friendly blockers, and enemy captures is working correctly!
Conclusion
Excellent work today! You've successfully implemented the move generation for a sliding piece, which is a significant milestone. This iterative, ray-casting approach is a core concept in chess programming.
Key Takeaways:
- Sliding piece moves can be generated by looping in each valid direction (horizontal/vertical for rooks).
- The loop for a given direction must terminate when it hits the board edge or any piece.
- A move is added if the square is empty or contains an enemy piece (a capture). A move is not added if the square contains a friendly piece.
- Creating custom board scenarios is essential for thoroughly testing move generation logic.
In the next lesson, we will tackle the bishop. You'll find that the logic is remarkably similar to the rook's, but applied to diagonals instead of straight lines. The code you wrote today will serve as a perfect template for it.