diff --git a/README.md b/README.md deleted file mode 100644 index 5ca6cb2..0000000 --- a/README.md +++ /dev/null @@ -1,113 +0,0 @@ -ChessFriend -=========== - -A chess engine written in Rust. - -The project is divided into crates for major components of the engine. These -crates are collected in a Cargo workspace. All crates have the `chessfriend_` -naming prefix. The directory structure omits this prefix, and I also frequently -do when referring to them. - - - -## Engine Crates - -The engine is divided into several crates, each providing vital types and -functionality. - - - -### `core` - -A collection of types for representing core concepts in a chess game and the -engine. Types like `Color` (player or piece color), `Shape` (the shape of a -piece: knight, etc), `Piece` (a piece of a particular color and shape), and -`Score` (for scoring a board position) live here. - - - -### `bitboard` - -Implements an efficient BitBoard type. Bitboards use a 64-bit bit field to mark a -square on a board as occupied or free. - - - -### `board` - -Implements a `Board` type that represents a moment-in-time board position. FEN -parsing and production lives here. - - - -### `moves` - -The `Move` type lives here, along with routines for encoding moves in efficient -forms, parsing moves from algebraic notation, and recording moves in a game -context. Additionally, the move generators for each shape of piece are here. - - - -### `position` - -Exports the `Position` type, representing a board position within the context of -a game. As such, it also provides a move list, and methods to make and unmake -moves. - - - -## Support Crates - -These crates are for debugging and testing. - - - -### `explorer` - -This crate implements a small command-line application for "exploring" board -positions. I meant for this program to be a debugging utility so that I could -examine bitboards and other board structures live. It has grown over time to -also support more aspects of interacting with the engine. So you can also use it -to play a game! - - - -### `perft` - -A small Perft utility that executes perft to a given depth from some starting -position. - - - - - -## Building - -Build the engine in the usual Rusty way. - -```sh -$ cargo build -``` - - - - - -## Testing - -Test in the usual Rusty way. - -```sh -$ cargo test -``` - -The engine has a fairly comprehensive unit test suite, as well as a decent pile -of integration tests. - - - - - -## Authors - -This engine is built entirely by me, Eryn Wells. diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 35ce927..ccee9bd 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -46,6 +46,16 @@ impl BitBoard { pub const EMPTY: BitBoard = BitBoard(u64::MIN); pub const FULL: BitBoard = BitBoard(u64::MAX); + #[deprecated(note = "Use BitBoard::EMPTY instead")] + pub const fn empty() -> BitBoard { + Self::EMPTY + } + + #[deprecated(note = "Use BitBoard::FULL instead")] + pub const fn full() -> BitBoard { + Self::FULL + } + pub const fn new(bits: u64) -> BitBoard { BitBoard(bits) } @@ -99,7 +109,7 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(BitBoard::EMPTY.is_empty()); + /// assert!(BitBoard::empty().is_empty()); /// assert!(!BitBoard::full().is_empty()); /// assert!(!BitBoard::new(0b1000).is_empty()); /// ``` @@ -115,7 +125,7 @@ impl BitBoard { /// /// ``` /// use chessfriend_bitboard::BitBoard; - /// assert!(!BitBoard::EMPTY.is_populated()); + /// assert!(!BitBoard::empty().is_populated()); /// assert!(BitBoard::full().is_populated()); /// assert!(BitBoard::new(0b1).is_populated()); /// ``` @@ -554,8 +564,8 @@ mod tests { let b = bitboard![B5 G7 H3]; assert_eq!(a ^ b, bitboard![B5 C5 H3]); - assert_eq!(a ^ BitBoard::EMPTY, a); - assert_eq!(BitBoard::EMPTY ^ BitBoard::EMPTY, BitBoard::EMPTY); + assert_eq!(a ^ BitBoard::empty(), a); + assert_eq!(BitBoard::empty() ^ BitBoard::empty(), BitBoard::empty()); } #[test] diff --git a/bitboard/src/library.rs b/bitboard/src/library.rs index 3ea670c..6a60392 100644 --- a/bitboard/src/library.rs +++ b/bitboard/src/library.rs @@ -110,14 +110,14 @@ pub(super) struct MoveLibrary { impl MoveLibrary { const fn new() -> MoveLibrary { MoveLibrary { - rays: [[BitBoard::EMPTY; Direction::NUM]; Square::NUM], - pawn_attacks: [[BitBoard::EMPTY; Square::NUM]; Color::NUM], - pawn_pushes: [[BitBoard::EMPTY; Square::NUM]; Color::NUM], - knight_moves: [BitBoard::EMPTY; Square::NUM], - bishop_moves: [BitBoard::EMPTY; Square::NUM], - rook_moves: [BitBoard::EMPTY; Square::NUM], - queen_moves: [BitBoard::EMPTY; Square::NUM], - king_moves: [BitBoard::EMPTY; Square::NUM], + rays: [[BitBoard::empty(); Direction::NUM]; Square::NUM], + pawn_attacks: [[BitBoard::empty(); Square::NUM]; Color::NUM], + pawn_pushes: [[BitBoard::empty(); Square::NUM]; Color::NUM], + knight_moves: [BitBoard::empty(); Square::NUM], + bishop_moves: [BitBoard::empty(); Square::NUM], + rook_moves: [BitBoard::empty(); Square::NUM], + queen_moves: [BitBoard::empty(); Square::NUM], + king_moves: [BitBoard::empty(); Square::NUM], } } @@ -238,7 +238,7 @@ impl MoveLibrary { } fn _generate_ray(sq: BitBoard, shift: fn(&BitBoard) -> BitBoard) -> BitBoard { - let mut ray = BitBoard::EMPTY; + let mut ray = BitBoard::empty(); let mut iter = shift(&sq); while !iter.is_empty() { 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/board/src/movement.rs b/board/src/movement.rs index 3ebf44c..2935eee 100644 --- a/board/src/movement.rs +++ b/board/src/movement.rs @@ -41,7 +41,7 @@ impl Movement for Piece { let parameters = Board::castling_parameters(Wing::KingSide, color); parameters.target.king.into() } else { - BitBoard::EMPTY + BitBoard::empty() }; let queenside_target_square = if board @@ -51,7 +51,7 @@ impl Movement for Piece { let parameters = Board::castling_parameters(Wing::QueenSide, color); parameters.target.king.into() } else { - BitBoard::EMPTY + BitBoard::empty() }; self.sight(square, board) | kingside_target_square | queenside_target_square @@ -99,11 +99,11 @@ mod tests { #[test] fn white_pushes_empty_board() { assert_eq!( - pawn_pushes(Square::E4.into(), Color::White, BitBoard::EMPTY), + pawn_pushes(Square::E4.into(), Color::White, BitBoard::empty()), bitboard![E5] ); assert_eq!( - pawn_pushes(Square::E2.into(), Color::White, BitBoard::EMPTY), + pawn_pushes(Square::E2.into(), Color::White, BitBoard::empty()), bitboard![E3 E4] ); } @@ -111,11 +111,11 @@ mod tests { #[test] fn black_pawn_empty_board() { assert_eq!( - pawn_pushes(Square::A4.into(), Color::Black, BitBoard::EMPTY), + pawn_pushes(Square::A4.into(), Color::Black, BitBoard::empty()), bitboard![A3] ); assert_eq!( - pawn_pushes(Square::B7.into(), Color::Black, BitBoard::EMPTY), + pawn_pushes(Square::B7.into(), Color::Black, BitBoard::empty()), bitboard![B6 B5] ); } @@ -124,7 +124,7 @@ mod tests { fn white_pushes_blocker() { assert_eq!( pawn_pushes(Square::C5.into(), Color::White, bitboard![C6]), - BitBoard::EMPTY + BitBoard::empty() ); assert_eq!( pawn_pushes(Square::D2.into(), Color::White, bitboard![D4]), @@ -132,7 +132,7 @@ mod tests { ); assert_eq!( pawn_pushes(Square::D2.into(), Color::White, bitboard![D3]), - BitBoard::EMPTY + BitBoard::empty() ); } @@ -140,7 +140,7 @@ mod tests { fn black_pushes_blocker() { assert_eq!( pawn_pushes(Square::C5.into(), Color::Black, bitboard![C4]), - BitBoard::EMPTY + BitBoard::empty() ); assert_eq!( pawn_pushes(Square::D7.into(), Color::Black, bitboard![D5]), @@ -148,7 +148,7 @@ mod tests { ); assert_eq!( pawn_pushes(Square::D7.into(), Color::Black, bitboard![D6]), - BitBoard::EMPTY + BitBoard::empty() ); } } diff --git a/board/src/piece_sets.rs b/board/src/piece_sets.rs index 52af054..de43caa 100644 --- a/board/src/piece_sets.rs +++ b/board/src/piece_sets.rs @@ -51,10 +51,11 @@ impl PieceSet { color_occupancy[color_index] |= bitboard; shape_occupancy[shape_index] |= bitboard; + counts.increment(color, shape); + for square in bitboard.occupied_squares(&IterationDirection::default()) { let piece = Piece::new(color, shape); mailbox.set(piece, square); - counts.increment(color, shape); } } } diff --git a/board/src/piece_sets/counts.rs b/board/src/piece_sets/counts.rs index 7d3cade..effbbe0 100644 --- a/board/src/piece_sets/counts.rs +++ b/board/src/piece_sets/counts.rs @@ -17,21 +17,20 @@ impl Counts { const SQUARE_NUM: u8 = Square::NUM as u8; let updated_value = self.0[color as usize][shape as usize] + 1; - if updated_value > SQUARE_NUM { - let shape_name = shape.name(); - panic!("piece count for {color} {shape_name} overflowed"); + if updated_value <= SQUARE_NUM { + self.0[color as usize][shape as usize] = updated_value; + } else { + unreachable!("piece count for {color} {shape} overflowed"); } - - self.0[color as usize][shape as usize] = updated_value; } pub fn decrement(&mut self, color: Color, shape: Shape) { let count = self.0[color as usize][shape as usize]; - let updated_count = count.checked_sub(1).unwrap_or_else(|| { - let shape_name = shape.name(); - panic!("piece count for {color} {shape_name} should not underflow"); - }); - self.0[color as usize][shape as usize] = updated_count; + if let Some(updated_count) = count.checked_sub(1) { + self.0[color as usize][shape as usize] = updated_count; + } else { + unreachable!("piece count for {color} {shape} underflowed"); + } } #[cfg(test)] diff --git a/board/src/sight.rs b/board/src/sight.rs index e682cb0..b8e366e 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -305,7 +305,7 @@ mod tests { let piece = piece!(White Pawn); let sight = piece.sight(Square::E4, &pos); - assert_eq!(sight, BitBoard::EMPTY); + assert_eq!(sight, BitBoard::empty()); } #[test] diff --git a/core/src/shapes.rs b/core/src/shapes.rs index 77126ba..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, @@ -96,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 {