diff --git a/crates/chess/src/move_generation.rs b/crates/chess/src/move_generation.rs index f785b4bb..cc80da28 100644 --- a/crates/chess/src/move_generation.rs +++ b/crates/chess/src/move_generation.rs @@ -6,15 +6,16 @@ use crate::{ attacks, bitboard::Bitboard, + bitboard_helpers, board::Board, move_generation::{self, enumerate::enumerate_moves, move_filter::MoveFilter}, move_list::MoveList, - moves::Move, + moves::{Move, MoveFlag}, pieces::Piece, rank::Rank, rays, side::Side, - square::{self, Square}, + square::Square, }; pub mod castling; @@ -180,103 +181,161 @@ fn get_piece_moves(piece: Piece, board: &Board, move_list: &mut MoveList, move_f } } +/// The promotion flags emitted, in queen-first order, whenever a pawn move +/// lands on the promotion rank. +const PROMOTION_FLAGS: [MoveFlag; 4] = [ + MoveFlag::PromotionQueen, + MoveFlag::PromotionRook, + MoveFlag::PromotionBishop, + MoveFlag::PromotionKnight, +]; + +/// Emit pawn moves for a set of destination squares generated by a single +/// directional shift. +/// +/// Each destination's origin is recovered by the inverse shift +/// `(inv_file, inv_rank)`, which is exact because every destination came from +/// shifting a real pawn (see [`Square::offset_unchecked`]). Destinations on the +/// promotion rank expand into the four [`PROMOTION_FLAGS`]; others use +/// `quiet_flag`. `emit_promotions` / `emit_quiets` gate the two cases so a single +/// target set can serve different [`MoveFilter`]s. +#[inline] +fn emit_pawn_targets( + move_list: &mut MoveList, + targets: Bitboard, + inv_file: i8, + inv_rank: i8, + quiet_flag: MoveFlag, + promotion_rank: Rank, + emit_promotions: bool, + emit_quiets: bool, +) { + for to in targets { + let from = to.offset_unchecked(inv_file, inv_rank); + if to.rank() == promotion_rank { + if emit_promotions { + for flag in PROMOTION_FLAGS { + move_list.push(Move::new(from, to, flag)); + } + } + } else if emit_quiets { + move_list.push(Move::new(from, to, quiet_flag)); + } + } +} + #[cfg_attr(not(debug_assertions), inline(always))] #[cfg_attr(debug_assertions, inline(never))] fn get_pawn_moves(board: &Board, move_list: &mut MoveList, move_filter: MoveFilter) { let us = board.side_to_move(); let them = us.opposite(); - let their_pieces = board.pieces(them); - let occupancy = board.all_pieces(); - let empty = !occupancy; - let pawns_bb = board.piece_bitboard(Piece::Pawn, us); - - // loop through all the pawns for us - for from_square in pawns_bb { - let attack_bb = attacks::pawn(from_square, us); - - let mut bb_moves = Bitboard::default(); - let to_square = from_square.forward_unchecked(us); - - // pawn non-capture moves - if matches!( - move_filter, - MoveFilter::All | MoveFilter::Quiets | MoveFilter::Tacticals - ) { - let bb_push = Bitboard::from(to_square); - let bb_single_push = bb_push & empty; - let can_double_push = match us { - Side::White => square::is_square_on_rank(from_square, Rank::R2), - Side::Black => square::is_square_on_rank(from_square, Rank::R7), - }; - - let double_push_square = if can_double_push { - to_square.forward(us) - } else { - None - }; - - // note that the single push square has to be empty in addition to the double push square being empty - let is_double_push_unobstructed = if let Some(push_square) = double_push_square { - !occupancy.is_square_occupied(to_square) - && !occupancy.is_square_occupied(push_square) - } else { - false - }; - - let bb_double_push = if can_double_push - && is_double_push_unobstructed - && let Some(dbl_push_sq) = double_push_square - { - Bitboard::from(dbl_push_sq) & empty - } else { - Bitboard::default() - }; - - // Tacticals include only promotion-rank pushes; Quiets exclude them. - let promo_rank_bb = Rank::promotion_rank(us).to_bitboard(); - let pushes = bb_single_push | bb_double_push; - let filtered_pushes = match move_filter { - MoveFilter::Tacticals => pushes & promo_rank_bb, - MoveFilter::Quiets => pushes & !promo_rank_bb, - _ => pushes, - }; - bb_moves |= filtered_pushes; - } + let pawns = board.piece_bitboard(Piece::Pawn, us); + if pawns.is_empty() { + return; + } - // pawn captures - if matches!( - move_filter, - MoveFilter::All | MoveFilter::Captures | MoveFilter::Tacticals - ) { - let bb_capture = attack_bb & their_pieces; - // En passant - let bb_en_passant = match board.en_passant_square() { - Some(en_passant_square) => { - // We only want to add the en passant square if it is within range of the pawn. - // This means that the en passant square is within 1 rank of the pawn and the en passant square - // is in the pawn's attack table. - let en_passant_bb = Bitboard::from(en_passant_square); - let result = en_passant_bb & !(attack_bb); - let is_in_range = result == 0; - if is_in_range { - en_passant_bb - } else { - Bitboard::default() - } - } - None => Bitboard::default(), - }; - bb_moves |= bb_capture | bb_en_passant; + let enemies = board.pieces(them); + let empty = !board.all_pieces(); + let promotion_rank = Rank::promotion_rank(us); + + // Non-promotion pushes, promotion pushes, and captures are gated independently + // so each `MoveFilter` selects exactly the same set the per-pawn generator did: + // Quiets = non-promo pushes; Captures = all captures/EP; Tacticals = promo + // pushes + all captures; All = everything. + let want_quiet_pushes = matches!(move_filter, MoveFilter::All | MoveFilter::Quiets); + let want_promo_pushes = matches!(move_filter, MoveFilter::All | MoveFilter::Tacticals); + let want_captures = matches!( + move_filter, + MoveFilter::All | MoveFilter::Captures | MoveFilter::Tacticals + ); + + // Forward shift and the third rank (where a legal single push from the start + // rank lands) depend on the side; captures always shift toward the lower file + // ("left") and higher file ("right"). + let (push, capture_left, capture_right, third_rank): ( + fn(Bitboard) -> Bitboard, + fn(Bitboard) -> Bitboard, + fn(Bitboard) -> Bitboard, + Bitboard, + ) = match us { + Side::White => ( + bitboard_helpers::north, + bitboard_helpers::north_west, + bitboard_helpers::north_east, + Rank::R3.to_bitboard(), + ), + Side::Black => ( + bitboard_helpers::south, + bitboard_helpers::south_west, + bitboard_helpers::south_east, + Rank::R6.to_bitboard(), + ), + }; + // Inverse rank delta of one forward push (used to recover a move's origin). + let back = -us.forward_delta(); + + // Pushes: the single-push set carries both quiet and promotion pushes. + if want_quiet_pushes || want_promo_pushes { + let single = push(pawns) & empty; + emit_pawn_targets( + move_list, + single, + 0, + back, + MoveFlag::Standard, + promotion_rank, + want_promo_pushes, + want_quiet_pushes, + ); + + // A double push exists only where the intermediate single-push square is + // empty (so it must be in `single`) and the pawn started on its home rank. + if want_quiet_pushes { + let double = push(single & third_rank) & empty; + emit_pawn_targets( + move_list, + double, + 0, + 2 * back, + MoveFlag::DoublePush, + promotion_rank, + false, + true, + ); } + } - enumerate::enumerate_moves( - &bb_moves, - from_square, - Piece::Pawn, - board, - move_filter, + // Captures (including capture-promotions) and en passant. + if want_captures { + emit_pawn_targets( + move_list, + capture_left(pawns) & enemies, + 1, + back, + MoveFlag::Standard, + promotion_rank, + true, + true, + ); + emit_pawn_targets( move_list, + capture_right(pawns) & enemies, + -1, + back, + MoveFlag::Standard, + promotion_rank, + true, + true, ); + + if let Some(ep_square) = board.en_passant_square() { + // Our pawns that attack the EP square are exactly those a `them` pawn + // on the EP square would attack. + let ep_attackers = pawns & attacks::pawn(ep_square, them); + for from in ep_attackers { + move_list.push(Move::new(from, ep_square, MoveFlag::EnPassant)); + } + } } }