diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 7037529..0000000 --- a/.drone.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -kind: pipeline -type: exec -name: abacus checks - -steps: -- name: flake check - commands: - - nix flake check - -- name: package check - commands: - - nix build - -- name: notifiy - commands: - - nix run github:ambroisie/matrix-notifier - environment: - ADDRESS: - from_secret: matrix_homeserver - ROOM: - from_secret: matrix_roomid - USER: - from_secret: matrix_username - PASS: - from_secret: matrix_password - when: - status: - - failure - - success -... diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..de77fcb --- /dev/null +++ b/.envrc @@ -0,0 +1,5 @@ +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 c2f669f..5f360ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -# Nix files -/result -/.pre-commit-config.yaml - -# Rust files +# Rust build directory /target + +# Nix generated files +/.pre-commit-config.yaml +/result diff --git a/.woodpecker/check.yml b/.woodpecker/check.yml new file mode 100644 index 0000000..9135f7b --- /dev/null +++ b/.woodpecker/check.yml @@ -0,0 +1,31 @@ +labels: + backend: local + +steps: +- name: pre-commit check + image: bash + commands: + - nix develop --command pre-commit run --all + +- name: nix flake check + image: bash + commands: + - nix flake check + +- name: notifiy + image: bash + secrets: + - source: matrix_homeserver + target: address + - source: matrix_roomid + target: room + - source: matrix_username + target: user + - source: matrix_password + target: pass + commands: + - nix run '.#matrix-notifier' + when: + status: + - failure + - success diff --git a/flake.lock b/flake.lock index 2601b96..18cea66 100644 --- a/flake.lock +++ b/flake.lock @@ -1,73 +1,111 @@ { "nodes": { - "flake-utils": { + "flake-compat": { + "flake": false, "locked": { - "lastModified": 1656928814, - "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", + "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=", "owner": "numtide", "repo": "flake-utils", - "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { "owner": "numtide", - "ref": "master", + "ref": "main", "repo": "flake-utils", "type": "github" } }, - "naersk": { + "gitignore": { "inputs": { "nixpkgs": [ + "pre-commit-hooks", "nixpkgs" ] }, "locked": { - "lastModified": 1655042882, - "narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=", - "owner": "nix-community", - "repo": "naersk", - "rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f", + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", "type": "github" }, "original": { - "owner": "nix-community", - "ref": "master", - "repo": "naersk", + "owner": "hercules-ci", + "repo": "gitignore.nix", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1657888067, - "narHash": "sha256-GnwJoFBTPfW3+mz7QEeJEEQ9OMHZOiIJ/qDhZxrlKh8=", + "lastModified": 1711523803, + "narHash": "sha256-UKcYiHWHQynzj6CN/vTcix4yd1eCu1uFdsuarupdCQQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "65fae659e31098ca4ac825a6fef26d890aaf3f4e", + "rev": "2726f127c15a4cc9810843b96cad73c7eb39e443", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "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", "repo": "nixpkgs", "type": "github" } }, "pre-commit-hooks": { "inputs": { + "flake-compat": "flake-compat", "flake-utils": [ - "flake-utils" + "futils" ], + "gitignore": "gitignore", "nixpkgs": [ "nixpkgs" - ] + ], + "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1656169028, - "narHash": "sha256-y9DRauokIeVHM7d29lwT8A+0YoGUBXV3H0VErxQeA8s=", + "lastModified": 1711519547, + "narHash": "sha256-Q7YmSCUJmDl71fJv/zD9lrOCJ1/SE/okZ2DsrmRjzhY=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "db3bd555d3a3ceab208bed48f983ccaa6a71a25e", + "rev": "7d47a32e5cd1ea481fab33c516356ce27c8cef4a", "type": "github" }, "original": { @@ -79,34 +117,23 @@ }, "root": { "inputs": { - "flake-utils": "flake-utils", - "naersk": "naersk", + "futils": "futils", "nixpkgs": "nixpkgs", - "pre-commit-hooks": "pre-commit-hooks", - "rust-overlay": "rust-overlay" + "pre-commit-hooks": "pre-commit-hooks" } }, - "rust-overlay": { - "inputs": { - "flake-utils": [ - "flake-utils" - ], - "nixpkgs": [ - "nixpkgs" - ] - }, + "systems": { "locked": { - "lastModified": 1657853760, - "narHash": "sha256-X6ERAyUXGsrhbhgkxNaQl40wcus5uyQZOCxUh5neK+g=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "a97a761cc11327bb109dc30af1c637b986be7959", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { - "owner": "oxalica", - "ref": "master", - "repo": "rust-overlay", + "owner": "nix-systems", + "repo": "default", "type": "github" } } diff --git a/flake.nix b/flake.nix index 22a05e4..ca06a36 100644 --- a/flake.nix +++ b/flake.nix @@ -1,29 +1,19 @@ { - description = "A handy file picker program"; + description = "A chess engine"; inputs = { - flake-utils = { + futils = { type = "github"; owner = "numtide"; repo = "flake-utils"; - ref = "master"; - }; - - naersk = { - type = "github"; - owner = "nix-community"; - repo = "naersk"; - ref = "master"; - inputs = { - nixpkgs.follows = "nixpkgs"; - }; + ref = "main"; }; nixpkgs = { type = "github"; owner = "NixOS"; repo = "nixpkgs"; - ref = "nixpkgs-unstable"; + ref = "nixos-unstable"; }; pre-commit-hooks = { @@ -32,123 +22,91 @@ repo = "pre-commit-hooks.nix"; ref = "master"; inputs = { - 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"; + flake-utils.follows = "futils"; nixpkgs.follows = "nixpkgs"; }; }; }; - outputs = - { self - , flake-utils - , naersk - , nixpkgs - , pre-commit-hooks - , rust-overlay - }: - let - inherit (flake-utils.lib) eachSystem system; + 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; - mySystems = [ - system.aarch64-linux - system.x86_64-darwin - system.x86_64-linux - ]; - - 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" ]; - }; - naersk-lib = naersk.lib."${system}".override { - cargo = my-rust; - rustc = my-rust; - }; - inherit (pkgs) lib; - in - rec { - checks = { - 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; - entry = lib.mkForce "${rust-env}/bin/cargo-clippy clippy"; - }; + cargoLock = { + lockFile = "${self}/Cargo.lock"; + }; - nixpkgs-fmt = { - enable = true; - }; - - rustfmt = { - enable = true; - entry = lib.mkForce "${rust-env}/bin/cargo-fmt fmt -- --check --color always"; - }; + meta = with lib; { + description = "A chess engine"; + homepage = "https://git.belanyi.fr/ambroisie/seer"; + license = licenses.mit; + maintainers = with maintainers; [ ambroisie ]; }; }; - }; - - devShells = { - default = pkgs.mkShell { - inputsFrom = [ - packages.seer - ]; - - nativeBuildInputs = with pkgs; [ - rust-analyzer - # Clippy, rustfmt, etc... - my-rust - ]; - - inherit (checks.pre-commit) shellHook; - - RUST_SRC_PATH = "${my-rust}/lib/rustlib/src/rust/library"; }; }; + } // futils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ + self.overlays.default + ]; + }; - packages = { - default = self.packages."${system}".seer; - - seer = naersk-lib.buildPackage { + pre-commit = pre-commit-hooks.lib.${system}.run { src = self; - doCheck = true; + hooks = { + clippy = { + enable = true; + settings = { + denyWarnings = true; + }; + }; - passthru = { - inherit my-rust; + nixpkgs-fmt = { + enable = true; + }; + + rustfmt = { + enable = true; + }; }; }; - }; - }); + in + { + checks = { + inherit (self.packages.${system}) seer; + }; + + devShells = { + default = pkgs.mkShell { + inputsFrom = with self.packages.${system}; [ + seer + ]; + + packages = with pkgs; [ + clippy + rust-analyzer + rustfmt + ]; + + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + + inherit (pre-commit) shellHook; + }; + }; + + packages = futils.lib.flattenTree { + default = pkgs.seer; + inherit (pkgs) seer; + }; + }); } diff --git a/src/board/bitboard/error.rs b/src/board/bitboard/error.rs new file mode 100644 index 0000000..c631482 --- /dev/null +++ b/src/board/bitboard/error.rs @@ -0,0 +1,19 @@ +#[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 fcd644c..7c01a9a 100644 --- a/src/board/bitboard/iterator.rs +++ b/src/board/bitboard/iterator.rs @@ -1,6 +1,14 @@ /// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a -/// [Bitboard](crate::board::Bitboard). -pub struct BitboardIterator(pub(crate) u64); +/// [Bitboard]. +use crate::board::Bitboard; + +pub struct BitboardIterator(u64); + +impl BitboardIterator { + pub fn new(board: Bitboard) -> Self { + Self(board.0) + } +} impl Iterator for BitboardIterator { type Item = crate::board::Square; diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 8b716be..b0ec90a 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,8 +1,12 @@ use super::Square; use crate::utils::static_assert; +mod error; +use error::*; mod iterator; use iterator::*; +mod superset; +use superset::*; /// Use a 64-bit number to represent a chessboard. Each bit is mapped from to a specific square, so /// that index 0 -> A1, 1 -> A2, ..., 63 -> H8. @@ -63,6 +67,22 @@ impl Bitboard { pub fn is_empty(self) -> bool { self == Self::EMPTY } + + /// Return true if there are more than piece in the [Bitboard]. This is faster than testing + /// `board.count() > 1`. + #[inline(always)] + pub fn has_more_than_one(self) -> bool { + (self.0 & (self.0.wrapping_sub(1))) != 0 + } + + /// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of + /// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that + /// contain all, some, or none of the [Square] that are in the given [Bitboard]. + /// If given an empty [Bitboard], yields the empty [Bitboard] back. + #[inline(always)] + pub fn iter_power_set(self) -> impl Iterator { + BitboardPowerSetIterator::new(self) + } } // Ensure zero-cost (at least size-wise) wrapping. @@ -74,13 +94,28 @@ impl Default for Bitboard { } } -/// Iterate over the [Square](crate::board::Square) values included in the board. +/// Iterate over the [Square] values included in the board. impl IntoIterator for Bitboard { type IntoIter = BitboardIterator; type Item = Square; fn into_iter(self) -> Self::IntoIter { - BitboardIterator(self.0) + 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 { + let index = match self.count() { + 1 => self.0.trailing_zeros() as usize, + 0 => return Err(IntoSquareError::EmptyBoard), + _ => return Err(IntoSquareError::TooManySquares), + }; + Ok(Square::from_index(index)) } } @@ -104,6 +139,22 @@ impl std::ops::Shr for Bitboard { } } +/// Treat bitboard as a set of squares, shift each square's index left by the amount given. +impl std::ops::ShlAssign for Bitboard { + #[inline(always)] + fn shl_assign(&mut self, rhs: usize) { + *self = *self << rhs; + } +} + +/// Treat bitboard as a set of squares, shift each square's index right by the amount given. +impl std::ops::ShrAssign for Bitboard { + #[inline(always)] + fn shr_assign(&mut self, rhs: usize) { + *self = *self >> rhs; + } +} + /// Treat bitboard as a set of squares, and invert the set. impl std::ops::Not for Bitboard { type Output = Bitboard; @@ -124,7 +175,7 @@ impl std::ops::BitOr for Bitboard { } } -/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +/// Treat the [Square] as a singleton bitboard, and apply the operator. impl std::ops::BitOr for Bitboard { type Output = Bitboard; @@ -134,6 +185,22 @@ impl std::ops::BitOr for Bitboard { } } +/// Treat each bitboard as a set of squares, keep squares that are in either sets. +impl std::ops::BitOrAssign for Bitboard { + #[inline(always)] + fn bitor_assign(&mut self, rhs: Bitboard) { + *self = *self | rhs; + } +} + +/// Treat the [Square] as a singleton bitboard, and apply the operator. +impl std::ops::BitOrAssign for Bitboard { + #[inline(always)] + fn bitor_assign(&mut self, rhs: Square) { + *self = *self | rhs; + } +} + /// Treat each bitboard as a set of squares, keep squares that are in both sets. impl std::ops::BitAnd for Bitboard { type Output = Bitboard; @@ -144,7 +211,7 @@ impl std::ops::BitAnd for Bitboard { } } -/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +/// Treat the [Square] as a singleton bitboard, and apply the operator. impl std::ops::BitAnd for Bitboard { type Output = Bitboard; @@ -154,6 +221,22 @@ impl std::ops::BitAnd for Bitboard { } } +/// Treat each bitboard as a set of squares, keep squares that are in both sets. +impl std::ops::BitAndAssign for Bitboard { + #[inline(always)] + fn bitand_assign(&mut self, rhs: Bitboard) { + *self = *self & rhs; + } +} + +/// Treat the [Square] as a singleton bitboard, and apply the operator. +impl std::ops::BitAndAssign for Bitboard { + #[inline(always)] + fn bitand_assign(&mut self, rhs: Square) { + *self = *self & rhs; + } +} + /// Treat each bitboard as a set of squares, keep squares that are in exactly one of either set. impl std::ops::BitXor for Bitboard { type Output = Bitboard; @@ -164,7 +247,7 @@ impl std::ops::BitXor for Bitboard { } } -/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +/// Treat the [Square] as a singleton bitboard, and apply the operator. impl std::ops::BitXor for Bitboard { type Output = Bitboard; @@ -174,6 +257,22 @@ impl std::ops::BitXor for Bitboard { } } +/// Treat each bitboard as a set of squares, keep squares that are in exactly one of either set. +impl std::ops::BitXorAssign for Bitboard { + #[inline(always)] + fn bitxor_assign(&mut self, rhs: Bitboard) { + *self = *self ^ rhs; + } +} + +/// Treat the [Square] as a singleton bitboard, and apply the operator. +impl std::ops::BitXorAssign for Bitboard { + #[inline(always)] + fn bitxor_assign(&mut self, rhs: Square) { + *self = *self ^ rhs; + } +} + /// Treat each bitboard as a set of squares, and substract one set from another. impl std::ops::Sub for Bitboard { type Output = Bitboard; @@ -184,7 +283,7 @@ impl std::ops::Sub for Bitboard { } } -/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +/// Treat the [Square] as a singleton bitboard, and apply the operator. impl std::ops::Sub for Bitboard { type Output = Bitboard; @@ -194,10 +293,28 @@ impl std::ops::Sub for Bitboard { } } +/// Treat each bitboard as a set of squares, and substract one set from another. +impl std::ops::SubAssign for Bitboard { + #[inline(always)] + fn sub_assign(&mut self, rhs: Bitboard) { + *self = *self - rhs; + } +} + +/// Treat the [Square] as a singleton bitboard, and apply the operator. +impl std::ops::SubAssign for Bitboard { + #[inline(always)] + fn sub_assign(&mut self, rhs: Square) { + *self = *self - rhs; + } +} + #[cfg(test)] mod test { + use std::collections::HashSet; + use super::*; - use crate::board::square::*; + use crate::board::{square::*, File, Rank}; #[test] fn count() { @@ -280,4 +397,104 @@ mod test { assert_eq!(Bitboard::FILES[0] - Bitboard::RANKS[0], Bitboard(0xff - 1)); assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1)); } + + #[test] + fn more_than_one() { + assert!(!Bitboard::EMPTY.has_more_than_one()); + for square in Square::iter() { + assert!(!square.into_bitboard().has_more_than_one()) + } + assert!((Square::A1 | Square::H8).has_more_than_one()); + assert!(Bitboard::ALL.has_more_than_one()); + } + + #[test] + fn iter_power_set_empty() { + assert_eq!( + Bitboard::EMPTY.iter_power_set().collect::>(), + vec![Bitboard::EMPTY] + ) + } + + #[test] + fn iter_power_set_one_square() { + for square in Square::iter() { + assert_eq!( + square + .into_bitboard() + .iter_power_set() + .collect::>(), + [Bitboard::EMPTY, square.into_bitboard()] + .into_iter() + .collect::>() + ) + } + } + + #[test] + fn iter_power_set_two_squares() { + assert_eq!( + (Square::A1 | Square::H8) + .iter_power_set() + .collect::>(), + [ + Bitboard::EMPTY, + Square::A1.into_bitboard(), + Square::H8.into_bitboard(), + Square::A1 | Square::H8 + ] + .into_iter() + .collect::>() + ) + } + + #[test] + fn iter_power_set_six_squares_exhaustive() { + let mask = (0..6) + .map(Square::from_index) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs); + assert_eq!( + mask.iter_power_set().collect::>(), + (0..(1 << 6)).map(Bitboard).collect::>() + ) + } + + #[test] + fn iter_power_set_eight_squares_length() { + assert_eq!( + File::A + .into_bitboard() + .iter_power_set() + .collect::>() + .len(), + 1 << 8 + ); + assert_eq!( + Rank::First + .into_bitboard() + .iter_power_set() + .collect::>() + .len(), + 1 << 8 + ); + } + + #[test] + fn into_square() { + for square in Square::iter() { + assert_eq!(square.into_bitboard().try_into(), Ok(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) + ) + } } diff --git a/src/board/bitboard/superset.rs b/src/board/bitboard/superset.rs new file mode 100644 index 0000000..1a82ca1 --- /dev/null +++ b/src/board/bitboard/superset.rs @@ -0,0 +1,46 @@ +use super::Bitboard; + +/// Iterator over a [Bitboard] mask, which yields all potential subsets of the given board. +/// 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, +} + +impl BitboardPowerSetIterator { + pub fn new(board: Bitboard) -> Self { + Self { + board, + subset: Bitboard::EMPTY, + done: false, + } + } +} + +impl Iterator for BitboardPowerSetIterator { + type Item = Bitboard; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + 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)) + } +} + +impl ExactSizeIterator for BitboardPowerSetIterator {} + +impl std::iter::FusedIterator for BitboardPowerSetIterator {} diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 0e44e03..b34d952 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -14,15 +14,22 @@ pub enum CastleRights { } impl CastleRights { + /// The number of [CastleRights] variants. + pub const NUM_VARIANTS: usize = 4; + /// Convert from a castle rights index into a [CastleRights] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 4); + 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()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) @@ -46,6 +53,25 @@ impl CastleRights { (self.index() & 2) != 0 } + /// Add king-side castling rights. + #[inline(always)] + pub fn with_king_side(self) -> Self { + self.add(Self::KingSide) + } + + /// Add queen-side castling rights. + #[inline(always)] + pub fn with_queen_side(self) -> Self { + self.add(Self::QueenSide) + } + + /// Add some [CastleRights], and return the resulting [CastleRights]. + #[inline(always)] + fn add(self, additional_rights: CastleRights) -> Self { + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(self.index() | additional_rights.index()) } + } + /// Remove king-side castling rights. #[inline(always)] pub fn without_king_side(self) -> Self { diff --git a/src/board/chess_board/builder.rs b/src/board/chess_board/builder.rs new file mode 100644 index 0000000..8221d92 --- /dev/null +++ b/src/board/chess_board/builder.rs @@ -0,0 +1,161 @@ +use crate::board::{Bitboard, CastleRights, ChessBoard, Color, InvalidError, Piece, Square}; + +/// 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)>; 64], + // Same fields as [ChessBoard]. + castle_rights: [CastleRights; Color::NUM_VARIANTS], + en_passant: Option, + half_move_clock: u8, + total_plies: u32, + side: Color, +} + +impl ChessBoardBuilder { + pub fn new() -> Self { + Self { + pieces: [None; 64], + castle_rights: [CastleRights::NoSide; 2], + en_passant: Default::default(), + half_move_clock: Default::default(), + total_plies: Default::default(), + side: Color::White, + } + } + + 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: u8) -> &mut Self { + self.half_move_clock = clock; + self + } + + pub fn with_total_plies(&mut self, plies: u32) -> &mut Self { + self.total_plies = plies; + 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 = InvalidError; + + 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, + total_plies, + side, + } = 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 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_total_plies(board.total_plies()) + .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 new file mode 100644 index 0000000..e6ef030 --- /dev/null +++ b/src/board/chess_board/error.rs @@ -0,0 +1,50 @@ +/// A singular type for all errors that could happen during [ChessBoard::is_valid]. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InvalidError { + /// 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, +} + +impl std::fmt::Display for InvalidError { + 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." + } + }; + write!(f, "{}", error_msg) + } +} + +impl std::error::Error for InvalidError {} diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs new file mode 100644 index 0000000..2a98537 --- /dev/null +++ b/src/board/chess_board/mod.rs @@ -0,0 +1,862 @@ +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: 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, +} + +/// 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: 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 { + 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) -> 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()) + } + + /// 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, 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; + + // 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. + pub fn is_valid(&self) -> bool { + self.validate().is_ok() + } + + /// Validate the state of the board. Return Err([InvalidError]) if an issue is found. + pub fn validate(&self) -> Result<(), InvalidError> { + // 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(InvalidError::OverlappingPieces); + } + } + } + } + + // Don't overlap colors. + if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() { + return Err(InvalidError::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(InvalidError::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(InvalidError::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(InvalidError::TooManyPieces); + } + } + + // Check that we have a king + if self.occupancy(Piece::King, color).count() != 1 { + return Err(InvalidError::MissingKing); + } + + // Check that don't have too many pieces in total + if self.color_occupancy(color).count() > 16 { + return Err(InvalidError::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(InvalidError::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(InvalidError::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(InvalidError::InvalidCastlingRights); + } + } + + // En-passant validation + if let Some(square) = self.en_passant() { + // Must be empty + if !(self.combined_occupancy() & square).is_empty() { + return Err(InvalidError::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(InvalidError::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(InvalidError::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(InvalidError::NeighbouringKings); + } + + // Check that the opponent is not currently in check. + if !self.compute_checkers(!self.current_player()).is_empty() { + return Err(InvalidError::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; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + } + } +} + +#[cfg(test)] +mod test { + use crate::board::MoveBuilder; + use crate::fen::FromFen; + + use super::*; + + #[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_eq!( + position.validate().err().unwrap(), + InvalidError::OverlappingPieces, + ); + } + + #[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_eq!( + position.validate().err().unwrap(), + InvalidError::OverlappingColors, + ); + } + + #[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_eq!( + position.validate().err().unwrap(), + InvalidError::ErroneousCombinedOccupancy, + ); + } + + #[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_eq!( + position.validate().err().unwrap(), + InvalidError::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(), InvalidError::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(), InvalidError::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(), InvalidError::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(), InvalidError::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(), InvalidError::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(), InvalidError::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(), InvalidError::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(), InvalidError::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(), InvalidError::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()) { + builder[square] = Some((Piece::Pawn, Color::White)); + } + for square in (File::F.into_bitboard() | File::G.into_bitboard()) { + builder[square] = Some((Piece::Pawn, Color::Black)); + } + TryInto::::try_into(builder) + }; + assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); + } + + #[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 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/color.rs b/src/board/color.rs index d71df67..66b21b3 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -8,15 +8,29 @@ pub enum Color { } impl Color { - /// Convert from a file index into a [Color] type. + /// The number of [Color] variants. + pub const NUM_VARIANTS: usize = 2; + + const ALL: [Self; Self::NUM_VARIANTS] = [Self::White, Self::Black]; + + /// Iterate over all colors in order. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + + /// Convert from a color index into a [Color] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 2); + assert!(index < Self::NUM_VARIANTS); // SAFETY: we know the value is in-bounds unsafe { Self::from_index_unchecked(index) } } - /// Convert from a file index into a [Color] type, no bounds checking. + /// Convert from a color index into a [Color] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Color::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) @@ -46,6 +60,16 @@ impl Color { } } + /// Return the third [Rank] for pieces of the given [Color], where its pawns move to after a + /// one-square move on the start position. + #[inline(always)] + pub fn third_rank(self) -> Rank { + match self { + Self::White => Rank::Third, + Self::Black => Rank::Sixth, + } + } + /// Return the fourth [Rank] for pieces of the given [Color], where its pawns move to after a /// two-square move. #[inline(always)] diff --git a/src/board/direction.rs b/src/board/direction.rs index 135f5f4..40c8d69 100644 --- a/src/board/direction.rs +++ b/src/board/direction.rs @@ -121,14 +121,28 @@ impl Direction { /// 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)] - pub fn slide_board(self, mut board: Bitboard) -> Bitboard { + pub fn slide_board(self, board: Bitboard) -> Bitboard { + self.slide_board_with_blockers(board, Bitboard::EMPTY) + } + + /// 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. + /// 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)] + pub fn slide_board_with_blockers(self, mut board: Bitboard, blockers: Bitboard) -> Bitboard { debug_assert!(!Self::KNIGHT_DIRECTIONS.contains(&self)); let mut res = Default::default(); while !board.is_empty() { board = self.move_board(board); - res = res | board; + res |= board; + if !(board & blockers).is_empty() { + break; + } } res @@ -655,4 +669,29 @@ mod test { Bitboard::DIAGONAL - Square::A1 ); } + + #[test] + fn blocked_slides() { + assert_eq!( + Direction::North + .slide_board_with_blockers(Square::A1.into_bitboard(), Square::A2.into_bitboard()), + Square::A2.into_bitboard() + ); + assert_eq!( + Direction::North + .slide_board_with_blockers(Square::A1.into_bitboard(), Square::A3.into_bitboard()), + Square::A2 | Square::A3 + ); + assert_eq!( + Direction::North + .slide_board_with_blockers(Square::A1.into_bitboard(), Square::A4.into_bitboard()), + Square::A2 | Square::A3 | Square::A4 + ); + // Ensure that the starting square being in `blockers` is not an issue + assert_eq!( + Direction::North + .slide_board_with_blockers(Square::A1.into_bitboard(), Square::A1.into_bitboard()), + File::A.into_bitboard() - Square::A1 + ); + } } diff --git a/src/board/file.rs b/src/board/file.rs index 1a64929..1475e9a 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -15,31 +15,38 @@ pub enum File { } impl File { - const ALL: [File; 8] = [ - File::A, - File::B, - File::C, - File::D, - File::E, - File::F, - File::G, - File::H, + /// The number of [File] variants. + pub const NUM_VARIANTS: usize = 8; + + const ALL: [Self; Self::NUM_VARIANTS] = [ + Self::A, + Self::B, + Self::C, + Self::D, + Self::E, + Self::F, + Self::G, + Self::H, ]; /// Iterate over all files in order. - pub fn iter() -> impl Iterator { + pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() } /// Convert from a file index into a [File] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 8); + assert!(index < Self::NUM_VARIANTS); // 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. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [File::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/mod.rs b/src/board/mod.rs index ad91192..0e34331 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -4,6 +4,9 @@ pub use bitboard::*; pub mod castle_rights; pub use castle_rights::*; +pub mod chess_board; +pub use chess_board::*; + pub mod color; pub use color::*; @@ -13,6 +16,12 @@ pub use direction::*; pub mod file; pub use file::*; +pub mod r#move; +pub use r#move::*; + +pub mod piece; +pub use piece::*; + pub mod rank; pub use rank::*; diff --git a/src/board/move.rs b/src/board/move.rs new file mode 100644 index 0000000..c7a6980 --- /dev/null +++ b/src/board/move.rs @@ -0,0 +1,232 @@ +use super::{Piece, Square}; + +type Bitset = u32; + +/// A chess move, containing: +/// * Piece type. +/// * Starting square. +/// * Destination square. +/// * Optional capture type. +/// * Optional promotion type. +/// * Optional captured type. +/// * Whether the move was an en-passant capture. +/// * Whether the move was a double-step. +/// * Whether the move was a castling. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Move(Bitset); + +/// A builder for [Move]. This is the prefered and only way of building a [Move]. +pub struct MoveBuilder { + pub piece: Piece, + pub start: Square, + pub destination: Square, + pub capture: Option, + 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. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + 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 { + let index = ((self.0 >> shift::START) & shift::START_MASK) as usize; + // SAFETY: we know the value is in-bounds + unsafe { Square::from_index_unchecked(index) } + } + + /// Get the [Square] that this move ends on. + #[inline(always)] + pub fn destination(self) -> Square { + let index = ((self.0 >> shift::DESTINATION) & shift::DESTINATION_MASK) as usize; + // SAFETY: we know the value is in-bounds + unsafe { Square::from_index_unchecked(index) } + } + + /// Get the [Piece] that this move captures, or `None` if there are no captures. + #[inline(always)] + pub fn capture(self) -> Option { + 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 { + 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 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 new file mode 100644 index 0000000..58f989a --- /dev/null +++ b/src/board/piece.rs @@ -0,0 +1,72 @@ +/// An enum representing the type of a piece. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Piece { + King, + Queen, + Rook, + Bishop, + Knight, + Pawn, +} + +impl Piece { + /// The number of [Piece] variants. + pub const NUM_VARIANTS: usize = 6; + + const ALL: [Self; Self::NUM_VARIANTS] = [ + Self::King, + Self::Queen, + Self::Rook, + Self::Bishop, + Self::Knight, + Self::Pawn, + ]; + + /// Iterate over all piece types. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + + /// Convert from a piece index into a [Piece] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + 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. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Piece::index()]. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [Piece]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(Piece::from_index(0), Piece::King); + assert_eq!(Piece::from_index(1), Piece::Queen); + assert_eq!(Piece::from_index(5), Piece::Pawn); + } + + #[test] + fn index() { + assert_eq!(Piece::King.index(), 0); + assert_eq!(Piece::Queen.index(), 1); + assert_eq!(Piece::Pawn.index(), 5); + } +} diff --git a/src/board/rank.rs b/src/board/rank.rs index a3c783a..f448df5 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -15,31 +15,38 @@ pub enum Rank { } impl Rank { - const ALL: [Rank; 8] = [ - Rank::First, - Rank::Second, - Rank::Third, - Rank::Fourth, - Rank::Fifth, - Rank::Sixth, - Rank::Seventh, - Rank::Eighth, + /// The number of [Rank] variants. + pub const NUM_VARIANTS: usize = 8; + + const ALL: [Self; Self::NUM_VARIANTS] = [ + Self::First, + Self::Second, + Self::Third, + Self::Fourth, + Self::Fifth, + Self::Sixth, + Self::Seventh, + Self::Eighth, ]; /// Iterate over all ranks in order. - pub fn iter() -> impl Iterator { + pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() } /// Convert from a rank index into a [Rank] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 8); + assert!(index < Self::NUM_VARIANTS); // 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. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Rank::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/square.rs b/src/board/square.rs index e8588eb..958c3c9 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -2,7 +2,7 @@ use super::{Bitboard, File, Rank}; use crate::utils::static_assert; /// Represent a square on a chessboard. Defined in the same order as the -/// [Bitboard](crate::board::Bitboard) squares. +/// [Bitboard] squares. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[rustfmt::skip] pub enum Square { @@ -18,13 +18,16 @@ pub enum Square { impl std::fmt::Display for Square { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", format!("{:?}", self)) + write!(f, "{:?}", self) } } impl Square { + /// The number of [Square] variants. + pub const NUM_VARIANTS: usize = 64; + #[rustfmt::skip] - const ALL: [Self; 64] = [ + const ALL: [Self; Self::NUM_VARIANTS] = [ Self::A1, Self::A2, Self::A3, Self::A4, Self::A5, Self::A6, Self::A7, Self::A8, Self::B1, Self::B2, Self::B3, Self::B4, Self::B5, Self::B6, Self::B7, Self::B8, Self::C1, Self::C2, Self::C3, Self::C4, Self::C5, Self::C6, Self::C7, Self::C8, @@ -43,19 +46,23 @@ impl Square { } /// Iterate over all squares in order. - pub fn iter() -> impl Iterator { + pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() } /// Convert from a square index into a [Square] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 64); + assert!(index < Self::NUM_VARIANTS); // 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. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Square::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) @@ -106,6 +113,7 @@ impl std::ops::Shl for Square { #[inline(always)] fn shl(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] Square::from_index(self as usize + rhs) } } @@ -116,6 +124,7 @@ impl std::ops::Shr for Square { #[inline(always)] fn shr(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] Square::from_index(self as usize - rhs) } } diff --git a/src/fen.rs b/src/fen.rs new file mode 100644 index 0000000..3096c95 --- /dev/null +++ b/src/fen.rs @@ -0,0 +1,283 @@ +use crate::board::{ + CastleRights, ChessBoard, ChessBoardBuilder, Color, File, InvalidError, Piece, Rank, Square, +}; + +/// 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(InvalidError), +} + +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 [InvalidError] into [FenError], for use with the '?' operator. +impl From for FenError { + fn from(err: InvalidError) -> Self { + Self::InvalidPosition(err) + } +} + +/// Convert the castling rights segment of a FEN string to an array of [CastleRights]. +impl FromFen for [CastleRights; 2] { + type Err = FenError; + + fn from_fen(s: &str) -> Result { + if s.len() > 4 { + return Err(FenError::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(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; 2]>::from_fen(castling_rights)?; + for color in Color::iter() { + builder.with_castle_rights(castle_rights[color.index()], color); + } + + let side = Color::from_fen(side_to_move)?; + builder.with_current_player(side); + + if let Some(square) = Option::::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_total_plies( + (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }, + ); + + { + 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())?, + }; + + // 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::MoveBuilder; + + 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.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!( + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap(), + position + ); + // 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!( + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap(), + position + ); + // 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!( + 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 3593172..82467ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,4 @@ pub mod board; +pub mod fen; +pub mod movegen; pub mod utils; diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs new file mode 100644 index 0000000..f9ce658 --- /dev/null +++ b/src/movegen/mod.rs @@ -0,0 +1,9 @@ +// Naive move generation +mod naive; + +// Magic bitboard generation +mod wizardry; + +// Magic bitboard definitions +mod moves; +pub use moves::*; diff --git a/src/movegen/moves.rs b/src/movegen/moves.rs new file mode 100644 index 0000000..9840083 --- /dev/null +++ b/src/movegen/moves.rs @@ -0,0 +1,145 @@ +use std::sync::OnceLock; + +use crate::{ + board::{Bitboard, Color, File, Square}, + movegen::{ + naive, + wizardry::{ + generate_bishop_magics, generate_rook_magics, MagicMoves, RandGen, BISHOP_SEED, + ROOK_SEED, + }, + }, +}; + +// A pre-rolled RNG for magic bitboard generation, using pre-determined values. +struct PreRolledRng { + numbers: [u64; 64], + current_index: usize, +} + +impl PreRolledRng { + pub fn new(numbers: [u64; 64]) -> 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; 64]; 2]> = 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; 64]; 2]; + 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; 64]; 2]> = OnceLock::new(); + + PAWN_ATTACKS.get_or_init(|| { + let mut res = [[Bitboard::EMPTY; 64]; 2]; + 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; 64]> = OnceLock::new(); + KNIGHT_MOVES.get_or_init(|| { + let mut res = [Bitboard::EMPTY; 64]; + 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; 64]> = OnceLock::new(); + KING_MOVES.get_or_init(|| { + let mut res = [Bitboard::EMPTY; 64]; + 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 new file mode 100644 index 0000000..7a2c97f --- /dev/null +++ b/src/movegen/naive/bishop.rs @@ -0,0 +1,69 @@ +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)) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::{File, Rank}; + + #[test] + fn moves_lower_left_square() { + assert_eq!( + bishop_moves(Square::A1, Bitboard::EMPTY), + Bitboard::DIAGONAL - Square::A1 + ); + assert_eq!( + bishop_moves(Square::A1, Bitboard::ALL), + Square::B2.into_bitboard() + ); + assert_eq!( + bishop_moves(Square::A1, Square::D4.into_bitboard()), + Square::B2 | Square::C3 | Square::D4 + ); + assert_eq!( + bishop_moves(Square::A1, File::D.into_bitboard()), + Square::B2 | Square::C3 | Square::D4 + ); + } + + #[test] + fn moves_middle() { + let cross = Bitboard::DIAGONAL | Direction::South.move_board(Bitboard::ANTI_DIAGONAL); + assert_eq!( + bishop_moves(Square::D4, Bitboard::EMPTY), + cross - Square::D4 + ); + assert_eq!( + bishop_moves(Square::D4, Bitboard::ALL), + Square::C3 | Square::C5 | Square::E3 | Square::E5 + ); + assert_eq!( + bishop_moves(Square::D4, Rank::Fifth.into_bitboard()), + Square::A1 + | Square::B2 + | Square::C3 + | Square::C5 + | Square::E3 + | Square::E5 + | Square::F2 + | Square::G1 + ); + assert_eq!( + bishop_moves(Square::D4, File::E.into_bitboard()), + Square::A1 + | Square::A7 + | Square::B2 + | Square::B6 + | Square::C3 + | Square::C5 + | Square::E3 + | Square::E5 + ); + } +} diff --git a/src/movegen/naive/king.rs b/src/movegen/naive/king.rs new file mode 100644 index 0000000..fdbedb7 --- /dev/null +++ b/src/movegen/naive/king.rs @@ -0,0 +1,172 @@ +use crate::board::{Bitboard, Direction, Square}; + +/// Compute a king's movement. No castling moves included +pub fn king_moves(square: Square) -> Bitboard { + let board = square.into_bitboard(); + + Direction::iter_royalty() + .map(|dir| dir.move_board(board)) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn moves_first_rank() { + assert_eq!(king_moves(Square::A1), Square::A2 | Square::B1 | Square::B2); + assert_eq!( + king_moves(Square::B1), + Square::A1 | Square::A2 | Square::B2 | Square::C1 | Square::C2 + ); + assert_eq!( + king_moves(Square::C1), + Square::B1 | Square::B2 | Square::C2 | Square::D1 | Square::D2 + ); + assert_eq!( + king_moves(Square::D1), + Square::C1 | Square::C2 | Square::D2 | Square::E1 | Square::E2 + ); + assert_eq!( + king_moves(Square::E1), + Square::D1 | Square::D2 | Square::E2 | Square::F1 | Square::F2 + ); + assert_eq!( + king_moves(Square::F1), + Square::E1 | Square::E2 | Square::F2 | Square::G1 | Square::G2 + ); + assert_eq!( + king_moves(Square::G1), + Square::F1 | Square::F2 | Square::G2 | Square::H1 | Square::H2 + ); + assert_eq!(king_moves(Square::H1), Square::G1 | Square::G2 | Square::H2); + } + + #[test] + fn moves_last_rank() { + assert_eq!(king_moves(Square::A8), Square::A7 | Square::B8 | Square::B7); + assert_eq!( + king_moves(Square::B8), + Square::A8 | Square::A7 | Square::B7 | Square::C8 | Square::C7 + ); + assert_eq!( + king_moves(Square::C8), + Square::B8 | Square::B7 | Square::C7 | Square::D8 | Square::D7 + ); + assert_eq!( + king_moves(Square::D8), + Square::C8 | Square::C7 | Square::D7 | Square::E8 | Square::E7 + ); + assert_eq!( + king_moves(Square::E8), + Square::D8 | Square::D7 | Square::E7 | Square::F8 | Square::F7 + ); + assert_eq!( + king_moves(Square::F8), + Square::E8 | Square::E7 | Square::F7 | Square::G8 | Square::G7 + ); + assert_eq!( + king_moves(Square::G8), + Square::F8 | Square::F7 | Square::G7 | Square::H8 | Square::H7 + ); + assert_eq!(king_moves(Square::H8), Square::G8 | Square::G7 | Square::H7); + } + + #[test] + fn moves_first_file() { + assert_eq!(king_moves(Square::A1), Square::A2 | Square::B1 | Square::B2); + assert_eq!( + king_moves(Square::A2), + Square::A1 | Square::A3 | Square::B1 | Square::B2 | Square::B3 + ); + assert_eq!( + king_moves(Square::A3), + Square::A2 | Square::A4 | Square::B2 | Square::B3 | Square::B4 + ); + assert_eq!( + king_moves(Square::A4), + Square::A3 | Square::A5 | Square::B3 | Square::B4 | Square::B5 + ); + assert_eq!( + king_moves(Square::A5), + Square::A4 | Square::A6 | Square::B4 | Square::B5 | Square::B6 + ); + assert_eq!( + king_moves(Square::A6), + Square::A5 | Square::A7 | Square::B5 | Square::B6 | Square::B7 + ); + assert_eq!( + king_moves(Square::A7), + Square::A6 | Square::A8 | Square::B6 | Square::B7 | Square::B8 + ); + assert_eq!(king_moves(Square::A8), Square::A7 | Square::B7 | Square::B8); + } + + #[test] + fn moves_last_file() { + assert_eq!(king_moves(Square::H1), Square::H2 | Square::G1 | Square::G2); + assert_eq!( + king_moves(Square::H2), + Square::H1 | Square::H3 | Square::G1 | Square::G2 | Square::G3 + ); + assert_eq!( + king_moves(Square::H3), + Square::H2 | Square::H4 | Square::G2 | Square::G3 | Square::G4 + ); + assert_eq!( + king_moves(Square::H4), + Square::H3 | Square::H5 | Square::G3 | Square::G4 | Square::G5 + ); + assert_eq!( + king_moves(Square::H5), + Square::H4 | Square::H6 | Square::G4 | Square::G5 | Square::G6 + ); + assert_eq!( + king_moves(Square::H6), + Square::H5 | Square::H7 | Square::G5 | Square::G6 | Square::G7 + ); + assert_eq!( + king_moves(Square::H7), + Square::H6 | Square::H8 | Square::G6 | Square::G7 | Square::G8 + ); + assert_eq!(king_moves(Square::H8), Square::H7 | Square::G7 | Square::G8); + } + + #[test] + fn moves_middle() { + assert_eq!( + king_moves(Square::D4), + Square::C3 + | Square::C4 + | Square::C5 + | Square::D3 + | Square::D5 + | Square::E3 + | Square::E4 + | Square::E5 + ); + assert_eq!( + king_moves(Square::D5), + Square::C4 + | Square::C5 + | Square::C6 + | Square::D4 + | Square::D6 + | Square::E4 + | Square::E5 + | Square::E6 + ); + assert_eq!( + king_moves(Square::E5), + Square::D4 + | Square::D5 + | Square::D6 + | Square::E4 + | Square::E6 + | Square::F4 + | Square::F5 + | Square::F6 + ); + } +} diff --git a/src/movegen/naive/knight.rs b/src/movegen/naive/knight.rs new file mode 100644 index 0000000..28ad7f2 --- /dev/null +++ b/src/movegen/naive/knight.rs @@ -0,0 +1,183 @@ +use crate::board::{Bitboard, Direction, Square}; + +/// Compute a knight's movement. +pub fn knight_moves(square: Square) -> Bitboard { + let board = square.into_bitboard(); + + Direction::iter_knight() + .map(|dir| dir.move_board(board)) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn moves_first_rank() { + assert_eq!(knight_moves(Square::A1), Square::B3 | Square::C2); + assert_eq!( + knight_moves(Square::B1), + Square::A3 | Square::C3 | Square::D2 + ); + assert_eq!( + knight_moves(Square::C1), + Square::A2 | Square::B3 | Square::D3 | Square::E2 + ); + assert_eq!( + knight_moves(Square::D1), + Square::B2 | Square::C3 | Square::E3 | Square::F2 + ); + assert_eq!( + knight_moves(Square::E1), + Square::C2 | Square::D3 | Square::F3 | Square::G2 + ); + assert_eq!( + knight_moves(Square::F1), + Square::D2 | Square::E3 | Square::G3 | Square::H2 + ); + assert_eq!( + knight_moves(Square::G1), + Square::E2 | Square::F3 | Square::H3 + ); + assert_eq!(knight_moves(Square::H1), Square::F2 | Square::G3); + } + + #[test] + fn moves_last_rank() { + assert_eq!(knight_moves(Square::A8), Square::B6 | Square::C7); + assert_eq!( + knight_moves(Square::B8), + Square::A6 | Square::C6 | Square::D7 + ); + assert_eq!( + knight_moves(Square::C8), + Square::A7 | Square::B6 | Square::D6 | Square::E7 + ); + assert_eq!( + knight_moves(Square::D8), + Square::B7 | Square::C6 | Square::E6 | Square::F7 + ); + assert_eq!( + knight_moves(Square::E8), + Square::C7 | Square::D6 | Square::F6 | Square::G7 + ); + assert_eq!( + knight_moves(Square::F8), + Square::D7 | Square::E6 | Square::G6 | Square::H7 + ); + assert_eq!( + knight_moves(Square::G8), + Square::E7 | Square::F6 | Square::H6 + ); + assert_eq!(knight_moves(Square::H8), Square::F7 | Square::G6); + } + + #[test] + fn moves_first_file() { + assert_eq!(knight_moves(Square::A1), Square::B3 | Square::C2); + assert_eq!( + knight_moves(Square::A2), + Square::B4 | Square::C1 | Square::C3 + ); + assert_eq!( + knight_moves(Square::A3), + Square::B1 | Square::B5 | Square::C2 | Square::C4 + ); + assert_eq!( + knight_moves(Square::A4), + Square::B2 | Square::B6 | Square::C3 | Square::C5 + ); + assert_eq!( + knight_moves(Square::A5), + Square::B3 | Square::B7 | Square::C4 | Square::C6 + ); + assert_eq!( + knight_moves(Square::A6), + Square::B4 | Square::B8 | Square::C5 | Square::C7 + ); + assert_eq!( + knight_moves(Square::A7), + Square::B5 | Square::C6 | Square::C8 + ); + assert_eq!(knight_moves(Square::A8), Square::B6 | Square::C7); + } + + #[test] + fn moves_last_file() { + assert_eq!(knight_moves(Square::H1), Square::G3 | Square::F2); + assert_eq!( + knight_moves(Square::H2), + Square::G4 | Square::F1 | Square::F3 + ); + assert_eq!( + knight_moves(Square::H3), + Square::G1 | Square::G5 | Square::F2 | Square::F4 + ); + assert_eq!( + knight_moves(Square::H4), + Square::G2 | Square::G6 | Square::F3 | Square::F5 + ); + assert_eq!( + knight_moves(Square::H5), + Square::G3 | Square::G7 | Square::F4 | Square::F6 + ); + assert_eq!( + knight_moves(Square::H6), + Square::G4 | Square::G8 | Square::F5 | Square::F7 + ); + assert_eq!( + knight_moves(Square::H7), + Square::G5 | Square::F6 | Square::F8 + ); + assert_eq!(knight_moves(Square::H8), Square::G6 | Square::F7); + } + + #[test] + fn moves_middle() { + assert_eq!( + knight_moves(Square::D4), + Square::B3 + | Square::B5 + | Square::C2 + | Square::C6 + | Square::E2 + | Square::E6 + | Square::F3 + | Square::F5 + ); + assert_eq!( + knight_moves(Square::D5), + Square::B4 + | Square::B6 + | Square::C3 + | Square::C7 + | Square::E3 + | Square::E7 + | Square::F4 + | Square::F6 + ); + assert_eq!( + knight_moves(Square::E4), + Square::C3 + | Square::C5 + | Square::D2 + | Square::D6 + | Square::F2 + | Square::F6 + | Square::G3 + | Square::G5 + ); + assert_eq!( + knight_moves(Square::E5), + Square::C4 + | Square::C6 + | Square::D3 + | Square::D7 + | Square::F3 + | Square::F7 + | Square::G4 + | Square::G6 + ); + } +} diff --git a/src/movegen/naive/mod.rs b/src/movegen/naive/mod.rs new file mode 100644 index 0000000..1c64606 --- /dev/null +++ b/src/movegen/naive/mod.rs @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..bde5215 --- /dev/null +++ b/src/movegen/naive/pawn.rs @@ -0,0 +1,137 @@ +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; + } + + let dir = color.forward_direction(); + + let first_push = dir.move_board(square.into_bitboard()); + let second_push = if square.rank() == color.second_rank() { + Square::new(square.file(), color.fourth_rank()).into_bitboard() + } else { + Bitboard::EMPTY + }; + + if (first_push & blockers).is_empty() { + first_push | second_push + } else { + Bitboard::EMPTY + } +} + +/// 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; + } + + let dir = color.forward_direction(); + + let advanced = dir.move_board(square.into_bitboard()); + + let attack_west = Direction::West.move_board(advanced); + let attack_east = Direction::East.move_board(advanced); + + attack_west | attack_east +} + +/// Computes the set of squares that can capture this one *en-passant*. +#[allow(unused)] +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::*; + + #[test] + fn moves() { + assert_eq!( + pawn_moves(Color::White, Square::A2, Bitboard::EMPTY), + Square::A3 | Square::A4 + ); + assert_eq!( + pawn_moves(Color::Black, Square::A7, Bitboard::EMPTY), + Square::A5 | Square::A6 + ); + assert_eq!( + pawn_moves(Color::Black, Square::A2, Bitboard::EMPTY), + Square::A1.into_bitboard() + ); + assert_eq!( + pawn_moves(Color::White, Square::A7, Bitboard::EMPTY), + Square::A8.into_bitboard() + ); + } + + #[test] + fn captures() { + assert_eq!( + pawn_captures(Color::White, Square::A2), + Square::B3.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::White, Square::B2), + Square::A3 | Square::C3 + ); + assert_eq!( + pawn_captures(Color::White, Square::H2), + Square::G3.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::Black, Square::A2), + Square::B1.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::Black, Square::B2), + Square::A1 | Square::C1 + ); + assert_eq!( + pawn_captures(Color::Black, Square::H2), + Square::G1.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::White, Square::A7), + Square::B8.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::White, Square::B7), + Square::A8 | Square::C8 + ); + assert_eq!( + pawn_captures(Color::Black, Square::H7), + Square::G6.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::Black, Square::A7), + Square::B6.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::Black, Square::B7), + Square::A6 | Square::C6 + ); + assert_eq!( + pawn_captures(Color::Black, Square::H7), + 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 new file mode 100644 index 0000000..e61f5ec --- /dev/null +++ b/src/movegen/naive/rook.rs @@ -0,0 +1,54 @@ +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)) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::{File, Rank}; + + #[test] + fn moves_lower_left_square() { + assert_eq!( + rook_moves(Square::A1, Bitboard::EMPTY), + (File::A.into_bitboard() | Rank::First.into_bitboard()) - Square::A1 + ); + assert_eq!( + rook_moves(Square::A1, Bitboard::ALL), + Square::A2 | Square::B1 + ); + assert_eq!( + rook_moves(Square::A1, Rank::First.into_bitboard()), + (File::A.into_bitboard() | Square::B1) - Square::A1 + ); + assert_eq!( + rook_moves(Square::A1, File::A.into_bitboard()), + (Rank::First.into_bitboard() | Square::A2) - Square::A1 + ); + } + + #[test] + fn moves_middle() { + assert_eq!( + rook_moves(Square::D4, Bitboard::EMPTY), + (File::D.into_bitboard() | Rank::Fourth.into_bitboard()) - Square::D4 + ); + assert_eq!( + rook_moves(Square::D4, Bitboard::ALL), + Square::C4 | Square::D3 | Square::D5 | Square::E4 + ); + assert_eq!( + rook_moves(Square::D4, Rank::Fourth.into_bitboard()), + (File::D.into_bitboard() | Square::C4 | Square::E4) - Square::D4 + ); + assert_eq!( + rook_moves(Square::D4, File::D.into_bitboard()), + (Rank::Fourth.into_bitboard() | Square::D3 | Square::D5) - Square::D4 + ); + } +} diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs new file mode 100644 index 0000000..0322977 --- /dev/null +++ b/src/movegen/wizardry/generation.rs @@ -0,0 +1,70 @@ +use crate::board::{Bitboard, Square}; +use crate::movegen::naive::{bishop_moves, rook_moves}; + +use super::mask::{generate_bishop_mask, generate_rook_mask}; +use super::Magic; + +/// A trait to represent RNG for u64 values. +pub(crate) trait RandGen { + fn gen(&mut self) -> u64; +} + +type MagicGenerationType = (Vec, Vec); + +pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType { + generate_magics(rng, generate_bishop_mask, bishop_moves) +} + +pub fn generate_rook_magics(rng: &mut dyn RandGen) -> MagicGenerationType { + generate_magics(rng, generate_rook_mask, rook_moves) +} + +fn generate_magics( + rng: &mut dyn RandGen, + mask_fn: impl Fn(Square) -> Bitboard, + moves_fn: impl Fn(Square, Bitboard) -> Bitboard, +) -> MagicGenerationType { + let mut magics = Vec::new(); + let mut boards = Vec::new(); + + for square in Square::iter() { + let mask = mask_fn(square); + + let occupancy_to_moves: Vec<_> = mask + .iter_power_set() + .map(|occupancy| (occupancy, moves_fn(square, occupancy))) + .collect(); + + 'candidate_search: loop { + let mut candidate = Magic { + magic: magic_candidate(rng), + offset: 0, + mask, + shift: (64 - mask.count()) as u8, + }; + let mut candidate_moves = vec![Bitboard::EMPTY; occupancy_to_moves.len()]; + + for (occupancy, moves) in occupancy_to_moves.iter().cloned() { + let index = candidate.get_index(occupancy); + // Non-constructive collision, try with another candidate + if !candidate_moves[index].is_empty() && candidate_moves[index] != moves { + continue 'candidate_search; + } + candidate_moves[index] = moves; + } + + // We have filled all candidate boards, record the correct offset and add the moves + candidate.offset = boards.len(); + magics.push(candidate); + boards.append(&mut candidate_moves); + break; + } + } + + (magics, boards) +} + +fn magic_candidate(rng: &mut dyn RandGen) -> u64 { + // Few bits makes for better candidates + rng.gen() & rng.gen() & rng.gen() +} diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs new file mode 100644 index 0000000..865c986 --- /dev/null +++ b/src/movegen/wizardry/mask.rs @@ -0,0 +1,38 @@ +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); + + let mask = File::A.into_bitboard() + | File::H.into_bitboard() + | Rank::First.into_bitboard() + | Rank::Eighth.into_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); + + let mask = { + let mut mask = Bitboard::EMPTY; + if square.file() != File::A { + mask |= File::A.into_bitboard() + }; + if square.file() != File::H { + mask |= File::H.into_bitboard() + }; + if square.rank() != Rank::First { + mask |= Rank::First.into_bitboard() + }; + if square.rank() != Rank::Eighth { + mask |= Rank::Eighth.into_bitboard() + }; + mask + }; + + rays - mask +} diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs new file mode 100644 index 0000000..00645d8 --- /dev/null +++ b/src/movegen/wizardry/mod.rs @@ -0,0 +1,283 @@ +mod generation; +pub(super) use 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; 64] = [ + 4908958787341189172, + 1157496606860279808, + 289395876198088778, + 649648646467355137, + 19162426089930848, + 564067194896448, + 18586170375029026, + 9185354800693760, + 72172012436987968, + 317226351607872, + 2597178509285688384, + 1162205282238464, + 144154788211329152, + 172197832046936160, + 4625762105940000802, + 1477217245166903296, + 2251937789583872, + 289373902621379585, + 4616200855845409024, + 2251909637357568, + 3532510975437640064, + 563517968228352, + 562953309660434, + 1196005458310201856, + 2350914225914520576, + 2287018679861376, + 13836188353273790593, + 11267795163676832, + 297519119119499264, + 18588344158519552, + 10453428171813953792, + 72128237668534272, + 1298164929055953920, + 865575144395900952, + 9293076573325312, + 108104018148197376, + 578503662094123152, + 4665870505495102224, + 6066493872259301520, + 285877477613857, + 2328941618281318466, + 721165292771739652, + 4899973577790523400, + 75050392749184, + 2305878200632215680, + 11530099074925593616, + 290561512873919880, + 18652187227888000, + 3379933716168704, + 9223409493537718272, + 22273835729926, + 1152921524003672064, + 4647812741240848385, + 1244225087719112712, + 7367907171013001728, + 9263922034316951570, + 300758214358598160, + 4611686331973636096, + 2377900605806479360, + 6958097192913601024, + 864691130877743617, + 703824948904066, + 612700674899317536, + 180742128018784384, +]; + +/// A set of magic numbers for rook move generation. +pub(crate) const ROOK_SEED: [u64; 64] = [ + 2341871943948451840, + 18015635528220736, + 72066665545773824, + 1188959097794342912, + 12141713393631625314, + 720649693658353672, + 36029896538981888, + 36033359356363520, + 140746619355268, + 1158339898446446661, + 36591886560003650, + 578853633228023808, + 2392554490300416, + 140814806160384, + 180706952366596608, + 10696087878779396, + 1153260703948210820, + 310748649170673678, + 36311372044308544, + 9223444604757615104, + 1267187285230592, + 282574622818306, + 18722484274726152, + 2271591090110593, + 1153063519847989248, + 10168327557107712, + 4507998211276833, + 1153203035420233728, + 4631961017139660032, + 2454499182462107776, + 289367288355753288, + 18015815850820609, + 9268726066908758912, + 11547264697673728000, + 2314929519368081536, + 140943655192577, + 20266215511427202, + 180706969441535248, + 1302683805944911874, + 11534000122299940994, + 22676602724843520, + 4639271120198041668, + 1302104069046927376, + 9184220895313928, + 4612249105954373649, + 562984581726212, + 2312678200579457040, + 4647736876550193157, + 3170604524138139776, + 4684447574787096704, + 20283792725901696, + 1152992019380963840, + 117383863558471808, + 1153488854922068096, + 17596884583424, + 90074759127192064, + 4900502436426416706, + 4573968656793901, + 1161084564408385, + 1657887889314811910, + 4614501455660058690, + 4612530729109422081, + 642458506527236, + 1116704154754, +]; +// endregion:sourcegen + +#[cfg(test)] +mod test { + use super::*; + + // A simple XOR-shift RNG implementation. + struct SimpleRng(u64); + + impl SimpleRng { + pub fn new() -> Self { + Self(4) // https://xkcd.com/221/ + } + } + + impl RandGen for SimpleRng { + fn gen(&mut self) -> u64 { + self.0 ^= self.0 >> 12; + self.0 ^= self.0 << 25; + self.0 ^= self.0 >> 27; + self.0 + } + } + + #[test] + fn rng() { + let mut rng = SimpleRng::new(); + + assert_eq!(rng.gen(), 134217733); + assert_eq!(rng.gen(), 4504699139039237); + assert_eq!(rng.gen(), 13512173405898766); + assert_eq!(rng.gen(), 9225626310854853124); + assert_eq!(rng.gen(), 29836777971867270); + } + + 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 mut res = format!( + "/// A set of magic numbers for {} move generation.\n", + piece_type + ); + res.push_str(&format!( + "pub(crate) const {}_SEED: [u64; 64] = [\n", + piece_type.to_uppercase() + )); + for magic in values { + res.push_str(&format!(" {},\n", magic.magic)); + } + res.push_str("];\n"); + res + } + + #[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/static_assert.rs b/src/utils/static_assert.rs index 81b7a86..2c7a9a8 100644 --- a/src/utils/static_assert.rs +++ b/src/utils/static_assert.rs @@ -15,12 +15,9 @@ /// ``` #[macro_export] macro_rules! static_assert { - ($condition:expr) => { - // Based on the latest one in `rustc`'s one before it was [removed]. - // - // [removed]: https://github.com/rust-lang/rust/commit/c2dad1c6b9f9636198d7c561b47a2974f5103f6d + ($($tt:tt)*) => { #[allow(dead_code)] - const _: () = [()][!($condition) as usize]; + const _: () = assert!($($tt)*); }; } diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 48a2e94..0481d8d 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -1,17 +1,24 @@ +import enum + +import gdb import gdb.printing + class Square(object): """ - Wrapper around GDB's representation of a 'seer::board::square::Square' in - memory. + Python representation of a 'seer::board::square::Square' raw value. """ - 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): self._val = 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] @@ -23,10 +30,10 @@ class Square(object): def file(self): return int(self._val) // 8 + class Bitboard(object): """ - Wrapper around GDB's representation of a 'seer::board::bitboard::Bitboard' - in memory. + Python representation of a 'seer::board::bitboard::Bitboard' raw value. """ def __init__(self, val): @@ -35,14 +42,260 @@ class Bitboard(object): 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 = int(self._val["__0"]) + n = self._val 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 + + 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 + + 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 + + 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 + + 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 + + def __str__(self): + return self.name.title() + + +class Move(object): + """ + Wrapper around GDB's representation of a 'seer::board::move::Move' + in memory. + """ + + # Should be kept in sync with the values in `move.rs` + PIECE_SHIFT = 0 + PIECE_MASK = 0b111 + START_SHIFT = 3 + START_MASK = 0b11_1111 + DESTINATION_SHIFT = 9 + DESTINATION_MASK = 0b11_1111 + CAPTURE_SHIFT = 15 + CAPTURE_MASK = 0b111 + PROMOTION_SHIFT = 18 + PROMOTION_MASK = 0b111 + EN_PASSANT_SHIFT = 21 + EN_PASSANT_MASK = 0b1 + DOUBLE_STEP_SHIFT = 22 + DOUBLE_STEP_MASK = 0b1 + CASTLING_SHIFT = 23 + CASTLING_MASK = 0b1 + + def __init__(self, val): + self._val = val + + @property + def piece(self): + return Piece(self._val >> self.PIECE_SHIFT & self.PIECE_MASK) + + @property + def start(self): + return Square(self._val >> self.START_SHIFT & self.START_MASK) + + @property + def destination(self): + return Square(self._val >> self.DESTINATION_SHIFT & self.DESTINATION_MASK) + + @property + def capture(self): + index = self._val >> self.CAPTURE_SHIFT & self.CAPTURE_MASK + if index == 7: + return None + return Piece(index) + + @property + def promotion(self): + index = self._val >> self.PROMOTION_SHIFT & self.PROMOTION_MASK + if index == 7: + return None + return Piece(index) + + @property + def en_passant(self): + return bool(self._val >> self.EN_PASSANT_SHIFT & self.EN_PASSANT_MASK) + + @property + def double_step(self): + return bool(self._val >> self.DOUBLE_STEP_SHIFT & self.DOUBLE_STEP_MASK) + + @property + def castling(self): + return bool(self._val >> self.CASTLING_SHIFT & self.CASTLING_MASK) + + def __str__(self): + KEYS = [ + "piece", + "start", + "destination", + "capture", + "promotion", + "en_passant", + "double_step", + "castling", + ] + print_opt = lambda val: "(None)" if val is None else str(val) + 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, + ): + 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) + + @classmethod + def from_gdb(cls, val): + return cls( + [int(val["piece_occupancy"][p]["__0"]) for p in Piece], + [int(val["color_occupancy"][c]["__0"]) for c in Color], + [int(val["castle_rights"][c]) for c in Color], + # FIXME: find out how to check for Some/None in val["en_passant"], + int(val["half_move_clock"]), + int(val["total_plies"]), + Color(int(val["side"])), + ) + + 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), + ] + return "\n".join(res) + + class SquarePrinter(object): "Print a seer::board::square::Square" @@ -52,27 +305,110 @@ class SquarePrinter(object): 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(val) + self._val = Bitboard(int(val["__0"])) def to_string(self): return "Bitboard{" + str(self._val)[1:-1] + "}" - def display_hint(self): - return 'string' + +class CastleRightsPrinter(object): + "Print a seer::board::castle_rights::CastleRights" + + def __init__(self, val): + self._val = CastleRights(int(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(int(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(int(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(int(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(int(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(int(val["__0"])) + + 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 build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') - pp.add_printer('BigNum', '^seer::board::square::Square$', SquarePrinter) - pp.add_printer('BigNum', '^seer::board::bitboard::Bitboard$', BitboardPrinter) + 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) return pp + +def register_commands(): + PrintBoard() + + gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True) +register_commands()