chessai 1.0.0

High-performance Xiangqi (Chinese Chess) AI engine with u128 bitboards
Documentation
use std::fmt;
use std::iter::FusedIterator;
use std::ops::BitAnd;
use std::ops::BitAndAssign;
use std::ops::BitOr;
use std::ops::BitOrAssign;
use std::ops::BitXor;
use std::ops::BitXorAssign;
use std::ops::Not;
use std::ops::Sub;

use crate::square::Square;

/// `1u128 << 90 - 1`; 1 bits in positions 0..=89.
pub const BOARD_MASK: u128 = (1u128 << 90) - 1;

#[derive(Copy, Clone, Debug, Default, Hash, PartialEq, Eq)]
#[repr(transparent)]
pub struct BitBoard(pub u128);

impl BitBoard {
    pub const EMPTY: BitBoard = BitBoard(0);
    #[cfg(test)]
    pub const FULL: BitBoard = BitBoard(BOARD_MASK);

    #[inline]
    pub const fn raw(self) -> u128 { self.0 }

    #[inline]
    pub const fn from_square(sq: Square) -> BitBoard { BitBoard(1u128 << sq.raw() as u32) }

    #[inline]
    pub const fn has(self, sq: Square) -> bool { (self.0 >> sq.raw() as u32) & 1 == 1 }

    #[inline]
    pub const fn is_empty(self) -> bool { self.0 == 0 }

    #[inline]
    pub const fn any(self) -> bool { self.0 != 0 }

    #[cfg(test)]
    #[inline]
    pub const fn popcount(self) -> u32 { self.0.count_ones() }

    /// Index of the lowest set bit. Caller must ensure the bitboard is non-empty.
    #[inline]
    pub const fn lsb_square(self) -> Square {
        debug_assert!(self.0 != 0);
        Square::new_unchecked(self.0.trailing_zeros() as u8)
    }

    /// Pops and returns the lowest set square (mutates).
    #[inline]
    pub fn pop_lsb(&mut self) -> Square {
        let sq = self.lsb_square();
        self.0 &= self.0 - 1;
        sq
    }

    /// Iterate over every set square in ascending order.
    #[inline]
    pub const fn iter(self) -> BitBoardIter { BitBoardIter(self.0) }
}

// -------- Iterator --------

#[derive(Copy, Clone, Debug)]
pub struct BitBoardIter(u128);

impl Iterator for BitBoardIter {
    type Item = Square;

    #[inline]
    fn next(&mut self) -> Option<Square> {
        if self.0 == 0 {
            return None;
        }
        let sq = Square::new_unchecked(self.0.trailing_zeros() as u8);
        self.0 &= self.0 - 1;
        Some(sq)
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        let n = self.0.count_ones() as usize;
        (n, Some(n))
    }
}

impl ExactSizeIterator for BitBoardIter {}
impl FusedIterator for BitBoardIter {}

impl IntoIterator for BitBoard {
    type Item = Square;
    type IntoIter = BitBoardIter;
    #[inline]
    fn into_iter(self) -> BitBoardIter { self.iter() }
}

// -------- Bitwise ops --------

impl BitOr for BitBoard {
    type Output = BitBoard;
    #[inline]
    fn bitor(self, rhs: BitBoard) -> BitBoard { BitBoard(self.0 | rhs.0) }
}

impl BitAnd for BitBoard {
    type Output = BitBoard;
    #[inline]
    fn bitand(self, rhs: BitBoard) -> BitBoard { BitBoard(self.0 & rhs.0) }
}

impl BitXor for BitBoard {
    type Output = BitBoard;
    #[inline]
    fn bitxor(self, rhs: BitBoard) -> BitBoard { BitBoard(self.0 ^ rhs.0) }
}

impl Sub for BitBoard {
    /// `a - b` == `a & !b`; convenient for "remove friendly pieces from target mask".
    type Output = BitBoard;
    #[inline]
    fn sub(self, rhs: BitBoard) -> BitBoard { BitBoard(self.0 & !rhs.0) }
}

