606 lines
18 KiB
Rust
606 lines
18 KiB
Rust
// Eryn Wells <eryn@erynwells.me>
|
|
|
|
use crate::bit_scanner::{LeadingBitScanner, TrailingBitScanner};
|
|
use crate::direction::IterationDirection;
|
|
use crate::library;
|
|
use chessfriend_core::{Color, Direction, File, Rank, Square};
|
|
use forward_ref::{forward_ref_binop, forward_ref_op_assign, forward_ref_unop};
|
|
use std::fmt;
|
|
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not};
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
const SQUARES_NUM: u8 = Square::NUM as u8;
|
|
|
|
/// A bitfield representation of a chess board that uses the bits of a 64-bit
|
|
/// unsigned integer to represent whether a square on the board is occupied.
|
|
/// Squares are laid out as follows, starting at the bottom left, going row-wise,
|
|
/// and ending at the top right corner:
|
|
///
|
|
/// ```text
|
|
/// +-------------------------+
|
|
/// 8 | 56 57 58 59 60 61 62 63 |
|
|
/// 7 | 48 49 50 51 52 53 54 55 |
|
|
/// 6 | 40 41 42 43 44 45 46 47 |
|
|
/// 5 | 32 33 34 35 36 37 38 39 |
|
|
/// 4 | 24 25 26 27 28 29 30 31 |
|
|
/// 3 | 16 17 18 19 20 21 22 23 |
|
|
/// 2 | 8 9 10 11 12 13 14 15 |
|
|
/// 1 | 0 1 2 3 4 5 6 7 |
|
|
/// +-------------------------+
|
|
/// A B C D E F G H
|
|
/// ```
|
|
///
|
|
#[must_use]
|
|
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
|
pub struct BitBoard(pub(crate) u64);
|
|
|
|
macro_rules! moves_getter {
|
|
($getter_name:ident) => {
|
|
pub fn $getter_name(sq: Square) -> BitBoard {
|
|
library::library().$getter_name(sq)
|
|
}
|
|
};
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
pub const fn rank(rank: Rank) -> BitBoard {
|
|
library::RANKS[rank.as_index()]
|
|
}
|
|
|
|
pub const fn file(file: File) -> BitBoard {
|
|
library::FILES[file.as_index()]
|
|
}
|
|
|
|
pub fn ray(sq: Square, dir: Direction) -> BitBoard {
|
|
library::library().ray(sq, dir)
|
|
}
|
|
|
|
pub fn pawn_attacks(sq: Square, color: Color) -> BitBoard {
|
|
library::library().pawn_attacks(sq, color)
|
|
}
|
|
|
|
pub fn pawn_pushes(sq: Square, color: Color) -> BitBoard {
|
|
library::library().pawn_pushes(sq, color)
|
|
}
|
|
|
|
moves_getter!(knight_moves);
|
|
moves_getter!(bishop_moves);
|
|
moves_getter!(rook_moves);
|
|
moves_getter!(queen_moves);
|
|
moves_getter!(king_moves);
|
|
|
|
pub const fn kingside(color: Color) -> &'static BitBoard {
|
|
&library::KINGSIDES[color as usize]
|
|
}
|
|
|
|
pub const fn queenside(color: Color) -> &'static BitBoard {
|
|
&library::QUEENSIDES[color as usize]
|
|
}
|
|
}
|
|
|
|
impl BitBoard {
|
|
/// Converts this [`BitBoard`] to an unsigned 64-bit integer.
|
|
#[must_use]
|
|
pub const fn as_bits(&self) -> u64 {
|
|
self.0
|
|
}
|
|
|
|
/// Returns `true` if this [`BitBoard`] has no bits set. This is the opposite
|
|
/// of [`BitBoard::is_populated`].
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// use chessfriend_bitboard::BitBoard;
|
|
/// assert!(BitBoard::empty().is_empty());
|
|
/// assert!(!BitBoard::full().is_empty());
|
|
/// assert!(!BitBoard::new(0b1000).is_empty());
|
|
/// ```
|
|
#[must_use]
|
|
pub const fn is_empty(&self) -> bool {
|
|
self.0 == 0
|
|
}
|
|
|
|
/// Returns `true` if the [`BitBoard`] has at least one bit set. This is the
|
|
/// opposite of [`BitBoard::is_empty`].
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// use chessfriend_bitboard::BitBoard;
|
|
/// assert!(!BitBoard::empty().is_populated());
|
|
/// assert!(BitBoard::full().is_populated());
|
|
/// assert!(BitBoard::new(0b1).is_populated());
|
|
/// ```
|
|
#[must_use]
|
|
pub const fn is_populated(&self) -> bool {
|
|
self.0 != 0
|
|
}
|
|
|
|
/// Returns `true` if this [`BitBoard`] has the bit corresponding to `square` set.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// use chessfriend_bitboard::BitBoard;
|
|
/// use chessfriend_core::Square;
|
|
///
|
|
/// let square = Square::E4;
|
|
/// let mut bitboard = BitBoard::new(0b1001100);
|
|
///
|
|
/// assert!(bitboard.contains(Square::C1));
|
|
/// assert!(!bitboard.contains(Square::B1));
|
|
/// ```
|
|
#[must_use]
|
|
pub fn contains(self, square: Square) -> bool {
|
|
let square_bitboard: BitBoard = square.into();
|
|
!(self & square_bitboard).is_empty()
|
|
}
|
|
|
|
/// Counts the number of set squares (1 bits) in this [`BitBoard`].
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// use chessfriend_bitboard::BitBoard;
|
|
/// assert_eq!(BitBoard::EMPTY.population_count(), 0);
|
|
/// assert_eq!(BitBoard::new(0b01011110010).population_count(), 6);
|
|
/// assert_eq!(BitBoard::FULL.population_count(), 64);
|
|
/// ```
|
|
#[must_use]
|
|
pub const fn population_count(&self) -> u32 {
|
|
self.0.count_ones()
|
|
}
|
|
|
|
/// Set a square in this [`BitBoard`] by toggling the corresponding bit to 1.
|
|
/// This always succeeds, even if the bit was already set.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// use chessfriend_bitboard::BitBoard;
|
|
/// use chessfriend_core::Square;
|
|
///
|
|
/// let mut bitboard = BitBoard::new(0b1001100);
|
|
/// bitboard.set(Square::E4);
|
|
/// assert!(bitboard.contains(Square::E4));
|
|
/// ```
|
|
pub fn set(&mut self, square: Square) {
|
|
let square_bitboard: BitBoard = square.into();
|
|
self.0 |= square_bitboard.0;
|
|
}
|
|
|
|
/// Clear a square (set it to 0) in this [`BitBoard`]. This always succeeds
|
|
/// even if the bit is not set.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// use chessfriend_bitboard::BitBoard;
|
|
/// use chessfriend_core::Square;
|
|
///
|
|
/// let mut bitboard = BitBoard::new(0b1001100);
|
|
/// bitboard.clear(Square::C1);
|
|
/// assert!(!bitboard.contains(Square::C1));
|
|
/// ```
|
|
pub fn clear(&mut self, square: Square) {
|
|
let square_bitboard: BitBoard = square.into();
|
|
self.0 &= !square_bitboard.0;
|
|
}
|
|
|
|
/// Returns `true` if this [`BitBoard`] represents a single square.
|
|
///
|
|
/// ## Examples
|
|
///
|
|
/// ```
|
|
/// 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::new(0b010011110101101100).is_single_square(), "This bitboard represents a bunch of squares");
|
|
/// assert!(BitBoard::new(0b10000000000000).is_single_square());
|
|
/// ```
|
|
#[must_use]
|
|
pub fn is_single_square(&self) -> bool {
|
|
self.0.is_power_of_two()
|
|
}
|
|
|
|
/// Return an Iterator over the occupied squares.
|
|
#[must_use]
|
|
pub fn occupied_squares(
|
|
&self,
|
|
direction: &IterationDirection,
|
|
) -> Box<dyn Iterator<Item = Square>> {
|
|
match direction {
|
|
IterationDirection::Leading => Box::new(self.occupied_squares_leading()),
|
|
IterationDirection::Trailing => Box::new(self.occupied_squares_trailing()),
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn occupied_squares_trailing(&self) -> TrailingBitScanner {
|
|
TrailingBitScanner::new(self.0)
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn first_occupied_square_direction(&self, direction: Direction) -> Option<Square> {
|
|
match direction {
|
|
Direction::North | Direction::NorthEast | Direction::NorthWest | Direction::East => {
|
|
self.first_occupied_square_trailing()
|
|
}
|
|
Direction::SouthEast | Direction::South | Direction::SouthWest | Direction::West => {
|
|
self.first_occupied_square_leading()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 {
|
|
IterationDirection::Leading => self.first_occupied_square_leading(),
|
|
IterationDirection::Trailing => self.first_occupied_square_trailing(),
|
|
}
|
|
}
|
|
|
|
/// If the board is not empty, returns the first occupied square on the
|
|
/// board, starting at the leading (most-significant) end of the board. If
|
|
/// the board is empty, returns `None`.
|
|
#[must_use]
|
|
pub fn first_occupied_square_leading(self) -> Option<Square> {
|
|
let leading_zeros = self.leading_zeros();
|
|
if leading_zeros < SQUARES_NUM {
|
|
unsafe {
|
|
Some(Square::from_index_unchecked(
|
|
SQUARES_NUM - leading_zeros - 1,
|
|
))
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// If the board is not empty, returns the first occupied square on the
|
|
/// board, starting at the trailing (least-significant) end of the board.
|
|
/// If the board is empty, returns `None`.
|
|
#[must_use]
|
|
pub fn first_occupied_square_trailing(self) -> Option<Square> {
|
|
let trailing_zeros = self.trailing_zeros();
|
|
|
|
if trailing_zeros < SQUARES_NUM {
|
|
unsafe { Some(Square::from_index_unchecked(trailing_zeros)) }
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BitBoard {
|
|
#[must_use]
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
fn leading_zeros(self) -> u8 {
|
|
self.0.leading_zeros() as u8
|
|
}
|
|
|
|
#[must_use]
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
fn trailing_zeros(self) -> u8 {
|
|
self.0.trailing_zeros() as u8
|
|
}
|
|
}
|
|
|
|
impl Default for BitBoard {
|
|
fn default() -> Self {
|
|
BitBoard::EMPTY
|
|
}
|
|
}
|
|
|
|
impl From<BitBoard> for u64 {
|
|
fn from(value: BitBoard) -> Self {
|
|
value.as_bits()
|
|
}
|
|
}
|
|
|
|
impl From<File> for BitBoard {
|
|
fn from(value: File) -> Self {
|
|
library::FILES[value.as_index()]
|
|
}
|
|
}
|
|
|
|
impl From<Option<Square>> for BitBoard {
|
|
fn from(value: Option<Square>) -> Self {
|
|
value.map_or(BitBoard::EMPTY, Into::<BitBoard>::into)
|
|
}
|
|
}
|
|
|
|
impl From<Rank> for BitBoard {
|
|
fn from(value: Rank) -> Self {
|
|
library::FILES[value.as_index()]
|
|
}
|
|
}
|
|
|
|
impl From<Square> for BitBoard {
|
|
fn from(value: Square) -> Self {
|
|
BitBoard(1u64 << value as u32)
|
|
}
|
|
}
|
|
|
|
impl FromIterator<Square> for BitBoard {
|
|
fn from_iter<T: IntoIterator<Item = Square>>(iter: T) -> Self {
|
|
iter.into_iter().fold(BitBoard::EMPTY, |mut acc, sq| {
|
|
acc.set(sq);
|
|
acc
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum TryFromBitBoardError {
|
|
NotSingleSquare,
|
|
}
|
|
|
|
impl TryFrom<BitBoard> for Square {
|
|
type Error = TryFromBitBoardError;
|
|
|
|
fn try_from(value: BitBoard) -> Result<Self, Self::Error> {
|
|
if !value.is_single_square() {
|
|
return Err(TryFromBitBoardError::NotSingleSquare);
|
|
}
|
|
|
|
unsafe { Ok(Square::from_index_unchecked(value.trailing_zeros())) }
|
|
}
|
|
}
|
|
|
|
impl fmt::Binary for BitBoard {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// Delegate to u64's implementation of Binary.
|
|
fmt::Binary::fmt(&self.0, f)
|
|
}
|
|
}
|
|
|
|
impl fmt::LowerHex for BitBoard {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// Delegate to u64's implementation of LowerHex.
|
|
fmt::LowerHex::fmt(&self.0, f)
|
|
}
|
|
}
|
|
|
|
impl fmt::UpperHex for BitBoard {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
// Delegate to u64's implementation of UpperHex.
|
|
fmt::UpperHex::fmt(&self.0, f)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for BitBoard {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let binary_ranks = format!("{:064b}", self.0)
|
|
.chars()
|
|
.rev()
|
|
.map(String::from)
|
|
.collect::<Vec<String>>();
|
|
|
|
let mut ranks_written = 0;
|
|
for rank in binary_ranks.chunks(8).rev() {
|
|
write!(f, "{}", rank.join(" "))?;
|
|
|
|
ranks_written += 1;
|
|
if ranks_written < 8 {
|
|
writeln!(f)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for BitBoard {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
|
write!(f, "BitBoard({:064b})", self.0)
|
|
}
|
|
}
|
|
|
|
macro_rules! infix_op {
|
|
($trait_type:ident, $func_name:ident, $left_type:ty, $right_type:ty) => {
|
|
impl std::ops::$trait_type<$right_type> for $left_type {
|
|
type Output = Self;
|
|
|
|
#[inline]
|
|
fn $func_name(self, rhs: $right_type) -> Self::Output {
|
|
BitBoard(std::ops::$trait_type::$func_name(self.0, rhs.0))
|
|
}
|
|
}
|
|
|
|
forward_ref_binop!(impl $trait_type, $func_name for $left_type, $right_type);
|
|
};
|
|
}
|
|
|
|
macro_rules! assign_op {
|
|
($trait_type:ident, $func_name:ident, $type:ty) => {
|
|
impl $trait_type for $type {
|
|
#[inline]
|
|
fn $func_name(&mut self, rhs: $type) {
|
|
$trait_type::$func_name(&mut self.0, rhs.0)
|
|
}
|
|
}
|
|
|
|
forward_ref_op_assign!(impl $trait_type, $func_name for $type, $type);
|
|
};
|
|
}
|
|
|
|
infix_op!(BitAnd, bitand, BitBoard, BitBoard);
|
|
infix_op!(BitOr, bitor, BitBoard, BitBoard);
|
|
infix_op!(BitXor, bitxor, BitBoard, BitBoard);
|
|
|
|
assign_op!(BitAndAssign, bitand_assign, BitBoard);
|
|
assign_op!(BitOrAssign, bitor_assign, BitBoard);
|
|
assign_op!(BitXorAssign, bitxor_assign, BitBoard);
|
|
|
|
impl Not for BitBoard {
|
|
type Output = Self;
|
|
|
|
#[inline]
|
|
fn not(self) -> Self::Output {
|
|
BitBoard(!self.0)
|
|
}
|
|
}
|
|
|
|
forward_ref_unop!(impl Not, not for BitBoard);
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::bitboard;
|
|
use chessfriend_core::Square;
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn display_and_debug() {
|
|
let bb = BitBoard::file(File::A)
|
|
| BitBoard::file(File::D)
|
|
| BitBoard::rank(Rank::FIVE)
|
|
| BitBoard::rank(Rank::EIGHT);
|
|
println!("{}", &bb);
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::unreadable_literal)]
|
|
fn rank() {
|
|
assert_eq!(BitBoard::rank(Rank::ONE).0, 0xFF, "Rank 1");
|
|
assert_eq!(BitBoard::rank(Rank::TWO).0, 0xFF00, "Rank 2");
|
|
assert_eq!(BitBoard::rank(Rank::THREE).0, 0xFF0000, "Rank 3");
|
|
assert_eq!(BitBoard::rank(Rank::FOUR).0, 0xFF000000, "Rank 4");
|
|
assert_eq!(BitBoard::rank(Rank::FIVE).0, 0xFF00000000, "Rank 5");
|
|
assert_eq!(BitBoard::rank(Rank::SIX).0, 0xFF0000000000, "Rank 6");
|
|
assert_eq!(BitBoard::rank(Rank::SEVEN).0, 0xFF000000000000, "Rank 7");
|
|
assert_eq!(BitBoard::rank(Rank::EIGHT).0, 0xFF00000000000000, "Rank 8");
|
|
}
|
|
|
|
#[test]
|
|
fn single_rank_occupancy() {
|
|
#[allow(clippy::unreadable_literal)]
|
|
let bb = BitBoard(0b01010100);
|
|
|
|
let expected_squares = [Square::G1, Square::E1, Square::C1];
|
|
bb.occupied_squares(&IterationDirection::Leading)
|
|
.zip(expected_squares)
|
|
.for_each(|(a, b)| assert_eq!(a, b));
|
|
}
|
|
|
|
#[test]
|
|
fn occupancy_spot_check() {
|
|
#[allow(clippy::unreadable_literal)]
|
|
let bb =
|
|
BitBoard(0b10000000_00000000_00100000_00000100_00000000_00000000_00010000_00001000);
|
|
|
|
let expected_squares = [Square::H8, Square::F6, Square::C5, Square::E2, Square::D1];
|
|
|
|
bb.occupied_squares(&IterationDirection::Leading)
|
|
.zip(expected_squares)
|
|
.for_each(|(a, b)| assert_eq!(a, b));
|
|
}
|
|
|
|
#[test]
|
|
fn xor() {
|
|
let a = bitboard![C5 G7];
|
|
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());
|
|
}
|
|
|
|
#[test]
|
|
fn bitand_assign() {
|
|
let mut a = bitboard![C5 G7];
|
|
let b = bitboard![B5 G7 H3];
|
|
|
|
a &= b;
|
|
|
|
assert_eq!(a, bitboard![G7]);
|
|
}
|
|
|
|
#[test]
|
|
fn bitor_assign() {
|
|
let mut a = bitboard![C5 G7];
|
|
let b = bitboard![B5 G7 H3];
|
|
|
|
a |= b;
|
|
|
|
assert_eq!(a, bitboard![B5 C5 G7 H3]);
|
|
}
|
|
|
|
#[test]
|
|
fn from_square() {
|
|
assert_eq!(BitBoard::from(Square::A1), BitBoard(0b1));
|
|
assert_eq!(BitBoard::from(Square::H8), BitBoard(1 << 63));
|
|
}
|
|
|
|
#[test]
|
|
fn first_occupied_squares() {
|
|
let bb = bitboard![A8 E1];
|
|
assert_eq!(bb.first_occupied_square_leading(), Some(Square::A8));
|
|
assert_eq!(bb.first_occupied_square_trailing(), Some(Square::E1));
|
|
|
|
let bb = bitboard![D6 E7 F8];
|
|
assert_eq!(bb.first_occupied_square_trailing(), Some(Square::D6));
|
|
}
|
|
}
|