Skip to main content

Welcome to a Grasp lesson

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

Lesson 23

Bishop Movement Rules

Recommended resources

Hello! Last time, we successfully implemented the move generation for rooks, our first sliding piece. You did a great job applying the "ray-casting" method, where we iterate outwards from a piece until we hit an obstacle.

In this lesson, we'll continue with that theme and tackle the bishop. Our learning outcome is to generate valid moves for bishops along diagonal lines, stopping at blocking pieces. The logic is very similar to the rook's, but applied to the four diagonal directions. Mastering this will make implementing the queen's movement—our next topic—remarkably straightforward.

1. Understanding Bishop Movement

A bishop moves any number of squares diagonally, in any of the four diagonal directions. Just like the rook, its path is blocked if it runs into another piece.

  • It cannot jump over any piece.
  • Its path is blocked by a friendly piece (it can move up to the square before it).
  • It can capture an opponent's piece by moving onto its square, but it cannot move any further in that direction.

This image clearly illustrates the bishop's lines of attack.

Bishop Movement on a Chess Board
A bishop's movement is restricted to diagonals. Notice how the paths are blocked by friendly or enemy pieces, limiting its range.

2. Planning the Implementation

We'll follow the same strategy we used for the rook, creating a new function generate_bishop_moves in src/move_generation/mod.rs. The function signature will be identical in structure:

pub fn generate_bishop_moves(piece: &Piece, position: (u8, u8), board: &Board) -> Vec<Move>

The core idea is to iterate outwards from the bishop's position in the four diagonal directions:

  1. Up-Right: Rank increases, file increases.
  2. Up-Left: Rank increases, file decreases.
  3. Down-Left: Rank decreases, file decreases.
  4. Down-Right: Rank decreases, file increases.

For the rook, we could use a simple for loop because only one coordinate was changing at a time. For the bishop, both rank and file change with each step. A while loop will be more suitable here, as we can manually update both coordinates in each iteration and check if we're still on the board.


3. Hands-On: Coding the Bishop Moves

Let's get to the code. Open src/move_generation/mod.rs and add the skeleton for our new function.

// In src/move_generation/mod.rs

// ... (your existing imports and functions)

pub fn generate_bishop_moves(piece: &Piece, position: (u8, u8), board: &Board) -> Vec<Move> {
    let mut moves = Vec::new();
    let (start_rank, start_file) = position;

    // TODO: Implement the four diagonal directions of movement

    moves
}

Now, let's implement the logic for the four directions. I'll provide the code for the "Up-Right" direction as a template. Notice the use of the while loop.

// Inside generate_bishop_moves, replace the TODO comment

// Direction 1: Up-Right
let (mut r, mut f) = (start_rank + 1, start_file + 1);
while r < 8 && f < 8 {
    let current_pos = (r, f);
    if let Some(target_piece) = &board.pieces[r as usize][f as usize] {
        if target_piece.color != piece.color {
            moves.push(Move { from: position, to: current_pos }); // Capture
        }
        break; // Stop at the first piece encountered
    } else {
        moves.push(Move { from: position, to: current_pos }); // Empty square
    }
    r += 1;
    f += 1;
}

As you can see, the logic inside the loop is nearly identical to the rook's. The main difference is the loop condition (while r < 8 && f < 8) and how we increment both r and f at the end of each iteration.

Your Turn: Complete the Other Diagonals

Now, it's your turn to implement the remaining three directions. The pattern is the same:

  1. Initialize r and f to the first square in the chosen direction.
  2. Set up a while loop that runs as long as r and f are within the board boundaries (0 to 7).
  3. Inside the loop, use the same logic to check for empty squares, captures, or friendly blockers.
  4. At the end of the loop, update r and f to move to the next square along that diagonal.

Take a few minutes to code the "Up-Left", "Down-Left", and "Down-Right" directions. This will really help solidify your understanding.

Click here to see the complete solution.
pub fn generate_bishop_moves(piece: &Piece, position: (u8, u8), board: &Board) -> Vec<Move> {
    let mut moves = Vec::new();
    let (start_rank, start_file) = position;

    // Direction 1: Up-Right
    let (mut r, mut f) = (start_rank + 1, start_file + 1);
    while r < 8 && f < 8 {
        let current_pos = (r, f);
        if let Some(target_piece) = &board.pieces[r 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 });
        }
        r += 1;
        f += 1;
    }

    // Direction 2: Up-Left
    let (mut r, mut f) = (start_rank + 1, start_file.wrapping_sub(1));
    while r < 8 && f < 8 { // f < 8 is safe because wrapping_sub makes it large if start_file is 0
        let current_pos = (r, f);
        if let Some(target_piece) = &board.pieces[r 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 });
        }
        r += 1;
        f = f.wrapping_sub(1);
    }

    // Direction 3: Down-Left
    let (mut r, mut f) = (start_rank.wrapping_sub(1), start_file.wrapping_sub(1));
    while r < 8 && f < 8 {
        let current_pos = (r, f);
        if let Some(target_piece) = &board.pieces[r 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 });
        }
        r = r.wrapping_sub(1);
        f = f.wrapping_sub(1);
    }
    
    // Direction 4: Down-Right
    let (mut r, mut f) = (start_rank.wrapping_sub(1), start_file + 1);
    while r < 8 && f < 8 {
        let current_pos = (r, f);
        if let Some(target_piece) = &board.pieces[r 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 });
        }
        r = r.wrapping_sub(1);
        f += 1;
    }

    moves
}

