Compare commits
No commits in common. "main" and "add-movegen" have entirely different histories.
main
...
add-movege
|
@ -1,19 +1,24 @@
|
|||
labels:
|
||||
backend: local
|
||||
---
|
||||
kind: pipeline
|
||||
type: exec
|
||||
name: abacus checks
|
||||
|
||||
steps:
|
||||
- name: pre-commit check
|
||||
image: bash
|
||||
- name: pre commit check
|
||||
commands:
|
||||
- nix develop --command pre-commit run --all
|
||||
- nix develop . --command pre-commit run --all
|
||||
|
||||
- name: nix flake check
|
||||
image: bash
|
||||
- name: flake check
|
||||
commands:
|
||||
- nix flake check
|
||||
|
||||
- name: package check
|
||||
commands:
|
||||
- nix build
|
||||
|
||||
- name: notifiy
|
||||
image: bash
|
||||
commands:
|
||||
- nix run github:ambroisie/matrix-notifier
|
||||
environment:
|
||||
ADDRESS:
|
||||
from_secret: matrix_homeserver
|
||||
|
@ -23,9 +28,8 @@ steps:
|
|||
from_secret: matrix_username
|
||||
PASS:
|
||||
from_secret: matrix_password
|
||||
commands:
|
||||
- nix run github:ambroisie/matrix-notifier
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
- success
|
||||
- failure
|
||||
- success
|
||||
...
|
5
.envrc
5
.envrc
|
@ -1,5 +0,0 @@
|
|||
if ! has nix_direnv_version || ! nix_direnv_version 3.0.0; then
|
||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.0/direnvrc" "sha256-21TMnI2xWX7HkSTjFFri2UaohXVj854mgvWapWrxRXg="
|
||||
fi
|
||||
|
||||
use flake
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1,6 +1,6 @@
|
|||
# Rust build directory
|
||||
/target
|
||||
|
||||
# Nix generated files
|
||||
/.pre-commit-config.yaml
|
||||
# Nix files
|
||||
/result
|
||||
/.pre-commit-config.yaml
|
||||
|
||||
# Rust files
|
||||
/target
|
||||
|
|
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -2,6 +2,15 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "random"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97d13a3485349981c90c79112a11222c3e6e75de1d52b87a7525b3bf5361420f"
|
||||
|
||||
[[package]]
|
||||
name = "seer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"random",
|
||||
]
|
||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -2,7 +2,19 @@
|
|||
name = "seer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "src/build.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
random = "0.12.2"
|
||||
|
||||
[build-dependencies]
|
||||
random = "0.12.2"
|
||||
|
||||
# Optimize build scripts to shorten compile times.
|
||||
[profile.dev.build-override]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release.build-override]
|
||||
opt-level = 3
|
||||
|
|
115
flake.lock
115
flake.lock
|
@ -1,111 +1,73 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"futils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"lastModified": 1656928814,
|
||||
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"ref": "main",
|
||||
"ref": "master",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"lastModified": 1655042882,
|
||||
"narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"owner": "nix-community",
|
||||
"ref": "master",
|
||||
"repo": "naersk",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1711523803,
|
||||
"narHash": "sha256-UKcYiHWHQynzj6CN/vTcix4yd1eCu1uFdsuarupdCQQ=",
|
||||
"lastModified": 1657888067,
|
||||
"narHash": "sha256-GnwJoFBTPfW3+mz7QEeJEEQ9OMHZOiIJ/qDhZxrlKh8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2726f127c15a4cc9810843b96cad73c7eb39e443",
|
||||
"rev": "65fae659e31098ca4ac825a6fef26d890aaf3f4e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1710695816,
|
||||
"narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "614b4613980a522ba49f0d194531beddbb7220d3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.11",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": [
|
||||
"futils"
|
||||
"flake-utils"
|
||||
],
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1711519547,
|
||||
"narHash": "sha256-Q7YmSCUJmDl71fJv/zD9lrOCJ1/SE/okZ2DsrmRjzhY=",
|
||||
"lastModified": 1656169028,
|
||||
"narHash": "sha256-y9DRauokIeVHM7d29lwT8A+0YoGUBXV3H0VErxQeA8s=",
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "7d47a32e5cd1ea481fab33c516356ce27c8cef4a",
|
||||
"rev": "db3bd555d3a3ceab208bed48f983ccaa6a71a25e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -117,23 +79,34 @@
|
|||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"futils": "futils",
|
||||
"flake-utils": "flake-utils",
|
||||
"naersk": "naersk",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
"pre-commit-hooks": "pre-commit-hooks",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"lastModified": 1657853760,
|
||||
"narHash": "sha256-X6ERAyUXGsrhbhgkxNaQl40wcus5uyQZOCxUh5neK+g=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "a97a761cc11327bb109dc30af1c637b986be7959",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"owner": "oxalica",
|
||||
"ref": "master",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
|
160
flake.nix
160
flake.nix
|
@ -1,19 +1,29 @@
|
|||
{
|
||||
description = "A chess engine";
|
||||
description = "A handy file picker program";
|
||||
|
||||
inputs = {
|
||||
futils = {
|
||||
flake-utils = {
|
||||
type = "github";
|
||||
owner = "numtide";
|
||||
repo = "flake-utils";
|
||||
ref = "main";
|
||||
ref = "master";
|
||||
};
|
||||
|
||||
naersk = {
|
||||
type = "github";
|
||||
owner = "nix-community";
|
||||
repo = "naersk";
|
||||
ref = "master";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
nixpkgs = {
|
||||
type = "github";
|
||||
owner = "NixOS";
|
||||
repo = "nixpkgs";
|
||||
ref = "nixos-unstable";
|
||||
ref = "nixpkgs-unstable";
|
||||
};
|
||||
|
||||
pre-commit-hooks = {
|
||||
|
@ -22,53 +32,76 @@
|
|||
repo = "pre-commit-hooks.nix";
|
||||
ref = "master";
|
||||
inputs = {
|
||||
flake-utils.follows = "futils";
|
||||
flake-utils.follows = "flake-utils";
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
rust-overlay = {
|
||||
type = "github";
|
||||
owner = "oxalica";
|
||||
repo = "rust-overlay";
|
||||
ref = "master";
|
||||
inputs = {
|
||||
flake-utils.follows = "flake-utils";
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
outputs =
|
||||
{ self
|
||||
, flake-utils
|
||||
, naersk
|
||||
, nixpkgs
|
||||
, pre-commit-hooks
|
||||
, rust-overlay
|
||||
}:
|
||||
let
|
||||
inherit (flake-utils.lib) eachSystem system;
|
||||
|
||||
src = self;
|
||||
mySystems = [
|
||||
system.aarch64-linux
|
||||
system.x86_64-darwin
|
||||
system.x86_64-linux
|
||||
];
|
||||
|
||||
cargoLock = {
|
||||
lockFile = "${self}/Cargo.lock";
|
||||
};
|
||||
|
||||
meta = with lib; {
|
||||
description = "A chess engine";
|
||||
homepage = "https://git.belanyi.fr/ambroisie/seer";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ ambroisie ];
|
||||
};
|
||||
};
|
||||
};
|
||||
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" ];
|
||||
};
|
||||
} // futils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
self.overlays.default
|
||||
];
|
||||
};
|
||||
|
||||
pre-commit = pre-commit-hooks.lib.${system}.run {
|
||||
naersk-lib = naersk.lib."${system}".override {
|
||||
cargo = my-rust;
|
||||
rustc = my-rust;
|
||||
};
|
||||
inherit (pkgs) lib;
|
||||
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;
|
||||
settings = {
|
||||
denyWarnings = true;
|
||||
};
|
||||
entry = lib.mkForce "${rust-env}/bin/cargo-clippy clippy";
|
||||
};
|
||||
|
||||
nixpkgs-fmt = {
|
||||
|
@ -77,36 +110,43 @@
|
|||
|
||||
rustfmt = {
|
||||
enable = true;
|
||||
entry = lib.mkForce "${rust-env}/bin/cargo-fmt fmt -- --check --color always";
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
checks = {
|
||||
inherit (self.packages.${system}) seer;
|
||||
in
|
||||
rec {
|
||||
|
||||
devShells = {
|
||||
default = pkgs.mkShell {
|
||||
inputsFrom = [
|
||||
packages.seer
|
||||
];
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
rust-analyzer
|
||||
# Clippy, rustfmt, etc...
|
||||
my-rust
|
||||
];
|
||||
|
||||
inherit (pre-commit) shellHook;
|
||||
|
||||
RUST_SRC_PATH = "${my-rust}/lib/rustlib/src/rust/library";
|
||||
};
|
||||
};
|
||||
|
||||
devShells = {
|
||||
default = pkgs.mkShell {
|
||||
inputsFrom = with self.packages.${system}; [
|
||||
seer
|
||||
];
|
||||
packages = {
|
||||
default = self.packages."${system}".seer;
|
||||
|
||||
packages = with pkgs; [
|
||||
clippy
|
||||
rust-analyzer
|
||||
rustfmt
|
||||
];
|
||||
seer = naersk-lib.buildPackage {
|
||||
src = self;
|
||||
|
||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
doCheck = true;
|
||||
|
||||
inherit (pre-commit) shellHook;
|
||||
passthru = {
|
||||
inherit my-rust;
|
||||
};
|
||||
};
|
||||
|
||||
packages = futils.lib.flattenTree {
|
||||
default = pkgs.seer;
|
||||
inherit (pkgs) seer;
|
||||
};
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum IntoSquareError {
|
||||
/// The board is empty.
|
||||
EmptyBoard,
|
||||
/// The board contains more than one square.
|
||||
TooManySquares,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IntoSquareError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let error_msg = match self {
|
||||
Self::EmptyBoard => "The board is empty",
|
||||
Self::TooManySquares => "The board contains more than one square",
|
||||
};
|
||||
write!(f, "{}", error_msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IntoSquareError {}
|
|
@ -1,28 +1,23 @@
|
|||
/// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a
|
||||
/// [Bitboard].
|
||||
use crate::board::Bitboard;
|
||||
|
||||
pub struct BitboardIterator(Bitboard);
|
||||
|
||||
impl BitboardIterator {
|
||||
pub fn new(board: Bitboard) -> Self {
|
||||
Self(board)
|
||||
}
|
||||
}
|
||||
/// [Bitboard](crate::board::Bitboard).
|
||||
pub struct BitboardIterator(pub(crate) u64);
|
||||
|
||||
impl Iterator for BitboardIterator {
|
||||
type Item = crate::board::Square;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let res = self.0.any_square();
|
||||
if let Some(square) = res {
|
||||
self.0 ^= square;
|
||||
};
|
||||
res
|
||||
if self.0 == 0 {
|
||||
None
|
||||
} else {
|
||||
let lsb = self.0.trailing_zeros() as usize;
|
||||
self.0 ^= 1 << lsb;
|
||||
// SAFETY: we know the value is in-bounds
|
||||
Some(unsafe { crate::board::Square::from_index_unchecked(lsb) })
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let size = self.0.count() as usize;
|
||||
let size = self.0.count_ones() as usize;
|
||||
|
||||
(size, Some(size))
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
use super::{File, Rank, Square};
|
||||
use super::Square;
|
||||
use crate::utils::static_assert;
|
||||
|
||||
mod error;
|
||||
use error::*;
|
||||
mod iterator;
|
||||
use iterator::*;
|
||||
mod superset;
|
||||
|
@ -21,7 +19,8 @@ impl Bitboard {
|
|||
pub const ALL: Bitboard = Bitboard(u64::MAX);
|
||||
|
||||
/// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8.
|
||||
pub const RANKS: [Self; Rank::NUM_VARIANTS] = [
|
||||
#[allow(clippy::unusual_byte_groupings)]
|
||||
pub const RANKS: [Self; 8] = [
|
||||
Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001),
|
||||
Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010),
|
||||
Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100),
|
||||
|
@ -33,7 +32,8 @@ impl Bitboard {
|
|||
];
|
||||
|
||||
/// Array of bitboards representing the eight files, in order from file A to file H.
|
||||
pub const FILES: [Self; File::NUM_VARIANTS] = [
|
||||
#[allow(clippy::unusual_byte_groupings)]
|
||||
pub const FILES: [Self; 8] = [
|
||||
Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111),
|
||||
Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000),
|
||||
Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000),
|
||||
|
@ -75,12 +75,6 @@ impl Bitboard {
|
|||
(self.0 & (self.0.wrapping_sub(1))) != 0
|
||||
}
|
||||
|
||||
/// Return a [Square] from the board, or `None` if it is empty.
|
||||
#[inline(always)]
|
||||
pub fn any_square(self) -> Option<Square> {
|
||||
Square::try_from_index(self.0.trailing_zeros() as usize)
|
||||
}
|
||||
|
||||
/// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of
|
||||
/// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that
|
||||
/// contain all, some, or none of the [Square] that are in the given [Bitboard].
|
||||
|
@ -89,6 +83,18 @@ impl Bitboard {
|
|||
pub fn iter_power_set(self) -> impl Iterator<Item = Self> {
|
||||
BitboardPowerSetIterator::new(self)
|
||||
}
|
||||
|
||||
/// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is
|
||||
/// occupying. Otherwise return `None`.
|
||||
pub fn try_into_square(self) -> Option<Square> {
|
||||
if self.count() != 1 {
|
||||
None
|
||||
} else {
|
||||
let index = self.0.trailing_zeros() as usize;
|
||||
// SAFETY: we know the value is in-bounds
|
||||
Some(unsafe { Square::from_index_unchecked(index) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure zero-cost (at least size-wise) wrapping.
|
||||
|
@ -106,20 +112,7 @@ impl IntoIterator for Bitboard {
|
|||
type Item = Square;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
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> {
|
||||
if self.has_more_than_one() {
|
||||
return Err(IntoSquareError::TooManySquares);
|
||||
}
|
||||
self.any_square().ok_or(IntoSquareError::EmptyBoard)
|
||||
BitboardIterator(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -455,11 +448,15 @@ mod test {
|
|||
#[test]
|
||||
fn iter_power_set_six_squares_exhaustive() {
|
||||
let mask = (0..6)
|
||||
.into_iter()
|
||||
.map(Square::from_index)
|
||||
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs);
|
||||
assert_eq!(
|
||||
mask.iter_power_set().collect::<HashSet<_>>(),
|
||||
(0..(1 << 6)).map(Bitboard).collect::<HashSet<_>>()
|
||||
(0..(1 << 6))
|
||||
.into_iter()
|
||||
.map(Bitboard)
|
||||
.collect::<HashSet<_>>()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -483,39 +480,16 @@ mod test {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_square() {
|
||||
for square in Square::iter() {
|
||||
assert_eq!(square.into_bitboard().any_square(), Some(square));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_square_empty() {
|
||||
assert!(Bitboard::EMPTY.any_square().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_square_full_board() {
|
||||
assert!(Bitboard::ALL.any_square().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_square() {
|
||||
for square in Square::iter() {
|
||||
assert_eq!(square.into_bitboard().try_into(), Ok(square));
|
||||
assert_eq!(square.into_bitboard().try_into_square(), Some(square));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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)
|
||||
)
|
||||
assert!(Bitboard::EMPTY.try_into_square().is_none());
|
||||
assert!((Square::A1 | Square::A2).try_into_square().is_none())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,20 +4,21 @@ use super::Bitboard;
|
|||
/// In other words, for each square that belongs to the mask, this will yield all sets that do
|
||||
/// 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,
|
||||
/// The mask.
|
||||
mask: Bitboard,
|
||||
/// The "index" of the next blocker set that should be generated.
|
||||
current: usize,
|
||||
/// The number of blocker sets that should be generated by [BlockerIterator], i.e: 2^n with n
|
||||
/// the number of squares belonging to `mask`.
|
||||
total: usize,
|
||||
}
|
||||
|
||||
impl BitboardPowerSetIterator {
|
||||
pub fn new(board: Bitboard) -> Self {
|
||||
pub fn new(mask: Bitboard) -> Self {
|
||||
Self {
|
||||
board,
|
||||
subset: Bitboard::EMPTY,
|
||||
done: false,
|
||||
mask,
|
||||
current: 0,
|
||||
total: 1 << mask.count(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,18 +27,22 @@ impl Iterator for BitboardPowerSetIterator {
|
|||
type Item = Bitboard;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.done {
|
||||
return None;
|
||||
if self.current >= self.total {
|
||||
None
|
||||
} else {
|
||||
let blockers = (0..)
|
||||
.into_iter()
|
||||
.zip(self.mask.into_iter())
|
||||
.filter(|(index, _)| self.current & (1 << index) != 0)
|
||||
.map(|(_, board)| board)
|
||||
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs);
|
||||
self.current += 1;
|
||||
Some(blockers)
|
||||
}
|
||||
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))
|
||||
(self.total, Some(self.total))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::{Bitboard, Color, File, Square};
|
||||
use super::{Bitboard, Color, File, FromFen, Square};
|
||||
use crate::error::Error;
|
||||
|
||||
/// Current castle rights for a player.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
|
@ -17,44 +18,19 @@ impl CastleRights {
|
|||
/// The number of [CastleRights] variants.
|
||||
pub const NUM_VARIANTS: usize = 4;
|
||||
|
||||
const ALL: [Self; Self::NUM_VARIANTS] = [
|
||||
Self::NoSide,
|
||||
Self::KingSide,
|
||||
Self::QueenSide,
|
||||
Self::BothSides,
|
||||
];
|
||||
|
||||
/// Iterate over all castle-rights variants.
|
||||
pub fn iter() -> impl Iterator<Item = Self> {
|
||||
Self::ALL.iter().cloned()
|
||||
}
|
||||
|
||||
/// Convert from a castle rights index into a [CastleRights] type.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the index is out of bounds.
|
||||
#[inline(always)]
|
||||
pub fn from_index(index: usize) -> Self {
|
||||
Self::try_from_index(index).expect("index out of bouds")
|
||||
}
|
||||
|
||||
/// Convert from a castle rights index into a [CastleRights] type. Returns [None] if the index
|
||||
/// is out of bounds.
|
||||
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||
if index < Self::NUM_VARIANTS {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
Some(unsafe { Self::from_index_unchecked(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
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()].
|
||||
/// 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)
|
||||
|
@ -91,10 +67,11 @@ impl CastleRights {
|
|||
}
|
||||
|
||||
/// Add some [CastleRights], and return the resulting [CastleRights].
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
#[inline(always)]
|
||||
fn add(self, additional_rights: CastleRights) -> Self {
|
||||
pub fn add(self, to_remove: CastleRights) -> Self {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
unsafe { Self::from_index_unchecked(self.index() | additional_rights.index()) }
|
||||
unsafe { Self::from_index_unchecked(self.index() | to_remove.index()) }
|
||||
}
|
||||
|
||||
/// Remove king-side castling rights.
|
||||
|
@ -133,6 +110,39 @@ impl CastleRights {
|
|||
}
|
||||
}
|
||||
|
||||
/// Convert the castling rights segment of a FEN string to an array of [CastleRights].
|
||||
impl FromFen for [CastleRights; 2] {
|
||||
type Err = Error;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.len() > 4 {
|
||||
return Err(Error::InvalidFen);
|
||||
}
|
||||
|
||||
let mut res = [CastleRights::NoSide; 2];
|
||||
|
||||
if s == "-" {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
for b in s.chars() {
|
||||
let color = if b.is_uppercase() {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
};
|
||||
let rights = &mut res[color.index()];
|
||||
match b {
|
||||
'k' | 'K' => *rights = rights.with_king_side(),
|
||||
'q' | 'Q' => *rights = rights.with_queen_side(),
|
||||
_ => return Err(Error::InvalidFen),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
|
1001
src/board/chess_board.rs
Normal file
1001
src/board/chess_board.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,164 +0,0 @@
|
|||
use crate::board::{Bitboard, CastleRights, ChessBoard, Color, Piece, Square, ValidationError};
|
||||
|
||||
/// Build a [ChessBoard] one piece at a time.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ChessBoardBuilder {
|
||||
/// The list of [Piece] on the board. Indexed by [Square::index].
|
||||
pieces: [Option<(Piece, Color)>; Square::NUM_VARIANTS],
|
||||
// Same fields as [ChessBoard].
|
||||
castle_rights: [CastleRights; Color::NUM_VARIANTS],
|
||||
en_passant: Option<Square>,
|
||||
half_move_clock: u32,
|
||||
side: Color,
|
||||
// 1-based, a turn is *two* half-moves (i.e: both players have played).
|
||||
turn_count: u32,
|
||||
}
|
||||
|
||||
impl ChessBoardBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pieces: [None; Square::NUM_VARIANTS],
|
||||
castle_rights: [CastleRights::NoSide; Color::NUM_VARIANTS],
|
||||
en_passant: Default::default(),
|
||||
half_move_clock: Default::default(),
|
||||
side: Color::White,
|
||||
turn_count: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_castle_rights(&mut self, rights: CastleRights, color: Color) -> &mut Self {
|
||||
self.castle_rights[color.index()] = rights;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_en_passant(&mut self, square: Square) -> &mut Self {
|
||||
self.en_passant = Some(square);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn without_en_passant(&mut self) -> &mut Self {
|
||||
self.en_passant = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_half_move_clock(&mut self, clock: u32) -> &mut Self {
|
||||
self.half_move_clock = clock;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_turn_count(&mut self, count: u32) -> &mut Self {
|
||||
self.turn_count = count;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_current_player(&mut self, color: Color) -> &mut Self {
|
||||
self.side = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChessBoardBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Index a [ChessBoardBuilder] with a [Square] to access its pieces.
|
||||
impl std::ops::Index<Square> for ChessBoardBuilder {
|
||||
type Output = Option<(Piece, Color)>;
|
||||
|
||||
fn index(&self, square: Square) -> &Self::Output {
|
||||
&self.pieces[square.index()]
|
||||
}
|
||||
}
|
||||
|
||||
/// Index a [ChessBoardBuilder] with a [Square] to access its pieces.
|
||||
impl std::ops::IndexMut<Square> for ChessBoardBuilder {
|
||||
fn index_mut(&mut self, square: Square) -> &mut Self::Output {
|
||||
&mut self.pieces[square.index()]
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ChessBoardBuilder> for ChessBoard {
|
||||
type Error = ValidationError;
|
||||
|
||||
fn try_from(builder: ChessBoardBuilder) -> Result<Self, Self::Error> {
|
||||
let mut piece_occupancy: [Bitboard; Piece::NUM_VARIANTS] = Default::default();
|
||||
let mut color_occupancy: [Bitboard; Color::NUM_VARIANTS] = Default::default();
|
||||
let mut combined_occupancy: Bitboard = Default::default();
|
||||
let ChessBoardBuilder {
|
||||
pieces,
|
||||
castle_rights,
|
||||
en_passant,
|
||||
half_move_clock,
|
||||
side,
|
||||
turn_count,
|
||||
} = builder;
|
||||
|
||||
for square in Square::iter() {
|
||||
let Some((piece, color)) = pieces[square.index()] else {
|
||||
continue;
|
||||
};
|
||||
piece_occupancy[piece.index()] |= square;
|
||||
color_occupancy[color.index()] |= square;
|
||||
combined_occupancy |= square;
|
||||
}
|
||||
|
||||
let total_plies = (turn_count - 1) * 2 + if side == Color::White { 0 } else { 1 };
|
||||
|
||||
let board = ChessBoard {
|
||||
piece_occupancy,
|
||||
color_occupancy,
|
||||
combined_occupancy,
|
||||
castle_rights,
|
||||
en_passant,
|
||||
half_move_clock,
|
||||
total_plies,
|
||||
side,
|
||||
};
|
||||
|
||||
board.validate()?;
|
||||
Ok(board)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn from_board(board: &ChessBoard) -> ChessBoardBuilder {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
|
||||
for piece in Piece::iter() {
|
||||
for color in Color::iter() {
|
||||
for square in board.occupancy(piece, color) {
|
||||
builder[square] = Some((piece, color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for color in Color::iter() {
|
||||
builder.with_castle_rights(board.castle_rights(color), color);
|
||||
}
|
||||
|
||||
if let Some(square) = board.en_passant() {
|
||||
builder.with_en_passant(square);
|
||||
} else {
|
||||
builder.without_en_passant();
|
||||
}
|
||||
|
||||
builder
|
||||
.with_half_move_clock(board.half_move_clock())
|
||||
.with_turn_count(board.total_plies() / 2 + 1)
|
||||
.with_current_player(board.current_player());
|
||||
|
||||
builder
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_board() {
|
||||
let board = ChessBoard::default();
|
||||
let builder = from_board(&board);
|
||||
assert_eq!(board, builder.try_into().unwrap())
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
/// A singular type for all errors that could happen during [crate::board::ChessBoard::is_valid].
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ValidationError {
|
||||
/// Too many pieces.
|
||||
TooManyPieces,
|
||||
/// Missing king.
|
||||
MissingKing,
|
||||
/// Pawns on the first/last rank.
|
||||
InvalidPawnPosition,
|
||||
/// Castling rights do not match up with the state of the board.
|
||||
InvalidCastlingRights,
|
||||
/// En-passant target square is not empty, behind an opponent's pawn, on the correct rank.
|
||||
InvalidEnPassant,
|
||||
/// The two kings are next to each other.
|
||||
NeighbouringKings,
|
||||
/// The opponent is currently in check.
|
||||
OpponentInCheck,
|
||||
/// The piece-specific boards are overlapping.
|
||||
OverlappingPieces,
|
||||
/// The color-specific boards are overlapping.
|
||||
OverlappingColors,
|
||||
/// The pre-computed combined occupancy boards does not match the other boards.
|
||||
ErroneousCombinedOccupancy,
|
||||
/// Half-move clock is higher than total number of plies.
|
||||
HalfMoveClockTooHigh,
|
||||
/// The total plie count does not match the current player.
|
||||
IncoherentPlieCount,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ValidationError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let error_msg = match self {
|
||||
Self::TooManyPieces => "too many pieces",
|
||||
Self::MissingKing => "missing king",
|
||||
Self::InvalidPawnPosition => "pawns on the first/last rank",
|
||||
Self::InvalidCastlingRights => {
|
||||
"castling rights do not match up with the state of the board"
|
||||
}
|
||||
Self::InvalidEnPassant => {
|
||||
"en-passant target square is not empty, behind an opponent's pawn, on the correct rank"
|
||||
}
|
||||
Self::NeighbouringKings => "the two kings are next to each other",
|
||||
Self::OpponentInCheck => "the opponent is currently in check",
|
||||
Self::OverlappingPieces => "the piece-specific boards are overlapping",
|
||||
Self::OverlappingColors => "the color-specific boards are overlapping",
|
||||
Self::ErroneousCombinedOccupancy => {
|
||||
"the pre-computed combined occupancy boards does not match the other boards"
|
||||
}
|
||||
Self::HalfMoveClockTooHigh => "half-move clock is higher than total number of plies",
|
||||
Self::IncoherentPlieCount => "the total plie count does not match the current player",
|
||||
};
|
||||
write!(f, "{}", error_msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ValidationError {}
|
|
@ -1,828 +0,0 @@
|
|||
use crate::movegen;
|
||||
|
||||
use super::{Bitboard, CastleRights, Color, File, Move, Piece, Rank, Square};
|
||||
|
||||
mod builder;
|
||||
pub use builder::*;
|
||||
|
||||
mod error;
|
||||
pub use error::*;
|
||||
|
||||
/// Represent an on-going chess game.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ChessBoard {
|
||||
/// A [Bitboard] of occupancy for each piece type, discarding color. Indexed by [Piece::index].
|
||||
piece_occupancy: [Bitboard; Piece::NUM_VARIANTS],
|
||||
/// A [Bitboard] of occupancy for each color, discarding piece type. Indexed by [Piece::index].
|
||||
color_occupancy: [Bitboard; Color::NUM_VARIANTS],
|
||||
/// A [Bitboard] representing all squares currently occupied by a piece.
|
||||
combined_occupancy: Bitboard,
|
||||
/// The allowed [CastleRights] for either color. Indexed by [Color::index].
|
||||
castle_rights: [CastleRights; Color::NUM_VARIANTS],
|
||||
/// A potential en-passant attack.
|
||||
/// Either `None` if no double-step pawn move was made in the previous half-turn, or
|
||||
/// `Some(target_square)` if a double-step move was made.
|
||||
en_passant: Option<Square>,
|
||||
/// The number of half-turns without either a pawn push or capture.
|
||||
half_move_clock: u32, // Should *probably* never go higher than 100.
|
||||
/// The number of half-turns so far.
|
||||
total_plies: u32, // Should be plenty.
|
||||
/// The current player turn.
|
||||
side: Color,
|
||||
}
|
||||
|
||||
/// The state which can't be reversed when doing/un-doing a [Move].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct NonReversibleState {
|
||||
castle_rights: [CastleRights; Color::NUM_VARIANTS],
|
||||
en_passant: Option<Square>,
|
||||
half_move_clock: u32, // Should *probably* never go higher than 100.
|
||||
captured_piece: Option<Piece>,
|
||||
}
|
||||
|
||||
impl ChessBoard {
|
||||
/// Which player's turn is it.
|
||||
#[inline(always)]
|
||||
pub fn current_player(&self) -> Color {
|
||||
self.side
|
||||
}
|
||||
|
||||
/// Return the target [Square] that can be captured en-passant, or `None`
|
||||
#[inline(always)]
|
||||
pub fn en_passant(&self) -> Option<Square> {
|
||||
self.en_passant
|
||||
}
|
||||
|
||||
/// Return the [CastleRights] for the given [Color].
|
||||
#[inline(always)]
|
||||
pub fn castle_rights(&self, color: Color) -> CastleRights {
|
||||
self.castle_rights[color.index()]
|
||||
}
|
||||
|
||||
/// Return the [CastleRights] for the given [Color]. Allow mutations.
|
||||
#[inline(always)]
|
||||
fn castle_rights_mut(&mut self, color: Color) -> &mut CastleRights {
|
||||
&mut self.castle_rights[color.index()]
|
||||
}
|
||||
|
||||
/// Get the [Bitboard] representing all pieces of the given [Piece] and [Color] type.
|
||||
#[inline(always)]
|
||||
pub fn occupancy(&self, piece: Piece, color: Color) -> Bitboard {
|
||||
self.piece_occupancy(piece) & self.color_occupancy(color)
|
||||
}
|
||||
|
||||
/// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color.
|
||||
#[inline(always)]
|
||||
pub fn piece_occupancy(&self, piece: Piece) -> Bitboard {
|
||||
self.piece_occupancy[piece.index()]
|
||||
}
|
||||
|
||||
/// Get the [Bitboard] representing all pieces of the given [Piece] type, discarding color.
|
||||
/// Allow mutating the state.
|
||||
#[inline(always)]
|
||||
fn piece_occupancy_mut(&mut self, piece: Piece) -> &mut Bitboard {
|
||||
&mut self.piece_occupancy[piece.index()]
|
||||
}
|
||||
|
||||
/// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece
|
||||
/// type.
|
||||
#[inline(always)]
|
||||
pub fn color_occupancy(&self, color: Color) -> Bitboard {
|
||||
self.color_occupancy[color.index()]
|
||||
}
|
||||
|
||||
/// Get the [Bitboard] representing all colors of the given [Color] type, discarding piece
|
||||
/// type. Allow mutating the state.
|
||||
#[inline(always)]
|
||||
fn color_occupancy_mut(&mut self, color: Color) -> &mut Bitboard {
|
||||
&mut self.color_occupancy[color.index()]
|
||||
}
|
||||
|
||||
/// Get the [Bitboard] representing all pieces on the board.
|
||||
#[inline(always)]
|
||||
pub fn combined_occupancy(&self) -> Bitboard {
|
||||
self.combined_occupancy
|
||||
}
|
||||
|
||||
/// Return the number of half-turns without either a pawn push or a capture.
|
||||
#[inline(always)]
|
||||
pub fn half_move_clock(&self) -> u32 {
|
||||
self.half_move_clock
|
||||
}
|
||||
|
||||
/// Return the total number of plies (i.e: half-turns) played so far.
|
||||
#[inline(always)]
|
||||
pub fn total_plies(&self) -> u32 {
|
||||
self.total_plies
|
||||
}
|
||||
|
||||
/// Return the [Bitboard] corresponding to all the opponent's pieces threatening the current
|
||||
/// player's king.
|
||||
#[inline(always)]
|
||||
pub fn checkers(&self) -> Bitboard {
|
||||
self.compute_checkers(self.current_player())
|
||||
}
|
||||
|
||||
/// Quickly add/remove a piece on the [Bitboard]s that are part of the [ChessBoard] state.
|
||||
#[inline(always)]
|
||||
fn xor(&mut self, color: Color, piece: Piece, square: Square) {
|
||||
*self.piece_occupancy_mut(piece) ^= square;
|
||||
*self.color_occupancy_mut(color) ^= square;
|
||||
self.combined_occupancy ^= square;
|
||||
}
|
||||
|
||||
/// Compute the change of [CastleRights] from moving/taking a piece.
|
||||
fn update_castling(&mut self, color: Color, piece: Piece, file: File) {
|
||||
let original = self.castle_rights(color);
|
||||
let new_rights = match (piece, file) {
|
||||
(Piece::Rook, File::A) => original.without_queen_side(),
|
||||
(Piece::Rook, File::H) => original.without_king_side(),
|
||||
(Piece::King, _) => CastleRights::NoSide,
|
||||
_ => return,
|
||||
};
|
||||
if new_rights != original {
|
||||
*self.castle_rights_mut(color) = new_rights;
|
||||
}
|
||||
}
|
||||
|
||||
/// Play the given [Move], return a copy of the board with the resulting state.
|
||||
#[inline(always)]
|
||||
pub fn play_move(&self, chess_move: Move) -> Self {
|
||||
let mut res = self.clone();
|
||||
res.play_move_inplace(chess_move);
|
||||
res
|
||||
}
|
||||
|
||||
/// Play the given [Move] in place, returning all non-revertible state (e.g: en-passant,
|
||||
/// etc...).
|
||||
#[inline(always)]
|
||||
pub fn play_move_inplace(&mut self, chess_move: Move) -> NonReversibleState {
|
||||
let opponent = !self.current_player();
|
||||
let move_piece = Piece::iter()
|
||||
.find(|&p| !(self.piece_occupancy(p) & chess_move.start()).is_empty())
|
||||
.unwrap();
|
||||
let captured_piece = Piece::iter()
|
||||
.skip(1) // No need to check for the king here
|
||||
.find(|&p| !(self.occupancy(p, opponent) & chess_move.destination()).is_empty());
|
||||
let is_double_step = move_piece == Piece::Pawn
|
||||
&& chess_move.start().rank() == self.current_player().second_rank()
|
||||
&& chess_move.destination().rank() == self.current_player().fourth_rank();
|
||||
|
||||
// Save non-revertible state
|
||||
let state = NonReversibleState {
|
||||
castle_rights: self.castle_rights,
|
||||
en_passant: self.en_passant,
|
||||
half_move_clock: self.half_move_clock,
|
||||
captured_piece,
|
||||
};
|
||||
|
||||
// Non-revertible state modification
|
||||
if captured_piece.is_some() || move_piece == Piece::Pawn {
|
||||
self.half_move_clock = 0;
|
||||
} else {
|
||||
self.half_move_clock += 1;
|
||||
}
|
||||
if is_double_step {
|
||||
let target_square = Square::new(
|
||||
chess_move.destination().file(),
|
||||
self.current_player().third_rank(),
|
||||
);
|
||||
self.en_passant = Some(target_square);
|
||||
} else {
|
||||
self.en_passant = None;
|
||||
}
|
||||
self.update_castling(self.current_player(), move_piece, chess_move.start().file());
|
||||
if let Some(piece) = captured_piece {
|
||||
self.xor(opponent, piece, chess_move.destination());
|
||||
// If a rook is captured, it loses its castling rights
|
||||
self.update_castling(opponent, piece, chess_move.destination().file());
|
||||
}
|
||||
|
||||
// Revertible state modification
|
||||
let dest_piece = chess_move.promotion().unwrap_or(move_piece);
|
||||
self.xor(self.current_player(), move_piece, chess_move.start());
|
||||
self.xor(self.current_player(), dest_piece, chess_move.destination());
|
||||
self.total_plies += 1;
|
||||
self.side = !self.side;
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
/// Reverse the effect of playing the given [Move], and return to the given
|
||||
/// [NonReversibleState].
|
||||
#[inline(always)]
|
||||
pub fn unplay_move(&mut self, chess_move: Move, previous: NonReversibleState) {
|
||||
// Restore non-revertible state
|
||||
self.castle_rights = previous.castle_rights;
|
||||
self.en_passant = previous.en_passant;
|
||||
self.half_move_clock = previous.half_move_clock;
|
||||
|
||||
let move_piece = Piece::iter()
|
||||
// We're looking for the *destination* as this is *undoing* the move
|
||||
.find(|&p| !(self.piece_occupancy(p) & chess_move.destination()).is_empty())
|
||||
.unwrap();
|
||||
|
||||
if let Some(piece) = previous.captured_piece {
|
||||
// The capture affected the *current* player, from our post-move POV
|
||||
self.xor(self.current_player(), piece, chess_move.destination());
|
||||
}
|
||||
|
||||
// Restore revertible state
|
||||
let start_piece = chess_move.promotion().map_or(move_piece, |_| Piece::Pawn);
|
||||
self.xor(!self.current_player(), move_piece, chess_move.destination());
|
||||
self.xor(!self.current_player(), start_piece, chess_move.start());
|
||||
self.total_plies -= 1;
|
||||
self.side = !self.side;
|
||||
}
|
||||
|
||||
/// Return true if the current state of the board looks valid, false if something is definitely
|
||||
/// wrong.
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.validate().is_ok()
|
||||
}
|
||||
|
||||
/// Validate the state of the board. Return Err([ValidationError]) if an issue is found.
|
||||
pub fn validate(&self) -> Result<(), ValidationError> {
|
||||
// The current plie count should be odd on white's turn, and vice-versa.
|
||||
if self.total_plies() % 2 != self.current_player().index() as u32 {
|
||||
return Err(ValidationError::IncoherentPlieCount);
|
||||
}
|
||||
|
||||
// Make sure the clocks are in agreement.
|
||||
if self.half_move_clock() > self.total_plies() {
|
||||
return Err(ValidationError::HalfMoveClockTooHigh);
|
||||
}
|
||||
|
||||
// Don't overlap pieces.
|
||||
for piece in Piece::iter() {
|
||||
#[allow(clippy::collapsible_if)]
|
||||
for other in Piece::iter() {
|
||||
if piece != other {
|
||||
if !(self.piece_occupancy(piece) & self.piece_occupancy(other)).is_empty() {
|
||||
return Err(ValidationError::OverlappingPieces);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't overlap colors.
|
||||
if !(self.color_occupancy(Color::White) & self.color_occupancy(Color::Black)).is_empty() {
|
||||
return Err(ValidationError::OverlappingColors);
|
||||
}
|
||||
|
||||
// Calculate the union of all pieces.
|
||||
let combined =
|
||||
Piece::iter().fold(Bitboard::EMPTY, |board, p| board | self.piece_occupancy(p));
|
||||
|
||||
// Ensure that the pre-computed version is accurate.
|
||||
if combined != self.combined_occupancy() {
|
||||
return Err(ValidationError::ErroneousCombinedOccupancy);
|
||||
}
|
||||
|
||||
// Ensure that all pieces belong to a color, and no color has pieces that don't exist.
|
||||
if combined != (self.color_occupancy(Color::White) | self.color_occupancy(Color::Black)) {
|
||||
return Err(ValidationError::ErroneousCombinedOccupancy);
|
||||
}
|
||||
|
||||
for color in Color::iter() {
|
||||
for piece in Piece::iter() {
|
||||
// Check that we have the expected number of piecese.
|
||||
let count = self.occupancy(piece, color).count();
|
||||
let possible = match piece {
|
||||
Piece::King => count <= 1,
|
||||
Piece::Pawn => count <= 8,
|
||||
Piece::Queen => count <= 9,
|
||||
_ => count <= 10,
|
||||
};
|
||||
if !possible {
|
||||
return Err(ValidationError::TooManyPieces);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we have a king
|
||||
if self.occupancy(Piece::King, color).count() != 1 {
|
||||
return Err(ValidationError::MissingKing);
|
||||
}
|
||||
|
||||
// Check that don't have too many pieces in total
|
||||
if self.color_occupancy(color).count() > 16 {
|
||||
return Err(ValidationError::TooManyPieces);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that pawns aren't in first/last rank.
|
||||
if !(self.piece_occupancy(Piece::Pawn)
|
||||
& (Rank::First.into_bitboard() | Rank::Eighth.into_bitboard()))
|
||||
.is_empty()
|
||||
{
|
||||
return Err(ValidationError::InvalidPawnPosition);
|
||||
}
|
||||
|
||||
// Verify that rooks and kings that are allowed to castle have not been moved.
|
||||
for color in Color::iter() {
|
||||
let castle_rights = self.castle_rights(color);
|
||||
|
||||
// Nothing to check if there are no castlings allowed.
|
||||
if castle_rights == CastleRights::NoSide {
|
||||
continue;
|
||||
}
|
||||
|
||||
let actual_rooks = self.occupancy(Piece::Rook, color);
|
||||
let expected_rooks = castle_rights.unmoved_rooks(color);
|
||||
// We must check the intersection, in case there are more than 2 rooks on the board.
|
||||
if (expected_rooks & actual_rooks) != expected_rooks {
|
||||
return Err(ValidationError::InvalidCastlingRights);
|
||||
}
|
||||
|
||||
let actual_king = self.occupancy(Piece::King, color);
|
||||
let expected_king = Square::new(File::E, color.first_rank());
|
||||
// We have checked that there is exactly one king, no need for intersecting the sets.
|
||||
if actual_king != expected_king.into_bitboard() {
|
||||
return Err(ValidationError::InvalidCastlingRights);
|
||||
}
|
||||
}
|
||||
|
||||
// En-passant validation
|
||||
if let Some(square) = self.en_passant() {
|
||||
// Must be empty
|
||||
if !(self.combined_occupancy() & square).is_empty() {
|
||||
return Err(ValidationError::InvalidEnPassant);
|
||||
}
|
||||
|
||||
let opponent = !self.current_player();
|
||||
|
||||
// Must be on the opponent's third rank
|
||||
if (square & opponent.third_rank().into_bitboard()).is_empty() {
|
||||
return Err(ValidationError::InvalidEnPassant);
|
||||
}
|
||||
|
||||
// Must be behind a pawn
|
||||
let opponent_pawns = self.occupancy(Piece::Pawn, opponent);
|
||||
let double_pushed_pawn = self
|
||||
.current_player()
|
||||
.backward_direction()
|
||||
.move_board(square.into_bitboard());
|
||||
if (opponent_pawns & double_pushed_pawn).is_empty() {
|
||||
return Err(ValidationError::InvalidEnPassant);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that kings don't touch each other.
|
||||
let white_king = self.occupancy(Piece::King, Color::White);
|
||||
let black_king = self.occupancy(Piece::King, Color::Black);
|
||||
// Unwrap is fine, we already checked that there is exactly one king of each color
|
||||
if !(movegen::king_moves(white_king.try_into().unwrap()) & black_king).is_empty() {
|
||||
return Err(ValidationError::NeighbouringKings);
|
||||
}
|
||||
|
||||
// Check that the opponent is not currently in check.
|
||||
if !self.compute_checkers(!self.current_player()).is_empty() {
|
||||
return Err(ValidationError::OpponentInCheck);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute all pieces that are currently threatening the given [Color]'s king.
|
||||
fn compute_checkers(&self, color: Color) -> Bitboard {
|
||||
// Unwrap is fine, there should always be exactly one king per color
|
||||
let king = (self.occupancy(Piece::King, color)).try_into().unwrap();
|
||||
|
||||
let opponent = !color;
|
||||
|
||||
// No need to remove our pieces from the generated moves, we just want to check if we
|
||||
// intersect with the opponent's pieces, rather than generate only valid moves.
|
||||
let bishops = {
|
||||
let queens = self.occupancy(Piece::Queen, opponent);
|
||||
let bishops = self.occupancy(Piece::Bishop, opponent);
|
||||
let bishop_attacks = movegen::bishop_moves(king, self.combined_occupancy());
|
||||
(queens | bishops) & bishop_attacks
|
||||
};
|
||||
let rooks = {
|
||||
let queens = self.occupancy(Piece::Queen, opponent);
|
||||
let rooks = self.occupancy(Piece::Rook, opponent);
|
||||
let rook_attacks = movegen::rook_moves(king, self.combined_occupancy());
|
||||
(queens | rooks) & rook_attacks
|
||||
};
|
||||
let knights = {
|
||||
let knights = self.occupancy(Piece::Knight, opponent);
|
||||
let knight_attacks = movegen::knight_moves(king);
|
||||
knights & knight_attacks
|
||||
};
|
||||
let pawns = {
|
||||
let pawns = self.occupancy(Piece::Pawn, opponent);
|
||||
let pawn_attacks = movegen::pawn_attacks(color, king);
|
||||
pawns & pawn_attacks
|
||||
};
|
||||
|
||||
bishops | rooks | knights | pawns
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the starting position as a default value, corresponding to the
|
||||
/// "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" FEN string
|
||||
impl Default for ChessBoard {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
piece_occupancy: [
|
||||
// King
|
||||
Square::E1 | Square::E8,
|
||||
// Queen
|
||||
Square::D1 | Square::D8,
|
||||
// Rook
|
||||
Square::A1 | Square::A8 | Square::H1 | Square::H8,
|
||||
// Bishop
|
||||
Square::C1 | Square::C8 | Square::F1 | Square::F8,
|
||||
// Knight
|
||||
Square::B1 | Square::B8 | Square::G1 | Square::G8,
|
||||
// Pawn
|
||||
Rank::Second.into_bitboard() | Rank::Seventh.into_bitboard(),
|
||||
],
|
||||
color_occupancy: [
|
||||
Rank::First.into_bitboard() | Rank::Second.into_bitboard(),
|
||||
Rank::Seventh.into_bitboard() | Rank::Eighth.into_bitboard(),
|
||||
],
|
||||
combined_occupancy: Rank::First.into_bitboard()
|
||||
| Rank::Second.into_bitboard()
|
||||
| Rank::Seventh.into_bitboard()
|
||||
| Rank::Eighth.into_bitboard(),
|
||||
castle_rights: [CastleRights::BothSides; Color::NUM_VARIANTS],
|
||||
en_passant: None,
|
||||
half_move_clock: 0,
|
||||
total_plies: 0,
|
||||
side: Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::fen::FromFen;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn valid() {
|
||||
let default_position = ChessBoard::default();
|
||||
assert!(default_position.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_incoherent_plie_count() {
|
||||
let position = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
let mut board = TryInto::<ChessBoard>::try_into(builder).unwrap();
|
||||
board.total_plies = 1;
|
||||
board
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
ValidationError::IncoherentPlieCount,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_half_moves_clock() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder.with_half_move_clock(10);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::HalfMoveClockTooHigh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_overlapping_pieces() {
|
||||
let position = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
let mut board: ChessBoard = builder.try_into().unwrap();
|
||||
*board.piece_occupancy_mut(Piece::Queen) |= Square::E1.into_bitboard();
|
||||
board
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
ValidationError::OverlappingPieces,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_overlapping_colors() {
|
||||
let position = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
let mut board: ChessBoard = builder.try_into().unwrap();
|
||||
*board.color_occupancy_mut(Color::White) |= Square::E8.into_bitboard();
|
||||
board
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
ValidationError::OverlappingColors,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_combined_does_not_equal_pieces() {
|
||||
let position = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
let mut board: ChessBoard = builder.try_into().unwrap();
|
||||
*board.piece_occupancy_mut(Piece::Pawn) |= Square::E2.into_bitboard();
|
||||
board
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
ValidationError::ErroneousCombinedOccupancy,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_combined_does_not_equal_colors() {
|
||||
let position = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
let mut board: ChessBoard = builder.try_into().unwrap();
|
||||
*board.color_occupancy_mut(Color::Black) |= Square::E2.into_bitboard();
|
||||
board
|
||||
};
|
||||
assert_eq!(
|
||||
position.validate().err().unwrap(),
|
||||
ValidationError::ErroneousCombinedOccupancy,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_multiple_kings() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E2] = Some((Piece::King, Color::White));
|
||||
builder[Square::E7] = Some((Piece::King, Color::Black));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_castling_rights_no_rooks() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder.with_castle_rights(CastleRights::BothSides, Color::White);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_castling_rights_moved_king() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E2] = Some((Piece::King, Color::White));
|
||||
builder[Square::A1] = Some((Piece::Rook, Color::White));
|
||||
builder[Square::H1] = Some((Piece::Rook, Color::White));
|
||||
builder[Square::E7] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A8] = Some((Piece::Rook, Color::Black));
|
||||
builder[Square::H8] = Some((Piece::Rook, Color::Black));
|
||||
builder.with_castle_rights(CastleRights::BothSides, Color::White);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::InvalidCastlingRights);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_en_passant() {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A5] = Some((Piece::Pawn, Color::Black));
|
||||
builder.with_en_passant(Square::A6);
|
||||
TryInto::<ChessBoard>::try_into(builder).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_en_passant_not_empty() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A6] = Some((Piece::Rook, Color::Black));
|
||||
builder[Square::A5] = Some((Piece::Pawn, Color::Black));
|
||||
builder.with_en_passant(Square::A6);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_en_passant_not_behind_pawn() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A5] = Some((Piece::Rook, Color::Black));
|
||||
builder.with_en_passant(Square::A6);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_en_passant_incorrect_rank() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder[Square::A4] = Some((Piece::Pawn, Color::Black));
|
||||
builder.with_en_passant(Square::A5);
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::InvalidEnPassant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_kings_next_to_each_other() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E2] = Some((Piece::King, Color::Black));
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::NeighbouringKings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_opponent_in_check() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::E1] = Some((Piece::King, Color::White));
|
||||
builder[Square::E7] = Some((Piece::Queen, Color::White));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::OpponentInCheck);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_pawn_on_first_rank() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::H1] = Some((Piece::King, Color::White));
|
||||
builder[Square::A1] = Some((Piece::Pawn, Color::White));
|
||||
builder[Square::H8] = Some((Piece::King, Color::Black));
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::InvalidPawnPosition);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_too_many_pieces() {
|
||||
let res = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::H1] = Some((Piece::King, Color::White));
|
||||
builder[Square::H8] = Some((Piece::King, Color::Black));
|
||||
for square in (File::B.into_bitboard() | File::C.into_bitboard()).into_iter() {
|
||||
builder[square] = Some((Piece::Pawn, Color::White));
|
||||
}
|
||||
for square in (File::F.into_bitboard() | File::G.into_bitboard()).into_iter() {
|
||||
builder[square] = Some((Piece::Pawn, Color::Black));
|
||||
}
|
||||
TryInto::<ChessBoard>::try_into(builder)
|
||||
};
|
||||
assert_eq!(res.err().unwrap(), ValidationError::TooManyPieces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkers() {
|
||||
let position = {
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
builder[Square::C1] = Some((Piece::Knight, Color::White));
|
||||
builder[Square::D3] = Some((Piece::Bishop, Color::White));
|
||||
builder[Square::E1] = Some((Piece::Rook, Color::White));
|
||||
builder[Square::E2] = Some((Piece::King, Color::White));
|
||||
builder[Square::H2] = Some((Piece::Queen, Color::White));
|
||||
builder[Square::G1] = Some((Piece::Knight, Color::Black));
|
||||
builder[Square::F3] = Some((Piece::Bishop, Color::Black));
|
||||
builder[Square::A2] = Some((Piece::Rook, Color::Black));
|
||||
builder[Square::E8] = Some((Piece::King, Color::Black));
|
||||
builder[Square::E7] = Some((Piece::Queen, Color::Black));
|
||||
TryInto::<ChessBoard>::try_into(builder).unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
position.checkers(),
|
||||
Square::A2 | Square::E7 | Square::F3 | Square::G1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_move() {
|
||||
// Start from default position
|
||||
let mut position = ChessBoard::default();
|
||||
// Modify it to account for e4 move
|
||||
position.play_move_inplace(Move::new(Square::E2, Square::E4, None));
|
||||
assert_eq!(
|
||||
position,
|
||||
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
|
||||
.unwrap()
|
||||
);
|
||||
// And now c5
|
||||
position.play_move_inplace(Move::new(Square::C7, Square::C5, None));
|
||||
assert_eq!(
|
||||
position,
|
||||
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
|
||||
.unwrap()
|
||||
);
|
||||
// Finally, Nf3
|
||||
position.play_move_inplace(Move::new(Square::G1, Square::F3, None));
|
||||
assert_eq!(
|
||||
position,
|
||||
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ")
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_move_capture_changes_castling() {
|
||||
let mut position = ChessBoard::from_fen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1").unwrap();
|
||||
let expected = ChessBoard::from_fen("r3k2R/8/8/8/8/8/8/R3K3 b Qq - 0 1").unwrap();
|
||||
|
||||
let capture = Move::new(Square::H1, Square::H8, None);
|
||||
|
||||
position.play_move_inplace(capture);
|
||||
assert_eq!(position, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_move_and_undo() {
|
||||
// Start from default position
|
||||
let mut position = ChessBoard::default();
|
||||
// Modify it to account for e4 move
|
||||
let move_1 = Move::new(Square::E2, Square::E4, None);
|
||||
let state_1 = position.play_move_inplace(move_1);
|
||||
// And now c5
|
||||
let move_2 = Move::new(Square::C7, Square::C5, None);
|
||||
let state_2 = position.play_move_inplace(move_2);
|
||||
// Finally, Nf3
|
||||
let move_3 = Move::new(Square::G1, Square::F3, None);
|
||||
let state_3 = position.play_move_inplace(move_3);
|
||||
// Now revert each move one-by-one
|
||||
position.unplay_move(move_3, state_3);
|
||||
assert_eq!(
|
||||
position,
|
||||
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
|
||||
.unwrap()
|
||||
);
|
||||
position.unplay_move(move_2, state_2);
|
||||
assert_eq!(
|
||||
position,
|
||||
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
|
||||
.unwrap()
|
||||
);
|
||||
position.unplay_move(move_1, state_1);
|
||||
assert_eq!(
|
||||
position,
|
||||
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_move_undo_capture() {
|
||||
let mut position = ChessBoard::from_fen("3q3k/8/8/8/8/8/8/K2Q4 w - - 0 1").unwrap();
|
||||
let expected = ChessBoard::from_fen("3Q3k/8/8/8/8/8/8/K7 b - - 0 1").unwrap();
|
||||
let original = position.clone();
|
||||
|
||||
let capture = Move::new(Square::D1, Square::D8, None);
|
||||
|
||||
let state = position.play_move_inplace(capture);
|
||||
assert_eq!(position, expected);
|
||||
|
||||
position.unplay_move(capture, state);
|
||||
assert_eq!(position, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn play_move_undo_promotion() {
|
||||
let mut position = ChessBoard::from_fen("7k/P7/8/8/8/8/8/K7 w - - 0 1").unwrap();
|
||||
let expected = ChessBoard::from_fen("N6k/8/8/8/8/8/8/K7 b - - 0 1").unwrap();
|
||||
let original = position.clone();
|
||||
|
||||
let promotion = Move::new(Square::A7, Square::A8, Some(Piece::Knight));
|
||||
|
||||
let state = position.play_move_inplace(promotion);
|
||||
assert_eq!(position, expected);
|
||||
|
||||
position.unplay_move(promotion, state);
|
||||
assert_eq!(position, original);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
use super::{Direction, Rank};
|
||||
use super::{Direction, FromFen, Rank};
|
||||
use crate::error::Error;
|
||||
|
||||
/// An enum representing the color of a player.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
|
@ -18,28 +19,15 @@ impl Color {
|
|||
Self::ALL.iter().cloned()
|
||||
}
|
||||
|
||||
/// Convert from a color index into a [Color] type.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the index is out of bounds.
|
||||
/// Convert from a piece index into a [Color] type.
|
||||
#[inline(always)]
|
||||
pub fn from_index(index: usize) -> Self {
|
||||
Self::try_from_index(index).expect("index out of bouds")
|
||||
assert!(index < Self::NUM_VARIANTS);
|
||||
// SAFETY: we know the value is in-bounds
|
||||
unsafe { Self::from_index_unchecked(index) }
|
||||
}
|
||||
|
||||
/// Convert from a color index into a [Color] type. Returns [None] if the index is out of
|
||||
/// bounds.
|
||||
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||
if index < Self::NUM_VARIANTS {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
Some(unsafe { Self::from_index_unchecked(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from a color index into a [Color] type, no bounds checking.
|
||||
/// Convert from a piece index into a [Color] type, no bounds checking.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
|
@ -119,6 +107,20 @@ impl Color {
|
|||
}
|
||||
}
|
||||
|
||||
/// Convert a side to move segment of a FEN string to a [Color].
|
||||
impl FromFen for Color {
|
||||
type Err = Error;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
let res = match s {
|
||||
"w" => Color::White,
|
||||
"b" => Color::Black,
|
||||
_ => return Err(Error::InvalidFen),
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Not for Color {
|
||||
type Output = Color;
|
||||
|
||||
|
|
|
@ -128,7 +128,8 @@ impl Direction {
|
|||
/// 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.
|
||||
/// cannot be slided over. The slide is over once a square that is part of `blockers` is
|
||||
/// reached.
|
||||
/// It does not make sense to use this method with knight-only directions, and it will panic in
|
||||
/// debug-mode if it happens.
|
||||
#[inline(always)]
|
||||
|
|
6
src/board/fen.rs
Normal file
6
src/board/fen.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
/// A trait to mark items that can be converted from a FEN input.
|
||||
pub trait FromFen: Sized {
|
||||
type Err;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err>;
|
||||
}
|
|
@ -35,23 +35,11 @@ impl File {
|
|||
}
|
||||
|
||||
/// Convert from a file index into a [File] type.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the index is out of bounds.
|
||||
#[inline(always)]
|
||||
pub fn from_index(index: usize) -> Self {
|
||||
Self::try_from_index(index).expect("index out of bouds")
|
||||
}
|
||||
|
||||
/// Convert from a file index into a [File] type. Returns [None] if the index is out of bounds.
|
||||
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||
if index < Self::NUM_VARIANTS {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
Some(unsafe { Self::from_index_unchecked(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
assert!(index < 8);
|
||||
// SAFETY: we know the value is in-bounds
|
||||
unsafe { Self::from_index_unchecked(index) }
|
||||
}
|
||||
|
||||
/// Convert from a file index into a [File] type, no bounds checking.
|
||||
|
|
|
@ -13,6 +13,9 @@ pub use color::*;
|
|||
pub mod direction;
|
||||
pub use direction::*;
|
||||
|
||||
pub mod fen;
|
||||
pub use fen::*;
|
||||
|
||||
pub mod file;
|
||||
pub use file::*;
|
||||
|
||||
|
|
|
@ -1,42 +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 {
|
||||
start: Square,
|
||||
destination: Square,
|
||||
promotion: Option<Piece>,
|
||||
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.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[inline(always)]
|
||||
pub fn new(start: Square, destination: Square, promotion: Option<Piece>) -> Self {
|
||||
Self {
|
||||
start,
|
||||
destination,
|
||||
promotion,
|
||||
}
|
||||
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 {
|
||||
self.start
|
||||
let index = ((self.0 >> shift::START) & shift::START_MASK) as usize;
|
||||
// SAFETY: we know the value is in-bounds
|
||||
unsafe { Square::from_index_unchecked(index) }
|
||||
}
|
||||
|
||||
/// Get the [Square] that this move ends on.
|
||||
#[inline(always)]
|
||||
pub fn destination(self) -> Square {
|
||||
self.destination
|
||||
let index = ((self.0 >> shift::DESTINATION) & shift::DESTINATION_MASK) as usize;
|
||||
// SAFETY: we know the value is in-bounds
|
||||
unsafe { Square::from_index_unchecked(index) }
|
||||
}
|
||||
|
||||
/// Get the [Piece] that this move captures, or `None` if there are no captures.
|
||||
#[inline(always)]
|
||||
pub fn capture(self) -> Option<Piece> {
|
||||
let index = ((self.0 >> shift::CAPTURE) & shift::CAPTURE_MASK) as usize;
|
||||
if index < Piece::NUM_VARIANTS {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
unsafe { Some(Piece::from_index_unchecked(index)) }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the [Piece] that this move promotes to, or `None` if there are no promotions.
|
||||
#[inline(always)]
|
||||
pub fn promotion(self) -> Option<Piece> {
|
||||
self.promotion
|
||||
let index = ((self.0 >> shift::PROMOTION) & shift::PROMOTION_MASK) as usize;
|
||||
if index < Piece::NUM_VARIANTS {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
unsafe { Some(Piece::from_index_unchecked(index)) }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the whether or not the move is an en-passant capture.
|
||||
#[inline(always)]
|
||||
pub fn is_en_passant(self) -> bool {
|
||||
let index = (self.0 >> shift::EN_PASSANT) & shift::EN_PASSANT_MASK;
|
||||
index != 0
|
||||
}
|
||||
|
||||
/// Get the whether or not the move is a pawn double step.
|
||||
#[inline(always)]
|
||||
pub fn is_double_step(self) -> bool {
|
||||
let index = (self.0 >> shift::DOUBLE_STEP) & shift::DOUBLE_STEP_MASK;
|
||||
index != 0
|
||||
}
|
||||
|
||||
/// Get the whether or not the move is a king castling.
|
||||
#[inline(always)]
|
||||
pub fn is_castling(self) -> bool {
|
||||
let index = (self.0 >> shift::CASTLING) & shift::CASTLING_MASK;
|
||||
index != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn builder_simple() {
|
||||
let chess_move: Move = MoveBuilder {
|
||||
piece: Piece::Queen,
|
||||
start: Square::A2,
|
||||
destination: Square::A3,
|
||||
capture: None,
|
||||
promotion: None,
|
||||
en_passant: false,
|
||||
double_step: false,
|
||||
castling: false,
|
||||
}
|
||||
.into();
|
||||
assert_eq!(chess_move.piece(), Piece::Queen);
|
||||
assert_eq!(chess_move.start(), Square::A2);
|
||||
assert_eq!(chess_move.destination(), Square::A3);
|
||||
assert_eq!(chess_move.capture(), None);
|
||||
assert_eq!(chess_move.promotion(), None);
|
||||
assert!(!chess_move.is_en_passant());
|
||||
assert!(!chess_move.is_double_step());
|
||||
assert!(!chess_move.is_castling());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_all_fields() {
|
||||
let chess_move: Move = MoveBuilder {
|
||||
piece: Piece::Pawn,
|
||||
start: Square::A7,
|
||||
destination: Square::B8,
|
||||
capture: Some(Piece::Queen),
|
||||
promotion: Some(Piece::Knight),
|
||||
en_passant: true,
|
||||
double_step: true,
|
||||
castling: true,
|
||||
}
|
||||
.into();
|
||||
assert_eq!(chess_move.piece(), Piece::Pawn);
|
||||
assert_eq!(chess_move.start(), Square::A7);
|
||||
assert_eq!(chess_move.destination(), Square::B8);
|
||||
assert_eq!(chess_move.capture(), Some(Piece::Queen));
|
||||
assert_eq!(chess_move.promotion(), Some(Piece::Knight));
|
||||
assert!(chess_move.is_en_passant());
|
||||
assert!(chess_move.is_double_step());
|
||||
assert!(chess_move.is_castling());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
use super::FromFen;
|
||||
use crate::error::Error;
|
||||
|
||||
/// An enum representing the type of a piece.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Piece {
|
||||
|
@ -28,24 +31,11 @@ impl Piece {
|
|||
}
|
||||
|
||||
/// Convert from a piece index into a [Piece] type.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the index is out of bounds.
|
||||
#[inline(always)]
|
||||
pub fn from_index(index: usize) -> Self {
|
||||
Self::try_from_index(index).expect("index out of bouds")
|
||||
}
|
||||
|
||||
/// Convert from a piece index into a [Piece] type. Returns [None] if the index is out of
|
||||
/// bounds.
|
||||
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||
if index < Self::NUM_VARIANTS {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
Some(unsafe { Self::from_index_unchecked(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
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.
|
||||
|
@ -65,6 +55,24 @@ impl Piece {
|
|||
}
|
||||
}
|
||||
|
||||
/// Convert a piece in FEN notation to a [Piece].
|
||||
impl FromFen for Piece {
|
||||
type Err = Error;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
let res = match s {
|
||||
"p" | "P" => Self::Pawn,
|
||||
"n" | "N" => Self::Knight,
|
||||
"b" | "B" => Self::Bishop,
|
||||
"r" | "R" => Self::Rook,
|
||||
"q" | "Q" => Self::Queen,
|
||||
"k" | "K" => Self::King,
|
||||
_ => return Err(Error::InvalidFen),
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
|
|
@ -35,23 +35,11 @@ impl Rank {
|
|||
}
|
||||
|
||||
/// Convert from a rank index into a [Rank] type.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the index is out of bounds.
|
||||
#[inline(always)]
|
||||
pub fn from_index(index: usize) -> Self {
|
||||
Self::try_from_index(index).expect("index out of bouds")
|
||||
}
|
||||
|
||||
/// Convert from a rank index into a [Rank] type. Returns [None] if the index is out of bounds.
|
||||
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||
if index < Self::NUM_VARIANTS {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
Some(unsafe { Self::from_index_unchecked(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
assert!(index < 8);
|
||||
// SAFETY: we know the value is in-bounds
|
||||
unsafe { Self::from_index_unchecked(index) }
|
||||
}
|
||||
|
||||
/// Convert from a rank index into a [Rank] type, no bounds checking.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::{Bitboard, File, Rank};
|
||||
use crate::utils::static_assert;
|
||||
use super::{Bitboard, File, FromFen, Rank};
|
||||
use crate::{error::Error, utils::static_assert};
|
||||
|
||||
/// Represent a square on a chessboard. Defined in the same order as the
|
||||
/// [Bitboard] squares.
|
||||
|
@ -39,10 +39,6 @@ impl Square {
|
|||
];
|
||||
|
||||
/// Construct a [Square] from a [File] and [Rank].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the index is out of bounds.
|
||||
#[inline(always)]
|
||||
pub fn new(file: File, rank: Rank) -> Self {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
|
@ -57,18 +53,9 @@ impl Square {
|
|||
/// Convert from a square index into a [Square] type.
|
||||
#[inline(always)]
|
||||
pub fn from_index(index: usize) -> Self {
|
||||
Self::try_from_index(index).expect("index out of bouds")
|
||||
}
|
||||
|
||||
/// Convert from a square index into a [Square] type. Returns [None] if the index is out of
|
||||
/// bounds.
|
||||
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||
if index < Self::NUM_VARIANTS {
|
||||
// SAFETY: we know the value is in-bounds
|
||||
Some(unsafe { Self::from_index_unchecked(index) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
assert!(index < 64);
|
||||
// SAFETY: we know the value is in-bounds
|
||||
unsafe { Self::from_index_unchecked(index) }
|
||||
}
|
||||
|
||||
/// Convert from a square index into a [Square] type, no bounds checking.
|
||||
|
@ -120,6 +107,23 @@ impl Square {
|
|||
}
|
||||
}
|
||||
|
||||
/// Convert an en-passant target square segment of a FEN string to an optional [Square].
|
||||
impl FromFen for Option<Square> {
|
||||
type Err = Error;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
let res = match s.as_bytes() {
|
||||
[b'-'] => None,
|
||||
[file @ b'a'..=b'h', rank @ b'1'..=b'8'] => Some(Square::new(
|
||||
File::from_index((file - b'a') as usize),
|
||||
Rank::from_index((rank - b'1') as usize),
|
||||
)),
|
||||
_ => return Err(Error::InvalidFen),
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shift the square's index left by the amount given.
|
||||
impl std::ops::Shl<usize> for Square {
|
||||
type Output = Square;
|
||||
|
|
144
src/build.rs
Normal file
144
src/build.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
use std::io::{Result, Write};
|
||||
|
||||
pub mod board;
|
||||
pub mod error;
|
||||
pub mod movegen;
|
||||
pub mod utils;
|
||||
|
||||
use crate::{
|
||||
board::{Bitboard, Color, File, Square},
|
||||
movegen::{
|
||||
naive::{
|
||||
king::king_moves,
|
||||
knight::knight_moves,
|
||||
pawn::{pawn_captures, pawn_moves},
|
||||
},
|
||||
wizardry::generation::{generate_bishop_magics, generate_rook_magics},
|
||||
Magic,
|
||||
},
|
||||
};
|
||||
|
||||
fn print_magics(out: &mut dyn Write, var_name: &str, magics: &[Magic]) -> Result<()> {
|
||||
writeln!(out, "static {}: [Magic; {}] = [", var_name, magics.len())?;
|
||||
for magic in magics.iter() {
|
||||
writeln!(
|
||||
out,
|
||||
" Magic{{magic: {}, offset: {}, mask: Bitboard({}), shift: {},}},",
|
||||
magic.magic, magic.offset, magic.mask.0, magic.shift
|
||||
)?;
|
||||
}
|
||||
writeln!(out, "];")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_boards(out: &mut dyn Write, var_name: &str, boards: &[Bitboard]) -> Result<()> {
|
||||
writeln!(out, "static {}: [Bitboard; {}] = [", var_name, boards.len())?;
|
||||
for board in boards.iter().cloned() {
|
||||
writeln!(out, " Bitboard({}),", board.0)?;
|
||||
}
|
||||
writeln!(out, "];")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_double_sided_boards(
|
||||
out: &mut dyn Write,
|
||||
var_name: &str,
|
||||
white_boards: &[Bitboard],
|
||||
black_boards: &[Bitboard],
|
||||
) -> Result<()> {
|
||||
assert_eq!(white_boards.len(), black_boards.len());
|
||||
writeln!(
|
||||
out,
|
||||
"static {}: [[Bitboard; {}]; 2] = [",
|
||||
var_name,
|
||||
white_boards.len()
|
||||
)?;
|
||||
for color in Color::iter() {
|
||||
let boards = if color == Color::White {
|
||||
white_boards
|
||||
} else {
|
||||
black_boards
|
||||
};
|
||||
writeln!(out, " [")?;
|
||||
for square in Square::iter() {
|
||||
writeln!(out, " Bitboard({}),", boards[square.index()].0)?;
|
||||
}
|
||||
writeln!(out, " ],")?;
|
||||
}
|
||||
writeln!(out, "];")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_clone)]
|
||||
fn main() -> Result<()> {
|
||||
// FIXME: rerun-if-changed directives
|
||||
|
||||
let out_dir = std::env::var_os("OUT_DIR").unwrap();
|
||||
let magic_path = std::path::Path::new(&out_dir).join("magic_tables.rs");
|
||||
let mut out = std::fs::File::create(&magic_path).unwrap();
|
||||
|
||||
let rng = random::default().seed([12, 27]);
|
||||
|
||||
{
|
||||
let (magics, moves) = generate_bishop_magics(&mut rng.clone());
|
||||
print_magics(&mut out, "BISHOP_MAGICS", &magics)?;
|
||||
print_boards(&mut out, "BISHOP_MOVES", &moves)?;
|
||||
}
|
||||
|
||||
{
|
||||
let (magics, moves) = generate_rook_magics(&mut rng.clone());
|
||||
print_magics(&mut out, "ROOK_MAGICS", &magics)?;
|
||||
print_boards(&mut out, "ROOK_MOVES", &moves)?;
|
||||
}
|
||||
|
||||
{
|
||||
let moves: Vec<_> = Square::iter().map(knight_moves).collect();
|
||||
print_boards(&mut out, "KNIGHT_MOVES", &moves)?;
|
||||
}
|
||||
|
||||
{
|
||||
let white_moves: Vec<_> = Square::iter()
|
||||
.map(|square| pawn_moves(Color::White, square, Bitboard::EMPTY))
|
||||
.collect();
|
||||
let black_moves: Vec<_> = Square::iter()
|
||||
.map(|square| pawn_moves(Color::Black, square, Bitboard::EMPTY))
|
||||
.collect();
|
||||
print_double_sided_boards(&mut out, "PAWN_MOVES", &white_moves, &black_moves)?;
|
||||
let white_attacks: Vec<_> = Square::iter()
|
||||
.map(|square| pawn_captures(Color::White, square))
|
||||
.collect();
|
||||
let black_attacks: Vec<_> = Square::iter()
|
||||
.map(|square| pawn_captures(Color::Black, square))
|
||||
.collect();
|
||||
print_double_sided_boards(&mut out, "PAWN_ATTACKS", &white_attacks, &black_attacks)?;
|
||||
}
|
||||
|
||||
{
|
||||
let moves: Vec<_> = Square::iter().map(king_moves).collect();
|
||||
print_boards(&mut out, "KING_MOVES", &moves)?;
|
||||
let king_blockers: Vec<_> = Color::iter()
|
||||
.map(|color| {
|
||||
Square::new(File::F, color.first_rank()) | Square::new(File::G, color.first_rank())
|
||||
})
|
||||
.collect();
|
||||
let queen_blockers: Vec<_> = Color::iter()
|
||||
.map(|color| {
|
||||
Square::new(File::B, color.first_rank())
|
||||
| Square::new(File::C, color.first_rank())
|
||||
| Square::new(File::D, color.first_rank())
|
||||
})
|
||||
.collect();
|
||||
print_boards(&mut out, "KING_SIDE_CASTLE_BLOCKERS", &king_blockers)?;
|
||||
print_boards(&mut out, "QUEEN_SIDE_CASTLE_BLOCKERS", &queen_blockers)?;
|
||||
}
|
||||
|
||||
// Include the generated files now that the build script has run.
|
||||
println!("cargo:rustc-cfg=generated_boards");
|
||||
|
||||
// Run the build script only if something in move generation might have changed.
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=movegen/naive/");
|
||||
println!("cargo:rerun-if-changed=movegen/wizardry/");
|
||||
|
||||
Ok(())
|
||||
}
|
19
src/error.rs
Normal file
19
src/error.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
/// A singular type for all errors that could happen when using this library.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
InvalidFen,
|
||||
InvalidPosition,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let error_msg = match self {
|
||||
Self::InvalidFen => "Invalid FEN input",
|
||||
Self::InvalidPosition => "Invalid position",
|
||||
};
|
||||
write!(f, "{}", error_msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
244
src/fen.rs
244
src/fen.rs
|
@ -1,244 +0,0 @@
|
|||
use crate::board::{
|
||||
CastleRights, ChessBoard, ChessBoardBuilder, Color, File, Piece, Rank, Square, ValidationError,
|
||||
};
|
||||
|
||||
/// A trait to mark items that can be converted from a FEN input.
|
||||
pub trait FromFen: Sized {
|
||||
type Err;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err>;
|
||||
}
|
||||
|
||||
/// A singular type for all errors that could happen during FEN parsing.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FenError {
|
||||
/// Invalid FEN input.
|
||||
InvalidFen,
|
||||
/// Invalid chess position.
|
||||
InvalidPosition(ValidationError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FenError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidFen => write!(f, "invalid FEN input"),
|
||||
Self::InvalidPosition(err) => write!(f, "invalid chess position: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FenError {}
|
||||
|
||||
/// Allow converting a [ValidationError] into [FenError], for use with the '?' operator.
|
||||
impl From<ValidationError> for FenError {
|
||||
fn from(err: ValidationError) -> Self {
|
||||
Self::InvalidPosition(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the castling rights segment of a FEN string to an array of [CastleRights].
|
||||
impl FromFen for [CastleRights; Color::NUM_VARIANTS] {
|
||||
type Err = FenError;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.len() > 4 {
|
||||
return Err(FenError::InvalidFen);
|
||||
}
|
||||
|
||||
let mut res = [CastleRights::NoSide; Color::NUM_VARIANTS];
|
||||
|
||||
if s == "-" {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
for b in s.chars() {
|
||||
let color = if b.is_uppercase() {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
};
|
||||
let rights = &mut res[color.index()];
|
||||
match b {
|
||||
'k' | 'K' => *rights = rights.with_king_side(),
|
||||
'q' | 'Q' => *rights = rights.with_queen_side(),
|
||||
_ => return Err(FenError::InvalidFen),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a side to move segment of a FEN string to a [Color].
|
||||
impl FromFen for Color {
|
||||
type Err = FenError;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
let res = match s {
|
||||
"w" => Color::White,
|
||||
"b" => Color::Black,
|
||||
_ => return Err(FenError::InvalidFen),
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an en-passant target square segment of a FEN string to an optional [Square].
|
||||
impl FromFen for Option<Square> {
|
||||
type Err = FenError;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
let res = match s.as_bytes() {
|
||||
[b'-'] => None,
|
||||
[file @ b'a'..=b'h', rank @ b'1'..=b'8'] => Some(Square::new(
|
||||
File::from_index((file - b'a') as usize),
|
||||
Rank::from_index((rank - b'1') as usize),
|
||||
)),
|
||||
_ => return Err(FenError::InvalidFen),
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a piece in FEN notation to a [Piece].
|
||||
impl FromFen for Piece {
|
||||
type Err = FenError;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
let res = match s {
|
||||
"p" | "P" => Self::Pawn,
|
||||
"n" | "N" => Self::Knight,
|
||||
"b" | "B" => Self::Bishop,
|
||||
"r" | "R" => Self::Rook,
|
||||
"q" | "Q" => Self::Queen,
|
||||
"k" | "K" => Self::King,
|
||||
_ => return Err(FenError::InvalidFen),
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a [ChessBoard] from the given FEN string.
|
||||
impl FromFen for ChessBoard {
|
||||
type Err = FenError;
|
||||
|
||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut split = s.split_ascii_whitespace();
|
||||
|
||||
let piece_placement = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let side_to_move = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let castling_rights = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let en_passant_square = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let half_move_clock = split.next().ok_or(FenError::InvalidFen)?;
|
||||
let full_move_counter = split.next().ok_or(FenError::InvalidFen)?;
|
||||
|
||||
let mut builder = ChessBoardBuilder::new();
|
||||
|
||||
let castle_rights = <[CastleRights; Color::NUM_VARIANTS]>::from_fen(castling_rights)?;
|
||||
for color in Color::iter() {
|
||||
builder.with_castle_rights(castle_rights[color.index()], color);
|
||||
}
|
||||
|
||||
builder.with_current_player(FromFen::from_fen(side_to_move)?);
|
||||
|
||||
if let Some(square) = FromFen::from_fen(en_passant_square)? {
|
||||
builder.with_en_passant(square);
|
||||
};
|
||||
|
||||
let half_move_clock = half_move_clock
|
||||
.parse::<_>()
|
||||
.map_err(|_| FenError::InvalidFen)?;
|
||||
builder.with_half_move_clock(half_move_clock);
|
||||
|
||||
let full_move_counter = full_move_counter
|
||||
.parse::<_>()
|
||||
.map_err(|_| FenError::InvalidFen)?;
|
||||
builder.with_turn_count(full_move_counter);
|
||||
|
||||
{
|
||||
let mut rank: usize = 8;
|
||||
for rank_str in piece_placement.split('/') {
|
||||
rank -= 1;
|
||||
let mut file: usize = 0;
|
||||
for c in rank_str.chars() {
|
||||
let color = if c.is_uppercase() {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
};
|
||||
let piece = match c {
|
||||
digit @ '1'..='8' => {
|
||||
// Unwrap is fine since this arm is only matched by digits
|
||||
file += digit.to_digit(10).unwrap() as usize;
|
||||
continue;
|
||||
}
|
||||
_ => FromFen::from_fen(&c.to_string())?,
|
||||
};
|
||||
|
||||
// Only need to worry about underflow since those are `usize` values.
|
||||
if file >= 8 || rank >= 8 {
|
||||
return Err(FenError::InvalidFen);
|
||||
};
|
||||
|
||||
let square = Square::new(File::from_index(file), Rank::from_index(rank));
|
||||
|
||||
builder[square] = Some((piece, color));
|
||||
file += 1;
|
||||
}
|
||||
// We haven't read exactly 8 files.
|
||||
if file != 8 {
|
||||
return Err(FenError::InvalidFen);
|
||||
}
|
||||
}
|
||||
// We haven't read exactly 8 ranks
|
||||
if rank != 0 {
|
||||
return Err(FenError::InvalidFen);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(builder.try_into()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::board::Move;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_position() {
|
||||
let default_position = ChessBoard::default();
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
|
||||
.unwrap(),
|
||||
default_position
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn en_passant() {
|
||||
// Start from default position
|
||||
let mut position = ChessBoard::default();
|
||||
position.play_move_inplace(Move::new(Square::E2, Square::E4, None));
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
|
||||
.unwrap(),
|
||||
position
|
||||
);
|
||||
// And now c5
|
||||
position.play_move_inplace(Move::new(Square::C7, Square::C5, None));
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2")
|
||||
.unwrap(),
|
||||
position
|
||||
);
|
||||
// Finally, Nf3
|
||||
position.play_move_inplace(Move::new(Square::G1, Square::F3, None));
|
||||
assert_eq!(
|
||||
ChessBoard::from_fen("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2 ")
|
||||
.unwrap(),
|
||||
position
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
pub mod board;
|
||||
pub mod fen;
|
||||
pub mod error;
|
||||
pub mod movegen;
|
||||
pub mod utils;
|
||||
|
|
67
src/movegen/magic/mod.rs
Normal file
67
src/movegen/magic/mod.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use crate::board::Bitboard;
|
||||
|
||||
/// A type representing the magic board indexing a given [crate::board::Square].
|
||||
pub struct Magic {
|
||||
/// Magic number.
|
||||
pub(crate) magic: u64,
|
||||
/// Base offset into the magic square table.
|
||||
pub(crate) offset: usize,
|
||||
/// Mask to apply to the blocker board before applying the magic.
|
||||
pub(crate) mask: Bitboard,
|
||||
/// Length of the resulting mask after applying the magic.
|
||||
pub(crate) shift: u8,
|
||||
}
|
||||
|
||||
impl Magic {
|
||||
pub fn get_index(&self, blockers: Bitboard) -> usize {
|
||||
let relevant_occupancy = (blockers & self.mask).0;
|
||||
let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize;
|
||||
base_index + self.offset
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(generated_boards)]
|
||||
mod moves;
|
||||
pub use moves::*;
|
||||
|
||||
#[cfg(not(generated_boards))]
|
||||
#[allow(unused_variables)]
|
||||
mod moves {
|
||||
use crate::board::{Bitboard, Color, Square};
|
||||
|
||||
pub fn quiet_pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn knight_moves(square: Square) -> Bitboard {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn king_moves(square: Square) -> Bitboard {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn king_side_castle_blockers(color: Color) -> Bitboard {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn queen_side_castle_blockers(color: Color) -> Bitboard {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
71
src/movegen/magic/moves.rs
Normal file
71
src/movegen/magic/moves.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use super::Magic;
|
||||
use crate::board::{Bitboard, Color, Square};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/magic_tables.rs"));
|
||||
|
||||
pub fn quiet_pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
|
||||
// If there is a piece in front of the pawn, it can't advance
|
||||
if !(color.backward_direction().move_board(blockers) & square).is_empty() {
|
||||
return Bitboard::EMPTY;
|
||||
}
|
||||
// SAFETY: we know the values are in-bounds
|
||||
unsafe {
|
||||
*PAWN_MOVES
|
||||
.get_unchecked(color.index())
|
||||
.get_unchecked(square.index())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
|
||||
// SAFETY: we know the values are in-bounds
|
||||
let attacks = unsafe {
|
||||
*PAWN_ATTACKS
|
||||
.get_unchecked(color.index())
|
||||
.get_unchecked(square.index())
|
||||
};
|
||||
quiet_pawn_moves(color, square, blockers) | attacks
|
||||
}
|
||||
|
||||
pub fn knight_moves(square: Square) -> Bitboard {
|
||||
// SAFETY: we know the values are in-bounds
|
||||
unsafe { *KNIGHT_MOVES.get_unchecked(square.index()) }
|
||||
}
|
||||
|
||||
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||
// SAFETY: we know the values are in-bounds
|
||||
unsafe {
|
||||
let index = BISHOP_MAGICS
|
||||
.get_unchecked(square.index())
|
||||
.get_index(blockers);
|
||||
*BISHOP_MOVES.get_unchecked(index)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||
// SAFETY: we know the values are in-bounds
|
||||
unsafe {
|
||||
let index = ROOK_MAGICS
|
||||
.get_unchecked(square.index())
|
||||
.get_index(blockers);
|
||||
*ROOK_MOVES.get_unchecked(index)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||
bishop_moves(square, blockers) | rook_moves(square, blockers)
|
||||
}
|
||||
|
||||
pub fn king_moves(square: Square) -> Bitboard {
|
||||
// SAFETY: we know the values are in-bounds
|
||||
unsafe { *KING_MOVES.get_unchecked(square.index()) }
|
||||
}
|
||||
|
||||
pub fn king_side_castle_blockers(color: Color) -> Bitboard {
|
||||
// SAFETY: we know the values are in-bounds
|
||||
unsafe { *KING_SIDE_CASTLE_BLOCKERS.get_unchecked(color.index()) }
|
||||
}
|
||||
|
||||
pub fn queen_side_castle_blockers(color: Color) -> Bitboard {
|
||||
// SAFETY: we know the values are in-bounds
|
||||
unsafe { *QUEEN_SIDE_CASTLE_BLOCKERS.get_unchecked(color.index()) }
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
// Magic bitboard
|
||||
pub mod magic;
|
||||
pub use magic::*;
|
||||
|
||||
// Naive move generation
|
||||
mod naive;
|
||||
pub mod naive;
|
||||
|
||||
// Magic bitboard generation
|
||||
mod wizardry;
|
||||
|
||||
// Magic bitboard definitions
|
||||
mod moves;
|
||||
pub use moves::*;
|
||||
pub(crate) mod wizardry;
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use crate::{
|
||||
board::{Bitboard, Color, File, Square},
|
||||
movegen::{
|
||||
naive,
|
||||
wizardry::{
|
||||
generate_bishop_magics, generate_rook_magics, MagicMoves, BISHOP_SEED, ROOK_SEED,
|
||||
},
|
||||
},
|
||||
utils::RandGen,
|
||||
};
|
||||
|
||||
// A pre-rolled RNG for magic bitboard generation, using pre-determined values.
|
||||
struct PreRolledRng {
|
||||
numbers: [u64; Square::NUM_VARIANTS],
|
||||
current_index: usize,
|
||||
}
|
||||
|
||||
impl PreRolledRng {
|
||||
pub fn new(numbers: [u64; Square::NUM_VARIANTS]) -> Self {
|
||||
Self {
|
||||
numbers,
|
||||
current_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RandGen for PreRolledRng {
|
||||
fn gen(&mut self) -> u64 {
|
||||
// We roll 3 numbers per square to bitwise-and them together.
|
||||
// Just return the same one 3 times as a work-around.
|
||||
let res = self.numbers[self.current_index / 3];
|
||||
self.current_index += 1;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the set of possible non-attack moves for a pawn on a [Square], given its [Color] and
|
||||
/// set of blockers.
|
||||
pub fn pawn_quiet_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
|
||||
static PAWN_MOVES: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> =
|
||||
OnceLock::new();
|
||||
|
||||
// If there is a piece in front of the pawn, it can't advance
|
||||
if !(color.backward_direction().move_board(blockers) & square).is_empty() {
|
||||
return Bitboard::EMPTY;
|
||||
}
|
||||
|
||||
PAWN_MOVES.get_or_init(|| {
|
||||
let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS];
|
||||
for color in Color::iter() {
|
||||
for square in Square::iter() {
|
||||
res[color.index()][square.index()] =
|
||||
naive::pawn_moves(color, square, Bitboard::EMPTY);
|
||||
}
|
||||
}
|
||||
res
|
||||
})[color.index()][square.index()]
|
||||
}
|
||||
|
||||
/// Compute the set of possible attacks for a pawn on a [Square], given its [Color].
|
||||
pub fn pawn_attacks(color: Color, square: Square) -> Bitboard {
|
||||
static PAWN_ATTACKS: OnceLock<[[Bitboard; Square::NUM_VARIANTS]; Color::NUM_VARIANTS]> =
|
||||
OnceLock::new();
|
||||
|
||||
PAWN_ATTACKS.get_or_init(|| {
|
||||
let mut res = [[Bitboard::EMPTY; Square::NUM_VARIANTS]; Color::NUM_VARIANTS];
|
||||
for color in Color::iter() {
|
||||
for square in Square::iter() {
|
||||
res[color.index()][square.index()] = naive::pawn_captures(color, square);
|
||||
}
|
||||
}
|
||||
res
|
||||
})[color.index()][square.index()]
|
||||
}
|
||||
|
||||
/// Compute the set of possible moves for a pawn on a [Square], given its [Color] and set of
|
||||
/// blockers.
|
||||
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
|
||||
pawn_quiet_moves(color, square, blockers) | pawn_attacks(color, square)
|
||||
}
|
||||
|
||||
/// Compute the set of possible moves for a knight on a [Square].
|
||||
pub fn knight_moves(square: Square) -> Bitboard {
|
||||
static KNIGHT_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new();
|
||||
KNIGHT_MOVES.get_or_init(|| {
|
||||
let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS];
|
||||
for square in Square::iter() {
|
||||
res[square.index()] = naive::knight_moves(square)
|
||||
}
|
||||
res
|
||||
})[square.index()]
|
||||
}
|
||||
|
||||
/// Compute the set of possible moves for a bishop on a [Square], given its set of blockers.
|
||||
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||
static BISHOP_MAGICS: OnceLock<MagicMoves> = OnceLock::new();
|
||||
BISHOP_MAGICS
|
||||
.get_or_init(|| {
|
||||
let (magics, moves) = generate_bishop_magics(&mut PreRolledRng::new(BISHOP_SEED));
|
||||
// SAFETY: we used the generator function to compute these values
|
||||
unsafe { MagicMoves::new(magics, moves) }
|
||||
})
|
||||
.query(square, blockers)
|
||||
}
|
||||
|
||||
/// Compute the set of possible moves for a rook on a [Square], given its set of blockers.
|
||||
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||
static ROOK_MAGICS: OnceLock<MagicMoves> = OnceLock::new();
|
||||
ROOK_MAGICS
|
||||
.get_or_init(|| {
|
||||
let (magics, moves) = generate_rook_magics(&mut PreRolledRng::new(ROOK_SEED));
|
||||
// SAFETY: we used the generator function to compute these values
|
||||
unsafe { MagicMoves::new(magics, moves) }
|
||||
})
|
||||
.query(square, blockers)
|
||||
}
|
||||
|
||||
/// Compute the set of possible moves for a queen on a [Square], given its set of blockers.
|
||||
pub fn queen_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||
bishop_moves(square, blockers) | rook_moves(square, blockers)
|
||||
}
|
||||
|
||||
/// Compute the set of possible moves for a king on a [Square].
|
||||
pub fn king_moves(square: Square) -> Bitboard {
|
||||
static KING_MOVES: OnceLock<[Bitboard; Square::NUM_VARIANTS]> = OnceLock::new();
|
||||
KING_MOVES.get_or_init(|| {
|
||||
let mut res = [Bitboard::EMPTY; Square::NUM_VARIANTS];
|
||||
for square in Square::iter() {
|
||||
res[square.index()] = naive::king_moves(square)
|
||||
}
|
||||
res
|
||||
})[square.index()]
|
||||
}
|
||||
|
||||
/// Compute the squares which should be empty for a king-side castle of the given [Color].
|
||||
pub fn kind_side_castle_blockers(color: Color) -> Bitboard {
|
||||
let rank = color.first_rank();
|
||||
Square::new(File::F, rank) | Square::new(File::G, rank)
|
||||
}
|
||||
|
||||
/// Compute the squares which should be empty for a queen-side castle of the given [Color].
|
||||
pub fn queen_side_castle_blockers(color: Color) -> Bitboard {
|
||||
let rank = color.first_rank();
|
||||
Square::new(File::B, rank) | Square::new(File::C, rank) | Square::new(File::D, rank)
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
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))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::board::{Bitboard, Direction, Square};
|
||||
use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square};
|
||||
|
||||
/// Compute a king's movement. No castling moves included
|
||||
// No castling moves included
|
||||
pub fn king_moves(square: Square) -> Bitboard {
|
||||
let board = square.into_bitboard();
|
||||
|
||||
|
@ -9,6 +9,20 @@ pub fn king_moves(square: Square) -> Bitboard {
|
|||
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs)
|
||||
}
|
||||
|
||||
pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard {
|
||||
let rank = color.first_rank();
|
||||
|
||||
let king_side_square = Square::new(File::G, rank);
|
||||
let queen_side_square = Square::new(File::C, rank);
|
||||
|
||||
match castle_rights {
|
||||
CastleRights::NoSide => Bitboard::EMPTY,
|
||||
CastleRights::KingSide => king_side_square.into_bitboard(),
|
||||
CastleRights::QueenSide => queen_side_square.into_bitboard(),
|
||||
CastleRights::BothSides => king_side_square | queen_side_square,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -169,4 +183,40 @@ mod test {
|
|||
| Square::F6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn castling_moves() {
|
||||
assert_eq!(
|
||||
king_castling_moves(Color::White, CastleRights::NoSide),
|
||||
Bitboard::EMPTY
|
||||
);
|
||||
assert_eq!(
|
||||
king_castling_moves(Color::Black, CastleRights::NoSide),
|
||||
Bitboard::EMPTY
|
||||
);
|
||||
assert_eq!(
|
||||
king_castling_moves(Color::White, CastleRights::KingSide),
|
||||
Square::G1.into_bitboard()
|
||||
);
|
||||
assert_eq!(
|
||||
king_castling_moves(Color::Black, CastleRights::KingSide),
|
||||
Square::G8.into_bitboard()
|
||||
);
|
||||
assert_eq!(
|
||||
king_castling_moves(Color::White, CastleRights::QueenSide),
|
||||
Square::C1.into_bitboard()
|
||||
);
|
||||
assert_eq!(
|
||||
king_castling_moves(Color::Black, CastleRights::QueenSide),
|
||||
Square::C8.into_bitboard()
|
||||
);
|
||||
assert_eq!(
|
||||
king_castling_moves(Color::White, CastleRights::BothSides),
|
||||
Square::C1 | Square::G1
|
||||
);
|
||||
assert_eq!(
|
||||
king_castling_moves(Color::Black, CastleRights::BothSides),
|
||||
Square::C8 | Square::G8
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use crate::board::{Bitboard, Direction, Square};
|
||||
|
||||
/// Compute a knight's movement.
|
||||
pub fn knight_moves(square: Square) -> Bitboard {
|
||||
let board = square.into_bitboard();
|
||||
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
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::*;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
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;
|
||||
|
@ -22,7 +21,6 @@ pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard
|
|||
}
|
||||
}
|
||||
|
||||
/// Computes the set of squares a pawn can capture, given its color.
|
||||
pub fn pawn_captures(color: Color, square: Square) -> Bitboard {
|
||||
if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) {
|
||||
return Bitboard::EMPTY;
|
||||
|
@ -38,6 +36,15 @@ pub fn pawn_captures(color: Color, square: Square) -> Bitboard {
|
|||
attack_west | attack_east
|
||||
}
|
||||
|
||||
pub fn en_passant_origins(square: Square) -> Bitboard {
|
||||
let board = square.into_bitboard();
|
||||
|
||||
let origin_west = Direction::West.move_board(board);
|
||||
let origin_east = Direction::East.move_board(board);
|
||||
|
||||
origin_west | origin_east
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -113,4 +120,14 @@ mod test {
|
|||
Square::G6.into_bitboard()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn en_passant() {
|
||||
assert_eq!(en_passant_origins(Square::A4), Square::B4.into_bitboard());
|
||||
assert_eq!(en_passant_origins(Square::A5), Square::B5.into_bitboard());
|
||||
assert_eq!(en_passant_origins(Square::B4), Square::A4 | Square::C4);
|
||||
assert_eq!(en_passant_origins(Square::B5), Square::A5 | Square::C5);
|
||||
assert_eq!(en_passant_origins(Square::H4), Square::G4.into_bitboard());
|
||||
assert_eq!(en_passant_origins(Square::H5), Square::G5.into_bitboard());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
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))
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
use crate::board::{Bitboard, Square};
|
||||
use crate::movegen::naive::{bishop_moves, rook_moves};
|
||||
use crate::utils::RandGen;
|
||||
use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves};
|
||||
use crate::movegen::Magic;
|
||||
|
||||
use super::mask::{generate_bishop_mask, generate_rook_mask};
|
||||
use super::Magic;
|
||||
|
||||
type MagicGenerationType = (Vec<Magic>, Vec<Bitboard>);
|
||||
|
||||
pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType {
|
||||
#[allow(unused)] // FIXME: remove when used
|
||||
pub fn generate_bishop_magics(rng: &mut dyn random::Source) -> MagicGenerationType {
|
||||
generate_magics(rng, generate_bishop_mask, bishop_moves)
|
||||
}
|
||||
|
||||
pub fn generate_rook_magics(rng: &mut dyn RandGen) -> MagicGenerationType {
|
||||
#[allow(unused)] // FIXME: remove when used
|
||||
pub fn generate_rook_magics(rng: &mut dyn random::Source) -> MagicGenerationType {
|
||||
generate_magics(rng, generate_rook_mask, rook_moves)
|
||||
}
|
||||
|
||||
fn generate_magics(
|
||||
rng: &mut dyn RandGen,
|
||||
rng: &mut dyn random::Source,
|
||||
mask_fn: impl Fn(Square) -> Bitboard,
|
||||
moves_fn: impl Fn(Square, Bitboard) -> Bitboard,
|
||||
) -> MagicGenerationType {
|
||||
|
@ -25,6 +26,7 @@ fn generate_magics(
|
|||
|
||||
for square in Square::iter() {
|
||||
let mask = mask_fn(square);
|
||||
let mut candidate: Magic;
|
||||
|
||||
let occupancy_to_moves: Vec<_> = mask
|
||||
.iter_power_set()
|
||||
|
@ -32,7 +34,7 @@ fn generate_magics(
|
|||
.collect();
|
||||
|
||||
'candidate_search: loop {
|
||||
let mut candidate = Magic {
|
||||
candidate = Magic {
|
||||
magic: magic_candidate(rng),
|
||||
offset: 0,
|
||||
mask,
|
||||
|
@ -43,7 +45,7 @@ fn generate_magics(
|
|||
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 {
|
||||
if candidate_moves[index] != Bitboard::EMPTY && candidate_moves[index] != moves {
|
||||
continue 'candidate_search;
|
||||
}
|
||||
candidate_moves[index] = moves;
|
||||
|
@ -51,8 +53,8 @@ fn generate_magics(
|
|||
|
||||
// We have filled all candidate boards, record the correct offset and add the moves
|
||||
candidate.offset = boards.len();
|
||||
magics.push(candidate);
|
||||
boards.append(&mut candidate_moves);
|
||||
magics.push(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +62,6 @@ fn generate_magics(
|
|||
(magics, boards)
|
||||
}
|
||||
|
||||
fn magic_candidate(rng: &mut dyn RandGen) -> u64 {
|
||||
// Few bits makes for better candidates
|
||||
rng.gen() & rng.gen() & rng.gen()
|
||||
fn magic_candidate(rng: &mut dyn random::Source) -> u64 {
|
||||
rng.read_u64() & rng.read_u64() & rng.read_u64()
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
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);
|
||||
|
||||
|
@ -13,7 +12,6 @@ pub fn generate_bishop_mask(square: Square) -> 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);
|
||||
|
||||
|
|
|
@ -1,266 +1,2 @@
|
|||
mod generation;
|
||||
pub(super) use generation::*;
|
||||
pub(crate) mod 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; Square::NUM_VARIANTS] = [
|
||||
4634226011293351952,
|
||||
6918109887683821586,
|
||||
76562328660738184,
|
||||
7242919606867744800,
|
||||
13871652069997347969,
|
||||
1171657252671901696,
|
||||
147001475087730752,
|
||||
1752045392763101248,
|
||||
288406435526639744,
|
||||
4612213818402029888,
|
||||
9808848818951710728,
|
||||
9223394181731320840,
|
||||
54047645651435648,
|
||||
9224780030482579712,
|
||||
9049059098626048,
|
||||
1442330840700035221,
|
||||
1126037887157508,
|
||||
1153488887004529665,
|
||||
290485130928332936,
|
||||
9226749771011592258,
|
||||
148636405693678112,
|
||||
2260596997758984,
|
||||
73470481646424336,
|
||||
2341907012146823680,
|
||||
2314955761652335121,
|
||||
2265544246165632,
|
||||
13598764778463296,
|
||||
563087425962496,
|
||||
563087425962048,
|
||||
2163991853573081088,
|
||||
567353402270020,
|
||||
6488844433713538048,
|
||||
288810987011448834,
|
||||
11830884701569344,
|
||||
2747549955031826688,
|
||||
35734665298432,
|
||||
18025943920672800,
|
||||
292892945404789012,
|
||||
1153520472160470528,
|
||||
2260949167801860,
|
||||
155446765112299521,
|
||||
379008324189818944,
|
||||
4616480181217005576,
|
||||
576461027453960704,
|
||||
2450556349601564416,
|
||||
1160556519943569536,
|
||||
4612900059821375552,
|
||||
5477089643453251617,
|
||||
9223532084785594632,
|
||||
2810391870219355200,
|
||||
36594222015453185,
|
||||
4612011546951352320,
|
||||
2392883590201344,
|
||||
1152956706186200064,
|
||||
9009415592510464,
|
||||
81077999302148128,
|
||||
576746627483043968,
|
||||
301267327789056,
|
||||
39586720976896,
|
||||
720878306081243648,
|
||||
9223512777841312257,
|
||||
5764609859566698625,
|
||||
8088544233436348496,
|
||||
4612856276794474560,
|
||||
];
|
||||
|
||||
/// A set of magic numbers for rook move generation.
|
||||
pub(crate) const ROOK_SEED: [u64; Square::NUM_VARIANTS] = [
|
||||
180144122814791812,
|
||||
10448386594766422036,
|
||||
9403533616331358856,
|
||||
108095189301858304,
|
||||
72076290316044288,
|
||||
36066182562054145,
|
||||
4647717564258980096,
|
||||
13979173385364603396,
|
||||
4620833992751489152,
|
||||
297800804633419904,
|
||||
578009002156298240,
|
||||
2450099003505838082,
|
||||
1175721046778052864,
|
||||
20406952999780864,
|
||||
1175861788231598592,
|
||||
36169538802827392,
|
||||
288371663414771712,
|
||||
423313050501155,
|
||||
604731668136450,
|
||||
580261214513399808,
|
||||
297661437206136832,
|
||||
1750211954976489600,
|
||||
9020393411186696,
|
||||
9259543770406356001,
|
||||
44532368556032,
|
||||
10376381507760693256,
|
||||
52778707714176,
|
||||
4612829512676149248,
|
||||
1882513444629184528,
|
||||
2369460754144428160,
|
||||
9223380850137104901,
|
||||
2666413562481640036,
|
||||
141012643087392,
|
||||
16735517094631719424,
|
||||
17594358702087,
|
||||
2344264412262574084,
|
||||
422813768878080,
|
||||
1126450811896320,
|
||||
54466576291772936,
|
||||
42784758060548372,
|
||||
292874851780165648,
|
||||
18015364885839937,
|
||||
282644818493504,
|
||||
1184447393488764944,
|
||||
4649966632473477184,
|
||||
563499910594566,
|
||||
17632049496086,
|
||||
18502729728001,
|
||||
140742121013504,
|
||||
9711024139665536,
|
||||
246293205270784,
|
||||
290772515771392256,
|
||||
9230131836490350720,
|
||||
73326432604127360,
|
||||
453174886517643776,
|
||||
2396271245728563712,
|
||||
324259242966026501,
|
||||
288953994406543363,
|
||||
1153557061259362338,
|
||||
40533496293515441,
|
||||
1407392197644307,
|
||||
1729945211427624002,
|
||||
587808330812164100,
|
||||
9511606812128903298,
|
||||
];
|
||||
// endregion:sourcegen
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use super::*;
|
||||
use crate::utils::SimpleRng;
|
||||
|
||||
fn split_twice<'a>(
|
||||
text: &'a str,
|
||||
start_marker: &str,
|
||||
end_marker: &str,
|
||||
) -> Option<(&'a str, &'a str, &'a str)> {
|
||||
let (prefix, rest) = text.split_once(start_marker)?;
|
||||
let (mid, suffix) = rest.split_once(end_marker)?;
|
||||
Some((prefix, mid, suffix))
|
||||
}
|
||||
|
||||
fn array_string(piece_type: &str, values: &[Magic]) -> String {
|
||||
let inner = || -> Result<String, std::fmt::Error> {
|
||||
let mut res = String::new();
|
||||
|
||||
writeln!(
|
||||
&mut res,
|
||||
"/// A set of magic numbers for {} move generation.",
|
||||
piece_type
|
||||
)?;
|
||||
writeln!(
|
||||
&mut res,
|
||||
"pub(crate) const {}_SEED: [u64; Square::NUM_VARIANTS] = [",
|
||||
piece_type.to_uppercase()
|
||||
)?;
|
||||
for magic in values {
|
||||
writeln!(&mut res, " {},", magic.magic)?;
|
||||
}
|
||||
writeln!(&mut res, "];")?;
|
||||
|
||||
Ok(res)
|
||||
};
|
||||
|
||||
inner().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "slow"]
|
||||
// Regenerates the magic bitboard numbers.
|
||||
fn regen_magic_seeds() {
|
||||
// We only care about the magics, the moves can be recomputed at runtime ~cheaply.
|
||||
let (bishop_magics, _) = generate_bishop_magics(&mut SimpleRng::new());
|
||||
let (rook_magics, _) = generate_rook_magics(&mut SimpleRng::new());
|
||||
|
||||
let original_text = std::fs::read_to_string(file!()).unwrap();
|
||||
|
||||
let bishop_array = array_string("bishop", &bishop_magics[..]);
|
||||
let rook_array = array_string("rook", &rook_magics[..]);
|
||||
|
||||
let new_text = {
|
||||
let start_marker = "// region:sourcegen\n";
|
||||
let end_marker = "// endregion:sourcegen\n";
|
||||
let (prefix, _, suffix) =
|
||||
split_twice(&original_text, start_marker, end_marker).unwrap();
|
||||
format!("{prefix}{start_marker}{bishop_array}\n{rook_array}{end_marker}{suffix}")
|
||||
};
|
||||
|
||||
if new_text != original_text {
|
||||
std::fs::write(file!(), new_text).unwrap();
|
||||
panic!("source was not up-to-date")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,2 @@
|
|||
pub(crate) mod rand;
|
||||
pub(crate) use rand::*;
|
||||
|
||||
pub mod static_assert;
|
||||
pub use static_assert::*;
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
/// A trait to represent RNG for u64 values.
|
||||
pub trait RandGen {
|
||||
fn gen(&mut self) -> u64;
|
||||
}
|
||||
|
||||
// A simple pcg64_fast RNG implementation, for code-generation.
|
||||
#[cfg(test)]
|
||||
pub struct SimpleRng(u128);
|
||||
|
||||
#[cfg(test)]
|
||||
impl SimpleRng {
|
||||
pub fn new() -> Self {
|
||||
Self(0xcafef00dd15ea5e5 | 1) // https://xkcd.com/221/
|
||||
}
|
||||
|
||||
pub fn gen(&mut self) -> u64 {
|
||||
const MULTIPLIER: u128 = 0x2360_ED05_1FC6_5DA4_4385_DF64_9FCC_F645;
|
||||
const XSHIFT: u32 = 64; // (128 - 64 + 64) / 2
|
||||
const ROTATE: u32 = 122; // 128 - 6
|
||||
|
||||
self.0 = self.0.wrapping_mul(MULTIPLIER);
|
||||
let rot = (self.0 >> ROTATE) as u32;
|
||||
let xsl = (self.0 >> XSHIFT) as u64 ^ (self.0 as u64);
|
||||
xsl.rotate_right(rot)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl RandGen for SimpleRng {
|
||||
fn gen(&mut self) -> u64 {
|
||||
self.gen()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rng() {
|
||||
let mut rng = SimpleRng::new();
|
||||
|
||||
assert_eq!(rng.gen(), 64934999470316615);
|
||||
assert_eq!(rng.gen(), 15459456780870779090);
|
||||
assert_eq!(rng.gen(), 13715484424881807779);
|
||||
assert_eq!(rng.gen(), 17718572936700675021);
|
||||
assert_eq!(rng.gen(), 14587996314750246637);
|
||||
}
|
||||
}
|
|
@ -15,9 +15,12 @@
|
|||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! static_assert {
|
||||
($($tt:tt)*) => {
|
||||
($condition:expr) => {
|
||||
// Based on the latest one in `rustc`'s one before it was [removed].
|
||||
//
|
||||
// [removed]: https://github.com/rust-lang/rust/commit/c2dad1c6b9f9636198d7c561b47a2974f5103f6d
|
||||
#[allow(dead_code)]
|
||||
const _: () = assert!($($tt)*);
|
||||
const _: () = [()][!($condition) as usize];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +1,17 @@
|
|||
import enum
|
||||
|
||||
import gdb
|
||||
import gdb.printing
|
||||
|
||||
|
||||
def optional(constructor, val):
|
||||
try:
|
||||
return constructor(val["Some"]["__0"])
|
||||
except gdb.error:
|
||||
return None
|
||||
|
||||
|
||||
def print_opt(val):
|
||||
return "(None)" if val is None else str(val)
|
||||
|
||||
|
||||
class Square(object):
|
||||
"""
|
||||
Python representation of a 'seer::board::square::Square' raw value.
|
||||
Wrapper around GDB's representation of a 'seer::board::square::Square' in
|
||||
memory.
|
||||
"""
|
||||
|
||||
FILES = list(map(lambda n: chr(ord("A") + n), range(8)))
|
||||
FILES = list(map(lambda n: chr(ord('A') + n), range(8)))
|
||||
RANKS = list(map(lambda n: str(n + 1), range(8)))
|
||||
|
||||
def __init__(self, val):
|
||||
if isinstance(val, Square):
|
||||
val = val._val
|
||||
self._val = val
|
||||
|
||||
@classmethod
|
||||
def from_gdb(cls, val):
|
||||
return cls(int(val))
|
||||
|
||||
@classmethod
|
||||
def from_file_rank(cls, file, rank):
|
||||
return cls(file * 8 + rank)
|
||||
|
||||
def __str__(self):
|
||||
return self.FILES[self.file] + self.RANKS[self.rank]
|
||||
|
||||
|
@ -47,373 +23,56 @@ class Square(object):
|
|||
def file(self):
|
||||
return int(self._val) // 8
|
||||
|
||||
|
||||
class Bitboard(object):
|
||||
"""
|
||||
Python representation of a 'seer::board::bitboard::Bitboard' raw value.
|
||||
Wrapper around GDB's representation of a 'seer::board::bitboard::Bitboard'
|
||||
in memory.
|
||||
"""
|
||||
|
||||
def __init__(self, val):
|
||||
if isinstance(val, Bitboard):
|
||||
val = val._val
|
||||
self._val = val
|
||||
|
||||
@classmethod
|
||||
def from_gdb(cls, val):
|
||||
return cls(int(val["__0"]))
|
||||
|
||||
def __str__(self):
|
||||
return "[" + ", ".join(map(str, self.squares)) + "]"
|
||||
|
||||
def at(self, square):
|
||||
return bool(self._val & (1 << square._val))
|
||||
|
||||
@property
|
||||
def squares(self):
|
||||
n = self._val
|
||||
n = int(self._val["__0"])
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def from_gdb(cls, val):
|
||||
return cls(int(val))
|
||||
|
||||
def __str__(self):
|
||||
return self.name.title().replace("_", "")
|
||||
|
||||
|
||||
class Color(enum.IntEnum):
|
||||
"""
|
||||
Python representation of a 'seer::board::color::Color' raw value.
|
||||
"""
|
||||
|
||||
# Should be kept in sync with the enum in `color.rs`
|
||||
WHITE = 0
|
||||
BLACK = 1
|
||||
|
||||
@classmethod
|
||||
def from_gdb(cls, val):
|
||||
return cls(int(val))
|
||||
|
||||
def __str__(self):
|
||||
return self.name.title()
|
||||
|
||||
|
||||
class File(enum.IntEnum):
|
||||
"""
|
||||
Python representation of a 'seer::board::file::File' raw value.
|
||||
"""
|
||||
|
||||
# Should be kept in sync with the enum in `file.rs`
|
||||
A = 0
|
||||
B = 1
|
||||
C = 2
|
||||
D = 3
|
||||
E = 4
|
||||
F = 5
|
||||
G = 6
|
||||
H = 7
|
||||
|
||||
@classmethod
|
||||
def from_gdb(cls, val):
|
||||
return cls(int(val))
|
||||
|
||||
def __str__(self):
|
||||
return self.name.title()
|
||||
|
||||
|
||||
class Rank(enum.IntEnum):
|
||||
"""
|
||||
Python representation of a 'seer::board::rank::Rank' raw value.
|
||||
"""
|
||||
|
||||
# Should be kept in sync with the enum in `rank.rs`
|
||||
First = 0
|
||||
Second = 1
|
||||
Third = 2
|
||||
Fourth = 3
|
||||
Fifth = 4
|
||||
Sixth = 5
|
||||
Seventh = 6
|
||||
Eighth = 7
|
||||
|
||||
@classmethod
|
||||
def from_gdb(cls, val):
|
||||
return cls(int(val))
|
||||
|
||||
def __str__(self):
|
||||
return self.name.title()
|
||||
|
||||
|
||||
class Piece(enum.IntEnum):
|
||||
"""
|
||||
Python representation of a 'seer::board::piece::Piece' raw value.
|
||||
"""
|
||||
|
||||
# Should be kept in sync with the enum in `piece.rs`
|
||||
KING = 0
|
||||
QUEEN = 1
|
||||
ROOK = 2
|
||||
BISHOP = 3
|
||||
KNIGHT = 4
|
||||
PAWN = 5
|
||||
|
||||
@classmethod
|
||||
def from_gdb(cls, val):
|
||||
return cls(int(val))
|
||||
|
||||
def __str__(self):
|
||||
return self.name.title()
|
||||
|
||||
|
||||
class Move(object):
|
||||
"""
|
||||
Wrapper around GDB's representation of a 'seer::board::move::Move'
|
||||
in memory.
|
||||
"""
|
||||
|
||||
def __init__(self, start, destination, promotion):
|
||||
self._start = Square(start)
|
||||
self._destination = Square(destination)
|
||||
self._promotion = Piece(promotion)
|
||||
|
||||
@classmethod
|
||||
def from_gdb(cls, val):
|
||||
start = Square(int(val["start"]))
|
||||
destination = Square(int(val["destination"]))
|
||||
promotion = optional(Piece.from_gdb, val["promotion"])
|
||||
cls(start, destination, promotion)
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
return self._start
|
||||
|
||||
@property
|
||||
def destination(self):
|
||||
return self._destination
|
||||
|
||||
@property
|
||||
def promotion(self):
|
||||
return self._promotion
|
||||
|
||||
def __str__(self):
|
||||
KEYS = [
|
||||
"start",
|
||||
"destination",
|
||||
"promotion",
|
||||
]
|
||||
indent = lambda s: " " + s
|
||||
|
||||
values = [key + ": " + print_opt(getattr(self, key)) + ",\n" for key in KEYS]
|
||||
return "Move{\n" + "".join(map(indent, values)) + "}"
|
||||
|
||||
|
||||
class ChessBoard(object):
|
||||
"""
|
||||
Wrapper around GDB's representation of a 'seer::board::chess_board::ChessBoard'
|
||||
in memory.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
piece_occupancy,
|
||||
color_occupancy,
|
||||
castle_rights,
|
||||
half_move_clock,
|
||||
total_plies,
|
||||
side,
|
||||
en_passant,
|
||||
):
|
||||
self._piece_occupancy = list(map(Bitboard, piece_occupancy))
|
||||
self._color_occupancy = list(map(Bitboard, color_occupancy))
|
||||
self._castle_rights = list(map(CastleRights, castle_rights))
|
||||
self._half_move_clock = int(half_move_clock)
|
||||
self._total_plies = int(total_plies)
|
||||
self._side = Color(side)
|
||||
self._en_passant = None if en_passant is None else Square(en_passant)
|
||||
|
||||
@classmethod
|
||||
def from_gdb(cls, val):
|
||||
return cls(
|
||||
[Bitboard.from_gdb(val["piece_occupancy"][p]) for p in Piece],
|
||||
[Bitboard.from_gdb(val["color_occupancy"][c]) for c in Color],
|
||||
[CastleRights.from_gdb(val["castle_rights"][c]) for c in Color],
|
||||
int(val["half_move_clock"]),
|
||||
int(val["total_plies"]),
|
||||
Color.from_gdb(val["side"]),
|
||||
optional(Square.from_gdb, val["en_passant"]),
|
||||
)
|
||||
|
||||
def at(self, square):
|
||||
for piece in Piece:
|
||||
if not self._piece_occupancy[piece].at(square):
|
||||
continue
|
||||
for color in Color:
|
||||
if not self._color_occupancy[color].at(square):
|
||||
continue
|
||||
return (piece, color)
|
||||
return None
|
||||
|
||||
def pretty_str(self):
|
||||
def pretty_piece(piece, color):
|
||||
return [
|
||||
("♚", "♔"),
|
||||
("♛", "♕"),
|
||||
("♜", "♖"),
|
||||
("♝", "♗"),
|
||||
("♞", "♘"),
|
||||
("♟", "♙"),
|
||||
][piece][color]
|
||||
|
||||
board = [
|
||||
[self.at(Square.from_file_rank(file, rank)) for file in File]
|
||||
for rank in Rank
|
||||
]
|
||||
|
||||
res = []
|
||||
res.append(" A B C D E F G H ")
|
||||
for n, line in reversed(list(enumerate(board, start=1))):
|
||||
strings = [str(n) + " "]
|
||||
strings.extend(" " if p is None else pretty_piece(*p) for p in line)
|
||||
strings.append(" " + str(n))
|
||||
res.append("|".join(strings))
|
||||
res.append(" A B C D E F G H ")
|
||||
res += [
|
||||
"Half-move clock: " + str(self._half_move_clock),
|
||||
"Total plies: " + str(self._total_plies),
|
||||
"Side to play: " + str(self._side),
|
||||
"En passant: " + print_opt(self._en_passant),
|
||||
]
|
||||
return "\n".join(res)
|
||||
|
||||
|
||||
class SquarePrinter(object):
|
||||
"Print a seer::board::square::Square"
|
||||
|
||||
def __init__(self, val):
|
||||
self._val = Square.from_gdb(val)
|
||||
self._val = Square(val)
|
||||
|
||||
def to_string(self):
|
||||
return str(self._val)
|
||||
|
||||
def display_hint(self):
|
||||
return 'string'
|
||||
|
||||
class BitboardPrinter(object):
|
||||
"Print a seer::board::bitboard::Bitboard"
|
||||
|
||||
def __init__(self, val):
|
||||
self._val = Bitboard.from_gdb(val)
|
||||
self._val = Bitboard(val)
|
||||
|
||||
def to_string(self):
|
||||
return "Bitboard{" + str(self._val)[1:-1] + "}"
|
||||
|
||||
|
||||
class CastleRightsPrinter(object):
|
||||
"Print a seer::board::castle_rights::CastleRights"
|
||||
|
||||
def __init__(self, val):
|
||||
self._val = CastleRights.from_gdb(val)
|
||||
|
||||
def to_string(self):
|
||||
return str(self._val)
|
||||
|
||||
|
||||
class ColorPrinter(object):
|
||||
"Print a seer::board::color::Color"
|
||||
|
||||
def __init__(self, val):
|
||||
self._val = Color.from_gdb(val)
|
||||
|
||||
def to_string(self):
|
||||
return str(self._val)
|
||||
|
||||
|
||||
class FilePrinter(object):
|
||||
"Print a seer::board::file::File"
|
||||
|
||||
def __init__(self, val):
|
||||
self._val = File.from_gdb(val)
|
||||
|
||||
def to_string(self):
|
||||
return str(self._val)
|
||||
|
||||
|
||||
class RankPrinter(object):
|
||||
"Print a seer::board::rank::Rank"
|
||||
|
||||
def __init__(self, val):
|
||||
self._val = Rank.from_gdb(val)
|
||||
|
||||
def to_string(self):
|
||||
return str(self._val)
|
||||
|
||||
|
||||
class PiecePrinter(object):
|
||||
"Print a seer::board::piece::Piece"
|
||||
|
||||
def __init__(self, val):
|
||||
self._val = Piece.from_gdb(val)
|
||||
|
||||
def to_string(self):
|
||||
return str(self._val)
|
||||
|
||||
|
||||
class MovePrinter(object):
|
||||
"Print a seer::board::move::Move"
|
||||
|
||||
def __init__(self, val):
|
||||
self._val = Move.from_gdb(val)
|
||||
|
||||
def to_string(self):
|
||||
return str(self._val)
|
||||
|
||||
|
||||
class PrintBoard(gdb.Command):
|
||||
"""
|
||||
Pretty-print a 'seer::board::chess_board::ChessBoard' as a 2D textual chess board.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(PrintBoard, self).__init__(
|
||||
"print-board", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
|
||||
)
|
||||
|
||||
def invoke(self, arg, from_tty):
|
||||
board = ChessBoard.from_gdb(gdb.parse_and_eval(arg))
|
||||
print(board.pretty_str())
|
||||
|
||||
def display_hint(self):
|
||||
return 'string'
|
||||
|
||||
def build_pretty_printer():
|
||||
pp = gdb.printing.RegexpCollectionPrettyPrinter('seer')
|
||||
|
||||
pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter)
|
||||
pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter)
|
||||
pp.add_printer('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)
|
||||
pp.add_printer('BigNum', '^seer::board::square::Square$', SquarePrinter)
|
||||
pp.add_printer('BigNum', '^seer::board::bitboard::Bitboard$', BitboardPrinter)
|
||||
|
||||
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