From 2ad2927b14232a7acaf8b7fdc14545684e5d236a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 19:56:01 +0200 Subject: [PATCH 001/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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/255] 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 500e6fd22f196c7df5998636929cc6ad78a22224 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 18:48:22 +0200 Subject: [PATCH 041/255] Add 'Color::slide_board_with_blockers' --- src/board/direction.rs | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/board/direction.rs b/src/board/direction.rs index 135f5f4..cfed3cb 100644 --- a/src/board/direction.rs +++ b/src/board/direction.rs @@ -121,7 +121,19 @@ 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 slided over. The slide is over once a square that is part of `blockers` is + /// reached. + /// It does not make sense to use this method with knight-only directions, and it will panic in + /// debug-mode if it happens. + #[inline(always)] + 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 +141,9 @@ impl Direction { while !board.is_empty() { board = self.move_board(board); res = res | board; + if !(board & blockers).is_empty() { + break; + } } res @@ -655,4 +670,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 3f4c23321f51f209a29c296c73af0445b4c9680c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:07:37 +0200 Subject: [PATCH 042/255] 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 d71df67..5f85d75 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 piece 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 piece 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) From 2c140d048127569ff6fff57f2b6fa3e066a9a771 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:18:46 +0200 Subject: [PATCH 043/255] 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 1a64929..2ff337b 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 a3c783a..16f5144 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 e8588eb..097e49a 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 337b8f61f445950ad6da171f90b52508f84e14a2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:19:38 +0200 Subject: [PATCH 044/255] Add 'NUM_VARIANTS' constant to all 'board' enums --- src/board/castle_rights.rs | 5 ++++- src/board/color.rs | 5 ++++- src/board/file.rs | 5 ++++- src/board/rank.rs | 5 ++++- src/board/square.rs | 5 ++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 0e44e03..8ed8764 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 5f85d75..09d017b 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 piece 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 2ff337b..55a138b 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, diff --git a/src/board/rank.rs b/src/board/rank.rs index 16f5144..70fad46 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, diff --git a/src/board/square.rs b/src/board/square.rs index 097e49a..a0a1009 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, From 2b9b637ab598d1c7a69f677d86dc6cd68f1b8ee1 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:07:16 +0200 Subject: [PATCH 045/255] Add 'Piece' enum --- src/board/mod.rs | 3 ++ src/board/piece.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 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..17ae913 --- /dev/null +++ b/src/board/piece.rs @@ -0,0 +1,68 @@ +/// 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. + #[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 fa68be533a5f7ef0d5483224ef87e49289c06600 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:25:29 +0200 Subject: [PATCH 046/255] Add 'Bitboard::iter_powerset' --- src/board/bitboard/mod.rs | 90 +++++++++++++++++++++++++++++++++- src/board/bitboard/superset.rs | 51 +++++++++++++++++++ 2 files changed, 140 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..9f86921 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,79 @@ 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) + .into_iter() + .map(Square::from_index) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs); + assert_eq!( + mask.iter_power_set().collect::>(), + (0..(1 << 6)) + .into_iter() + .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..46f1ad2 --- /dev/null +++ b/src/board/bitboard/superset.rs @@ -0,0 +1,51 @@ +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 mask. + mask: Bitboard, + /// The "index" of the next blocker set that should be generated. + current: usize, + /// The number of blocker sets that should be generated by [BlockerIterator], i.e: 2^n with n + /// the number of squares belonging to `mask`. + total: usize, +} + +impl BitboardPowerSetIterator { + pub fn new(mask: Bitboard) -> Self { + Self { + mask, + current: 0, + total: 1 << mask.count(), + } + } +} + +impl Iterator for BitboardPowerSetIterator { + type Item = Bitboard; + + fn next(&mut self) -> Option { + if self.current >= self.total { + None + } else { + let blockers = (0..) + .into_iter() + .zip(self.mask.into_iter()) + .filter(|(index, _)| self.current & (1 << index) != 0) + .map(|(_, board)| board) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs); + self.current += 1; + Some(blockers) + } + } + + fn size_hint(&self) -> (usize, Option) { + (self.total, Some(self.total)) + } +} + +impl ExactSizeIterator for BitboardPowerSetIterator {} + +impl std::iter::FusedIterator for BitboardPowerSetIterator {} From 8fdbdd1f610136630767693847234ddb43dbde2b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:43:18 +0200 Subject: [PATCH 047/255] 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 9f86921..91c3122 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 e3ea602bb9a3823e59d3cdea3229f7b8fe556b73 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:44:33 +0200 Subject: [PATCH 048/255] 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 a0a1009..b5a033c 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 8428ac00284631db06c7f1c98e539c4a8b9d13e5 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:22:37 +0200 Subject: [PATCH 049/255] Add naive king move generation --- src/lib.rs | 1 + src/movegen/king.rs | 224 ++++++++++++++++++++++++++++++++++++++++++++ src/movegen/mod.rs | 2 + 3 files changed, 227 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..932b4f4 --- /dev/null +++ b/src/movegen/king.rs @@ -0,0 +1,224 @@ +use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square}; + +// No castling moves included +#[allow(unused)] +pub fn king_moves(square: Square) -> Bitboard { + let board = square.into_bitboard(); + + Direction::iter_royalty() + .map(|dir| dir.move_board(board)) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) +} + +#[allow(unused)] +pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard { + let rank = color.first_rank(); + + 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 76f3c41110bc6cd521939446de0f3effc3304667 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:30:16 +0200 Subject: [PATCH 050/255] Add naive knight move generation --- src/movegen/knight.rs | 183 ++++++++++++++++++++++++++++++++++++++++++ src/movegen/mod.rs | 1 + 2 files changed, 184 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..4783bde --- /dev/null +++ b/src/movegen/knight.rs @@ -0,0 +1,183 @@ +use crate::board::{Bitboard, Direction, Square}; + +#[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 5101d8c285ea40a9c01cb10601341273b34793aa Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:33:33 +0200 Subject: [PATCH 051/255] Add naive bishop move generation --- src/movegen/bishop.rs | 69 +++++++++++++++++++++++++++++++++++++++++++ src/movegen/mod.rs | 1 + 2 files changed, 70 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..0fe0247 --- /dev/null +++ b/src/movegen/bishop.rs @@ -0,0 +1,69 @@ +use crate::board::{Bitboard, Direction, Square}; + +#[allow(unused)] +pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { + Direction::iter_bishop() + .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) + .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 dbe3539a66863cdcabd43798775d65c1ad4de245 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:33:45 +0200 Subject: [PATCH 052/255] Add naive rook move generation --- src/movegen/mod.rs | 1 + src/movegen/rook.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 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..31fd7d8 --- /dev/null +++ b/src/movegen/rook.rs @@ -0,0 +1,54 @@ +use crate::board::{Bitboard, Direction, Square}; + +#[allow(unused)] +pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { + Direction::iter_rook() + .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) + .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 847e18dac1994f1841d23902342c65058857a2fd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:36:28 +0200 Subject: [PATCH 053/255] Add naive pawn move generation --- src/movegen/mod.rs | 1 + src/movegen/pawn.rs | 136 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 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..5c929fa --- /dev/null +++ b/src/movegen/pawn.rs @@ -0,0 +1,136 @@ +use crate::board::{Bitboard, Color, Direction, Rank, Square}; + +#[allow(unused)] +pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { + return Bitboard::EMPTY; + } + + 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 + } +} + +#[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 +} + +#[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 6b0563b1bd1d8c1afd8fb629fed0e790f7b61ce4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:14:46 +0200 Subject: [PATCH 054/255] Add 'Magic' type --- src/movegen/magic/magic.rs | 23 +++++++++++++++++++++++ src/movegen/magic/mod.rs | 2 ++ src/movegen/mod.rs | 4 ++++ 3 files changed, 29 insertions(+) create mode 100644 src/movegen/magic/magic.rs create mode 100644 src/movegen/magic/mod.rs diff --git a/src/movegen/magic/magic.rs b/src/movegen/magic/magic.rs new file mode 100644 index 0000000..0f328d5 --- /dev/null +++ b/src/movegen/magic/magic.rs @@ -0,0 +1,23 @@ +use crate::board::Bitboard; + +/// A type representing the magic board indexing a given [Square]. +#[allow(unused)] // FIXME: remove once used +pub struct Magic { + /// Magic number. + magic: u64, + /// Base offset into the magic square table. + offset: usize, + /// Mask to apply to the blocker board before applying the magic. + mask: Bitboard, + /// Length of the resulting mask after applying the magic. + shift: u8, +} + +impl Magic { + #[allow(unused)] // FIXME: remove once used + pub fn get_index(&self, blockers: Bitboard) -> usize { + let relevant_occupancy = (blockers & self.mask).0; + let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; + base_index + self.offset + } +} diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs new file mode 100644 index 0000000..068c6e7 --- /dev/null +++ b/src/movegen/magic/mod.rs @@ -0,0 +1,2 @@ +pub mod magic; +pub use magic::*; 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 acbbfc7c8bc90db6217466c82e1c871b5306ca01 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:37:56 +0200 Subject: [PATCH 055/255] Add magic mask generation --- src/movegen/mod.rs | 3 +++ src/movegen/wizardry/mask.rs | 39 ++++++++++++++++++++++++++++++++++++ src/movegen/wizardry/mod.rs | 1 + 3 files changed, 43 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..13d7cd6 --- /dev/null +++ b/src/movegen/wizardry/mask.rs @@ -0,0 +1,39 @@ +use crate::board::{Bitboard, File, Rank, Square}; +use crate::movegen::bishop::bishop_moves; +use crate::movegen::rook::rook_moves; + +#[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 +} + +#[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 7a120cee83dc2c6753b1db22986469761c36a5f8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:53:22 +0200 Subject: [PATCH 056/255] Add 'random' dependency --- Cargo.lock | 9 +++++++++ Cargo.toml | 1 + 2 files changed, 10 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1e43342..e9dfdc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "random" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d13a3485349981c90c79112a11222c3e6e75de1d52b87a7525b3bf5361420f" + [[package]] name = "seer" version = "0.1.0" +dependencies = [ + "random", +] diff --git a/Cargo.toml b/Cargo.toml index f191c81..52a2217 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +random = "0.12.2" From c8d7c17711e6e23c44f9cb5aeb32cd44b3111ee5 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 16:34:35 +0200 Subject: [PATCH 057/255] Fix pre-commit check in CI Now that we have actual dependencies, we need to run 'pre-commit run' outside of the build sandbox. --- .drone.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.yml b/.drone.yml index 7037529..a6c1ce5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,9 +4,9 @@ type: exec name: abacus checks steps: -- name: flake check +- name: pre commit check commands: - - nix flake check + - nix develop . --command pre-commit run --all - name: package check commands: From ae1c3322d53e4aebe82e056df38597a62e48a332 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 16:40:56 +0200 Subject: [PATCH 058/255] Remove 'pre-commit' check from flake Since trying to run it results into a failure due to 'clippy' trying to fetch the network inside the build sandbox, remove it from the 'checks' for now, but do keep it around for its 'shellHook'. --- flake.nix | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/flake.nix b/flake.nix index 22a05e4..c8e7566 100644 --- a/flake.nix +++ b/flake.nix @@ -80,44 +80,42 @@ 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 - ''; + 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"; }; - 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; + }; - nixpkgs-fmt = { - enable = true; - }; - - rustfmt = { - enable = true; - entry = lib.mkForce "${rust-env}/bin/cargo-fmt fmt -- --check --color always"; - }; + rustfmt = { + enable = true; + entry = lib.mkForce "${rust-env}/bin/cargo-fmt fmt -- --check --color always"; }; }; - }; + }; + in + rec { devShells = { default = pkgs.mkShell { @@ -131,7 +129,7 @@ my-rust ]; - inherit (checks.pre-commit) shellHook; + inherit (pre-commit) shellHook; RUST_SRC_PATH = "${my-rust}/lib/rustlib/src/rust/library"; }; From 5bee69c38e633dd1251e4b7f8dd4a77902245b63 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 16:42:32 +0200 Subject: [PATCH 059/255] Add 'flake check' stage to CI Now that the 'pre-commit' is in another stage, add this one back in. --- .drone.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.drone.yml b/.drone.yml index a6c1ce5..8fb7774 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,6 +8,10 @@ steps: commands: - nix develop . --command pre-commit run --all +- name: flake check + commands: + - nix flake check + - name: package check commands: - nix build From 3f00c6d1fce4a83ac98418c137c1a8b7325ae646 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:41:09 +0200 Subject: [PATCH 060/255] Make 'Magic' fields 'pub(crate)' --- src/movegen/magic/magic.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/movegen/magic/magic.rs b/src/movegen/magic/magic.rs index 0f328d5..6fe0400 100644 --- a/src/movegen/magic/magic.rs +++ b/src/movegen/magic/magic.rs @@ -4,13 +4,13 @@ use crate::board::Bitboard; #[allow(unused)] // FIXME: remove once used 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 a04b1f3a42a65cde3b4d85291c570334b9f50c2d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:42:05 +0200 Subject: [PATCH 061/255] Add magic bitboard generation --- src/movegen/magic/magic.rs | 2 - src/movegen/wizardry/generation.rs | 68 ++++++++++++++++++++++++++++++ src/movegen/wizardry/mask.rs | 2 - src/movegen/wizardry/mod.rs | 1 + 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 src/movegen/wizardry/generation.rs diff --git a/src/movegen/magic/magic.rs b/src/movegen/magic/magic.rs index 6fe0400..f59bde2 100644 --- a/src/movegen/magic/magic.rs +++ b/src/movegen/magic/magic.rs @@ -1,7 +1,6 @@ use crate::board::Bitboard; /// A type representing the magic board indexing a given [Square]. -#[allow(unused)] // FIXME: remove once used pub struct Magic { /// Magic number. pub(crate) magic: u64, @@ -14,7 +13,6 @@ pub struct Magic { } impl Magic { - #[allow(unused)] // FIXME: remove once used pub fn get_index(&self, blockers: Bitboard) -> usize { let relevant_occupancy = (blockers & self.mask).0; let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs new file mode 100644 index 0000000..48e80e1 --- /dev/null +++ b/src/movegen/wizardry/generation.rs @@ -0,0 +1,68 @@ +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}; + +type MagicGenerationType = (Vec, Vec); + +#[allow(unused)] // FIXME: remove when used +pub fn generate_bishop_magics(rng: &mut dyn random::Source) -> MagicGenerationType { + generate_magics(rng, generate_bishop_mask, bishop_moves) +} + +#[allow(unused)] // FIXME: remove when used +pub fn generate_rook_magics(rng: &mut dyn random::Source) -> MagicGenerationType { + generate_magics(rng, generate_rook_mask, rook_moves) +} + +fn generate_magics( + rng: &mut dyn random::Source, + mask_fn: impl Fn(Square) -> Bitboard, + moves_fn: impl Fn(Square, Bitboard) -> Bitboard, +) -> MagicGenerationType { + let mut offset = 0; + + let mut magics = Vec::new(); + let mut boards = Vec::new(); + + for square in Square::iter() { + let mask = mask_fn(square); + let mut candidate: Magic; + let potential_occupancy: Vec<_> = mask.iter_power_set().collect(); + let moves_len = potential_occupancy.len(); + + 'candidate_search: loop { + candidate = Magic { + magic: magic_candidate(rng), + offset, + mask, + shift: (64 - mask.count()) as u8, + }; + let mut candidate_moves = Vec::new(); + candidate_moves.resize(moves_len, Bitboard::EMPTY); + + for occupancy in potential_occupancy.iter().cloned() { + let index = candidate.get_index(occupancy); + let moves = moves_fn(square, occupancy); + if candidate_moves[index] != Bitboard::EMPTY && candidate_moves[index] != moves { + continue 'candidate_search; + } + candidate_moves[index] = moves; + } + + boards.append(&mut candidate_moves); + break; + } + + magics.push(candidate); + offset += moves_len; + } + + (magics, boards) +} + +fn magic_candidate(rng: &mut dyn random::Source) -> u64 { + rng.read_u64() & rng.read_u64() & rng.read_u64() +} diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs index 13d7cd6..0c0bbdf 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -2,7 +2,6 @@ use crate::board::{Bitboard, File, Rank, Square}; use crate::movegen::bishop::bishop_moves; use crate::movegen::rook::rook_moves; -#[allow(unused)] // FIXME: remove once used pub fn generate_bishop_mask(square: Square) -> Bitboard { let rays = bishop_moves(square, Bitboard::EMPTY); @@ -14,7 +13,6 @@ pub fn generate_bishop_mask(square: Square) -> Bitboard { rays - mask } -#[allow(unused)] // FIXME: remove once used pub fn generate_rook_mask(square: Square) -> Bitboard { let rays = rook_moves(square, Bitboard::EMPTY); diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index f6053c6..8b5ba4e 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1 +1,2 @@ +mod generation; mod mask; From 1a0aa5fddb77a24b1ace7d0bf796b1ade6f9630a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:46:39 +0200 Subject: [PATCH 062/255] Remove superfluous 'format!' call --- 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 b5a033c..0d58c2b 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -18,7 +18,7 @@ pub enum Square { impl std::fmt::Display for Square { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", format!("{:?}", self)) + write!(f, "{:?}", self) } } From 2eb7e4c8ef30a49418b58c3f9abab516c3402e57 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:46:56 +0200 Subject: [PATCH 063/255] Allow some clippy warnings --- src/board/bitboard/mod.rs | 2 ++ src/board/square.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 91c3122..d5ca8eb 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -19,6 +19,7 @@ impl Bitboard { pub const ALL: Bitboard = Bitboard(u64::MAX); /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. + #[allow(clippy::unusual_byte_groupings)] pub const RANKS: [Self; 8] = [ Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010), @@ -31,6 +32,7 @@ impl Bitboard { ]; /// Array of bitboards representing the eight files, in order from file A to file H. + #[allow(clippy::unusual_byte_groupings)] pub const FILES: [Self; 8] = [ Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111), Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000), diff --git a/src/board/square.rs b/src/board/square.rs index 0d58c2b..97d9bdc 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -109,6 +109,7 @@ impl std::ops::Shl for Square { #[inline(always)] fn shl(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] Square::from_index(self as usize + rhs) } } @@ -119,6 +120,7 @@ impl std::ops::Shr for Square { #[inline(always)] fn shr(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] Square::from_index(self as usize - rhs) } } From 7dbe48ad230a7cc770ceb9576f8f949aea4ba9aa Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:49:16 +0200 Subject: [PATCH 064/255] Add '# Safety' section to 'unsafe fn' doc --- src/board/castle_rights.rs | 4 ++++ src/board/color.rs | 4 ++++ src/board/file.rs | 4 ++++ src/board/piece.rs | 4 ++++ src/board/rank.rs | 4 ++++ src/board/square.rs | 4 ++++ 6 files changed, 24 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 8ed8764..d727fd8 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -26,6 +26,10 @@ impl CastleRights { } /// Convert from a castle rights index into a [CastleRights] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [CastleRights::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/color.rs b/src/board/color.rs index 09d017b..07024ee 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -20,6 +20,10 @@ impl Color { } /// Convert from a piece index into a [Color] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Color::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/file.rs b/src/board/file.rs index 55a138b..4c84f20 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -43,6 +43,10 @@ impl File { } /// Convert from a file index into a [File] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [File::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/piece.rs b/src/board/piece.rs index 17ae913..58f989a 100644 --- a/src/board/piece.rs +++ b/src/board/piece.rs @@ -36,6 +36,10 @@ impl Piece { } /// Convert from a piece index into a [Piece] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Piece::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/rank.rs b/src/board/rank.rs index 70fad46..a0b04d3 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -43,6 +43,10 @@ impl Rank { } /// Convert from a rank index into a [Rank] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Rank::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) diff --git a/src/board/square.rs b/src/board/square.rs index 97d9bdc..9ffa824 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -59,6 +59,10 @@ impl Square { } /// Convert from a square index into a [Square] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Square::index()]. #[inline(always)] pub unsafe fn from_index_unchecked(index: usize) -> Self { std::mem::transmute(index as u8) From d97e7d646e721b691b01069b77bac0c59b60b76f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:50:44 +0200 Subject: [PATCH 065/255] Move 'Magic' into 'seer::movegen::magic' --- src/movegen/magic/magic.rs | 21 --------------------- src/movegen/magic/mod.rs | 23 +++++++++++++++++++++-- src/movegen/wizardry/generation.rs | 24 ++++++++++++------------ 3 files changed, 33 insertions(+), 35 deletions(-) delete mode 100644 src/movegen/magic/magic.rs diff --git a/src/movegen/magic/magic.rs b/src/movegen/magic/magic.rs deleted file mode 100644 index f59bde2..0000000 --- a/src/movegen/magic/magic.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::board::Bitboard; - -/// A type representing the magic board indexing a given [Square]. -pub struct Magic { - /// Magic number. - pub(crate) magic: u64, - /// Base offset into the magic square table. - pub(crate) offset: usize, - /// Mask to apply to the blocker board before applying the magic. - pub(crate) mask: Bitboard, - /// Length of the resulting mask after applying the magic. - pub(crate) shift: u8, -} - -impl Magic { - pub fn get_index(&self, blockers: Bitboard) -> usize { - let relevant_occupancy = (blockers & self.mask).0; - let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; - base_index + self.offset - } -} diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs index 068c6e7..251a5d9 100644 --- a/src/movegen/magic/mod.rs +++ b/src/movegen/magic/mod.rs @@ -1,2 +1,21 @@ -pub mod magic; -pub use magic::*; +use crate::board::Bitboard; + +/// A type representing the magic board indexing a given [crate::board::Square]. +pub struct Magic { + /// Magic number. + pub(crate) magic: u64, + /// Base offset into the magic square table. + pub(crate) offset: usize, + /// Mask to apply to the blocker board before applying the magic. + pub(crate) mask: Bitboard, + /// Length of the resulting mask after applying the magic. + pub(crate) shift: u8, +} + +impl Magic { + pub fn get_index(&self, blockers: Bitboard) -> usize { + let relevant_occupancy = (blockers & self.mask).0; + let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; + base_index + self.offset + } +} diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs index 48e80e1..ee75d57 100644 --- a/src/movegen/wizardry/generation.rs +++ b/src/movegen/wizardry/generation.rs @@ -22,42 +22,42 @@ fn generate_magics( mask_fn: impl Fn(Square) -> Bitboard, moves_fn: impl Fn(Square, Bitboard) -> Bitboard, ) -> MagicGenerationType { - let mut offset = 0; - let mut magics = Vec::new(); let mut boards = Vec::new(); for square in Square::iter() { let mask = mask_fn(square); let mut candidate: Magic; - let potential_occupancy: Vec<_> = mask.iter_power_set().collect(); - let moves_len = potential_occupancy.len(); + + let occupancy_to_moves: Vec<_> = mask + .iter_power_set() + .map(|occupancy| (occupancy, moves_fn(square, occupancy))) + .collect(); 'candidate_search: loop { candidate = Magic { magic: magic_candidate(rng), - offset, + offset: 0, mask, shift: (64 - mask.count()) as u8, }; - let mut candidate_moves = Vec::new(); - candidate_moves.resize(moves_len, Bitboard::EMPTY); + let mut candidate_moves = vec![Bitboard::EMPTY; occupancy_to_moves.len()]; - for occupancy in potential_occupancy.iter().cloned() { + for (occupancy, moves) in occupancy_to_moves.iter().cloned() { let index = candidate.get_index(occupancy); - let moves = moves_fn(square, occupancy); + // Non-constructive collision, try with another candidate if candidate_moves[index] != Bitboard::EMPTY && candidate_moves[index] != moves { continue 'candidate_search; } candidate_moves[index] = moves; } + // We have filled all candidate boards, record the correct offset and add the moves + candidate.offset = boards.len(); boards.append(&mut candidate_moves); + magics.push(candidate); break; } - - magics.push(candidate); - offset += moves_len; } (magics, boards) From 0222ec4c2da738bb59f4f521283f80e3dd447b5e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 23 Jul 2022 15:32:56 +0200 Subject: [PATCH 066/255] 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 07024ee..ca87ade 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -11,6 +11,13 @@ impl Color { /// The number of [Color] variants. pub const NUM_VARIANTS: usize = 2; + const ALL: [Self; Self::NUM_VARIANTS] = [Self::White, Self::Black]; + + /// Iterate over all colors in order. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + /// Convert from a piece index into a [Color] type. #[inline(always)] pub fn from_index(index: usize) -> Self { From d2eda07036d9d0cf6be5c5ed7b8ebe28ea0532ff Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 13:40:01 +0200 Subject: [PATCH 067/255] 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 8289204e4ba6052c36e4a3119e3d8138c0b02974 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 13:40:01 +0200 Subject: [PATCH 068/255] Generate magic tables with build script --- Cargo.toml | 11 +++++ src/build.rs | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/build.rs diff --git a/Cargo.toml b/Cargo.toml index 52a2217..756e025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,19 @@ name = "seer" version = "0.1.0" edition = "2021" +build = "src/build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] random = "0.12.2" + +[build-dependencies] +random = "0.12.2" + +# Optimize build scripts to shorten compile times. +[profile.dev.build-override] +opt-level = 3 + +[profile.release.build-override] +opt-level = 3 diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..29721d1 --- /dev/null +++ b/src/build.rs @@ -0,0 +1,133 @@ +use std::io::{Result, Write}; + +pub mod board; +pub mod movegen; +pub mod utils; + +use crate::{ + board::{Bitboard, Color, File, Square}, + movegen::{ + king::king_moves, + knight::knight_moves, + pawn::{pawn_captures, pawn_moves}, + wizardry::generation::{generate_bishop_magics, generate_rook_magics}, + Magic, + }, +}; + +fn print_magics(out: &mut dyn Write, var_name: &str, magics: &[Magic]) -> Result<()> { + writeln!(out, "static {}: [Magic; {}] = [", var_name, magics.len())?; + for magic in magics.iter() { + writeln!( + out, + " Magic{{magic: {}, offset: {}, mask: Bitboard({}), shift: {},}},", + magic.magic, magic.offset, magic.mask.0, magic.shift + )?; + } + writeln!(out, "];")?; + Ok(()) +} + +fn print_boards(out: &mut dyn Write, var_name: &str, boards: &[Bitboard]) -> Result<()> { + writeln!(out, "static {}: [Bitboard; {}] = [", var_name, boards.len())?; + for board in boards.iter().cloned() { + writeln!(out, " Bitboard({}),", board.0)?; + } + writeln!(out, "];")?; + Ok(()) +} + +fn print_double_sided_boards( + out: &mut dyn Write, + var_name: &str, + white_boards: &[Bitboard], + black_boards: &[Bitboard], +) -> Result<()> { + assert_eq!(white_boards.len(), black_boards.len()); + writeln!( + out, + "static {}: [[Bitboard; {}]; 2] = [", + var_name, + white_boards.len() + )?; + for color in Color::iter() { + let boards = if color == Color::White { + white_boards + } else { + black_boards + }; + writeln!(out, " [")?; + for square in Square::iter() { + writeln!(out, " Bitboard({}),", boards[square.index()].0)?; + } + writeln!(out, " ],")?; + } + writeln!(out, "];")?; + Ok(()) +} + +#[allow(clippy::redundant_clone)] +fn main() -> Result<()> { + // FIXME: rerun-if-changed directives + + let out_dir = std::env::var_os("OUT_DIR").unwrap(); + let magic_path = std::path::Path::new(&out_dir).join("magic_tables.rs"); + let mut out = std::fs::File::create(&magic_path).unwrap(); + + let rng = random::default().seed([12, 27]); + + { + let (magics, moves) = generate_bishop_magics(&mut rng.clone()); + print_magics(&mut out, "BISHOP_MAGICS", &magics)?; + print_boards(&mut out, "BISHOP_MOVES", &moves)?; + } + + { + let (magics, moves) = generate_rook_magics(&mut rng.clone()); + print_magics(&mut out, "ROOK_MAGICS", &magics)?; + print_boards(&mut out, "ROOK_MOVES", &moves)?; + } + + { + let moves: Vec<_> = Square::iter().map(knight_moves).collect(); + print_boards(&mut out, "KNIGHT_MOVES", &moves)?; + } + + { + let white_moves: Vec<_> = Square::iter() + .map(|square| pawn_moves(Color::White, square, Bitboard::EMPTY)) + .collect(); + let black_moves: Vec<_> = Square::iter() + .map(|square| pawn_moves(Color::Black, square, Bitboard::EMPTY)) + .collect(); + print_double_sided_boards(&mut out, "PAWN_MOVES", &white_moves, &black_moves)?; + let white_attacks: Vec<_> = Square::iter() + .map(|square| pawn_captures(Color::White, square)) + .collect(); + let black_attacks: Vec<_> = Square::iter() + .map(|square| pawn_captures(Color::Black, square)) + .collect(); + print_double_sided_boards(&mut out, "PAWN_ATTACKS", &white_attacks, &black_attacks)?; + } + + { + let moves: Vec<_> = Square::iter().map(king_moves).collect(); + print_boards(&mut out, "KING_MOVES", &moves)?; + let king_blockers: Vec<_> = Color::iter() + .map(|color| { + Square::new(File::F, color.first_rank()) | Square::new(File::G, color.first_rank()) + }) + .collect(); + let queen_blockers: Vec<_> = Color::iter() + .map(|color| { + Square::new(File::B, color.first_rank()) + | Square::new(File::C, color.first_rank()) + | Square::new(File::D, color.first_rank()) + }) + .collect(); + print_boards(&mut out, "KING_SIDE_CASTLE_BLOCKERS", &king_blockers)?; + print_boards(&mut out, "QUEEN_SIDE_CASTLE_BLOCKERS", &queen_blockers)?; + } + + Ok(()) +} From bd435351924ebf5200311b33f5d7ad42c71f106c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:24:35 +0200 Subject: [PATCH 069/255] Move naive move generation into sub-module --- src/build.rs | 8 +++++--- src/movegen/mod.rs | 8 ++------ src/movegen/{ => naive}/bishop.rs | 0 src/movegen/{ => naive}/king.rs | 0 src/movegen/{ => naive}/knight.rs | 0 src/movegen/naive/mod.rs | 5 +++++ src/movegen/{ => naive}/pawn.rs | 0 src/movegen/{ => naive}/rook.rs | 0 src/movegen/wizardry/generation.rs | 3 +-- src/movegen/wizardry/mask.rs | 3 +-- 10 files changed, 14 insertions(+), 13 deletions(-) rename src/movegen/{ => naive}/bishop.rs (100%) rename src/movegen/{ => naive}/king.rs (100%) 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/build.rs b/src/build.rs index 29721d1..e05410d 100644 --- a/src/build.rs +++ b/src/build.rs @@ -7,9 +7,11 @@ pub mod utils; use crate::{ board::{Bitboard, Color, File, Square}, movegen::{ - king::king_moves, - knight::knight_moves, - pawn::{pawn_captures, pawn_moves}, + naive::{ + king::king_moves, + knight::knight_moves, + pawn::{pawn_captures, pawn_moves}, + }, wizardry::generation::{generate_bishop_magics, generate_rook_magics}, Magic, }, 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 100% rename from src/movegen/king.rs rename to src/movegen/naive/king.rs 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..f1bbe3e --- /dev/null +++ b/src/movegen/naive/mod.rs @@ -0,0 +1,5 @@ +pub mod bishop; +pub mod king; +pub mod knight; +pub mod pawn; +pub mod rook; diff --git a/src/movegen/pawn.rs b/src/movegen/naive/pawn.rs similarity index 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 ee75d57..173eef0 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::bishop_moves, rook::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 0c0bbdf..eed93a0 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}; pub fn generate_bishop_mask(square: Square) -> Bitboard { let rays = bishop_moves(square, Bitboard::EMPTY); From d2c61a81b5b9302ad01eaa8ad64637f5c15e90bd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:31:26 +0200 Subject: [PATCH 070/255] Make use of generated move tables --- src/build.rs | 3 ++ src/movegen/magic/mod.rs | 5 +++ src/movegen/magic/moves.rs | 71 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 src/movegen/magic/moves.rs diff --git a/src/build.rs b/src/build.rs index e05410d..29265f5 100644 --- a/src/build.rs +++ b/src/build.rs @@ -131,5 +131,8 @@ fn main() -> Result<()> { print_boards(&mut out, "QUEEN_SIDE_CASTLE_BLOCKERS", &queen_blockers)?; } + // Include the generated files now that the build script has run. + println!("cargo:rustc-cfg=generated_boards"); + Ok(()) } diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs index 251a5d9..8b3f605 100644 --- a/src/movegen/magic/mod.rs +++ b/src/movegen/magic/mod.rs @@ -19,3 +19,8 @@ impl Magic { base_index + self.offset } } + +#[cfg(generated_boards)] +mod moves; +#[cfg(generated_boards)] +pub use moves::*; diff --git a/src/movegen/magic/moves.rs b/src/movegen/magic/moves.rs new file mode 100644 index 0000000..2901b28 --- /dev/null +++ b/src/movegen/magic/moves.rs @@ -0,0 +1,71 @@ +use super::Magic; +use crate::board::{Bitboard, Color, Square}; + +include!(concat!(env!("OUT_DIR"), "/magic_tables.rs")); + +pub fn quiet_pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + // If there is a piece in front of the pawn, it can't advance + if !(color.backward_direction().move_board(blockers) & square).is_empty() { + return Bitboard::EMPTY; + } + // SAFETY: we know the values are in-bounds + unsafe { + *PAWN_MOVES + .get_unchecked(color.index()) + .get_unchecked(square.index()) + } +} + +pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + // SAFETY: we know the values are in-bounds + let attacks = unsafe { + *PAWN_ATTACKS + .get_unchecked(color.index()) + .get_unchecked(square.index()) + }; + quiet_pawn_moves(color, square, blockers) | attacks +} + +pub fn knight_moves(square: Square) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { *KNIGHT_MOVES.get_unchecked(square.index()) } +} + +pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { + let index = BISHOP_MAGICS + .get_unchecked(square.index()) + .get_index(blockers); + *BISHOP_MOVES.get_unchecked(index) + } +} + +pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { + let index = ROOK_MAGICS + .get_unchecked(square.index()) + .get_index(blockers); + *ROOK_MOVES.get_unchecked(index) + } +} + +pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard { + bishop_moves(square, blockers) | rook_moves(square, blockers) +} + +pub fn king_moves(square: Square) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { *KING_MOVES.get_unchecked(square.index()) } +} + +pub fn king_side_castle_blockers(color: Color) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { *KING_SIDE_CASTLE_BLOCKERS.get_unchecked(color.index()) } +} + +pub fn queen_side_castle_blockers(color: Color) -> Bitboard { + // SAFETY: we know the values are in-bounds + unsafe { *QUEEN_SIDE_CASTLE_BLOCKERS.get_unchecked(color.index()) } +} From 915244b23849035fec39867783a7c32104428fb2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:33:06 +0200 Subject: [PATCH 071/255] Add 'rerun-if-changed' directives to build script --- src/build.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/build.rs b/src/build.rs index 29265f5..10d7a94 100644 --- a/src/build.rs +++ b/src/build.rs @@ -134,5 +134,10 @@ fn main() -> Result<()> { // Include the generated files now that the build script has run. println!("cargo:rustc-cfg=generated_boards"); + // Run the build script only if something in move generation might have changed. + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=movegen/naive/"); + println!("cargo:rerun-if-changed=movegen/wizardry/"); + Ok(()) } From 02d48fe52621fc3a6efab951006b9a1dccb77afd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:43:18 +0200 Subject: [PATCH 072/255] Remove all useless 'allow(unused)' --- src/movegen/naive/bishop.rs | 1 - src/movegen/naive/king.rs | 2 -- src/movegen/naive/knight.rs | 1 - src/movegen/naive/pawn.rs | 3 --- src/movegen/naive/rook.rs | 1 - 5 files changed, 8 deletions(-) diff --git a/src/movegen/naive/bishop.rs b/src/movegen/naive/bishop.rs index 0fe0247..0806077 100644 --- a/src/movegen/naive/bishop.rs +++ b/src/movegen/naive/bishop.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Direction, Square}; -#[allow(unused)] pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { Direction::iter_bishop() .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) diff --git a/src/movegen/naive/king.rs b/src/movegen/naive/king.rs index 932b4f4..6e98df7 100644 --- a/src/movegen/naive/king.rs +++ b/src/movegen/naive/king.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square}; // No castling moves included -#[allow(unused)] pub fn king_moves(square: Square) -> Bitboard { let board = square.into_bitboard(); @@ -10,7 +9,6 @@ pub fn king_moves(square: Square) -> Bitboard { .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) } -#[allow(unused)] pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard { let rank = color.first_rank(); diff --git a/src/movegen/naive/knight.rs b/src/movegen/naive/knight.rs index 4783bde..f850d71 100644 --- a/src/movegen/naive/knight.rs +++ b/src/movegen/naive/knight.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Direction, Square}; -#[allow(unused)] pub fn knight_moves(square: Square) -> Bitboard { let board = square.into_bitboard(); diff --git a/src/movegen/naive/pawn.rs b/src/movegen/naive/pawn.rs index 5c929fa..55b5bf6 100644 --- a/src/movegen/naive/pawn.rs +++ b/src/movegen/naive/pawn.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Color, Direction, Rank, Square}; -#[allow(unused)] pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { return Bitboard::EMPTY; @@ -22,7 +21,6 @@ pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard } } -#[allow(unused)] pub fn pawn_captures(color: Color, square: Square) -> Bitboard { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { return Bitboard::EMPTY; @@ -38,7 +36,6 @@ pub fn pawn_captures(color: Color, square: Square) -> Bitboard { attack_west | attack_east } -#[allow(unused)] pub fn en_passant_origins(square: Square) -> Bitboard { let board = square.into_bitboard(); diff --git a/src/movegen/naive/rook.rs b/src/movegen/naive/rook.rs index 31fd7d8..0b06cef 100644 --- a/src/movegen/naive/rook.rs +++ b/src/movegen/naive/rook.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Direction, Square}; -#[allow(unused)] pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { Direction::iter_rook() .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) From 80e3ace8fc64bf047300f011ba0d29679f1034b4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 17:34:11 +0200 Subject: [PATCH 073/255] Add '*Assign' operators to 'Bitboard' --- src/board/bitboard/mod.rs | 80 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index d5ca8eb..8eff5e1 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -117,6 +117,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; @@ -147,6 +163,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; @@ -167,6 +199,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; @@ -187,6 +235,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; @@ -207,6 +271,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; From e7e59279020787bb1f00f49b86dfd0f91ce0516a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 19:12:51 +0200 Subject: [PATCH 074/255] Add 'Move' --- src/board/mod.rs | 3 + src/board/move.rs | 231 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 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..0372a47 --- /dev/null +++ b/src/board/move.rs @@ -0,0 +1,231 @@ +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)] + 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 king castling. + #[inline(always)] + pub fn is_castling(self) -> bool { + let index = (self.0 >> shift::CASTLING) & shift::CASTLING_MASK; + index != 0 + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn builder_simple() { + let chess_move: Move = MoveBuilder { + piece: Piece::Queen, + start: Square::A2, + destination: Square::A3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(); + assert_eq!(chess_move.piece(), Piece::Queen); + assert_eq!(chess_move.start(), Square::A2); + assert_eq!(chess_move.destination(), Square::A3); + assert_eq!(chess_move.capture(), None); + assert_eq!(chess_move.promotion(), None); + assert!(!chess_move.is_en_passant()); + assert!(!chess_move.is_double_step()); + assert!(!chess_move.is_castling()); + } + + #[test] + fn builder_all_fields() { + let chess_move: Move = MoveBuilder { + piece: Piece::Pawn, + start: Square::A7, + destination: Square::B8, + capture: Some(Piece::Queen), + promotion: Some(Piece::Knight), + en_passant: true, + double_step: true, + castling: true, + } + .into(); + assert_eq!(chess_move.piece(), Piece::Pawn); + assert_eq!(chess_move.start(), Square::A7); + assert_eq!(chess_move.destination(), Square::B8); + assert_eq!(chess_move.capture(), Some(Piece::Queen)); + assert_eq!(chess_move.promotion(), Some(Piece::Knight)); + assert!(chess_move.is_en_passant()); + assert!(chess_move.is_double_step()); + assert!(chess_move.is_castling()); + } +} From 8102b08cf0936bb530ad692cae631549e17d9c18 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:26:39 +0200 Subject: [PATCH 075/255] 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 d727fd8..b8d8f2b 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)] + pub fn add(self, to_remove: CastleRights) -> Self { + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(self.index() | to_remove.index()) } + } + /// Remove king-side castling rights. #[inline(always)] pub fn without_king_side(self) -> Self { From 7e23cb8f777d7c0b50df3c874109f52ef78822bd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:24:24 +0200 Subject: [PATCH 076/255] Introduce 'Error' enum --- src/build.rs | 1 + src/error.rs | 16 ++++++++++++++++ src/lib.rs | 1 + 3 files changed, 18 insertions(+) create mode 100644 src/error.rs diff --git a/src/build.rs b/src/build.rs index 10d7a94..c0bcdc6 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,6 +1,7 @@ use std::io::{Result, Write}; pub mod board; +pub mod error; pub mod movegen; pub mod utils; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..535bd7b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,16 @@ +/// A singular type for all errors that could happen when using this library. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Error { + InvalidFen, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let error_msg = match self { + Self::InvalidFen => "Invalid FEN input", + }; + write!(f, "{}", error_msg) + } +} + +impl std::error::Error for Error {} diff --git a/src/lib.rs b/src/lib.rs index bfcf0bd..c1b793f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod board; +pub mod error; pub mod movegen; pub mod utils; From dde5b69f81a3162df5c0de979625a6ca7727a0b4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:25:35 +0200 Subject: [PATCH 077/255] Add 'FromFen' trait --- src/board/fen.rs | 6 ++++++ src/board/mod.rs | 3 +++ 2 files changed, 9 insertions(+) create mode 100644 src/board/fen.rs diff --git a/src/board/fen.rs b/src/board/fen.rs new file mode 100644 index 0000000..f112bc9 --- /dev/null +++ b/src/board/fen.rs @@ -0,0 +1,6 @@ +/// A trait to mark items that can be converted from a FEN input. +pub trait FromFen: Sized { + type Err; + + fn from_fen(s: &str) -> Result; +} diff --git a/src/board/mod.rs b/src/board/mod.rs index d23ef25..b4e746b 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -10,6 +10,9 @@ pub use color::*; pub mod direction; pub use direction::*; +pub mod fen; +pub use fen::*; + pub mod file; pub use file::*; From 611e12c033d3e804c7554a03683053400b4ab1ef Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:38:51 +0200 Subject: [PATCH 078/255] 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 ca87ade..6e828a6 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 6f0e2f732bbcfd92d7f4c0b896bd2fcc1d5db25d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:15 +0200 Subject: [PATCH 079/255] Add FEN side to move parsing --- 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 6e828a6..828b1cf 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -1,4 +1,5 @@ -use super::{Direction, Rank}; +use super::{Direction, FromFen, Rank}; +use crate::error::Error; /// An enum representing the color of a player. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -106,6 +107,20 @@ impl Color { } } +/// Convert a side to move segment of a FEN string to a [Color]. +impl FromFen for Color { + type Err = Error; + + fn from_fen(s: &str) -> Result { + let res = match s { + "w" => Color::White, + "b" => Color::Black, + _ => return Err(Error::InvalidFen), + }; + Ok(res) + } +} + impl std::ops::Not for Color { type Output = Color; From dba4d94e354cb5713047b4756c2ef0388a0170a2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:38 +0200 Subject: [PATCH 080/255] Add FEN en-passant target square parsing --- src/board/square.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/board/square.rs b/src/board/square.rs index 9ffa824..9fbd5ee 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -1,5 +1,5 @@ -use super::{Bitboard, File, Rank}; -use crate::utils::static_assert; +use super::{Bitboard, File, FromFen, Rank}; +use crate::{error::Error, utils::static_assert}; /// Represent a square on a chessboard. Defined in the same order as the /// [Bitboard] squares. @@ -107,6 +107,23 @@ impl Square { } } +/// Convert an en-passant target square segment of a FEN string to an optional [Square]. +impl FromFen for Option { + type Err = Error; + + fn from_fen(s: &str) -> Result { + let res = match s.as_bytes() { + [b'-'] => None, + [file @ b'a'..=b'h', rank @ b'1'..=b'8'] => Some(Square::new( + File::from_index((file - b'a') as usize), + Rank::from_index((rank - b'1') as usize), + )), + _ => return Err(Error::InvalidFen), + }; + Ok(res) + } +} + /// Shift the square's index left by the amount given. impl std::ops::Shl for Square { type Output = Square; From 7df442e03cd9cb482b56970c705c07865dd9a668 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:55 +0200 Subject: [PATCH 081/255] Add FEN piece type parsing --- src/board/piece.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/board/piece.rs b/src/board/piece.rs index 58f989a..6c35288 100644 --- a/src/board/piece.rs +++ b/src/board/piece.rs @@ -1,3 +1,6 @@ +use super::FromFen; +use crate::error::Error; + /// An enum representing the type of a piece. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Piece { @@ -52,6 +55,24 @@ impl Piece { } } +/// Convert a piece in FEN notation to a [Piece]. +impl FromFen for Piece { + type Err = Error; + + fn from_fen(s: &str) -> Result { + let res = match s { + "p" | "P" => Self::Pawn, + "n" | "N" => Self::Knight, + "b" | "B" => Self::Bishop, + "r" | "R" => Self::Rook, + "q" | "Q" => Self::Queen, + "k" | "K" => Self::King, + _ => return Err(Error::InvalidFen), + }; + Ok(res) + } +} + #[cfg(test)] mod test { use super::*; From 76577718d8701b21bd67118532f61c87a2367d89 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:41:08 +0200 Subject: [PATCH 082/255] Add FEN castling rights parsing --- src/board/castle_rights.rs | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index b8d8f2b..a48bf4d 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -1,4 +1,5 @@ -use super::{Bitboard, Color, File, Square}; +use super::{Bitboard, Color, File, FromFen, Square}; +use crate::error::Error; /// Current castle rights for a player. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -108,6 +109,39 @@ impl CastleRights { } } +/// Convert the castling rights segment of a FEN string to an array of [CastleRights]. +impl FromFen for [CastleRights; 2] { + type Err = Error; + + fn from_fen(s: &str) -> Result { + if s.len() > 4 { + return Err(Error::InvalidFen); + } + + let mut res = [CastleRights::NoSide; 2]; + + if s == "-" { + return Ok(res); + } + + for b in s.chars() { + let color = if b.is_uppercase() { + Color::White + } else { + Color::Black + }; + let rights = &mut res[color.index()]; + match b { + 'k' | 'K' => *rights = rights.with_king_side(), + 'q' | 'Q' => *rights = rights.with_queen_side(), + _ => return Err(Error::InvalidFen), + } + } + + Ok(res) + } +} + #[cfg(test)] mod test { use super::*; From 08ff8db0ac5cec2909841ab61341f647e15318c2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:17:12 +0200 Subject: [PATCH 083/255] Add 'Error::InvalidPosition' variant --- src/error.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/error.rs b/src/error.rs index 535bd7b..d4a2004 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,12 +2,14 @@ #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Error { InvalidFen, + InvalidPosition, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let error_msg = match self { Self::InvalidFen => "Invalid FEN input", + Self::InvalidPosition => "Invalid position", }; write!(f, "{}", error_msg) } From f633c6e224ec33c5c24c60c3e58531b2becebeac Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:19:08 +0200 Subject: [PATCH 084/255] Mark'Error' as non-exhaustive This simplifies semantic versionning constraints. --- src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/error.rs b/src/error.rs index d4a2004..f95a38c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,6 @@ /// A singular type for all errors that could happen when using this library. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] pub enum Error { InvalidFen, InvalidPosition, From 384f361da2d76bd195924c6a37edd1e3c26f82ec Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:19:40 +0200 Subject: [PATCH 085/255] Add 'ChessBoard' --- src/board/chess_board.rs | 187 +++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 + 2 files changed, 190 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..178c54f --- /dev/null +++ b/src/board/chess_board.rs @@ -0,0 +1,187 @@ +use crate::error::Error; + +use super::{Bitboard, CastleRights, Color, File, FromFen, Move, Piece, Rank, Square}; + +/// Represent an on-going chess game. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ChessBoard { + /// A [Bitboard] of occupancy for each piece type, discarding color. Indexed by [Piece::index]. + piece_occupancy: [Bitboard; Piece::NUM_VARIANTS], + /// A [Bitboard] of occupancy for each color, discarding piece type. Indexed by [Piece::index]. + color_occupancy: [Bitboard; Color::NUM_VARIANTS], + /// A [Bitboard] representing all squares currently occupied by a piece. + combined_occupancy: Bitboard, + /// The allowed [CastleRights] for either color. Indexed by [Color::index]. + castle_rights: [CastleRights; Color::NUM_VARIANTS], + /// A potential en-passant attack. + /// Either `None` if no 2-square pawn move was made in the previous half-turn, or + /// `Some(target_square)` if a 2-square move was made. + en_passant: Option, + /// The number of half-turns without either a pawn push or capture. + half_move_clock: u8, // Should never go higher than 50. + /// The number of half-turns so far. + total_plies: u32, // Should be plenty. + /// The current player turn. + side: Color, +} + +pub struct NonReversibleState { + castle_rights: [CastleRights; Color::NUM_VARIANTS], + en_passant: Option, + half_move_clock: u8, // Should never go higher than 50. +} + +impl ChessBoard { + /// Which player's turn is it. + #[inline(always)] + pub fn current_player(&self) -> Color { + self.side + } + + /// Return the [Square] currently occupied by a pawn that can be captured en-passant, or `None` + #[inline(always)] + pub fn en_passant(&self) -> Option { + self.en_passant + } + + /// Return the [CastleRights] for the given [Color]. + #[inline(always)] + pub fn castle_rights(&self, color: Color) -> CastleRights { + // SAFETY: we know the value is in-bounds + unsafe { *self.castle_rights.get_unchecked(color.index()) } + } + + /// Return the [CastleRights] for the given [Color]. Allow mutations. + #[inline(always)] + fn castle_rights_mut(&mut self, color: Color) -> &mut CastleRights { + // SAFETY: we know the value is in-bounds + unsafe { &mut *self.castle_rights.get_unchecked_mut(color.index()) } + } + + /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. + #[inline(always)] + pub fn piece_occupancy(&self, piece: Piece) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *self.piece_occupancy.get_unchecked(piece.index()) } + } + + /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. + /// Allow mutating the state. + #[inline(always)] + fn piece_occupancy_mut(&mut self, piece: Piece) -> &mut Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { &mut *self.piece_occupancy.get_unchecked_mut(piece.index()) } + } + + /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece + /// type. + #[inline(always)] + pub fn color_occupancy(&self, color: Color) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *self.color_occupancy.get_unchecked(color.index()) } + } + + /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece + /// type. Allow mutating the state. + #[inline(always)] + fn color_occupancy_mut(&mut self, color: Color) -> &mut Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { &mut *self.color_occupancy.get_unchecked_mut(color.index()) } + } + + /// Get the [Bitboard] representing all pieces on the board. + #[inline(always)] + pub fn combined_occupancy(&self) -> Bitboard { + self.combined_occupancy + } + + /// Return the number of half-turns without either a pawn push or a capture. + #[inline(always)] + pub fn half_move_clock(&self) -> u8 { + self.half_move_clock + } + + /// Return the total number of plies (i.e: half-turns) played so far. + #[inline(always)] + pub fn total_plies(&self) -> u32 { + self.total_plies + } + + /// Quickly do and undo a move on the [Bitboard]s that are part of the [ChessBoard] state. Does + /// not account for all non-revertible changes such as en-passant state or half-move clock. + #[inline(always)] + fn xor(&mut self, color: Color, piece: Piece, start_end: Bitboard) { + *self.piece_occupancy_mut(piece) ^= start_end; + *self.color_occupancy_mut(color) ^= start_end; + self.combined_occupancy ^= start_end; + } + + /// Play the given [Move], returning all non-revertible state (e.g: en-passant, etc...). + #[inline(always)] + pub fn do_move(&mut self, chess_move: Move) -> NonReversibleState { + // Save non-revertible state + let state = NonReversibleState { + castle_rights: self.castle_rights, + en_passant: self.en_passant, + half_move_clock: self.half_move_clock, + }; + + // Non-revertible state modification + if chess_move.capture().is_some() || chess_move.piece() == Piece::Pawn { + self.half_move_clock = 0; + } else { + self.half_move_clock += 1; + } + if chess_move.is_double_step() { + let target_square = Square::new( + chess_move.destination().file(), + self.current_player().third_rank(), + ); + self.en_passant = Some(target_square); + } else { + self.en_passant = None; + } + if chess_move.is_castling() || chess_move.piece() == Piece::King { + *self.castle_rights_mut(self.current_player()) = CastleRights::NoSide; + } + if chess_move.piece() == Piece::Rook { + let castle_rights = self.castle_rights_mut(self.current_player()); + *castle_rights = match chess_move.start().file() { + File::A => castle_rights.without_queen_side(), + File::H => castle_rights.without_king_side(), + _ => *castle_rights, + } + } + + // Revertible state modification + self.xor( + self.current_player(), + chess_move.piece(), + chess_move.start() | chess_move.destination(), + ); + self.total_plies += 1; + self.side = !self.side; + + state + } + + /// Reverse the effect of playing the given [Move], and return to the given + /// [NonReversibleState]. + #[inline(always)] + pub fn undo_move(&mut self, chess_move: Move, state: NonReversibleState) { + // Restore non-revertible state + self.castle_rights = state.castle_rights; + self.en_passant = state.en_passant; + self.half_move_clock = state.half_move_clock; + + // Restore revertible state + self.xor( + // The move was applied at the turn *before* the current player + !self.current_player(), + chess_move.piece(), + chess_move.start() | chess_move.destination(), + ); + self.total_plies -= 1; + self.side = !self.side; + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index b4e746b..866b920 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 fb0e289fa08b9ad3e7838be707ed3d7943a7830d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:37:47 +0200 Subject: [PATCH 086/255] Add 'ChessBoard::is_valid' --- src/board/chess_board.rs | 329 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 178c54f..9b8d56e 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -184,4 +184,333 @@ 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. + fn is_valid(&self) -> bool { + // Don't overlap pieces. + for piece in Piece::iter() { + 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 + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn valid() { + let default_position = ChessBoard { + 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, + }; + 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 0cefb050173741dc36be8f83718a624852e22342 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:38:50 +0200 Subject: [PATCH 087/255] Add FEN board parsing --- src/board/chess_board.rs | 197 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 9b8d56e..b95a427 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -272,9 +272,103 @@ impl ChessBoard { } } +/// Return a [ChessBoard] from the given FEN string. +impl FromFen for ChessBoard { + type Err = Error; + + fn from_fen(s: &str) -> Result { + let mut split = s.split_ascii_whitespace(); + + let piece_placement = split.next().ok_or(Error::InvalidFen)?; + let side_to_move = split.next().ok_or(Error::InvalidFen)?; + let castling_rights = split.next().ok_or(Error::InvalidFen)?; + let en_passant_square = split.next().ok_or(Error::InvalidFen)?; + let half_move_clock = split.next().ok_or(Error::InvalidFen)?; + let full_move_counter = split.next().ok_or(Error::InvalidFen)?; + + let castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?; + let side = Color::from_fen(side_to_move)?; + let en_passant = Option::::from_fen(en_passant_square)?; + + let half_move_clock = half_move_clock + .parse::() + .map_err(|_| Error::InvalidFen)?; + let full_move_counter = full_move_counter + .parse::() + .map_err(|_| Error::InvalidFen)?; + let total_plies = (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }; + + let (piece_occupancy, color_occupancy, combined_occupancy) = { + let (mut pieces, mut colors, mut combined) = + ([Bitboard::EMPTY; 6], [Bitboard::EMPTY; 2], Bitboard::EMPTY); + + let mut rank: usize = 8; + for rank_str in piece_placement.split('/') { + rank -= 1; + let mut file: usize = 0; + for c in rank_str.chars() { + let color = if c.is_uppercase() { + Color::White + } else { + Color::Black + }; + let piece = match c { + digit @ '1'..='8' => { + // Unwrap is fine since this arm is only matched by digits + file += digit.to_digit(10).unwrap() as usize; + continue; + } + _ => Piece::from_fen(&c.to_string())?, + }; + let (piece_board, color_board) = + (&mut pieces[piece.index()], &mut colors[color.index()]); + + // Only need to worry about underflow since those are `usize` values. + if file >= 8 || rank >= 8 { + return Err(Error::InvalidFen); + }; + let square = Square::new(File::from_index(file), Rank::from_index(rank)); + *piece_board |= square; + *color_board |= square; + combined |= square; + file += 1; + } + // We haven't read exactly 8 files. + if file != 8 { + return Err(Error::InvalidFen); + } + } + // We haven't read exactly 8 ranks + if rank != 0 { + return Err(Error::InvalidFen); + } + + (pieces, colors, combined) + }; + + let res = Self { + piece_occupancy, + color_occupancy, + combined_occupancy, + castle_rights, + en_passant, + half_move_clock, + total_plies, + side, + }; + + if !res.is_valid() { + return Err(Error::InvalidPosition); + } + + Ok(res) + } +} + #[cfg(test)] mod test { use super::*; + use crate::board::MoveBuilder; #[test] fn valid() { @@ -513,4 +607,107 @@ mod test { }; assert!(!position.is_valid()); } + + #[test] + fn fen_default_position() { + let default_position = ChessBoard { + 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, + }; + 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 { + 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, + }; + // Modify it to account for e4 move + position.xor(Color::White, Piece::Pawn, Square::E2 | Square::E4); + position.en_passant = Some(Square::E3); + position.total_plies = 1; + position.side = Color::Black; + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap(), + position + ); + // And now c5 + position.xor(Color::Black, Piece::Pawn, Square::C5 | Square::C7); + position.en_passant = Some(Square::C6); + position.total_plies = 2; + position.side = Color::White; + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap(), + position + ); + // Finally, Nf3 + position.xor(Color::White, Piece::Knight, Square::G1 | Square::F3); + position.en_passant = None; + position.total_plies = 3; + position.half_move_clock = 1; + position.side = Color::Black; + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") + .unwrap(), + position + ); + } } From b5bb613b5ed3d57d2ab7f8e4e1dcbea330601cdf Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:39:29 +0200 Subject: [PATCH 088/255] Test 'ChessBoard::{do,undo}_move' machinery --- src/board/chess_board.rs | 183 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index b95a427..a7f84b1 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -710,4 +710,187 @@ mod test { position ); } + + #[test] + fn do_move() { + // Start from default position + let mut position = ChessBoard { + 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, + }; + // 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 { + 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, + }; + // 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 9cddda74787f2a9e6f4babd68ba2ae0eafa05ba1 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:45:04 +0200 Subject: [PATCH 089/255] Implement 'Default' for 'ChessBoard' --- 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 a7f84b1..2a2e30d 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -272,6 +272,42 @@ impl ChessBoard { } } +/// Use the starting position as a default value, corresponding to the +/// "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" FEN string +impl Default for ChessBoard { + fn default() -> Self { + Self { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Square::D1 | Square::D8, + // Rook + Square::A1 | Square::A8 | Square::H1 | Square::H8, + // Bishop + Square::C1 | Square::C8 | Square::F1 | Square::F8, + // Knight + Square::B1 | Square::B8 | Square::G1 | Square::G8, + // Pawn + Rank::Second.into_bitboard() | Rank::Seventh.into_bitboard(), + ], + color_occupancy: [ + Rank::First.into_bitboard() | Rank::Second.into_bitboard(), + Rank::Seventh.into_bitboard() | Rank::Eighth.into_bitboard(), + ], + combined_occupancy: Rank::First.into_bitboard() + | Rank::Second.into_bitboard() + | Rank::Seventh.into_bitboard() + | Rank::Eighth.into_bitboard(), + castle_rights: [CastleRights::BothSides; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + } + } +} + /// Return a [ChessBoard] from the given FEN string. impl FromFen for ChessBoard { type Err = Error; From 99129e453c19ce0d6504634269a460df701efaa3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:45:28 +0200 Subject: [PATCH 090/255] Make use of 'ChessBoard::default' in tests --- src/board/chess_board.rs | 150 ++------------------------------------- 1 file changed, 5 insertions(+), 145 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 2a2e30d..c10510a 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -408,35 +408,7 @@ mod test { #[test] fn valid() { - let default_position = ChessBoard { - 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, - }; + let default_position = ChessBoard::default(); assert!(default_position.is_valid()); } @@ -646,35 +618,7 @@ mod test { #[test] fn fen_default_position() { - let default_position = ChessBoard { - 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, - }; + let default_position = ChessBoard::default(); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") .unwrap(), @@ -685,35 +629,7 @@ mod test { #[test] fn fen_en_passant() { // Start from default position - let mut position = ChessBoard { - 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, - }; + let mut position = ChessBoard::default(); // Modify it to account for e4 move position.xor(Color::White, Piece::Pawn, Square::E2 | Square::E4); position.en_passant = Some(Square::E3); @@ -750,35 +666,7 @@ mod test { #[test] fn do_move() { // Start from default position - let mut position = ChessBoard { - 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, - }; + let mut position = ChessBoard::default(); // Modify it to account for e4 move position.do_move( MoveBuilder { @@ -841,35 +729,7 @@ mod test { #[test] fn do_move_and_undo() { // Start from default position - let mut position = ChessBoard { - 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, - }; + let mut position = ChessBoard::default(); // Modify it to account for e4 move let move_1 = MoveBuilder { piece: Piece::Pawn, From a6e8ac06b67d48e5fe4274a1f136c7d7733cc09d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:13:23 +0200 Subject: [PATCH 091/255] Add dummy 'magic::moves' module for build script Since I'm am doing something quite *weird*, by writing a build script that makes use of the module I am generating code for, I need to ensure that it compiles both without the generated code, and with it. So this dummy implementation of the module ensures that the code will keep compiling in both cases, and panics if any of the functions that depend on generated code are called during the build script execution. --- src/movegen/magic/mod.rs | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs index 8b3f605..7609199 100644 --- a/src/movegen/magic/mod.rs +++ b/src/movegen/magic/mod.rs @@ -22,5 +22,46 @@ impl Magic { #[cfg(generated_boards)] mod moves; -#[cfg(generated_boards)] pub use moves::*; + +#[cfg(not(generated_boards))] +#[allow(unused_variables)] +mod moves { + use crate::board::{Bitboard, Color, Square}; + + pub fn quiet_pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + unreachable!() + } + + pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + unreachable!() + } + + pub fn knight_moves(square: Square) -> Bitboard { + unreachable!() + } + + pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { + unreachable!() + } + + pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { + unreachable!() + } + + pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard { + unreachable!() + } + + pub fn king_moves(square: Square) -> Bitboard { + unreachable!() + } + + pub fn king_side_castle_blockers(color: Color) -> Bitboard { + unreachable!() + } + + pub fn queen_side_castle_blockers(color: Color) -> Bitboard { + unreachable!() + } +} From 2e410ba1044e9f3d53b782798be9c0658aea76e2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:23:10 +0200 Subject: [PATCH 092/255] Make use of assignment operators for 'Bitboard' --- src/board/direction.rs | 2 +- src/movegen/wizardry/mask.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/board/direction.rs b/src/board/direction.rs index cfed3cb..6b350a0 100644 --- a/src/board/direction.rs +++ b/src/board/direction.rs @@ -140,7 +140,7 @@ impl Direction { while !board.is_empty() { board = self.move_board(board); - res = res | board; + res |= board; if !(board & blockers).is_empty() { break; } diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs index eed93a0..b3d4f46 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -18,16 +18,16 @@ pub fn generate_rook_mask(square: Square) -> Bitboard { let mask = { let mut mask = Bitboard::EMPTY; if square.file() != File::A { - mask = mask | File::A.into_bitboard() + mask |= File::A.into_bitboard() }; if square.file() != File::H { - mask = mask | File::H.into_bitboard() + mask |= File::H.into_bitboard() }; if square.rank() != Rank::First { - mask = mask | Rank::First.into_bitboard() + mask |= Rank::First.into_bitboard() }; if square.rank() != Rank::Eighth { - mask = mask | Rank::Eighth.into_bitboard() + mask |= Rank::Eighth.into_bitboard() }; mask }; From eefa707c0794323874dacc5abee1752447863ba0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:24:47 +0200 Subject: [PATCH 093/255] Silence useless clippy warnings Those warnings are either explicitly accounted for in the code, or make the code look worse if fixed. --- src/board/castle_rights.rs | 1 + src/board/chess_board.rs | 1 + src/board/move.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index a48bf4d..6b0bb66 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -67,6 +67,7 @@ impl CastleRights { } /// Add some [CastleRights], and return the resulting [CastleRights]. + #[allow(clippy::should_implement_trait)] #[inline(always)] pub fn add(self, to_remove: CastleRights) -> Self { // SAFETY: we know the value is in-bounds diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index c10510a..3ef9707 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -190,6 +190,7 @@ impl ChessBoard { 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() { diff --git a/src/board/move.rs b/src/board/move.rs index 0372a47..60672f8 100644 --- a/src/board/move.rs +++ b/src/board/move.rs @@ -84,6 +84,7 @@ mod shift { impl Move { /// Construct a new move. + #[allow(clippy::too_many_arguments)] #[inline(always)] fn new( piece: Piece, From 024a41fa18418b3a6459f4d7e08dbbe5e2b3e05e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:26:05 +0200 Subject: [PATCH 094/255] Use unchecked conversion in 'BitboardIterator --- src/board/bitboard/iterator.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs index fcd644c..ea6d489 100644 --- a/src/board/bitboard/iterator.rs +++ b/src/board/bitboard/iterator.rs @@ -11,7 +11,8 @@ impl Iterator for BitboardIterator { } else { let lsb = self.0.trailing_zeros() as usize; self.0 ^= 1 << lsb; - Some(crate::board::Square::from_index(lsb)) + // SAFETY: we know the value is in-bounds + Some(unsafe { crate::board::Square::from_index_unchecked(lsb) }) } } From 934597d63d90f08069f372171edeb4accc7e936d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:26:31 +0200 Subject: [PATCH 095/255] Add 'Bitboard::try_into_square' --- src/board/bitboard/mod.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 8eff5e1..0aa18f7 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -76,6 +76,18 @@ impl Bitboard { pub fn iter_power_set(self) -> impl Iterator { BitboardPowerSetIterator::new(self) } + + /// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is + /// occupying. Otherwise return `None`. + pub fn try_into_square(self) -> Option { + if self.count() != 1 { + None + } else { + let index = self.0.trailing_zeros() as usize; + // SAFETY: we know the value is in-bounds + Some(unsafe { Square::from_index_unchecked(index) }) + } + } } // Ensure zero-cost (at least size-wise) wrapping. @@ -450,4 +462,17 @@ mod test { 1 << 8 ); } + + #[test] + fn into_square() { + for square in Square::iter() { + assert_eq!(square.into_bitboard().try_into_square(), Some(square)); + } + } + + #[test] + fn into_square_invalid() { + assert!(Bitboard::EMPTY.try_into_square().is_none()); + assert!((Square::A1 | Square::A2).try_into_square().is_none()) + } } From cb3b1ee74573328421048c6adde95e03f49c0f8b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:27:17 +0200 Subject: [PATCH 096/255] Check kings' position in 'ChessBoard::is_valid' --- src/board/chess_board.rs | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 3ef9707..8c718a2 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,4 +1,4 @@ -use crate::error::Error; +use crate::{error::Error, movegen::magic::king_moves}; use super::{Bitboard, CastleRights, Color, File, FromFen, Move, Piece, Rank, Square}; @@ -266,8 +266,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 !(king_moves(white_king.try_into_square().unwrap()) & black_king).is_empty() { + return false; + } + // FIXME: check for opponent being in check. - // FIXME: check for kings touching. true } @@ -617,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 f13d44b8e3dfc0ebd6f3ed893702e5347875595b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 11:00:23 +0200 Subject: [PATCH 097/255] 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 0aa18f7..8dff351 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -68,6 +68,13 @@ impl Bitboard { self == Self::EMPTY } + /// Return true if there are more than piece in the [Bitboard]. This is faster than testing + /// `board.count() > 1`. + #[inline(always)] + pub fn has_more_than_one(self) -> bool { + (self.0 & (self.0.wrapping_sub(1))) != 0 + } + /// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of /// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that /// contain all, some, or none of the [Square] that are in the given [Bitboard]. @@ -388,6 +395,16 @@ mod test { assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1)); } + #[test] + fn more_than_one() { + assert!(!Bitboard::EMPTY.has_more_than_one()); + for square in Square::iter() { + assert!(!square.into_bitboard().has_more_than_one()) + } + assert!((Square::A1 | Square::H8).has_more_than_one()); + assert!(Bitboard::ALL.has_more_than_one()); + } + #[test] fn iter_power_set_empty() { assert_eq!( From 168d9e69aba415cf2b211c4b88cc482e57d50740 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 11:58:18 +0200 Subject: [PATCH 098/255] Test for opponent being in check during validation --- src/board/chess_board.rs | 77 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 8c718a2..be8bc4d 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,4 +1,9 @@ -use crate::{error::Error, movegen::magic::king_moves}; +use crate::{ + error::Error, + movegen::{ + bishop_moves, knight_moves, magic::king_moves, naive::pawn::pawn_captures, rook_moves, + }, +}; use super::{Bitboard, CastleRights, Color, File, FromFen, Move, Piece, Rank, Square}; @@ -274,10 +279,50 @@ 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())) != Bitboard::EMPTY { + return false; + } true } + + /// Compute all pieces that are currently threatening the given [Color]'s king. + fn compute_checkers(&self, color: Color) -> Bitboard { + // Unwrap is fine, there should always be exactly one king per color + let king = (self.piece_occupancy(Piece::King) & self.color_occupancy(color)) + .try_into_square() + .unwrap(); + + let opponent = !color; + + // No need to remove our pieces from the generated moves, we just want to check if we + // intersect with the opponent's pieces, rather than generate only valid moves. + let bishops = { + let queens = self.piece_occupancy(Piece::Queen) & self.color_occupancy(opponent); + let bishops = self.piece_occupancy(Piece::Bishop) & self.color_occupancy(opponent); + let bishop_attacks = bishop_moves(king, self.combined_occupancy()); + (queens | bishops) & bishop_attacks + }; + let rooks = { + let queens = self.piece_occupancy(Piece::Queen) & self.color_occupancy(opponent); + let rooks = self.piece_occupancy(Piece::Rook) & self.color_occupancy(opponent); + let rook_attacks = rook_moves(king, self.combined_occupancy()); + (queens | rooks) & rook_attacks + }; + let knights = { + let knights = self.piece_occupancy(Piece::Knight) & self.color_occupancy(opponent); + let knight_attacks = knight_moves(king); + knights & knight_attacks + }; + let pawns = { + let pawns = self.piece_occupancy(Piece::Pawn) & self.color_occupancy(opponent); + let pawn_attacks = pawn_captures(color, king); + pawns & pawn_attacks + }; + + bishops | rooks | knights | pawns + } } /// Use the starting position as a default value, corresponding to the @@ -652,6 +697,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 87258c1084e41e1113b1594dcb3cb79a95fcf900 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 12:02:41 +0200 Subject: [PATCH 099/255] 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 be8bc4d..f3af598 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)] @@ -725,6 +732,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 1a7763e1f4390486f8560e21e8a1bb36b51f5c4f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 12:26:33 +0200 Subject: [PATCH 100/255] Add 'ChessBoard::at' --- 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 f3af598..23d702c 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -119,6 +119,24 @@ impl ChessBoard { self.compute_checkers(self.current_player()) } + /// Convenience function which returns the [Piece] and [Color] at a given [Square], or `None` + /// if it is empty. + pub fn at(&self, square: Square) -> Option<(Piece, Color)> { + let color = if !(self.color_occupancy(Color::White) & square).is_empty() { + Color::White + } else { + Color::Black + }; + + for piece in Piece::iter() { + if !(self.piece_occupancy(piece) & square).is_empty() { + return Some((piece, color)); + } + } + + None + } + /// Quickly do and undo a move on the [Bitboard]s that are part of the [ChessBoard] state. Does /// not account for all non-revertible changes such as en-passant state or half-move clock. #[inline(always)] @@ -775,6 +793,38 @@ mod test { ); } + #[test] + fn at() { + let default_position = ChessBoard::default(); + assert_eq!(default_position.at(Square::D4), None); + assert_eq!(default_position.at(Square::D5), None); + + assert_eq!( + default_position.at(Square::A1).unwrap(), + (Piece::Rook, Color::White) + ); + assert_eq!( + default_position.at(Square::H8).unwrap(), + (Piece::Rook, Color::Black) + ); + assert_eq!( + default_position.at(Square::D1).unwrap(), + (Piece::Queen, Color::White) + ); + assert_eq!( + default_position.at(Square::D8).unwrap(), + (Piece::Queen, Color::Black) + ); + assert_eq!( + default_position.at(Square::C2).unwrap(), + (Piece::Pawn, Color::White) + ); + assert_eq!( + default_position.at(Square::F7).unwrap(), + (Piece::Pawn, Color::Black) + ); + } + #[test] fn fen_default_position() { let default_position = ChessBoard::default(); From 3677040e0318be455750949c7c9b1939abb81698 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Mar 2024 20:30:43 +0000 Subject: [PATCH 101/255] 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..4ff7dba --- /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 + environment: + ADDRESS: + from_secret: matrix_homeserver + ROOM: + from_secret: matrix_roomid + USER: + from_secret: matrix_username + PASS: + from_secret: matrix_password + commands: + - nix run github:ambroisie/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 44eb79f35babe5458068398c7d34a56ead4bbaf3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Mar 2024 20:31:45 +0000 Subject: [PATCH 102/255] 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 015485a4c5892cea1a1bd049413e06033d3e7c8c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:33:07 +0200 Subject: [PATCH 103/255] 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 f4a92c068135dafbf2a9f9caef594ce087bd459a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:52:09 +0200 Subject: [PATCH 104/255] 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 a0fcf3285cbfdcfbeab3eee20da45f6c8ec971ed Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:53:08 +0200 Subject: [PATCH 105/255] 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 47b1854669a74d4a02041904ed45f7cb51f38936 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:58:46 +0200 Subject: [PATCH 106/255] 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 97ca224608361c842234634520caba76975f2ac2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 23:52:16 +0200 Subject: [PATCH 107/255] 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 54d7f0d69f137c44ea2660abe08f2276d7fdc739 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:05:49 +0200 Subject: [PATCH 108/255] 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 ef15da41ea90d151f30d82199721de6d08eebf3d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:05:56 +0200 Subject: [PATCH 109/255] 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 c3c36841506195fff8d99a53336c02439ab69c59 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:20:33 +0200 Subject: [PATCH 110/255] 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 6501466d3eeeaa56331df33f90f7f205ecce6227 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:23:19 +0200 Subject: [PATCH 111/255] 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 2b4797ec47da3a367d5e7e42664e7b22540f285f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:34:33 +0200 Subject: [PATCH 112/255] 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 7a7e7f3665a1c902ce1dfeb32a2c4da2537856c4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:34:46 +0200 Subject: [PATCH 113/255] 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 8261b0c06b854ca6ca8818e1281b915583d7e4e2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 20:39:47 +0200 Subject: [PATCH 114/255] 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 281c79556ae48d5109444a4187e044bd74103354 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 21:30:23 +0200 Subject: [PATCH 115/255] 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 c177d13b756debdcdcf853d323eec7c34d83b004 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 21:30:23 +0200 Subject: [PATCH 116/255] 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 b840bfc570159bc598d193bf0d267fc44d558248 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:06:43 +0200 Subject: [PATCH 117/255] 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 924689ec0285dd60f5525be8b6dbaaf19322eeaa Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:06:50 +0200 Subject: [PATCH 118/255] 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 1ab024fce8cedd9ad0bd3ea16803a66222176b67 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:27:33 +0200 Subject: [PATCH 119/255] 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 251c10cbc7f84c6e3ebf8d391c88dd50774fe2c3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:03:35 +0200 Subject: [PATCH 120/255] 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 dc974ec0e99fa86bf4b0798ef798e2dc57f97c1f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:27:08 +0200 Subject: [PATCH 121/255] 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 9ef600c1bb100c29ddb1f8f2e8f271b04851d91e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:27:46 +0200 Subject: [PATCH 122/255] 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 f66b0276d885a2debe7011f7d2aca0f5f655b0e1 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:31:33 +0200 Subject: [PATCH 123/255] 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 659e3f1c9a721827514c8c4d8b1a811913e8990e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:32:40 +0200 Subject: [PATCH 124/255] 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 643b6883847f8e489704f2f613213bf08f645385 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:43:48 +0200 Subject: [PATCH 125/255] 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 d51d72377e71085e90d36428b04101442775bca3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 11:04:52 +0200 Subject: [PATCH 126/255] 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 585c127381d909d10fb64eb162169d109564b57a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 11:05:38 +0200 Subject: [PATCH 127/255] 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 042f5dbc4dc1c4714c14b614d83034f6b4fc01fc Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:11:37 +0200 Subject: [PATCH 128/255] 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 f351f056a9f3a7655ddd9650c64fc2632822292e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:22:04 +0200 Subject: [PATCH 129/255] 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 88e74d5ffffe99e0a9d95f381311edab6fe4ef09 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:27:03 +0200 Subject: [PATCH 130/255] 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 a21841c0ad4322199bbe62eab77d1af1df948482 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:27:37 +0200 Subject: [PATCH 131/255] 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 e84ec552fe13ea5d2f806f92c892dbfa4b3524a5 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:35:31 +0200 Subject: [PATCH 132/255] 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 0812d916ff621e789bbd29e290af99debe0dec91 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:50:23 +0200 Subject: [PATCH 133/255] 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 f3a83065dab4be42a365646fb431847024db460c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 17:27:09 +0200 Subject: [PATCH 134/255] 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 0315e2fb519536cb090d19e5826d4be21e6ad970 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 18:21:14 +0200 Subject: [PATCH 135/255] 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 2c1142324c04e0d28e831326dfe185cdbc812161 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 18:48:22 +0200 Subject: [PATCH 136/255] 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 b68dd132e8ca7a91b92ae21e681c2b2d1d57433f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:07:37 +0200 Subject: [PATCH 137/255] 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 407f85c19bda3a0e53741df1ba5333ee9138badf Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:18:46 +0200 Subject: [PATCH 138/255] 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 d919b956ed6fc5137ab137fc46b26016cc5564a8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:19:38 +0200 Subject: [PATCH 139/255] 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 a3a9f6421350700a4f4b73d94b49f4b121da0c02 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:07:16 +0200 Subject: [PATCH 140/255] 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 8d03242e83e5a807cb7bde2eca964e9a4f769a6f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:25:29 +0200 Subject: [PATCH 141/255] 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 4491e5be001294d522ad220efdc6c713e61ef37b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:43:18 +0200 Subject: [PATCH 142/255] 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 bed7ec3be2fcf16bd76715e25ff695362bef8e22 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:44:33 +0200 Subject: [PATCH 143/255] 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 d91f63b5edff132240832bb22f2f17f11a33e2c9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:22:37 +0200 Subject: [PATCH 144/255] 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 f0847a4e49585f887a1eced9be372602cc581459 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:30:16 +0200 Subject: [PATCH 145/255] 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 8ff47231a043f69baff262e1840cd27178db649e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:33:33 +0200 Subject: [PATCH 146/255] 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 bf23d0eaaea4233bc994e001a5a5b4ba4fb817dd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:33:45 +0200 Subject: [PATCH 147/255] 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 d3a84750f58066b3fa162e439edee07fa8a4d80c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:36:28 +0200 Subject: [PATCH 148/255] 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 2601abdc76011e722748474b704b636fdadfe2e3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:14:46 +0200 Subject: [PATCH 149/255] 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 1951db07202d4102fbfc0e40a3569ba05bed06f1 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:37:56 +0200 Subject: [PATCH 150/255] 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 5ef3737b988a7436ac9fd165fe74fa15c54c8c62 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:41:09 +0200 Subject: [PATCH 151/255] 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 066d4428231629605399607ad2173a12de3f38e5 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:42:05 +0200 Subject: [PATCH 152/255] 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 bd9238d68626e92b621517cc573d6f4675e0daaf Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 23 Jul 2022 15:32:56 +0200 Subject: [PATCH 153/255] 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 23d01d4d3ff7f4e5ad0fc16027595486bdaf968b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 13:40:01 +0200 Subject: [PATCH 154/255] 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 868edda9d7db09e682bc135fc4c4be315b9f277b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:24:35 +0200 Subject: [PATCH 155/255] 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 028c4543e78cd12bc5e2090251d3037a2999e1c0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 01:43:27 +0000 Subject: [PATCH 156/255] 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 25494700d7134e3893194e343b656329ace41593 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 02:54:24 +0000 Subject: [PATCH 157/255] 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 d519cfb8178d42a748f3abbdb84ff34ac7296f53 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 02:55:03 +0000 Subject: [PATCH 158/255] 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 2bdfbbf4670e92ec9448af3d1cdf5aa7a7dfa275 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 02:56:01 +0000 Subject: [PATCH 159/255] 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 459b878342ed838bc8ffee21c95f71edf389f8ed Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 11:55:59 +0100 Subject: [PATCH 160/255] 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 06087358de5e924d513663068b25fafbe238b8d5 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 12:00:57 +0100 Subject: [PATCH 161/255] 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 3b1735da798f948e1b3a861597db6c6b0e401db8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 12:08:34 +0100 Subject: [PATCH 162/255] 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 af421a9452e74232a3f5bb3b2fe891a5f94411ce Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 12:09:16 +0100 Subject: [PATCH 163/255] 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 02768b6d96b395491727904c3afa9a034bf8b745 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:43:18 +0200 Subject: [PATCH 164/255] 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 072a1ea13c9730c21666dcb6bd04ed3dc68ed104 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 17:34:11 +0200 Subject: [PATCH 165/255] 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 0cc1fcf912f55a0c41e66c09b471a700bf64af6e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:26:39 +0200 Subject: [PATCH 166/255] 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 e673e20a63232aa028d015e3376a63b9697fe2a7 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:38:51 +0200 Subject: [PATCH 167/255] 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 0b9318cdf325750a6d5df60f86af1c6cc5b32516 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 11:00:23 +0200 Subject: [PATCH 168/255] 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 be8024d17617540726d46fd41ba9a3cd5764d415 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 15:56:08 +0100 Subject: [PATCH 169/255] 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 4d69d34fa05d4af3468d4d27e75353587249158e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 19:12:51 +0200 Subject: [PATCH 170/255] 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 8f3687d8620a47d19e00016a2130995908639c25 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:02:23 +0100 Subject: [PATCH 171/255] 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 d3c3790db49e49108dd3d20b5562d56735f1f32a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:50:49 +0100 Subject: [PATCH 172/255] 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 93e9a51589346d672c6e91bb9ca0ab5b81420eac Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:50:49 +0100 Subject: [PATCH 173/255] 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 3b530b324f58961f8944c7d1136413a827f00790 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:02:23 +0100 Subject: [PATCH 174/255] 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 de27c186d3c7554f50054742b7555000fdd91e37 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:02:23 +0100 Subject: [PATCH 175/255] 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 0fd9766db041f1c03a1dfea081643641136396f3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:25:35 +0200 Subject: [PATCH 176/255] 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 bd662fdd27b7f363458c765b8447883051e45e7a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:24:24 +0200 Subject: [PATCH 177/255] 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 3c2a5a412e478517c1c5fdd8f0395d23615f0d36 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:15 +0200 Subject: [PATCH 178/255] 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 b5365f8a82c9ead4030feaa70727d7a50de8ec69 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:38 +0200 Subject: [PATCH 179/255] 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 7c896d5dba03f6cfa13f8532d44315fb8b3dbcc3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:55 +0200 Subject: [PATCH 180/255] 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 64e93b39fdf85a4432204105e50f93f69f131531 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:41:08 +0200 Subject: [PATCH 181/255] 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 d44461e35c38af2a73d9b1dfa2a0fb93750a59fa Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:19:40 +0200 Subject: [PATCH 182/255] 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 ce3ebf05ee60b9407e4cd99bf036a90115167290 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:19:40 +0200 Subject: [PATCH 183/255] 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 c112ddc4cdc295e626da423f8710ea9c8b7e245c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:45:04 +0200 Subject: [PATCH 184/255] 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 ddcef6f9c0f847a7b0ad7b620e9d36ccc2e9266b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:17:12 +0200 Subject: [PATCH 185/255] 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 3cd2601f07f81db5b133ade2af514f1f55b5b4e0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:37:47 +0200 Subject: [PATCH 186/255] 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 c1419f0e44dc2a1f14b86ee98c6f43fd19efe704 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:38:50 +0200 Subject: [PATCH 187/255] 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 bc67ee3e9a0195a33e6e5a4cfd37ba6cf4e7b242 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:39:29 +0200 Subject: [PATCH 188/255] 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 6976c60fe67535c5a6258627065037c000332e69 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:26:31 +0200 Subject: [PATCH 189/255] 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 08ce3787dff431ba6701ef6760872a9bb52808ba Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:27:17 +0200 Subject: [PATCH 190/255] 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 d9e4f16ec8be800d2fadcfc9fe3a234f1c311db3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 01:29:39 +0100 Subject: [PATCH 191/255] 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 58d87431066e53df152d6088d99d0cc32cfa5656 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 01:29:39 +0100 Subject: [PATCH 192/255] 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 f582a76e700f96e86d9c760306d1e2f628acd8b0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 11:58:18 +0200 Subject: [PATCH 193/255] 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 9dea85054d5ee81af9de36f39242bf20ddd07af6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 12:02:41 +0200 Subject: [PATCH 194/255] 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 52772167a6af2cd4f02fa52e8d2ff2448aa0623c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 11:39:29 +0100 Subject: [PATCH 195/255] 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 83a29cae2a4ee7633e6d715aaf841192e99525be Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 12:28:22 +0100 Subject: [PATCH 196/255] 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 a1005c1bec6a329af7cb7240a52f8f4a9cc05efe Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 14:37:37 +0100 Subject: [PATCH 197/255] 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 dcd76c5befc6f372d96f5ccae82e95e3b1324013 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 14:38:26 +0100 Subject: [PATCH 198/255] 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 33eb5d0707bbd38d6be91b3c9ad5947814251afb Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 14:39:05 +0100 Subject: [PATCH 199/255] 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 3fa56b36a4c28dcde04e16846c61bb6dc2b05db8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 12 Aug 2022 16:17:51 +0200 Subject: [PATCH 200/255] 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 46ff51552aaa59746e72ba9a013fe46f8cae4dbd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 12 Aug 2022 16:17:03 +0200 Subject: [PATCH 201/255] 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 463b2a1a8f0b9f3bf6b7cbaa4238093ddc7a7be1 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 19:56:48 +0100 Subject: [PATCH 202/255] 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 d3386bcb528e727f8476cce799f66feb64032d27 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:21:05 +0100 Subject: [PATCH 203/255] 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 714feedbd23104d845e4497525a7524bdb689ef0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:21:05 +0100 Subject: [PATCH 204/255] 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 127dea25b41a1ac8b78fc29f4db6bae316090cc3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:28:16 +0100 Subject: [PATCH 205/255] 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 8173fa2ccdc8f4c23f14cdfa89bb09708ae6ce85 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:41:54 +0100 Subject: [PATCH 206/255] 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 12909377e4a326a324412d340c5b8eefc959bc1f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:42:28 +0100 Subject: [PATCH 207/255] 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 1cf05b5f55c1b18833e008b82bc1ca2e2a803758 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:19:55 +0100 Subject: [PATCH 208/255] 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 a676094dc1d744d66ebcabf69df8ca767ac70336 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:20:20 +0100 Subject: [PATCH 209/255] 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 829362dbced2edce8d0cd5570a02e9446714a297 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:30:52 +0100 Subject: [PATCH 210/255] 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 b9cc60be9c4370374e5fd08b163d5e931e23aaeb Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:32:02 +0100 Subject: [PATCH 211/255] 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 c3be661719a25eda900c711d1a67d711ffc939e0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:34:01 +0100 Subject: [PATCH 212/255] 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 62c2be48c49c563e79489db5adca50405f483e67 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:35:48 +0100 Subject: [PATCH 213/255] 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 ff7bea05085be7b758fcd30c2a83e492767d3c8f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:54:32 +0100 Subject: [PATCH 214/255] 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 2853cec7c9b11130884f755cf61eef2d291a69fd Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:56:01 +0100 Subject: [PATCH 215/255] 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 ef3a1e4695a5089415c6a05e6cf808e60945a72a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 22:41:01 +0100 Subject: [PATCH 216/255] Add half-move clock validation --- src/board/chess_board/error.rs | 3 +++ src/board/chess_board/mod.rs | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index e6ef030..4265de0 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -21,6 +21,8 @@ pub enum InvalidError { OverlappingColors, /// The pre-computed combined occupancy boards does not match the other boards. ErroneousCombinedOccupancy, + /// Half-move clock is higher than total number of plies. + HalfMoveClockTooHigh, } impl std::fmt::Display for InvalidError { @@ -42,6 +44,7 @@ impl std::fmt::Display for InvalidError { Self::ErroneousCombinedOccupancy => { "The pre-computed combined occupancy boards does not match the other boards." } + Self::HalfMoveClockTooHigh => "Half-move clock is higher than total number of plies.", }; write!(f, "{}", error_msg) } diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 99d6df6..879aba6 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -208,6 +208,11 @@ impl ChessBoard { /// Validate the state of the board. Return Err([InvalidError]) if an issue is found. pub fn validate(&self) -> Result<(), InvalidError> { + // Make sure the clocks are in agreement. + if u32::from(self.half_move_clock()) > self.total_plies() { + return Err(InvalidError::HalfMoveClockTooHigh); + } + // Don't overlap pieces. for piece in Piece::iter() { #[allow(clippy::collapsible_if)] @@ -423,6 +428,18 @@ mod test { assert!(default_position.is_valid()); } + #[test] + fn invalid_half_moves_clock() { + let res = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + builder.with_half_move_clock(10); + TryInto::::try_into(builder) + }; + assert_eq!(res.err().unwrap(), InvalidError::HalfMoveClockTooHigh); + } + #[test] fn invalid_overlapping_pieces() { let position = ChessBoard { From f4764f2174bd2581ed17a5c802addb77350b105e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 22:46:44 +0100 Subject: [PATCH 217/255] Use turn counts in 'ChessBoardBuilder' This makes more sense from a user's perspective. --- src/board/chess_board/builder.rs | 15 +++++++++------ src/fen.rs | 4 +--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/board/chess_board/builder.rs b/src/board/chess_board/builder.rs index 8221d92..d509e67 100644 --- a/src/board/chess_board/builder.rs +++ b/src/board/chess_board/builder.rs @@ -9,8 +9,9 @@ pub struct ChessBoardBuilder { castle_rights: [CastleRights; Color::NUM_VARIANTS], en_passant: Option, half_move_clock: u8, - total_plies: u32, side: Color, + // 1-based, a turn is *two* half-moves (i.e: both players have played). + turn_count: u32, } impl ChessBoardBuilder { @@ -20,8 +21,8 @@ impl ChessBoardBuilder { castle_rights: [CastleRights::NoSide; 2], en_passant: Default::default(), half_move_clock: Default::default(), - total_plies: Default::default(), side: Color::White, + turn_count: 1, } } @@ -45,8 +46,8 @@ impl ChessBoardBuilder { self } - pub fn with_total_plies(&mut self, plies: u32) -> &mut Self { - self.total_plies = plies; + pub fn with_turn_count(&mut self, count: u32) -> &mut Self { + self.turn_count = count; self } @@ -90,8 +91,8 @@ impl TryFrom for ChessBoard { castle_rights, en_passant, half_move_clock, - total_plies, side, + turn_count, } = builder; for square in Square::iter() { @@ -103,6 +104,8 @@ impl TryFrom for ChessBoard { combined_occupancy |= square; } + let total_plies = (turn_count - 1) * 2 + if side == Color::White { 0 } else { 1 }; + let board = ChessBoard { piece_occupancy, color_occupancy, @@ -146,7 +149,7 @@ mod test { builder .with_half_move_clock(board.half_move_clock()) - .with_total_plies(board.total_plies()) + .with_turn_count(board.total_plies() / 2 + 1) .with_current_player(board.current_player()); builder diff --git a/src/fen.rs b/src/fen.rs index 3096c95..a7a6825 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -154,9 +154,7 @@ impl FromFen for ChessBoard { 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 }, - ); + builder.with_turn_count(full_move_counter); { let mut rank: usize = 8; From 08f010ed32c2f2d25802bf11d836d5ea08252068 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 22:59:06 +0100 Subject: [PATCH 218/255] Add total plie count validation --- src/board/chess_board/error.rs | 3 +++ src/board/chess_board/mod.rs | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index 4265de0..7b570a4 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -23,6 +23,8 @@ pub enum InvalidError { ErroneousCombinedOccupancy, /// Half-move clock is higher than total number of plies. HalfMoveClockTooHigh, + /// The total plie count does not match the current player. + IncoherentPlieCount, } impl std::fmt::Display for InvalidError { @@ -45,6 +47,7 @@ impl std::fmt::Display for InvalidError { "The pre-computed combined occupancy boards does not match the other boards." } Self::HalfMoveClockTooHigh => "Half-move clock is higher than total number of plies.", + Self::IncoherentPlieCount => "The total plie count does not match the current player.", }; write!(f, "{}", error_msg) } diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 879aba6..d28d69c 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -208,6 +208,11 @@ impl ChessBoard { /// Validate the state of the board. Return Err([InvalidError]) if an issue is found. pub fn validate(&self) -> Result<(), InvalidError> { + // The current plie count should be odd on white's turn, and vice-versa. + if self.total_plies() % 2 != self.current_player().index() as u32 { + return Err(InvalidError::IncoherentPlieCount); + } + // Make sure the clocks are in agreement. if u32::from(self.half_move_clock()) > self.total_plies() { return Err(InvalidError::HalfMoveClockTooHigh); @@ -428,6 +433,22 @@ mod test { assert!(default_position.is_valid()); } + #[test] + fn invalid_incoherent_plie_count() { + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + let mut board = TryInto::::try_into(builder).unwrap(); + board.total_plies = 1; + board + }; + assert_eq!( + position.validate().err().unwrap(), + InvalidError::IncoherentPlieCount, + ); + } + #[test] fn invalid_half_moves_clock() { let res = { From 353271f427ed18c752f9238db72577b00117cb4d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 23:11:43 +0100 Subject: [PATCH 219/255] Simplify 'FromFen' for 'ChessBoard' --- src/fen.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/fen.rs b/src/fen.rs index a7a6825..90f6dd1 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -139,20 +139,19 @@ impl FromFen for ChessBoard { builder.with_castle_rights(castle_rights[color.index()], color); } - let side = Color::from_fen(side_to_move)?; - builder.with_current_player(side); + builder.with_current_player(FromFen::from_fen(side_to_move)?); - if let Some(square) = Option::::from_fen(en_passant_square)? { + if let Some(square) = FromFen::from_fen(en_passant_square)? { builder.with_en_passant(square); }; let half_move_clock = half_move_clock - .parse::() + .parse::<_>() .map_err(|_| FenError::InvalidFen)?; builder.with_half_move_clock(half_move_clock); let full_move_counter = full_move_counter - .parse::() + .parse::<_>() .map_err(|_| FenError::InvalidFen)?; builder.with_turn_count(full_move_counter); @@ -173,7 +172,7 @@ impl FromFen for ChessBoard { file += digit.to_digit(10).unwrap() as usize; continue; } - _ => Piece::from_fen(&c.to_string())?, + _ => FromFen::from_fen(&c.to_string())?, }; // Only need to worry about underflow since those are `usize` values. From a4aa4ae1e47a221d5b932e81bfe0d51fa7e0c4ac Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 23:14:11 +0100 Subject: [PATCH 220/255] Make 'half_move_clock' a 'u32' It *could* be set to a high value due to e.g: starting the engine in the middle of a game. Moving from a `u8` to a `u32` does not change the size of the type, so let's just do that. Use that opportunity to fix the comment about the number of *half-moves* (it's 50 moves *per player*). --- src/board/chess_board/builder.rs | 4 ++-- src/board/chess_board/mod.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/board/chess_board/builder.rs b/src/board/chess_board/builder.rs index d509e67..d16c881 100644 --- a/src/board/chess_board/builder.rs +++ b/src/board/chess_board/builder.rs @@ -8,7 +8,7 @@ pub struct ChessBoardBuilder { // Same fields as [ChessBoard]. castle_rights: [CastleRights; Color::NUM_VARIANTS], en_passant: Option, - half_move_clock: u8, + half_move_clock: u32, side: Color, // 1-based, a turn is *two* half-moves (i.e: both players have played). turn_count: u32, @@ -41,7 +41,7 @@ impl ChessBoardBuilder { self } - pub fn with_half_move_clock(&mut self, clock: u8) -> &mut Self { + pub fn with_half_move_clock(&mut self, clock: u32) -> &mut Self { self.half_move_clock = clock; self } diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index d28d69c..9ceb673 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -24,7 +24,7 @@ pub struct ChessBoard { /// `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. + half_move_clock: u32, // Should *probably* never go higher than 100. /// The number of half-turns so far. total_plies: u32, // Should be plenty. /// The current player turn. @@ -36,7 +36,7 @@ pub struct ChessBoard { pub struct NonReversibleState { castle_rights: [CastleRights; Color::NUM_VARIANTS], en_passant: Option, - half_move_clock: u8, // Should never go higher than 50. + half_move_clock: u32, // Should *probably* never go higher than 100. } impl ChessBoard { @@ -105,7 +105,7 @@ impl ChessBoard { /// Return the number of half-turns without either a pawn push or a capture. #[inline(always)] - pub fn half_move_clock(&self) -> u8 { + pub fn half_move_clock(&self) -> u32 { self.half_move_clock } @@ -214,7 +214,7 @@ impl ChessBoard { } // Make sure the clocks are in agreement. - if u32::from(self.half_move_clock()) > self.total_plies() { + if self.half_move_clock() > self.total_plies() { return Err(InvalidError::HalfMoveClockTooHigh); } From 388c26f4ac6e005f4e844e2b35e91859a017b75d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:00:57 +0100 Subject: [PATCH 221/255] Use 'writeln' in magic seed generation --- src/movegen/wizardry/mod.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 00645d8..f2794aa 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -197,6 +197,8 @@ pub(crate) const ROOK_SEED: [u64; 64] = [ #[cfg(test)] mod test { + use std::fmt::Write as _; + use super::*; // A simple XOR-shift RNG implementation. @@ -238,20 +240,25 @@ mod test { 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", + fn array_string(piece_type: &str, values: &[Magic]) -> Result { + let mut res = String::new(); + + writeln!( + &mut res, + "/// A set of magic numbers for {} move generation.", piece_type - ); - res.push_str(&format!( - "pub(crate) const {}_SEED: [u64; 64] = [\n", + )?; + writeln!( + &mut res, + "pub(crate) const {}_SEED: [u64; 64] = [", piece_type.to_uppercase() - )); + )?; for magic in values { - res.push_str(&format!(" {},\n", magic.magic)); + writeln!(&mut res, " {},", magic.magic)?; } - res.push_str("];\n"); - res + writeln!(&mut res, "];")?; + + Ok(res) } #[test] @@ -264,8 +271,8 @@ mod test { 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 bishop_array = array_string("bishop", &bishop_magics[..]).unwrap(); + let rook_array = array_string("rook", &rook_magics[..]).unwrap(); let new_text = { let start_marker = "// region:sourcegen\n"; From 6f161d067d577a1f4ae2a986a9f013b70a708608 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:07:50 +0100 Subject: [PATCH 222/255] Use 'ChessBoardBuilder' in more validation tests --- src/board/chess_board/mod.rs | 116 +++++++++-------------------------- 1 file changed, 28 insertions(+), 88 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 9ceb673..7fc43af 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -463,28 +463,13 @@ mod test { #[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, + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + let mut board: ChessBoard = builder.try_into().unwrap(); + *board.piece_occupancy_mut(Piece::Queen) |= Square::E1.into_bitboard(); + board }; assert_eq!( position.validate().err().unwrap(), @@ -494,28 +479,13 @@ mod test { #[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, + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + let mut board: ChessBoard = builder.try_into().unwrap(); + *board.color_occupancy_mut(Color::White) |= Square::E8.into_bitboard(); + board }; assert_eq!( position.validate().err().unwrap(), @@ -525,28 +495,13 @@ mod test { #[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, + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + let mut board: ChessBoard = builder.try_into().unwrap(); + *board.piece_occupancy_mut(Piece::Pawn) |= Square::E2.into_bitboard(); + board }; assert_eq!( position.validate().err().unwrap(), @@ -556,28 +511,13 @@ mod test { #[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, + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::E1] = Some((Piece::King, Color::White)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + let mut board: ChessBoard = builder.try_into().unwrap(); + *board.color_occupancy_mut(Color::Black) |= Square::E2.into_bitboard(); + board }; assert_eq!( position.validate().err().unwrap(), From 7d9c5edb994f9ba4b887471d83bd01b862ea77ef Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:15:17 +0100 Subject: [PATCH 223/255] Rename 'ValidationError' This is a better, clearer name. --- src/board/chess_board/builder.rs | 4 +- src/board/chess_board/error.rs | 6 +-- src/board/chess_board/mod.rs | 70 ++++++++++++++++---------------- src/fen.rs | 10 ++--- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/board/chess_board/builder.rs b/src/board/chess_board/builder.rs index d16c881..0304af5 100644 --- a/src/board/chess_board/builder.rs +++ b/src/board/chess_board/builder.rs @@ -1,4 +1,4 @@ -use crate::board::{Bitboard, CastleRights, ChessBoard, Color, InvalidError, Piece, Square}; +use crate::board::{Bitboard, CastleRights, ChessBoard, Color, Piece, Square, ValidationError}; /// Build a [ChessBoard] one piece at a time. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -80,7 +80,7 @@ impl std::ops::IndexMut for ChessBoardBuilder { } impl TryFrom for ChessBoard { - type Error = InvalidError; + type Error = ValidationError; fn try_from(builder: ChessBoardBuilder) -> Result { let mut piece_occupancy: [Bitboard; Piece::NUM_VARIANTS] = Default::default(); diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index 7b570a4..fe8e4c0 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -1,6 +1,6 @@ /// 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 { +pub enum ValidationError { /// Too many pieces. TooManyPieces, /// Missing king. @@ -27,7 +27,7 @@ pub enum InvalidError { IncoherentPlieCount, } -impl std::fmt::Display for InvalidError { +impl std::fmt::Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let error_msg = match self { Self::TooManyPieces => "Too many pieces.", @@ -53,4 +53,4 @@ impl std::fmt::Display for InvalidError { } } -impl std::error::Error for InvalidError {} +impl std::error::Error for ValidationError {} diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 7fc43af..edf92a2 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -206,16 +206,16 @@ impl ChessBoard { self.validate().is_ok() } - /// Validate the state of the board. Return Err([InvalidError]) if an issue is found. - pub fn validate(&self) -> Result<(), InvalidError> { + /// Validate the state of the board. Return Err([ValidationError]) if an issue is found. + pub fn validate(&self) -> Result<(), ValidationError> { // The current plie count should be odd on white's turn, and vice-versa. if self.total_plies() % 2 != self.current_player().index() as u32 { - return Err(InvalidError::IncoherentPlieCount); + return Err(ValidationError::IncoherentPlieCount); } // Make sure the clocks are in agreement. if self.half_move_clock() > self.total_plies() { - return Err(InvalidError::HalfMoveClockTooHigh); + return Err(ValidationError::HalfMoveClockTooHigh); } // Don't overlap pieces. @@ -224,7 +224,7 @@ impl ChessBoard { for other in Piece::iter() { if piece != other { if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() { - return Err(InvalidError::OverlappingPieces); + return Err(ValidationError::OverlappingPieces); } } } @@ -232,7 +232,7 @@ impl ChessBoard { // Don't overlap colors. if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() { - return Err(InvalidError::OverlappingColors); + return Err(ValidationError::OverlappingColors); } // Calculate the union of all pieces. @@ -241,12 +241,12 @@ impl ChessBoard { // Ensure that the pre-computed version is accurate. if combined != self.combined_occupancy() { - return Err(InvalidError::ErroneousCombinedOccupancy); + return Err(ValidationError::ErroneousCombinedOccupancy); } // Ensure that all pieces belong to a color, and no color has pieces that don't exist. if combined != (self.color_occupancy(Color::White) | self.color_occupancy(Color::Black)) { - return Err(InvalidError::ErroneousCombinedOccupancy); + return Err(ValidationError::ErroneousCombinedOccupancy); } for color in Color::iter() { @@ -260,18 +260,18 @@ impl ChessBoard { _ => count <= 10, }; if !possible { - return Err(InvalidError::TooManyPieces); + return Err(ValidationError::TooManyPieces); } } // Check that we have a king if self.occupancy(Piece::King, color).count() != 1 { - return Err(InvalidError::MissingKing); + return Err(ValidationError::MissingKing); } // Check that don't have too many pieces in total if self.color_occupancy(color).count() > 16 { - return Err(InvalidError::TooManyPieces); + return Err(ValidationError::TooManyPieces); } } @@ -280,7 +280,7 @@ impl ChessBoard { & (Rank::First.into_bitboard() | Rank::Eighth.into_bitboard())) .is_empty() { - return Err(InvalidError::InvalidPawnPosition); + return Err(ValidationError::InvalidPawnPosition); } // Verify that rooks and kings that are allowed to castle have not been moved. @@ -296,14 +296,14 @@ 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 Err(InvalidError::InvalidCastlingRights); + return Err(ValidationError::InvalidCastlingRights); } let actual_king = self.occupancy(Piece::King, color); let expected_king = Square::new(File::E, color.first_rank()); // We have checked that there is exactly one king, no need for intersecting the sets. if actual_king != expected_king.into_bitboard() { - return Err(InvalidError::InvalidCastlingRights); + return Err(ValidationError::InvalidCastlingRights); } } @@ -311,14 +311,14 @@ impl ChessBoard { if let Some(square) = self.en_passant() { // Must be empty if !(self.combined_occupancy() & square).is_empty() { - return Err(InvalidError::InvalidEnPassant); + return Err(ValidationError::InvalidEnPassant); } let opponent = !self.current_player(); // Must be on the opponent's third rank if (square & opponent.third_rank().into_bitboard()).is_empty() { - return Err(InvalidError::InvalidEnPassant); + return Err(ValidationError::InvalidEnPassant); } // Must be behind a pawn @@ -328,7 +328,7 @@ impl ChessBoard { .backward_direction() .move_board(square.into_bitboard()); if (opponent_pawns & double_pushed_pawn).is_empty() { - return Err(InvalidError::InvalidEnPassant); + return Err(ValidationError::InvalidEnPassant); } } @@ -337,12 +337,12 @@ 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 Err(InvalidError::NeighbouringKings); + return Err(ValidationError::NeighbouringKings); } // Check that the opponent is not currently in check. if !self.compute_checkers(!self.current_player()).is_empty() { - return Err(InvalidError::OpponentInCheck); + return Err(ValidationError::OpponentInCheck); } Ok(()) @@ -445,7 +445,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::IncoherentPlieCount, + ValidationError::IncoherentPlieCount, ); } @@ -458,7 +458,7 @@ mod test { builder.with_half_move_clock(10); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::HalfMoveClockTooHigh); + assert_eq!(res.err().unwrap(), ValidationError::HalfMoveClockTooHigh); } #[test] @@ -473,7 +473,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::OverlappingPieces, + ValidationError::OverlappingPieces, ); } @@ -489,7 +489,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::OverlappingColors, + ValidationError::OverlappingColors, ); } @@ -505,7 +505,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::ErroneousCombinedOccupancy, + ValidationError::ErroneousCombinedOccupancy, ); } @@ -521,7 +521,7 @@ mod test { }; assert_eq!( position.validate().err().unwrap(), - InvalidError::ErroneousCombinedOccupancy, + ValidationError::ErroneousCombinedOccupancy, ); } @@ -535,7 +535,7 @@ mod test { builder[Square::E8] = Some((Piece::King, Color::Black)); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); + assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces); } #[test] @@ -547,7 +547,7 @@ mod test { builder.with_castle_rights(CastleRights::BothSides, Color::White); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights); + assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights); } #[test] @@ -563,7 +563,7 @@ mod test { builder.with_castle_rights(CastleRights::BothSides, Color::White); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidCastlingRights); + assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights); } #[test] @@ -587,7 +587,7 @@ mod test { builder.with_en_passant(Square::A6); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant); + assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant); } #[test] @@ -600,7 +600,7 @@ mod test { builder.with_en_passant(Square::A6); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant); + assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant); } #[test] @@ -613,7 +613,7 @@ mod test { builder.with_en_passant(Square::A5); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidEnPassant); + assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant); } #[test] @@ -624,7 +624,7 @@ mod test { builder[Square::E2] = Some((Piece::King, Color::Black)); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::NeighbouringKings); + assert_eq!(res.err().unwrap(), ValidationError::NeighbouringKings); } #[test] @@ -636,7 +636,7 @@ mod test { builder[Square::E8] = Some((Piece::King, Color::Black)); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::OpponentInCheck); + assert_eq!(res.err().unwrap(), ValidationError::OpponentInCheck); } #[test] @@ -648,7 +648,7 @@ mod test { builder[Square::H8] = Some((Piece::King, Color::Black)); TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::InvalidPawnPosition); + assert_eq!(res.err().unwrap(), ValidationError::InvalidPawnPosition); } #[test] @@ -665,7 +665,7 @@ mod test { } TryInto::::try_into(builder) }; - assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); + assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces); } #[test] diff --git a/src/fen.rs b/src/fen.rs index 90f6dd1..41a0696 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,5 +1,5 @@ use crate::board::{ - CastleRights, ChessBoard, ChessBoardBuilder, Color, File, InvalidError, Piece, Rank, Square, + CastleRights, ChessBoard, ChessBoardBuilder, Color, File, Piece, Rank, Square, ValidationError, }; /// A trait to mark items that can be converted from a FEN input. @@ -15,7 +15,7 @@ pub enum FenError { /// Invalid FEN input. InvalidFen, /// Invalid chess position. - InvalidPosition(InvalidError), + InvalidPosition(ValidationError), } impl std::fmt::Display for FenError { @@ -29,9 +29,9 @@ 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 { +/// Allow converting a [ValidationError] into [FenError], for use with the '?' operator. +impl From for FenError { + fn from(err: ValidationError) -> Self { Self::InvalidPosition(err) } } From 753f1590d1cb3d509229f1628dc344b4522fc203 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:32:59 +0100 Subject: [PATCH 224/255] Add 'Panics' section to 'from_index' methods --- src/board/castle_rights.rs | 4 ++++ src/board/color.rs | 4 ++++ src/board/file.rs | 4 ++++ src/board/piece.rs | 4 ++++ src/board/rank.rs | 4 ++++ src/board/square.rs | 4 ++++ 6 files changed, 24 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index b34d952..5b06544 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -18,6 +18,10 @@ impl CastleRights { pub const NUM_VARIANTS: usize = 4; /// Convert from a castle rights index into a [CastleRights] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/color.rs b/src/board/color.rs index 66b21b3..17ebcb5 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -19,6 +19,10 @@ impl Color { } /// Convert from a color index into a [Color] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/file.rs b/src/board/file.rs index 1475e9a..5398660 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -35,6 +35,10 @@ impl File { } /// Convert from a file index into a [File] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/piece.rs b/src/board/piece.rs index 58f989a..76d9df9 100644 --- a/src/board/piece.rs +++ b/src/board/piece.rs @@ -28,6 +28,10 @@ impl Piece { } /// Convert from a piece index into a [Piece] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/rank.rs b/src/board/rank.rs index f448df5..f716bde 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -35,6 +35,10 @@ impl Rank { } /// Convert from a rank index into a [Rank] type. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < Self::NUM_VARIANTS); diff --git a/src/board/square.rs b/src/board/square.rs index 958c3c9..afbd9bf 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -39,6 +39,10 @@ impl Square { ]; /// Construct a [Square] from a [File] and [Rank]. + /// + /// # Panics + /// + /// Panics if the index is out of bounds. #[inline(always)] pub fn new(file: File, rank: Rank) -> Self { // SAFETY: we know the value is in-bounds From 8e688a0cac264339791e22db14dbef05243ef977 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 20:44:15 +0100 Subject: [PATCH 225/255] Add 'try_from_index' implementations --- src/board/castle_rights.rs | 15 ++++++++++++--- src/board/color.rs | 15 ++++++++++++--- src/board/file.rs | 14 +++++++++++--- src/board/piece.rs | 15 ++++++++++++--- src/board/rank.rs | 14 +++++++++++--- src/board/square.rs | 15 ++++++++++++--- 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 5b06544..5bd9d91 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -24,9 +24,18 @@ impl CastleRights { /// Panics if the index is out of bounds. #[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) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a castle rights index into a [CastleRights] type. Returns [None] if the index + /// is out of bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a castle rights index into a [CastleRights] type, no bounds checking. diff --git a/src/board/color.rs b/src/board/color.rs index 17ebcb5..e41d3c5 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -25,9 +25,18 @@ impl Color { /// Panics if the index is out of bounds. #[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) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a color index into a [Color] type. Returns [None] if the index is out of + /// bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a color index into a [Color] type, no bounds checking. diff --git a/src/board/file.rs b/src/board/file.rs index 5398660..1641498 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -41,9 +41,17 @@ impl File { /// Panics if the index is out of bounds. #[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) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a file index into a [File] type. Returns [None] if the index is out of bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a file index into a [File] type, no bounds checking. diff --git a/src/board/piece.rs b/src/board/piece.rs index 76d9df9..f6fdce4 100644 --- a/src/board/piece.rs +++ b/src/board/piece.rs @@ -34,9 +34,18 @@ impl Piece { /// Panics if the index is out of bounds. #[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) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a piece index into a [Piece] type. Returns [None] if the index is out of + /// bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a piece index into a [Piece] type, no bounds checking. diff --git a/src/board/rank.rs b/src/board/rank.rs index f716bde..1632229 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -41,9 +41,17 @@ impl Rank { /// Panics if the index is out of bounds. #[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) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a rank index into a [Rank] type. Returns [None] if the index is out of bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a rank index into a [Rank] type, no bounds checking. diff --git a/src/board/square.rs b/src/board/square.rs index afbd9bf..b5de25b 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -57,9 +57,18 @@ impl Square { /// Convert from a square index into a [Square] 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) } + Self::try_from_index(index).expect("index out of bouds") + } + + /// Convert from a square index into a [Square] type. Returns [None] if the index is out of + /// bounds. + pub fn try_from_index(index: usize) -> Option { + if index < Self::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + Some(unsafe { Self::from_index_unchecked(index) }) + } else { + None + } } /// Convert from a square index into a [Square] type, no bounds checking. From d74605ba5c34b1dbc2424cebc077b99d30378c3b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 21:14:31 +0100 Subject: [PATCH 226/255] Use 'NUM_VARIANTS' where appropriate --- src/board/bitboard/mod.rs | 6 +++--- src/board/chess_board/builder.rs | 6 +++--- src/board/chess_board/mod.rs | 4 ++-- src/fen.rs | 6 +++--- src/movegen/moves.rs | 22 ++++++++++++---------- src/movegen/wizardry/mod.rs | 6 +++--- 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index b0ec90a..9059235 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,4 +1,4 @@ -use super::Square; +use super::{File, Rank, Square}; use crate::utils::static_assert; mod error; @@ -21,7 +21,7 @@ impl Bitboard { pub const ALL: Bitboard = Bitboard(u64::MAX); /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. - pub const RANKS: [Self; 8] = [ + pub const RANKS: [Self; Rank::NUM_VARIANTS] = [ Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010), Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100), @@ -33,7 +33,7 @@ impl Bitboard { ]; /// Array of bitboards representing the eight files, in order from file A to file H. - pub const FILES: [Self; 8] = [ + pub const FILES: [Self; File::NUM_VARIANTS] = [ 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), diff --git a/src/board/chess_board/builder.rs b/src/board/chess_board/builder.rs index 0304af5..679b3b7 100644 --- a/src/board/chess_board/builder.rs +++ b/src/board/chess_board/builder.rs @@ -4,7 +4,7 @@ use crate::board::{Bitboard, CastleRights, ChessBoard, Color, Piece, Square, Val #[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], + pieces: [Option<(Piece, Color)>; Square::NUM_VARIANTS], // Same fields as [ChessBoard]. castle_rights: [CastleRights; Color::NUM_VARIANTS], en_passant: Option, @@ -17,8 +17,8 @@ pub struct ChessBoardBuilder { impl ChessBoardBuilder { pub fn new() -> Self { Self { - pieces: [None; 64], - castle_rights: [CastleRights::NoSide; 2], + pieces: [None; Square::NUM_VARIANTS], + castle_rights: [CastleRights::NoSide; Color::NUM_VARIANTS], en_passant: Default::default(), half_move_clock: Default::default(), side: Color::White, diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index edf92a2..63dbe51 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -411,7 +411,7 @@ impl Default for ChessBoard { | Rank::Second.into_bitboard() | Rank::Seventh.into_bitboard() | Rank::Eighth.into_bitboard(), - castle_rights: [CastleRights::BothSides; 2], + castle_rights: [CastleRights::BothSides; Color::NUM_VARIANTS], en_passant: None, half_move_clock: 0, total_plies: 0, @@ -699,7 +699,7 @@ mod test { | Square::F3 | Square::G1 | Square::H2, - castle_rights: [CastleRights::NoSide; 2], + castle_rights: [CastleRights::NoSide; Color::NUM_VARIANTS], en_passant: None, half_move_clock: 0, total_plies: 0, diff --git a/src/fen.rs b/src/fen.rs index 41a0696..03c60c1 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -37,7 +37,7 @@ impl From for FenError { } /// Convert the castling rights segment of a FEN string to an array of [CastleRights]. -impl FromFen for [CastleRights; 2] { +impl FromFen for [CastleRights; Color::NUM_VARIANTS] { type Err = FenError; fn from_fen(s: &str) -> Result { @@ -45,7 +45,7 @@ impl FromFen for [CastleRights; 2] { return Err(FenError::InvalidFen); } - let mut res = [CastleRights::NoSide; 2]; + let mut res = [CastleRights::NoSide; Color::NUM_VARIANTS]; if s == "-" { return Ok(res); @@ -134,7 +134,7 @@ impl FromFen for ChessBoard { let mut builder = ChessBoardBuilder::new(); - let castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?; + let castle_rights = <[CastleRights; Color::NUM_VARIANTS]>::from_fen(castling_rights)?; for color in Color::iter() { builder.with_castle_rights(castle_rights[color.index()], color); } diff --git a/src/movegen/moves.rs b/src/movegen/moves.rs index 9840083..d46a733 100644 --- a/src/movegen/moves.rs +++ b/src/movegen/moves.rs @@ -13,12 +13,12 @@ use crate::{ // A pre-rolled RNG for magic bitboard generation, using pre-determined values. struct PreRolledRng { - numbers: [u64; 64], + numbers: [u64; Square::NUM_VARIANTS], current_index: usize, } impl PreRolledRng { - pub fn new(numbers: [u64; 64]) -> Self { + pub fn new(numbers: [u64; Square::NUM_VARIANTS]) -> Self { Self { numbers, current_index: 0, @@ -39,7 +39,8 @@ impl RandGen for PreRolledRng { /// 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(); + static PAWN_MOVES: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> = + OnceLock::new(); // If there is a piece in front of the pawn, it can't advance if !(color.backward_direction().move_board(blockers) & square).is_empty() { @@ -47,7 +48,7 @@ pub fn pawn_quiet_moves(color: Color, square: Square, blockers: Bitboard) -> Bit } PAWN_MOVES.get_or_init(|| { - let mut res = [[Bitboard::EMPTY; 64]; 2]; + let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]; for color in Color::iter() { for square in Square::iter() { res[color.index()][square.index()] = @@ -60,10 +61,11 @@ pub fn pawn_quiet_moves(color: Color, square: Square, blockers: Bitboard) -> Bit /// 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(); + static PAWN_ATTACKS: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> = + OnceLock::new(); PAWN_ATTACKS.get_or_init(|| { - let mut res = [[Bitboard::EMPTY; 64]; 2]; + let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]; for color in Color::iter() { for square in Square::iter() { res[color.index()][square.index()] = naive::pawn_captures(color, square); @@ -81,9 +83,9 @@ pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard /// 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(); + static KNIGHT_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new(); KNIGHT_MOVES.get_or_init(|| { - let mut res = [Bitboard::EMPTY; 64]; + let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS]; for square in Square::iter() { res[square.index()] = naive::knight_moves(square) } @@ -122,9 +124,9 @@ pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard { /// 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(); + static KING_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new(); KING_MOVES.get_or_init(|| { - let mut res = [Bitboard::EMPTY; 64]; + let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS]; for square in Square::iter() { res[square.index()] = naive::king_moves(square) } diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index f2794aa..6918d09 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -59,7 +59,7 @@ impl MagicMoves { // region:sourcegen /// A set of magic numbers for bishop move generation. -pub(crate) const BISHOP_SEED: [u64; 64] = [ +pub(crate) const BISHOP_SEED: [u64; Square::NUM_VARIANTS] = [ 4908958787341189172, 1157496606860279808, 289395876198088778, @@ -127,7 +127,7 @@ pub(crate) const BISHOP_SEED: [u64; 64] = [ ]; /// A set of magic numbers for rook move generation. -pub(crate) const ROOK_SEED: [u64; 64] = [ +pub(crate) const ROOK_SEED: [u64; Square::NUM_VARIANTS] = [ 2341871943948451840, 18015635528220736, 72066665545773824, @@ -250,7 +250,7 @@ mod test { )?; writeln!( &mut res, - "pub(crate) const {}_SEED: [u64; 64] = [", + "pub(crate) const {}_SEED: [u64; Square::NUM_VARIANTS] = [", piece_type.to_uppercase() )?; for magic in values { From b0e9e3cbcce686a466b0596df1e6704e52920c7f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 21:16:59 +0100 Subject: [PATCH 227/255] Add explicit 'rustfmt' configuration --- rustfmt.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..e69de29 From cb06fc10c8d7109eb61c942ec61f41aae96e967e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 21:25:45 +0100 Subject: [PATCH 228/255] Fix broken link in documentation --- src/board/chess_board/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index fe8e4c0..bfc3bdc 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -1,4 +1,4 @@ -/// A singular type for all errors that could happen during [ChessBoard::is_valid]. +/// A singular type for all errors that could happen during [crate::board::ChessBoard::is_valid]. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ValidationError { /// Too many pieces. From b289927e3a9201f5d63e38dae1e8046b6ea876d6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 23:39:37 +0100 Subject: [PATCH 229/255] Loosen GDB utils constructors --- 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 0481d8d..494fb38 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -13,6 +13,8 @@ class Square(object): RANKS = list(map(lambda n: str(n + 1), range(8))) def __init__(self, val): + if isinstance(val, Square): + val = val._val self._val = val @classmethod @@ -37,6 +39,8 @@ class Bitboard(object): """ def __init__(self, val): + if isinstance(val, Bitboard): + val = val._val self._val = val def __str__(self): From f0edd0abc7d46f6fe8e78bb7c8228046ccc9fd45 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 23:39:37 +0100 Subject: [PATCH 230/255] Properly handle 'Optional' in pretty-printers --- utils/gdb/seer_pretty_printers.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 494fb38..47bce35 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -4,6 +4,17 @@ import gdb import gdb.printing +def optional(constructor, val): + try: + return constructor(val["Some"]["__0"]) + except gdb.error: + return None + + +def print_opt(val): + return "(None)" if val is None else str(val) + + class Square(object): """ Python representation of a 'seer::board::square::Square' raw value. @@ -217,7 +228,6 @@ class Move(object): "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] @@ -238,6 +248,7 @@ class ChessBoard(object): half_move_clock, total_plies, side, + en_passant, ): self._piece_occupancy = list(map(Bitboard, piece_occupancy)) self._color_occupancy = list(map(Bitboard, color_occupancy)) @@ -245,6 +256,7 @@ class ChessBoard(object): self._half_move_clock = int(half_move_clock) self._total_plies = int(total_plies) self._side = Color(side) + self._en_passant = None if en_passant is None else Square(en_passant) @classmethod def from_gdb(cls, val): @@ -252,10 +264,10 @@ class ChessBoard(object): [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"])), + optional(int, val["en_passant"]), ) def at(self, square): @@ -296,6 +308,7 @@ class ChessBoard(object): "Half-move clock: " + str(self._half_move_clock), "Total plies: " + str(self._total_plies), "Side to play: " + str(self._side), + "En passant: " + print_opt(self._en_passant), ] return "\n".join(res) From fcbcc3cdefeb2590cd7c77e979a897e97ca1cb3d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 4 Apr 2024 00:41:46 +0100 Subject: [PATCH 231/255] Add 'from_gdb' constructors in GDB utils --- utils/gdb/seer_pretty_printers.py | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 47bce35..56caadb 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -28,6 +28,10 @@ class Square(object): val = val._val self._val = val + @classmethod + def from_gdb(cls, val): + return cls(int(val)) + @classmethod def from_file_rank(cls, file, rank): return cls(file * 8 + rank) @@ -54,6 +58,10 @@ class Bitboard(object): val = val._val self._val = val + @classmethod + def from_gdb(cls, val): + return cls(int(val["__0"])) + def __str__(self): return "[" + ", ".join(map(str, self.squares)) + "]" @@ -80,6 +88,10 @@ class CastleRights(enum.IntEnum): QUEEN_SIDE = 2 BOTH_SIDES = 3 + @classmethod + def from_gdb(cls, val): + return cls(int(val)) + def __str__(self): return self.name.title().replace("_", "") @@ -93,6 +105,10 @@ class Color(enum.IntEnum): WHITE = 0 BLACK = 1 + @classmethod + def from_gdb(cls, val): + return cls(int(val)) + def __str__(self): return self.name.title() @@ -112,6 +128,10 @@ class File(enum.IntEnum): G = 6 H = 7 + @classmethod + def from_gdb(cls, val): + return cls(int(val)) + def __str__(self): return self.name.title() @@ -131,6 +151,10 @@ class Rank(enum.IntEnum): Seventh = 6 Eighth = 7 + @classmethod + def from_gdb(cls, val): + return cls(int(val)) + def __str__(self): return self.name.title() @@ -148,6 +172,10 @@ class Piece(enum.IntEnum): KNIGHT = 4 PAWN = 5 + @classmethod + def from_gdb(cls, val): + return cls(int(val)) + def __str__(self): return self.name.title() @@ -179,6 +207,10 @@ class Move(object): def __init__(self, val): self._val = val + @classmethod + def from_gdb(cls, val): + return cls(int(val)) + @property def piece(self): return Piece(self._val >> self.PIECE_SHIFT & self.PIECE_MASK) From 1646c055fd9d6c7c8ff34457d267b7465292c65b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 4 Apr 2024 00:41:46 +0100 Subject: [PATCH 232/255] Use 'from_gdb' constructors in GDB utils Makes it much more readable. --- utils/gdb/seer_pretty_printers.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 56caadb..c03e992 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -293,13 +293,13 @@ class ChessBoard(object): @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], + [Bitboard.from_gdb(val["piece_occupancy"][p]) for p in Piece], + [Bitboard.from_gdb(val["color_occupancy"][c]) for c in Color], + [CastleRights.from_gdb(val["castle_rights"][c]) for c in Color], int(val["half_move_clock"]), int(val["total_plies"]), - Color(int(val["side"])), - optional(int, val["en_passant"]), + Color.from_gdb(val["side"]), + optional(Square.from_gdb, val["en_passant"]), ) def at(self, square): @@ -349,7 +349,7 @@ class SquarePrinter(object): "Print a seer::board::square::Square" def __init__(self, val): - self._val = Square(val) + self._val = Square.from_gdb(val) def to_string(self): return str(self._val) @@ -359,7 +359,7 @@ class BitboardPrinter(object): "Print a seer::board::bitboard::Bitboard" def __init__(self, val): - self._val = Bitboard(int(val["__0"])) + self._val = Bitboard.from_gdb(val) def to_string(self): return "Bitboard{" + str(self._val)[1:-1] + "}" @@ -369,7 +369,7 @@ class CastleRightsPrinter(object): "Print a seer::board::castle_rights::CastleRights" def __init__(self, val): - self._val = CastleRights(int(val)) + self._val = CastleRights.from_gdb(val) def to_string(self): return str(self._val) @@ -379,7 +379,7 @@ class ColorPrinter(object): "Print a seer::board::color::Color" def __init__(self, val): - self._val = Color(int(val)) + self._val = Color.from_gdb(val) def to_string(self): return str(self._val) @@ -389,7 +389,7 @@ class FilePrinter(object): "Print a seer::board::file::File" def __init__(self, val): - self._val = File(int(val)) + self._val = File.from_gdb(val) def to_string(self): return str(self._val) @@ -399,7 +399,7 @@ class RankPrinter(object): "Print a seer::board::rank::Rank" def __init__(self, val): - self._val = Rank(int(val)) + self._val = Rank.from_gdb(val) def to_string(self): return str(self._val) @@ -409,7 +409,7 @@ class PiecePrinter(object): "Print a seer::board::piece::Piece" def __init__(self, val): - self._val = Piece(int(val)) + self._val = Piece.from_gdb(val) def to_string(self): return str(self._val) @@ -419,7 +419,7 @@ class MovePrinter(object): "Print a seer::board::move::Move" def __init__(self, val): - self._val = Move(int(val["__0"])) + self._val = Move.from_gdb(val) def to_string(self): return str(self._val) From adad4118ae73008bccb27b5e8a7e9df0f51b50aa Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 23:24:12 +0100 Subject: [PATCH 233/255] Account for captures in 'ChessBoard::{,un}do_move' This is a silly thing to forget... --- src/board/chess_board/mod.rs | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 63dbe51..e7d9efd 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -37,6 +37,7 @@ pub struct NonReversibleState { castle_rights: [CastleRights; Color::NUM_VARIANTS], en_passant: Option, half_move_clock: u32, // Should *probably* never go higher than 100. + captured_piece: Option, } impl ChessBoard { @@ -134,11 +135,13 @@ impl ChessBoard { /// 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 { + let opponent = !self.current_player(); // Save non-revertible state let state = NonReversibleState { castle_rights: self.castle_rights, en_passant: self.en_passant, half_move_clock: self.half_move_clock, + captured_piece: chess_move.capture(), }; // Non-revertible state modification @@ -167,6 +170,11 @@ impl ChessBoard { _ => *castle_rights, } } + if let Some(piece) = chess_move.capture() { + *self.piece_occupancy_mut(piece) ^= chess_move.destination(); + *self.color_occupancy_mut(opponent) ^= chess_move.destination(); + self.combined_occupancy ^= chess_move.destination(); + } // Revertible state modification self.xor( @@ -188,6 +196,11 @@ impl ChessBoard { self.castle_rights = previous.castle_rights; self.en_passant = previous.en_passant; self.half_move_clock = previous.half_move_clock; + if let Some(piece) = previous.captured_piece { + *self.piece_occupancy_mut(piece) ^= chess_move.destination(); + *self.color_occupancy_mut(self.current_player()) ^= chess_move.destination(); + self.combined_occupancy ^= chess_move.destination(); + } // Restore revertible state self.xor( @@ -837,4 +850,29 @@ mod test { .unwrap() ); } + + #[test] + fn do_move_undo_capture() { + let mut position = ChessBoard::from_fen("3q3k/8/8/8/8/8/8/K2Q4 w - - 0 1").unwrap(); + let expected = ChessBoard::from_fen("3Q3k/8/8/8/8/8/8/K7 b - - 0 1").unwrap(); + let original = position.clone(); + + let capture = MoveBuilder { + piece: Piece::Queen, + start: Square::D1, + destination: Square::D8, + capture: Some(Piece::Queen), + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(); + + let state = position.do_move(capture); + assert_eq!(position, expected); + + position.undo_move(capture, state); + assert_eq!(position, original); + } } From 1c8a101689d934b0f78b137bd848f345d5f22e0b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 3 Apr 2024 23:39:37 +0100 Subject: [PATCH 234/255] Simplify 'Move' Making 'Move' lightweight sounds like a better idea now that I am looking at it with fresh eyes... --- src/board/chess_board/mod.rs | 125 +++++------------ src/board/move.rs | 216 ++---------------------------- src/fen.rs | 44 +----- utils/gdb/seer_pretty_printers.py | 66 ++------- 4 files changed, 61 insertions(+), 390 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index e7d9efd..33a5580 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -136,21 +136,32 @@ impl ChessBoard { #[inline(always)] pub fn do_move(&mut self, chess_move: Move) -> NonReversibleState { let opponent = !self.current_player(); + let is_capture = !(self.combined_occupancy() & chess_move.destination()).is_empty(); + let move_piece = Piece::iter() + .find(|&p| !(self.piece_occupancy(p) & chess_move.start()).is_empty()) + .unwrap(); + let captured_piece = Piece::iter() + .skip(1) // No need to check for the king here + .find(|&p| !(self.occupancy(p, opponent) & chess_move.destination()).is_empty()); + let is_double_step = move_piece == Piece::Pawn + && chess_move.start().rank() == self.current_player().second_rank() + && chess_move.destination().rank() == self.current_player().fourth_rank(); + // Save non-revertible state let state = NonReversibleState { castle_rights: self.castle_rights, en_passant: self.en_passant, half_move_clock: self.half_move_clock, - captured_piece: chess_move.capture(), + captured_piece, }; // Non-revertible state modification - if chess_move.capture().is_some() || chess_move.piece() == Piece::Pawn { + if is_capture || move_piece == Piece::Pawn { self.half_move_clock = 0; } else { self.half_move_clock += 1; } - if chess_move.is_double_step() { + if is_double_step { let target_square = Square::new( chess_move.destination().file(), self.current_player().third_rank(), @@ -159,10 +170,10 @@ impl ChessBoard { } else { self.en_passant = None; } - if chess_move.is_castling() || chess_move.piece() == Piece::King { + if move_piece == Piece::King { *self.castle_rights_mut(self.current_player()) = CastleRights::NoSide; } - if chess_move.piece() == Piece::Rook { + if 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(), @@ -170,7 +181,7 @@ impl ChessBoard { _ => *castle_rights, } } - if let Some(piece) = chess_move.capture() { + if let Some(piece) = captured_piece { *self.piece_occupancy_mut(piece) ^= chess_move.destination(); *self.color_occupancy_mut(opponent) ^= chess_move.destination(); self.combined_occupancy ^= chess_move.destination(); @@ -179,7 +190,7 @@ impl ChessBoard { // Revertible state modification self.xor( self.current_player(), - chess_move.piece(), + move_piece, chess_move.start() | chess_move.destination(), ); self.total_plies += 1; @@ -196,8 +207,15 @@ impl ChessBoard { self.castle_rights = previous.castle_rights; self.en_passant = previous.en_passant; self.half_move_clock = previous.half_move_clock; + + let move_piece = Piece::iter() + // We're looking for the *destination* as this is *undoing* the move + .find(|&p| !(self.piece_occupancy(p) & chess_move.destination()).is_empty()) + .unwrap(); + if let Some(piece) = previous.captured_piece { *self.piece_occupancy_mut(piece) ^= chess_move.destination(); + // The capture affected the *current* player, from our post-move POV *self.color_occupancy_mut(self.current_player()) ^= chess_move.destination(); self.combined_occupancy ^= chess_move.destination(); } @@ -206,7 +224,7 @@ impl ChessBoard { self.xor( // The move was applied at the turn *before* the current player !self.current_player(), - chess_move.piece(), + move_piece, chess_move.start() | chess_move.destination(), ); self.total_plies -= 1; @@ -435,7 +453,6 @@ impl Default for ChessBoard { #[cfg(test)] mod test { - use crate::board::MoveBuilder; use crate::fen::FromFen; use super::*; @@ -729,57 +746,21 @@ mod test { // 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(), - ); + position.do_move(Move::new(Square::E2, Square::E4, None)); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") .unwrap() ); // And now c5 - position.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(), - ); + position.do_move(Move::new(Square::C7, Square::C5, None)); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") .unwrap() ); // Finally, Nf3 - position.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(), - ); + position.do_move(Move::new(Square::G1, Square::F3, None)); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") @@ -792,43 +773,13 @@ mod test { // 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 move_1 = Move::new(Square::E2, Square::E4, None); 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 move_2 = Move::new(Square::C7, Square::C5, None); 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 move_3 = Move::new(Square::G1, Square::F3, None); let state_3 = position.do_move(move_3); // Now revert each move one-by-one position.undo_move(move_3, state_3); @@ -857,17 +808,7 @@ mod test { let expected = ChessBoard::from_fen("3Q3k/8/8/8/8/8/8/K7 b - - 0 1").unwrap(); let original = position.clone(); - let capture = MoveBuilder { - piece: Piece::Queen, - start: Square::D1, - destination: Square::D8, - capture: Some(Piece::Queen), - promotion: None, - en_passant: false, - double_step: false, - castling: false, - } - .into(); + let capture = Move::new(Square::D1, Square::D8, None); let state = position.do_move(capture); assert_eq!(position, expected); diff --git a/src/board/move.rs b/src/board/move.rs index c7a6980..7897988 100644 --- a/src/board/move.rs +++ b/src/board/move.rs @@ -1,232 +1,42 @@ 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; +pub struct Move { + start: Square, + destination: Square, + promotion: Option, } 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) } + pub fn new(start: Square, destination: Square, promotion: Option) -> Self { + Self { + start, + destination, + promotion, + } } /// 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) } + self.start } /// 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 - } + self.destination } /// 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()); + self.promotion } } diff --git a/src/fen.rs b/src/fen.rs index 03c60c1..81d5e74 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -202,7 +202,7 @@ impl FromFen for ChessBoard { #[cfg(test)] mod test { - use crate::board::MoveBuilder; + use crate::board::Move; use super::*; @@ -220,57 +220,21 @@ mod 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(), - ); + position.do_move(Move::new(Square::E2, Square::E4, None)); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") .unwrap(), position ); // And now c5 - position.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(), - ); + position.do_move(Move::new(Square::C7, Square::C5, None)); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") .unwrap(), position ); // Finally, Nf3 - position.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(), - ); + position.do_move(Move::new(Square::G1, Square::F3, None)); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") .unwrap(), diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index c03e992..37a5297 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -186,79 +186,35 @@ class Move(object): 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 + def __init__(self, start, destination, promotion): + self._start = Square(start) + self._destination = Square(destination) + self._promotion = Piece(promotion) @classmethod def from_gdb(cls, val): - return cls(int(val)) - - @property - def piece(self): - return Piece(self._val >> self.PIECE_SHIFT & self.PIECE_MASK) + start = Square(int(val["start"])) + destination = Square(int(val["destination"])) + promotion = optional(Piece.from_gdb, val["promotion"]) + cls(start, destination, promotion) @property def start(self): - return Square(self._val >> self.START_SHIFT & self.START_MASK) + return self._start @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) + return self._destination @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) + return self._promotion def __str__(self): KEYS = [ - "piece", "start", "destination", - "capture", "promotion", - "en_passant", - "double_step", - "castling", ] indent = lambda s: " " + s From 9507432bd3572d8a2c55e4c2b2974a3fb2f2dc4f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 5 Apr 2024 23:35:46 +0100 Subject: [PATCH 235/255] Modify castling rights after rook capture --- src/board/chess_board/mod.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 33a5580..ba010d4 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -185,6 +185,13 @@ impl ChessBoard { *self.piece_occupancy_mut(piece) ^= chess_move.destination(); *self.color_occupancy_mut(opponent) ^= chess_move.destination(); self.combined_occupancy ^= chess_move.destination(); + // If a rook is captured, it loses its castling rights + let castle_rights = self.castle_rights_mut(opponent); + *castle_rights = match (piece, chess_move.destination().file()) { + (Piece::Rook, File::A) => castle_rights.without_queen_side(), + (Piece::Rook, File::H) => castle_rights.without_king_side(), + _ => *castle_rights, + }; } // Revertible state modification @@ -768,6 +775,17 @@ mod test { ); } + #[test] + fn do_move_capture_changes_castling() { + let mut position = ChessBoard::from_fen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1").unwrap(); + let expected = ChessBoard::from_fen("r3k2R/8/8/8/8/8/8/R3K3 b Qq - 0 1").unwrap(); + + let capture = Move::new(Square::H1, Square::H8, None); + + position.do_move(capture); + assert_eq!(position, expected); + } + #[test] fn do_move_and_undo() { // Start from default position From f9ba6fa68090f7266163c08fcf3efd7f4f14c732 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 00:20:09 +0100 Subject: [PATCH 236/255] Refactor castling right management in 'do_move' I find it more readable. --- src/board/chess_board/mod.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index ba010d4..c594adf 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -170,17 +170,13 @@ impl ChessBoard { } else { self.en_passant = None; } - if move_piece == Piece::King { - *self.castle_rights_mut(self.current_player()) = CastleRights::NoSide; - } - if 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, - } - } + let castle_rights = self.castle_rights_mut(self.current_player()); + *castle_rights = match (move_piece, chess_move.start().file()) { + (Piece::Rook, File::A) => castle_rights.without_queen_side(), + (Piece::Rook, File::H) => castle_rights.without_king_side(), + (Piece::King, _) => CastleRights::NoSide, + _ => *castle_rights, + }; if let Some(piece) = captured_piece { *self.piece_occupancy_mut(piece) ^= chess_move.destination(); *self.color_occupancy_mut(opponent) ^= chess_move.destination(); From fd7ff60e1bf154e62ca06457f66535cd3de394b0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 11:11:41 +0100 Subject: [PATCH 237/255] Rename 'ChessBoard::{,un}play_move' --- src/board/chess_board/mod.rs | 36 ++++++++++++++++++------------------ src/fen.rs | 6 +++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index c594adf..9de3fe2 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -134,7 +134,7 @@ impl ChessBoard { /// 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 { + pub fn play_move(&mut self, chess_move: Move) -> NonReversibleState { let opponent = !self.current_player(); let is_capture = !(self.combined_occupancy() & chess_move.destination()).is_empty(); let move_piece = Piece::iter() @@ -205,7 +205,7 @@ impl ChessBoard { /// 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) { + pub fn unplay_move(&mut self, chess_move: Move, previous: NonReversibleState) { // Restore non-revertible state self.castle_rights = previous.castle_rights; self.en_passant = previous.en_passant; @@ -745,25 +745,25 @@ mod test { } #[test] - fn do_move() { + fn play_move() { // Start from default position let mut position = ChessBoard::default(); // Modify it to account for e4 move - position.do_move(Move::new(Square::E2, Square::E4, None)); + position.play_move(Move::new(Square::E2, Square::E4, None)); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") .unwrap() ); // And now c5 - position.do_move(Move::new(Square::C7, Square::C5, None)); + position.play_move(Move::new(Square::C7, Square::C5, None)); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") .unwrap() ); // Finally, Nf3 - position.do_move(Move::new(Square::G1, Square::F3, None)); + position.play_move(Move::new(Square::G1, Square::F3, None)); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") @@ -772,43 +772,43 @@ mod test { } #[test] - fn do_move_capture_changes_castling() { + fn play_move_capture_changes_castling() { let mut position = ChessBoard::from_fen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1").unwrap(); let expected = ChessBoard::from_fen("r3k2R/8/8/8/8/8/8/R3K3 b Qq - 0 1").unwrap(); let capture = Move::new(Square::H1, Square::H8, None); - position.do_move(capture); + position.play_move(capture); assert_eq!(position, expected); } #[test] - fn do_move_and_undo() { + fn play_move_and_undo() { // Start from default position let mut position = ChessBoard::default(); // Modify it to account for e4 move let move_1 = Move::new(Square::E2, Square::E4, None); - let state_1 = position.do_move(move_1); + let state_1 = position.play_move(move_1); // And now c5 let move_2 = Move::new(Square::C7, Square::C5, None); - let state_2 = position.do_move(move_2); + let state_2 = position.play_move(move_2); // Finally, Nf3 let move_3 = Move::new(Square::G1, Square::F3, None); - let state_3 = position.do_move(move_3); + let state_3 = position.play_move(move_3); // Now revert each move one-by-one - position.undo_move(move_3, state_3); + position.unplay_move(move_3, state_3); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") .unwrap() ); - position.undo_move(move_2, state_2); + position.unplay_move(move_2, state_2); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") .unwrap() ); - position.undo_move(move_1, state_1); + position.unplay_move(move_1, state_1); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") @@ -817,17 +817,17 @@ mod test { } #[test] - fn do_move_undo_capture() { + fn play_move_undo_capture() { let mut position = ChessBoard::from_fen("3q3k/8/8/8/8/8/8/K2Q4 w - - 0 1").unwrap(); let expected = ChessBoard::from_fen("3Q3k/8/8/8/8/8/8/K7 b - - 0 1").unwrap(); let original = position.clone(); let capture = Move::new(Square::D1, Square::D8, None); - let state = position.do_move(capture); + let state = position.play_move(capture); assert_eq!(position, expected); - position.undo_move(capture, state); + position.unplay_move(capture, state); assert_eq!(position, original); } } diff --git a/src/fen.rs b/src/fen.rs index 81d5e74..df4005d 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -220,21 +220,21 @@ mod test { fn en_passant() { // Start from default position let mut position = ChessBoard::default(); - position.do_move(Move::new(Square::E2, Square::E4, None)); + position.play_move(Move::new(Square::E2, Square::E4, None)); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") .unwrap(), position ); // And now c5 - position.do_move(Move::new(Square::C7, Square::C5, None)); + position.play_move(Move::new(Square::C7, Square::C5, None)); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") .unwrap(), position ); // Finally, Nf3 - position.do_move(Move::new(Square::G1, Square::F3, None)); + position.play_move(Move::new(Square::G1, Square::F3, None)); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") .unwrap(), From 7c4f5317b04e8dfc8df9252d076a6242d8deb674 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 11:15:30 +0100 Subject: [PATCH 238/255] Remove period at the end of error messages --- src/board/chess_board/error.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index bfc3bdc..5af3b25 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -30,24 +30,24 @@ pub enum ValidationError { impl std::fmt::Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let error_msg = match self { - Self::TooManyPieces => "Too many pieces.", - Self::MissingKing => "Missing king.", - Self::InvalidPawnPosition => "Pawns on the first/last rank.", + Self::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." + "Castling rights do not match up with the state of the board" } Self::InvalidEnPassant => { - "En-passant target square is not empty, behind an opponent's pawn, on the correct rank." + "En-passant target square is not empty, behind an opponent's pawn, on the correct rank" } - Self::NeighbouringKings => "The two kings are next to each other.", - Self::OpponentInCheck => "The opponent is currently in check.", - Self::OverlappingPieces => "The piece-specific boards are overlapping.", - Self::OverlappingColors => "The color-specific boards are overlapping.", + Self::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." + "The pre-computed combined occupancy boards does not match the other boards" } - Self::HalfMoveClockTooHigh => "Half-move clock is higher than total number of plies.", - Self::IncoherentPlieCount => "The total plie count does not match the current player.", + Self::HalfMoveClockTooHigh => "Half-move clock is higher than total number of plies", + Self::IncoherentPlieCount => "The total plie count does not match the current player", }; write!(f, "{}", error_msg) } From 5dce65c57023bb7d17b81724caf80518a599fd79 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 11:17:45 +0100 Subject: [PATCH 239/255] Use lower-case in error messages --- src/board/chess_board/error.rs | 24 ++++++++++++------------ src/fen.rs | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs index 5af3b25..b1ed769 100644 --- a/src/board/chess_board/error.rs +++ b/src/board/chess_board/error.rs @@ -30,24 +30,24 @@ pub enum ValidationError { impl std::fmt::Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let error_msg = match self { - Self::TooManyPieces => "Too many pieces", - Self::MissingKing => "Missing king", - Self::InvalidPawnPosition => "Pawns on the first/last rank", + Self::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" + "castling rights do not match up with the state of the board" } Self::InvalidEnPassant => { - "En-passant target square is not empty, behind an opponent's pawn, on the correct rank" + "en-passant target square is not empty, behind an opponent's pawn, on the correct rank" } - Self::NeighbouringKings => "The two kings are next to each other", - Self::OpponentInCheck => "The opponent is currently in check", - Self::OverlappingPieces => "The piece-specific boards are overlapping", - Self::OverlappingColors => "The color-specific boards are overlapping", + Self::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" + "the pre-computed combined occupancy boards does not match the other boards" } - Self::HalfMoveClockTooHigh => "Half-move clock is higher than total number of plies", - Self::IncoherentPlieCount => "The total plie count does not match the current player", + Self::HalfMoveClockTooHigh => "half-move clock is higher than total number of plies", + Self::IncoherentPlieCount => "the total plie count does not match the current player", }; write!(f, "{}", error_msg) } diff --git a/src/fen.rs b/src/fen.rs index df4005d..4c05879 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -21,8 +21,8 @@ pub enum FenError { impl std::fmt::Display for FenError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::InvalidFen => write!(f, "Invalid FEN input"), - Self::InvalidPosition(err) => write!(f, "Invalid chess position: {}", err), + Self::InvalidFen => write!(f, "invalid FEN input"), + Self::InvalidPosition(err) => write!(f, "invalid chess position: {}", err), } } } From c412be501f495d8e1ff58e548cff2662f2a22f40 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 11:22:13 +0100 Subject: [PATCH 240/255] Fix 'ChessBoard::en_passant' documentation --- 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 9de3fe2..b0ddf42 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -47,7 +47,7 @@ impl ChessBoard { self.side } - /// Return the [Square] currently occupied by a pawn that can be captured en-passant, or `None` + /// Return the target [Square] that can be captured en-passant, or `None` #[inline(always)] pub fn en_passant(&self) -> Option { self.en_passant From 29e50a65dc9093055bdfcb3dee730a248eb10edf Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 12:17:22 +0100 Subject: [PATCH 241/255] Add '_inplace' suffix to 'ChessBoard::play_move' --- src/board/chess_board/mod.rs | 21 +++++++++++---------- src/fen.rs | 6 +++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index b0ddf42..2f0f31b 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -132,9 +132,10 @@ impl ChessBoard { self.combined_occupancy ^= start_end; } - /// Play the given [Move], returning all non-revertible state (e.g: en-passant, etc...). + /// Play the given [Move] in place, returning all non-revertible state (e.g: en-passant, + /// etc...). #[inline(always)] - pub fn play_move(&mut self, chess_move: Move) -> NonReversibleState { + pub fn play_move_inplace(&mut self, chess_move: Move) -> NonReversibleState { let opponent = !self.current_player(); let is_capture = !(self.combined_occupancy() & chess_move.destination()).is_empty(); let move_piece = Piece::iter() @@ -749,21 +750,21 @@ mod test { // Start from default position let mut position = ChessBoard::default(); // Modify it to account for e4 move - position.play_move(Move::new(Square::E2, Square::E4, None)); + position.play_move_inplace(Move::new(Square::E2, Square::E4, None)); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") .unwrap() ); // And now c5 - position.play_move(Move::new(Square::C7, Square::C5, None)); + position.play_move_inplace(Move::new(Square::C7, Square::C5, None)); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") .unwrap() ); // Finally, Nf3 - position.play_move(Move::new(Square::G1, Square::F3, None)); + position.play_move_inplace(Move::new(Square::G1, Square::F3, None)); assert_eq!( position, ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") @@ -778,7 +779,7 @@ mod test { let capture = Move::new(Square::H1, Square::H8, None); - position.play_move(capture); + position.play_move_inplace(capture); assert_eq!(position, expected); } @@ -788,13 +789,13 @@ mod test { let mut position = ChessBoard::default(); // Modify it to account for e4 move let move_1 = Move::new(Square::E2, Square::E4, None); - let state_1 = position.play_move(move_1); + let state_1 = position.play_move_inplace(move_1); // And now c5 let move_2 = Move::new(Square::C7, Square::C5, None); - let state_2 = position.play_move(move_2); + let state_2 = position.play_move_inplace(move_2); // Finally, Nf3 let move_3 = Move::new(Square::G1, Square::F3, None); - let state_3 = position.play_move(move_3); + let state_3 = position.play_move_inplace(move_3); // Now revert each move one-by-one position.unplay_move(move_3, state_3); assert_eq!( @@ -824,7 +825,7 @@ mod test { let capture = Move::new(Square::D1, Square::D8, None); - let state = position.play_move(capture); + let state = position.play_move_inplace(capture); assert_eq!(position, expected); position.unplay_move(capture, state); diff --git a/src/fen.rs b/src/fen.rs index 4c05879..8aede73 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -220,21 +220,21 @@ mod test { fn en_passant() { // Start from default position let mut position = ChessBoard::default(); - position.play_move(Move::new(Square::E2, Square::E4, None)); + position.play_move_inplace(Move::new(Square::E2, Square::E4, None)); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") .unwrap(), position ); // And now c5 - position.play_move(Move::new(Square::C7, Square::C5, None)); + position.play_move_inplace(Move::new(Square::C7, Square::C5, None)); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") .unwrap(), position ); // Finally, Nf3 - position.play_move(Move::new(Square::G1, Square::F3, None)); + position.play_move_inplace(Move::new(Square::G1, Square::F3, None)); assert_eq!( ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") .unwrap(), From b913f4673a5ddb9111c6510e582c313a0d09caec Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 12:19:10 +0100 Subject: [PATCH 242/255] Add 'copy-on-make' 'ChessBoard::play_move' --- src/board/chess_board/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 2f0f31b..2b1d5cb 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -132,6 +132,14 @@ impl ChessBoard { self.combined_occupancy ^= start_end; } + /// Play the given [Move], return a copy of the board with the resulting state. + #[inline(always)] + pub fn play_move(&self, chess_move: Move) -> Self { + let mut res = self.clone(); + res.play_move_inplace(chess_move); + res + } + /// Play the given [Move] in place, returning all non-revertible state (e.g: en-passant, /// etc...). #[inline(always)] From 37a6862dda9af1e57cb8c9a33353c3ef36c36a13 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 12:27:01 +0100 Subject: [PATCH 243/255] Remove redundant 'is_capture' --- src/board/chess_board/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 2b1d5cb..c9ec565 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -145,7 +145,6 @@ impl ChessBoard { #[inline(always)] pub fn play_move_inplace(&mut self, chess_move: Move) -> NonReversibleState { let opponent = !self.current_player(); - let is_capture = !(self.combined_occupancy() & chess_move.destination()).is_empty(); let move_piece = Piece::iter() .find(|&p| !(self.piece_occupancy(p) & chess_move.start()).is_empty()) .unwrap(); @@ -165,7 +164,7 @@ impl ChessBoard { }; // Non-revertible state modification - if is_capture || move_piece == Piece::Pawn { + if captured_piece.is_some() || move_piece == Piece::Pawn { self.half_move_clock = 0; } else { self.half_move_clock += 1; From f1468334e12a109b9650467a2682183102306d7f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 12:37:18 +0100 Subject: [PATCH 244/255] Use 'ChessBoardBuilder' in checkers test --- src/board/chess_board/mod.rs | 47 ++++++++++-------------------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index c9ec565..bc752a2 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -711,40 +711,19 @@ mod test { #[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; Color::NUM_VARIANTS], - en_passant: None, - half_move_clock: 0, - total_plies: 0, - side: Color::White, + let position = { + let mut builder = ChessBoardBuilder::new(); + builder[Square::C1] = Some((Piece::Knight, Color::White)); + builder[Square::D3] = Some((Piece::Bishop, Color::White)); + builder[Square::E1] = Some((Piece::Rook, Color::White)); + builder[Square::E2] = Some((Piece::King, Color::White)); + builder[Square::H2] = Some((Piece::Queen, Color::White)); + builder[Square::G1] = Some((Piece::Knight, Color::Black)); + builder[Square::F3] = Some((Piece::Bishop, Color::Black)); + builder[Square::A2] = Some((Piece::Rook, Color::Black)); + builder[Square::E8] = Some((Piece::King, Color::Black)); + builder[Square::E7] = Some((Piece::Queen, Color::Black)); + TryInto::::try_into(builder).unwrap() }; assert_eq!( position.checkers(), From 85ac65408fee5da9c8cf6359d14e731ae74eb195 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 12:50:31 +0100 Subject: [PATCH 245/255] Remove unused 'en_passant_origins' I don't think I'll need it after all. --- src/movegen/naive/pawn.rs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/movegen/naive/pawn.rs b/src/movegen/naive/pawn.rs index bde5215..c27f743 100644 --- a/src/movegen/naive/pawn.rs +++ b/src/movegen/naive/pawn.rs @@ -38,17 +38,6 @@ pub fn pawn_captures(color: Color, square: Square) -> Bitboard { 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::*; @@ -124,14 +113,4 @@ mod test { Square::G6.into_bitboard() ); } - - #[test] - fn en_passant() { - assert_eq!(en_passant_origins(Square::A4), Square::B4.into_bitboard()); - assert_eq!(en_passant_origins(Square::A5), Square::B5.into_bitboard()); - assert_eq!(en_passant_origins(Square::B4), Square::A4 | Square::C4); - assert_eq!(en_passant_origins(Square::B5), Square::A5 | Square::C5); - assert_eq!(en_passant_origins(Square::H4), Square::G4.into_bitboard()); - assert_eq!(en_passant_origins(Square::H5), Square::G5.into_bitboard()); - } } From b2560aa18309c3e3d0c6f3ce8135572db72f28b1 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 17:44:27 +0100 Subject: [PATCH 246/255] Make 'ChessBoard::xor' apply square-wise This will make it easier to add Zobrist hashing afterwards. --- src/board/chess_board/mod.rs | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index bc752a2..1dffcd2 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -123,13 +123,12 @@ impl ChessBoard { 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. + /// Quickly add/remove a piece on the [Bitboard]s that are part of the [ChessBoard] state. #[inline(always)] - fn xor(&mut self, color: Color, piece: Piece, start_end: Bitboard) { - *self.piece_occupancy_mut(piece) ^= start_end; - *self.color_occupancy_mut(color) ^= start_end; - self.combined_occupancy ^= start_end; + fn xor(&mut self, color: Color, piece: Piece, square: Square) { + *self.piece_occupancy_mut(piece) ^= square; + *self.color_occupancy_mut(color) ^= square; + self.combined_occupancy ^= square; } /// Play the given [Move], return a copy of the board with the resulting state. @@ -186,9 +185,7 @@ impl ChessBoard { _ => *castle_rights, }; if let Some(piece) = captured_piece { - *self.piece_occupancy_mut(piece) ^= chess_move.destination(); - *self.color_occupancy_mut(opponent) ^= chess_move.destination(); - self.combined_occupancy ^= chess_move.destination(); + self.xor(opponent, piece, chess_move.destination()); // If a rook is captured, it loses its castling rights let castle_rights = self.castle_rights_mut(opponent); *castle_rights = match (piece, chess_move.destination().file()) { @@ -199,11 +196,8 @@ impl ChessBoard { } // Revertible state modification - self.xor( - self.current_player(), - move_piece, - chess_move.start() | chess_move.destination(), - ); + self.xor(self.current_player(), move_piece, chess_move.start()); + self.xor(self.current_player(), move_piece, chess_move.destination()); self.total_plies += 1; self.side = !self.side; @@ -225,19 +219,13 @@ impl ChessBoard { .unwrap(); if let Some(piece) = previous.captured_piece { - *self.piece_occupancy_mut(piece) ^= chess_move.destination(); // The capture affected the *current* player, from our post-move POV - *self.color_occupancy_mut(self.current_player()) ^= chess_move.destination(); - self.combined_occupancy ^= chess_move.destination(); + self.xor(self.current_player(), piece, chess_move.destination()); } // Restore revertible state - self.xor( - // The move was applied at the turn *before* the current player - !self.current_player(), - move_piece, - chess_move.start() | chess_move.destination(), - ); + self.xor(!self.current_player(), move_piece, chess_move.destination()); + self.xor(!self.current_player(), move_piece, chess_move.start()); self.total_plies -= 1; self.side = !self.side; } From a667e6b7f2bb38f40504cba3dfcd1d7de3e1632d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 17:56:09 +0100 Subject: [PATCH 247/255] Move RNG code to its own module --- src/movegen/moves.rs | 4 +-- src/movegen/wizardry/generation.rs | 6 +--- src/movegen/wizardry/mod.rs | 30 +------------------- src/utils/mod.rs | 3 ++ src/utils/rand.rs | 45 ++++++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 src/utils/rand.rs diff --git a/src/movegen/moves.rs b/src/movegen/moves.rs index d46a733..72162d1 100644 --- a/src/movegen/moves.rs +++ b/src/movegen/moves.rs @@ -5,10 +5,10 @@ use crate::{ movegen::{ naive, wizardry::{ - generate_bishop_magics, generate_rook_magics, MagicMoves, RandGen, BISHOP_SEED, - ROOK_SEED, + generate_bishop_magics, generate_rook_magics, MagicMoves, BISHOP_SEED, ROOK_SEED, }, }, + utils::RandGen, }; // A pre-rolled RNG for magic bitboard generation, using pre-determined values. diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs index 0322977..aa06f25 100644 --- a/src/movegen/wizardry/generation.rs +++ b/src/movegen/wizardry/generation.rs @@ -1,14 +1,10 @@ use crate::board::{Bitboard, Square}; use crate::movegen::naive::{bishop_moves, rook_moves}; +use crate::utils::RandGen; use super::mask::{generate_bishop_mask, generate_rook_mask}; use super::Magic; -/// A trait to represent RNG for u64 values. -pub(crate) trait RandGen { - fn gen(&mut self) -> u64; -} - type MagicGenerationType = (Vec, Vec); pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType { diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 6918d09..663336d 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -200,35 +200,7 @@ mod test { use std::fmt::Write as _; 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); - } + use crate::utils::SimpleRng; fn split_twice<'a>( text: &'a str, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 2833a48..d6fd569 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,5 @@ +pub(crate) mod rand; +pub(crate) use rand::*; + pub mod static_assert; pub use static_assert::*; diff --git a/src/utils/rand.rs b/src/utils/rand.rs new file mode 100644 index 0000000..9b18ec7 --- /dev/null +++ b/src/utils/rand.rs @@ -0,0 +1,45 @@ +/// A trait to represent RNG for u64 values. +pub trait RandGen { + fn gen(&mut self) -> u64; +} + +// A simple XOR-shift RNG implementation, for code-generation. +#[cfg(test)] +pub struct SimpleRng(u64); + +#[cfg(test)] +impl SimpleRng { + pub fn new() -> Self { + Self(4) // https://xkcd.com/221/ + } + + pub fn gen(&mut self) -> u64 { + self.0 ^= self.0 >> 12; + self.0 ^= self.0 << 25; + self.0 ^= self.0 >> 27; + self.0 + } +} + +#[cfg(test)] +impl RandGen for SimpleRng { + fn gen(&mut self) -> u64 { + self.gen() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn rng() { + let mut rng = SimpleRng::new(); + + assert_eq!(rng.gen(), 134217733); + assert_eq!(rng.gen(), 4504699139039237); + assert_eq!(rng.gen(), 13512173405898766); + assert_eq!(rng.gen(), 9225626310854853124); + assert_eq!(rng.gen(), 29836777971867270); + } +} From 7dd0da6628fc618164092076ca4a30744a1b242c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 19:22:16 +0100 Subject: [PATCH 248/255] Simplify error-handling in seed generation --- src/movegen/wizardry/mod.rs | 42 ++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 663336d..555fdf5 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -212,25 +212,29 @@ mod test { Some((prefix, mid, suffix)) } - fn array_string(piece_type: &str, values: &[Magic]) -> Result { - let mut res = String::new(); + fn array_string(piece_type: &str, values: &[Magic]) -> String { + let inner = || -> Result { + let mut res = String::new(); - writeln!( - &mut res, - "/// A set of magic numbers for {} move generation.", - piece_type - )?; - writeln!( - &mut res, - "pub(crate) const {}_SEED: [u64; Square::NUM_VARIANTS] = [", - piece_type.to_uppercase() - )?; - for magic in values { - writeln!(&mut res, " {},", magic.magic)?; - } - writeln!(&mut res, "];")?; + writeln!( + &mut res, + "/// A set of magic numbers for {} move generation.", + piece_type + )?; + writeln!( + &mut res, + "pub(crate) const {}_SEED: [u64; Square::NUM_VARIANTS] = [", + piece_type.to_uppercase() + )?; + for magic in values { + writeln!(&mut res, " {},", magic.magic)?; + } + writeln!(&mut res, "];")?; - Ok(res) + Ok(res) + }; + + inner().unwrap() } #[test] @@ -243,8 +247,8 @@ mod test { let original_text = std::fs::read_to_string(file!()).unwrap(); - let bishop_array = array_string("bishop", &bishop_magics[..]).unwrap(); - let rook_array = array_string("rook", &rook_magics[..]).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"; From 93d255623b35ee0fa174cae36049127e8aae28bf Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 19:30:53 +0100 Subject: [PATCH 249/255] Use 'pcg64_fast' for RNG This is a higher quality source of randomness than a XOR-shift generator, while still being fast and easy to write. --- src/movegen/wizardry/mod.rs | 256 ++++++++++++++++++------------------ src/utils/rand.rs | 28 ++-- 2 files changed, 144 insertions(+), 140 deletions(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 555fdf5..37ddb6e 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -60,138 +60,138 @@ impl MagicMoves { // region:sourcegen /// A set of magic numbers for bishop move generation. pub(crate) const BISHOP_SEED: [u64; Square::NUM_VARIANTS] = [ - 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, + 4634226011293351952, + 6918109887683821586, + 76562328660738184, + 7242919606867744800, + 13871652069997347969, + 1171657252671901696, + 147001475087730752, + 1752045392763101248, + 288406435526639744, + 4612213818402029888, + 9808848818951710728, + 9223394181731320840, + 54047645651435648, + 9224780030482579712, + 9049059098626048, + 1442330840700035221, + 1126037887157508, + 1153488887004529665, + 290485130928332936, + 9226749771011592258, + 148636405693678112, + 2260596997758984, + 73470481646424336, + 2341907012146823680, + 2314955761652335121, + 2265544246165632, + 13598764778463296, + 563087425962496, + 563087425962048, + 2163991853573081088, + 567353402270020, + 6488844433713538048, + 288810987011448834, + 11830884701569344, + 2747549955031826688, + 35734665298432, + 18025943920672800, + 292892945404789012, + 1153520472160470528, + 2260949167801860, + 155446765112299521, + 379008324189818944, + 4616480181217005576, + 576461027453960704, + 2450556349601564416, + 1160556519943569536, + 4612900059821375552, + 5477089643453251617, + 9223532084785594632, + 2810391870219355200, + 36594222015453185, + 4612011546951352320, + 2392883590201344, + 1152956706186200064, + 9009415592510464, + 81077999302148128, + 576746627483043968, + 301267327789056, + 39586720976896, + 720878306081243648, + 9223512777841312257, + 5764609859566698625, + 8088544233436348496, + 4612856276794474560, ]; /// A set of magic numbers for rook move generation. pub(crate) const ROOK_SEED: [u64; Square::NUM_VARIANTS] = [ - 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, + 180144122814791812, + 10448386594766422036, + 9403533616331358856, + 108095189301858304, + 72076290316044288, + 36066182562054145, + 4647717564258980096, + 13979173385364603396, + 4620833992751489152, + 297800804633419904, + 578009002156298240, + 2450099003505838082, + 1175721046778052864, + 20406952999780864, + 1175861788231598592, + 36169538802827392, + 288371663414771712, + 423313050501155, + 604731668136450, + 580261214513399808, + 297661437206136832, + 1750211954976489600, + 9020393411186696, + 9259543770406356001, + 44532368556032, + 10376381507760693256, + 52778707714176, + 4612829512676149248, + 1882513444629184528, + 2369460754144428160, + 9223380850137104901, + 2666413562481640036, + 141012643087392, + 16735517094631719424, + 17594358702087, + 2344264412262574084, + 422813768878080, + 1126450811896320, + 54466576291772936, + 42784758060548372, + 292874851780165648, + 18015364885839937, + 282644818493504, + 1184447393488764944, + 4649966632473477184, + 563499910594566, + 17632049496086, + 18502729728001, + 140742121013504, + 9711024139665536, + 246293205270784, + 290772515771392256, + 9230131836490350720, + 73326432604127360, + 453174886517643776, + 2396271245728563712, + 324259242966026501, + 288953994406543363, + 1153557061259362338, + 40533496293515441, + 1407392197644307, + 1729945211427624002, + 587808330812164100, + 9511606812128903298, ]; // endregion:sourcegen diff --git a/src/utils/rand.rs b/src/utils/rand.rs index 9b18ec7..889b13f 100644 --- a/src/utils/rand.rs +++ b/src/utils/rand.rs @@ -3,21 +3,25 @@ pub trait RandGen { fn gen(&mut self) -> u64; } -// A simple XOR-shift RNG implementation, for code-generation. +// A simple pcg64_fast RNG implementation, for code-generation. #[cfg(test)] -pub struct SimpleRng(u64); +pub struct SimpleRng(u128); #[cfg(test)] impl SimpleRng { pub fn new() -> Self { - Self(4) // https://xkcd.com/221/ + Self(0xcafef00dd15ea5e5 | 1) // https://xkcd.com/221/ } pub fn gen(&mut self) -> u64 { - self.0 ^= self.0 >> 12; - self.0 ^= self.0 << 25; - self.0 ^= self.0 >> 27; - self.0 + const MULTIPLIER: u128 = 0x2360_ED05_1FC6_5DA4_4385_DF64_9FCC_F645; + const XSHIFT: u32 = 64; // (128 - 64 + 64) / 2 + const ROTATE: u32 = 122; // 128 - 6 + + self.0 = self.0.wrapping_mul(MULTIPLIER); + let rot = (self.0 >> ROTATE) as u32; + let xsl = (self.0 >> XSHIFT) as u64 ^ (self.0 as u64); + xsl.rotate_right(rot) } } @@ -36,10 +40,10 @@ mod 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); + assert_eq!(rng.gen(), 64934999470316615); + assert_eq!(rng.gen(), 15459456780870779090); + assert_eq!(rng.gen(), 13715484424881807779); + assert_eq!(rng.gen(), 17718572936700675021); + assert_eq!(rng.gen(), 14587996314750246637); } } From d729d63c75f2032a6374c40f6061980d1677e721 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 19:48:34 +0100 Subject: [PATCH 250/255] Add 'CastleRights::iter' --- src/board/castle_rights.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 5bd9d91..81cb5a6 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -17,6 +17,18 @@ impl CastleRights { /// The number of [CastleRights] variants. pub const NUM_VARIANTS: usize = 4; + const ALL: [Self; Self::NUM_VARIANTS] = [ + Self::NoSide, + Self::KingSide, + Self::QueenSide, + Self::BothSides, + ]; + + /// Iterate over all castle-rights variants. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + /// Convert from a castle rights index into a [CastleRights] type. /// /// # Panics From 8be7105ac51e7852bdba59c052f7b9fe00e2f73a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 6 Apr 2024 20:26:04 +0100 Subject: [PATCH 251/255] Refactor castling-rights handling in 'play_move' --- src/board/chess_board/mod.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 1dffcd2..63753a4 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -131,6 +131,20 @@ impl ChessBoard { self.combined_occupancy ^= square; } + /// Compute the change of [CastleRights] from moving/taking a piece. + fn update_castling(&mut self, color: Color, piece: Piece, file: File) { + let original = self.castle_rights(color); + let new_rights = match (piece, file) { + (Piece::Rook, File::A) => original.without_queen_side(), + (Piece::Rook, File::H) => original.without_king_side(), + (Piece::King, _) => CastleRights::NoSide, + _ => return, + }; + if new_rights != original { + *self.castle_rights_mut(color) = new_rights; + } + } + /// Play the given [Move], return a copy of the board with the resulting state. #[inline(always)] pub fn play_move(&self, chess_move: Move) -> Self { @@ -177,22 +191,11 @@ impl ChessBoard { } else { self.en_passant = None; } - let castle_rights = self.castle_rights_mut(self.current_player()); - *castle_rights = match (move_piece, chess_move.start().file()) { - (Piece::Rook, File::A) => castle_rights.without_queen_side(), - (Piece::Rook, File::H) => castle_rights.without_king_side(), - (Piece::King, _) => CastleRights::NoSide, - _ => *castle_rights, - }; + self.update_castling(self.current_player(), move_piece, chess_move.start().file()); if let Some(piece) = captured_piece { self.xor(opponent, piece, chess_move.destination()); // If a rook is captured, it loses its castling rights - let castle_rights = self.castle_rights_mut(opponent); - *castle_rights = match (piece, chess_move.destination().file()) { - (Piece::Rook, File::A) => castle_rights.without_queen_side(), - (Piece::Rook, File::H) => castle_rights.without_king_side(), - _ => *castle_rights, - }; + self.update_castling(opponent, piece, chess_move.destination().file()); } // Revertible state modification From ed38c6c12d98fc09729e18cd7ab4547f86198289 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 14 Apr 2024 16:09:58 +0100 Subject: [PATCH 252/255] Add 'Bitboard::any_square' --- src/board/bitboard/mod.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 9059235..81762ed 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -75,6 +75,12 @@ impl Bitboard { (self.0 & (self.0.wrapping_sub(1))) != 0 } + /// Return a [Square] from the board, or `None` if it is empty. + #[inline(always)] + pub fn any_square(self) -> Option { + Square::try_from_index(self.0.trailing_zeros() as usize) + } + /// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of /// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that /// contain all, some, or none of the [Square] that are in the given [Bitboard]. @@ -479,6 +485,23 @@ mod test { ); } + #[test] + fn any_square() { + for square in Square::iter() { + assert_eq!(square.into_bitboard().any_square(), Some(square)); + } + } + + #[test] + fn any_square_empty() { + assert!(Bitboard::EMPTY.any_square().is_none()); + } + + #[test] + fn any_square_full_board() { + assert!(Bitboard::ALL.any_square().is_some()); + } + #[test] fn into_square() { for square in Square::iter() { From 524e3b2c767334b206354d7fc2f0023d92e37995 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 14 Apr 2024 16:09:58 +0100 Subject: [PATCH 253/255] Simplify 'TryInto' for 'Bitboard' --- src/board/bitboard/mod.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 81762ed..3cb9d7b 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -116,12 +116,10 @@ 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)) + if self.has_more_than_one() { + return Err(IntoSquareError::TooManySquares); + } + self.any_square().ok_or(IntoSquareError::EmptyBoard) } } From 4960286557c7c6db972e60e9e230822068f517de Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 14 Apr 2024 16:10:41 +0100 Subject: [PATCH 254/255] Simplify 'BitboardIterator' --- src/board/bitboard/iterator.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs index 7c01a9a..f5711ee 100644 --- a/src/board/bitboard/iterator.rs +++ b/src/board/bitboard/iterator.rs @@ -2,11 +2,11 @@ /// [Bitboard]. use crate::board::Bitboard; -pub struct BitboardIterator(u64); +pub struct BitboardIterator(Bitboard); impl BitboardIterator { pub fn new(board: Bitboard) -> Self { - Self(board.0) + Self(board) } } @@ -14,17 +14,15 @@ 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)) - } + let res = self.0.any_square(); + if let Some(square) = res { + self.0 ^= square; + }; + res } fn size_hint(&self) -> (usize, Option) { - let size = self.0.count_ones() as usize; + let size = self.0.count() as usize; (size, Some(size)) } From dd49c6474b33192cff0677b00684c1bb13c689c0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 15 Apr 2024 22:44:28 +0100 Subject: [PATCH 255/255] Fix promotion in 'ChessBoard::{,un}play_move' --- src/board/chess_board/mod.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 63753a4..f3a92e9 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -199,8 +199,9 @@ impl ChessBoard { } // Revertible state modification + let dest_piece = chess_move.promotion().unwrap_or(move_piece); self.xor(self.current_player(), move_piece, chess_move.start()); - self.xor(self.current_player(), move_piece, chess_move.destination()); + self.xor(self.current_player(), dest_piece, chess_move.destination()); self.total_plies += 1; self.side = !self.side; @@ -227,8 +228,9 @@ impl ChessBoard { } // Restore revertible state + let start_piece = chess_move.promotion().map_or(move_piece, |_| Piece::Pawn); self.xor(!self.current_player(), move_piece, chess_move.destination()); - self.xor(!self.current_player(), move_piece, chess_move.start()); + self.xor(!self.current_player(), start_piece, chess_move.start()); self.total_plies -= 1; self.side = !self.side; } @@ -808,4 +810,19 @@ mod test { position.unplay_move(capture, state); assert_eq!(position, original); } + + #[test] + fn play_move_undo_promotion() { + let mut position = ChessBoard::from_fen("7k/P7/8/8/8/8/8/K7 w - - 0 1").unwrap(); + let expected = ChessBoard::from_fen("N6k/8/8/8/8/8/8/K7 b - - 0 1").unwrap(); + let original = position.clone(); + + let promotion = Move::new(Square::A7, Square::A8, Some(Piece::Knight)); + + let state = position.play_move_inplace(promotion); + assert_eq!(position, expected); + + position.unplay_move(promotion, state); + assert_eq!(position, original); + } }