Compare commits

...

No commits in common. "b3222276abea45deaf5be7bcbd875f32a7de06a6" and "47f532cd0934286e48d335c87fd1a41b3f7272d8" have entirely different histories.

37 changed files with 3825 additions and 255 deletions

View file

@ -1,31 +0,0 @@
---
kind: pipeline
type: exec
name: abacus checks
steps:
- name: flake check
commands:
- nix flake check
- name: package check
commands:
- nix build
- name: notifiy
commands:
- nix run github:ambroisie/matrix-notifier
environment:
ADDRESS:
from_secret: matrix_homeserver
ROOM:
from_secret: matrix_roomid
USER:
from_secret: matrix_username
PASS:
from_secret: matrix_password
when:
status:
- failure
- success
...

5
.envrc Normal file
View file

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

10
.gitignore vendored
View file

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

31
.woodpecker/check.yml Normal file
View file

@ -0,0 +1,31 @@
labels:
backend: local
steps:
- name: pre-commit check
image: bash
commands:
- nix develop --command pre-commit run --all
- name: nix flake check
image: bash
commands:
- nix flake check
- name: notifiy
image: bash
secrets:
- source: matrix_homeserver
target: address
- source: matrix_roomid
target: room
- source: matrix_username
target: user
- source: matrix_password
target: pass
commands:
- nix run '.#matrix-notifier'
when:
status:
- failure
- success

View file

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

186
flake.nix
View file

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

View file

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

View file

@ -1,6 +1,14 @@
/// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a
/// [Bitboard](crate::board::Bitboard).
pub struct BitboardIterator(pub(crate) u64);
/// [Bitboard].
use crate::board::Bitboard;
pub struct BitboardIterator(u64);
impl BitboardIterator {
pub fn new(board: Bitboard) -> Self {
Self(board.0)
}
}
impl Iterator for BitboardIterator {
type Item = crate::board::Square;

View file

@ -1,8 +1,12 @@
use super::Square;
use crate::utils::static_assert;
mod error;
use error::*;
mod iterator;
use iterator::*;
mod superset;
use superset::*;
/// Use a 64-bit number to represent a chessboard. Each bit is mapped from to a specific square, so
/// that index 0 -> A1, 1 -> A2, ..., 63 -> H8.
@ -63,6 +67,22 @@ impl Bitboard {
pub fn is_empty(self) -> bool {
self == Self::EMPTY
}
/// Return true if there are more than piece in the [Bitboard]. This is faster than testing
/// `board.count() > 1`.
#[inline(always)]
pub fn has_more_than_one(self) -> bool {
(self.0 & (self.0.wrapping_sub(1))) != 0
}
/// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of
/// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that
/// contain all, some, or none of the [Square] that are in the given [Bitboard].
/// If given an empty [Bitboard], yields the empty [Bitboard] back.
#[inline(always)]
pub fn iter_power_set(self) -> impl Iterator<Item = Self> {
BitboardPowerSetIterator::new(self)
}
}
// Ensure zero-cost (at least size-wise) wrapping.
@ -74,13 +94,28 @@ impl Default for Bitboard {
}
}
/// Iterate over the [Square](crate::board::Square) values included in the board.
/// Iterate over the [Square] values included in the board.
impl IntoIterator for Bitboard {
type IntoIter = BitboardIterator;
type Item = Square;
fn into_iter(self) -> Self::IntoIter {
BitboardIterator(self.0)
BitboardIterator::new(self)
}
}
/// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is
/// occupying. Otherwise return `None`.
impl TryInto<Square> for Bitboard {
type Error = IntoSquareError;
fn try_into(self) -> Result<Square, Self::Error> {
let index = match self.count() {
1 => self.0.trailing_zeros() as usize,
0 => return Err(IntoSquareError::EmptyBoard),
_ => return Err(IntoSquareError::TooManySquares),
};
Ok(Square::from_index(index))
}
}
@ -104,6 +139,22 @@ impl std::ops::Shr<usize> for Bitboard {
}
}
/// Treat bitboard as a set of squares, shift each square's index left by the amount given.
impl std::ops::ShlAssign<usize> for Bitboard {
#[inline(always)]
fn shl_assign(&mut self, rhs: usize) {
*self = *self << rhs;
}
}
/// Treat bitboard as a set of squares, shift each square's index right by the amount given.
impl std::ops::ShrAssign<usize> for Bitboard {
#[inline(always)]
fn shr_assign(&mut self, rhs: usize) {
*self = *self >> rhs;
}
}
/// Treat bitboard as a set of squares, and invert the set.
impl std::ops::Not for Bitboard {
type Output = Bitboard;
@ -124,7 +175,7 @@ impl std::ops::BitOr<Bitboard> for Bitboard {
}
}
/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator.
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::BitOr<Square> for Bitboard {
type Output = Bitboard;
@ -134,6 +185,22 @@ impl std::ops::BitOr<Square> for Bitboard {
}
}
/// Treat each bitboard as a set of squares, keep squares that are in either sets.
impl std::ops::BitOrAssign<Bitboard> for Bitboard {
#[inline(always)]
fn bitor_assign(&mut self, rhs: Bitboard) {
*self = *self | rhs;
}
}
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::BitOrAssign<Square> for Bitboard {
#[inline(always)]
fn bitor_assign(&mut self, rhs: Square) {
*self = *self | rhs;
}
}
/// Treat each bitboard as a set of squares, keep squares that are in both sets.
impl std::ops::BitAnd<Bitboard> for Bitboard {
type Output = Bitboard;
@ -144,7 +211,7 @@ impl std::ops::BitAnd<Bitboard> for Bitboard {
}
}
/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator.
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::BitAnd<Square> for Bitboard {
type Output = Bitboard;
@ -154,6 +221,22 @@ impl std::ops::BitAnd<Square> for Bitboard {
}
}
/// Treat each bitboard as a set of squares, keep squares that are in both sets.
impl std::ops::BitAndAssign<Bitboard> for Bitboard {
#[inline(always)]
fn bitand_assign(&mut self, rhs: Bitboard) {
*self = *self & rhs;
}
}
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::BitAndAssign<Square> for Bitboard {
#[inline(always)]
fn bitand_assign(&mut self, rhs: Square) {
*self = *self & rhs;
}
}
/// Treat each bitboard as a set of squares, keep squares that are in exactly one of either set.
impl std::ops::BitXor<Bitboard> for Bitboard {
type Output = Bitboard;
@ -164,7 +247,7 @@ impl std::ops::BitXor<Bitboard> for Bitboard {
}
}
/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator.
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::BitXor<Square> for Bitboard {
type Output = Bitboard;
@ -174,6 +257,22 @@ impl std::ops::BitXor<Square> for Bitboard {
}
}
/// Treat each bitboard as a set of squares, keep squares that are in exactly one of either set.
impl std::ops::BitXorAssign<Bitboard> for Bitboard {
#[inline(always)]
fn bitxor_assign(&mut self, rhs: Bitboard) {
*self = *self ^ rhs;
}
}
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::BitXorAssign<Square> for Bitboard {
#[inline(always)]
fn bitxor_assign(&mut self, rhs: Square) {
*self = *self ^ rhs;
}
}
/// Treat each bitboard as a set of squares, and substract one set from another.
impl std::ops::Sub<Bitboard> for Bitboard {
type Output = Bitboard;
@ -184,7 +283,7 @@ impl std::ops::Sub<Bitboard> for Bitboard {
}
}
/// Treat the [Square](crate::board::Square) as a singleton bitboard, and apply the operator.
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::Sub<Square> for Bitboard {
type Output = Bitboard;
@ -194,10 +293,28 @@ impl std::ops::Sub<Square> for Bitboard {
}
}
/// Treat each bitboard as a set of squares, and substract one set from another.
impl std::ops::SubAssign<Bitboard> for Bitboard {
#[inline(always)]
fn sub_assign(&mut self, rhs: Bitboard) {
*self = *self - rhs;
}
}
/// Treat the [Square] as a singleton bitboard, and apply the operator.
impl std::ops::SubAssign<Square> for Bitboard {
#[inline(always)]
fn sub_assign(&mut self, rhs: Square) {
*self = *self - rhs;
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::*;
use crate::board::square::*;
use crate::board::{square::*, File, Rank};
#[test]
fn count() {
@ -280,4 +397,104 @@ mod test {
assert_eq!(Bitboard::FILES[0] - Bitboard::RANKS[0], Bitboard(0xff - 1));
assert_eq!(Bitboard::FILES[0] - Square::A1, Bitboard(0xff - 1));
}
#[test]
fn more_than_one() {
assert!(!Bitboard::EMPTY.has_more_than_one());
for square in Square::iter() {
assert!(!square.into_bitboard().has_more_than_one())
}
assert!((Square::A1 | Square::H8).has_more_than_one());
assert!(Bitboard::ALL.has_more_than_one());
}
#[test]
fn iter_power_set_empty() {
assert_eq!(
Bitboard::EMPTY.iter_power_set().collect::<Vec<_>>(),
vec![Bitboard::EMPTY]
)
}
#[test]
fn iter_power_set_one_square() {
for square in Square::iter() {
assert_eq!(
square
.into_bitboard()
.iter_power_set()
.collect::<HashSet<_>>(),
[Bitboard::EMPTY, square.into_bitboard()]
.into_iter()
.collect::<HashSet<_>>()
)
}
}
#[test]
fn iter_power_set_two_squares() {
assert_eq!(
(Square::A1 | Square::H8)
.iter_power_set()
.collect::<HashSet<_>>(),
[
Bitboard::EMPTY,
Square::A1.into_bitboard(),
Square::H8.into_bitboard(),
Square::A1 | Square::H8
]
.into_iter()
.collect::<HashSet<_>>()
)
}
#[test]
fn iter_power_set_six_squares_exhaustive() {
let mask = (0..6)
.map(Square::from_index)
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs);
assert_eq!(
mask.iter_power_set().collect::<HashSet<_>>(),
(0..(1 << 6)).map(Bitboard).collect::<HashSet<_>>()
)
}
#[test]
fn iter_power_set_eight_squares_length() {
assert_eq!(
File::A
.into_bitboard()
.iter_power_set()
.collect::<HashSet<_>>()
.len(),
1 << 8
);
assert_eq!(
Rank::First
.into_bitboard()
.iter_power_set()
.collect::<HashSet<_>>()
.len(),
1 << 8
);
}
#[test]
fn into_square() {
for square in Square::iter() {
assert_eq!(square.into_bitboard().try_into(), Ok(square));
}
}
#[test]
fn into_square_invalid() {
assert_eq!(
TryInto::<Square>::try_into(Bitboard::EMPTY),
Err(IntoSquareError::EmptyBoard)
);
assert_eq!(
TryInto::<Square>::try_into(Square::A1 | Square::A2),
Err(IntoSquareError::TooManySquares)
)
}
}

View file

@ -0,0 +1,46 @@
use super::Bitboard;
/// Iterator over a [Bitboard] mask, which yields all potential subsets of the given board.
/// In other words, for each square that belongs to the mask, this will yield all sets that do
/// contain the square, and all sets that do not.
pub struct BitboardPowerSetIterator {
/// The starting board.
board: Bitboard,
/// The next subset.
subset: Bitboard,
/// Whether or not iteration is done.
done: bool,
}
impl BitboardPowerSetIterator {
pub fn new(board: Bitboard) -> Self {
Self {
board,
subset: Bitboard::EMPTY,
done: false,
}
}
}
impl Iterator for BitboardPowerSetIterator {
type Item = Bitboard;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
let res = self.subset;
self.subset = Bitboard(self.subset.0.wrapping_sub(self.board.0)) & self.board;
self.done = self.subset.is_empty();
Some(res)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let size = 1 << self.board.count();
(size, Some(size))
}
}
impl ExactSizeIterator for BitboardPowerSetIterator {}
impl std::iter::FusedIterator for BitboardPowerSetIterator {}