impl Not for BitBoard {
    type Output = BitBoard;
    #[inline]
    fn not(self) -> BitBoard { BitBoard(!self.0 & BOARD_MASK) }
}

impl BitOrAssign for BitBoard {
    #[inline]
    fn bitor_assign(&mut self, rhs: BitBoard) { self.0 |= rhs.0; }
}

impl BitAndAssign for BitBoard {
    #[inline]
    fn bitand_assign(&mut self, rhs: BitBoard) { self.0 &= rhs.0; }
}

impl BitXorAssign for BitBoard {
    #[inline]
    fn bitxor_assign(&mut self, rhs: BitBoard) { self.0 ^= rhs.0; }
}

impl fmt::Display for BitBoard {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "  a b c d e f g h i")?;
        for rank in (0..10).rev() {
            write!(f, "{rank} ")?;
            for file in 0..9 {
                let sq = Square::from_rank_file(rank, file).unwrap();
                f.write_str(if self.has(sq) { "x " } else { ". " })?;
            }
            writeln!(f)?;
        }
        Ok(())
    }
}

// -------- Board region masks --------

const fn build_red_palace() -> BitBoard {
    let mut m = 0u128;
    let mut r = 0u8;
    while r <= 2 {
        let mut f = 3u8;
        while f <= 5 {
            m |= 1u128 << (r * 9 + f) as u32;
            f += 1;
        }
        r += 1;
    }
    BitBoard(m)
}

const fn build_black_palace() -> BitBoard {
    let mut m = 0u128;
    let mut r = 7u8;
    while r <= 9 {
        let mut f = 3u8;
        while f <= 5 {
            m |= 1u128 << (r * 9 + f) as u32;
            f += 1;
        }
        r += 1;
    }
    BitBoard(m)
}

const fn build_half(red: bool) -> BitBoard {
    let mut m = 0u128;
    let (lo, hi) = if red { (0u8, 4u8) } else { (5u8, 9u8) };
    let mut r = lo;
    while r <= hi {
        let mut f = 0u8;
        while f < 9 {
            m |= 1u128 << (r * 9 + f) as u32;
            f += 1;
        }
        r += 1;
    }
    BitBoard(m)
}

pub const RED_PALACE: BitBoard = build_red_palace();
pub const BLACK_PALACE: BitBoard = build_black_palace();
pub const PALACES: [BitBoard; 2] = [RED_PALACE, BLACK_PALACE];
pub const RED_HALF: BitBoard = build_half(true);
pub const BLACK_HALF: BitBoard = build_half(false);
pub const HOME_HALVES: [BitBoard; 2] = [RED_HALF, BLACK_HALF];

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn full_has_90_bits() {
        assert_eq!(BitBoard::FULL.popcount(), 90);
    }

    #[test]
    fn top_bits_always_zero() {
        let all = !BitBoard::EMPTY;
        assert_eq!(all.popcount(), 90);
    }

    #[test]
    fn iter_yields_every_set_bit() {
        let mut bb = BitBoard::EMPTY;
        let sqs = [3u8, 17, 40, 89];
        for s in sqs {
            bb |= BitBoard::from_square(Square::new_unchecked(s));
        }
        let collected: Vec<u8> = bb.iter().map(|s| s.raw()).collect();
        assert_eq!(collected, sqs);
    }

    #[test]
    fn palace_has_nine_squares() {
        assert_eq!(RED_PALACE.popcount(), 9);
        assert_eq!(BLACK_PALACE.popcount(), 9);
        assert!((RED_PALACE & BLACK_PALACE).is_empty());
    }

    #[test]
    fn halves_split_board() {
        assert_eq!(RED_HALF.popcount(), 45);
        assert_eq!(BLACK_HALF.popcount(), 45);
        assert_eq!(RED_HALF | BLACK_HALF, BitBoard::FULL);
    }
}