Compare commits
No commits in common. "b3222276abea45deaf5be7bcbd875f32a7de06a6" and "47f532cd0934286e48d335c87fd1a41b3f7272d8" have entirely different histories.
b3222276ab
...
47f532cd09
31
.drone.yml
31
.drone.yml
|
@ -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
5
.envrc
Normal 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
10
.gitignore
vendored
|
@ -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
31
.woodpecker/check.yml
Normal 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
|
115
flake.lock
115
flake.lock
|
@ -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
186
flake.nix
|
@ -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;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
19
src/board/bitboard/error.rs
Normal file
19
src/board/bitboard/error.rs
Normal 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 {}
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
46
src/board/bitboard/superset.rs
Normal file
46
src/board/bitboard/superset.rs
Normal 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 {}
|
|
@ -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 {
|
||||
|
|
161
src/board/chess_board/builder.rs
Normal file
161
src/board/chess_board/builder.rs
Normal 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())
|
||||
}
|
||||
}
|
50
src/board/chess_board/error.rs
Normal file
50
src/board/chess_board/error.rs
Normal 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 {}
|
862
src/board/chess_board/mod.rs
Normal file
862
src/board/chess_board/mod.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
232
src/board/move.rs
Normal 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
72
src/board/piece.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
283
src/fen.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,2 +1,4 @@
|
|||
pub mod board;
|
||||
pub mod fen;
|
||||
pub mod movegen;
|
||||
pub mod utils;
|
||||
|
|
9
src/movegen/mod.rs
Normal file
9
src/movegen/mod.rs
Normal 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
145
src/movegen/moves.rs
Normal 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)
|
||||
}
|
69
src/movegen/naive/bishop.rs
Normal file
69
src/movegen/naive/bishop.rs
Normal 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
172
src/movegen/naive/king.rs
Normal 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
183
src/movegen/naive/knight.rs
Normal 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
14
src/movegen/naive/mod.rs
Normal 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
137
src/movegen/naive/pawn.rs
Normal 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
54
src/movegen/naive/rook.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
70
src/movegen/wizardry/generation.rs
Normal file
70
src/movegen/wizardry/generation.rs
Normal 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()
|
||||
}
|
38
src/movegen/wizardry/mask.rs
Normal file
38
src/movegen/wizardry/mask.rs
Normal 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
283
src/movegen/wizardry/mod.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)*);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue