Compare commits

...

36 commits

Author SHA1 Message Date
Bruno BELANYI 1a7763e1f4 Add 'ChessBoard::at'
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 12:38:45 +02:00
Bruno BELANYI 87258c1084 Add 'ChessBoard::checkers' 2022-07-30 12:38:45 +02:00
Bruno BELANYI 168d9e69ab Test for opponent being in check during validation 2022-07-30 12:38:45 +02:00
Bruno BELANYI f13d44b8e3 Add 'Bitboard::has_more_than_one' 2022-07-30 12:38:45 +02:00
Bruno BELANYI cb3b1ee745 Check kings' position in 'ChessBoard::is_valid' 2022-07-30 12:38:45 +02:00
Bruno BELANYI 934597d63d Add 'Bitboard::try_into_square' 2022-07-30 12:38:45 +02:00
Bruno BELANYI 024a41fa18 Use unchecked conversion in 'BitboardIterator 2022-07-30 12:38:45 +02:00
Bruno BELANYI eefa707c07 Silence useless clippy warnings
Those warnings are either explicitly accounted for in the code, or make
the code look worse if fixed.
2022-07-30 12:38:45 +02:00
Bruno BELANYI 2e410ba104 Make use of assignment operators for 'Bitboard' 2022-07-30 12:38:45 +02:00
Bruno BELANYI a6e8ac06b6 Add dummy 'magic::moves' module for build script
Since I'm am doing something quite *weird*, by writing a build script
that makes use of the module I am generating code for, I need to ensure
that it compiles both without the generated code, and with it.

So this dummy implementation of the module ensures that the code will
keep compiling in both cases, and panics if any of the functions that
depend on generated code are called during the build script execution.
2022-07-30 12:38:45 +02:00
Bruno BELANYI 99129e453c Make use of 'ChessBoard::default' in tests 2022-07-30 12:38:45 +02:00
Bruno BELANYI 9cddda7478 Implement 'Default' for 'ChessBoard' 2022-07-30 12:38:45 +02:00
Bruno BELANYI b5bb613b5e Test 'ChessBoard::{do,undo}_move' machinery 2022-07-30 12:38:45 +02:00
Bruno BELANYI 0cefb05017 Add FEN board parsing 2022-07-30 12:38:45 +02:00
Bruno BELANYI fb0e289fa0 Add 'ChessBoard::is_valid' 2022-07-30 12:38:45 +02:00
Bruno BELANYI 384f361da2 Add 'ChessBoard' 2022-07-30 12:38:45 +02:00
Bruno BELANYI f633c6e224 Mark'Error' as non-exhaustive
This simplifies semantic versionning constraints.
2022-07-30 12:38:45 +02:00
Bruno BELANYI 08ff8db0ac Add 'Error::InvalidPosition' variant 2022-07-30 12:38:45 +02:00
Bruno BELANYI 76577718d8 Add FEN castling rights parsing 2022-07-30 12:38:45 +02:00
Bruno BELANYI 7df442e03c Add FEN piece type parsing 2022-07-30 12:38:45 +02:00
Bruno BELANYI dba4d94e35 Add FEN en-passant target square parsing 2022-07-28 20:28:45 +02:00
Bruno BELANYI 6f0e2f732b Add FEN side to move parsing 2022-07-28 20:28:45 +02:00
Bruno BELANYI 611e12c033 Add 'Color::third_rank' 2022-07-28 20:28:45 +02:00
Bruno BELANYI dde5b69f81 Add 'FromFen' trait 2022-07-28 20:28:45 +02:00
Bruno BELANYI 7e23cb8f77 Introduce 'Error' enum 2022-07-28 20:28:45 +02:00
Bruno BELANYI 8102b08cf0 Add 'CastleRights::with_{king,queen}_side' 2022-07-28 20:28:45 +02:00
Bruno BELANYI e7e5927902 Add 'Move' 2022-07-28 20:28:45 +02:00
Bruno BELANYI 80e3ace8fc Add '*Assign' operators to 'Bitboard' 2022-07-27 23:36:54 +02:00
Bruno BELANYI 02d48fe526 Remove all useless 'allow(unused)' 2022-07-27 23:36:54 +02:00
Bruno BELANYI 915244b238 Add 'rerun-if-changed' directives to build script 2022-07-27 23:36:54 +02:00
Bruno BELANYI d2c61a81b5 Make use of generated move tables 2022-07-27 23:36:54 +02:00
Bruno BELANYI bd43535192 Move naive move generation into sub-module 2022-07-27 23:36:53 +02:00
Bruno BELANYI 8289204e4b Generate magic tables with build script 2022-07-27 23:36:53 +02:00
Bruno BELANYI d2eda07036 Make all modules at least 'pub(crate)' 2022-07-27 23:36:53 +02:00
Bruno BELANYI 0222ec4c2d Add 'Color::iter' 2022-07-27 23:36:53 +02:00
Bruno BELANYI d97e7d646e Move 'Magic' into 'seer::movegen::magic' 2022-07-27 23:36:53 +02:00
28 changed files with 1841 additions and 65 deletions

