From 0d22c59cc362cf814cbddcb88c84f5c5e1814feb Mon Sep 17 00:00:00 2001 From: Paul-Nicolas Madelaine Date: Wed, 22 Oct 2025 22:59:53 +0200 Subject: [PATCH] const lookup table --- src/bitboard.rs | 17 +- src/board.rs | 71 ++++-- src/lib.rs | 3 +- src/lookup.rs | 601 +++++++++++++++++++++++++++++++++--------------- src/magics.rs | 195 +--------------- src/position.rs | 169 +++++++------- src/rays.rs | 44 ---- src/setup.rs | 15 +- 8 files changed, 573 insertions(+), 542 deletions(-) delete mode 100644 src/rays.rs diff --git a/src/bitboard.rs b/src/bitboard.rs index c74e435..b4c18d3 100644 --- a/src/bitboard.rs +++ b/src/bitboard.rs @@ -18,7 +18,7 @@ impl Bitboard { } #[inline] - pub fn first(&self) -> Option { + pub const fn first(&self) -> Option { let mask = self.0; match mask { 0 => None, @@ -26,6 +26,17 @@ impl Bitboard { } } + #[inline] + pub const fn last(&self) -> Option { + let mask = self.0; + match mask { + 0 => None, + _ => { + Some(unsafe { Square::transmute(63_u8.unchecked_sub(mask.leading_zeros() as u8)) }) + } + } + } + #[inline] pub fn pop(&mut self) -> Option { let Self(ref mut mask) = self; @@ -38,7 +49,7 @@ impl Bitboard { } #[inline] - pub fn insert(&mut self, square: Square) { + pub const fn insert(&mut self, square: Square) { self.0 |= 1 << square as u8; } @@ -57,7 +68,7 @@ impl Bitboard { } #[inline] - pub fn contains(&self, square: Square) -> bool { + pub const fn contains(&self, square: Square) -> bool { self.0 & (1 << square as u8) != 0 } diff --git a/src/board.rs b/src/board.rs index 186693b..9833d61 100644 --- a/src/board.rs +++ b/src/board.rs @@ -23,6 +23,9 @@ macro_rules! container { pub fn get_mut(&mut self, k: $a) -> &mut T { unsafe { self.0.get_unchecked_mut(k as usize) } } + pub(crate) const fn get_const(&self, k: $a) -> &T { + &self.0[k as usize] + } } }; } @@ -133,12 +136,12 @@ impl File { } #[inline] - pub(crate) fn bitboard(self) -> Bitboard { + pub(crate) const fn bitboard(self) -> Bitboard { Bitboard(0x0101010101010101 << (self as u8)) } #[inline] - pub(crate) unsafe fn transmute(value: u8) -> Self { + pub(crate) const unsafe fn transmute(value: u8) -> Self { debug_assert!(value < 8); std::mem::transmute(value) } @@ -210,12 +213,12 @@ impl Rank { } #[inline] - pub(crate) fn bitboard(self) -> Bitboard { + pub(crate) const fn bitboard(self) -> Bitboard { Bitboard(0xFF << ((self as u64) << 3)) } #[inline] - pub(crate) unsafe fn transmute(value: u8) -> Self { + pub(crate) const unsafe fn transmute(value: u8) -> Self { debug_assert!(value < 8); std::mem::transmute(value) } @@ -261,18 +264,26 @@ impl Square { ] } + pub(crate) const fn from_index(index: u8) -> Option { + if index < 64 { + Some(unsafe { Self::transmute(index) }) + } else { + None + } + } + #[inline] pub fn new(file: File, rank: Rank) -> Self { unsafe { Self::transmute(((rank as u8) << 3) | file as u8) } } #[inline] - pub fn file(self) -> File { + pub const fn file(self) -> File { unsafe { File::transmute((self as u8) & 7) } } #[inline] - pub fn rank(self) -> Rank { + pub const fn rank(self) -> Rank { unsafe { Rank::transmute((self as u8) >> 3) } } @@ -283,7 +294,7 @@ impl Square { } #[inline] - pub(crate) fn bitboard(self) -> Bitboard { + pub(crate) const fn bitboard(self) -> Bitboard { Bitboard(1 << self as u8) } @@ -317,16 +328,17 @@ impl Square { } #[inline] - pub(crate) fn trans(self, direction: Direction) -> Option { - self.check_trans(direction).then(|| unsafe { - // SAFETY: condition is checked before doing the translation - self.trans_unchecked(direction) - }) + pub(crate) const fn trans(self, direction: Direction) -> Option { + if self.check_trans(direction) { + Some(unsafe { self.trans_unchecked(direction) }) + } else { + None + } } /// SAFETY: the translation must not move the square outside the board #[inline] - pub(crate) unsafe fn trans_unchecked(self, direction: Direction) -> Self { + pub(crate) const unsafe fn trans_unchecked(self, direction: Direction) -> Self { debug_assert!(self.check_trans(direction)); let i = self as u8; unsafe { @@ -345,21 +357,23 @@ impl Square { /// Returns `false` if the translation would move the square outside the board #[inline] - fn check_trans(self, direction: Direction) -> bool { + const fn check_trans(self, direction: Direction) -> bool { + let file = self.file() as u8; + let rank = self.rank() as u8; match direction { - Direction::East => self.file() < File::H, - Direction::NorthEast => self.file() < File::H && self.rank() < Rank::Eighth, - Direction::North => self.rank() < Rank::Eighth, - Direction::NorthWest => self.file() > File::A && self.rank() < Rank::Eighth, - Direction::SouthEast => self.file() < File::H && self.rank() > Rank::First, - Direction::South => self.rank() > Rank::First, - Direction::SouthWest => self.file() > File::A && self.rank() > Rank::First, - Direction::West => self.file() > File::A, + Direction::East => file < 7, + Direction::NorthEast => file < 7 && rank < 7, + Direction::North => rank < 7, + Direction::NorthWest => file > 0 && rank < 7, + Direction::SouthEast => file < 7 && rank > 0, + Direction::South => rank > 0, + Direction::SouthWest => file > 0 && rank > 0, + Direction::West => file > 0, } } #[inline] - pub(crate) unsafe fn transmute(value: u8) -> Self { + pub(crate) const unsafe fn transmute(value: u8) -> Self { debug_assert!(value < 64); std::mem::transmute(value) } @@ -591,7 +605,16 @@ impl Direction { } #[inline] - unsafe fn transmute(value: u8) -> Self { + pub(crate) const fn from_index(index: u8) -> Option { + if index < 8 { + Some(unsafe { Self::transmute(index) }) + } else { + None + } + } + + #[inline] + const unsafe fn transmute(value: u8) -> Self { debug_assert!(value < 8); std::mem::transmute(value) } diff --git a/src/lib.rs b/src/lib.rs index 6c36c79..8a0a4e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,11 +74,10 @@ pub(crate) mod array_vec; pub(crate) mod bitboard; +pub(crate) mod lookup; pub(crate) mod magics; -pub(crate) mod rays; pub mod board; -pub mod lookup; pub mod position; pub mod san; pub mod setup; diff --git a/src/lookup.rs b/src/lookup.rs index d1378a6..aaabdf3 100644 --- a/src/lookup.rs +++ b/src/lookup.rs @@ -1,202 +1,441 @@ -//! Lookup tables initialisation. -//! -//! Move generation in eschac requires about 1MB of precomputed lookup tables. - use crate::bitboard::*; use crate::board::*; use crate::magics::*; -use crate::rays::*; -pub(crate) use init::InitialisedLookup; - -/// Forces the initialisation of the lookup tables. -/// -/// It is not necessary to call this function, as lookup tables are initialised lazily, but it can -/// be used to ensure that they are initialised before a given time. -pub fn init() { - InitialisedLookup::init(); -} - -pub(crate) struct Lookup { - rays: Rays, - lines: BySquare>, - segments: BySquare>, - pawn_attacks: ByColor>, - king_moves: BySquare, - knight_moves: BySquare, - pub(crate) magics: Magics, -} - -impl Lookup { - #[inline] - pub(crate) fn line(&self, a: Square, b: Square) -> Bitboard { - *self.lines.get(a).get(b) - } - - #[inline] - pub(crate) fn segment(&self, a: Square, b: Square) -> Bitboard { - *self.segments.get(a).get(b) - } - - #[inline] - pub(crate) fn ray(&self, square: Square, direction: Direction) -> Bitboard { - self.rays.ray(square, direction) - } - - #[inline] - pub(crate) fn king(&self, square: Square) -> Bitboard { - *self.king_moves.get(square) - } - - #[inline] - pub(crate) fn knight(&self, square: Square) -> Bitboard { - *self.knight_moves.get(square) - } - - #[inline] - pub(crate) fn pawn_attack(&self, color: Color, square: Square) -> Bitboard { - *self.pawn_attacks.get(color).get(square) - } - - #[inline] - pub(crate) fn bishop(&self, square: Square, blockers: Bitboard) -> Bitboard { - self.magics.bishop(square, blockers) - } - - #[inline] - pub(crate) fn rook(&self, square: Square, blockers: Bitboard) -> Bitboard { - self.magics.rook(square, blockers) - } - - /// `role != Pawn` - #[inline] - pub(crate) fn targets(&self, role: Role, from: Square, blockers: Bitboard) -> Bitboard { - match role { - Role::Pawn => unreachable!(), - Role::Knight => self.knight(from), - Role::Bishop => self.bishop(from, blockers), - Role::Rook => self.rook(from, blockers), - Role::Queen => self.bishop(from, blockers) | self.rook(from, blockers), - Role::King => self.king(from), +macro_rules! loop_subsets { + ($premask: ident, $subset: ident, $e: expr) => {{ + let mut $subset: u64 = 0; + loop { + $subset = $subset.wrapping_sub($premask) & $premask; + $e + if $subset == 0 { + break; + } } - } + }}; +} - pub(crate) fn compute() -> Self { - let rays = Rays::new(); +macro_rules! by_color { + ($c: ident, $e: expr) => {{ + ByColor([ + { + let $c = Color::White; + $e + }, + { + let $c = Color::Black; + $e + }, + ]) + }}; +} - let lines = BySquare::new(|a| { - BySquare::new(|b| { - for d in Direction::all() { - let r = rays.ray(a, d); - if r.contains(b) { - return r; - } - } - Bitboard::new() - }) - }); - - let segments = BySquare::new(|a| { - BySquare::new(|b| { - for d in Direction::all() { - let r = rays.ray(a, d); - if r.contains(b) { - return r & !rays.ray(b, d); - } - } - b.bitboard() - }) - }); - - let pawn_attacks = ByColor::new(|color| { - let direction = match color { - Color::White => Direction::North, - Color::Black => Direction::South, +macro_rules! by_square { + ($sq: ident, $init: expr, $e: expr) => {{ + let mut res = [$init; 64]; + let mut $sq: u8 = 0; + while $sq < 64 { + res[$sq as usize] = { + let $sq = Square::from_index($sq).unwrap(); + $e }; - BySquare::new(|square| { - let mut res = Bitboard::new(); - if let Some(square) = square.trans(direction) { - square.trans(Direction::East).map(|s| res.insert(s)); - square.trans(Direction::West).map(|s| res.insert(s)); - } - res - }) - }); - - let king_moves = BySquare::new(|square| { - let mut res = Bitboard::new(); - for direction in Direction::all() { - if let Some(x) = square.trans(direction) { - res |= x.bitboard(); - } - } - res - }); - - let knight_moves = BySquare::new(|s| { - let mut res = Bitboard::new(); - if let Some(s) = s.trans(Direction::North) { - s.trans(Direction::NorthEast).map(|s| res.insert(s)); - s.trans(Direction::NorthWest).map(|s| res.insert(s)); - } - if let Some(s) = s.trans(Direction::West) { - s.trans(Direction::NorthWest).map(|s| res.insert(s)); - s.trans(Direction::SouthWest).map(|s| res.insert(s)); - } - if let Some(s) = s.trans(Direction::South) { - s.trans(Direction::SouthWest).map(|s| res.insert(s)); - s.trans(Direction::SouthEast).map(|s| res.insert(s)); - } - if let Some(s) = s.trans(Direction::East) { - s.trans(Direction::SouthEast).map(|s| res.insert(s)); - s.trans(Direction::NorthEast).map(|s| res.insert(s)); - } - res - }); - - let magics = Magics::compute(&rays); - - Self { - rays, - lines, - segments, - pawn_attacks, - king_moves, - knight_moves, - magics, + $sq += 1; } - } + BySquare(res) + }}; } -mod init { - use std::{mem::MaybeUninit, sync::LazyLock}; +macro_rules! loop_bishop_directions { + ($d: ident, $e: expr) => {{ + { + let $d = Direction::NorthEast; + $e + } + { + let $d = Direction::NorthWest; + $e + } + { + let $d = Direction::SouthWest; + $e + } + { + let $d = Direction::SouthEast; + $e + } + }}; +} - use super::Lookup; +macro_rules! loop_rook_directions { + ($d: ident, $e: expr) => {{ + { + let $d = Direction::East; + $e + } + { + let $d = Direction::North; + $e + } + { + let $d = Direction::West; + $e + } + { + let $d = Direction::South; + $e + } + }}; +} - static mut LOOKUP: MaybeUninit = MaybeUninit::uninit(); +macro_rules! loop_all_directions { + ($d: ident, $e: expr) => {{ + loop_bishop_directions!($d, $e); + loop_rook_directions!($d, $e); + }}; +} - #[allow(static_mut_refs)] - static INIT: LazyLock<()> = LazyLock::new(|| unsafe { - LOOKUP.write(Lookup::compute()); +macro_rules! by_direction { + ($d: ident, $e: expr) => {{ + let mut res = [Bitboard(0); 8]; + let mut $d: u8 = 0; + while $d < 8 { + res[$d as usize] = { + let $d = Direction::from_index($d).unwrap(); + $e + }; + $d += 1; + } + ByDirection(res) + }}; +} + +const RAYS: BySquare> = by_square!( + square, + ByDirection([Bitboard(0); 8]), + by_direction!(direction, { + let mut square = square; + let mut res = 0; + while let Some(x) = square.trans(direction) { + square = x; + res |= square.bitboard().0; + } + Bitboard(res) + }) +); + +const LINES: BySquare> = by_square!(a, BySquare([Bitboard(0); 64]), { + by_square!(b, Bitboard(0), { + let mut res = Bitboard(0); + loop_all_directions!(d, { + let r = *RAYS.get_const(a).get_const(d); + if r.contains(b) { + res = r; + } + }); + res + }) +}); + +const SEGMENTS: BySquare> = by_square!(a, BySquare([Bitboard(0); 64]), { + by_square!(b, Bitboard(0), { + let mut res = 0; + loop_all_directions!(d, { + let r = *RAYS.get_const(a).get_const(d); + if r.contains(b) { + res = r.0 & !RAYS.get_const(b).get_const(d).0; + } + }); + Bitboard(res | b.bitboard().0) + }) +}); + +const KING_MOVES: BySquare = by_square!(sq, Bitboard(0), { + let mut res = 0; + loop_all_directions!(d, { + if let Some(x) = sq.trans(d) { + res |= x.bitboard().0; + } }); + Bitboard(res) +}); - #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub(crate) struct InitialisedLookup(()); - - impl InitialisedLookup { - #[inline] - pub(crate) fn init() -> Self { - LazyLock::force(&INIT); - Self(()) +const KNIGHT_MOVES: BySquare = by_square!(s, Bitboard(0), { + let mut res = Bitboard(0); + if let Some(s) = s.trans(Direction::North) { + if let Some(s) = s.trans(Direction::NorthEast) { + res.insert(s); } + if let Some(s) = s.trans(Direction::NorthWest) { + res.insert(s) + }; } + if let Some(s) = s.trans(Direction::West) { + if let Some(s) = s.trans(Direction::NorthWest) { + res.insert(s) + }; + if let Some(s) = s.trans(Direction::SouthWest) { + res.insert(s) + }; + } + if let Some(s) = s.trans(Direction::South) { + if let Some(s) = s.trans(Direction::SouthWest) { + res.insert(s) + }; + if let Some(s) = s.trans(Direction::SouthEast) { + res.insert(s) + }; + } + if let Some(s) = s.trans(Direction::East) { + if let Some(s) = s.trans(Direction::SouthEast) { + res.insert(s) + }; + if let Some(s) = s.trans(Direction::NorthEast) { + res.insert(s) + }; + } + res +}); - impl std::ops::Deref for InitialisedLookup { - type Target = Lookup; - #[allow(static_mut_refs)] - #[inline] - fn deref(&self) -> &Self::Target { - unsafe { LOOKUP.assume_init_ref() } - } +const PAWN_ATTACKS: ByColor> = { + by_color!(color, { + let direction = match color { + Color::White => Direction::North, + Color::Black => Direction::South, + }; + by_square!(square, Bitboard(0), { + let mut res = Bitboard(0); + if let Some(square) = square.trans(direction) { + if let Some(s) = square.trans(Direction::East) { + res.insert(s) + }; + if let Some(s) = square.trans(Direction::West) { + res.insert(s) + }; + } + res + }) + }) +}; + +const fn blocked_ray(square: Square, direction: Direction, blockers: Bitboard) -> Bitboard { + let ray = *RAYS.get_const(square).get_const(direction); + let blockers = Bitboard(blockers.0 & ray.0); + let square = if (direction as u8) < 4 { + Bitboard::first(&blockers) + } else { + Bitboard::last(&blockers) + }; + match square { + None => ray, + Some(square) => Bitboard(ray.0 & !RAYS.get_const(square).get_const(direction).0), + } +} + +#[inline] +pub(crate) fn ray(square: Square, direction: Direction) -> Bitboard { + *RAYS.get(square).get(direction) +} +#[inline] +pub(crate) fn line(a: Square, b: Square) -> Bitboard { + *LINES.get(a).get(b) +} +#[inline] +pub(crate) fn segment(a: Square, b: Square) -> Bitboard { + *SEGMENTS.get(a).get(b) +} +#[inline] +pub(crate) fn king(square: Square) -> Bitboard { + *KING_MOVES.get(square) +} +#[inline] +pub(crate) fn knight(square: Square) -> Bitboard { + *KNIGHT_MOVES.get(square) +} +#[inline] +pub(crate) fn pawn_attack(color: Color, square: Square) -> Bitboard { + *PAWN_ATTACKS.get(color).get(square) +} + +const fn bishop_premask(square: Square) -> Bitboard { + let mut premask = 0; + loop_bishop_directions!(direction, { + premask |= RAYS.get_const(square).get_const(direction).0; + }); + premask &= !Rank::First.bitboard().0; + premask &= !Rank::Eighth.bitboard().0; + premask &= !File::A.bitboard().0; + premask &= !File::H.bitboard().0; + Bitboard(premask) +} + +const fn rook_premask(square: Square) -> Bitboard { + let rays = RAYS.get_const(square); + let mut premask = 0; + premask |= rays.get_const(Direction::East).0 & !File::H.bitboard().0; + premask |= rays.get_const(Direction::North).0 & !Rank::Eighth.bitboard().0; + premask |= rays.get_const(Direction::West).0 & !File::A.bitboard().0; + premask |= rays.get_const(Direction::South).0 & !Rank::First.bitboard().0; + Bitboard(premask) +} + +const MAGICS: (BySquare, BySquare, usize) = { + let mut len: usize = 0; + ( + by_square!( + square, + Magic { + premask: Bitboard(0), + factor: 0, + offset: 0 + }, + { + let premask = bishop_premask(square).0; + let factor = bishop_factor(square); + let mut i = usize::MAX; + let mut j = 0; + loop_subsets!(premask, blockers, { + let cur = hash(BISHOP_SHR, factor, Bitboard(blockers | !premask)); + if cur < i { + i = cur; + } + if cur > j { + j = cur; + } + }); + let offset = len as isize - i as isize; + len += j - i + 1; + Magic { + premask: Bitboard(!premask), + factor, + offset, + } + } + ), + by_square!( + square, + Magic { + premask: Bitboard(0), + factor: 0, + offset: 0 + }, + { + let premask = rook_premask(square).0; + let factor = rook_factor(square); + let mut i = usize::MAX; + let mut j = 0; + loop_subsets!(premask, blockers, { + let cur = hash(ROOK_SHR, factor, Bitboard(blockers | !premask)); + if cur < i { + i = cur; + } + if cur > j { + j = cur; + } + }); + let offset = len as isize - i as isize; + len += j - i + 1; + Magic { + premask: Bitboard(!premask), + factor, + offset, + } + } + ), + len, + ) +}; + +#[derive(Clone, Copy)] +struct Magic { + premask: Bitboard, + factor: u64, + offset: isize, +} + +#[allow(long_running_const_eval)] +const MAGIC_TABLE: [Bitboard; MAGICS.2] = { + let mut table = [Bitboard(0); MAGICS.2]; + by_square!(square, (), { + let Magic { + premask, + factor, + offset, + } = *MAGICS.0.get_const(square); + let premask = !premask.0; + loop_subsets!(premask, blockers, { + let index = (hash(BISHOP_SHR, factor, Bitboard(blockers | !premask)) as isize + offset) + as usize; + let mut res = 0; + loop_bishop_directions!(direction, { + res |= blocked_ray(square, direction, Bitboard(blockers)).0; + }); + assert!(table[index].0 == 0 || table[index].0 == res); + table[index] = Bitboard(res); + }); + }); + by_square!(square, (), { + let Magic { + premask, + factor, + offset, + } = *MAGICS.1.get_const(square); + let premask = !premask.0; + loop_subsets!(premask, blockers, { + let index = + (offset + hash(ROOK_SHR, factor, Bitboard(blockers | !premask)) as isize) as usize; + let mut res = 0; + loop_rook_directions!(direction, { + res |= blocked_ray(square, direction, Bitboard(blockers)).0; + }); + assert!(table[index].0 == 0 || table[index].0 == res); + table[index] = Bitboard(res); + }); + }); + table +}; + +#[inline] +pub(crate) fn bishop(square: Square, blockers: Bitboard) -> Bitboard { + unsafe { magic_aux(BISHOP_SHR, *(MAGICS.0).get(square), blockers) } +} + +#[inline] +pub(crate) fn rook(square: Square, blockers: Bitboard) -> Bitboard { + unsafe { magic_aux(ROOK_SHR, *(MAGICS.1).get(square), blockers) } +} + +#[inline] +unsafe fn magic_aux(shr: u8, magic: Magic, blockers: Bitboard) -> Bitboard { + let Magic { + premask, + factor, + offset, + } = magic; + debug_assert!(MAGIC_TABLE + .get( + (hash(shr, factor, blockers | premask) as isize) + .checked_add(offset) + .unwrap() as usize + ) + .is_some()); + *MAGIC_TABLE.get_unchecked( + ((hash(shr, factor, blockers | premask) as isize).unchecked_add(offset)) as usize, + ) +} + +#[inline] +const fn hash(shr: u8, factor: u64, x: Bitboard) -> usize { + (x.0.wrapping_mul(factor) >> shr) as usize +} + +#[inline] +pub(crate) fn targets(role: Role, from: Square, blockers: Bitboard) -> Bitboard { + match role { + Role::Pawn => unreachable!(), + Role::Knight => knight(from), + Role::Bishop => bishop(from, blockers), + Role::Rook => rook(from, blockers), + Role::Queen => bishop(from, blockers) | rook(from, blockers), + Role::King => king(from), } } diff --git a/src/magics.rs b/src/magics.rs index d82e531..d338b91 100644 --- a/src/magics.rs +++ b/src/magics.rs @@ -1,193 +1,7 @@ -use crate::bitboard::*; -use crate::board::*; -use crate::rays::Rays; +use crate::board::Square; -const BISHOP_SHR: u8 = 55; -const ROOK_SHR: u8 = 52; - -pub(crate) struct Magics { - bishop: BySquare, - rook: BySquare, - table: Box<[Bitboard]>, -} - -#[derive(Clone, Copy)] -struct Magic { - premask: Bitboard, - factor: u64, - offset: isize, -} - -impl Magics { - pub(crate) fn compute(rays: &Rays) -> Self { - let mut data = Vec::new(); - - let mut aux = - |shr, - factors: fn(Square) -> u64, - make_table: fn(&Rays, Square) -> (Bitboard, Vec<(Bitboard, Bitboard)>)| { - BySquare::new(|square| { - let (premask, table) = make_table(rays, square); - let factor = factors(square); - let offset = fill_table(&mut data, shr, factor, table); - Magic { - premask, - factor, - offset, - } - }) - }; - - let bishop = aux(BISHOP_SHR, bishop_factors, make_bishop_table); - let rook = aux(ROOK_SHR, rook_factors, make_rook_table); - - let mut table = Box::new_uninit_slice(data.len()); - for (i, entry) in data.into_iter().enumerate() { - table[i].write(entry); - } - - Self { - bishop, - rook, - table: unsafe { table.assume_init() }, - } - } - - #[inline] - pub(crate) fn bishop(&self, square: Square, blockers: Bitboard) -> Bitboard { - unsafe { self.get_unchecked(BISHOP_SHR, *self.bishop.get(square), blockers) } - } - - #[inline] - pub(crate) fn rook(&self, square: Square, blockers: Bitboard) -> Bitboard { - unsafe { self.get_unchecked(ROOK_SHR, *self.rook.get(square), blockers) } - } - - #[inline] - unsafe fn get_unchecked(&self, shr: u8, magic: Magic, blockers: Bitboard) -> Bitboard { - let Magic { - premask, - factor, - offset, - } = magic; - *self.table.get_unchecked( - ((hash(shr, factor, blockers | premask) as isize).unchecked_add(offset)) as usize, - ) - } -} - -fn fill_table( - data: &mut Vec, - shr: u8, - factor: u64, - table: Vec<(Bitboard, Bitboard)>, -) -> isize { - let offset = data.len() as isize - - table - .iter() - .map(|(x, _)| hash(shr, factor, *x) as isize) - .min() - .unwrap(); - for (x, y) in &table { - let i = (hash(shr, factor, *x) as isize + offset) as usize; - while data.len() <= i { - data.push(Bitboard::new()); - } - if data[i] != Bitboard::new() && data[i] != *y { - panic!(); - } - data[i] = *y; - } - offset -} - -fn make_bishop_table(rays: &Rays, square: Square) -> (Bitboard, Vec<(Bitboard, Bitboard)>) { - let mut premask = Bitboard::new(); - for direction in [ - Direction::NorthWest, - Direction::SouthWest, - Direction::SouthEast, - Direction::NorthEast, - ] { - premask |= rays.ray(square, direction); - } - premask &= !Rank::First.bitboard(); - premask &= !Rank::Eighth.bitboard(); - premask &= !File::A.bitboard(); - premask &= !File::H.bitboard(); - - let mut table = make_table(premask, |blockers| { - let mut res = Bitboard::new(); - for direction in [ - Direction::NorthWest, - Direction::SouthWest, - Direction::SouthEast, - Direction::NorthEast, - ] { - res |= rays.blocked(square, direction, blockers); - } - res - }); - - premask = !premask; - for (x, _) in &mut table { - *x |= premask; - } - - (premask, table) -} - -fn make_rook_table(rays: &Rays, square: Square) -> (Bitboard, Vec<(Bitboard, Bitboard)>) { - let mut premask = Bitboard::new(); - premask |= rays.ray(square, Direction::North) & !Rank::Eighth.bitboard(); - premask |= rays.ray(square, Direction::West) & !File::A.bitboard(); - premask |= rays.ray(square, Direction::South) & !Rank::First.bitboard(); - premask |= rays.ray(square, Direction::East) & !File::H.bitboard(); - - let mut table = make_table(premask, |blockers| { - let mut res = Bitboard::new(); - for direction in [ - Direction::North, - Direction::West, - Direction::South, - Direction::East, - ] { - res |= rays.blocked(square, direction, blockers); - } - res - }); - - premask = !premask; - for (x, _) in &mut table { - *x |= premask; - } - - (premask, table) -} - -fn make_table(premask: Bitboard, f: F) -> Vec<(Bitboard, T)> -where - F: Fn(Bitboard) -> T, -{ - let mut res = Vec::new(); - let mut subset: u64 = 0; - loop { - subset = subset.wrapping_sub(premask.0) & premask.0; - let x = Bitboard(subset); - let y = f(x); - res.push((x, y)); - if subset == 0 { - break; - } - } - res -} - -fn hash(shr: u8, factor: u64, x: Bitboard) -> usize { - (x.0.wrapping_mul(factor) >> shr) as usize -} - -fn bishop_factors(square: Square) -> u64 { +pub(crate) const BISHOP_SHR: u8 = 55; +pub(crate) const fn bishop_factor(square: Square) -> u64 { match square { Square::A1 => 0x0000404040404040, Square::B1 => 0x0040C100081000E8, @@ -256,7 +70,8 @@ fn bishop_factors(square: Square) -> u64 { } } -fn rook_factors(square: Square) -> u64 { +pub(crate) const ROOK_SHR: u8 = 52; +pub(crate) const fn rook_factor(square: Square) -> u64 { match square { Square::A1 => 0x002000A28110000C, Square::B1 => 0x0018000C01060001, diff --git a/src/position.rs b/src/position.rs index 590c975..6132df7 100644 --- a/src/position.rs +++ b/src/position.rs @@ -3,7 +3,7 @@ use crate::array_vec::*; use crate::bitboard::*; use crate::board::*; -use crate::lookup::*; +use crate::lookup; use crate::san::*; use crate::setup::*; use crate::uci::*; @@ -55,10 +55,7 @@ use std::iter::FusedIterator; /// requirement is to be efficient while respecting the [`Ord`] trait protocol. It should not be /// considered stable. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Position { - setup: Setup, - lookup: InitialisedLookup, -} +pub struct Position(Setup); const MAX_LEGAL_MOVES: usize = 218; @@ -68,18 +65,15 @@ impl Position { /// i.e. `rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -` #[inline] pub fn new() -> Self { - Self { - setup: Setup { - w: Bitboard(0x000000000000FFFF), - p_b_q: Bitboard(0x2CFF00000000FF2C), - n_b_k: Bitboard(0x7600000000000076), - r_q_k: Bitboard(0x9900000000000099), - turn: Color::White, - castling_rights: CastlingRights::full(), - en_passant: OptionSquare::None, - }, - lookup: InitialisedLookup::init(), - } + Self(Setup { + w: Bitboard(0x000000000000FFFF), + p_b_q: Bitboard(0x2CFF00000000FF2C), + n_b_k: Bitboard(0x7600000000000076), + r_q_k: Bitboard(0x9900000000000099), + turn: Color::White, + castling_rights: CastlingRights::full(), + en_passant: OptionSquare::None, + }) } /// Tries to read a valid position from a text record. @@ -160,25 +154,25 @@ impl Position { /// responsibility to rule out the legality of en passant before calling this function. #[inline] pub fn remove_en_passant_target_square(&mut self) { - self.setup.en_passant = OptionSquare::None; + self.0.en_passant = OptionSquare::None; } /// Returns the occupancy of a square. #[inline] pub fn get(&self, square: Square) -> Option { - self.setup.get(square) + self.0.get(square) } /// Returns the color whose turn it is to play. #[inline] pub fn turn(&self) -> Color { - self.setup.turn() + self.0.turn() } /// Returns `true` if castling is available for the given color and side. #[inline] pub fn castling_rights(&self, color: Color, side: CastlingSide) -> bool { - self.setup.castling_rights(color, side) + self.0.castling_rights(color, side) } /// Returns the en passant target square if it exists. @@ -187,26 +181,26 @@ impl Position { /// legal or even pseudo-legal. #[inline] pub fn en_passant_target_square(&self) -> Option { - self.setup.en_passant_target_square() + self.0.en_passant_target_square() } /// Discards the castling rights for the given color and side. #[inline] pub fn remove_castling_rights(&mut self, color: Color, side: CastlingSide) { - self.setup.set_castling_rights(color, side, false); + self.0.set_castling_rights(color, side, false); } /// Borrows the position as a [`Setup`]. #[inline] pub fn as_setup(&self) -> &Setup { - &self.setup + &self.0 } /// Converts a position to a [`Setup`], allowing to edit the position without enforcing its /// legality. #[inline] pub fn into_setup(self) -> Setup { - self.setup + self.0 } /// Tries to pass the turn to the other color, failing if it would leave the king in check. @@ -214,8 +208,7 @@ impl Position { /// When possible, this inverts the color to play and removes the en passant square if it /// exists. pub fn pass(&self) -> Option { - let d = self.lookup; - let setup = &self.setup; + let setup = &self.0; let blockers = setup.p_b_q | setup.n_b_k | setup.r_q_k; let k = setup.n_b_k & setup.r_q_k; let q = setup.p_b_q & setup.r_q_k; @@ -229,27 +222,23 @@ impl Position { }; let king_square = (us & k).next().unwrap(); let checkers = them - & (d.pawn_attack(setup.turn, king_square) & p - | d.knight(king_square) & n - | d.bishop(king_square, blockers) & (q | b) - | d.rook(king_square, blockers) & (q | r)); - checkers.is_empty().then(|| Self { - setup: Setup { + & (lookup::pawn_attack(setup.turn, king_square) & p + | lookup::knight(king_square) & n + | lookup::bishop(king_square, blockers) & (q | b) + | lookup::rook(king_square, blockers) & (q | r)); + checkers.is_empty().then(|| { + Self(Setup { turn: !setup.turn, en_passant: OptionSquare::None, ..setup.clone() - }, - lookup: d, + }) }) } /// Returns the mirror image of the position (see [`Setup::mirror`]). #[inline] pub fn mirror(&self) -> Self { - Self { - setup: self.setup.mirror(), - lookup: self.lookup, - } + Self(self.0.mirror()) } /// Returns the number of possible chess games for a given number of moves. @@ -329,7 +318,7 @@ impl Position { to, promotion, } = uci; - let role = self.setup.get_role(from).ok_or(InvalidUciMove::Illegal)?; + let role = self.0.get_role(from).ok_or(InvalidUciMove::Illegal)?; #[inline] fn aux<'l, const ROLE: u8>( position: &'l Position, @@ -536,9 +525,7 @@ impl<'l> Move<'l> { #[inline] pub fn is_capture(self) -> bool { self.raw.kind == MoveType::EnPassant - || !((self.position.setup.p_b_q - | self.position.setup.n_b_k - | self.position.setup.r_q_k) + || !((self.position.0.p_b_q | self.position.0.n_b_k | self.position.0.r_q_k) & self.to().bitboard()) .is_empty() } @@ -548,7 +535,7 @@ impl<'l> Move<'l> { pub fn captured(self) -> Option { match self.raw.kind { MoveType::EnPassant => Some(Role::Pawn), - _ => self.position.setup.get_role(self.raw.to), + _ => self.position.0.get_role(self.raw.to), } } @@ -918,10 +905,7 @@ impl<'l> Visitor for Moves<'l> { impl Position { /// SAFETY: The position must be valid. pub(crate) unsafe fn from_setup(setup: Setup) -> Self { - Self { - setup, - lookup: InitialisedLookup::init(), - } + Self(setup) } fn generate_moves(&self, visitor: &mut T) @@ -939,8 +923,7 @@ impl Position { turn, en_passant, castling_rights, - } = self.setup; - let d = self.lookup; + } = self.0; let blockers = p_b_q | n_b_k | r_q_k; let (us, them) = match turn { @@ -970,10 +953,10 @@ impl Position { let forward = turn.forward(); - let x = d.bishop(king_square, blockers); - let y = d.rook(king_square, blockers); - let checkers = d.pawn_attack(turn, king_square) & theirs.pawn() - | d.knight(king_square) & theirs.knight() + let x = lookup::bishop(king_square, blockers); + let y = lookup::rook(king_square, blockers); + let checkers = lookup::pawn_attack(turn, king_square) & theirs.pawn() + | lookup::knight(king_square) & theirs.knight() | x & theirs.bishop() | y & theirs.rook(); @@ -982,10 +965,10 @@ impl Position { let blockers = blockers ^ ours.king(); theirs .king() - .map(|sq| d.king(sq)) - .chain(theirs.bishop().map(|sq| d.bishop(sq, blockers))) - .chain(theirs.rook().map(|sq| d.rook(sq, blockers))) - .chain(theirs.knight().map(|sq| d.knight(sq))) + .map(|sq| lookup::king(sq)) + .chain(theirs.bishop().map(|sq| lookup::bishop(sq, blockers))) + .chain(theirs.rook().map(|sq| lookup::rook(sq, blockers))) + .chain(theirs.knight().map(|sq| lookup::knight(sq))) .chain(std::iter::once( theirs.pawn().trans(!forward).trans(Direction::East), )) @@ -996,7 +979,7 @@ impl Position { }; // king moves visitor.moves( - (global_mask_to & d.king(king_square) & !us & !attacked).map(|to| RawMove { + (global_mask_to & lookup::king(king_square) & !us & !attacked).map(|to| RawMove { kind: MoveType::KingMove, from: king_square, to, @@ -1054,17 +1037,17 @@ impl Position { } let blockers_x_ray = blockers & !(x | y); - let pinned_diagonally = (d.bishop(king_square, blockers_x_ray) & theirs.bishop()) - .map(|sq| d.segment(king_square, sq)) + let pinned_diagonally = (lookup::bishop(king_square, blockers_x_ray) & theirs.bishop()) + .map(|sq| lookup::segment(king_square, sq)) .reduce_or(); - let pinned_horizontally = (d.rook(king_square, blockers_x_ray) & theirs.rook()) - .map(|sq| d.segment(king_square, sq)) + let pinned_horizontally = (lookup::rook(king_square, blockers_x_ray) & theirs.rook()) + .map(|sq| lookup::segment(king_square, sq)) .reduce_or(); let pinned = pinned_diagonally | pinned_horizontally; let checker = checkers.first(); let block_check = checker - .map(|checker| d.segment(king_square, checker)) + .map(|checker| lookup::segment(king_square, checker)) .unwrap_or(Bitboard(!0)); let target_mask = global_mask_to & block_check; @@ -1107,7 +1090,7 @@ impl Position { // pawn attacks kingside { let targets = - (global_mask_from & ours.pawn() & (!pinned | d.ray(king_square, kside))) + (global_mask_from & ours.pawn() & (!pinned | lookup::ray(king_square, kside))) .trans(kside) & them & target_mask; @@ -1129,7 +1112,7 @@ impl Position { // pawn attacks queenside { let targets = - (global_mask_from & ours.pawn() & (!pinned | d.ray(king_square, qside))) + (global_mask_from & ours.pawn() & (!pinned | lookup::ray(king_square, qside))) .trans(qside) & them & target_mask; @@ -1169,23 +1152,27 @@ impl Position { if block_check.contains(to) || checker.is_none_or(|checker| checker == capture_square) { - let candidates = d.pawn_attack(!turn, to) & ours.pawn(); + let candidates = lookup::pawn_attack(!turn, to) & ours.pawn(); let blockers = blockers ^ capture_square.bitboard(); let pinned = pinned - | (d.rook(king_square, blockers & !(d.rook(king_square, blockers))) - & theirs.rook()) - .map(|sq| d.segment(king_square, sq)) + | (lookup::rook( + king_square, + blockers & !(lookup::rook(king_square, blockers)), + ) & theirs.rook()) + .map(|sq| lookup::segment(king_square, sq)) .reduce_or(); - (global_mask_from & candidates & (!pinned | d.segment(king_square, to))) - .for_each(|from| { - visitor.en_passant_is_legal(); - visitor.moves(std::iter::once(RawMove { - kind: MoveType::EnPassant, - from, - to, - role: Role::Pawn, - })) - }) + (global_mask_from + & candidates + & (!pinned | lookup::segment(king_square, to))) + .for_each(|from| { + visitor.en_passant_is_legal(); + visitor.moves(std::iter::once(RawMove { + kind: MoveType::EnPassant, + from, + to, + role: Role::Pawn, + })) + }) } } } @@ -1196,11 +1183,13 @@ impl Position { let aux = |visitor: &mut T, role| { for from in global_mask_from & *ours.get(role) & !pinned { visitor.moves( - (d.targets(role, from, blockers) & !us & target_mask).map(|to| RawMove { - kind: MoveType::PieceMove, - from, - to, - role, + (lookup::targets(role, from, blockers) & !us & target_mask).map(|to| { + RawMove { + kind: MoveType::PieceMove, + from, + to, + role, + } }), ) } @@ -1229,14 +1218,14 @@ impl Position { let aux = |visitor: &mut T, role, mask| { for from in global_mask_from & *ours.get(role) & mask { visitor.moves( - (global_mask_to & !us & pinned & d.line(king_square, from)).map(|to| { - RawMove { + (global_mask_to & !us & pinned & lookup::line(king_square, from)).map( + |to| RawMove { kind: MoveType::PieceMove, from, to, role, - } - }), + }, + ), ) } }; @@ -1254,7 +1243,7 @@ impl Position { #[inline] unsafe fn play_unchecked(&mut self, m: RawMove) { - let Self { setup, .. } = self; + let Self(setup) = self; setup.en_passant = OptionSquare::None; diff --git a/src/rays.rs b/src/rays.rs deleted file mode 100644 index d758ffc..0000000 --- a/src/rays.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::bitboard::*; -use crate::board::*; - -pub(crate) struct Rays(BySquare>); - -impl Rays { - pub(crate) fn new() -> Self { - Self(BySquare::new(|square| { - ByDirection::new(|direction| { - let mut square = square; - let mut res = Bitboard::new(); - while let Some(x) = square.trans(direction) { - square = x; - res |= square.bitboard(); - } - res - }) - })) - } - - #[inline] - pub(crate) fn ray(&self, square: Square, direction: Direction) -> Bitboard { - *self.0.get(square).get(direction) - } - - pub(crate) fn blocked( - &self, - square: Square, - direction: Direction, - blockers: Bitboard, - ) -> Bitboard { - let blockers = blockers & *self.0.get(square).get(direction); - let square2 = if (direction as u8) < 4 { - blockers.first() - } else { - blockers.last() - }; - *self.0.get(square).get(direction) - & !match square2 { - Some(square2) => *self.0.get(square2).get(direction), - None => Bitboard::new(), - } - } -} diff --git a/src/setup.rs b/src/setup.rs index b5cc44a..e0b0899 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -4,7 +4,7 @@ use crate::bitboard::*; use crate::board::*; -use crate::lookup::*; +use crate::lookup; use crate::position::*; /// **A builder type for chess positions.** @@ -253,7 +253,6 @@ impl Setup { debug_assert!((self.p_b_q & self.n_b_k & self.r_q_k).is_empty()); let mut reasons = IllegalPositionReasons::new(); - let d = InitialisedLookup::init(); let blockers = self.p_b_q | self.n_b_k | self.r_q_k; let pieces = self.bitboards(); @@ -274,13 +273,13 @@ impl Setup { if pieces.get(!self.turn).king().any(|enemy_king| { let pieces = pieces.get(self.turn); - !(d.king(enemy_king) & *pieces.get(Role::King) - | d.bishop(enemy_king, blockers) + !(lookup::king(enemy_king) & *pieces.get(Role::King) + | lookup::bishop(enemy_king, blockers) & (*pieces.get(Role::Queen) | *pieces.get(Role::Bishop)) - | d.rook(enemy_king, blockers) + | lookup::rook(enemy_king, blockers) & (*pieces.get(Role::Queen) | *pieces.get(Role::Rook)) - | d.knight(enemy_king) & *pieces.get(Role::Knight) - | d.pawn_attack(!self.turn, enemy_king) & *pieces.get(Role::Pawn)) + | lookup::knight(enemy_king) & *pieces.get(Role::Knight) + | lookup::pawn_attack(!self.turn, enemy_king) & *pieces.get(Role::Pawn)) .is_empty() }) { reasons.add(IllegalPositionReason::HangingKing); @@ -353,7 +352,7 @@ impl Setup { .king() .first() .is_some_and(|king_square| { - !(d.bishop(king_square, blockers) + !(lookup::bishop(king_square, blockers) & (pieces.get(!self.turn).queen() | pieces.get(!self.turn).bishop())) .is_empty() })