Compare commits
10 commits
a904e4a5bb
...
1c94f157e6
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