View file

@ -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

View file

@ -11,7 +11,8 @@ impl Iterator for BitboardIterator {
} else {
let lsb = self.0.trailing_zeros() as usize;
self.0 ^= 1 << lsb;
Some(crate::board::Square::from_index(lsb))
// SAFETY: we know the value is in-bounds
Some(unsafe { crate::board::Square::from_index_unchecked(lsb) })
}
}

View file

@ -68,6 +68,13 @@ impl Bitboard {
self == Self::EMPTY
}
/// Return true if there are more than piece in the [Bitboard]. This is faster than testing
/// `board.count() > 1`.
#[inline(always)]
pub fn has_more_than_one(self) -> bool {
(self.0 & (self.0.wrapping_sub(1))) != 0
}
/// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of
/// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that
/// contain all, some, or none of the [Square] that are in the given [Bitboard].
@ -76,6 +83,18 @@ impl Bitboard {
pub fn iter_power_set(self) -> impl Iterator<Item = Self> {
BitboardPowerSetIterator::new(self)
}
/// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is
/// occupying. Otherwise return `None`.
pub fn try_into_square(self) -> Option<Square> {
if self.count() != 1 {
None
} else {
let index = self.0.trailing_zeros() as usize;
// SAFETY: we know the value is in-bounds
Some(unsafe { Square::from_index_unchecked(index) })
}
}
}
// Ensure zero-cost (at least size-wise) wrapping.
@ -117,6 +136,22 @@ impl std::ops::Shr<usize> for Bitboard {
}
}
/// Treat bitboard as a set of squares, shift each square's index left by the amount given.
impl std::ops::ShlAssign<usize> for Bitboard {
#[inline(always)]
fn shl_assign(&mut self, rhs: usize) {
*self = *self << rhs;
}
}
/// Treat bitboard as a set of squares, shift each square's index right by the amount given.
impl std::ops::ShrAssign<usize> for Bitboard {
#[inline(always)]
fn shr_assign(&mut self, rhs: usize) {
*self = *self >> rhs;
}
}
/// Treat bitboard as a set of squares, and invert the set.
impl std::ops::Not for Bitboard {
type Output = Bitboard;
@ -147,6 +182,22 @@ impl std::ops::BitOr<Square> for Bitboard {
}
}
/// Treat each bitboard as a set of squares, keep squares that are in either sets.
impl std::ops::BitOrAssign<Bitboard> for Bitboard {
#[inline(always)]
fn bitor_assign(&mut self, rhs: Bitboard) {
*self = *self | rhs;
}
}
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::BitOrAssign<Square> for Bitboard {
#[inline(always)]
fn bitor_assign(&mut self, rhs: Square) {
*self = *self | rhs;
}
}
/// Treat each bitboard as a set of squares, keep squares that are in both sets.
impl std::ops::BitAnd<Bitboard> for Bitboard {
type Output = Bitboard;
@ -167,6 +218,22 @@ impl std::ops::BitAnd<Square> for Bitboard {
}
}
/// Treat each bitboard as a set of squares, keep squares that are in both sets.
impl std::ops::BitAndAssign<Bitboard> for Bitboard {
#[inline(always)]
fn bitand_assign(&mut self, rhs: Bitboard) {
*self = *self & rhs;
}
}
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::BitAndAssign<Square> for Bitboard {
#[inline(always)]
fn bitand_assign(&mut self, rhs: Square) {
*self = *self & rhs;
}
}
/// Treat each bitboard as a set of squares, keep squares that are in exactly one of either set.
impl std::ops::BitXor<Bitboard> for Bitboard {
type Output = Bitboard;
@ -187,6 +254,22 @@ impl std::ops::BitXor<Square> for Bitboard {
}
}
/// Treat each bitboard as a set of squares, keep squares that are in exactly one of either set.
impl std::ops::BitXorAssign<Bitboard> for Bitboard {
#[inline(always)]
fn bitxor_assign(&mut self, rhs: Bitboard) {
*self = *self ^ rhs;
}
}
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::BitXorAssign<Square> for Bitboard {
#[inline(always)]
fn bitxor_assign(&mut self, rhs: Square) {
*self = *self ^ rhs;
}
}
/// Treat each bitboard as a set of squares, and substract one set from another.
impl std::ops::Sub<Bitboard> for Bitboard {
type Output = Bitboard;
@ -207,6 +290,22 @@ impl std::ops::Sub<Square> for Bitboard {
}
}
/// Treat each bitboard as a set of squares, and substract one set from another.
impl std::ops::SubAssign<Bitboard> for Bitboard {
#[inline(always)]
fn sub_assign(&mut self, rhs: Bitboard) {
*self = *self - rhs;
}
}
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::SubAssign<Square> for Bitboard {
#[inline(always)]
fn sub_assign(&mut self, rhs: Square) {
*self = *self - rhs;
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
@ -296,6 +395,16 @@ mod test {
assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1));
}
#[test]
fn more_than_one() {
assert!(!Bitboard::EMPTY.has_more_than_one());
for square in Square::iter() {
assert!(!square.into_bitboard().has_more_than_one())
}
assert!((Square::A1 | Square::H8).has_more_than_one());
assert!(Bitboard::ALL.has_more_than_one());
}
#[test]
fn iter_power_set_empty() {
assert_eq!(
@ -370,4 +479,17 @@ mod test {
1 << 8
);
}
#[test]
fn into_square() {
for square in Square::iter() {
assert_eq!(square.into_bitboard().try_into_square(), Some(square));
}
}
#[test]
fn into_square_invalid() {
assert!(Bitboard::EMPTY.try_into_square().is_none());
assert!((Square::A1 | Square::A2).try_into_square().is_none())
}
}

View file

@ -1,4 +1,5 @@
use super::{Bitboard, Color, File, Square};
use super::{Bitboard, Color, File, FromFen, Square};
use crate::error::Error;
/// Current castle rights for a player.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@ -53,6 +54,26 @@ impl CastleRights {
(self.index() & 2) != 0
}
/// Add king-side castling rights.
#[inline(always)]
pub fn with_king_side(self) -> Self {
self.add(Self::KingSide)
}
/// Add queen-side castling rights.
#[inline(always)]
pub fn with_queen_side(self) -> Self {
self.add(Self::QueenSide)
}
/// Add some [CastleRights], and return the resulting [CastleRights].
#[allow(clippy::should_implement_trait)]
#[inline(always)]
pub fn add(self, to_remove: CastleRights) -> Self {
// SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(self.index() | to_remove.index()) }
}
/// Remove king-side castling rights.
#[inline(always)]
pub fn without_king_side(self) -> Self {
@ -89,6 +110,39 @@ impl CastleRights {
}
}
/// Convert the castling rights segment of a FEN string to an array of [CastleRights].
impl FromFen for [CastleRights; 2] {
type Err = Error;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
if s.len() > 4 {
return Err(Error::InvalidFen);
}
let mut res = [CastleRights::NoSide; 2];
if s == "-" {
return Ok(res);
}
for b in s.chars() {
let color = if b.is_uppercase() {
Color::White
} else {
Color::Black
};
let rights = &mut res[color.index()];
match b {
'k' | 'K' => *rights = rights.with_king_side(),
'q' | 'Q' => *rights = rights.with_queen_side(),
_ => return Err(Error::InvalidFen),
}
}
Ok(res)
}
}
#[cfg(test)]
mod test {
use super::*;

1001
src/board/chess_board.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
use super::{Direction, Rank};
use super::{Direction, FromFen, Rank};
use crate::error::Error;
/// An enum representing the color of a player.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@ -11,6 +12,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<Item = Self> {
Self::ALL.iter().cloned()
}
/// Convert from a piece index into a [Color] type.
#[inline(always)]
pub fn from_index(index: usize) -> Self {
@ -53,6 +61,16 @@ impl Color {
}
}
/// Return the third [Rank] for pieces of the given [Color], where its pawns move to after a
/// one-square move on the start position.
#[inline(always)]
pub fn third_rank(self) -> Rank {
match self {
Self::White => Rank::Third,
Self::Black => Rank::Sixth,
}
}
/// Return the fourth [Rank] for pieces of the given [Color], where its pawns move to after a
/// two-square move.
#[inline(always)]
@ -89,6 +107,20 @@ impl Color {
}
}
/// Convert a side to move segment of a FEN string to a [Color].
impl FromFen for Color {
type Err = Error;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let res = match s {
"w" => Color::White,
"b" => Color::Black,
_ => return Err(Error::InvalidFen),
};
Ok(res)
}
}
impl std::ops::Not for Color {
type Output = Color;

View file

@ -140,7 +140,7 @@ impl Direction {
while !board.is_empty() {
board = self.move_board(board);
res = res | board;
res |= board;
if !(board & blockers).is_empty() {
break;
}

6
src/board/fen.rs Normal file
View file

@ -0,0 +1,6 @@
/// A trait to mark items that can be converted from a FEN input.
pub trait FromFen: Sized {
type Err;
fn from_fen(s: &str) -> Result<Self, Self::Err>;
}

View file

@ -4,15 +4,24 @@ pub use bitboard::*;
pub mod castle_rights;
pub use castle_rights::*;
pub mod chess_board;
pub use chess_board::*;
pub mod color;
pub use color::*;
pub mod direction;
pub use direction::*;
pub mod fen;
pub use fen::*;
pub mod file;
pub use file::*;
pub mod r#move;
pub use r#move::*;
pub mod piece;
pub use piece::*;

232
src/board/move.rs Normal file
View file

@ -0,0 +1,232 @@
use super::{Piece, Square};
type Bitset = u32;
/// A chess move, containing:
/// * Piece type.
/// * Starting square.
/// * Destination square.
/// * Optional capture type.
/// * Optional promotion type.
/// * Optional captured type.
/// * Whether the move was an en-passant capture.
/// * Whether the move was a double-step.
/// * Whether the move was a castling.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Move(Bitset);
/// A builder for [Move]. This is the prefered and only way of building a [Move].
pub struct MoveBuilder {
pub piece: Piece,
pub start: Square,
pub destination: Square,
pub capture: Option<Piece>,
pub promotion: Option<Piece>,
pub en_passant: bool,
pub double_step: bool,
pub castling: bool,
}
impl From<MoveBuilder> for Move {
#[inline(always)]
fn from(builder: MoveBuilder) -> Self {
Self::new(
builder.piece,
builder.start,
builder.destination,
builder.capture,
builder.promotion,
builder.en_passant,
builder.double_step,
builder.castling,
)
}
}
/// A [Move] is structured as a bitset with the following fields:
/// | Field | Size | Range of values | Note |
/// |-------------|------|-----------------|-------------------------------------------------|
/// | Piece | 3 | 0-6 | Can be interpreted as a [Piece] index |
/// | Start | 6 | 0-63 | Can be interpreted as a [Square] index |
/// | Destination | 6 | 0-63 | Can be interpreted as a [Square] index |
/// | Capture | 3 | 0-7 | Can be interpreted as a [Piece] index if not 7 |
/// | Promotion | 3 | 0-7 | Can be interpreted as a [Piece] index if not 7 |
/// | En-pasant | 1 | 0-1 | Boolean value |
/// | Double-step | 1 | 0-1 | Boolean value |
/// | Castling | 1 | 0-1 | Boolean value |
mod shift {
use super::Bitset;
pub const PIECE: usize = 0;
pub const PIECE_MASK: Bitset = 0b111;
pub const START: usize = 3;
pub const START_MASK: Bitset = 0b11_1111;
pub const DESTINATION: usize = 9;
pub const DESTINATION_MASK: Bitset = 0b11_1111;
pub const CAPTURE: usize = 15;
pub const CAPTURE_MASK: Bitset = 0b111;
pub const PROMOTION: usize = 18;
pub const PROMOTION_MASK: Bitset = 0b111;
pub const EN_PASSANT: usize = 21;
pub const EN_PASSANT_MASK: Bitset = 0b1;
pub const DOUBLE_STEP: usize = 22;
pub const DOUBLE_STEP_MASK: Bitset = 0b1;
pub const CASTLING: usize = 23;
pub const CASTLING_MASK: Bitset = 0b1;
}
impl Move {
/// Construct a new move.
#[allow(clippy::too_many_arguments)]
#[inline(always)]
fn new(
piece: Piece,
start: Square,
destination: Square,
capture: Option<Piece>,
promotion: Option<Piece>,
en_passant: bool,
double_step: bool,
castling: bool,
) -> Self {
let mut value = 0;
value |= (piece.index() as Bitset) << shift::PIECE;
value |= (start.index() as Bitset) << shift::START;
value |= (destination.index() as Bitset) << shift::DESTINATION;
value |=
(capture.map(Piece::index).unwrap_or(Piece::NUM_VARIANTS) as Bitset) << shift::CAPTURE;
value |= (promotion.map(Piece::index).unwrap_or(Piece::NUM_VARIANTS) as Bitset)
<< shift::PROMOTION;
value |= (en_passant as Bitset) << shift::EN_PASSANT;
value |= (double_step as Bitset) << shift::DOUBLE_STEP;
value |= (castling as Bitset) << shift::CASTLING;
Self(value)
}
/// Get the [Piece] that is being moved.
#[inline(always)]
pub fn piece(self) -> Piece {
let index = ((self.0 >> shift::PIECE) & shift::PIECE_MASK) as usize;
// SAFETY: we know the value is in-bounds
unsafe { Piece::from_index_unchecked(index) }
}
/// Get the [Square] that this move starts from.
#[inline(always)]
pub fn start(self) -> Square {
let index = ((self.0 >> shift::START) & shift::START_MASK) as usize;
// SAFETY: we know the value is in-bounds
unsafe { Square::from_index_unchecked(index) }
}
/// Get the [Square] that this move ends on.
#[inline(always)]
pub fn destination(self) -> Square {
let index = ((self.0 >> shift::DESTINATION) & shift::DESTINATION_MASK) as usize;
// SAFETY: we know the value is in-bounds
unsafe { Square::from_index_unchecked(index) }
}
/// Get the [Piece] that this move captures, or `None` if there are no captures.
#[inline(always)]
pub fn capture(self) -> Option<Piece> {
let index = ((self.0 >> shift::CAPTURE) & shift::CAPTURE_MASK) as usize;
if index < Piece::NUM_VARIANTS {
// SAFETY: we know the value is in-bounds
unsafe { Some(Piece::from_index_unchecked(index)) }
} else {
None
}
}
/// Get the [Piece] that this move promotes to, or `None` if there are no promotions.
#[inline(always)]
pub fn promotion(self) -> Option<Piece> {
let index = ((self.0 >> shift::PROMOTION) & shift::PROMOTION_MASK) as usize;
if index < Piece::NUM_VARIANTS {
// SAFETY: we know the value is in-bounds
unsafe { Some(Piece::from_index_unchecked(index)) }
} else {
None
}
}
/// Get the whether or not the move is an en-passant capture.
#[inline(always)]
pub fn is_en_passant(self) -> bool {
let index = (self.0 >> shift::EN_PASSANT) & shift::EN_PASSANT_MASK;
index != 0
}
/// Get the whether or not the move is a pawn double step.
#[inline(always)]
pub fn is_double_step(self) -> bool {
let index = (self.0 >> shift::DOUBLE_STEP) & shift::DOUBLE_STEP_MASK;
index != 0
}
/// Get the whether or not the move is a king castling.
#[inline(always)]
pub fn is_castling(self) -> bool {
let index = (self.0 >> shift::CASTLING) & shift::CASTLING_MASK;
index != 0
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn builder_simple() {
let chess_move: Move = MoveBuilder {
piece: Piece::Queen,
start: Square::A2,
destination: Square::A3,
capture: None,
promotion: None,
en_passant: false,
double_step: false,
castling: false,
}
.into();
assert_eq!(chess_move.piece(), Piece::Queen);
assert_eq!(chess_move.start(), Square::A2);
assert_eq!(chess_move.destination(), Square::A3);
assert_eq!(chess_move.capture(), None);
assert_eq!(chess_move.promotion(), None);
assert!(!chess_move.is_en_passant());
assert!(!chess_move.is_double_step());
assert!(!chess_move.is_castling());
}
#[test]
fn builder_all_fields() {
let chess_move: Move = MoveBuilder {
piece: Piece::Pawn,
start: Square::A7,
destination: Square::B8,
capture: Some(Piece::Queen),
promotion: Some(Piece::Knight),
en_passant: true,
double_step: true,
castling: true,
}
.into();
assert_eq!(chess_move.piece(), Piece::Pawn);
assert_eq!(chess_move.start(), Square::A7);
assert_eq!(chess_move.destination(), Square::B8);
assert_eq!(chess_move.capture(), Some(Piece::Queen));
assert_eq!(chess_move.promotion(), Some(Piece::Knight));
assert!(chess_move.is_en_passant());
assert!(chess_move.is_double_step());
assert!(chess_move.is_castling());
}
}

View file

@ -1,3 +1,6 @@
use super::FromFen;
use crate::error::Error;
/// An enum representing the type of a piece.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Piece {
@ -52,6 +55,24 @@ impl Piece {
}
}
/// Convert a piece in FEN notation to a [Piece].
impl FromFen for Piece {
type Err = Error;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let res = match s {
"p" | "P" => Self::Pawn,
"n" | "N" => Self::Knight,
"b" | "B" => Self::Bishop,
"r" | "R" => Self::Rook,
"q" | "Q" => Self::Queen,
"k" | "K" => Self::King,
_ => return Err(Error::InvalidFen),
};
Ok(res)
}
}
#[cfg(test)]
mod test {
use super::*;

View file

@ -1,5 +1,5 @@
use super::{Bitboard, File, Rank};
use crate::utils::static_assert;
use super::{Bitboard, File, FromFen, Rank};
use crate::{error::Error, utils::static_assert};
/// Represent a square on a chessboard. Defined in the same order as the
/// [Bitboard] squares.
@ -107,6 +107,23 @@ impl Square {
}
}
/// Convert an en-passant target square segment of a FEN string to an optional [Square].
impl FromFen for Option<Square> {
type Err = Error;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let res = match s.as_bytes() {
[b'-'] => None,
[file @ b'a'..=b'h', rank @ b'1'..=b'8'] => Some(Square::new(
File::from_index((file - b'a') as usize),
Rank::from_index((rank - b'1') as usize),
)),
_ => return Err(Error::InvalidFen),
};
Ok(res)
}
}
/// Shift the square's index left by the amount given.
impl std::ops::Shl<usize> for Square {
type Output = Square;

144
src/build.rs Normal file
View file

@ -0,0 +1,144 @@
use std::io::{Result, Write};
pub mod board;
pub mod error;
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(())
}

19
src/error.rs Normal file
View file

@ -0,0 +1,19 @@
/// A singular type for all errors that could happen when using this library.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Error {
InvalidFen,
InvalidPosition,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let error_msg = match self {
Self::InvalidFen => "Invalid FEN input",
Self::InvalidPosition => "Invalid position",
};
write!(f, "{}", error_msg)
}
}
impl std::error::Error for Error {}

