Compare commits

...

No commits in common. "main" and "add-movegen" have entirely different histories.

47 changed files with 1989 additions and 2538 deletions

View file

@ -1,19 +1,24 @@
labels: ---
backend: local kind: pipeline
type: exec
name: abacus checks
steps: steps:
- name: pre-commit check - name: pre commit check
image: bash
commands: commands:
- nix develop --command pre-commit run --all - nix develop . --command pre-commit run --all
- name: nix flake check - name: flake check
image: bash
commands: commands:
- nix flake check - nix flake check
- name: package check
commands:
- nix build
- name: notifiy - name: notifiy
image: bash commands:
- nix run github:ambroisie/matrix-notifier
environment: environment:
ADDRESS: ADDRESS:
from_secret: matrix_homeserver from_secret: matrix_homeserver
@ -23,9 +28,8 @@ steps:
from_secret: matrix_username from_secret: matrix_username
PASS: PASS:
from_secret: matrix_password from_secret: matrix_password
commands:
- nix run github:ambroisie/matrix-notifier
when: when:
status: status:
- failure - failure
- success - success
...

5
.envrc
View file

@ -1,5 +0,0 @@
if ! has nix_direnv_version || ! nix_direnv_version 3.0.0; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.0/direnvrc" "sha256-21TMnI2xWX7HkSTjFFri2UaohXVj854mgvWapWrxRXg="
fi
use flake

10
.gitignore vendored
View file

@ -1,6 +1,6 @@
# Rust build directory # Nix files
/target
# Nix generated files
/.pre-commit-config.yaml
/result /result
/.pre-commit-config.yaml
# Rust files
/target

9
Cargo.lock generated
View file

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "random"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d13a3485349981c90c79112a11222c3e6e75de1d52b87a7525b3bf5361420f"
[[package]] [[package]]
name = "seer" name = "seer"
version = "0.1.0" version = "0.1.0"
dependencies = [
"random",
]

View file

@ -2,7 +2,19 @@
name = "seer" name = "seer"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
build = "src/build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
random = "0.12.2"
[build-dependencies]
random = "0.12.2"
# Optimize build scripts to shorten compile times.
[profile.dev.build-override]
opt-level = 3
[profile.release.build-override]
opt-level = 3

View file

@ -1,111 +1,73 @@
{ {
"nodes": { "nodes": {
"flake-compat": { "flake-utils": {
"flake": false,
"locked": { "locked": {
"lastModified": 1696426674, "lastModified": 1656928814,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"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", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "numtide",
"ref": "main", "ref": "master",
"repo": "flake-utils", "repo": "flake-utils",
"type": "github" "type": "github"
} }
}, },
"gitignore": { "naersk": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"pre-commit-hooks",
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1709087332, "lastModified": 1655042882,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", "narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=",
"owner": "hercules-ci", "owner": "nix-community",
"repo": "gitignore.nix", "repo": "naersk",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394", "rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "hercules-ci", "owner": "nix-community",
"repo": "gitignore.nix", "ref": "master",
"repo": "naersk",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1711523803, "lastModified": 1657888067,
"narHash": "sha256-UKcYiHWHQynzj6CN/vTcix4yd1eCu1uFdsuarupdCQQ=", "narHash": "sha256-GnwJoFBTPfW3+mz7QEeJEEQ9OMHZOiIJ/qDhZxrlKh8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2726f127c15a4cc9810843b96cad73c7eb39e443", "rev": "65fae659e31098ca4ac825a6fef26d890aaf3f4e",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-unstable", "ref": "nixpkgs-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", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"pre-commit-hooks": { "pre-commit-hooks": {
"inputs": { "inputs": {
"flake-compat": "flake-compat",
"flake-utils": [ "flake-utils": [
"futils" "flake-utils"
], ],
"gitignore": "gitignore",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
], ]
"nixpkgs-stable": "nixpkgs-stable"
}, },
"locked": { "locked": {
"lastModified": 1711519547, "lastModified": 1656169028,
"narHash": "sha256-Q7YmSCUJmDl71fJv/zD9lrOCJ1/SE/okZ2DsrmRjzhY=", "narHash": "sha256-y9DRauokIeVHM7d29lwT8A+0YoGUBXV3H0VErxQeA8s=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "7d47a32e5cd1ea481fab33c516356ce27c8cef4a", "rev": "db3bd555d3a3ceab208bed48f983ccaa6a71a25e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -117,23 +79,34 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"futils": "futils", "flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks" "pre-commit-hooks": "pre-commit-hooks",
"rust-overlay": "rust-overlay"
} }
}, },
"systems": { "rust-overlay": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1657853760,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "narHash": "sha256-X6ERAyUXGsrhbhgkxNaQl40wcus5uyQZOCxUh5neK+g=",
"owner": "nix-systems", "owner": "oxalica",
"repo": "default", "repo": "rust-overlay",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "rev": "a97a761cc11327bb109dc30af1c637b986be7959",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-systems", "owner": "oxalica",
"repo": "default", "ref": "master",
"repo": "rust-overlay",
"type": "github" "type": "github"
} }
} }

160
flake.nix
View file

