diff --git a/moves/src/defs.rs b/moves/src/defs.rs index caea420..03f2a19 100644 --- a/moves/src/defs.rs +++ b/moves/src/defs.rs @@ -24,6 +24,17 @@ pub enum PromotionShape { Queen = 0b11, } +impl PromotionShape { + pub const NUM: usize = 4; + + pub const ALL: [PromotionShape; PromotionShape::NUM] = [ + PromotionShape::Knight, + PromotionShape::Bishop, + PromotionShape::Rook, + PromotionShape::Queen, + ]; +} + impl From for Shape { fn from(value: PromotionShape) -> Self { match value { diff --git a/moves/src/generators.rs b/moves/src/generators.rs index dda0a5a..fec84a2 100644 --- a/moves/src/generators.rs +++ b/moves/src/generators.rs @@ -6,7 +6,13 @@ pub use pawn::PawnMoveGenerator; use crate::Move; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct GeneratedMove { pub(crate) ply: Move, } + +impl From for GeneratedMove { + fn from(value: Move) -> Self { + GeneratedMove { ply: value } + } +} diff --git a/moves/src/generators/pawn.rs b/moves/src/generators/pawn.rs index d0a39af..29cca6a 100644 --- a/moves/src/generators/pawn.rs +++ b/moves/src/generators/pawn.rs @@ -2,12 +2,12 @@ //! Implements a move generator for pawns. -use crate::Move; - use super::GeneratedMove; +use crate::{Move, PromotionShape}; use chessfriend_bitboard::{bit_scanner::TrailingBitScanner, BitBoard}; use chessfriend_board::Board; -use chessfriend_core::{Color, Direction, Rank, Square}; +use chessfriend_core::{Color, Direction, Rank, Shape, Square}; +use std::slice; pub struct PawnMoveGenerator { color: Color, @@ -16,7 +16,8 @@ pub struct PawnMoveGenerator { left_captures: BitBoard, right_captures: BitBoard, en_passant: BitBoard, - iterator: TrailingBitScanner, + target_iterator: TrailingBitScanner, + promotion_iterator: Option, move_type: MoveType, } @@ -29,6 +30,12 @@ enum MoveType { EnPassant, } +struct PromotionIterator { + origin: Square, + target: Square, + iterator: slice::Iter<'static, PromotionShape>, +} + impl PawnMoveGenerator { #[must_use] pub fn new(board: &Board, color: Option) -> Self { @@ -50,7 +57,8 @@ impl PawnMoveGenerator { left_captures, right_captures, en_passant, - iterator: single_pushes.occupied_squares_trailing(), + target_iterator: single_pushes.occupied_squares_trailing(), + promotion_iterator: None, move_type: MoveType::SinglePushes, } } @@ -102,16 +110,28 @@ impl PawnMoveGenerator { Color::Black => target.neighbor(Direction::North, Some(2)), }, MoveType::LeftCaptures => match self.color { - Color::White => target.neighbor(Direction::NorthWest, None), - Color::Black => target.neighbor(Direction::SouthEast, None), + Color::White => target.neighbor(Direction::SouthEast, None), + Color::Black => target.neighbor(Direction::NorthWest, None), }, MoveType::RightCaptures => match self.color { - Color::White => target.neighbor(Direction::NorthEast, None), - Color::Black => target.neighbor(Direction::SouthWest, None), + Color::White => target.neighbor(Direction::SouthWest, None), + Color::Black => target.neighbor(Direction::NorthEast, None), }, MoveType::EnPassant => match self.color { - Color::White => unimplemented!(), - Color::Black => unimplemented!(), + Color::White => { + if (self.en_passant & self.left_captures).is_populated() { + target.neighbor(Direction::NorthWest, None) + } else { + target.neighbor(Direction::NorthEast, None) + } + } + Color::Black => { + if (self.en_passant & self.left_captures).is_populated() { + target.neighbor(Direction::SouthEast, None) + } else { + target.neighbor(Direction::SouthWest, None) + } + } }, } } @@ -128,23 +148,60 @@ impl PawnMoveGenerator { MoveType::EnPassant => self.en_passant, }; - self.iterator = next_bitboard.occupied_squares_trailing(); + self.target_iterator = next_bitboard.occupied_squares_trailing(); self.move_type = next_move_type; } next_move_type } + + fn next_promotion_move(&mut self) -> Option { + if let Some(promotion_iterator) = self.promotion_iterator.as_mut() { + if let Some(shape) = promotion_iterator.iterator.next() { + let origin = promotion_iterator.origin; + let target = promotion_iterator.target; + + return if matches!( + self.move_type, + MoveType::LeftCaptures | MoveType::RightCaptures + ) { + Some(Move::capture_promotion(origin, target, *shape).into()) + } else { + Some(Move::promotion(origin, target, *shape).into()) + }; + } + } + + None + } } impl std::iter::Iterator for PawnMoveGenerator { type Item = GeneratedMove; fn next(&mut self) -> Option { - if let Some(target) = self.iterator.next() { + let next_promotion_move = self.next_promotion_move(); + if next_promotion_move.is_some() { + return next_promotion_move; + } + + self.promotion_iterator = None; + + if let Some(target) = self.target_iterator.next() { let origin = self .calculate_origin_square(target) .expect("Bogus origin square"); + if target.rank().is_promotable_rank() { + self.promotion_iterator = Some(PromotionIterator { + origin, + target, + iterator: PromotionShape::ALL.iter(), + }); + + return self.next(); + } + match self.move_type { MoveType::SinglePushes => Some(GeneratedMove { ply: Move::quiet(origin, target), @@ -178,3 +235,214 @@ impl MoveType { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::Move; + use chessfriend_board::test_board; + use chessfriend_core::{Color, Square}; + use std::collections::HashSet; + + #[test] + fn black_b7_pushes_and_double_pushes() { + let board = test_board!(Black Pawn on B7); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::quiet(Square::B7, Square::B6) + }, + GeneratedMove { + ply: Move::double_push(Square::B7, Square::B5) + } + ] + .into() + ); + } + + #[test] + fn black_e2_pushes_and_double_pushes() { + let board = test_board!(White Pawn on E2); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::quiet(Square::E2, Square::E3) + }, + GeneratedMove { + ply: Move::double_push(Square::E2, Square::E4) + } + ] + .into() + ); + } + + #[test] + fn black_b7_pushes_with_b5_blocker() { + let board = test_board!(Black Pawn on B7, White Bishop on B5); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [GeneratedMove { + ply: Move::quiet(Square::B7, Square::B6) + }] + .into() + ); + } + + #[test] + fn black_b7_pushes_with_b6_blocker() { + let board = test_board!(Black Pawn on B7, Black Bishop on B6); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert!(generated_moves.is_empty()); + } + + #[test] + fn white_e2_moves_with_e4_blocker() { + let board = test_board!(White Pawn on E2, White Bishop on E4); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [GeneratedMove { + ply: Move::quiet(Square::E2, Square::E3) + }] + .into() + ); + } + + #[test] + fn white_e2_moves_with_e3_blocker() { + let board = test_board!(White Pawn on E2, White Bishop on E3); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert!(generated_moves.is_empty()); + } + + #[test] + fn white_f6_left_captures() { + let white_captures_board = test_board!(White Pawn on F6, Black Queen on E7); + let generated_moves: HashSet = + PawnMoveGenerator::new(&white_captures_board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::capture(Square::F6, Square::E7) + }, + GeneratedMove { + ply: Move::quiet(Square::F6, Square::F7) + } + ] + .into() + ); + } + + #[test] + fn black_d5_left_captures() { + let black_captures_board = test_board!(Black Pawn on D5, White Queen on E4); + let generated_moves: HashSet = + PawnMoveGenerator::new(&black_captures_board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [ + GeneratedMove { + ply: Move::capture(Square::D5, Square::E4) + }, + GeneratedMove { + ply: Move::quiet(Square::D5, Square::D4) + } + ] + .into() + ); + } + + #[test] + fn white_g7_push_promotions() { + let board = test_board!(White Pawn on G7); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [ + Move::promotion(Square::G7, Square::G8, PromotionShape::Knight).into(), + Move::promotion(Square::G7, Square::G8, PromotionShape::Bishop).into(), + Move::promotion(Square::G7, Square::G8, PromotionShape::Rook).into(), + Move::promotion(Square::G7, Square::G8, PromotionShape::Queen).into(), + ] + .into() + ); + } + + #[test] + fn white_d7_push_and_capture_promotions() { + let board = test_board!( + White Pawn on D7, + Black Queen on E8 + ); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::White)).collect(); + assert_eq!( + generated_moves, + [ + Move::promotion(Square::D7, Square::D8, PromotionShape::Knight).into(), + Move::promotion(Square::D7, Square::D8, PromotionShape::Bishop).into(), + Move::promotion(Square::D7, Square::D8, PromotionShape::Rook).into(), + Move::promotion(Square::D7, Square::D8, PromotionShape::Queen).into(), + Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Knight).into(), + Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Bishop).into(), + Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Rook).into(), + Move::capture_promotion(Square::D7, Square::E8, PromotionShape::Queen).into(), + ] + .into() + ); + } + + #[test] + fn black_a2_push_promotions() { + let board = test_board!(Black Pawn on A2); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [ + Move::promotion(Square::A2, Square::A1, PromotionShape::Knight).into(), + Move::promotion(Square::A2, Square::A1, PromotionShape::Bishop).into(), + Move::promotion(Square::A2, Square::A1, PromotionShape::Rook).into(), + Move::promotion(Square::A2, Square::A1, PromotionShape::Queen).into(), + ] + .into() + ); + } + + #[test] + fn black_h2_push_and_capture_promotions() { + let board = test_board!( + Black Pawn on H2, + White Queen on G1, + ); + let generated_moves: HashSet = + PawnMoveGenerator::new(&board, Some(Color::Black)).collect(); + assert_eq!( + generated_moves, + [ + Move::promotion(Square::H2, Square::H1, PromotionShape::Knight).into(), + Move::promotion(Square::H2, Square::H1, PromotionShape::Bishop).into(), + Move::promotion(Square::H2, Square::H1, PromotionShape::Rook).into(), + Move::promotion(Square::H2, Square::H1, PromotionShape::Queen).into(), + Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Knight).into(), + Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Bishop).into(), + Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Rook).into(), + Move::capture_promotion(Square::H2, Square::G1, PromotionShape::Queen).into(), + ] + .into() + ); + } +} diff --git a/moves/src/moves.rs b/moves/src/moves.rs index f489ad6..4c0ac74 100644 --- a/moves/src/moves.rs +++ b/moves/src/moves.rs @@ -39,6 +39,12 @@ impl Move { Move(origin_bits(origin) | target_bits(target) | flag_bits) } + pub fn capture_promotion(origin: Square, target: Square, shape: PromotionShape) -> Self { + let flag_bits = Kind::CapturePromotion as u16; + let shape_bits = shape as u16; + Move(origin_bits(origin) | target_bits(target) | flag_bits | shape_bits) + } + #[must_use] pub fn en_passant_capture(origin: Square, target: Square) -> Self { let flag_bits = Kind::EnPassantCapture as u16;