View file

@ -1,3 +1,4 @@
pub mod board;
pub mod error;
pub mod movegen;
pub mod utils;

View file

@ -1,21 +0,0 @@
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
}
}

View file

@ -1,2 +1,67 @@
pub mod magic;
pub use magic::*;
use crate::board::Bitboard;
/// A type representing the magic board indexing a given [crate::board::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;
pub use moves::*;
#[cfg(not(generated_boards))]
#[allow(unused_variables)]
mod moves {
use crate::board::{Bitboard, Color, Square};
pub fn quiet_pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn knight_moves(square: Square) -> Bitboard {
unreachable!()
}
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn king_moves(square: Square) -> Bitboard {
unreachable!()
}
pub fn king_side_castle_blockers(color: Color) -> Bitboard {
unreachable!()
}
pub fn queen_side_castle_blockers(color: Color) -> Bitboard {
unreachable!()
}
}

View file

@ -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()) }
}

View file

@ -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;

View file

@ -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))

View file

@ -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();

View file

@ -1,6 +1,5 @@
use crate::board::{Bitboard, Direction, Square};
#[allow(unused)]
pub fn knight_moves(square: Square) -> Bitboard {
let board = square.into_bitboard();

5
src/movegen/naive/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod bishop;
pub mod king;
pub mod knight;
pub mod pawn;
pub mod rook;

View file

@ -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();

View file

@ -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))

