From 2a6b098cb8fd3fd3e21a75f409c8590983586469 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 25 Feb 2024 10:51:27 -0800 Subject: [PATCH] Fix the pawn unit tests --- core/src/coordinates.rs | 11 +++ moves/src/en_passant.rs | 4 +- position/src/move_generator/move_set.rs | 106 ++++++++++++++++++------ position/src/move_generator/pawn.rs | 36 ++++---- 4 files changed, 112 insertions(+), 45 deletions(-) diff --git a/core/src/coordinates.rs b/core/src/coordinates.rs index 68ff3fe..54aeb1a 100644 --- a/core/src/coordinates.rs +++ b/core/src/coordinates.rs @@ -1,5 +1,6 @@ // Eryn Wells +use crate::Color; use std::fmt; use std::str::FromStr; @@ -168,6 +169,16 @@ impl Rank { /// assert_eq!(Rank::PAWN_STARTING_RANKS[Color::Black as usize], Rank::SEVEN); /// ``` pub const PAWN_STARTING_RANKS: [Rank; 2] = [Rank::TWO, Rank::SEVEN]; + + pub const PAWN_DOUBLE_PUSH_TARGET_RANKS: [Rank; 2] = [Rank::FOUR, Rank::FIVE]; + + pub fn is_pawn_starting_rank(&self, color: Color) -> bool { + self == &Self::PAWN_STARTING_RANKS[color as usize] + } + + pub fn is_pawn_double_push_target_rank(&self, color: Color) -> bool { + self == &Self::PAWN_DOUBLE_PUSH_TARGET_RANKS[color as usize] + } } #[rustfmt::skip] diff --git a/moves/src/en_passant.rs b/moves/src/en_passant.rs index ac57835..49e88a6 100644 --- a/moves/src/en_passant.rs +++ b/moves/src/en_passant.rs @@ -3,7 +3,7 @@ use chessfriend_core::{Rank, Square}; /// En passant information. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct EnPassant { target: Square, capture: Square, @@ -39,10 +39,12 @@ impl EnPassant { } } + /// The square the capturing piece will move to. pub fn target_square(&self) -> Square { self.target } + /// The square on which the captured pawn sits. pub fn capture_square(&self) -> Square { self.capture } diff --git a/position/src/move_generator/move_set.rs b/position/src/move_generator/move_set.rs index f1042c9..5ba9467 100644 --- a/position/src/move_generator/move_set.rs +++ b/position/src/move_generator/move_set.rs @@ -2,7 +2,7 @@ use chessfriend_bitboard::BitBoard; use chessfriend_core::{PlacedPiece, Square}; -use chessfriend_moves::{Builder as MoveBuilder, Castle, Move}; +use chessfriend_moves::{Builder as MoveBuilder, Castle, EnPassant, Move}; /// A set of bitboards defining the moves for a single piece on the board. #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -11,12 +11,18 @@ struct BitBoardSet { captures: BitBoard, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum Special { + Pawn { en_passant: EnPassant }, + King { castles: u8 }, +} + /// A set of moves for a single piece on the board. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct MoveSet { piece: PlacedPiece, bitboards: BitBoardSet, - special: u8, + special: Option, } impl MoveSet { @@ -24,7 +30,7 @@ impl MoveSet { MoveSet { piece, bitboards: BitBoardSet::default(), - special: 0, + special: None, } } @@ -33,9 +39,9 @@ impl MoveSet { } pub(crate) fn can_castle(&self, castle: Castle) -> bool { - match castle { - Castle::KingSide => (self.special & 0b1) != 0, - Castle::QueenSide => (self.special & 0b10) != 0, + match self.special { + Some(Special::King { castles }) => (castles & 1 << castle as u8) != 0, + _ => false, } } @@ -50,49 +56,99 @@ impl MoveSet { } pub(super) fn kingside_castle(&mut self) -> &mut MoveSet { - self.special |= 0b1; + match self.special { + Some(Special::King { ref mut castles }) => *castles |= 1 << Castle::KingSide as u8, + _ => { + self.special = Some(Special::King { + castles: 1 << Castle::KingSide as u8, + }) + } + } + self } pub(super) fn queenside_castle(&mut self) -> &mut MoveSet { - self.special |= 0b10; + match self.special { + Some(Special::King { ref mut castles }) => *castles |= 1 << Castle::QueenSide as u8, + _ => { + self.special = Some(Special::King { + castles: 1 << Castle::QueenSide as u8, + }) + } + } + self } - /// Return a BitBoard representing all possible moves. + pub(super) fn en_passant(&mut self, en_passant: EnPassant) -> &mut MoveSet { + self.special = Some(Special::Pawn { en_passant }); + self + } + + /// A `BitBoard` representing all possible moves. pub(super) fn bitboard(&self) -> BitBoard { self.bitboards.captures | self.bitboards.quiet } pub(crate) fn moves(&self) -> impl Iterator + '_ { let piece = &self.piece; + let color = piece.color(); + + let is_pawn_on_starting_rank = + piece.is_pawn() && piece.square().rank().is_pawn_starting_rank(color); self.bitboards .quiet .occupied_squares() - .filter_map(|to_square| MoveBuilder::push(&self.piece).to(to_square).build().ok()) + .filter_map(move |to_square| { + if is_pawn_on_starting_rank + && to_square.rank().is_pawn_double_push_target_rank(color) + { + MoveBuilder::double_push(piece.square().file(), color) + .build() + .ok() + } else { + MoveBuilder::push(piece).to(to_square).build().ok() + } + }) .chain( self.bitboards .captures .occupied_squares() - .filter_map(|to_square| MoveBuilder::push(piece).to(to_square).build().ok()), + .filter_map(|to_square| { + MoveBuilder::push(piece) + .capturing_on(to_square) + .build() + .ok() + }), ) - .chain( - if (self.special & 0b1) != 0 { - let mv = MoveBuilder::castling(Castle::KingSide).build(); - Some(mv) + .chain(self.castle_move(Castle::KingSide)) + .chain(self.castle_move(Castle::QueenSide)) + .chain(self.en_passant_move()) + } + + fn castle_move(&self, castle: Castle) -> Option { + match self.special { + Some(Special::King { castles }) => { + if (castles & 1 << castle as u8) != 0 { + Some(MoveBuilder::castling(castle).build()) } else { None } - .into_iter(), - ) - .chain( - if (self.special & 0b10) != 0 { - Some(MoveBuilder::castling(Castle::QueenSide).build()) - } else { - None - } - .into_iter(), - ) + } + _ => None, + } + } + + fn en_passant_move(&self) -> Option { + match self.special { + Some(Special::Pawn { en_passant }) => Some(unsafe { + MoveBuilder::push(&self.piece) + .capturing_en_passant_on(en_passant.target_square()) + .build_unchecked() + }), + _ => None, + } } } diff --git a/position/src/move_generator/pawn.rs b/position/src/move_generator/pawn.rs index 67fefb3..e5db4cf 100644 --- a/position/src/move_generator/pawn.rs +++ b/position/src/move_generator/pawn.rs @@ -4,7 +4,7 @@ use super::{move_generator_declaration, MoveGeneratorInternal, MoveSet}; use crate::Position; use chessfriend_bitboard::BitBoard; use chessfriend_core::{Color, PlacedPiece, Rank, Shape, Square}; -use chessfriend_moves::{Builder as MoveBuilder, Move}; +use chessfriend_moves::{EnPassant, Move}; use std::collections::BTreeMap; #[derive(Debug)] @@ -33,27 +33,33 @@ impl MoveGeneratorInternal for PawnMoveGenerator { let capture_moves = Self::attacks(position, &placed_piece) & capture_mask; let quiet_moves = Self::pushes(position, &placed_piece) & push_mask; - MoveSet::new(*placed_piece) + let mut move_set = MoveSet::new(*placed_piece) .quiet_moves(quiet_moves) - .capture_moves(capture_moves) + .capture_moves(capture_moves); + + if let Some(en_passant) = Self::en_passant(position, placed_piece) { + move_set.en_passant(en_passant); + } + + move_set } } impl PawnMoveGenerator { pub(super) fn new( position: &Position, - color: Color, + player_to_move: Color, capture_mask: BitBoard, push_mask: BitBoard, ) -> Self { let move_sets = if !capture_mask.is_empty() && !push_mask.is_empty() { - Self::move_sets(position, color, capture_mask, push_mask) + Self::move_sets(position, player_to_move, capture_mask, push_mask) } else { std::collections::BTreeMap::new() }; Self { - color, + color: player_to_move, move_sets, en_passant_captures: Vec::new(), } @@ -115,7 +121,7 @@ impl PawnMoveGenerator { BitBoard::pawn_attacks(piece.square(), color) & opponent_pieces } - fn en_passant_attack(position: &Position, piece: &PlacedPiece) -> Option { + fn en_passant(position: &Position, piece: &PlacedPiece) -> Option { match position.en_passant() { Some(en_passant) => { let target_square = en_passant.target_square(); @@ -128,12 +134,7 @@ impl PawnMoveGenerator { } match position.piece_on_square(en_passant.capture_square()) { - Some(_) => Some( - MoveBuilder::push(piece) - .capturing_en_passant_on(target_square) - .build() - .ok()?, - ), + Some(_) => Some(en_passant), None => None, } } @@ -164,7 +165,7 @@ mod tests { let generated_moves: HashSet<_> = generator.iter().collect(); - assert_eq!(generated_moves, expected_moves); + assert_move_list!(generated_moves, expected_moves, pos); Ok(()) } @@ -278,15 +279,12 @@ mod tests { Black Pawn on E4, ], D3); - let generator = PawnMoveGenerator::new(&pos, Color::White, BitBoard::FULL, BitBoard::FULL); + let generator = PawnMoveGenerator::new(&pos, Color::Black, BitBoard::FULL, BitBoard::FULL); let generated_moves: HashSet = generator.iter().collect(); let builder = MoveBuilder::push(&piece!(Black Pawn on E4)); let expected_moves = HashSet::from_iter([ - builder - .clone() - .capturing_en_passant_on(Square::D3) - .build()?, + builder.capturing_en_passant_on(Square::D3).build()?, builder.clone().to(Square::E3).build()?, ]);