Compare commits
No commits in common. "add-movegen" and "main" have entirely different histories.
add-movege
...
main
5
.envrc
Normal file
5
.envrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
if ! has nix_direnv_version || ! nix_direnv_version 3.0.0; then
|
||||||
|
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.0/direnvrc" "sha256-21TMnI2xWX7HkSTjFFri2UaohXVj854mgvWapWrxRXg="
|
||||||
|
fi
|
||||||
|
|
||||||
|
use flake
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1,6 +1,6 @@
|
||||||
# Nix files
|
# Rust build directory
|
||||||
/result
|
|
||||||
/.pre-commit-config.yaml
|
|
||||||
|
|
||||||
# Rust files
|
|
||||||
/target
|
/target
|
||||||
|
|
||||||
|
# Nix generated files
|
||||||
|
/.pre-commit-config.yaml
|
||||||
|
/result
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
---
|
labels:
|
||||||
kind: pipeline
|
backend: local
|
||||||
type: exec
|
|
||||||
name: abacus checks
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: pre commit check
|
- name: pre-commit check
|
||||||
|
image: bash
|
||||||
commands:
|
commands:
|
||||||
- nix develop . --command pre-commit run --all
|
- nix develop --command pre-commit run --all
|
||||||
|
|
||||||
- name: flake check
|
- name: nix flake check
|
||||||
|
image: bash
|
||||||
commands:
|
commands:
|
||||||
- nix flake check
|
- nix flake check
|
||||||
|
|
||||||
- name: package check
|
|
||||||
commands:
|
|
||||||
- nix build
|
|
||||||
|
|
||||||
- name: notifiy
|
- name: notifiy
|
||||||
commands:
|
image: bash
|
||||||
- nix run github:ambroisie/matrix-notifier
|
|
||||||
environment:
|
environment:
|
||||||
ADDRESS:
|
ADDRESS:
|
||||||
from_secret: matrix_homeserver
|
from_secret: matrix_homeserver
|
||||||
|
@ -28,8 +23,9 @@ steps:
|
||||||
from_secret: matrix_username
|
from_secret: matrix_username
|
||||||
PASS:
|
PASS:
|
||||||
from_secret: matrix_password
|
from_secret: matrix_password
|
||||||
|
commands:
|
||||||
|
- nix run github:ambroisie/matrix-notifier
|
||||||
when:
|
when:
|
||||||
status:
|
status:
|
||||||
- failure
|
- failure
|
||||||
- success
|
- success
|
||||||
...
|
|
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -2,15 +2,6 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "random"
|
|
||||||
version = "0.12.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97d13a3485349981c90c79112a11222c3e6e75de1d52b87a7525b3bf5361420f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "seer"
|
name = "seer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
|
||||||
"random",
|
|
||||||
]
|
|
||||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -2,19 +2,7 @@
|
||||||
name = "seer"
|
name = "seer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "src/build.rs"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
random = "0.12.2"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
random = "0.12.2"
|
|
||||||
|
|
||||||
# Optimize build scripts to shorten compile times.
|
|
||||||
[profile.dev.build-override]
|
|
||||||
opt-level = 3
|
|
||||||
|
|
||||||
[profile.release.build-override]
|
|
||||||
opt-level = 3
|
|
||||||
|
|
115
flake.lock
115
flake.lock
|
@ -1,73 +1,111 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1656928814,
|
"lastModified": 1696426674,
|
||||||
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
|
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"futils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"ref": "master",
|
"ref": "main",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"naersk": {
|
"gitignore": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
|
"pre-commit-hooks",
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1655042882,
|
"lastModified": 1709087332,
|
||||||
"narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=",
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
"owner": "nix-community",
|
"owner": "hercules-ci",
|
||||||
"repo": "naersk",
|
"repo": "gitignore.nix",
|
||||||
"rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f",
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"owner": "hercules-ci",
|
||||||
"ref": "master",
|
"repo": "gitignore.nix",
|
||||||
"repo": "naersk",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1657888067,
|
"lastModified": 1711523803,
|
||||||
"narHash": "sha256-GnwJoFBTPfW3+mz7QEeJEEQ9OMHZOiIJ/qDhZxrlKh8=",
|
"narHash": "sha256-UKcYiHWHQynzj6CN/vTcix4yd1eCu1uFdsuarupdCQQ=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "65fae659e31098ca4ac825a6fef26d890aaf3f4e",
|
"rev": "2726f127c15a4cc9810843b96cad73c7eb39e443",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixpkgs-unstable",
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-stable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710695816,
|
||||||
|
"narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "614b4613980a522ba49f0d194531beddbb7220d3",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-23.11",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pre-commit-hooks": {
|
"pre-commit-hooks": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
"flake-utils": [
|
"flake-utils": [
|
||||||
"flake-utils"
|
"futils"
|
||||||
],
|
],
|
||||||
|
"gitignore": "gitignore",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
],
|
||||||
|
"nixpkgs-stable": "nixpkgs-stable"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1656169028,
|
"lastModified": 1711519547,
|
||||||
"narHash": "sha256-y9DRauokIeVHM7d29lwT8A+0YoGUBXV3H0VErxQeA8s=",
|
"narHash": "sha256-Q7YmSCUJmDl71fJv/zD9lrOCJ1/SE/okZ2DsrmRjzhY=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "pre-commit-hooks.nix",
|
"repo": "pre-commit-hooks.nix",
|
||||||
"rev": "db3bd555d3a3ceab208bed48f983ccaa6a71a25e",
|
"rev": "7d47a32e5cd1ea481fab33c516356ce27c8cef4a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -79,34 +117,23 @@
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
"futils": "futils",
|
||||||
"naersk": "naersk",
|
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"pre-commit-hooks": "pre-commit-hooks",
|
"pre-commit-hooks": "pre-commit-hooks"
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"systems": {
|
||||||
"inputs": {
|
|
||||||
"flake-utils": [
|
|
||||||
"flake-utils"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1657853760,
|
"lastModified": 1681028828,
|
||||||
"narHash": "sha256-X6ERAyUXGsrhbhgkxNaQl40wcus5uyQZOCxUh5neK+g=",
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
"owner": "oxalica",
|
"owner": "nix-systems",
|
||||||
"repo": "rust-overlay",
|
"repo": "default",
|
||||||
"rev": "a97a761cc11327bb109dc30af1c637b986be7959",
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "oxalica",
|
"owner": "nix-systems",
|
||||||
"ref": "master",
|
"repo": "default",
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
160
flake.nix
160
flake.nix
|
@ -1,29 +1,19 @@
|
||||||
{
|
{
|
||||||
description = "A handy file picker program";
|
description = "A chess engine";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
flake-utils = {
|
futils = {
|
||||||
type = "github";
|
type = "github";
|
||||||
owner = "numtide";
|
owner = "numtide";
|
||||||
repo = "flake-utils";
|
repo = "flake-utils";
|
||||||
ref = "master";
|
ref = "main";
|
||||||
};
|
|
||||||
|
|
||||||
naersk = {
|
|
||||||
type = "github";
|
|
||||||
owner = "nix-community";
|
|
||||||
repo = "naersk";
|
|
||||||
ref = "master";
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nixpkgs = {
|
nixpkgs = {
|
||||||
type = "github";
|
type = "github";
|
||||||
owner = "NixOS";
|
owner = "NixOS";
|
||||||
repo = "nixpkgs";
|
repo = "nixpkgs";
|
||||||
ref = "nixpkgs-unstable";
|
ref = "nixos-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
pre-commit-hooks = {
|
pre-commit-hooks = {
|
||||||
|
@ -32,76 +22,53 @@
|
||||||
repo = "pre-commit-hooks.nix";
|
repo = "pre-commit-hooks.nix";
|
||||||
ref = "master";
|
ref = "master";
|
||||||
inputs = {
|
inputs = {
|
||||||
flake-utils.follows = "flake-utils";
|
flake-utils.follows = "futils";
|
||||||
nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
rust-overlay = {
|
|
||||||
type = "github";
|
|
||||||
owner = "oxalica";
|
|
||||||
repo = "rust-overlay";
|
|
||||||
ref = "master";
|
|
||||||
inputs = {
|
|
||||||
flake-utils.follows = "flake-utils";
|
|
||||||
nixpkgs.follows = "nixpkgs";
|
nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs = { self, futils, nixpkgs, pre-commit-hooks }:
|
||||||
{ self
|
{
|
||||||
, flake-utils
|
overlays = {
|
||||||
, naersk
|
default = final: _prev: {
|
||||||
, nixpkgs
|
seer = with final; rustPlatform.buildRustPackage {
|
||||||
, pre-commit-hooks
|
pname = "seer";
|
||||||
, rust-overlay
|
version = (final.lib.importTOML ./Cargo.toml).package.version;
|
||||||
}:
|
|
||||||
let
|
|
||||||
inherit (flake-utils.lib) eachSystem system;
|
|
||||||
|
|
||||||
mySystems = [
|
src = self;
|
||||||
system.aarch64-linux
|
|
||||||
system.x86_64-darwin
|
|
||||||
system.x86_64-linux
|
|
||||||
];
|
|
||||||
|
|
||||||
eachMySystem = eachSystem mySystems;
|
cargoLock = {
|
||||||
in
|
lockFile = "${self}/Cargo.lock";
|
||||||
eachMySystem (system:
|
};
|
||||||
let
|
|
||||||
overlays = [ (import rust-overlay) ];
|
meta = with lib; {
|
||||||
pkgs = import nixpkgs { inherit overlays system; };
|
description = "A chess engine";
|
||||||
my-rust = pkgs.rust-bin.stable.latest.default.override {
|
homepage = "https://git.belanyi.fr/ambroisie/seer";
|
||||||
extensions = [ "rust-src" ];
|
license = licenses.mit;
|
||||||
};
|
maintainers = with maintainers; [ ambroisie ];
|
||||||
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 {
|
};
|
||||||
|
} // futils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [
|
||||||
|
self.overlays.default
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
pre-commit = pre-commit-hooks.lib.${system}.run {
|
||||||
src = self;
|
src = self;
|
||||||
|
|
||||||
hooks = {
|
hooks = {
|
||||||
clippy = {
|
clippy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
entry = lib.mkForce "${rust-env}/bin/cargo-clippy clippy";
|
settings = {
|
||||||
|
denyWarnings = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
nixpkgs-fmt = {
|
nixpkgs-fmt = {
|
||||||
|
@ -110,43 +77,36 @@
|
||||||
|
|
||||||
rustfmt = {
|
rustfmt = {
|
||||||
enable = true;
|
enable = true;
|
||||||
entry = lib.mkForce "${rust-env}/bin/cargo-fmt fmt -- --check --color always";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
rec {
|
{
|
||||||
|
checks = {
|
||||||
devShells = {
|
inherit (self.packages.${system}) seer;
|
||||||
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";
|
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
packages = {
|
devShells = {
|
||||||
default = self.packages."${system}".seer;
|
default = pkgs.mkShell {
|
||||||
|
inputsFrom = with self.packages.${system}; [
|
||||||
|
seer
|
||||||
|
];
|
||||||
|
|
||||||
seer = naersk-lib.buildPackage {
|
packages = with pkgs; [
|
||||||
src = self;
|
clippy
|
||||||
|
rust-analyzer
|
||||||
|
rustfmt
|
||||||
|
];
|
||||||
|
|
||||||
doCheck = true;
|
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||||
|
|
||||||
passthru = {
|
inherit (pre-commit) shellHook;
|
||||||
inherit my-rust;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
});
|
packages = futils.lib.flattenTree {
|
||||||
|
default = pkgs.seer;
|
||||||
|
inherit (pkgs) seer;
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
0
rustfmt.toml
Normal file
0
rustfmt.toml
Normal file
19
src/board/bitboard/error.rs
Normal file
19
src/board/bitboard/error.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum IntoSquareError {
|
||||||
|
/// The board is empty.
|
||||||
|
EmptyBoard,
|
||||||
|
/// The board contains more than one square.
|
||||||
|
TooManySquares,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for IntoSquareError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let error_msg = match self {
|
||||||
|
Self::EmptyBoard => "The board is empty",
|
||||||
|
Self::TooManySquares => "The board contains more than one square",
|
||||||
|
};
|
||||||
|
write!(f, "{}", error_msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for IntoSquareError {}
|
|
@ -1,23 +1,28 @@
|
||||||
/// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a
|
/// An [Iterator](std::iter::Iterator) of [Square](crate::board::Square) contained in a
|
||||||
/// [Bitboard](crate::board::Bitboard).
|
/// [Bitboard].
|
||||||
pub struct BitboardIterator(pub(crate) u64);
|
use crate::board::Bitboard;
|
||||||
|
|
||||||
|
pub struct BitboardIterator(Bitboard);
|
||||||
|
|
||||||
|
impl BitboardIterator {
|
||||||
|
pub fn new(board: Bitboard) -> Self {
|
||||||
|
Self(board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Iterator for BitboardIterator {
|
impl Iterator for BitboardIterator {
|
||||||
type Item = crate::board::Square;
|
type Item = crate::board::Square;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if self.0 == 0 {
|
let res = self.0.any_square();
|
||||||
None
|
if let Some(square) = res {
|
||||||
} else {
|
self.0 ^= square;
|
||||||
let lsb = self.0.trailing_zeros() as usize;
|
};
|
||||||
self.0 ^= 1 << lsb;
|
res
|
||||||
// SAFETY: we know the value is in-bounds
|
|
||||||
Some(unsafe { crate::board::Square::from_index_unchecked(lsb) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
let size = self.0.count_ones() as usize;
|
let size = self.0.count() as usize;
|
||||||
|
|
||||||
(size, Some(size))
|
(size, Some(size))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use super::Square;
|
use super::{File, Rank, Square};
|
||||||
use crate::utils::static_assert;
|
use crate::utils::static_assert;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
use error::*;
|
||||||
mod iterator;
|
mod iterator;
|
||||||
use iterator::*;
|
use iterator::*;
|
||||||
mod superset;
|
mod superset;
|
||||||
|
@ -19,8 +21,7 @@ impl Bitboard {
|
||||||
pub const ALL: Bitboard = Bitboard(u64::MAX);
|
pub const ALL: Bitboard = Bitboard(u64::MAX);
|
||||||
|
|
||||||
/// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8.
|
/// Array of bitboards representing the eight ranks, in order from rank 1 to rank 8.
|
||||||
#[allow(clippy::unusual_byte_groupings)]
|
pub const RANKS: [Self; Rank::NUM_VARIANTS] = [
|
||||||
pub const RANKS: [Self; 8] = [
|
|
||||||
Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001),
|
Bitboard(0b00000001_00000001_00000001_00000001_00000001_00000001_00000001_00000001),
|
||||||
Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010),
|
Bitboard(0b00000010_00000010_00000010_00000010_00000010_00000010_00000010_00000010),
|
||||||
Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100),
|
Bitboard(0b00000100_00000100_00000100_00000100_00000100_00000100_00000100_00000100),
|
||||||
|
@ -32,8 +33,7 @@ impl Bitboard {
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Array of bitboards representing the eight files, in order from file A to file H.
|
/// Array of bitboards representing the eight files, in order from file A to file H.
|
||||||
#[allow(clippy::unusual_byte_groupings)]
|
pub const FILES: [Self; File::NUM_VARIANTS] = [
|
||||||
pub const FILES: [Self; 8] = [
|
|
||||||
Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111),
|
Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111),
|
||||||
Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000),
|
Bitboard(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_00000000),
|
||||||
Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000),
|
Bitboard(0b00000000_00000000_00000000_00000000_00000000_11111111_00000000_00000000),
|
||||||
|
@ -75,6 +75,12 @@ impl Bitboard {
|
||||||
(self.0 & (self.0.wrapping_sub(1))) != 0
|
(self.0 & (self.0.wrapping_sub(1))) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a [Square] from the board, or `None` if it is empty.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn any_square(self) -> Option<Square> {
|
||||||
|
Square::try_from_index(self.0.trailing_zeros() as usize)
|
||||||
|
}
|
||||||
|
|
||||||
/// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of
|
/// Iterate over the power-set of a given [Bitboard], yielding each possible sub-set of
|
||||||
/// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that
|
/// [Square] that belong to the [Bitboard]. In other words, generate all set of [Square] that
|
||||||
/// contain all, some, or none of the [Square] that are in the given [Bitboard].
|
/// contain all, some, or none of the [Square] that are in the given [Bitboard].
|
||||||
|
@ -83,18 +89,6 @@ impl Bitboard {
|
||||||
pub fn iter_power_set(self) -> impl Iterator<Item = Self> {
|
pub fn iter_power_set(self) -> impl Iterator<Item = Self> {
|
||||||
BitboardPowerSetIterator::new(self)
|
BitboardPowerSetIterator::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is
|
|
||||||
/// occupying. Otherwise return `None`.
|
|
||||||
pub fn try_into_square(self) -> Option<Square> {
|
|
||||||
if self.count() != 1 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let index = self.0.trailing_zeros() as usize;
|
|
||||||
// SAFETY: we know the value is in-bounds
|
|
||||||
Some(unsafe { Square::from_index_unchecked(index) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure zero-cost (at least size-wise) wrapping.
|
// Ensure zero-cost (at least size-wise) wrapping.
|
||||||
|
@ -112,7 +106,20 @@ impl IntoIterator for Bitboard {
|
||||||
type Item = Square;
|
type Item = Square;
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
BitboardIterator(self.0)
|
BitboardIterator::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the given [Bitboard] is a singleton piece on a board, return the [Square] that it is
|
||||||
|
/// occupying. Otherwise return `None`.
|
||||||
|
impl TryInto<Square> for Bitboard {
|
||||||
|
type Error = IntoSquareError;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<Square, Self::Error> {
|
||||||
|
if self.has_more_than_one() {
|
||||||
|
return Err(IntoSquareError::TooManySquares);
|
||||||
|
}
|
||||||
|
self.any_square().ok_or(IntoSquareError::EmptyBoard)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,15 +455,11 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn iter_power_set_six_squares_exhaustive() {
|
fn iter_power_set_six_squares_exhaustive() {
|
||||||
let mask = (0..6)
|
let mask = (0..6)
|
||||||
.into_iter()
|
|
||||||
.map(Square::from_index)
|
.map(Square::from_index)
|
||||||
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs);
|
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mask.iter_power_set().collect::<HashSet<_>>(),
|
mask.iter_power_set().collect::<HashSet<_>>(),
|
||||||
(0..(1 << 6))
|
(0..(1 << 6)).map(Bitboard).collect::<HashSet<_>>()
|
||||||
.into_iter()
|
|
||||||
.map(Bitboard)
|
|
||||||
.collect::<HashSet<_>>()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,16 +483,39 @@ mod test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn any_square() {
|
||||||
|
for square in Square::iter() {
|
||||||
|
assert_eq!(square.into_bitboard().any_square(), Some(square));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn any_square_empty() {
|
||||||
|
assert!(Bitboard::EMPTY.any_square().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn any_square_full_board() {
|
||||||
|
assert!(Bitboard::ALL.any_square().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn into_square() {
|
fn into_square() {
|
||||||
for square in Square::iter() {
|
for square in Square::iter() {
|
||||||
assert_eq!(square.into_bitboard().try_into_square(), Some(square));
|
assert_eq!(square.into_bitboard().try_into(), Ok(square));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn into_square_invalid() {
|
fn into_square_invalid() {
|
||||||
assert!(Bitboard::EMPTY.try_into_square().is_none());
|
assert_eq!(
|
||||||
assert!((Square::A1 | Square::A2).try_into_square().is_none())
|
TryInto::<Square>::try_into(Bitboard::EMPTY),
|
||||||
|
Err(IntoSquareError::EmptyBoard)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
TryInto::<Square>::try_into(Square::A1 | Square::A2),
|
||||||
|
Err(IntoSquareError::TooManySquares)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,21 +4,20 @@ use super::Bitboard;
|
||||||
/// In other words, for each square that belongs to the mask, this will yield all sets that do
|
/// In other words, for each square that belongs to the mask, this will yield all sets that do
|
||||||
/// contain the square, and all sets that do not.
|
/// contain the square, and all sets that do not.
|
||||||
pub struct BitboardPowerSetIterator {
|
pub struct BitboardPowerSetIterator {
|
||||||
/// The mask.
|
/// The starting board.
|
||||||
mask: Bitboard,
|
board: Bitboard,
|
||||||
/// The "index" of the next blocker set that should be generated.
|
/// The next subset.
|
||||||
current: usize,
|
subset: Bitboard,
|
||||||
/// The number of blocker sets that should be generated by [BlockerIterator], i.e: 2^n with n
|
/// Whether or not iteration is done.
|
||||||
/// the number of squares belonging to `mask`.
|
done: bool,
|
||||||
total: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitboardPowerSetIterator {
|
impl BitboardPowerSetIterator {
|
||||||
pub fn new(mask: Bitboard) -> Self {
|
pub fn new(board: Bitboard) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mask,
|
board,
|
||||||
current: 0,
|
subset: Bitboard::EMPTY,
|
||||||
total: 1 << mask.count(),
|
done: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,22 +26,18 @@ impl Iterator for BitboardPowerSetIterator {
|
||||||
type Item = Bitboard;
|
type Item = Bitboard;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if self.current >= self.total {
|
if self.done {
|
||||||
None
|
return None;
|
||||||
} else {
|
|
||||||
let blockers = (0..)
|
|
||||||
.into_iter()
|
|
||||||
.zip(self.mask.into_iter())
|
|
||||||
.filter(|(index, _)| self.current & (1 << index) != 0)
|
|
||||||
.map(|(_, board)| board)
|
|
||||||
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs);
|
|
||||||
self.current += 1;
|
|
||||||
Some(blockers)
|
|
||||||
}
|
}
|
||||||
|
let res = self.subset;
|
||||||
|
self.subset = Bitboard(self.subset.0.wrapping_sub(self.board.0)) & self.board;
|
||||||
|
self.done = self.subset.is_empty();
|
||||||
|
Some(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
(self.total, Some(self.total))
|
let size = 1 << self.board.count();
|
||||||
|
(size, Some(size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use super::{Bitboard, Color, File, FromFen, Square};
|
use super::{Bitboard, Color, File, Square};
|
||||||
use crate::error::Error;
|
|
||||||
|
|
||||||
/// Current castle rights for a player.
|
/// Current castle rights for a player.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
@ -18,19 +17,44 @@ impl CastleRights {
|
||||||
/// The number of [CastleRights] variants.
|
/// The number of [CastleRights] variants.
|
||||||
pub const NUM_VARIANTS: usize = 4;
|
pub const NUM_VARIANTS: usize = 4;
|
||||||
|
|
||||||
|
const ALL: [Self; Self::NUM_VARIANTS] = [
|
||||||
|
Self::NoSide,
|
||||||
|
Self::KingSide,
|
||||||
|
Self::QueenSide,
|
||||||
|
Self::BothSides,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Iterate over all castle-rights variants.
|
||||||
|
pub fn iter() -> impl Iterator<Item = Self> {
|
||||||
|
Self::ALL.iter().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert from a castle rights index into a [CastleRights] type.
|
/// Convert from a castle rights index into a [CastleRights] type.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the index is out of bounds.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_index(index: usize) -> Self {
|
pub fn from_index(index: usize) -> Self {
|
||||||
assert!(index < Self::NUM_VARIANTS);
|
Self::try_from_index(index).expect("index out of bouds")
|
||||||
// SAFETY: we know the value is in-bounds
|
}
|
||||||
unsafe { Self::from_index_unchecked(index) }
|
|
||||||
|
/// Convert from a castle rights index into a [CastleRights] type. Returns [None] if the index
|
||||||
|
/// is out of bounds.
|
||||||
|
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||||
|
if index < Self::NUM_VARIANTS {
|
||||||
|
// SAFETY: we know the value is in-bounds
|
||||||
|
Some(unsafe { Self::from_index_unchecked(index) })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a castle rights index into a [CastleRights] type, no bounds checking.
|
/// Convert from a castle rights index into a [CastleRights] type, no bounds checking.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
///
|
///
|
||||||
/// Should only be called with values that can be output by [CastleRights::index()].
|
/// This should only be called with values that can be output by [CastleRights::index()].
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub unsafe fn from_index_unchecked(index: usize) -> Self {
|
pub unsafe fn from_index_unchecked(index: usize) -> Self {
|
||||||
std::mem::transmute(index as u8)
|
std::mem::transmute(index as u8)
|
||||||
|
@ -67,11 +91,10 @@ impl CastleRights {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add some [CastleRights], and return the resulting [CastleRights].
|
/// Add some [CastleRights], and return the resulting [CastleRights].
|
||||||
#[allow(clippy::should_implement_trait)]
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn add(self, to_remove: CastleRights) -> Self {
|
fn add(self, additional_rights: CastleRights) -> Self {
|
||||||
// SAFETY: we know the value is in-bounds
|
// SAFETY: we know the value is in-bounds
|
||||||
unsafe { Self::from_index_unchecked(self.index() | to_remove.index()) }
|
unsafe { Self::from_index_unchecked(self.index() | additional_rights.index()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove king-side castling rights.
|
/// Remove king-side castling rights.
|
||||||
|
@ -110,39 +133,6 @@ impl CastleRights {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the castling rights segment of a FEN string to an array of [CastleRights].
|
|
||||||
impl FromFen for [CastleRights; 2] {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
if s.len() > 4 {
|
|
||||||
return Err(Error::InvalidFen);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut res = [CastleRights::NoSide; 2];
|
|
||||||
|
|
||||||
if s == "-" {
|
|
||||||
return Ok(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
for b in s.chars() {
|
|
||||||
let color = if b.is_uppercase() {
|
|
||||||
Color::White
|
|
||||||
} else {
|
|
||||||
Color::Black
|
|
||||||
};
|
|
||||||
let rights = &mut res[color.index()];
|
|
||||||
match b {
|
|
||||||
'k' | 'K' => *rights = rights.with_king_side(),
|
|
||||||
'q' | 'Q' => *rights = rights.with_queen_side(),
|
|
||||||
_ => return Err(Error::InvalidFen),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
164
src/board/chess_board/builder.rs
Normal file
164
src/board/chess_board/builder.rs
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
56
src/board/chess_board/error.rs
Normal file
56
src/board/chess_board/error.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/// 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 {}
|
828
src/board/chess_board/mod.rs
Normal file
828
src/board/chess_board/mod.rs
Normal file
|
@ -0,0 +1,828 @@
|
||||||
|
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,5 +1,4 @@
|
||||||
use super::{Direction, FromFen, Rank};
|
use super::{Direction, Rank};
|
||||||
use crate::error::Error;
|
|
||||||
|
|
||||||
/// An enum representing the color of a player.
|
/// An enum representing the color of a player.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
@ -19,15 +18,28 @@ impl Color {
|
||||||
Self::ALL.iter().cloned()
|
Self::ALL.iter().cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a piece index into a [Color] type.
|
/// Convert from a color index into a [Color] type.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the index is out of bounds.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_index(index: usize) -> Self {
|
pub fn from_index(index: usize) -> Self {
|
||||||
assert!(index < Self::NUM_VARIANTS);
|
Self::try_from_index(index).expect("index out of bouds")
|
||||||
// SAFETY: we know the value is in-bounds
|
|
||||||
unsafe { Self::from_index_unchecked(index) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a piece index into a [Color] type, no bounds checking.
|
/// 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.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
///
|
///
|
||||||
|
@ -107,20 +119,6 @@ impl Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a side to move segment of a FEN string to a [Color].
|
|
||||||
impl FromFen for Color {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let res = match s {
|
|
||||||
"w" => Color::White,
|
|
||||||
"b" => Color::Black,
|
|
||||||
_ => return Err(Error::InvalidFen),
|
|
||||||
};
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Not for Color {
|
impl std::ops::Not for Color {
|
||||||
type Output = Color;
|
type Output = Color;
|
||||||
|
|
||||||
|
|
|
@ -128,8 +128,7 @@ impl Direction {
|
||||||
/// Slide a board along the given [Direction], i.e: return all successive applications of
|
/// Slide a board along the given [Direction], i.e: return all successive applications of
|
||||||
/// [Direction::move_board] until no new squares can be reached.
|
/// [Direction::move_board] until no new squares can be reached.
|
||||||
/// Take into account the `blockers` [Bitboard]: a combination of all pieces on the board which
|
/// Take into account the `blockers` [Bitboard]: a combination of all pieces on the board which
|
||||||
/// cannot be slided over. The slide is over once a square that is part of `blockers` is
|
/// cannot be slid over. The slide is over once a square that is part of `blockers` is reached.
|
||||||
/// reached.
|
|
||||||
/// It does not make sense to use this method with knight-only directions, and it will panic in
|
/// It does not make sense to use this method with knight-only directions, and it will panic in
|
||||||
/// debug-mode if it happens.
|
/// debug-mode if it happens.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
/// 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,11 +35,23 @@ impl File {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a file index into a [File] type.
|
/// Convert from a file index into a [File] type.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the index is out of bounds.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_index(index: usize) -> Self {
|
pub fn from_index(index: usize) -> Self {
|
||||||
assert!(index < 8);
|
Self::try_from_index(index).expect("index out of bouds")
|
||||||
// SAFETY: we know the value is in-bounds
|
}
|
||||||
unsafe { Self::from_index_unchecked(index) }
|
|
||||||
|
/// Convert from a file index into a [File] type. Returns [None] if the index is out of bounds.
|
||||||
|
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||||
|
if index < Self::NUM_VARIANTS {
|
||||||
|
// SAFETY: we know the value is in-bounds
|
||||||
|
Some(unsafe { Self::from_index_unchecked(index) })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a file index into a [File] type, no bounds checking.
|
/// Convert from a file index into a [File] type, no bounds checking.
|
||||||
|
|
|
@ -13,9 +13,6 @@ pub use color::*;
|
||||||
pub mod direction;
|
pub mod direction;
|
||||||
pub use direction::*;
|
pub use direction::*;
|
||||||
|
|
||||||
pub mod fen;
|
|
||||||
pub use fen::*;
|
|
||||||
|
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub use file::*;
|
pub use file::*;
|
||||||
|
|
||||||
|
|
|
@ -1,232 +1,42 @@
|
||||||
use super::{Piece, Square};
|
use super::{Piece, Square};
|
||||||
|
|
||||||
type Bitset = u32;
|
|
||||||
|
|
||||||
/// A chess move, containing:
|
/// A chess move, containing:
|
||||||
/// * Piece type.
|
|
||||||
/// * Starting square.
|
/// * Starting square.
|
||||||
/// * Destination square.
|
/// * Destination square.
|
||||||
/// * Optional capture type.
|
|
||||||
/// * Optional promotion type.
|
/// * Optional promotion type.
|
||||||
/// * Optional captured type.
|
|
||||||
/// * Whether the move was an en-passant capture.
|
|
||||||
/// * Whether the move was a double-step.
|
|
||||||
/// * Whether the move was a castling.
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct Move(Bitset);
|
pub struct Move {
|
||||||
|
start: Square,
|
||||||
/// A builder for [Move]. This is the prefered and only way of building a [Move].
|
destination: Square,
|
||||||
pub struct MoveBuilder {
|
promotion: Option<Piece>,
|
||||||
pub piece: Piece,
|
|
||||||
pub start: Square,
|
|
||||||
pub destination: Square,
|
|
||||||
pub capture: Option<Piece>,
|
|
||||||
pub promotion: Option<Piece>,
|
|
||||||
pub en_passant: bool,
|
|
||||||
pub double_step: bool,
|
|
||||||
pub castling: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<MoveBuilder> for Move {
|
|
||||||
#[inline(always)]
|
|
||||||
fn from(builder: MoveBuilder) -> Self {
|
|
||||||
Self::new(
|
|
||||||
builder.piece,
|
|
||||||
builder.start,
|
|
||||||
builder.destination,
|
|
||||||
builder.capture,
|
|
||||||
builder.promotion,
|
|
||||||
builder.en_passant,
|
|
||||||
builder.double_step,
|
|
||||||
builder.castling,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [Move] is structured as a bitset with the following fields:
|
|
||||||
/// | Field | Size | Range of values | Note |
|
|
||||||
/// |-------------|------|-----------------|-------------------------------------------------|
|
|
||||||
/// | Piece | 3 | 0-6 | Can be interpreted as a [Piece] index |
|
|
||||||
/// | Start | 6 | 0-63 | Can be interpreted as a [Square] index |
|
|
||||||
/// | Destination | 6 | 0-63 | Can be interpreted as a [Square] index |
|
|
||||||
/// | Capture | 3 | 0-7 | Can be interpreted as a [Piece] index if not 7 |
|
|
||||||
/// | Promotion | 3 | 0-7 | Can be interpreted as a [Piece] index if not 7 |
|
|
||||||
/// | En-pasant | 1 | 0-1 | Boolean value |
|
|
||||||
/// | Double-step | 1 | 0-1 | Boolean value |
|
|
||||||
/// | Castling | 1 | 0-1 | Boolean value |
|
|
||||||
mod shift {
|
|
||||||
use super::Bitset;
|
|
||||||
|
|
||||||
pub const PIECE: usize = 0;
|
|
||||||
pub const PIECE_MASK: Bitset = 0b111;
|
|
||||||
|
|
||||||
pub const START: usize = 3;
|
|
||||||
pub const START_MASK: Bitset = 0b11_1111;
|
|
||||||
|
|
||||||
pub const DESTINATION: usize = 9;
|
|
||||||
pub const DESTINATION_MASK: Bitset = 0b11_1111;
|
|
||||||
|
|
||||||
pub const CAPTURE: usize = 15;
|
|
||||||
pub const CAPTURE_MASK: Bitset = 0b111;
|
|
||||||
|
|
||||||
pub const PROMOTION: usize = 18;
|
|
||||||
pub const PROMOTION_MASK: Bitset = 0b111;
|
|
||||||
|
|
||||||
pub const EN_PASSANT: usize = 21;
|
|
||||||
pub const EN_PASSANT_MASK: Bitset = 0b1;
|
|
||||||
|
|
||||||
pub const DOUBLE_STEP: usize = 22;
|
|
||||||
pub const DOUBLE_STEP_MASK: Bitset = 0b1;
|
|
||||||
|
|
||||||
pub const CASTLING: usize = 23;
|
|
||||||
pub const CASTLING_MASK: Bitset = 0b1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Move {
|
impl Move {
|
||||||
/// Construct a new move.
|
/// Construct a new move.
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn new(
|
pub fn new(start: Square, destination: Square, promotion: Option<Piece>) -> Self {
|
||||||
piece: Piece,
|
Self {
|
||||||
start: Square,
|
start,
|
||||||
destination: Square,
|
destination,
|
||||||
capture: Option<Piece>,
|
promotion,
|
||||||
promotion: Option<Piece>,
|
}
|
||||||
en_passant: bool,
|
|
||||||
double_step: bool,
|
|
||||||
castling: bool,
|
|
||||||
) -> Self {
|
|
||||||
let mut value = 0;
|
|
||||||
value |= (piece.index() as Bitset) << shift::PIECE;
|
|
||||||
value |= (start.index() as Bitset) << shift::START;
|
|
||||||
value |= (destination.index() as Bitset) << shift::DESTINATION;
|
|
||||||
value |=
|
|
||||||
(capture.map(Piece::index).unwrap_or(Piece::NUM_VARIANTS) as Bitset) << shift::CAPTURE;
|
|
||||||
value |= (promotion.map(Piece::index).unwrap_or(Piece::NUM_VARIANTS) as Bitset)
|
|
||||||
<< shift::PROMOTION;
|
|
||||||
value |= (en_passant as Bitset) << shift::EN_PASSANT;
|
|
||||||
value |= (double_step as Bitset) << shift::DOUBLE_STEP;
|
|
||||||
value |= (castling as Bitset) << shift::CASTLING;
|
|
||||||
Self(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the [Piece] that is being moved.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn piece(self) -> Piece {
|
|
||||||
let index = ((self.0 >> shift::PIECE) & shift::PIECE_MASK) as usize;
|
|
||||||
// SAFETY: we know the value is in-bounds
|
|
||||||
unsafe { Piece::from_index_unchecked(index) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the [Square] that this move starts from.
|
/// Get the [Square] that this move starts from.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn start(self) -> Square {
|
pub fn start(self) -> Square {
|
||||||
let index = ((self.0 >> shift::START) & shift::START_MASK) as usize;
|
self.start
|
||||||
// SAFETY: we know the value is in-bounds
|
|
||||||
unsafe { Square::from_index_unchecked(index) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the [Square] that this move ends on.
|
/// Get the [Square] that this move ends on.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn destination(self) -> Square {
|
pub fn destination(self) -> Square {
|
||||||
let index = ((self.0 >> shift::DESTINATION) & shift::DESTINATION_MASK) as usize;
|
self.destination
|
||||||
// SAFETY: we know the value is in-bounds
|
|
||||||
unsafe { Square::from_index_unchecked(index) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the [Piece] that this move captures, or `None` if there are no captures.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn capture(self) -> Option<Piece> {
|
|
||||||
let index = ((self.0 >> shift::CAPTURE) & shift::CAPTURE_MASK) as usize;
|
|
||||||
if index < Piece::NUM_VARIANTS {
|
|
||||||
// SAFETY: we know the value is in-bounds
|
|
||||||
unsafe { Some(Piece::from_index_unchecked(index)) }
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the [Piece] that this move promotes to, or `None` if there are no promotions.
|
/// Get the [Piece] that this move promotes to, or `None` if there are no promotions.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn promotion(self) -> Option<Piece> {
|
pub fn promotion(self) -> Option<Piece> {
|
||||||
let index = ((self.0 >> shift::PROMOTION) & shift::PROMOTION_MASK) as usize;
|
self.promotion
|
||||||
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,6 +1,3 @@
|
||||||
use super::FromFen;
|
|
||||||
use crate::error::Error;
|
|
||||||
|
|
||||||
/// An enum representing the type of a piece.
|
/// An enum representing the type of a piece.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum Piece {
|
pub enum Piece {
|
||||||
|
@ -31,11 +28,24 @@ impl Piece {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a piece index into a [Piece] type.
|
/// Convert from a piece index into a [Piece] type.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the index is out of bounds.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_index(index: usize) -> Self {
|
pub fn from_index(index: usize) -> Self {
|
||||||
assert!(index < Self::NUM_VARIANTS);
|
Self::try_from_index(index).expect("index out of bouds")
|
||||||
// SAFETY: we know the value is in-bounds
|
}
|
||||||
unsafe { Self::from_index_unchecked(index) }
|
|
||||||
|
/// Convert from a piece index into a [Piece] type. Returns [None] if the index is out of
|
||||||
|
/// bounds.
|
||||||
|
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||||
|
if index < Self::NUM_VARIANTS {
|
||||||
|
// SAFETY: we know the value is in-bounds
|
||||||
|
Some(unsafe { Self::from_index_unchecked(index) })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a piece index into a [Piece] type, no bounds checking.
|
/// Convert from a piece index into a [Piece] type, no bounds checking.
|
||||||
|
@ -55,24 +65,6 @@ impl Piece {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a piece in FEN notation to a [Piece].
|
|
||||||
impl FromFen for Piece {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let res = match s {
|
|
||||||
"p" | "P" => Self::Pawn,
|
|
||||||
"n" | "N" => Self::Knight,
|
|
||||||
"b" | "B" => Self::Bishop,
|
|
||||||
"r" | "R" => Self::Rook,
|
|
||||||
"q" | "Q" => Self::Queen,
|
|
||||||
"k" | "K" => Self::King,
|
|
||||||
_ => return Err(Error::InvalidFen),
|
|
||||||
};
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -35,11 +35,23 @@ impl Rank {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a rank index into a [Rank] type.
|
/// Convert from a rank index into a [Rank] type.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the index is out of bounds.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_index(index: usize) -> Self {
|
pub fn from_index(index: usize) -> Self {
|
||||||
assert!(index < 8);
|
Self::try_from_index(index).expect("index out of bouds")
|
||||||
// SAFETY: we know the value is in-bounds
|
}
|
||||||
unsafe { Self::from_index_unchecked(index) }
|
|
||||||
|
/// Convert from a rank index into a [Rank] type. Returns [None] if the index is out of bounds.
|
||||||
|
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||||
|
if index < Self::NUM_VARIANTS {
|
||||||
|
// SAFETY: we know the value is in-bounds
|
||||||
|
Some(unsafe { Self::from_index_unchecked(index) })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a rank index into a [Rank] type, no bounds checking.
|
/// Convert from a rank index into a [Rank] type, no bounds checking.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::{Bitboard, File, FromFen, Rank};
|
use super::{Bitboard, File, Rank};
|
||||||
use crate::{error::Error, utils::static_assert};
|
use crate::utils::static_assert;
|
||||||
|
|
||||||
/// Represent a square on a chessboard. Defined in the same order as the
|
/// Represent a square on a chessboard. Defined in the same order as the
|
||||||
/// [Bitboard] squares.
|
/// [Bitboard] squares.
|
||||||
|
@ -39,6 +39,10 @@ impl Square {
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Construct a [Square] from a [File] and [Rank].
|
/// Construct a [Square] from a [File] and [Rank].
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the index is out of bounds.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn new(file: File, rank: Rank) -> Self {
|
pub fn new(file: File, rank: Rank) -> Self {
|
||||||
// SAFETY: we know the value is in-bounds
|
// SAFETY: we know the value is in-bounds
|
||||||
|
@ -53,9 +57,18 @@ impl Square {
|
||||||
/// Convert from a square index into a [Square] type.
|
/// Convert from a square index into a [Square] type.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_index(index: usize) -> Self {
|
pub fn from_index(index: usize) -> Self {
|
||||||
assert!(index < 64);
|
Self::try_from_index(index).expect("index out of bouds")
|
||||||
// SAFETY: we know the value is in-bounds
|
}
|
||||||
unsafe { Self::from_index_unchecked(index) }
|
|
||||||
|
/// Convert from a square index into a [Square] type. Returns [None] if the index is out of
|
||||||
|
/// bounds.
|
||||||
|
pub fn try_from_index(index: usize) -> Option<Self> {
|
||||||
|
if index < Self::NUM_VARIANTS {
|
||||||
|
// SAFETY: we know the value is in-bounds
|
||||||
|
Some(unsafe { Self::from_index_unchecked(index) })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from a square index into a [Square] type, no bounds checking.
|
/// Convert from a square index into a [Square] type, no bounds checking.
|
||||||
|
@ -107,23 +120,6 @@ impl Square {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert an en-passant target square segment of a FEN string to an optional [Square].
|
|
||||||
impl FromFen for Option<Square> {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_fen(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let res = match s.as_bytes() {
|
|
||||||
[b'-'] => None,
|
|
||||||
[file @ b'a'..=b'h', rank @ b'1'..=b'8'] => Some(Square::new(
|
|
||||||
File::from_index((file - b'a') as usize),
|
|
||||||
Rank::from_index((rank - b'1') as usize),
|
|
||||||
)),
|
|
||||||
_ => return Err(Error::InvalidFen),
|
|
||||||
};
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shift the square's index left by the amount given.
|
/// Shift the square's index left by the amount given.
|
||||||
impl std::ops::Shl<usize> for Square {
|
impl std::ops::Shl<usize> for Square {
|
||||||
type Output = Square;
|
type Output = Square;
|
||||||
|
|
144
src/build.rs
144
src/build.rs
|
@ -1,144 +0,0 @@
|
||||||
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
19
src/error.rs
|
@ -1,19 +0,0 @@
|
||||||
/// 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
Normal file
244
src/fen.rs
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
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 board;
|
||||||
pub mod error;
|
pub mod fen;
|
||||||
pub mod movegen;
|
pub mod movegen;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
use crate::board::Bitboard;
|
|
||||||
|
|
||||||
/// A type representing the magic board indexing a given [crate::board::Square].
|
|
||||||
pub struct Magic {
|
|
||||||
/// Magic number.
|
|
||||||
pub(crate) magic: u64,
|
|
||||||
/// Base offset into the magic square table.
|
|
||||||
pub(crate) offset: usize,
|
|
||||||
/// Mask to apply to the blocker board before applying the magic.
|
|
||||||
pub(crate) mask: Bitboard,
|
|
||||||
/// Length of the resulting mask after applying the magic.
|
|
||||||
pub(crate) shift: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Magic {
|
|
||||||
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!()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
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
|
// Naive move generation
|
||||||
pub mod naive;
|
mod naive;
|
||||||
|
|
||||||
// Magic bitboard generation
|
// Magic bitboard generation
|
||||||
pub(crate) mod wizardry;
|
mod wizardry;
|
||||||
|
|
||||||
|
// Magic bitboard definitions
|
||||||
|
mod moves;
|
||||||
|
pub use moves::*;
|
||||||
|
|
147
src/movegen/moves.rs
Normal file
147
src/movegen/moves.rs
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
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,5 +1,6 @@
|
||||||
use crate::board::{Bitboard, Direction, Square};
|
use crate::board::{Bitboard, Direction, Square};
|
||||||
|
|
||||||
|
/// Compute a bishop's movement given a set of blockers that cannot be moved past.
|
||||||
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
pub fn bishop_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||||
Direction::iter_bishop()
|
Direction::iter_bishop()
|
||||||
.map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers))
|
.map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers))
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::board::{Bitboard, CastleRights, Color, Direction, File, Square};
|
use crate::board::{Bitboard, Direction, Square};
|
||||||
|
|
||||||
// No castling moves included
|
/// Compute a king's movement. No castling moves included
|
||||||
pub fn king_moves(square: Square) -> Bitboard {
|
pub fn king_moves(square: Square) -> Bitboard {
|
||||||
let board = square.into_bitboard();
|
let board = square.into_bitboard();
|
||||||
|
|
||||||
|
@ -9,20 +9,6 @@ pub fn king_moves(square: Square) -> Bitboard {
|
||||||
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs)
|
.fold(Bitboard::EMPTY, |lhs, rhs| lhs | rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn king_castling_moves(color: Color, castle_rights: CastleRights) -> Bitboard {
|
|
||||||
let rank = color.first_rank();
|
|
||||||
|
|
||||||
let king_side_square = Square::new(File::G, rank);
|
|
||||||
let queen_side_square = Square::new(File::C, rank);
|
|
||||||
|
|
||||||
match castle_rights {
|
|
||||||
CastleRights::NoSide => Bitboard::EMPTY,
|
|
||||||
CastleRights::KingSide => king_side_square.into_bitboard(),
|
|
||||||
CastleRights::QueenSide => queen_side_square.into_bitboard(),
|
|
||||||
CastleRights::BothSides => king_side_square | queen_side_square,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -183,40 +169,4 @@ mod test {
|
||||||
| Square::F6
|
| Square::F6
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn castling_moves() {
|
|
||||||
assert_eq!(
|
|
||||||
king_castling_moves(Color::White, CastleRights::NoSide),
|
|
||||||
Bitboard::EMPTY
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
king_castling_moves(Color::Black, CastleRights::NoSide),
|
|
||||||
Bitboard::EMPTY
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
king_castling_moves(Color::White, CastleRights::KingSide),
|
|
||||||
Square::G1.into_bitboard()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
king_castling_moves(Color::Black, CastleRights::KingSide),
|
|
||||||
Square::G8.into_bitboard()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
king_castling_moves(Color::White, CastleRights::QueenSide),
|
|
||||||
Square::C1.into_bitboard()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
king_castling_moves(Color::Black, CastleRights::QueenSide),
|
|
||||||
Square::C8.into_bitboard()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
king_castling_moves(Color::White, CastleRights::BothSides),
|
|
||||||
Square::C1 | Square::G1
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
king_castling_moves(Color::Black, CastleRights::BothSides),
|
|
||||||
Square::C8 | Square::G8
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::board::{Bitboard, Direction, Square};
|
use crate::board::{Bitboard, Direction, Square};
|
||||||
|
|
||||||
|
/// Compute a knight's movement.
|
||||||
pub fn knight_moves(square: Square) -> Bitboard {
|
pub fn knight_moves(square: Square) -> Bitboard {
|
||||||
let board = square.into_bitboard();
|
let board = square.into_bitboard();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
pub mod bishop;
|
pub mod bishop;
|
||||||
|
pub use bishop::*;
|
||||||
|
|
||||||
pub mod king;
|
pub mod king;
|
||||||
|
pub use king::*;
|
||||||
|
|
||||||
pub mod knight;
|
pub mod knight;
|
||||||
|
pub use knight::*;
|
||||||
|
|
||||||
pub mod pawn;
|
pub mod pawn;
|
||||||
|
pub use pawn::*;
|
||||||
|
|
||||||
pub mod rook;
|
pub mod rook;
|
||||||
|
pub use rook::*;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::board::{Bitboard, Color, Direction, Rank, Square};
|
use crate::board::{Bitboard, Color, Direction, Rank, Square};
|
||||||
|
|
||||||
|
/// Compute a pawn's movement given its color, and a set of blockers that cannot be moved past.
|
||||||
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
|
pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard {
|
||||||
if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) {
|
if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) {
|
||||||
return Bitboard::EMPTY;
|
return Bitboard::EMPTY;
|
||||||
|
@ -21,6 +22,7 @@ pub fn pawn_moves(color: Color, square: Square, blockers: Bitboard) -> Bitboard
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Computes the set of squares a pawn can capture, given its color.
|
||||||
pub fn pawn_captures(color: Color, square: Square) -> Bitboard {
|
pub fn pawn_captures(color: Color, square: Square) -> Bitboard {
|
||||||
if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) {
|
if (square.rank() == Rank::First) || (square.rank() == Rank::Eighth) {
|
||||||
return Bitboard::EMPTY;
|
return Bitboard::EMPTY;
|
||||||
|
@ -36,15 +38,6 @@ pub fn pawn_captures(color: Color, square: Square) -> Bitboard {
|
||||||
attack_west | attack_east
|
attack_west | attack_east
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn en_passant_origins(square: Square) -> Bitboard {
|
|
||||||
let board = square.into_bitboard();
|
|
||||||
|
|
||||||
let origin_west = Direction::West.move_board(board);
|
|
||||||
let origin_east = Direction::East.move_board(board);
|
|
||||||
|
|
||||||
origin_west | origin_east
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -120,14 +113,4 @@ mod test {
|
||||||
Square::G6.into_bitboard()
|
Square::G6.into_bitboard()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn en_passant() {
|
|
||||||
assert_eq!(en_passant_origins(Square::A4), Square::B4.into_bitboard());
|
|
||||||
assert_eq!(en_passant_origins(Square::A5), Square::B5.into_bitboard());
|
|
||||||
assert_eq!(en_passant_origins(Square::B4), Square::A4 | Square::C4);
|
|
||||||
assert_eq!(en_passant_origins(Square::B5), Square::A5 | Square::C5);
|
|
||||||
assert_eq!(en_passant_origins(Square::H4), Square::G4.into_bitboard());
|
|
||||||
assert_eq!(en_passant_origins(Square::H5), Square::G5.into_bitboard());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::board::{Bitboard, Direction, Square};
|
use crate::board::{Bitboard, Direction, Square};
|
||||||
|
|
||||||
|
/// Compute a rook's movement given a set of blockers that cannot be moved past.
|
||||||
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
pub fn rook_moves(square: Square, blockers: Bitboard) -> Bitboard {
|
||||||
Direction::iter_rook()
|
Direction::iter_rook()
|
||||||
.map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers))
|
.map(|dir| dir.slide_board_with_blockers(square.into_bitboard(), blockers))
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
use crate::board::{Bitboard, Square};
|
use crate::board::{Bitboard, Square};
|
||||||
use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves};
|
use crate::movegen::naive::{bishop_moves, rook_moves};
|
||||||
use crate::movegen::Magic;
|
use crate::utils::RandGen;
|
||||||
|
|
||||||
use super::mask::{generate_bishop_mask, generate_rook_mask};
|
use super::mask::{generate_bishop_mask, generate_rook_mask};
|
||||||
|
use super::Magic;
|
||||||
|
|
||||||
type MagicGenerationType = (Vec<Magic>, Vec<Bitboard>);
|
type MagicGenerationType = (Vec<Magic>, Vec<Bitboard>);
|
||||||
|
|
||||||
#[allow(unused)] // FIXME: remove when used
|
pub fn generate_bishop_magics(rng: &mut dyn RandGen) -> MagicGenerationType {
|
||||||
pub fn generate_bishop_magics(rng: &mut dyn random::Source) -> MagicGenerationType {
|
|
||||||
generate_magics(rng, generate_bishop_mask, bishop_moves)
|
generate_magics(rng, generate_bishop_mask, bishop_moves)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)] // FIXME: remove when used
|
pub fn generate_rook_magics(rng: &mut dyn RandGen) -> MagicGenerationType {
|
||||||
pub fn generate_rook_magics(rng: &mut dyn random::Source) -> MagicGenerationType {
|
|
||||||
generate_magics(rng, generate_rook_mask, rook_moves)
|
generate_magics(rng, generate_rook_mask, rook_moves)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_magics(
|
fn generate_magics(
|
||||||
rng: &mut dyn random::Source,
|
rng: &mut dyn RandGen,
|
||||||
mask_fn: impl Fn(Square) -> Bitboard,
|
mask_fn: impl Fn(Square) -> Bitboard,
|
||||||
moves_fn: impl Fn(Square, Bitboard) -> Bitboard,
|
moves_fn: impl Fn(Square, Bitboard) -> Bitboard,
|
||||||
) -> MagicGenerationType {
|
) -> MagicGenerationType {
|
||||||
|
@ -26,7 +25,6 @@ fn generate_magics(
|
||||||
|
|
||||||
for square in Square::iter() {
|
for square in Square::iter() {
|
||||||
let mask = mask_fn(square);
|
let mask = mask_fn(square);
|
||||||
let mut candidate: Magic;
|
|
||||||
|
|
||||||
let occupancy_to_moves: Vec<_> = mask
|
let occupancy_to_moves: Vec<_> = mask
|
||||||
.iter_power_set()
|
.iter_power_set()
|
||||||
|
@ -34,7 +32,7 @@ fn generate_magics(
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
'candidate_search: loop {
|
'candidate_search: loop {
|
||||||
candidate = Magic {
|
let mut candidate = Magic {
|
||||||
magic: magic_candidate(rng),
|
magic: magic_candidate(rng),
|
||||||
offset: 0,
|
offset: 0,
|
||||||
mask,
|
mask,
|
||||||
|
@ -45,7 +43,7 @@ fn generate_magics(
|
||||||
for (occupancy, moves) in occupancy_to_moves.iter().cloned() {
|
for (occupancy, moves) in occupancy_to_moves.iter().cloned() {
|
||||||
let index = candidate.get_index(occupancy);
|
let index = candidate.get_index(occupancy);
|
||||||
// Non-constructive collision, try with another candidate
|
// Non-constructive collision, try with another candidate
|
||||||
if candidate_moves[index] != Bitboard::EMPTY && candidate_moves[index] != moves {
|
if !candidate_moves[index].is_empty() && candidate_moves[index] != moves {
|
||||||
continue 'candidate_search;
|
continue 'candidate_search;
|
||||||
}
|
}
|
||||||
candidate_moves[index] = moves;
|
candidate_moves[index] = moves;
|
||||||
|
@ -53,8 +51,8 @@ fn generate_magics(
|
||||||
|
|
||||||
// We have filled all candidate boards, record the correct offset and add the moves
|
// We have filled all candidate boards, record the correct offset and add the moves
|
||||||
candidate.offset = boards.len();
|
candidate.offset = boards.len();
|
||||||
boards.append(&mut candidate_moves);
|
|
||||||
magics.push(candidate);
|
magics.push(candidate);
|
||||||
|
boards.append(&mut candidate_moves);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +60,7 @@ fn generate_magics(
|
||||||
(magics, boards)
|
(magics, boards)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn magic_candidate(rng: &mut dyn random::Source) -> u64 {
|
fn magic_candidate(rng: &mut dyn RandGen) -> u64 {
|
||||||
rng.read_u64() & rng.read_u64() & rng.read_u64()
|
// Few bits makes for better candidates
|
||||||
|
rng.gen() & rng.gen() & rng.gen()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::board::{Bitboard, File, Rank, Square};
|
use crate::board::{Bitboard, File, Rank, Square};
|
||||||
use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves};
|
use crate::movegen::naive::{bishop::bishop_moves, rook::rook_moves};
|
||||||
|
|
||||||
|
/// Compute the relevancy mask for a bishop on a given [Square].
|
||||||
pub fn generate_bishop_mask(square: Square) -> Bitboard {
|
pub fn generate_bishop_mask(square: Square) -> Bitboard {
|
||||||
let rays = bishop_moves(square, Bitboard::EMPTY);
|
let rays = bishop_moves(square, Bitboard::EMPTY);
|
||||||
|
|
||||||
|
@ -12,6 +13,7 @@ pub fn generate_bishop_mask(square: Square) -> Bitboard {
|
||||||
rays - mask
|
rays - mask
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute the relevancy mask for a rook on a given [Square].
|
||||||
pub fn generate_rook_mask(square: Square) -> Bitboard {
|
pub fn generate_rook_mask(square: Square) -> Bitboard {
|
||||||
let rays = rook_moves(square, Bitboard::EMPTY);
|
let rays = rook_moves(square, Bitboard::EMPTY);
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,266 @@
|
||||||
pub(crate) mod generation;
|
mod generation;
|
||||||
|
pub(super) use generation::*;
|
||||||
mod mask;
|
mod mask;
|
||||||
|
|
||||||
|
use crate::board::{Bitboard, Square};
|
||||||
|
|
||||||
|
/// A type representing the magic board indexing a given [crate::board::Square].
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(super) struct Magic {
|
||||||
|
/// Magic number.
|
||||||
|
pub(self) magic: u64,
|
||||||
|
/// Base offset into the magic square table.
|
||||||
|
pub(self) offset: usize,
|
||||||
|
/// Mask to apply to the blocker board before applying the magic.
|
||||||
|
pub(self) mask: Bitboard,
|
||||||
|
/// Length of the resulting mask after applying the magic.
|
||||||
|
pub(self) shift: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Magic {
|
||||||
|
/// Compute the index into the magics database for this set of `blockers`.
|
||||||
|
pub fn get_index(&self, blockers: Bitboard) -> usize {
|
||||||
|
let relevant_occupancy = (blockers & self.mask).0;
|
||||||
|
let base_index = ((relevant_occupancy.wrapping_mul(self.magic)) >> self.shift) as usize;
|
||||||
|
base_index + self.offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type encapsulating a database of [Magic] bitboard moves.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct MagicMoves {
|
||||||
|
magics: Vec<Magic>,
|
||||||
|
moves: Vec<Bitboard>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MagicMoves {
|
||||||
|
/// Initialize a new [MagicMoves] given a matching list of [Magic] and its corresponding moves
|
||||||
|
/// as a [Bitboard].
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This should only be called with values generated by [crate::movegen::wizardry::generation].
|
||||||
|
pub unsafe fn new(magics: Vec<Magic>, moves: Vec<Bitboard>) -> Self {
|
||||||
|
Self { magics, moves }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the set of valid moves for a piece standing on a [Square], given a set of blockers.
|
||||||
|
pub fn query(&self, square: Square, blockers: Bitboard) -> Bitboard {
|
||||||
|
// SAFETY: indices are in range by construction
|
||||||
|
unsafe {
|
||||||
|
let index = self
|
||||||
|
.magics
|
||||||
|
.get_unchecked(square.index())
|
||||||
|
.get_index(blockers);
|
||||||
|
*self.moves.get_unchecked(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region:sourcegen
|
||||||
|
/// A set of magic numbers for bishop move generation.
|
||||||
|
pub(crate) const BISHOP_SEED: [u64; Square::NUM_VARIANTS] = [
|
||||||
|
4634226011293351952,
|
||||||
|
6918109887683821586,
|
||||||
|
76562328660738184,
|
||||||
|
7242919606867744800,
|
||||||
|
13871652069997347969,
|
||||||
|
1171657252671901696,
|
||||||
|
147001475087730752,
|
||||||
|
1752045392763101248,
|
||||||
|
288406435526639744,
|
||||||
|
4612213818402029888,
|
||||||
|
9808848818951710728,
|
||||||
|
9223394181731320840,
|
||||||
|
54047645651435648,
|
||||||
|
9224780030482579712,
|
||||||
|
9049059098626048,
|
||||||
|
1442330840700035221,
|
||||||
|
1126037887157508,
|
||||||
|
1153488887004529665,
|
||||||
|
290485130928332936,
|
||||||
|
9226749771011592258,
|
||||||
|
148636405693678112,
|
||||||
|
2260596997758984,
|
||||||
|
73470481646424336,
|
||||||
|
2341907012146823680,
|
||||||
|
2314955761652335121,
|
||||||
|
2265544246165632,
|
||||||
|
13598764778463296,
|
||||||
|
563087425962496,
|
||||||
|
563087425962048,
|
||||||
|
2163991853573081088,
|
||||||
|
567353402270020,
|
||||||
|
6488844433713538048,
|
||||||
|
288810987011448834,
|
||||||
|
11830884701569344,
|
||||||
|
2747549955031826688,
|
||||||
|
35734665298432,
|
||||||
|
18025943920672800,
|
||||||
|
292892945404789012,
|
||||||
|
1153520472160470528,
|
||||||
|
2260949167801860,
|
||||||
|
155446765112299521,
|
||||||
|
379008324189818944,
|
||||||
|
4616480181217005576,
|
||||||
|
576461027453960704,
|
||||||
|
2450556349601564416,
|
||||||
|
1160556519943569536,
|
||||||
|
4612900059821375552,
|
||||||
|
5477089643453251617,
|
||||||
|
9223532084785594632,
|
||||||
|
2810391870219355200,
|
||||||
|
36594222015453185,
|
||||||
|
4612011546951352320,
|
||||||
|
2392883590201344,
|
||||||
|
1152956706186200064,
|
||||||
|
9009415592510464,
|
||||||
|
81077999302148128,
|
||||||
|
576746627483043968,
|
||||||
|
301267327789056,
|
||||||
|
39586720976896,
|
||||||
|
720878306081243648,
|
||||||
|
9223512777841312257,
|
||||||
|
5764609859566698625,
|
||||||
|
8088544233436348496,
|
||||||
|
4612856276794474560,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A set of magic numbers for rook move generation.
|
||||||
|
pub(crate) const ROOK_SEED: [u64; Square::NUM_VARIANTS] = [
|
||||||
|
180144122814791812,
|
||||||
|
10448386594766422036,
|
||||||
|
9403533616331358856,
|
||||||
|
108095189301858304,
|
||||||
|
72076290316044288,
|
||||||
|
36066182562054145,
|
||||||
|
4647717564258980096,
|
||||||
|
13979173385364603396,
|
||||||
|
4620833992751489152,
|
||||||
|
297800804633419904,
|
||||||
|
578009002156298240,
|
||||||
|
2450099003505838082,
|
||||||
|
1175721046778052864,
|
||||||
|
20406952999780864,
|
||||||
|
1175861788231598592,
|
||||||
|
36169538802827392,
|
||||||
|
288371663414771712,
|
||||||
|
423313050501155,
|
||||||
|
604731668136450,
|
||||||
|
580261214513399808,
|
||||||
|
297661437206136832,
|
||||||
|
1750211954976489600,
|
||||||
|
9020393411186696,
|
||||||
|
9259543770406356001,
|
||||||
|
44532368556032,
|
||||||
|
10376381507760693256,
|
||||||
|
52778707714176,
|
||||||
|
4612829512676149248,
|
||||||
|
1882513444629184528,
|
||||||
|
2369460754144428160,
|
||||||
|
9223380850137104901,
|
||||||
|
2666413562481640036,
|
||||||
|
141012643087392,
|
||||||
|
16735517094631719424,
|
||||||
|
17594358702087,
|
||||||
|
2344264412262574084,
|
||||||
|
422813768878080,
|
||||||
|
1126450811896320,
|
||||||
|
54466576291772936,
|
||||||
|
42784758060548372,
|
||||||
|
292874851780165648,
|
||||||
|
18015364885839937,
|
||||||
|
282644818493504,
|
||||||
|
1184447393488764944,
|
||||||
|
4649966632473477184,
|
||||||
|
563499910594566,
|
||||||
|
17632049496086,
|
||||||
|
18502729728001,
|
||||||
|
140742121013504,
|
||||||
|
9711024139665536,
|
||||||
|
246293205270784,
|
||||||
|
290772515771392256,
|
||||||
|
9230131836490350720,
|
||||||
|
73326432604127360,
|
||||||
|
453174886517643776,
|
||||||
|
2396271245728563712,
|
||||||
|
324259242966026501,
|
||||||
|
288953994406543363,
|
||||||
|
1153557061259362338,
|
||||||
|
40533496293515441,
|
||||||
|
1407392197644307,
|
||||||
|
1729945211427624002,
|
||||||
|
587808330812164100,
|
||||||
|
9511606812128903298,
|
||||||
|
];
|
||||||
|
// endregion:sourcegen
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::utils::SimpleRng;
|
||||||
|
|
||||||
|
fn split_twice<'a>(
|
||||||
|
text: &'a str,
|
||||||
|
start_marker: &str,
|
||||||
|
end_marker: &str,
|
||||||
|
) -> Option<(&'a str, &'a str, &'a str)> {
|
||||||
|
let (prefix, rest) = text.split_once(start_marker)?;
|
||||||
|
let (mid, suffix) = rest.split_once(end_marker)?;
|
||||||
|
Some((prefix, mid, suffix))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn array_string(piece_type: &str, values: &[Magic]) -> String {
|
||||||
|
let inner = || -> Result<String, std::fmt::Error> {
|
||||||
|
let mut res = String::new();
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
&mut res,
|
||||||
|
"/// A set of magic numbers for {} move generation.",
|
||||||
|
piece_type
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
&mut res,
|
||||||
|
"pub(crate) const {}_SEED: [u64; Square::NUM_VARIANTS] = [",
|
||||||
|
piece_type.to_uppercase()
|
||||||
|
)?;
|
||||||
|
for magic in values {
|
||||||
|
writeln!(&mut res, " {},", magic.magic)?;
|
||||||
|
}
|
||||||
|
writeln!(&mut res, "];")?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
};
|
||||||
|
|
||||||
|
inner().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "slow"]
|
||||||
|
// Regenerates the magic bitboard numbers.
|
||||||
|
fn regen_magic_seeds() {
|
||||||
|
// We only care about the magics, the moves can be recomputed at runtime ~cheaply.
|
||||||
|
let (bishop_magics, _) = generate_bishop_magics(&mut SimpleRng::new());
|
||||||
|
let (rook_magics, _) = generate_rook_magics(&mut SimpleRng::new());
|
||||||
|
|
||||||
|
let original_text = std::fs::read_to_string(file!()).unwrap();
|
||||||
|
|
||||||
|
let bishop_array = array_string("bishop", &bishop_magics[..]);
|
||||||
|
let rook_array = array_string("rook", &rook_magics[..]);
|
||||||
|
|
||||||
|
let new_text = {
|
||||||
|
let start_marker = "// region:sourcegen\n";
|
||||||
|
let end_marker = "// endregion:sourcegen\n";
|
||||||
|
let (prefix, _, suffix) =
|
||||||
|
split_twice(&original_text, start_marker, end_marker).unwrap();
|
||||||
|
format!("{prefix}{start_marker}{bishop_array}\n{rook_array}{end_marker}{suffix}")
|
||||||
|
};
|
||||||
|
|
||||||
|
if new_text != original_text {
|
||||||
|
std::fs::write(file!(), new_text).unwrap();
|
||||||
|
panic!("source was not up-to-date")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
|
pub(crate) mod rand;
|
||||||
|
pub(crate) use rand::*;
|
||||||
|
|
||||||
pub mod static_assert;
|
pub mod static_assert;
|
||||||
pub use static_assert::*;
|
pub use static_assert::*;
|
||||||
|
|
49
src/utils/rand.rs
Normal file
49
src/utils/rand.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/// 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,12 +15,9 @@
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! static_assert {
|
macro_rules! static_assert {
|
||||||
($condition:expr) => {
|
($($tt:tt)*) => {
|
||||||
// Based on the latest one in `rustc`'s one before it was [removed].
|
|
||||||
//
|
|
||||||
// [removed]: https://github.com/rust-lang/rust/commit/c2dad1c6b9f9636198d7c561b47a2974f5103f6d
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const _: () = [()][!($condition) as usize];
|
const _: () = assert!($($tt)*);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,41 @@
|
||||||
|
import enum
|
||||||
|
|
||||||
|
import gdb
|
||||||
import gdb.printing
|
import gdb.printing
|
||||||
|
|
||||||
|
|
||||||
|
def optional(constructor, val):
|
||||||
|
try:
|
||||||
|
return constructor(val["Some"]["__0"])
|
||||||
|
except gdb.error:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def print_opt(val):
|
||||||
|
return "(None)" if val is None else str(val)
|
||||||
|
|
||||||
|
|
||||||
class Square(object):
|
class Square(object):
|
||||||
"""
|
"""
|
||||||
Wrapper around GDB's representation of a 'seer::board::square::Square' in
|
Python representation of a 'seer::board::square::Square' raw value.
|
||||||
memory.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
FILES = list(map(lambda n: chr(ord('A') + n), range(8)))
|
FILES = list(map(lambda n: chr(ord("A") + n), range(8)))
|
||||||
RANKS = list(map(lambda n: str(n + 1), range(8)))
|
RANKS = list(map(lambda n: str(n + 1), range(8)))
|
||||||
|
|
||||||
def __init__(self, val):
|
def __init__(self, val):
|
||||||
|
if isinstance(val, Square):
|
||||||
|
val = val._val
|
||||||
self._val = val
|
self._val = val
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gdb(cls, val):
|
||||||
|
return cls(int(val))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file_rank(cls, file, rank):
|
||||||
|
return cls(file * 8 + rank)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.FILES[self.file] + self.RANKS[self.rank]
|
return self.FILES[self.file] + self.RANKS[self.rank]
|
||||||
|
|
||||||
|
@ -23,56 +47,373 @@ class Square(object):
|
||||||
def file(self):
|
def file(self):
|
||||||
return int(self._val) // 8
|
return int(self._val) // 8
|
||||||
|
|
||||||
|
|
||||||
class Bitboard(object):
|
class Bitboard(object):
|
||||||
"""
|
"""
|
||||||
Wrapper around GDB's representation of a 'seer::board::bitboard::Bitboard'
|
Python representation of a 'seer::board::bitboard::Bitboard' raw value.
|
||||||
in memory.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, val):
|
def __init__(self, val):
|
||||||
|
if isinstance(val, Bitboard):
|
||||||
|
val = val._val
|
||||||
self._val = val
|
self._val = val
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gdb(cls, val):
|
||||||
|
return cls(int(val["__0"]))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "[" + ", ".join(map(str, self.squares)) + "]"
|
return "[" + ", ".join(map(str, self.squares)) + "]"
|
||||||
|
|
||||||
|
def at(self, square):
|
||||||
|
return bool(self._val & (1 << square._val))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def squares(self):
|
def squares(self):
|
||||||
n = int(self._val["__0"])
|
n = self._val
|
||||||
while n:
|
while n:
|
||||||
b = n & (~n+1)
|
b = n & (~n + 1)
|
||||||
yield Square(b.bit_length() - 1)
|
yield Square(b.bit_length() - 1)
|
||||||
n ^= b
|
n ^= b
|
||||||
|
|
||||||
|
|
||||||
|
class CastleRights(enum.IntEnum):
|
||||||
|
"""
|
||||||
|
Python representation of a 'seer::board::castle_rights::CastleRights' raw value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Should be kept in sync with the enum in `color.rs`
|
||||||
|
NO_SIDE = 0
|
||||||
|
KING_SIDE = 1
|
||||||
|
QUEEN_SIDE = 2
|
||||||
|
BOTH_SIDES = 3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gdb(cls, val):
|
||||||
|
return cls(int(val))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name.title().replace("_", "")
|
||||||
|
|
||||||
|
|
||||||
|
class Color(enum.IntEnum):
|
||||||
|
"""
|
||||||
|
Python representation of a 'seer::board::color::Color' raw value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Should be kept in sync with the enum in `color.rs`
|
||||||
|
WHITE = 0
|
||||||
|
BLACK = 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gdb(cls, val):
|
||||||
|
return cls(int(val))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name.title()
|
||||||
|
|
||||||
|
|
||||||
|
class File(enum.IntEnum):
|
||||||
|
"""
|
||||||
|
Python representation of a 'seer::board::file::File' raw value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Should be kept in sync with the enum in `file.rs`
|
||||||
|
A = 0
|
||||||
|
B = 1
|
||||||
|
C = 2
|
||||||
|
D = 3
|
||||||
|
E = 4
|
||||||
|
F = 5
|
||||||
|
G = 6
|
||||||
|
H = 7
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gdb(cls, val):
|
||||||
|
return cls(int(val))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name.title()
|
||||||
|
|
||||||
|
|
||||||
|
class Rank(enum.IntEnum):
|
||||||
|
"""
|
||||||
|
Python representation of a 'seer::board::rank::Rank' raw value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Should be kept in sync with the enum in `rank.rs`
|
||||||
|
First = 0
|
||||||
|
Second = 1
|
||||||
|
Third = 2
|
||||||
|
Fourth = 3
|
||||||
|
Fifth = 4
|
||||||
|
Sixth = 5
|
||||||
|
Seventh = 6
|
||||||
|
Eighth = 7
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gdb(cls, val):
|
||||||
|
return cls(int(val))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name.title()
|
||||||
|
|
||||||
|
|
||||||
|
class Piece(enum.IntEnum):
|
||||||
|
"""
|
||||||
|
Python representation of a 'seer::board::piece::Piece' raw value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Should be kept in sync with the enum in `piece.rs`
|
||||||
|
KING = 0
|
||||||
|
QUEEN = 1
|
||||||
|
ROOK = 2
|
||||||
|
BISHOP = 3
|
||||||
|
KNIGHT = 4
|
||||||
|
PAWN = 5
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gdb(cls, val):
|
||||||
|
return cls(int(val))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name.title()
|
||||||
|
|
||||||
|
|
||||||
|
class Move(object):
|
||||||
|
"""
|
||||||
|
Wrapper around GDB's representation of a 'seer::board::move::Move'
|
||||||
|
in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, start, destination, promotion):
|
||||||
|
self._start = Square(start)
|
||||||
|
self._destination = Square(destination)
|
||||||
|
self._promotion = Piece(promotion)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gdb(cls, val):
|
||||||
|
start = Square(int(val["start"]))
|
||||||
|
destination = Square(int(val["destination"]))
|
||||||
|
promotion = optional(Piece.from_gdb, val["promotion"])
|
||||||
|
cls(start, destination, promotion)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start(self):
|
||||||
|
return self._start
|
||||||
|
|
||||||
|
@property
|
||||||
|
def destination(self):
|
||||||
|
return self._destination
|
||||||
|
|
||||||
|
@property
|
||||||
|
def promotion(self):
|
||||||
|
return self._promotion
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
KEYS = [
|
||||||
|
"start",
|
||||||
|
"destination",
|
||||||
|
"promotion",
|
||||||
|
]
|
||||||
|
indent = lambda s: " " + s
|
||||||
|
|
||||||
|
values = [key + ": " + print_opt(getattr(self, key)) + ",\n" for key in KEYS]
|
||||||
|
return "Move{\n" + "".join(map(indent, values)) + "}"
|
||||||
|
|
||||||
|
|
||||||
|
class ChessBoard(object):
|
||||||
|
"""
|
||||||
|
Wrapper around GDB's representation of a 'seer::board::chess_board::ChessBoard'
|
||||||
|
in memory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
piece_occupancy,
|
||||||
|
color_occupancy,
|
||||||
|
castle_rights,
|
||||||
|
half_move_clock,
|
||||||
|
total_plies,
|
||||||
|
side,
|
||||||
|
en_passant,
|
||||||
|
):
|
||||||
|
self._piece_occupancy = list(map(Bitboard, piece_occupancy))
|
||||||
|
self._color_occupancy = list(map(Bitboard, color_occupancy))
|
||||||
|
self._castle_rights = list(map(CastleRights, castle_rights))
|
||||||
|
self._half_move_clock = int(half_move_clock)
|
||||||
|
self._total_plies = int(total_plies)
|
||||||
|
self._side = Color(side)
|
||||||
|
self._en_passant = None if en_passant is None else Square(en_passant)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gdb(cls, val):
|
||||||
|
return cls(
|
||||||
|
[Bitboard.from_gdb(val["piece_occupancy"][p]) for p in Piece],
|
||||||
|
[Bitboard.from_gdb(val["color_occupancy"][c]) for c in Color],
|
||||||
|
[CastleRights.from_gdb(val["castle_rights"][c]) for c in Color],
|
||||||
|
int(val["half_move_clock"]),
|
||||||
|
int(val["total_plies"]),
|
||||||
|
Color.from_gdb(val["side"]),
|
||||||
|
optional(Square.from_gdb, val["en_passant"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def at(self, square):
|
||||||
|
for piece in Piece:
|
||||||
|
if not self._piece_occupancy[piece].at(square):
|
||||||
|
continue
|
||||||
|
for color in Color:
|
||||||
|
if not self._color_occupancy[color].at(square):
|
||||||
|
continue
|
||||||
|
return (piece, color)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def pretty_str(self):
|
||||||
|
def pretty_piece(piece, color):
|
||||||
|
return [
|
||||||
|
("♚", "♔"),
|
||||||
|
("♛", "♕"),
|
||||||
|
("♜", "♖"),
|
||||||
|
("♝", "♗"),
|
||||||
|
("♞", "♘"),
|
||||||
|
("♟", "♙"),
|
||||||
|
][piece][color]
|
||||||
|
|
||||||
|
board = [
|
||||||
|
[self.at(Square.from_file_rank(file, rank)) for file in File]
|
||||||
|
for rank in Rank
|
||||||
|
]
|
||||||
|
|
||||||
|
res = []
|
||||||
|
res.append(" A B C D E F G H ")
|
||||||
|
for n, line in reversed(list(enumerate(board, start=1))):
|
||||||
|
strings = [str(n) + " "]
|
||||||
|
strings.extend(" " if p is None else pretty_piece(*p) for p in line)
|
||||||
|
strings.append(" " + str(n))
|
||||||
|
res.append("|".join(strings))
|
||||||
|
res.append(" A B C D E F G H ")
|
||||||
|
res += [
|
||||||
|
"Half-move clock: " + str(self._half_move_clock),
|
||||||
|
"Total plies: " + str(self._total_plies),
|
||||||
|
"Side to play: " + str(self._side),
|
||||||
|
"En passant: " + print_opt(self._en_passant),
|
||||||
|
]
|
||||||
|
return "\n".join(res)
|
||||||
|
|
||||||
|
|
||||||
class SquarePrinter(object):
|
class SquarePrinter(object):
|
||||||
"Print a seer::board::square::Square"
|
"Print a seer::board::square::Square"
|
||||||
|
|
||||||
def __init__(self, val):
|
def __init__(self, val):
|
||||||
self._val = Square(val)
|
self._val = Square.from_gdb(val)
|
||||||
|
|
||||||
def to_string(self):
|
def to_string(self):
|
||||||
return str(self._val)
|
return str(self._val)
|
||||||
|
|
||||||
def display_hint(self):
|
|
||||||
return 'string'
|
|
||||||
|
|
||||||
class BitboardPrinter(object):
|
class BitboardPrinter(object):
|
||||||
"Print a seer::board::bitboard::Bitboard"
|
"Print a seer::board::bitboard::Bitboard"
|
||||||
|
|
||||||
def __init__(self, val):
|
def __init__(self, val):
|
||||||
self._val = Bitboard(val)
|
self._val = Bitboard.from_gdb(val)
|
||||||
|
|
||||||
def to_string(self):
|
def to_string(self):
|
||||||
return "Bitboard{" + str(self._val)[1:-1] + "}"
|
return "Bitboard{" + str(self._val)[1:-1] + "}"
|
||||||
|
|
||||||
def display_hint(self):
|
|
||||||
return 'string'
|
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 build_pretty_printer():
|
def build_pretty_printer():
|
||||||
pp = gdb.printing.RegexpCollectionPrettyPrinter('seer')
|
pp = gdb.printing.RegexpCollectionPrettyPrinter('seer')
|
||||||
|
|
||||||
pp.add_printer('BigNum', '^seer::board::square::Square$', SquarePrinter)
|
pp.add_printer('Square', '^seer::board::square::Square$', SquarePrinter)
|
||||||
pp.add_printer('BigNum', '^seer::board::bitboard::Bitboard$', BitboardPrinter)
|
pp.add_printer('Bitboard', '^seer::board::bitboard::Bitboard$', BitboardPrinter)
|
||||||
|
pp.add_printer('CastleRights', '^seer::board::castle_rights::CastleRights$', CastleRightsPrinter)
|
||||||
|
pp.add_printer('Color', '^seer::board::color::Color$', ColorPrinter)
|
||||||
|
pp.add_printer('File', '^seer::board::file::File$', FilePrinter)
|
||||||
|
pp.add_printer('Rank', '^seer::board::rank::Rank$', RankPrinter)
|
||||||
|
pp.add_printer('Piece', '^seer::board::piece::Piece$', ColorPrinter)
|
||||||
|
pp.add_printer('Move', '^seer::board::move::Move$', MovePrinter)
|
||||||
|
|
||||||
return pp
|
return pp
|
||||||
|
|
||||||
|
|
||||||
|
def register_commands():
|
||||||
|
PrintBoard()
|
||||||
|
|
||||||
|
|
||||||
gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True)
|
gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printer(), True)
|
||||||
|
register_commands()
|
||||||
|
|
Loading…
Reference in a new issue