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