Compare commits
	
		
			10 commits
		
	
	
		
			main
			...
			calculate-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1c94f157e6 | |||
| 5444d8ea3a | |||
| d3c94356bd | |||
| 6e8c0d3466 | |||
| f658903b54 | |||
| 7d3fd4b20a | |||
| b4536ffbe3 | |||
| 0a2dd7e09d | |||
| 21a2a37e1f | |||
| 89a9588e69 | 
					 5 changed files with 362 additions and 21 deletions
				
			
		|  | @ -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>>(),
 | ||||
|     ///     vec![Square::E5, Square::E6, Square::E7, Square::E8]
 | ||||
|     /// );
 | ||||
|     /// ```
 | ||||
|     ///
 | ||||
|     #[must_use] | ||||
|     pub fn occupied_squares_direction( | ||||
|         &self, | ||||
|         direction: Direction, | ||||
|     ) -> Box<dyn Iterator<Item = Square>> { | ||||
|         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<Square> { | ||||
|         match direction { | ||||
|  |  | |||
|  | @ -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 }); | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<CheckInfo> = 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<BitBoard, ()> { | ||||
|         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<CheckInfo> { | ||||
|         println!( | ||||
|             "Examining {} on {square} for checks", | ||||
|             Into::<Shape>::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::<BitBoard>::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::<BitBoard>::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); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -1,10 +1,9 @@ | |||
| // Eryn Wells <eryn@erynwells.me>
 | ||||
| 
 | ||||
| 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>>(),
 | ||||
|     ///     vec![
 | ||||
|     ///         Direction::NorthWest,
 | ||||
|     ///         Direction::NorthEast,
 | ||||
|     ///         Direction::SouthEast,
 | ||||
|     ///         Direction::SouthWest
 | ||||
|     ///     ]
 | ||||
|     /// );
 | ||||
|     /// ```
 | ||||
|     ///
 | ||||
|     #[must_use] | ||||
|     pub fn ray_directions(self) -> Box<dyn Iterator<Item = Direction>> { | ||||
|         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<Slider> for Shape { | ||||
|     fn from(value: Slider) -> Self { | ||||
|         match value { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue