[moves] Bug: En passant moves were generated when no pawn was available to capture

Found another bug in the pawn move generator related to en passant moves. The
generator was emitting e.p. captures even when no pawn was available to capture
on that target square.

The solution was to include the e.p. square in the enemies list, effectively
treating the e.p. target as if it were occupied by an enemy piece, and then remove
it from the capture bitboards before move generation.

Include a couple tests to exercise this functionality.
This commit is contained in:
Eryn Wells 2025-06-07 10:09:33 -07:00
parent 6b5a54f6b4
commit 6cca3a0f52

View file

@ -46,10 +46,26 @@ impl PawnMoveGenerator {
let empty = !occupied;
let enemies = board.enemies(color);
let (single_pushes, double_pushes) = Self::pushes(pawns, color, empty);
let (left_captures, right_captures) = Self::captures(pawns, color, enemies);
// En passant captures present a particular challenge. Include the
// target e.p. square when computing captures (i.e. treat it like an
// enemy piece is on that square) but do not include it when generating
// capture moves. If it is included, a regular capture move will be
// generated where a special e.p. move should be created instead.
//
// So, include it in the enemies set when computing captures, then
// remove it from the left and right captures bitboards before passing
// them into the move generator. Additionally, include the target e.p.
// square in the e.p. bitboard iff the capture bitboards include it.
let en_passant: BitBoard = board.en_passant_target().into();
let (single_pushes, double_pushes) = Self::pushes(pawns, color, empty);
let (left_captures, right_captures) = Self::captures(pawns, color, enemies | en_passant);
let en_passant = en_passant & (left_captures | right_captures);
let left_captures = left_captures & !en_passant;
let right_captures = right_captures & !en_passant;
Self {
color,
single_pushes,
@ -241,7 +257,7 @@ impl MoveType {
#[cfg(test)]
mod tests {
use super::*;
use crate::{assert_move_list, ply, Move};
use crate::{assert_move_list, assert_move_list_does_not_contain, ply, Move};
use chessfriend_board::test_board;
use chessfriend_core::{Color, Square};
use std::collections::HashSet;
@ -509,4 +525,31 @@ mod tests {
assert_move_list!(generated_moves, [ply!(E5 - E6), ply!(E5 x F6 e.p.),]);
}
#[test]
fn white_no_en_passant_if_no_pawn() {
let board = test_board!(White, [
White Pawn on A3,
Black Pawn on F5,
], F6);
let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect();
assert_move_list_does_not_contain!(
generated_moves,
[ply!(E5 x F6 e.p.), ply!(G5 x F6 e.p.)]
);
}
#[test]
fn black_no_en_passant_if_no_pawn() {
let board = test_board!(Black, [
White Pawn on A4,
Black Pawn on D4,
], A3);
let generated_moves: HashSet<_> = PawnMoveGenerator::new(&board, None).collect();
assert_move_list_does_not_contain!(generated_moves, [ply!(B4 x A3 e.p.)]);
}
}