From 2ad2927b14232a7acaf8b7fdc14545684e5d236a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 19:56:01 +0200 Subject: [PATCH 001/157] Bootstrap build system --- Cargo.lock | 7 +++++++ Cargo.toml | 8 ++++++++ src/main.rs | 3 +++ 3 files changed, 18 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1e43342 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "seer" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f191c81 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "seer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From 454267e4addf0fa84f212dc9c9a61af578d61508 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 19:56:54 +0200 Subject: [PATCH 002/157] Add nix flake --- flake.lock | 116 ++++++++++++++++++++++++++++++++++++++++ flake.nix | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..2601b96 --- /dev/null +++ b/flake.lock @@ -0,0 +1,116 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1656928814, + "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", + "type": "github" + }, + "original": { + "owner": "numtide", + "ref": "master", + "repo": "flake-utils", + "type": "github" + } + }, + "naersk": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1655042882, + "narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=", + "owner": "nix-community", + "repo": "naersk", + "rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "master", + "repo": "naersk", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1657888067, + "narHash": "sha256-GnwJoFBTPfW3+mz7QEeJEEQ9OMHZOiIJ/qDhZxrlKh8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "65fae659e31098ca4ac825a6fef26d890aaf3f4e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1656169028, + "narHash": "sha256-y9DRauokIeVHM7d29lwT8A+0YoGUBXV3H0VErxQeA8s=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "db3bd555d3a3ceab208bed48f983ccaa6a71a25e", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "master", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "naersk": "naersk", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1657853760, + "narHash": "sha256-X6ERAyUXGsrhbhgkxNaQl40wcus5uyQZOCxUh5neK+g=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "a97a761cc11327bb109dc30af1c637b986be7959", + "type": "github" + }, + "original": { + "owner": "oxalica", + "ref": "master", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5767191 --- /dev/null +++ b/flake.nix @@ -0,0 +1,153 @@ +{ + description = "A handy file picker program"; + + inputs = { + flake-utils = { + type = "github"; + owner = "numtide"; + repo = "flake-utils"; + ref = "master"; + }; + + naersk = { + type = "github"; + owner = "nix-community"; + repo = "naersk"; + ref = "master"; + inputs = { + nixpkgs.follows = "nixpkgs"; + }; + }; + + nixpkgs = { + type = "github"; + owner = "NixOS"; + repo = "nixpkgs"; + ref = "nixpkgs-unstable"; + }; + + pre-commit-hooks = { + type = "github"; + owner = "cachix"; + 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"; + nixpkgs.follows = "nixpkgs"; + }; + }; + }; + + outputs = + { self + , flake-utils + , naersk + , nixpkgs + , pre-commit-hooks + , rust-overlay + }: + let + inherit (flake-utils.lib) eachSystem system; + + 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"; + }; + + nixpkgs-fmt = { + enable = true; + }; + + rustfmt = { + enable = true; + entry = lib.mkForce "${rust-env}/bin/cargo-fmt fmt -- --check --color always"; + }; + }; + }; + }; + + devShells = { + default = pkgs.mkShell { + inputsFrom = [ + packages.seer + ]; + + nativeBuildInputs = with pkgs; [ + rust-analyzer + # Not included in the pre-commit hook unfortunately... + clippy + rustfmt + ]; + + inherit (checks.pre-commit) shellHook; + + RUST_SRC_PATH = "${my-rust}/lib/rustlib/src/rust/library"; + }; + }; + + packages = { + default = self.packages."${system}".seer; + + seer = naersk-lib.buildPackage { + src = self; + + passthru = { + inherit my-rust; + }; + }; + }; + }); +} From 36656a6a4042b0fad30f07fa8254b30d09ab21ac Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 19:57:21 +0200 Subject: [PATCH 003/157] Add generated files to git ignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2f669f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Nix files +/result +/.pre-commit-config.yaml + +# Rust files +/target From 2c36ee266d5dacf74ade869365ce52d7a992df02 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:32:39 +0200 Subject: [PATCH 004/157] Move binary crate into 'bin' folder --- src/{main.rs => bin/seer.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{main.rs => bin/seer.rs} (100%) diff --git a/src/main.rs b/src/bin/seer.rs similarity index 100% rename from src/main.rs rename to src/bin/seer.rs From 755e891b179352a6736838dc467fc04dca5013da Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:33:07 +0200 Subject: [PATCH 005/157] Add 'Bitboard' and 'Square' definitions --- src/board/bitboard.rs | 201 ++++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 5 + src/board/square.rs | 208 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 4 files changed, 415 insertions(+) create mode 100644 src/board/bitboard.rs create mode 100644 src/board/mod.rs create mode 100644 src/board/square.rs create mode 100644 src/lib.rs diff --git a/src/board/bitboard.rs b/src/board/bitboard.rs new file mode 100644 index 0000000..50298d9 --- /dev/null +++ b/src/board/bitboard.rs @@ -0,0 +1,201 @@ +use super::Square; + +/// 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. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Bitboard(pub(crate) u64); + +impl Bitboard { + /// An empty bitboard. + pub const EMPTY: Bitboard = Bitboard(0); + + /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. + pub const RANKS: [Self; 8] = [ + Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), + Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010), + Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100), + Bitboard(0b00001000_00001000_00001000_00001000_00001000_00001000_00001000_00001000), + Bitboard(0b00010000_00010000_00010000_00010000_00010000_00010000_00010000_00010000), + Bitboard(0b00100000_00100000_00100000_00100000_00100000_00100000_00100000_00100000), + Bitboard(0b01000000_01000000_01000000_01000000_01000000_01000000_01000000_01000000), + Bitboard(0b10000000_10000000_10000000_10000000_10000000_10000000_10000000_10000000), + ]; + + /// Array of bitboards representing the eight files, in order from file A to file H. + pub const FILES: [Self; 8] = [ + Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111), + Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000), + Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000), + Bitboard(0b00000000_00000000_00000000_00000000_11111111_00000000_00000000_00000000), + Bitboard(0b00000000_00000000_00000000_11111111_00000000_00000000_00000000_00000000), + Bitboard(0b00000000_00000000_11111111_00000000_00000000_00000000_00000000_00000000), + Bitboard(0b00000000_11111111_00000000_00000000_00000000_00000000_00000000_00000000), + Bitboard(0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000), + ]; +} + +impl Default for Bitboard { + fn default() -> Self { + Self::EMPTY + } +} + +/// Treat bitboard as a set of squares, shift each square's index left by the amount given. +impl std::ops::Shl for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn shl(self, rhs: usize) -> Self::Output { + Bitboard(self.0 << rhs) + } +} + +/// Treat bitboard as a set of squares, shift each square's index right by the amount given. +impl std::ops::Shr for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn shr(self, rhs: usize) -> Self::Output { + Bitboard(self.0 >> rhs) + } +} + +/// Treat bitboard as a set of squares, and invert the set. +impl std::ops::Not for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn not(self) -> Self::Output { + Bitboard(!self.0) + } +} + +/// Treat each bitboard as a set of squares, keep squares that are in either sets. +impl std::ops::BitOr for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 | rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::BitOr for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Square) -> Self::Output { + self | rhs.into_bitboard() + } +} + +/// Treat each bitboard as a set of squares, keep squares that are in both sets. +impl std::ops::BitAnd for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitand(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 & rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::BitAnd for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitand(self, rhs: Square) -> Self::Output { + self & rhs.into_bitboard() + } +} + +/// 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; + + #[inline(always)] + fn bitxor(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 ^ rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::BitXor for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitxor(self, rhs: Square) -> Self::Output { + self ^ rhs.into_bitboard() + } +} + +/// Treat each bitboard as a set of squares, and substract one set from another. +impl std::ops::Sub for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn sub(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 & !rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::Sub for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn sub(self, rhs: Square) -> Self::Output { + self - rhs.into_bitboard() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::square::*; + + #[test] + fn left_shift() { + assert_eq!(Bitboard::RANKS[0] << 1, Bitboard::RANKS[1]); + assert_eq!(Bitboard::FILES[0] << 8, Bitboard::FILES[1]); + } + + #[test] + fn right_shift() { + assert_eq!(Bitboard::RANKS[1] >> 1, Bitboard::RANKS[0]); + assert_eq!(Bitboard::FILES[1] >> 8, Bitboard::FILES[0]); + } + + #[test] + fn not() { + assert_eq!(!Bitboard::EMPTY, Bitboard(u64::MAX)); + } + + #[test] + fn or() { + assert_eq!(Bitboard::FILES[0] | Bitboard::FILES[1], Bitboard(0xff_ff)); + assert_eq!(Bitboard::FILES[0] | Square::B1, Bitboard(0x1_ff)); + } + + #[test] + fn and() { + assert_eq!(Bitboard::FILES[0] & Bitboard::FILES[1], Bitboard::EMPTY); + assert_eq!( + Bitboard::FILES[0] & Bitboard::RANKS[0], + Square::A1.into_bitboard() + ); + assert_eq!(Bitboard::FILES[0] & Square::A1, Square::A1.into_bitboard()); + } + + #[test] + fn xor() { + assert_eq!(Bitboard::FILES[0] ^ Square::A1, Bitboard(0xff - 1)); + } + + #[test] + fn sub() { + assert_eq!(Bitboard::FILES[0] - Bitboard::RANKS[0], Bitboard(0xff - 1)); + assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1)); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs new file mode 100644 index 0000000..06a7d91 --- /dev/null +++ b/src/board/mod.rs @@ -0,0 +1,5 @@ +pub mod bitboard; +pub use bitboard::*; + +pub mod square; +pub use square::*; diff --git a/src/board/square.rs b/src/board/square.rs new file mode 100644 index 0000000..78ea625 --- /dev/null +++ b/src/board/square.rs @@ -0,0 +1,208 @@ +use super::Bitboard; + +/// Represent a square on a chessboard. Defined in the same order as the +/// [Bitboard](crate::board::Bitboard) squares. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[rustfmt::skip] +pub enum Square { + A1, A2, A3, A4, A5, A6, A7, A8, + B1, B2, B3, B4, B5, B6, B7, B8, + C1, C2, C3, C4, C5, C6, C7, C8, + D1, D2, D3, D4, D5, D6, D7, D8, + E1, E2, E3, E4, E5, E6, E7, E8, + F1, F2, F3, F4, F5, F6, F7, F8, + G1, G2, G3, G4, G5, G6, G7, G8, + H1, H2, H3, H4, H5, H6, H7, H8, +} + +impl std::fmt::Display for Square { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", format!("{:?}", self)) + } +} + +impl Square { + #[rustfmt::skip] + const ALL: [Self; 64] = [ + 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, + Self::D1, Self::D2, Self::D3, Self::D4, Self::D5, Self::D6, Self::D7, Self::D8, + Self::E1, Self::E2, Self::E3, Self::E4, Self::E5, Self::E6, Self::E7, Self::E8, + Self::F1, Self::F2, Self::F3, Self::F4, Self::F5, Self::F6, Self::F7, Self::F8, + Self::G1, Self::G2, Self::G3, Self::G4, Self::G5, Self::G6, Self::G7, Self::G8, + Self::H1, Self::H2, Self::H3, Self::H4, Self::H5, Self::H6, Self::H7, Self::H8, + ]; + + /// Iterate over all squares in order. + 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); + // 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. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of the rank of this square (0 -> rank 1, ..., 7 -> rank 8). + #[inline(always)] + pub fn rank_index(self) -> usize { + (self as usize) % 8 + } + + /// Return the index of the rank of this square (0 -> file A, ..., 7 -> file H). + #[inline(always)] + pub fn file_index(self) -> usize { + (self as usize) / 8 + } + + /// Return a bitboard representing the rank of this square. + #[inline(always)] + pub fn rank(self) -> Bitboard { + Bitboard::RANKS[self.rank_index()] + } + + /// Return a bitboard representing the rank of this square. + #[inline(always)] + pub fn file(self) -> Bitboard { + Bitboard::FILES[self.file_index()] + } + + /// Turn a square into a singleton bitboard. + #[inline(always)] + pub fn into_bitboard(self) -> Bitboard { + Bitboard(1 << (self as usize)) + } +} + +/// Shift the square's index left by the amount given. +impl std::ops::Shl for Square { + type Output = Square; + + #[inline(always)] + fn shl(self, rhs: usize) -> Self::Output { + Square::from_index(self as usize + rhs) + } +} + +/// Shift the square's index right by the amount given. +impl std::ops::Shr for Square { + type Output = Square; + + #[inline(always)] + fn shr(self, rhs: usize) -> Self::Output { + Square::from_index(self as usize - rhs) + } +} + +/// Return a board containing all squares but the one given. +impl std::ops::Not for Square { + type Output = Bitboard; + + #[inline(always)] + fn not(self) -> Self::Output { + !self.into_bitboard() + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitOr for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Square) -> Self::Output { + self.into_bitboard() | rhs.into_bitboard() + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitOr for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() | rhs + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitAnd for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitand(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() & rhs + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitXor for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitxor(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() ^ rhs + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::Sub for Square { + type Output = Bitboard; + + #[inline(always)] + fn sub(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() - rhs + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::bitboard::*; + + #[test] + fn left_shift() { + assert_eq!(Square::A1 << 1, Square::A2); + assert_eq!(Square::A1 << 8, Square::B1); + } + + #[test] + fn right_shift() { + assert_eq!(Square::A2 >> 1, Square::A1); + assert_eq!(Square::B1 >> 8, Square::A1); + } + + #[test] + fn not() { + assert_eq!(!Square::A1, Bitboard(u64::MAX - 1)); + } + + #[test] + fn or() { + assert_eq!(Square::A1 | Square::A2, Bitboard(0b11)); + } + + #[test] + fn and() { + assert_eq!(Square::A1 & Bitboard::FILES[0], Square::A1.into_bitboard()); + } + + #[test] + fn xor() { + assert_eq!(Square::A1 ^ Bitboard::FILES[0], Bitboard(0xff - 1)); + } + + #[test] + fn sub() { + assert_eq!(Square::A1 - Bitboard::FILES[0], Bitboard::EMPTY); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..667c357 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod board; From 4bc597439ee7df5a14e59c5377a9d2335776ceea Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:52:09 +0200 Subject: [PATCH 006/157] Move 'board::bitboard' into folder module I will be adding a 'BitboardIterator' type, and it makes more sense to use a folder for this module at this point. --- src/board/{bitboard.rs => bitboard/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/board/{bitboard.rs => bitboard/mod.rs} (100%) diff --git a/src/board/bitboard.rs b/src/board/bitboard/mod.rs similarity index 100% rename from src/board/bitboard.rs rename to src/board/bitboard/mod.rs From 3c157efe84f70aaec49b6e434f2c5406f0970381 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:53:08 +0200 Subject: [PATCH 007/157] Add bitboard iteration Introduce 'BitboardIterator', use it to implement 'IntoIterator' for 'Bitboard'. --- src/board/bitboard/iterator.rs | 17 ++++++++++++++ src/board/bitboard/mod.rs | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/board/bitboard/iterator.rs diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs new file mode 100644 index 0000000..06db283 --- /dev/null +++ b/src/board/bitboard/iterator.rs @@ -0,0 +1,17 @@ +/// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a +/// [Bitboard](crate::board::Bitboard). +pub struct BitboardIterator(pub(crate) u64); + +impl Iterator for BitboardIterator { + type Item = crate::board::Square; + + fn next(&mut self) -> Option { + if self.0 == 0 { + None + } else { + let lsb = self.0.trailing_zeros() as usize; + self.0 ^= 1 << lsb; + Some(crate::board::Square::from_index(lsb)) + } + } +} diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 50298d9..edb1015 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,4 +1,6 @@ use super::Square; +mod iterator; +use iterator::*; /// 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. @@ -40,6 +42,16 @@ impl Default for Bitboard { } } +/// Iterate over the [Square](crate::board::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) + } +} + /// Treat bitboard as a set of squares, shift each square's index left by the amount given. impl std::ops::Shl for Bitboard { type Output = Bitboard; @@ -155,6 +167,37 @@ mod test { use super::*; use crate::board::square::*; + #[test] + fn iter() { + assert_eq!(Bitboard::EMPTY.into_iter().collect::>(), Vec::new()); + assert_eq!( + Bitboard::RANKS[0].into_iter().collect::>(), + vec![ + Square::A1, + Square::B1, + Square::C1, + Square::D1, + Square::E1, + Square::F1, + Square::G1, + Square::H1, + ] + ); + assert_eq!( + Bitboard::FILES[0].into_iter().collect::>(), + vec![ + Square::A1, + Square::A2, + Square::A3, + Square::A4, + Square::A5, + Square::A6, + Square::A7, + Square::A8, + ] + ); + } + #[test] fn left_shift() { assert_eq!(Bitboard::RANKS[0] << 1, Bitboard::RANKS[1]); From bf71a5c205010e60fc68c99b077c499e93856e42 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:53:50 +0200 Subject: [PATCH 008/157] Add Drone CI --- .drone.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .drone.yml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..7037529 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,31 @@ +--- +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 +... From 41903be1433fd64d8b55a399c9984078c64ce38e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:58:46 +0200 Subject: [PATCH 009/157] Introduce 'Bitboard::ALL' --- src/board/bitboard/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index edb1015..524cf33 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -11,6 +11,9 @@ impl Bitboard { /// An empty bitboard. pub const EMPTY: Bitboard = Bitboard(0); + /// A full bitboard. + pub const ALL: Bitboard = Bitboard(u64::MAX); + /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. pub const RANKS: [Self; 8] = [ Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), @@ -212,7 +215,7 @@ mod test { #[test] fn not() { - assert_eq!(!Bitboard::EMPTY, Bitboard(u64::MAX)); + assert_eq!(!Bitboard::EMPTY, Bitboard::ALL); } #[test] From fdfc1fcf6303175070f7dd479a379e2c8a7640ac Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 23:52:16 +0200 Subject: [PATCH 010/157] Add GDB pretty-printers --- .gdbinit | 2 + utils/gdb/seer_pretty_printers.py | 78 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 .gdbinit create mode 100644 utils/gdb/seer_pretty_printers.py diff --git a/.gdbinit b/.gdbinit new file mode 100644 index 0000000..d04df33 --- /dev/null +++ b/.gdbinit @@ -0,0 +1,2 @@ +# Register pretty-printers +source utils/gdb/seer_pretty_printers.py diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py new file mode 100644 index 0000000..48a2e94 --- /dev/null +++ b/utils/gdb/seer_pretty_printers.py @@ -0,0 +1,78 @@ +import gdb.printing + +class Square(object): + """ + Wrapper around GDB's representation of a 'seer::board::square::Square' in + memory. + """ + + 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 + + def __str__(self): + return self.FILES[self.file] + self.RANKS[self.rank] + + @property + def rank(self): + return int(self._val) % 8 + + @property + def file(self): + return int(self._val) // 8 + +class Bitboard(object): + """ + Wrapper around GDB's representation of a 'seer::board::bitboard::Bitboard' + in memory. + """ + + def __init__(self, val): + self._val = val + + def __str__(self): + return "[" + ", ".join(map(str, self.squares)) + "]" + + @property + def squares(self): + n = int(self._val["__0"]) + while n: + b = n & (~n+1) + yield Square(b.bit_length() - 1) + n ^= b + +class SquarePrinter(object): + "Print a seer::board::square::Square" + + def __init__(self, val): + self._val = Square(val) + + def to_string(self): + return str(self._val) + + def display_hint(self): + return 'string' + +class BitboardPrinter(object): + "Print a seer::board::bitboard::Bitboard" + + def __init__(self, val): + self._val = Bitboard(val) + + def to_string(self): + return "Bitboard{" + str(self._val)[1:-1] + "}" + + def display_hint(self): + return 'string' + +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) + + return pp + +gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True) From 4eff49f36795e1e2601f69ad73c915e968747ace Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:05:49 +0200 Subject: [PATCH 011/157] Add 'Rank' enum --- src/board/mod.rs | 3 ++ src/board/rank.rs | 85 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/board/rank.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index 06a7d91..ef264e1 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,5 +1,8 @@ pub mod bitboard; pub use bitboard::*; +pub mod rank; +pub use rank::*; + pub mod square; pub use square::*; diff --git a/src/board/rank.rs b/src/board/rank.rs new file mode 100644 index 0000000..fab3cdd --- /dev/null +++ b/src/board/rank.rs @@ -0,0 +1,85 @@ +use super::Bitboard; + +/// An enum representing a singular rank on a chess board (i.e: the rows). +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Rank { + First, + Second, + Third, + Fourth, + Fifth, + Sixth, + Seventh, + Eighth, +} + +impl Rank { + const ALL: [Rank; 8] = [ + Rank::First, + Rank::Second, + Rank::Third, + Rank::Fourth, + Rank::Fifth, + Rank::Sixth, + Rank::Seventh, + Rank::Eighth, + ]; + + /// Iterate over all ranks in order. + 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); + // 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. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [Rank]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + + /// Turn a [Rank] into a [Bitboard] of all squares in that rank. + #[inline(always)] + pub fn into_bitboard(self) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *Bitboard::RANKS.get_unchecked(self.index()) } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(Rank::from_index(0), Rank::First); + assert_eq!(Rank::from_index(1), Rank::Second); + assert_eq!(Rank::from_index(7), Rank::Eighth); + } + + #[test] + fn index() { + assert_eq!(Rank::First.index(), 0); + assert_eq!(Rank::Second.index(), 1); + assert_eq!(Rank::Eighth.index(), 7); + } + + #[test] + fn into_bitboard() { + assert_eq!(Rank::First.into_bitboard(), Bitboard::RANKS[0]); + assert_eq!(Rank::Second.into_bitboard(), Bitboard::RANKS[1]); + assert_eq!(Rank::Eighth.into_bitboard(), Bitboard::RANKS[7]); + } +} From bb04368f41acccb976ff0df63bed2190b2740d10 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:05:56 +0200 Subject: [PATCH 012/157] Add 'File' enum --- src/board/file.rs | 85 +++++++++++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 88 insertions(+) create mode 100644 src/board/file.rs diff --git a/src/board/file.rs b/src/board/file.rs new file mode 100644 index 0000000..a7ab385 --- /dev/null +++ b/src/board/file.rs @@ -0,0 +1,85 @@ +use super::Bitboard; + +/// An enum representing a singular file on a chess board (i.e: the columns). +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum File { + A, + B, + C, + D, + E, + F, + G, + H, +} + +impl File { + const ALL: [File; 8] = [ + File::A, + File::B, + File::C, + File::D, + File::E, + File::F, + File::G, + File::H, + ]; + + /// Iterate over all files in order. + 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); + // 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. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [File]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + + /// Turn a [File] into a [Bitboard] of all squares in that file. + #[inline(always)] + pub fn into_bitboard(self) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *Bitboard::FILES.get_unchecked(self.index()) } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(File::from_index(0), File::A); + assert_eq!(File::from_index(1), File::B); + assert_eq!(File::from_index(7), File::H); + } + + #[test] + fn index() { + assert_eq!(File::A.index(), 0); + assert_eq!(File::B.index(), 1); + assert_eq!(File::H.index(), 7); + } + + #[test] + fn into_bitboard() { + assert_eq!(File::A.into_bitboard(), Bitboard::FILES[0]); + assert_eq!(File::B.into_bitboard(), Bitboard::FILES[1]); + assert_eq!(File::H.into_bitboard(), Bitboard::FILES[7]); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index ef264e1..7923cab 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod file; +pub use file::*; + pub mod rank; pub use rank::*; From 77b15edc36c3f36d9ecf39df29afc85c367ff88e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:20:33 +0200 Subject: [PATCH 013/157] Don't return 'Bitboard' from 'Square::{file,rank}' --- src/board/square.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/board/square.rs b/src/board/square.rs index 78ea625..3cae19e 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -1,4 +1,4 @@ -use super::Bitboard; +use super::{Bitboard, File, Rank}; /// Represent a square on a chessboard. Defined in the same order as the /// [Bitboard](crate::board::Bitboard) squares. @@ -65,16 +65,18 @@ impl Square { (self as usize) / 8 } - /// Return a bitboard representing the rank of this square. + /// Return a [Rank] representing the rank of this square. #[inline(always)] - pub fn rank(self) -> Bitboard { - Bitboard::RANKS[self.rank_index()] + pub fn rank(self) -> Rank { + // SAFETY: we know the value is in-bounds + unsafe { Rank::from_index_unchecked(self.rank_index()) } } - /// Return a bitboard representing the rank of this square. + /// Return a [File] representing the rank of this square. #[inline(always)] - pub fn file(self) -> Bitboard { - Bitboard::FILES[self.file_index()] + pub fn file(self) -> File { + // SAFETY: we know the value is in-bounds + unsafe { File::from_index_unchecked(self.file_index()) } } /// Turn a square into a singleton bitboard. @@ -168,6 +170,24 @@ impl std::ops::Sub for Square { mod test { use super::*; use crate::board::bitboard::*; + use crate::board::file::*; + use crate::board::rank::*; + + #[test] + fn file() { + assert_eq!(Square::A1.file(), File::A); + assert_eq!(Square::A2.file(), File::A); + assert_eq!(Square::B1.file(), File::B); + assert_eq!(Square::H8.file(), File::H); + } + + #[test] + fn rank() { + assert_eq!(Square::A1.rank(), Rank::First); + assert_eq!(Square::A2.rank(), Rank::Second); + assert_eq!(Square::B1.rank(), Rank::First); + assert_eq!(Square::H8.rank(), Rank::Eighth); + } #[test] fn left_shift() { From 87473908cfc861f04c1faa733934a2dfaad39264 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:23:19 +0200 Subject: [PATCH 014/157] Add 'Square' constructor from 'File', 'Rank' --- src/board/square.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/board/square.rs b/src/board/square.rs index 3cae19e..1b8e932 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -34,6 +34,13 @@ impl Square { Self::H1, Self::H2, Self::H3, Self::H4, Self::H5, Self::H6, Self::H7, Self::H8, ]; + /// Construct a [Square] from a [File] and [Rank]. + #[inline(always)] + pub fn new(file: File, rank: Rank) -> Self { + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(file.index() * 8 + rank.index()) } + } + /// Iterate over all squares in order. pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() @@ -173,6 +180,14 @@ mod test { use crate::board::file::*; use crate::board::rank::*; + #[test] + fn new() { + assert_eq!(Square::new(File::A, Rank::First), Square::A1); + assert_eq!(Square::new(File::A, Rank::Second), Square::A2); + assert_eq!(Square::new(File::B, Rank::First), Square::B1); + assert_eq!(Square::new(File::H, Rank::Eighth), Square::H8); + } + #[test] fn file() { assert_eq!(Square::A1.file(), File::A); From 63228c2d9e820d3827169393cb9610220f033e0a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:34:33 +0200 Subject: [PATCH 015/157] Add 'File::{left,right}' --- src/board/file.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/board/file.rs b/src/board/file.rs index a7ab385..ac3e91e 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -50,6 +50,18 @@ impl File { self as usize } + /// Return the [File] to the left, as seen from white's perspective. Wraps around the board. + pub fn left(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_sub(1) & 7) } + } + + /// Return the [File] to the right, as seen from white's perspective. Wraps around the board. + pub fn right(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_add(1) & 7) } + } + /// Turn a [File] into a [Bitboard] of all squares in that file. #[inline(always)] pub fn into_bitboard(self) -> Bitboard { @@ -76,6 +88,20 @@ mod test { assert_eq!(File::H.index(), 7); } + #[test] + fn left() { + assert_eq!(File::A.left(), File::H); + assert_eq!(File::B.left(), File::A); + assert_eq!(File::H.left(), File::G); + } + + #[test] + fn right() { + assert_eq!(File::A.right(), File::B); + assert_eq!(File::B.right(), File::C); + assert_eq!(File::H.right(), File::A); + } + #[test] fn into_bitboard() { assert_eq!(File::A.into_bitboard(), Bitboard::FILES[0]); From db0a7e9f60b2a43d579dbfeff37b2dc89d941e8a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:34:46 +0200 Subject: [PATCH 016/157] Add 'Rank::{up,down}' --- src/board/rank.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/board/rank.rs b/src/board/rank.rs index fab3cdd..ffc5314 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -50,6 +50,18 @@ impl Rank { self as usize } + /// Return the [Rank] one-row up, as seen from white's perspective. Wraps around the board. + pub fn up(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_add(1) & 7) } + } + + /// Return the [Rank] one-row down, as seen from white's perspective. Wraps around the board. + pub fn down(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_sub(1) & 7) } + } + /// Turn a [Rank] into a [Bitboard] of all squares in that rank. #[inline(always)] pub fn into_bitboard(self) -> Bitboard { @@ -76,6 +88,20 @@ mod test { assert_eq!(Rank::Eighth.index(), 7); } + #[test] + fn up() { + assert_eq!(Rank::First.up(), Rank::Second); + assert_eq!(Rank::Second.up(), Rank::Third); + assert_eq!(Rank::Eighth.up(), Rank::First); + } + + #[test] + fn down() { + assert_eq!(Rank::First.down(), Rank::Eighth); + assert_eq!(Rank::Second.down(), Rank::First); + assert_eq!(Rank::Eighth.down(), Rank::Seventh); + } + #[test] fn into_bitboard() { assert_eq!(Rank::First.into_bitboard(), Bitboard::RANKS[0]); From 61e7a4e8d1cbaaf45e5263627bedc24ac7e16d23 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 20:39:47 +0200 Subject: [PATCH 017/157] Add 'Bitboard::count' --- src/board/bitboard/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 524cf33..d2c8921 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -37,6 +37,12 @@ impl Bitboard { Bitboard(0b00000000_11111111_00000000_00000000_00000000_00000000_00000000_00000000), Bitboard(0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000), ]; + + /// Count the number of pieces in the [Bitboard]. + #[inline(always)] + pub fn count(self) -> u32 { + self.0.count_ones() + } } impl Default for Bitboard { @@ -170,6 +176,13 @@ mod test { use super::*; use crate::board::square::*; + #[test] + fn count() { + assert_eq!(Bitboard::EMPTY.count(), 0); + assert_eq!(Bitboard::FILES[0].count(), 8); + assert_eq!(Bitboard::ALL.count(), 64); + } + #[test] fn iter() { assert_eq!(Bitboard::EMPTY.into_iter().collect::>(), Vec::new()); From 17b8ee5eb30a5df8d844dd8b2d96042afb725b47 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 21:30:23 +0200 Subject: [PATCH 018/157] Add 'Square::index' --- src/board/square.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/board/square.rs b/src/board/square.rs index 1b8e932..7f1c822 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -60,6 +60,12 @@ impl Square { std::mem::transmute(index as u8) } + /// Return the index of a given [Square]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + /// Return the index of the rank of this square (0 -> rank 1, ..., 7 -> rank 8). #[inline(always)] pub fn rank_index(self) -> usize { @@ -188,6 +194,14 @@ mod test { assert_eq!(Square::new(File::H, Rank::Eighth), Square::H8); } + #[test] + fn index() { + assert_eq!(Square::A1.index(), 0); + assert_eq!(Square::A2.index(), 1); + assert_eq!(Square::B1.index(), 8); + assert_eq!(Square::H8.index(), 63); + } + #[test] fn file() { assert_eq!(Square::A1.file(), File::A); From e8b5c9f73c9d9b9fe296dd6510e45c7a1a1104a8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 21:30:23 +0200 Subject: [PATCH 019/157] Use 'Square::index' in 'Square::{file,rank}_index' --- src/board/square.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/board/square.rs b/src/board/square.rs index 7f1c822..437d2e6 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -69,13 +69,13 @@ impl Square { /// Return the index of the rank of this square (0 -> rank 1, ..., 7 -> rank 8). #[inline(always)] pub fn rank_index(self) -> usize { - (self as usize) % 8 + self.index() % 8 } /// Return the index of the rank of this square (0 -> file A, ..., 7 -> file H). #[inline(always)] pub fn file_index(self) -> usize { - (self as usize) / 8 + self.index() / 8 } /// Return a [Rank] representing the rank of this square. From 4e98678ccdff498400380fd5dd2d7e8b73097d11 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:06:43 +0200 Subject: [PATCH 020/157] Add 'board::Direction' enum --- src/board/directions.rs | 22 ++++++++++++++++++++++ src/board/mod.rs | 3 +++ 2 files changed, 25 insertions(+) create mode 100644 src/board/directions.rs diff --git a/src/board/directions.rs b/src/board/directions.rs new file mode 100644 index 0000000..7e10ab3 --- /dev/null +++ b/src/board/directions.rs @@ -0,0 +1,22 @@ +/// A direction on the board. Either along the rook, bishop, or knight directions +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Direction { + North, + West, + South, + East, + + NorthWest, + SouthWest, + SouthEast, + NorthEast, + + NorthNorthWest, + NorthWestWest, + SouthWestWest, + SouthSouthWest, + SouthSouthEast, + SouthEastEast, + NorthEastEast, + NorthNorthEast, +} diff --git a/src/board/mod.rs b/src/board/mod.rs index 7923cab..bc9d1d9 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod directions; +pub use directions::*; + pub mod file; pub use file::*; From 8b27d302d788096a7004069fcc05a96b6f4108d1 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:06:50 +0200 Subject: [PATCH 021/157] Add 'Direction::move_board' Encapsulates the way to move a piece on a board, avoiding the need to mask and shift by hand. --- src/board/directions.rs | 524 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 524 insertions(+) diff --git a/src/board/directions.rs b/src/board/directions.rs index 7e10ab3..3345256 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -1,3 +1,5 @@ +use super::{Bitboard, Rank}; + /// A direction on the board. Either along the rook, bishop, or knight directions #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Direction { @@ -20,3 +22,525 @@ pub enum Direction { NorthEastEast, NorthNorthEast, } + +impl Direction { + /// Move every piece on a board along the given direction. Do not wrap around the board. + #[inline(always)] + pub fn move_board(self, board: Bitboard) -> Bitboard { + // No need to filter for A/H ranks thanks to wrapping + match self { + Self::North => (board - Rank::Eighth.into_bitboard()) << 1, + Self::West => board >> 8, + Self::South => (board - Rank::First.into_bitboard()) >> 1, + Self::East => board << 8, + + Self::NorthWest => (board - Rank::Eighth.into_bitboard()) >> 7, + Self::SouthWest => (board - Rank::First.into_bitboard()) >> 9, + Self::SouthEast => (board - Rank::First.into_bitboard()) << 7, + Self::NorthEast => (board - Rank::Eighth.into_bitboard()) << 9, + + Self::NorthNorthWest => { + (board - Rank::Eighth.into_bitboard() - Rank::Seventh.into_bitboard()) >> 6 + } + Self::NorthWestWest => (board - Rank::Eighth.into_bitboard()) >> 15, + Self::SouthWestWest => (board - Rank::First.into_bitboard()) >> 17, + Self::SouthSouthWest => { + (board - Rank::First.into_bitboard() - Rank::Second.into_bitboard()) >> 10 + } + Self::SouthSouthEast => { + (board - Rank::First.into_bitboard() - Rank::Second.into_bitboard()) << 6 + } + Self::SouthEastEast => (board - Rank::First.into_bitboard()) << 15, + Self::NorthEastEast => (board - Rank::Eighth.into_bitboard()) << 17, + Self::NorthNorthEast => { + (board - Rank::Eighth.into_bitboard() - Rank::Seventh.into_bitboard()) << 10 + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::Square; + + #[test] + fn north() { + assert_eq!( + Direction::North.move_board(Square::A1.into_bitboard()), + Square::A2.into_bitboard() + ); + assert_eq!( + Direction::North.move_board(Square::A2.into_bitboard()), + Square::A3.into_bitboard() + ); + assert_eq!( + Direction::North.move_board(Square::A7.into_bitboard()), + Square::A8.into_bitboard() + ); + assert_eq!( + Direction::North.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn west() { + assert_eq!( + Direction::West.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::West.move_board(Square::B1.into_bitboard()), + Square::A1.into_bitboard() + ); + assert_eq!( + Direction::West.move_board(Square::G1.into_bitboard()), + Square::F1.into_bitboard() + ); + assert_eq!( + Direction::West.move_board(Square::H1.into_bitboard()), + Square::G1.into_bitboard() + ); + } + + #[test] + fn south() { + assert_eq!( + Direction::South.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::South.move_board(Square::A2.into_bitboard()), + Square::A1.into_bitboard() + ); + assert_eq!( + Direction::South.move_board(Square::A7.into_bitboard()), + Square::A6.into_bitboard() + ); + assert_eq!( + Direction::South.move_board(Square::A8.into_bitboard()), + Square::A7.into_bitboard() + ); + } + + #[test] + fn east() { + assert_eq!( + Direction::East.move_board(Square::A1.into_bitboard()), + Square::B1.into_bitboard() + ); + assert_eq!( + Direction::East.move_board(Square::B1.into_bitboard()), + Square::C1.into_bitboard() + ); + assert_eq!( + Direction::East.move_board(Square::G1.into_bitboard()), + Square::H1.into_bitboard() + ); + assert_eq!( + Direction::East.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn north_west() { + assert_eq!( + Direction::NorthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthWest.move_board(Square::B1.into_bitboard()), + Square::A2.into_bitboard() + ); + assert_eq!( + Direction::NorthWest.move_board(Square::H1.into_bitboard()), + Square::G2.into_bitboard() + ); + assert_eq!( + Direction::NorthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthWest.move_board(Square::B8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthWest.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn south_west() { + assert_eq!( + Direction::SouthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::B1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::B8.into_bitboard()), + Square::A7.into_bitboard() + ); + assert_eq!( + Direction::SouthWest.move_board(Square::H8.into_bitboard()), + Square::G7.into_bitboard() + ); + } + + #[test] + fn south_east() { + assert_eq!( + Direction::SouthEast.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthEast.move_board(Square::B1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthEast.move_board(Square::A8.into_bitboard()), + Square::B7.into_bitboard() + ); + assert_eq!( + Direction::SouthEast.move_board(Square::B8.into_bitboard()), + Square::C7.into_bitboard() + ); + assert_eq!( + Direction::SouthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn north_east() { + assert_eq!( + Direction::NorthEast.move_board(Square::A1.into_bitboard()), + Square::B2.into_bitboard() + ); + assert_eq!( + Direction::NorthEast.move_board(Square::B1.into_bitboard()), + Square::C2.into_bitboard() + ); + assert_eq!( + Direction::NorthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthEast.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthEast.move_board(Square::B8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn north_north_west() { + assert_eq!( + Direction::NorthNorthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::B2.into_bitboard()), + Square::A4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::H1.into_bitboard()), + Square::G3.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::G2.into_bitboard()), + Square::F4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } + + #[test] + fn north_west_west() { + assert_eq!( + Direction::NorthWestWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::H1.into_bitboard()), + Square::F2.into_bitboard() + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::G2.into_bitboard()), + Square::E3.into_bitboard() + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::G7.into_bitboard()), + Square::E8.into_bitboard() + ); + } + + #[test] + fn south_west_west() { + assert_eq!( + Direction::SouthWestWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::G2.into_bitboard()), + Square::E1.into_bitboard() + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::H8.into_bitboard()), + Square::F7.into_bitboard() + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::G7.into_bitboard()), + Square::E6.into_bitboard() + ); + } + + #[test] + fn south_south_west() { + assert_eq!( + Direction::SouthSouthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::B7.into_bitboard()), + Square::A5.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::H8.into_bitboard()), + Square::G6.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::G7.into_bitboard()), + Square::F5.into_bitboard() + ); + } + + #[test] + fn south_south_east() { + assert_eq!( + Direction::SouthSouthEast.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::A8.into_bitboard()), + Square::B6.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::B7.into_bitboard()), + Square::C5.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::G7.into_bitboard()), + Square::H5.into_bitboard() + ); + } + + #[test] + fn south_east_east() { + assert_eq!( + Direction::SouthEastEast.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::B2.into_bitboard()), + Square::D1.into_bitboard() + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::A8.into_bitboard()), + Square::C7.into_bitboard() + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::B7.into_bitboard()), + Square::D6.into_bitboard() + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } + + #[test] + fn north_east_east() { + assert_eq!( + Direction::NorthEastEast.move_board(Square::A1.into_bitboard()), + Square::C2.into_bitboard() + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::B2.into_bitboard()), + Square::D3.into_bitboard() + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::B7.into_bitboard()), + Square::D8.into_bitboard() + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } + + #[test] + fn north_north_east() { + assert_eq!( + Direction::NorthNorthEast.move_board(Square::A1.into_bitboard()), + Square::B3.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::B2.into_bitboard()), + Square::C4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::G2.into_bitboard()), + Square::H4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } +} From 52d39740635d9c927d2e9c526f1e09060f2e6995 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:27:33 +0200 Subject: [PATCH 022/157] Add 'Direction::iter_{rook,bishop,royalty,knight}' --- src/board/directions.rs | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/board/directions.rs b/src/board/directions.rs index 3345256..d339bc1 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -24,6 +24,49 @@ pub enum Direction { } impl Direction { + /// Directions that a rook could use. + pub const ROOK_DIRECTIONS: [Self; 4] = [Self::North, Self::West, Self::South, Self::East]; + + /// Directions that a bishop could use. + pub const BISHOP_DIRECTIONS: [Self; 4] = [ + Self::NorthWest, + Self::SouthWest, + Self::SouthEast, + Self::NorthEast, + ]; + + /// Directions that a knight could use. + pub const KNIGHT_DIRECTIONS: [Self; 8] = [ + Self::NorthNorthWest, + Self::NorthWestWest, + Self::SouthWestWest, + Self::SouthSouthWest, + Self::SouthSouthEast, + Self::SouthEastEast, + Self::NorthEastEast, + Self::NorthNorthEast, + ]; + + /// Iterate over all directions a rook can take. + pub fn iter_rook() -> impl Iterator { + Self::ROOK_DIRECTIONS.iter().cloned() + } + + /// Iterate over all directions a bishop can take. + pub fn iter_bishop() -> impl Iterator { + Self::BISHOP_DIRECTIONS.iter().cloned() + } + + /// Iterate over all directions a queen or king can take. + pub fn iter_royalty() -> impl Iterator { + Self::iter_rook().chain(Self::iter_bishop()) + } + + /// Iterate over all directions a knight can take. + pub fn iter_knight() -> impl Iterator { + Self::KNIGHT_DIRECTIONS.iter().cloned() + } + /// Move every piece on a board along the given direction. Do not wrap around the board. #[inline(always)] pub fn move_board(self, board: Bitboard) -> Bitboard { From 8e92bc2370b76207168b1c6ae266f70b88df2af5 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:03:35 +0200 Subject: [PATCH 023/157] Add 'Direction::move_square' --- src/board/directions.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/board/directions.rs b/src/board/directions.rs index d339bc1..1c73a16 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -1,4 +1,4 @@ -use super::{Bitboard, Rank}; +use super::{Bitboard, Rank, Square}; /// A direction on the board. Either along the rook, bishop, or knight directions #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -67,6 +67,12 @@ impl Direction { Self::KNIGHT_DIRECTIONS.iter().cloned() } + /// Move a [Square] along the given [Direction], unless it would wrap at the end of the board + pub fn move_square(self, square: Square) -> Option { + let res = self.move_board(square.into_bitboard()); + res.into_iter().next() + } + /// Move every piece on a board along the given direction. Do not wrap around the board. #[inline(always)] pub fn move_board(self, board: Bitboard) -> Bitboard { @@ -105,7 +111,6 @@ impl Direction { #[cfg(test)] mod test { use super::*; - use crate::board::Square; #[test] fn north() { From 896f615bba2a9340d0894e381d679fee60ffbd1a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:27:08 +0200 Subject: [PATCH 024/157] Add 'Bitboard::{ANTI_,}DIAGONAL' --- src/board/bitboard/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index d2c8921..d2f3723 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -38,6 +38,12 @@ impl Bitboard { Bitboard(0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000), ]; + /// The diagonal from [Square::A1] to [Square::H8]. + pub const DIAGONAL: Bitboard = Bitboard(0x8040201008040201); + + /// The diagonal from [Square::A8] to [Square::H1]. + pub const ANTI_DIAGONAL: Bitboard = Bitboard(0x0102040810204080); + /// Count the number of pieces in the [Bitboard]. #[inline(always)] pub fn count(self) -> u32 { From 0dde0d5dbd6d0aa5d9c15a387046b35ffb07820b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:27:46 +0200 Subject: [PATCH 025/157] Add 'Direction::slide_{square,board}' --- src/board/directions.rs | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/board/directions.rs b/src/board/directions.rs index 1c73a16..26b75de 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -106,11 +106,39 @@ impl Direction { } } } + + /// Slide a board along the given [Direction], i.e: return all successive applications of + /// [Direction::move_square] until no new squares can be 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_square(self, square: Square) -> Bitboard { + self.slide_board(square.into_bitboard()) + } + + /// Slide a board along the given [Direction], i.e: return all successive applications of + /// [Direction::move_board] until no new squares can be 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(self, mut board: Bitboard) -> Bitboard { + debug_assert!(!Self::KNIGHT_DIRECTIONS.contains(&self)); + + let mut res = Default::default(); + + while board != Bitboard::EMPTY { + board = self.move_board(board); + res = res | board; + } + + res + } } #[cfg(test)] mod test { use super::*; + use crate::board::{File, Rank}; #[test] fn north() { @@ -591,4 +619,40 @@ mod test { Bitboard::EMPTY, ); } + + #[test] + fn slide() { + assert_eq!( + Direction::North.slide_square(Square::A1), + File::A.into_bitboard() - Square::A1 + ); + assert_eq!( + Direction::West.slide_square(Square::H1), + Rank::First.into_bitboard() - Square::H1 + ); + assert_eq!( + Direction::South.slide_square(Square::A8), + File::A.into_bitboard() - Square::A8 + ); + assert_eq!( + Direction::East.slide_square(Square::A1), + Rank::First.into_bitboard() - Square::A1 + ); + assert_eq!( + Direction::NorthWest.slide_square(Square::H1), + Bitboard::ANTI_DIAGONAL - Square::H1 + ); + assert_eq!( + Direction::SouthWest.slide_square(Square::H8), + Bitboard::DIAGONAL - Square::H8 + ); + assert_eq!( + Direction::SouthEast.slide_square(Square::A8), + Bitboard::ANTI_DIAGONAL - Square::A8 + ); + assert_eq!( + Direction::NorthEast.slide_square(Square::A1), + Bitboard::DIAGONAL - Square::A1 + ); + } } From 057b383f8c1d44dc4ecbc19f3f6625af3eb97c42 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:31:33 +0200 Subject: [PATCH 026/157] Add 'Bitboard::is_empty' --- src/board/bitboard/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index d2f3723..04b205a 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -49,6 +49,12 @@ impl Bitboard { pub fn count(self) -> u32 { self.0.count_ones() } + + /// Return true if there are no pieces in the [Bitboard], otherwise false. + #[inline(always)] + pub fn is_empty(self) -> bool { + self == Self::EMPTY + } } impl Default for Bitboard { From f1f6198e5f7487f8f171ba8d74a9e504cfcb22aa Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:32:40 +0200 Subject: [PATCH 027/157] Make use of 'Bitboard::is_empty' --- src/board/directions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/directions.rs b/src/board/directions.rs index 26b75de..135f5f4 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -126,7 +126,7 @@ impl Direction { let mut res = Default::default(); - while board != Bitboard::EMPTY { + while !board.is_empty() { board = self.move_board(board); res = res | board; } From 74d2a2cf6aef3aea71efb979f99d2b612ec3b826 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:43:48 +0200 Subject: [PATCH 028/157] Add 'Bitboard::{LIGHT,DARK}_SQUARES --- src/board/bitboard/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 04b205a..092dba7 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -44,6 +44,12 @@ impl Bitboard { /// The diagonal from [Square::A8] to [Square::H1]. pub const ANTI_DIAGONAL: Bitboard = Bitboard(0x0102040810204080); + /// The light [Square]s on a board, e.g: [Square::H1]. + pub const LIGHT_SQUARES: Bitboard = Bitboard(0x55AA55AA55AA55AA); + + /// The dark [Square]s on a board, e.g: [Square::A1]. + pub const DARK_SQUARES: Bitboard = Bitboard(0x55AA55AA55AA55AA); + /// Count the number of pieces in the [Bitboard]. #[inline(always)] pub fn count(self) -> u32 { From d132e3779e4ada952fc26f9d02ad5c4f60867e36 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 10:04:47 +0200 Subject: [PATCH 029/157] Enable 'doCheck' in nix package --- flake.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flake.nix b/flake.nix index 5767191..6526384 100644 --- a/flake.nix +++ b/flake.nix @@ -144,6 +144,8 @@ seer = naersk-lib.buildPackage { src = self; + doCheck = true; + passthru = { inherit my-rust; }; From ca4603ff028b8768f8eef28ec0fab48d29aaa976 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 11:04:52 +0200 Subject: [PATCH 030/157] Add 'static_assert' macro --- src/lib.rs | 1 + src/utils/mod.rs | 2 ++ src/utils/static_assert.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 src/utils/mod.rs create mode 100644 src/utils/static_assert.rs diff --git a/src/lib.rs b/src/lib.rs index 667c357..3593172 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ pub mod board; +pub mod utils; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..2833a48 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod static_assert; +pub use static_assert::*; diff --git a/src/utils/static_assert.rs b/src/utils/static_assert.rs new file mode 100644 index 0000000..81b7a86 --- /dev/null +++ b/src/utils/static_assert.rs @@ -0,0 +1,28 @@ +//! Static assert. + +/// Assert a condition at compile-time. +/// +/// See [RFC 2790] for potential addition into Rust itself. +/// +/// [RFC 2790]: https://github.com/rust-lang/rfcs/issues/2790 +/// +/// # Examples +/// +/// ``` +/// use seer::utils::static_assert; +/// +/// static_assert!(42 > 0); +/// ``` +#[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 + #[allow(dead_code)] + const _: () = [()][!($condition) as usize]; + }; +} + +// I want it namespaced, even though it is exported to the root of the crate by `#[macro_export]`. +pub use static_assert; From 66e5109157c9189722031c93dd0c62791f6f0b91 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 11:05:38 +0200 Subject: [PATCH 031/157] Statically assert zero-cost invariants Since some or all of those invariants will come in handy to ensure we use as little memory as possible, to maximize the speed of the move generation later on. --- src/board/bitboard/mod.rs | 5 +++++ src/board/file.rs | 4 ++++ src/board/rank.rs | 4 ++++ src/board/square.rs | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 092dba7..8b716be 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,4 +1,6 @@ use super::Square; +use crate::utils::static_assert; + mod iterator; use iterator::*; @@ -63,6 +65,9 @@ impl Bitboard { } } +// Ensure zero-cost (at least size-wise) wrapping. +static_assert!(std::mem::size_of::() == std::mem::size_of::()); + impl Default for Bitboard { fn default() -> Self { Self::EMPTY diff --git a/src/board/file.rs b/src/board/file.rs index ac3e91e..1a64929 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -1,4 +1,5 @@ use super::Bitboard; +use crate::utils::static_assert; /// An enum representing a singular file on a chess board (i.e: the columns). #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -70,6 +71,9 @@ impl File { } } +// Ensure that niche-optimization is in effect. +static_assert!(std::mem::size_of::>() == std::mem::size_of::()); + #[cfg(test)] mod test { use super::*; diff --git a/src/board/rank.rs b/src/board/rank.rs index ffc5314..a3c783a 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -1,4 +1,5 @@ use super::Bitboard; +use crate::utils::static_assert; /// An enum representing a singular rank on a chess board (i.e: the rows). #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -70,6 +71,9 @@ impl Rank { } } +// Ensure that niche-optimization is in effect. +static_assert!(std::mem::size_of::>() == std::mem::size_of::()); + #[cfg(test)] mod test { use super::*; diff --git a/src/board/square.rs b/src/board/square.rs index 437d2e6..e8588eb 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -1,4 +1,5 @@ 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. @@ -179,6 +180,9 @@ impl std::ops::Sub for Square { } } +// Ensure that niche-optimization is in effect. +static_assert!(std::mem::size_of::>() == std::mem::size_of::()); + #[cfg(test)] mod test { use super::*; From fcf309fad4308c1c6541b2016c5c2e021b73ab0e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 11:50:08 +0200 Subject: [PATCH 032/157] Fix 'cargo clippy' launched by hand Mis-matching 'rustc' versions lead to errors. --- flake.nix | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 6526384..22a05e4 100644 --- a/flake.nix +++ b/flake.nix @@ -127,9 +127,8 @@ nativeBuildInputs = with pkgs; [ rust-analyzer - # Not included in the pre-commit hook unfortunately... - clippy - rustfmt + # Clippy, rustfmt, etc... + my-rust ]; inherit (checks.pre-commit) shellHook; From 3a6c4113fcff3b17de1438c0a4ce9689c1af40bf Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:11:37 +0200 Subject: [PATCH 033/157] Rename 'board::direction{s,}' --- src/board/{directions.rs => direction.rs} | 0 src/board/mod.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/board/{directions.rs => direction.rs} (100%) diff --git a/src/board/directions.rs b/src/board/direction.rs similarity index 100% rename from src/board/directions.rs rename to src/board/direction.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index bc9d1d9..ea3c69e 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,8 +1,8 @@ pub mod bitboard; pub use bitboard::*; -pub mod directions; -pub use directions::*; +pub mod direction; +pub use direction::*; pub mod file; pub use file::*; From e0c667d090256dcc26634e8d5fa3f008a420e9bb Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:22:04 +0200 Subject: [PATCH 034/157] Add 'Color' enum --- src/board/color.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 105 insertions(+) create mode 100644 src/board/color.rs diff --git a/src/board/color.rs b/src/board/color.rs new file mode 100644 index 0000000..a7af8b6 --- /dev/null +++ b/src/board/color.rs @@ -0,0 +1,102 @@ +use super::Rank; + +/// An enum representing the color of a player. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Color { + White, + Black, +} + +impl Color { + /// Convert from a file index into a [Color] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < 2); + // 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. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [Color]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + + /// Return the first [Rank] for pieces of the given [Color], where its pieces start. + #[inline(always)] + pub fn first_rank(self) -> Rank { + match self { + Self::White => Rank::First, + Self::Black => Rank::Eighth, + } + } + + /// Return the second [Rank] for pieces of the given [Color], where its pawns start. + #[inline(always)] + pub fn second_rank(self) -> Rank { + match self { + Self::White => Rank::Second, + Self::Black => Rank::Seventh, + } + } + + /// Return the fourth [Rank] for pieces of the given [Color], where its pawns move to after a + /// two-square move. + #[inline(always)] + pub fn fourth_rank(self) -> Rank { + match self { + Self::White => Rank::Fourth, + Self::Black => Rank::Fifth, + } + } + + /// Return the seventh [Rank] for pieces of the given [Color], which is the rank before a pawn + /// gets promoted. + #[inline(always)] + pub fn seventh_rank(self) -> Rank { + match self { + Self::White => Rank::Seventh, + Self::Black => Rank::Second, + } + } +} + +impl std::ops::Not for Color { + type Output = Color; + + fn not(self) -> Self::Output { + match self { + Self::White => Self::Black, + Self::Black => Self::White, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(Color::from_index(0), Color::White); + assert_eq!(Color::from_index(1), Color::Black); + } + + #[test] + fn index() { + assert_eq!(Color::White.index(), 0); + assert_eq!(Color::Black.index(), 1); + } + + #[test] + fn not() { + assert_eq!(!Color::White, Color::Black); + assert_eq!(!Color::Black, Color::White); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index ea3c69e..6a29cbe 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod color; +pub use color::*; + pub mod direction; pub use direction::*; From 63c5d2dc3465411dfdd74c5c95efe4b095a7a52f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:27:03 +0200 Subject: [PATCH 035/157] Add 'CastleRights' enum --- src/board/castle_rights.rs | 55 ++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 +++ 2 files changed, 58 insertions(+) create mode 100644 src/board/castle_rights.rs diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs new file mode 100644 index 0000000..9ea7ad6 --- /dev/null +++ b/src/board/castle_rights.rs @@ -0,0 +1,55 @@ +/// Current castle rights for a player. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CastleRights { + /// No castling allowed. + NoSide, + /// King-side castling only. + KingSide, + /// Queen-side castling only. + QueenSide, + /// Either side allowed. + BothSides, +} + +impl CastleRights { + /// Convert from a castle rights index into a [CastleRights] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < 4); + // 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. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [CastleRights]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(CastleRights::from_index(0), CastleRights::NoSide); + assert_eq!(CastleRights::from_index(1), CastleRights::KingSide); + assert_eq!(CastleRights::from_index(2), CastleRights::QueenSide); + assert_eq!(CastleRights::from_index(3), CastleRights::BothSides); + } + + #[test] + fn index() { + assert_eq!(CastleRights::NoSide.index(), 0); + assert_eq!(CastleRights::KingSide.index(), 1); + assert_eq!(CastleRights::QueenSide.index(), 2); + assert_eq!(CastleRights::BothSides.index(), 3); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index 6a29cbe..ad91192 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod castle_rights; +pub use castle_rights::*; + pub mod color; pub use color::*; From 562182d26b63bcd7e521e91558f568336b3ba875 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:27:37 +0200 Subject: [PATCH 036/157] Add 'CastleRights::has_{king,queen}_side' --- src/board/castle_rights.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 9ea7ad6..10a602e 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -31,6 +31,18 @@ impl CastleRights { pub fn index(self) -> usize { self as usize } + + /// Can the player castle king-side. + #[inline(always)] + pub fn has_king_side(self) -> bool { + (self.index() & 1) != 0 + } + + /// Can the player castle king-side. + #[inline(always)] + pub fn has_queen_side(self) -> bool { + (self.index() & 2) != 0 + } } #[cfg(test)] @@ -52,4 +64,20 @@ mod test { assert_eq!(CastleRights::QueenSide.index(), 2); assert_eq!(CastleRights::BothSides.index(), 3); } + + #[test] + fn has_kingside() { + assert!(!CastleRights::NoSide.has_king_side()); + assert!(!CastleRights::QueenSide.has_king_side()); + assert!(CastleRights::KingSide.has_king_side()); + assert!(CastleRights::BothSides.has_king_side()); + } + + #[test] + fn has_queenside() { + assert!(!CastleRights::NoSide.has_queen_side()); + assert!(!CastleRights::KingSide.has_queen_side()); + assert!(CastleRights::QueenSide.has_queen_side()); + assert!(CastleRights::BothSides.has_queen_side()); + } } From c5949fb01e4a68b5e27a96ee48ca8f850abfa524 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:35:31 +0200 Subject: [PATCH 037/157] Add 'CastleRights::without_{king,queen}_side' --- src/board/castle_rights.rs | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 10a602e..9663bc0 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -43,6 +43,25 @@ impl CastleRights { pub fn has_queen_side(self) -> bool { (self.index() & 2) != 0 } + + /// Remove king-side castling rights. + #[inline(always)] + pub fn without_king_side(self) -> Self { + self.remove(Self::KingSide) + } + + /// Remove queen-side castling rights. + #[inline(always)] + pub fn without_queen_side(self) -> Self { + self.remove(Self::QueenSide) + } + + /// Remove some [CastleRights], and return the resulting [CastleRights]. + #[inline(always)] + pub fn remove(self, to_remove: CastleRights) -> Self { + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(self.index() & !to_remove.index()) } + } } #[cfg(test)] @@ -80,4 +99,44 @@ mod test { assert!(CastleRights::QueenSide.has_queen_side()); assert!(CastleRights::BothSides.has_queen_side()); } + + #[test] + fn without_king_side() { + assert_eq!( + CastleRights::NoSide.without_king_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::KingSide.without_king_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::QueenSide.without_king_side(), + CastleRights::QueenSide + ); + assert_eq!( + CastleRights::BothSides.without_king_side(), + CastleRights::QueenSide + ); + } + + #[test] + fn without_queen_side() { + assert_eq!( + CastleRights::NoSide.without_queen_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::QueenSide.without_queen_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::KingSide.without_queen_side(), + CastleRights::KingSide + ); + assert_eq!( + CastleRights::BothSides.without_queen_side(), + CastleRights::KingSide + ); + } } From aa3b464bb8e10f6ca67841e5fdc2c4a989c4f4bd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:50:23 +0200 Subject: [PATCH 038/157] Add 'CastleRights::unmoved_rooks' --- src/board/castle_rights.rs | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 9663bc0..0e44e03 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -1,3 +1,5 @@ +use super::{Bitboard, Color, File, Square}; + /// Current castle rights for a player. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum CastleRights { @@ -62,6 +64,22 @@ impl CastleRights { // SAFETY: we know the value is in-bounds unsafe { Self::from_index_unchecked(self.index() & !to_remove.index()) } } + + /// Which rooks have not been moved for a given [CastleRights] and [Color]. + #[inline(always)] + pub fn unmoved_rooks(self, color: Color) -> Bitboard { + let rank = color.first_rank(); + + let king_side_square = Square::new(File::H, rank); + let queen_side_square = Square::new(File::A, rank); + + match self { + Self::NoSide => Bitboard::EMPTY, + Self::KingSide => king_side_square.into_bitboard(), + Self::QueenSide => queen_side_square.into_bitboard(), + Self::BothSides => king_side_square | queen_side_square, + } + } } #[cfg(test)] @@ -139,4 +157,40 @@ mod test { CastleRights::KingSide ); } + + #[test] + fn unmoved_rooks() { + assert_eq!( + CastleRights::NoSide.unmoved_rooks(Color::White), + Bitboard::EMPTY + ); + assert_eq!( + CastleRights::NoSide.unmoved_rooks(Color::Black), + Bitboard::EMPTY + ); + assert_eq!( + CastleRights::KingSide.unmoved_rooks(Color::White), + Square::H1.into_bitboard() + ); + assert_eq!( + CastleRights::KingSide.unmoved_rooks(Color::Black), + Square::H8.into_bitboard() + ); + assert_eq!( + CastleRights::QueenSide.unmoved_rooks(Color::White), + Square::A1.into_bitboard() + ); + assert_eq!( + CastleRights::QueenSide.unmoved_rooks(Color::Black), + Square::A8.into_bitboard() + ); + assert_eq!( + CastleRights::BothSides.unmoved_rooks(Color::White), + Square::A1 | Square::H1 + ); + assert_eq!( + CastleRights::BothSides.unmoved_rooks(Color::Black), + Square::A8 | Square::H8 + ); + } } From 7c8dce8f49036342f2f4563bbcb6fd13a0ba5547 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 17:27:09 +0200 Subject: [PATCH 039/157] Add 'Color::{forward,backward}_direction' --- src/board/color.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/board/color.rs b/src/board/color.rs index a7af8b6..d71df67 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -1,4 +1,4 @@ -use super::Rank; +use super::{Direction, Rank}; /// An enum representing the color of a player. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -65,6 +65,21 @@ impl Color { Self::Black => Rank::Second, } } + + /// Which way do pawns advance for this color. + #[inline(always)] + pub fn forward_direction(self) -> Direction { + match self { + Self::White => Direction::North, + Self::Black => Direction::South, + } + } + + /// Which way do the opponent's pawns advance for this color. + #[inline(always)] + pub fn backward_direction(self) -> Direction { + (!self).forward_direction() + } } impl std::ops::Not for Color { From b3222276abea45deaf5be7bcbd875f32a7de06a6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 18:21:14 +0200 Subject: [PATCH 040/157] Improve 'board::BitboardIterator' * Accurate 'size_hint'. * Exact size. * Fused iterator (keeps returning 'None' after returning 'None' once). --- src/board/bitboard/iterator.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs index 06db283..fcd644c 100644 --- a/src/board/bitboard/iterator.rs +++ b/src/board/bitboard/iterator.rs @@ -14,4 +14,14 @@ impl Iterator for BitboardIterator { Some(crate::board::Square::from_index(lsb)) } } + + fn size_hint(&self) -> (usize, Option) { + let size = self.0.count_ones() as usize; + + (size, Some(size)) + } } + +impl ExactSizeIterator for BitboardIterator {} + +impl std::iter::FusedIterator for BitboardIterator {} From 65a0939d5305d3fdd62895e6fcc1ac9b0e39efe3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Mar 2024 20:30:43 +0000 Subject: [PATCH 041/157] Bootstrap project --- .envrc | 5 ++ .gitignore | 6 ++ .woodpecker/check.yml | 31 +++++++++ Cargo.lock | 7 +++ Cargo.toml | 8 +++ flake.lock | 143 ++++++++++++++++++++++++++++++++++++++++++ flake.nix | 109 ++++++++++++++++++++++++++++++++ src/main.rs | 3 + 8 files changed, 312 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 .woodpecker/check.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/main.rs 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 new file mode 100644 index 0000000..5f360ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# 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/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1e43342 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "seer" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f191c81 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "seer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..18cea66 --- /dev/null +++ b/flake.lock @@ -0,0 +1,143 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "futils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "ref": "main", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1711523803, + "narHash": "sha256-UKcYiHWHQynzj6CN/vTcix4yd1eCu1uFdsuarupdCQQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2726f127c15a4cc9810843b96cad73c7eb39e443", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1710695816, + "narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "614b4613980a522ba49f0d194531beddbb7220d3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": [ + "futils" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1711519547, + "narHash": "sha256-Q7YmSCUJmDl71fJv/zD9lrOCJ1/SE/okZ2DsrmRjzhY=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "7d47a32e5cd1ea481fab33c516356ce27c8cef4a", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "master", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "futils": "futils", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..134bcdc --- /dev/null +++ b/flake.nix @@ -0,0 +1,109 @@ +{ + description = "A chess engine"; + + inputs = { + futils = { + type = "github"; + owner = "numtide"; + repo = "flake-utils"; + ref = "main"; + }; + + nixpkgs = { + type = "github"; + owner = "NixOS"; + repo = "nixpkgs"; + ref = "nixos-unstable"; + }; + + pre-commit-hooks = { + type = "github"; + owner = "cachix"; + repo = "pre-commit-hooks.nix"; + ref = "master"; + inputs = { + flake-utils.follows = "futils"; + nixpkgs.follows = "nixpkgs"; + }; + }; + }; + + outputs = { self, futils, nixpkgs, pre-commit-hooks }: + { + overlays = { + default = final: _prev: { + seer = with final; rustPlatform.buildRustPackage { + pname = "seer"; + version = (final.lib.importTOML ./Cargo.toml).package.version; + + src = self; + + cargoLock = { + lockFile = "${self}/Cargo.lock"; + }; + + meta = with lib; { + description = "A chess engine"; + homepage = "https://git.belanyi.fr/ambroisie/seer"; + license = licenses.mit; + maintainers = with maintainers; [ ambroisie ]; + }; + }; + }; + }; + } // futils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ + self.overlays.default + ]; + }; + + pre-commit = pre-commit-hooks.lib.${system}.run { + src = self; + + hooks = { + clippy = { + enable = true; + }; + + 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/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From 0d94438b467d05fdd439f3ee897325c1e18fd2d8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Mar 2024 20:31:45 +0000 Subject: [PATCH 042/157] Move binary crate into 'bin' folder --- src/{main.rs => bin/seer.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{main.rs => bin/seer.rs} (100%) diff --git a/src/main.rs b/src/bin/seer.rs similarity index 100% rename from src/main.rs rename to src/bin/seer.rs From 98efd390a6b47a48fccbc349cb3a3609b3bdf701 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:33:07 +0200 Subject: [PATCH 043/157] Add 'Bitboard' and 'Square' definitions --- src/board/bitboard.rs | 201 +++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 5 + src/board/square.rs | 214 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 4 files changed, 421 insertions(+) create mode 100644 src/board/bitboard.rs create mode 100644 src/board/mod.rs create mode 100644 src/board/square.rs create mode 100644 src/lib.rs diff --git a/src/board/bitboard.rs b/src/board/bitboard.rs new file mode 100644 index 0000000..50298d9 --- /dev/null +++ b/src/board/bitboard.rs @@ -0,0 +1,201 @@ +use super::Square; + +/// 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. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Bitboard(pub(crate) u64); + +impl Bitboard { + /// An empty bitboard. + pub const EMPTY: Bitboard = Bitboard(0); + + /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. + pub const RANKS: [Self; 8] = [ + Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), + Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010), + Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100), + Bitboard(0b00001000_00001000_00001000_00001000_00001000_00001000_00001000_00001000), + Bitboard(0b00010000_00010000_00010000_00010000_00010000_00010000_00010000_00010000), + Bitboard(0b00100000_00100000_00100000_00100000_00100000_00100000_00100000_00100000), + Bitboard(0b01000000_01000000_01000000_01000000_01000000_01000000_01000000_01000000), + Bitboard(0b10000000_10000000_10000000_10000000_10000000_10000000_10000000_10000000), + ]; + + /// Array of bitboards representing the eight files, in order from file A to file H. + pub const FILES: [Self; 8] = [ + Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111), + Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000), + Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000), + Bitboard(0b00000000_00000000_00000000_00000000_11111111_00000000_00000000_00000000), + Bitboard(0b00000000_00000000_00000000_11111111_00000000_00000000_00000000_00000000), + Bitboard(0b00000000_00000000_11111111_00000000_00000000_00000000_00000000_00000000), + Bitboard(0b00000000_11111111_00000000_00000000_00000000_00000000_00000000_00000000), + Bitboard(0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000), + ]; +} + +impl Default for Bitboard { + fn default() -> Self { + Self::EMPTY + } +} + +/// Treat bitboard as a set of squares, shift each square's index left by the amount given. +impl std::ops::Shl for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn shl(self, rhs: usize) -> Self::Output { + Bitboard(self.0 << rhs) + } +} + +/// Treat bitboard as a set of squares, shift each square's index right by the amount given. +impl std::ops::Shr for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn shr(self, rhs: usize) -> Self::Output { + Bitboard(self.0 >> rhs) + } +} + +/// Treat bitboard as a set of squares, and invert the set. +impl std::ops::Not for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn not(self) -> Self::Output { + Bitboard(!self.0) + } +} + +/// Treat each bitboard as a set of squares, keep squares that are in either sets. +impl std::ops::BitOr for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 | rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::BitOr for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Square) -> Self::Output { + self | rhs.into_bitboard() + } +} + +/// Treat each bitboard as a set of squares, keep squares that are in both sets. +impl std::ops::BitAnd for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitand(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 & rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::BitAnd for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitand(self, rhs: Square) -> Self::Output { + self & rhs.into_bitboard() + } +} + +/// 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; + + #[inline(always)] + fn bitxor(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 ^ rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::BitXor for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitxor(self, rhs: Square) -> Self::Output { + self ^ rhs.into_bitboard() + } +} + +/// Treat each bitboard as a set of squares, and substract one set from another. +impl std::ops::Sub for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn sub(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 & !rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::Sub for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn sub(self, rhs: Square) -> Self::Output { + self - rhs.into_bitboard() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::square::*; + + #[test] + fn left_shift() { + assert_eq!(Bitboard::RANKS[0] << 1, Bitboard::RANKS[1]); + assert_eq!(Bitboard::FILES[0] << 8, Bitboard::FILES[1]); + } + + #[test] + fn right_shift() { + assert_eq!(Bitboard::RANKS[1] >> 1, Bitboard::RANKS[0]); + assert_eq!(Bitboard::FILES[1] >> 8, Bitboard::FILES[0]); + } + + #[test] + fn not() { + assert_eq!(!Bitboard::EMPTY, Bitboard(u64::MAX)); + } + + #[test] + fn or() { + assert_eq!(Bitboard::FILES[0] | Bitboard::FILES[1], Bitboard(0xff_ff)); + assert_eq!(Bitboard::FILES[0] | Square::B1, Bitboard(0x1_ff)); + } + + #[test] + fn and() { + assert_eq!(Bitboard::FILES[0] & Bitboard::FILES[1], Bitboard::EMPTY); + assert_eq!( + Bitboard::FILES[0] & Bitboard::RANKS[0], + Square::A1.into_bitboard() + ); + assert_eq!(Bitboard::FILES[0] & Square::A1, Square::A1.into_bitboard()); + } + + #[test] + fn xor() { + assert_eq!(Bitboard::FILES[0] ^ Square::A1, Bitboard(0xff - 1)); + } + + #[test] + fn sub() { + assert_eq!(Bitboard::FILES[0] - Bitboard::RANKS[0], Bitboard(0xff - 1)); + assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1)); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs new file mode 100644 index 0000000..06a7d91 --- /dev/null +++ b/src/board/mod.rs @@ -0,0 +1,5 @@ +pub mod bitboard; +pub use bitboard::*; + +pub mod square; +pub use square::*; diff --git a/src/board/square.rs b/src/board/square.rs new file mode 100644 index 0000000..640214c --- /dev/null +++ b/src/board/square.rs @@ -0,0 +1,214 @@ +use super::Bitboard; + +/// Represent a square on a chessboard. Defined in the same order as the +/// [Bitboard](crate::board::Bitboard) squares. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[rustfmt::skip] +pub enum Square { + A1, A2, A3, A4, A5, A6, A7, A8, + B1, B2, B3, B4, B5, B6, B7, B8, + C1, C2, C3, C4, C5, C6, C7, C8, + D1, D2, D3, D4, D5, D6, D7, D8, + E1, E2, E3, E4, E5, E6, E7, E8, + F1, F2, F3, F4, F5, F6, F7, F8, + G1, G2, G3, G4, G5, G6, G7, G8, + H1, H2, H3, H4, H5, H6, H7, H8, +} + +impl std::fmt::Display for Square { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl Square { + #[rustfmt::skip] + const ALL: [Self; 64] = [ + 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, + Self::D1, Self::D2, Self::D3, Self::D4, Self::D5, Self::D6, Self::D7, Self::D8, + Self::E1, Self::E2, Self::E3, Self::E4, Self::E5, Self::E6, Self::E7, Self::E8, + Self::F1, Self::F2, Self::F3, Self::F4, Self::F5, Self::F6, Self::F7, Self::F8, + Self::G1, Self::G2, Self::G3, Self::G4, Self::G5, Self::G6, Self::G7, Self::G8, + Self::H1, Self::H2, Self::H3, Self::H4, Self::H5, Self::H6, Self::H7, Self::H8, + ]; + + /// Iterate over all squares in order. + 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); + // 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) + } + + /// Return the index of the rank of this square (0 -> rank 1, ..., 7 -> rank 8). + #[inline(always)] + pub fn rank_index(self) -> usize { + (self as usize) % 8 + } + + /// Return the index of the rank of this square (0 -> file A, ..., 7 -> file H). + #[inline(always)] + pub fn file_index(self) -> usize { + (self as usize) / 8 + } + + /// Return a bitboard representing the rank of this square. + #[inline(always)] + pub fn rank(self) -> Bitboard { + Bitboard::RANKS[self.rank_index()] + } + + /// Return a bitboard representing the rank of this square. + #[inline(always)] + pub fn file(self) -> Bitboard { + Bitboard::FILES[self.file_index()] + } + + /// Turn a square into a singleton bitboard. + #[inline(always)] + pub fn into_bitboard(self) -> Bitboard { + Bitboard(1 << (self as usize)) + } +} + +/// Shift the square's index left by the amount given. +impl std::ops::Shl for Square { + type Output = Square; + + #[inline(always)] + fn shl(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] + Square::from_index(self as usize + rhs) + } +} + +/// Shift the square's index right by the amount given. +impl std::ops::Shr for Square { + type Output = Square; + + #[inline(always)] + fn shr(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] + Square::from_index(self as usize - rhs) + } +} + +/// Return a board containing all squares but the one given. +impl std::ops::Not for Square { + type Output = Bitboard; + + #[inline(always)] + fn not(self) -> Self::Output { + !self.into_bitboard() + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitOr for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Square) -> Self::Output { + self.into_bitboard() | rhs.into_bitboard() + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitOr for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() | rhs + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitAnd for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitand(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() & rhs + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitXor for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitxor(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() ^ rhs + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::Sub for Square { + type Output = Bitboard; + + #[inline(always)] + fn sub(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() - rhs + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::bitboard::*; + + #[test] + fn left_shift() { + assert_eq!(Square::A1 << 1, Square::A2); + assert_eq!(Square::A1 << 8, Square::B1); + } + + #[test] + fn right_shift() { + assert_eq!(Square::A2 >> 1, Square::A1); + assert_eq!(Square::B1 >> 8, Square::A1); + } + + #[test] + fn not() { + assert_eq!(!Square::A1, Bitboard(u64::MAX - 1)); + } + + #[test] + fn or() { + assert_eq!(Square::A1 | Square::A2, Bitboard(0b11)); + } + + #[test] + fn and() { + assert_eq!(Square::A1 & Bitboard::FILES[0], Square::A1.into_bitboard()); + } + + #[test] + fn xor() { + assert_eq!(Square::A1 ^ Bitboard::FILES[0], Bitboard(0xff - 1)); + } + + #[test] + fn sub() { + assert_eq!(Square::A1 - Bitboard::FILES[0], Bitboard::EMPTY); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..667c357 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod board; From afec72d646521d6266c30e6eb80d07ffe5ca60e2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:52:09 +0200 Subject: [PATCH 044/157] Move 'board::bitboard' into folder module I will be adding a 'BitboardIterator' type, and it makes more sense to use a folder for this module at this point. --- src/board/{bitboard.rs => bitboard/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/board/{bitboard.rs => bitboard/mod.rs} (100%) diff --git a/src/board/bitboard.rs b/src/board/bitboard/mod.rs similarity index 100% rename from src/board/bitboard.rs rename to src/board/bitboard/mod.rs From 1235e4a51c1dc2f4d23866284bb1c21d39431f92 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:53:08 +0200 Subject: [PATCH 045/157] Add bitboard iteration Introduce 'BitboardIterator', use it to implement 'IntoIterator' for 'Bitboard'. --- src/board/bitboard/iterator.rs | 17 ++++++++++++++ src/board/bitboard/mod.rs | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/board/bitboard/iterator.rs diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs new file mode 100644 index 0000000..06db283 --- /dev/null +++ b/src/board/bitboard/iterator.rs @@ -0,0 +1,17 @@ +/// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a +/// [Bitboard](crate::board::Bitboard). +pub struct BitboardIterator(pub(crate) u64); + +impl Iterator for BitboardIterator { + type Item = crate::board::Square; + + fn next(&mut self) -> Option { + if self.0 == 0 { + None + } else { + let lsb = self.0.trailing_zeros() as usize; + self.0 ^= 1 << lsb; + Some(crate::board::Square::from_index(lsb)) + } + } +} diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 50298d9..edb1015 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,4 +1,6 @@ use super::Square; +mod iterator; +use iterator::*; /// 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. @@ -40,6 +42,16 @@ impl Default for Bitboard { } } +/// Iterate over the [Square](crate::board::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) + } +} + /// Treat bitboard as a set of squares, shift each square's index left by the amount given. impl std::ops::Shl for Bitboard { type Output = Bitboard; @@ -155,6 +167,37 @@ mod test { use super::*; use crate::board::square::*; + #[test] + fn iter() { + assert_eq!(Bitboard::EMPTY.into_iter().collect::>(), Vec::new()); + assert_eq!( + Bitboard::RANKS[0].into_iter().collect::>(), + vec![ + Square::A1, + Square::B1, + Square::C1, + Square::D1, + Square::E1, + Square::F1, + Square::G1, + Square::H1, + ] + ); + assert_eq!( + Bitboard::FILES[0].into_iter().collect::>(), + vec![ + Square::A1, + Square::A2, + Square::A3, + Square::A4, + Square::A5, + Square::A6, + Square::A7, + Square::A8, + ] + ); + } + #[test] fn left_shift() { assert_eq!(Bitboard::RANKS[0] << 1, Bitboard::RANKS[1]); From d8f4057a8ccadbb2919d0927dee35d2d464e7996 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:58:46 +0200 Subject: [PATCH 046/157] Introduce 'Bitboard::ALL' --- src/board/bitboard/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index edb1015..524cf33 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -11,6 +11,9 @@ impl Bitboard { /// An empty bitboard. pub const EMPTY: Bitboard = Bitboard(0); + /// A full bitboard. + pub const ALL: Bitboard = Bitboard(u64::MAX); + /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. pub const RANKS: [Self; 8] = [ Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), @@ -212,7 +215,7 @@ mod test { #[test] fn not() { - assert_eq!(!Bitboard::EMPTY, Bitboard(u64::MAX)); + assert_eq!(!Bitboard::EMPTY, Bitboard::ALL); } #[test] From 1c7857488203d3fdfbf402ff9578b3a7e3d99f9b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 23:52:16 +0200 Subject: [PATCH 047/157] Add GDB pretty-printers --- .gdbinit | 2 + utils/gdb/seer_pretty_printers.py | 75 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 .gdbinit create mode 100644 utils/gdb/seer_pretty_printers.py diff --git a/.gdbinit b/.gdbinit new file mode 100644 index 0000000..d04df33 --- /dev/null +++ b/.gdbinit @@ -0,0 +1,2 @@ +# Register pretty-printers +source utils/gdb/seer_pretty_printers.py diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py new file mode 100644 index 0000000..2bfce31 --- /dev/null +++ b/utils/gdb/seer_pretty_printers.py @@ -0,0 +1,75 @@ +import gdb.printing + + +class Square(object): + """ + Python representation of a 'seer::board::square::Square' raw value. + """ + + 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 + + def __str__(self): + return self.FILES[self.file] + self.RANKS[self.rank] + + @property + def rank(self): + return int(self._val) % 8 + + @property + def file(self): + return int(self._val) // 8 + + +class Bitboard(object): + """ + Python representation of a 'seer::board::bitboard::Bitboard' raw value. + """ + + def __init__(self, val): + self._val = val + + def __str__(self): + return "[" + ", ".join(map(str, self.squares)) + "]" + + @property + def squares(self): + n = self._val + while n: + b = n & (~n + 1) + yield Square(b.bit_length() - 1) + n ^= b + + +class SquarePrinter(object): + "Print a seer::board::square::Square" + + def __init__(self, val): + self._val = Square(val) + + def to_string(self): + return str(self._val) + + +class BitboardPrinter(object): + "Print a seer::board::bitboard::Bitboard" + + def __init__(self, val): + self._val = Bitboard(int(val["__0"])) + + def to_string(self): + return "Bitboard{" + str(self._val)[1:-1] + "}" + + +def build_pretty_printer(): + pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') + + pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter) + pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) + + return pp + +gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True) From 5a35c5509190ba48e5c5842d6ad43831f21024bf Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:05:49 +0200 Subject: [PATCH 048/157] Add 'Rank' enum --- src/board/mod.rs | 3 ++ src/board/rank.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/board/rank.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index 06a7d91..ef264e1 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,5 +1,8 @@ pub mod bitboard; pub use bitboard::*; +pub mod rank; +pub use rank::*; + pub mod square; pub use square::*; diff --git a/src/board/rank.rs b/src/board/rank.rs new file mode 100644 index 0000000..11ba5e8 --- /dev/null +++ b/src/board/rank.rs @@ -0,0 +1,89 @@ +use super::Bitboard; + +/// An enum representing a singular rank on a chess board (i.e: the rows). +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Rank { + First, + Second, + Third, + Fourth, + Fifth, + Sixth, + Seventh, + Eighth, +} + +impl Rank { + const ALL: [Rank; 8] = [ + Rank::First, + Rank::Second, + Rank::Third, + Rank::Fourth, + Rank::Fifth, + Rank::Sixth, + Rank::Seventh, + Rank::Eighth, + ]; + + /// Iterate over all ranks in order. + 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); + // 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) + } + + /// Return the index of a given [Rank]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + + /// Turn a [Rank] into a [Bitboard] of all squares in that rank. + #[inline(always)] + pub fn into_bitboard(self) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *Bitboard::RANKS.get_unchecked(self.index()) } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(Rank::from_index(0), Rank::First); + assert_eq!(Rank::from_index(1), Rank::Second); + assert_eq!(Rank::from_index(7), Rank::Eighth); + } + + #[test] + fn index() { + assert_eq!(Rank::First.index(), 0); + assert_eq!(Rank::Second.index(), 1); + assert_eq!(Rank::Eighth.index(), 7); + } + + #[test] + fn into_bitboard() { + assert_eq!(Rank::First.into_bitboard(), Bitboard::RANKS[0]); + assert_eq!(Rank::Second.into_bitboard(), Bitboard::RANKS[1]); + assert_eq!(Rank::Eighth.into_bitboard(), Bitboard::RANKS[7]); + } +} From 9678180c75e3ee697b4f2d47438797baa71ef08c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:05:56 +0200 Subject: [PATCH 049/157] Add 'File' enum --- src/board/file.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 92 insertions(+) create mode 100644 src/board/file.rs diff --git a/src/board/file.rs b/src/board/file.rs new file mode 100644 index 0000000..b2e87f1 --- /dev/null +++ b/src/board/file.rs @@ -0,0 +1,89 @@ +use super::Bitboard; + +/// An enum representing a singular file on a chess board (i.e: the columns). +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum File { + A, + B, + C, + D, + E, + F, + G, + H, +} + +impl File { + const ALL: [File; 8] = [ + File::A, + File::B, + File::C, + File::D, + File::E, + File::F, + File::G, + File::H, + ]; + + /// Iterate over all files in order. + 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); + // 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) + } + + /// Return the index of a given [File]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + + /// Turn a [File] into a [Bitboard] of all squares in that file. + #[inline(always)] + pub fn into_bitboard(self) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *Bitboard::FILES.get_unchecked(self.index()) } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(File::from_index(0), File::A); + assert_eq!(File::from_index(1), File::B); + assert_eq!(File::from_index(7), File::H); + } + + #[test] + fn index() { + assert_eq!(File::A.index(), 0); + assert_eq!(File::B.index(), 1); + assert_eq!(File::H.index(), 7); + } + + #[test] + fn into_bitboard() { + assert_eq!(File::A.into_bitboard(), Bitboard::FILES[0]); + assert_eq!(File::B.into_bitboard(), Bitboard::FILES[1]); + assert_eq!(File::H.into_bitboard(), Bitboard::FILES[7]); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index ef264e1..7923cab 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod file; +pub use file::*; + pub mod rank; pub use rank::*; From 47e4e77f6cd07ebd3848585689ce8b3d238752d0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:20:33 +0200 Subject: [PATCH 050/157] Don't return 'Bitboard' from 'Square::{file,rank}' --- src/board/square.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/board/square.rs b/src/board/square.rs index 640214c..5088a4a 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -1,4 +1,4 @@ -use super::Bitboard; +use super::{Bitboard, File, Rank}; /// Represent a square on a chessboard. Defined in the same order as the /// [Bitboard](crate::board::Bitboard) squares. @@ -69,16 +69,18 @@ impl Square { (self as usize) / 8 } - /// Return a bitboard representing the rank of this square. + /// Return a [Rank] representing the rank of this square. #[inline(always)] - pub fn rank(self) -> Bitboard { - Bitboard::RANKS[self.rank_index()] + pub fn rank(self) -> Rank { + // SAFETY: we know the value is in-bounds + unsafe { Rank::from_index_unchecked(self.rank_index()) } } - /// Return a bitboard representing the rank of this square. + /// Return a [File] representing the rank of this square. #[inline(always)] - pub fn file(self) -> Bitboard { - Bitboard::FILES[self.file_index()] + pub fn file(self) -> File { + // SAFETY: we know the value is in-bounds + unsafe { File::from_index_unchecked(self.file_index()) } } /// Turn a square into a singleton bitboard. @@ -174,6 +176,24 @@ impl std::ops::Sub for Square { mod test { use super::*; use crate::board::bitboard::*; + use crate::board::file::*; + use crate::board::rank::*; + + #[test] + fn file() { + assert_eq!(Square::A1.file(), File::A); + assert_eq!(Square::A2.file(), File::A); + assert_eq!(Square::B1.file(), File::B); + assert_eq!(Square::H8.file(), File::H); + } + + #[test] + fn rank() { + assert_eq!(Square::A1.rank(), Rank::First); + assert_eq!(Square::A2.rank(), Rank::Second); + assert_eq!(Square::B1.rank(), Rank::First); + assert_eq!(Square::H8.rank(), Rank::Eighth); + } #[test] fn left_shift() { From f873b64f15653616c02a45e1c03fe18f14941b6b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:23:19 +0200 Subject: [PATCH 051/157] Add 'Square' constructor from 'File', 'Rank' --- src/board/square.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/board/square.rs b/src/board/square.rs index 5088a4a..3fb23ac 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -34,6 +34,13 @@ impl Square { Self::H1, Self::H2, Self::H3, Self::H4, Self::H5, Self::H6, Self::H7, Self::H8, ]; + /// Construct a [Square] from a [File] and [Rank]. + #[inline(always)] + pub fn new(file: File, rank: Rank) -> Self { + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(file.index() * 8 + rank.index()) } + } + /// Iterate over all squares in order. pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() @@ -179,6 +186,14 @@ mod test { use crate::board::file::*; use crate::board::rank::*; + #[test] + fn new() { + assert_eq!(Square::new(File::A, Rank::First), Square::A1); + assert_eq!(Square::new(File::A, Rank::Second), Square::A2); + assert_eq!(Square::new(File::B, Rank::First), Square::B1); + assert_eq!(Square::new(File::H, Rank::Eighth), Square::H8); + } + #[test] fn file() { assert_eq!(Square::A1.file(), File::A); From 1c4a0938372d663c97491e936be5cbe1d0141d8d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:34:33 +0200 Subject: [PATCH 052/157] Add 'File::{left,right}' --- src/board/file.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/board/file.rs b/src/board/file.rs index b2e87f1..3dac5c8 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -54,6 +54,18 @@ impl File { self as usize } + /// Return the [File] to the left, as seen from white's perspective. Wraps around the board. + pub fn left(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_sub(1) & 7) } + } + + /// Return the [File] to the right, as seen from white's perspective. Wraps around the board. + pub fn right(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_add(1) & 7) } + } + /// Turn a [File] into a [Bitboard] of all squares in that file. #[inline(always)] pub fn into_bitboard(self) -> Bitboard { @@ -80,6 +92,20 @@ mod test { assert_eq!(File::H.index(), 7); } + #[test] + fn left() { + assert_eq!(File::A.left(), File::H); + assert_eq!(File::B.left(), File::A); + assert_eq!(File::H.left(), File::G); + } + + #[test] + fn right() { + assert_eq!(File::A.right(), File::B); + assert_eq!(File::B.right(), File::C); + assert_eq!(File::H.right(), File::A); + } + #[test] fn into_bitboard() { assert_eq!(File::A.into_bitboard(), Bitboard::FILES[0]); From e48994e7bebf7afb8ba8619a403bbfe1d5883e29 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:34:46 +0200 Subject: [PATCH 053/157] Add 'Rank::{up,down}' --- src/board/rank.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/board/rank.rs b/src/board/rank.rs index 11ba5e8..c2499d7 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -54,6 +54,18 @@ impl Rank { self as usize } + /// Return the [Rank] one-row up, as seen from white's perspective. Wraps around the board. + pub fn up(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_add(1) & 7) } + } + + /// Return the [Rank] one-row down, as seen from white's perspective. Wraps around the board. + pub fn down(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_sub(1) & 7) } + } + /// Turn a [Rank] into a [Bitboard] of all squares in that rank. #[inline(always)] pub fn into_bitboard(self) -> Bitboard { @@ -80,6 +92,20 @@ mod test { assert_eq!(Rank::Eighth.index(), 7); } + #[test] + fn up() { + assert_eq!(Rank::First.up(), Rank::Second); + assert_eq!(Rank::Second.up(), Rank::Third); + assert_eq!(Rank::Eighth.up(), Rank::First); + } + + #[test] + fn down() { + assert_eq!(Rank::First.down(), Rank::Eighth); + assert_eq!(Rank::Second.down(), Rank::First); + assert_eq!(Rank::Eighth.down(), Rank::Seventh); + } + #[test] fn into_bitboard() { assert_eq!(Rank::First.into_bitboard(), Bitboard::RANKS[0]); From 93d5fe97eda044123b6a068849aae33f4e01e4c8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 20:39:47 +0200 Subject: [PATCH 054/157] Add 'Bitboard::count' --- src/board/bitboard/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 524cf33..d2c8921 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -37,6 +37,12 @@ impl Bitboard { Bitboard(0b00000000_11111111_00000000_00000000_00000000_00000000_00000000_00000000), Bitboard(0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000), ]; + + /// Count the number of pieces in the [Bitboard]. + #[inline(always)] + pub fn count(self) -> u32 { + self.0.count_ones() + } } impl Default for Bitboard { @@ -170,6 +176,13 @@ mod test { use super::*; use crate::board::square::*; + #[test] + fn count() { + assert_eq!(Bitboard::EMPTY.count(), 0); + assert_eq!(Bitboard::FILES[0].count(), 8); + assert_eq!(Bitboard::ALL.count(), 64); + } + #[test] fn iter() { assert_eq!(Bitboard::EMPTY.into_iter().collect::>(), Vec::new()); From be5d2831bdbd095351c9cf374efc13f4d5106848 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 21:30:23 +0200 Subject: [PATCH 055/157] Add 'Square::index' --- src/board/square.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/board/square.rs b/src/board/square.rs index 3fb23ac..04e2300 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -64,6 +64,12 @@ impl Square { std::mem::transmute(index as u8) } + /// Return the index of a given [Square]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + /// Return the index of the rank of this square (0 -> rank 1, ..., 7 -> rank 8). #[inline(always)] pub fn rank_index(self) -> usize { @@ -194,6 +200,14 @@ mod test { assert_eq!(Square::new(File::H, Rank::Eighth), Square::H8); } + #[test] + fn index() { + assert_eq!(Square::A1.index(), 0); + assert_eq!(Square::A2.index(), 1); + assert_eq!(Square::B1.index(), 8); + assert_eq!(Square::H8.index(), 63); + } + #[test] fn file() { assert_eq!(Square::A1.file(), File::A); From 8e2b7c1714ee16b822b2c2b0046c3ca9cdb1c06c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 21:30:23 +0200 Subject: [PATCH 056/157] Use 'Square::index' in 'Square::{file,rank}_index' --- src/board/square.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/board/square.rs b/src/board/square.rs index 04e2300..0079bbb 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -73,13 +73,13 @@ impl Square { /// Return the index of the rank of this square (0 -> rank 1, ..., 7 -> rank 8). #[inline(always)] pub fn rank_index(self) -> usize { - (self as usize) % 8 + self.index() % 8 } /// Return the index of the rank of this square (0 -> file A, ..., 7 -> file H). #[inline(always)] pub fn file_index(self) -> usize { - (self as usize) / 8 + self.index() / 8 } /// Return a [Rank] representing the rank of this square. From c95d7f078a67b1c7de976a03d1ceff3e49d0e8c4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:06:43 +0200 Subject: [PATCH 057/157] Add 'board::Direction' enum --- src/board/directions.rs | 22 ++++++++++++++++++++++ src/board/mod.rs | 3 +++ 2 files changed, 25 insertions(+) create mode 100644 src/board/directions.rs diff --git a/src/board/directions.rs b/src/board/directions.rs new file mode 100644 index 0000000..7e10ab3 --- /dev/null +++ b/src/board/directions.rs @@ -0,0 +1,22 @@ +/// A direction on the board. Either along the rook, bishop, or knight directions +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Direction { + North, + West, + South, + East, + + NorthWest, + SouthWest, + SouthEast, + NorthEast, + + NorthNorthWest, + NorthWestWest, + SouthWestWest, + SouthSouthWest, + SouthSouthEast, + SouthEastEast, + NorthEastEast, + NorthNorthEast, +} diff --git a/src/board/mod.rs b/src/board/mod.rs index 7923cab..bc9d1d9 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod directions; +pub use directions::*; + pub mod file; pub use file::*; From d03e2510501c3b99b69f0cad0f97d0ce1a357ed6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:06:50 +0200 Subject: [PATCH 058/157] Add 'Direction::move_board' Encapsulates the way to move a piece on a board, avoiding the need to mask and shift by hand. --- src/board/directions.rs | 524 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 524 insertions(+) diff --git a/src/board/directions.rs b/src/board/directions.rs index 7e10ab3..3345256 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -1,3 +1,5 @@ +use super::{Bitboard, Rank}; + /// A direction on the board. Either along the rook, bishop, or knight directions #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Direction { @@ -20,3 +22,525 @@ pub enum Direction { NorthEastEast, NorthNorthEast, } + +impl Direction { + /// Move every piece on a board along the given direction. Do not wrap around the board. + #[inline(always)] + pub fn move_board(self, board: Bitboard) -> Bitboard { + // No need to filter for A/H ranks thanks to wrapping + match self { + Self::North => (board - Rank::Eighth.into_bitboard()) << 1, + Self::West => board >> 8, + Self::South => (board - Rank::First.into_bitboard()) >> 1, + Self::East => board << 8, + + Self::NorthWest => (board - Rank::Eighth.into_bitboard()) >> 7, + Self::SouthWest => (board - Rank::First.into_bitboard()) >> 9, + Self::SouthEast => (board - Rank::First.into_bitboard()) << 7, + Self::NorthEast => (board - Rank::Eighth.into_bitboard()) << 9, + + Self::NorthNorthWest => { + (board - Rank::Eighth.into_bitboard() - Rank::Seventh.into_bitboard()) >> 6 + } + Self::NorthWestWest => (board - Rank::Eighth.into_bitboard()) >> 15, + Self::SouthWestWest => (board - Rank::First.into_bitboard()) >> 17, + Self::SouthSouthWest => { + (board - Rank::First.into_bitboard() - Rank::Second.into_bitboard()) >> 10 + } + Self::SouthSouthEast => { + (board - Rank::First.into_bitboard() - Rank::Second.into_bitboard()) << 6 + } + Self::SouthEastEast => (board - Rank::First.into_bitboard()) << 15, + Self::NorthEastEast => (board - Rank::Eighth.into_bitboard()) << 17, + Self::NorthNorthEast => { + (board - Rank::Eighth.into_bitboard() - Rank::Seventh.into_bitboard()) << 10 + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::Square; + + #[test] + fn north() { + assert_eq!( + Direction::North.move_board(Square::A1.into_bitboard()), + Square::A2.into_bitboard() + ); + assert_eq!( + Direction::North.move_board(Square::A2.into_bitboard()), + Square::A3.into_bitboard() + ); + assert_eq!( + Direction::North.move_board(Square::A7.into_bitboard()), + Square::A8.into_bitboard() + ); + assert_eq!( + Direction::North.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn west() { + assert_eq!( + Direction::West.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::West.move_board(Square::B1.into_bitboard()), + Square::A1.into_bitboard() + ); + assert_eq!( + Direction::West.move_board(Square::G1.into_bitboard()), + Square::F1.into_bitboard() + ); + assert_eq!( + Direction::West.move_board(Square::H1.into_bitboard()), + Square::G1.into_bitboard() + ); + } + + #[test] + fn south() { + assert_eq!( + Direction::South.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::South.move_board(Square::A2.into_bitboard()), + Square::A1.into_bitboard() + ); + assert_eq!( + Direction::South.move_board(Square::A7.into_bitboard()), + Square::A6.into_bitboard() + ); + assert_eq!( + Direction::South.move_board(Square::A8.into_bitboard()), + Square::A7.into_bitboard() + ); + } + + #[test] + fn east() { + assert_eq!( + Direction::East.move_board(Square::A1.into_bitboard()), + Square::B1.into_bitboard() + ); + assert_eq!( + Direction::East.move_board(Square::B1.into_bitboard()), + Square::C1.into_bitboard() + ); + assert_eq!( + Direction::East.move_board(Square::G1.into_bitboard()), + Square::H1.into_bitboard() + ); + assert_eq!( + Direction::East.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn north_west() { + assert_eq!( + Direction::NorthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthWest.move_board(Square::B1.into_bitboard()), + Square::A2.into_bitboard() + ); + assert_eq!( + Direction::NorthWest.move_board(Square::H1.into_bitboard()), + Square::G2.into_bitboard() + ); + assert_eq!( + Direction::NorthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthWest.move_board(Square::B8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthWest.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn south_west() { + assert_eq!( + Direction::SouthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::B1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::B8.into_bitboard()), + Square::A7.into_bitboard() + ); + assert_eq!( + Direction::SouthWest.move_board(Square::H8.into_bitboard()), + Square::G7.into_bitboard() + ); + } + + #[test] + fn south_east() { + assert_eq!( + Direction::SouthEast.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthEast.move_board(Square::B1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthEast.move_board(Square::A8.into_bitboard()), + Square::B7.into_bitboard() + ); + assert_eq!( + Direction::SouthEast.move_board(Square::B8.into_bitboard()), + Square::C7.into_bitboard() + ); + assert_eq!( + Direction::SouthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn north_east() { + assert_eq!( + Direction::NorthEast.move_board(Square::A1.into_bitboard()), + Square::B2.into_bitboard() + ); + assert_eq!( + Direction::NorthEast.move_board(Square::B1.into_bitboard()), + Square::C2.into_bitboard() + ); + assert_eq!( + Direction::NorthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthEast.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthEast.move_board(Square::B8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn north_north_west() { + assert_eq!( + Direction::NorthNorthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::B2.into_bitboard()), + Square::A4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::H1.into_bitboard()), + Square::G3.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::G2.into_bitboard()), + Square::F4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } + + #[test] + fn north_west_west() { + assert_eq!( + Direction::NorthWestWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::H1.into_bitboard()), + Square::F2.into_bitboard() + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::G2.into_bitboard()), + Square::E3.into_bitboard() + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::G7.into_bitboard()), + Square::E8.into_bitboard() + ); + } + + #[test] + fn south_west_west() { + assert_eq!( + Direction::SouthWestWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::G2.into_bitboard()), + Square::E1.into_bitboard() + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::H8.into_bitboard()), + Square::F7.into_bitboard() + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::G7.into_bitboard()), + Square::E6.into_bitboard() + ); + } + + #[test] + fn south_south_west() { + assert_eq!( + Direction::SouthSouthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::B7.into_bitboard()), + Square::A5.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::H8.into_bitboard()), + Square::G6.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::G7.into_bitboard()), + Square::F5.into_bitboard() + ); + } + + #[test] + fn south_south_east() { + assert_eq!( + Direction::SouthSouthEast.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::A8.into_bitboard()), + Square::B6.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::B7.into_bitboard()), + Square::C5.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::G7.into_bitboard()), + Square::H5.into_bitboard() + ); + } + + #[test] + fn south_east_east() { + assert_eq!( + Direction::SouthEastEast.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::B2.into_bitboard()), + Square::D1.into_bitboard() + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::A8.into_bitboard()), + Square::C7.into_bitboard() + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::B7.into_bitboard()), + Square::D6.into_bitboard() + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } + + #[test] + fn north_east_east() { + assert_eq!( + Direction::NorthEastEast.move_board(Square::A1.into_bitboard()), + Square::C2.into_bitboard() + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::B2.into_bitboard()), + Square::D3.into_bitboard() + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::B7.into_bitboard()), + Square::D8.into_bitboard() + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } + + #[test] + fn north_north_east() { + assert_eq!( + Direction::NorthNorthEast.move_board(Square::A1.into_bitboard()), + Square::B3.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::B2.into_bitboard()), + Square::C4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::G2.into_bitboard()), + Square::H4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } +} From be3a8030a77fd6abdeb67952ecd85807706e8653 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:27:33 +0200 Subject: [PATCH 059/157] Add 'Direction::iter_{rook,bishop,royalty,knight}' --- src/board/directions.rs | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/board/directions.rs b/src/board/directions.rs index 3345256..d339bc1 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -24,6 +24,49 @@ pub enum Direction { } impl Direction { + /// Directions that a rook could use. + pub const ROOK_DIRECTIONS: [Self; 4] = [Self::North, Self::West, Self::South, Self::East]; + + /// Directions that a bishop could use. + pub const BISHOP_DIRECTIONS: [Self; 4] = [ + Self::NorthWest, + Self::SouthWest, + Self::SouthEast, + Self::NorthEast, + ]; + + /// Directions that a knight could use. + pub const KNIGHT_DIRECTIONS: [Self; 8] = [ + Self::NorthNorthWest, + Self::NorthWestWest, + Self::SouthWestWest, + Self::SouthSouthWest, + Self::SouthSouthEast, + Self::SouthEastEast, + Self::NorthEastEast, + Self::NorthNorthEast, + ]; + + /// Iterate over all directions a rook can take. + pub fn iter_rook() -> impl Iterator { + Self::ROOK_DIRECTIONS.iter().cloned() + } + + /// Iterate over all directions a bishop can take. + pub fn iter_bishop() -> impl Iterator { + Self::BISHOP_DIRECTIONS.iter().cloned() + } + + /// Iterate over all directions a queen or king can take. + pub fn iter_royalty() -> impl Iterator { + Self::iter_rook().chain(Self::iter_bishop()) + } + + /// Iterate over all directions a knight can take. + pub fn iter_knight() -> impl Iterator { + Self::KNIGHT_DIRECTIONS.iter().cloned() + } + /// Move every piece on a board along the given direction. Do not wrap around the board. #[inline(always)] pub fn move_board(self, board: Bitboard) -> Bitboard { From 21eecbdac96f07698aaca50022eb4c78fe5eef43 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:03:35 +0200 Subject: [PATCH 060/157] Add 'Direction::move_square' --- src/board/directions.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/board/directions.rs b/src/board/directions.rs index d339bc1..1c73a16 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -1,4 +1,4 @@ -use super::{Bitboard, Rank}; +use super::{Bitboard, Rank, Square}; /// A direction on the board. Either along the rook, bishop, or knight directions #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -67,6 +67,12 @@ impl Direction { Self::KNIGHT_DIRECTIONS.iter().cloned() } + /// Move a [Square] along the given [Direction], unless it would wrap at the end of the board + pub fn move_square(self, square: Square) -> Option { + let res = self.move_board(square.into_bitboard()); + res.into_iter().next() + } + /// Move every piece on a board along the given direction. Do not wrap around the board. #[inline(always)] pub fn move_board(self, board: Bitboard) -> Bitboard { @@ -105,7 +111,6 @@ impl Direction { #[cfg(test)] mod test { use super::*; - use crate::board::Square; #[test] fn north() { From 5ef6e545f3d56abd1a57515f1c0869e06543ca4b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:27:08 +0200 Subject: [PATCH 061/157] Add 'Bitboard::{ANTI_,}DIAGONAL' --- src/board/bitboard/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index d2c8921..d2f3723 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -38,6 +38,12 @@ impl Bitboard { Bitboard(0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000), ]; + /// The diagonal from [Square::A1] to [Square::H8]. + pub const DIAGONAL: Bitboard = Bitboard(0x8040201008040201); + + /// The diagonal from [Square::A8] to [Square::H1]. + pub const ANTI_DIAGONAL: Bitboard = Bitboard(0x0102040810204080); + /// Count the number of pieces in the [Bitboard]. #[inline(always)] pub fn count(self) -> u32 { From eba64a98d4ee16c7d7038ae4d461ffd2a466be9b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:27:46 +0200 Subject: [PATCH 062/157] Add 'Direction::slide_{square,board}' --- src/board/directions.rs | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/board/directions.rs b/src/board/directions.rs index 1c73a16..26b75de 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -106,11 +106,39 @@ impl Direction { } } } + + /// Slide a board along the given [Direction], i.e: return all successive applications of + /// [Direction::move_square] until no new squares can be 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_square(self, square: Square) -> Bitboard { + self.slide_board(square.into_bitboard()) + } + + /// Slide a board along the given [Direction], i.e: return all successive applications of + /// [Direction::move_board] until no new squares can be 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(self, mut board: Bitboard) -> Bitboard { + debug_assert!(!Self::KNIGHT_DIRECTIONS.contains(&self)); + + let mut res = Default::default(); + + while board != Bitboard::EMPTY { + board = self.move_board(board); + res = res | board; + } + + res + } } #[cfg(test)] mod test { use super::*; + use crate::board::{File, Rank}; #[test] fn north() { @@ -591,4 +619,40 @@ mod test { Bitboard::EMPTY, ); } + + #[test] + fn slide() { + assert_eq!( + Direction::North.slide_square(Square::A1), + File::A.into_bitboard() - Square::A1 + ); + assert_eq!( + Direction::West.slide_square(Square::H1), + Rank::First.into_bitboard() - Square::H1 + ); + assert_eq!( + Direction::South.slide_square(Square::A8), + File::A.into_bitboard() - Square::A8 + ); + assert_eq!( + Direction::East.slide_square(Square::A1), + Rank::First.into_bitboard() - Square::A1 + ); + assert_eq!( + Direction::NorthWest.slide_square(Square::H1), + Bitboard::ANTI_DIAGONAL - Square::H1 + ); + assert_eq!( + Direction::SouthWest.slide_square(Square::H8), + Bitboard::DIAGONAL - Square::H8 + ); + assert_eq!( + Direction::SouthEast.slide_square(Square::A8), + Bitboard::ANTI_DIAGONAL - Square::A8 + ); + assert_eq!( + Direction::NorthEast.slide_square(Square::A1), + Bitboard::DIAGONAL - Square::A1 + ); + } } From 63058f6c4abb660c8d81c6c89e8a8d92dc7a4209 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:31:33 +0200 Subject: [PATCH 063/157] Add 'Bitboard::is_empty' --- src/board/bitboard/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index d2f3723..04b205a 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -49,6 +49,12 @@ impl Bitboard { pub fn count(self) -> u32 { self.0.count_ones() } + + /// Return true if there are no pieces in the [Bitboard], otherwise false. + #[inline(always)] + pub fn is_empty(self) -> bool { + self == Self::EMPTY + } } impl Default for Bitboard { From 5a9f0db1b97313c318911712a4bfb9019075a380 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:32:40 +0200 Subject: [PATCH 064/157] Make use of 'Bitboard::is_empty' --- src/board/directions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/directions.rs b/src/board/directions.rs index 26b75de..135f5f4 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -126,7 +126,7 @@ impl Direction { let mut res = Default::default(); - while board != Bitboard::EMPTY { + while !board.is_empty() { board = self.move_board(board); res = res | board; } From c6916cc21af3c55be41e47d88932f1231a48e58f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:43:48 +0200 Subject: [PATCH 065/157] Add 'Bitboard::{LIGHT,DARK}_SQUARES --- src/board/bitboard/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 04b205a..092dba7 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -44,6 +44,12 @@ impl Bitboard { /// The diagonal from [Square::A8] to [Square::H1]. pub const ANTI_DIAGONAL: Bitboard = Bitboard(0x0102040810204080); + /// The light [Square]s on a board, e.g: [Square::H1]. + pub const LIGHT_SQUARES: Bitboard = Bitboard(0x55AA55AA55AA55AA); + + /// The dark [Square]s on a board, e.g: [Square::A1]. + pub const DARK_SQUARES: Bitboard = Bitboard(0x55AA55AA55AA55AA); + /// Count the number of pieces in the [Bitboard]. #[inline(always)] pub fn count(self) -> u32 { From accbbef64e7386344a4351e0699621ecd636607d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 11:04:52 +0200 Subject: [PATCH 066/157] Add 'static_assert' macro --- src/lib.rs | 1 + src/utils/mod.rs | 2 ++ src/utils/static_assert.rs | 25 +++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 src/utils/mod.rs create mode 100644 src/utils/static_assert.rs diff --git a/src/lib.rs b/src/lib.rs index 667c357..3593172 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ pub mod board; +pub mod utils; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..2833a48 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod static_assert; +pub use static_assert::*; diff --git a/src/utils/static_assert.rs b/src/utils/static_assert.rs new file mode 100644 index 0000000..2c7a9a8 --- /dev/null +++ b/src/utils/static_assert.rs @@ -0,0 +1,25 @@ +//! Static assert. + +/// Assert a condition at compile-time. +/// +/// See [RFC 2790] for potential addition into Rust itself. +/// +/// [RFC 2790]: https://github.com/rust-lang/rfcs/issues/2790 +/// +/// # Examples +/// +/// ``` +/// use seer::utils::static_assert; +/// +/// static_assert!(42 > 0); +/// ``` +#[macro_export] +macro_rules! static_assert { + ($($tt:tt)*) => { + #[allow(dead_code)] + const _: () = assert!($($tt)*); + }; +} + +// I want it namespaced, even though it is exported to the root of the crate by `#[macro_export]`. +pub use static_assert; From a6f4e7e686a021ece3abe70f7b0755f5e5fa3b40 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 11:05:38 +0200 Subject: [PATCH 067/157] Statically assert zero-cost invariants Since some or all of those invariants will come in handy to ensure we use as little memory as possible, to maximize the speed of the move generation later on. --- src/board/bitboard/mod.rs | 5 +++++ src/board/file.rs | 4 ++++ src/board/rank.rs | 4 ++++ src/board/square.rs | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 092dba7..8b716be 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,4 +1,6 @@ use super::Square; +use crate::utils::static_assert; + mod iterator; use iterator::*; @@ -63,6 +65,9 @@ impl Bitboard { } } +// Ensure zero-cost (at least size-wise) wrapping. +static_assert!(std::mem::size_of::() == std::mem::size_of::()); + impl Default for Bitboard { fn default() -> Self { Self::EMPTY diff --git a/src/board/file.rs b/src/board/file.rs index 3dac5c8..fa7ba15 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -1,4 +1,5 @@ use super::Bitboard; +use crate::utils::static_assert; /// An enum representing a singular file on a chess board (i.e: the columns). #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -74,6 +75,9 @@ impl File { } } +// Ensure that niche-optimization is in effect. +static_assert!(std::mem::size_of::>() == std::mem::size_of::()); + #[cfg(test)] mod test { use super::*; diff --git a/src/board/rank.rs b/src/board/rank.rs index c2499d7..59374c7 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -1,4 +1,5 @@ use super::Bitboard; +use crate::utils::static_assert; /// An enum representing a singular rank on a chess board (i.e: the rows). #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -74,6 +75,9 @@ impl Rank { } } +// Ensure that niche-optimization is in effect. +static_assert!(std::mem::size_of::>() == std::mem::size_of::()); + #[cfg(test)] mod test { use super::*; diff --git a/src/board/square.rs b/src/board/square.rs index 0079bbb..69d8167 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -1,4 +1,5 @@ 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. @@ -185,6 +186,9 @@ impl std::ops::Sub for Square { } } +// Ensure that niche-optimization is in effect. +static_assert!(std::mem::size_of::>() == std::mem::size_of::()); + #[cfg(test)] mod test { use super::*; From ffaad7b7d411b3e0d08c1b4fa2f10e390b103c86 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:11:37 +0200 Subject: [PATCH 068/157] Rename 'board::direction{s,}' --- src/board/{directions.rs => direction.rs} | 0 src/board/mod.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/board/{directions.rs => direction.rs} (100%) diff --git a/src/board/directions.rs b/src/board/direction.rs similarity index 100% rename from src/board/directions.rs rename to src/board/direction.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index bc9d1d9..ea3c69e 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,8 +1,8 @@ pub mod bitboard; pub use bitboard::*; -pub mod directions; -pub use directions::*; +pub mod direction; +pub use direction::*; pub mod file; pub use file::*; From 3553428bb9f02d32f0f1a8b864e85249ac941e38 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:22:04 +0200 Subject: [PATCH 069/157] Add 'Color' enum --- src/board/color.rs | 106 +++++++++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 109 insertions(+) create mode 100644 src/board/color.rs diff --git a/src/board/color.rs b/src/board/color.rs new file mode 100644 index 0000000..ada896d --- /dev/null +++ b/src/board/color.rs @@ -0,0 +1,106 @@ +use super::Rank; + +/// An enum representing the color of a player. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Color { + White, + Black, +} + +impl Color { + /// Convert from a file index into a [Color] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < 2); + // 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. + /// + /// # 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) + } + + /// Return the index of a given [Color]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + + /// Return the first [Rank] for pieces of the given [Color], where its pieces start. + #[inline(always)] + pub fn first_rank(self) -> Rank { + match self { + Self::White => Rank::First, + Self::Black => Rank::Eighth, + } + } + + /// Return the second [Rank] for pieces of the given [Color], where its pawns start. + #[inline(always)] + pub fn second_rank(self) -> Rank { + match self { + Self::White => Rank::Second, + Self::Black => Rank::Seventh, + } + } + + /// Return the fourth [Rank] for pieces of the given [Color], where its pawns move to after a + /// two-square move. + #[inline(always)] + pub fn fourth_rank(self) -> Rank { + match self { + Self::White => Rank::Fourth, + Self::Black => Rank::Fifth, + } + } + + /// Return the seventh [Rank] for pieces of the given [Color], which is the rank before a pawn + /// gets promoted. + #[inline(always)] + pub fn seventh_rank(self) -> Rank { + match self { + Self::White => Rank::Seventh, + Self::Black => Rank::Second, + } + } +} + +impl std::ops::Not for Color { + type Output = Color; + + fn not(self) -> Self::Output { + match self { + Self::White => Self::Black, + Self::Black => Self::White, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(Color::from_index(0), Color::White); + assert_eq!(Color::from_index(1), Color::Black); + } + + #[test] + fn index() { + assert_eq!(Color::White.index(), 0); + assert_eq!(Color::Black.index(), 1); + } + + #[test] + fn not() { + assert_eq!(!Color::White, Color::Black); + assert_eq!(!Color::Black, Color::White); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index ea3c69e..6a29cbe 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod color; +pub use color::*; + pub mod direction; pub use direction::*; From 688b624dd19835498fc766952772ac684a204a7e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:27:03 +0200 Subject: [PATCH 070/157] Add 'CastleRights' enum --- src/board/castle_rights.rs | 59 ++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 62 insertions(+) create mode 100644 src/board/castle_rights.rs diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs new file mode 100644 index 0000000..630fcfb --- /dev/null +++ b/src/board/castle_rights.rs @@ -0,0 +1,59 @@ +/// Current castle rights for a player. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CastleRights { + /// No castling allowed. + NoSide, + /// King-side castling only. + KingSide, + /// Queen-side castling only. + QueenSide, + /// Either side allowed. + BothSides, +} + +impl CastleRights { + /// Convert from a castle rights index into a [CastleRights] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < 4); + // 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) + } + + /// Return the index of a given [CastleRights]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(CastleRights::from_index(0), CastleRights::NoSide); + assert_eq!(CastleRights::from_index(1), CastleRights::KingSide); + assert_eq!(CastleRights::from_index(2), CastleRights::QueenSide); + assert_eq!(CastleRights::from_index(3), CastleRights::BothSides); + } + + #[test] + fn index() { + assert_eq!(CastleRights::NoSide.index(), 0); + assert_eq!(CastleRights::KingSide.index(), 1); + assert_eq!(CastleRights::QueenSide.index(), 2); + assert_eq!(CastleRights::BothSides.index(), 3); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index 6a29cbe..ad91192 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod castle_rights; +pub use castle_rights::*; + pub mod color; pub use color::*; From d8e003ef943655df4c83b0063cc3d871e70eea99 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:27:37 +0200 Subject: [PATCH 071/157] Add 'CastleRights::has_{king,queen}_side' --- src/board/castle_rights.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 630fcfb..0d02986 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -35,6 +35,18 @@ impl CastleRights { pub fn index(self) -> usize { self as usize } + + /// Can the player castle king-side. + #[inline(always)] + pub fn has_king_side(self) -> bool { + (self.index() & 1) != 0 + } + + /// Can the player castle king-side. + #[inline(always)] + pub fn has_queen_side(self) -> bool { + (self.index() & 2) != 0 + } } #[cfg(test)] @@ -56,4 +68,20 @@ mod test { assert_eq!(CastleRights::QueenSide.index(), 2); assert_eq!(CastleRights::BothSides.index(), 3); } + + #[test] + fn has_kingside() { + assert!(!CastleRights::NoSide.has_king_side()); + assert!(!CastleRights::QueenSide.has_king_side()); + assert!(CastleRights::KingSide.has_king_side()); + assert!(CastleRights::BothSides.has_king_side()); + } + + #[test] + fn has_queenside() { + assert!(!CastleRights::NoSide.has_queen_side()); + assert!(!CastleRights::KingSide.has_queen_side()); + assert!(CastleRights::QueenSide.has_queen_side()); + assert!(CastleRights::BothSides.has_queen_side()); + } } From 96b9e0f6d7d8ad4e84a12d73d3c1cf455dcf2358 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:35:31 +0200 Subject: [PATCH 072/157] Add 'CastleRights::without_{king,queen}_side' --- src/board/castle_rights.rs | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 0d02986..05dd438 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -47,6 +47,25 @@ impl CastleRights { pub fn has_queen_side(self) -> bool { (self.index() & 2) != 0 } + + /// Remove king-side castling rights. + #[inline(always)] + pub fn without_king_side(self) -> Self { + self.remove(Self::KingSide) + } + + /// Remove queen-side castling rights. + #[inline(always)] + pub fn without_queen_side(self) -> Self { + self.remove(Self::QueenSide) + } + + /// Remove some [CastleRights], and return the resulting [CastleRights]. + #[inline(always)] + pub fn remove(self, to_remove: CastleRights) -> Self { + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(self.index() & !to_remove.index()) } + } } #[cfg(test)] @@ -84,4 +103,44 @@ mod test { assert!(CastleRights::QueenSide.has_queen_side()); assert!(CastleRights::BothSides.has_queen_side()); } + + #[test] + fn without_king_side() { + assert_eq!( + CastleRights::NoSide.without_king_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::KingSide.without_king_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::QueenSide.without_king_side(), + CastleRights::QueenSide + ); + assert_eq!( + CastleRights::BothSides.without_king_side(), + CastleRights::QueenSide + ); + } + + #[test] + fn without_queen_side() { + assert_eq!( + CastleRights::NoSide.without_queen_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::QueenSide.without_queen_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::KingSide.without_queen_side(), + CastleRights::KingSide + ); + assert_eq!( + CastleRights::BothSides.without_queen_side(), + CastleRights::KingSide + ); + } } From 04ef2828152e6d0688156d38d67d9927e16cefa9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:50:23 +0200 Subject: [PATCH 073/157] Add 'CastleRights::unmoved_rooks' --- src/board/castle_rights.rs | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 05dd438..b398b57 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -1,3 +1,5 @@ +use super::{Bitboard, Color, File, Square}; + /// Current castle rights for a player. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum CastleRights { @@ -66,6 +68,22 @@ impl CastleRights { // SAFETY: we know the value is in-bounds unsafe { Self::from_index_unchecked(self.index() & !to_remove.index()) } } + + /// Which rooks have not been moved for a given [CastleRights] and [Color]. + #[inline(always)] + pub fn unmoved_rooks(self, color: Color) -> Bitboard { + let rank = color.first_rank(); + + let king_side_square = Square::new(File::H, rank); + let queen_side_square = Square::new(File::A, rank); + + match self { + Self::NoSide => Bitboard::EMPTY, + Self::KingSide => king_side_square.into_bitboard(), + Self::QueenSide => queen_side_square.into_bitboard(), + Self::BothSides => king_side_square | queen_side_square, + } + } } #[cfg(test)] @@ -143,4 +161,40 @@ mod test { CastleRights::KingSide ); } + + #[test] + fn unmoved_rooks() { + assert_eq!( + CastleRights::NoSide.unmoved_rooks(Color::White), + Bitboard::EMPTY + ); + assert_eq!( + CastleRights::NoSide.unmoved_rooks(Color::Black), + Bitboard::EMPTY + ); + assert_eq!( + CastleRights::KingSide.unmoved_rooks(Color::White), + Square::H1.into_bitboard() + ); + assert_eq!( + CastleRights::KingSide.unmoved_rooks(Color::Black), + Square::H8.into_bitboard() + ); + assert_eq!( + CastleRights::QueenSide.unmoved_rooks(Color::White), + Square::A1.into_bitboard() + ); + assert_eq!( + CastleRights::QueenSide.unmoved_rooks(Color::Black), + Square::A8.into_bitboard() + ); + assert_eq!( + CastleRights::BothSides.unmoved_rooks(Color::White), + Square::A1 | Square::H1 + ); + assert_eq!( + CastleRights::BothSides.unmoved_rooks(Color::Black), + Square::A8 | Square::H8 + ); + } } From c571b53840851badbaaf4c6c25cea39087ac0e4c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 17:27:09 +0200 Subject: [PATCH 074/157] Add 'Color::{forward,backward}_direction' --- src/board/color.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/board/color.rs b/src/board/color.rs index ada896d..92fb4b5 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -1,4 +1,4 @@ -use super::Rank; +use super::{Direction, Rank}; /// An enum representing the color of a player. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -69,6 +69,21 @@ impl Color { Self::Black => Rank::Second, } } + + /// Which way do pawns advance for this color. + #[inline(always)] + pub fn forward_direction(self) -> Direction { + match self { + Self::White => Direction::North, + Self::Black => Direction::South, + } + } + + /// Which way do the opponent's pawns advance for this color. + #[inline(always)] + pub fn backward_direction(self) -> Direction { + (!self).forward_direction() + } } impl std::ops::Not for Color { From 6e871dbfe67afbcdf6bb9c1d1c432064e1b4f333 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 18:21:14 +0200 Subject: [PATCH 075/157] Improve 'board::BitboardIterator' * Accurate 'size_hint'. * Exact size. * Fused iterator (keeps returning 'None' after returning 'None' once). --- src/board/bitboard/iterator.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs index 06db283..fcd644c 100644 --- a/src/board/bitboard/iterator.rs +++ b/src/board/bitboard/iterator.rs @@ -14,4 +14,14 @@ impl Iterator for BitboardIterator { Some(crate::board::Square::from_index(lsb)) } } + + fn size_hint(&self) -> (usize, Option) { + let size = self.0.count_ones() as usize; + + (size, Some(size)) + } } + +impl ExactSizeIterator for BitboardIterator {} + +impl std::iter::FusedIterator for BitboardIterator {} From f4052461273dd09efe61238c42808a8eb8b96e9c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 18:48:22 +0200 Subject: [PATCH 076/157] Add 'Color::slide_board_with_blockers' --- src/board/direction.rs | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/board/direction.rs b/src/board/direction.rs index 135f5f4..324f97c 100644 --- a/src/board/direction.rs +++ b/src/board/direction.rs @@ -121,7 +121,18 @@ 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(); @@ -129,6 +140,9 @@ impl Direction { while !board.is_empty() { board = self.move_board(board); res = 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 + ); + } } From 568c3696838ed75dc3988dcf8b482c8c884f2506 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:07:37 +0200 Subject: [PATCH 077/157] Fix typo in 'board::Color' documentation --- src/board/color.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/board/color.rs b/src/board/color.rs index 92fb4b5..d5c66d3 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -8,7 +8,7 @@ pub enum Color { } impl Color { - /// Convert from a file index into a [Color] type. + /// Convert from a color index into a [Color] type. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < 2); @@ -16,7 +16,7 @@ impl Color { 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 /// From 3f9341709219fbe776fcf296ab4337b99ca3442a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:18:46 +0200 Subject: [PATCH 078/157] Consistently use 'Self' type in 'impl' blocks --- src/board/file.rs | 20 ++++++++++---------- src/board/rank.rs | 20 ++++++++++---------- src/board/square.rs | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/board/file.rs b/src/board/file.rs index fa7ba15..1601397 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -15,19 +15,19 @@ 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, + const ALL: [Self; 8] = [ + 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() } diff --git a/src/board/rank.rs b/src/board/rank.rs index 59374c7..c679278 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -15,19 +15,19 @@ 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, + const ALL: [Self; 8] = [ + 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() } diff --git a/src/board/square.rs b/src/board/square.rs index 69d8167..9c0178e 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -43,7 +43,7 @@ impl Square { } /// Iterate over all squares in order. - pub fn iter() -> impl Iterator { + pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() } From 42126e1c8804edad2998ef829ff507d76afa6c06 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:19:38 +0200 Subject: [PATCH 079/157] Add 'NUM_VARIANTS' constant to all 'board' enums --- src/board/castle_rights.rs | 5 ++++- src/board/color.rs | 5 ++++- src/board/file.rs | 7 +++++-- src/board/rank.rs | 7 +++++-- src/board/square.rs | 7 +++++-- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index b398b57..01f0235 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -14,10 +14,13 @@ 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) } } diff --git a/src/board/color.rs b/src/board/color.rs index d5c66d3..62fdd13 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -8,10 +8,13 @@ pub enum Color { } impl Color { + /// The number of [Color] variants. + pub const NUM_VARIANTS: usize = 2; + /// 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) } } diff --git a/src/board/file.rs b/src/board/file.rs index 1601397..1475e9a 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -15,7 +15,10 @@ pub enum File { } impl File { - const ALL: [Self; 8] = [ + /// The number of [File] variants. + pub const NUM_VARIANTS: usize = 8; + + const ALL: [Self; Self::NUM_VARIANTS] = [ Self::A, Self::B, Self::C, @@ -34,7 +37,7 @@ impl File { /// 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) } } diff --git a/src/board/rank.rs b/src/board/rank.rs index c679278..f448df5 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -15,7 +15,10 @@ pub enum Rank { } impl Rank { - const ALL: [Self; 8] = [ + /// The number of [Rank] variants. + pub const NUM_VARIANTS: usize = 8; + + const ALL: [Self; Self::NUM_VARIANTS] = [ Self::First, Self::Second, Self::Third, @@ -34,7 +37,7 @@ impl Rank { /// 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) } } diff --git a/src/board/square.rs b/src/board/square.rs index 9c0178e..c164320 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -23,8 +23,11 @@ impl std::fmt::Display for Square { } 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, @@ -50,7 +53,7 @@ impl Square { /// 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) } } From 1ea4cd0ea44bd9f1160f58f0b0b777e95f8be9d5 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:07:16 +0200 Subject: [PATCH 080/157] Add 'Piece' enum --- src/board/mod.rs | 3 ++ src/board/piece.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/board/piece.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index ad91192..da449df 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -13,6 +13,9 @@ pub use direction::*; pub mod file; pub use file::*; +pub mod piece; +pub use piece::*; + pub mod rank; pub use rank::*; 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); + } +} From 34b9c18ca5d5758ad65a5d8728649ee9fbf6f858 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:25:29 +0200 Subject: [PATCH 081/157] Add 'Bitboard::iter_powerset' --- src/board/bitboard/mod.rs | 86 +++++++++++++++++++++++++++++++++- src/board/bitboard/superset.rs | 46 ++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/board/bitboard/superset.rs diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 8b716be..377bbf1 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -3,6 +3,8 @@ use crate::utils::static_assert; 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 +65,15 @@ impl Bitboard { pub fn is_empty(self) -> bool { self == Self::EMPTY } + + /// 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. @@ -196,8 +207,10 @@ impl std::ops::Sub for Bitboard { #[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 +293,75 @@ 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 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 + ); + } } 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 {} From a4952376a160212b0e9680d4187a85579df5a9e6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:43:18 +0200 Subject: [PATCH 082/157] Remove spurious links in 'Bitboard' documentation --- src/board/bitboard/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 377bbf1..bccbbfa 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -85,7 +85,7 @@ 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; @@ -135,7 +135,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; @@ -155,7 +155,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; @@ -175,7 +175,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; @@ -195,7 +195,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; From 7ccca5a5931749e2c757cfe72ac5cf71571408c6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:44:33 +0200 Subject: [PATCH 083/157] Remove spurious links in 'Square' documentation --- src/board/square.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/square.rs b/src/board/square.rs index c164320..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 { From 064846a4fd0000cad59e39a36deab9dfe34ccb13 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:22:37 +0200 Subject: [PATCH 084/157] Add naive king move generation --- src/lib.rs | 1 + src/movegen/king.rs | 225 ++++++++++++++++++++++++++++++++++++++++++++ src/movegen/mod.rs | 2 + 3 files changed, 228 insertions(+) create mode 100644 src/movegen/king.rs create mode 100644 src/movegen/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 3593172..bfcf0bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ pub mod board; +pub mod movegen; pub mod utils; diff --git a/src/movegen/king.rs b/src/movegen/king.rs new file mode 100644 index 0000000..ce99b25 --- /dev/null +++ b/src/movegen/king.rs @@ -0,0 +1,225 @@ +use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square}; + +/// Compute a king's movement. No castling moves included +#[allow(unused)] +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) +} + +/// Compute a king's castling moves, given its [Color] and [CastleRights]. +#[allow(unused)] +pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard { + let rank = color.first_rank(); + + let king_side_square = Square::new(File::G, rank); + let queen_side_square = Square::new(File::C, rank); + + match castle_rights { + CastleRights::NoSide => Bitboard::EMPTY, + CastleRights::KingSide => king_side_square.into_bitboard(), + CastleRights::QueenSide => queen_side_square.into_bitboard(), + CastleRights::BothSides => king_side_square | queen_side_square, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[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 + ); + } + + #[test] + fn castling_moves() { + assert_eq!( + king_castling_moves(Color::White, CastleRights::NoSide), + Bitboard::EMPTY + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::NoSide), + Bitboard::EMPTY + ); + assert_eq!( + king_castling_moves(Color::White, CastleRights::KingSide), + Square::G1.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::KingSide), + Square::G8.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::White, CastleRights::QueenSide), + Square::C1.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::QueenSide), + Square::C8.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::White, CastleRights::BothSides), + Square::C1 | Square::G1 + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::BothSides), + Square::C8 | Square::G8 + ); + } +} diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs new file mode 100644 index 0000000..bca1bf7 --- /dev/null +++ b/src/movegen/mod.rs @@ -0,0 +1,2 @@ +// Move generation implementation details +mod king; From 0c4b01138698011908933c3101ab706fae35a898 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:30:16 +0200 Subject: [PATCH 085/157] Add naive knight move generation --- src/movegen/knight.rs | 184 ++++++++++++++++++++++++++++++++++++++++++ src/movegen/mod.rs | 1 + 2 files changed, 185 insertions(+) create mode 100644 src/movegen/knight.rs diff --git a/src/movegen/knight.rs b/src/movegen/knight.rs new file mode 100644 index 0000000..5cc4fc9 --- /dev/null +++ b/src/movegen/knight.rs @@ -0,0 +1,184 @@ +use crate::board::{Bitboard, Direction, Square}; + +/// Compute a knight's movement. +#[allow(unused)] +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/mod.rs b/src/movegen/mod.rs index bca1bf7..35193b2 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,2 +1,3 @@ // Move generation implementation details mod king; +mod knight; From f7fc30227a9fdf000f75bb61024b543766fddf0f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:33:33 +0200 Subject: [PATCH 086/157] Add naive bishop move generation --- src/movegen/bishop.rs | 70 +++++++++++++++++++++++++++++++++++++++++++ src/movegen/mod.rs | 1 + 2 files changed, 71 insertions(+) create mode 100644 src/movegen/bishop.rs diff --git a/src/movegen/bishop.rs b/src/movegen/bishop.rs new file mode 100644 index 0000000..9409cb8 --- /dev/null +++ b/src/movegen/bishop.rs @@ -0,0 +1,70 @@ +use crate::board::{Bitboard, Direction, Square}; + +/// Compute a bishop's movement given a set of blockers that cannot be moved past. +#[allow(unused)] +pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { + Direction::iter_bishop() + .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) + .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/mod.rs b/src/movegen/mod.rs index 35193b2..aacfcb4 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,3 +1,4 @@ // Move generation implementation details +mod bishop; mod king; mod knight; From b1a460a5804d89acf01b448e59370fcd49232ca2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:33:45 +0200 Subject: [PATCH 087/157] Add naive rook move generation --- src/movegen/mod.rs | 1 + src/movegen/rook.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/movegen/rook.rs diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index aacfcb4..9f4f280 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -2,3 +2,4 @@ mod bishop; mod king; mod knight; +mod rook; diff --git a/src/movegen/rook.rs b/src/movegen/rook.rs new file mode 100644 index 0000000..2dbacd9 --- /dev/null +++ b/src/movegen/rook.rs @@ -0,0 +1,55 @@ +use crate::board::{Bitboard, Direction, Square}; + +/// Compute a rook's movement given a set of blockers that cannot be moved past. +#[allow(unused)] +pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { + Direction::iter_rook() + .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) + .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 + ); + } +} From 3eb140b7574beb4f9a0d565021f0cb69eb04be7e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:36:28 +0200 Subject: [PATCH 088/157] Add naive pawn move generation --- src/movegen/mod.rs | 1 + src/movegen/pawn.rs | 139 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 src/movegen/pawn.rs diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 9f4f280..746011d 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -2,4 +2,5 @@ mod bishop; mod king; mod knight; +mod pawn; mod rook; diff --git a/src/movegen/pawn.rs b/src/movegen/pawn.rs new file mode 100644 index 0000000..53551c5 --- /dev/null +++ b/src/movegen/pawn.rs @@ -0,0 +1,139 @@ +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. +#[allow(unused)] +pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { + return Bitboard::EMPTY; + } + + 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. +#[allow(unused)] +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()); + } +} From 55d9780156253a69bdf8b866ef9e73145b6b1124 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:14:46 +0200 Subject: [PATCH 089/157] Add 'Magic' type --- src/movegen/magic/mod.rs | 23 +++++++++++++++++++++++ src/movegen/mod.rs | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 src/movegen/magic/mod.rs diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs new file mode 100644 index 0000000..43acd99 --- /dev/null +++ b/src/movegen/magic/mod.rs @@ -0,0 +1,23 @@ +use crate::board::Bitboard; + +/// A type representing the magic board indexing a given [crate::board::Square]. +pub struct Magic { + /// Magic number. + magic: u64, + /// Base offset into the magic square table. + offset: usize, + /// Mask to apply to the blocker board before applying the magic. + mask: Bitboard, + /// Length of the resulting mask after applying the magic. + shift: u8, +} + +impl Magic { + /// Compute the index into the magics database for this set of `blockers`. + #[allow(unused)] // FIXME: remove once used + pub fn get_index(&self, blockers: Bitboard) -> usize { + let relevant_occupancy = (blockers & self.mask).0; + let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; + base_index + self.offset + } +} diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 746011d..d379194 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,3 +1,7 @@ +// Magic bitboard +pub mod magic; +pub use magic::*; + // Move generation implementation details mod bishop; mod king; From f1cdec2b5e8883fa2db7c9010c3d3d979329da1a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:37:56 +0200 Subject: [PATCH 090/157] Add magic mask generation --- src/movegen/mod.rs | 3 +++ src/movegen/wizardry/mask.rs | 41 ++++++++++++++++++++++++++++++++++++ src/movegen/wizardry/mod.rs | 1 + 3 files changed, 45 insertions(+) create mode 100644 src/movegen/wizardry/mask.rs create mode 100644 src/movegen/wizardry/mod.rs diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index d379194..3d22eb0 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -8,3 +8,6 @@ mod king; mod knight; mod pawn; mod rook; + +// Magic bitboard generation +mod wizardry; diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs new file mode 100644 index 0000000..af1b8fa --- /dev/null +++ b/src/movegen/wizardry/mask.rs @@ -0,0 +1,41 @@ +use crate::board::{Bitboard, File, Rank, Square}; +use crate::movegen::bishop::bishop_moves; +use crate::movegen::rook::rook_moves; + +/// Compute the relevancy mask for a bishop on a given [Square]. +#[allow(unused)] // FIXME: remove once used +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]. +#[allow(unused)] // FIXME: remove once used +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 = mask | File::A.into_bitboard() + }; + if square.file() != File::H { + mask = mask | File::H.into_bitboard() + }; + if square.rank() != Rank::First { + mask = mask | Rank::First.into_bitboard() + }; + if square.rank() != Rank::Eighth { + mask = 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..f6053c6 --- /dev/null +++ b/src/movegen/wizardry/mod.rs @@ -0,0 +1 @@ +mod mask; From 3410ba518e9cd7adfbe02a934288b0e6aa6f3652 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:41:09 +0200 Subject: [PATCH 091/157] Make 'Magic' fields 'pub(crate)' --- src/movegen/magic/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs index 43acd99..f9d01d1 100644 --- a/src/movegen/magic/mod.rs +++ b/src/movegen/magic/mod.rs @@ -3,13 +3,13 @@ use crate::board::Bitboard; /// A type representing the magic board indexing a given [crate::board::Square]. pub struct Magic { /// Magic number. - magic: u64, + pub(crate) magic: u64, /// Base offset into the magic square table. - offset: usize, + pub(crate) offset: usize, /// Mask to apply to the blocker board before applying the magic. - mask: Bitboard, + pub(crate) mask: Bitboard, /// Length of the resulting mask after applying the magic. - shift: u8, + pub(crate) shift: u8, } impl Magic { From d268db38c1b53bad0e2af33bf0e188ff3c40db79 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:42:05 +0200 Subject: [PATCH 092/157] Add magic bitboard generation --- src/movegen/magic/mod.rs | 1 - src/movegen/wizardry/generation.rs | 74 ++++++++++++++++++++++++++++++ src/movegen/wizardry/mask.rs | 2 - src/movegen/wizardry/mod.rs | 1 + 4 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/movegen/wizardry/generation.rs diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs index f9d01d1..242a0b4 100644 --- a/src/movegen/magic/mod.rs +++ b/src/movegen/magic/mod.rs @@ -14,7 +14,6 @@ pub struct Magic { impl Magic { /// Compute the index into the magics database for this set of `blockers`. - #[allow(unused)] // FIXME: remove once used pub fn get_index(&self, blockers: Bitboard) -> usize { let relevant_occupancy = (blockers & self.mask).0; let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs new file mode 100644 index 0000000..a5e47e2 --- /dev/null +++ b/src/movegen/wizardry/generation.rs @@ -0,0 +1,74 @@ +use crate::board::{Bitboard, Square}; +use crate::movegen::bishop::bishop_moves; +use crate::movegen::rook::rook_moves; +use crate::movegen::Magic; + +use super::mask::{generate_bishop_mask, generate_rook_mask}; + +/// A trait to represent RNG for u64 values. +#[allow(unused)] // FIXME: remove when used +pub(crate) trait RandGen { + fn gen(&mut self) -> u64; +} + +type MagicGenerationType = (Vec, Vec); + +#[allow(unused)] // FIXME: remove when used +pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType { + generate_magics(rng, generate_bishop_mask, bishop_moves) +} + +#[allow(unused)] // FIXME: remove when used +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 index af1b8fa..aca9f4f 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -3,7 +3,6 @@ use crate::movegen::bishop::bishop_moves; use crate::movegen::rook::rook_moves; /// Compute the relevancy mask for a bishop on a given [Square]. -#[allow(unused)] // FIXME: remove once used pub fn generate_bishop_mask(square: Square) -> Bitboard { let rays = bishop_moves(square, Bitboard::EMPTY); @@ -16,7 +15,6 @@ pub fn generate_bishop_mask(square: Square) -> Bitboard { } /// Compute the relevancy mask for a rook on a given [Square]. -#[allow(unused)] // FIXME: remove once used pub fn generate_rook_mask(square: Square) -> Bitboard { let rays = rook_moves(square, Bitboard::EMPTY); diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index f6053c6..8b5ba4e 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1 +1,2 @@ +mod generation; mod mask; From 5b56fcfcda45b66d15bf86daa16baef82354801e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 23 Jul 2022 15:32:56 +0200 Subject: [PATCH 093/157] Add 'Color::iter' --- src/board/color.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/board/color.rs b/src/board/color.rs index 62fdd13..f909aca 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -11,6 +11,13 @@ impl Color { /// The number of [Color] variants. pub const NUM_VARIANTS: usize = 2; + const ALL: [Self; Self::NUM_VARIANTS] = [Self::White, Self::Black]; + + /// Iterate over all colors in order. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + /// Convert from a color index into a [Color] type. #[inline(always)] pub fn from_index(index: usize) -> Self { From 9601a5657afc7edd02c1fb4c283eb4d9ed9dea8b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 13:40:01 +0200 Subject: [PATCH 094/157] Make all modules at least 'pub(crate)' --- src/movegen/mod.rs | 12 ++++++------ src/movegen/wizardry/mod.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 3d22eb0..26b60a3 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -3,11 +3,11 @@ pub mod magic; pub use magic::*; // Move generation implementation details -mod bishop; -mod king; -mod knight; -mod pawn; -mod rook; +pub(crate) mod bishop; +pub(crate) mod king; +pub(crate) mod knight; +pub(crate) mod pawn; +pub(crate) mod rook; // Magic bitboard generation -mod wizardry; +pub(crate) mod wizardry; diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 8b5ba4e..dfd732d 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,2 +1,2 @@ -mod generation; +pub(crate) mod generation; mod mask; From a4fd4f5cc47b0799796ac17f15f4e49f1484517e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:24:35 +0200 Subject: [PATCH 095/157] Move naive move generation into sub-module --- src/movegen/mod.rs | 8 ++--- src/movegen/{ => naive}/bishop.rs | 0 src/movegen/{ => naive}/king.rs | 54 +----------------------------- src/movegen/{ => naive}/knight.rs | 0 src/movegen/naive/mod.rs | 14 ++++++++ src/movegen/{ => naive}/pawn.rs | 0 src/movegen/{ => naive}/rook.rs | 0 src/movegen/wizardry/generation.rs | 3 +- src/movegen/wizardry/mask.rs | 3 +- 9 files changed, 19 insertions(+), 63 deletions(-) rename src/movegen/{ => naive}/bishop.rs (100%) rename src/movegen/{ => naive}/king.rs (74%) rename src/movegen/{ => naive}/knight.rs (100%) create mode 100644 src/movegen/naive/mod.rs rename src/movegen/{ => naive}/pawn.rs (100%) rename src/movegen/{ => naive}/rook.rs (100%) diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 26b60a3..9ddbf36 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -2,12 +2,8 @@ pub mod magic; pub use magic::*; -// Move generation implementation details -pub(crate) mod bishop; -pub(crate) mod king; -pub(crate) mod knight; -pub(crate) mod pawn; -pub(crate) mod rook; +// Naive move generation +pub mod naive; // Magic bitboard generation pub(crate) mod wizardry; diff --git a/src/movegen/bishop.rs b/src/movegen/naive/bishop.rs similarity index 100% rename from src/movegen/bishop.rs rename to src/movegen/naive/bishop.rs diff --git a/src/movegen/king.rs b/src/movegen/naive/king.rs similarity index 74% rename from src/movegen/king.rs rename to src/movegen/naive/king.rs index ce99b25..9080667 100644 --- a/src/movegen/king.rs +++ b/src/movegen/naive/king.rs @@ -1,4 +1,4 @@ -use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square}; +use crate::board::{Bitboard, Direction, Square}; /// Compute a king's movement. No castling moves included #[allow(unused)] @@ -10,22 +10,6 @@ pub fn king_moves(square: Square) -> Bitboard { .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) } -/// Compute a king's castling moves, given its [Color] and [CastleRights]. -#[allow(unused)] -pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard { - let rank = color.first_rank(); - - let king_side_square = Square::new(File::G, rank); - let queen_side_square = Square::new(File::C, rank); - - match castle_rights { - CastleRights::NoSide => Bitboard::EMPTY, - CastleRights::KingSide => king_side_square.into_bitboard(), - CastleRights::QueenSide => queen_side_square.into_bitboard(), - CastleRights::BothSides => king_side_square | queen_side_square, - } -} - #[cfg(test)] mod test { use super::*; @@ -186,40 +170,4 @@ mod test { | Square::F6 ); } - - #[test] - fn castling_moves() { - assert_eq!( - king_castling_moves(Color::White, CastleRights::NoSide), - Bitboard::EMPTY - ); - assert_eq!( - king_castling_moves(Color::Black, CastleRights::NoSide), - Bitboard::EMPTY - ); - assert_eq!( - king_castling_moves(Color::White, CastleRights::KingSide), - Square::G1.into_bitboard() - ); - assert_eq!( - king_castling_moves(Color::Black, CastleRights::KingSide), - Square::G8.into_bitboard() - ); - assert_eq!( - king_castling_moves(Color::White, CastleRights::QueenSide), - Square::C1.into_bitboard() - ); - assert_eq!( - king_castling_moves(Color::Black, CastleRights::QueenSide), - Square::C8.into_bitboard() - ); - assert_eq!( - king_castling_moves(Color::White, CastleRights::BothSides), - Square::C1 | Square::G1 - ); - assert_eq!( - king_castling_moves(Color::Black, CastleRights::BothSides), - Square::C8 | Square::G8 - ); - } } diff --git a/src/movegen/knight.rs b/src/movegen/naive/knight.rs similarity index 100% rename from src/movegen/knight.rs rename to src/movegen/naive/knight.rs 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/pawn.rs b/src/movegen/naive/pawn.rs similarity index 100% rename from src/movegen/pawn.rs rename to src/movegen/naive/pawn.rs diff --git a/src/movegen/rook.rs b/src/movegen/naive/rook.rs similarity index 100% rename from src/movegen/rook.rs rename to src/movegen/naive/rook.rs diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs index a5e47e2..23da62a 100644 --- a/src/movegen/wizardry/generation.rs +++ b/src/movegen/wizardry/generation.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Square}; -use crate::movegen::bishop::bishop_moves; -use crate::movegen::rook::rook_moves; +use crate::movegen::naive::{bishop_moves, rook_moves}; use crate::movegen::Magic; use super::mask::{generate_bishop_mask, generate_rook_mask}; diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs index aca9f4f..5a6c56e 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, File, Rank, Square}; -use crate::movegen::bishop::bishop_moves; -use crate::movegen::rook::rook_moves; +use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves}; /// Compute the relevancy mask for a bishop on a given [Square]. pub fn generate_bishop_mask(square: Square) -> Bitboard { From 2254830ddc3da0733d22e6370f25ad4ea766a661 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 01:43:27 +0000 Subject: [PATCH 096/157] Move 'Magic' to 'wizardry' submodule --- src/movegen/magic/mod.rs | 22 ---------------------- src/movegen/mod.rs | 4 ---- src/movegen/wizardry/generation.rs | 2 +- src/movegen/wizardry/mod.rs | 23 +++++++++++++++++++++++ 4 files changed, 24 insertions(+), 27 deletions(-) delete mode 100644 src/movegen/magic/mod.rs diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs deleted file mode 100644 index 242a0b4..0000000 --- a/src/movegen/magic/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::board::Bitboard; - -/// A type representing the magic board indexing a given [crate::board::Square]. -pub struct Magic { - /// Magic number. - pub(crate) magic: u64, - /// Base offset into the magic square table. - pub(crate) offset: usize, - /// Mask to apply to the blocker board before applying the magic. - pub(crate) mask: Bitboard, - /// Length of the resulting mask after applying the magic. - pub(crate) shift: u8, -} - -impl Magic { - /// 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 - } -} diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 9ddbf36..50262d2 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,7 +1,3 @@ -// Magic bitboard -pub mod magic; -pub use magic::*; - // Naive move generation pub mod naive; diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs index 23da62a..c7c73dd 100644 --- a/src/movegen/wizardry/generation.rs +++ b/src/movegen/wizardry/generation.rs @@ -1,8 +1,8 @@ use crate::board::{Bitboard, Square}; use crate::movegen::naive::{bishop_moves, rook_moves}; -use crate::movegen::Magic; use super::mask::{generate_bishop_mask, generate_rook_mask}; +use super::Magic; /// A trait to represent RNG for u64 values. #[allow(unused)] // FIXME: remove when used diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index dfd732d..2405710 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,2 +1,25 @@ pub(crate) mod generation; mod mask; + +use crate::board::Bitboard; + +/// A type representing the magic board indexing a given [crate::board::Square]. +pub struct Magic { + /// Magic number. + pub(crate) magic: u64, + /// Base offset into the magic square table. + pub(crate) offset: usize, + /// Mask to apply to the blocker board before applying the magic. + pub(crate) mask: Bitboard, + /// Length of the resulting mask after applying the magic. + pub(crate) shift: u8, +} + +impl Magic { + /// 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 + } +} From 01966d1015078bce74299a2a14d1a6541fc08874 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 02:54:24 +0000 Subject: [PATCH 097/157] Add missing derives to 'Magic' --- src/movegen/wizardry/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 2405710..eb84fa9 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -4,6 +4,7 @@ mod mask; use crate::board::Bitboard; /// A type representing the magic board indexing a given [crate::board::Square]. +#[derive(Clone, Debug)] pub struct Magic { /// Magic number. pub(crate) magic: u64, From fd3da78224ce86010ed78a8fd6ca8c3263521679 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 02:55:03 +0000 Subject: [PATCH 098/157] Make 'Magic' 'pub(crate)' --- src/movegen/wizardry/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index eb84fa9..0727293 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -5,7 +5,7 @@ use crate::board::Bitboard; /// A type representing the magic board indexing a given [crate::board::Square]. #[derive(Clone, Debug)] -pub struct Magic { +pub(crate) struct Magic { /// Magic number. pub(crate) magic: u64, /// Base offset into the magic square table. From 6cc0380b02109b8b16c5e3b0faef9f8d210ca139 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 02:56:01 +0000 Subject: [PATCH 099/157] Add 'MagicMoves' --- src/movegen/wizardry/mod.rs | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 0727293..5fb8af1 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,7 +1,7 @@ pub(crate) mod generation; mod mask; -use crate::board::Bitboard; +use crate::board::{Bitboard, Square}; /// A type representing the magic board indexing a given [crate::board::Square]. #[derive(Clone, Debug)] @@ -24,3 +24,36 @@ impl Magic { base_index + self.offset } } + +/// A type encapsulating a database of [Magic] bitboard moves. +#[derive(Clone, Debug)] +#[allow(unused)] // FIXME: remove when used +pub(crate) struct MagicMoves { + magics: Vec, + moves: Vec, +} + +#[allow(unused)] // FIXME: remove when used +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) + } + } +} From 8fe444c59a7aacf0109712e5dc6952588454ec15 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 11:55:59 +0100 Subject: [PATCH 100/157] Expose magic bitboard generation to parent module --- src/movegen/wizardry/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 5fb8af1..6ee6bd0 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,4 +1,5 @@ -pub(crate) mod generation; +mod generation; +pub(crate) use generation::*; mod mask; use crate::board::{Bitboard, Square}; From 9cf5fc38528e32a1689b883be3066e17ec26ff22 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 12:00:57 +0100 Subject: [PATCH 101/157] Add bitboard-based move generation --- src/movegen/mod.rs | 4 ++ src/movegen/moves.rs | 135 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 src/movegen/moves.rs diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 50262d2..8b5be56 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -3,3 +3,7 @@ pub mod naive; // Magic bitboard generation pub(crate) 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..7e40a18 --- /dev/null +++ b/src/movegen/moves.rs @@ -0,0 +1,135 @@ +use std::sync::OnceLock; + +use crate::{ + board::{Bitboard, Color, File, Square}, + movegen::{ + naive, + wizardry::{generate_bishop_magics, generate_rook_magics, MagicMoves, RandGen}, + }, +}; + +// 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 + } +} + +/// 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 SimpleRng::new()); + // 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 SimpleRng::new()); + // 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) +} From 6feca16b612167c3436ac4c4964ff6ee612e5251 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 12:08:34 +0100 Subject: [PATCH 102/157] Add 'BitboardIterator::new' --- src/board/bitboard/iterator.rs | 12 ++++++++++-- src/board/bitboard/mod.rs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) 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 bccbbfa..9ef0348 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -91,7 +91,7 @@ impl IntoIterator for Bitboard { type Item = Square; fn into_iter(self) -> Self::IntoIter { - BitboardIterator(self.0) + BitboardIterator::new(self) } } From be506747f8c2d19917fe4fb923b46062ee719f58 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 12:09:16 +0100 Subject: [PATCH 103/157] Tighten item visibilities --- src/movegen/mod.rs | 4 ++-- src/movegen/wizardry/mod.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 8b5be56..f9ce658 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,8 +1,8 @@ // Naive move generation -pub mod naive; +mod naive; // Magic bitboard generation -pub(crate) mod wizardry; +mod wizardry; // Magic bitboard definitions mod moves; diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 6ee6bd0..6ed82d7 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,20 +1,20 @@ mod generation; -pub(crate) use 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(crate) struct Magic { +pub(super) struct Magic { /// Magic number. - pub(crate) magic: u64, + pub(self) magic: u64, /// Base offset into the magic square table. - pub(crate) offset: usize, + pub(self) offset: usize, /// Mask to apply to the blocker board before applying the magic. - pub(crate) mask: Bitboard, + pub(self) mask: Bitboard, /// Length of the resulting mask after applying the magic. - pub(crate) shift: u8, + pub(self) shift: u8, } impl Magic { From 4de41a5544f96058737846b8a838bcc8dd591ed9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:43:18 +0200 Subject: [PATCH 104/157] Remove all useless 'allow(unused)' --- src/movegen/naive/bishop.rs | 1 - src/movegen/naive/king.rs | 1 - src/movegen/naive/knight.rs | 1 - src/movegen/naive/pawn.rs | 2 -- src/movegen/naive/rook.rs | 1 - src/movegen/wizardry/generation.rs | 3 --- src/movegen/wizardry/mod.rs | 2 -- 7 files changed, 11 deletions(-) diff --git a/src/movegen/naive/bishop.rs b/src/movegen/naive/bishop.rs index 9409cb8..7a2c97f 100644 --- a/src/movegen/naive/bishop.rs +++ b/src/movegen/naive/bishop.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, Direction, Square}; /// Compute a bishop's movement given a set of blockers that cannot be moved past. -#[allow(unused)] pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { Direction::iter_bishop() .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) diff --git a/src/movegen/naive/king.rs b/src/movegen/naive/king.rs index 9080667..fdbedb7 100644 --- a/src/movegen/naive/king.rs +++ b/src/movegen/naive/king.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, Direction, Square}; /// Compute a king's movement. No castling moves included -#[allow(unused)] pub fn king_moves(square: Square) -> Bitboard { let board = square.into_bitboard(); diff --git a/src/movegen/naive/knight.rs b/src/movegen/naive/knight.rs index 5cc4fc9..28ad7f2 100644 --- a/src/movegen/naive/knight.rs +++ b/src/movegen/naive/knight.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, Direction, Square}; /// Compute a knight's movement. -#[allow(unused)] pub fn knight_moves(square: Square) -> Bitboard { let board = square.into_bitboard(); diff --git a/src/movegen/naive/pawn.rs b/src/movegen/naive/pawn.rs index 53551c5..bde5215 100644 --- a/src/movegen/naive/pawn.rs +++ b/src/movegen/naive/pawn.rs @@ -1,7 +1,6 @@ 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. -#[allow(unused)] pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { return Bitboard::EMPTY; @@ -24,7 +23,6 @@ pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard } /// Computes the set of squares a pawn can capture, given its color. -#[allow(unused)] pub fn pawn_captures(color: Color, square: Square) -> Bitboard { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { return Bitboard::EMPTY; diff --git a/src/movegen/naive/rook.rs b/src/movegen/naive/rook.rs index 2dbacd9..e61f5ec 100644 --- a/src/movegen/naive/rook.rs +++ b/src/movegen/naive/rook.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, Direction, Square}; /// Compute a rook's movement given a set of blockers that cannot be moved past. -#[allow(unused)] pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { Direction::iter_rook() .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs index c7c73dd..0322977 100644 --- a/src/movegen/wizardry/generation.rs +++ b/src/movegen/wizardry/generation.rs @@ -5,19 +5,16 @@ use super::mask::{generate_bishop_mask, generate_rook_mask}; use super::Magic; /// A trait to represent RNG for u64 values. -#[allow(unused)] // FIXME: remove when used pub(crate) trait RandGen { fn gen(&mut self) -> u64; } type MagicGenerationType = (Vec, Vec); -#[allow(unused)] // FIXME: remove when used pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType { generate_magics(rng, generate_bishop_mask, bishop_moves) } -#[allow(unused)] // FIXME: remove when used pub fn generate_rook_magics(rng: &mut dyn RandGen) -> MagicGenerationType { generate_magics(rng, generate_rook_mask, rook_moves) } diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 6ed82d7..83f4d69 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -28,13 +28,11 @@ impl Magic { /// A type encapsulating a database of [Magic] bitboard moves. #[derive(Clone, Debug)] -#[allow(unused)] // FIXME: remove when used pub(crate) struct MagicMoves { magics: Vec, moves: Vec, } -#[allow(unused)] // FIXME: remove when used impl MagicMoves { /// Initialize a new [MagicMoves] given a matching list of [Magic] and its corresponding moves /// as a [Bitboard]. From 54d2e78954730e27bee2300aed3f746967a30709 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 17:34:11 +0200 Subject: [PATCH 105/157] Add '*Assign' operators to 'Bitboard' --- src/board/bitboard/mod.rs | 80 ++++++++++++++++++++++++++++++++++++ src/board/direction.rs | 2 +- src/movegen/wizardry/mask.rs | 8 ++-- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 9ef0348..0c06625 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -115,6 +115,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; @@ -145,6 +161,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; @@ -165,6 +197,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; @@ -185,6 +233,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; @@ -205,6 +269,22 @@ 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; diff --git a/src/board/direction.rs b/src/board/direction.rs index 324f97c..40c8d69 100644 --- a/src/board/direction.rs +++ b/src/board/direction.rs @@ -139,7 +139,7 @@ impl Direction { while !board.is_empty() { board = self.move_board(board); - res = res | board; + res |= board; if !(board & blockers).is_empty() { break; } diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs index 5a6c56e..865c986 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -20,16 +20,16 @@ pub fn generate_rook_mask(square: Square) -> Bitboard { let mask = { let mut mask = Bitboard::EMPTY; if square.file() != File::A { - mask = mask | File::A.into_bitboard() + mask |= File::A.into_bitboard() }; if square.file() != File::H { - mask = mask | File::H.into_bitboard() + mask |= File::H.into_bitboard() }; if square.rank() != Rank::First { - mask = mask | Rank::First.into_bitboard() + mask |= Rank::First.into_bitboard() }; if square.rank() != Rank::Eighth { - mask = mask | Rank::Eighth.into_bitboard() + mask |= Rank::Eighth.into_bitboard() }; mask }; From 80ea35babe25cfc75332be4c226f667a7794466c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:26:39 +0200 Subject: [PATCH 106/157] Add 'CastleRights::with_{king,queen}_side' --- src/board/castle_rights.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 01f0235..b34d952 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -53,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 { From be5e9722de5e581978cef994899f2db1056c1712 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:38:51 +0200 Subject: [PATCH 107/157] Add 'Color::third_rank' --- src/board/color.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/board/color.rs b/src/board/color.rs index f909aca..66b21b3 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -60,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)] From 19d0e1bd34e2d244c72e3d9b8bca358272483379 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 11:00:23 +0200 Subject: [PATCH 108/157] Add 'Bitboard::has_more_than_one' --- src/board/bitboard/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 0c06625..e186a07 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -66,6 +66,13 @@ impl Bitboard { self == Self::EMPTY } + /// Return true if there are more than piece in the [Bitboard]. This is faster than testing + /// `board.count() > 1`. + #[inline(always)] + pub fn has_more_than_one(self) -> bool { + (self.0 & (self.0.wrapping_sub(1))) != 0 + } + /// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of /// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that /// contain all, some, or none of the [Square] that are in the given [Bitboard]. @@ -374,6 +381,16 @@ mod test { assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1)); } + #[test] + fn more_than_one() { + assert!(!Bitboard::EMPTY.has_more_than_one()); + for square in Square::iter() { + assert!(!square.into_bitboard().has_more_than_one()) + } + assert!((Square::A1 | Square::H8).has_more_than_one()); + assert!(Bitboard::ALL.has_more_than_one()); + } + #[test] fn iter_power_set_empty() { assert_eq!( From 856c3a873d947d640dd94c857d81b63c314d17c6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 15:56:08 +0100 Subject: [PATCH 109/157] Deny warnings in 'clippy' pre-commit hook --- flake.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flake.nix b/flake.nix index 134bcdc..ca06a36 100644 --- a/flake.nix +++ b/flake.nix @@ -66,6 +66,9 @@ hooks = { clippy = { enable = true; + settings = { + denyWarnings = true; + }; }; nixpkgs-fmt = { From ddc8ecc4746d3723c0766f7baa5997f244b13d6c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 19:12:51 +0200 Subject: [PATCH 110/157] Add 'Move' --- src/board/mod.rs | 3 + src/board/move.rs | 232 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 src/board/move.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index da449df..d23ef25 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -13,6 +13,9 @@ pub use direction::*; pub mod file; pub use file::*; +pub mod r#move; +pub use r#move::*; + pub mod piece; pub use piece::*; 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()); + } +} From a7ed24c752ce861491fc4032a625aea18026e9a9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:02:23 +0100 Subject: [PATCH 111/157] Add 'Color' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 2bfce31..995a05c 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -1,3 +1,5 @@ +import enum + import gdb.printing @@ -44,6 +46,19 @@ class Bitboard(object): n ^= b +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 SquarePrinter(object): "Print a seer::board::square::Square" @@ -64,11 +79,22 @@ class BitboardPrinter(object): return "Bitboard{" + str(self._val)[1:-1] + "}" +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) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter) pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) + pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter) return pp From ca68ccf92c6e1237807f82915385384838f4f628 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:50:49 +0100 Subject: [PATCH 112/157] Add 'File' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 995a05c..78efca0 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -59,6 +59,25 @@ class Color(enum.IntEnum): 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 SquarePrinter(object): "Print a seer::board::square::Square" @@ -89,12 +108,23 @@ class ColorPrinter(object): 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) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter) pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter) + pp.add_printer('File', '^seer::board::file::File$', FilePrinter) return pp From 1a71854589528550219cc0ceb0cbee9657235e76 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:50:49 +0100 Subject: [PATCH 113/157] Add 'Rank' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 78efca0..aec66d6 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -78,6 +78,25 @@ class File(enum.IntEnum): 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 SquarePrinter(object): "Print a seer::board::square::Square" @@ -118,6 +137,16 @@ class FilePrinter(object): 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) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') @@ -125,6 +154,7 @@ def build_pretty_printer(): pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) 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) return pp From e1a15328728d9fdf797db82c6ebdc0bbe00c3242 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:02:23 +0100 Subject: [PATCH 114/157] Add 'Piece' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index aec66d6..523c52f 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -97,6 +97,23 @@ class Rank(enum.IntEnum): 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 SquarePrinter(object): "Print a seer::board::square::Square" @@ -147,6 +164,16 @@ class RankPrinter(object): 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) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') @@ -155,6 +182,7 @@ def build_pretty_printer(): 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) return pp From a26c53201515c9af63e408ed04d9394af4747471 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:02:23 +0100 Subject: [PATCH 115/157] Add 'Move' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 523c52f..1072cb8 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -114,6 +114,89 @@ class Piece(enum.IntEnum): 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 SquarePrinter(object): "Print a seer::board::square::Square" @@ -174,6 +257,16 @@ class PiecePrinter(object): 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) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') @@ -183,6 +276,7 @@ def build_pretty_printer(): 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 From 43fb428749daf83f4f75b1bca90c3772a73a06b9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:25:35 +0200 Subject: [PATCH 116/157] Add 'FromFen' trait --- src/fen.rs | 6 ++++++ src/lib.rs | 1 + 2 files changed, 7 insertions(+) create mode 100644 src/fen.rs diff --git a/src/fen.rs b/src/fen.rs new file mode 100644 index 0000000..f112bc9 --- /dev/null +++ b/src/fen.rs @@ -0,0 +1,6 @@ +/// A trait to mark items that can be converted from a FEN input. +pub trait FromFen: Sized { + type Err; + + fn from_fen(s: &str) -> Result; +} diff --git a/src/lib.rs b/src/lib.rs index bfcf0bd..82467ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod board; +pub mod fen; pub mod movegen; pub mod utils; From e8a7825215fa93b96f937b8d2abef6814aefe5e4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:24:24 +0200 Subject: [PATCH 117/157] Introduce 'FenError' enum --- src/fen.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/fen.rs b/src/fen.rs index f112bc9..9c406ef 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -4,3 +4,21 @@ pub trait FromFen: Sized { 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, +} + +impl std::fmt::Display for FenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let error_msg = match self { + Self::InvalidFen => "Invalid FEN input", + }; + write!(f, "{}", error_msg) + } +} + +impl std::error::Error for FenError {} From 8866c35423e574103f2ee369fecf78338dd608da Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:15 +0200 Subject: [PATCH 118/157] Add FEN side to move parsing --- src/fen.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/fen.rs b/src/fen.rs index 9c406ef..4ee320d 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,3 +1,5 @@ +use crate::board::Color; + /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { type Err; @@ -22,3 +24,17 @@ impl std::fmt::Display for FenError { } impl std::error::Error for FenError {} + +/// 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) + } +} From 3ab73a08d8bb2efff00511c32785604cf0564020 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:38 +0200 Subject: [PATCH 119/157] Add FEN en-passant target square parsing --- src/fen.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/fen.rs b/src/fen.rs index 4ee320d..4118b59 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,4 @@ -use crate::board::Color; +use crate::board::{Color, File, Rank, Square}; /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { @@ -38,3 +38,20 @@ impl FromFen for Color { 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) + } +} From 4ed310511143ab489e6812075c8e1dce17d72da1 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:55 +0200 Subject: [PATCH 120/157] Add FEN piece type parsing --- src/fen.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/fen.rs b/src/fen.rs index 4118b59..f058e84 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,4 @@ -use crate::board::{Color, File, Rank, Square}; +use crate::board::{Color, File, Piece, Rank, Square}; /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { @@ -55,3 +55,21 @@ impl FromFen for Option { 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) + } +} From bbf5149f7116d7876b3cc3920182d96f3df627a2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:41:08 +0200 Subject: [PATCH 121/157] Add FEN castling rights parsing --- src/fen.rs | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/fen.rs b/src/fen.rs index f058e84..8273392 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,4 @@ -use crate::board::{Color, File, Piece, Rank, Square}; +use crate::board::{CastleRights, Color, File, Piece, Rank, Square}; /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { @@ -25,6 +25,39 @@ impl std::fmt::Display for FenError { impl std::error::Error for FenError {} +/// 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; From 5a9b3a7d97658c3699de9ad8f741f0ef13cc5a0a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:19:40 +0200 Subject: [PATCH 122/157] Add 'ChessBoard' --- src/board/chess_board.rs | 98 ++++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 101 insertions(+) create mode 100644 src/board/chess_board.rs diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs new file mode 100644 index 0000000..c09d893 --- /dev/null +++ b/src/board/chess_board.rs @@ -0,0 +1,98 @@ +use super::{Bitboard, CastleRights, Color, Piece, Square}; + +/// Represent an on-going chess game. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ChessBoard { + /// A [Bitboard] of occupancy for each piece type, discarding color. Indexed by [Piece::index]. + piece_occupancy: [Bitboard; Piece::NUM_VARIANTS], + /// A [Bitboard] of occupancy for each color, discarding piece type. Indexed by [Piece::index]. + color_occupancy: [Bitboard; Color::NUM_VARIANTS], + /// A [Bitboard] representing all squares currently occupied by a piece. + combined_occupancy: Bitboard, + /// The allowed [CastleRights] for either color. Indexed by [Color::index]. + castle_rights: [CastleRights; Color::NUM_VARIANTS], + /// A potential en-passant attack. + /// Either `None` if no 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, +} + +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)] + #[allow(unused)] + 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] 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)] + #[allow(unused)] + 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)] + #[allow(unused)] + 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 + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index d23ef25..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::*; From 22de0785fa4998cec0a7236864f9d75403bfdd2b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:19:40 +0200 Subject: [PATCH 123/157] Add 'ChessBoard::{,un}do_move' --- src/board/chess_board.rs | 91 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index c09d893..31cd4bf 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,4 +1,4 @@ -use super::{Bitboard, CastleRights, Color, Piece, Square}; +use super::{Bitboard, CastleRights, Color, File, Move, Piece, Square}; /// Represent an on-going chess game. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -23,6 +23,14 @@ pub struct ChessBoard { 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)] @@ -44,7 +52,6 @@ impl ChessBoard { /// Return the [CastleRights] for the given [Color]. Allow mutations. #[inline(always)] - #[allow(unused)] fn castle_rights_mut(&mut self, color: Color) -> &mut CastleRights { &mut self.castle_rights[color.index()] } @@ -58,7 +65,6 @@ impl ChessBoard { /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. /// Allow mutating the state. #[inline(always)] - #[allow(unused)] fn piece_occupancy_mut(&mut self, piece: Piece) -> &mut Bitboard { &mut self.piece_occupancy[piece.index()] } @@ -73,7 +79,6 @@ impl ChessBoard { /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece /// type. Allow mutating the state. #[inline(always)] - #[allow(unused)] fn color_occupancy_mut(&mut self, color: Color) -> &mut Bitboard { &mut self.color_occupancy[color.index()] } @@ -95,4 +100,82 @@ impl ChessBoard { pub fn total_plies(&self) -> u32 { self.total_plies } + + /// 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; + } } From 63380cabbc0f9b159c998b813061b9f6b090bf5d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:45:04 +0200 Subject: [PATCH 124/157] Implement 'Default' for 'ChessBoard' --- src/board/chess_board.rs | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 31cd4bf..24b66eb 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,4 +1,4 @@ -use super::{Bitboard, CastleRights, Color, File, Move, Piece, Square}; +use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; /// Represent an on-going chess game. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -179,3 +179,39 @@ impl ChessBoard { self.side = !self.side; } } + +/// 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, + } + } +} From 4b2eb4604f3f35f649b50a6ac5e24449d0d44c38 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:17:12 +0200 Subject: [PATCH 125/157] Add 'FenError::InvalidPosition' variant --- src/fen.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/fen.rs b/src/fen.rs index 8273392..d8af180 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -12,12 +12,15 @@ pub trait FromFen: Sized { pub enum FenError { /// Invalid FEN input. InvalidFen, + /// Invalid chess position. + InvalidPosition, } impl std::fmt::Display for FenError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let error_msg = match self { Self::InvalidFen => "Invalid FEN input", + Self::InvalidPosition => "Invalid chess position", }; write!(f, "{}", error_msg) } From 2e1b7329963c4cbc062e9d95570bcc8bf9e7f29f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:37:47 +0200 Subject: [PATCH 126/157] Add 'ChessBoard::is_valid' --- src/board/chess_board.rs | 302 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 24b66eb..095f82e 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -178,6 +178,93 @@ impl ChessBoard { 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 { + // Don't overlap pieces. + for piece in Piece::iter() { + #[allow(clippy::collapsible_if)] + for other in Piece::iter() { + if piece != other { + if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() { + return false; + } + } + } + } + + // Don't overlap colors. + if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() { + return false; + } + + // Calculate the union of all pieces. + let combined = + Piece::iter().fold(Bitboard::EMPTY, |board, p| board | self.piece_occupancy(p)); + + // Ensure that the pre-computed version is accurate. + if combined != self.combined_occupancy() { + return false; + } + + // Ensure that all pieces belong to a color, and no color has pieces that don't exist. + if combined != (self.color_occupancy(Color::White) | self.color_occupancy(Color::Black)) { + return false; + } + + // Have exactly one king of each color. + for color in Color::iter() { + if (self.piece_occupancy(Piece::King) & self.color_occupancy(color)).count() != 1 { + return false; + } + } + + // Verify that rooks and kings that are allowed to castle have not been moved. + for color in Color::iter() { + let castle_rights = self.castle_rights(color); + + // Nothing to check if there are no castlings allowed. + if castle_rights == CastleRights::NoSide { + continue; + } + + let actual_rooks = self.piece_occupancy(Piece::Rook) & self.color_occupancy(color); + let expected_rooks = castle_rights.unmoved_rooks(color); + // We must check the intersection, in case there are more than 2 rooks on the board. + if (expected_rooks & actual_rooks) != expected_rooks { + return false; + } + + let actual_king = self.piece_occupancy(Piece::King) & self.color_occupancy(color); + let expected_king = Square::new(File::E, color.first_rank()); + // We have checked that there is exactly one king, no need for intersecting the sets. + if actual_king != expected_king.into_bitboard() { + return false; + } + } + + // The current en-passant target square must be empty, right behind an opponent's pawn. + if let Some(square) = self.en_passant() { + if !(self.combined_occupancy() & square).is_empty() { + return false; + } + let opponent_pawns = + self.piece_occupancy(Piece::Pawn) & self.color_occupancy(!self.current_player()); + let double_pushed_pawn = self + .current_player() + .backward_direction() + .move_board(square.into_bitboard()); + if (opponent_pawns & double_pushed_pawn).is_empty() { + return false; + } + } + + // FIXME: check for opponent being in check. + // FIXME: check for kings touching. + + true + } } /// Use the starting position as a default value, corresponding to the @@ -215,3 +302,218 @@ impl Default for ChessBoard { } } } + +#[cfg(test)] +mod test { + 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!(!position.is_valid()); + } + + #[test] + fn invalid_overlapping_colors() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::E8, Square::E1 | Square::E8], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_combined_does_not_equal_pieces() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], + combined_occupancy: Square::E1.into_bitboard(), + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_combined_does_not_equal_colors() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::H1, Square::E8 | Square::H8], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_multiple_kings() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E2 | Square::E7 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::E2, Square::E7 | Square::E8], + combined_occupancy: Square::E1 | Square::E2 | Square::E7 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_castling_rights_no_rooks() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::BothSides; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_castling_rights_moved_king() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E2 | Square::E7, + // Queen + Bitboard::EMPTY, + // Rook + Square::A1 | Square::A8 | Square::H1 | Square::H8, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [ + Square::A1 | Square::E2 | Square::H1, + Square::A8 | Square::E7 | Square::H8, + ], + combined_occupancy: Square::A1 + | Square::A8 + | Square::E1 + | Square::E8 + | Square::H1 + | Square::H8, + castle_rights: [CastleRights::BothSides; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } +} From fb78e0365691331e4d557ce1cb982283530cac76 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:38:50 +0200 Subject: [PATCH 127/157] Add FEN board parsing Unfortunately, given that I *don't* want to expose all the `ChessBoard` fields to the rest of the crate, this implementation will have to live alongside its module instead of inside `crate::fen`... --- src/board/chess_board.rs | 169 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 095f82e..a327edc 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,3 +1,5 @@ +use crate::fen::{FenError, FromFen}; + use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; /// Represent an on-going chess game. @@ -303,8 +305,103 @@ impl Default for ChessBoard { } } +/// 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 castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?; + let side = Color::from_fen(side_to_move)?; + let en_passant = Option::::from_fen(en_passant_square)?; + + let half_move_clock = half_move_clock + .parse::() + .map_err(|_| FenError::InvalidFen)?; + let full_move_counter = full_move_counter + .parse::() + .map_err(|_| FenError::InvalidFen)?; + let total_plies = (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }; + + let (piece_occupancy, color_occupancy, combined_occupancy) = { + let (mut pieces, mut colors, mut combined) = + ([Bitboard::EMPTY; 6], [Bitboard::EMPTY; 2], Bitboard::EMPTY); + + let mut rank: usize = 8; + for rank_str in piece_placement.split('/') { + rank -= 1; + let mut file: usize = 0; + for c in rank_str.chars() { + let color = if c.is_uppercase() { + Color::White + } else { + Color::Black + }; + let piece = match c { + digit @ '1'..='8' => { + // Unwrap is fine since this arm is only matched by digits + file += digit.to_digit(10).unwrap() as usize; + continue; + } + _ => Piece::from_fen(&c.to_string())?, + }; + let (piece_board, color_board) = + (&mut pieces[piece.index()], &mut colors[color.index()]); + + // Only need to worry about underflow since those are `usize` values. + if file >= 8 || rank >= 8 { + return Err(FenError::InvalidFen); + }; + let square = Square::new(File::from_index(file), Rank::from_index(rank)); + *piece_board |= square; + *color_board |= square; + combined |= square; + file += 1; + } + // We haven't read exactly 8 files. + if file != 8 { + return Err(FenError::InvalidFen); + } + } + // We haven't read exactly 8 ranks + if rank != 0 { + return Err(FenError::InvalidFen); + } + + (pieces, colors, combined) + }; + + let res = Self { + piece_occupancy, + color_occupancy, + combined_occupancy, + castle_rights, + en_passant, + half_move_clock, + total_plies, + side, + }; + + if !res.is_valid() { + return Err(FenError::InvalidPosition); + } + + Ok(res) + } +} + #[cfg(test)] mod test { + use crate::board::MoveBuilder; + use super::*; #[test] @@ -516,4 +613,76 @@ mod test { }; assert!(!position.is_valid()); } + + #[test] + fn fen_default_position() { + let default_position = ChessBoard::default(); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + .unwrap(), + default_position + ); + } + + #[test] + fn fen_en_passant() { + // Start from default position + let mut position = ChessBoard::default(); + 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 + ); + } } From 0d8feca6b157cb2e888de54a16487ed2b594a0ec Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:39:29 +0200 Subject: [PATCH 128/157] Test 'ChessBoard::{do,undo}_move' machinery --- src/board/chess_board.rs | 127 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index a327edc..af26886 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -685,4 +685,131 @@ mod test { position ); } + + #[test] + fn do_move() { + // Start from default position + let mut position = ChessBoard::default(); + // Modify it to account for e4 move + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::E2, + destination: Square::E4, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap() + ); + // And now c5 + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::C7, + destination: Square::C5, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap() + ); + // Finally, Nf3 + position.do_move( + MoveBuilder { + piece: Piece::Knight, + start: Square::G1, + destination: Square::F3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(), + ); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") + .unwrap() + ); + } + + #[test] + fn do_move_and_undo() { + // Start from default position + let mut position = ChessBoard::default(); + // Modify it to account for e4 move + let move_1 = MoveBuilder { + piece: Piece::Pawn, + start: Square::E2, + destination: Square::E4, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(); + let state_1 = position.do_move(move_1); + // And now c5 + let move_2 = MoveBuilder { + piece: Piece::Pawn, + start: Square::C7, + destination: Square::C5, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(); + let state_2 = position.do_move(move_2); + // Finally, Nf3 + let move_3 = MoveBuilder { + piece: Piece::Knight, + start: Square::G1, + destination: Square::F3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(); + let state_3 = position.do_move(move_3); + // Now revert each move one-by-one + position.undo_move(move_3, state_3); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap() + ); + position.undo_move(move_2, state_2); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap() + ); + position.undo_move(move_1, state_1); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + .unwrap() + ); + } } From aaab4397646db5c29bcdf85b43a53071ef777d0d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:26:31 +0200 Subject: [PATCH 129/157] Add 'TryInto' for 'Bitboard' --- src/board/bitboard/error.rs | 19 +++++++++++++++++++ src/board/bitboard/mod.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/board/bitboard/error.rs 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/mod.rs b/src/board/bitboard/mod.rs index e186a07..b0ec90a 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,6 +1,8 @@ use super::Square; use crate::utils::static_assert; +mod error; +use error::*; mod iterator; use iterator::*; mod superset; @@ -102,6 +104,21 @@ impl IntoIterator for Bitboard { } } +/// 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)) + } +} + /// Treat bitboard as a set of squares, shift each square's index left by the amount given. impl std::ops::Shl for Bitboard { type Output = Bitboard; @@ -461,4 +478,23 @@ mod test { 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) + ) + } } From c115e357e10ebf275bf2aff48f11f3275b838717 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:27:17 +0200 Subject: [PATCH 130/157] Check kings' position in 'ChessBoard::is_valid' --- src/board/chess_board.rs | 42 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index af26886..9b5427a 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,4 +1,7 @@ -use crate::fen::{FenError, FromFen}; +use crate::{ + fen::{FenError, FromFen}, + movegen, +}; use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; @@ -262,8 +265,15 @@ impl ChessBoard { } } + // Check that kings don't touch each other. + let white_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::White); + let black_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::Black); + // Unwrap is fine, we already checked that there is exactly one king of each color + if !(movegen::king_moves(white_king.try_into().unwrap()) & black_king).is_empty() { + return false; + } + // FIXME: check for opponent being in check. - // FIXME: check for kings touching. true } @@ -614,6 +624,34 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn invalid_kings_next_to_each_other() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E2 | Square::E3, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E2.into_bitboard(), Square::E3.into_bitboard()], + combined_occupancy: Square::E2 | Square::E3, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + #[test] fn fen_default_position() { let default_position = ChessBoard::default(); From 23a4e857a0846d19d01c0409ef4a400f97aae4e6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 01:29:39 +0100 Subject: [PATCH 131/157] Add 'ChessBoard::occupancy' --- src/board/chess_board.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 9b5427a..2f58b31 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -61,6 +61,12 @@ impl ChessBoard { &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 { From b75136f7d63f3549eeaf712ae3875177dda5175b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 01:29:39 +0100 Subject: [PATCH 132/157] Use 'ChessBoard::occupancy' --- src/board/chess_board.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 2f58b31..9a50894 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -226,7 +226,7 @@ impl ChessBoard { // Have exactly one king of each color. for color in Color::iter() { - if (self.piece_occupancy(Piece::King) & self.color_occupancy(color)).count() != 1 { + if self.occupancy(Piece::King, color).count() != 1 { return false; } } @@ -240,14 +240,14 @@ impl ChessBoard { continue; } - let actual_rooks = self.piece_occupancy(Piece::Rook) & self.color_occupancy(color); + 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 false; } - let actual_king = self.piece_occupancy(Piece::King) & self.color_occupancy(color); + 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() { @@ -260,8 +260,7 @@ impl ChessBoard { if !(self.combined_occupancy() & square).is_empty() { return false; } - let opponent_pawns = - self.piece_occupancy(Piece::Pawn) & self.color_occupancy(!self.current_player()); + let opponent_pawns = self.occupancy(Piece::Pawn, !self.current_player()); let double_pushed_pawn = self .current_player() .backward_direction() @@ -272,8 +271,8 @@ impl ChessBoard { } // Check that kings don't touch each other. - let white_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::White); - let black_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::Black); + 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 false; From eb53e27f9dfd54f554c9b2210bc78325ca226c22 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 11:58:18 +0200 Subject: [PATCH 133/157] Test for opponent being in check during validation --- src/board/chess_board.rs | 68 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 9a50894..e72abec 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -278,10 +278,48 @@ impl ChessBoard { return false; } - // FIXME: check for opponent being in check. + // Check that the opponent is not currently in check. + if !self.compute_checkers(!self.current_player()).is_empty() { + return false; + } true } + + /// Compute all pieces that are currently threatening the given [Color]'s king. + fn compute_checkers(&self, color: Color) -> Bitboard { + // Unwrap is fine, there should always be exactly one king per color + let king = (self.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 @@ -657,6 +695,34 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn invalid_opponent_in_check() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Square::E7.into_bitboard(), + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::E7, Square::E8.into_bitboard()], + combined_occupancy: Square::E1 | Square::E7 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + #[test] fn fen_default_position() { let default_position = ChessBoard::default(); From 2ab1f363ea7463a75621bf6a6878655df4aecde3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 12:02:41 +0200 Subject: [PATCH 134/157] Add 'ChessBoard::checkers' --- src/board/chess_board.rs | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index e72abec..e08b3ee 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -112,6 +112,13 @@ impl ChessBoard { 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)] @@ -723,6 +730,49 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn checkers() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E2 | Square::E8, + // Queen + Square::E7 | Square::H2, + // Rook + Square::A2 | Square::E1, + // Bishop + Square::D3 | Square::F3, + // Knight + Square::C1 | Square::G1, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [ + Square::C1 | Square::D3 | Square::E1 | Square::E2 | Square::H2, + Square::A2 | Square::E7 | Square::E8 | Square::F3 | Square::G1, + ], + combined_occupancy: Square::A2 + | Square::C1 + | Square::D3 + | Square::E1 + | Square::E2 + | Square::E7 + | Square::E8 + | Square::F3 + | Square::G1 + | Square::H2, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert_eq!( + position.checkers(), + Square::A2 | Square::E7 | Square::F3 | Square::G1 + ); + } + #[test] fn fen_default_position() { let default_position = ChessBoard::default(); From a1065baff5870e654949bf2659fca57f762cc859 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 11:39:29 +0100 Subject: [PATCH 135/157] Pre-generate the magic bitboard seeds My naive RNG implementation takes about ~40 seconds to generate the magic bitboards for both bishops and rooks (or ~1 second in release mode). If we pre-generate the seeds, we can instead make it ~instantaneous. The self-modifying code is inspired by matklad [1]. [1]: https://matklad.github.io/2022/03/26/self-modifying-code.html --- src/movegen/moves.rs | 36 +++--- src/movegen/wizardry/mod.rs | 225 ++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 13 deletions(-) diff --git a/src/movegen/moves.rs b/src/movegen/moves.rs index 7e40a18..9840083 100644 --- a/src/movegen/moves.rs +++ b/src/movegen/moves.rs @@ -4,25 +4,35 @@ use crate::{ board::{Bitboard, Color, File, Square}, movegen::{ naive, - wizardry::{generate_bishop_magics, generate_rook_magics, MagicMoves, RandGen}, + wizardry::{ + generate_bishop_magics, generate_rook_magics, MagicMoves, RandGen, BISHOP_SEED, + ROOK_SEED, + }, }, }; -// A simple XOR-shift RNG implementation. -struct SimpleRng(u64); +// A pre-rolled RNG for magic bitboard generation, using pre-determined values. +struct PreRolledRng { + numbers: [u64; 64], + current_index: usize, +} -impl SimpleRng { - pub fn new() -> Self { - Self(4) // https://xkcd.com/221/ +impl PreRolledRng { + pub fn new(numbers: [u64; 64]) -> Self { + Self { + numbers, + current_index: 0, + } } } -impl RandGen for SimpleRng { +impl RandGen for PreRolledRng { fn gen(&mut self) -> u64 { - self.0 ^= self.0 >> 12; - self.0 ^= self.0 << 25; - self.0 ^= self.0 >> 27; - self.0 + // 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 } } @@ -86,7 +96,7 @@ 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 SimpleRng::new()); + 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) } }) @@ -98,7 +108,7 @@ 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 SimpleRng::new()); + 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) } }) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 83f4d69..00645d8 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -56,3 +56,228 @@ impl MagicMoves { } } } + +// 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") + } + } +} From 66fff65a55b495247af416e846c4baade833f613 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 12:28:22 +0100 Subject: [PATCH 136/157] Add 'CastleRights' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 1072cb8..8d983c6 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -46,6 +46,21 @@ class Bitboard(object): 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. @@ -217,6 +232,16 @@ class BitboardPrinter(object): return "Bitboard{" + str(self._val)[1:-1] + "}" +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" @@ -272,6 +297,7 @@ def build_pretty_printer(): 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) From 905cca4576da29631487643de65feaecc1763e58 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 14:37:37 +0100 Subject: [PATCH 137/157] Add convenience 'Square' constructor in GDB utils --- utils/gdb/seer_pretty_printers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 8d983c6..c80f6a7 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -14,6 +14,10 @@ class Square(object): 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] From 9056747448255295a43408d95d030bbeaba93c69 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 14:38:26 +0100 Subject: [PATCH 138/157] Add 'Bitboard.at' in GDB utils --- utils/gdb/seer_pretty_printers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index c80f6a7..56336d6 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -41,6 +41,9 @@ 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 = self._val From afa0172f6a879151e9bb5b780da8783835151d02 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 14:39:05 +0100 Subject: [PATCH 139/157] Add 'print-board' GDB command --- utils/gdb/seer_pretty_printers.py | 98 +++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 56336d6..0481d8d 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -1,5 +1,6 @@ import enum +import gdb import gdb.printing @@ -219,6 +220,82 @@ class Move(object): 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" @@ -299,6 +376,21 @@ class MovePrinter(object): 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') @@ -313,4 +405,10 @@ def build_pretty_printer(): return pp + +def register_commands(): + PrintBoard() + + gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True) +register_commands() From ea301659c0a72aaec3315fb43ec241db33aa4210 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 12 Aug 2022 16:17:51 +0200 Subject: [PATCH 140/157] Check for invalid pawns in 'ChessBoard::is_valid' --- src/board/chess_board.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index e08b3ee..573ecfa 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -238,6 +238,14 @@ impl ChessBoard { } } + // 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 false; + } + // Verify that rooks and kings that are allowed to castle have not been moved. for color in Color::iter() { let castle_rights = self.castle_rights(color); @@ -730,6 +738,34 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn invalid_pawn_on_first_rank() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::H1 | Square::H8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Square::A1.into_bitboard(), + ], + color_occupancy: [Square::A1 | Square::H1, Square::H8.into_bitboard()], + combined_occupancy: Square::A1 | Square::H1 | Square::H8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + #[test] fn checkers() { let position = ChessBoard { From 009cb44eab515569744bad37485e6e45ce0ff0c0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 12 Aug 2022 16:17:03 +0200 Subject: [PATCH 141/157] Check all piece counts in 'ChessBoard::is_valid' --- src/board/chess_board.rs | 57 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 573ecfa..838769a 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -231,9 +231,23 @@ impl ChessBoard { return false; } - // Have exactly one king of each color. for color in Color::iter() { - if self.occupancy(Piece::King, color).count() != 1 { + 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 false; + } + } + + // Check that don't have too many pieces in total + if self.color_occupancy(color).count() > 16 { return false; } } @@ -766,6 +780,45 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn invalid_too_many_pieces() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::H1 | Square::H8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + File::B.into_bitboard() + | File::C.into_bitboard() + | File::D.into_bitboard() + | File::E.into_bitboard(), + ], + color_occupancy: [ + File::B.into_bitboard() | File::C.into_bitboard() | Square::H1, + File::D.into_bitboard() | File::E.into_bitboard() | Square::H8, + ], + combined_occupancy: File::B.into_bitboard() + | File::C.into_bitboard() + | File::D.into_bitboard() + | File::E.into_bitboard() + | Square::H1 + | Square::H8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + #[test] fn checkers() { let position = ChessBoard { From c3a9a55be1c6c6bf9e5c4303e758afe8a18c4d7c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 19:56:48 +0100 Subject: [PATCH 142/157] Move 'ChessBoard' to its own sub-folder --- src/board/{chess_board.rs => chess_board/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/board/{chess_board.rs => chess_board/mod.rs} (100%) diff --git a/src/board/chess_board.rs b/src/board/chess_board/mod.rs similarity index 100% rename from src/board/chess_board.rs rename to src/board/chess_board/mod.rs From 5ddab6af0eee2039a657d5159b425a564520ce8b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:21:05 +0100 Subject: [PATCH 143/157] Add 'chess_board::InvalidError' --- src/board/chess_board/error.rs | 50 ++++++++++++++++++++++++++++++++++ src/board/chess_board/mod.rs | 3 ++ 2 files changed, 53 insertions(+) create mode 100644 src/board/chess_board/error.rs diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs new file mode 100644 index 0000000..e531f54 --- /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 and behind an opponent's pawn. + 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 and behind an opponent's pawn." + } + 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 index 838769a..17d34e8 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -5,6 +5,9 @@ use crate::{ use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; +mod error; +pub use error::*; + /// Represent an on-going chess game. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct ChessBoard { From 4098f188ce77e26fc6e58c73c377cf73f82b27a4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:21:05 +0100 Subject: [PATCH 144/157] Add 'ChessBoard::validate' --- src/board/chess_board/mod.rs | 40 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 17d34e8..d51eabb 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -203,13 +203,18 @@ impl ChessBoard { /// 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 false; + return Err(InvalidError::OverlappingPieces); } } } @@ -217,7 +222,7 @@ impl ChessBoard { // Don't overlap colors. if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() { - return false; + return Err(InvalidError::OverlappingColors); } // Calculate the union of all pieces. @@ -226,12 +231,12 @@ impl ChessBoard { // Ensure that the pre-computed version is accurate. if combined != self.combined_occupancy() { - return false; + 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 false; + return Err(InvalidError::ErroneousCombinedOccupancy); } for color in Color::iter() { @@ -239,19 +244,24 @@ impl ChessBoard { // 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::King => count <= 1, Piece::Pawn => count <= 8, Piece::Queen => count <= 9, _ => count <= 10, }; if !possible { - return false; + 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 false; + return Err(InvalidError::TooManyPieces); } } @@ -260,7 +270,7 @@ impl ChessBoard { & (Rank::First.into_bitboard() | Rank::Eighth.into_bitboard())) .is_empty() { - return false; + return Err(InvalidError::InvalidPawnPosition); } // Verify that rooks and kings that are allowed to castle have not been moved. @@ -276,21 +286,21 @@ impl ChessBoard { let expected_rooks = castle_rights.unmoved_rooks(color); // We must check the intersection, in case there are more than 2 rooks on the board. if (expected_rooks & actual_rooks) != expected_rooks { - return false; + 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 false; + return Err(InvalidError::InvalidCastlingRights); } } // The current en-passant target square must be empty, right behind an opponent's pawn. if let Some(square) = self.en_passant() { if !(self.combined_occupancy() & square).is_empty() { - return false; + return Err(InvalidError::InvalidEnPassant); } let opponent_pawns = self.occupancy(Piece::Pawn, !self.current_player()); let double_pushed_pawn = self @@ -298,7 +308,7 @@ impl ChessBoard { .backward_direction() .move_board(square.into_bitboard()); if (opponent_pawns & double_pushed_pawn).is_empty() { - return false; + return Err(InvalidError::InvalidEnPassant); } } @@ -307,15 +317,15 @@ impl ChessBoard { 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 false; + return Err(InvalidError::NeighbouringKings); } // Check that the opponent is not currently in check. if !self.compute_checkers(!self.current_player()).is_empty() { - return false; + return Err(InvalidError::OpponentInCheck); } - true + Ok(()) } /// Compute all pieces that are currently threatening the given [Color]'s king. From 90a9ac0f7a3f66c444b97437e9f4573ff78ccf2a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:28:16 +0100 Subject: [PATCH 145/157] Add validation error detail in 'FenError' --- src/board/chess_board/mod.rs | 4 ++-- src/fen.rs | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index d51eabb..d1d3751 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -485,8 +485,8 @@ impl FromFen for ChessBoard { side, }; - if !res.is_valid() { - return Err(FenError::InvalidPosition); + if let Err(err) = res.validate() { + return Err(FenError::InvalidPosition(err)); } Ok(res) diff --git a/src/fen.rs b/src/fen.rs index d8af180..3034003 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,4 @@ -use crate::board::{CastleRights, Color, File, Piece, Rank, Square}; +use crate::board::{CastleRights, Color, File, InvalidError, Piece, Rank, Square}; /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { @@ -13,16 +13,15 @@ pub enum FenError { /// Invalid FEN input. InvalidFen, /// Invalid chess position. - InvalidPosition, + InvalidPosition(InvalidError), } impl std::fmt::Display for FenError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let error_msg = match self { - Self::InvalidFen => "Invalid FEN input", - Self::InvalidPosition => "Invalid chess position", - }; - write!(f, "{}", error_msg) + match self { + Self::InvalidFen => write!(f, "Invalid FEN input"), + Self::InvalidPosition(err) => write!(f, "Invalid chess position: {}", err), + } } } From 4b13bd9a0bfa0b6005345c647d9c20b2ecbc41f0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:41:54 +0100 Subject: [PATCH 146/157] Fix 'ChessBoard' validation test It wasn't actually testing the right thing due to the typo... --- src/board/chess_board/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index d1d3751..3a53336 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -696,8 +696,8 @@ mod test { ], combined_occupancy: Square::A1 | Square::A8 - | Square::E1 - | Square::E8 + | Square::E2 + | Square::E7 | Square::H1 | Square::H8, castle_rights: [CastleRights::BothSides; 2], From bce690d363c1ba47e3b4a093068d9d2aa43ae2a8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:42:28 +0100 Subject: [PATCH 147/157] Use 'ChessBoard::validate' in tests This makes the test more explicit and exact in what they're testing. --- src/board/chess_board/mod.rs | 55 ++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 3a53336..1f0b293 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -530,7 +530,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::OverlappingPieces, + ); } #[test] @@ -558,7 +561,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::OverlappingColors, + ); } #[test] @@ -586,7 +592,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::ErroneousCombinedOccupancy, + ); } #[test] @@ -614,7 +623,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::ErroneousCombinedOccupancy, + ); } #[test] @@ -642,7 +654,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::TooManyPieces, + ); } #[test] @@ -670,7 +685,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::InvalidCastlingRights, + ); } #[test] @@ -706,7 +724,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::InvalidCastlingRights, + ); } #[test] @@ -734,7 +755,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::NeighbouringKings, + ); } #[test] @@ -762,7 +786,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::OpponentInCheck, + ); } #[test] @@ -790,7 +817,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::InvalidPawnPosition, + ); } #[test] @@ -829,7 +859,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::TooManyPieces, + ); } #[test] From 8962fd90f4ac740b3547db4873c320d3c53c1519 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:19:55 +0100 Subject: [PATCH 148/157] Add 'ChessBoardBuilder' --- src/board/chess_board/builder.rs | 161 +++++++++++++++++++++++++++++++ src/board/chess_board/mod.rs | 3 + 2 files changed, 164 insertions(+) create mode 100644 src/board/chess_board/builder.rs 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/mod.rs b/src/board/chess_board/mod.rs index 1f0b293..d04fdec 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -5,6 +5,9 @@ use crate::{ use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; +mod builder; +pub use builder::*; + mod error; pub use error::*; From 1d47f2917826c25a3525c2cc467f2723f94e300a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:20:20 +0100 Subject: [PATCH 149/157] Use 'ChessBoardBuilder' in validation tests The various tests for overlapping can't be triggered with the builder API, so those have stayed unchanged. --- src/board/chess_board/mod.rs | 259 ++++++++--------------------------- 1 file changed, 58 insertions(+), 201 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index d04fdec..cb1b95c 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -634,238 +634,95 @@ mod test { #[test] fn invalid_multiple_kings() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E1 | Square::E2 | Square::E7 | Square::E8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E1 | Square::E2, Square::E7 | Square::E8], - combined_occupancy: Square::E1 | Square::E2 | Square::E7 | Square::E8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + 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!( - position.validate().err().unwrap(), - InvalidError::TooManyPieces, - ); + assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); } #[test] fn invalid_castling_rights_no_rooks() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E1 | Square::E8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], - combined_occupancy: Square::E1 | Square::E8, - castle_rights: [CastleRights::BothSides; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + 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!( - position.validate().err().unwrap(), - InvalidError::InvalidCastlingRights, - ); + assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights); } #[test] fn invalid_castling_rights_moved_king() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E2 | Square::E7, - // Queen - Bitboard::EMPTY, - // Rook - Square::A1 | Square::A8 | Square::H1 | Square::H8, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [ - Square::A1 | Square::E2 | Square::H1, - Square::A8 | Square::E7 | Square::H8, - ], - combined_occupancy: Square::A1 - | Square::A8 - | Square::E2 - | Square::E7 - | Square::H1 - | Square::H8, - castle_rights: [CastleRights::BothSides; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + 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!( - position.validate().err().unwrap(), - InvalidError::InvalidCastlingRights, - ); + assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights); } #[test] fn invalid_kings_next_to_each_other() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E2 | Square::E3, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E2.into_bitboard(), Square::E3.into_bitboard()], - combined_occupancy: Square::E2 | Square::E3, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + 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!( - position.validate().err().unwrap(), - InvalidError::NeighbouringKings, - ); + assert_eq!(res.err().unwrap(), InvalidError::NeighbouringKings); } #[test] fn invalid_opponent_in_check() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::E1 | Square::E8, - // Queen - Square::E7.into_bitboard(), - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Bitboard::EMPTY, - ], - color_occupancy: [Square::E1 | Square::E7, Square::E8.into_bitboard()], - combined_occupancy: Square::E1 | Square::E7 | Square::E8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + 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!( - position.validate().err().unwrap(), - InvalidError::OpponentInCheck, - ); + assert_eq!(res.err().unwrap(), InvalidError::OpponentInCheck); } #[test] fn invalid_pawn_on_first_rank() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::H1 | Square::H8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - Square::A1.into_bitboard(), - ], - color_occupancy: [Square::A1 | Square::H1, Square::H8.into_bitboard()], - combined_occupancy: Square::A1 | Square::H1 | Square::H8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + 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!( - position.validate().err().unwrap(), - InvalidError::InvalidPawnPosition, - ); + assert_eq!(res.err().unwrap(), InvalidError::InvalidPawnPosition); } #[test] fn invalid_too_many_pieces() { - let position = ChessBoard { - piece_occupancy: [ - // King - Square::H1 | Square::H8, - // Queen - Bitboard::EMPTY, - // Rook - Bitboard::EMPTY, - // Bishop - Bitboard::EMPTY, - // Knight - Bitboard::EMPTY, - // Pawn - File::B.into_bitboard() - | File::C.into_bitboard() - | File::D.into_bitboard() - | File::E.into_bitboard(), - ], - color_occupancy: [ - File::B.into_bitboard() | File::C.into_bitboard() | Square::H1, - File::D.into_bitboard() | File::E.into_bitboard() | Square::H8, - ], - combined_occupancy: File::B.into_bitboard() - | File::C.into_bitboard() - | File::D.into_bitboard() - | File::E.into_bitboard() - | Square::H1 - | Square::H8, - castle_rights: [CastleRights::NoSide; 2], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::H1] = Some((Piece::King, Color::White)); + builder[Square::H8] = Some((Piece::King, Color::Black)); + for square in (File::B.into_bitboard() | File::C.into_bitboard()).into_iter() { + builder[square] = Some((Piece::Pawn, Color::White)); + } + for square in (File::F.into_bitboard() | File::G.into_bitboard()).into_iter() { + builder[square] = Some((Piece::Pawn, Color::Black)); + } + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::TooManyPieces, - ); + assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); } #[test] From 14c58a60a8e1b7538ebfc783644a722e5ccdb01f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:30:52 +0100 Subject: [PATCH 150/157] Add 'From' for 'FenError' --- src/fen.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/fen.rs b/src/fen.rs index 3034003..78452c2 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -27,6 +27,13 @@ impl std::fmt::Display for FenError { 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; From 8b7a70b2f0e606082a6a8989b28b26dbc8f43ee3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:32:02 +0100 Subject: [PATCH 151/157] Use 'ChessBoardBuilder' in 'FromFen' This will allow taking this *out* of the module, now that we don't need to reach into the internals of 'ChessBoard'. --- src/board/chess_board/mod.rs | 50 +++++++++++++++--------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index cb1b95c..c502ada 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -417,22 +417,33 @@ impl FromFen for ChessBoard { 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)?; - let en_passant = Option::::from_fen(en_passant_square)?; + 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)?; - let total_plies = (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }; - - let (piece_occupancy, color_occupancy, combined_occupancy) = { - let (mut pieces, mut colors, mut combined) = - ([Bitboard::EMPTY; 6], [Bitboard::EMPTY; 2], Bitboard::EMPTY); + 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; @@ -451,17 +462,15 @@ impl FromFen for ChessBoard { } _ => Piece::from_fen(&c.to_string())?, }; - let (piece_board, color_board) = - (&mut pieces[piece.index()], &mut colors[color.index()]); // Only need to worry about underflow since those are `usize` values. if file >= 8 || rank >= 8 { return Err(FenError::InvalidFen); }; + let square = Square::new(File::from_index(file), Rank::from_index(rank)); - *piece_board |= square; - *color_board |= square; - combined |= square; + + builder[square] = Some((piece, color)); file += 1; } // We haven't read exactly 8 files. @@ -473,26 +482,9 @@ impl FromFen for ChessBoard { if rank != 0 { return Err(FenError::InvalidFen); } - - (pieces, colors, combined) }; - let res = Self { - piece_occupancy, - color_occupancy, - combined_occupancy, - castle_rights, - en_passant, - half_move_clock, - total_plies, - side, - }; - - if let Err(err) = res.validate() { - return Err(FenError::InvalidPosition(err)); - } - - Ok(res) + Ok(builder.try_into()?) } } From fe1e773717305e25a0cd5db41cfd9238dc681afe Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:34:01 +0100 Subject: [PATCH 152/157] Move 'FromFen' for 'ChessBoard' into 'fen' module --- src/board/chess_board/mod.rs | 91 +----------------------------------- src/fen.rs | 89 ++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 90 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index c502ada..824426f 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -1,7 +1,4 @@ -use crate::{ - fen::{FenError, FromFen}, - movegen, -}; +use crate::movegen; use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; @@ -403,94 +400,10 @@ impl Default for ChessBoard { } } -/// 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 crate::fen::FromFen; use super::*; diff --git a/src/fen.rs b/src/fen.rs index 78452c2..8baf4af 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,6 @@ -use crate::board::{CastleRights, Color, File, InvalidError, Piece, Rank, Square}; +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 { @@ -115,3 +117,88 @@ impl FromFen for Piece { 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()?) + } +} From 3787149f04cf77f458c08d90df0d7cd881c41a8c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:35:48 +0100 Subject: [PATCH 153/157] Move FEN-related tests to its module --- src/board/chess_board/mod.rs | 72 -------------------------------- src/fen.rs | 79 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 72 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 824426f..9b25dd3 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -673,78 +673,6 @@ mod test { ); } - #[test] - fn fen_default_position() { - let default_position = ChessBoard::default(); - assert_eq!( - ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") - .unwrap(), - default_position - ); - } - - #[test] - fn fen_en_passant() { - // Start from default position - let mut position = ChessBoard::default(); - 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 - ); - } - #[test] fn do_move() { // Start from default position diff --git a/src/fen.rs b/src/fen.rs index 8baf4af..3096c95 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -202,3 +202,82 @@ impl FromFen for ChessBoard { 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 + ); + } +} From ba275c887523faa1f3ab8f0cc9f88ea87f989ef0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:54:32 +0100 Subject: [PATCH 154/157] Validate en-passant square's rank in 'ChessBoard' --- src/board/chess_board/error.rs | 4 ++-- src/board/chess_board/mod.rs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index e531f54..e6ef030 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -9,7 +9,7 @@ pub enum InvalidError { InvalidPawnPosition, /// Castling rights do not match up with the state of the board. InvalidCastlingRights, - /// En-passant target square is not empty and behind an opponent's pawn. + /// 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, @@ -33,7 +33,7 @@ impl std::fmt::Display for InvalidError { "Castling rights do not match up with the state of the board." } Self::InvalidEnPassant => { - "En-passant target square is not empty and behind an opponent's pawn." + "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.", diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 9b25dd3..122880a 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -297,12 +297,22 @@ impl ChessBoard { } } - // The current en-passant target square must be empty, right behind an opponent's pawn. + // 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_pawns = self.occupancy(Piece::Pawn, !self.current_player()); + + 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() From 27d5bb49b1a3452daba99f8a2c820bf6109c7e81 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:56:01 +0100 Subject: [PATCH 155/157] Add tests for en-passant validation --- src/board/chess_board/mod.rs | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 122880a..99d6df6 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -588,6 +588,56 @@ mod test { 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 = { From 4f1d2f7320d2a9035bf5e02d4d8925db9e5bbd05 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 22:12:26 +0100 Subject: [PATCH 156/157] fixup! Use 'ChessBoardBuilder' in validation tests --- src/board/chess_board/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 99d6df6..aa00dd1 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -679,7 +679,7 @@ mod test { let mut builder = ChessBoardBuilder::new(); builder[Square::H1] = Some((Piece::King, Color::White)); builder[Square::H8] = Some((Piece::King, Color::Black)); - for square in (File::B.into_bitboard() | File::C.into_bitboard()).into_iter() { + 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()).into_iter() { From 47f532cd0934286e48d335c87fd1a41b3f7272d8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 22:12:26 +0100 Subject: [PATCH 157/157] fixup! Use 'ChessBoardBuilder' in validation tests --- src/board/chess_board/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index aa00dd1..2a98537 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -682,7 +682,7 @@ mod test { 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()).into_iter() { + for square in (File::F.into_bitboard() | File::G.into_bitboard()) { builder[square] = Some((Piece::Pawn, Color::Black)); } TryInto::::try_into(builder)