diff --git a/.drone.yml b/.drone.yml index 7037529..a6c1ce5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,9 +4,9 @@ type: exec name: abacus checks steps: -- name: flake check +- name: pre commit check commands: - - nix flake check + - nix develop . --command pre-commit run --all - name: package check commands: diff --git a/Cargo.toml b/Cargo.toml index 52a2217..756e025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,19 @@ name = "seer" version = "0.1.0" edition = "2021" +build = "src/build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] random = "0.12.2" + +[build-dependencies] +random = "0.12.2" + +# Optimize build scripts to shorten compile times. +[profile.dev.build-override] +opt-level = 3 + +[profile.release.build-override] +opt-level = 3 diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 91c3122..d5ca8eb 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -19,6 +19,7 @@ impl Bitboard { pub const ALL: Bitboard = Bitboard(u64::MAX); /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. + #[allow(clippy::unusual_byte_groupings)] pub const RANKS: [Self; 8] = [ Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010), @@ -31,6 +32,7 @@ impl Bitboard { ]; /// Array of bitboards representing the eight files, in order from file A to file H. + #[allow(clippy::unusual_byte_groupings)] pub const FILES: [Self; 8] = [ Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111), Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000), diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 8ed8764..d727fd8 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -26,6 +26,10 @@ impl CastleRights { } /// Convert from a castle rights index into a [CastleRights] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [CastleRights::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/color.rs b/src/board/color.rs index 09d017b..ca87ade 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -11,6 +11,13 @@ impl Color { /// The number of [Color] variants. pub const NUM_VARIANTS: usize = 2; + const ALL: [Self; Self::NUM_VARIANTS] = [Self::White, Self::Black]; + + /// Iterate over all colors in order. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + /// Convert from a piece index into a [Color] type. #[inline(always)] pub fn from_index(index: usize) -> Self { @@ -20,6 +27,10 @@ impl Color { } /// Convert from a piece index into a [Color] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Color::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/file.rs b/src/board/file.rs index 55a138b..4c84f20 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -43,6 +43,10 @@ impl File { } /// Convert from a file index into a [File] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [File::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/piece.rs b/src/board/piece.rs index 17ae913..58f989a 100644 --- a/src/board/piece.rs +++ b/src/board/piece.rs @@ -36,6 +36,10 @@ impl Piece { } /// Convert from a piece index into a [Piece] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Piece::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/rank.rs b/src/board/rank.rs index 70fad46..a0b04d3 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -43,6 +43,10 @@ impl Rank { } /// Convert from a rank index into a [Rank] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Rank::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/square.rs b/src/board/square.rs index b5a033c..9ffa824 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -18,7 +18,7 @@ pub enum Square { impl std::fmt::Display for Square { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", format!("{:?}", self)) + write!(f, "{:?}", self) } } @@ -59,6 +59,10 @@ impl Square { } /// Convert from a square index into a [Square] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Square::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) @@ -109,6 +113,7 @@ impl std::ops::Shl for Square { #[inline(always)] fn shl(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] Square::from_index(self as usize + rhs) } } @@ -119,6 +124,7 @@ impl std::ops::Shr for Square { #[inline(always)] fn shr(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] Square::from_index(self as usize - rhs) } } diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..10d7a94 --- /dev/null +++ b/src/build.rs @@ -0,0 +1,143 @@ +use std::io::{Result, Write}; + +pub mod board; +pub mod movegen; +pub mod utils; + +use crate::{ + board::{Bitboard, Color, File, Square}, + movegen::{ + naive::{ + king::king_moves, + knight::knight_moves, + pawn::{pawn_captures, pawn_moves}, + }, + wizardry::generation::{generate_bishop_magics, generate_rook_magics}, + Magic, + }, +}; + +fn print_magics(out: &mut dyn Write, var_name: &str, magics: &[Magic]) -> Result<()> { + writeln!(out, "static {}: [Magic; {}] = [", var_name, magics.len())?; + for magic in magics.iter() { + writeln!( + out, + " Magic{{magic: {}, offset: {}, mask: Bitboard({}), shift: {},}},", + magic.magic, magic.offset, magic.mask.0, magic.shift + )?; + } + writeln!(out, "];")?; + Ok(()) +} + +fn print_boards(out: &mut dyn Write, var_name: &str, boards: &[Bitboard]) -> Result<()> { + writeln!(out, "static {}: [Bitboard; {}] = [", var_name, boards.len())?; + for board in boards.iter().cloned() { + writeln!(out, " Bitboard({}),", board.0)?; + } + writeln!(out, "];")?; + Ok(()) +} + +fn print_double_sided_boards( + out: &mut dyn Write, + var_name: &str, + white_boards: &[Bitboard], + black_boards: &[Bitboard], +) -> Result<()> { + assert_eq!(white_boards.len(), black_boards.len()); + writeln!( + out, + "static {}: [[Bitboard; {}]; 2] = [", + var_name, + white_boards.len() + )?; + for color in Color::iter() { + let boards = if color == Color::White { + white_boards + } else { + black_boards + }; + writeln!(out, " [")?; + for square in Square::iter() { + writeln!(out, " Bitboard({}),", boards[square.index()].0)?; + } + writeln!(out, " ],")?; + } + writeln!(out, "];")?; + Ok(()) +} + +#[allow(clippy::redundant_clone)] +fn main() -> Result<()> { + // FIXME: rerun-if-changed directives + + let out_dir = std::env::var_os("OUT_DIR").unwrap(); + let magic_path = std::path::Path::new(&out_dir).join("magic_tables.rs"); + let mut out = std::fs::File::create(&magic_path).unwrap(); + + let rng = random::default().seed([12, 27]); + + { + let (magics, moves) = generate_bishop_magics(&mut rng.clone()); + print_magics(&mut out, "BISHOP_MAGICS", &magics)?; + print_boards(&mut out, "BISHOP_MOVES", &moves)?; + } + + { + let (magics, moves) = generate_rook_magics(&mut rng.clone()); + print_magics(&mut out, "ROOK_MAGICS", &magics)?; + print_boards(&mut out, "ROOK_MOVES", &moves)?; + } + + { + let moves: Vec<_> = Square::iter().map(knight_moves).collect(); + print_boards(&mut out, "KNIGHT_MOVES", &moves)?; + } + + { + let white_moves: Vec<_> = Square::iter() + .map(|square| pawn_moves(Color::White, square, Bitboard::EMPTY)) + .collect(); + let black_moves: Vec<_> = Square::iter() + .map(|square| pawn_moves(Color::Black, square, Bitboard::EMPTY)) + .collect(); + print_double_sided_boards(&mut out, "PAWN_MOVES", &white_moves, &black_moves)?; + let white_attacks: Vec<_> = Square::iter() + .map(|square| pawn_captures(Color::White, square)) + .collect(); + let black_attacks: Vec<_> = Square::iter() + .map(|square| pawn_captures(Color::Black, square)) + .collect(); + print_double_sided_boards(&mut out, "PAWN_ATTACKS", &white_attacks, &black_attacks)?; + } + + { + let moves: Vec<_> = Square::iter().map(king_moves).collect(); + print_boards(&mut out, "KING_MOVES", &moves)?; + let king_blockers: Vec<_> = Color::iter() + .map(|color| { + Square::new(File::F, color.first_rank()) | Square::new(File::G, color.first_rank()) + }) + .collect(); + let queen_blockers: Vec<_> = Color::iter() + .map(|color| { + Square::new(File::B, color.first_rank()) + | Square::new(File::C, color.first_rank()) + | Square::new(File::D, color.first_rank()) + }) + .collect(); + print_boards(&mut out, "KING_SIDE_CASTLE_BLOCKERS", &king_blockers)?; + print_boards(&mut out, "QUEEN_SIDE_CASTLE_BLOCKERS", &queen_blockers)?; + } + + // Include the generated files now that the build script has run. + println!("cargo:rustc-cfg=generated_boards"); + + // Run the build script only if something in move generation might have changed. + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=movegen/naive/"); + println!("cargo:rerun-if-changed=movegen/wizardry/"); + + Ok(()) +} diff --git a/src/movegen/magic/magic.rs b/src/movegen/magic/magic.rs deleted file mode 100644 index 0f328d5..0000000 --- a/src/movegen/magic/magic.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::board::Bitboard; - -/// A type representing the magic board indexing a given [Square]. -#[allow(unused)] // FIXME: remove once used -pub struct Magic { - /// Magic number. - magic: u64, - /// Base offset into the magic square table. - offset: usize, - /// Mask to apply to the blocker board before applying the magic. - mask: Bitboard, - /// Length of the resulting mask after applying the magic. - shift: u8, -} - -impl Magic { - #[allow(unused)] // FIXME: remove once used - pub fn get_index(&self, blockers: Bitboard) -> usize { - let relevant_occupancy = (blockers & self.mask).0; - let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; - base_index + self.offset - } -} diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs index 068c6e7..b31abeb 100644 --- a/src/movegen/magic/mod.rs +++ b/src/movegen/magic/mod.rs @@ -1,2 +1,26 @@ -pub mod magic; -pub use magic::*; +use crate::board::Bitboard; + +/// A type representing the magic board indexing a given [Square]. +pub struct Magic { + /// Magic number. + pub(crate) magic: u64, + /// Base offset into the magic square table. + pub(crate) offset: usize, + /// Mask to apply to the blocker board before applying the magic. + pub(crate) mask: Bitboard, + /// Length of the resulting mask after applying the magic. + pub(crate) shift: u8, +} + +impl Magic { + pub fn get_index(&self, blockers: Bitboard) -> usize { + let relevant_occupancy = (blockers & self.mask).0; + let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; + base_index + self.offset + } +} + +#[cfg(generated_boards)] +mod moves; +#[cfg(generated_boards)] +pub use moves::*; diff --git a/src/movegen/magic/moves.rs b/src/movegen/magic/moves.rs new file mode 100644 index 0000000..2901b28 --- /dev/null +++ b/src/movegen/magic/moves.rs @@ -0,0 +1,71 @@ +use super::Magic; +use crate::board::{Bitboard, Color, Square}; + +include!(concat!(env!("OUT_DIR"), "/magic_tables.rs")); + +pub fn quiet_pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + // If there is a piece in front of the pawn, it can't advance + if !(color.backward_direction().move_board(blockers) & square).is_empty() { + return Bitboard::EMPTY; + } + // SAFETY: we know the values are in-bounds + unsafe { + *PAWN_MOVES + .get_unchecked(color.index()) + .get_unchecked(square.index()) + } +} + +pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + // SAFETY: we know the values are in-bounds + let attacks = unsafe { + *PAWN_ATTACKS + .get_unchecked(color.index()) + .get_unchecked(square.index()) + }; + quiet_pawn_moves(color, square, blockers) | attacks +} + +pub fn knight_moves(square: Square) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { *KNIGHT_MOVES.get_unchecked(square.index()) } +} + +pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { + let index = BISHOP_MAGICS + .get_unchecked(square.index()) + .get_index(blockers); + *BISHOP_MOVES.get_unchecked(index) + } +} + +pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { + let index = ROOK_MAGICS + .get_unchecked(square.index()) + .get_index(blockers); + *ROOK_MOVES.get_unchecked(index) + } +} + +pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard { + bishop_moves(square, blockers) | rook_moves(square, blockers) +} + +pub fn king_moves(square: Square) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { *KING_MOVES.get_unchecked(square.index()) } +} + +pub fn king_side_castle_blockers(color: Color) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { *KING_SIDE_CASTLE_BLOCKERS.get_unchecked(color.index()) } +} + +pub fn queen_side_castle_blockers(color: Color) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { *QUEEN_SIDE_CASTLE_BLOCKERS.get_unchecked(color.index()) } +} diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 3d22eb0..9ddbf36 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -2,12 +2,8 @@ pub mod magic; pub use magic::*; -// Move generation implementation details -mod bishop; -mod king; -mod knight; -mod pawn; -mod rook; +// Naive move generation +pub mod naive; // Magic bitboard generation -mod wizardry; +pub(crate) mod wizardry; diff --git a/src/movegen/bishop.rs b/src/movegen/naive/bishop.rs similarity index 99% rename from src/movegen/bishop.rs rename to src/movegen/naive/bishop.rs index 0fe0247..0806077 100644 --- a/src/movegen/bishop.rs +++ b/src/movegen/naive/bishop.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Direction, Square}; -#[allow(unused)] pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { Direction::iter_bishop() .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) diff --git a/src/movegen/king.rs b/src/movegen/naive/king.rs similarity index 99% rename from src/movegen/king.rs rename to src/movegen/naive/king.rs index 932b4f4..6e98df7 100644 --- a/src/movegen/king.rs +++ b/src/movegen/naive/king.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square}; // No castling moves included -#[allow(unused)] pub fn king_moves(square: Square) -> Bitboard { let board = square.into_bitboard(); @@ -10,7 +9,6 @@ pub fn king_moves(square: Square) -> Bitboard { .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) } -#[allow(unused)] pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard { let rank = color.first_rank(); diff --git a/src/movegen/knight.rs b/src/movegen/naive/knight.rs similarity index 99% rename from src/movegen/knight.rs rename to src/movegen/naive/knight.rs index 4783bde..f850d71 100644 --- a/src/movegen/knight.rs +++ b/src/movegen/naive/knight.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Direction, Square}; -#[allow(unused)] pub fn knight_moves(square: Square) -> Bitboard { let board = square.into_bitboard(); diff --git a/src/movegen/naive/mod.rs b/src/movegen/naive/mod.rs new file mode 100644 index 0000000..f1bbe3e --- /dev/null +++ b/src/movegen/naive/mod.rs @@ -0,0 +1,5 @@ +pub mod bishop; +pub mod king; +pub mod knight; +pub mod pawn; +pub mod rook; diff --git a/src/movegen/pawn.rs b/src/movegen/naive/pawn.rs similarity index 98% rename from src/movegen/pawn.rs rename to src/movegen/naive/pawn.rs index 5c929fa..55b5bf6 100644 --- a/src/movegen/pawn.rs +++ b/src/movegen/naive/pawn.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Color, Direction, Rank, Square}; -#[allow(unused)] pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { return Bitboard::EMPTY; @@ -22,7 +21,6 @@ pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard } } -#[allow(unused)] pub fn pawn_captures(color: Color, square: Square) -> Bitboard { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { return Bitboard::EMPTY; @@ -38,7 +36,6 @@ pub fn pawn_captures(color: Color, square: Square) -> Bitboard { attack_west | attack_east } -#[allow(unused)] pub fn en_passant_origins(square: Square) -> Bitboard { let board = square.into_bitboard(); diff --git a/src/movegen/rook.rs b/src/movegen/naive/rook.rs similarity index 99% rename from src/movegen/rook.rs rename to src/movegen/naive/rook.rs index 31fd7d8..0b06cef 100644 --- a/src/movegen/rook.rs +++ b/src/movegen/naive/rook.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Direction, Square}; -#[allow(unused)] pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { Direction::iter_rook() .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs new file mode 100644 index 0000000..173eef0 --- /dev/null +++ b/src/movegen/wizardry/generation.rs @@ -0,0 +1,67 @@ +use crate::board::{Bitboard, Square}; +use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves}; +use crate::movegen::Magic; + +use super::mask::{generate_bishop_mask, generate_rook_mask}; + +type MagicGenerationType = (Vec, Vec); + +#[allow(unused)] // FIXME: remove when used +pub fn generate_bishop_magics(rng: &mut dyn random::Source) -> MagicGenerationType { + generate_magics(rng, generate_bishop_mask, bishop_moves) +} + +#[allow(unused)] // FIXME: remove when used +pub fn generate_rook_magics(rng: &mut dyn random::Source) -> MagicGenerationType { + generate_magics(rng, generate_rook_mask, rook_moves) +} + +fn generate_magics( + rng: &mut dyn random::Source, + mask_fn: impl Fn(Square) -> Bitboard, + moves_fn: impl Fn(Square, Bitboard) -> Bitboard, +) -> MagicGenerationType { + let mut magics = Vec::new(); + let mut boards = Vec::new(); + + for square in Square::iter() { + let mask = mask_fn(square); + let mut candidate: Magic; + + let occupancy_to_moves: Vec<_> = mask + .iter_power_set() + .map(|occupancy| (occupancy, moves_fn(square, occupancy))) + .collect(); + + 'candidate_search: loop { + candidate = Magic { + magic: magic_candidate(rng), + offset: 0, + mask, + shift: (64 - mask.count()) as u8, + }; + let mut candidate_moves = vec![Bitboard::EMPTY; occupancy_to_moves.len()]; + + for (occupancy, moves) in occupancy_to_moves.iter().cloned() { + let index = candidate.get_index(occupancy); + // Non-constructive collision, try with another candidate + if candidate_moves[index] != Bitboard::EMPTY && candidate_moves[index] != moves { + continue 'candidate_search; + } + candidate_moves[index] = moves; + } + + // We have filled all candidate boards, record the correct offset and add the moves + candidate.offset = boards.len(); + boards.append(&mut candidate_moves); + magics.push(candidate); + break; + } + } + + (magics, boards) +} + +fn magic_candidate(rng: &mut dyn random::Source) -> u64 { + rng.read_u64() & rng.read_u64() & rng.read_u64() +} diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs index 13d7cd6..eed93a0 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -1,8 +1,6 @@ use crate::board::{Bitboard, File, Rank, Square}; -use crate::movegen::bishop::bishop_moves; -use crate::movegen::rook::rook_moves; +use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves}; -#[allow(unused)] // FIXME: remove once used pub fn generate_bishop_mask(square: Square) -> Bitboard { let rays = bishop_moves(square, Bitboard::EMPTY); @@ -14,7 +12,6 @@ pub fn generate_bishop_mask(square: Square) -> Bitboard { rays - mask } -#[allow(unused)] // FIXME: remove once used pub fn generate_rook_mask(square: Square) -> Bitboard { let rays = rook_moves(square, Bitboard::EMPTY); diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index f6053c6..dfd732d 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1 +1,2 @@ +pub(crate) mod generation; mod mask;