View file

@ -1,6 +1,5 @@
use crate::board::{Bitboard, Square};
use crate::movegen::bishop::bishop_moves;
use crate::movegen::rook::rook_moves;
use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves};
use crate::movegen::Magic;
use super::mask::{generate_bishop_mask, generate_rook_mask};
@ -22,42 +21,42 @@ fn generate_magics(
mask_fn: impl Fn(Square) -> Bitboard,
moves_fn: impl Fn(Square, Bitboard) -> Bitboard,
) -> MagicGenerationType {
let mut offset = 0;
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 potential_occupancy: Vec<_> = mask.iter_power_set().collect();
let moves_len = potential_occupancy.len();
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,
offset: 0,
mask,
shift: (64 - mask.count()) as u8,
};
let mut candidate_moves = Vec::new();
candidate_moves.resize(moves_len, Bitboard::EMPTY);
let mut candidate_moves = vec![Bitboard::EMPTY; occupancy_to_moves.len()];
for occupancy in potential_occupancy.iter().cloned() {
for (occupancy, moves) in occupancy_to_moves.iter().cloned() {
let index = candidate.get_index(occupancy);
let moves = moves_fn(square, 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.push(candidate);
offset += moves_len;
}
(magics, boards)

View file

@ -1,6 +1,5 @@
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};
pub fn generate_bishop_mask(square: Square) -> Bitboard {
let rays = bishop_moves(square, Bitboard::EMPTY);
@ -19,16 +18,16 @@ pub fn generate_rook_mask(square: Square) -> Bitboard {
let mask = {
let mut mask = Bitboard::EMPTY;
if square.file() != File::A {
mask = mask | File::A.into_bitboard()
mask |= File::A.into_bitboard()
};
if square.file() != File::H {
mask = mask | File::H.into_bitboard()
mask |= File::H.into_bitboard()
};
if square.rank() != Rank::First {
mask = mask | Rank::First.into_bitboard()
mask |= Rank::First.into_bitboard()
};
if square.rank() != Rank::Eighth {
mask = mask | Rank::Eighth.into_bitboard()
mask |= Rank::Eighth.into_bitboard()
};
mask
};

View file

@ -1,2 +1,2 @@
mod generation;
pub(crate) mod generation;
mod mask;