View file

@ -14,15 +14,22 @@ pub enum CastleRights {
}
impl CastleRights {
/// The number of [CastleRights] variants.
pub const NUM_VARIANTS: usize = 4;
/// Convert from a castle rights index into a [CastleRights] type.
#[inline(always)]
pub fn from_index(index: usize) -> Self {
assert!(index < 4);
assert!(index < Self::NUM_VARIANTS);
// SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(index) }
}
/// Convert from a castle rights index into a [CastleRights] type, no bounds checking.
///
/// # Safety
///
/// This should only be called with values that can be output by [CastleRights::index()].
#[inline(always)]
pub unsafe fn from_index_unchecked(index: usize) -> Self {
std::mem::transmute(index as u8)
@ -46,6 +53,25 @@ impl CastleRights {
(self.index() & 2) != 0
}
/// Add king-side castling rights.
#[inline(always)]
pub fn with_king_side(self) -> Self {
self.add(Self::KingSide)
}
/// Add queen-side castling rights.
#[inline(always)]
pub fn with_queen_side(self) -> Self {
self.add(Self::QueenSide)
}
/// Add some [CastleRights], and return the resulting [CastleRights].
#[inline(always)]
fn add(self, additional_rights: CastleRights) -> Self {
// SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(self.index() | additional_rights.index()) }
}
/// Remove king-side castling rights.
#[inline(always)]
pub fn without_king_side(self) -> Self {

View file

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

View file

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

View file

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

View file

@ -8,15 +8,29 @@ pub enum Color {
}
impl Color {
/// Convert from a file index into a [Color] type.
/// The number of [Color] variants.
pub const NUM_VARIANTS: usize = 2;
const ALL: [Self; Self::NUM_VARIANTS] = [Self::White, Self::Black];
/// Iterate over all colors in order.
pub fn iter() -> impl Iterator<Item = Self> {
Self::ALL.iter().cloned()
}
/// Convert from a color index into a [Color] type.
#[inline(always)]
pub fn from_index(index: usize) -> Self {
assert!(index < 2);
assert!(index < Self::NUM_VARIANTS);
// SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(index) }
}
/// Convert from a file index into a [Color] type, no bounds checking.
/// Convert from a color index into a [Color] type, no bounds checking.
///
/// # Safety
///
/// Should only be called with values that can be output by [Color::index()].
#[inline(always)]
pub unsafe fn from_index_unchecked(index: usize) -> Self {
std::mem::transmute(index as u8)
@ -46,6 +60,16 @@ impl Color {
}
}
/// Return the third [Rank] for pieces of the given [Color], where its pawns move to after a
/// one-square move on the start position.
#[inline(always)]
pub fn third_rank(self) -> Rank {
match self {
Self::White => Rank::Third,
Self::Black => Rank::Sixth,
}
}
/// Return the fourth [Rank] for pieces of the given [Color], where its pawns move to after a
/// two-square move.
#[inline(always)]

View file

@ -121,14 +121,28 @@ impl Direction {
/// It does not make sense to use this method with knight-only directions, and it will panic in
/// debug-mode if it happens.
#[inline(always)]
pub fn slide_board(self, mut board: Bitboard) -> Bitboard {
pub fn slide_board(self, board: Bitboard) -> Bitboard {
self.slide_board_with_blockers(board, Bitboard::EMPTY)
}
/// Slide a board along the given [Direction], i.e: return all successive applications of
/// [Direction::move_board] until no new squares can be reached.
/// Take into account the `blockers` [Bitboard]: a combination of all pieces on the board which
/// cannot be slid over. The slide is over once a square that is part of `blockers` is reached.
/// It does not make sense to use this method with knight-only directions, and it will panic in
/// debug-mode if it happens.
#[inline(always)]
pub fn slide_board_with_blockers(self, mut board: Bitboard, blockers: Bitboard) -> Bitboard {
debug_assert!(!Self::KNIGHT_DIRECTIONS.contains(&self));
let mut res = Default::default();
while !board.is_empty() {
board = self.move_board(board);
res = res | board;
res |= board;
if !(board & blockers).is_empty() {
break;
}
}
res
@ -655,4 +669,29 @@ mod test {
Bitboard::DIAGONAL - Square::A1
);
}
#[test]
fn blocked_slides() {
assert_eq!(
Direction::North
.slide_board_with_blockers(Square::A1.into_bitboard(), Square::A2.into_bitboard()),
Square::A2.into_bitboard()
);
assert_eq!(
Direction::North
.slide_board_with_blockers(Square::A1.into_bitboard(), Square::A3.into_bitboard()),
Square::A2 | Square::A3
);
assert_eq!(
Direction::North
.slide_board_with_blockers(Square::A1.into_bitboard(), Square::A4.into_bitboard()),
Square::A2 | Square::A3 | Square::A4
);
// Ensure that the starting square being in `blockers` is not an issue
assert_eq!(
Direction::North
.slide_board_with_blockers(Square::A1.into_bitboard(), Square::A1.into_bitboard()),
File::A.into_bitboard() - Square::A1
);
}
}

View file

@ -15,31 +15,38 @@ pub enum File {
}
impl File {
const ALL: [File; 8] = [
File::A,
File::B,
File::C,
File::D,
File::E,
File::F,
File::G,
File::H,
/// The number of [File] variants.
pub const NUM_VARIANTS: usize = 8;
const ALL: [Self; Self::NUM_VARIANTS] = [
Self::A,
Self::B,
Self::C,
Self::D,
Self::E,
Self::F,
Self::G,
Self::H,
];
/// Iterate over all files in order.
pub fn iter() -> impl Iterator<Item = File> {
pub fn iter() -> impl Iterator<Item = Self> {
Self::ALL.iter().cloned()
}
/// Convert from a file index into a [File] type.
#[inline(always)]
pub fn from_index(index: usize) -> Self {
assert!(index < 8);
assert!(index < Self::NUM_VARIANTS);
// SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(index) }
}
/// Convert from a file index into a [File] type, no bounds checking.
///
/// # Safety
///
/// Should only be called with values that can be output by [File::index()].
#[inline(always)]
pub unsafe fn from_index_unchecked(index: usize) -> Self {
std::mem::transmute(index as u8)

View file

@ -4,6 +4,9 @@ pub use bitboard::*;
pub mod castle_rights;
pub use castle_rights::*;
pub mod chess_board;
pub use chess_board::*;
pub mod color;
pub use color::*;
@ -13,6 +16,12 @@ pub use direction::*;
pub mod file;
pub use file::*;
pub mod r#move;
pub use r#move::*;
pub mod piece;
pub use piece::*;
pub mod rank;
pub use rank::*;

232
src/board/move.rs Normal file
View file

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

72
src/board/piece.rs Normal file
View file

@ -0,0 +1,72 @@
/// An enum representing the type of a piece.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Piece {
King,
Queen,
Rook,
Bishop,
Knight,
Pawn,
}
impl Piece {
/// The number of [Piece] variants.
pub const NUM_VARIANTS: usize = 6;
const ALL: [Self; Self::NUM_VARIANTS] = [
Self::King,
Self::Queen,
Self::Rook,
Self::Bishop,
Self::Knight,
Self::Pawn,
];
/// Iterate over all piece types.
pub fn iter() -> impl Iterator<Item = Self> {
Self::ALL.iter().cloned()
}
/// Convert from a piece index into a [Piece] type.
#[inline(always)]
pub fn from_index(index: usize) -> Self {
assert!(index < Self::NUM_VARIANTS);
// SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(index) }
}
/// Convert from a piece index into a [Piece] type, no bounds checking.
///
/// # Safety
///
/// Should only be called with values that can be output by [Piece::index()].
#[inline(always)]
pub unsafe fn from_index_unchecked(index: usize) -> Self {
std::mem::transmute(index as u8)
}
/// Return the index of a given [Piece].
#[inline(always)]
pub fn index(self) -> usize {
self as usize
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn from_index() {
assert_eq!(Piece::from_index(0), Piece::King);
assert_eq!(Piece::from_index(1), Piece::Queen);
assert_eq!(Piece::from_index(5), Piece::Pawn);
}
#[test]
fn index() {
assert_eq!(Piece::King.index(), 0);
assert_eq!(Piece::Queen.index(), 1);
assert_eq!(Piece::Pawn.index(), 5);
}
}

View file

@ -15,31 +15,38 @@ pub enum Rank {
}
impl Rank {
const ALL: [Rank; 8] = [
Rank::First,
Rank::Second,
Rank::Third,
Rank::Fourth,
Rank::Fifth,
Rank::Sixth,
Rank::Seventh,
Rank::Eighth,
/// The number of [Rank] variants.
pub const NUM_VARIANTS: usize = 8;
const ALL: [Self; Self::NUM_VARIANTS] = [
Self::First,
Self::Second,
Self::Third,
Self::Fourth,
Self::Fifth,
Self::Sixth,
Self::Seventh,
Self::Eighth,
];
/// Iterate over all ranks in order.
pub fn iter() -> impl Iterator<Item = Rank> {
pub fn iter() -> impl Iterator<Item = Self> {
Self::ALL.iter().cloned()
}
/// Convert from a rank index into a [Rank] type.
#[inline(always)]
pub fn from_index(index: usize) -> Self {
assert!(index < 8);
assert!(index < Self::NUM_VARIANTS);
// SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(index) }
}
/// Convert from a rank index into a [Rank] type, no bounds checking.
///
/// # Safety
///
/// Should only be called with values that can be output by [Rank::index()].
#[inline(always)]
pub unsafe fn from_index_unchecked(index: usize) -> Self {
std::mem::transmute(index as u8)

View file

@ -2,7 +2,7 @@ use super::{Bitboard, File, Rank};
use crate::utils::static_assert;
/// Represent a square on a chessboard. Defined in the same order as the
/// [Bitboard](crate::board::Bitboard) squares.
/// [Bitboard] squares.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[rustfmt::skip]
pub enum Square {
@ -18,13 +18,16 @@ pub enum Square {
impl std::fmt::Display for Square {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", format!("{:?}", self))
write!(f, "{:?}", self)
}
}
impl Square {
/// The number of [Square] variants.
pub const NUM_VARIANTS: usize = 64;
#[rustfmt::skip]
const ALL: [Self; 64] = [
const ALL: [Self; Self::NUM_VARIANTS] = [
Self::A1, Self::A2, Self::A3, Self::A4, Self::A5, Self::A6, Self::A7, Self::A8,
Self::B1, Self::B2, Self::B3, Self::B4, Self::B5, Self::B6, Self::B7, Self::B8,
Self::C1, Self::C2, Self::C3, Self::C4, Self::C5, Self::C6, Self::C7, Self::C8,
@ -43,19 +46,23 @@ impl Square {
}
/// Iterate over all squares in order.
pub fn iter() -> impl Iterator<Item = Square> {
pub fn iter() -> impl Iterator<Item = Self> {
Self::ALL.iter().cloned()
}
/// Convert from a square index into a [Square] type.
#[inline(always)]
pub fn from_index(index: usize) -> Self {
assert!(index < 64);
assert!(index < Self::NUM_VARIANTS);
// SAFETY: we know the value is in-bounds
unsafe { Self::from_index_unchecked(index) }
}
/// Convert from a square index into a [Square] type, no bounds checking.
///
/// # Safety
///
/// Should only be called with values that can be output by [Square::index()].
#[inline(always)]
pub unsafe fn from_index_unchecked(index: usize) -> Self {
std::mem::transmute(index as u8)
@ -106,6 +113,7 @@ impl std::ops::Shl<usize> for Square {
#[inline(always)]
fn shl(self, rhs: usize) -> Self::Output {
#[allow(clippy::suspicious_arithmetic_impl)]
Square::from_index(self as usize + rhs)
}
}
@ -116,6 +124,7 @@ impl std::ops::Shr<usize> for Square {
#[inline(always)]
fn shr(self, rhs: usize) -> Self::Output {
#[allow(clippy::suspicious_arithmetic_impl)]
Square::from_index(self as usize - rhs)
}
}

283
src/fen.rs Normal file
View file

@ -0,0 +1,283 @@
use crate::board::{
CastleRights, ChessBoard, ChessBoardBuilder, Color, File, InvalidError, Piece, Rank, Square,
};
/// A trait to mark items that can be converted from a FEN input.
pub trait FromFen: Sized {
type Err;
fn from_fen(s: &str) -> Result<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(InvalidError),
}
impl std::fmt::Display for FenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidFen => write!(f, "Invalid FEN input"),
Self::InvalidPosition(err) => write!(f, "Invalid chess position: {}", err),
}
}
}
impl std::error::Error for FenError {}
/// Allow converting a [InvalidError] into [FenError], for use with the '?' operator.
impl From<InvalidError> for FenError {
fn from(err: InvalidError) -> Self {
Self::InvalidPosition(err)
}
}
/// Convert the castling rights segment of a FEN string to an array of [CastleRights].
impl FromFen for [CastleRights; 2] {
type Err = FenError;
fn from_fen(s: &str) -> Result<Self, Self::Err> {
if s.len() > 4 {
return Err(FenError::InvalidFen);
}
let mut res = [CastleRights::NoSide; 2];
if s == "-" {
return Ok(res);
}
for b in s.chars() {
let color = if b.is_uppercase() {
Color::White
} else {
Color::Black
};
let rights = &mut res[color.index()];
match b {
'k' | 'K' => *rights = rights.with_king_side(),
'q' | 'Q' => *rights = rights.with_queen_side(),
_ => return Err(FenError::InvalidFen),
}
}
Ok(res)
}
}
/// Convert a side to move segment of a FEN string to a [Color].
impl FromFen for Color {
type Err = FenError;
fn from_fen(s: &str) -> Result<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; 2]>::from_fen(castling_rights)?;
for color in Color::iter() {
builder.with_castle_rights(castle_rights[color.index()], color);
}
let side = Color::from_fen(side_to_move)?;
builder.with_current_player(side);
if let Some(square) = Option::<Square>::from_fen(en_passant_square)? {
builder.with_en_passant(square);
};
let half_move_clock = half_move_clock
.parse::<u8>()
.map_err(|_| FenError::InvalidFen)?;
builder.with_half_move_clock(half_move_clock);
let full_move_counter = full_move_counter
.parse::<u32>()
.map_err(|_| FenError::InvalidFen)?;
builder.with_total_plies(
(full_move_counter - 1) * 2 + if side == Color::White { 0 } else { 1 },
);
{
let mut rank: usize = 8;
for rank_str in piece_placement.split('/') {
rank -= 1;
let mut file: usize = 0;
for c in rank_str.chars() {
let color = if c.is_uppercase() {
Color::White
} else {
Color::Black
};
let piece = match c {
digit @ '1'..='8' => {
// Unwrap is fine since this arm is only matched by digits
file += digit.to_digit(10).unwrap() as usize;
continue;
}
_ => Piece::from_fen(&c.to_string())?,
};
// Only need to worry about underflow since those are `usize` values.
if file >= 8 || rank >= 8 {
return Err(FenError::InvalidFen);
};
let square = Square::new(File::from_index(file), Rank::from_index(rank));
builder[square] = Some((piece, color));
file += 1;
}
// We haven't read exactly 8 files.
if file != 8 {
return Err(FenError::InvalidFen);
}
}
// We haven't read exactly 8 ranks
if rank != 0 {
return Err(FenError::InvalidFen);
}
};
Ok(builder.try_into()?)
}
}
#[cfg(test)]
mod test {
use crate::board::MoveBuilder;
use super::*;
#[test]
fn default_position() {
let default_position = ChessBoard::default();
assert_eq!(
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
.unwrap(),
default_position
);
}
#[test]
fn en_passant() {
// Start from default position
let mut position = ChessBoard::default();
position.do_move(
MoveBuilder {
piece: Piece::Pawn,
start: Square::E2,
destination: Square::E4,
capture: None,
promotion: None,
en_passant: false,
double_step: true,
castling: false,
}
.into(),
);
assert_eq!(
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
.unwrap(),
position
);
// And now c5
position.do_move(
MoveBuilder {
piece: Piece::Pawn,
start: Square::C7,
destination: Square::C5,
capture: None,
promotion: None,
en_passant: false,
double_step: true,
castling: false,
}
.into(),
);
assert_eq!(
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
.unwrap(),
position
);
// Finally, Nf3
position.do_move(
MoveBuilder {
piece: Piece::Knight,
start: Square::G1,
destination: Square::F3,
capture: None,
promotion: None,
en_passant: false,
double_step: false,
castling: false,
}
.into(),
);
assert_eq!(
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ")
.unwrap(),
position
);
}
}

View file

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

9
src/movegen/mod.rs Normal file
View file

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

145
src/movegen/moves.rs Normal file
View file

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

View file

@ -0,0 +1,69 @@
use crate::board::{Bitboard, Direction, Square};
/// Compute a bishop's movement given a set of blockers that cannot be moved past.
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
Direction::iter_bishop()
.map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers))
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs)
}
#[cfg(test)]
mod test {
use super::*;
use crate::board::{File, Rank};
#[test]
fn moves_lower_left_square() {
assert_eq!(
bishop_moves(Square::A1, Bitboard::EMPTY),
Bitboard::DIAGONAL - Square::A1
);
assert_eq!(
bishop_moves(Square::A1, Bitboard::ALL),
Square::B2.into_bitboard()
);
assert_eq!(
bishop_moves(Square::A1, Square::D4.into_bitboard()),
Square::B2 | Square::C3 | Square::D4
);
assert_eq!(
bishop_moves(Square::A1, File::D.into_bitboard()),
Square::B2 | Square::C3 | Square::D4
);
}
#[test]
fn moves_middle() {
let cross = Bitboard::DIAGONAL | Direction::South.move_board(Bitboard::ANTI_DIAGONAL);
assert_eq!(
bishop_moves(Square::D4, Bitboard::EMPTY),
cross - Square::D4
);
assert_eq!(
bishop_moves(Square::D4, Bitboard::ALL),
Square::C3 | Square::C5 | Square::E3 | Square::E5
);
assert_eq!(
bishop_moves(Square::D4, Rank::Fifth.into_bitboard()),
Square::A1
| Square::B2
| Square::C3
| Square::C5
| Square::E3
| Square::E5
| Square::F2
| Square::G1
);
assert_eq!(
bishop_moves(Square::D4, File::E.into_bitboard()),
Square::A1
| Square::A7
| Square::B2
| Square::B6
| Square::C3
| Square::C5
| Square::E3
| Square::E5
);
}
}

172
src/movegen/naive/king.rs Normal file
View file

@ -0,0 +1,172 @@
use crate::board::{Bitboard, Direction, Square};
/// Compute a king's movement. No castling moves included
pub fn king_moves(square: Square) -> Bitboard {
let board = square.into_bitboard();
Direction::iter_royalty()
.map(|dir| dir.move_board(board))
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn moves_first_rank() {
assert_eq!(king_moves(Square::A1), Square::A2 | Square::B1 | Square::B2);
assert_eq!(
king_moves(Square::B1),
Square::A1 | Square::A2 | Square::B2 | Square::C1 | Square::C2
);
assert_eq!(
king_moves(Square::C1),
Square::B1 | Square::B2 | Square::C2 | Square::D1 | Square::D2
);
assert_eq!(
king_moves(Square::D1),
Square::C1 | Square::C2 | Square::D2 | Square::E1 | Square::E2
);
assert_eq!(
king_moves(Square::E1),
Square::D1 | Square::D2 | Square::E2 | Square::F1 | Square::F2
);
assert_eq!(
king_moves(Square::F1),
Square::E1 | Square::E2 | Square::F2 | Square::G1 | Square::G2
);
assert_eq!(
king_moves(Square::G1),
Square::F1 | Square::F2 | Square::G2 | Square::H1 | Square::H2
);
assert_eq!(king_moves(Square::H1), Square::G1 | Square::G2 | Square::H2);
}
#[test]
fn moves_last_rank() {
assert_eq!(king_moves(Square::A8), Square::A7 | Square::B8 | Square::B7);
assert_eq!(
king_moves(Square::B8),
Square::A8 | Square::A7 | Square::B7 | Square::C8 | Square::C7
);
assert_eq!(
king_moves(Square::C8),
Square::B8 | Square::B7 | Square::C7 | Square::D8 | Square::D7
);
assert_eq!(
king_moves(Square::D8),
Square::C8 | Square::C7 | Square::D7 | Square::E8 | Square::E7
);
assert_eq!(
king_moves(Square::E8),
Square::D8 | Square::D7 | Square::E7 | Square::F8 | Square::F7
);
assert_eq!(
king_moves(Square::F8),
Square::E8 | Square::E7 | Square::F7 | Square::G8 | Square::G7
);
assert_eq!(
king_moves(Square::G8),
Square::F8 | Square::F7 | Square::G7 | Square::H8 | Square::H7
);
assert_eq!(king_moves(Square::H8), Square::G8 | Square::G7 | Square::H7);
}
#[test]
fn moves_first_file() {
assert_eq!(king_moves(Square::A1), Square::A2 | Square::B1 | Square::B2);
assert_eq!(
king_moves(Square::A2),
Square::A1 | Square::A3 | Square::B1 | Square::B2 | Square::B3
);
assert_eq!(
king_moves(Square::A3),
Square::A2 | Square::A4 | Square::B2 | Square::B3 | Square::B4
);
assert_eq!(
king_moves(Square::A4),
Square::A3 | Square::A5 | Square::B3 | Square::B4 | Square::B5
);
assert_eq!(
king_moves(Square::A5),
Square::A4 | Square::A6 | Square::B4 | Square::B5 | Square::B6
);
assert_eq!(
king_moves(Square::A6),
Square::A5 | Square::A7 | Square::B5 | Square::B6 | Square::B7
);
assert_eq!(
king_moves(Square::A7),
Square::A6 | Square::A8 | Square::B6 | Square::B7 | Square::B8
);
assert_eq!(king_moves(Square::A8), Square::A7 | Square::B7 | Square::B8);
}
#[test]
fn moves_last_file() {
assert_eq!(king_moves(Square::H1), Square::H2 | Square::G1 | Square::G2);
assert_eq!(
king_moves(Square::H2),
Square::H1 | Square::H3 | Square::G1 | Square::G2 | Square::G3
);
assert_eq!(
king_moves(Square::H3),
Square::H2 | Square::H4 | Square::G2 | Square::G3 | Square::G4
);
assert_eq!(
king_moves(Square::H4),
Square::H3 | Square::H5 | Square::G3 | Square::G4 | Square::G5
);
assert_eq!(
king_moves(Square::H5),
Square::H4 | Square::H6 | Square::G4 | Square::G5 | Square::G6
);
assert_eq!(
king_moves(Square::H6),
Square::H5 | Square::H7 | Square::G5 | Square::G6 | Square::G7
);
assert_eq!(
king_moves(Square::H7),
Square::H6 | Square::H8 | Square::G6 | Square::G7 | Square::G8
);
assert_eq!(king_moves(Square::H8), Square::H7 | Square::G7 | Square::G8);
}
#[test]
fn moves_middle() {
assert_eq!(
king_moves(Square::D4),
Square::C3
| Square::C4
| Square::C5
| Square::D3
| Square::D5
| Square::E3
| Square::E4
| Square::E5
);
assert_eq!(
king_moves(Square::D5),
Square::C4
| Square::C5
| Square::C6
| Square::D4
| Square::D6
| Square::E4
| Square::E5
| Square::E6
);
assert_eq!(
king_moves(Square::E5),
Square::D4
| Square::D5
| Square::D6
| Square::E4
| Square::E6
| Square::F4
| Square::F5
| Square::F6
);
}
}

183
src/movegen/naive/knight.rs Normal file
View file

@ -0,0 +1,183 @@
use crate::board::{Bitboard, Direction, Square};
/// Compute a knight's movement.
pub fn knight_moves(square: Square) -> Bitboard {
let board = square.into_bitboard();
Direction::iter_knight()
.map(|dir| dir.move_board(board))
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn moves_first_rank() {
assert_eq!(knight_moves(Square::A1), Square::B3 | Square::C2);
assert_eq!(
knight_moves(Square::B1),
Square::A3 | Square::C3 | Square::D2
);
assert_eq!(
knight_moves(Square::C1),
Square::A2 | Square::B3 | Square::D3 | Square::E2
);
assert_eq!(
knight_moves(Square::D1),
Square::B2 | Square::C3 | Square::E3 | Square::F2
);
assert_eq!(
knight_moves(Square::E1),
Square::C2 | Square::D3 | Square::F3 | Square::G2
);
assert_eq!(
knight_moves(Square::F1),
Square::D2 | Square::E3 | Square::G3 | Square::H2
);
assert_eq!(
knight_moves(Square::G1),
Square::E2 | Square::F3 | Square::H3
);
assert_eq!(knight_moves(Square::H1), Square::F2 | Square::G3);
}
#[test]
fn moves_last_rank() {
assert_eq!(knight_moves(Square::A8), Square::B6 | Square::C7);
assert_eq!(
knight_moves(Square::B8),
Square::A6 | Square::C6 | Square::D7
);
assert_eq!(
knight_moves(Square::C8),
Square::A7 | Square::B6 | Square::D6 | Square::E7
);
assert_eq!(
knight_moves(Square::D8),
Square::B7 | Square::C6 | Square::E6 | Square::F7
);
assert_eq!(
knight_moves(Square::E8),
Square::C7 | Square::D6 | Square::F6 | Square::G7
);
assert_eq!(
knight_moves(Square::F8),
Square::D7 | Square::E6 | Square::G6 | Square::H7
);
assert_eq!(
knight_moves(Square::G8),
Square::E7 | Square::F6 | Square::H6
);
assert_eq!(knight_moves(Square::H8), Square::F7 | Square::G6);
}
#[test]
fn moves_first_file() {
assert_eq!(knight_moves(Square::A1), Square::B3 | Square::C2);
assert_eq!(
knight_moves(Square::A2),
Square::B4 | Square::C1 | Square::C3
);
assert_eq!(
knight_moves(Square::A3),
Square::B1 | Square::B5 | Square::C2 | Square::C4
);
assert_eq!(
knight_moves(Square::A4),
Square::B2 | Square::B6 | Square::C3 | Square::C5
);
assert_eq!(
knight_moves(Square::A5),
Square::B3 | Square::B7 | Square::C4 | Square::C6
);
assert_eq!(
knight_moves(Square::A6),
Square::B4 | Square::B8 | Square::C5 | Square::C7
);
assert_eq!(
knight_moves(Square::A7),
Square::B5 | Square::C6 | Square::C8
);
assert_eq!(knight_moves(Square::A8), Square::B6 | Square::C7);
}
#[test]
fn moves_last_file() {
assert_eq!(knight_moves(Square::H1), Square::G3 | Square::F2);
assert_eq!(
knight_moves(Square::H2),
Square::G4 | Square::F1 | Square::F3
);
assert_eq!(
knight_moves(Square::H3),
Square::G1 | Square::G5 | Square::F2 | Square::F4
);
assert_eq!(
knight_moves(Square::H4),
Square::G2 | Square::G6 | Square::F3 | Square::F5
);
assert_eq!(
knight_moves(Square::H5),
Square::G3 | Square::G7 | Square::F4 | Square::F6
);
assert_eq!(
knight_moves(Square::H6),
Square::G4 | Square::G8 | Square::F5 | Square::F7
);
assert_eq!(
knight_moves(Square::H7),
Square::G5 | Square::F6 | Square::F8
);
assert_eq!(knight_moves(Square::H8), Square::G6 | Square::F7);
}
#[test]
fn moves_middle() {
assert_eq!(
knight_moves(Square::D4),
Square::B3
| Square::B5
| Square::C2
| Square::C6
| Square::E2
| Square::E6
| Square::F3
| Square::F5
);
assert_eq!(
knight_moves(Square::D5),
Square::B4
| Square::B6
| Square::C3
| Square::C7
| Square::E3
| Square::E7
| Square::F4
| Square::F6
);
assert_eq!(
knight_moves(Square::E4),
Square::C3
| Square::C5
| Square::D2
| Square::D6
| Square::F2
| Square::F6
| Square::G3
| Square::G5
);
assert_eq!(
knight_moves(Square::E5),
Square::C4
| Square::C6
| Square::D3
| Square::D7
| Square::F3
| Square::F7
| Square::G4
| Square::G6
);
}
}

14
src/movegen/naive/mod.rs Normal file
View file

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

137
src/movegen/naive/pawn.rs Normal file
View file

@ -0,0 +1,137 @@
use crate::board::{Bitboard, Color, Direction, Rank, Square};
/// Compute a pawn's movement given its color, and a set of blockers that cannot be moved past.
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) {
return Bitboard::EMPTY;
}
let dir = color.forward_direction();
let first_push = dir.move_board(square.into_bitboard());
let second_push = if square.rank() == color.second_rank() {
Square::new(square.file(), color.fourth_rank()).into_bitboard()
} else {
Bitboard::EMPTY
};
if (first_push & blockers).is_empty() {
first_push | second_push
} else {
Bitboard::EMPTY
}
}
/// Computes the set of squares a pawn can capture, given its color.
pub fn pawn_captures(color: Color, square: Square) -> Bitboard {
if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) {
return Bitboard::EMPTY;
}
let dir = color.forward_direction();
let advanced = dir.move_board(square.into_bitboard());
let attack_west = Direction::West.move_board(advanced);
let attack_east = Direction::East.move_board(advanced);
attack_west | attack_east
}
/// Computes the set of squares that can capture this one *en-passant*.
#[allow(unused)]
pub fn en_passant_origins(square: Square) -> Bitboard {
let board = square.into_bitboard();
let origin_west = Direction::West.move_board(board);
let origin_east = Direction::East.move_board(board);
origin_west | origin_east
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn moves() {
assert_eq!(
pawn_moves(Color::White, Square::A2, Bitboard::EMPTY),
Square::A3 | Square::A4
);
assert_eq!(
pawn_moves(Color::Black, Square::A7, Bitboard::EMPTY),
Square::A5 | Square::A6
);
assert_eq!(
pawn_moves(Color::Black, Square::A2, Bitboard::EMPTY),
Square::A1.into_bitboard()
);
assert_eq!(
pawn_moves(Color::White, Square::A7, Bitboard::EMPTY),
Square::A8.into_bitboard()
);
}
#[test]
fn captures() {
assert_eq!(
pawn_captures(Color::White, Square::A2),
Square::B3.into_bitboard()
);
assert_eq!(
pawn_captures(Color::White, Square::B2),
Square::A3 | Square::C3
);
assert_eq!(
pawn_captures(Color::White, Square::H2),
Square::G3.into_bitboard()
);
assert_eq!(
pawn_captures(Color::Black, Square::A2),
Square::B1.into_bitboard()
);
assert_eq!(
pawn_captures(Color::Black, Square::B2),
Square::A1 | Square::C1
);
assert_eq!(
pawn_captures(Color::Black, Square::H2),
Square::G1.into_bitboard()
);
assert_eq!(
pawn_captures(Color::White, Square::A7),
Square::B8.into_bitboard()
);
assert_eq!(
pawn_captures(Color::White, Square::B7),
Square::A8 | Square::C8
);
assert_eq!(
pawn_captures(Color::Black, Square::H7),
Square::G6.into_bitboard()
);
assert_eq!(
pawn_captures(Color::Black, Square::A7),
Square::B6.into_bitboard()
);
assert_eq!(
pawn_captures(Color::Black, Square::B7),
Square::A6 | Square::C6
);
assert_eq!(
pawn_captures(Color::Black, Square::H7),
Square::G6.into_bitboard()
);
}
#[test]
fn en_passant() {
assert_eq!(en_passant_origins(Square::A4), Square::B4.into_bitboard());
assert_eq!(en_passant_origins(Square::A5), Square::B5.into_bitboard());
assert_eq!(en_passant_origins(Square::B4), Square::A4 | Square::C4);
assert_eq!(en_passant_origins(Square::B5), Square::A5 | Square::C5);
assert_eq!(en_passant_origins(Square::H4), Square::G4.into_bitboard());
assert_eq!(en_passant_origins(Square::H5), Square::G5.into_bitboard());
}
}

54
src/movegen/naive/rook.rs Normal file
View file

@ -0,0 +1,54 @@
use crate::board::{Bitboard, Direction, Square};
/// Compute a rook's movement given a set of blockers that cannot be moved past.
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
Direction::iter_rook()
.map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers))
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs)
}
#[cfg(test)]
mod test {
use super::*;
use crate::board::{File, Rank};
#[test]
fn moves_lower_left_square() {
assert_eq!(
rook_moves(Square::A1, Bitboard::EMPTY),
(File::A.into_bitboard() | Rank::First.into_bitboard()) - Square::A1
);
assert_eq!(
rook_moves(Square::A1, Bitboard::ALL),
Square::A2 | Square::B1
);
assert_eq!(
rook_moves(Square::A1, Rank::First.into_bitboard()),
(File::A.into_bitboard() | Square::B1) - Square::A1
);
assert_eq!(
rook_moves(Square::A1, File::A.into_bitboard()),
(Rank::First.into_bitboard() | Square::A2) - Square::A1
);
}
#[test]
fn moves_middle() {
assert_eq!(
rook_moves(Square::D4, Bitboard::EMPTY),
(File::D.into_bitboard() | Rank::Fourth.into_bitboard()) - Square::D4
);
assert_eq!(
rook_moves(Square::D4, Bitboard::ALL),
Square::C4 | Square::D3 | Square::D5 | Square::E4
);
assert_eq!(
rook_moves(Square::D4, Rank::Fourth.into_bitboard()),
(File::D.into_bitboard() | Square::C4 | Square::E4) - Square::D4
);
assert_eq!(
rook_moves(Square::D4, File::D.into_bitboard()),
(Rank::Fourth.into_bitboard() | Square::D3 | Square::D5) - Square::D4
);
}
}

View file

@ -0,0 +1,70 @@
use crate::board::{Bitboard, Square};
use crate::movegen::naive::{bishop_moves, rook_moves};
use super::mask::{generate_bishop_mask, generate_rook_mask};
use super::Magic;
/// A trait to represent RNG for u64 values.
pub(crate) trait RandGen {
fn gen(&mut self) -> u64;
}
type MagicGenerationType = (Vec<Magic>, Vec<Bitboard>);
pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType {
generate_magics(rng, generate_bishop_mask, bishop_moves)
}
pub fn generate_rook_magics(rng: &mut dyn RandGen) -> MagicGenerationType {
generate_magics(rng, generate_rook_mask, rook_moves)
}
fn generate_magics(
rng: &mut dyn RandGen,
mask_fn: impl Fn(Square) -> Bitboard,
moves_fn: impl Fn(Square, Bitboard) -> Bitboard,
) -> MagicGenerationType {
let mut magics = Vec::new();
let mut boards = Vec::new();
for square in Square::iter() {
let mask = mask_fn(square);
let occupancy_to_moves: Vec<_> = mask
.iter_power_set()
.map(|occupancy| (occupancy, moves_fn(square, occupancy)))
.collect();
'candidate_search: loop {
let mut candidate = Magic {
magic: magic_candidate(rng),
offset: 0,
mask,
shift: (64 - mask.count()) as u8,
};
let mut candidate_moves = vec![Bitboard::EMPTY; occupancy_to_moves.len()];
for (occupancy, moves) in occupancy_to_moves.iter().cloned() {
let index = candidate.get_index(occupancy);
// Non-constructive collision, try with another candidate
if !candidate_moves[index].is_empty() && candidate_moves[index] != moves {
continue 'candidate_search;
}
candidate_moves[index] = moves;
}
// We have filled all candidate boards, record the correct offset and add the moves
candidate.offset = boards.len();
magics.push(candidate);
boards.append(&mut candidate_moves);
break;
}
}
(magics, boards)
}
fn magic_candidate(rng: &mut dyn RandGen) -> u64 {
// Few bits makes for better candidates
rng.gen() & rng.gen() & rng.gen()
}

View file

@ -0,0 +1,38 @@
use crate::board::{Bitboard, File, Rank, Square};
use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves};
/// Compute the relevancy mask for a bishop on a given [Square].
pub fn generate_bishop_mask(square: Square) -> Bitboard {
let rays = bishop_moves(square, Bitboard::EMPTY);
let mask = File::A.into_bitboard()
| File::H.into_bitboard()
| Rank::First.into_bitboard()
| Rank::Eighth.into_bitboard();
rays - mask
}
/// Compute the relevancy mask for a rook on a given [Square].
pub fn generate_rook_mask(square: Square) -> Bitboard {
let rays = rook_moves(square, Bitboard::EMPTY);
let mask = {
let mut mask = Bitboard::EMPTY;
if square.file() != File::A {
mask |= File::A.into_bitboard()
};
if square.file() != File::H {
mask |= File::H.into_bitboard()
};
if square.rank() != Rank::First {
mask |= Rank::First.into_bitboard()
};
if square.rank() != Rank::Eighth {
mask |= Rank::Eighth.into_bitboard()
};
mask
};
rays - mask
}

283
src/movegen/wizardry/mod.rs Normal file
View file

@ -0,0 +1,283 @@
mod generation;
pub(super) use generation::*;
mod mask;
use crate::board::{Bitboard, Square};
/// A type representing the magic board indexing a given [crate::board::Square].
#[derive(Clone, Debug)]
pub(super) struct Magic {
/// Magic number.
pub(self) magic: u64,
/// Base offset into the magic square table.
pub(self) offset: usize,
/// Mask to apply to the blocker board before applying the magic.
pub(self) mask: Bitboard,
/// Length of the resulting mask after applying the magic.
pub(self) shift: u8,
}
impl Magic {
/// Compute the index into the magics database for this set of `blockers`.
pub fn get_index(&self, blockers: Bitboard) -> usize {
let relevant_occupancy = (blockers & self.mask).0;
let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize;
base_index + self.offset
}
}
/// A type encapsulating a database of [Magic] bitboard moves.
#[derive(Clone, Debug)]
pub(crate) struct MagicMoves {
magics: Vec<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; 64] = [
4908958787341189172,
1157496606860279808,
289395876198088778,
649648646467355137,
19162426089930848,
564067194896448,
18586170375029026,
9185354800693760,
72172012436987968,
317226351607872,
2597178509285688384,
1162205282238464,
144154788211329152,
172197832046936160,
4625762105940000802,
1477217245166903296,
2251937789583872,
289373902621379585,
4616200855845409024,
2251909637357568,
3532510975437640064,
563517968228352,
562953309660434,
1196005458310201856,
2350914225914520576,
2287018679861376,
13836188353273790593,
11267795163676832,
297519119119499264,
18588344158519552,
10453428171813953792,
72128237668534272,
1298164929055953920,
865575144395900952,
9293076573325312,
108104018148197376,
578503662094123152,
4665870505495102224,
6066493872259301520,
285877477613857,
2328941618281318466,
721165292771739652,
4899973577790523400,
75050392749184,
2305878200632215680,
11530099074925593616,
290561512873919880,
18652187227888000,
3379933716168704,
9223409493537718272,
22273835729926,
1152921524003672064,
4647812741240848385,
1244225087719112712,
7367907171013001728,
9263922034316951570,
300758214358598160,
4611686331973636096,
2377900605806479360,
6958097192913601024,
864691130877743617,
703824948904066,
612700674899317536,
180742128018784384,
];
/// A set of magic numbers for rook move generation.
pub(crate) const ROOK_SEED: [u64; 64] = [
2341871943948451840,
18015635528220736,
72066665545773824,
1188959097794342912,
12141713393631625314,
720649693658353672,
36029896538981888,
36033359356363520,
140746619355268,
1158339898446446661,
36591886560003650,
578853633228023808,
2392554490300416,
140814806160384,
180706952366596608,
10696087878779396,
1153260703948210820,
310748649170673678,
36311372044308544,
9223444604757615104,
1267187285230592,
282574622818306,
18722484274726152,
2271591090110593,
1153063519847989248,
10168327557107712,
4507998211276833,
1153203035420233728,
4631961017139660032,
2454499182462107776,
289367288355753288,
18015815850820609,
9268726066908758912,
11547264697673728000,
2314929519368081536,
140943655192577,
20266215511427202,
180706969441535248,
1302683805944911874,
11534000122299940994,
22676602724843520,
4639271120198041668,
1302104069046927376,
9184220895313928,
4612249105954373649,
562984581726212,
2312678200579457040,
4647736876550193157,
3170604524138139776,
4684447574787096704,
20283792725901696,
1152992019380963840,
117383863558471808,
1153488854922068096,
17596884583424,
90074759127192064,
4900502436426416706,
4573968656793901,
1161084564408385,
1657887889314811910,
4614501455660058690,
4612530729109422081,
642458506527236,
1116704154754,
];
// endregion:sourcegen
#[cfg(test)]
mod test {
use super::*;
// A simple XOR-shift RNG implementation.
struct SimpleRng(u64);
impl SimpleRng {
pub fn new() -> Self {
Self(4) // https://xkcd.com/221/
}
}
impl RandGen for SimpleRng {
fn gen(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0
}
}
#[test]
fn rng() {
let mut rng = SimpleRng::new();
assert_eq!(rng.gen(), 134217733);
assert_eq!(rng.gen(), 4504699139039237);
assert_eq!(rng.gen(), 13512173405898766);
assert_eq!(rng.gen(), 9225626310854853124);
assert_eq!(rng.gen(), 29836777971867270);
}
fn split_twice<'a>(
text: &'a str,
start_marker: &str,
end_marker: &str,
) -> Option<(&'a str, &'a str, &'a str)> {
let (prefix, rest) = text.split_once(start_marker)?;
let (mid, suffix) = rest.split_once(end_marker)?;
Some((prefix, mid, suffix))
}
fn array_string(piece_type: &str, values: &[Magic]) -> String {
let mut res = format!(
"/// A set of magic numbers for {} move generation.\n",
piece_type
);
res.push_str(&format!(
"pub(crate) const {}_SEED: [u64; 64] = [\n",
piece_type.to_uppercase()
));
for magic in values {
res.push_str(&format!(" {},\n", magic.magic));
}
res.push_str("];\n");
res
}
#[test]
#[ignore = "slow"]
// Regenerates the magic bitboard numbers.
fn regen_magic_seeds() {
// We only care about the magics, the moves can be recomputed at runtime ~cheaply.
let (bishop_magics, _) = generate_bishop_magics(&mut SimpleRng::new());
let (rook_magics, _) = generate_rook_magics(&mut SimpleRng::new());
let original_text = std::fs::read_to_string(file!()).unwrap();
let bishop_array = array_string("bishop", &bishop_magics[..]);
let rook_array = array_string("rook", &rook_magics[..]);
let new_text = {
let start_marker = "// region:sourcegen\n";
let end_marker = "// endregion:sourcegen\n";
let (prefix, _, suffix) =
split_twice(&original_text, start_marker, end_marker).unwrap();
format!("{prefix}{start_marker}{bishop_array}\n{rook_array}{end_marker}{suffix}")
};
if new_text != original_text {
std::fs::write(file!(), new_text).unwrap();
panic!("source was not up-to-date")
}
}
}

