diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index f896e57..ccee9bd 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -160,9 +160,9 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert_eq!(BitBoard::empty().population_count(), 0); + /// assert_eq!(BitBoard::EMPTY.population_count(), 0); /// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6); - /// assert_eq!(BitBoard::full().population_count(), 64); + /// assert_eq!(BitBoard::FULL.population_count(), 64); /// ``` #[must_use] pub const fn population_count(&self) -> u32 { @@ -211,8 +211,8 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::empty().is_single_square(), "Empty bitboards represent no squares"); - /// assert!(!BitBoard::full().is_single_square(), "Full bitboards represent all the squares"); + /// assert!(!BitBoard::EMPTY.is_single_square(), "Empty bitboards represent no squares"); + /// assert!(!BitBoard::FULL.is_single_square(), "Full bitboards represent all the squares"); /// assert!(!BitBoard::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares"); /// assert!(BitBoard::new(0b10000000000000).is_single_square()); /// ``` @@ -233,6 +233,38 @@ impl BitBoard { } } + /// Iterate through the occupied squares in a direction specified by a + /// compass direction. This method is mose useful for bitboards of slider + /// rays so that iteration proceeds in order along the ray's direction. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_bitboard::BitBoard; + /// use chessfriend_core::{Direction, Square}; + /// + /// let ray = BitBoard::ray(Square::E4, Direction::North); + /// assert_eq!( + /// ray.occupied_squares_direction(Direction::North).collect::>(), + /// vec![Square::E5, Square::E6, Square::E7, Square::E8] + /// ); + /// ``` + /// + #[must_use] + pub fn occupied_squares_direction( + &self, + direction: Direction, + ) -> Box> { + match direction { + Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => { + Box::new(self.occupied_squares_trailing()) + } + Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => { + Box::new(self.occupied_squares_leading()) + } + } + } + #[must_use] pub fn occupied_squares_leading(&self) -> LeadingBitScanner { LeadingBitScanner::new(self.0) @@ -255,6 +287,12 @@ impl BitBoard { } } + /// Get the first occupied square in the given direction. + /// + /// ## To-Do + /// + /// - Take `direction` by value instead of reference + /// #[must_use] pub fn first_occupied_square(&self, direction: &IterationDirection) -> Option { match direction { diff --git a/board/src/castle.rs b/board/src/castle.rs index 5acdaaf..4ba9a4b 100644 --- a/board/src/castle.rs +++ b/board/src/castle.rs @@ -46,7 +46,7 @@ impl Board { let color = self.unwrap_color(color); - if !self.has_castling_right_unwrapped(color, wing.into()) { + if !self.has_castling_right_unwrapped(color, wing) { return Err(CastleEvaluationError::NoRights { color, wing }); } diff --git a/board/src/check.rs b/board/src/check.rs index 6d8ceba..ca7eca2 100644 --- a/board/src/check.rs +++ b/board/src/check.rs @@ -2,27 +2,241 @@ use crate::Board; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Piece}; +use chessfriend_core::{Piece, Shape, Slider, Square}; +use std::{convert::Into, ops::BitOr}; impl Board { /// Return whether the active color is in check. #[must_use] pub fn is_in_check(&self) -> bool { let color = self.active_color(); - let king = self.king_bitboard(color); + let king = self.kings(self.active_color()); let opposing_sight = self.opposing_sight(color); (king & opposing_sight).is_populated() } - fn king_bitboard(&self, color: Color) -> BitBoard { - self.find_pieces(Piece::king(color)) + /// Calculate checks on the board. + /// + /// ## Panics + /// + /// If the board has multiple kings for any color, this method will panic. + /// + #[must_use] + pub fn calculate_checks( + &self, + piece_moved: &Piece, + last_ply_origin: Square, + last_ply_target: Square, + ) -> CheckingPieces { + let active_king = self.kings(self.active_color()); + + let active_king_square: Square = active_king + .try_into() + .expect("active color has more than one king on the board"); + + let mut checks: Vec = vec![]; + + // Calculate a direct check, where the piece that just moved attacks the + // king. + + if let Ok(sight) = self.is_king_attacked_from_square(last_ply_target) { + checks.push(CheckInfo { + square: last_ply_target, + piece: *piece_moved, + sight, + }); + } + + // Look for revealed checks, where moving a piece opens up an attacking + // ray from a slider to the king. + + let inactive_color = self.active_color().other(); + + for slider in Slider::ALL { + let shape: Shape = slider.into(); + let piece = Piece::new(inactive_color, shape); + + for square in self.find_pieces(piece).occupied_squares_trailing() { + if let Some(check) = + self.calculate_revealed_check(slider, square, active_king, last_ply_origin) + { + checks.push(check); + } + } + } + + let king_moves = BitBoard::king_moves(active_king_square); + // TODO: Bitwise OR with the sight mask of the opposing color. + + CheckingPieces::new(&checks, active_king_square, king_moves) + } + + fn is_king_attacked_from_square(&self, square: Square) -> Result { + let active_king = self.kings(self.active_color()); + let sight_from_square = self.sight_piece(square); + if (active_king & sight_from_square).is_populated() { + Ok(sight_from_square) + } else { + Err(()) + } + } + + fn calculate_revealed_check( + &self, + slider: Slider, + square: Square, + active_king: BitBoard, + last_ply_origin: Square, + ) -> Option { + println!( + "Examining {} on {square} for checks", + Into::::into(slider).name() + ); + + let origin: BitBoard = last_ply_origin.into(); + + let sight = self.sight_piece(square); + let piece_is_attacking_origin = (sight & origin).is_populated(); + if !piece_is_attacking_origin { + println!("does not attack move origin"); + return None; + } + + let origin_and_king = active_king | origin; + println!("origin and king\n{origin_and_king}"); + + for direction in slider.ray_directions() { + let ray = BitBoard::ray(square, direction); + println!("ray\n{ray}"); + + let ray_attacks = ray & origin_and_king; + println!("ray attacks\n{ray_attacks}"); + + // When a check by a slider is revealed by moving another piece, the + // slider will first attack the square the piece was moved from + // (a.k.a. the origin), and also attack the king square. The + // attacked king square must immediately follow the attacked origin + // square, and the attacked origin square must be the first attacked + // square in the ray. + + let mut occupied_squares = ray_attacks.occupied_squares_direction(direction); + if let (Some(first_square), Some(second_square)) = + (occupied_squares.next(), occupied_squares.next()) + { + if first_square != last_ply_origin { + continue; + } + + if Into::::into(second_square) != active_king { + continue; + } + + println!("found check ray\n{ray}"); + + return Some(CheckInfo { + square, + piece: Piece::new(self.active_color(), slider.into()), + sight: ray, + }); + } + } + + None + } +} + +struct CheckInfo { + /// The square the checking piece is on in the current position. + square: Square, + + /// The piece checking the king. + piece: Piece, + + /// The complete sight or direct ray that attacks the king. + sight: BitBoard, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CheckingPieces { + /// Number of checking pieces + count: usize, + + /// A [`BitBoard`] representing the set of pieces to which the color in + /// check can move a piece to block the check. This bitboard includes + /// squares along sight rays of opposing slider pieces. + push_mask: BitBoard, + + /// A [`BitBoard`] representing the set of pieces that can be captured to + /// resolve the check. + capture_mask: BitBoard, + + /// A [`BitBoard`] representing the moves the king can make to get out of + /// check. + king_moves: BitBoard, +} + +impl CheckingPieces { + fn new(checks: &[CheckInfo], king: Square, king_moves: BitBoard) -> Self { + if checks.is_empty() { + return Self { + count: 0, + push_mask: BitBoard::FULL, + capture_mask: BitBoard::EMPTY, + king_moves, + }; + } + + let count = checks.len(); + let push_mask = CheckingPieces::calculate_push_mask(king, checks); + let capture_mask = CheckingPieces::calculate_capture_mask(checks); + + Self { + count, + push_mask, + capture_mask, + king_moves, + } + } + + pub fn is_empty(&self) -> bool { + self.count == 0 + } + + /// The number of checking pieces. + pub fn len(&self) -> usize { + self.count + } + + /// A [`BitBoard`] representing the set of pieces that must be captured to + /// resolve check. + fn calculate_capture_mask(checks: &[CheckInfo]) -> BitBoard { + if checks.is_empty() { + BitBoard::FULL + } else { + checks + .iter() + .map(|c| Into::::into(c.square)) + .fold(BitBoard::EMPTY, BitOr::bitor) + } + } + + /// A [`BitBoard`] representing the set of squares to which a player can + /// move a piece to block a checking piece. + fn calculate_push_mask(king: Square, checks: &[CheckInfo]) -> BitBoard { + let king_moves = BitBoard::king_moves(king); + let push_mask = checks + .iter() + .map(|c| c.sight) + .fold(BitBoard::EMPTY, BitOr::bitor); + + king_moves & !push_mask } } #[cfg(test)] mod tests { use crate::test_board; - use chessfriend_core::Color; + use chessfriend_core::{Square, piece}; #[test] fn active_color_is_in_check() { @@ -43,4 +257,43 @@ mod tests { assert!(!board.is_in_check()); } + + #[test] + fn king_is_attacked_from_square() { + let board = test_board!( + White King on D3, + Black Rook on H3, + Black Queen on F4 + ); + + assert!(board.is_king_attacked_from_square(Square::H3).is_ok()); + assert!(board.is_king_attacked_from_square(Square::F4).is_err()); + assert!(board.is_king_attacked_from_square(Square::A3).is_err()); + } + + #[test] + fn calculate_two_checks() { + let board = test_board!( + White King on D3, + Black Rook on H3, + Black Queen on F5 + ); + + let checks = board.calculate_checks(&piece!(Black Queen), Square::F3, Square::F5); + + assert!(!checks.is_empty()); + assert_eq!(checks.len(), 2); + } + + #[test] + fn calculate_en_passant_revealed_check() { + let board = test_board!(White, [ + White King on D4, + Black Pawn on E3, + Black Rook on H4 + ], E4); + + let checks = board.calculate_checks(&piece!(Black Pawn), Square::F4, Square::E3); + assert_eq!(checks.len(), 1); + } } diff --git a/core/src/score.rs b/core/src/score.rs index 2dfd7c9..3528861 100644 --- a/core/src/score.rs +++ b/core/src/score.rs @@ -23,7 +23,7 @@ impl Score { /// /// ``` /// use chessfriend_core::score::Score; - /// assert_eq!(!Score::MIN, Score::MAX); + /// assert_eq!(-Score::MIN, Score::MAX); /// ``` /// pub const MIN: Score = Score(Value::MIN + 1); @@ -31,7 +31,7 @@ impl Score { /// The maximum possible value of a score. pub const MAX: Score = Score(Value::MAX); - const CENTIPAWNS_PER_POINT: f32 = 100.0; + pub(crate) const CENTIPAWNS_PER_POINT: f32 = 100.0; #[must_use] pub const fn new(value: Value) -> Self { diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 0cedc7c..ba3a57b 100644 --- a/core/src/shapes.rs +++ b/core/src/shapes.rs @@ -1,10 +1,9 @@ // Eryn Wells +use crate::{Direction, score::Score}; use std::{array, fmt, slice, str::FromStr}; use thiserror::Error; -use crate::score::Score; - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Shape { Pawn = 0, @@ -70,18 +69,21 @@ impl Shape { } #[must_use] - pub fn is_promotable(&self) -> bool { + pub const fn is_promotable(&self) -> bool { matches!(self, Self::Knight | Self::Bishop | Self::Rook | Self::Queen) } #[must_use] - pub fn score(self) -> Score { + pub const fn score(self) -> Score { + #[allow(clippy::cast_possible_truncation)] + const CP_PER_PT: i32 = Score::CENTIPAWNS_PER_POINT as i32; + match self { - Shape::Pawn => Score::new(100), - Shape::Knight | Shape::Bishop => Score::new(300), - Shape::Rook => Score::new(500), - Shape::Queen => Score::new(900), - Shape::King => Score::new(20000), + Shape::Pawn => Score::new(CP_PER_PT), + Shape::Knight | Shape::Bishop => Score::new(3 * CP_PER_PT), + Shape::Rook => Score::new(5 * CP_PER_PT), + Shape::Queen => Score::new(9 * CP_PER_PT), + Shape::King => Score::new(200 * CP_PER_PT), } } } @@ -93,6 +95,54 @@ pub enum Slider { Queen, } +impl Slider { + pub const NUM: usize = 3; + pub const ALL: [Self; Self::NUM] = [Slider::Bishop, Slider::Rook, Slider::Queen]; + + /// Return the set of directions a slider can move. + /// + /// ## Examples + /// + /// ``` + /// use chessfriend_core::{Direction, Slider}; + /// + /// assert_eq!( + /// Slider::Bishop.ray_directions().collect::>(), + /// vec![ + /// Direction::NorthWest, + /// Direction::NorthEast, + /// Direction::SouthEast, + /// Direction::SouthWest + /// ] + /// ); + /// ``` + /// + #[must_use] + pub fn ray_directions(self) -> Box> { + match self { + Slider::Bishop => Box::new( + [ + Direction::NorthWest, + Direction::NorthEast, + Direction::SouthEast, + Direction::SouthWest, + ] + .into_iter(), + ), + Slider::Rook => Box::new( + [ + Direction::North, + Direction::East, + Direction::South, + Direction::West, + ] + .into_iter(), + ), + Slider::Queen => Box::new(Direction::ALL.into_iter()), + } + } +} + impl From for Shape { fn from(value: Slider) -> Self { match value {