@ -1,19 +1,29 @@
{ {
description = "A chess engine"; description = "A handy file picker program";
inputs = { inputs = {
futils = { flake-utils = {
type = "github"; type = "github";
owner = "numtide"; owner = "numtide";
repo = "flake-utils"; repo = "flake-utils";
ref = "main"; ref = "master";
};
naersk = {
type = "github";
owner = "nix-community";
repo = "naersk";
ref = "master";
inputs = {
nixpkgs.follows = "nixpkgs";
};
}; };
nixpkgs = { nixpkgs = {
type = "github"; type = "github";
owner = "NixOS"; owner = "NixOS";
repo = "nixpkgs"; repo = "nixpkgs";
ref = "nixos-unstable"; ref = "nixpkgs-unstable";
}; };
pre-commit-hooks = { pre-commit-hooks = {
@ -22,53 +32,76 @@
repo = "pre-commit-hooks.nix"; repo = "pre-commit-hooks.nix";
ref = "master"; ref = "master";
inputs = { inputs = {
flake-utils.follows = "futils"; flake-utils.follows = "flake-utils";
nixpkgs.follows = "nixpkgs";
};
};
rust-overlay = {
type = "github";
owner = "oxalica";
repo = "rust-overlay";
ref = "master";
inputs = {
flake-utils.follows = "flake-utils";
nixpkgs.follows = "nixpkgs"; nixpkgs.follows = "nixpkgs";
}; };
}; };
}; };
outputs = { self, futils, nixpkgs, pre-commit-hooks }: outputs =
{ { self
overlays = { , flake-utils
default = final: _prev: { , naersk
seer = with final; rustPlatform.buildRustPackage { , nixpkgs
pname = "seer"; , pre-commit-hooks
version = (final.lib.importTOML ./Cargo.toml).package.version; , rust-overlay
}:
let
inherit (flake-utils.lib) eachSystem system;
src = self; mySystems = [
system.aarch64-linux
system.x86_64-darwin
system.x86_64-linux
];
cargoLock = { eachMySystem = eachSystem mySystems;
lockFile = "${self}/Cargo.lock"; in
}; eachMySystem (system:
let
meta = with lib; { overlays = [ (import rust-overlay) ];
description = "A chess engine"; pkgs = import nixpkgs { inherit overlays system; };
homepage = "https://git.belanyi.fr/ambroisie/seer"; my-rust = pkgs.rust-bin.stable.latest.default.override {
license = licenses.mit; extensions = [ "rust-src" ];
maintainers = with maintainers; [ ambroisie ];
};
};
};
}; };
} // futils.lib.eachDefaultSystem (system: naersk-lib = naersk.lib."${system}".override {
let cargo = my-rust;
pkgs = import nixpkgs { rustc = my-rust;
inherit system; };
overlays = [ inherit (pkgs) lib;
self.overlays.default pre-commit =
]; let
}; # See https://github.com/cachix/pre-commit-hooks.nix/issues/126
rust-env = pkgs.buildEnv {
pre-commit = pre-commit-hooks.lib.${system}.run { name = "rust-env";
buildInputs = [ pkgs.makeWrapper ];
paths = [ my-rust ];
pathsToLink = [ "/" "/bin" ];
postBuild = ''
for i in $out/bin/*; do
wrapProgram "$i" --prefix PATH : "$out/bin"
done
'';
};
in
pre-commit-hooks.lib.${system}.run {
src = self; src = self;
hooks = { hooks = {
clippy = { clippy = {
enable = true; enable = true;
settings = { entry = lib.mkForce "${rust-env}/bin/cargo-clippy clippy";
denyWarnings = true;
};
}; };
nixpkgs-fmt = { nixpkgs-fmt = {
@ -77,36 +110,43 @@
rustfmt = { rustfmt = {
enable = true; enable = true;
entry = lib.mkForce "${rust-env}/bin/cargo-fmt fmt -- --check --color always";
}; };
}; };
}; };
in in
{ rec {
checks = {
inherit (self.packages.${system}) seer; devShells = {
default = pkgs.mkShell {
inputsFrom = [
packages.seer
];
nativeBuildInputs = with pkgs; [
rust-analyzer
# Clippy, rustfmt, etc...
my-rust
];
inherit (pre-commit) shellHook;
RUST_SRC_PATH = "${my-rust}/lib/rustlib/src/rust/library";
}; };
};
devShells = { packages = {
default = pkgs.mkShell { default = self.packages."${system}".seer;
inputsFrom = with self.packages.${system}; [
seer
];
packages = with pkgs; [ seer = naersk-lib.buildPackage {
clippy src = self;
rust-analyzer
rustfmt
];
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; doCheck = true;
inherit (pre-commit) shellHook; passthru = {
inherit my-rust;
}; };
}; };
};
packages = futils.lib.flattenTree { });
default = pkgs.seer;
inherit (pkgs) seer;
};
});
} }

View file

View file

@ -1,19 +0,0 @@
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum IntoSquareError {
/// The board is empty.
EmptyBoard,
/// The board contains more than one square.
TooManySquares,
}
impl std::fmt::Display for IntoSquareError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let error_msg = match self {
Self::EmptyBoard => "The board is empty",
Self::TooManySquares => "The board contains more than one square",
};
write!(f, "{}", error_msg)
}
}
impl std::error::Error for IntoSquareError {}

View file

@ -1,28 +1,23 @@
/// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a /// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a
/// [Bitboard]. /// [Bitboard](crate::board::Bitboard).
use crate::board::Bitboard; pub struct BitboardIterator(pub(crate) u64);
pub struct BitboardIterator(Bitboard);
impl BitboardIterator {
pub fn new(board: Bitboard) -> Self {
Self(board)
}
}
impl Iterator for BitboardIterator { impl Iterator for BitboardIterator {
type Item = crate::board::Square; type Item = crate::board::Square;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let res = self.0.any_square(); if self.0 == 0 {
if let Some(square) = res { None
self.0 ^= square; } else {
}; let lsb = self.0.trailing_zeros() as usize;
res self.0 ^= 1 << lsb;
// SAFETY: we know the value is in-bounds
Some(unsafe { crate::board::Square::from_index_unchecked(lsb) })
}
} }
fn size_hint(&self) -> (usize, Option<usize>) { fn size_hint(&self) -> (usize, Option<usize>) {
let size = self.0.count() as usize; let size = self.0.count_ones() as usize;
(size, Some(size)) (size, Some(size))
} }

View file

@ -1,8 +1,6 @@
use super::{File, Rank, Square}; use super::Square;
use crate::utils::static_assert; use crate::utils::static_assert;
mod error;
use error::*;
mod iterator; mod iterator;
use iterator::*; use iterator::*;
mod superset; mod superset;
@ -21,7 +19,8 @@ impl Bitboard {
pub const ALL: Bitboard = Bitboard(u64::MAX); pub const ALL: Bitboard = Bitboard(u64::MAX);
/// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8. /// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8.
pub const RANKS: [Self; Rank::NUM_VARIANTS] = [ #[allow(clippy::unusual_byte_groupings)]
pub const RANKS: [Self; 8] = [
Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001), Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001),
Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010), Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010),
Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100), Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100),
@ -33,7 +32,8 @@ impl Bitboard {
]; ];
/// Array of bitboards representing the eight files, in order from file A to file H. /// Array of bitboards representing the eight files, in order from file A to file H.
pub const FILES: [Self; File::NUM_VARIANTS] = [ #[allow(clippy::unusual_byte_groupings)]
pub const FILES: [Self; 8] = [
Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111), Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111),
Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000), Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000),
Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000), Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000),
@ -75,12 +75,6 @@ impl Bitboard {
(self.0 & (self.0.wrapping_sub(1))) != 0 (self.0 & (self.0.wrapping_sub(1))) != 0
} }
/// Return a [Square] from the board, or `None` if it is empty.
#[inline(always)]
pub fn any_square(self) -> Option<Square> {
Square::try_from_index(self.0.trailing_zeros() as usize)
}
/// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of /// 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 /// [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]. /// contain all, some, or none of the [Square] that are in the given [Bitboard].
@ -89,6 +83,18 @@ impl Bitboard {
pub fn iter_power_set(self) -> impl Iterator<Item = Self> { pub fn iter_power_set(self) -> impl Iterator<Item = Self> {
BitboardPowerSetIterator::new(self) BitboardPowerSetIterator::new(self)
} }
/// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is
/// occupying. Otherwise return `None`.
pub fn try_into_square(self) -> Option<Square> {
if self.count() != 1 {
None
} else {
let index = self.0.trailing_zeros() as usize;
// SAFETY: we know the value is in-bounds
Some(unsafe { Square::from_index_unchecked(index) })
}
}
} }
// Ensure zero-cost (at least size-wise) wrapping. // Ensure zero-cost (at least size-wise) wrapping.
@ -106,20 +112,7 @@ impl IntoIterator for Bitboard {
type Item = Square; type Item = Square;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
BitboardIterator::new(self) BitboardIterator(self.0)
}
}
/// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is
/// occupying. Otherwise return `None`.
impl TryInto<Square> for Bitboard {
type Error = IntoSquareError;
fn try_into(self) -> Result<Square, Self::Error> {
if self.has_more_than_one() {
return Err(IntoSquareError::TooManySquares);
}
self.any_square().ok_or(IntoSquareError::EmptyBoard)
} }
} }
@ -455,11 +448,15 @@ mod test {
#[test] #[test]
fn iter_power_set_six_squares_exhaustive() { fn iter_power_set_six_squares_exhaustive() {
let mask = (0..6) let mask = (0..6)
.into_iter()
.map(Square::from_index) .map(Square::from_index)
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs); .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs);
assert_eq!( assert_eq!(
mask.iter_power_set().collect::<HashSet<_>>(), mask.iter_power_set().collect::<HashSet<_>>(),
(0..(1 << 6)).map(Bitboard).collect::<HashSet<_>>() (0..(1 << 6))
.into_iter()
.map(Bitboard)
.collect::<HashSet<_>>()
) )
} }
@ -483,39 +480,16 @@ mod test {
); );
} }
#[test]
fn any_square() {
for square in Square::iter() {
assert_eq!(square.into_bitboard().any_square(), Some(square));
}
}
#[test]
fn any_square_empty() {
assert!(Bitboard::EMPTY.any_square().is_none());
}
#[test]
fn any_square_full_board() {
assert!(Bitboard::ALL.any_square().is_some());
}
#[test] #[test]
fn into_square() { fn into_square() {
for square in Square::iter() { for square in Square::iter() {
assert_eq!(square.into_bitboard().try_into(), Ok(square)); assert_eq!(square.into_bitboard().try_into_square(), Some(square));
} }
} }
#[test] #[test]
fn into_square_invalid() { fn into_square_invalid() {
assert_eq!( assert!(Bitboard::EMPTY.try_into_square().is_none());
TryInto::<Square>::try_into(Bitboard::EMPTY), assert!((Square::A1 | Square::A2).try_into_square().is_none())
Err(IntoSquareError::EmptyBoard)
);
assert_eq!(
TryInto::<Square>::try_into(Square::A1 | Square::A2),
Err(IntoSquareError::TooManySquares)
)
} }
} }

View file

@ -4,20 +4,21 @@ use super::Bitboard;
/// In other words, for each square that belongs to the mask, this will yield all sets that do /// 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. /// contain the square, and all sets that do not.
pub struct BitboardPowerSetIterator { pub struct BitboardPowerSetIterator {
/// The starting board. /// The mask.
board: Bitboard, mask: Bitboard,
/// The next subset. /// The "index" of the next blocker set that should be generated.
subset: Bitboard, current: usize,
/// Whether or not iteration is done. /// The number of blocker sets that should be generated by [BlockerIterator], i.e: 2^n with n
done: bool, /// the number of squares belonging to `mask`.
total: usize,
} }
impl BitboardPowerSetIterator { impl BitboardPowerSetIterator {
pub fn new(board: Bitboard) -> Self { pub fn new(mask: Bitboard) -> Self {
Self { Self {
board, mask,
subset: Bitboard::EMPTY, current: 0,
done: false, total: 1 << mask.count(),
} }
} }
} }
@ -26,18 +27,22 @@ impl Iterator for BitboardPowerSetIterator {
type Item = Bitboard; type Item = Bitboard;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.done { if self.current >= self.total {
return None; None
} else {
let blockers = (0..)
.into_iter()
.zip(self.mask.into_iter())
.filter(|(index, _)| self.current & (1 << index) != 0)
.map(|(_, board)| board)
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs);
self.current += 1;
Some(blockers)
} }
let res = self.subset;
self.subset = Bitboard(self.subset.0.wrapping_sub(self.board.0)) & self.board;
self.done = self.subset.is_empty();
Some(res)
} }
fn size_hint(&self) -> (usize, Option<usize>) { fn size_hint(&self) -> (usize, Option<usize>) {
let size = 1 << self.board.count(); (self.total, Some(self.total))
(size, Some(size))
} }
} }

View file

@ -1,4 +1,5 @@
use super::{Bitboard, Color, File, Square}; use super::{Bitboard, Color, File, FromFen, Square};
use crate::error::Error;
/// Current castle rights for a player. /// Current castle rights for a player.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@ -17,44 +18,19 @@ impl CastleRights {
/// The number of [CastleRights] variants. /// The number of [CastleRights] variants.
pub const NUM_VARIANTS: usize = 4; pub const NUM_VARIANTS: usize = 4;
const ALL: [Self; Self::NUM_VARIANTS] = [
Self::NoSide,
Self::KingSide,
Self::QueenSide,
Self::BothSides,
];
/// Iterate over all castle-rights variants.
pub fn iter() -> impl Iterator<Item = Self> {
Self::ALL.iter().cloned()
}
/// Convert from a castle rights index into a [CastleRights] type. /// Convert from a castle rights index into a [CastleRights] type.
///
/// # Panics
///
/// Panics if the index is out of bounds.
#[inline(always)] #[inline(always)]
pub fn from_index(index: usize) -> Self { pub fn from_index(index: usize) -> Self {
Self::try_from_index(index).expect("index out of bouds") assert!(index < Self::NUM_VARIANTS);
} // SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(index) }
/// Convert from a castle rights index into a [CastleRights] type. Returns [None] if the index
/// is out of bounds.
pub fn try_from_index(index: usize) -> Option<Self> {
if index < Self::NUM_VARIANTS {
// SAFETY: we know the value is in-bounds
Some(unsafe { Self::from_index_unchecked(index) })
} else {
None
}
} }
/// Convert from a castle rights index into a [CastleRights] type, no bounds checking. /// Convert from a castle rights index into a [CastleRights] type, no bounds checking.
/// ///
/// # Safety /// # Safety
/// ///
/// This should only be called with values that can be output by [CastleRights::index()]. /// Should only be called with values that can be output by [CastleRights::index()].
#[inline(always)] #[inline(always)]
pub unsafe fn from_index_unchecked(index: usize) -> Self { pub unsafe fn from_index_unchecked(index: usize) -> Self {
std::mem::transmute(index as u8) std::mem::transmute(index as u8)
@ -91,10 +67,11 @@ impl CastleRights {
} }
/// Add some [CastleRights], and return the resulting [CastleRights]. /// Add some [CastleRights], and return the resulting [CastleRights].
#[allow(clippy::should_implement_trait)]
#[inline(always)] #[inline(always)]
fn add(self, additional_rights: CastleRights) -> Self { pub fn add(self, to_remove: CastleRights) -> Self {
// SAFETY: we know the value is in-bounds // SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(self.index() | additional_rights.index()) } unsafe { Self::from_index_unchecked(self.index() | to_remove.index()) }
} }
/// Remove king-side castling rights. /// Remove king-side castling rights.
@ -133,6 +110,39 @@ impl CastleRights {
} }
} }
/// Convert the castling rights segment of a FEN string to an array of [CastleRights].
impl FromFen for [CastleRights; 2] {
type Err = Error;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
if s.len() > 4 {
return Err(Error::InvalidFen);
}
let mut res = [CastleRights::NoSide; 2];
if s == "-" {
return Ok(res);
}
for b in s.chars() {
let color = if b.is_uppercase() {
Color::White
} else {
Color::Black
};
let rights = &mut res[color.index()];
match b {
'k' | 'K' => *rights = rights.with_king_side(),
'q' | 'Q' => *rights = rights.with_queen_side(),
_ => return Err(Error::InvalidFen),
}
}
Ok(res)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

1001
src/board/chess_board.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,164 +0,0 @@
use crate::board::{Bitboard, CastleRights, ChessBoard, Color, Piece, Square, ValidationError};
/// Build a [ChessBoard] one piece at a time.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ChessBoardBuilder {
/// The list of [Piece] on the board. Indexed by [Square::index].
pieces: [Option<(Piece, Color)>; Square::NUM_VARIANTS],
// Same fields as [ChessBoard].
castle_rights: [CastleRights; Color::NUM_VARIANTS],
en_passant: Option<Square>,
half_move_clock: u32,
side: Color,
// 1-based, a turn is *two* half-moves (i.e: both players have played).
turn_count: u32,
}
impl ChessBoardBuilder {
pub fn new() -> Self {
Self {
pieces: [None; Square::NUM_VARIANTS],
castle_rights: [CastleRights::NoSide; Color::NUM_VARIANTS],
en_passant: Default::default(),
half_move_clock: Default::default(),
side: Color::White,
turn_count: 1,
}
}
pub fn with_castle_rights(&mut self, rights: CastleRights, color: Color) -> &mut Self {
self.castle_rights[color.index()] = rights;
self
}
pub fn with_en_passant(&mut self, square: Square) -> &mut Self {
self.en_passant = Some(square);
self
}
pub fn without_en_passant(&mut self) -> &mut Self {
self.en_passant = None;
self
}
pub fn with_half_move_clock(&mut self, clock: u32) -> &mut Self {
self.half_move_clock = clock;
self
}
pub fn with_turn_count(&mut self, count: u32) -> &mut Self {
self.turn_count = count;
self
}
pub fn with_current_player(&mut self, color: Color) -> &mut Self {
self.side = color;
self
}
}
impl Default for ChessBoardBuilder {
fn default() -> Self {
Self::new()
}
}
/// Index a [ChessBoardBuilder] with a [Square] to access its pieces.
impl std::ops::Index<Square> 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<Square> for ChessBoardBuilder {
fn index_mut(&mut self, square: Square) -> &mut Self::Output {
&mut self.pieces[square.index()]
}
}
impl TryFrom<ChessBoardBuilder> for ChessBoard {
type Error = ValidationError;
fn try_from(builder: ChessBoardBuilder) -> Result<Self, Self::Error> {
let mut piece_occupancy: [Bitboard; Piece::NUM_VARIANTS] = Default::default();
let mut color_occupancy: [Bitboard; Color::NUM_VARIANTS] = Default::default();
let mut combined_occupancy: Bitboard = Default::default();
let ChessBoardBuilder {
pieces,
castle_rights,
en_passant,
half_move_clock,
side,
turn_count,
} = builder;
for square in Square::iter() {
let Some((piece, color)) = pieces[square.index()] else {
continue;
};
piece_occupancy[piece.index()] |= square;
color_occupancy[color.index()] |= square;
combined_occupancy |= square;
}
let total_plies = (turn_count - 1) * 2 + if side == Color::White { 0 } else { 1 };
let board = ChessBoard {
piece_occupancy,
color_occupancy,
combined_occupancy,
castle_rights,
en_passant,
half_move_clock,
total_plies,
side,
};
board.validate()?;
Ok(board)
}
}
#[cfg(test)]
mod test {
use super::*;
fn from_board(board: &ChessBoard) -> ChessBoardBuilder {
let mut builder = ChessBoardBuilder::new();
for piece in Piece::iter() {
for color in Color::iter() {
for square in board.occupancy(piece, color) {
builder[square] = Some((piece, color));
}
}
}
for color in Color::iter() {
builder.with_castle_rights(board.castle_rights(color), color);
}
if let Some(square) = board.en_passant() {
builder.with_en_passant(square);
} else {
builder.without_en_passant();
}
builder
.with_half_move_clock(board.half_move_clock())
.with_turn_count(board.total_plies() / 2 + 1)
.with_current_player(board.current_player());
builder
}
#[test]
fn default_board() {
let board = ChessBoard::default();
let builder = from_board(&board);
assert_eq!(board, builder.try_into().unwrap())
}
}

View file

@ -1,56 +0,0 @@
/// A singular type for all errors that could happen during [crate::board::ChessBoard::is_valid].
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ValidationError {
/// Too many pieces.
TooManyPieces,
/// Missing king.
MissingKing,
/// Pawns on the first/last rank.
InvalidPawnPosition,
/// Castling rights do not match up with the state of the board.
InvalidCastlingRights,
/// En-passant target square is not empty, behind an opponent's pawn, on the correct rank.
InvalidEnPassant,
/// The two kings are next to each other.
NeighbouringKings,
/// The opponent is currently in check.
OpponentInCheck,
/// The piece-specific boards are overlapping.
OverlappingPieces,
/// The color-specific boards are overlapping.
OverlappingColors,
/// The pre-computed combined occupancy boards does not match the other boards.
ErroneousCombinedOccupancy,
/// Half-move clock is higher than total number of plies.
HalfMoveClockTooHigh,
/// The total plie count does not match the current player.
IncoherentPlieCount,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let error_msg = match self {
Self::TooManyPieces => "too many pieces",
Self::MissingKing => "missing king",
Self::InvalidPawnPosition => "pawns on the first/last rank",
Self::InvalidCastlingRights => {
"castling rights do not match up with the state of the board"
}
Self::InvalidEnPassant => {
"en-passant target square is not empty, behind an opponent's pawn, on the correct rank"
}
Self::NeighbouringKings => "the two kings are next to each other",
Self::OpponentInCheck => "the opponent is currently in check",
Self::OverlappingPieces => "the piece-specific boards are overlapping",
Self::OverlappingColors => "the color-specific boards are overlapping",
Self::ErroneousCombinedOccupancy => {
"the pre-computed combined occupancy boards does not match the other boards"
}
Self::HalfMoveClockTooHigh => "half-move clock is higher than total number of plies",
Self::IncoherentPlieCount => "the total plie count does not match the current player",
};
write!(f, "{}", error_msg)
}
}
impl std::error::Error for ValidationError {}

View file

@ -1,828 +0,0 @@
use crate::movegen;
use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square};
mod builder;
pub use builder::*;
mod error;
pub use error::*;
/// Represent an on-going chess game.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ChessBoard {
/// A [Bitboard] of occupancy for each piece type, discarding color. Indexed by [Piece::index].
piece_occupancy: [Bitboard; Piece::NUM_VARIANTS],
/// A [Bitboard] of occupancy for each color, discarding piece type. Indexed by [Piece::index].
color_occupancy: [Bitboard; Color::NUM_VARIANTS],
/// A [Bitboard] representing all squares currently occupied by a piece.
combined_occupancy: Bitboard,
/// The allowed [CastleRights] for either color. Indexed by [Color::index].
castle_rights: [CastleRights; Color::NUM_VARIANTS],
/// A potential en-passant attack.
/// Either `None` if no double-step pawn move was made in the previous half-turn, or
/// `Some(target_square)` if a double-step move was made.
en_passant: Option<Square>,
/// The number of half-turns without either a pawn push or capture.
half_move_clock: u32, // Should *probably* never go higher than 100.
/// The number of half-turns so far.
total_plies: u32, // Should be plenty.
/// The current player turn.
side: Color,
}
/// The state which can't be reversed when doing/un-doing a [Move].
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct NonReversibleState {
castle_rights: [CastleRights; Color::NUM_VARIANTS],
en_passant: Option<Square>,
half_move_clock: u32, // Should *probably* never go higher than 100.
captured_piece: Option<Piece>,
}
impl ChessBoard {
/// Which player's turn is it.
#[inline(always)]
pub fn current_player(&self) -> Color {
self.side
}
/// Return the target [Square] that can be captured en-passant, or `None`
#[inline(always)]
pub fn en_passant(&self) -> Option<Square> {
self.en_passant
}
/// Return the [CastleRights] for the given [Color].
#[inline(always)]
pub fn castle_rights(&self, color: Color) -> CastleRights {
self.castle_rights[color.index()]
}
/// Return the [CastleRights] for the given [Color]. Allow mutations.
#[inline(always)]
fn castle_rights_mut(&mut self, color: Color) -> &mut CastleRights {
&mut self.castle_rights[color.index()]
}
/// Get the [Bitboard] representing all pieces of the given [Piece] and [Color] type.
#[inline(always)]
pub fn occupancy(&self, piece: Piece, color: Color) -> Bitboard {
self.piece_occupancy(piece) & self.color_occupancy(color)
}
/// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color.
#[inline(always)]
pub fn piece_occupancy(&self, piece: Piece) -> Bitboard {
self.piece_occupancy[piece.index()]
}
/// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color.
/// Allow mutating the state.
#[inline(always)]
fn piece_occupancy_mut(&mut self, piece: Piece) -> &mut Bitboard {
&mut self.piece_occupancy[piece.index()]
}
/// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece
/// type.
#[inline(always)]
pub fn color_occupancy(&self, color: Color) -> Bitboard {
self.color_occupancy[color.index()]
}
/// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece
/// type. Allow mutating the state.
#[inline(always)]
fn color_occupancy_mut(&mut self, color: Color) -> &mut Bitboard {
&mut self.color_occupancy[color.index()]
}
/// Get the [Bitboard] representing all pieces on the board.
#[inline(always)]
pub fn combined_occupancy(&self) -> Bitboard {
self.combined_occupancy
}
/// Return the number of half-turns without either a pawn push or a capture.
#[inline(always)]
pub fn half_move_clock(&self) -> u32 {
self.half_move_clock
}
/// Return the total number of plies (i.e: half-turns) played so far.
#[inline(always)]
pub fn total_plies(&self) -> u32 {
self.total_plies
}
/// Return the [Bitboard] corresponding to all the opponent's pieces threatening the current
/// player's king.
#[inline(always)]
pub fn checkers(&self) -> Bitboard {
self.compute_checkers(self.current_player())
}
/// Quickly add/remove a piece on the [Bitboard]s that are part of the [ChessBoard] state.
#[inline(always)]
fn xor(&mut self, color: Color, piece: Piece, square: Square) {
*self.piece_occupancy_mut(piece) ^= square;
*self.color_occupancy_mut(color) ^= square;
self.combined_occupancy ^= square;
}
/// Compute the change of [CastleRights] from moving/taking a piece.
fn update_castling(&mut self, color: Color, piece: Piece, file: File) {
let original = self.castle_rights(color);
let new_rights = match (piece, file) {
(Piece::Rook, File::A) => original.without_queen_side(),
(Piece::Rook, File::H) => original.without_king_side(),
(Piece::King, _) => CastleRights::NoSide,
_ => return,
};
if new_rights != original {
*self.castle_rights_mut(color) = new_rights;
}
}
/// Play the given [Move], return a copy of the board with the resulting state.
#[inline(always)]
pub fn play_move(&self, chess_move: Move) -> Self {
let mut res = self.clone();
res.play_move_inplace(chess_move);
res
}
/// Play the given [Move] in place, returning all non-revertible state (e.g: en-passant,
/// etc...).
#[inline(always)]
pub fn play_move_inplace(&mut self, chess_move: Move) -> NonReversibleState {
let opponent = !self.current_player();
let move_piece = Piece::iter()
.find(|&p| !(self.piece_occupancy(p) & chess_move.start()).is_empty())
.unwrap();
let captured_piece = Piece::iter()
.skip(1) // No need to check for the king here
.find(|&p| !(self.occupancy(p, opponent) & chess_move.destination()).is_empty());
let is_double_step = move_piece == Piece::Pawn
&& chess_move.start().rank() == self.current_player().second_rank()
&& chess_move.destination().rank() == self.current_player().fourth_rank();
// Save non-revertible state
let state = NonReversibleState {
castle_rights: self.castle_rights,
en_passant: self.en_passant,
half_move_clock: self.half_move_clock,
captured_piece,
};
// Non-revertible state modification
if captured_piece.is_some() || move_piece == Piece::Pawn {
self.half_move_clock = 0;
} else {
self.half_move_clock += 1;
}
if is_double_step {
let target_square = Square::new(
chess_move.destination().file(),
self.current_player().third_rank(),
);
self.en_passant = Some(target_square);
} else {
self.en_passant = None;
}
self.update_castling(self.current_player(), move_piece, chess_move.start().file());
if let Some(piece) = captured_piece {
self.xor(opponent, piece, chess_move.destination());
// If a rook is captured, it loses its castling rights
self.update_castling(opponent, piece, chess_move.destination().file());
}
// Revertible state modification
let dest_piece = chess_move.promotion().unwrap_or(move_piece);
self.xor(self.current_player(), move_piece, chess_move.start());
self.xor(self.current_player(), dest_piece, chess_move.destination());
self.total_plies += 1;
self.side = !self.side;
state
}
/// Reverse the effect of playing the given [Move], and return to the given
/// [NonReversibleState].
#[inline(always)]
pub fn unplay_move(&mut self, chess_move: Move, previous: NonReversibleState) {
// Restore non-revertible state
self.castle_rights = previous.castle_rights;
self.en_passant = previous.en_passant;
self.half_move_clock = previous.half_move_clock;
let move_piece = Piece::iter()
// We're looking for the *destination* as this is *undoing* the move
.find(|&p| !(self.piece_occupancy(p) & chess_move.destination()).is_empty())
.unwrap();
if let Some(piece) = previous.captured_piece {
// The capture affected the *current* player, from our post-move POV
self.xor(self.current_player(), piece, chess_move.destination());
}
// Restore revertible state
let start_piece = chess_move.promotion().map_or(move_piece, |_| Piece::Pawn);
self.xor(!self.current_player(), move_piece, chess_move.destination());
self.xor(!self.current_player(), start_piece, chess_move.start());
self.total_plies -= 1;
self.side = !self.side;
}
/// Return true if the current state of the board looks valid, false if something is definitely
/// wrong.
pub fn is_valid(&self) -> bool {
self.validate().is_ok()
}
/// Validate the state of the board. Return Err([ValidationError]) if an issue is found.
pub fn validate(&self) -> Result<(), ValidationError> {
// The current plie count should be odd on white's turn, and vice-versa.
if self.total_plies() % 2 != self.current_player().index() as u32 {
return Err(ValidationError::IncoherentPlieCount);
}
// Make sure the clocks are in agreement.
if self.half_move_clock() > self.total_plies() {
return Err(ValidationError::HalfMoveClockTooHigh);
}
// Don't overlap pieces.
for piece in Piece::iter() {
#[allow(clippy::collapsible_if)]
for other in Piece::iter() {
if piece != other {
if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() {
return Err(ValidationError::OverlappingPieces);
}
}
}
}
// Don't overlap colors.
if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() {
return Err(ValidationError::OverlappingColors);
}
// Calculate the union of all pieces.
let combined =
Piece::iter().fold(Bitboard::EMPTY, |board, p| board | self.piece_occupancy(p));
// Ensure that the pre-computed version is accurate.
if combined != self.combined_occupancy() {
return Err(ValidationError::ErroneousCombinedOccupancy);
}
// Ensure that all pieces belong to a color, and no color has pieces that don't exist.
if combined != (self.color_occupancy(Color::White) | self.color_occupancy(Color::Black)) {
return Err(ValidationError::ErroneousCombinedOccupancy);
}
for color in Color::iter() {
for piece in Piece::iter() {
// Check that we have the expected number of piecese.
let count = self.occupancy(piece, color).count();
let possible = match piece {
Piece::King => count <= 1,
Piece::Pawn => count <= 8,
Piece::Queen => count <= 9,
_ => count <= 10,
};
if !possible {
return Err(ValidationError::TooManyPieces);
}
}
// Check that we have a king
if self.occupancy(Piece::King, color).count() != 1 {
return Err(ValidationError::MissingKing);
}
// Check that don't have too many pieces in total
if self.color_occupancy(color).count() > 16 {
return Err(ValidationError::TooManyPieces);
}
}
// Check that pawns aren't in first/last rank.
if !(self.piece_occupancy(Piece::Pawn)
& (Rank::First.into_bitboard() | Rank::Eighth.into_bitboard()))
.is_empty()
{
return Err(ValidationError::InvalidPawnPosition);
}
// Verify that rooks and kings that are allowed to castle have not been moved.
for color in Color::iter() {
let castle_rights = self.castle_rights(color);
// Nothing to check if there are no castlings allowed.
if castle_rights == CastleRights::NoSide {
continue;
}
let actual_rooks = self.occupancy(Piece::Rook, color);
let expected_rooks = castle_rights.unmoved_rooks(color);
// We must check the intersection, in case there are more than 2 rooks on the board.
if (expected_rooks & actual_rooks) != expected_rooks {
return Err(ValidationError::InvalidCastlingRights);
}
let actual_king = self.occupancy(Piece::King, color);
let expected_king = Square::new(File::E, color.first_rank());
// We have checked that there is exactly one king, no need for intersecting the sets.
if actual_king != expected_king.into_bitboard() {
return Err(ValidationError::InvalidCastlingRights);
}
}
// En-passant validation
if let Some(square) = self.en_passant() {
// Must be empty
if !(self.combined_occupancy() & square).is_empty() {
return Err(ValidationError::InvalidEnPassant);
}
let opponent = !self.current_player();
// Must be on the opponent's third rank
if (square & opponent.third_rank().into_bitboard()).is_empty() {
return Err(ValidationError::InvalidEnPassant);
}
// Must be behind a pawn
let opponent_pawns = self.occupancy(Piece::Pawn, opponent);
let double_pushed_pawn = self
.current_player()
.backward_direction()
.move_board(square.into_bitboard());
if (opponent_pawns & double_pushed_pawn).is_empty() {
return Err(ValidationError::InvalidEnPassant);
}
}
// Check that kings don't touch each other.
let white_king = self.occupancy(Piece::King, Color::White);
let black_king = self.occupancy(Piece::King, Color::Black);
// Unwrap is fine, we already checked that there is exactly one king of each color
if !(movegen::king_moves(white_king.try_into().unwrap()) & black_king).is_empty() {
return Err(ValidationError::NeighbouringKings);
}
// Check that the opponent is not currently in check.
if !self.compute_checkers(!self.current_player()).is_empty() {
return Err(ValidationError::OpponentInCheck);
}
Ok(())
}
/// Compute all pieces that are currently threatening the given [Color]'s king.
fn compute_checkers(&self, color: Color) -> Bitboard {
// Unwrap is fine, there should always be exactly one king per color
let king = (self.occupancy(Piece::King, color)).try_into().unwrap();
let opponent = !color;
// No need to remove our pieces from the generated moves, we just want to check if we
// intersect with the opponent's pieces, rather than generate only valid moves.
let bishops = {
let queens = self.occupancy(Piece::Queen, opponent);
let bishops = self.occupancy(Piece::Bishop, opponent);
let bishop_attacks = movegen::bishop_moves(king, self.combined_occupancy());
(queens | bishops) & bishop_attacks
};
let rooks = {
let queens = self.occupancy(Piece::Queen, opponent);
let rooks = self.occupancy(Piece::Rook, opponent);
let rook_attacks = movegen::rook_moves(king, self.combined_occupancy());
(queens | rooks) & rook_attacks
};
let knights = {
let knights = self.occupancy(Piece::Knight, opponent);
let knight_attacks = movegen::knight_moves(king);
knights & knight_attacks
};
let pawns = {
let pawns = self.occupancy(Piece::Pawn, opponent);
let pawn_attacks = movegen::pawn_attacks(color, king);
pawns & pawn_attacks
};
bishops | rooks | knights | pawns
}
}
/// Use the starting position as a default value, corresponding to the
/// "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" FEN string
impl Default for ChessBoard {
fn default() -> Self {
Self {
piece_occupancy: [
// King
Square::E1 | Square::E8,
// Queen
Square::D1 | Square::D8,
// Rook
Square::A1 | Square::A8 | Square::H1 | Square::H8,
// Bishop
Square::C1 | Square::C8 | Square::F1 | Square::F8,
// Knight
Square::B1 | Square::B8 | Square::G1 | Square::G8,
// Pawn
Rank::Second.into_bitboard() | Rank::Seventh.into_bitboard(),
],
color_occupancy: [
Rank::First.into_bitboard() | Rank::Second.into_bitboard(),
Rank::Seventh.into_bitboard() | Rank::Eighth.into_bitboard(),
],
combined_occupancy: Rank::First.into_bitboard()
| Rank::Second.into_bitboard()
| Rank::Seventh.into_bitboard()
| Rank::Eighth.into_bitboard(),
castle_rights: [CastleRights::BothSides; Color::NUM_VARIANTS],
en_passant: None,
half_move_clock: 0,
total_plies: 0,
side: Color::White,
}
}
}
#[cfg(test)]
mod test {
use crate::fen::FromFen;
use super::*;
#[test]
fn valid() {
let default_position = ChessBoard::default();
assert!(default_position.is_valid());
}
#[test]
fn invalid_incoherent_plie_count() {
let position = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
let mut board = TryInto::<ChessBoard>::try_into(builder).unwrap();
board.total_plies = 1;
board
};
assert_eq!(
position.validate().err().unwrap(),
ValidationError::IncoherentPlieCount,
);
}
#[test]
fn invalid_half_moves_clock() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
builder.with_half_move_clock(10);
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::HalfMoveClockTooHigh);
}
#[test]
fn invalid_overlapping_pieces() {
let position = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
let mut board: ChessBoard = builder.try_into().unwrap();
*board.piece_occupancy_mut(Piece::Queen) |= Square::E1.into_bitboard();
board
};
assert_eq!(
position.validate().err().unwrap(),
ValidationError::OverlappingPieces,
);
}
#[test]
fn invalid_overlapping_colors() {
let position = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
let mut board: ChessBoard = builder.try_into().unwrap();
*board.color_occupancy_mut(Color::White) |= Square::E8.into_bitboard();
board
};
assert_eq!(
position.validate().err().unwrap(),
ValidationError::OverlappingColors,
);
}
#[test]
fn invalid_combined_does_not_equal_pieces() {
let position = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
let mut board: ChessBoard = builder.try_into().unwrap();
*board.piece_occupancy_mut(Piece::Pawn) |= Square::E2.into_bitboard();
board
};
assert_eq!(
position.validate().err().unwrap(),
ValidationError::ErroneousCombinedOccupancy,
);
}
#[test]
fn invalid_combined_does_not_equal_colors() {
let position = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
let mut board: ChessBoard = builder.try_into().unwrap();
*board.color_occupancy_mut(Color::Black) |= Square::E2.into_bitboard();
board
};
assert_eq!(
position.validate().err().unwrap(),
ValidationError::ErroneousCombinedOccupancy,
);
}
#[test]
fn invalid_multiple_kings() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E2] = Some((Piece::King, Color::White));
builder[Square::E7] = Some((Piece::King, Color::Black));
builder[Square::E8] = Some((Piece::King, Color::Black));
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces);
}
#[test]
fn invalid_castling_rights_no_rooks() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
builder.with_castle_rights(CastleRights::BothSides, Color::White);
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights);
}
#[test]
fn invalid_castling_rights_moved_king() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E2] = Some((Piece::King, Color::White));
builder[Square::A1] = Some((Piece::Rook, Color::White));
builder[Square::H1] = Some((Piece::Rook, Color::White));
builder[Square::E7] = Some((Piece::King, Color::Black));
builder[Square::A8] = Some((Piece::Rook, Color::Black));
builder[Square::H8] = Some((Piece::Rook, Color::Black));
builder.with_castle_rights(CastleRights::BothSides, Color::White);
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights);
}
#[test]
fn valid_en_passant() {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
builder[Square::A5] = Some((Piece::Pawn, Color::Black));
builder.with_en_passant(Square::A6);
TryInto::<ChessBoard>::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::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant);
}
#[test]
fn invalid_en_passant_not_behind_pawn() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
builder[Square::A5] = Some((Piece::Rook, Color::Black));
builder.with_en_passant(Square::A6);
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant);
}
#[test]
fn invalid_en_passant_incorrect_rank() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
builder[Square::A4] = Some((Piece::Pawn, Color::Black));
builder.with_en_passant(Square::A5);
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant);
}
#[test]
fn invalid_kings_next_to_each_other() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E2] = Some((Piece::King, Color::Black));
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::NeighbouringKings);
}
#[test]
fn invalid_opponent_in_check() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::E1] = Some((Piece::King, Color::White));
builder[Square::E7] = Some((Piece::Queen, Color::White));
builder[Square::E8] = Some((Piece::King, Color::Black));
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::OpponentInCheck);
}
#[test]
fn invalid_pawn_on_first_rank() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::H1] = Some((Piece::King, Color::White));
builder[Square::A1] = Some((Piece::Pawn, Color::White));
builder[Square::H8] = Some((Piece::King, Color::Black));
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::InvalidPawnPosition);
}
#[test]
fn invalid_too_many_pieces() {
let res = {
let mut builder = ChessBoardBuilder::new();
builder[Square::H1] = Some((Piece::King, Color::White));
builder[Square::H8] = Some((Piece::King, Color::Black));
for square in (File::B.into_bitboard() | File::C.into_bitboard()).into_iter() {
builder[square] = Some((Piece::Pawn, Color::White));
}
for square in (File::F.into_bitboard() | File::G.into_bitboard()).into_iter() {
builder[square] = Some((Piece::Pawn, Color::Black));
}
TryInto::<ChessBoard>::try_into(builder)
};
assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces);
}
#[test]
fn checkers() {
let position = {
let mut builder = ChessBoardBuilder::new();
builder[Square::C1] = Some((Piece::Knight, Color::White));
builder[Square::D3] = Some((Piece::Bishop, Color::White));
builder[Square::E1] = Some((Piece::Rook, Color::White));
builder[Square::E2] = Some((Piece::King, Color::White));
builder[Square::H2] = Some((Piece::Queen, Color::White));
builder[Square::G1] = Some((Piece::Knight, Color::Black));
builder[Square::F3] = Some((Piece::Bishop, Color::Black));
builder[Square::A2] = Some((Piece::Rook, Color::Black));
builder[Square::E8] = Some((Piece::King, Color::Black));
builder[Square::E7] = Some((Piece::Queen, Color::Black));
TryInto::<ChessBoard>::try_into(builder).unwrap()
};
assert_eq!(
position.checkers(),
Square::A2 | Square::E7 | Square::F3 | Square::G1
);
}
#[test]
fn play_move() {
// Start from default position
let mut position = ChessBoard::default();
// Modify it to account for e4 move
position.play_move_inplace(Move::new(Square::E2, Square::E4, None));
assert_eq!(
position,
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
.unwrap()
);
// And now c5
position.play_move_inplace(Move::new(Square::C7, Square::C5, None));
assert_eq!(
position,
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
.unwrap()
);
// Finally, Nf3
position.play_move_inplace(Move::new(Square::G1, Square::F3, None));
assert_eq!(
position,
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ")
.unwrap()
);
}
#[test]
fn play_move_capture_changes_castling() {
let mut position = ChessBoard::from_fen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1").unwrap();
let expected = ChessBoard::from_fen("r3k2R/8/8/8/8/8/8/R3K3 b Qq - 0 1").unwrap();
let capture = Move::new(Square::H1, Square::H8, None);
position.play_move_inplace(capture);
assert_eq!(position, expected);
}
#[test]
fn play_move_and_undo() {
// Start from default position
let mut position = ChessBoard::default();
// Modify it to account for e4 move
let move_1 = Move::new(Square::E2, Square::E4, None);
let state_1 = position.play_move_inplace(move_1);
// And now c5
let move_2 = Move::new(Square::C7, Square::C5, None);
let state_2 = position.play_move_inplace(move_2);
// Finally, Nf3
let move_3 = Move::new(Square::G1, Square::F3, None);
let state_3 = position.play_move_inplace(move_3);
// Now revert each move one-by-one
position.unplay_move(move_3, state_3);
assert_eq!(
position,
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
.unwrap()
);
position.unplay_move(move_2, state_2);
assert_eq!(
position,
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
.unwrap()
);
position.unplay_move(move_1, state_1);
assert_eq!(
position,
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
.unwrap()
);
}
#[test]
fn play_move_undo_capture() {
let mut position = ChessBoard::from_fen("3q3k/8/8/8/8/8/8/K2Q4 w - - 0 1").unwrap();
let expected = ChessBoard::from_fen("3Q3k/8/8/8/8/8/8/K7 b - - 0 1").unwrap();
let original = position.clone();
let capture = Move::new(Square::D1, Square::D8, None);
let state = position.play_move_inplace(capture);
assert_eq!(position, expected);
position.unplay_move(capture, state);
assert_eq!(position, original);
}
#[test]
fn play_move_undo_promotion() {
let mut position = ChessBoard::from_fen("7k/P7/8/8/8/8/8/K7 w - - 0 1").unwrap();
let expected = ChessBoard::from_fen("N6k/8/8/8/8/8/8/K7 b - - 0 1").unwrap();
let original = position.clone();
let promotion = Move::new(Square::A7, Square::A8, Some(Piece::Knight));
let state = position.play_move_inplace(promotion);
assert_eq!(position, expected);
position.unplay_move(promotion, state);
assert_eq!(position, original);
}
}

View file

@ -1,4 +1,5 @@
use super::{Direction, Rank}; use super::{Direction, FromFen, Rank};
use crate::error::Error;
/// An enum representing the color of a player. /// An enum representing the color of a player.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@ -18,28 +19,15 @@ impl Color {
Self::ALL.iter().cloned() Self::ALL.iter().cloned()
} }
/// Convert from a color index into a [Color] type. /// Convert from a piece index into a [Color] type.
///
/// # Panics
///
/// Panics if the index is out of bounds.
#[inline(always)] #[inline(always)]
pub fn from_index(index: usize) -> Self { pub fn from_index(index: usize) -> Self {
Self::try_from_index(index).expect("index out of bouds") assert!(index < Self::NUM_VARIANTS);
// SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(index) }
} }
/// Convert from a color index into a [Color] type. Returns [None] if the index is out of /// Convert from a piece index into a [Color] type, no bounds checking.
/// bounds.
pub fn try_from_index(index: usize) -> Option<Self> {
if index < Self::NUM_VARIANTS {
// SAFETY: we know the value is in-bounds
Some(unsafe { Self::from_index_unchecked(index) })
} else {
None
}
}
/// Convert from a color index into a [Color] type, no bounds checking.
/// ///
/// # Safety /// # Safety
/// ///
@ -119,6 +107,20 @@ impl Color {
} }
} }
/// Convert a side to move segment of a FEN string to a [Color].
impl FromFen for Color {
type Err = Error;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let res = match s {
"w" => Color::White,
"b" => Color::Black,
_ => return Err(Error::InvalidFen),
};
Ok(res)
}
}
impl std::ops::Not for Color { impl std::ops::Not for Color {
type Output = Color; type Output = Color;

View file

@ -128,7 +128,8 @@ impl Direction {
/// Slide a board along the given [Direction], i.e: return all successive applications of /// Slide a board along the given [Direction], i.e: return all successive applications of
/// [Direction::move_board] until no new squares can be reached. /// [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 /// Take into account the `blockers` [Bitboard]: a combination of all pieces on the board which
/// cannot be slid over. The slide is over once a square that is part of `blockers` is reached. /// cannot be slided over. The slide is over once a square that is part of `blockers` is
/// reached.
/// It does not make sense to use this method with knight-only directions, and it will panic in /// It does not make sense to use this method with knight-only directions, and it will panic in
/// debug-mode if it happens. /// debug-mode if it happens.
#[inline(always)] #[inline(always)]

6
src/board/fen.rs Normal file
View file

@ -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<Self, Self::Err>;
}

View file

@ -35,23 +35,11 @@ impl File {
} }
/// Convert from a file index into a [File] type. /// Convert from a file index into a [File] type.
///
/// # Panics
///
/// Panics if the index is out of bounds.
#[inline(always)] #[inline(always)]
pub fn from_index(index: usize) -> Self { pub fn from_index(index: usize) -> Self {
Self::try_from_index(index).expect("index out of bouds") 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. Returns [None] if the index is out of bounds.
pub fn try_from_index(index: usize) -> Option<Self> {
if index < Self::NUM_VARIANTS {
// SAFETY: we know the value is in-bounds
Some(unsafe { Self::from_index_unchecked(index) })
} else {
None
}
} }
/// Convert from a file index into a [File] type, no bounds checking. /// Convert from a file index into a [File] type, no bounds checking.

View file

@ -13,6 +13,9 @@ pub use color::*;
pub mod direction; pub mod direction;
pub use direction::*; pub use direction::*;
pub mod fen;
pub use fen::*;
pub mod file; pub mod file;
pub use file::*; pub use file::*;

View file

@ -1,42 +1,232 @@
use super::{Piece, Square}; use super::{Piece, Square};
type Bitset = u32;
/// A chess move, containing: /// A chess move, containing:
/// * Piece type.
/// * Starting square. /// * Starting square.
/// * Destination square. /// * Destination square.
/// * Optional capture type.
/// * Optional promotion 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)] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Move { pub struct Move(Bitset);
start: Square,
destination: Square, /// A builder for [Move]. This is the prefered and only way of building a [Move].
promotion: Option<Piece>, pub struct MoveBuilder {
pub piece: Piece,
pub start: Square,
pub destination: Square,
pub capture: Option<Piece>,
pub promotion: Option<Piece>,
pub en_passant: bool,
pub double_step: bool,
pub castling: bool,
}
impl From<MoveBuilder> 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 { impl Move {
/// Construct a new move. /// Construct a new move.
#[allow(clippy::too_many_arguments)]
#[inline(always)] #[inline(always)]
pub fn new(start: Square, destination: Square, promotion: Option<Piece>) -> Self { fn new(
Self { piece: Piece,
start, start: Square,
destination, destination: Square,
promotion, capture: Option<Piece>,
} promotion: Option<Piece>,
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. /// Get the [Square] that this move starts from.
#[inline(always)] #[inline(always)]
pub fn start(self) -> Square { pub fn start(self) -> Square {
self.start let index = ((self.0 >> shift::START) & shift::START_MASK) as usize;
// SAFETY: we know the value is in-bounds
unsafe { Square::from_index_unchecked(index) }
} }
/// Get the [Square] that this move ends on. /// Get the [Square] that this move ends on.
#[inline(always)] #[inline(always)]
pub fn destination(self) -> Square { pub fn destination(self) -> Square {
self.destination let index = ((self.0 >> shift::DESTINATION) & shift::DESTINATION_MASK) as usize;
// SAFETY: we know the value is in-bounds
unsafe { Square::from_index_unchecked(index) }
}
/// Get the [Piece] that this move captures, or `None` if there are no captures.
#[inline(always)]
pub fn capture(self) -> Option<Piece> {
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. /// Get the [Piece] that this move promotes to, or `None` if there are no promotions.
#[inline(always)] #[inline(always)]
pub fn promotion(self) -> Option<Piece> { pub fn promotion(self) -> Option<Piece> {
self.promotion let index = ((self.0 >> shift::PROMOTION) & shift::PROMOTION_MASK) as usize;
if index < Piece::NUM_VARIANTS {
// SAFETY: we know the value is in-bounds
unsafe { Some(Piece::from_index_unchecked(index)) }
} else {
None
}
}
/// Get the whether or not the move is an en-passant capture.
#[inline(always)]
pub fn is_en_passant(self) -> bool {
let index = (self.0 >> shift::EN_PASSANT) & shift::EN_PASSANT_MASK;
index != 0
}
/// Get the whether or not the move is a pawn double step.
#[inline(always)]
pub fn is_double_step(self) -> bool {
let index = (self.0 >> shift::DOUBLE_STEP) & shift::DOUBLE_STEP_MASK;
index != 0
}
/// Get the whether or not the move is a king castling.
#[inline(always)]
pub fn is_castling(self) -> bool {
let index = (self.0 >> shift::CASTLING) & shift::CASTLING_MASK;
index != 0
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn builder_simple() {
let chess_move: Move = MoveBuilder {
piece: Piece::Queen,
start: Square::A2,
destination: Square::A3,
capture: None,
promotion: None,
en_passant: false,
double_step: false,
castling: false,
}
.into();
assert_eq!(chess_move.piece(), Piece::Queen);
assert_eq!(chess_move.start(), Square::A2);
assert_eq!(chess_move.destination(), Square::A3);
assert_eq!(chess_move.capture(), None);
assert_eq!(chess_move.promotion(), None);
assert!(!chess_move.is_en_passant());
assert!(!chess_move.is_double_step());
assert!(!chess_move.is_castling());
}
#[test]
fn builder_all_fields() {
let chess_move: Move = MoveBuilder {
piece: Piece::Pawn,
start: Square::A7,
destination: Square::B8,
capture: Some(Piece::Queen),
promotion: Some(Piece::Knight),
en_passant: true,
double_step: true,
castling: true,
}
.into();
assert_eq!(chess_move.piece(), Piece::Pawn);
assert_eq!(chess_move.start(), Square::A7);
assert_eq!(chess_move.destination(), Square::B8);
assert_eq!(chess_move.capture(), Some(Piece::Queen));
assert_eq!(chess_move.promotion(), Some(Piece::Knight));
assert!(chess_move.is_en_passant());
assert!(chess_move.is_double_step());
assert!(chess_move.is_castling());
} }
} }

View file

@ -1,3 +1,6 @@
use super::FromFen;
use crate::error::Error;
/// An enum representing the type of a piece. /// An enum representing the type of a piece.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Piece { pub enum Piece {
@ -28,24 +31,11 @@ impl Piece {
} }
/// Convert from a piece index into a [Piece] type. /// Convert from a piece index into a [Piece] type.
///
/// # Panics
///
/// Panics if the index is out of bounds.
#[inline(always)] #[inline(always)]
pub fn from_index(index: usize) -> Self { pub fn from_index(index: usize) -> Self {
Self::try_from_index(index).expect("index out of bouds") assert!(index < Self::NUM_VARIANTS);
} // SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(index) }
/// Convert from a piece index into a [Piece] type. Returns [None] if the index is out of
/// bounds.
pub fn try_from_index(index: usize) -> Option<Self> {
if index < Self::NUM_VARIANTS {
// SAFETY: we know the value is in-bounds
Some(unsafe { Self::from_index_unchecked(index) })
} else {
None
}
} }
/// Convert from a piece index into a [Piece] type, no bounds checking. /// Convert from a piece index into a [Piece] type, no bounds checking.
@ -65,6 +55,24 @@ impl Piece {
} }
} }
/// Convert a piece in FEN notation to a [Piece].
impl FromFen for Piece {
type Err = Error;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let res = match s {
"p" | "P" => Self::Pawn,
"n" | "N" => Self::Knight,
"b" | "B" => Self::Bishop,
"r" | "R" => Self::Rook,
"q" | "Q" => Self::Queen,
"k" | "K" => Self::King,
_ => return Err(Error::InvalidFen),
};
Ok(res)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View file

@ -35,23 +35,11 @@ impl Rank {
} }
/// Convert from a rank index into a [Rank] type. /// Convert from a rank index into a [Rank] type.
///
/// # Panics
///
/// Panics if the index is out of bounds.
#[inline(always)] #[inline(always)]
pub fn from_index(index: usize) -> Self { pub fn from_index(index: usize) -> Self {
Self::try_from_index(index).expect("index out of bouds") 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. Returns [None] if the index is out of bounds.
pub fn try_from_index(index: usize) -> Option<Self> {
if index < Self::NUM_VARIANTS {
// SAFETY: we know the value is in-bounds
Some(unsafe { Self::from_index_unchecked(index) })
} else {
None
}
} }
/// Convert from a rank index into a [Rank] type, no bounds checking. /// Convert from a rank index into a [Rank] type, no bounds checking.

View file

@ -1,5 +1,5 @@
use super::{Bitboard, File, Rank}; use super::{Bitboard, File, FromFen, Rank};
use crate::utils::static_assert; use crate::{error::Error, utils::static_assert};
/// Represent a square on a chessboard. Defined in the same order as the /// Represent a square on a chessboard. Defined in the same order as the
/// [Bitboard] squares. /// [Bitboard] squares.
@ -39,10 +39,6 @@ impl Square {
]; ];
/// Construct a [Square] from a [File] and [Rank]. /// Construct a [Square] from a [File] and [Rank].
///
/// # Panics
///
/// Panics if the index is out of bounds.
#[inline(always)] #[inline(always)]
pub fn new(file: File, rank: Rank) -> Self { pub fn new(file: File, rank: Rank) -> Self {
// SAFETY: we know the value is in-bounds // SAFETY: we know the value is in-bounds
@ -57,18 +53,9 @@ impl Square {
/// Convert from a square index into a [Square] type. /// Convert from a square index into a [Square] type.
#[inline(always)] #[inline(always)]
pub fn from_index(index: usize) -> Self { pub fn from_index(index: usize) -> Self {
Self::try_from_index(index).expect("index out of bouds") 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. Returns [None] if the index is out of
/// bounds.
pub fn try_from_index(index: usize) -> Option<Self> {
if index < Self::NUM_VARIANTS {
// SAFETY: we know the value is in-bounds
Some(unsafe { Self::from_index_unchecked(index) })
} else {
None
}
} }
/// Convert from a square index into a [Square] type, no bounds checking. /// Convert from a square index into a [Square] type, no bounds checking.
@ -120,6 +107,23 @@ impl Square {
} }
} }
/// Convert an en-passant target square segment of a FEN string to an optional [Square].
impl FromFen for Option<Square> {
type Err = Error;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let res = match s.as_bytes() {
[b'-'] => None,
[file @ b'a'..=b'h', rank @ b'1'..=b'8'] => Some(Square::new(
File::from_index((file - b'a') as usize),
Rank::from_index((rank - b'1') as usize),
)),
_ => return Err(Error::InvalidFen),
};
Ok(res)
}
}
/// Shift the square's index left by the amount given. /// Shift the square's index left by the amount given.
impl std::ops::Shl<usize> for Square { impl std::ops::Shl<usize> for Square {
type Output = Square; type Output = Square;

144
src/build.rs Normal file
View file

@ -0,0 +1,144 @@
use std::io::{Result, Write};
pub mod board;
pub mod error;
pub mod movegen;
pub mod utils;
use crate::{
board::{Bitboard, Color, File, Square},
movegen::{
naive::{
king::king_moves,
knight::knight_moves,
pawn::{pawn_captures, pawn_moves},
},
wizardry::generation::{generate_bishop_magics, generate_rook_magics},
Magic,
},
};
fn print_magics(out: &mut dyn Write, var_name: &str, magics: &[Magic]) -> Result<()> {
writeln!(out, "static {}: [Magic; {}] = [", var_name, magics.len())?;
for magic in magics.iter() {
writeln!(
out,
" Magic{{magic: {}, offset: {}, mask: Bitboard({}), shift: {},}},",
magic.magic, magic.offset, magic.mask.0, magic.shift
)?;
}
writeln!(out, "];")?;
Ok(())
}
fn print_boards(out: &mut dyn Write, var_name: &str, boards: &[Bitboard]) -> Result<()> {
writeln!(out, "static {}: [Bitboard; {}] = [", var_name, boards.len())?;
for board in boards.iter().cloned() {
writeln!(out, " Bitboard({}),", board.0)?;
}
writeln!(out, "];")?;
Ok(())
}
fn print_double_sided_boards(
out: &mut dyn Write,
var_name: &str,
white_boards: &[Bitboard],
black_boards: &[Bitboard],
) -> Result<()> {
assert_eq!(white_boards.len(), black_boards.len());
writeln!(
out,
"static {}: [[Bitboard; {}]; 2] = [",
var_name,
white_boards.len()
)?;
for color in Color::iter() {
let boards = if color == Color::White {
white_boards
} else {
black_boards
};
writeln!(out, " [")?;
for square in Square::iter() {
writeln!(out, " Bitboard({}),", boards[square.index()].0)?;
}
writeln!(out, " ],")?;
}
writeln!(out, "];")?;
Ok(())
}
#[allow(clippy::redundant_clone)]
fn main() -> Result<()> {
// FIXME: rerun-if-changed directives
let out_dir = std::env::var_os("OUT_DIR").unwrap();
let magic_path = std::path::Path::new(&out_dir).join("magic_tables.rs");
let mut out = std::fs::File::create(&magic_path).unwrap();
let rng = random::default().seed([12, 27]);
{
let (magics, moves) = generate_bishop_magics(&mut rng.clone());
print_magics(&mut out, "BISHOP_MAGICS", &magics)?;
print_boards(&mut out, "BISHOP_MOVES", &moves)?;
}
{
let (magics, moves) = generate_rook_magics(&mut rng.clone());
print_magics(&mut out, "ROOK_MAGICS", &magics)?;
print_boards(&mut out, "ROOK_MOVES", &moves)?;
}
{
let moves: Vec<_> = Square::iter().map(knight_moves).collect();
print_boards(&mut out, "KNIGHT_MOVES", &moves)?;
}
{
let white_moves: Vec<_> = Square::iter()
.map(|square| pawn_moves(Color::White, square, Bitboard::EMPTY))
.collect();
let black_moves: Vec<_> = Square::iter()
.map(|square| pawn_moves(Color::Black, square, Bitboard::EMPTY))
.collect();
print_double_sided_boards(&mut out, "PAWN_MOVES", &white_moves, &black_moves)?;
let white_attacks: Vec<_> = Square::iter()
.map(|square| pawn_captures(Color::White, square))
.collect();
let black_attacks: Vec<_> = Square::iter()
.map(|square| pawn_captures(Color::Black, square))
.collect();
print_double_sided_boards(&mut out, "PAWN_ATTACKS", &white_attacks, &black_attacks)?;
}
{
let moves: Vec<_> = Square::iter().map(king_moves).collect();
print_boards(&mut out, "KING_MOVES", &moves)?;
let king_blockers: Vec<_> = Color::iter()
.map(|color| {
Square::new(File::F, color.first_rank()) | Square::new(File::G, color.first_rank())
})
.collect();
let queen_blockers: Vec<_> = Color::iter()
.map(|color| {
Square::new(File::B, color.first_rank())
| Square::new(File::C, color.first_rank())
| Square::new(File::D, color.first_rank())
})
.collect();
print_boards(&mut out, "KING_SIDE_CASTLE_BLOCKERS", &king_blockers)?;
print_boards(&mut out, "QUEEN_SIDE_CASTLE_BLOCKERS", &queen_blockers)?;
}
// Include the generated files now that the build script has run.
println!("cargo:rustc-cfg=generated_boards");
// Run the build script only if something in move generation might have changed.
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=movegen/naive/");
println!("cargo:rerun-if-changed=movegen/wizardry/");
Ok(())
}

19
src/error.rs Normal file
View file

@ -0,0 +1,19 @@
/// A singular type for all errors that could happen when using this library.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Error {
InvalidFen,
InvalidPosition,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let error_msg = match self {
Self::InvalidFen => "Invalid FEN input",
Self::InvalidPosition => "Invalid position",
};
write!(f, "{}", error_msg)
}
}
impl std::error::Error for Error {}

View file

@ -1,244 +0,0 @@
use crate::board::{
CastleRights, ChessBoard, ChessBoardBuilder, Color, File, Piece, Rank, Square, ValidationError,
};
/// A trait to mark items that can be converted from a FEN input.
pub trait FromFen: Sized {
type Err;
fn from_fen(s: &str) -> Result<Self, Self::Err>;
}
/// A singular type for all errors that could happen during FEN parsing.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FenError {
/// Invalid FEN input.
InvalidFen,
/// Invalid chess position.
InvalidPosition(ValidationError),
}
impl std::fmt::Display for FenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidFen => write!(f, "invalid FEN input"),
Self::InvalidPosition(err) => write!(f, "invalid chess position: {}", err),
}
}
}
impl std::error::Error for FenError {}
/// Allow converting a [ValidationError] into [FenError], for use with the '?' operator.
impl From<ValidationError> for FenError {
fn from(err: ValidationError) -> Self {
Self::InvalidPosition(err)
}
}
/// Convert the castling rights segment of a FEN string to an array of [CastleRights].
impl FromFen for [CastleRights; Color::NUM_VARIANTS] {
type Err = FenError;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
if s.len() > 4 {
return Err(FenError::InvalidFen);
}
let mut res = [CastleRights::NoSide; Color::NUM_VARIANTS];
if s == "-" {
return Ok(res);
}
for b in s.chars() {
let color = if b.is_uppercase() {
Color::White
} else {
Color::Black
};
let rights = &mut res[color.index()];
match b {
'k' | 'K' => *rights = rights.with_king_side(),
'q' | 'Q' => *rights = rights.with_queen_side(),
_ => return Err(FenError::InvalidFen),
}
}
Ok(res)
}
}
/// Convert a side to move segment of a FEN string to a [Color].
impl FromFen for Color {
type Err = FenError;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let res = match s {
"w" => Color::White,
"b" => Color::Black,
_ => return Err(FenError::InvalidFen),
};
Ok(res)
}
}
/// Convert an en-passant target square segment of a FEN string to an optional [Square].
impl FromFen for Option<Square> {
type Err = FenError;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let res = match s.as_bytes() {
[b'-'] => None,
[file @ b'a'..=b'h', rank @ b'1'..=b'8'] => Some(Square::new(
File::from_index((file - b'a') as usize),
Rank::from_index((rank - b'1') as usize),
)),
_ => return Err(FenError::InvalidFen),
};
Ok(res)
}
}
/// Convert a piece in FEN notation to a [Piece].
impl FromFen for Piece {
type Err = FenError;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let res = match s {
"p" | "P" => Self::Pawn,
"n" | "N" => Self::Knight,
"b" | "B" => Self::Bishop,
"r" | "R" => Self::Rook,
"q" | "Q" => Self::Queen,
"k" | "K" => Self::King,
_ => return Err(FenError::InvalidFen),
};
Ok(res)
}
}
/// Return a [ChessBoard] from the given FEN string.
impl FromFen for ChessBoard {
type Err = FenError;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
let mut split = s.split_ascii_whitespace();
let piece_placement = split.next().ok_or(FenError::InvalidFen)?;
let side_to_move = split.next().ok_or(FenError::InvalidFen)?;
let castling_rights = split.next().ok_or(FenError::InvalidFen)?;
let en_passant_square = split.next().ok_or(FenError::InvalidFen)?;
let half_move_clock = split.next().ok_or(FenError::InvalidFen)?;
let full_move_counter = split.next().ok_or(FenError::InvalidFen)?;
let mut builder = ChessBoardBuilder::new();
let castle_rights = <[CastleRights; Color::NUM_VARIANTS]>::from_fen(castling_rights)?;
for color in Color::iter() {
builder.with_castle_rights(castle_rights[color.index()], color);
}
builder.with_current_player(FromFen::from_fen(side_to_move)?);
if let Some(square) = FromFen::from_fen(en_passant_square)? {
builder.with_en_passant(square);
};
let half_move_clock = half_move_clock
.parse::<_>()
.map_err(|_| FenError::InvalidFen)?;
builder.with_half_move_clock(half_move_clock);
let full_move_counter = full_move_counter
.parse::<_>()
.map_err(|_| FenError::InvalidFen)?;
builder.with_turn_count(full_move_counter);
{
let mut rank: usize = 8;
for rank_str in piece_placement.split('/') {
rank -= 1;
let mut file: usize = 0;
for c in rank_str.chars() {
let color = if c.is_uppercase() {
Color::White
} else {
Color::Black
};
let piece = match c {
digit @ '1'..='8' => {
// Unwrap is fine since this arm is only matched by digits
file += digit.to_digit(10).unwrap() as usize;
continue;
}
_ => FromFen::from_fen(&c.to_string())?,
};
// Only need to worry about underflow since those are `usize` values.
if file >= 8 || rank >= 8 {
return Err(FenError::InvalidFen);
};
let square = Square::new(File::from_index(file), Rank::from_index(rank));
builder[square] = Some((piece, color));
file += 1;
}
// We haven't read exactly 8 files.
if file != 8 {
return Err(FenError::InvalidFen);
}
}
// We haven't read exactly 8 ranks
if rank != 0 {
return Err(FenError::InvalidFen);
}
};
Ok(builder.try_into()?)
}
}
#[cfg(test)]
mod test {
use crate::board::Move;
use super::*;
#[test]
fn default_position() {
let default_position = ChessBoard::default();
assert_eq!(
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
.unwrap(),
default_position
);
}
#[test]
fn en_passant() {
// Start from default position
let mut position = ChessBoard::default();
position.play_move_inplace(Move::new(Square::E2, Square::E4, None));
assert_eq!(
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
.unwrap(),
position
);
// And now c5
position.play_move_inplace(Move::new(Square::C7, Square::C5, None));
assert_eq!(
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
.unwrap(),
position
);
// Finally, Nf3
position.play_move_inplace(Move::new(Square::G1, Square::F3, None));
assert_eq!(
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ")
.unwrap(),
position
);
}
}

View file

@ -1,4 +1,4 @@
pub mod board; pub mod board;
pub mod fen; pub mod error;
pub mod movegen; pub mod movegen;
pub mod utils; pub mod utils;

67
src/movegen/magic/mod.rs Normal file
View file

@ -0,0 +1,67 @@
use crate::board::Bitboard;
/// A type representing the magic board indexing a given [crate::board::Square].
pub struct Magic {
/// Magic number.
pub(crate) magic: u64,
/// Base offset into the magic square table.
pub(crate) offset: usize,
/// Mask to apply to the blocker board before applying the magic.
pub(crate) mask: Bitboard,
/// Length of the resulting mask after applying the magic.
pub(crate) shift: u8,
}
impl Magic {
pub fn get_index(&self, blockers: Bitboard) -> usize {
let relevant_occupancy = (blockers & self.mask).0;
let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize;
base_index + self.offset
}
}
#[cfg(generated_boards)]
mod moves;
pub use moves::*;
#[cfg(not(generated_boards))]
#[allow(unused_variables)]
mod moves {
use crate::board::{Bitboard, Color, Square};
pub fn quiet_pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn knight_moves(square: Square) -> Bitboard {
unreachable!()
}
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard {
unreachable!()
}
pub fn king_moves(square: Square) -> Bitboard {
unreachable!()
}
pub fn king_side_castle_blockers(color: Color) -> Bitboard {
unreachable!()
}
pub fn queen_side_castle_blockers(color: Color) -> Bitboard {
unreachable!()
}
}

View file

@ -0,0 +1,71 @@
use super::Magic;
use crate::board::{Bitboard, Color, Square};
include!(concat!(env!("OUT_DIR"), "/magic_tables.rs"));
pub fn quiet_pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
// If there is a piece in front of the pawn, it can't advance
if !(color.backward_direction().move_board(blockers) & square).is_empty() {
return Bitboard::EMPTY;
}
// SAFETY: we know the values are in-bounds
unsafe {
*PAWN_MOVES
.get_unchecked(color.index())
.get_unchecked(square.index())
}
}
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
// SAFETY: we know the values are in-bounds
let attacks = unsafe {
*PAWN_ATTACKS
.get_unchecked(color.index())
.get_unchecked(square.index())
};
quiet_pawn_moves(color, square, blockers) | attacks
}
pub fn knight_moves(square: Square) -> Bitboard {
// SAFETY: we know the values are in-bounds
unsafe { *KNIGHT_MOVES.get_unchecked(square.index()) }
}
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
// SAFETY: we know the values are in-bounds
unsafe {
let index = BISHOP_MAGICS
.get_unchecked(square.index())
.get_index(blockers);
*BISHOP_MOVES.get_unchecked(index)
}
}
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
// SAFETY: we know the values are in-bounds
unsafe {
let index = ROOK_MAGICS
.get_unchecked(square.index())
.get_index(blockers);
*ROOK_MOVES.get_unchecked(index)
}
}
pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard {
bishop_moves(square, blockers) | rook_moves(square, blockers)
}
pub fn king_moves(square: Square) -> Bitboard {
// SAFETY: we know the values are in-bounds
unsafe { *KING_MOVES.get_unchecked(square.index()) }
}
pub fn king_side_castle_blockers(color: Color) -> Bitboard {
// SAFETY: we know the values are in-bounds
unsafe { *KING_SIDE_CASTLE_BLOCKERS.get_unchecked(color.index()) }
}
pub fn queen_side_castle_blockers(color: Color) -> Bitboard {
// SAFETY: we know the values are in-bounds
unsafe { *QUEEN_SIDE_CASTLE_BLOCKERS.get_unchecked(color.index()) }
}

View file

@ -1,9 +1,9 @@
// Magic bitboard
pub mod magic;
pub use magic::*;
// Naive move generation // Naive move generation
mod naive; pub mod naive;
// Magic bitboard generation // Magic bitboard generation
mod wizardry; pub(crate) mod wizardry;
// Magic bitboard definitions
mod moves;
pub use moves::*;

View file

@ -1,147 +0,0 @@
use std::sync::OnceLock;
use crate::{
board::{Bitboard, Color, File, Square},
movegen::{
naive,
wizardry::{
generate_bishop_magics, generate_rook_magics, MagicMoves, BISHOP_SEED, ROOK_SEED,
},
},
utils::RandGen,
};
// A pre-rolled RNG for magic bitboard generation, using pre-determined values.
struct PreRolledRng {
numbers: [u64; Square::NUM_VARIANTS],
current_index: usize,
}
impl PreRolledRng {
pub fn new(numbers: [u64; Square::NUM_VARIANTS]) -> Self {
Self {
numbers,
current_index: 0,
}
}
}
impl RandGen for PreRolledRng {
fn gen(&mut self) -> u64 {
// We roll 3 numbers per square to bitwise-and them together.
// Just return the same one 3 times as a work-around.
let res = self.numbers[self.current_index / 3];
self.current_index += 1;
res
}
}
/// Compute the set of possible non-attack moves for a pawn on a [Square], given its [Color] and
/// set of blockers.
pub fn pawn_quiet_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
static PAWN_MOVES: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> =
OnceLock::new();
// If there is a piece in front of the pawn, it can't advance
if !(color.backward_direction().move_board(blockers) & square).is_empty() {
return Bitboard::EMPTY;
}
PAWN_MOVES.get_or_init(|| {
let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS];
for color in Color::iter() {
for square in Square::iter() {
res[color.index()][square.index()] =
naive::pawn_moves(color, square, Bitboard::EMPTY);
}
}
res
})[color.index()][square.index()]
}
/// Compute the set of possible attacks for a pawn on a [Square], given its [Color].
pub fn pawn_attacks(color: Color, square: Square) -> Bitboard {
static PAWN_ATTACKS: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> =
OnceLock::new();
PAWN_ATTACKS.get_or_init(|| {
let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS];
for color in Color::iter() {
for square in Square::iter() {
res[color.index()][square.index()] = naive::pawn_captures(color, square);
}
}
res
})[color.index()][square.index()]
}
/// Compute the set of possible moves for a pawn on a [Square], given its [Color] and set of
/// blockers.
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
pawn_quiet_moves(color, square, blockers) | pawn_attacks(color, square)
}
/// Compute the set of possible moves for a knight on a [Square].
pub fn knight_moves(square: Square) -> Bitboard {
static KNIGHT_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new();
KNIGHT_MOVES.get_or_init(|| {
let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS];
for square in Square::iter() {
res[square.index()] = naive::knight_moves(square)
}
res
})[square.index()]
}
/// Compute the set of possible moves for a bishop on a [Square], given its set of blockers.
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
static BISHOP_MAGICS: OnceLock<MagicMoves> = OnceLock::new();
BISHOP_MAGICS
.get_or_init(|| {
let (magics, moves) = generate_bishop_magics(&mut PreRolledRng::new(BISHOP_SEED));
// SAFETY: we used the generator function to compute these values
unsafe { MagicMoves::new(magics, moves) }
})
.query(square, blockers)
}
/// Compute the set of possible moves for a rook on a [Square], given its set of blockers.
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
static ROOK_MAGICS: OnceLock<MagicMoves> = OnceLock::new();
ROOK_MAGICS
.get_or_init(|| {
let (magics, moves) = generate_rook_magics(&mut PreRolledRng::new(ROOK_SEED));
// SAFETY: we used the generator function to compute these values
unsafe { MagicMoves::new(magics, moves) }
})
.query(square, blockers)
}
/// Compute the set of possible moves for a queen on a [Square], given its set of blockers.
pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard {
bishop_moves(square, blockers) | rook_moves(square, blockers)
}
/// Compute the set of possible moves for a king on a [Square].
pub fn king_moves(square: Square) -> Bitboard {
static KING_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new();
KING_MOVES.get_or_init(|| {
let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS];
for square in Square::iter() {
res[square.index()] = naive::king_moves(square)
}
res
})[square.index()]
}
/// Compute the squares which should be empty for a king-side castle of the given [Color].
pub fn kind_side_castle_blockers(color: Color) -> Bitboard {
let rank = color.first_rank();
Square::new(File::F, rank) | Square::new(File::G, rank)
}
/// Compute the squares which should be empty for a queen-side castle of the given [Color].
pub fn queen_side_castle_blockers(color: Color) -> Bitboard {
let rank = color.first_rank();
Square::new(File::B, rank) | Square::new(File::C, rank) | Square::new(File::D, rank)
}

View file

@ -1,6 +1,5 @@
use crate::board::{Bitboard, Direction, Square}; use crate::board::{Bitboard, Direction, Square};
/// Compute a bishop's movement given a set of blockers that cannot be moved past.
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard { pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
Direction::iter_bishop() Direction::iter_bishop()
.map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers))

View file

@ -1,6 +1,6 @@
use crate::board::{Bitboard, Direction, Square}; use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square};
/// Compute a king's movement. No castling moves included // No castling moves included
pub fn king_moves(square: Square) -> Bitboard { pub fn king_moves(square: Square) -> Bitboard {
let board = square.into_bitboard(); let board = square.into_bitboard();
@ -9,6 +9,20 @@ pub fn king_moves(square: Square) -> Bitboard {
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs) .fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs)
} }
pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard {
let rank = color.first_rank();
let king_side_square = Square::new(File::G, rank);
let queen_side_square = Square::new(File::C, rank);
match castle_rights {
CastleRights::NoSide => Bitboard::EMPTY,
CastleRights::KingSide => king_side_square.into_bitboard(),
CastleRights::QueenSide => queen_side_square.into_bitboard(),
CastleRights::BothSides => king_side_square | queen_side_square,
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -169,4 +183,40 @@ mod test {
| Square::F6 | 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
);
}
} }

View file

@ -1,6 +1,5 @@
use crate::board::{Bitboard, Direction, Square}; use crate::board::{Bitboard, Direction, Square};
/// Compute a knight's movement.
pub fn knight_moves(square: Square) -> Bitboard { pub fn knight_moves(square: Square) -> Bitboard {
let board = square.into_bitboard(); let board = square.into_bitboard();

View file

@ -1,14 +1,5 @@
pub mod bishop; pub mod bishop;
pub use bishop::*;
pub mod king; pub mod king;
pub use king::*;
pub mod knight; pub mod knight;
pub use knight::*;
pub mod pawn; pub mod pawn;
pub use pawn::*;
pub mod rook; pub mod rook;
pub use rook::*;

View file

@ -1,6 +1,5 @@
use crate::board::{Bitboard, Color, Direction, Rank, Square}; use crate::board::{Bitboard, Color, Direction, Rank, Square};
/// Compute a pawn's movement given its color, and a set of blockers that cannot be moved past.
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard { pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) {
return Bitboard::EMPTY; return Bitboard::EMPTY;
@ -22,7 +21,6 @@ pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard
} }
} }
/// Computes the set of squares a pawn can capture, given its color.
pub fn pawn_captures(color: Color, square: Square) -> Bitboard { pub fn pawn_captures(color: Color, square: Square) -> Bitboard {
if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) { if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) {
return Bitboard::EMPTY; return Bitboard::EMPTY;
@ -38,6 +36,15 @@ pub fn pawn_captures(color: Color, square: Square) -> Bitboard {
attack_west | attack_east attack_west | attack_east
} }
pub fn en_passant_origins(square: Square) -> Bitboard {
let board = square.into_bitboard();
let origin_west = Direction::West.move_board(board);
let origin_east = Direction::East.move_board(board);
origin_west | origin_east
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -113,4 +120,14 @@ mod test {
Square::G6.into_bitboard() 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());
}
} }

View file

@ -1,6 +1,5 @@
use crate::board::{Bitboard, Direction, Square}; use crate::board::{Bitboard, Direction, Square};
/// Compute a rook's movement given a set of blockers that cannot be moved past.
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard { pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
Direction::iter_rook() Direction::iter_rook()
.map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers)) .map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers))

View file

@ -1,22 +1,23 @@
use crate::board::{Bitboard, Square}; use crate::board::{Bitboard, Square};
use crate::movegen::naive::{bishop_moves, rook_moves}; use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves};
use crate::utils::RandGen; use crate::movegen::Magic;
use super::mask::{generate_bishop_mask, generate_rook_mask}; use super::mask::{generate_bishop_mask, generate_rook_mask};
use super::Magic;
type MagicGenerationType = (Vec<Magic>, Vec<Bitboard>); type MagicGenerationType = (Vec<Magic>, Vec<Bitboard>);
pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType { #[allow(unused)] // FIXME: remove when used
pub fn generate_bishop_magics(rng: &mut dyn random::Source) -> MagicGenerationType {
generate_magics(rng, generate_bishop_mask, bishop_moves) generate_magics(rng, generate_bishop_mask, bishop_moves)
} }
pub fn generate_rook_magics(rng: &mut dyn RandGen) -> MagicGenerationType { #[allow(unused)] // FIXME: remove when used
pub fn generate_rook_magics(rng: &mut dyn random::Source) -> MagicGenerationType {
generate_magics(rng, generate_rook_mask, rook_moves) generate_magics(rng, generate_rook_mask, rook_moves)
} }
fn generate_magics( fn generate_magics(
rng: &mut dyn RandGen, rng: &mut dyn random::Source,
mask_fn: impl Fn(Square) -> Bitboard, mask_fn: impl Fn(Square) -> Bitboard,
moves_fn: impl Fn(Square, Bitboard) -> Bitboard, moves_fn: impl Fn(Square, Bitboard) -> Bitboard,
) -> MagicGenerationType { ) -> MagicGenerationType {
@ -25,6 +26,7 @@ fn generate_magics(
for square in Square::iter() { for square in Square::iter() {
let mask = mask_fn(square); let mask = mask_fn(square);
let mut candidate: Magic;
let occupancy_to_moves: Vec<_> = mask let occupancy_to_moves: Vec<_> = mask
.iter_power_set() .iter_power_set()
@ -32,7 +34,7 @@ fn generate_magics(
.collect(); .collect();
'candidate_search: loop { 'candidate_search: loop {
let mut candidate = Magic { candidate = Magic {
magic: magic_candidate(rng), magic: magic_candidate(rng),
offset: 0, offset: 0,
mask, mask,
@ -43,7 +45,7 @@ fn generate_magics(
for (occupancy, moves) in occupancy_to_moves.iter().cloned() { for (occupancy, moves) in occupancy_to_moves.iter().cloned() {
let index = candidate.get_index(occupancy); let index = candidate.get_index(occupancy);
// Non-constructive collision, try with another candidate // Non-constructive collision, try with another candidate
if !candidate_moves[index].is_empty() && candidate_moves[index] != moves { if candidate_moves[index] != Bitboard::EMPTY && candidate_moves[index] != moves {
continue 'candidate_search; continue 'candidate_search;
} }
candidate_moves[index] = moves; candidate_moves[index] = moves;
@ -51,8 +53,8 @@ fn generate_magics(
// We have filled all candidate boards, record the correct offset and add the moves // We have filled all candidate boards, record the correct offset and add the moves
candidate.offset = boards.len(); candidate.offset = boards.len();
magics.push(candidate);
boards.append(&mut candidate_moves); boards.append(&mut candidate_moves);
magics.push(candidate);
break; break;
} }
} }
@ -60,7 +62,6 @@ fn generate_magics(
(magics, boards) (magics, boards)
} }
fn magic_candidate(rng: &mut dyn RandGen) -> u64 { fn magic_candidate(rng: &mut dyn random::Source) -> u64 {
// Few bits makes for better candidates rng.read_u64() & rng.read_u64() & rng.read_u64()
rng.gen() & rng.gen() & rng.gen()
} }

View file

@ -1,7 +1,6 @@
use crate::board::{Bitboard, File, Rank, Square}; use crate::board::{Bitboard, File, Rank, Square};
use crate::movegen::naive::{bishop::bishop_moves, 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 { pub fn generate_bishop_mask(square: Square) -> Bitboard {
let rays = bishop_moves(square, Bitboard::EMPTY); let rays = bishop_moves(square, Bitboard::EMPTY);
@ -13,7 +12,6 @@ pub fn generate_bishop_mask(square: Square) -> Bitboard {
rays - mask rays - mask
} }
/// Compute the relevancy mask for a rook on a given [Square].
pub fn generate_rook_mask(square: Square) -> Bitboard { pub fn generate_rook_mask(square: Square) -> Bitboard {
let rays = rook_moves(square, Bitboard::EMPTY); let rays = rook_moves(square, Bitboard::EMPTY);

View file

@ -1,266 +1,2 @@
mod generation; pub(crate) mod generation;
pub(super) use generation::*;
mod mask; mod mask;
use crate::board::{Bitboard, Square};
/// A type representing the magic board indexing a given [crate::board::Square].
#[derive(Clone, Debug)]
pub(super) struct Magic {
/// Magic number.
pub(self) magic: u64,
/// Base offset into the magic square table.
pub(self) offset: usize,
/// Mask to apply to the blocker board before applying the magic.
pub(self) mask: Bitboard,
/// Length of the resulting mask after applying the magic.
pub(self) shift: u8,
}
impl Magic {
/// Compute the index into the magics database for this set of `blockers`.
pub fn get_index(&self, blockers: Bitboard) -> usize {
let relevant_occupancy = (blockers & self.mask).0;
let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize;
base_index + self.offset
}
}
/// A type encapsulating a database of [Magic] bitboard moves.
#[derive(Clone, Debug)]
pub(crate) struct MagicMoves {
magics: Vec<Magic>,
moves: Vec<Bitboard>,
}
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<Magic>, moves: Vec<Bitboard>) -> Self {
Self { magics, moves }
}
/// Get the set of valid moves for a piece standing on a [Square], given a set of blockers.
pub fn query(&self, square: Square, blockers: Bitboard) -> Bitboard {
// SAFETY: indices are in range by construction
unsafe {
let index = self
.magics
.get_unchecked(square.index())
.get_index(blockers);
*self.moves.get_unchecked(index)
}
}
}
// region:sourcegen
/// A set of magic numbers for bishop move generation.
pub(crate) const BISHOP_SEED: [u64; Square::NUM_VARIANTS] = [
4634226011293351952,
6918109887683821586,
76562328660738184,
7242919606867744800,
13871652069997347969,
1171657252671901696,
147001475087730752,
1752045392763101248,
288406435526639744,
4612213818402029888,
9808848818951710728,
9223394181731320840,
54047645651435648,
9224780030482579712,
9049059098626048,
1442330840700035221,
1126037887157508,
1153488887004529665,
290485130928332936,
9226749771011592258,
148636405693678112,
2260596997758984,
73470481646424336,
2341907012146823680,
2314955761652335121,
2265544246165632,
13598764778463296,
563087425962496,
563087425962048,
2163991853573081088,
567353402270020,
6488844433713538048,
288810987011448834,
11830884701569344,
2747549955031826688,
35734665298432,
18025943920672800,
292892945404789012,
1153520472160470528,
2260949167801860,
155446765112299521,
379008324189818944,
4616480181217005576,
576461027453960704,
2450556349601564416,
1160556519943569536,
4612900059821375552,
5477089643453251617,
9223532084785594632,
2810391870219355200,
36594222015453185,
4612011546951352320,
2392883590201344,
1152956706186200064,
9009415592510464,
81077999302148128,
576746627483043968,
301267327789056,
39586720976896,
720878306081243648,
9223512777841312257,
5764609859566698625,
8088544233436348496,
4612856276794474560,
];
/// A set of magic numbers for rook move generation.
pub(crate) const ROOK_SEED: [u64; Square::NUM_VARIANTS] = [
180144122814791812,
10448386594766422036,
9403533616331358856,
108095189301858304,
72076290316044288,
36066182562054145,
4647717564258980096,
13979173385364603396,
4620833992751489152,
297800804633419904,
578009002156298240,
2450099003505838082,
1175721046778052864,
20406952999780864,
1175861788231598592,
36169538802827392,
288371663414771712,
423313050501155,
604731668136450,
580261214513399808,
297661437206136832,
1750211954976489600,
9020393411186696,
9259543770406356001,
44532368556032,
10376381507760693256,
52778707714176,
4612829512676149248,
1882513444629184528,
2369460754144428160,
9223380850137104901,
2666413562481640036,
141012643087392,
16735517094631719424,
17594358702087,
2344264412262574084,
422813768878080,
1126450811896320,
54466576291772936,
42784758060548372,
292874851780165648,
18015364885839937,
282644818493504,
1184447393488764944,
4649966632473477184,
563499910594566,
17632049496086,
18502729728001,
140742121013504,
9711024139665536,
246293205270784,
290772515771392256,
9230131836490350720,
73326432604127360,
453174886517643776,
2396271245728563712,
324259242966026501,
288953994406543363,
1153557061259362338,
40533496293515441,
1407392197644307,
1729945211427624002,
587808330812164100,
9511606812128903298,
];
// endregion:sourcegen
#[cfg(test)]
mod test {
use std::fmt::Write as _;
use super::*;
use crate::utils::SimpleRng;
fn split_twice<'a>(
text: &'a str,
start_marker: &str,
end_marker: &str,
) -> Option<(&'a str, &'a str, &'a str)> {
let (prefix, rest) = text.split_once(start_marker)?;
let (mid, suffix) = rest.split_once(end_marker)?;
Some((prefix, mid, suffix))
}
fn array_string(piece_type: &str, values: &[Magic]) -> String {
let inner = || -> Result<String, std::fmt::Error> {
let mut res = String::new();
writeln!(
&mut res,
"/// A set of magic numbers for {} move generation.",
piece_type
)?;
writeln!(
&mut res,
"pub(crate) const {}_SEED: [u64; Square::NUM_VARIANTS] = [",
piece_type.to_uppercase()
)?;
for magic in values {
writeln!(&mut res, " {},", magic.magic)?;
}
writeln!(&mut res, "];")?;
Ok(res)
};
inner().unwrap()
}
#[test]
#[ignore = "slow"]
// Regenerates the magic bitboard numbers.
fn regen_magic_seeds() {
// We only care about the magics, the moves can be recomputed at runtime ~cheaply.
let (bishop_magics, _) = generate_bishop_magics(&mut SimpleRng::new());
let (rook_magics, _) = generate_rook_magics(&mut SimpleRng::new());
let original_text = std::fs::read_to_string(file!()).unwrap();
let bishop_array = array_string("bishop", &bishop_magics[..]);
let rook_array = array_string("rook", &rook_magics[..]);
let new_text = {
let start_marker = "// region:sourcegen\n";
let end_marker = "// endregion:sourcegen\n";
let (prefix, _, suffix) =
split_twice(&original_text, start_marker, end_marker).unwrap();
format!("{prefix}{start_marker}{bishop_array}\n{rook_array}{end_marker}{suffix}")
};
if new_text != original_text {
std::fs::write(file!(), new_text).unwrap();
panic!("source was not up-to-date")
}
}
}

View file

@ -1,5 +1,2 @@
pub(crate) mod rand;
pub(crate) use rand::*;
pub mod static_assert; pub mod static_assert;
pub use static_assert::*; pub use static_assert::*;

View file

@ -1,49 +0,0 @@
/// A trait to represent RNG for u64 values.
pub trait RandGen {
fn gen(&mut self) -> u64;
}
// A simple pcg64_fast RNG implementation, for code-generation.
#[cfg(test)]
pub struct SimpleRng(u128);
#[cfg(test)]
impl SimpleRng {
pub fn new() -> Self {
Self(0xcafef00dd15ea5e5 | 1) // https://xkcd.com/221/
}
pub fn gen(&mut self) -> u64 {
const MULTIPLIER: u128 = 0x2360_ED05_1FC6_5DA4_4385_DF64_9FCC_F645;
const XSHIFT: u32 = 64; // (128 - 64 + 64) / 2
const ROTATE: u32 = 122; // 128 - 6
self.0 = self.0.wrapping_mul(MULTIPLIER);
let rot = (self.0 >> ROTATE) as u32;
let xsl = (self.0 >> XSHIFT) as u64 ^ (self.0 as u64);
xsl.rotate_right(rot)
}
}
#[cfg(test)]
impl RandGen for SimpleRng {
fn gen(&mut self) -> u64 {
self.gen()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn rng() {
let mut rng = SimpleRng::new();
assert_eq!(rng.gen(), 64934999470316615);
assert_eq!(rng.gen(), 15459456780870779090);
assert_eq!(rng.gen(), 13715484424881807779);
assert_eq!(rng.gen(), 17718572936700675021);
assert_eq!(rng.gen(), 14587996314750246637);
}
}

View file

@ -15,9 +15,12 @@
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! static_assert { macro_rules! static_assert {
($($tt:tt)*) => { ($condition:expr) => {
// Based on the latest one in `rustc`'s one before it was [removed].
//
// [removed]: https://github.com/rust-lang/rust/commit/c2dad1c6b9f9636198d7c561b47a2974f5103f6d
#[allow(dead_code)] #[allow(dead_code)]
const _: () = assert!($($tt)*); const _: () = [()][!($condition) as usize];
}; };
} }

View file

@ -1,41 +1,17 @@
import enum
import gdb
import gdb.printing import gdb.printing
def optional(constructor, val):
try:
return constructor(val["Some"]["__0"])
except gdb.error:
return None
def print_opt(val):
return "(None)" if val is None else str(val)
class Square(object): class Square(object):
""" """
Python representation of a 'seer::board::square::Square' raw value. Wrapper around GDB's representation of a 'seer::board::square::Square' in
memory.
""" """
FILES = list(map(lambda n: chr(ord("A") + n), range(8))) FILES = list(map(lambda n: chr(ord('A') + n), range(8)))
RANKS = list(map(lambda n: str(n + 1), range(8))) RANKS = list(map(lambda n: str(n + 1), range(8)))
def __init__(self, val): def __init__(self, val):
if isinstance(val, Square):
val = val._val
self._val = val self._val = val
@classmethod
def from_gdb(cls, val):
return cls(int(val))
@classmethod
def from_file_rank(cls, file, rank):
return cls(file * 8 + rank)
def __str__(self): def __str__(self):
return self.FILES[self.file] + self.RANKS[self.rank] return self.FILES[self.file] + self.RANKS[self.rank]
@ -47,373 +23,56 @@ class Square(object):
def file(self): def file(self):
return int(self._val) // 8 return int(self._val) // 8
class Bitboard(object): class Bitboard(object):
""" """
Python representation of a 'seer::board::bitboard::Bitboard' raw value. Wrapper around GDB's representation of a 'seer::board::bitboard::Bitboard'
in memory.
""" """
def __init__(self, val): def __init__(self, val):
if isinstance(val, Bitboard):
val = val._val
self._val = val self._val = val
@classmethod
def from_gdb(cls, val):
return cls(int(val["__0"]))
def __str__(self): def __str__(self):
return "[" + ", ".join(map(str, self.squares)) + "]" return "[" + ", ".join(map(str, self.squares)) + "]"
def at(self, square):
return bool(self._val & (1 << square._val))
@property @property
def squares(self): def squares(self):
n = self._val n = int(self._val["__0"])
while n: while n:
b = n & (~n + 1) b = n & (~n+1)
yield Square(b.bit_length() - 1) yield Square(b.bit_length() - 1)
n ^= b n ^= b
class CastleRights(enum.IntEnum):
"""
Python representation of a 'seer::board::castle_rights::CastleRights' raw value.
"""
# Should be kept in sync with the enum in `color.rs`
NO_SIDE = 0
KING_SIDE = 1
QUEEN_SIDE = 2
BOTH_SIDES = 3
@classmethod
def from_gdb(cls, val):
return cls(int(val))
def __str__(self):
return self.name.title().replace("_", "")
class Color(enum.IntEnum):
"""
Python representation of a 'seer::board::color::Color' raw value.
"""
# Should be kept in sync with the enum in `color.rs`
WHITE = 0
BLACK = 1
@classmethod
def from_gdb(cls, val):
return cls(int(val))
def __str__(self):
return self.name.title()
class File(enum.IntEnum):
"""
Python representation of a 'seer::board::file::File' raw value.
"""
# Should be kept in sync with the enum in `file.rs`
A = 0
B = 1
C = 2
D = 3
E = 4
F = 5
G = 6
H = 7
@classmethod
def from_gdb(cls, val):
return cls(int(val))
def __str__(self):
return self.name.title()
class Rank(enum.IntEnum):
"""
Python representation of a 'seer::board::rank::Rank' raw value.
"""
# Should be kept in sync with the enum in `rank.rs`
First = 0
Second = 1
Third = 2
Fourth = 3
Fifth = 4
Sixth = 5
Seventh = 6
Eighth = 7
@classmethod
def from_gdb(cls, val):
return cls(int(val))
def __str__(self):
return self.name.title()
class Piece(enum.IntEnum):
"""
Python representation of a 'seer::board::piece::Piece' raw value.
"""
# Should be kept in sync with the enum in `piece.rs`
KING = 0
QUEEN = 1
ROOK = 2
BISHOP = 3
KNIGHT = 4
PAWN = 5
@classmethod
def from_gdb(cls, val):
return cls(int(val))
def __str__(self):
return self.name.title()
class Move(object):
"""
Wrapper around GDB's representation of a 'seer::board::move::Move'
in memory.
"""
def __init__(self, start, destination, promotion):
self._start = Square(start)
self._destination = Square(destination)
self._promotion = Piece(promotion)
@classmethod
def from_gdb(cls, val):
start = Square(int(val["start"]))
destination = Square(int(val["destination"]))
promotion = optional(Piece.from_gdb, val["promotion"])
cls(start, destination, promotion)
@property
def start(self):
return self._start
@property
def destination(self):
return self._destination
@property
def promotion(self):
return self._promotion
def __str__(self):
KEYS = [
"start",
"destination",
"promotion",
]
indent = lambda s: " " + s
values = [key + ": " + print_opt(getattr(self, key)) + ",\n" for key in KEYS]
return "Move{\n" + "".join(map(indent, values)) + "}"
class ChessBoard(object):
"""
Wrapper around GDB's representation of a 'seer::board::chess_board::ChessBoard'
in memory.
"""
def __init__(
self,
piece_occupancy,
color_occupancy,
castle_rights,
half_move_clock,
total_plies,
side,
en_passant,
):
self._piece_occupancy = list(map(Bitboard, piece_occupancy))
self._color_occupancy = list(map(Bitboard, color_occupancy))
self._castle_rights = list(map(CastleRights, castle_rights))
self._half_move_clock = int(half_move_clock)
self._total_plies = int(total_plies)
self._side = Color(side)
self._en_passant = None if en_passant is None else Square(en_passant)
@classmethod
def from_gdb(cls, val):
return cls(
[Bitboard.from_gdb(val["piece_occupancy"][p]) for p in Piece],
[Bitboard.from_gdb(val["color_occupancy"][c]) for c in Color],
[CastleRights.from_gdb(val["castle_rights"][c]) for c in Color],
int(val["half_move_clock"]),
int(val["total_plies"]),
Color.from_gdb(val["side"]),
optional(Square.from_gdb, val["en_passant"]),
)
def at(self, square):
for piece in Piece:
if not self._piece_occupancy[piece].at(square):
continue
for color in Color:
if not self._color_occupancy[color].at(square):
continue
return (piece, color)
return None
def pretty_str(self):
def pretty_piece(piece, color):
return [
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
][piece][color]
board = [
[self.at(Square.from_file_rank(file, rank)) for file in File]
for rank in Rank
]
res = []
res.append(" A B C D E F G H ")
for n, line in reversed(list(enumerate(board, start=1))):
strings = [str(n) + " "]
strings.extend(" " if p is None else pretty_piece(*p) for p in line)
strings.append(" " + str(n))
res.append("|".join(strings))
res.append(" A B C D E F G H ")
res += [
"Half-move clock: " + str(self._half_move_clock),
"Total plies: " + str(self._total_plies),
"Side to play: " + str(self._side),
"En passant: " + print_opt(self._en_passant),
]
return "\n".join(res)
class SquarePrinter(object): class SquarePrinter(object):
"Print a seer::board::square::Square" "Print a seer::board::square::Square"
def __init__(self, val): def __init__(self, val):
self._val = Square.from_gdb(val) self._val = Square(val)
def to_string(self): def to_string(self):
return str(self._val) return str(self._val)
def display_hint(self):
return 'string'
class BitboardPrinter(object): class BitboardPrinter(object):
"Print a seer::board::bitboard::Bitboard" "Print a seer::board::bitboard::Bitboard"
def __init__(self, val): def __init__(self, val):
self._val = Bitboard.from_gdb(val) self._val = Bitboard(val)
def to_string(self): def to_string(self):
return "Bitboard{" + str(self._val)[1:-1] + "}" return "Bitboard{" + str(self._val)[1:-1] + "}"
def display_hint(self):
class CastleRightsPrinter(object): return 'string'
"Print a seer::board::castle_rights::CastleRights"
def __init__(self, val):
self._val = CastleRights.from_gdb(val)
def to_string(self):
return str(self._val)
class ColorPrinter(object):
"Print a seer::board::color::Color"
def __init__(self, val):
self._val = Color.from_gdb(val)
def to_string(self):
return str(self._val)
class FilePrinter(object):
"Print a seer::board::file::File"
def __init__(self, val):
self._val = File.from_gdb(val)
def to_string(self):
return str(self._val)
class RankPrinter(object):
"Print a seer::board::rank::Rank"
def __init__(self, val):
self._val = Rank.from_gdb(val)
def to_string(self):
return str(self._val)
class PiecePrinter(object):
"Print a seer::board::piece::Piece"
def __init__(self, val):
self._val = Piece.from_gdb(val)
def to_string(self):
return str(self._val)
class MovePrinter(object):
"Print a seer::board::move::Move"
def __init__(self, val):
self._val = Move.from_gdb(val)
def to_string(self):
return str(self._val)
class PrintBoard(gdb.Command):
"""
Pretty-print a 'seer::board::chess_board::ChessBoard' as a 2D textual chess board.
"""
def __init__(self):
super(PrintBoard, self).__init__(
"print-board", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
)
def invoke(self, arg, from_tty):
board = ChessBoard.from_gdb(gdb.parse_and_eval(arg))
print(board.pretty_str())
def build_pretty_printer(): def build_pretty_printer():
pp = gdb.printing.RegexpCollectionPrettyPrinter('seer') pp = gdb.printing.RegexpCollectionPrettyPrinter('seer')
pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter) pp.add_printer('BigNum', '^seer::board::square::Square$', SquarePrinter)
pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter) pp.add_printer('BigNum', '^seer::board::bitboard::Bitboard$', BitboardPrinter)
pp.add_printer('CastleRights', '^seer::board::castle_rights::CastleRights$', CastleRightsPrinter)
pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter)
pp.add_printer('File', '^seer::board::file::File$', FilePrinter)
pp.add_printer('Rank', '^seer::board::rank::Rank$', RankPrinter)
pp.add_printer('Piece', '^seer::board::piece::Piece$', ColorPrinter)
pp.add_printer('Move', '^seer::board::move::Move$', MovePrinter)
return pp return pp
def register_commands():
PrintBoard()
gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True) gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True)
register_commands()