View file

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

View file

@ -1,17 +1,24 @@
import enum
import gdb
import gdb.printing
class Square(object):
"""
Wrapper around GDB's representation of a 'seer::board::square::Square' in
memory.
Python representation of a 'seer::board::square::Square' raw value.
"""
FILES = list(map(lambda n: chr(ord('A') + n), range(8)))
FILES = list(map(lambda n: chr(ord("A") + n), range(8)))
RANKS = list(map(lambda n: str(n + 1), range(8)))
def __init__(self, val):
self._val = val
@classmethod
def from_file_rank(cls, file, rank):
return cls(file * 8 + rank)
def __str__(self):
return self.FILES[self.file] + self.RANKS[self.rank]
@ -23,10 +30,10 @@ class Square(object):
def file(self):
return int(self._val) // 8
class Bitboard(object):
"""
Wrapper around GDB's representation of a 'seer::board::bitboard::Bitboard'
in memory.
Python representation of a 'seer::board::bitboard::Bitboard' raw value.
"""
def __init__(self, val):
@ -35,14 +42,260 @@ class Bitboard(object):
def __str__(self):
return "[" + ", ".join(map(str, self.squares)) + "]"
def at(self, square):
return bool(self._val & (1 << square._val))
@property
def squares(self):
n = int(self._val["__0"])
n = self._val
while n:
b = n & (~n+1)
b = n & (~n + 1)
yield Square(b.bit_length() - 1)
n ^= b
class CastleRights(enum.IntEnum):
"""
Python representation of a 'seer::board::castle_rights::CastleRights' raw value.
"""
# Should be kept in sync with the enum in `color.rs`
NO_SIDE = 0
KING_SIDE = 1
QUEEN_SIDE = 2
BOTH_SIDES = 3
def __str__(self):
return self.name.title().replace("_", "")
class Color(enum.IntEnum):
"""
Python representation of a 'seer::board::color::Color' raw value.
"""
# Should be kept in sync with the enum in `color.rs`
WHITE = 0
BLACK = 1
def __str__(self):
return self.name.title()
class File(enum.IntEnum):
"""
Python representation of a 'seer::board::file::File' raw value.
"""
# Should be kept in sync with the enum in `file.rs`
A = 0
B = 1
C = 2
D = 3
E = 4
F = 5
G = 6
H = 7
def __str__(self):
return self.name.title()
class Rank(enum.IntEnum):
"""
Python representation of a 'seer::board::rank::Rank' raw value.
"""
# Should be kept in sync with the enum in `rank.rs`
First = 0
Second = 1
Third = 2
Fourth = 3
Fifth = 4
Sixth = 5
Seventh = 6
Eighth = 7
def __str__(self):
return self.name.title()
class Piece(enum.IntEnum):
"""
Python representation of a 'seer::board::piece::Piece' raw value.
"""
# Should be kept in sync with the enum in `piece.rs`
KING = 0
QUEEN = 1
ROOK = 2
BISHOP = 3
KNIGHT = 4
PAWN = 5
def __str__(self):
return self.name.title()
class Move(object):
"""
Wrapper around GDB's representation of a 'seer::board::move::Move'
in memory.
"""
# Should be kept in sync with the values in `move.rs`
PIECE_SHIFT = 0
PIECE_MASK = 0b111
START_SHIFT = 3
START_MASK = 0b11_1111
DESTINATION_SHIFT = 9
DESTINATION_MASK = 0b11_1111
CAPTURE_SHIFT = 15
CAPTURE_MASK = 0b111
PROMOTION_SHIFT = 18
PROMOTION_MASK = 0b111
EN_PASSANT_SHIFT = 21
EN_PASSANT_MASK = 0b1
DOUBLE_STEP_SHIFT = 22
DOUBLE_STEP_MASK = 0b1
CASTLING_SHIFT = 23
CASTLING_MASK = 0b1
def __init__(self, val):
self._val = val
@property
def piece(self):
return Piece(self._val >> self.PIECE_SHIFT & self.PIECE_MASK)
@property
def start(self):
return Square(self._val >> self.START_SHIFT & self.START_MASK)
@property
def destination(self):
return Square(self._val >> self.DESTINATION_SHIFT & self.DESTINATION_MASK)
@property
def capture(self):
index = self._val >> self.CAPTURE_SHIFT & self.CAPTURE_MASK
if index == 7:
return None
return Piece(index)
@property
def promotion(self):
index = self._val >> self.PROMOTION_SHIFT & self.PROMOTION_MASK
if index == 7:
return None
return Piece(index)
@property
def en_passant(self):
return bool(self._val >> self.EN_PASSANT_SHIFT & self.EN_PASSANT_MASK)
@property
def double_step(self):
return bool(self._val >> self.DOUBLE_STEP_SHIFT & self.DOUBLE_STEP_MASK)
@property
def castling(self):
return bool(self._val >> self.CASTLING_SHIFT & self.CASTLING_MASK)
def __str__(self):
KEYS = [
"piece",
"start",
"destination",
"capture",
"promotion",
"en_passant",
"double_step",
"castling",
]
print_opt = lambda val: "(None)" if val is None else str(val)
indent = lambda s: " " + s
values = [key + ": " + print_opt(getattr(self, key)) + ",\n" for key in KEYS]
return "Move{\n" + "".join(map(indent, values)) + "}"
class ChessBoard(object):
"""
Wrapper around GDB's representation of a 'seer::board::chess_board::ChessBoard'
in memory.
"""
def __init__(
self,
piece_occupancy,
color_occupancy,
castle_rights,
half_move_clock,
total_plies,
side,
):
self._piece_occupancy = list(map(Bitboard, piece_occupancy))
self._color_occupancy = list(map(Bitboard, color_occupancy))
self._castle_rights = list(map(CastleRights, castle_rights))
self._half_move_clock = int(half_move_clock)
self._total_plies = int(total_plies)
self._side = Color(side)
@classmethod
def from_gdb(cls, val):
return cls(
[int(val["piece_occupancy"][p]["__0"]) for p in Piece],
[int(val["color_occupancy"][c]["__0"]) for c in Color],
[int(val["castle_rights"][c]) for c in Color],
# FIXME: find out how to check for Some/None in val["en_passant"],
int(val["half_move_clock"]),
int(val["total_plies"]),
Color(int(val["side"])),
)
def at(self, square):
for piece in Piece:
if not self._piece_occupancy[piece].at(square):
continue
for color in Color:
if not self._color_occupancy[color].at(square):
continue
return (piece, color)
return None
def pretty_str(self):
def pretty_piece(piece, color):
return [
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
][piece][color]
board = [
[self.at(Square.from_file_rank(file, rank)) for file in File]
for rank in Rank
]
res = []
res.append(" A B C D E F G H ")
for n, line in reversed(list(enumerate(board, start=1))):
strings = [str(n) + " "]
strings.extend(" " if p is None else pretty_piece(*p) for p in line)
strings.append(" " + str(n))
res.append("|".join(strings))
res.append(" A B C D E F G H ")
res += [
"Half-move clock: " + str(self._half_move_clock),
"Total plies: " + str(self._total_plies),
"Side to play: " + str(self._side),
]
return "\n".join(res)
class SquarePrinter(object):
"Print a seer::board::square::Square"
@ -52,27 +305,110 @@ class SquarePrinter(object):
def to_string(self):
return str(self._val)
def display_hint(self):
return 'string'
class BitboardPrinter(object):
"Print a seer::board::bitboard::Bitboard"
def __init__(self, val):
self._val = Bitboard(val)
self._val = Bitboard(int(val["__0"]))
def to_string(self):
return "Bitboard{" + str(self._val)[1:-1] + "}"
def display_hint(self):
return 'string'
class CastleRightsPrinter(object):
"Print a seer::board::castle_rights::CastleRights"
def __init__(self, val):
self._val = CastleRights(int(val))
def to_string(self):
return str(self._val)
class ColorPrinter(object):
"Print a seer::board::color::Color"
def __init__(self, val):
self._val = Color(int(val))
def to_string(self):
return str(self._val)
class FilePrinter(object):
"Print a seer::board::file::File"
def __init__(self, val):
self._val = File(int(val))
def to_string(self):
return str(self._val)
class RankPrinter(object):
"Print a seer::board::rank::Rank"
def __init__(self, val):
self._val = Rank(int(val))
def to_string(self):
return str(self._val)
class PiecePrinter(object):
"Print a seer::board::piece::Piece"
def __init__(self, val):
self._val = Piece(int(val))
def to_string(self):
return str(self._val)
class MovePrinter(object):
"Print a seer::board::move::Move"
def __init__(self, val):
self._val = Move(int(val["__0"]))
def to_string(self):
return str(self._val)
class PrintBoard(gdb.Command):
"""
Pretty-print a 'seer::board::chess_board::ChessBoard' as a 2D textual chess board.
"""
def __init__(self):
super(PrintBoard, self).__init__(
"print-board", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
)
def invoke(self, arg, from_tty):
board = ChessBoard.from_gdb(gdb.parse_and_eval(arg))
print(board.pretty_str())
def build_pretty_printer():
pp = gdb.printing.RegexpCollectionPrettyPrinter('seer')
pp.add_printer('BigNum', '^seer::board::square::Square$', SquarePrinter)
pp.add_printer('BigNum', '^seer::board::bitboard::Bitboard$', BitboardPrinter)
pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter)
pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter)
pp.add_printer('CastleRights', '^seer::board::castle_rights::CastleRights$', CastleRightsPrinter)
pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter)
pp.add_printer('File', '^seer::board::file::File$', FilePrinter)
pp.add_printer('Rank', '^seer::board::rank::Rank$', RankPrinter)
pp.add_printer('Piece', '^seer::board::piece::Piece$', ColorPrinter)
pp.add_printer('Move', '^seer::board::move::Move$', MovePrinter)
return pp
def register_commands():
PrintBoard()
gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True)
register_commands()