From 65a0939d5305d3fdd62895e6fcc1ac9b0e39efe3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Mar 2024 20:30:43 +0000 Subject: [PATCH 001/230] Bootstrap project --- .envrc | 5 ++ .gitignore | 6 ++ .woodpecker/check.yml | 31 +++++++++ Cargo.lock | 7 +++ Cargo.toml | 8 +++ flake.lock | 143 ++++++++++++++++++++++++++++++++++++++++++ flake.nix | 109 ++++++++++++++++++++++++++++++++ src/main.rs | 3 + 8 files changed, 312 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 .woodpecker/check.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/main.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..de77fcb --- /dev/null +++ b/.envrc @@ -0,0 +1,5 @@ +if ! has nix_direnv_version || ! nix_direnv_version 3.0.0; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.0/direnvrc" "sha256-21TMnI2xWX7HkSTjFFri2UaohXVj854mgvWapWrxRXg=" +fi + +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f360ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Rust build directory +/target + +# Nix generated files +/.pre-commit-config.yaml +/result diff --git a/.woodpecker/check.yml b/.woodpecker/check.yml new file mode 100644 index 0000000..9135f7b --- /dev/null +++ b/.woodpecker/check.yml @@ -0,0 +1,31 @@ +labels: + backend: local + +steps: +- name: pre-commit check + image: bash + commands: + - nix develop --command pre-commit run --all + +- name: nix flake check + image: bash + commands: + - nix flake check + +- name: notifiy + image: bash + secrets: + - source: matrix_homeserver + target: address + - source: matrix_roomid + target: room + - source: matrix_username + target: user + - source: matrix_password + target: pass + commands: + - nix run '.#matrix-notifier' + when: + status: + - failure + - success diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1e43342 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "seer" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f191c81 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "seer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..18cea66 --- /dev/null +++ b/flake.lock @@ -0,0 +1,143 @@ +{ + "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "futils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "ref": "main", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1711523803, + "narHash": "sha256-UKcYiHWHQynzj6CN/vTcix4yd1eCu1uFdsuarupdCQQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2726f127c15a4cc9810843b96cad73c7eb39e443", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1710695816, + "narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "614b4613980a522ba49f0d194531beddbb7220d3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": [ + "futils" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1711519547, + "narHash": "sha256-Q7YmSCUJmDl71fJv/zD9lrOCJ1/SE/okZ2DsrmRjzhY=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "7d47a32e5cd1ea481fab33c516356ce27c8cef4a", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "master", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "futils": "futils", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..134bcdc --- /dev/null +++ b/flake.nix @@ -0,0 +1,109 @@ +{ + description = "A chess engine"; + + inputs = { + futils = { + type = "github"; + owner = "numtide"; + repo = "flake-utils"; + ref = "main"; + }; + + nixpkgs = { + type = "github"; + owner = "NixOS"; + repo = "nixpkgs"; + ref = "nixos-unstable"; + }; + + pre-commit-hooks = { + type = "github"; + owner = "cachix"; + repo = "pre-commit-hooks.nix"; + ref = "master"; + inputs = { + flake-utils.follows = "futils"; + nixpkgs.follows = "nixpkgs"; + }; + }; + }; + + outputs = { self, futils, nixpkgs, pre-commit-hooks }: + { + overlays = { + default = final: _prev: { + seer = with final; rustPlatform.buildRustPackage { + pname = "seer"; + version = (final.lib.importTOML ./Cargo.toml).package.version; + + src = self; + + cargoLock = { + lockFile = "${self}/Cargo.lock"; + }; + + meta = with lib; { + description = "A chess engine"; + homepage = "https://git.belanyi.fr/ambroisie/seer"; + license = licenses.mit; + maintainers = with maintainers; [ ambroisie ]; + }; + }; + }; + }; + } // futils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ + self.overlays.default + ]; + }; + + pre-commit = pre-commit-hooks.lib.${system}.run { + src = self; + + hooks = { + clippy = { + enable = true; + }; + + nixpkgs-fmt = { + enable = true; + }; + + rustfmt = { + enable = true; + }; + }; + }; + in + { + checks = { + inherit (self.packages.${system}) seer; + }; + + devShells = { + default = pkgs.mkShell { + inputsFrom = with self.packages.${system}; [ + seer + ]; + + packages = with pkgs; [ + clippy + rust-analyzer + rustfmt + ]; + + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + + inherit (pre-commit) shellHook; + }; + }; + + packages = futils.lib.flattenTree { + default = pkgs.seer; + inherit (pkgs) seer; + }; + }); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From 0d94438b467d05fdd439f3ee897325c1e18fd2d8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Mar 2024 20:31:45 +0000 Subject: [PATCH 002/230] Move binary crate into 'bin' folder --- src/{main.rs => bin/seer.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{main.rs => bin/seer.rs} (100%) diff --git a/src/main.rs b/src/bin/seer.rs similarity index 100% rename from src/main.rs rename to src/bin/seer.rs From 98efd390a6b47a48fccbc349cb3a3609b3bdf701 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:33:07 +0200 Subject: [PATCH 003/230] Add 'Bitboard' and 'Square' definitions --- src/board/bitboard.rs | 201 +++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 5 + src/board/square.rs | 214 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 4 files changed, 421 insertions(+) create mode 100644 src/board/bitboard.rs create mode 100644 src/board/mod.rs create mode 100644 src/board/square.rs create mode 100644 src/lib.rs diff --git a/src/board/bitboard.rs b/src/board/bitboard.rs new file mode 100644 index 0000000..50298d9 --- /dev/null +++ b/src/board/bitboard.rs @@ -0,0 +1,201 @@ +use super::Square; + +/// Use a 64-bit number to represent a chessboard. Each bit is mapped from to a specific square, so +/// that index 0 -> A1, 1 -> A2, ..., 63 -> H8. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Bitboard(pub(crate) u64); + +impl Bitboard { + /// An empty bitboard. + pub const EMPTY: Bitboard = Bitboard(0); + + /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. + pub const RANKS: [Self; 8] = [ + Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), + Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010), + Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100), + Bitboard(0b00001000_00001000_00001000_00001000_00001000_00001000_00001000_00001000), + Bitboard(0b00010000_00010000_00010000_00010000_00010000_00010000_00010000_00010000), + Bitboard(0b00100000_00100000_00100000_00100000_00100000_00100000_00100000_00100000), + Bitboard(0b01000000_01000000_01000000_01000000_01000000_01000000_01000000_01000000), + Bitboard(0b10000000_10000000_10000000_10000000_10000000_10000000_10000000_10000000), + ]; + + /// Array of bitboards representing the eight files, in order from file A to file H. + pub const FILES: [Self; 8] = [ + Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111), + Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000), + Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000), + Bitboard(0b00000000_00000000_00000000_00000000_11111111_00000000_00000000_00000000), + Bitboard(0b00000000_00000000_00000000_11111111_00000000_00000000_00000000_00000000), + Bitboard(0b00000000_00000000_11111111_00000000_00000000_00000000_00000000_00000000), + Bitboard(0b00000000_11111111_00000000_00000000_00000000_00000000_00000000_00000000), + Bitboard(0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000), + ]; +} + +impl Default for Bitboard { + fn default() -> Self { + Self::EMPTY + } +} + +/// Treat bitboard as a set of squares, shift each square's index left by the amount given. +impl std::ops::Shl for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn shl(self, rhs: usize) -> Self::Output { + Bitboard(self.0 << rhs) + } +} + +/// Treat bitboard as a set of squares, shift each square's index right by the amount given. +impl std::ops::Shr for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn shr(self, rhs: usize) -> Self::Output { + Bitboard(self.0 >> rhs) + } +} + +/// Treat bitboard as a set of squares, and invert the set. +impl std::ops::Not for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn not(self) -> Self::Output { + Bitboard(!self.0) + } +} + +/// Treat each bitboard as a set of squares, keep squares that are in either sets. +impl std::ops::BitOr for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 | rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::BitOr for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Square) -> Self::Output { + self | rhs.into_bitboard() + } +} + +/// Treat each bitboard as a set of squares, keep squares that are in both sets. +impl std::ops::BitAnd for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitand(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 & rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::BitAnd for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitand(self, rhs: Square) -> Self::Output { + self & rhs.into_bitboard() + } +} + +/// Treat each bitboard as a set of squares, keep squares that are in exactly one of either set. +impl std::ops::BitXor for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitxor(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 ^ rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::BitXor for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn bitxor(self, rhs: Square) -> Self::Output { + self ^ rhs.into_bitboard() + } +} + +/// Treat each bitboard as a set of squares, and substract one set from another. +impl std::ops::Sub for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn sub(self, rhs: Bitboard) -> Self::Output { + Bitboard(self.0 & !rhs.0) + } +} + +/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +impl std::ops::Sub for Bitboard { + type Output = Bitboard; + + #[inline(always)] + fn sub(self, rhs: Square) -> Self::Output { + self - rhs.into_bitboard() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::square::*; + + #[test] + fn left_shift() { + assert_eq!(Bitboard::RANKS[0] << 1, Bitboard::RANKS[1]); + assert_eq!(Bitboard::FILES[0] << 8, Bitboard::FILES[1]); + } + + #[test] + fn right_shift() { + assert_eq!(Bitboard::RANKS[1] >> 1, Bitboard::RANKS[0]); + assert_eq!(Bitboard::FILES[1] >> 8, Bitboard::FILES[0]); + } + + #[test] + fn not() { + assert_eq!(!Bitboard::EMPTY, Bitboard(u64::MAX)); + } + + #[test] + fn or() { + assert_eq!(Bitboard::FILES[0] | Bitboard::FILES[1], Bitboard(0xff_ff)); + assert_eq!(Bitboard::FILES[0] | Square::B1, Bitboard(0x1_ff)); + } + + #[test] + fn and() { + assert_eq!(Bitboard::FILES[0] & Bitboard::FILES[1], Bitboard::EMPTY); + assert_eq!( + Bitboard::FILES[0] & Bitboard::RANKS[0], + Square::A1.into_bitboard() + ); + assert_eq!(Bitboard::FILES[0] & Square::A1, Square::A1.into_bitboard()); + } + + #[test] + fn xor() { + assert_eq!(Bitboard::FILES[0] ^ Square::A1, Bitboard(0xff - 1)); + } + + #[test] + fn sub() { + assert_eq!(Bitboard::FILES[0] - Bitboard::RANKS[0], Bitboard(0xff - 1)); + assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1)); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs new file mode 100644 index 0000000..06a7d91 --- /dev/null +++ b/src/board/mod.rs @@ -0,0 +1,5 @@ +pub mod bitboard; +pub use bitboard::*; + +pub mod square; +pub use square::*; diff --git a/src/board/square.rs b/src/board/square.rs new file mode 100644 index 0000000..640214c --- /dev/null +++ b/src/board/square.rs @@ -0,0 +1,214 @@ +use super::Bitboard; + +/// Represent a square on a chessboard. Defined in the same order as the +/// [Bitboard](crate::board::Bitboard) squares. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[rustfmt::skip] +pub enum Square { + A1, A2, A3, A4, A5, A6, A7, A8, + B1, B2, B3, B4, B5, B6, B7, B8, + C1, C2, C3, C4, C5, C6, C7, C8, + D1, D2, D3, D4, D5, D6, D7, D8, + E1, E2, E3, E4, E5, E6, E7, E8, + F1, F2, F3, F4, F5, F6, F7, F8, + G1, G2, G3, G4, G5, G6, G7, G8, + H1, H2, H3, H4, H5, H6, H7, H8, +} + +impl std::fmt::Display for Square { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl Square { + #[rustfmt::skip] + const ALL: [Self; 64] = [ + Self::A1, Self::A2, Self::A3, Self::A4, Self::A5, Self::A6, Self::A7, Self::A8, + Self::B1, Self::B2, Self::B3, Self::B4, Self::B5, Self::B6, Self::B7, Self::B8, + Self::C1, Self::C2, Self::C3, Self::C4, Self::C5, Self::C6, Self::C7, Self::C8, + Self::D1, Self::D2, Self::D3, Self::D4, Self::D5, Self::D6, Self::D7, Self::D8, + Self::E1, Self::E2, Self::E3, Self::E4, Self::E5, Self::E6, Self::E7, Self::E8, + Self::F1, Self::F2, Self::F3, Self::F4, Self::F5, Self::F6, Self::F7, Self::F8, + Self::G1, Self::G2, Self::G3, Self::G4, Self::G5, Self::G6, Self::G7, Self::G8, + Self::H1, Self::H2, Self::H3, Self::H4, Self::H5, Self::H6, Self::H7, Self::H8, + ]; + + /// Iterate over all squares in order. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + + /// Convert from a square index into a [Square] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < 64); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } + } + + /// Convert from a square index into a [Square] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Square::index()]. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of the rank of this square (0 -> rank 1, ..., 7 -> rank 8). + #[inline(always)] + pub fn rank_index(self) -> usize { + (self as usize) % 8 + } + + /// Return the index of the rank of this square (0 -> file A, ..., 7 -> file H). + #[inline(always)] + pub fn file_index(self) -> usize { + (self as usize) / 8 + } + + /// Return a bitboard representing the rank of this square. + #[inline(always)] + pub fn rank(self) -> Bitboard { + Bitboard::RANKS[self.rank_index()] + } + + /// Return a bitboard representing the rank of this square. + #[inline(always)] + pub fn file(self) -> Bitboard { + Bitboard::FILES[self.file_index()] + } + + /// Turn a square into a singleton bitboard. + #[inline(always)] + pub fn into_bitboard(self) -> Bitboard { + Bitboard(1 << (self as usize)) + } +} + +/// Shift the square's index left by the amount given. +impl std::ops::Shl for Square { + type Output = Square; + + #[inline(always)] + fn shl(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] + Square::from_index(self as usize + rhs) + } +} + +/// Shift the square's index right by the amount given. +impl std::ops::Shr for Square { + type Output = Square; + + #[inline(always)] + fn shr(self, rhs: usize) -> Self::Output { + #[allow(clippy::suspicious_arithmetic_impl)] + Square::from_index(self as usize - rhs) + } +} + +/// Return a board containing all squares but the one given. +impl std::ops::Not for Square { + type Output = Bitboard; + + #[inline(always)] + fn not(self) -> Self::Output { + !self.into_bitboard() + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitOr for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Square) -> Self::Output { + self.into_bitboard() | rhs.into_bitboard() + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitOr for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitor(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() | rhs + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitAnd for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitand(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() & rhs + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::BitXor for Square { + type Output = Bitboard; + + #[inline(always)] + fn bitxor(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() ^ rhs + } +} + +/// Treat the square as a singleton board, and apply the operator. +impl std::ops::Sub for Square { + type Output = Bitboard; + + #[inline(always)] + fn sub(self, rhs: Bitboard) -> Self::Output { + self.into_bitboard() - rhs + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::bitboard::*; + + #[test] + fn left_shift() { + assert_eq!(Square::A1 << 1, Square::A2); + assert_eq!(Square::A1 << 8, Square::B1); + } + + #[test] + fn right_shift() { + assert_eq!(Square::A2 >> 1, Square::A1); + assert_eq!(Square::B1 >> 8, Square::A1); + } + + #[test] + fn not() { + assert_eq!(!Square::A1, Bitboard(u64::MAX - 1)); + } + + #[test] + fn or() { + assert_eq!(Square::A1 | Square::A2, Bitboard(0b11)); + } + + #[test] + fn and() { + assert_eq!(Square::A1 & Bitboard::FILES[0], Square::A1.into_bitboard()); + } + + #[test] + fn xor() { + assert_eq!(Square::A1 ^ Bitboard::FILES[0], Bitboard(0xff - 1)); + } + + #[test] + fn sub() { + assert_eq!(Square::A1 - Bitboard::FILES[0], Bitboard::EMPTY); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..667c357 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod board; From afec72d646521d6266c30e6eb80d07ffe5ca60e2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:52:09 +0200 Subject: [PATCH 004/230] Move 'board::bitboard' into folder module I will be adding a 'BitboardIterator' type, and it makes more sense to use a folder for this module at this point. --- src/board/{bitboard.rs => bitboard/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/board/{bitboard.rs => bitboard/mod.rs} (100%) diff --git a/src/board/bitboard.rs b/src/board/bitboard/mod.rs similarity index 100% rename from src/board/bitboard.rs rename to src/board/bitboard/mod.rs From 1235e4a51c1dc2f4d23866284bb1c21d39431f92 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:53:08 +0200 Subject: [PATCH 005/230] Add bitboard iteration Introduce 'BitboardIterator', use it to implement 'IntoIterator' for 'Bitboard'. --- src/board/bitboard/iterator.rs | 17 ++++++++++++++ src/board/bitboard/mod.rs | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/board/bitboard/iterator.rs diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs new file mode 100644 index 0000000..06db283 --- /dev/null +++ b/src/board/bitboard/iterator.rs @@ -0,0 +1,17 @@ +/// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a +/// [Bitboard](crate::board::Bitboard). +pub struct BitboardIterator(pub(crate) u64); + +impl Iterator for BitboardIterator { + type Item = crate::board::Square; + + fn next(&mut self) -> Option { + if self.0 == 0 { + None + } else { + let lsb = self.0.trailing_zeros() as usize; + self.0 ^= 1 << lsb; + Some(crate::board::Square::from_index(lsb)) + } + } +} diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 50298d9..edb1015 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,4 +1,6 @@ use super::Square; +mod iterator; +use iterator::*; /// Use a 64-bit number to represent a chessboard. Each bit is mapped from to a specific square, so /// that index 0 -> A1, 1 -> A2, ..., 63 -> H8. @@ -40,6 +42,16 @@ impl Default for Bitboard { } } +/// Iterate over the [Square](crate::board::Square) values included in the board. +impl IntoIterator for Bitboard { + type IntoIter = BitboardIterator; + type Item = Square; + + fn into_iter(self) -> Self::IntoIter { + BitboardIterator(self.0) + } +} + /// Treat bitboard as a set of squares, shift each square's index left by the amount given. impl std::ops::Shl for Bitboard { type Output = Bitboard; @@ -155,6 +167,37 @@ mod test { use super::*; use crate::board::square::*; + #[test] + fn iter() { + assert_eq!(Bitboard::EMPTY.into_iter().collect::>(), Vec::new()); + assert_eq!( + Bitboard::RANKS[0].into_iter().collect::>(), + vec![ + Square::A1, + Square::B1, + Square::C1, + Square::D1, + Square::E1, + Square::F1, + Square::G1, + Square::H1, + ] + ); + assert_eq!( + Bitboard::FILES[0].into_iter().collect::>(), + vec![ + Square::A1, + Square::A2, + Square::A3, + Square::A4, + Square::A5, + Square::A6, + Square::A7, + Square::A8, + ] + ); + } + #[test] fn left_shift() { assert_eq!(Bitboard::RANKS[0] << 1, Bitboard::RANKS[1]); From d8f4057a8ccadbb2919d0927dee35d2d464e7996 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 21:58:46 +0200 Subject: [PATCH 006/230] Introduce 'Bitboard::ALL' --- src/board/bitboard/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index edb1015..524cf33 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -11,6 +11,9 @@ impl Bitboard { /// An empty bitboard. pub const EMPTY: Bitboard = Bitboard(0); + /// A full bitboard. + pub const ALL: Bitboard = Bitboard(u64::MAX); + /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. pub const RANKS: [Self; 8] = [ Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), @@ -212,7 +215,7 @@ mod test { #[test] fn not() { - assert_eq!(!Bitboard::EMPTY, Bitboard(u64::MAX)); + assert_eq!(!Bitboard::EMPTY, Bitboard::ALL); } #[test] From 1c7857488203d3fdfbf402ff9578b3a7e3d99f9b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 15 Jul 2022 23:52:16 +0200 Subject: [PATCH 007/230] Add GDB pretty-printers --- .gdbinit | 2 + utils/gdb/seer_pretty_printers.py | 75 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 .gdbinit create mode 100644 utils/gdb/seer_pretty_printers.py diff --git a/.gdbinit b/.gdbinit new file mode 100644 index 0000000..d04df33 --- /dev/null +++ b/.gdbinit @@ -0,0 +1,2 @@ +# Register pretty-printers +source utils/gdb/seer_pretty_printers.py diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py new file mode 100644 index 0000000..2bfce31 --- /dev/null +++ b/utils/gdb/seer_pretty_printers.py @@ -0,0 +1,75 @@ +import gdb.printing + + +class Square(object): + """ + Python representation of a 'seer::board::square::Square' raw value. + """ + + FILES = list(map(lambda n: chr(ord("A") + n), range(8))) + RANKS = list(map(lambda n: str(n + 1), range(8))) + + def __init__(self, val): + self._val = val + + def __str__(self): + return self.FILES[self.file] + self.RANKS[self.rank] + + @property + def rank(self): + return int(self._val) % 8 + + @property + def file(self): + return int(self._val) // 8 + + +class Bitboard(object): + """ + Python representation of a 'seer::board::bitboard::Bitboard' raw value. + """ + + def __init__(self, val): + self._val = val + + def __str__(self): + return "[" + ", ".join(map(str, self.squares)) + "]" + + @property + def squares(self): + n = self._val + while n: + b = n & (~n + 1) + yield Square(b.bit_length() - 1) + n ^= b + + +class SquarePrinter(object): + "Print a seer::board::square::Square" + + def __init__(self, val): + self._val = Square(val) + + def to_string(self): + return str(self._val) + + +class BitboardPrinter(object): + "Print a seer::board::bitboard::Bitboard" + + def __init__(self, val): + self._val = Bitboard(int(val["__0"])) + + def to_string(self): + return "Bitboard{" + str(self._val)[1:-1] + "}" + + +def build_pretty_printer(): + pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') + + pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter) + pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) + + return pp + +gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True) From 5a35c5509190ba48e5c5842d6ad43831f21024bf Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:05:49 +0200 Subject: [PATCH 008/230] Add 'Rank' enum --- src/board/mod.rs | 3 ++ src/board/rank.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/board/rank.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index 06a7d91..ef264e1 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,5 +1,8 @@ pub mod bitboard; pub use bitboard::*; +pub mod rank; +pub use rank::*; + pub mod square; pub use square::*; diff --git a/src/board/rank.rs b/src/board/rank.rs new file mode 100644 index 0000000..11ba5e8 --- /dev/null +++ b/src/board/rank.rs @@ -0,0 +1,89 @@ +use super::Bitboard; + +/// An enum representing a singular rank on a chess board (i.e: the rows). +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Rank { + First, + Second, + Third, + Fourth, + Fifth, + Sixth, + Seventh, + Eighth, +} + +impl Rank { + const ALL: [Rank; 8] = [ + Rank::First, + Rank::Second, + Rank::Third, + Rank::Fourth, + Rank::Fifth, + Rank::Sixth, + Rank::Seventh, + Rank::Eighth, + ]; + + /// Iterate over all ranks in order. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + + /// Convert from a rank index into a [Rank] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < 8); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } + } + + /// Convert from a rank index into a [Rank] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Rank::index()]. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [Rank]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + + /// Turn a [Rank] into a [Bitboard] of all squares in that rank. + #[inline(always)] + pub fn into_bitboard(self) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *Bitboard::RANKS.get_unchecked(self.index()) } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(Rank::from_index(0), Rank::First); + assert_eq!(Rank::from_index(1), Rank::Second); + assert_eq!(Rank::from_index(7), Rank::Eighth); + } + + #[test] + fn index() { + assert_eq!(Rank::First.index(), 0); + assert_eq!(Rank::Second.index(), 1); + assert_eq!(Rank::Eighth.index(), 7); + } + + #[test] + fn into_bitboard() { + assert_eq!(Rank::First.into_bitboard(), Bitboard::RANKS[0]); + assert_eq!(Rank::Second.into_bitboard(), Bitboard::RANKS[1]); + assert_eq!(Rank::Eighth.into_bitboard(), Bitboard::RANKS[7]); + } +} From 9678180c75e3ee697b4f2d47438797baa71ef08c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:05:56 +0200 Subject: [PATCH 009/230] Add 'File' enum --- src/board/file.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 92 insertions(+) create mode 100644 src/board/file.rs diff --git a/src/board/file.rs b/src/board/file.rs new file mode 100644 index 0000000..b2e87f1 --- /dev/null +++ b/src/board/file.rs @@ -0,0 +1,89 @@ +use super::Bitboard; + +/// An enum representing a singular file on a chess board (i.e: the columns). +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum File { + A, + B, + C, + D, + E, + F, + G, + H, +} + +impl File { + const ALL: [File; 8] = [ + File::A, + File::B, + File::C, + File::D, + File::E, + File::F, + File::G, + File::H, + ]; + + /// Iterate over all files in order. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + + /// Convert from a file index into a [File] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < 8); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } + } + + /// Convert from a file index into a [File] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [File::index()]. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [File]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + + /// Turn a [File] into a [Bitboard] of all squares in that file. + #[inline(always)] + pub fn into_bitboard(self) -> Bitboard { + // SAFETY: we know the value is in-bounds + unsafe { *Bitboard::FILES.get_unchecked(self.index()) } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(File::from_index(0), File::A); + assert_eq!(File::from_index(1), File::B); + assert_eq!(File::from_index(7), File::H); + } + + #[test] + fn index() { + assert_eq!(File::A.index(), 0); + assert_eq!(File::B.index(), 1); + assert_eq!(File::H.index(), 7); + } + + #[test] + fn into_bitboard() { + assert_eq!(File::A.into_bitboard(), Bitboard::FILES[0]); + assert_eq!(File::B.into_bitboard(), Bitboard::FILES[1]); + assert_eq!(File::H.into_bitboard(), Bitboard::FILES[7]); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index ef264e1..7923cab 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod file; +pub use file::*; + pub mod rank; pub use rank::*; From 47e4e77f6cd07ebd3848585689ce8b3d238752d0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:20:33 +0200 Subject: [PATCH 010/230] Don't return 'Bitboard' from 'Square::{file,rank}' --- src/board/square.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/board/square.rs b/src/board/square.rs index 640214c..5088a4a 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -1,4 +1,4 @@ -use super::Bitboard; +use super::{Bitboard, File, Rank}; /// Represent a square on a chessboard. Defined in the same order as the /// [Bitboard](crate::board::Bitboard) squares. @@ -69,16 +69,18 @@ impl Square { (self as usize) / 8 } - /// Return a bitboard representing the rank of this square. + /// Return a [Rank] representing the rank of this square. #[inline(always)] - pub fn rank(self) -> Bitboard { - Bitboard::RANKS[self.rank_index()] + pub fn rank(self) -> Rank { + // SAFETY: we know the value is in-bounds + unsafe { Rank::from_index_unchecked(self.rank_index()) } } - /// Return a bitboard representing the rank of this square. + /// Return a [File] representing the rank of this square. #[inline(always)] - pub fn file(self) -> Bitboard { - Bitboard::FILES[self.file_index()] + pub fn file(self) -> File { + // SAFETY: we know the value is in-bounds + unsafe { File::from_index_unchecked(self.file_index()) } } /// Turn a square into a singleton bitboard. @@ -174,6 +176,24 @@ impl std::ops::Sub for Square { mod test { use super::*; use crate::board::bitboard::*; + use crate::board::file::*; + use crate::board::rank::*; + + #[test] + fn file() { + assert_eq!(Square::A1.file(), File::A); + assert_eq!(Square::A2.file(), File::A); + assert_eq!(Square::B1.file(), File::B); + assert_eq!(Square::H8.file(), File::H); + } + + #[test] + fn rank() { + assert_eq!(Square::A1.rank(), Rank::First); + assert_eq!(Square::A2.rank(), Rank::Second); + assert_eq!(Square::B1.rank(), Rank::First); + assert_eq!(Square::H8.rank(), Rank::Eighth); + } #[test] fn left_shift() { From f873b64f15653616c02a45e1c03fe18f14941b6b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:23:19 +0200 Subject: [PATCH 011/230] Add 'Square' constructor from 'File', 'Rank' --- src/board/square.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/board/square.rs b/src/board/square.rs index 5088a4a..3fb23ac 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -34,6 +34,13 @@ impl Square { Self::H1, Self::H2, Self::H3, Self::H4, Self::H5, Self::H6, Self::H7, Self::H8, ]; + /// Construct a [Square] from a [File] and [Rank]. + #[inline(always)] + pub fn new(file: File, rank: Rank) -> Self { + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(file.index() * 8 + rank.index()) } + } + /// Iterate over all squares in order. pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() @@ -179,6 +186,14 @@ mod test { use crate::board::file::*; use crate::board::rank::*; + #[test] + fn new() { + assert_eq!(Square::new(File::A, Rank::First), Square::A1); + assert_eq!(Square::new(File::A, Rank::Second), Square::A2); + assert_eq!(Square::new(File::B, Rank::First), Square::B1); + assert_eq!(Square::new(File::H, Rank::Eighth), Square::H8); + } + #[test] fn file() { assert_eq!(Square::A1.file(), File::A); From 1c4a0938372d663c97491e936be5cbe1d0141d8d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:34:33 +0200 Subject: [PATCH 012/230] Add 'File::{left,right}' --- src/board/file.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/board/file.rs b/src/board/file.rs index b2e87f1..3dac5c8 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -54,6 +54,18 @@ impl File { self as usize } + /// Return the [File] to the left, as seen from white's perspective. Wraps around the board. + pub fn left(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_sub(1) & 7) } + } + + /// Return the [File] to the right, as seen from white's perspective. Wraps around the board. + pub fn right(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_add(1) & 7) } + } + /// Turn a [File] into a [Bitboard] of all squares in that file. #[inline(always)] pub fn into_bitboard(self) -> Bitboard { @@ -80,6 +92,20 @@ mod test { assert_eq!(File::H.index(), 7); } + #[test] + fn left() { + assert_eq!(File::A.left(), File::H); + assert_eq!(File::B.left(), File::A); + assert_eq!(File::H.left(), File::G); + } + + #[test] + fn right() { + assert_eq!(File::A.right(), File::B); + assert_eq!(File::B.right(), File::C); + assert_eq!(File::H.right(), File::A); + } + #[test] fn into_bitboard() { assert_eq!(File::A.into_bitboard(), Bitboard::FILES[0]); From e48994e7bebf7afb8ba8619a403bbfe1d5883e29 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 16 Jul 2022 14:34:46 +0200 Subject: [PATCH 013/230] Add 'Rank::{up,down}' --- src/board/rank.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/board/rank.rs b/src/board/rank.rs index 11ba5e8..c2499d7 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -54,6 +54,18 @@ impl Rank { self as usize } + /// Return the [Rank] one-row up, as seen from white's perspective. Wraps around the board. + pub fn up(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_add(1) & 7) } + } + + /// Return the [Rank] one-row down, as seen from white's perspective. Wraps around the board. + pub fn down(self) -> Self { + // SAFETY: we know the value is in-bounds, through masking + unsafe { Self::from_index_unchecked(self.index().wrapping_sub(1) & 7) } + } + /// Turn a [Rank] into a [Bitboard] of all squares in that rank. #[inline(always)] pub fn into_bitboard(self) -> Bitboard { @@ -80,6 +92,20 @@ mod test { assert_eq!(Rank::Eighth.index(), 7); } + #[test] + fn up() { + assert_eq!(Rank::First.up(), Rank::Second); + assert_eq!(Rank::Second.up(), Rank::Third); + assert_eq!(Rank::Eighth.up(), Rank::First); + } + + #[test] + fn down() { + assert_eq!(Rank::First.down(), Rank::Eighth); + assert_eq!(Rank::Second.down(), Rank::First); + assert_eq!(Rank::Eighth.down(), Rank::Seventh); + } + #[test] fn into_bitboard() { assert_eq!(Rank::First.into_bitboard(), Bitboard::RANKS[0]); From 93d5fe97eda044123b6a068849aae33f4e01e4c8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 20:39:47 +0200 Subject: [PATCH 014/230] Add 'Bitboard::count' --- src/board/bitboard/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 524cf33..d2c8921 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -37,6 +37,12 @@ impl Bitboard { Bitboard(0b00000000_11111111_00000000_00000000_00000000_00000000_00000000_00000000), Bitboard(0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000), ]; + + /// Count the number of pieces in the [Bitboard]. + #[inline(always)] + pub fn count(self) -> u32 { + self.0.count_ones() + } } impl Default for Bitboard { @@ -170,6 +176,13 @@ mod test { use super::*; use crate::board::square::*; + #[test] + fn count() { + assert_eq!(Bitboard::EMPTY.count(), 0); + assert_eq!(Bitboard::FILES[0].count(), 8); + assert_eq!(Bitboard::ALL.count(), 64); + } + #[test] fn iter() { assert_eq!(Bitboard::EMPTY.into_iter().collect::>(), Vec::new()); From be5d2831bdbd095351c9cf374efc13f4d5106848 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 21:30:23 +0200 Subject: [PATCH 015/230] Add 'Square::index' --- src/board/square.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/board/square.rs b/src/board/square.rs index 3fb23ac..04e2300 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -64,6 +64,12 @@ impl Square { std::mem::transmute(index as u8) } + /// Return the index of a given [Square]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + /// Return the index of the rank of this square (0 -> rank 1, ..., 7 -> rank 8). #[inline(always)] pub fn rank_index(self) -> usize { @@ -194,6 +200,14 @@ mod test { assert_eq!(Square::new(File::H, Rank::Eighth), Square::H8); } + #[test] + fn index() { + assert_eq!(Square::A1.index(), 0); + assert_eq!(Square::A2.index(), 1); + assert_eq!(Square::B1.index(), 8); + assert_eq!(Square::H8.index(), 63); + } + #[test] fn file() { assert_eq!(Square::A1.file(), File::A); From 8e2b7c1714ee16b822b2c2b0046c3ca9cdb1c06c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 21:30:23 +0200 Subject: [PATCH 016/230] Use 'Square::index' in 'Square::{file,rank}_index' --- src/board/square.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/board/square.rs b/src/board/square.rs index 04e2300..0079bbb 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -73,13 +73,13 @@ impl Square { /// Return the index of the rank of this square (0 -> rank 1, ..., 7 -> rank 8). #[inline(always)] pub fn rank_index(self) -> usize { - (self as usize) % 8 + self.index() % 8 } /// Return the index of the rank of this square (0 -> file A, ..., 7 -> file H). #[inline(always)] pub fn file_index(self) -> usize { - (self as usize) / 8 + self.index() / 8 } /// Return a [Rank] representing the rank of this square. From c95d7f078a67b1c7de976a03d1ceff3e49d0e8c4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:06:43 +0200 Subject: [PATCH 017/230] Add 'board::Direction' enum --- src/board/directions.rs | 22 ++++++++++++++++++++++ src/board/mod.rs | 3 +++ 2 files changed, 25 insertions(+) create mode 100644 src/board/directions.rs diff --git a/src/board/directions.rs b/src/board/directions.rs new file mode 100644 index 0000000..7e10ab3 --- /dev/null +++ b/src/board/directions.rs @@ -0,0 +1,22 @@ +/// A direction on the board. Either along the rook, bishop, or knight directions +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Direction { + North, + West, + South, + East, + + NorthWest, + SouthWest, + SouthEast, + NorthEast, + + NorthNorthWest, + NorthWestWest, + SouthWestWest, + SouthSouthWest, + SouthSouthEast, + SouthEastEast, + NorthEastEast, + NorthNorthEast, +} diff --git a/src/board/mod.rs b/src/board/mod.rs index 7923cab..bc9d1d9 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod directions; +pub use directions::*; + pub mod file; pub use file::*; From d03e2510501c3b99b69f0cad0f97d0ce1a357ed6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:06:50 +0200 Subject: [PATCH 018/230] Add 'Direction::move_board' Encapsulates the way to move a piece on a board, avoiding the need to mask and shift by hand. --- src/board/directions.rs | 524 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 524 insertions(+) diff --git a/src/board/directions.rs b/src/board/directions.rs index 7e10ab3..3345256 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -1,3 +1,5 @@ +use super::{Bitboard, Rank}; + /// A direction on the board. Either along the rook, bishop, or knight directions #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Direction { @@ -20,3 +22,525 @@ pub enum Direction { NorthEastEast, NorthNorthEast, } + +impl Direction { + /// Move every piece on a board along the given direction. Do not wrap around the board. + #[inline(always)] + pub fn move_board(self, board: Bitboard) -> Bitboard { + // No need to filter for A/H ranks thanks to wrapping + match self { + Self::North => (board - Rank::Eighth.into_bitboard()) << 1, + Self::West => board >> 8, + Self::South => (board - Rank::First.into_bitboard()) >> 1, + Self::East => board << 8, + + Self::NorthWest => (board - Rank::Eighth.into_bitboard()) >> 7, + Self::SouthWest => (board - Rank::First.into_bitboard()) >> 9, + Self::SouthEast => (board - Rank::First.into_bitboard()) << 7, + Self::NorthEast => (board - Rank::Eighth.into_bitboard()) << 9, + + Self::NorthNorthWest => { + (board - Rank::Eighth.into_bitboard() - Rank::Seventh.into_bitboard()) >> 6 + } + Self::NorthWestWest => (board - Rank::Eighth.into_bitboard()) >> 15, + Self::SouthWestWest => (board - Rank::First.into_bitboard()) >> 17, + Self::SouthSouthWest => { + (board - Rank::First.into_bitboard() - Rank::Second.into_bitboard()) >> 10 + } + Self::SouthSouthEast => { + (board - Rank::First.into_bitboard() - Rank::Second.into_bitboard()) << 6 + } + Self::SouthEastEast => (board - Rank::First.into_bitboard()) << 15, + Self::NorthEastEast => (board - Rank::Eighth.into_bitboard()) << 17, + Self::NorthNorthEast => { + (board - Rank::Eighth.into_bitboard() - Rank::Seventh.into_bitboard()) << 10 + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::Square; + + #[test] + fn north() { + assert_eq!( + Direction::North.move_board(Square::A1.into_bitboard()), + Square::A2.into_bitboard() + ); + assert_eq!( + Direction::North.move_board(Square::A2.into_bitboard()), + Square::A3.into_bitboard() + ); + assert_eq!( + Direction::North.move_board(Square::A7.into_bitboard()), + Square::A8.into_bitboard() + ); + assert_eq!( + Direction::North.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn west() { + assert_eq!( + Direction::West.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::West.move_board(Square::B1.into_bitboard()), + Square::A1.into_bitboard() + ); + assert_eq!( + Direction::West.move_board(Square::G1.into_bitboard()), + Square::F1.into_bitboard() + ); + assert_eq!( + Direction::West.move_board(Square::H1.into_bitboard()), + Square::G1.into_bitboard() + ); + } + + #[test] + fn south() { + assert_eq!( + Direction::South.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::South.move_board(Square::A2.into_bitboard()), + Square::A1.into_bitboard() + ); + assert_eq!( + Direction::South.move_board(Square::A7.into_bitboard()), + Square::A6.into_bitboard() + ); + assert_eq!( + Direction::South.move_board(Square::A8.into_bitboard()), + Square::A7.into_bitboard() + ); + } + + #[test] + fn east() { + assert_eq!( + Direction::East.move_board(Square::A1.into_bitboard()), + Square::B1.into_bitboard() + ); + assert_eq!( + Direction::East.move_board(Square::B1.into_bitboard()), + Square::C1.into_bitboard() + ); + assert_eq!( + Direction::East.move_board(Square::G1.into_bitboard()), + Square::H1.into_bitboard() + ); + assert_eq!( + Direction::East.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn north_west() { + assert_eq!( + Direction::NorthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthWest.move_board(Square::B1.into_bitboard()), + Square::A2.into_bitboard() + ); + assert_eq!( + Direction::NorthWest.move_board(Square::H1.into_bitboard()), + Square::G2.into_bitboard() + ); + assert_eq!( + Direction::NorthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthWest.move_board(Square::B8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthWest.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn south_west() { + assert_eq!( + Direction::SouthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::B1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthWest.move_board(Square::B8.into_bitboard()), + Square::A7.into_bitboard() + ); + assert_eq!( + Direction::SouthWest.move_board(Square::H8.into_bitboard()), + Square::G7.into_bitboard() + ); + } + + #[test] + fn south_east() { + assert_eq!( + Direction::SouthEast.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthEast.move_board(Square::B1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::SouthEast.move_board(Square::A8.into_bitboard()), + Square::B7.into_bitboard() + ); + assert_eq!( + Direction::SouthEast.move_board(Square::B8.into_bitboard()), + Square::C7.into_bitboard() + ); + assert_eq!( + Direction::SouthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn north_east() { + assert_eq!( + Direction::NorthEast.move_board(Square::A1.into_bitboard()), + Square::B2.into_bitboard() + ); + assert_eq!( + Direction::NorthEast.move_board(Square::B1.into_bitboard()), + Square::C2.into_bitboard() + ); + assert_eq!( + Direction::NorthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthEast.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthEast.move_board(Square::B8.into_bitboard()), + Bitboard::EMPTY + ); + assert_eq!( + Direction::NorthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY + ); + } + + #[test] + fn north_north_west() { + assert_eq!( + Direction::NorthNorthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::B2.into_bitboard()), + Square::A4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::H1.into_bitboard()), + Square::G3.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::G2.into_bitboard()), + Square::F4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthWest.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } + + #[test] + fn north_west_west() { + assert_eq!( + Direction::NorthWestWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::H1.into_bitboard()), + Square::F2.into_bitboard() + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::G2.into_bitboard()), + Square::E3.into_bitboard() + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthWestWest.move_board(Square::G7.into_bitboard()), + Square::E8.into_bitboard() + ); + } + + #[test] + fn south_west_west() { + assert_eq!( + Direction::SouthWestWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::G2.into_bitboard()), + Square::E1.into_bitboard() + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::H8.into_bitboard()), + Square::F7.into_bitboard() + ); + assert_eq!( + Direction::SouthWestWest.move_board(Square::G7.into_bitboard()), + Square::E6.into_bitboard() + ); + } + + #[test] + fn south_south_west() { + assert_eq!( + Direction::SouthSouthWest.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::B7.into_bitboard()), + Square::A5.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::H8.into_bitboard()), + Square::G6.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthWest.move_board(Square::G7.into_bitboard()), + Square::F5.into_bitboard() + ); + } + + #[test] + fn south_south_east() { + assert_eq!( + Direction::SouthSouthEast.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::B2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::A8.into_bitboard()), + Square::B6.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::B7.into_bitboard()), + Square::C5.into_bitboard() + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthSouthEast.move_board(Square::G7.into_bitboard()), + Square::H5.into_bitboard() + ); + } + + #[test] + fn south_east_east() { + assert_eq!( + Direction::SouthEastEast.move_board(Square::A1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::B2.into_bitboard()), + Square::D1.into_bitboard() + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::A8.into_bitboard()), + Square::C7.into_bitboard() + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::B7.into_bitboard()), + Square::D6.into_bitboard() + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::SouthEastEast.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } + + #[test] + fn north_east_east() { + assert_eq!( + Direction::NorthEastEast.move_board(Square::A1.into_bitboard()), + Square::C2.into_bitboard() + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::B2.into_bitboard()), + Square::D3.into_bitboard() + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::G2.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::B7.into_bitboard()), + Square::D8.into_bitboard() + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthEastEast.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } + + #[test] + fn north_north_east() { + assert_eq!( + Direction::NorthNorthEast.move_board(Square::A1.into_bitboard()), + Square::B3.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::B2.into_bitboard()), + Square::C4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::H1.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::G2.into_bitboard()), + Square::H4.into_bitboard() + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::A8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::B7.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::H8.into_bitboard()), + Bitboard::EMPTY, + ); + assert_eq!( + Direction::NorthNorthEast.move_board(Square::G7.into_bitboard()), + Bitboard::EMPTY, + ); + } +} From be3a8030a77fd6abdeb67952ecd85807706e8653 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 17 Jul 2022 23:27:33 +0200 Subject: [PATCH 019/230] Add 'Direction::iter_{rook,bishop,royalty,knight}' --- src/board/directions.rs | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/board/directions.rs b/src/board/directions.rs index 3345256..d339bc1 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -24,6 +24,49 @@ pub enum Direction { } impl Direction { + /// Directions that a rook could use. + pub const ROOK_DIRECTIONS: [Self; 4] = [Self::North, Self::West, Self::South, Self::East]; + + /// Directions that a bishop could use. + pub const BISHOP_DIRECTIONS: [Self; 4] = [ + Self::NorthWest, + Self::SouthWest, + Self::SouthEast, + Self::NorthEast, + ]; + + /// Directions that a knight could use. + pub const KNIGHT_DIRECTIONS: [Self; 8] = [ + Self::NorthNorthWest, + Self::NorthWestWest, + Self::SouthWestWest, + Self::SouthSouthWest, + Self::SouthSouthEast, + Self::SouthEastEast, + Self::NorthEastEast, + Self::NorthNorthEast, + ]; + + /// Iterate over all directions a rook can take. + pub fn iter_rook() -> impl Iterator { + Self::ROOK_DIRECTIONS.iter().cloned() + } + + /// Iterate over all directions a bishop can take. + pub fn iter_bishop() -> impl Iterator { + Self::BISHOP_DIRECTIONS.iter().cloned() + } + + /// Iterate over all directions a queen or king can take. + pub fn iter_royalty() -> impl Iterator { + Self::iter_rook().chain(Self::iter_bishop()) + } + + /// Iterate over all directions a knight can take. + pub fn iter_knight() -> impl Iterator { + Self::KNIGHT_DIRECTIONS.iter().cloned() + } + /// Move every piece on a board along the given direction. Do not wrap around the board. #[inline(always)] pub fn move_board(self, board: Bitboard) -> Bitboard { From 21eecbdac96f07698aaca50022eb4c78fe5eef43 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:03:35 +0200 Subject: [PATCH 020/230] Add 'Direction::move_square' --- src/board/directions.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/board/directions.rs b/src/board/directions.rs index d339bc1..1c73a16 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -1,4 +1,4 @@ -use super::{Bitboard, Rank}; +use super::{Bitboard, Rank, Square}; /// A direction on the board. Either along the rook, bishop, or knight directions #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -67,6 +67,12 @@ impl Direction { Self::KNIGHT_DIRECTIONS.iter().cloned() } + /// Move a [Square] along the given [Direction], unless it would wrap at the end of the board + pub fn move_square(self, square: Square) -> Option { + let res = self.move_board(square.into_bitboard()); + res.into_iter().next() + } + /// Move every piece on a board along the given direction. Do not wrap around the board. #[inline(always)] pub fn move_board(self, board: Bitboard) -> Bitboard { @@ -105,7 +111,6 @@ impl Direction { #[cfg(test)] mod test { use super::*; - use crate::board::Square; #[test] fn north() { From 5ef6e545f3d56abd1a57515f1c0869e06543ca4b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:27:08 +0200 Subject: [PATCH 021/230] Add 'Bitboard::{ANTI_,}DIAGONAL' --- src/board/bitboard/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index d2c8921..d2f3723 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -38,6 +38,12 @@ impl Bitboard { Bitboard(0b11111111_00000000_00000000_00000000_00000000_00000000_00000000_00000000), ]; + /// The diagonal from [Square::A1] to [Square::H8]. + pub const DIAGONAL: Bitboard = Bitboard(0x8040201008040201); + + /// The diagonal from [Square::A8] to [Square::H1]. + pub const ANTI_DIAGONAL: Bitboard = Bitboard(0x0102040810204080); + /// Count the number of pieces in the [Bitboard]. #[inline(always)] pub fn count(self) -> u32 { From eba64a98d4ee16c7d7038ae4d461ffd2a466be9b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:27:46 +0200 Subject: [PATCH 022/230] Add 'Direction::slide_{square,board}' --- src/board/directions.rs | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/board/directions.rs b/src/board/directions.rs index 1c73a16..26b75de 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -106,11 +106,39 @@ impl Direction { } } } + + /// Slide a board along the given [Direction], i.e: return all successive applications of + /// [Direction::move_square] until no new squares can be reached. + /// It does not make sense to use this method with knight-only directions, and it will panic in + /// debug-mode if it happens. + #[inline(always)] + pub fn slide_square(self, square: Square) -> Bitboard { + self.slide_board(square.into_bitboard()) + } + + /// Slide a board along the given [Direction], i.e: return all successive applications of + /// [Direction::move_board] until no new squares can be reached. + /// It does not make sense to use this method with knight-only directions, and it will panic in + /// debug-mode if it happens. + #[inline(always)] + pub fn slide_board(self, mut board: Bitboard) -> Bitboard { + debug_assert!(!Self::KNIGHT_DIRECTIONS.contains(&self)); + + let mut res = Default::default(); + + while board != Bitboard::EMPTY { + board = self.move_board(board); + res = res | board; + } + + res + } } #[cfg(test)] mod test { use super::*; + use crate::board::{File, Rank}; #[test] fn north() { @@ -591,4 +619,40 @@ mod test { Bitboard::EMPTY, ); } + + #[test] + fn slide() { + assert_eq!( + Direction::North.slide_square(Square::A1), + File::A.into_bitboard() - Square::A1 + ); + assert_eq!( + Direction::West.slide_square(Square::H1), + Rank::First.into_bitboard() - Square::H1 + ); + assert_eq!( + Direction::South.slide_square(Square::A8), + File::A.into_bitboard() - Square::A8 + ); + assert_eq!( + Direction::East.slide_square(Square::A1), + Rank::First.into_bitboard() - Square::A1 + ); + assert_eq!( + Direction::NorthWest.slide_square(Square::H1), + Bitboard::ANTI_DIAGONAL - Square::H1 + ); + assert_eq!( + Direction::SouthWest.slide_square(Square::H8), + Bitboard::DIAGONAL - Square::H8 + ); + assert_eq!( + Direction::SouthEast.slide_square(Square::A8), + Bitboard::ANTI_DIAGONAL - Square::A8 + ); + assert_eq!( + Direction::NorthEast.slide_square(Square::A1), + Bitboard::DIAGONAL - Square::A1 + ); + } } From 63058f6c4abb660c8d81c6c89e8a8d92dc7a4209 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:31:33 +0200 Subject: [PATCH 023/230] Add 'Bitboard::is_empty' --- src/board/bitboard/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index d2f3723..04b205a 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -49,6 +49,12 @@ impl Bitboard { pub fn count(self) -> u32 { self.0.count_ones() } + + /// Return true if there are no pieces in the [Bitboard], otherwise false. + #[inline(always)] + pub fn is_empty(self) -> bool { + self == Self::EMPTY + } } impl Default for Bitboard { From 5a9f0db1b97313c318911712a4bfb9019075a380 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:32:40 +0200 Subject: [PATCH 024/230] Make use of 'Bitboard::is_empty' --- src/board/directions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/directions.rs b/src/board/directions.rs index 26b75de..135f5f4 100644 --- a/src/board/directions.rs +++ b/src/board/directions.rs @@ -126,7 +126,7 @@ impl Direction { let mut res = Default::default(); - while board != Bitboard::EMPTY { + while !board.is_empty() { board = self.move_board(board); res = res | board; } From c6916cc21af3c55be41e47d88932f1231a48e58f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 00:43:48 +0200 Subject: [PATCH 025/230] Add 'Bitboard::{LIGHT,DARK}_SQUARES --- src/board/bitboard/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 04b205a..092dba7 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -44,6 +44,12 @@ impl Bitboard { /// The diagonal from [Square::A8] to [Square::H1]. pub const ANTI_DIAGONAL: Bitboard = Bitboard(0x0102040810204080); + /// The light [Square]s on a board, e.g: [Square::H1]. + pub const LIGHT_SQUARES: Bitboard = Bitboard(0x55AA55AA55AA55AA); + + /// The dark [Square]s on a board, e.g: [Square::A1]. + pub const DARK_SQUARES: Bitboard = Bitboard(0x55AA55AA55AA55AA); + /// Count the number of pieces in the [Bitboard]. #[inline(always)] pub fn count(self) -> u32 { From accbbef64e7386344a4351e0699621ecd636607d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 11:04:52 +0200 Subject: [PATCH 026/230] Add 'static_assert' macro --- src/lib.rs | 1 + src/utils/mod.rs | 2 ++ src/utils/static_assert.rs | 25 +++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 src/utils/mod.rs create mode 100644 src/utils/static_assert.rs diff --git a/src/lib.rs b/src/lib.rs index 667c357..3593172 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ pub mod board; +pub mod utils; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..2833a48 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod static_assert; +pub use static_assert::*; diff --git a/src/utils/static_assert.rs b/src/utils/static_assert.rs new file mode 100644 index 0000000..2c7a9a8 --- /dev/null +++ b/src/utils/static_assert.rs @@ -0,0 +1,25 @@ +//! Static assert. + +/// Assert a condition at compile-time. +/// +/// See [RFC 2790] for potential addition into Rust itself. +/// +/// [RFC 2790]: https://github.com/rust-lang/rfcs/issues/2790 +/// +/// # Examples +/// +/// ``` +/// use seer::utils::static_assert; +/// +/// static_assert!(42 > 0); +/// ``` +#[macro_export] +macro_rules! static_assert { + ($($tt:tt)*) => { + #[allow(dead_code)] + const _: () = assert!($($tt)*); + }; +} + +// I want it namespaced, even though it is exported to the root of the crate by `#[macro_export]`. +pub use static_assert; From a6f4e7e686a021ece3abe70f7b0755f5e5fa3b40 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 11:05:38 +0200 Subject: [PATCH 027/230] Statically assert zero-cost invariants Since some or all of those invariants will come in handy to ensure we use as little memory as possible, to maximize the speed of the move generation later on. --- src/board/bitboard/mod.rs | 5 +++++ src/board/file.rs | 4 ++++ src/board/rank.rs | 4 ++++ src/board/square.rs | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 092dba7..8b716be 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,4 +1,6 @@ use super::Square; +use crate::utils::static_assert; + mod iterator; use iterator::*; @@ -63,6 +65,9 @@ impl Bitboard { } } +// Ensure zero-cost (at least size-wise) wrapping. +static_assert!(std::mem::size_of::() == std::mem::size_of::()); + impl Default for Bitboard { fn default() -> Self { Self::EMPTY diff --git a/src/board/file.rs b/src/board/file.rs index 3dac5c8..fa7ba15 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -1,4 +1,5 @@ use super::Bitboard; +use crate::utils::static_assert; /// An enum representing a singular file on a chess board (i.e: the columns). #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -74,6 +75,9 @@ impl File { } } +// Ensure that niche-optimization is in effect. +static_assert!(std::mem::size_of::>() == std::mem::size_of::()); + #[cfg(test)] mod test { use super::*; diff --git a/src/board/rank.rs b/src/board/rank.rs index c2499d7..59374c7 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -1,4 +1,5 @@ use super::Bitboard; +use crate::utils::static_assert; /// An enum representing a singular rank on a chess board (i.e: the rows). #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -74,6 +75,9 @@ impl Rank { } } +// Ensure that niche-optimization is in effect. +static_assert!(std::mem::size_of::>() == std::mem::size_of::()); + #[cfg(test)] mod test { use super::*; diff --git a/src/board/square.rs b/src/board/square.rs index 0079bbb..69d8167 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -1,4 +1,5 @@ use super::{Bitboard, File, Rank}; +use crate::utils::static_assert; /// Represent a square on a chessboard. Defined in the same order as the /// [Bitboard](crate::board::Bitboard) squares. @@ -185,6 +186,9 @@ impl std::ops::Sub for Square { } } +// Ensure that niche-optimization is in effect. +static_assert!(std::mem::size_of::>() == std::mem::size_of::()); + #[cfg(test)] mod test { use super::*; From ffaad7b7d411b3e0d08c1b4fa2f10e390b103c86 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:11:37 +0200 Subject: [PATCH 028/230] Rename 'board::direction{s,}' --- src/board/{directions.rs => direction.rs} | 0 src/board/mod.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/board/{directions.rs => direction.rs} (100%) diff --git a/src/board/directions.rs b/src/board/direction.rs similarity index 100% rename from src/board/directions.rs rename to src/board/direction.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index bc9d1d9..ea3c69e 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,8 +1,8 @@ pub mod bitboard; pub use bitboard::*; -pub mod directions; -pub use directions::*; +pub mod direction; +pub use direction::*; pub mod file; pub use file::*; From 3553428bb9f02d32f0f1a8b864e85249ac941e38 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:22:04 +0200 Subject: [PATCH 029/230] Add 'Color' enum --- src/board/color.rs | 106 +++++++++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 109 insertions(+) create mode 100644 src/board/color.rs diff --git a/src/board/color.rs b/src/board/color.rs new file mode 100644 index 0000000..ada896d --- /dev/null +++ b/src/board/color.rs @@ -0,0 +1,106 @@ +use super::Rank; + +/// An enum representing the color of a player. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Color { + White, + Black, +} + +impl Color { + /// Convert from a file index into a [Color] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < 2); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } + } + + /// Convert from a file index into a [Color] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Color::index()]. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [Color]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } + + /// Return the first [Rank] for pieces of the given [Color], where its pieces start. + #[inline(always)] + pub fn first_rank(self) -> Rank { + match self { + Self::White => Rank::First, + Self::Black => Rank::Eighth, + } + } + + /// Return the second [Rank] for pieces of the given [Color], where its pawns start. + #[inline(always)] + pub fn second_rank(self) -> Rank { + match self { + Self::White => Rank::Second, + Self::Black => Rank::Seventh, + } + } + + /// Return the fourth [Rank] for pieces of the given [Color], where its pawns move to after a + /// two-square move. + #[inline(always)] + pub fn fourth_rank(self) -> Rank { + match self { + Self::White => Rank::Fourth, + Self::Black => Rank::Fifth, + } + } + + /// Return the seventh [Rank] for pieces of the given [Color], which is the rank before a pawn + /// gets promoted. + #[inline(always)] + pub fn seventh_rank(self) -> Rank { + match self { + Self::White => Rank::Seventh, + Self::Black => Rank::Second, + } + } +} + +impl std::ops::Not for Color { + type Output = Color; + + fn not(self) -> Self::Output { + match self { + Self::White => Self::Black, + Self::Black => Self::White, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(Color::from_index(0), Color::White); + assert_eq!(Color::from_index(1), Color::Black); + } + + #[test] + fn index() { + assert_eq!(Color::White.index(), 0); + assert_eq!(Color::Black.index(), 1); + } + + #[test] + fn not() { + assert_eq!(!Color::White, Color::Black); + assert_eq!(!Color::Black, Color::White); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index ea3c69e..6a29cbe 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod color; +pub use color::*; + pub mod direction; pub use direction::*; From 688b624dd19835498fc766952772ac684a204a7e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:27:03 +0200 Subject: [PATCH 030/230] Add 'CastleRights' enum --- src/board/castle_rights.rs | 59 ++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 62 insertions(+) create mode 100644 src/board/castle_rights.rs diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs new file mode 100644 index 0000000..630fcfb --- /dev/null +++ b/src/board/castle_rights.rs @@ -0,0 +1,59 @@ +/// Current castle rights for a player. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CastleRights { + /// No castling allowed. + NoSide, + /// King-side castling only. + KingSide, + /// Queen-side castling only. + QueenSide, + /// Either side allowed. + BothSides, +} + +impl CastleRights { + /// Convert from a castle rights index into a [CastleRights] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < 4); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } + } + + /// Convert from a castle rights index into a [CastleRights] type, no bounds checking. + /// + /// # Safety + /// + /// This should only be called with values that can be output by [CastleRights::index()]. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [CastleRights]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(CastleRights::from_index(0), CastleRights::NoSide); + assert_eq!(CastleRights::from_index(1), CastleRights::KingSide); + assert_eq!(CastleRights::from_index(2), CastleRights::QueenSide); + assert_eq!(CastleRights::from_index(3), CastleRights::BothSides); + } + + #[test] + fn index() { + assert_eq!(CastleRights::NoSide.index(), 0); + assert_eq!(CastleRights::KingSide.index(), 1); + assert_eq!(CastleRights::QueenSide.index(), 2); + assert_eq!(CastleRights::BothSides.index(), 3); + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index 6a29cbe..ad91192 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,6 +1,9 @@ pub mod bitboard; pub use bitboard::*; +pub mod castle_rights; +pub use castle_rights::*; + pub mod color; pub use color::*; From d8e003ef943655df4c83b0063cc3d871e70eea99 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:27:37 +0200 Subject: [PATCH 031/230] Add 'CastleRights::has_{king,queen}_side' --- src/board/castle_rights.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 630fcfb..0d02986 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -35,6 +35,18 @@ impl CastleRights { pub fn index(self) -> usize { self as usize } + + /// Can the player castle king-side. + #[inline(always)] + pub fn has_king_side(self) -> bool { + (self.index() & 1) != 0 + } + + /// Can the player castle king-side. + #[inline(always)] + pub fn has_queen_side(self) -> bool { + (self.index() & 2) != 0 + } } #[cfg(test)] @@ -56,4 +68,20 @@ mod test { assert_eq!(CastleRights::QueenSide.index(), 2); assert_eq!(CastleRights::BothSides.index(), 3); } + + #[test] + fn has_kingside() { + assert!(!CastleRights::NoSide.has_king_side()); + assert!(!CastleRights::QueenSide.has_king_side()); + assert!(CastleRights::KingSide.has_king_side()); + assert!(CastleRights::BothSides.has_king_side()); + } + + #[test] + fn has_queenside() { + assert!(!CastleRights::NoSide.has_queen_side()); + assert!(!CastleRights::KingSide.has_queen_side()); + assert!(CastleRights::QueenSide.has_queen_side()); + assert!(CastleRights::BothSides.has_queen_side()); + } } From 96b9e0f6d7d8ad4e84a12d73d3c1cf455dcf2358 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:35:31 +0200 Subject: [PATCH 032/230] Add 'CastleRights::without_{king,queen}_side' --- src/board/castle_rights.rs | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 0d02986..05dd438 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -47,6 +47,25 @@ impl CastleRights { pub fn has_queen_side(self) -> bool { (self.index() & 2) != 0 } + + /// Remove king-side castling rights. + #[inline(always)] + pub fn without_king_side(self) -> Self { + self.remove(Self::KingSide) + } + + /// Remove queen-side castling rights. + #[inline(always)] + pub fn without_queen_side(self) -> Self { + self.remove(Self::QueenSide) + } + + /// Remove some [CastleRights], and return the resulting [CastleRights]. + #[inline(always)] + pub fn remove(self, to_remove: CastleRights) -> Self { + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(self.index() & !to_remove.index()) } + } } #[cfg(test)] @@ -84,4 +103,44 @@ mod test { assert!(CastleRights::QueenSide.has_queen_side()); assert!(CastleRights::BothSides.has_queen_side()); } + + #[test] + fn without_king_side() { + assert_eq!( + CastleRights::NoSide.without_king_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::KingSide.without_king_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::QueenSide.without_king_side(), + CastleRights::QueenSide + ); + assert_eq!( + CastleRights::BothSides.without_king_side(), + CastleRights::QueenSide + ); + } + + #[test] + fn without_queen_side() { + assert_eq!( + CastleRights::NoSide.without_queen_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::QueenSide.without_queen_side(), + CastleRights::NoSide + ); + assert_eq!( + CastleRights::KingSide.without_queen_side(), + CastleRights::KingSide + ); + assert_eq!( + CastleRights::BothSides.without_queen_side(), + CastleRights::KingSide + ); + } } From 04ef2828152e6d0688156d38d67d9927e16cefa9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 13:50:23 +0200 Subject: [PATCH 033/230] Add 'CastleRights::unmoved_rooks' --- src/board/castle_rights.rs | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 05dd438..b398b57 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -1,3 +1,5 @@ +use super::{Bitboard, Color, File, Square}; + /// Current castle rights for a player. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum CastleRights { @@ -66,6 +68,22 @@ impl CastleRights { // SAFETY: we know the value is in-bounds unsafe { Self::from_index_unchecked(self.index() & !to_remove.index()) } } + + /// Which rooks have not been moved for a given [CastleRights] and [Color]. + #[inline(always)] + pub fn unmoved_rooks(self, color: Color) -> Bitboard { + let rank = color.first_rank(); + + let king_side_square = Square::new(File::H, rank); + let queen_side_square = Square::new(File::A, rank); + + match self { + Self::NoSide => Bitboard::EMPTY, + Self::KingSide => king_side_square.into_bitboard(), + Self::QueenSide => queen_side_square.into_bitboard(), + Self::BothSides => king_side_square | queen_side_square, + } + } } #[cfg(test)] @@ -143,4 +161,40 @@ mod test { CastleRights::KingSide ); } + + #[test] + fn unmoved_rooks() { + assert_eq!( + CastleRights::NoSide.unmoved_rooks(Color::White), + Bitboard::EMPTY + ); + assert_eq!( + CastleRights::NoSide.unmoved_rooks(Color::Black), + Bitboard::EMPTY + ); + assert_eq!( + CastleRights::KingSide.unmoved_rooks(Color::White), + Square::H1.into_bitboard() + ); + assert_eq!( + CastleRights::KingSide.unmoved_rooks(Color::Black), + Square::H8.into_bitboard() + ); + assert_eq!( + CastleRights::QueenSide.unmoved_rooks(Color::White), + Square::A1.into_bitboard() + ); + assert_eq!( + CastleRights::QueenSide.unmoved_rooks(Color::Black), + Square::A8.into_bitboard() + ); + assert_eq!( + CastleRights::BothSides.unmoved_rooks(Color::White), + Square::A1 | Square::H1 + ); + assert_eq!( + CastleRights::BothSides.unmoved_rooks(Color::Black), + Square::A8 | Square::H8 + ); + } } From c571b53840851badbaaf4c6c25cea39087ac0e4c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 17:27:09 +0200 Subject: [PATCH 034/230] Add 'Color::{forward,backward}_direction' --- src/board/color.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/board/color.rs b/src/board/color.rs index ada896d..92fb4b5 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -1,4 +1,4 @@ -use super::Rank; +use super::{Direction, Rank}; /// An enum representing the color of a player. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -69,6 +69,21 @@ impl Color { Self::Black => Rank::Second, } } + + /// Which way do pawns advance for this color. + #[inline(always)] + pub fn forward_direction(self) -> Direction { + match self { + Self::White => Direction::North, + Self::Black => Direction::South, + } + } + + /// Which way do the opponent's pawns advance for this color. + #[inline(always)] + pub fn backward_direction(self) -> Direction { + (!self).forward_direction() + } } impl std::ops::Not for Color { From 6e871dbfe67afbcdf6bb9c1d1c432064e1b4f333 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 18 Jul 2022 18:21:14 +0200 Subject: [PATCH 035/230] Improve 'board::BitboardIterator' * Accurate 'size_hint'. * Exact size. * Fused iterator (keeps returning 'None' after returning 'None' once). --- src/board/bitboard/iterator.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs index 06db283..fcd644c 100644 --- a/src/board/bitboard/iterator.rs +++ b/src/board/bitboard/iterator.rs @@ -14,4 +14,14 @@ impl Iterator for BitboardIterator { Some(crate::board::Square::from_index(lsb)) } } + + fn size_hint(&self) -> (usize, Option) { + let size = self.0.count_ones() as usize; + + (size, Some(size)) + } } + +impl ExactSizeIterator for BitboardIterator {} + +impl std::iter::FusedIterator for BitboardIterator {} From f4052461273dd09efe61238c42808a8eb8b96e9c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 18:48:22 +0200 Subject: [PATCH 036/230] Add 'Color::slide_board_with_blockers' --- src/board/direction.rs | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/board/direction.rs b/src/board/direction.rs index 135f5f4..324f97c 100644 --- a/src/board/direction.rs +++ b/src/board/direction.rs @@ -121,7 +121,18 @@ impl Direction { /// It does not make sense to use this method with knight-only directions, and it will panic in /// debug-mode if it happens. #[inline(always)] - pub fn slide_board(self, mut board: Bitboard) -> Bitboard { + pub fn slide_board(self, board: Bitboard) -> Bitboard { + self.slide_board_with_blockers(board, Bitboard::EMPTY) + } + + /// Slide a board along the given [Direction], i.e: return all successive applications of + /// [Direction::move_board] until no new squares can be reached. + /// Take into account the `blockers` [Bitboard]: a combination of all pieces on the board which + /// cannot be slid over. The slide is over once a square that is part of `blockers` is reached. + /// It does not make sense to use this method with knight-only directions, and it will panic in + /// debug-mode if it happens. + #[inline(always)] + pub fn slide_board_with_blockers(self, mut board: Bitboard, blockers: Bitboard) -> Bitboard { debug_assert!(!Self::KNIGHT_DIRECTIONS.contains(&self)); let mut res = Default::default(); @@ -129,6 +140,9 @@ impl Direction { while !board.is_empty() { board = self.move_board(board); res = res | board; + if !(board & blockers).is_empty() { + break; + } } res @@ -655,4 +669,29 @@ mod test { Bitboard::DIAGONAL - Square::A1 ); } + + #[test] + fn blocked_slides() { + assert_eq!( + Direction::North + .slide_board_with_blockers(Square::A1.into_bitboard(), Square::A2.into_bitboard()), + Square::A2.into_bitboard() + ); + assert_eq!( + Direction::North + .slide_board_with_blockers(Square::A1.into_bitboard(), Square::A3.into_bitboard()), + Square::A2 | Square::A3 + ); + assert_eq!( + Direction::North + .slide_board_with_blockers(Square::A1.into_bitboard(), Square::A4.into_bitboard()), + Square::A2 | Square::A3 | Square::A4 + ); + // Ensure that the starting square being in `blockers` is not an issue + assert_eq!( + Direction::North + .slide_board_with_blockers(Square::A1.into_bitboard(), Square::A1.into_bitboard()), + File::A.into_bitboard() - Square::A1 + ); + } } From 568c3696838ed75dc3988dcf8b482c8c884f2506 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:07:37 +0200 Subject: [PATCH 037/230] Fix typo in 'board::Color' documentation --- src/board/color.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/board/color.rs b/src/board/color.rs index 92fb4b5..d5c66d3 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -8,7 +8,7 @@ pub enum Color { } impl Color { - /// Convert from a file index into a [Color] type. + /// Convert from a color index into a [Color] type. #[inline(always)] pub fn from_index(index: usize) -> Self { assert!(index < 2); @@ -16,7 +16,7 @@ impl Color { unsafe { Self::from_index_unchecked(index) } } - /// Convert from a file index into a [Color] type, no bounds checking. + /// Convert from a color index into a [Color] type, no bounds checking. /// /// # Safety /// From 3f9341709219fbe776fcf296ab4337b99ca3442a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:18:46 +0200 Subject: [PATCH 038/230] Consistently use 'Self' type in 'impl' blocks --- src/board/file.rs | 20 ++++++++++---------- src/board/rank.rs | 20 ++++++++++---------- src/board/square.rs | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/board/file.rs b/src/board/file.rs index fa7ba15..1601397 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -15,19 +15,19 @@ pub enum File { } impl File { - const ALL: [File; 8] = [ - File::A, - File::B, - File::C, - File::D, - File::E, - File::F, - File::G, - File::H, + const ALL: [Self; 8] = [ + Self::A, + Self::B, + Self::C, + Self::D, + Self::E, + Self::F, + Self::G, + Self::H, ]; /// Iterate over all files in order. - pub fn iter() -> impl Iterator { + pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() } diff --git a/src/board/rank.rs b/src/board/rank.rs index 59374c7..c679278 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -15,19 +15,19 @@ pub enum Rank { } impl Rank { - const ALL: [Rank; 8] = [ - Rank::First, - Rank::Second, - Rank::Third, - Rank::Fourth, - Rank::Fifth, - Rank::Sixth, - Rank::Seventh, - Rank::Eighth, + const ALL: [Self; 8] = [ + Self::First, + Self::Second, + Self::Third, + Self::Fourth, + Self::Fifth, + Self::Sixth, + Self::Seventh, + Self::Eighth, ]; /// Iterate over all ranks in order. - pub fn iter() -> impl Iterator { + pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() } diff --git a/src/board/square.rs b/src/board/square.rs index 69d8167..9c0178e 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -43,7 +43,7 @@ impl Square { } /// Iterate over all squares in order. - pub fn iter() -> impl Iterator { + pub fn iter() -> impl Iterator { Self::ALL.iter().cloned() } From 42126e1c8804edad2998ef829ff507d76afa6c06 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:19:38 +0200 Subject: [PATCH 039/230] Add 'NUM_VARIANTS' constant to all 'board' enums --- src/board/castle_rights.rs | 5 ++++- src/board/color.rs | 5 ++++- src/board/file.rs | 7 +++++-- src/board/rank.rs | 7 +++++-- src/board/square.rs | 7 +++++-- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index b398b57..01f0235 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -14,10 +14,13 @@ pub enum CastleRights { } impl CastleRights { + /// The number of [CastleRights] variants. + pub const NUM_VARIANTS: usize = 4; + /// Convert from a castle rights index into a [CastleRights] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 4); + assert!(index < Self::NUM_VARIANTS); // SAFETY: we know the value is in-bounds unsafe { Self::from_index_unchecked(index) } } diff --git a/src/board/color.rs b/src/board/color.rs index d5c66d3..62fdd13 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -8,10 +8,13 @@ pub enum Color { } impl Color { + /// The number of [Color] variants. + pub const NUM_VARIANTS: usize = 2; + /// Convert from a color index into a [Color] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 2); + assert!(index < Self::NUM_VARIANTS); // SAFETY: we know the value is in-bounds unsafe { Self::from_index_unchecked(index) } } diff --git a/src/board/file.rs b/src/board/file.rs index 1601397..1475e9a 100644 --- a/src/board/file.rs +++ b/src/board/file.rs @@ -15,7 +15,10 @@ pub enum File { } impl File { - const ALL: [Self; 8] = [ + /// The number of [File] variants. + pub const NUM_VARIANTS: usize = 8; + + const ALL: [Self; Self::NUM_VARIANTS] = [ Self::A, Self::B, Self::C, @@ -34,7 +37,7 @@ impl File { /// Convert from a file index into a [File] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 8); + assert!(index < Self::NUM_VARIANTS); // SAFETY: we know the value is in-bounds unsafe { Self::from_index_unchecked(index) } } diff --git a/src/board/rank.rs b/src/board/rank.rs index c679278..f448df5 100644 --- a/src/board/rank.rs +++ b/src/board/rank.rs @@ -15,7 +15,10 @@ pub enum Rank { } impl Rank { - const ALL: [Self; 8] = [ + /// The number of [Rank] variants. + pub const NUM_VARIANTS: usize = 8; + + const ALL: [Self; Self::NUM_VARIANTS] = [ Self::First, Self::Second, Self::Third, @@ -34,7 +37,7 @@ impl Rank { /// Convert from a rank index into a [Rank] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 8); + assert!(index < Self::NUM_VARIANTS); // SAFETY: we know the value is in-bounds unsafe { Self::from_index_unchecked(index) } } diff --git a/src/board/square.rs b/src/board/square.rs index 9c0178e..c164320 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -23,8 +23,11 @@ impl std::fmt::Display for Square { } impl Square { + /// The number of [Square] variants. + pub const NUM_VARIANTS: usize = 64; + #[rustfmt::skip] - const ALL: [Self; 64] = [ + const ALL: [Self; Self::NUM_VARIANTS] = [ Self::A1, Self::A2, Self::A3, Self::A4, Self::A5, Self::A6, Self::A7, Self::A8, Self::B1, Self::B2, Self::B3, Self::B4, Self::B5, Self::B6, Self::B7, Self::B8, Self::C1, Self::C2, Self::C3, Self::C4, Self::C5, Self::C6, Self::C7, Self::C8, @@ -50,7 +53,7 @@ impl Square { /// Convert from a square index into a [Square] type. #[inline(always)] pub fn from_index(index: usize) -> Self { - assert!(index < 64); + assert!(index < Self::NUM_VARIANTS); // SAFETY: we know the value is in-bounds unsafe { Self::from_index_unchecked(index) } } From 1ea4cd0ea44bd9f1160f58f0b0b777e95f8be9d5 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 20 Jul 2022 19:07:16 +0200 Subject: [PATCH 040/230] Add 'Piece' enum --- src/board/mod.rs | 3 ++ src/board/piece.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/board/piece.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index ad91192..da449df 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -13,6 +13,9 @@ pub use direction::*; pub mod file; pub use file::*; +pub mod piece; +pub use piece::*; + pub mod rank; pub use rank::*; diff --git a/src/board/piece.rs b/src/board/piece.rs new file mode 100644 index 0000000..58f989a --- /dev/null +++ b/src/board/piece.rs @@ -0,0 +1,72 @@ +/// An enum representing the type of a piece. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Piece { + King, + Queen, + Rook, + Bishop, + Knight, + Pawn, +} + +impl Piece { + /// The number of [Piece] variants. + pub const NUM_VARIANTS: usize = 6; + + const ALL: [Self; Self::NUM_VARIANTS] = [ + Self::King, + Self::Queen, + Self::Rook, + Self::Bishop, + Self::Knight, + Self::Pawn, + ]; + + /// Iterate over all piece types. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + + /// Convert from a piece index into a [Piece] type. + #[inline(always)] + pub fn from_index(index: usize) -> Self { + assert!(index < Self::NUM_VARIANTS); + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(index) } + } + + /// Convert from a piece index into a [Piece] type, no bounds checking. + /// + /// # Safety + /// + /// Should only be called with values that can be output by [Piece::index()]. + #[inline(always)] + pub unsafe fn from_index_unchecked(index: usize) -> Self { + std::mem::transmute(index as u8) + } + + /// Return the index of a given [Piece]. + #[inline(always)] + pub fn index(self) -> usize { + self as usize + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn from_index() { + assert_eq!(Piece::from_index(0), Piece::King); + assert_eq!(Piece::from_index(1), Piece::Queen); + assert_eq!(Piece::from_index(5), Piece::Pawn); + } + + #[test] + fn index() { + assert_eq!(Piece::King.index(), 0); + assert_eq!(Piece::Queen.index(), 1); + assert_eq!(Piece::Pawn.index(), 5); + } +} From 34b9c18ca5d5758ad65a5d8728649ee9fbf6f858 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:25:29 +0200 Subject: [PATCH 041/230] Add 'Bitboard::iter_powerset' --- src/board/bitboard/mod.rs | 86 +++++++++++++++++++++++++++++++++- src/board/bitboard/superset.rs | 46 ++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/board/bitboard/superset.rs diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 8b716be..377bbf1 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -3,6 +3,8 @@ use crate::utils::static_assert; mod iterator; use iterator::*; +mod superset; +use superset::*; /// Use a 64-bit number to represent a chessboard. Each bit is mapped from to a specific square, so /// that index 0 -> A1, 1 -> A2, ..., 63 -> H8. @@ -63,6 +65,15 @@ impl Bitboard { pub fn is_empty(self) -> bool { self == Self::EMPTY } + + /// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of + /// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that + /// contain all, some, or none of the [Square] that are in the given [Bitboard]. + /// If given an empty [Bitboard], yields the empty [Bitboard] back. + #[inline(always)] + pub fn iter_power_set(self) -> impl Iterator { + BitboardPowerSetIterator::new(self) + } } // Ensure zero-cost (at least size-wise) wrapping. @@ -196,8 +207,10 @@ impl std::ops::Sub for Bitboard { #[cfg(test)] mod test { + use std::collections::HashSet; + use super::*; - use crate::board::square::*; + use crate::board::{square::*, File, Rank}; #[test] fn count() { @@ -280,4 +293,75 @@ mod test { assert_eq!(Bitboard::FILES[0] - Bitboard::RANKS[0], Bitboard(0xff - 1)); assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1)); } + + #[test] + fn iter_power_set_empty() { + assert_eq!( + Bitboard::EMPTY.iter_power_set().collect::>(), + vec![Bitboard::EMPTY] + ) + } + + #[test] + fn iter_power_set_one_square() { + for square in Square::iter() { + assert_eq!( + square + .into_bitboard() + .iter_power_set() + .collect::>(), + [Bitboard::EMPTY, square.into_bitboard()] + .into_iter() + .collect::>() + ) + } + } + + #[test] + fn iter_power_set_two_squares() { + assert_eq!( + (Square::A1 | Square::H8) + .iter_power_set() + .collect::>(), + [ + Bitboard::EMPTY, + Square::A1.into_bitboard(), + Square::H8.into_bitboard(), + Square::A1 | Square::H8 + ] + .into_iter() + .collect::>() + ) + } + + #[test] + fn iter_power_set_six_squares_exhaustive() { + let mask = (0..6) + .map(Square::from_index) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs); + assert_eq!( + mask.iter_power_set().collect::>(), + (0..(1 << 6)).map(Bitboard).collect::>() + ) + } + + #[test] + fn iter_power_set_eight_squares_length() { + assert_eq!( + File::A + .into_bitboard() + .iter_power_set() + .collect::>() + .len(), + 1 << 8 + ); + assert_eq!( + Rank::First + .into_bitboard() + .iter_power_set() + .collect::>() + .len(), + 1 << 8 + ); + } } diff --git a/src/board/bitboard/superset.rs b/src/board/bitboard/superset.rs new file mode 100644 index 0000000..1a82ca1 --- /dev/null +++ b/src/board/bitboard/superset.rs @@ -0,0 +1,46 @@ +use super::Bitboard; + +/// Iterator over a [Bitboard] mask, which yields all potential subsets of the given board. +/// In other words, for each square that belongs to the mask, this will yield all sets that do +/// contain the square, and all sets that do not. +pub struct BitboardPowerSetIterator { + /// The starting board. + board: Bitboard, + /// The next subset. + subset: Bitboard, + /// Whether or not iteration is done. + done: bool, +} + +impl BitboardPowerSetIterator { + pub fn new(board: Bitboard) -> Self { + Self { + board, + subset: Bitboard::EMPTY, + done: false, + } + } +} + +impl Iterator for BitboardPowerSetIterator { + type Item = Bitboard; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + let res = self.subset; + self.subset = Bitboard(self.subset.0.wrapping_sub(self.board.0)) & self.board; + self.done = self.subset.is_empty(); + Some(res) + } + + fn size_hint(&self) -> (usize, Option) { + let size = 1 << self.board.count(); + (size, Some(size)) + } +} + +impl ExactSizeIterator for BitboardPowerSetIterator {} + +impl std::iter::FusedIterator for BitboardPowerSetIterator {} From a4952376a160212b0e9680d4187a85579df5a9e6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:43:18 +0200 Subject: [PATCH 042/230] Remove spurious links in 'Bitboard' documentation --- src/board/bitboard/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 377bbf1..bccbbfa 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -85,7 +85,7 @@ impl Default for Bitboard { } } -/// Iterate over the [Square](crate::board::Square) values included in the board. +/// Iterate over the [Square] values included in the board. impl IntoIterator for Bitboard { type IntoIter = BitboardIterator; type Item = Square; @@ -135,7 +135,7 @@ impl std::ops::BitOr for Bitboard { } } -/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +/// Treat the [Square] as a singleton bitboard, and apply the operator. impl std::ops::BitOr for Bitboard { type Output = Bitboard; @@ -155,7 +155,7 @@ impl std::ops::BitAnd for Bitboard { } } -/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +/// Treat the [Square] as a singleton bitboard, and apply the operator. impl std::ops::BitAnd for Bitboard { type Output = Bitboard; @@ -175,7 +175,7 @@ impl std::ops::BitXor for Bitboard { } } -/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +/// Treat the [Square] as a singleton bitboard, and apply the operator. impl std::ops::BitXor for Bitboard { type Output = Bitboard; @@ -195,7 +195,7 @@ impl std::ops::Sub for Bitboard { } } -/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator. +/// Treat the [Square] as a singleton bitboard, and apply the operator. impl std::ops::Sub for Bitboard { type Output = Bitboard; From 7ccca5a5931749e2c757cfe72ac5cf71571408c6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 21 Jul 2022 20:44:33 +0200 Subject: [PATCH 043/230] Remove spurious links in 'Square' documentation --- src/board/square.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board/square.rs b/src/board/square.rs index c164320..958c3c9 100644 --- a/src/board/square.rs +++ b/src/board/square.rs @@ -2,7 +2,7 @@ use super::{Bitboard, File, Rank}; use crate::utils::static_assert; /// Represent a square on a chessboard. Defined in the same order as the -/// [Bitboard](crate::board::Bitboard) squares. +/// [Bitboard] squares. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[rustfmt::skip] pub enum Square { From 064846a4fd0000cad59e39a36deab9dfe34ccb13 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:22:37 +0200 Subject: [PATCH 044/230] Add naive king move generation --- src/lib.rs | 1 + src/movegen/king.rs | 225 ++++++++++++++++++++++++++++++++++++++++++++ src/movegen/mod.rs | 2 + 3 files changed, 228 insertions(+) create mode 100644 src/movegen/king.rs create mode 100644 src/movegen/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 3593172..bfcf0bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ pub mod board; +pub mod movegen; pub mod utils; diff --git a/src/movegen/king.rs b/src/movegen/king.rs new file mode 100644 index 0000000..ce99b25 --- /dev/null +++ b/src/movegen/king.rs @@ -0,0 +1,225 @@ +use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square}; + +/// Compute a king's movement. No castling moves included +#[allow(unused)] +pub fn king_moves(square: Square) -> Bitboard { + let board = square.into_bitboard(); + + Direction::iter_royalty() + .map(|dir| dir.move_board(board)) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) +} + +/// Compute a king's castling moves, given its [Color] and [CastleRights]. +#[allow(unused)] +pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard { + let rank = color.first_rank(); + + let king_side_square = Square::new(File::G, rank); + let queen_side_square = Square::new(File::C, rank); + + match castle_rights { + CastleRights::NoSide => Bitboard::EMPTY, + CastleRights::KingSide => king_side_square.into_bitboard(), + CastleRights::QueenSide => queen_side_square.into_bitboard(), + CastleRights::BothSides => king_side_square | queen_side_square, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn moves_first_rank() { + assert_eq!(king_moves(Square::A1), Square::A2 | Square::B1 | Square::B2); + assert_eq!( + king_moves(Square::B1), + Square::A1 | Square::A2 | Square::B2 | Square::C1 | Square::C2 + ); + assert_eq!( + king_moves(Square::C1), + Square::B1 | Square::B2 | Square::C2 | Square::D1 | Square::D2 + ); + assert_eq!( + king_moves(Square::D1), + Square::C1 | Square::C2 | Square::D2 | Square::E1 | Square::E2 + ); + assert_eq!( + king_moves(Square::E1), + Square::D1 | Square::D2 | Square::E2 | Square::F1 | Square::F2 + ); + assert_eq!( + king_moves(Square::F1), + Square::E1 | Square::E2 | Square::F2 | Square::G1 | Square::G2 + ); + assert_eq!( + king_moves(Square::G1), + Square::F1 | Square::F2 | Square::G2 | Square::H1 | Square::H2 + ); + assert_eq!(king_moves(Square::H1), Square::G1 | Square::G2 | Square::H2); + } + + #[test] + fn moves_last_rank() { + assert_eq!(king_moves(Square::A8), Square::A7 | Square::B8 | Square::B7); + assert_eq!( + king_moves(Square::B8), + Square::A8 | Square::A7 | Square::B7 | Square::C8 | Square::C7 + ); + assert_eq!( + king_moves(Square::C8), + Square::B8 | Square::B7 | Square::C7 | Square::D8 | Square::D7 + ); + assert_eq!( + king_moves(Square::D8), + Square::C8 | Square::C7 | Square::D7 | Square::E8 | Square::E7 + ); + assert_eq!( + king_moves(Square::E8), + Square::D8 | Square::D7 | Square::E7 | Square::F8 | Square::F7 + ); + assert_eq!( + king_moves(Square::F8), + Square::E8 | Square::E7 | Square::F7 | Square::G8 | Square::G7 + ); + assert_eq!( + king_moves(Square::G8), + Square::F8 | Square::F7 | Square::G7 | Square::H8 | Square::H7 + ); + assert_eq!(king_moves(Square::H8), Square::G8 | Square::G7 | Square::H7); + } + + #[test] + fn moves_first_file() { + assert_eq!(king_moves(Square::A1), Square::A2 | Square::B1 | Square::B2); + assert_eq!( + king_moves(Square::A2), + Square::A1 | Square::A3 | Square::B1 | Square::B2 | Square::B3 + ); + assert_eq!( + king_moves(Square::A3), + Square::A2 | Square::A4 | Square::B2 | Square::B3 | Square::B4 + ); + assert_eq!( + king_moves(Square::A4), + Square::A3 | Square::A5 | Square::B3 | Square::B4 | Square::B5 + ); + assert_eq!( + king_moves(Square::A5), + Square::A4 | Square::A6 | Square::B4 | Square::B5 | Square::B6 + ); + assert_eq!( + king_moves(Square::A6), + Square::A5 | Square::A7 | Square::B5 | Square::B6 | Square::B7 + ); + assert_eq!( + king_moves(Square::A7), + Square::A6 | Square::A8 | Square::B6 | Square::B7 | Square::B8 + ); + assert_eq!(king_moves(Square::A8), Square::A7 | Square::B7 | Square::B8); + } + + #[test] + fn moves_last_file() { + assert_eq!(king_moves(Square::H1), Square::H2 | Square::G1 | Square::G2); + assert_eq!( + king_moves(Square::H2), + Square::H1 | Square::H3 | Square::G1 | Square::G2 | Square::G3 + ); + assert_eq!( + king_moves(Square::H3), + Square::H2 | Square::H4 | Square::G2 | Square::G3 | Square::G4 + ); + assert_eq!( + king_moves(Square::H4), + Square::H3 | Square::H5 | Square::G3 | Square::G4 | Square::G5 + ); + assert_eq!( + king_moves(Square::H5), + Square::H4 | Square::H6 | Square::G4 | Square::G5 | Square::G6 + ); + assert_eq!( + king_moves(Square::H6), + Square::H5 | Square::H7 | Square::G5 | Square::G6 | Square::G7 + ); + assert_eq!( + king_moves(Square::H7), + Square::H6 | Square::H8 | Square::G6 | Square::G7 | Square::G8 + ); + assert_eq!(king_moves(Square::H8), Square::H7 | Square::G7 | Square::G8); + } + + #[test] + fn moves_middle() { + assert_eq!( + king_moves(Square::D4), + Square::C3 + | Square::C4 + | Square::C5 + | Square::D3 + | Square::D5 + | Square::E3 + | Square::E4 + | Square::E5 + ); + assert_eq!( + king_moves(Square::D5), + Square::C4 + | Square::C5 + | Square::C6 + | Square::D4 + | Square::D6 + | Square::E4 + | Square::E5 + | Square::E6 + ); + assert_eq!( + king_moves(Square::E5), + Square::D4 + | Square::D5 + | Square::D6 + | Square::E4 + | Square::E6 + | Square::F4 + | Square::F5 + | Square::F6 + ); + } + + #[test] + fn castling_moves() { + assert_eq!( + king_castling_moves(Color::White, CastleRights::NoSide), + Bitboard::EMPTY + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::NoSide), + Bitboard::EMPTY + ); + assert_eq!( + king_castling_moves(Color::White, CastleRights::KingSide), + Square::G1.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::KingSide), + Square::G8.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::White, CastleRights::QueenSide), + Square::C1.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::QueenSide), + Square::C8.into_bitboard() + ); + assert_eq!( + king_castling_moves(Color::White, CastleRights::BothSides), + Square::C1 | Square::G1 + ); + assert_eq!( + king_castling_moves(Color::Black, CastleRights::BothSides), + Square::C8 | Square::G8 + ); + } +} diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs new file mode 100644 index 0000000..bca1bf7 --- /dev/null +++ b/src/movegen/mod.rs @@ -0,0 +1,2 @@ +// Move generation implementation details +mod king; From 0c4b01138698011908933c3101ab706fae35a898 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:30:16 +0200 Subject: [PATCH 045/230] Add naive knight move generation --- src/movegen/knight.rs | 184 ++++++++++++++++++++++++++++++++++++++++++ src/movegen/mod.rs | 1 + 2 files changed, 185 insertions(+) create mode 100644 src/movegen/knight.rs diff --git a/src/movegen/knight.rs b/src/movegen/knight.rs new file mode 100644 index 0000000..5cc4fc9 --- /dev/null +++ b/src/movegen/knight.rs @@ -0,0 +1,184 @@ +use crate::board::{Bitboard, Direction, Square}; + +/// Compute a knight's movement. +#[allow(unused)] +pub fn knight_moves(square: Square) -> Bitboard { + let board = square.into_bitboard(); + + Direction::iter_knight() + .map(|dir| dir.move_board(board)) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn moves_first_rank() { + assert_eq!(knight_moves(Square::A1), Square::B3 | Square::C2); + assert_eq!( + knight_moves(Square::B1), + Square::A3 | Square::C3 | Square::D2 + ); + assert_eq!( + knight_moves(Square::C1), + Square::A2 | Square::B3 | Square::D3 | Square::E2 + ); + assert_eq!( + knight_moves(Square::D1), + Square::B2 | Square::C3 | Square::E3 | Square::F2 + ); + assert_eq!( + knight_moves(Square::E1), + Square::C2 | Square::D3 | Square::F3 | Square::G2 + ); + assert_eq!( + knight_moves(Square::F1), + Square::D2 | Square::E3 | Square::G3 | Square::H2 + ); + assert_eq!( + knight_moves(Square::G1), + Square::E2 | Square::F3 | Square::H3 + ); + assert_eq!(knight_moves(Square::H1), Square::F2 | Square::G3); + } + + #[test] + fn moves_last_rank() { + assert_eq!(knight_moves(Square::A8), Square::B6 | Square::C7); + assert_eq!( + knight_moves(Square::B8), + Square::A6 | Square::C6 | Square::D7 + ); + assert_eq!( + knight_moves(Square::C8), + Square::A7 | Square::B6 | Square::D6 | Square::E7 + ); + assert_eq!( + knight_moves(Square::D8), + Square::B7 | Square::C6 | Square::E6 | Square::F7 + ); + assert_eq!( + knight_moves(Square::E8), + Square::C7 | Square::D6 | Square::F6 | Square::G7 + ); + assert_eq!( + knight_moves(Square::F8), + Square::D7 | Square::E6 | Square::G6 | Square::H7 + ); + assert_eq!( + knight_moves(Square::G8), + Square::E7 | Square::F6 | Square::H6 + ); + assert_eq!(knight_moves(Square::H8), Square::F7 | Square::G6); + } + + #[test] + fn moves_first_file() { + assert_eq!(knight_moves(Square::A1), Square::B3 | Square::C2); + assert_eq!( + knight_moves(Square::A2), + Square::B4 | Square::C1 | Square::C3 + ); + assert_eq!( + knight_moves(Square::A3), + Square::B1 | Square::B5 | Square::C2 | Square::C4 + ); + assert_eq!( + knight_moves(Square::A4), + Square::B2 | Square::B6 | Square::C3 | Square::C5 + ); + assert_eq!( + knight_moves(Square::A5), + Square::B3 | Square::B7 | Square::C4 | Square::C6 + ); + assert_eq!( + knight_moves(Square::A6), + Square::B4 | Square::B8 | Square::C5 | Square::C7 + ); + assert_eq!( + knight_moves(Square::A7), + Square::B5 | Square::C6 | Square::C8 + ); + assert_eq!(knight_moves(Square::A8), Square::B6 | Square::C7); + } + + #[test] + fn moves_last_file() { + assert_eq!(knight_moves(Square::H1), Square::G3 | Square::F2); + assert_eq!( + knight_moves(Square::H2), + Square::G4 | Square::F1 | Square::F3 + ); + assert_eq!( + knight_moves(Square::H3), + Square::G1 | Square::G5 | Square::F2 | Square::F4 + ); + assert_eq!( + knight_moves(Square::H4), + Square::G2 | Square::G6 | Square::F3 | Square::F5 + ); + assert_eq!( + knight_moves(Square::H5), + Square::G3 | Square::G7 | Square::F4 | Square::F6 + ); + assert_eq!( + knight_moves(Square::H6), + Square::G4 | Square::G8 | Square::F5 | Square::F7 + ); + assert_eq!( + knight_moves(Square::H7), + Square::G5 | Square::F6 | Square::F8 + ); + assert_eq!(knight_moves(Square::H8), Square::G6 | Square::F7); + } + + #[test] + fn moves_middle() { + assert_eq!( + knight_moves(Square::D4), + Square::B3 + | Square::B5 + | Square::C2 + | Square::C6 + | Square::E2 + | Square::E6 + | Square::F3 + | Square::F5 + ); + assert_eq!( + knight_moves(Square::D5), + Square::B4 + | Square::B6 + | Square::C3 + | Square::C7 + | Square::E3 + | Square::E7 + | Square::F4 + | Square::F6 + ); + assert_eq!( + knight_moves(Square::E4), + Square::C3 + | Square::C5 + | Square::D2 + | Square::D6 + | Square::F2 + | Square::F6 + | Square::G3 + | Square::G5 + ); + assert_eq!( + knight_moves(Square::E5), + Square::C4 + | Square::C6 + | Square::D3 + | Square::D7 + | Square::F3 + | Square::F7 + | Square::G4 + | Square::G6 + ); + } +} diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index bca1bf7..35193b2 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,2 +1,3 @@ // Move generation implementation details mod king; +mod knight; From f7fc30227a9fdf000f75bb61024b543766fddf0f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:33:33 +0200 Subject: [PATCH 046/230] Add naive bishop move generation --- src/movegen/bishop.rs | 70 +++++++++++++++++++++++++++++++++++++++++++ src/movegen/mod.rs | 1 + 2 files changed, 71 insertions(+) create mode 100644 src/movegen/bishop.rs diff --git a/src/movegen/bishop.rs b/src/movegen/bishop.rs new file mode 100644 index 0000000..9409cb8 --- /dev/null +++ b/src/movegen/bishop.rs @@ -0,0 +1,70 @@ +use crate::board::{Bitboard, Direction, Square}; + +/// Compute a bishop's movement given a set of blockers that cannot be moved past. +#[allow(unused)] +pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { + Direction::iter_bishop() + .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::{File, Rank}; + + #[test] + fn moves_lower_left_square() { + assert_eq!( + bishop_moves(Square::A1, Bitboard::EMPTY), + Bitboard::DIAGONAL - Square::A1 + ); + assert_eq!( + bishop_moves(Square::A1, Bitboard::ALL), + Square::B2.into_bitboard() + ); + assert_eq!( + bishop_moves(Square::A1, Square::D4.into_bitboard()), + Square::B2 | Square::C3 | Square::D4 + ); + assert_eq!( + bishop_moves(Square::A1, File::D.into_bitboard()), + Square::B2 | Square::C3 | Square::D4 + ); + } + + #[test] + fn moves_middle() { + let cross = Bitboard::DIAGONAL | Direction::South.move_board(Bitboard::ANTI_DIAGONAL); + assert_eq!( + bishop_moves(Square::D4, Bitboard::EMPTY), + cross - Square::D4 + ); + assert_eq!( + bishop_moves(Square::D4, Bitboard::ALL), + Square::C3 | Square::C5 | Square::E3 | Square::E5 + ); + assert_eq!( + bishop_moves(Square::D4, Rank::Fifth.into_bitboard()), + Square::A1 + | Square::B2 + | Square::C3 + | Square::C5 + | Square::E3 + | Square::E5 + | Square::F2 + | Square::G1 + ); + assert_eq!( + bishop_moves(Square::D4, File::E.into_bitboard()), + Square::A1 + | Square::A7 + | Square::B2 + | Square::B6 + | Square::C3 + | Square::C5 + | Square::E3 + | Square::E5 + ); + } +} diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 35193b2..aacfcb4 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,3 +1,4 @@ // Move generation implementation details +mod bishop; mod king; mod knight; From b1a460a5804d89acf01b448e59370fcd49232ca2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:33:45 +0200 Subject: [PATCH 047/230] Add naive rook move generation --- src/movegen/mod.rs | 1 + src/movegen/rook.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/movegen/rook.rs diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index aacfcb4..9f4f280 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -2,3 +2,4 @@ mod bishop; mod king; mod knight; +mod rook; diff --git a/src/movegen/rook.rs b/src/movegen/rook.rs new file mode 100644 index 0000000..2dbacd9 --- /dev/null +++ b/src/movegen/rook.rs @@ -0,0 +1,55 @@ +use crate::board::{Bitboard, Direction, Square}; + +/// Compute a rook's movement given a set of blockers that cannot be moved past. +#[allow(unused)] +pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { + Direction::iter_rook() + .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) + .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::board::{File, Rank}; + + #[test] + fn moves_lower_left_square() { + assert_eq!( + rook_moves(Square::A1, Bitboard::EMPTY), + (File::A.into_bitboard() | Rank::First.into_bitboard()) - Square::A1 + ); + assert_eq!( + rook_moves(Square::A1, Bitboard::ALL), + Square::A2 | Square::B1 + ); + assert_eq!( + rook_moves(Square::A1, Rank::First.into_bitboard()), + (File::A.into_bitboard() | Square::B1) - Square::A1 + ); + assert_eq!( + rook_moves(Square::A1, File::A.into_bitboard()), + (Rank::First.into_bitboard() | Square::A2) - Square::A1 + ); + } + + #[test] + fn moves_middle() { + assert_eq!( + rook_moves(Square::D4, Bitboard::EMPTY), + (File::D.into_bitboard() | Rank::Fourth.into_bitboard()) - Square::D4 + ); + assert_eq!( + rook_moves(Square::D4, Bitboard::ALL), + Square::C4 | Square::D3 | Square::D5 | Square::E4 + ); + assert_eq!( + rook_moves(Square::D4, Rank::Fourth.into_bitboard()), + (File::D.into_bitboard() | Square::C4 | Square::E4) - Square::D4 + ); + assert_eq!( + rook_moves(Square::D4, File::D.into_bitboard()), + (Rank::Fourth.into_bitboard() | Square::D3 | Square::D5) - Square::D4 + ); + } +} From 3eb140b7574beb4f9a0d565021f0cb69eb04be7e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:36:28 +0200 Subject: [PATCH 048/230] Add naive pawn move generation --- src/movegen/mod.rs | 1 + src/movegen/pawn.rs | 139 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 src/movegen/pawn.rs diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 9f4f280..746011d 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -2,4 +2,5 @@ mod bishop; mod king; mod knight; +mod pawn; mod rook; diff --git a/src/movegen/pawn.rs b/src/movegen/pawn.rs new file mode 100644 index 0000000..53551c5 --- /dev/null +++ b/src/movegen/pawn.rs @@ -0,0 +1,139 @@ +use crate::board::{Bitboard, Color, Direction, Rank, Square}; + +/// Compute a pawn's movement given its color, and a set of blockers that cannot be moved past. +#[allow(unused)] +pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { + return Bitboard::EMPTY; + } + + let dir = color.forward_direction(); + + let first_push = dir.move_board(square.into_bitboard()); + let second_push = if square.rank() == color.second_rank() { + Square::new(square.file(), color.fourth_rank()).into_bitboard() + } else { + Bitboard::EMPTY + }; + + if (first_push & blockers).is_empty() { + first_push | second_push + } else { + Bitboard::EMPTY + } +} + +/// Computes the set of squares a pawn can capture, given its color. +#[allow(unused)] +pub fn pawn_captures(color: Color, square: Square) -> Bitboard { + if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { + return Bitboard::EMPTY; + } + + let dir = color.forward_direction(); + + let advanced = dir.move_board(square.into_bitboard()); + + let attack_west = Direction::West.move_board(advanced); + let attack_east = Direction::East.move_board(advanced); + + attack_west | attack_east +} + +/// Computes the set of squares that can capture this one *en-passant*. +#[allow(unused)] +pub fn en_passant_origins(square: Square) -> Bitboard { + let board = square.into_bitboard(); + + let origin_west = Direction::West.move_board(board); + let origin_east = Direction::East.move_board(board); + + origin_west | origin_east +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn moves() { + assert_eq!( + pawn_moves(Color::White, Square::A2, Bitboard::EMPTY), + Square::A3 | Square::A4 + ); + assert_eq!( + pawn_moves(Color::Black, Square::A7, Bitboard::EMPTY), + Square::A5 | Square::A6 + ); + assert_eq!( + pawn_moves(Color::Black, Square::A2, Bitboard::EMPTY), + Square::A1.into_bitboard() + ); + assert_eq!( + pawn_moves(Color::White, Square::A7, Bitboard::EMPTY), + Square::A8.into_bitboard() + ); + } + + #[test] + fn captures() { + assert_eq!( + pawn_captures(Color::White, Square::A2), + Square::B3.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::White, Square::B2), + Square::A3 | Square::C3 + ); + assert_eq!( + pawn_captures(Color::White, Square::H2), + Square::G3.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::Black, Square::A2), + Square::B1.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::Black, Square::B2), + Square::A1 | Square::C1 + ); + assert_eq!( + pawn_captures(Color::Black, Square::H2), + Square::G1.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::White, Square::A7), + Square::B8.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::White, Square::B7), + Square::A8 | Square::C8 + ); + assert_eq!( + pawn_captures(Color::Black, Square::H7), + Square::G6.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::Black, Square::A7), + Square::B6.into_bitboard() + ); + assert_eq!( + pawn_captures(Color::Black, Square::B7), + Square::A6 | Square::C6 + ); + assert_eq!( + pawn_captures(Color::Black, Square::H7), + Square::G6.into_bitboard() + ); + } + + #[test] + fn en_passant() { + assert_eq!(en_passant_origins(Square::A4), Square::B4.into_bitboard()); + assert_eq!(en_passant_origins(Square::A5), Square::B5.into_bitboard()); + assert_eq!(en_passant_origins(Square::B4), Square::A4 | Square::C4); + assert_eq!(en_passant_origins(Square::B5), Square::A5 | Square::C5); + assert_eq!(en_passant_origins(Square::H4), Square::G4.into_bitboard()); + assert_eq!(en_passant_origins(Square::H5), Square::G5.into_bitboard()); + } +} From 55d9780156253a69bdf8b866ef9e73145b6b1124 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:14:46 +0200 Subject: [PATCH 049/230] Add 'Magic' type --- src/movegen/magic/mod.rs | 23 +++++++++++++++++++++++ src/movegen/mod.rs | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 src/movegen/magic/mod.rs diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs new file mode 100644 index 0000000..43acd99 --- /dev/null +++ b/src/movegen/magic/mod.rs @@ -0,0 +1,23 @@ +use crate::board::Bitboard; + +/// A type representing the magic board indexing a given [crate::board::Square]. +pub struct Magic { + /// Magic number. + magic: u64, + /// Base offset into the magic square table. + offset: usize, + /// Mask to apply to the blocker board before applying the magic. + mask: Bitboard, + /// Length of the resulting mask after applying the magic. + shift: u8, +} + +impl Magic { + /// Compute the index into the magics database for this set of `blockers`. + #[allow(unused)] // FIXME: remove once used + pub fn get_index(&self, blockers: Bitboard) -> usize { + let relevant_occupancy = (blockers & self.mask).0; + let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; + base_index + self.offset + } +} diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 746011d..d379194 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,3 +1,7 @@ +// Magic bitboard +pub mod magic; +pub use magic::*; + // Move generation implementation details mod bishop; mod king; From f1cdec2b5e8883fa2db7c9010c3d3d979329da1a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 10:37:56 +0200 Subject: [PATCH 050/230] Add magic mask generation --- src/movegen/mod.rs | 3 +++ src/movegen/wizardry/mask.rs | 41 ++++++++++++++++++++++++++++++++++++ src/movegen/wizardry/mod.rs | 1 + 3 files changed, 45 insertions(+) create mode 100644 src/movegen/wizardry/mask.rs create mode 100644 src/movegen/wizardry/mod.rs diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index d379194..3d22eb0 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -8,3 +8,6 @@ mod king; mod knight; mod pawn; mod rook; + +// Magic bitboard generation +mod wizardry; diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs new file mode 100644 index 0000000..af1b8fa --- /dev/null +++ b/src/movegen/wizardry/mask.rs @@ -0,0 +1,41 @@ +use crate::board::{Bitboard, File, Rank, Square}; +use crate::movegen::bishop::bishop_moves; +use crate::movegen::rook::rook_moves; + +/// Compute the relevancy mask for a bishop on a given [Square]. +#[allow(unused)] // FIXME: remove once used +pub fn generate_bishop_mask(square: Square) -> Bitboard { + let rays = bishop_moves(square, Bitboard::EMPTY); + + let mask = File::A.into_bitboard() + | File::H.into_bitboard() + | Rank::First.into_bitboard() + | Rank::Eighth.into_bitboard(); + + rays - mask +} + +/// Compute the relevancy mask for a rook on a given [Square]. +#[allow(unused)] // FIXME: remove once used +pub fn generate_rook_mask(square: Square) -> Bitboard { + let rays = rook_moves(square, Bitboard::EMPTY); + + let mask = { + let mut mask = Bitboard::EMPTY; + if square.file() != File::A { + mask = mask | File::A.into_bitboard() + }; + if square.file() != File::H { + mask = mask | File::H.into_bitboard() + }; + if square.rank() != Rank::First { + mask = mask | Rank::First.into_bitboard() + }; + if square.rank() != Rank::Eighth { + mask = mask | Rank::Eighth.into_bitboard() + }; + mask + }; + + rays - mask +} diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs new file mode 100644 index 0000000..f6053c6 --- /dev/null +++ b/src/movegen/wizardry/mod.rs @@ -0,0 +1 @@ +mod mask; From 3410ba518e9cd7adfbe02a934288b0e6aa6f3652 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:41:09 +0200 Subject: [PATCH 051/230] Make 'Magic' fields 'pub(crate)' --- src/movegen/magic/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs index 43acd99..f9d01d1 100644 --- a/src/movegen/magic/mod.rs +++ b/src/movegen/magic/mod.rs @@ -3,13 +3,13 @@ use crate::board::Bitboard; /// A type representing the magic board indexing a given [crate::board::Square]. pub struct Magic { /// Magic number. - magic: u64, + pub(crate) magic: u64, /// Base offset into the magic square table. - offset: usize, + pub(crate) offset: usize, /// Mask to apply to the blocker board before applying the magic. - mask: Bitboard, + pub(crate) mask: Bitboard, /// Length of the resulting mask after applying the magic. - shift: u8, + pub(crate) shift: u8, } impl Magic { From d268db38c1b53bad0e2af33bf0e188ff3c40db79 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 22 Jul 2022 18:42:05 +0200 Subject: [PATCH 052/230] Add magic bitboard generation --- src/movegen/magic/mod.rs | 1 - src/movegen/wizardry/generation.rs | 74 ++++++++++++++++++++++++++++++ src/movegen/wizardry/mask.rs | 2 - src/movegen/wizardry/mod.rs | 1 + 4 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/movegen/wizardry/generation.rs diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs index f9d01d1..242a0b4 100644 --- a/src/movegen/magic/mod.rs +++ b/src/movegen/magic/mod.rs @@ -14,7 +14,6 @@ pub struct Magic { impl Magic { /// Compute the index into the magics database for this set of `blockers`. - #[allow(unused)] // FIXME: remove once used pub fn get_index(&self, blockers: Bitboard) -> usize { let relevant_occupancy = (blockers & self.mask).0; let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs new file mode 100644 index 0000000..a5e47e2 --- /dev/null +++ b/src/movegen/wizardry/generation.rs @@ -0,0 +1,74 @@ +use crate::board::{Bitboard, Square}; +use crate::movegen::bishop::bishop_moves; +use crate::movegen::rook::rook_moves; +use crate::movegen::Magic; + +use super::mask::{generate_bishop_mask, generate_rook_mask}; + +/// A trait to represent RNG for u64 values. +#[allow(unused)] // FIXME: remove when used +pub(crate) trait RandGen { + fn gen(&mut self) -> u64; +} + +type MagicGenerationType = (Vec, Vec); + +#[allow(unused)] // FIXME: remove when used +pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType { + generate_magics(rng, generate_bishop_mask, bishop_moves) +} + +#[allow(unused)] // FIXME: remove when used +pub fn generate_rook_magics(rng: &mut dyn RandGen) -> MagicGenerationType { + generate_magics(rng, generate_rook_mask, rook_moves) +} + +fn generate_magics( + rng: &mut dyn RandGen, + mask_fn: impl Fn(Square) -> Bitboard, + moves_fn: impl Fn(Square, Bitboard) -> Bitboard, +) -> MagicGenerationType { + let mut magics = Vec::new(); + let mut boards = Vec::new(); + + for square in Square::iter() { + let mask = mask_fn(square); + + let occupancy_to_moves: Vec<_> = mask + .iter_power_set() + .map(|occupancy| (occupancy, moves_fn(square, occupancy))) + .collect(); + + 'candidate_search: loop { + let mut candidate = Magic { + magic: magic_candidate(rng), + offset: 0, + mask, + shift: (64 - mask.count()) as u8, + }; + let mut candidate_moves = vec![Bitboard::EMPTY; occupancy_to_moves.len()]; + + for (occupancy, moves) in occupancy_to_moves.iter().cloned() { + let index = candidate.get_index(occupancy); + // Non-constructive collision, try with another candidate + if !candidate_moves[index].is_empty() && candidate_moves[index] != moves { + continue 'candidate_search; + } + candidate_moves[index] = moves; + } + + // We have filled all candidate boards, record the correct offset and add the moves + candidate.offset = boards.len(); + magics.push(candidate); + boards.append(&mut candidate_moves); + break; + } + } + + (magics, boards) +} + +fn magic_candidate(rng: &mut dyn RandGen) -> u64 { + // Few bits makes for better candidates + rng.gen() & rng.gen() & rng.gen() +} diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs index af1b8fa..aca9f4f 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -3,7 +3,6 @@ use crate::movegen::bishop::bishop_moves; use crate::movegen::rook::rook_moves; /// Compute the relevancy mask for a bishop on a given [Square]. -#[allow(unused)] // FIXME: remove once used pub fn generate_bishop_mask(square: Square) -> Bitboard { let rays = bishop_moves(square, Bitboard::EMPTY); @@ -16,7 +15,6 @@ pub fn generate_bishop_mask(square: Square) -> Bitboard { } /// Compute the relevancy mask for a rook on a given [Square]. -#[allow(unused)] // FIXME: remove once used pub fn generate_rook_mask(square: Square) -> Bitboard { let rays = rook_moves(square, Bitboard::EMPTY); diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index f6053c6..8b5ba4e 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1 +1,2 @@ +mod generation; mod mask; From 5b56fcfcda45b66d15bf86daa16baef82354801e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 23 Jul 2022 15:32:56 +0200 Subject: [PATCH 053/230] Add 'Color::iter' --- src/board/color.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/board/color.rs b/src/board/color.rs index 62fdd13..f909aca 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -11,6 +11,13 @@ impl Color { /// The number of [Color] variants. pub const NUM_VARIANTS: usize = 2; + const ALL: [Self; Self::NUM_VARIANTS] = [Self::White, Self::Black]; + + /// Iterate over all colors in order. + pub fn iter() -> impl Iterator { + Self::ALL.iter().cloned() + } + /// Convert from a color index into a [Color] type. #[inline(always)] pub fn from_index(index: usize) -> Self { From 9601a5657afc7edd02c1fb4c283eb4d9ed9dea8b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 13:40:01 +0200 Subject: [PATCH 054/230] Make all modules at least 'pub(crate)' --- src/movegen/mod.rs | 12 ++++++------ src/movegen/wizardry/mod.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 3d22eb0..26b60a3 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -3,11 +3,11 @@ pub mod magic; pub use magic::*; // Move generation implementation details -mod bishop; -mod king; -mod knight; -mod pawn; -mod rook; +pub(crate) mod bishop; +pub(crate) mod king; +pub(crate) mod knight; +pub(crate) mod pawn; +pub(crate) mod rook; // Magic bitboard generation -mod wizardry; +pub(crate) mod wizardry; diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 8b5ba4e..dfd732d 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,2 +1,2 @@ -mod generation; +pub(crate) mod generation; mod mask; From a4fd4f5cc47b0799796ac17f15f4e49f1484517e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:24:35 +0200 Subject: [PATCH 055/230] Move naive move generation into sub-module --- src/movegen/mod.rs | 8 ++--- src/movegen/{ => naive}/bishop.rs | 0 src/movegen/{ => naive}/king.rs | 54 +----------------------------- src/movegen/{ => naive}/knight.rs | 0 src/movegen/naive/mod.rs | 14 ++++++++ src/movegen/{ => naive}/pawn.rs | 0 src/movegen/{ => naive}/rook.rs | 0 src/movegen/wizardry/generation.rs | 3 +- src/movegen/wizardry/mask.rs | 3 +- 9 files changed, 19 insertions(+), 63 deletions(-) rename src/movegen/{ => naive}/bishop.rs (100%) rename src/movegen/{ => naive}/king.rs (74%) rename src/movegen/{ => naive}/knight.rs (100%) create mode 100644 src/movegen/naive/mod.rs rename src/movegen/{ => naive}/pawn.rs (100%) rename src/movegen/{ => naive}/rook.rs (100%) diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 26b60a3..9ddbf36 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -2,12 +2,8 @@ pub mod magic; pub use magic::*; -// Move generation implementation details -pub(crate) mod bishop; -pub(crate) mod king; -pub(crate) mod knight; -pub(crate) mod pawn; -pub(crate) mod rook; +// Naive move generation +pub mod naive; // Magic bitboard generation pub(crate) mod wizardry; diff --git a/src/movegen/bishop.rs b/src/movegen/naive/bishop.rs similarity index 100% rename from src/movegen/bishop.rs rename to src/movegen/naive/bishop.rs diff --git a/src/movegen/king.rs b/src/movegen/naive/king.rs similarity index 74% rename from src/movegen/king.rs rename to src/movegen/naive/king.rs index ce99b25..9080667 100644 --- a/src/movegen/king.rs +++ b/src/movegen/naive/king.rs @@ -1,4 +1,4 @@ -use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square}; +use crate::board::{Bitboard, Direction, Square}; /// Compute a king's movement. No castling moves included #[allow(unused)] @@ -10,22 +10,6 @@ pub fn king_moves(square: Square) -> Bitboard { .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) } -/// Compute a king's castling moves, given its [Color] and [CastleRights]. -#[allow(unused)] -pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard { - let rank = color.first_rank(); - - let king_side_square = Square::new(File::G, rank); - let queen_side_square = Square::new(File::C, rank); - - match castle_rights { - CastleRights::NoSide => Bitboard::EMPTY, - CastleRights::KingSide => king_side_square.into_bitboard(), - CastleRights::QueenSide => queen_side_square.into_bitboard(), - CastleRights::BothSides => king_side_square | queen_side_square, - } -} - #[cfg(test)] mod test { use super::*; @@ -186,40 +170,4 @@ mod test { | Square::F6 ); } - - #[test] - fn castling_moves() { - assert_eq!( - king_castling_moves(Color::White, CastleRights::NoSide), - Bitboard::EMPTY - ); - assert_eq!( - king_castling_moves(Color::Black, CastleRights::NoSide), - Bitboard::EMPTY - ); - assert_eq!( - king_castling_moves(Color::White, CastleRights::KingSide), - Square::G1.into_bitboard() - ); - assert_eq!( - king_castling_moves(Color::Black, CastleRights::KingSide), - Square::G8.into_bitboard() - ); - assert_eq!( - king_castling_moves(Color::White, CastleRights::QueenSide), - Square::C1.into_bitboard() - ); - assert_eq!( - king_castling_moves(Color::Black, CastleRights::QueenSide), - Square::C8.into_bitboard() - ); - assert_eq!( - king_castling_moves(Color::White, CastleRights::BothSides), - Square::C1 | Square::G1 - ); - assert_eq!( - king_castling_moves(Color::Black, CastleRights::BothSides), - Square::C8 | Square::G8 - ); - } } diff --git a/src/movegen/knight.rs b/src/movegen/naive/knight.rs similarity index 100% rename from src/movegen/knight.rs rename to src/movegen/naive/knight.rs diff --git a/src/movegen/naive/mod.rs b/src/movegen/naive/mod.rs new file mode 100644 index 0000000..1c64606 --- /dev/null +++ b/src/movegen/naive/mod.rs @@ -0,0 +1,14 @@ +pub mod bishop; +pub use bishop::*; + +pub mod king; +pub use king::*; + +pub mod knight; +pub use knight::*; + +pub mod pawn; +pub use pawn::*; + +pub mod rook; +pub use rook::*; diff --git a/src/movegen/pawn.rs b/src/movegen/naive/pawn.rs similarity index 100% rename from src/movegen/pawn.rs rename to src/movegen/naive/pawn.rs diff --git a/src/movegen/rook.rs b/src/movegen/naive/rook.rs similarity index 100% rename from src/movegen/rook.rs rename to src/movegen/naive/rook.rs diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs index a5e47e2..23da62a 100644 --- a/src/movegen/wizardry/generation.rs +++ b/src/movegen/wizardry/generation.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, Square}; -use crate::movegen::bishop::bishop_moves; -use crate::movegen::rook::rook_moves; +use crate::movegen::naive::{bishop_moves, rook_moves}; use crate::movegen::Magic; use super::mask::{generate_bishop_mask, generate_rook_mask}; diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs index aca9f4f..5a6c56e 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -1,6 +1,5 @@ use crate::board::{Bitboard, File, Rank, Square}; -use crate::movegen::bishop::bishop_moves; -use crate::movegen::rook::rook_moves; +use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves}; /// Compute the relevancy mask for a bishop on a given [Square]. pub fn generate_bishop_mask(square: Square) -> Bitboard { From 2254830ddc3da0733d22e6370f25ad4ea766a661 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 01:43:27 +0000 Subject: [PATCH 056/230] Move 'Magic' to 'wizardry' submodule --- src/movegen/magic/mod.rs | 22 ---------------------- src/movegen/mod.rs | 4 ---- src/movegen/wizardry/generation.rs | 2 +- src/movegen/wizardry/mod.rs | 23 +++++++++++++++++++++++ 4 files changed, 24 insertions(+), 27 deletions(-) delete mode 100644 src/movegen/magic/mod.rs diff --git a/src/movegen/magic/mod.rs b/src/movegen/magic/mod.rs deleted file mode 100644 index 242a0b4..0000000 --- a/src/movegen/magic/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::board::Bitboard; - -/// A type representing the magic board indexing a given [crate::board::Square]. -pub struct Magic { - /// Magic number. - pub(crate) magic: u64, - /// Base offset into the magic square table. - pub(crate) offset: usize, - /// Mask to apply to the blocker board before applying the magic. - pub(crate) mask: Bitboard, - /// Length of the resulting mask after applying the magic. - pub(crate) shift: u8, -} - -impl Magic { - /// Compute the index into the magics database for this set of `blockers`. - pub fn get_index(&self, blockers: Bitboard) -> usize { - let relevant_occupancy = (blockers & self.mask).0; - let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; - base_index + self.offset - } -} diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 9ddbf36..50262d2 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,7 +1,3 @@ -// Magic bitboard -pub mod magic; -pub use magic::*; - // Naive move generation pub mod naive; diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs index 23da62a..c7c73dd 100644 --- a/src/movegen/wizardry/generation.rs +++ b/src/movegen/wizardry/generation.rs @@ -1,8 +1,8 @@ use crate::board::{Bitboard, Square}; use crate::movegen::naive::{bishop_moves, rook_moves}; -use crate::movegen::Magic; use super::mask::{generate_bishop_mask, generate_rook_mask}; +use super::Magic; /// A trait to represent RNG for u64 values. #[allow(unused)] // FIXME: remove when used diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index dfd732d..2405710 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,2 +1,25 @@ pub(crate) mod generation; mod mask; + +use crate::board::Bitboard; + +/// A type representing the magic board indexing a given [crate::board::Square]. +pub struct Magic { + /// Magic number. + pub(crate) magic: u64, + /// Base offset into the magic square table. + pub(crate) offset: usize, + /// Mask to apply to the blocker board before applying the magic. + pub(crate) mask: Bitboard, + /// Length of the resulting mask after applying the magic. + pub(crate) shift: u8, +} + +impl Magic { + /// Compute the index into the magics database for this set of `blockers`. + pub fn get_index(&self, blockers: Bitboard) -> usize { + let relevant_occupancy = (blockers & self.mask).0; + let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize; + base_index + self.offset + } +} From 01966d1015078bce74299a2a14d1a6541fc08874 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 02:54:24 +0000 Subject: [PATCH 057/230] Add missing derives to 'Magic' --- src/movegen/wizardry/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 2405710..eb84fa9 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -4,6 +4,7 @@ mod mask; use crate::board::Bitboard; /// A type representing the magic board indexing a given [crate::board::Square]. +#[derive(Clone, Debug)] pub struct Magic { /// Magic number. pub(crate) magic: u64, From fd3da78224ce86010ed78a8fd6ca8c3263521679 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 02:55:03 +0000 Subject: [PATCH 058/230] Make 'Magic' 'pub(crate)' --- src/movegen/wizardry/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index eb84fa9..0727293 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -5,7 +5,7 @@ use crate::board::Bitboard; /// A type representing the magic board indexing a given [crate::board::Square]. #[derive(Clone, Debug)] -pub struct Magic { +pub(crate) struct Magic { /// Magic number. pub(crate) magic: u64, /// Base offset into the magic square table. From 6cc0380b02109b8b16c5e3b0faef9f8d210ca139 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Mar 2024 02:56:01 +0000 Subject: [PATCH 059/230] Add 'MagicMoves' --- src/movegen/wizardry/mod.rs | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 0727293..5fb8af1 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,7 +1,7 @@ pub(crate) mod generation; mod mask; -use crate::board::Bitboard; +use crate::board::{Bitboard, Square}; /// A type representing the magic board indexing a given [crate::board::Square]. #[derive(Clone, Debug)] @@ -24,3 +24,36 @@ impl Magic { base_index + self.offset } } + +/// A type encapsulating a database of [Magic] bitboard moves. +#[derive(Clone, Debug)] +#[allow(unused)] // FIXME: remove when used +pub(crate) struct MagicMoves { + magics: Vec, + moves: Vec, +} + +#[allow(unused)] // FIXME: remove when used +impl MagicMoves { + /// Initialize a new [MagicMoves] given a matching list of [Magic] and its corresponding moves + /// as a [Bitboard]. + /// + /// # Safety + /// + /// This should only be called with values generated by [crate::movegen::wizardry::generation]. + pub unsafe fn new(magics: Vec, moves: Vec) -> Self { + Self { magics, moves } + } + + /// Get the set of valid moves for a piece standing on a [Square], given a set of blockers. + pub fn query(&self, square: Square, blockers: Bitboard) -> Bitboard { + // SAFETY: indices are in range by construction + unsafe { + let index = self + .magics + .get_unchecked(square.index()) + .get_index(blockers); + *self.moves.get_unchecked(index) + } + } +} From 8fe444c59a7aacf0109712e5dc6952588454ec15 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 11:55:59 +0100 Subject: [PATCH 060/230] Expose magic bitboard generation to parent module --- src/movegen/wizardry/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 5fb8af1..6ee6bd0 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,4 +1,5 @@ -pub(crate) mod generation; +mod generation; +pub(crate) use generation::*; mod mask; use crate::board::{Bitboard, Square}; From 9cf5fc38528e32a1689b883be3066e17ec26ff22 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 12:00:57 +0100 Subject: [PATCH 061/230] Add bitboard-based move generation --- src/movegen/mod.rs | 4 ++ src/movegen/moves.rs | 135 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 src/movegen/moves.rs diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 50262d2..8b5be56 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -3,3 +3,7 @@ pub mod naive; // Magic bitboard generation pub(crate) mod wizardry; + +// Magic bitboard definitions +mod moves; +pub use moves::*; diff --git a/src/movegen/moves.rs b/src/movegen/moves.rs new file mode 100644 index 0000000..7e40a18 --- /dev/null +++ b/src/movegen/moves.rs @@ -0,0 +1,135 @@ +use std::sync::OnceLock; + +use crate::{ + board::{Bitboard, Color, File, Square}, + movegen::{ + naive, + wizardry::{generate_bishop_magics, generate_rook_magics, MagicMoves, RandGen}, + }, +}; + +// A simple XOR-shift RNG implementation. +struct SimpleRng(u64); + +impl SimpleRng { + pub fn new() -> Self { + Self(4) // https://xkcd.com/221/ + } +} + +impl RandGen for SimpleRng { + fn gen(&mut self) -> u64 { + self.0 ^= self.0 >> 12; + self.0 ^= self.0 << 25; + self.0 ^= self.0 >> 27; + self.0 + } +} + +/// Compute the set of possible non-attack moves for a pawn on a [Square], given its [Color] and +/// set of blockers. +pub fn pawn_quiet_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + static PAWN_MOVES: OnceLock<[[Bitboard; 64]; 2]> = OnceLock::new(); + + // If there is a piece in front of the pawn, it can't advance + if !(color.backward_direction().move_board(blockers) & square).is_empty() { + return Bitboard::EMPTY; + } + + PAWN_MOVES.get_or_init(|| { + let mut res = [[Bitboard::EMPTY; 64]; 2]; + for color in Color::iter() { + for square in Square::iter() { + res[color.index()][square.index()] = + naive::pawn_moves(color, square, Bitboard::EMPTY); + } + } + res + })[color.index()][square.index()] +} + +/// Compute the set of possible attacks for a pawn on a [Square], given its [Color]. +pub fn pawn_attacks(color: Color, square: Square) -> Bitboard { + static PAWN_ATTACKS: OnceLock<[[Bitboard; 64]; 2]> = OnceLock::new(); + + PAWN_ATTACKS.get_or_init(|| { + let mut res = [[Bitboard::EMPTY; 64]; 2]; + for color in Color::iter() { + for square in Square::iter() { + res[color.index()][square.index()] = naive::pawn_captures(color, square); + } + } + res + })[color.index()][square.index()] +} + +/// Compute the set of possible moves for a pawn on a [Square], given its [Color] and set of +/// blockers. +pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { + pawn_quiet_moves(color, square, blockers) | pawn_attacks(color, square) +} + +/// Compute the set of possible moves for a knight on a [Square]. +pub fn knight_moves(square: Square) -> Bitboard { + static KNIGHT_MOVES: OnceLock<[Bitboard; 64]> = OnceLock::new(); + KNIGHT_MOVES.get_or_init(|| { + let mut res = [Bitboard::EMPTY; 64]; + for square in Square::iter() { + res[square.index()] = naive::knight_moves(square) + } + res + })[square.index()] +} + +/// Compute the set of possible moves for a bishop on a [Square], given its set of blockers. +pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { + static BISHOP_MAGICS: OnceLock = OnceLock::new(); + BISHOP_MAGICS + .get_or_init(|| { + let (magics, moves) = generate_bishop_magics(&mut SimpleRng::new()); + // SAFETY: we used the generator function to compute these values + unsafe { MagicMoves::new(magics, moves) } + }) + .query(square, blockers) +} + +/// Compute the set of possible moves for a rook on a [Square], given its set of blockers. +pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { + static ROOK_MAGICS: OnceLock = OnceLock::new(); + ROOK_MAGICS + .get_or_init(|| { + let (magics, moves) = generate_rook_magics(&mut SimpleRng::new()); + // SAFETY: we used the generator function to compute these values + unsafe { MagicMoves::new(magics, moves) } + }) + .query(square, blockers) +} + +/// Compute the set of possible moves for a queen on a [Square], given its set of blockers. +pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard { + bishop_moves(square, blockers) | rook_moves(square, blockers) +} + +/// Compute the set of possible moves for a king on a [Square]. +pub fn king_moves(square: Square) -> Bitboard { + static KING_MOVES: OnceLock<[Bitboard; 64]> = OnceLock::new(); + KING_MOVES.get_or_init(|| { + let mut res = [Bitboard::EMPTY; 64]; + for square in Square::iter() { + res[square.index()] = naive::king_moves(square) + } + res + })[square.index()] +} + +/// Compute the squares which should be empty for a king-side castle of the given [Color]. +pub fn kind_side_castle_blockers(color: Color) -> Bitboard { + let rank = color.first_rank(); + Square::new(File::F, rank) | Square::new(File::G, rank) +} + +/// Compute the squares which should be empty for a queen-side castle of the given [Color]. +pub fn queen_side_castle_blockers(color: Color) -> Bitboard { + let rank = color.first_rank(); + Square::new(File::B, rank) | Square::new(File::C, rank) | Square::new(File::D, rank) +} From 6feca16b612167c3436ac4c4964ff6ee612e5251 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 12:08:34 +0100 Subject: [PATCH 062/230] Add 'BitboardIterator::new' --- src/board/bitboard/iterator.rs | 12 ++++++++++-- src/board/bitboard/mod.rs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/board/bitboard/iterator.rs b/src/board/bitboard/iterator.rs index fcd644c..7c01a9a 100644 --- a/src/board/bitboard/iterator.rs +++ b/src/board/bitboard/iterator.rs @@ -1,6 +1,14 @@ /// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a -/// [Bitboard](crate::board::Bitboard). -pub struct BitboardIterator(pub(crate) u64); +/// [Bitboard]. +use crate::board::Bitboard; + +pub struct BitboardIterator(u64); + +impl BitboardIterator { + pub fn new(board: Bitboard) -> Self { + Self(board.0) + } +} impl Iterator for BitboardIterator { type Item = crate::board::Square; diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index bccbbfa..9ef0348 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -91,7 +91,7 @@ impl IntoIterator for Bitboard { type Item = Square; fn into_iter(self) -> Self::IntoIter { - BitboardIterator(self.0) + BitboardIterator::new(self) } } From be506747f8c2d19917fe4fb923b46062ee719f58 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 12:09:16 +0100 Subject: [PATCH 063/230] Tighten item visibilities --- src/movegen/mod.rs | 4 ++-- src/movegen/wizardry/mod.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 8b5be56..f9ce658 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,8 +1,8 @@ // Naive move generation -pub mod naive; +mod naive; // Magic bitboard generation -pub(crate) mod wizardry; +mod wizardry; // Magic bitboard definitions mod moves; diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 6ee6bd0..6ed82d7 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -1,20 +1,20 @@ mod generation; -pub(crate) use generation::*; +pub(super) use generation::*; mod mask; use crate::board::{Bitboard, Square}; /// A type representing the magic board indexing a given [crate::board::Square]. #[derive(Clone, Debug)] -pub(crate) struct Magic { +pub(super) struct Magic { /// Magic number. - pub(crate) magic: u64, + pub(self) magic: u64, /// Base offset into the magic square table. - pub(crate) offset: usize, + pub(self) offset: usize, /// Mask to apply to the blocker board before applying the magic. - pub(crate) mask: Bitboard, + pub(self) mask: Bitboard, /// Length of the resulting mask after applying the magic. - pub(crate) shift: u8, + pub(self) shift: u8, } impl Magic { From 4de41a5544f96058737846b8a838bcc8dd591ed9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 24 Jul 2022 16:43:18 +0200 Subject: [PATCH 064/230] Remove all useless 'allow(unused)' --- src/movegen/naive/bishop.rs | 1 - src/movegen/naive/king.rs | 1 - src/movegen/naive/knight.rs | 1 - src/movegen/naive/pawn.rs | 2 -- src/movegen/naive/rook.rs | 1 - src/movegen/wizardry/generation.rs | 3 --- src/movegen/wizardry/mod.rs | 2 -- 7 files changed, 11 deletions(-) diff --git a/src/movegen/naive/bishop.rs b/src/movegen/naive/bishop.rs index 9409cb8..7a2c97f 100644 --- a/src/movegen/naive/bishop.rs +++ b/src/movegen/naive/bishop.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, Direction, Square}; /// Compute a bishop's movement given a set of blockers that cannot be moved past. -#[allow(unused)] pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { Direction::iter_bishop() .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) diff --git a/src/movegen/naive/king.rs b/src/movegen/naive/king.rs index 9080667..fdbedb7 100644 --- a/src/movegen/naive/king.rs +++ b/src/movegen/naive/king.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, Direction, Square}; /// Compute a king's movement. No castling moves included -#[allow(unused)] pub fn king_moves(square: Square) -> Bitboard { let board = square.into_bitboard(); diff --git a/src/movegen/naive/knight.rs b/src/movegen/naive/knight.rs index 5cc4fc9..28ad7f2 100644 --- a/src/movegen/naive/knight.rs +++ b/src/movegen/naive/knight.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, Direction, Square}; /// Compute a knight's movement. -#[allow(unused)] pub fn knight_moves(square: Square) -> Bitboard { let board = square.into_bitboard(); diff --git a/src/movegen/naive/pawn.rs b/src/movegen/naive/pawn.rs index 53551c5..bde5215 100644 --- a/src/movegen/naive/pawn.rs +++ b/src/movegen/naive/pawn.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, Color, Direction, Rank, Square}; /// Compute a pawn's movement given its color, and a set of blockers that cannot be moved past. -#[allow(unused)] pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { return Bitboard::EMPTY; @@ -24,7 +23,6 @@ pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard } /// Computes the set of squares a pawn can capture, given its color. -#[allow(unused)] pub fn pawn_captures(color: Color, square: Square) -> Bitboard { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { return Bitboard::EMPTY; diff --git a/src/movegen/naive/rook.rs b/src/movegen/naive/rook.rs index 2dbacd9..e61f5ec 100644 --- a/src/movegen/naive/rook.rs +++ b/src/movegen/naive/rook.rs @@ -1,7 +1,6 @@ use crate::board::{Bitboard, Direction, Square}; /// Compute a rook's movement given a set of blockers that cannot be moved past. -#[allow(unused)] pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { Direction::iter_rook() .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) diff --git a/src/movegen/wizardry/generation.rs b/src/movegen/wizardry/generation.rs index c7c73dd..0322977 100644 --- a/src/movegen/wizardry/generation.rs +++ b/src/movegen/wizardry/generation.rs @@ -5,19 +5,16 @@ use super::mask::{generate_bishop_mask, generate_rook_mask}; use super::Magic; /// A trait to represent RNG for u64 values. -#[allow(unused)] // FIXME: remove when used pub(crate) trait RandGen { fn gen(&mut self) -> u64; } type MagicGenerationType = (Vec, Vec); -#[allow(unused)] // FIXME: remove when used pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType { generate_magics(rng, generate_bishop_mask, bishop_moves) } -#[allow(unused)] // FIXME: remove when used pub fn generate_rook_magics(rng: &mut dyn RandGen) -> MagicGenerationType { generate_magics(rng, generate_rook_mask, rook_moves) } diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 6ed82d7..83f4d69 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -28,13 +28,11 @@ impl Magic { /// A type encapsulating a database of [Magic] bitboard moves. #[derive(Clone, Debug)] -#[allow(unused)] // FIXME: remove when used pub(crate) struct MagicMoves { magics: Vec, moves: Vec, } -#[allow(unused)] // FIXME: remove when used impl MagicMoves { /// Initialize a new [MagicMoves] given a matching list of [Magic] and its corresponding moves /// as a [Bitboard]. From 54d2e78954730e27bee2300aed3f746967a30709 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 17:34:11 +0200 Subject: [PATCH 065/230] Add '*Assign' operators to 'Bitboard' --- src/board/bitboard/mod.rs | 80 ++++++++++++++++++++++++++++++++++++ src/board/direction.rs | 2 +- src/movegen/wizardry/mask.rs | 8 ++-- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 9ef0348..0c06625 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -115,6 +115,22 @@ impl std::ops::Shr for Bitboard { } } +/// Treat bitboard as a set of squares, shift each square's index left by the amount given. +impl std::ops::ShlAssign for Bitboard { + #[inline(always)] + fn shl_assign(&mut self, rhs: usize) { + *self = *self << rhs; + } +} + +/// Treat bitboard as a set of squares, shift each square's index right by the amount given. +impl std::ops::ShrAssign for Bitboard { + #[inline(always)] + fn shr_assign(&mut self, rhs: usize) { + *self = *self >> rhs; + } +} + /// Treat bitboard as a set of squares, and invert the set. impl std::ops::Not for Bitboard { type Output = Bitboard; @@ -145,6 +161,22 @@ impl std::ops::BitOr for Bitboard { } } +/// Treat each bitboard as a set of squares, keep squares that are in either sets. +impl std::ops::BitOrAssign for Bitboard { + #[inline(always)] + fn bitor_assign(&mut self, rhs: Bitboard) { + *self = *self | rhs; + } +} + +/// Treat the [Square] as a singleton bitboard, and apply the operator. +impl std::ops::BitOrAssign for Bitboard { + #[inline(always)] + fn bitor_assign(&mut self, rhs: Square) { + *self = *self | rhs; + } +} + /// Treat each bitboard as a set of squares, keep squares that are in both sets. impl std::ops::BitAnd for Bitboard { type Output = Bitboard; @@ -165,6 +197,22 @@ impl std::ops::BitAnd for Bitboard { } } +/// Treat each bitboard as a set of squares, keep squares that are in both sets. +impl std::ops::BitAndAssign for Bitboard { + #[inline(always)] + fn bitand_assign(&mut self, rhs: Bitboard) { + *self = *self & rhs; + } +} + +/// Treat the [Square] as a singleton bitboard, and apply the operator. +impl std::ops::BitAndAssign for Bitboard { + #[inline(always)] + fn bitand_assign(&mut self, rhs: Square) { + *self = *self & rhs; + } +} + /// Treat each bitboard as a set of squares, keep squares that are in exactly one of either set. impl std::ops::BitXor for Bitboard { type Output = Bitboard; @@ -185,6 +233,22 @@ impl std::ops::BitXor for Bitboard { } } +/// Treat each bitboard as a set of squares, keep squares that are in exactly one of either set. +impl std::ops::BitXorAssign for Bitboard { + #[inline(always)] + fn bitxor_assign(&mut self, rhs: Bitboard) { + *self = *self ^ rhs; + } +} + +/// Treat the [Square] as a singleton bitboard, and apply the operator. +impl std::ops::BitXorAssign for Bitboard { + #[inline(always)] + fn bitxor_assign(&mut self, rhs: Square) { + *self = *self ^ rhs; + } +} + /// Treat each bitboard as a set of squares, and substract one set from another. impl std::ops::Sub for Bitboard { type Output = Bitboard; @@ -205,6 +269,22 @@ impl std::ops::Sub for Bitboard { } } +/// Treat each bitboard as a set of squares, and substract one set from another. +impl std::ops::SubAssign for Bitboard { + #[inline(always)] + fn sub_assign(&mut self, rhs: Bitboard) { + *self = *self - rhs; + } +} + +/// Treat the [Square] as a singleton bitboard, and apply the operator. +impl std::ops::SubAssign for Bitboard { + #[inline(always)] + fn sub_assign(&mut self, rhs: Square) { + *self = *self - rhs; + } +} + #[cfg(test)] mod test { use std::collections::HashSet; diff --git a/src/board/direction.rs b/src/board/direction.rs index 324f97c..40c8d69 100644 --- a/src/board/direction.rs +++ b/src/board/direction.rs @@ -139,7 +139,7 @@ impl Direction { while !board.is_empty() { board = self.move_board(board); - res = res | board; + res |= board; if !(board & blockers).is_empty() { break; } diff --git a/src/movegen/wizardry/mask.rs b/src/movegen/wizardry/mask.rs index 5a6c56e..865c986 100644 --- a/src/movegen/wizardry/mask.rs +++ b/src/movegen/wizardry/mask.rs @@ -20,16 +20,16 @@ pub fn generate_rook_mask(square: Square) -> Bitboard { let mask = { let mut mask = Bitboard::EMPTY; if square.file() != File::A { - mask = mask | File::A.into_bitboard() + mask |= File::A.into_bitboard() }; if square.file() != File::H { - mask = mask | File::H.into_bitboard() + mask |= File::H.into_bitboard() }; if square.rank() != Rank::First { - mask = mask | Rank::First.into_bitboard() + mask |= Rank::First.into_bitboard() }; if square.rank() != Rank::Eighth { - mask = mask | Rank::Eighth.into_bitboard() + mask |= Rank::Eighth.into_bitboard() }; mask }; From 80ea35babe25cfc75332be4c226f667a7794466c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:26:39 +0200 Subject: [PATCH 066/230] Add 'CastleRights::with_{king,queen}_side' --- src/board/castle_rights.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/board/castle_rights.rs b/src/board/castle_rights.rs index 01f0235..b34d952 100644 --- a/src/board/castle_rights.rs +++ b/src/board/castle_rights.rs @@ -53,6 +53,25 @@ impl CastleRights { (self.index() & 2) != 0 } + /// Add king-side castling rights. + #[inline(always)] + pub fn with_king_side(self) -> Self { + self.add(Self::KingSide) + } + + /// Add queen-side castling rights. + #[inline(always)] + pub fn with_queen_side(self) -> Self { + self.add(Self::QueenSide) + } + + /// Add some [CastleRights], and return the resulting [CastleRights]. + #[inline(always)] + fn add(self, additional_rights: CastleRights) -> Self { + // SAFETY: we know the value is in-bounds + unsafe { Self::from_index_unchecked(self.index() | additional_rights.index()) } + } + /// Remove king-side castling rights. #[inline(always)] pub fn without_king_side(self) -> Self { From be5e9722de5e581978cef994899f2db1056c1712 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:38:51 +0200 Subject: [PATCH 067/230] Add 'Color::third_rank' --- src/board/color.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/board/color.rs b/src/board/color.rs index f909aca..66b21b3 100644 --- a/src/board/color.rs +++ b/src/board/color.rs @@ -60,6 +60,16 @@ impl Color { } } + /// Return the third [Rank] for pieces of the given [Color], where its pawns move to after a + /// one-square move on the start position. + #[inline(always)] + pub fn third_rank(self) -> Rank { + match self { + Self::White => Rank::Third, + Self::Black => Rank::Sixth, + } + } + /// Return the fourth [Rank] for pieces of the given [Color], where its pawns move to after a /// two-square move. #[inline(always)] From 19d0e1bd34e2d244c72e3d9b8bca358272483379 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 11:00:23 +0200 Subject: [PATCH 068/230] Add 'Bitboard::has_more_than_one' --- src/board/bitboard/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index 0c06625..e186a07 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -66,6 +66,13 @@ impl Bitboard { self == Self::EMPTY } + /// Return true if there are more than piece in the [Bitboard]. This is faster than testing + /// `board.count() > 1`. + #[inline(always)] + pub fn has_more_than_one(self) -> bool { + (self.0 & (self.0.wrapping_sub(1))) != 0 + } + /// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of /// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that /// contain all, some, or none of the [Square] that are in the given [Bitboard]. @@ -374,6 +381,16 @@ mod test { assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1)); } + #[test] + fn more_than_one() { + assert!(!Bitboard::EMPTY.has_more_than_one()); + for square in Square::iter() { + assert!(!square.into_bitboard().has_more_than_one()) + } + assert!((Square::A1 | Square::H8).has_more_than_one()); + assert!(Bitboard::ALL.has_more_than_one()); + } + #[test] fn iter_power_set_empty() { assert_eq!( From 856c3a873d947d640dd94c857d81b63c314d17c6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 15:56:08 +0100 Subject: [PATCH 069/230] Deny warnings in 'clippy' pre-commit hook --- flake.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flake.nix b/flake.nix index 134bcdc..ca06a36 100644 --- a/flake.nix +++ b/flake.nix @@ -66,6 +66,9 @@ hooks = { clippy = { enable = true; + settings = { + denyWarnings = true; + }; }; nixpkgs-fmt = { From ddc8ecc4746d3723c0766f7baa5997f244b13d6c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 25 Jul 2022 19:12:51 +0200 Subject: [PATCH 070/230] Add 'Move' --- src/board/mod.rs | 3 + src/board/move.rs | 232 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 src/board/move.rs diff --git a/src/board/mod.rs b/src/board/mod.rs index da449df..d23ef25 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -13,6 +13,9 @@ pub use direction::*; pub mod file; pub use file::*; +pub mod r#move; +pub use r#move::*; + pub mod piece; pub use piece::*; diff --git a/src/board/move.rs b/src/board/move.rs new file mode 100644 index 0000000..c7a6980 --- /dev/null +++ b/src/board/move.rs @@ -0,0 +1,232 @@ +use super::{Piece, Square}; + +type Bitset = u32; + +/// A chess move, containing: +/// * Piece type. +/// * Starting square. +/// * Destination square. +/// * Optional capture type. +/// * Optional promotion type. +/// * Optional captured type. +/// * Whether the move was an en-passant capture. +/// * Whether the move was a double-step. +/// * Whether the move was a castling. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Move(Bitset); + +/// A builder for [Move]. This is the prefered and only way of building a [Move]. +pub struct MoveBuilder { + pub piece: Piece, + pub start: Square, + pub destination: Square, + pub capture: Option, + pub promotion: Option, + pub en_passant: bool, + pub double_step: bool, + pub castling: bool, +} + +impl From for Move { + #[inline(always)] + fn from(builder: MoveBuilder) -> Self { + Self::new( + builder.piece, + builder.start, + builder.destination, + builder.capture, + builder.promotion, + builder.en_passant, + builder.double_step, + builder.castling, + ) + } +} + +/// A [Move] is structured as a bitset with the following fields: +/// | Field | Size | Range of values | Note | +/// |-------------|------|-----------------|-------------------------------------------------| +/// | Piece | 3 | 0-6 | Can be interpreted as a [Piece] index | +/// | Start | 6 | 0-63 | Can be interpreted as a [Square] index | +/// | Destination | 6 | 0-63 | Can be interpreted as a [Square] index | +/// | Capture | 3 | 0-7 | Can be interpreted as a [Piece] index if not 7 | +/// | Promotion | 3 | 0-7 | Can be interpreted as a [Piece] index if not 7 | +/// | En-pasant | 1 | 0-1 | Boolean value | +/// | Double-step | 1 | 0-1 | Boolean value | +/// | Castling | 1 | 0-1 | Boolean value | +mod shift { + use super::Bitset; + + pub const PIECE: usize = 0; + pub const PIECE_MASK: Bitset = 0b111; + + pub const START: usize = 3; + pub const START_MASK: Bitset = 0b11_1111; + + pub const DESTINATION: usize = 9; + pub const DESTINATION_MASK: Bitset = 0b11_1111; + + pub const CAPTURE: usize = 15; + pub const CAPTURE_MASK: Bitset = 0b111; + + pub const PROMOTION: usize = 18; + pub const PROMOTION_MASK: Bitset = 0b111; + + pub const EN_PASSANT: usize = 21; + pub const EN_PASSANT_MASK: Bitset = 0b1; + + pub const DOUBLE_STEP: usize = 22; + pub const DOUBLE_STEP_MASK: Bitset = 0b1; + + pub const CASTLING: usize = 23; + pub const CASTLING_MASK: Bitset = 0b1; +} + +impl Move { + /// Construct a new move. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + fn new( + piece: Piece, + start: Square, + destination: Square, + capture: Option, + promotion: Option, + en_passant: bool, + double_step: bool, + castling: bool, + ) -> Self { + let mut value = 0; + value |= (piece.index() as Bitset) << shift::PIECE; + value |= (start.index() as Bitset) << shift::START; + value |= (destination.index() as Bitset) << shift::DESTINATION; + value |= + (capture.map(Piece::index).unwrap_or(Piece::NUM_VARIANTS) as Bitset) << shift::CAPTURE; + value |= (promotion.map(Piece::index).unwrap_or(Piece::NUM_VARIANTS) as Bitset) + << shift::PROMOTION; + value |= (en_passant as Bitset) << shift::EN_PASSANT; + value |= (double_step as Bitset) << shift::DOUBLE_STEP; + value |= (castling as Bitset) << shift::CASTLING; + Self(value) + } + + /// Get the [Piece] that is being moved. + #[inline(always)] + pub fn piece(self) -> Piece { + let index = ((self.0 >> shift::PIECE) & shift::PIECE_MASK) as usize; + // SAFETY: we know the value is in-bounds + unsafe { Piece::from_index_unchecked(index) } + } + + /// Get the [Square] that this move starts from. + #[inline(always)] + pub fn start(self) -> Square { + let index = ((self.0 >> shift::START) & shift::START_MASK) as usize; + // SAFETY: we know the value is in-bounds + unsafe { Square::from_index_unchecked(index) } + } + + /// Get the [Square] that this move ends on. + #[inline(always)] + pub fn destination(self) -> Square { + let index = ((self.0 >> shift::DESTINATION) & shift::DESTINATION_MASK) as usize; + // SAFETY: we know the value is in-bounds + unsafe { Square::from_index_unchecked(index) } + } + + /// Get the [Piece] that this move captures, or `None` if there are no captures. + #[inline(always)] + pub fn capture(self) -> Option { + let index = ((self.0 >> shift::CAPTURE) & shift::CAPTURE_MASK) as usize; + if index < Piece::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + unsafe { Some(Piece::from_index_unchecked(index)) } + } else { + None + } + } + + /// Get the [Piece] that this move promotes to, or `None` if there are no promotions. + #[inline(always)] + pub fn promotion(self) -> Option { + let index = ((self.0 >> shift::PROMOTION) & shift::PROMOTION_MASK) as usize; + if index < Piece::NUM_VARIANTS { + // SAFETY: we know the value is in-bounds + unsafe { Some(Piece::from_index_unchecked(index)) } + } else { + None + } + } + + /// Get the whether or not the move is an en-passant capture. + #[inline(always)] + pub fn is_en_passant(self) -> bool { + let index = (self.0 >> shift::EN_PASSANT) & shift::EN_PASSANT_MASK; + index != 0 + } + + /// Get the whether or not the move is a pawn double step. + #[inline(always)] + pub fn is_double_step(self) -> bool { + let index = (self.0 >> shift::DOUBLE_STEP) & shift::DOUBLE_STEP_MASK; + index != 0 + } + + /// Get the whether or not the move is a castling. + #[inline(always)] + pub fn is_castling(self) -> bool { + let index = (self.0 >> shift::CASTLING) & shift::CASTLING_MASK; + index != 0 + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn builder_simple() { + let chess_move: Move = MoveBuilder { + piece: Piece::Queen, + start: Square::A2, + destination: Square::A3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(); + assert_eq!(chess_move.piece(), Piece::Queen); + assert_eq!(chess_move.start(), Square::A2); + assert_eq!(chess_move.destination(), Square::A3); + assert_eq!(chess_move.capture(), None); + assert_eq!(chess_move.promotion(), None); + assert!(!chess_move.is_en_passant()); + assert!(!chess_move.is_double_step()); + assert!(!chess_move.is_castling()); + } + + #[test] + fn builder_all_fields() { + let chess_move: Move = MoveBuilder { + piece: Piece::Pawn, + start: Square::A7, + destination: Square::B8, + capture: Some(Piece::Queen), + promotion: Some(Piece::Knight), + en_passant: true, + double_step: true, + castling: true, + } + .into(); + assert_eq!(chess_move.piece(), Piece::Pawn); + assert_eq!(chess_move.start(), Square::A7); + assert_eq!(chess_move.destination(), Square::B8); + assert_eq!(chess_move.capture(), Some(Piece::Queen)); + assert_eq!(chess_move.promotion(), Some(Piece::Knight)); + assert!(chess_move.is_en_passant()); + assert!(chess_move.is_double_step()); + assert!(chess_move.is_castling()); + } +} From a7ed24c752ce861491fc4032a625aea18026e9a9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:02:23 +0100 Subject: [PATCH 071/230] Add 'Color' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 2bfce31..995a05c 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -1,3 +1,5 @@ +import enum + import gdb.printing @@ -44,6 +46,19 @@ class Bitboard(object): n ^= b +class Color(enum.IntEnum): + """ + Python representation of a 'seer::board::color::Color' raw value. + """ + + # Should be kept in sync with the enum in `color.rs` + WHITE = 0 + BLACK = 1 + + def __str__(self): + return self.name.title() + + class SquarePrinter(object): "Print a seer::board::square::Square" @@ -64,11 +79,22 @@ class BitboardPrinter(object): return "Bitboard{" + str(self._val)[1:-1] + "}" +class ColorPrinter(object): + "Print a seer::board::color::Color" + + def __init__(self, val): + self._val = Color(int(val)) + + def to_string(self): + return str(self._val) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter) pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) + pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter) return pp From ca68ccf92c6e1237807f82915385384838f4f628 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:50:49 +0100 Subject: [PATCH 072/230] Add 'File' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 995a05c..78efca0 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -59,6 +59,25 @@ class Color(enum.IntEnum): return self.name.title() +class File(enum.IntEnum): + """ + Python representation of a 'seer::board::file::File' raw value. + """ + + # Should be kept in sync with the enum in `file.rs` + A = 0 + B = 1 + C = 2 + D = 3 + E = 4 + F = 5 + G = 6 + H = 7 + + def __str__(self): + return self.name.title() + + class SquarePrinter(object): "Print a seer::board::square::Square" @@ -89,12 +108,23 @@ class ColorPrinter(object): return str(self._val) +class FilePrinter(object): + "Print a seer::board::file::File" + + def __init__(self, val): + self._val = File(int(val)) + + def to_string(self): + return str(self._val) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter) pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter) + pp.add_printer('File', '^seer::board::file::File$', FilePrinter) return pp From 1a71854589528550219cc0ceb0cbee9657235e76 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:50:49 +0100 Subject: [PATCH 073/230] Add 'Rank' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 78efca0..aec66d6 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -78,6 +78,25 @@ class File(enum.IntEnum): return self.name.title() +class Rank(enum.IntEnum): + """ + Python representation of a 'seer::board::rank::Rank' raw value. + """ + + # Should be kept in sync with the enum in `rank.rs` + First = 0 + Second = 1 + Third = 2 + Fourth = 3 + Fifth = 4 + Sixth = 5 + Seventh = 6 + Eighth = 7 + + def __str__(self): + return self.name.title() + + class SquarePrinter(object): "Print a seer::board::square::Square" @@ -118,6 +137,16 @@ class FilePrinter(object): return str(self._val) +class RankPrinter(object): + "Print a seer::board::rank::Rank" + + def __init__(self, val): + self._val = Rank(int(val)) + + def to_string(self): + return str(self._val) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') @@ -125,6 +154,7 @@ def build_pretty_printer(): pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter) pp.add_printer('File', '^seer::board::file::File$', FilePrinter) + pp.add_printer('Rank', '^seer::board::rank::Rank$', RankPrinter) return pp From e1a15328728d9fdf797db82c6ebdc0bbe00c3242 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:02:23 +0100 Subject: [PATCH 074/230] Add 'Piece' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index aec66d6..523c52f 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -97,6 +97,23 @@ class Rank(enum.IntEnum): return self.name.title() +class Piece(enum.IntEnum): + """ + Python representation of a 'seer::board::piece::Piece' raw value. + """ + + # Should be kept in sync with the enum in `piece.rs` + KING = 0 + QUEEN = 1 + ROOK = 2 + BISHOP = 3 + KNIGHT = 4 + PAWN = 5 + + def __str__(self): + return self.name.title() + + class SquarePrinter(object): "Print a seer::board::square::Square" @@ -147,6 +164,16 @@ class RankPrinter(object): return str(self._val) +class PiecePrinter(object): + "Print a seer::board::piece::Piece" + + def __init__(self, val): + self._val = Piece(int(val)) + + def to_string(self): + return str(self._val) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') @@ -155,6 +182,7 @@ def build_pretty_printer(): pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter) pp.add_printer('File', '^seer::board::file::File$', FilePrinter) pp.add_printer('Rank', '^seer::board::rank::Rank$', RankPrinter) + pp.add_printer('Piece', '^seer::board::piece::Piece$', ColorPrinter) return pp From a26c53201515c9af63e408ed04d9394af4747471 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sun, 31 Mar 2024 20:02:23 +0100 Subject: [PATCH 075/230] Add 'Move' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 523c52f..1072cb8 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -114,6 +114,89 @@ class Piece(enum.IntEnum): return self.name.title() +class Move(object): + """ + Wrapper around GDB's representation of a 'seer::board::move::Move' + in memory. + """ + + # Should be kept in sync with the values in `move.rs` + PIECE_SHIFT = 0 + PIECE_MASK = 0b111 + START_SHIFT = 3 + START_MASK = 0b11_1111 + DESTINATION_SHIFT = 9 + DESTINATION_MASK = 0b11_1111 + CAPTURE_SHIFT = 15 + CAPTURE_MASK = 0b111 + PROMOTION_SHIFT = 18 + PROMOTION_MASK = 0b111 + EN_PASSANT_SHIFT = 21 + EN_PASSANT_MASK = 0b1 + DOUBLE_STEP_SHIFT = 22 + DOUBLE_STEP_MASK = 0b1 + CASTLING_SHIFT = 23 + CASTLING_MASK = 0b1 + + def __init__(self, val): + self._val = val + + @property + def piece(self): + return Piece(self._val >> self.PIECE_SHIFT & self.PIECE_MASK) + + @property + def start(self): + return Square(self._val >> self.START_SHIFT & self.START_MASK) + + @property + def destination(self): + return Square(self._val >> self.DESTINATION_SHIFT & self.DESTINATION_MASK) + + @property + def capture(self): + index = self._val >> self.CAPTURE_SHIFT & self.CAPTURE_MASK + if index == 7: + return None + return Piece(index) + + @property + def promotion(self): + index = self._val >> self.PROMOTION_SHIFT & self.PROMOTION_MASK + if index == 7: + return None + return Piece(index) + + @property + def en_passant(self): + return bool(self._val >> self.EN_PASSANT_SHIFT & self.EN_PASSANT_MASK) + + @property + def double_step(self): + return bool(self._val >> self.DOUBLE_STEP_SHIFT & self.DOUBLE_STEP_MASK) + + @property + def castling(self): + return bool(self._val >> self.CASTLING_SHIFT & self.CASTLING_MASK) + + def __str__(self): + KEYS = [ + "piece", + "start", + "destination", + "capture", + "promotion", + "en_passant", + "double_step", + "castling", + ] + print_opt = lambda val: "(None)" if val is None else str(val) + indent = lambda s: " " + s + + values = [key + ": " + print_opt(getattr(self, key)) + ",\n" for key in KEYS] + return "Move{\n" + "".join(map(indent, values)) + "}" + + class SquarePrinter(object): "Print a seer::board::square::Square" @@ -174,6 +257,16 @@ class PiecePrinter(object): return str(self._val) +class MovePrinter(object): + "Print a seer::board::move::Move" + + def __init__(self, val): + self._val = Move(int(val["__0"])) + + def to_string(self): + return str(self._val) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') @@ -183,6 +276,7 @@ def build_pretty_printer(): pp.add_printer('File', '^seer::board::file::File$', FilePrinter) pp.add_printer('Rank', '^seer::board::rank::Rank$', RankPrinter) pp.add_printer('Piece', '^seer::board::piece::Piece$', ColorPrinter) + pp.add_printer('Move', '^seer::board::move::Move$', MovePrinter) return pp From 43fb428749daf83f4f75b1bca90c3772a73a06b9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:25:35 +0200 Subject: [PATCH 076/230] Add 'FromFen' trait --- src/fen.rs | 6 ++++++ src/lib.rs | 1 + 2 files changed, 7 insertions(+) create mode 100644 src/fen.rs diff --git a/src/fen.rs b/src/fen.rs new file mode 100644 index 0000000..f112bc9 --- /dev/null +++ b/src/fen.rs @@ -0,0 +1,6 @@ +/// A trait to mark items that can be converted from a FEN input. +pub trait FromFen: Sized { + type Err; + + fn from_fen(s: &str) -> Result; +} diff --git a/src/lib.rs b/src/lib.rs index bfcf0bd..82467ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod board; +pub mod fen; pub mod movegen; pub mod utils; From e8a7825215fa93b96f937b8d2abef6814aefe5e4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:24:24 +0200 Subject: [PATCH 077/230] Introduce 'FenError' enum --- src/fen.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/fen.rs b/src/fen.rs index f112bc9..9c406ef 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -4,3 +4,21 @@ pub trait FromFen: Sized { fn from_fen(s: &str) -> Result; } + +/// A singular type for all errors that could happen during FEN parsing. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum FenError { + /// Invalid FEN input. + InvalidFen, +} + +impl std::fmt::Display for FenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let error_msg = match self { + Self::InvalidFen => "Invalid FEN input", + }; + write!(f, "{}", error_msg) + } +} + +impl std::error::Error for FenError {} From 8866c35423e574103f2ee369fecf78338dd608da Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:15 +0200 Subject: [PATCH 078/230] Add FEN side to move parsing --- src/fen.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/fen.rs b/src/fen.rs index 9c406ef..4ee320d 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,3 +1,5 @@ +use crate::board::Color; + /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { type Err; @@ -22,3 +24,17 @@ impl std::fmt::Display for FenError { } impl std::error::Error for FenError {} + +/// Convert a side to move segment of a FEN string to a [Color]. +impl FromFen for Color { + type Err = FenError; + + fn from_fen(s: &str) -> Result { + let res = match s { + "w" => Color::White, + "b" => Color::Black, + _ => return Err(FenError::InvalidFen), + }; + Ok(res) + } +} From 3ab73a08d8bb2efff00511c32785604cf0564020 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:38 +0200 Subject: [PATCH 079/230] Add FEN en-passant target square parsing --- src/fen.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/fen.rs b/src/fen.rs index 4ee320d..4118b59 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,4 @@ -use crate::board::Color; +use crate::board::{Color, File, Rank, Square}; /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { @@ -38,3 +38,20 @@ impl FromFen for Color { Ok(res) } } + +/// Convert an en-passant target square segment of a FEN string to an optional [Square]. +impl FromFen for Option { + type Err = FenError; + + fn from_fen(s: &str) -> Result { + let res = match s.as_bytes() { + [b'-'] => None, + [file @ b'a'..=b'h', rank @ b'1'..=b'8'] => Some(Square::new( + File::from_index((file - b'a') as usize), + Rank::from_index((rank - b'1') as usize), + )), + _ => return Err(FenError::InvalidFen), + }; + Ok(res) + } +} From 4ed310511143ab489e6812075c8e1dce17d72da1 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:40:55 +0200 Subject: [PATCH 080/230] Add FEN piece type parsing --- src/fen.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/fen.rs b/src/fen.rs index 4118b59..f058e84 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,4 @@ -use crate::board::{Color, File, Rank, Square}; +use crate::board::{Color, File, Piece, Rank, Square}; /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { @@ -55,3 +55,21 @@ impl FromFen for Option { Ok(res) } } + +/// Convert a piece in FEN notation to a [Piece]. +impl FromFen for Piece { + type Err = FenError; + + fn from_fen(s: &str) -> Result { + let res = match s { + "p" | "P" => Self::Pawn, + "n" | "N" => Self::Knight, + "b" | "B" => Self::Bishop, + "r" | "R" => Self::Rook, + "q" | "Q" => Self::Queen, + "k" | "K" => Self::King, + _ => return Err(FenError::InvalidFen), + }; + Ok(res) + } +} From bbf5149f7116d7876b3cc3920182d96f3df627a2 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Wed, 27 Jul 2022 23:41:08 +0200 Subject: [PATCH 081/230] Add FEN castling rights parsing --- src/fen.rs | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/fen.rs b/src/fen.rs index f058e84..8273392 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,4 @@ -use crate::board::{Color, File, Piece, Rank, Square}; +use crate::board::{CastleRights, Color, File, Piece, Rank, Square}; /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { @@ -25,6 +25,39 @@ impl std::fmt::Display for FenError { impl std::error::Error for FenError {} +/// Convert the castling rights segment of a FEN string to an array of [CastleRights]. +impl FromFen for [CastleRights; 2] { + type Err = FenError; + + fn from_fen(s: &str) -> Result { + if s.len() > 4 { + return Err(FenError::InvalidFen); + } + + let mut res = [CastleRights::NoSide; 2]; + + if s == "-" { + return Ok(res); + } + + for b in s.chars() { + let color = if b.is_uppercase() { + Color::White + } else { + Color::Black + }; + let rights = &mut res[color.index()]; + match b { + 'k' | 'K' => *rights = rights.with_king_side(), + 'q' | 'Q' => *rights = rights.with_queen_side(), + _ => return Err(FenError::InvalidFen), + } + } + + Ok(res) + } +} + /// Convert a side to move segment of a FEN string to a [Color]. impl FromFen for Color { type Err = FenError; From 5a9b3a7d97658c3699de9ad8f741f0ef13cc5a0a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:19:40 +0200 Subject: [PATCH 082/230] Add 'ChessBoard' --- src/board/chess_board.rs | 98 ++++++++++++++++++++++++++++++++++++++++ src/board/mod.rs | 3 ++ 2 files changed, 101 insertions(+) create mode 100644 src/board/chess_board.rs diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs new file mode 100644 index 0000000..c09d893 --- /dev/null +++ b/src/board/chess_board.rs @@ -0,0 +1,98 @@ +use super::{Bitboard, CastleRights, Color, Piece, Square}; + +/// Represent an on-going chess game. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ChessBoard { + /// A [Bitboard] of occupancy for each piece type, discarding color. Indexed by [Piece::index]. + piece_occupancy: [Bitboard; Piece::NUM_VARIANTS], + /// A [Bitboard] of occupancy for each color, discarding piece type. Indexed by [Piece::index]. + color_occupancy: [Bitboard; Color::NUM_VARIANTS], + /// A [Bitboard] representing all squares currently occupied by a piece. + combined_occupancy: Bitboard, + /// The allowed [CastleRights] for either color. Indexed by [Color::index]. + castle_rights: [CastleRights; Color::NUM_VARIANTS], + /// A potential en-passant attack. + /// Either `None` if no double-step pawn move was made in the previous half-turn, or + /// `Some(target_square)` if a double-step move was made. + en_passant: Option, + /// The number of half-turns without either a pawn push or capture. + half_move_clock: u8, // Should never go higher than 50. + /// The number of half-turns so far. + total_plies: u32, // Should be plenty. + /// The current player turn. + side: Color, +} + +impl ChessBoard { + /// Which player's turn is it. + #[inline(always)] + pub fn current_player(&self) -> Color { + self.side + } + + /// Return the [Square] currently occupied by a pawn that can be captured en-passant, or `None` + #[inline(always)] + pub fn en_passant(&self) -> Option { + self.en_passant + } + + /// Return the [CastleRights] for the given [Color]. + #[inline(always)] + pub fn castle_rights(&self, color: Color) -> CastleRights { + self.castle_rights[color.index()] + } + + /// Return the [CastleRights] for the given [Color]. Allow mutations. + #[inline(always)] + #[allow(unused)] + fn castle_rights_mut(&mut self, color: Color) -> &mut CastleRights { + &mut self.castle_rights[color.index()] + } + + /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. + #[inline(always)] + pub fn piece_occupancy(&self, piece: Piece) -> Bitboard { + self.piece_occupancy[piece.index()] + } + + /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. + /// Allow mutating the state. + #[inline(always)] + #[allow(unused)] + fn piece_occupancy_mut(&mut self, piece: Piece) -> &mut Bitboard { + &mut self.piece_occupancy[piece.index()] + } + + /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece + /// type. + #[inline(always)] + pub fn color_occupancy(&self, color: Color) -> Bitboard { + self.color_occupancy[color.index()] + } + + /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece + /// type. Allow mutating the state. + #[inline(always)] + #[allow(unused)] + fn color_occupancy_mut(&mut self, color: Color) -> &mut Bitboard { + &mut self.color_occupancy[color.index()] + } + + /// Get the [Bitboard] representing all pieces on the board. + #[inline(always)] + pub fn combined_occupancy(&self) -> Bitboard { + self.combined_occupancy + } + + /// Return the number of half-turns without either a pawn push or a capture. + #[inline(always)] + pub fn half_move_clock(&self) -> u8 { + self.half_move_clock + } + + /// Return the total number of plies (i.e: half-turns) played so far. + #[inline(always)] + pub fn total_plies(&self) -> u32 { + self.total_plies + } +} diff --git a/src/board/mod.rs b/src/board/mod.rs index d23ef25..0e34331 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -4,6 +4,9 @@ pub use bitboard::*; pub mod castle_rights; pub use castle_rights::*; +pub mod chess_board; +pub use chess_board::*; + pub mod color; pub use color::*; From 22de0785fa4998cec0a7236864f9d75403bfdd2b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:19:40 +0200 Subject: [PATCH 083/230] Add 'ChessBoard::{,un}do_move' --- src/board/chess_board.rs | 91 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index c09d893..31cd4bf 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,4 +1,4 @@ -use super::{Bitboard, CastleRights, Color, Piece, Square}; +use super::{Bitboard, CastleRights, Color, File, Move, Piece, Square}; /// Represent an on-going chess game. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -23,6 +23,14 @@ pub struct ChessBoard { side: Color, } +/// The state which can't be reversed when doing/un-doing a [Move]. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct NonReversibleState { + castle_rights: [CastleRights; Color::NUM_VARIANTS], + en_passant: Option, + half_move_clock: u8, // Should never go higher than 50. +} + impl ChessBoard { /// Which player's turn is it. #[inline(always)] @@ -44,7 +52,6 @@ impl ChessBoard { /// Return the [CastleRights] for the given [Color]. Allow mutations. #[inline(always)] - #[allow(unused)] fn castle_rights_mut(&mut self, color: Color) -> &mut CastleRights { &mut self.castle_rights[color.index()] } @@ -58,7 +65,6 @@ impl ChessBoard { /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. /// Allow mutating the state. #[inline(always)] - #[allow(unused)] fn piece_occupancy_mut(&mut self, piece: Piece) -> &mut Bitboard { &mut self.piece_occupancy[piece.index()] } @@ -73,7 +79,6 @@ impl ChessBoard { /// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece /// type. Allow mutating the state. #[inline(always)] - #[allow(unused)] fn color_occupancy_mut(&mut self, color: Color) -> &mut Bitboard { &mut self.color_occupancy[color.index()] } @@ -95,4 +100,82 @@ impl ChessBoard { pub fn total_plies(&self) -> u32 { self.total_plies } + + /// Quickly do and undo a move on the [Bitboard]s that are part of the [ChessBoard] state. Does + /// not account for all non-revertible changes such as en-passant state or half-move clock. + #[inline(always)] + fn xor(&mut self, color: Color, piece: Piece, start_end: Bitboard) { + *self.piece_occupancy_mut(piece) ^= start_end; + *self.color_occupancy_mut(color) ^= start_end; + self.combined_occupancy ^= start_end; + } + + /// Play the given [Move], returning all non-revertible state (e.g: en-passant, etc...). + #[inline(always)] + pub fn do_move(&mut self, chess_move: Move) -> NonReversibleState { + // Save non-revertible state + let state = NonReversibleState { + castle_rights: self.castle_rights, + en_passant: self.en_passant, + half_move_clock: self.half_move_clock, + }; + + // Non-revertible state modification + if chess_move.capture().is_some() || chess_move.piece() == Piece::Pawn { + self.half_move_clock = 0; + } else { + self.half_move_clock += 1; + } + if chess_move.is_double_step() { + let target_square = Square::new( + chess_move.destination().file(), + self.current_player().third_rank(), + ); + self.en_passant = Some(target_square); + } else { + self.en_passant = None; + } + if chess_move.is_castling() || chess_move.piece() == Piece::King { + *self.castle_rights_mut(self.current_player()) = CastleRights::NoSide; + } + if chess_move.piece() == Piece::Rook { + let castle_rights = self.castle_rights_mut(self.current_player()); + *castle_rights = match chess_move.start().file() { + File::A => castle_rights.without_queen_side(), + File::H => castle_rights.without_king_side(), + _ => *castle_rights, + } + } + + // Revertible state modification + self.xor( + self.current_player(), + chess_move.piece(), + chess_move.start() | chess_move.destination(), + ); + self.total_plies += 1; + self.side = !self.side; + + state + } + + /// Reverse the effect of playing the given [Move], and return to the given + /// [NonReversibleState]. + #[inline(always)] + pub fn undo_move(&mut self, chess_move: Move, previous: NonReversibleState) { + // Restore non-revertible state + self.castle_rights = previous.castle_rights; + self.en_passant = previous.en_passant; + self.half_move_clock = previous.half_move_clock; + + // Restore revertible state + self.xor( + // The move was applied at the turn *before* the current player + !self.current_player(), + chess_move.piece(), + chess_move.start() | chess_move.destination(), + ); + self.total_plies -= 1; + self.side = !self.side; + } } From 63380cabbc0f9b159c998b813061b9f6b090bf5d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:45:04 +0200 Subject: [PATCH 084/230] Implement 'Default' for 'ChessBoard' --- src/board/chess_board.rs | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 31cd4bf..24b66eb 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,4 +1,4 @@ -use super::{Bitboard, CastleRights, Color, File, Move, Piece, Square}; +use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; /// Represent an on-going chess game. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -179,3 +179,39 @@ impl ChessBoard { self.side = !self.side; } } + +/// Use the starting position as a default value, corresponding to the +/// "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" FEN string +impl Default for ChessBoard { + fn default() -> Self { + Self { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Square::D1 | Square::D8, + // Rook + Square::A1 | Square::A8 | Square::H1 | Square::H8, + // Bishop + Square::C1 | Square::C8 | Square::F1 | Square::F8, + // Knight + Square::B1 | Square::B8 | Square::G1 | Square::G8, + // Pawn + Rank::Second.into_bitboard() | Rank::Seventh.into_bitboard(), + ], + color_occupancy: [ + Rank::First.into_bitboard() | Rank::Second.into_bitboard(), + Rank::Seventh.into_bitboard() | Rank::Eighth.into_bitboard(), + ], + combined_occupancy: Rank::First.into_bitboard() + | Rank::Second.into_bitboard() + | Rank::Seventh.into_bitboard() + | Rank::Eighth.into_bitboard(), + castle_rights: [CastleRights::BothSides; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + } + } +} From 4b2eb4604f3f35f649b50a6ac5e24449d0d44c38 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:17:12 +0200 Subject: [PATCH 085/230] Add 'FenError::InvalidPosition' variant --- src/fen.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/fen.rs b/src/fen.rs index 8273392..d8af180 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -12,12 +12,15 @@ pub trait FromFen: Sized { pub enum FenError { /// Invalid FEN input. InvalidFen, + /// Invalid chess position. + InvalidPosition, } impl std::fmt::Display for FenError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let error_msg = match self { Self::InvalidFen => "Invalid FEN input", + Self::InvalidPosition => "Invalid chess position", }; write!(f, "{}", error_msg) } From 2e1b7329963c4cbc062e9d95570bcc8bf9e7f29f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:37:47 +0200 Subject: [PATCH 086/230] Add 'ChessBoard::is_valid' --- src/board/chess_board.rs | 302 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 24b66eb..095f82e 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -178,6 +178,93 @@ impl ChessBoard { self.total_plies -= 1; self.side = !self.side; } + + /// Return true if the current state of the board looks valid, false if something is definitely + /// wrong. + pub fn is_valid(&self) -> bool { + // Don't overlap pieces. + for piece in Piece::iter() { + #[allow(clippy::collapsible_if)] + for other in Piece::iter() { + if piece != other { + if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() { + return false; + } + } + } + } + + // Don't overlap colors. + if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() { + return false; + } + + // Calculate the union of all pieces. + let combined = + Piece::iter().fold(Bitboard::EMPTY, |board, p| board | self.piece_occupancy(p)); + + // Ensure that the pre-computed version is accurate. + if combined != self.combined_occupancy() { + return false; + } + + // Ensure that all pieces belong to a color, and no color has pieces that don't exist. + if combined != (self.color_occupancy(Color::White) | self.color_occupancy(Color::Black)) { + return false; + } + + // Have exactly one king of each color. + for color in Color::iter() { + if (self.piece_occupancy(Piece::King) & self.color_occupancy(color)).count() != 1 { + return false; + } + } + + // Verify that rooks and kings that are allowed to castle have not been moved. + for color in Color::iter() { + let castle_rights = self.castle_rights(color); + + // Nothing to check if there are no castlings allowed. + if castle_rights == CastleRights::NoSide { + continue; + } + + let actual_rooks = self.piece_occupancy(Piece::Rook) & self.color_occupancy(color); + let expected_rooks = castle_rights.unmoved_rooks(color); + // We must check the intersection, in case there are more than 2 rooks on the board. + if (expected_rooks & actual_rooks) != expected_rooks { + return false; + } + + let actual_king = self.piece_occupancy(Piece::King) & self.color_occupancy(color); + let expected_king = Square::new(File::E, color.first_rank()); + // We have checked that there is exactly one king, no need for intersecting the sets. + if actual_king != expected_king.into_bitboard() { + return false; + } + } + + // The current en-passant target square must be empty, right behind an opponent's pawn. + if let Some(square) = self.en_passant() { + if !(self.combined_occupancy() & square).is_empty() { + return false; + } + let opponent_pawns = + self.piece_occupancy(Piece::Pawn) & self.color_occupancy(!self.current_player()); + let double_pushed_pawn = self + .current_player() + .backward_direction() + .move_board(square.into_bitboard()); + if (opponent_pawns & double_pushed_pawn).is_empty() { + return false; + } + } + + // FIXME: check for opponent being in check. + // FIXME: check for kings touching. + + true + } } /// Use the starting position as a default value, corresponding to the @@ -215,3 +302,218 @@ impl Default for ChessBoard { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn valid() { + let default_position = ChessBoard::default(); + assert!(default_position.is_valid()); + } + + #[test] + fn invalid_overlapping_pieces() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Square::E1 | Square::E8, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_overlapping_colors() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::E8, Square::E1 | Square::E8], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_combined_does_not_equal_pieces() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], + combined_occupancy: Square::E1.into_bitboard(), + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_combined_does_not_equal_colors() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::H1, Square::E8 | Square::H8], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_multiple_kings() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E2 | Square::E7 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::E2, Square::E7 | Square::E8], + combined_occupancy: Square::E1 | Square::E2 | Square::E7 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_castling_rights_no_rooks() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1.into_bitboard(), Square::E8.into_bitboard()], + combined_occupancy: Square::E1 | Square::E8, + castle_rights: [CastleRights::BothSides; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + + #[test] + fn invalid_castling_rights_moved_king() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E2 | Square::E7, + // Queen + Bitboard::EMPTY, + // Rook + Square::A1 | Square::A8 | Square::H1 | Square::H8, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [ + Square::A1 | Square::E2 | Square::H1, + Square::A8 | Square::E7 | Square::H8, + ], + combined_occupancy: Square::A1 + | Square::A8 + | Square::E1 + | Square::E8 + | Square::H1 + | Square::H8, + castle_rights: [CastleRights::BothSides; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } +} From fb78e0365691331e4d557ce1cb982283530cac76 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:38:50 +0200 Subject: [PATCH 087/230] Add FEN board parsing Unfortunately, given that I *don't* want to expose all the `ChessBoard` fields to the rest of the crate, this implementation will have to live alongside its module instead of inside `crate::fen`... --- src/board/chess_board.rs | 169 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 095f82e..a327edc 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,3 +1,5 @@ +use crate::fen::{FenError, FromFen}; + use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; /// Represent an on-going chess game. @@ -303,8 +305,103 @@ impl Default for ChessBoard { } } +/// Return a [ChessBoard] from the given FEN string. +impl FromFen for ChessBoard { + type Err = FenError; + + fn from_fen(s: &str) -> Result { + let mut split = s.split_ascii_whitespace(); + + let piece_placement = split.next().ok_or(FenError::InvalidFen)?; + let side_to_move = split.next().ok_or(FenError::InvalidFen)?; + let castling_rights = split.next().ok_or(FenError::InvalidFen)?; + let en_passant_square = split.next().ok_or(FenError::InvalidFen)?; + let half_move_clock = split.next().ok_or(FenError::InvalidFen)?; + let full_move_counter = split.next().ok_or(FenError::InvalidFen)?; + + let castle_rights = <[CastleRights; 2]>::from_fen(castling_rights)?; + let side = Color::from_fen(side_to_move)?; + let en_passant = Option::::from_fen(en_passant_square)?; + + let half_move_clock = half_move_clock + .parse::() + .map_err(|_| FenError::InvalidFen)?; + let full_move_counter = full_move_counter + .parse::() + .map_err(|_| FenError::InvalidFen)?; + let total_plies = (full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 }; + + let (piece_occupancy, color_occupancy, combined_occupancy) = { + let (mut pieces, mut colors, mut combined) = + ([Bitboard::EMPTY; 6], [Bitboard::EMPTY; 2], Bitboard::EMPTY); + + let mut rank: usize = 8; + for rank_str in piece_placement.split('/') { + rank -= 1; + let mut file: usize = 0; + for c in rank_str.chars() { + let color = if c.is_uppercase() { + Color::White + } else { + Color::Black + }; + let piece = match c { + digit @ '1'..='8' => { + // Unwrap is fine since this arm is only matched by digits + file += digit.to_digit(10).unwrap() as usize; + continue; + } + _ => Piece::from_fen(&c.to_string())?, + }; + let (piece_board, color_board) = + (&mut pieces[piece.index()], &mut colors[color.index()]); + + // Only need to worry about underflow since those are `usize` values. + if file >= 8 || rank >= 8 { + return Err(FenError::InvalidFen); + }; + let square = Square::new(File::from_index(file), Rank::from_index(rank)); + *piece_board |= square; + *color_board |= square; + combined |= square; + file += 1; + } + // We haven't read exactly 8 files. + if file != 8 { + return Err(FenError::InvalidFen); + } + } + // We haven't read exactly 8 ranks + if rank != 0 { + return Err(FenError::InvalidFen); + } + + (pieces, colors, combined) + }; + + let res = Self { + piece_occupancy, + color_occupancy, + combined_occupancy, + castle_rights, + en_passant, + half_move_clock, + total_plies, + side, + }; + + if !res.is_valid() { + return Err(FenError::InvalidPosition); + } + + Ok(res) + } +} + #[cfg(test)] mod test { + use crate::board::MoveBuilder; + use super::*; #[test] @@ -516,4 +613,76 @@ mod test { }; assert!(!position.is_valid()); } + + #[test] + fn fen_default_position() { + let default_position = ChessBoard::default(); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + .unwrap(), + default_position + ); + } + + #[test] + fn fen_en_passant() { + // Start from default position + let mut position = ChessBoard::default(); + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::E2, + destination: Square::E4, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap(), + position + ); + // And now c5 + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::C7, + destination: Square::C5, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap(), + position + ); + // Finally, Nf3 + position.do_move( + MoveBuilder { + piece: Piece::Knight, + start: Square::G1, + destination: Square::F3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(), + ); + assert_eq!( + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") + .unwrap(), + position + ); + } } From 0d8feca6b157cb2e888de54a16487ed2b594a0ec Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Jul 2022 21:39:29 +0200 Subject: [PATCH 088/230] Test 'ChessBoard::{do,undo}_move' machinery --- src/board/chess_board.rs | 127 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index a327edc..af26886 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -685,4 +685,131 @@ mod test { position ); } + + #[test] + fn do_move() { + // Start from default position + let mut position = ChessBoard::default(); + // Modify it to account for e4 move + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::E2, + destination: Square::E4, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap() + ); + // And now c5 + position.do_move( + MoveBuilder { + piece: Piece::Pawn, + start: Square::C7, + destination: Square::C5, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(), + ); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap() + ); + // Finally, Nf3 + position.do_move( + MoveBuilder { + piece: Piece::Knight, + start: Square::G1, + destination: Square::F3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(), + ); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ") + .unwrap() + ); + } + + #[test] + fn do_move_and_undo() { + // Start from default position + let mut position = ChessBoard::default(); + // Modify it to account for e4 move + let move_1 = MoveBuilder { + piece: Piece::Pawn, + start: Square::E2, + destination: Square::E4, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(); + let state_1 = position.do_move(move_1); + // And now c5 + let move_2 = MoveBuilder { + piece: Piece::Pawn, + start: Square::C7, + destination: Square::C5, + capture: None, + promotion: None, + en_passant: false, + double_step: true, + castling: false, + } + .into(); + let state_2 = position.do_move(move_2); + // Finally, Nf3 + let move_3 = MoveBuilder { + piece: Piece::Knight, + start: Square::G1, + destination: Square::F3, + capture: None, + promotion: None, + en_passant: false, + double_step: false, + castling: false, + } + .into(); + let state_3 = position.do_move(move_3); + // Now revert each move one-by-one + position.undo_move(move_3, state_3); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2") + .unwrap() + ); + position.undo_move(move_2, state_2); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1") + .unwrap() + ); + position.undo_move(move_1, state_1); + assert_eq!( + position, + ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + .unwrap() + ); + } } From aaab4397646db5c29bcdf85b43a53071ef777d0d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:26:31 +0200 Subject: [PATCH 089/230] Add 'TryInto' for 'Bitboard' --- src/board/bitboard/error.rs | 19 +++++++++++++++++++ src/board/bitboard/mod.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/board/bitboard/error.rs diff --git a/src/board/bitboard/error.rs b/src/board/bitboard/error.rs new file mode 100644 index 0000000..c631482 --- /dev/null +++ b/src/board/bitboard/error.rs @@ -0,0 +1,19 @@ +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum IntoSquareError { + /// The board is empty. + EmptyBoard, + /// The board contains more than one square. + TooManySquares, +} + +impl std::fmt::Display for IntoSquareError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let error_msg = match self { + Self::EmptyBoard => "The board is empty", + Self::TooManySquares => "The board contains more than one square", + }; + write!(f, "{}", error_msg) + } +} + +impl std::error::Error for IntoSquareError {} diff --git a/src/board/bitboard/mod.rs b/src/board/bitboard/mod.rs index e186a07..b0ec90a 100644 --- a/src/board/bitboard/mod.rs +++ b/src/board/bitboard/mod.rs @@ -1,6 +1,8 @@ use super::Square; use crate::utils::static_assert; +mod error; +use error::*; mod iterator; use iterator::*; mod superset; @@ -102,6 +104,21 @@ impl IntoIterator for Bitboard { } } +/// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is +/// occupying. Otherwise return `None`. +impl TryInto for Bitboard { + type Error = IntoSquareError; + + fn try_into(self) -> Result { + let index = match self.count() { + 1 => self.0.trailing_zeros() as usize, + 0 => return Err(IntoSquareError::EmptyBoard), + _ => return Err(IntoSquareError::TooManySquares), + }; + Ok(Square::from_index(index)) + } +} + /// Treat bitboard as a set of squares, shift each square's index left by the amount given. impl std::ops::Shl for Bitboard { type Output = Bitboard; @@ -461,4 +478,23 @@ mod test { 1 << 8 ); } + + #[test] + fn into_square() { + for square in Square::iter() { + assert_eq!(square.into_bitboard().try_into(), Ok(square)); + } + } + + #[test] + fn into_square_invalid() { + assert_eq!( + TryInto::::try_into(Bitboard::EMPTY), + Err(IntoSquareError::EmptyBoard) + ); + assert_eq!( + TryInto::::try_into(Square::A1 | Square::A2), + Err(IntoSquareError::TooManySquares) + ) + } } From c115e357e10ebf275bf2aff48f11f3275b838717 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 29 Jul 2022 19:27:17 +0200 Subject: [PATCH 090/230] Check kings' position in 'ChessBoard::is_valid' --- src/board/chess_board.rs | 42 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index af26886..9b5427a 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -1,4 +1,7 @@ -use crate::fen::{FenError, FromFen}; +use crate::{ + fen::{FenError, FromFen}, + movegen, +}; use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; @@ -262,8 +265,15 @@ impl ChessBoard { } } + // Check that kings don't touch each other. + let white_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::White); + let black_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::Black); + // Unwrap is fine, we already checked that there is exactly one king of each color + if !(movegen::king_moves(white_king.try_into().unwrap()) & black_king).is_empty() { + return false; + } + // FIXME: check for opponent being in check. - // FIXME: check for kings touching. true } @@ -614,6 +624,34 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn invalid_kings_next_to_each_other() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E2 | Square::E3, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E2.into_bitboard(), Square::E3.into_bitboard()], + combined_occupancy: Square::E2 | Square::E3, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + #[test] fn fen_default_position() { let default_position = ChessBoard::default(); From 23a4e857a0846d19d01c0409ef4a400f97aae4e6 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 01:29:39 +0100 Subject: [PATCH 091/230] Add 'ChessBoard::occupancy' --- src/board/chess_board.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 9b5427a..2f58b31 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -61,6 +61,12 @@ impl ChessBoard { &mut self.castle_rights[color.index()] } + /// Get the [Bitboard] representing all pieces of the given [Piece] and [Color] type. + #[inline(always)] + pub fn occupancy(&self, piece: Piece, color: Color) -> Bitboard { + self.piece_occupancy(piece) & self.color_occupancy(color) + } + /// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color. #[inline(always)] pub fn piece_occupancy(&self, piece: Piece) -> Bitboard { From b75136f7d63f3549eeaf712ae3875177dda5175b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 01:29:39 +0100 Subject: [PATCH 092/230] Use 'ChessBoard::occupancy' --- src/board/chess_board.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 2f58b31..9a50894 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -226,7 +226,7 @@ impl ChessBoard { // Have exactly one king of each color. for color in Color::iter() { - if (self.piece_occupancy(Piece::King) & self.color_occupancy(color)).count() != 1 { + if self.occupancy(Piece::King, color).count() != 1 { return false; } } @@ -240,14 +240,14 @@ impl ChessBoard { continue; } - let actual_rooks = self.piece_occupancy(Piece::Rook) & self.color_occupancy(color); + let actual_rooks = self.occupancy(Piece::Rook, color); let expected_rooks = castle_rights.unmoved_rooks(color); // We must check the intersection, in case there are more than 2 rooks on the board. if (expected_rooks & actual_rooks) != expected_rooks { return false; } - let actual_king = self.piece_occupancy(Piece::King) & self.color_occupancy(color); + let actual_king = self.occupancy(Piece::King, color); let expected_king = Square::new(File::E, color.first_rank()); // We have checked that there is exactly one king, no need for intersecting the sets. if actual_king != expected_king.into_bitboard() { @@ -260,8 +260,7 @@ impl ChessBoard { if !(self.combined_occupancy() & square).is_empty() { return false; } - let opponent_pawns = - self.piece_occupancy(Piece::Pawn) & self.color_occupancy(!self.current_player()); + let opponent_pawns = self.occupancy(Piece::Pawn, !self.current_player()); let double_pushed_pawn = self .current_player() .backward_direction() @@ -272,8 +271,8 @@ impl ChessBoard { } // Check that kings don't touch each other. - let white_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::White); - let black_king = self.piece_occupancy(Piece::King) & self.color_occupancy(Color::Black); + let white_king = self.occupancy(Piece::King, Color::White); + let black_king = self.occupancy(Piece::King, Color::Black); // Unwrap is fine, we already checked that there is exactly one king of each color if !(movegen::king_moves(white_king.try_into().unwrap()) & black_king).is_empty() { return false; From eb53e27f9dfd54f554c9b2210bc78325ca226c22 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 11:58:18 +0200 Subject: [PATCH 093/230] Test for opponent being in check during validation --- src/board/chess_board.rs | 68 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 9a50894..e72abec 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -278,10 +278,48 @@ impl ChessBoard { return false; } - // FIXME: check for opponent being in check. + // Check that the opponent is not currently in check. + if !self.compute_checkers(!self.current_player()).is_empty() { + return false; + } true } + + /// Compute all pieces that are currently threatening the given [Color]'s king. + fn compute_checkers(&self, color: Color) -> Bitboard { + // Unwrap is fine, there should always be exactly one king per color + let king = (self.occupancy(Piece::King, color)).try_into().unwrap(); + + let opponent = !color; + + // No need to remove our pieces from the generated moves, we just want to check if we + // intersect with the opponent's pieces, rather than generate only valid moves. + let bishops = { + let queens = self.occupancy(Piece::Queen, opponent); + let bishops = self.occupancy(Piece::Bishop, opponent); + let bishop_attacks = movegen::bishop_moves(king, self.combined_occupancy()); + (queens | bishops) & bishop_attacks + }; + let rooks = { + let queens = self.occupancy(Piece::Queen, opponent); + let rooks = self.occupancy(Piece::Rook, opponent); + let rook_attacks = movegen::rook_moves(king, self.combined_occupancy()); + (queens | rooks) & rook_attacks + }; + let knights = { + let knights = self.occupancy(Piece::Knight, opponent); + let knight_attacks = movegen::knight_moves(king); + knights & knight_attacks + }; + let pawns = { + let pawns = self.occupancy(Piece::Pawn, opponent); + let pawn_attacks = movegen::pawn_attacks(color, king); + pawns & pawn_attacks + }; + + bishops | rooks | knights | pawns + } } /// Use the starting position as a default value, corresponding to the @@ -657,6 +695,34 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn invalid_opponent_in_check() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E1 | Square::E8, + // Queen + Square::E7.into_bitboard(), + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [Square::E1 | Square::E7, Square::E8.into_bitboard()], + combined_occupancy: Square::E1 | Square::E7 | Square::E8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + #[test] fn fen_default_position() { let default_position = ChessBoard::default(); From 2ab1f363ea7463a75621bf6a6878655df4aecde3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 30 Jul 2022 12:02:41 +0200 Subject: [PATCH 094/230] Add 'ChessBoard::checkers' --- src/board/chess_board.rs | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index e72abec..e08b3ee 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -112,6 +112,13 @@ impl ChessBoard { self.total_plies } + /// Return the [Bitboard] corresponding to all the opponent's pieces threatening the current + /// player's king. + #[inline(always)] + pub fn checkers(&self) -> Bitboard { + self.compute_checkers(self.current_player()) + } + /// Quickly do and undo a move on the [Bitboard]s that are part of the [ChessBoard] state. Does /// not account for all non-revertible changes such as en-passant state or half-move clock. #[inline(always)] @@ -723,6 +730,49 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn checkers() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::E2 | Square::E8, + // Queen + Square::E7 | Square::H2, + // Rook + Square::A2 | Square::E1, + // Bishop + Square::D3 | Square::F3, + // Knight + Square::C1 | Square::G1, + // Pawn + Bitboard::EMPTY, + ], + color_occupancy: [ + Square::C1 | Square::D3 | Square::E1 | Square::E2 | Square::H2, + Square::A2 | Square::E7 | Square::E8 | Square::F3 | Square::G1, + ], + combined_occupancy: Square::A2 + | Square::C1 + | Square::D3 + | Square::E1 + | Square::E2 + | Square::E7 + | Square::E8 + | Square::F3 + | Square::G1 + | Square::H2, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert_eq!( + position.checkers(), + Square::A2 | Square::E7 | Square::F3 | Square::G1 + ); + } + #[test] fn fen_default_position() { let default_position = ChessBoard::default(); From a1065baff5870e654949bf2659fca57f762cc859 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 11:39:29 +0100 Subject: [PATCH 095/230] Pre-generate the magic bitboard seeds My naive RNG implementation takes about ~40 seconds to generate the magic bitboards for both bishops and rooks (or ~1 second in release mode). If we pre-generate the seeds, we can instead make it ~instantaneous. The self-modifying code is inspired by matklad [1]. [1]: https://matklad.github.io/2022/03/26/self-modifying-code.html --- src/movegen/moves.rs | 36 +++--- src/movegen/wizardry/mod.rs | 225 ++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 13 deletions(-) diff --git a/src/movegen/moves.rs b/src/movegen/moves.rs index 7e40a18..9840083 100644 --- a/src/movegen/moves.rs +++ b/src/movegen/moves.rs @@ -4,25 +4,35 @@ use crate::{ board::{Bitboard, Color, File, Square}, movegen::{ naive, - wizardry::{generate_bishop_magics, generate_rook_magics, MagicMoves, RandGen}, + wizardry::{ + generate_bishop_magics, generate_rook_magics, MagicMoves, RandGen, BISHOP_SEED, + ROOK_SEED, + }, }, }; -// A simple XOR-shift RNG implementation. -struct SimpleRng(u64); +// A pre-rolled RNG for magic bitboard generation, using pre-determined values. +struct PreRolledRng { + numbers: [u64; 64], + current_index: usize, +} -impl SimpleRng { - pub fn new() -> Self { - Self(4) // https://xkcd.com/221/ +impl PreRolledRng { + pub fn new(numbers: [u64; 64]) -> Self { + Self { + numbers, + current_index: 0, + } } } -impl RandGen for SimpleRng { +impl RandGen for PreRolledRng { fn gen(&mut self) -> u64 { - self.0 ^= self.0 >> 12; - self.0 ^= self.0 << 25; - self.0 ^= self.0 >> 27; - self.0 + // We roll 3 numbers per square to bitwise-and them together. + // Just return the same one 3 times as a work-around. + let res = self.numbers[self.current_index / 3]; + self.current_index += 1; + res } } @@ -86,7 +96,7 @@ pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { static BISHOP_MAGICS: OnceLock = OnceLock::new(); BISHOP_MAGICS .get_or_init(|| { - let (magics, moves) = generate_bishop_magics(&mut SimpleRng::new()); + let (magics, moves) = generate_bishop_magics(&mut PreRolledRng::new(BISHOP_SEED)); // SAFETY: we used the generator function to compute these values unsafe { MagicMoves::new(magics, moves) } }) @@ -98,7 +108,7 @@ pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { static ROOK_MAGICS: OnceLock = OnceLock::new(); ROOK_MAGICS .get_or_init(|| { - let (magics, moves) = generate_rook_magics(&mut SimpleRng::new()); + let (magics, moves) = generate_rook_magics(&mut PreRolledRng::new(ROOK_SEED)); // SAFETY: we used the generator function to compute these values unsafe { MagicMoves::new(magics, moves) } }) diff --git a/src/movegen/wizardry/mod.rs b/src/movegen/wizardry/mod.rs index 83f4d69..00645d8 100644 --- a/src/movegen/wizardry/mod.rs +++ b/src/movegen/wizardry/mod.rs @@ -56,3 +56,228 @@ impl MagicMoves { } } } + +// region:sourcegen +/// A set of magic numbers for bishop move generation. +pub(crate) const BISHOP_SEED: [u64; 64] = [ + 4908958787341189172, + 1157496606860279808, + 289395876198088778, + 649648646467355137, + 19162426089930848, + 564067194896448, + 18586170375029026, + 9185354800693760, + 72172012436987968, + 317226351607872, + 2597178509285688384, + 1162205282238464, + 144154788211329152, + 172197832046936160, + 4625762105940000802, + 1477217245166903296, + 2251937789583872, + 289373902621379585, + 4616200855845409024, + 2251909637357568, + 3532510975437640064, + 563517968228352, + 562953309660434, + 1196005458310201856, + 2350914225914520576, + 2287018679861376, + 13836188353273790593, + 11267795163676832, + 297519119119499264, + 18588344158519552, + 10453428171813953792, + 72128237668534272, + 1298164929055953920, + 865575144395900952, + 9293076573325312, + 108104018148197376, + 578503662094123152, + 4665870505495102224, + 6066493872259301520, + 285877477613857, + 2328941618281318466, + 721165292771739652, + 4899973577790523400, + 75050392749184, + 2305878200632215680, + 11530099074925593616, + 290561512873919880, + 18652187227888000, + 3379933716168704, + 9223409493537718272, + 22273835729926, + 1152921524003672064, + 4647812741240848385, + 1244225087719112712, + 7367907171013001728, + 9263922034316951570, + 300758214358598160, + 4611686331973636096, + 2377900605806479360, + 6958097192913601024, + 864691130877743617, + 703824948904066, + 612700674899317536, + 180742128018784384, +]; + +/// A set of magic numbers for rook move generation. +pub(crate) const ROOK_SEED: [u64; 64] = [ + 2341871943948451840, + 18015635528220736, + 72066665545773824, + 1188959097794342912, + 12141713393631625314, + 720649693658353672, + 36029896538981888, + 36033359356363520, + 140746619355268, + 1158339898446446661, + 36591886560003650, + 578853633228023808, + 2392554490300416, + 140814806160384, + 180706952366596608, + 10696087878779396, + 1153260703948210820, + 310748649170673678, + 36311372044308544, + 9223444604757615104, + 1267187285230592, + 282574622818306, + 18722484274726152, + 2271591090110593, + 1153063519847989248, + 10168327557107712, + 4507998211276833, + 1153203035420233728, + 4631961017139660032, + 2454499182462107776, + 289367288355753288, + 18015815850820609, + 9268726066908758912, + 11547264697673728000, + 2314929519368081536, + 140943655192577, + 20266215511427202, + 180706969441535248, + 1302683805944911874, + 11534000122299940994, + 22676602724843520, + 4639271120198041668, + 1302104069046927376, + 9184220895313928, + 4612249105954373649, + 562984581726212, + 2312678200579457040, + 4647736876550193157, + 3170604524138139776, + 4684447574787096704, + 20283792725901696, + 1152992019380963840, + 117383863558471808, + 1153488854922068096, + 17596884583424, + 90074759127192064, + 4900502436426416706, + 4573968656793901, + 1161084564408385, + 1657887889314811910, + 4614501455660058690, + 4612530729109422081, + 642458506527236, + 1116704154754, +]; +// endregion:sourcegen + +#[cfg(test)] +mod test { + use super::*; + + // A simple XOR-shift RNG implementation. + struct SimpleRng(u64); + + impl SimpleRng { + pub fn new() -> Self { + Self(4) // https://xkcd.com/221/ + } + } + + impl RandGen for SimpleRng { + fn gen(&mut self) -> u64 { + self.0 ^= self.0 >> 12; + self.0 ^= self.0 << 25; + self.0 ^= self.0 >> 27; + self.0 + } + } + + #[test] + fn rng() { + let mut rng = SimpleRng::new(); + + assert_eq!(rng.gen(), 134217733); + assert_eq!(rng.gen(), 4504699139039237); + assert_eq!(rng.gen(), 13512173405898766); + assert_eq!(rng.gen(), 9225626310854853124); + assert_eq!(rng.gen(), 29836777971867270); + } + + fn split_twice<'a>( + text: &'a str, + start_marker: &str, + end_marker: &str, + ) -> Option<(&'a str, &'a str, &'a str)> { + let (prefix, rest) = text.split_once(start_marker)?; + let (mid, suffix) = rest.split_once(end_marker)?; + Some((prefix, mid, suffix)) + } + + fn array_string(piece_type: &str, values: &[Magic]) -> String { + let mut res = format!( + "/// A set of magic numbers for {} move generation.\n", + piece_type + ); + res.push_str(&format!( + "pub(crate) const {}_SEED: [u64; 64] = [\n", + piece_type.to_uppercase() + )); + for magic in values { + res.push_str(&format!(" {},\n", magic.magic)); + } + res.push_str("];\n"); + res + } + + #[test] + #[ignore = "slow"] + // Regenerates the magic bitboard numbers. + fn regen_magic_seeds() { + // We only care about the magics, the moves can be recomputed at runtime ~cheaply. + let (bishop_magics, _) = generate_bishop_magics(&mut SimpleRng::new()); + let (rook_magics, _) = generate_rook_magics(&mut SimpleRng::new()); + + let original_text = std::fs::read_to_string(file!()).unwrap(); + + let bishop_array = array_string("bishop", &bishop_magics[..]); + let rook_array = array_string("rook", &rook_magics[..]); + + let new_text = { + let start_marker = "// region:sourcegen\n"; + let end_marker = "// endregion:sourcegen\n"; + let (prefix, _, suffix) = + split_twice(&original_text, start_marker, end_marker).unwrap(); + format!("{prefix}{start_marker}{bishop_array}\n{rook_array}{end_marker}{suffix}") + }; + + if new_text != original_text { + std::fs::write(file!(), new_text).unwrap(); + panic!("source was not up-to-date") + } + } +} From 66fff65a55b495247af416e846c4baade833f613 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 12:28:22 +0100 Subject: [PATCH 096/230] Add 'CastleRights' GDB pretty-printing --- utils/gdb/seer_pretty_printers.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 1072cb8..8d983c6 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -46,6 +46,21 @@ class Bitboard(object): n ^= b +class CastleRights(enum.IntEnum): + """ + Python representation of a 'seer::board::castle_rights::CastleRights' raw value. + """ + + # Should be kept in sync with the enum in `color.rs` + NO_SIDE = 0 + KING_SIDE = 1 + QUEEN_SIDE = 2 + BOTH_SIDES = 3 + + def __str__(self): + return self.name.title().replace("_", "") + + class Color(enum.IntEnum): """ Python representation of a 'seer::board::color::Color' raw value. @@ -217,6 +232,16 @@ class BitboardPrinter(object): return "Bitboard{" + str(self._val)[1:-1] + "}" +class CastleRightsPrinter(object): + "Print a seer::board::castle_rights::CastleRights" + + def __init__(self, val): + self._val = CastleRights(int(val)) + + def to_string(self): + return str(self._val) + + class ColorPrinter(object): "Print a seer::board::color::Color" @@ -272,6 +297,7 @@ def build_pretty_printer(): pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter) pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) + pp.add_printer('CastleRights', '^seer::board::castle_rights::CastleRights$', CastleRightsPrinter) pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter) pp.add_printer('File', '^seer::board::file::File$', FilePrinter) pp.add_printer('Rank', '^seer::board::rank::Rank$', RankPrinter) From 905cca4576da29631487643de65feaecc1763e58 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 14:37:37 +0100 Subject: [PATCH 097/230] Add convenience 'Square' constructor in GDB utils --- utils/gdb/seer_pretty_printers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 8d983c6..c80f6a7 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -14,6 +14,10 @@ class Square(object): def __init__(self, val): self._val = val + @classmethod + def from_file_rank(cls, file, rank): + return cls(file * 8 + rank) + def __str__(self): return self.FILES[self.file] + self.RANKS[self.rank] From 9056747448255295a43408d95d030bbeaba93c69 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 14:38:26 +0100 Subject: [PATCH 098/230] Add 'Bitboard.at' in GDB utils --- utils/gdb/seer_pretty_printers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index c80f6a7..56336d6 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -41,6 +41,9 @@ class Bitboard(object): def __str__(self): return "[" + ", ".join(map(str, self.squares)) + "]" + def at(self, square): + return bool(self._val & (1 << square._val)) + @property def squares(self): n = self._val From afa0172f6a879151e9bb5b780da8783835151d02 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 14:39:05 +0100 Subject: [PATCH 099/230] Add 'print-board' GDB command --- utils/gdb/seer_pretty_printers.py | 98 +++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/utils/gdb/seer_pretty_printers.py b/utils/gdb/seer_pretty_printers.py index 56336d6..0481d8d 100644 --- a/utils/gdb/seer_pretty_printers.py +++ b/utils/gdb/seer_pretty_printers.py @@ -1,5 +1,6 @@ import enum +import gdb import gdb.printing @@ -219,6 +220,82 @@ class Move(object): return "Move{\n" + "".join(map(indent, values)) + "}" +class ChessBoard(object): + """ + Wrapper around GDB's representation of a 'seer::board::chess_board::ChessBoard' + in memory. + """ + + def __init__( + self, + piece_occupancy, + color_occupancy, + castle_rights, + half_move_clock, + total_plies, + side, + ): + self._piece_occupancy = list(map(Bitboard, piece_occupancy)) + self._color_occupancy = list(map(Bitboard, color_occupancy)) + self._castle_rights = list(map(CastleRights, castle_rights)) + self._half_move_clock = int(half_move_clock) + self._total_plies = int(total_plies) + self._side = Color(side) + + @classmethod + def from_gdb(cls, val): + return cls( + [int(val["piece_occupancy"][p]["__0"]) for p in Piece], + [int(val["color_occupancy"][c]["__0"]) for c in Color], + [int(val["castle_rights"][c]) for c in Color], + # FIXME: find out how to check for Some/None in val["en_passant"], + int(val["half_move_clock"]), + int(val["total_plies"]), + Color(int(val["side"])), + ) + + def at(self, square): + for piece in Piece: + if not self._piece_occupancy[piece].at(square): + continue + for color in Color: + if not self._color_occupancy[color].at(square): + continue + return (piece, color) + return None + + def pretty_str(self): + def pretty_piece(piece, color): + return [ + ("♚", "♔"), + ("♛", "♕"), + ("♜", "♖"), + ("♝", "♗"), + ("♞", "♘"), + ("♟", "♙"), + ][piece][color] + + board = [ + [self.at(Square.from_file_rank(file, rank)) for file in File] + for rank in Rank + ] + + res = [] + res.append(" A B C D E F G H ") + for n, line in reversed(list(enumerate(board, start=1))): + strings = [str(n) + " "] + strings.extend(" " if p is None else pretty_piece(*p) for p in line) + strings.append(" " + str(n)) + res.append("|".join(strings)) + res.append(" A B C D E F G H ") + res += [ + "Half-move clock: " + str(self._half_move_clock), + "Total plies: " + str(self._total_plies), + "Side to play: " + str(self._side), + ] + return "\n".join(res) + + class SquarePrinter(object): "Print a seer::board::square::Square" @@ -299,6 +376,21 @@ class MovePrinter(object): return str(self._val) +class PrintBoard(gdb.Command): + """ + Pretty-print a 'seer::board::chess_board::ChessBoard' as a 2D textual chess board. + """ + + def __init__(self): + super(PrintBoard, self).__init__( + "print-board", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION + ) + + def invoke(self, arg, from_tty): + board = ChessBoard.from_gdb(gdb.parse_and_eval(arg)) + print(board.pretty_str()) + + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') @@ -313,4 +405,10 @@ def build_pretty_printer(): return pp + +def register_commands(): + PrintBoard() + + gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True) +register_commands() From ea301659c0a72aaec3315fb43ec241db33aa4210 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 12 Aug 2022 16:17:51 +0200 Subject: [PATCH 100/230] Check for invalid pawns in 'ChessBoard::is_valid' --- src/board/chess_board.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index e08b3ee..573ecfa 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -238,6 +238,14 @@ impl ChessBoard { } } + // Check that pawns aren't in first/last rank. + if !(self.piece_occupancy(Piece::Pawn) + & (Rank::First.into_bitboard() | Rank::Eighth.into_bitboard())) + .is_empty() + { + return false; + } + // Verify that rooks and kings that are allowed to castle have not been moved. for color in Color::iter() { let castle_rights = self.castle_rights(color); @@ -730,6 +738,34 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn invalid_pawn_on_first_rank() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::H1 | Square::H8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + Square::A1.into_bitboard(), + ], + color_occupancy: [Square::A1 | Square::H1, Square::H8.into_bitboard()], + combined_occupancy: Square::A1 | Square::H1 | Square::H8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + #[test] fn checkers() { let position = ChessBoard { From 009cb44eab515569744bad37485e6e45ce0ff0c0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Fri, 12 Aug 2022 16:17:03 +0200 Subject: [PATCH 101/230] Check all piece counts in 'ChessBoard::is_valid' --- src/board/chess_board.rs | 57 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/board/chess_board.rs b/src/board/chess_board.rs index 573ecfa..838769a 100644 --- a/src/board/chess_board.rs +++ b/src/board/chess_board.rs @@ -231,9 +231,23 @@ impl ChessBoard { return false; } - // Have exactly one king of each color. for color in Color::iter() { - if self.occupancy(Piece::King, color).count() != 1 { + for piece in Piece::iter() { + // Check that we have the expected number of piecese. + let count = self.occupancy(piece, color).count(); + let possible = match piece { + Piece::King => count == 1, + Piece::Pawn => count <= 8, + Piece::Queen => count <= 9, + _ => count <= 10, + }; + if !possible { + return false; + } + } + + // Check that don't have too many pieces in total + if self.color_occupancy(color).count() > 16 { return false; } } @@ -766,6 +780,45 @@ mod test { assert!(!position.is_valid()); } + #[test] + fn invalid_too_many_pieces() { + let position = ChessBoard { + piece_occupancy: [ + // King + Square::H1 | Square::H8, + // Queen + Bitboard::EMPTY, + // Rook + Bitboard::EMPTY, + // Bishop + Bitboard::EMPTY, + // Knight + Bitboard::EMPTY, + // Pawn + File::B.into_bitboard() + | File::C.into_bitboard() + | File::D.into_bitboard() + | File::E.into_bitboard(), + ], + color_occupancy: [ + File::B.into_bitboard() | File::C.into_bitboard() | Square::H1, + File::D.into_bitboard() | File::E.into_bitboard() | Square::H8, + ], + combined_occupancy: File::B.into_bitboard() + | File::C.into_bitboard() + | File::D.into_bitboard() + | File::E.into_bitboard() + | Square::H1 + | Square::H8, + castle_rights: [CastleRights::NoSide; 2], + en_passant: None, + half_move_clock: 0, + total_plies: 0, + side: Color::White, + }; + assert!(!position.is_valid()); + } + #[test] fn checkers() { let position = ChessBoard { From c3a9a55be1c6c6bf9e5c4303e758afe8a18c4d7c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 19:56:48 +0100 Subject: [PATCH 102/230] Move 'ChessBoard' to its own sub-folder --- src/board/{chess_board.rs => chess_board/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/board/{chess_board.rs => chess_board/mod.rs} (100%) diff --git a/src/board/chess_board.rs b/src/board/chess_board/mod.rs similarity index 100% rename from src/board/chess_board.rs rename to src/board/chess_board/mod.rs From 5ddab6af0eee2039a657d5159b425a564520ce8b Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:21:05 +0100 Subject: [PATCH 103/230] Add 'chess_board::InvalidError' --- src/board/chess_board/error.rs | 50 ++++++++++++++++++++++++++++++++++ src/board/chess_board/mod.rs | 3 ++ 2 files changed, 53 insertions(+) create mode 100644 src/board/chess_board/error.rs diff --git a/src/board/chess_board/error.rs b/src/board/chess_board/error.rs new file mode 100644 index 0000000..e531f54 --- /dev/null +++ b/src/board/chess_board/error.rs @@ -0,0 +1,50 @@ +/// A singular type for all errors that could happen during [ChessBoard::is_valid]. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InvalidError { + /// Too many pieces. + TooManyPieces, + /// Missing king. + MissingKing, + /// Pawns on the first/last rank. + InvalidPawnPosition, + /// Castling rights do not match up with the state of the board. + InvalidCastlingRights, + /// En-passant target square is not empty and behind an opponent's pawn. + InvalidEnPassant, + /// The two kings are next to each other. + NeighbouringKings, + /// The opponent is currently in check. + OpponentInCheck, + /// The piece-specific boards are overlapping. + OverlappingPieces, + /// The color-specific boards are overlapping. + OverlappingColors, + /// The pre-computed combined occupancy boards does not match the other boards. + ErroneousCombinedOccupancy, +} + +impl std::fmt::Display for InvalidError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let error_msg = match self { + Self::TooManyPieces => "Too many pieces.", + Self::MissingKing => "Missing king.", + Self::InvalidPawnPosition => "Pawns on the first/last rank.", + Self::InvalidCastlingRights => { + "Castling rights do not match up with the state of the board." + } + Self::InvalidEnPassant => { + "En-passant target square is not empty and behind an opponent's pawn." + } + Self::NeighbouringKings => "The two kings are next to each other.", + Self::OpponentInCheck => "The opponent is currently in check.", + Self::OverlappingPieces => "The piece-specific boards are overlapping.", + Self::OverlappingColors => "The color-specific boards are overlapping.", + Self::ErroneousCombinedOccupancy => { + "The pre-computed combined occupancy boards does not match the other boards." + } + }; + write!(f, "{}", error_msg) + } +} + +impl std::error::Error for InvalidError {} diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 838769a..17d34e8 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -5,6 +5,9 @@ use crate::{ use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square}; +mod error; +pub use error::*; + /// Represent an on-going chess game. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct ChessBoard { From 4098f188ce77e26fc6e58c73c377cf73f82b27a4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:21:05 +0100 Subject: [PATCH 104/230] Add 'ChessBoard::validate' --- src/board/chess_board/mod.rs | 40 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 17d34e8..d51eabb 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -203,13 +203,18 @@ impl ChessBoard { /// Return true if the current state of the board looks valid, false if something is definitely /// wrong. pub fn is_valid(&self) -> bool { + self.validate().is_ok() + } + + /// Validate the state of the board. Return Err([InvalidError]) if an issue is found. + pub fn validate(&self) -> Result<(), InvalidError> { // Don't overlap pieces. for piece in Piece::iter() { #[allow(clippy::collapsible_if)] for other in Piece::iter() { if piece != other { if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() { - return false; + return Err(InvalidError::OverlappingPieces); } } } @@ -217,7 +222,7 @@ impl ChessBoard { // Don't overlap colors. if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() { - return false; + return Err(InvalidError::OverlappingColors); } // Calculate the union of all pieces. @@ -226,12 +231,12 @@ impl ChessBoard { // Ensure that the pre-computed version is accurate. if combined != self.combined_occupancy() { - return false; + return Err(InvalidError::ErroneousCombinedOccupancy); } // Ensure that all pieces belong to a color, and no color has pieces that don't exist. if combined != (self.color_occupancy(Color::White) | self.color_occupancy(Color::Black)) { - return false; + return Err(InvalidError::ErroneousCombinedOccupancy); } for color in Color::iter() { @@ -239,19 +244,24 @@ impl ChessBoard { // Check that we have the expected number of piecese. let count = self.occupancy(piece, color).count(); let possible = match piece { - Piece::King => count == 1, + Piece::King => count <= 1, Piece::Pawn => count <= 8, Piece::Queen => count <= 9, _ => count <= 10, }; if !possible { - return false; + return Err(InvalidError::TooManyPieces); } } + // Check that we have a king + if self.occupancy(Piece::King, color).count() != 1 { + return Err(InvalidError::MissingKing); + } + // Check that don't have too many pieces in total if self.color_occupancy(color).count() > 16 { - return false; + return Err(InvalidError::TooManyPieces); } } @@ -260,7 +270,7 @@ impl ChessBoard { & (Rank::First.into_bitboard() | Rank::Eighth.into_bitboard())) .is_empty() { - return false; + return Err(InvalidError::InvalidPawnPosition); } // Verify that rooks and kings that are allowed to castle have not been moved. @@ -276,21 +286,21 @@ impl ChessBoard { let expected_rooks = castle_rights.unmoved_rooks(color); // We must check the intersection, in case there are more than 2 rooks on the board. if (expected_rooks & actual_rooks) != expected_rooks { - return false; + return Err(InvalidError::InvalidCastlingRights); } let actual_king = self.occupancy(Piece::King, color); let expected_king = Square::new(File::E, color.first_rank()); // We have checked that there is exactly one king, no need for intersecting the sets. if actual_king != expected_king.into_bitboard() { - return false; + return Err(InvalidError::InvalidCastlingRights); } } // The current en-passant target square must be empty, right behind an opponent's pawn. if let Some(square) = self.en_passant() { if !(self.combined_occupancy() & square).is_empty() { - return false; + return Err(InvalidError::InvalidEnPassant); } let opponent_pawns = self.occupancy(Piece::Pawn, !self.current_player()); let double_pushed_pawn = self @@ -298,7 +308,7 @@ impl ChessBoard { .backward_direction() .move_board(square.into_bitboard()); if (opponent_pawns & double_pushed_pawn).is_empty() { - return false; + return Err(InvalidError::InvalidEnPassant); } } @@ -307,15 +317,15 @@ impl ChessBoard { let black_king = self.occupancy(Piece::King, Color::Black); // Unwrap is fine, we already checked that there is exactly one king of each color if !(movegen::king_moves(white_king.try_into().unwrap()) & black_king).is_empty() { - return false; + return Err(InvalidError::NeighbouringKings); } // Check that the opponent is not currently in check. if !self.compute_checkers(!self.current_player()).is_empty() { - return false; + return Err(InvalidError::OpponentInCheck); } - true + Ok(()) } /// Compute all pieces that are currently threatening the given [Color]'s king. From 90a9ac0f7a3f66c444b97437e9f4573ff78ccf2a Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:28:16 +0100 Subject: [PATCH 105/230] Add validation error detail in 'FenError' --- src/board/chess_board/mod.rs | 4 ++-- src/fen.rs | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index d51eabb..d1d3751 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -485,8 +485,8 @@ impl FromFen for ChessBoard { side, }; - if !res.is_valid() { - return Err(FenError::InvalidPosition); + if let Err(err) = res.validate() { + return Err(FenError::InvalidPosition(err)); } Ok(res) diff --git a/src/fen.rs b/src/fen.rs index d8af180..3034003 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -1,4 +1,4 @@ -use crate::board::{CastleRights, Color, File, Piece, Rank, Square}; +use crate::board::{CastleRights, Color, File, InvalidError, Piece, Rank, Square}; /// A trait to mark items that can be converted from a FEN input. pub trait FromFen: Sized { @@ -13,16 +13,15 @@ pub enum FenError { /// Invalid FEN input. InvalidFen, /// Invalid chess position. - InvalidPosition, + InvalidPosition(InvalidError), } impl std::fmt::Display for FenError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let error_msg = match self { - Self::InvalidFen => "Invalid FEN input", - Self::InvalidPosition => "Invalid chess position", - }; - write!(f, "{}", error_msg) + match self { + Self::InvalidFen => write!(f, "Invalid FEN input"), + Self::InvalidPosition(err) => write!(f, "Invalid chess position: {}", err), + } } } From 4b13bd9a0bfa0b6005345c647d9c20b2ecbc41f0 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:41:54 +0100 Subject: [PATCH 106/230] Fix 'ChessBoard' validation test It wasn't actually testing the right thing due to the typo... --- src/board/chess_board/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index d1d3751..3a53336 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -696,8 +696,8 @@ mod test { ], combined_occupancy: Square::A1 | Square::A8 - | Square::E1 - | Square::E8 + | Square::E2 + | Square::E7 | Square::H1 | Square::H8, castle_rights: [CastleRights::BothSides; 2], From bce690d363c1ba47e3b4a093068d9d2aa43ae2a8 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 20:42:28 +0100 Subject: [PATCH 107/230] Use 'ChessBoard::validate' in tests This makes the test more explicit and exact in what they're testing. --- src/board/chess_board/mod.rs | 55 ++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/board/chess_board/mod.rs b/src/board/chess_board/mod.rs index 3a53336..1f0b293 100644 --- a/src/board/chess_board/mod.rs +++ b/src/board/chess_board/mod.rs @@ -530,7 +530,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::OverlappingPieces, + ); } #[test] @@ -558,7 +561,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::OverlappingColors, + ); } #[test] @@ -586,7 +592,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::ErroneousCombinedOccupancy, + ); } #[test] @@ -614,7 +623,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::ErroneousCombinedOccupancy, + ); } #[test] @@ -642,7 +654,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::TooManyPieces, + ); } #[test] @@ -670,7 +685,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::InvalidCastlingRights, + ); } #[test] @@ -706,7 +724,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::InvalidCastlingRights, + ); } #[test] @@ -734,7 +755,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::NeighbouringKings, + ); } #[test] @@ -762,7 +786,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::OpponentInCheck, + ); } #[test] @@ -790,7 +817,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::InvalidPawnPosition, + ); } #[test] @@ -829,7 +859,10 @@ mod test { total_plies: 0, side: Color::White, }; - assert!(!position.is_valid()); + assert_eq!( + position.validate().err().unwrap(), + InvalidError::TooManyPieces, + ); } #[test] From 8962fd90f4ac740b3547db4873c320d3c53c1519 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:19:55 +0100 Subject: [PATCH 108/230] 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 cc520ea413eb321ab29d38293d98e7226acbdc1f Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:20:20 +0100 Subject: [PATCH 109/230] 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..e0e9780 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()) { + builder[square] = Some((Piece::Pawn, Color::White)); + } + for square in (File::F.into_bitboard() | File::G.into_bitboard()) { + builder[square] = Some((Piece::Pawn, Color::Black)); + } + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::TooManyPieces, - ); + assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); } #[test] From 5126d8ad766499c6e52950bd5878897c722afab9 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:30:52 +0100 Subject: [PATCH 110/230] 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 58fe6df32d5fa25783831366aa6902afa054700d Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:32:02 +0100 Subject: [PATCH 111/230] 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 e0e9780..2a48538 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 be79b99512984aa232fd8273c5382082140d8180 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:34:01 +0100 Subject: [PATCH 112/230] 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 2a48538..c5105ab 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 555fdd08918579ee6ec61c19aab9c67e21bd43a3 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:35:48 +0100 Subject: [PATCH 113/230] 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 c5105ab..16e402d 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 115eb0e82655654880f8624058b2114d938916fc Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:54:32 +0100 Subject: [PATCH 114/230] 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 16e402d..e4cae1f 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 9ccd67ec69611383bc204e69342c61f38e312804 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:56:01 +0100 Subject: [PATCH 115/230] 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 e4cae1f..2a98537 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 3677040e0318be455750949c7c9b1939abb81698 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Thu, 28 Mar 2024 20:30:43 +0000 Subject: [PATCH 116/230] 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 117/230] 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 118/230] 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 119/230] 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 120/230] 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 121/230] 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 122/230] 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 123/230] 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 124/230] 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 125/230] 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 126/230] 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 127/230] 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 128/230] 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 129/230] 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 130/230] 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 131/230] 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 132/230] 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 133/230] 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 134/230] 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 135/230] 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 136/230] 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 137/230] 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 138/230] 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 139/230] 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 140/230] 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 141/230] 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 142/230] 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 143/230] 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 144/230] 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 145/230] 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 146/230] 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 147/230] 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 148/230] 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 149/230] 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 150/230] 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 151/230] 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 152/230] 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 153/230] 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 154/230] 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 155/230] 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 156/230] 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 157/230] 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 158/230] 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 159/230] 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 160/230] 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 161/230] 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 162/230] 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 163/230] 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 164/230] 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 165/230] 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 166/230] 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 167/230] 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 168/230] 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 169/230] 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 170/230] 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 171/230] 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 172/230] 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 173/230] 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 174/230] 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 175/230] 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 176/230] 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 177/230] 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 178/230] 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 179/230] 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 180/230] 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 181/230] 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 182/230] 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 183/230] 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 184/230] 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 185/230] 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 186/230] 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 187/230] 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 188/230] 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 189/230] 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 190/230] 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 191/230] 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 192/230] 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 193/230] 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 194/230] 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 195/230] 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 196/230] 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 197/230] 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 198/230] 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 199/230] 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 200/230] 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 201/230] 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 202/230] 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 203/230] 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 204/230] 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 205/230] 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 206/230] 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 207/230] 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 208/230] 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 209/230] 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 210/230] 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 211/230] 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 212/230] 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 213/230] 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 214/230] 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 215/230] 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 216/230] 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 217/230] 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 218/230] 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 219/230] 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 220/230] 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 221/230] 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 222/230] 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 223/230] 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 dfdc11b1fc7f81433ca6663e2113abf5871f53a4 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:20:20 +0100 Subject: [PATCH 224/230] 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..e0e9780 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()) { + builder[square] = Some((Piece::Pawn, Color::White)); + } + for square in (File::F.into_bitboard() | File::G.into_bitboard()) { + builder[square] = Some((Piece::Pawn, Color::Black)); + } + TryInto::::try_into(builder) }; - assert_eq!( - position.validate().err().unwrap(), - InvalidError::TooManyPieces, - ); + assert_eq!(res.err().unwrap(), InvalidError::TooManyPieces); } #[test] From 5611a59449e146cbe70c8e2e38dd2852b92532de Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:30:52 +0100 Subject: [PATCH 225/230] 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 8e0eabe1871d37b6c3dbefe5fc3243bc3655027e Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:32:02 +0100 Subject: [PATCH 226/230] 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 e0e9780..2a48538 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 dbde58987c11b1a89cd8ed19065eb141ae44a12c Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:34:01 +0100 Subject: [PATCH 227/230] 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 2a48538..c5105ab 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 92a69ee74196894adc45c01fe045d3f622938390 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:35:48 +0100 Subject: [PATCH 228/230] 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 c5105ab..16e402d 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 385629b3a9f1485bddaa2d7d9d1123174d7c0b34 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:54:32 +0100 Subject: [PATCH 229/230] 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 16e402d..e4cae1f 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 1f52bb93465f27ab4fbc991c5aee76e97023c115 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Mon, 1 Apr 2024 21:56:01 +0100 Subject: [PATCH 230/230] 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 e4cae1f..2a98537 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 = {