diff --git a/.woodpecker/check.yml b/.drone.yml similarity index 60% rename from .woodpecker/check.yml rename to .drone.yml index 4ff7dba..8fb7774 100644 --- a/.woodpecker/check.yml +++ b/.drone.yml @@ -1,19 +1,24 @@ -labels: - backend: local +--- +kind: pipeline +type: exec +name: abacus checks steps: -- name: pre-commit check - image: bash +- name: pre commit check commands: - - nix develop --command pre-commit run --all + - nix develop . --command pre-commit run --all -- name: nix flake check - image: bash +- name: flake check commands: - nix flake check +- name: package check + commands: + - nix build + - name: notifiy - image: bash + commands: + - nix run github:ambroisie/matrix-notifier environment: ADDRESS: from_secret: matrix_homeserver @@ -23,9 +28,8 @@ steps: from_secret: matrix_username PASS: from_secret: matrix_password - commands: - - nix run github:ambroisie/matrix-notifier when: status: - - failure - - success + - failure + - success +... diff --git a/.envrc b/.envrc deleted file mode 100644 index de77fcb..0000000 --- a/.envrc +++ /dev/null @@ -1,5 +0,0 @@ -if ! has nix_direnv_version || ! nix_direnv_version 3.0.0; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.0/direnvrc" "sha256-21TMnI2xWX7HkSTjFFri2UaohXVj854mgvWapWrxRXg=" -fi - -use flake diff --git a/.gitignore b/.gitignore index 5f360ff..c2f669f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -# Rust build directory -/target - -# Nix generated files -/.pre-commit-config.yaml +# Nix files /result +/.pre-commit-config.yaml + +# Rust files +/target diff --git a/Cargo.lock b/Cargo.lock index 1e43342..e9dfdc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "random" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d13a3485349981c90c79112a11222c3e6e75de1d52b87a7525b3bf5361420f" + [[package]] name = "seer" version = "0.1.0" +dependencies = [ + "random", +] diff --git a/Cargo.toml b/Cargo.toml index f191c81..756e025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +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/flake.lock b/flake.lock index 18cea66..2601b96 100644 --- a/flake.lock +++ b/flake.lock @@ -1,111 +1,73 @@ { "nodes": { - "flake-compat": { - "flake": false, + "flake-utils": { "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "futils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1656928814, + "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", "type": "github" }, "original": { "owner": "numtide", - "ref": "main", + "ref": "master", "repo": "flake-utils", "type": "github" } }, - "gitignore": { + "naersk": { "inputs": { "nixpkgs": [ - "pre-commit-hooks", "nixpkgs" ] }, "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "lastModified": 1655042882, + "narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=", + "owner": "nix-community", + "repo": "naersk", + "rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f", "type": "github" }, "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", + "owner": "nix-community", + "ref": "master", + "repo": "naersk", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1711523803, - "narHash": "sha256-UKcYiHWHQynzj6CN/vTcix4yd1eCu1uFdsuarupdCQQ=", + "lastModified": 1657888067, + "narHash": "sha256-GnwJoFBTPfW3+mz7QEeJEEQ9OMHZOiIJ/qDhZxrlKh8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2726f127c15a4cc9810843b96cad73c7eb39e443", + "rev": "65fae659e31098ca4ac825a6fef26d890aaf3f4e", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1710695816, - "narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "614b4613980a522ba49f0d194531beddbb7220d3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-23.11", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "pre-commit-hooks": { "inputs": { - "flake-compat": "flake-compat", "flake-utils": [ - "futils" + "flake-utils" ], - "gitignore": "gitignore", "nixpkgs": [ "nixpkgs" - ], - "nixpkgs-stable": "nixpkgs-stable" + ] }, "locked": { - "lastModified": 1711519547, - "narHash": "sha256-Q7YmSCUJmDl71fJv/zD9lrOCJ1/SE/okZ2DsrmRjzhY=", + "lastModified": 1656169028, + "narHash": "sha256-y9DRauokIeVHM7d29lwT8A+0YoGUBXV3H0VErxQeA8s=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "7d47a32e5cd1ea481fab33c516356ce27c8cef4a", + "rev": "db3bd555d3a3ceab208bed48f983ccaa6a71a25e", "type": "github" }, "original": { @@ -117,23 +79,34 @@ }, "root": { "inputs": { - "futils": "futils", + "flake-utils": "flake-utils", + "naersk": "naersk", "nixpkgs": "nixpkgs", - "pre-commit-hooks": "pre-commit-hooks" + "pre-commit-hooks": "pre-commit-hooks", + "rust-overlay": "rust-overlay" } }, - "systems": { + "rust-overlay": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "lastModified": 1657853760, + "narHash": "sha256-X6ERAyUXGsrhbhgkxNaQl40wcus5uyQZOCxUh5neK+g=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "a97a761cc11327bb109dc30af1c637b986be7959", "type": "github" }, "original": { - "owner": "nix-systems", - "repo": "default", + "owner": "oxalica", + "ref": "master", + "repo": "rust-overlay", "type": "github" } } diff --git a/flake.nix b/flake.nix index ca06a36..c8e7566 100644 --- a/flake.nix +++ b/flake.nix @@ -1,19 +1,29 @@ { - description = "A chess engine"; + description = "A handy file picker program"; inputs = { - futils = { + flake-utils = { type = "github"; owner = "numtide"; repo = "flake-utils"; - ref = "main"; + ref = "master"; + }; + + naersk = { + type = "github"; + owner = "nix-community"; + repo = "naersk"; + ref = "master"; + inputs = { + nixpkgs.follows = "nixpkgs"; + }; }; nixpkgs = { type = "github"; owner = "NixOS"; repo = "nixpkgs"; - ref = "nixos-unstable"; + ref = "nixpkgs-unstable"; }; pre-commit-hooks = { @@ -22,53 +32,76 @@ repo = "pre-commit-hooks.nix"; ref = "master"; inputs = { - flake-utils.follows = "futils"; + flake-utils.follows = "flake-utils"; + nixpkgs.follows = "nixpkgs"; + }; + }; + + rust-overlay = { + type = "github"; + owner = "oxalica"; + repo = "rust-overlay"; + ref = "master"; + inputs = { + flake-utils.follows = "flake-utils"; nixpkgs.follows = "nixpkgs"; }; }; }; - outputs = { self, futils, nixpkgs, pre-commit-hooks }: - { - overlays = { - default = final: _prev: { - seer = with final; rustPlatform.buildRustPackage { - pname = "seer"; - version = (final.lib.importTOML ./Cargo.toml).package.version; + outputs = + { self + , flake-utils + , naersk + , nixpkgs + , pre-commit-hooks + , rust-overlay + }: + let + inherit (flake-utils.lib) eachSystem system; - src = self; + mySystems = [ + system.aarch64-linux + system.x86_64-darwin + system.x86_64-linux + ]; - cargoLock = { - lockFile = "${self}/Cargo.lock"; - }; - - meta = with lib; { - description = "A chess engine"; - homepage = "https://git.belanyi.fr/ambroisie/seer"; - license = licenses.mit; - maintainers = with maintainers; [ ambroisie ]; - }; - }; - }; + eachMySystem = eachSystem mySystems; + in + eachMySystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit overlays system; }; + my-rust = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; }; - } // futils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { - inherit system; - overlays = [ - self.overlays.default - ]; - }; - - pre-commit = pre-commit-hooks.lib.${system}.run { + naersk-lib = naersk.lib."${system}".override { + cargo = my-rust; + rustc = my-rust; + }; + inherit (pkgs) lib; + pre-commit = + let + # See https://github.com/cachix/pre-commit-hooks.nix/issues/126 + rust-env = pkgs.buildEnv { + name = "rust-env"; + buildInputs = [ pkgs.makeWrapper ]; + paths = [ my-rust ]; + pathsToLink = [ "/" "/bin" ]; + postBuild = '' + for i in $out/bin/*; do + wrapProgram "$i" --prefix PATH : "$out/bin" + done + ''; + }; + in + pre-commit-hooks.lib.${system}.run { src = self; hooks = { clippy = { enable = true; - settings = { - denyWarnings = true; - }; + entry = lib.mkForce "${rust-env}/bin/cargo-clippy clippy"; }; nixpkgs-fmt = { @@ -77,36 +110,43 @@ rustfmt = { enable = true; + entry = lib.mkForce "${rust-env}/bin/cargo-fmt fmt -- --check --color always"; }; }; }; - in - { - checks = { - inherit (self.packages.${system}) seer; + in + rec { + + devShells = { + default = pkgs.mkShell { + inputsFrom = [ + packages.seer + ]; + + nativeBuildInputs = with pkgs; [ + rust-analyzer + # Clippy, rustfmt, etc... + my-rust + ]; + + inherit (pre-commit) shellHook; + + RUST_SRC_PATH = "${my-rust}/lib/rustlib/src/rust/library"; }; + }; - devShells = { - default = pkgs.mkShell { - inputsFrom = with self.packages.${system}; [ - seer - ]; + packages = { + default = self.packages."${system}".seer; - packages = with pkgs; [ - clippy - rust-analyzer - rustfmt - ]; + seer = naersk-lib.buildPackage { + src = self; - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + doCheck = true; - inherit (pre-commit) shellHook; + passthru = { + inherit my-rust; }; }; - - packages = futils.lib.flattenTree { - default = pkgs.seer; - inherit (pkgs) seer; - }; - }); + }; + }); } diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index e69de29..0000000 diff --git a/src/board/bitboard/error.rs b/src/board/bitboard/error.rs deleted file mode 100644 index c631482..0000000 --- a/src/board/bitboard/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum IntoSquareError { - /// The board is empty. - EmptyBoard, - /// The board contains more than one square. - TooManySquares, -} - -impl std::fmt::Display for IntoSquareError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let error_msg = match self { - Self::EmptyBoard => "The board is empty", - Self::TooManySquares => "The board contains more than one square", - }; - write!(f, "{}", error_msg) - } -} - -impl std::error::Error for IntoSquareError {} diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs index f5711ee..ea6d489 100644 --- a/src/board/bitboard/iterator.rs +++ b/src/board/bitboard/iterator.rs @@ -1,28 +1,23 @@ /// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a -/// [Bitboard]. -use crate::board::Bitboard; - -pub struct BitboardIterator(Bitboard); - -impl BitboardIterator { - pub fn new(board: Bitboard) -> Self { - Self(board) - } -} +/// [Bitboard](crate::board::Bitboard). +pub struct BitboardIterator(pub(crate) u64); impl Iterator for BitboardIterator { type Item = crate::board::Square; fn next(&mut self) -> Option { - let res = self.0.any_square(); - if let Some(square) = res { - self.0 ^= square; - }; - res + if self.0 == 0 { + None + } else { + let lsb = self.0.trailing_zeros() as usize; + self.0 ^= 1 << lsb; + // SAFETY: we know the value is in-bounds + Some(unsafe { crate::board::Square::from_index_unchecked(lsb) }) + } } fn size_hint(&self) -> (usize, Option) { - let size = self.0.count() as usize; + let size = self.0.count_ones() as usize; (size, Some(size)) } diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 3cb9d7b..8dff351 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,8 +1,6 @@ -use super::{File, Rank, Square}; +use super::Square; use crate::utils::static_assert; -mod error; -use error::*; mod iterator; use iterator::*; mod superset; @@ -21,7 +19,8 @@ impl Bitboard { pub const ALL: Bitboard = Bitboard(u64::MAX); /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. - pub const RANKS: [Self; Rank::NUM_VARIANTS] = [ + #[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), Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100), @@ -33,7 +32,8 @@ impl Bitboard { ]; /// Array of bitboards representing the eight files, in order from file A to file H. - pub const FILES: [Self; File::NUM_VARIANTS] = [ + #[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), Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000), @@ -75,12 +75,6 @@ impl Bitboard { (self.0 & (self.0.wrapping_sub(1))) != 0 } - /// Return a [Square] from the board, or `None` if it is empty. - #[inline(always)] - pub fn any_square(self) -> Option { - Square::try_from_index(self.0.trailing_zeros() as usize) - } - /// 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]. @@ -89,6 +83,18 @@ impl Bitboard { pub fn iter_power_set(self) -> impl Iterator { 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 { + 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. @@ -106,20 +112,7 @@ impl IntoIterator for Bitboard { type Item = Square; fn into_iter(self) -> Self::IntoIter { - BitboardIterator::new(self) - } -} - -/// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is -/// occupying. Otherwise return `None`. -impl TryInto for Bitboard { - type Error = IntoSquareError; - - fn try_into(self) -> Result { - if self.has_more_than_one() { - return Err(IntoSquareError::TooManySquares); - } - self.any_square().ok_or(IntoSquareError::EmptyBoard) + BitboardIterator(self.0) } } @@ -455,11 +448,15 @@ mod test { #[test] fn iter_power_set_six_squares_exhaustive() { let mask = (0..6) + .into_iter() .map(Square::from_index) .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs); assert_eq!( mask.iter_power_set().collect::>(), - (0..(1 << 6)).map(Bitboard).collect::>() + (0..(1 << 6)) + .into_iter() + .map(Bitboard) + .collect::>() ) } @@ -483,39 +480,16 @@ mod test { ); } - #[test] - fn any_square() { - for square in Square::iter() { - assert_eq!(square.into_bitboard().any_square(), Some(square)); - } - } - - #[test] - fn any_square_empty() { - assert!(Bitboard::EMPTY.any_square().is_none()); - } - - #[test] - fn any_square_full_board() { - assert!(Bitboard::ALL.any_square().is_some()); - } - #[test] fn into_square() { for square in Square::iter() { - assert_eq!(square.into_bitboard().try_into(), Ok(square)); + assert_eq!(square.into_bitboard().try_into_square(), Some(square)); } } #[test] fn into_square_invalid() { - assert_eq!( - TryInto::::try_into(Bitboard::EMPTY), - Err(IntoSquareError::EmptyBoard) - ); - assert_eq!( - TryInto::::try_into(Square::A1 | Square::A2), - Err(IntoSquareError::TooManySquares) - ) + assert!(Bitboard::EMPTY.try_into_square().is_none()); + assert!((Square::A1 | Square::A2).try_into_square().is_none()) } } diff --git a/src/board/bitboard/superset.rs b/src/board/bitboard/superset.rs index 1a82ca1..46f1ad2 100644 --- a/src/board/bitboard/superset.rs +++ b/src/board/bitboard/superset.rs @@ -4,20 +4,21 @@ use super::Bitboard; /// In other words, for each square that belongs to the mask, this will yield all sets that do /// contain the square, and all sets that do not. pub struct BitboardPowerSetIterator { - /// The starting board. - board: Bitboard, - /// The next subset. - subset: Bitboard, - /// Whether or not iteration is done. - done: bool, + /// The mask. + mask: Bitboard, + /// The "index" of the next blocker set that should be generated. + current: usize, + /// The number of blocker sets that should be generated by [BlockerIterator], i.e: 2^n with n + /// the number of squares belonging to `mask`. + total: usize, } impl BitboardPowerSetIterator { - pub fn new(board: Bitboard) -> Self { + pub fn new(mask: Bitboard) -> Self { Self { - board, - subset: Bitboard::EMPTY, - done: false, + mask, + current: 0, + total: 1 << mask.count(), } } } @@ -26,18 +27,22 @@ impl Iterator for BitboardPowerSetIterator { type Item = Bitboard; fn next(&mut self) -> Option { - if self.done { - return None; + if self.current >= self.total { + None + } else { + let blockers = (0..) + .into_iter() + .zip(self.mask.into_iter()) + .filter(|(index, _)| self.current & (1 << index) != 0) + .map(|(_, board)| board) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs); + self.current += 1; + Some(blockers) } - let res = self.subset; - self.subset = Bitboard(self.subset.0.wrapping_sub(self.board.0)) & self.board; - self.done = self.subset.is_empty(); - Some(res) } fn size_hint(&self) -> (usize, Option) { - let size = 1 << self.board.count(); - (size, Some(size)) + (self.total, Some(self.total)) } } diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 81cb5a6..6b0bb66 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -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)] @@ -17,44 +18,19 @@ impl CastleRights { /// The number of [CastleRights] variants. pub const NUM_VARIANTS: usize = 4; - const ALL: [Self; Self::NUM_VARIANTS] = [ - Self::NoSide, - Self::KingSide, - Self::QueenSide, - Self::BothSides, - ]; - - /// Iterate over all castle-rights variants. - pub fn iter() -> impl Iterator { - Self::ALL.iter().cloned() - } - /// Convert from a castle rights index into a [CastleRights] type. - /// - /// # Panics - /// - /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { - Self::try_from_index(index).expect("index out of bouds") - } - - /// Convert from a castle rights index into a [CastleRights] type. Returns [None] if the index - /// is out of bounds. - pub fn try_from_index(index: usize) -> Option { - if index < Self::NUM_VARIANTS { - // SAFETY: we know the value is in-bounds - Some(unsafe { Self::from_index_unchecked(index) }) - } else { - None - } + assert!(index < Self::NUM_VARIANTS); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } } /// Convert from a castle rights index into a [CastleRights] type, no bounds checking. /// /// # Safety /// - /// This should only be called with values that can be output by [CastleRights::index()]. + /// 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) @@ -91,10 +67,11 @@ impl CastleRights { } /// Add some [CastleRights], and return the resulting [CastleRights]. + #[allow(clippy::should_implement_trait)] #[inline(always)] - fn add(self, additional_rights: CastleRights) -> Self { + pub fn add(self, to_remove: CastleRights) -> Self { // SAFETY: we know the value is in-bounds - unsafe { Self::from_index_unchecked(self.index() | additional_rights.index()) } + unsafe { Self::from_index_unchecked(self.index() | to_remove.index()) } } /// Remove king-side castling rights. @@ -133,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 { + 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::*; diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs new file mode 100644 index 0000000..23d702c --- /dev/null +++ b/src/board/chess_board.rs @@ -0,0 +1,1001 @@ +use crate::{ + error::Error, + movegen::{ + bishop_moves, knight_moves, magic::king_moves, naive::pawn::pawn_captures, rook_moves, + }, +}; + +use super::{Bitboard, CastleRights, Color, File, FromFen, Move, Piece, Rank, Square}; + +/// Represent an on-going chess game. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ChessBoard { + /// A [Bitboard] of occupancy for each piece type, discarding color. Indexed by [Piece::index]. + piece_occupancy: [Bitboard; Piece::NUM_VARIANTS], + /// A [Bitboard] of occupancy for each color, discarding piece type. Indexed by [Piece::index]. + color_occupancy: [Bitboard; Color::NUM_VARIANTS], + /// A [Bitboard] representing all squares currently occupied by a piece. + combined_occupancy: Bitboard, + /// The allowed [CastleRights] for either color. Indexed by [Color::index]. + castle_rights: [CastleRights; Color::NUM_VARIANTS], + /// A potential en-passant attack. + /// Either `None` if no 2-square pawn move was made in the previous half-turn, or + /// `Some(target_square)` if a 2-square move was made. + en_passant: Option, + /// The number of half-turns without either a pawn push or capture. + half_move_clock: u8, // Should never go higher than 50. + /// The number of half-turns so far. + total_plies: u32, // Should be plenty. + /// The current player turn. + side: Color, +} + +pub struct NonReversibleState { + castle_rights: [CastleRights; Color::NUM_VARIANTS], + en_passant: Option, + half_move_clock: u8, // Should never go higher than 50. +} + +impl ChessBoard { + /// Which player's turn is it. + #[inline(always)] + pub fn current_player(&self) -> Color { + self.side + } + + /// Return the [Square] currently occupied by a pawn that can be captured en-passant, or `None` + #[inline(always)] + pub fn en_passant(&self) -> Option { + self.en_passant + } + + /// Return the [CastleRights] for the given [Color]. + #[inline(always)] + pub fn castle_rights(&self, color: Color) -> CastleRights { + // SAFETY: we know the value is in-bounds + unsafe { *self.castle_rights.get_unchecked(color.index()) } + } + + /// Return the [CastleRights] for the given [Color]. Allow mutations. + #[inline(always)] + fn castle_rights_mut(&mut self, color: Color) -> &mut CastleRights { + // SAFETY: we know the value is in-bounds + unsafe { &mut *self.castle_rights.get_unchecked_mut(color.index()) } + } + + /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. + #[inline(always)] + pub fn piece_occupancy(&self, piece: Piece) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *self.piece_occupancy.get_unchecked(piece.index()) } + } + + /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. + /// Allow mutating the state. + #[inline(always)] + fn piece_occupancy_mut(&mut self, piece: Piece) -> &mut Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { &mut *self.piece_occupancy.get_unchecked_mut(piece.index()) } + } + + /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece + /// type. + #[inline(always)] + pub fn color_occupancy(&self, color: Color) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *self.color_occupancy.get_unchecked(color.index()) } + } + + /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece + /// type. Allow mutating the state. + #[inline(always)] + fn color_occupancy_mut(&mut self, color: Color) -> &mut Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { &mut *self.color_occupancy.get_unchecked_mut(color.index()) } + } + + /// Get the [Bitboard] representing all pieces on the board. + #[inline(always)] + pub fn combined_occupancy(&self) -> Bitboard { + self.combined_occupancy + } + + /// Return the number of half-turns without either a pawn push or a capture. + #[inline(always)] + pub fn half_move_clock(&self) -> u8 { + self.half_move_clock + } + + /// Return the total number of plies (i.e: half-turns) played so far. + #[inline(always)] + pub fn total_plies(&self) -> u32 { + self.total_plies + } + + /// Return the [Bitboard] corresponding to all the opponent's pieces threatening the current + /// player's king. + #[inline(always)] + pub fn checkers(&self) -> Bitboard { + self.compute_checkers(self.current_player()) + } + + /// Convenience function which returns the [Piece] and [Color] at a given [Square], or `None` + /// if it is empty. + pub fn at(&self, square: Square) -> Option<(Piece, Color)> { + let color = if !(self.color_occupancy(Color::White) & square).is_empty() { + Color::White + } else { + Color::Black + }; + + for piece in Piece::iter() { + if !(self.piece_occupancy(piece) & square).is_empty() { + return Some((piece, color)); + } + } + + None + } + + /// Quickly do and undo a move on the [Bitboard]s that are part of the [ChessBoard] state. Does + /// not account for all non-revertible changes such as en-passant state or half-move clock. + #[inline(always)] + fn xor(&mut self, color: Color, piece: Piece, start_end: Bitboard) { + *self.piece_occupancy_mut(piece) ^= start_end; + *self.color_occupancy_mut(color) ^= start_end; + self.combined_occupancy ^= start_end; + } + + /// Play the given [Move], returning all non-revertible state (e.g: en-passant, etc...). + #[inline(always)] + pub fn do_move(&mut self, chess_move: Move) -> NonReversibleState { + // Save non-revertible state + let state = NonReversibleState { + castle_rights: self.castle_rights, + en_passant: self.en_passant, + half_move_clock: self.half_move_clock, + }; + + // Non-revertible state modification + if chess_move.capture().is_some() || chess_move.piece() == Piece::Pawn { + self.half_move_clock = 0; + } else { + self.half_move_clock += 1; + } + if chess_move.is_double_step() { + let target_square = Square::new( + chess_move.destination().file(), + self.current_player().third_rank(), + ); + self.en_passant = Some(target_square); + } else { + self.en_passant = None; + } + if chess_move.is_castling() || chess_move.piece() == Piece::King { + *self.castle_rights_mut(self.current_player()) = CastleRights::NoSide; + } + if chess_move.piece() == Piece::Rook { + let castle_rights = self.castle_rights_mut(self.current_player()); + *castle_rights = match chess_move.start().file() { + File::A => castle_rights.without_queen_side(), + File::H => castle_rights.without_king_side(), + _ => *castle_rights, + } + } + + // Revertible state modification + self.xor( + self.current_player(), + chess_move.piece(), + chess_move.start() | chess_move.destination(), + ); + self.total_plies += 1; + self.side = !self.side; + + state + } + + /// Reverse the effect of playing the given [Move], and return to the given + /// [NonReversibleState]. + #[inline(always)] + pub fn undo_move(&mut self, chess_move: Move, state: NonReversibleState) { + // Restore non-revertible state + self.castle_rights = state.castle_rights; + self.en_passant = state.en_passant; + self.half_move_clock = state.half_move_clock; + + // Restore revertible state + self.xor( + // The move was applied at the turn *before* the current player + !self.current_player(), + chess_move.piece(), + chess_move.start() | chess_move.destination(), + ); + self.total_plies -= 1; + self.side = !self.side; + } + + /// Return true if the current state of the board looks valid, false if something is definitely + /// wrong. + fn is_valid(&self) -> bool { + // Don't overlap pieces. + for piece in Piece::iter() { + #[allow(clippy::collapsible_if)] + for other in Piece::iter() { + if piece != other { + if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() { + return false; + } + } + } + } + + // Don't overlap colors. + if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() { + return false; + } + + // Calculate the union of all pieces. + let combined = + Piece::iter().fold(Bitboard::EMPTY, |board, p| board | self.piece_occupancy(p)); + + // Ensure that the pre-computed version is accurate. + if combined != self.combined_occupancy() { + return false; + } + + // Ensure that all pieces belong to a color, and no color has pieces that don't exist. + if combined != (self.color_occupancy(Color::White) | self.color_occupancy(Color::Black)) { + return false; + } + + // Have exactly one king of each color. + for color in Color::iter() { + if (self.piece_occupancy(Piece::King) & self.color_occupancy(color)).count() != 1 { + return false; + } + } + + // Verify that rooks and kings that are allowed to castle have not been moved. + for color in Color::iter() { + let castle_rights = self.castle_rights(color); + + // Nothing to check if there are no castlings allowed. + if castle_rights == CastleRights::NoSide { + continue; + } + + let actual_rooks = self.piece_occupancy(Piece::Rook) & self.color_occupancy(color); + let expected_rooks = castle_rights.unmoved_rooks(color); + // We must check the intersection, in case there are more than 2 rooks on the board. + if (expected_rooks & actual_rooks) != expected_rooks { + return false; + } + + let actual_king = self.piece_occupancy(Piece::King) & self.color_occupancy(color); + let expected_king = Square::new(File::E, color.first_rank()); + // We have checked that there is exactly one king, no need for intersecting the sets. + if actual_king != expected_king.into_bitboard() { + return false; + } + } + + // The current en-passant target square must be empty, right behind an opponent's pawn. + if let Some(square) = self.en_passant() { + if !(self.combined_occupancy() & square).is_empty() { + return false; + } + let opponent_pawns = + self.piece_occupancy(Piece::Pawn) & self.color_occupancy(!self.current_player()); + let double_pushed_pawn = self + .current_player() + .backward_direction() + .move_board(square.into_bitboard()); + if (opponent_pawns & double_pushed_pawn).is_empty() { + return false; + } + } + + // Check that kings don't touch each other. + let white_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::White); + let black_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::Black); + // Unwrap is fine, we already checked that there is exactly one king of each color + if !(king_moves(white_king.try_into_square().unwrap()) & black_king).is_empty() { + return false; + } + + // Check that the opponent is not currently in check. + if (self.compute_checkers(!self.current_player())) != Bitboard::EMPTY { + return false; + } + + true + } + + /// Compute all pieces that are currently threatening the given [Color]'s king. + fn compute_checkers(&self, color: Color) -> Bitboard { + // Unwrap is fine, there should always be exactly one king per color + let king = (self.piece_occupancy(Piece::King) & self.color_occupancy(color)) + .try_into_square() + .unwrap(); + + let opponent = !color; + + // No need to remove our pieces from the generated moves, we just want to check if we + // intersect with the opponent's pieces, rather than generate only valid moves. + let bishops = { + let queens = self.piece_occupancy(Piece::Queen) & self.color_occupancy(opponent); + let bishops = self.piece_occupancy(Piece::Bishop) & self.color_occupancy(opponent); + let bishop_attacks = bishop_moves(king, self.combined_occupancy()); + (queens | bishops) & bishop_attacks + }; + let rooks = { + let queens = self.piece_occupancy(Piece::Queen) & self.color_occupancy(opponent); + let rooks = self.piece_occupancy(Piece::Rook) & self.color_occupancy(opponent); + let rook_attacks = rook_moves(king, self.combined_occupancy()); + (queens | rooks) & rook_attacks + }; + let knights = { + let knights = self.piece_occupancy(Piece::Knight) & self.color_occupancy(opponent); + let knight_attacks = knight_moves(king); + knights & knight_attacks + }; + let pawns = { + let pawns = self.piece_occupancy(Piece::Pawn) & self.color_occupancy(opponent); + let pawn_attacks = pawn_captures(color, king); + pawns & pawn_attacks + }; + + bishops | rooks | knights | pawns + } +} + +/// Use the starting position as a default value, corresponding to the +/// "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" FEN string +impl Default for ChessBoard { + fn default() -> Self { + Self { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Square::D1 | Square::D8, + // Rook + Square::A1 | Square::A8 | Square::H1 | Square::H8, + // Bishop + Square::C1 | Square::C8 | Square::F1 | Square::F8, + // Knight + Square::B1 | Square::B8 | Square::G1 | Square::G8, + // Pawn + Rank::Second.into_bitboard() | Rank::Seventh.into_bitboard(), + ], + color_occupancy: [ + Rank::First.into_bitboard() | Rank::Second.into_bitboard(), + Rank::Seventh.into_bitboard() | Rank::Eighth.into_bitboard(), + ], + combined_occupancy: Rank::First.into_bitboard() + | Rank::Second.into_bitboard() + | Rank::Seventh.into_bitboard() + | Rank::Eighth.into_bitboard(), + castle_rights: [CastleRights::BothSides; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + } + } +} + +/// Return a [ChessBoard] from the given FEN string. +impl FromFen for ChessBoard { + type Err = Error; + + fn from_fen(s: &str) -> Result { + let mut split = s.split_ascii_whitespace(); + + let piece_placement = split.next().ok_or(Error::InvalidFen)?; + let side_to_move = split.next().ok_or(Error::InvalidFen)?; + let castling_rights = split.next().ok_or(Error::InvalidFen)?; + let en_passant_square = split.next().ok_or(Error::InvalidFen)?; + let half_move_clock = split.next().ok_or(Error::InvalidFen)?; + let full_move_counter = split.next().ok_or(Error::InvalidFen)?; + + let castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?; + let side = Color::from_fen(side_to_move)?; + let en_passant = Option::::from_fen(en_passant_square)?; + + let half_move_clock = half_move_clock + .parse::() + .map_err(|_| Error::InvalidFen)?; + let full_move_counter = full_move_counter + .parse::() + .map_err(|_| Error::InvalidFen)?; + let total_plies = (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }; + + let (piece_occupancy, color_occupancy, combined_occupancy) = { + let (mut pieces, mut colors, mut combined) = + ([Bitboard::EMPTY; 6], [Bitboard::EMPTY; 2], Bitboard::EMPTY); + + let mut rank: usize = 8; + for rank_str in piece_placement.split('/') { + rank -= 1; + let mut file: usize = 0; + for c in rank_str.chars() { + let color = if c.is_uppercase() { + Color::White + } else { + Color::Black + }; + let piece = match c { + digit @ '1'..='8' => { + // Unwrap is fine since this arm is only matched by digits + file += digit.to_digit(10).unwrap() as usize; + continue; + } + _ => Piece::from_fen(&c.to_string())?, + }; + let (piece_board, color_board) = + (&mut pieces[piece.index()], &mut colors[color.index()]); + + // Only need to worry about underflow since those are `usize` values. + if file >= 8 || rank >= 8 { + return Err(Error::InvalidFen); + }; + let square = Square::new(File::from_index(file), Rank::from_index(rank)); + *piece_board |= square; + *color_board |= square; + combined |= square; + file += 1; + } + // We haven't read exactly 8 files. + if file != 8 { + return Err(Error::InvalidFen); + } + } + // We haven't read exactly 8 ranks + if rank != 0 { + return Err(Error::InvalidFen); + } + + (pieces, colors, combined) + }; + + let res = Self { + piece_occupancy, + color_occupancy, + combined_occupancy, + castle_rights, + en_passant, + half_move_clock, + total_plies, + side, + }; + + if !res.is_valid() { + return Err(Error::InvalidPosition); + } + + Ok(res) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::MoveBuilder; + + #[test] + fn valid() { + let default_position = ChessBoard::default(); + assert!(default_position.is_valid()); + } + + #[test] + fn invalid_overlapping_pieces() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Square::E1 | Square::E8, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_overlapping_colors() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::E8, Square::E1 | Square::E8], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_combined_does_not_equal_pieces() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], + combined_occupancy: Square::E1.into_bitboard(), + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_combined_does_not_equal_colors() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::H1, Square::E8 | Square::H8], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_multiple_kings() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E2 | Square::E7 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::E2, Square::E7 | Square::E8], + combined_occupancy: Square::E1 | Square::E2 | Square::E7 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_castling_rights_no_rooks() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::BothSides; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_castling_rights_moved_king() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E2 | Square::E7, + // Queen + Bitboard::EMPTY, + // Rook + Square::A1 | Square::A8 | Square::H1 | Square::H8, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [ + Square::A1 | Square::E2 | Square::H1, + Square::A8 | Square::E7 | Square::H8, + ], + combined_occupancy: Square::A1 + | Square::A8 + | Square::E1 + | Square::E8 + | Square::H1 + | Square::H8, + castle_rights: [CastleRights::BothSides; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_kings_next_to_each_other() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E2 | Square::E3, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E2.into_bitboard(), Square::E3.into_bitboard()], + combined_occupancy: Square::E2 | Square::E3, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_opponent_in_check() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Square::E7.into_bitboard(), + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::E7, Square::E8.into_bitboard()], + combined_occupancy: Square::E1 | Square::E7 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn checkers() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E2 | Square::E8, + // Queen + Square::E7 | Square::H2, + // Rook + Square::A2 | Square::E1, + // Bishop + Square::D3 | Square::F3, + // Knight + Square::C1 | Square::G1, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [ + Square::C1 | Square::D3 | Square::E1 | Square::E2 | Square::H2, + Square::A2 | Square::E7 | Square::E8 | Square::F3 | Square::G1, + ], + combined_occupancy: Square::A2 + | Square::C1 + | Square::D3 + | Square::E1 + | Square::E2 + | Square::E7 + | Square::E8 + | Square::F3 + | Square::G1 + | Square::H2, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert_eq!( + position.checkers(), + Square::A2 | Square::E7 | Square::F3 | Square::G1 + ); + } + + #[test] + fn at() { + let default_position = ChessBoard::default(); + assert_eq!(default_position.at(Square::D4), None); + assert_eq!(default_position.at(Square::D5), None); + + assert_eq!( + default_position.at(Square::A1).unwrap(), + (Piece::Rook, Color::White) + ); + assert_eq!( + default_position.at(Square::H8).unwrap(), + (Piece::Rook, Color::Black) + ); + assert_eq!( + default_position.at(Square::D1).unwrap(), + (Piece::Queen, Color::White) + ); + assert_eq!( + default_position.at(Square::D8).unwrap(), + (Piece::Queen, Color::Black) + ); + assert_eq!( + default_position.at(Square::C2).unwrap(), + (Piece::Pawn, Color::White) + ); + assert_eq!( + default_position.at(Square::F7).unwrap(), + (Piece::Pawn, Color::Black) + ); + } + + #[test] + fn fen_default_position() { + let default_position = ChessBoard::default(); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + .unwrap(), + default_position + ); + } + + #[test] + fn fen_en_passant() { + // Start from default position + let mut position = ChessBoard::default(); + // Modify it to account for e4 move + position.xor(Color::White, Piece::Pawn, Square::E2 | Square::E4); + position.en_passant = Some(Square::E3); + position.total_plies = 1; + position.side = Color::Black; + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap(), + position + ); + // And now c5 + position.xor(Color::Black, Piece::Pawn, Square::C5 | Square::C7); + position.en_passant = Some(Square::C6); + position.total_plies = 2; + position.side = Color::White; + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap(), + position + ); + // Finally, Nf3 + position.xor(Color::White, Piece::Knight, Square::G1 | Square::F3); + position.en_passant = None; + position.total_plies = 3; + position.half_move_clock = 1; + position.side = Color::Black; + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") + .unwrap(), + position + ); + } + + #[test] + fn do_move() { + // Start from default position + let mut position = ChessBoard::default(); + // Modify it to account for e4 move + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::E2, + destination: Square::E4, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap() + ); + // And now c5 + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::C7, + destination: Square::C5, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap() + ); + // Finally, Nf3 + position.do_move( + MoveBuilder { + piece: Piece::Knight, + start: Square::G1, + destination: Square::F3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(), + ); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") + .unwrap() + ); + } + + #[test] + fn do_move_and_undo() { + // Start from default position + let mut position = ChessBoard::default(); + // Modify it to account for e4 move + let move_1 = MoveBuilder { + piece: Piece::Pawn, + start: Square::E2, + destination: Square::E4, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(); + let state_1 = position.do_move(move_1); + // And now c5 + let move_2 = MoveBuilder { + piece: Piece::Pawn, + start: Square::C7, + destination: Square::C5, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(); + let state_2 = position.do_move(move_2); + // Finally, Nf3 + let move_3 = MoveBuilder { + piece: Piece::Knight, + start: Square::G1, + destination: Square::F3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(); + let state_3 = position.do_move(move_3); + // Now revert each move one-by-one + position.undo_move(move_3, state_3); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap() + ); + position.undo_move(move_2, state_2); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap() + ); + position.undo_move(move_1, state_1); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + .unwrap() + ); + } +} diff --git a/src/board/chess_board/builder.rs b/src/board/chess_board/builder.rs deleted file mode 100644 index 679b3b7..0000000 --- a/src/board/chess_board/builder.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::board::{Bitboard, CastleRights, ChessBoard, Color, Piece, Square, ValidationError}; - -/// Build a [ChessBoard] one piece at a time. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct ChessBoardBuilder { - /// The list of [Piece] on the board. Indexed by [Square::index]. - pieces: [Option<(Piece, Color)>; Square::NUM_VARIANTS], - // Same fields as [ChessBoard]. - castle_rights: [CastleRights; Color::NUM_VARIANTS], - en_passant: Option, - half_move_clock: u32, - side: Color, - // 1-based, a turn is *two* half-moves (i.e: both players have played). - turn_count: u32, -} - -impl ChessBoardBuilder { - pub fn new() -> Self { - Self { - pieces: [None; Square::NUM_VARIANTS], - castle_rights: [CastleRights::NoSide; Color::NUM_VARIANTS], - en_passant: Default::default(), - half_move_clock: Default::default(), - side: Color::White, - turn_count: 1, - } - } - - pub fn with_castle_rights(&mut self, rights: CastleRights, color: Color) -> &mut Self { - self.castle_rights[color.index()] = rights; - self - } - - pub fn with_en_passant(&mut self, square: Square) -> &mut Self { - self.en_passant = Some(square); - self - } - - pub fn without_en_passant(&mut self) -> &mut Self { - self.en_passant = None; - self - } - - pub fn with_half_move_clock(&mut self, clock: u32) -> &mut Self { - self.half_move_clock = clock; - self - } - - pub fn with_turn_count(&mut self, count: u32) -> &mut Self { - self.turn_count = count; - self - } - - pub fn with_current_player(&mut self, color: Color) -> &mut Self { - self.side = color; - self - } -} - -impl Default for ChessBoardBuilder { - fn default() -> Self { - Self::new() - } -} - -/// Index a [ChessBoardBuilder] with a [Square] to access its pieces. -impl std::ops::Index for ChessBoardBuilder { - type Output = Option<(Piece, Color)>; - - fn index(&self, square: Square) -> &Self::Output { - &self.pieces[square.index()] - } -} - -/// Index a [ChessBoardBuilder] with a [Square] to access its pieces. -impl std::ops::IndexMut for ChessBoardBuilder { - fn index_mut(&mut self, square: Square) -> &mut Self::Output { - &mut self.pieces[square.index()] - } -} - -impl TryFrom for ChessBoard { - type Error = ValidationError; - - fn try_from(builder: ChessBoardBuilder) -> Result { - let mut piece_occupancy: [Bitboard; Piece::NUM_VARIANTS] = Default::default(); - let mut color_occupancy: [Bitboard; Color::NUM_VARIANTS] = Default::default(); - let mut combined_occupancy: Bitboard = Default::default(); - let ChessBoardBuilder { - pieces, - castle_rights, - en_passant, - half_move_clock, - side, - turn_count, - } = builder; - - for square in Square::iter() { - let Some((piece, color)) = pieces[square.index()] else { - continue; - }; - piece_occupancy[piece.index()] |= square; - color_occupancy[color.index()] |= square; - combined_occupancy |= square; - } - - let total_plies = (turn_count - 1) * 2 + if side == Color::White { 0 } else { 1 }; - - let board = ChessBoard { - piece_occupancy, - color_occupancy, - combined_occupancy, - castle_rights, - en_passant, - half_move_clock, - total_plies, - side, - }; - - board.validate()?; - Ok(board) - } -} - -#[cfg(test)] -mod test { - use super::*; - - fn from_board(board: &ChessBoard) -> ChessBoardBuilder { - let mut builder = ChessBoardBuilder::new(); - - for piece in Piece::iter() { - for color in Color::iter() { - for square in board.occupancy(piece, color) { - builder[square] = Some((piece, color)); - } - } - } - - for color in Color::iter() { - builder.with_castle_rights(board.castle_rights(color), color); - } - - if let Some(square) = board.en_passant() { - builder.with_en_passant(square); - } else { - builder.without_en_passant(); - } - - builder - .with_half_move_clock(board.half_move_clock()) - .with_turn_count(board.total_plies() / 2 + 1) - .with_current_player(board.current_player()); - - builder - } - - #[test] - fn default_board() { - let board = ChessBoard::default(); - let builder = from_board(&board); - assert_eq!(board, builder.try_into().unwrap()) - } -} diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs deleted file mode 100644 index b1ed769..0000000 --- a/src/board/chess_board/error.rs +++ /dev/null @@ -1,56 +0,0 @@ -/// A singular type for all errors that could happen during [crate::board::ChessBoard::is_valid]. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum ValidationError { - /// Too many pieces. - TooManyPieces, - /// Missing king. - MissingKing, - /// Pawns on the first/last rank. - InvalidPawnPosition, - /// Castling rights do not match up with the state of the board. - InvalidCastlingRights, - /// En-passant target square is not empty, behind an opponent's pawn, on the correct rank. - InvalidEnPassant, - /// The two kings are next to each other. - NeighbouringKings, - /// The opponent is currently in check. - OpponentInCheck, - /// The piece-specific boards are overlapping. - OverlappingPieces, - /// The color-specific boards are overlapping. - OverlappingColors, - /// The pre-computed combined occupancy boards does not match the other boards. - ErroneousCombinedOccupancy, - /// Half-move clock is higher than total number of plies. - HalfMoveClockTooHigh, - /// The total plie count does not match the current player. - IncoherentPlieCount, -} - -impl std::fmt::Display for ValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let error_msg = match self { - Self::TooManyPieces => "too many pieces", - Self::MissingKing => "missing king", - Self::InvalidPawnPosition => "pawns on the first/last rank", - Self::InvalidCastlingRights => { - "castling rights do not match up with the state of the board" - } - Self::InvalidEnPassant => { - "en-passant target square is not empty, behind an opponent's pawn, on the correct rank" - } - Self::NeighbouringKings => "the two kings are next to each other", - Self::OpponentInCheck => "the opponent is currently in check", - Self::OverlappingPieces => "the piece-specific boards are overlapping", - Self::OverlappingColors => "the color-specific boards are overlapping", - Self::ErroneousCombinedOccupancy => { - "the pre-computed combined occupancy boards does not match the other boards" - } - Self::HalfMoveClockTooHigh => "half-move clock is higher than total number of plies", - Self::IncoherentPlieCount => "the total plie count does not match the current player", - }; - write!(f, "{}", error_msg) - } -} - -impl std::error::Error for ValidationError {} diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs deleted file mode 100644 index f3a92e9..0000000 --- a/src/board/chess_board/mod.rs +++ /dev/null @@ -1,828 +0,0 @@ -use crate::movegen; - -use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; - -mod builder; -pub use builder::*; - -mod error; -pub use error::*; - -/// Represent an on-going chess game. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct ChessBoard { - /// A [Bitboard] of occupancy for each piece type, discarding color. Indexed by [Piece::index]. - piece_occupancy: [Bitboard; Piece::NUM_VARIANTS], - /// A [Bitboard] of occupancy for each color, discarding piece type. Indexed by [Piece::index]. - color_occupancy: [Bitboard; Color::NUM_VARIANTS], - /// A [Bitboard] representing all squares currently occupied by a piece. - combined_occupancy: Bitboard, - /// The allowed [CastleRights] for either color. Indexed by [Color::index]. - castle_rights: [CastleRights; Color::NUM_VARIANTS], - /// A potential en-passant attack. - /// Either `None` if no double-step pawn move was made in the previous half-turn, or - /// `Some(target_square)` if a double-step move was made. - en_passant: Option, - /// The number of half-turns without either a pawn push or capture. - half_move_clock: u32, // Should *probably* never go higher than 100. - /// The number of half-turns so far. - total_plies: u32, // Should be plenty. - /// The current player turn. - side: Color, -} - -/// The state which can't be reversed when doing/un-doing a [Move]. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct NonReversibleState { - castle_rights: [CastleRights; Color::NUM_VARIANTS], - en_passant: Option, - half_move_clock: u32, // Should *probably* never go higher than 100. - captured_piece: Option, -} - -impl ChessBoard { - /// Which player's turn is it. - #[inline(always)] - pub fn current_player(&self) -> Color { - self.side - } - - /// Return the target [Square] that can be captured en-passant, or `None` - #[inline(always)] - pub fn en_passant(&self) -> Option { - self.en_passant - } - - /// Return the [CastleRights] for the given [Color]. - #[inline(always)] - pub fn castle_rights(&self, color: Color) -> CastleRights { - self.castle_rights[color.index()] - } - - /// Return the [CastleRights] for the given [Color]. Allow mutations. - #[inline(always)] - fn castle_rights_mut(&mut self, color: Color) -> &mut CastleRights { - &mut self.castle_rights[color.index()] - } - - /// Get the [Bitboard] representing all pieces of the given [Piece] and [Color] type. - #[inline(always)] - pub fn occupancy(&self, piece: Piece, color: Color) -> Bitboard { - self.piece_occupancy(piece) & self.color_occupancy(color) - } - - /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. - #[inline(always)] - pub fn piece_occupancy(&self, piece: Piece) -> Bitboard { - self.piece_occupancy[piece.index()] - } - - /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. - /// Allow mutating the state. - #[inline(always)] - fn piece_occupancy_mut(&mut self, piece: Piece) -> &mut Bitboard { - &mut self.piece_occupancy[piece.index()] - } - - /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece - /// type. - #[inline(always)] - pub fn color_occupancy(&self, color: Color) -> Bitboard { - self.color_occupancy[color.index()] - } - - /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece - /// type. Allow mutating the state. - #[inline(always)] - fn color_occupancy_mut(&mut self, color: Color) -> &mut Bitboard { - &mut self.color_occupancy[color.index()] - } - - /// Get the [Bitboard] representing all pieces on the board. - #[inline(always)] - pub fn combined_occupancy(&self) -> Bitboard { - self.combined_occupancy - } - - /// Return the number of half-turns without either a pawn push or a capture. - #[inline(always)] - pub fn half_move_clock(&self) -> u32 { - self.half_move_clock - } - - /// Return the total number of plies (i.e: half-turns) played so far. - #[inline(always)] - pub fn total_plies(&self) -> u32 { - self.total_plies - } - - /// Return the [Bitboard] corresponding to all the opponent's pieces threatening the current - /// player's king. - #[inline(always)] - pub fn checkers(&self) -> Bitboard { - self.compute_checkers(self.current_player()) - } - - /// Quickly add/remove a piece on the [Bitboard]s that are part of the [ChessBoard] state. - #[inline(always)] - fn xor(&mut self, color: Color, piece: Piece, square: Square) { - *self.piece_occupancy_mut(piece) ^= square; - *self.color_occupancy_mut(color) ^= square; - self.combined_occupancy ^= square; - } - - /// Compute the change of [CastleRights] from moving/taking a piece. - fn update_castling(&mut self, color: Color, piece: Piece, file: File) { - let original = self.castle_rights(color); - let new_rights = match (piece, file) { - (Piece::Rook, File::A) => original.without_queen_side(), - (Piece::Rook, File::H) => original.without_king_side(), - (Piece::King, _) => CastleRights::NoSide, - _ => return, - }; - if new_rights != original { - *self.castle_rights_mut(color) = new_rights; - } - } - - /// Play the given [Move], return a copy of the board with the resulting state. - #[inline(always)] - pub fn play_move(&self, chess_move: Move) -> Self { - let mut res = self.clone(); - res.play_move_inplace(chess_move); - res - } - - /// Play the given [Move] in place, returning all non-revertible state (e.g: en-passant, - /// etc...). - #[inline(always)] - pub fn play_move_inplace(&mut self, chess_move: Move) -> NonReversibleState { - let opponent = !self.current_player(); - let move_piece = Piece::iter() - .find(|&p| !(self.piece_occupancy(p) & chess_move.start()).is_empty()) - .unwrap(); - let captured_piece = Piece::iter() - .skip(1) // No need to check for the king here - .find(|&p| !(self.occupancy(p, opponent) & chess_move.destination()).is_empty()); - let is_double_step = move_piece == Piece::Pawn - && chess_move.start().rank() == self.current_player().second_rank() - && chess_move.destination().rank() == self.current_player().fourth_rank(); - - // Save non-revertible state - let state = NonReversibleState { - castle_rights: self.castle_rights, - en_passant: self.en_passant, - half_move_clock: self.half_move_clock, - captured_piece, - }; - - // Non-revertible state modification - if captured_piece.is_some() || move_piece == Piece::Pawn { - self.half_move_clock = 0; - } else { - self.half_move_clock += 1; - } - if is_double_step { - let target_square = Square::new( - chess_move.destination().file(), - self.current_player().third_rank(), - ); - self.en_passant = Some(target_square); - } else { - self.en_passant = None; - } - self.update_castling(self.current_player(), move_piece, chess_move.start().file()); - if let Some(piece) = captured_piece { - self.xor(opponent, piece, chess_move.destination()); - // If a rook is captured, it loses its castling rights - self.update_castling(opponent, piece, chess_move.destination().file()); - } - - // Revertible state modification - let dest_piece = chess_move.promotion().unwrap_or(move_piece); - self.xor(self.current_player(), move_piece, chess_move.start()); - self.xor(self.current_player(), dest_piece, chess_move.destination()); - self.total_plies += 1; - self.side = !self.side; - - state - } - - /// Reverse the effect of playing the given [Move], and return to the given - /// [NonReversibleState]. - #[inline(always)] - pub fn unplay_move(&mut self, chess_move: Move, previous: NonReversibleState) { - // Restore non-revertible state - self.castle_rights = previous.castle_rights; - self.en_passant = previous.en_passant; - self.half_move_clock = previous.half_move_clock; - - let move_piece = Piece::iter() - // We're looking for the *destination* as this is *undoing* the move - .find(|&p| !(self.piece_occupancy(p) & chess_move.destination()).is_empty()) - .unwrap(); - - if let Some(piece) = previous.captured_piece { - // The capture affected the *current* player, from our post-move POV - self.xor(self.current_player(), piece, chess_move.destination()); - } - - // Restore revertible state - let start_piece = chess_move.promotion().map_or(move_piece, |_| Piece::Pawn); - self.xor(!self.current_player(), move_piece, chess_move.destination()); - self.xor(!self.current_player(), start_piece, chess_move.start()); - self.total_plies -= 1; - self.side = !self.side; - } - - /// Return true if the current state of the board looks valid, false if something is definitely - /// wrong. - pub fn is_valid(&self) -> bool { - self.validate().is_ok() - } - - /// Validate the state of the board. Return Err([ValidationError]) if an issue is found. - pub fn validate(&self) -> Result<(), ValidationError> { - // The current plie count should be odd on white's turn, and vice-versa. - if self.total_plies() % 2 != self.current_player().index() as u32 { - return Err(ValidationError::IncoherentPlieCount); - } - - // Make sure the clocks are in agreement. - if self.half_move_clock() > self.total_plies() { - return Err(ValidationError::HalfMoveClockTooHigh); - } - - // Don't overlap pieces. - for piece in Piece::iter() { - #[allow(clippy::collapsible_if)] - for other in Piece::iter() { - if piece != other { - if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() { - return Err(ValidationError::OverlappingPieces); - } - } - } - } - - // Don't overlap colors. - if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() { - return Err(ValidationError::OverlappingColors); - } - - // Calculate the union of all pieces. - let combined = - Piece::iter().fold(Bitboard::EMPTY, |board, p| board | self.piece_occupancy(p)); - - // Ensure that the pre-computed version is accurate. - if combined != self.combined_occupancy() { - return Err(ValidationError::ErroneousCombinedOccupancy); - } - - // Ensure that all pieces belong to a color, and no color has pieces that don't exist. - if combined != (self.color_occupancy(Color::White) | self.color_occupancy(Color::Black)) { - return Err(ValidationError::ErroneousCombinedOccupancy); - } - - for color in Color::iter() { - for piece in Piece::iter() { - // Check that we have the expected number of piecese. - let count = self.occupancy(piece, color).count(); - let possible = match piece { - Piece::King => count <= 1, - Piece::Pawn => count <= 8, - Piece::Queen => count <= 9, - _ => count <= 10, - }; - if !possible { - return Err(ValidationError::TooManyPieces); - } - } - - // Check that we have a king - if self.occupancy(Piece::King, color).count() != 1 { - return Err(ValidationError::MissingKing); - } - - // Check that don't have too many pieces in total - if self.color_occupancy(color).count() > 16 { - return Err(ValidationError::TooManyPieces); - } - } - - // Check that pawns aren't in first/last rank. - if !(self.piece_occupancy(Piece::Pawn) - & (Rank::First.into_bitboard() | Rank::Eighth.into_bitboard())) - .is_empty() - { - return Err(ValidationError::InvalidPawnPosition); - } - - // Verify that rooks and kings that are allowed to castle have not been moved. - for color in Color::iter() { - let castle_rights = self.castle_rights(color); - - // Nothing to check if there are no castlings allowed. - if castle_rights == CastleRights::NoSide { - continue; - } - - let actual_rooks = self.occupancy(Piece::Rook, color); - let expected_rooks = castle_rights.unmoved_rooks(color); - // We must check the intersection, in case there are more than 2 rooks on the board. - if (expected_rooks & actual_rooks) != expected_rooks { - return Err(ValidationError::InvalidCastlingRights); - } - - let actual_king = self.occupancy(Piece::King, color); - let expected_king = Square::new(File::E, color.first_rank()); - // We have checked that there is exactly one king, no need for intersecting the sets. - if actual_king != expected_king.into_bitboard() { - return Err(ValidationError::InvalidCastlingRights); - } - } - - // En-passant validation - if let Some(square) = self.en_passant() { - // Must be empty - if !(self.combined_occupancy() & square).is_empty() { - return Err(ValidationError::InvalidEnPassant); - } - - let opponent = !self.current_player(); - - // Must be on the opponent's third rank - if (square & opponent.third_rank().into_bitboard()).is_empty() { - return Err(ValidationError::InvalidEnPassant); - } - - // Must be behind a pawn - let opponent_pawns = self.occupancy(Piece::Pawn, opponent); - let double_pushed_pawn = self - .current_player() - .backward_direction() - .move_board(square.into_bitboard()); - if (opponent_pawns & double_pushed_pawn).is_empty() { - return Err(ValidationError::InvalidEnPassant); - } - } - - // Check that kings don't touch each other. - let white_king = self.occupancy(Piece::King, Color::White); - let black_king = self.occupancy(Piece::King, Color::Black); - // Unwrap is fine, we already checked that there is exactly one king of each color - if !(movegen::king_moves(white_king.try_into().unwrap()) & black_king).is_empty() { - return Err(ValidationError::NeighbouringKings); - } - - // Check that the opponent is not currently in check. - if !self.compute_checkers(!self.current_player()).is_empty() { - return Err(ValidationError::OpponentInCheck); - } - - Ok(()) - } - - /// Compute all pieces that are currently threatening the given [Color]'s king. - fn compute_checkers(&self, color: Color) -> Bitboard { - // Unwrap is fine, there should always be exactly one king per color - let king = (self.occupancy(Piece::King, color)).try_into().unwrap(); - - let opponent = !color; - - // No need to remove our pieces from the generated moves, we just want to check if we - // intersect with the opponent's pieces, rather than generate only valid moves. - let bishops = { - let queens = self.occupancy(Piece::Queen, opponent); - let bishops = self.occupancy(Piece::Bishop, opponent); - let bishop_attacks = movegen::bishop_moves(king, self.combined_occupancy()); - (queens | bishops) & bishop_attacks - }; - let rooks = { - let queens = self.occupancy(Piece::Queen, opponent); - let rooks = self.occupancy(Piece::Rook, opponent); - let rook_attacks = movegen::rook_moves(king, self.combined_occupancy()); - (queens | rooks) & rook_attacks - }; - let knights = { - let knights = self.occupancy(Piece::Knight, opponent); - let knight_attacks = movegen::knight_moves(king); - knights & knight_attacks - }; - let pawns = { - let pawns = self.occupancy(Piece::Pawn, opponent); - let pawn_attacks = movegen::pawn_attacks(color, king); - pawns & pawn_attacks - }; - - bishops | rooks | knights | pawns - } -} - -/// Use the starting position as a default value, corresponding to the -/// "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" FEN string -impl Default for ChessBoard { - fn default() -> Self { - Self { - piece_occupancy: [ - // King - Square::E1 | Square::E8, - // Queen - Square::D1 | Square::D8, - // Rook - Square::A1 | Square::A8 | Square::H1 | Square::H8, - // Bishop - Square::C1 | Square::C8 | Square::F1 | Square::F8, - // Knight - Square::B1 | Square::B8 | Square::G1 | Square::G8, - // Pawn - Rank::Second.into_bitboard() | Rank::Seventh.into_bitboard(), - ], - color_occupancy: [ - Rank::First.into_bitboard() | Rank::Second.into_bitboard(), - Rank::Seventh.into_bitboard() | Rank::Eighth.into_bitboard(), - ], - combined_occupancy: Rank::First.into_bitboard() - | Rank::Second.into_bitboard() - | Rank::Seventh.into_bitboard() - | Rank::Eighth.into_bitboard(), - castle_rights: [CastleRights::BothSides; Color::NUM_VARIANTS], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, - } - } -} - -#[cfg(test)] -mod test { - use crate::fen::FromFen; - - use super::*; - - #[test] - fn valid() { - let default_position = ChessBoard::default(); - assert!(default_position.is_valid()); - } - - #[test] - fn invalid_incoherent_plie_count() { - let position = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - let mut board = TryInto::::try_into(builder).unwrap(); - board.total_plies = 1; - board - }; - assert_eq!( - position.validate().err().unwrap(), - ValidationError::IncoherentPlieCount, - ); - } - - #[test] - fn invalid_half_moves_clock() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - builder.with_half_move_clock(10); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::HalfMoveClockTooHigh); - } - - #[test] - fn invalid_overlapping_pieces() { - let position = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - let mut board: ChessBoard = builder.try_into().unwrap(); - *board.piece_occupancy_mut(Piece::Queen) |= Square::E1.into_bitboard(); - board - }; - assert_eq!( - position.validate().err().unwrap(), - ValidationError::OverlappingPieces, - ); - } - - #[test] - fn invalid_overlapping_colors() { - let position = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - let mut board: ChessBoard = builder.try_into().unwrap(); - *board.color_occupancy_mut(Color::White) |= Square::E8.into_bitboard(); - board - }; - assert_eq!( - position.validate().err().unwrap(), - ValidationError::OverlappingColors, - ); - } - - #[test] - fn invalid_combined_does_not_equal_pieces() { - let position = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - let mut board: ChessBoard = builder.try_into().unwrap(); - *board.piece_occupancy_mut(Piece::Pawn) |= Square::E2.into_bitboard(); - board - }; - assert_eq!( - position.validate().err().unwrap(), - ValidationError::ErroneousCombinedOccupancy, - ); - } - - #[test] - fn invalid_combined_does_not_equal_colors() { - let position = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - let mut board: ChessBoard = builder.try_into().unwrap(); - *board.color_occupancy_mut(Color::Black) |= Square::E2.into_bitboard(); - board - }; - assert_eq!( - position.validate().err().unwrap(), - ValidationError::ErroneousCombinedOccupancy, - ); - } - - #[test] - fn invalid_multiple_kings() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E2] = Some((Piece::King, Color::White)); - builder[Square::E7] = Some((Piece::King, Color::Black)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces); - } - - #[test] - fn invalid_castling_rights_no_rooks() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - builder.with_castle_rights(CastleRights::BothSides, Color::White); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights); - } - - #[test] - fn invalid_castling_rights_moved_king() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E2] = Some((Piece::King, Color::White)); - builder[Square::A1] = Some((Piece::Rook, Color::White)); - builder[Square::H1] = Some((Piece::Rook, Color::White)); - builder[Square::E7] = Some((Piece::King, Color::Black)); - builder[Square::A8] = Some((Piece::Rook, Color::Black)); - builder[Square::H8] = Some((Piece::Rook, Color::Black)); - builder.with_castle_rights(CastleRights::BothSides, Color::White); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights); - } - - #[test] - fn valid_en_passant() { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - builder[Square::A5] = Some((Piece::Pawn, Color::Black)); - builder.with_en_passant(Square::A6); - TryInto::::try_into(builder).unwrap(); - } - - #[test] - fn invalid_en_passant_not_empty() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - builder[Square::A6] = Some((Piece::Rook, Color::Black)); - builder[Square::A5] = Some((Piece::Pawn, Color::Black)); - builder.with_en_passant(Square::A6); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant); - } - - #[test] - fn invalid_en_passant_not_behind_pawn() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - builder[Square::A5] = Some((Piece::Rook, Color::Black)); - builder.with_en_passant(Square::A6); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant); - } - - #[test] - fn invalid_en_passant_incorrect_rank() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - builder[Square::A4] = Some((Piece::Pawn, Color::Black)); - builder.with_en_passant(Square::A5); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant); - } - - #[test] - fn invalid_kings_next_to_each_other() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E2] = Some((Piece::King, Color::Black)); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::NeighbouringKings); - } - - #[test] - fn invalid_opponent_in_check() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::E1] = Some((Piece::King, Color::White)); - builder[Square::E7] = Some((Piece::Queen, Color::White)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::OpponentInCheck); - } - - #[test] - fn invalid_pawn_on_first_rank() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::H1] = Some((Piece::King, Color::White)); - builder[Square::A1] = Some((Piece::Pawn, Color::White)); - builder[Square::H8] = Some((Piece::King, Color::Black)); - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::InvalidPawnPosition); - } - - #[test] - fn invalid_too_many_pieces() { - let res = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::H1] = Some((Piece::King, Color::White)); - builder[Square::H8] = Some((Piece::King, Color::Black)); - for square in (File::B.into_bitboard() | File::C.into_bitboard()).into_iter() { - builder[square] = Some((Piece::Pawn, Color::White)); - } - for square in (File::F.into_bitboard() | File::G.into_bitboard()).into_iter() { - builder[square] = Some((Piece::Pawn, Color::Black)); - } - TryInto::::try_into(builder) - }; - assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces); - } - - #[test] - fn checkers() { - let position = { - let mut builder = ChessBoardBuilder::new(); - builder[Square::C1] = Some((Piece::Knight, Color::White)); - builder[Square::D3] = Some((Piece::Bishop, Color::White)); - builder[Square::E1] = Some((Piece::Rook, Color::White)); - builder[Square::E2] = Some((Piece::King, Color::White)); - builder[Square::H2] = Some((Piece::Queen, Color::White)); - builder[Square::G1] = Some((Piece::Knight, Color::Black)); - builder[Square::F3] = Some((Piece::Bishop, Color::Black)); - builder[Square::A2] = Some((Piece::Rook, Color::Black)); - builder[Square::E8] = Some((Piece::King, Color::Black)); - builder[Square::E7] = Some((Piece::Queen, Color::Black)); - TryInto::::try_into(builder).unwrap() - }; - assert_eq!( - position.checkers(), - Square::A2 | Square::E7 | Square::F3 | Square::G1 - ); - } - - #[test] - fn play_move() { - // Start from default position - let mut position = ChessBoard::default(); - // Modify it to account for e4 move - position.play_move_inplace(Move::new(Square::E2, Square::E4, None)); - assert_eq!( - position, - ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") - .unwrap() - ); - // And now c5 - position.play_move_inplace(Move::new(Square::C7, Square::C5, None)); - assert_eq!( - position, - ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") - .unwrap() - ); - // Finally, Nf3 - position.play_move_inplace(Move::new(Square::G1, Square::F3, None)); - assert_eq!( - position, - ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") - .unwrap() - ); - } - - #[test] - fn play_move_capture_changes_castling() { - let mut position = ChessBoard::from_fen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1").unwrap(); - let expected = ChessBoard::from_fen("r3k2R/8/8/8/8/8/8/R3K3 b Qq - 0 1").unwrap(); - - let capture = Move::new(Square::H1, Square::H8, None); - - position.play_move_inplace(capture); - assert_eq!(position, expected); - } - - #[test] - fn play_move_and_undo() { - // Start from default position - let mut position = ChessBoard::default(); - // Modify it to account for e4 move - let move_1 = Move::new(Square::E2, Square::E4, None); - let state_1 = position.play_move_inplace(move_1); - // And now c5 - let move_2 = Move::new(Square::C7, Square::C5, None); - let state_2 = position.play_move_inplace(move_2); - // Finally, Nf3 - let move_3 = Move::new(Square::G1, Square::F3, None); - let state_3 = position.play_move_inplace(move_3); - // Now revert each move one-by-one - position.unplay_move(move_3, state_3); - assert_eq!( - position, - ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") - .unwrap() - ); - position.unplay_move(move_2, state_2); - assert_eq!( - position, - ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") - .unwrap() - ); - position.unplay_move(move_1, state_1); - assert_eq!( - position, - ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") - .unwrap() - ); - } - - #[test] - fn play_move_undo_capture() { - let mut position = ChessBoard::from_fen("3q3k/8/8/8/8/8/8/K2Q4 w - - 0 1").unwrap(); - let expected = ChessBoard::from_fen("3Q3k/8/8/8/8/8/8/K7 b - - 0 1").unwrap(); - let original = position.clone(); - - let capture = Move::new(Square::D1, Square::D8, None); - - let state = position.play_move_inplace(capture); - assert_eq!(position, expected); - - position.unplay_move(capture, state); - assert_eq!(position, original); - } - - #[test] - fn play_move_undo_promotion() { - let mut position = ChessBoard::from_fen("7k/P7/8/8/8/8/8/K7 w - - 0 1").unwrap(); - let expected = ChessBoard::from_fen("N6k/8/8/8/8/8/8/K7 b - - 0 1").unwrap(); - let original = position.clone(); - - let promotion = Move::new(Square::A7, Square::A8, Some(Piece::Knight)); - - let state = position.play_move_inplace(promotion); - assert_eq!(position, expected); - - position.unplay_move(promotion, state); - assert_eq!(position, original); - } -} diff --git a/src/board/color.rs b/src/board/color.rs index e41d3c5..828b1cf 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -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)] @@ -18,28 +19,15 @@ impl Color { Self::ALL.iter().cloned() } - /// Convert from a color index into a [Color] type. - /// - /// # Panics - /// - /// Panics if the index is out of bounds. + /// Convert from a piece index into a [Color] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - Self::try_from_index(index).expect("index out of bouds") + assert!(index < Self::NUM_VARIANTS); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } } - /// Convert from a color index into a [Color] type. Returns [None] if the index is out of - /// bounds. - pub fn try_from_index(index: usize) -> Option { - if index < Self::NUM_VARIANTS { - // SAFETY: we know the value is in-bounds - Some(unsafe { Self::from_index_unchecked(index) }) - } else { - None - } - } - - /// Convert from a color index into a [Color] type, no bounds checking. + /// Convert from a piece index into a [Color] type, no bounds checking. /// /// # Safety /// @@ -119,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 { + 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; diff --git a/src/board/direction.rs b/src/board/direction.rs index 40c8d69..6b350a0 100644 --- a/src/board/direction.rs +++ b/src/board/direction.rs @@ -128,7 +128,8 @@ impl Direction { /// Slide a board along the given [Direction], i.e: return all successive applications of /// [Direction::move_board] until no new squares can be reached. /// Take into account the `blockers` [Bitboard]: a combination of all pieces on the board which - /// cannot be slid over. The slide is over once a square that is part of `blockers` is reached. + /// cannot be slided over. The slide is over once a square that is part of `blockers` is + /// reached. /// It does not make sense to use this method with knight-only directions, and it will panic in /// debug-mode if it happens. #[inline(always)] diff --git a/src/board/fen.rs b/src/board/fen.rs new file mode 100644 index 0000000..f112bc9 --- /dev/null +++ b/src/board/fen.rs @@ -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; +} diff --git a/src/board/file.rs b/src/board/file.rs index 1641498..4c84f20 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -35,23 +35,11 @@ impl File { } /// Convert from a file index into a [File] type. - /// - /// # Panics - /// - /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { - Self::try_from_index(index).expect("index out of bouds") - } - - /// Convert from a file index into a [File] type. Returns [None] if the index is out of bounds. - pub fn try_from_index(index: usize) -> Option { - if index < Self::NUM_VARIANTS { - // SAFETY: we know the value is in-bounds - Some(unsafe { Self::from_index_unchecked(index) }) - } else { - None - } + assert!(index < 8); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } } /// Convert from a file index into a [File] type, no bounds checking. diff --git a/src/board/mod.rs b/src/board/mod.rs index 0e34331..866b920 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -13,6 +13,9 @@ pub use color::*; pub mod direction; pub use direction::*; +pub mod fen; +pub use fen::*; + pub mod file; pub use file::*; diff --git a/src/board/move.rs b/src/board/move.rs index 7897988..60672f8 100644 --- a/src/board/move.rs +++ b/src/board/move.rs @@ -1,42 +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 { - start: Square, - destination: Square, - promotion: Option, +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, + pub promotion: Option, + pub en_passant: bool, + pub double_step: bool, + pub castling: bool, +} + +impl From 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)] - pub fn new(start: Square, destination: Square, promotion: Option) -> Self { - Self { - start, - destination, - promotion, - } + fn new( + piece: Piece, + start: Square, + destination: Square, + capture: Option, + promotion: Option, + 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 { - self.start + 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 { - self.destination + 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 { + 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 { - self.promotion + 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()); } } diff --git a/src/board/piece.rs b/src/board/piece.rs index f6fdce4..6c35288 100644 --- a/src/board/piece.rs +++ b/src/board/piece.rs @@ -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 { @@ -28,24 +31,11 @@ impl Piece { } /// Convert from a piece index into a [Piece] type. - /// - /// # Panics - /// - /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { - Self::try_from_index(index).expect("index out of bouds") - } - - /// Convert from a piece index into a [Piece] type. Returns [None] if the index is out of - /// bounds. - pub fn try_from_index(index: usize) -> Option { - if index < Self::NUM_VARIANTS { - // SAFETY: we know the value is in-bounds - Some(unsafe { Self::from_index_unchecked(index) }) - } else { - None - } + assert!(index < Self::NUM_VARIANTS); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } } /// Convert from a piece index into a [Piece] type, no bounds checking. @@ -65,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 { + 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::*; diff --git a/src/board/rank.rs b/src/board/rank.rs index 1632229..a0b04d3 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -35,23 +35,11 @@ impl Rank { } /// Convert from a rank index into a [Rank] type. - /// - /// # Panics - /// - /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { - Self::try_from_index(index).expect("index out of bouds") - } - - /// Convert from a rank index into a [Rank] type. Returns [None] if the index is out of bounds. - pub fn try_from_index(index: usize) -> Option { - if index < Self::NUM_VARIANTS { - // SAFETY: we know the value is in-bounds - Some(unsafe { Self::from_index_unchecked(index) }) - } else { - None - } + assert!(index < 8); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } } /// Convert from a rank index into a [Rank] type, no bounds checking. diff --git a/src/board/square.rs b/src/board/square.rs index b5de25b..9fbd5ee 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -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. @@ -39,10 +39,6 @@ impl Square { ]; /// Construct a [Square] from a [File] and [Rank]. - /// - /// # Panics - /// - /// Panics if the index is out of bounds. #[inline(always)] pub fn new(file: File, rank: Rank) -> Self { // SAFETY: we know the value is in-bounds @@ -57,18 +53,9 @@ impl Square { /// Convert from a square index into a [Square] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - Self::try_from_index(index).expect("index out of bouds") - } - - /// Convert from a square index into a [Square] type. Returns [None] if the index is out of - /// bounds. - pub fn try_from_index(index: usize) -> Option { - if index < Self::NUM_VARIANTS { - // SAFETY: we know the value is in-bounds - Some(unsafe { Self::from_index_unchecked(index) }) - } else { - None - } + assert!(index < 64); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } } /// Convert from a square index into a [Square] type, no bounds checking. @@ -120,6 +107,23 @@ impl Square { } } +/// Convert an en-passant target square segment of a FEN string to an optional [Square]. +impl FromFen for Option { + type Err = Error; + + fn from_fen(s: &str) -> Result { + 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 for Square { type Output = Square; diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..c0bcdc6 --- /dev/null +++ b/src/build.rs @@ -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(()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..f95a38c --- /dev/null +++ b/src/error.rs @@ -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 {} diff --git a/src/fen.rs b/src/fen.rs deleted file mode 100644 index 8aede73..0000000 --- a/src/fen.rs +++ /dev/null @@ -1,244 +0,0 @@ -use crate::board::{ - CastleRights, ChessBoard, ChessBoardBuilder, Color, File, Piece, Rank, Square, ValidationError, -}; - -/// 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; -} - -/// A singular type for all errors that could happen during FEN parsing. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum FenError { - /// Invalid FEN input. - InvalidFen, - /// Invalid chess position. - InvalidPosition(ValidationError), -} - -impl std::fmt::Display for FenError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::InvalidFen => write!(f, "invalid FEN input"), - Self::InvalidPosition(err) => write!(f, "invalid chess position: {}", err), - } - } -} - -impl std::error::Error for FenError {} - -/// Allow converting a [ValidationError] into [FenError], for use with the '?' operator. -impl From for FenError { - fn from(err: ValidationError) -> Self { - Self::InvalidPosition(err) - } -} - -/// Convert the castling rights segment of a FEN string to an array of [CastleRights]. -impl FromFen for [CastleRights; Color::NUM_VARIANTS] { - type Err = FenError; - - fn from_fen(s: &str) -> Result { - if s.len() > 4 { - return Err(FenError::InvalidFen); - } - - let mut res = [CastleRights::NoSide; Color::NUM_VARIANTS]; - - 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(FenError::InvalidFen), - } - } - - Ok(res) - } -} - -/// Convert a side to move segment of a FEN string to a [Color]. -impl FromFen for Color { - type Err = FenError; - - fn from_fen(s: &str) -> Result { - let res = match s { - "w" => Color::White, - "b" => Color::Black, - _ => return Err(FenError::InvalidFen), - }; - Ok(res) - } -} - -/// Convert an en-passant target square segment of a FEN string to an optional [Square]. -impl FromFen for Option { - type Err = FenError; - - fn from_fen(s: &str) -> Result { - 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(FenError::InvalidFen), - }; - Ok(res) - } -} - -/// Convert a piece in FEN notation to a [Piece]. -impl FromFen for Piece { - type Err = FenError; - - fn from_fen(s: &str) -> Result { - 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(FenError::InvalidFen), - }; - Ok(res) - } -} - -/// Return a [ChessBoard] from the given FEN string. -impl FromFen for ChessBoard { - type Err = FenError; - - fn from_fen(s: &str) -> Result { - let mut split = s.split_ascii_whitespace(); - - let piece_placement = split.next().ok_or(FenError::InvalidFen)?; - let side_to_move = split.next().ok_or(FenError::InvalidFen)?; - let castling_rights = split.next().ok_or(FenError::InvalidFen)?; - let en_passant_square = split.next().ok_or(FenError::InvalidFen)?; - let half_move_clock = split.next().ok_or(FenError::InvalidFen)?; - let full_move_counter = split.next().ok_or(FenError::InvalidFen)?; - - let mut builder = ChessBoardBuilder::new(); - - let castle_rights = <[CastleRights; Color::NUM_VARIANTS]>::from_fen(castling_rights)?; - for color in Color::iter() { - builder.with_castle_rights(castle_rights[color.index()], color); - } - - builder.with_current_player(FromFen::from_fen(side_to_move)?); - - if let Some(square) = FromFen::from_fen(en_passant_square)? { - builder.with_en_passant(square); - }; - - let half_move_clock = half_move_clock - .parse::<_>() - .map_err(|_| FenError::InvalidFen)?; - builder.with_half_move_clock(half_move_clock); - - let full_move_counter = full_move_counter - .parse::<_>() - .map_err(|_| FenError::InvalidFen)?; - builder.with_turn_count(full_move_counter); - - { - let mut rank: usize = 8; - for rank_str in piece_placement.split('/') { - rank -= 1; - let mut file: usize = 0; - for c in rank_str.chars() { - let color = if c.is_uppercase() { - Color::White - } else { - Color::Black - }; - let piece = match c { - digit @ '1'..='8' => { - // Unwrap is fine since this arm is only matched by digits - file += digit.to_digit(10).unwrap() as usize; - continue; - } - _ => FromFen::from_fen(&c.to_string())?, - }; - - // Only need to worry about underflow since those are `usize` values. - if file >= 8 || rank >= 8 { - return Err(FenError::InvalidFen); - }; - - let square = Square::new(File::from_index(file), Rank::from_index(rank)); - - builder[square] = Some((piece, color)); - file += 1; - } - // We haven't read exactly 8 files. - if file != 8 { - return Err(FenError::InvalidFen); - } - } - // We haven't read exactly 8 ranks - if rank != 0 { - return Err(FenError::InvalidFen); - } - }; - - Ok(builder.try_into()?) - } -} - -#[cfg(test)] -mod test { - use crate::board::Move; - - use super::*; - - #[test] - fn default_position() { - let default_position = ChessBoard::default(); - assert_eq!( - ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") - .unwrap(), - default_position - ); - } - - #[test] - fn en_passant() { - // Start from default position - let mut position = ChessBoard::default(); - position.play_move_inplace(Move::new(Square::E2, Square::E4, None)); - assert_eq!( - ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") - .unwrap(), - position - ); - // And now c5 - position.play_move_inplace(Move::new(Square::C7, Square::C5, None)); - assert_eq!( - ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") - .unwrap(), - position - ); - // Finally, Nf3 - position.play_move_inplace(Move::new(Square::G1, Square::F3, None)); - assert_eq!( - ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") - .unwrap(), - position - ); - } -} diff --git a/src/lib.rs b/src/lib.rs index 82467ad..c1b793f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ pub mod board; -pub mod fen; +pub mod error; pub mod movegen; pub mod utils; diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs new file mode 100644 index 0000000..7609199 --- /dev/null +++ b/src/movegen/magic/mod.rs @@ -0,0 +1,67 @@ +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!() + } +} 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 f9ce658..9ddbf36 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,9 +1,9 @@ +// Magic bitboard +pub mod magic; +pub use magic::*; + // Naive move generation -mod naive; +pub mod naive; // Magic bitboard generation -mod wizardry; - -// Magic bitboard definitions -mod moves; -pub use moves::*; +pub(crate) mod wizardry; diff --git a/src/movegen/moves.rs b/src/movegen/moves.rs deleted file mode 100644 index 72162d1..0000000 --- a/src/movegen/moves.rs +++ /dev/null @@ -1,147 +0,0 @@ -use std::sync::OnceLock; - -use crate::{ - board::{Bitboard, Color, File, Square}, - movegen::{ - naive, - wizardry::{ - generate_bishop_magics, generate_rook_magics, MagicMoves, BISHOP_SEED, ROOK_SEED, - }, - }, - utils::RandGen, -}; - -// A pre-rolled RNG for magic bitboard generation, using pre-determined values. -struct PreRolledRng { - numbers: [u64; Square::NUM_VARIANTS], - current_index: usize, -} - -impl PreRolledRng { - pub fn new(numbers: [u64; Square::NUM_VARIANTS]) -> Self { - Self { - numbers, - current_index: 0, - } - } -} - -impl RandGen for PreRolledRng { - fn gen(&mut self) -> u64 { - // We roll 3 numbers per square to bitwise-and them together. - // Just return the same one 3 times as a work-around. - let res = self.numbers[self.current_index / 3]; - self.current_index += 1; - res - } -} - -/// Compute the set of possible non-attack moves for a pawn on a [Square], given its [Color] and -/// set of blockers. -pub fn pawn_quiet_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { - static PAWN_MOVES: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> = - OnceLock::new(); - - // 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; - } - - PAWN_MOVES.get_or_init(|| { - let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]; - for color in Color::iter() { - for square in Square::iter() { - res[color.index()][square.index()] = - naive::pawn_moves(color, square, Bitboard::EMPTY); - } - } - res - })[color.index()][square.index()] -} - -/// Compute the set of possible attacks for a pawn on a [Square], given its [Color]. -pub fn pawn_attacks(color: Color, square: Square) -> Bitboard { - static PAWN_ATTACKS: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> = - OnceLock::new(); - - PAWN_ATTACKS.get_or_init(|| { - let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]; - for color in Color::iter() { - for square in Square::iter() { - res[color.index()][square.index()] = naive::pawn_captures(color, square); - } - } - res - })[color.index()][square.index()] -} - -/// Compute the set of possible moves for a pawn on a [Square], given its [Color] and set of -/// blockers. -pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { - pawn_quiet_moves(color, square, blockers) | pawn_attacks(color, square) -} - -/// Compute the set of possible moves for a knight on a [Square]. -pub fn knight_moves(square: Square) -> Bitboard { - static KNIGHT_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new(); - KNIGHT_MOVES.get_or_init(|| { - let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS]; - for square in Square::iter() { - res[square.index()] = naive::knight_moves(square) - } - res - })[square.index()] -} - -/// Compute the set of possible moves for a bishop on a [Square], given its set of blockers. -pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { - static BISHOP_MAGICS: OnceLock = OnceLock::new(); - BISHOP_MAGICS - .get_or_init(|| { - let (magics, moves) = generate_bishop_magics(&mut PreRolledRng::new(BISHOP_SEED)); - // SAFETY: we used the generator function to compute these values - unsafe { MagicMoves::new(magics, moves) } - }) - .query(square, blockers) -} - -/// Compute the set of possible moves for a rook on a [Square], given its set of blockers. -pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { - static ROOK_MAGICS: OnceLock = OnceLock::new(); - ROOK_MAGICS - .get_or_init(|| { - let (magics, moves) = generate_rook_magics(&mut PreRolledRng::new(ROOK_SEED)); - // SAFETY: we used the generator function to compute these values - unsafe { MagicMoves::new(magics, moves) } - }) - .query(square, blockers) -} - -/// Compute the set of possible moves for a queen on a [Square], given its set of blockers. -pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard { - bishop_moves(square, blockers) | rook_moves(square, blockers) -} - -/// Compute the set of possible moves for a king on a [Square]. -pub fn king_moves(square: Square) -> Bitboard { - static KING_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new(); - KING_MOVES.get_or_init(|| { - let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS]; - for square in Square::iter() { - res[square.index()] = naive::king_moves(square) - } - res - })[square.index()] -} - -/// Compute the squares which should be empty for a king-side castle of the given [Color]. -pub fn kind_side_castle_blockers(color: Color) -> Bitboard { - let rank = color.first_rank(); - Square::new(File::F, rank) | Square::new(File::G, rank) -} - -/// Compute the squares which should be empty for a queen-side castle of the given [Color]. -pub fn queen_side_castle_blockers(color: Color) -> Bitboard { - let rank = color.first_rank(); - Square::new(File::B, rank) | Square::new(File::C, rank) | Square::new(File::D, rank) -} diff --git a/src/movegen/naive/bishop.rs b/src/movegen/naive/bishop.rs index 7a2c97f..0806077 100644 --- a/src/movegen/naive/bishop.rs +++ b/src/movegen/naive/bishop.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Direction, Square}; -/// Compute a bishop's movement given a set of blockers that cannot be moved past. 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/naive/king.rs b/src/movegen/naive/king.rs index fdbedb7..6e98df7 100644 --- a/src/movegen/naive/king.rs +++ b/src/movegen/naive/king.rs @@ -1,6 +1,6 @@ -use crate::board::{Bitboard, Direction, Square}; +use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square}; -/// Compute a king's movement. No castling moves included +// No castling moves included pub fn king_moves(square: Square) -> Bitboard { let board = square.into_bitboard(); @@ -9,6 +9,20 @@ pub fn king_moves(square: Square) -> Bitboard { .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) } +pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard { + let rank = color.first_rank(); + + let king_side_square = Square::new(File::G, rank); + let queen_side_square = Square::new(File::C, rank); + + match castle_rights { + CastleRights::NoSide => Bitboard::EMPTY, + CastleRights::KingSide => king_side_square.into_bitboard(), + CastleRights::QueenSide => queen_side_square.into_bitboard(), + CastleRights::BothSides => king_side_square | queen_side_square, + } +} + #[cfg(test)] mod test { use super::*; @@ -169,4 +183,40 @@ mod test { | Square::F6 ); } + + #[test] + fn castling_moves() { + assert_eq!( + king_castling_moves(Color::White, CastleRights::NoSide), + Bitboard::EMPTY + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::NoSide), + Bitboard::EMPTY + ); + assert_eq!( + king_castling_moves(Color::White, CastleRights::KingSide), + Square::G1.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::KingSide), + Square::G8.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::White, CastleRights::QueenSide), + Square::C1.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::QueenSide), + Square::C8.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::White, CastleRights::BothSides), + Square::C1 | Square::G1 + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::BothSides), + Square::C8 | Square::G8 + ); + } } diff --git a/src/movegen/naive/knight.rs b/src/movegen/naive/knight.rs index 28ad7f2..f850d71 100644 --- a/src/movegen/naive/knight.rs +++ b/src/movegen/naive/knight.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Direction, Square}; -/// Compute a knight's movement. 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 index 1c64606..f1bbe3e 100644 --- a/src/movegen/naive/mod.rs +++ b/src/movegen/naive/mod.rs @@ -1,14 +1,5 @@ pub mod bishop; -pub use bishop::*; - pub mod king; -pub use king::*; - pub mod knight; -pub use knight::*; - pub mod pawn; -pub use pawn::*; - pub mod rook; -pub use rook::*; diff --git a/src/movegen/naive/pawn.rs b/src/movegen/naive/pawn.rs index c27f743..55b5bf6 100644 --- a/src/movegen/naive/pawn.rs +++ b/src/movegen/naive/pawn.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Color, Direction, Rank, Square}; -/// Compute a pawn's movement given its color, and a set of blockers that cannot be moved past. 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 } } -/// Computes the set of squares a pawn can capture, given its color. pub fn pawn_captures(color: Color, square: Square) -> Bitboard { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { return Bitboard::EMPTY; @@ -38,6 +36,15 @@ pub fn pawn_captures(color: Color, square: Square) -> Bitboard { attack_west | attack_east } +pub fn en_passant_origins(square: Square) -> Bitboard { + let board = square.into_bitboard(); + + let origin_west = Direction::West.move_board(board); + let origin_east = Direction::East.move_board(board); + + origin_west | origin_east +} + #[cfg(test)] mod test { use super::*; @@ -113,4 +120,14 @@ mod test { Square::G6.into_bitboard() ); } + + #[test] + fn en_passant() { + assert_eq!(en_passant_origins(Square::A4), Square::B4.into_bitboard()); + assert_eq!(en_passant_origins(Square::A5), Square::B5.into_bitboard()); + assert_eq!(en_passant_origins(Square::B4), Square::A4 | Square::C4); + assert_eq!(en_passant_origins(Square::B5), Square::A5 | Square::C5); + assert_eq!(en_passant_origins(Square::H4), Square::G4.into_bitboard()); + assert_eq!(en_passant_origins(Square::H5), Square::G5.into_bitboard()); + } } diff --git a/src/movegen/naive/rook.rs b/src/movegen/naive/rook.rs index e61f5ec..0b06cef 100644 --- a/src/movegen/naive/rook.rs +++ b/src/movegen/naive/rook.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Direction, Square}; -/// Compute a rook's movement given a set of blockers that cannot be moved past. 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 index aa06f25..173eef0 100644 --- a/src/movegen/wizardry/generation.rs +++ b/src/movegen/wizardry/generation.rs @@ -1,22 +1,23 @@ use crate::board::{Bitboard, Square}; -use crate::movegen::naive::{bishop_moves, rook_moves}; -use crate::utils::RandGen; +use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves}; +use crate::movegen::Magic; use super::mask::{generate_bishop_mask, generate_rook_mask}; -use super::Magic; type MagicGenerationType = (Vec, Vec); -pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType { +#[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) } -pub fn generate_rook_magics(rng: &mut dyn RandGen) -> MagicGenerationType { +#[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 RandGen, + rng: &mut dyn random::Source, mask_fn: impl Fn(Square) -> Bitboard, moves_fn: impl Fn(Square, Bitboard) -> Bitboard, ) -> MagicGenerationType { @@ -25,6 +26,7 @@ fn generate_magics( for square in Square::iter() { let mask = mask_fn(square); + let mut candidate: Magic; let occupancy_to_moves: Vec<_> = mask .iter_power_set() @@ -32,7 +34,7 @@ fn generate_magics( .collect(); 'candidate_search: loop { - let mut candidate = Magic { + candidate = Magic { magic: magic_candidate(rng), offset: 0, mask, @@ -43,7 +45,7 @@ fn generate_magics( 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].is_empty() && candidate_moves[index] != moves { + if candidate_moves[index] != Bitboard::EMPTY && candidate_moves[index] != moves { continue 'candidate_search; } candidate_moves[index] = moves; @@ -51,8 +53,8 @@ fn generate_magics( // We have filled all candidate boards, record the correct offset and add the moves candidate.offset = boards.len(); - magics.push(candidate); boards.append(&mut candidate_moves); + magics.push(candidate); break; } } @@ -60,7 +62,6 @@ fn generate_magics( (magics, boards) } -fn magic_candidate(rng: &mut dyn RandGen) -> u64 { - // Few bits makes for better candidates - rng.gen() & rng.gen() & rng.gen() +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 865c986..b3d4f46 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, File, Rank, Square}; use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves}; -/// Compute the relevancy mask for a bishop on a given [Square]. pub fn generate_bishop_mask(square: Square) -> Bitboard { let rays = bishop_moves(square, Bitboard::EMPTY); @@ -13,7 +12,6 @@ pub fn generate_bishop_mask(square: Square) -> Bitboard { rays - mask } -/// Compute the relevancy mask for a rook on a given [Square]. 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 37ddb6e..dfd732d 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,266 +1,2 @@ -mod generation; -pub(super) use generation::*; +pub(crate) mod generation; mod mask; - -use crate::board::{Bitboard, Square}; - -/// A type representing the magic board indexing a given [crate::board::Square]. -#[derive(Clone, Debug)] -pub(super) struct Magic { - /// Magic number. - pub(self) magic: u64, - /// Base offset into the magic square table. - pub(self) offset: usize, - /// Mask to apply to the blocker board before applying the magic. - pub(self) mask: Bitboard, - /// Length of the resulting mask after applying the magic. - pub(self) shift: u8, -} - -impl Magic { - /// Compute the index into the magics database for this set of `blockers`. - 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 - } -} - -/// A type encapsulating a database of [Magic] bitboard moves. -#[derive(Clone, Debug)] -pub(crate) struct MagicMoves { - magics: Vec, - moves: Vec, -} - -impl MagicMoves { - /// Initialize a new [MagicMoves] given a matching list of [Magic] and its corresponding moves - /// as a [Bitboard]. - /// - /// # Safety - /// - /// This should only be called with values generated by [crate::movegen::wizardry::generation]. - pub unsafe fn new(magics: Vec, moves: Vec) -> Self { - Self { magics, moves } - } - - /// Get the set of valid moves for a piece standing on a [Square], given a set of blockers. - pub fn query(&self, square: Square, blockers: Bitboard) -> Bitboard { - // SAFETY: indices are in range by construction - unsafe { - let index = self - .magics - .get_unchecked(square.index()) - .get_index(blockers); - *self.moves.get_unchecked(index) - } - } -} - -// region:sourcegen -/// A set of magic numbers for bishop move generation. -pub(crate) const BISHOP_SEED: [u64; Square::NUM_VARIANTS] = [ - 4634226011293351952, - 6918109887683821586, - 76562328660738184, - 7242919606867744800, - 13871652069997347969, - 1171657252671901696, - 147001475087730752, - 1752045392763101248, - 288406435526639744, - 4612213818402029888, - 9808848818951710728, - 9223394181731320840, - 54047645651435648, - 9224780030482579712, - 9049059098626048, - 1442330840700035221, - 1126037887157508, - 1153488887004529665, - 290485130928332936, - 9226749771011592258, - 148636405693678112, - 2260596997758984, - 73470481646424336, - 2341907012146823680, - 2314955761652335121, - 2265544246165632, - 13598764778463296, - 563087425962496, - 563087425962048, - 2163991853573081088, - 567353402270020, - 6488844433713538048, - 288810987011448834, - 11830884701569344, - 2747549955031826688, - 35734665298432, - 18025943920672800, - 292892945404789012, - 1153520472160470528, - 2260949167801860, - 155446765112299521, - 379008324189818944, - 4616480181217005576, - 576461027453960704, - 2450556349601564416, - 1160556519943569536, - 4612900059821375552, - 5477089643453251617, - 9223532084785594632, - 2810391870219355200, - 36594222015453185, - 4612011546951352320, - 2392883590201344, - 1152956706186200064, - 9009415592510464, - 81077999302148128, - 576746627483043968, - 301267327789056, - 39586720976896, - 720878306081243648, - 9223512777841312257, - 5764609859566698625, - 8088544233436348496, - 4612856276794474560, -]; - -/// A set of magic numbers for rook move generation. -pub(crate) const ROOK_SEED: [u64; Square::NUM_VARIANTS] = [ - 180144122814791812, - 10448386594766422036, - 9403533616331358856, - 108095189301858304, - 72076290316044288, - 36066182562054145, - 4647717564258980096, - 13979173385364603396, - 4620833992751489152, - 297800804633419904, - 578009002156298240, - 2450099003505838082, - 1175721046778052864, - 20406952999780864, - 1175861788231598592, - 36169538802827392, - 288371663414771712, - 423313050501155, - 604731668136450, - 580261214513399808, - 297661437206136832, - 1750211954976489600, - 9020393411186696, - 9259543770406356001, - 44532368556032, - 10376381507760693256, - 52778707714176, - 4612829512676149248, - 1882513444629184528, - 2369460754144428160, - 9223380850137104901, - 2666413562481640036, - 141012643087392, - 16735517094631719424, - 17594358702087, - 2344264412262574084, - 422813768878080, - 1126450811896320, - 54466576291772936, - 42784758060548372, - 292874851780165648, - 18015364885839937, - 282644818493504, - 1184447393488764944, - 4649966632473477184, - 563499910594566, - 17632049496086, - 18502729728001, - 140742121013504, - 9711024139665536, - 246293205270784, - 290772515771392256, - 9230131836490350720, - 73326432604127360, - 453174886517643776, - 2396271245728563712, - 324259242966026501, - 288953994406543363, - 1153557061259362338, - 40533496293515441, - 1407392197644307, - 1729945211427624002, - 587808330812164100, - 9511606812128903298, -]; -// endregion:sourcegen - -#[cfg(test)] -mod test { - use std::fmt::Write as _; - - use super::*; - use crate::utils::SimpleRng; - - fn split_twice<'a>( - text: &'a str, - start_marker: &str, - end_marker: &str, - ) -> Option<(&'a str, &'a str, &'a str)> { - let (prefix, rest) = text.split_once(start_marker)?; - let (mid, suffix) = rest.split_once(end_marker)?; - Some((prefix, mid, suffix)) - } - - fn array_string(piece_type: &str, values: &[Magic]) -> String { - let inner = || -> Result { - let mut res = String::new(); - - writeln!( - &mut res, - "/// A set of magic numbers for {} move generation.", - piece_type - )?; - writeln!( - &mut res, - "pub(crate) const {}_SEED: [u64; Square::NUM_VARIANTS] = [", - piece_type.to_uppercase() - )?; - for magic in values { - writeln!(&mut res, " {},", magic.magic)?; - } - writeln!(&mut res, "];")?; - - Ok(res) - }; - - inner().unwrap() - } - - #[test] - #[ignore = "slow"] - // Regenerates the magic bitboard numbers. - fn regen_magic_seeds() { - // We only care about the magics, the moves can be recomputed at runtime ~cheaply. - let (bishop_magics, _) = generate_bishop_magics(&mut SimpleRng::new()); - let (rook_magics, _) = generate_rook_magics(&mut SimpleRng::new()); - - let original_text = std::fs::read_to_string(file!()).unwrap(); - - let bishop_array = array_string("bishop", &bishop_magics[..]); - let rook_array = array_string("rook", &rook_magics[..]); - - let new_text = { - let start_marker = "// region:sourcegen\n"; - let end_marker = "// endregion:sourcegen\n"; - let (prefix, _, suffix) = - split_twice(&original_text, start_marker, end_marker).unwrap(); - format!("{prefix}{start_marker}{bishop_array}\n{rook_array}{end_marker}{suffix}") - }; - - if new_text != original_text { - std::fs::write(file!(), new_text).unwrap(); - panic!("source was not up-to-date") - } - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d6fd569..2833a48 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,2 @@ -pub(crate) mod rand; -pub(crate) use rand::*; - pub mod static_assert; pub use static_assert::*; diff --git a/src/utils/rand.rs b/src/utils/rand.rs deleted file mode 100644 index 889b13f..0000000 --- a/src/utils/rand.rs +++ /dev/null @@ -1,49 +0,0 @@ -/// A trait to represent RNG for u64 values. -pub trait RandGen { - fn gen(&mut self) -> u64; -} - -// A simple pcg64_fast RNG implementation, for code-generation. -#[cfg(test)] -pub struct SimpleRng(u128); - -#[cfg(test)] -impl SimpleRng { - pub fn new() -> Self { - Self(0xcafef00dd15ea5e5 | 1) // https://xkcd.com/221/ - } - - pub fn gen(&mut self) -> u64 { - const MULTIPLIER: u128 = 0x2360_ED05_1FC6_5DA4_4385_DF64_9FCC_F645; - const XSHIFT: u32 = 64; // (128 - 64 + 64) / 2 - const ROTATE: u32 = 122; // 128 - 6 - - self.0 = self.0.wrapping_mul(MULTIPLIER); - let rot = (self.0 >> ROTATE) as u32; - let xsl = (self.0 >> XSHIFT) as u64 ^ (self.0 as u64); - xsl.rotate_right(rot) - } -} - -#[cfg(test)] -impl RandGen for SimpleRng { - fn gen(&mut self) -> u64 { - self.gen() - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn rng() { - let mut rng = SimpleRng::new(); - - assert_eq!(rng.gen(), 64934999470316615); - assert_eq!(rng.gen(), 15459456780870779090); - assert_eq!(rng.gen(), 13715484424881807779); - assert_eq!(rng.gen(), 17718572936700675021); - assert_eq!(rng.gen(), 14587996314750246637); - } -} diff --git a/src/utils/static_assert.rs b/src/utils/static_assert.rs index 2c7a9a8..81b7a86 100644 --- a/src/utils/static_assert.rs +++ b/src/utils/static_assert.rs @@ -15,9 +15,12 @@ /// ``` #[macro_export] macro_rules! static_assert { - ($($tt:tt)*) => { + ($condition:expr) => { + // Based on the latest one in `rustc`'s one before it was [removed]. + // + // [removed]: https://github.com/rust-lang/rust/commit/c2dad1c6b9f9636198d7c561b47a2974f5103f6d #[allow(dead_code)] - const _: () = assert!($($tt)*); + const _: () = [()][!($condition) as usize]; }; } diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 37a5297..48a2e94 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -1,41 +1,17 @@ -import enum - -import gdb import gdb.printing - -def optional(constructor, val): - try: - return constructor(val["Some"]["__0"]) - except gdb.error: - return None - - -def print_opt(val): - return "(None)" if val is None else str(val) - - class Square(object): """ - Python representation of a 'seer::board::square::Square' raw value. + Wrapper around GDB's representation of a 'seer::board::square::Square' in + memory. """ - FILES = list(map(lambda n: chr(ord("A") + n), range(8))) + FILES = list(map(lambda n: chr(ord('A') + n), range(8))) RANKS = list(map(lambda n: str(n + 1), range(8))) def __init__(self, val): - if isinstance(val, Square): - val = val._val self._val = val - @classmethod - def from_gdb(cls, val): - return cls(int(val)) - - @classmethod - def from_file_rank(cls, file, rank): - return cls(file * 8 + rank) - def __str__(self): return self.FILES[self.file] + self.RANKS[self.rank] @@ -47,373 +23,56 @@ class Square(object): def file(self): return int(self._val) // 8 - class Bitboard(object): """ - Python representation of a 'seer::board::bitboard::Bitboard' raw value. + Wrapper around GDB's representation of a 'seer::board::bitboard::Bitboard' + in memory. """ def __init__(self, val): - if isinstance(val, Bitboard): - val = val._val self._val = val - @classmethod - def from_gdb(cls, val): - return cls(int(val["__0"])) - def __str__(self): return "[" + ", ".join(map(str, self.squares)) + "]" - def at(self, square): - return bool(self._val & (1 << square._val)) - @property def squares(self): - n = self._val + n = int(self._val["__0"]) while n: - b = n & (~n + 1) + b = n & (~n+1) yield Square(b.bit_length() - 1) n ^= b - -class CastleRights(enum.IntEnum): - """ - Python representation of a 'seer::board::castle_rights::CastleRights' raw value. - """ - - # Should be kept in sync with the enum in `color.rs` - NO_SIDE = 0 - KING_SIDE = 1 - QUEEN_SIDE = 2 - BOTH_SIDES = 3 - - @classmethod - def from_gdb(cls, val): - return cls(int(val)) - - def __str__(self): - return self.name.title().replace("_", "") - - -class Color(enum.IntEnum): - """ - Python representation of a 'seer::board::color::Color' raw value. - """ - - # Should be kept in sync with the enum in `color.rs` - WHITE = 0 - BLACK = 1 - - @classmethod - def from_gdb(cls, val): - return cls(int(val)) - - def __str__(self): - return self.name.title() - - -class File(enum.IntEnum): - """ - Python representation of a 'seer::board::file::File' raw value. - """ - - # Should be kept in sync with the enum in `file.rs` - A = 0 - B = 1 - C = 2 - D = 3 - E = 4 - F = 5 - G = 6 - H = 7 - - @classmethod - def from_gdb(cls, val): - return cls(int(val)) - - def __str__(self): - return self.name.title() - - -class Rank(enum.IntEnum): - """ - Python representation of a 'seer::board::rank::Rank' raw value. - """ - - # Should be kept in sync with the enum in `rank.rs` - First = 0 - Second = 1 - Third = 2 - Fourth = 3 - Fifth = 4 - Sixth = 5 - Seventh = 6 - Eighth = 7 - - @classmethod - def from_gdb(cls, val): - return cls(int(val)) - - def __str__(self): - return self.name.title() - - -class Piece(enum.IntEnum): - """ - Python representation of a 'seer::board::piece::Piece' raw value. - """ - - # Should be kept in sync with the enum in `piece.rs` - KING = 0 - QUEEN = 1 - ROOK = 2 - BISHOP = 3 - KNIGHT = 4 - PAWN = 5 - - @classmethod - def from_gdb(cls, val): - return cls(int(val)) - - def __str__(self): - return self.name.title() - - -class Move(object): - """ - Wrapper around GDB's representation of a 'seer::board::move::Move' - in memory. - """ - - def __init__(self, start, destination, promotion): - self._start = Square(start) - self._destination = Square(destination) - self._promotion = Piece(promotion) - - @classmethod - def from_gdb(cls, val): - start = Square(int(val["start"])) - destination = Square(int(val["destination"])) - promotion = optional(Piece.from_gdb, val["promotion"]) - cls(start, destination, promotion) - - @property - def start(self): - return self._start - - @property - def destination(self): - return self._destination - - @property - def promotion(self): - return self._promotion - - def __str__(self): - KEYS = [ - "start", - "destination", - "promotion", - ] - indent = lambda s: " " + s - - values = [key + ": " + print_opt(getattr(self, key)) + ",\n" for key in KEYS] - return "Move{\n" + "".join(map(indent, values)) + "}" - - -class ChessBoard(object): - """ - Wrapper around GDB's representation of a 'seer::board::chess_board::ChessBoard' - in memory. - """ - - def __init__( - self, - piece_occupancy, - color_occupancy, - castle_rights, - half_move_clock, - total_plies, - side, - en_passant, - ): - self._piece_occupancy = list(map(Bitboard, piece_occupancy)) - self._color_occupancy = list(map(Bitboard, color_occupancy)) - self._castle_rights = list(map(CastleRights, castle_rights)) - self._half_move_clock = int(half_move_clock) - self._total_plies = int(total_plies) - self._side = Color(side) - self._en_passant = None if en_passant is None else Square(en_passant) - - @classmethod - def from_gdb(cls, val): - return cls( - [Bitboard.from_gdb(val["piece_occupancy"][p]) for p in Piece], - [Bitboard.from_gdb(val["color_occupancy"][c]) for c in Color], - [CastleRights.from_gdb(val["castle_rights"][c]) for c in Color], - int(val["half_move_clock"]), - int(val["total_plies"]), - Color.from_gdb(val["side"]), - optional(Square.from_gdb, val["en_passant"]), - ) - - def at(self, square): - for piece in Piece: - if not self._piece_occupancy[piece].at(square): - continue - for color in Color: - if not self._color_occupancy[color].at(square): - continue - return (piece, color) - return None - - def pretty_str(self): - def pretty_piece(piece, color): - return [ - ("♚", "♔"), - ("♛", "♕"), - ("♜", "♖"), - ("♝", "♗"), - ("♞", "♘"), - ("♟", "♙"), - ][piece][color] - - board = [ - [self.at(Square.from_file_rank(file, rank)) for file in File] - for rank in Rank - ] - - res = [] - res.append(" A B C D E F G H ") - for n, line in reversed(list(enumerate(board, start=1))): - strings = [str(n) + " "] - strings.extend(" " if p is None else pretty_piece(*p) for p in line) - strings.append(" " + str(n)) - res.append("|".join(strings)) - res.append(" A B C D E F G H ") - res += [ - "Half-move clock: " + str(self._half_move_clock), - "Total plies: " + str(self._total_plies), - "Side to play: " + str(self._side), - "En passant: " + print_opt(self._en_passant), - ] - return "\n".join(res) - - class SquarePrinter(object): "Print a seer::board::square::Square" def __init__(self, val): - self._val = Square.from_gdb(val) + self._val = Square(val) def to_string(self): return str(self._val) + def display_hint(self): + return 'string' class BitboardPrinter(object): "Print a seer::board::bitboard::Bitboard" def __init__(self, val): - self._val = Bitboard.from_gdb(val) + self._val = Bitboard(val) def to_string(self): return "Bitboard{" + str(self._val)[1:-1] + "}" - -class CastleRightsPrinter(object): - "Print a seer::board::castle_rights::CastleRights" - - def __init__(self, val): - self._val = CastleRights.from_gdb(val) - - def to_string(self): - return str(self._val) - - -class ColorPrinter(object): - "Print a seer::board::color::Color" - - def __init__(self, val): - self._val = Color.from_gdb(val) - - def to_string(self): - return str(self._val) - - -class FilePrinter(object): - "Print a seer::board::file::File" - - def __init__(self, val): - self._val = File.from_gdb(val) - - def to_string(self): - return str(self._val) - - -class RankPrinter(object): - "Print a seer::board::rank::Rank" - - def __init__(self, val): - self._val = Rank.from_gdb(val) - - def to_string(self): - return str(self._val) - - -class PiecePrinter(object): - "Print a seer::board::piece::Piece" - - def __init__(self, val): - self._val = Piece.from_gdb(val) - - def to_string(self): - return str(self._val) - - -class MovePrinter(object): - "Print a seer::board::move::Move" - - def __init__(self, val): - self._val = Move.from_gdb(val) - - def to_string(self): - return str(self._val) - - -class PrintBoard(gdb.Command): - """ - Pretty-print a 'seer::board::chess_board::ChessBoard' as a 2D textual chess board. - """ - - def __init__(self): - super(PrintBoard, self).__init__( - "print-board", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION - ) - - def invoke(self, arg, from_tty): - board = ChessBoard.from_gdb(gdb.parse_and_eval(arg)) - print(board.pretty_str()) - + def display_hint(self): + return 'string' def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') - pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter) - pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) - pp.add_printer('CastleRights', '^seer::board::castle_rights::CastleRights$', CastleRightsPrinter) - pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter) - pp.add_printer('File', '^seer::board::file::File$', FilePrinter) - pp.add_printer('Rank', '^seer::board::rank::Rank$', RankPrinter) - pp.add_printer('Piece', '^seer::board::piece::Piece$', ColorPrinter) - pp.add_printer('Move', '^seer::board::move::Move$', MovePrinter) + pp.add_printer('BigNum', '^seer::board::square::Square$', SquarePrinter) + pp.add_printer('BigNum', '^seer::board::bitboard::Bitboard$', BitboardPrinter) return pp - -def register_commands(): - PrintBoard() - - gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True) -register_commands()