Note on wrapping_sub(1): We use this for decrementing u8 values. If we try to subtract 1 from 0, it would panic in a debug build. wrapping_sub handles this by "wrapping around" to 255. The r < 8 && f < 8 condition in our while loop correctly handles this by immediately terminating the loop if a coordinate wraps around, effectively treating it as going off the board.


4. Alternative Perspectives

Our while loop approach is clear and effective. As you grow as a programmer, you'll see other clever ways to solve the same problem. Let's look at a couple.

The Bevy chess tutorial you've seen uses a helper function is_path_empty to check for blockers between a start and end square. The bishop logic then first checks if a move is diagonal (x_diff == y_diff) and then if the path is clear.

Chess game in Rust using Bevy - lmao

Let's revisit the Bevy chess tutorial. It shows an alternative to our move generation approach: move validation. Pay attention to how it handles diagonal path checking.

First, find the function is_path_empty and look for the comment // Diagonals. This is how it iterates along a diagonal to check for blockers. Then, find the is_move_valid function and look for the PieceType::Bishop case. See how it combines the diagonal check with the path-clearing check.

For a more abstract and highly efficient approach, many chess engines represent directions as numerical offsets. For a bishop, the four diagonal directions on an 8x8 grid can be represented by adding or subtracting specific numbers from a square's index in a 1D array.

Coding Adventure: Chess

Sebastian Lague's video on creating a chess AI demonstrates this offset-based approach very well. It's a common technique in high-performance chess engines.

Watch from 03:22 to 05:16. He shows an array of 8 direction offsets. For a bishop, he only uses the last four (the diagonal ones). He then loops in each direction until he hits the edge of the board or a blocking piece. This is conceptually the same as our while loop, but organized differently.


5. Testing Your Bishop Logic

Time to test our work. 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, generate_bishop_moves};

Then, at the end of your main function, let's test a bishop on the starting board. The bishop at c1 (position (0, 2)) is completely blocked.

// In src/main.rs, at the end of main()

println!("\nTesting Bishop Move Generation:");

// Test the bishop at c1 (0, 2) on the starting board. It should have 0 moves.
let c1_pos = (0, 2);
if let Some(bishop) = &board.pieces[c1_pos.0 as usize][c1_pos.1 as usize] {
    let bishop_moves = generate_bishop_moves(bishop, c1_pos, &board);
    println!("Moves for white bishop at c1: {:?}", bishop_moves);
    assert!(bishop_moves.is_empty());
}

Run cargo run. The program should complete without error, confirming the bishop has no moves.

Now, let's set up a custom scenario to test the logic more thoroughly. We'll place a bishop at d4 with a friendly piece and an enemy piece blocking different diagonals.

// In src/main.rs, after the first bishop 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 { // White Bishop at d4
    piece_type: PieceType::Bishop,
    color: PieceColor::White,
});
test_board.pieces[5][5] = Some(Piece { // Enemy piece at f6
    piece_type: PieceType::Pawn,
    color: PieceColor::Black,
});
test_board.pieces[1][1] = Some(Piece { // Friendly piece at b2
    piece_type: PieceType::Pawn,
    color: PieceColor::White,
});

let d4_pos = (3, 3);
if let Some(bishop) = &test_board.pieces[d4_pos.0 as usize][d4_pos.1 as usize] {
    let bishop_moves = generate_bishop_moves(bishop, d4_pos, &test_board);
    println!("\nMoves for test bishop at d4: {:?}", bishop_moves);
}

Run this and examine the output. You should see moves along all four diagonals, but with specific limitations:

  • Up-Right: Moves to (4,4) and (5,5) (capture), but not beyond.
  • Down-Left: Moves to (2,2), but not to (1,1) (blocked by friendly pawn) or beyond.
  • Up-Left & Down-Right: Full, unblocked paths to the edge of the board.

This confirms all aspects of your logic are working correctly!


Conclusion

Fantastic! You have now implemented move generation for both rooks and bishops. You've adapted the core ray-casting logic to work for both straight and diagonal lines, which is a crucial skill.

Key Takeaways:

  • Bishop move generation follows the same "ray-casting" principle as the rook, but along diagonals.
  • A while loop is a good way to iterate when multiple coordinates change simultaneously.
  • Using wrapping_sub is a safe way to handle subtraction with unsigned integers like u8 when you might go below zero.
  • Custom test cases are vital for ensuring your logic correctly handles empty squares, captures, and friendly blocks.

In our next lesson, we will implement move generation for the queen. Thanks to the work you've done today and in the last lesson, this will be surprisingly easy. We'll see how we can combine our generate_rook_moves and generate_bishop_moves functions to create the most powerful piece in the game.