Compare commits

...
This repository has been archived on 2023-03-04. You can view files and clone it, but cannot push or open issues or pull requests.

26 commits

Author SHA1 Message Date
dece 412b019584 zobrist: add zobrist hashing to nodes 2020-07-01 23:50:10 +02:00
dece 94bd4ec01c readme: update and minor fixes 2020-06-28 19:53:11 +02:00
dece 53a96f8787 movement: thoroughly test unmake with castling 2020-06-25 00:57:10 +02:00
dece 47cc483d9e rules: rework illegal moves detection 2020-06-24 22:19:09 +02:00
dece 54cfd43911 engine: use "vatunode" as debug log command 2020-06-24 22:17:36 +02:00
dece 7ff52bc2b5 board: minor optimisations and test 2020-06-24 22:17:00 +02:00
dece 85d5ba1a62 movement: add unmake 2020-06-24 19:48:44 +02:00
dece 3d5bbb8d2c rules: rework illegal moves detection (WIP) 2020-06-23 00:22:29 +02:00
dece 97444db39c rules: rework move generation for bitboards 2020-06-23 00:22:29 +02:00
dece 5a6893acc7 rules: fix issue with bishop's promoting 2020-06-23 00:22:29 +02:00
dece 9595d0f435 engine: use rays for move generation 2020-06-23 00:22:29 +02:00
dece ce62d3ab3a rules: use generic bitboard move generation
For bishops, knights, rooks and queens that is.
2020-06-23 00:22:29 +02:00
dece 5efdd5407b board: compute move ray bitboards 2020-06-23 00:22:29 +02:00
dece 8b0f4c9255 castling: simplify castle updates 2020-06-20 21:05:54 +02:00
dece b62dd1c076 rules: factorize move/take checks 2020-06-20 19:56:00 +02:00
dece d958e617d0 stats: work with bitboards 2020-06-20 18:46:37 +02:00
dece fce38693bf bitboard: various changes and test fixes 2020-06-20 03:42:58 +02:00
dece b4dd16d87d bitboard: complete moving all code to new structs 2020-06-19 20:27:03 +02:00
dece 5b678dd595 stats: use new structures 2020-06-19 20:00:33 +02:00
dece 4cea0e34e9 rules: use new structures 2020-06-19 19:29:10 +02:00
dece 91e1fbbe21 uci: use new Move struct 2020-06-19 17:46:32 +02:00
dece e4d2b20e23 notation: rename module to fen
Move UCI movement stuff to the Move struct impl.
2020-06-19 17:36:26 +02:00
dece e114138a48 movement: use bitboards 2020-06-19 02:44:33 +02:00
dece 9c6327ec91 node: use bitboards 2020-06-19 02:11:01 +02:00
dece ea58c72436 board: make all board types use bitboards 2020-06-19 02:07:32 +02:00
dece 8485714a24 board: use bitboards struct 2020-06-19 02:07:29 +02:00
23 changed files with 3402 additions and 1358 deletions

80
Cargo.lock generated
View file

@ -1,13 +1,5 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]]
name = "ahash"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "ansi_term" name = "ansi_term"
version = "0.11.0" version = "0.11.0"
@ -31,11 +23,6 @@ name = "bitflags"
version = "1.2.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.33.1" version = "2.33.1"
@ -50,44 +37,6 @@ dependencies = [
"vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "const-random"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "const-random-macro"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dashmap"
version = "3.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ahash 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "getrandom"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.13" version = "0.1.13"
@ -101,20 +50,6 @@ name = "libc"
version = "0.2.71" version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "num_cpus"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"hermit-abi 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@ -138,7 +73,6 @@ name = "vatu"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)",
"dashmap 3.11.4 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -146,11 +80,6 @@ name = "vec_map"
version = "0.8.2" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.8" version = "0.3.8"
@ -171,25 +100,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata] [metadata]
"checksum ahash 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217"
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
"checksum clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" "checksum clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
"checksum const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a"
"checksum const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a"
"checksum dashmap 3.11.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8cfcd41ae02d60edded204341d2798ba519c336c51a37330aa4b98a1128def32"
"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
"checksum hermit-abi 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71" "checksum hermit-abi 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71"
"checksum libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" "checksum libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
"checksum num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
"checksum proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)" = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
"checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" "checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View file

@ -6,4 +6,3 @@ edition = "2018"
[dependencies] [dependencies]
clap = "2.33" clap = "2.33"
dashmap = "3.11"

View file

@ -67,6 +67,8 @@ cp external/lichess-bot/config.yml.example /tmp/vatu-config/config.yml
docker build -f res/docker/Dockerfile -t vatu . docker build -f res/docker/Dockerfile -t vatu .
# Run with the config folder mounted at /config. # Run with the config folder mounted at /config.
docker run -v /tmp/vatu-config:/config -ti vatu docker run -v /tmp/vatu-config:/config -ti vatu
# In the container, use the following command:
python lichess-bot.py --config /config/config.yml
``` ```
@ -74,10 +76,12 @@ docker run -v /tmp/vatu-config:/config -ti vatu
TODO TODO
---- ----
- Support time constraints - [X] Support time constraints
- Proper unmake mechanism instead of allocating boards like there is no tomorrow - [X] Unmake mechanism instead of allocating nodes like there is no tomorrow
- Precompute some pieces moves, maybe - [X] Precompute some pieces moves, maybe (done for knights)
- Transposition table that does not actually slows search down - [ ] Transposition table that does not actually slows search down
- Check Zobrist hashes for previous point - [ ] Check Zobrist hashes for previous point
- Actual bitboard - [X] Actual bitboard
- Multithreading (never) - [ ] Some kind of move ordering could be great
- [ ] Multithreading (never)
- [ ] Avoid 3-fold repetitions when winning

30
res/scripts/gen_king_rays.py Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Pre-compute king ras bitboards for each square."""
TEMPLATE = """\
/// Pre-computed king rays.
pub const KING_RAYS: [Bitboard; 64] = [
{}
];
"""
DIRS = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)]
def bit_pos(square):
return 1 << square
def get_rays():
rays = []
for f in range(8):
for r in range(8):
bitboard = 0
for dir_f, dir_r in DIRS:
ray_f = f + dir_f
ray_r = r + dir_r
if ray_f < 0 or ray_f > 7 or ray_r < 0 or ray_r > 7:
continue
bitboard |= bit_pos(ray_f * 8 + ray_r)
rays.append(" 0b{:064b},".format(bitboard))
return rays
print(TEMPLATE.format("\n".join(get_rays())))

30
res/scripts/gen_knight_rays.py Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Pre-compute knight ray bitboards for each square."""
TEMPLATE = """\
/// Pre-computed knight rays.
pub const KNIGHT_RAYS: [Bitboard; 64] = [
{}
];
"""
DIRS = [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)]
def bit_pos(square):
return 1 << square
def get_rays():
rays = []
for f in range(8):
for r in range(8):
bitboard = 0
for dir_f, dir_r in DIRS:
ray_f = f + dir_f
ray_r = r + dir_r
if ray_f < 0 or ray_f > 7 or ray_r < 0 or ray_r > 7:
continue
bitboard |= bit_pos(ray_f * 8 + ray_r)
rays.append(" 0b{:064b},".format(bitboard))
return rays
print(TEMPLATE.format("\n".join(get_rays())))

View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Pre-compute pawn captures bitboards for each square."""
TEMPLATE = """\
/// Pre-computed pawn captures.
pub const PAWN_CAPTURES: [[Bitboard; 64]; 2] = [
[
{}
],
[
{}
],
];
"""
def bit_pos(square):
return 1 << square
def get_captures():
both_captures = []
for direction in [1, -1]:
captures = []
for f in range(8):
for r in range(8):
bitboard = 0
prog_r = r + direction
if 0 < prog_r < 7:
prev_f = f - 1
if prev_f >= 0:
bitboard |= bit_pos(prev_f * 8 + prog_r)
next_f = f + 1
if next_f <= 7:
bitboard |= bit_pos(next_f * 8 + prog_r)
captures.append(" 0b{:064b},".format(bitboard))
both_captures.append(captures)
return both_captures
CAPTURES = get_captures()
print(TEMPLATE.format("\n".join(CAPTURES[0]), "\n".join(CAPTURES[1])))

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Pre-compute pawn progress bitboards for each square."""
TEMPLATE = """\
/// Pre-computed pawn progresses.
pub const PAWN_PROGRESSES: [[Bitboard; 64]; 2] = [
[
{}
],
[
{}
],
];
"""
def bit_pos(square):
return 1 << square
def get_progresses():
both_progresses = []
for direction in [1, -1]:
progresses = []
for f in range(8):
for r in range(8):
bitboard = 0
if 0 < r < 7:
prog_r = r + direction
bitboard |= bit_pos(f * 8 + prog_r)
if direction == 1 and r == 1:
bitboard |= bit_pos(f * 8 + prog_r + 1)
elif direction == -1 and r == 6:
bitboard |= bit_pos(f * 8 + (prog_r - 1))
progresses.append(" 0b{:064b},".format(bitboard))
both_progresses.append(progresses)
return both_progresses
PROGRESSES = get_progresses()
print(TEMPLATE.format("\n".join(PROGRESSES[0]), "\n".join(PROGRESSES[1])))

5
res/scripts/gen_squares.py Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env python3
for f in range(8):
for r in range(8):
print("pub const {}{}: Pos = {};".format(chr(f + 65), r + 1, f * 8 + r))

56
res/scripts/gen_zobrist_keys.py Executable file
View file

@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Generate Zobrist keys."""
import random
random.seed("vatu stop playing your queen first you have other pieces")
TEMPLATE = """\
pub const ZOBRIST_PIECES: [[[ZobristHash; 64]; 6]; 2] = [
{}
];
pub const ZOBRIST_BLACK_TURN: ZobristHash = {};
pub const ZOBRIST_CASTLE_WH_K: usize = 0;
pub const ZOBRIST_CASTLE_WH_Q: usize = 1;
pub const ZOBRIST_CASTLE_BL_K: usize = 2;
pub const ZOBRIST_CASTLE_BL_Q: usize = 3;
pub const ZOBRIST_CASTLES: [ZobristHash; 4] = [
{}
];
pub const ZOBRIST_EN_PASSANT: [ZobristHash; 8] = [
{}
];
"""
def gen_hash():
return random.getrandbits(64)
def gen_pieces_keys():
pieces_str = ""
for color in range(2):
pieces_str += " [\n"
for piece in range(6):
pieces_str += " [\n"
for square in range(64):
k = gen_hash()
pieces_str += " {},\n".format(k)
pieces_str += " ],\n"
pieces_str += " ],\n"
return pieces_str
print(
TEMPLATE.format(
gen_pieces_keys(),
gen_hash(),
"\n".join([" {},".format(gen_hash()) for _ in range(4)]),
"\n".join([" {},".format(gen_hash()) for _ in range(8)]),
)
)

View file

@ -7,7 +7,6 @@ use crate::board;
use crate::engine; use crate::engine;
use crate::movement::Move; use crate::movement::Move;
use crate::node::Node; use crate::node::Node;
use crate::notation;
use crate::rules; use crate::rules;
use crate::stats; use crate::stats;
@ -104,27 +103,24 @@ impl Analyzer {
if self.debug { if self.debug {
self.log(format!("Analyzing node:\n{}", &self.node)); self.log(format!("Analyzing node:\n{}", &self.node));
let moves = self.node.get_player_moves(true); let moves = self.node.get_player_moves();
self.log(format!("Legal moves: {}", notation::move_list_to_string(&moves))); self.log(format!("Legal moves: {}", Move::list_to_uci_string(&moves)));
self.log(format!("Move time: {}", self.time_limit)); self.log(format!("Move time: {}", self.time_limit));
} }
self.start_time = Some(Instant::now()); self.start_time = Some(Instant::now());
self.current_per_second_timer = Some(Instant::now()); self.current_per_second_timer = Some(Instant::now());
let (max_score, best_move) = self.negamax(&self.node.clone(), MIN_F32, MAX_F32, 0); let (max_score, best_move) = self.negamax(&mut self.node.clone(), MIN_F32, MAX_F32, 0);
if best_move.is_some() { if let Some(m) = best_move {
let log_str = format!( let log_str = format!("Best move {} evaluated {}", m.to_uci_string(), max_score);
"Best move {} evaluated {}",
notation::move_to_string(&best_move.unwrap()), max_score
);
self.log(log_str); self.log(log_str);
self.report_best_move(best_move); self.report_best_move(Some(m));
} else { } else {
// If no best move could be found, checkmate is unavoidable; send the first legal move. // If no best move could be found, checkmate is unavoidable; send the first legal move.
self.log("Checkmate is unavoidable.".to_string()); self.log("Checkmate is unavoidable.".to_string());
let moves = rules::get_player_moves(&self.node.board, &self.node.game_state, true); let moves = rules::get_player_moves(&mut self.node.board, &mut self.node.game_state);
let m = if moves.len() > 0 { Some(moves[0]) } else { None }; let m = if moves.len() > 0 { Some(moves[0].clone()) } else { None };
self.report_best_move(m); self.report_best_move(m);
} }
} }
@ -135,7 +131,7 @@ impl Analyzer {
self.time_limit = if args.move_time != -1 { self.time_limit = if args.move_time != -1 {
args.move_time args.move_time
} else { } else {
let (time, inc) = if board::is_white(self.node.game_state.color) { let (time, inc) = if self.node.game_state.color == board::WHITE {
(args.white_time, args.white_inc) (args.white_time, args.white_inc)
} else { } else {
(args.black_time, args.black_inc) (args.black_time, args.black_inc)
@ -162,7 +158,7 @@ impl Analyzer {
/// lower score bound and `beta` the upper bound. /// lower score bound and `beta` the upper bound.
fn negamax( fn negamax(
&mut self, &mut self,
node: &Node, node: &mut Node,
alpha: f32, alpha: f32,
beta: f32, beta: f32,
depth: u32, depth: u32,
@ -189,18 +185,21 @@ impl Analyzer {
} }
// Get negamax for playable moves. // Get negamax for playable moves.
let moves = node.get_player_moves(true); let mut moves = node.get_player_moves();
let mut alpha = alpha; let mut alpha = alpha;
let mut best_score = MIN_F32; let mut best_score = MIN_F32;
let mut best_move = None; let mut best_move = None;
for m in moves { for m in &mut moves {
let mut sub_node = node.clone(); let hash_changes = node.apply_move(m);
sub_node.apply_move(&m); let result = self.negamax(node, -beta, -alpha, depth + 1);
let result = self.negamax(&sub_node, -beta, -alpha, depth + 1); node.unmake_move(m, hash_changes);
let score = -result.0; let score = -result.0;
if self.debug && depth == 0 {
self.log(format!("move {} evaluated {}", m.to_uci_string(), score));
}
if score > best_score { if score > best_score {
best_score = score; best_score = score;
best_move = Some(m); best_move = Some(m.to_owned());
} }
if best_score > alpha { if best_score > alpha {
alpha = best_score; alpha = best_score;

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,53 @@
//! Castling flags. //! Castling flags.
pub const CASTLING_WH_K: u8 = 0b00000001; use crate::board::{Bitboard, RANK_1, RANK_8};
pub const CASTLING_WH_Q: u8 = 0b00000010;
pub const CASTLING_WH_MASK: u8 = 0b00000011;
pub const CASTLING_BL_K: u8 = 0b00000100;
pub const CASTLING_BL_Q: u8 = 0b00001000;
pub const CASTLING_BL_MASK: u8 = 0b00001100;
pub const CASTLING_K_MASK: u8 = 0b00000101;
pub const CASTLING_Q_MASK: u8 = 0b00001010;
pub const CASTLING_MASK: u8 = 0b00001111;
/// Castling sides parameters. pub type Castle = u8;
pub const CASTLE_WH_K: Castle = 0b00000001;
pub const CASTLE_WH_Q: Castle = 0b00000010;
pub const CASTLE_WH_MASK: Castle = 0b00000011;
pub const CASTLE_BL_K: Castle = 0b00000100;
pub const CASTLE_BL_Q: Castle = 0b00001000;
pub const CASTLE_BL_MASK: Castle = 0b00001100;
pub const CASTLE_K_MASK: Castle = 0b00000101;
pub const CASTLE_Q_MASK: Castle = 0b00001010;
pub const CASTLE_MASK: Castle = 0b00001111;
/// Index castling masks with their color.
pub const CASTLE_MASK_BY_COLOR: [Castle; 2] = [CASTLE_WH_MASK, CASTLE_BL_MASK];
/// Index castling ranks with their color.
pub const CASTLE_RANK_BY_COLOR: [i8; 2] = [RANK_1, RANK_8];
pub const CASTLE_SIDE_K: usize = 0;
pub const CASTLE_SIDE_Q: usize = 1;
pub const NUM_CASTLE_SIDES: usize = 2;
/// Index castling sides using CASTLE_SIDE_K and CASTLE_SIDE_Q.
pub const CASTLE_SIDES: [Castle; 2] = [CASTLE_K_MASK, CASTLE_Q_MASK];
/// Castle paths that must not be under attack, by color and side.
/// ///
/// For both sides, the 3-uple contains files that should be empty /// This includes the original king position, its target square and
/// and not attacked, an optional file that should be empty for /// the square in between.
/// queen-side, and the castling side-mask. pub const CASTLE_LEGALITY_PATHS: [[Bitboard; 2]; 2] = [
pub const CASTLING_SIDES: [([i8; 2], Option<i8>, u8); 2] = [
[([5i8, 6i8], None, CASTLING_K_MASK), ([3i8, 2i8], Some(1i8), CASTLING_Q_MASK)]; 0b00000000_00000001_00000001_00000001_00000000_00000000_00000000_00000000, // White Kside.
0b00000000_00000000_00000000_00000001_00000001_00000001_00000000_00000000, // White Qside.
], [
0b00000000_10000000_10000000_10000000_00000000_00000000_00000000_00000000, // Black Kside.
0b00000000_00000000_00000000_10000000_10000000_10000000_00000000_00000000, // Black Qside.
]
];
/// Castle paths that must be empty.
pub const CASTLE_MOVE_PATHS: [[Bitboard; 2]; 2] = [
[
0b00000000_00000001_00000001_00000000_00000000_00000000_00000000_00000000, // White Kside.
0b00000000_00000000_00000000_00000000_00000001_00000001_00000001_00000000, // White Qside.
], [
0b00000000_10000000_10000000_00000000_00000000_00000000_00000000_00000000, // Black Kside.
0b00000000_00000000_00000000_00000000_10000000_10000000_10000000_00000000, // Black Qside.
]
];

View file

@ -11,9 +11,9 @@ use std::thread;
use crate::analysis; use crate::analysis;
use crate::board; use crate::board;
use crate::castling; use crate::castling;
use crate::movement::{self, Move}; use crate::fen;
use crate::movement::Move;
use crate::node::Node; use crate::node::Node;
use crate::notation;
use crate::uci; use crate::uci;
/// Analysis engine. /// Analysis engine.
@ -62,6 +62,8 @@ pub enum Cmd {
WorkerInfo(Vec<analysis::AnalysisInfo>), WorkerInfo(Vec<analysis::AnalysisInfo>),
/// Send best move found by analysis worker. /// Send best move found by analysis worker.
WorkerBestMove(Option<Move>), WorkerBestMove(Option<Move>),
/// Log current node.
LogNode,
// Commands that can be sent by the engine. // Commands that can be sent by the engine.
@ -123,7 +125,17 @@ impl Engine {
// Workers commands. // Workers commands.
Cmd::Log(s) => self.reply(Cmd::Log(s.to_string())), Cmd::Log(s) => self.reply(Cmd::Log(s.to_string())),
Cmd::WorkerInfo(infos) => self.reply(Cmd::Info(infos.to_vec())), Cmd::WorkerInfo(infos) => self.reply(Cmd::Info(infos.to_vec())),
Cmd::WorkerBestMove(m) => self.reply(Cmd::BestMove(*m)), Cmd::WorkerBestMove(m) => self.reply(Cmd::BestMove(m.clone())),
// Other commands.
Cmd::LogNode => {
let mut s = vec!();
self.node.board.draw_to(&mut s);
self.reply(Cmd::Log(format!(
"Current node:\n{}{}",
String::from_utf8_lossy(&s),
self.node.game_state
)));
}
_ => eprintln!("Not an engine input command: {:?}", cmd), _ => eprintln!("Not an engine input command: {:?}", cmd),
} }
} }
@ -141,29 +153,30 @@ impl Engine {
/// Apply a FEN string to the engine state, replacing it. /// Apply a FEN string to the engine state, replacing it.
/// ///
/// For speed purposes, it assumes values are always valid. /// For speed purposes, it assumes values are always valid.
fn apply_fen(&mut self, fen: &notation::Fen) { fn apply_fen(&mut self, fen: &fen::Fen) {
// Placement. // Placement.
self.node.board = board::new_from_fen(&fen.placement); self.node.board = board::Board::new_from_fen(&fen.placement);
// Color. // Color.
match fen.color.chars().next().unwrap() { match fen.color.chars().next().unwrap() {
'w' => self.node.game_state.color = board::SQ_WH, 'w' => self.node.game_state.color = board::WHITE,
'b' => self.node.game_state.color = board::SQ_BL, 'b' => self.node.game_state.color = board::BLACK,
_ => {} _ => {}
}; };
// Castling. // Castling.
self.node.game_state.castling = 0;
for c in fen.castling.chars() { for c in fen.castling.chars() {
match c { match c {
'K' => self.node.game_state.castling |= castling::CASTLING_WH_K, 'K' => self.node.game_state.castling |= castling::CASTLE_WH_K,
'Q' => self.node.game_state.castling |= castling::CASTLING_WH_Q, 'Q' => self.node.game_state.castling |= castling::CASTLE_WH_Q,
'k' => self.node.game_state.castling |= castling::CASTLING_BL_K, 'k' => self.node.game_state.castling |= castling::CASTLE_BL_K,
'q' => self.node.game_state.castling |= castling::CASTLING_BL_Q, 'q' => self.node.game_state.castling |= castling::CASTLE_BL_Q,
_ => {} _ => {}
} }
} }
// En passant. // En passant.
self.node.game_state.en_passant = match fen.en_passant.as_ref() { self.node.game_state.en_passant = match fen.en_passant.as_ref() {
"-" => None, "-" => None,
p => Some(board::pos(p)), s => Some(board::sq_from_string(s)),
}; };
// Half moves. // Half moves.
self.node.game_state.halfmove = fen.halfmove.parse::<i32>().ok().unwrap(); self.node.game_state.halfmove = fen.halfmove.parse::<i32>().ok().unwrap();
@ -172,13 +185,10 @@ impl Engine {
} }
/// Apply a series of moves to the current node. /// Apply a series of moves to the current node.
fn apply_moves(&mut self, moves: &Vec<Move>) { fn apply_moves(&mut self, moves: &mut Vec<Move>) {
moves.iter().for_each(|m| self.apply_move(m)); for m in moves.iter_mut() {
m.apply_to(&mut self.node.board, &mut self.node.game_state);
} }
/// Apply a move to the current node.
fn apply_move(&mut self, m: &Move) {
movement::apply_move_to(&mut self.node.board, &mut self.node.game_state, m);
} }
/// Start working on board, returning the best move found. /// Start working on board, returning the best move found.
@ -221,11 +231,11 @@ impl Engine {
self.apply_fen(&fen); self.apply_fen(&fen);
}, },
uci::PositionArgs::Startpos => { uci::PositionArgs::Startpos => {
let fen = notation::parse_fen(notation::FEN_START).unwrap(); let fen = fen::parse_fen(fen::FEN_START).unwrap();
self.apply_fen(&fen); self.apply_fen(&fen);
}, },
uci::PositionArgs::Moves(moves) => { uci::PositionArgs::Moves(moves) => {
self.apply_moves(&moves); self.apply_moves(&mut moves.clone());
} }
} }
} }

55
src/fen.rs Normal file
View file

@ -0,0 +1,55 @@
//! Functions to parse FEN strings.
use crate::board;
pub const FEN_START: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
/// FEN notation for positions, split into fields.
#[derive(Debug, Clone)]
pub struct Fen {
pub placement: String,
pub color: String,
pub castling: String,
pub en_passant: String,
pub halfmove: String,
pub fullmove: String,
}
pub fn parse_fen(i: &str) -> Option<Fen> {
let fields: Vec<&str> = i.split_whitespace().collect();
parse_fen_fields(&fields)
}
pub fn parse_fen_fields(fields: &[&str]) -> Option<Fen> {
if fields.len() < 6 {
return None
}
Some(Fen {
placement: fields[0].to_string(),
color: fields[1].to_string(),
castling: fields[2].to_string(),
en_passant: fields[3].to_string(),
halfmove: fields[4].to_string(),
fullmove: fields[5].to_string(),
})
}
pub fn en_passant_to_string(ep: Option<board::Square>) -> String {
ep.and_then(|p| Some(board::sq_to_string(p))).unwrap_or("-".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_fen() {
let fen_start = parse_fen(FEN_START).unwrap();
assert_eq!(&fen_start.placement, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR");
assert_eq!(&fen_start.color, "w");
assert_eq!(&fen_start.castling, "KQkq");
assert_eq!(&fen_start.en_passant, "-");
assert_eq!(&fen_start.halfmove, "0");
assert_eq!(&fen_start.fullmove, "1");
}
}

View file

@ -4,12 +4,14 @@ pub mod analysis;
pub mod board; pub mod board;
pub mod castling; pub mod castling;
pub mod engine; pub mod engine;
pub mod fen;
pub mod movement; pub mod movement;
pub mod node; pub mod node;
pub mod notation; pub mod precomputed;
pub mod rules; pub mod rules;
pub mod stats; pub mod stats;
pub mod uci; pub mod uci;
pub mod zobrist;
fn main() { fn main() {
let args = App::new("Vatu") let args = App::new("Vatu")

View file

@ -1,230 +1,374 @@
//! Move functions along with some castling helpers. //! Move functions along with some castling helpers.
use std::fmt;
use crate::board::*; use crate::board::*;
use crate::castling::*; use crate::castling::*;
use crate::rules; use crate::rules::GameState;
use crate::zobrist::*;
const START_WH_K_POS: Pos = pos("e1");
const START_BL_K_POS: Pos = pos("e8");
/// A movement, with before/after positions and optional promotion. /// A movement, with before/after positions and optional promotion.
pub type Move = (Pos, Pos, Option<u8>); #[derive(Clone, PartialEq)]
pub struct Move {
/// Apply a move `m` to copies to `board` and `game_state`. /// Square from which a piece moves.
/// pub source: Square,
/// Can be used for conveniance but it's better to write in existing /// Square to which a piece moves.
/// instances as often as possible using `apply_move_to`. pub dest: Square,
pub fn apply_move( /// Promotion piece for pawns reaching the last rank.
board: &Board, pub promotion: Option<Piece>,
game_state: &rules::GameState, /// Captured piece, if any.
m: &Move pub capture: Option<Piece>,
) -> (Board, rules::GameState) { /// Castle options before the move. This is set when the move is first applied.
let mut new_board = board.clone(); pub old_castles: Castle,
let mut new_state = game_state.clone();
apply_move_to(&mut new_board, &mut new_state, m);
(new_board, new_state)
} }
/// Update `board` and `game_state` to reflect the move `m`. impl fmt::Debug for Move {
/// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
/// The board is updated with correct piece placement. write!(f, "{}", self.to_uci_string())
/// }
/// The game state is updated with the new player turn and the new }
/// castling options.
pub fn apply_move_to( /// Null move string in UCI exchanges.
board: &mut Board, pub const UCI_NULL_MOVE_STR: &str = "0000";
game_state: &mut rules::GameState,
m: &Move impl Move {
) { /// Build a move from `source` to `dest`, no promotion.
// If a rook is taken, remove its castling option. Needs to be checked before we update board. pub const fn new(source: Square, dest: Square) -> Move {
if m.1 == pos("a1") && get_square(board, &pos("a1")) == SQ_WH_R { Move { source, dest, promotion: None, capture: None, old_castles: 0 }
game_state.castling &= !CASTLING_WH_Q;
} else if m.1 == pos("h1") && get_square(board, &pos("h1")) == SQ_WH_R {
game_state.castling &= !CASTLING_WH_K;
} else if m.1 == pos("a8") && get_square(board, &pos("a8")) == SQ_BL_R {
game_state.castling &= !CASTLING_BL_Q;
} else if m.1 == pos("h8") && get_square(board, &pos("h8")) == SQ_BL_R {
game_state.castling &= !CASTLING_BL_K;
} }
// Update board and game state. /// Build a move from `source` to `dest`, with a promotion.
apply_move_to_board(board, m); pub const fn new_promotion(source: Square, dest: Square, promotion: Piece) -> Move {
Move { source, dest, promotion: Some(promotion), capture: None, old_castles: 0 }
}
/// Apply this move to `board` and `game_state`.
///
/// Set automatic queen promotion for pawns, register captured
/// pieces and castle options.
pub fn apply_to(&mut self, board: &mut Board, game_state: &mut GameState) -> ZobristHash {
// Changes to a Zobrist hash due to this move.
let mut changes: ZobristHash = 0;
// Save current castling options to unmake later.
self.old_castles = game_state.castling;
let piece = board.get_piece_on(self.source);
if piece == KING {
// Handle king castling.
if let Some(castle) = self.get_castle() {
match castle {
CASTLE_WH_K => {
board.move_square(E1, G1);
board.move_square(H1, F1);
game_state.castling &= !CASTLE_WH_MASK;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_K];
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_Q];
}
CASTLE_WH_Q => {
board.move_square(E1, C1);
board.move_square(A1, D1);
game_state.castling &= !CASTLE_WH_MASK;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_K];
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_Q];
}
CASTLE_BL_K => {
board.move_square(E8, G8);
board.move_square(H8, F8);
game_state.castling &= !CASTLE_BL_MASK;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_K];
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_Q];
}
CASTLE_BL_Q => {
board.move_square(E8, C8);
board.move_square(A8, D8);
game_state.castling &= !CASTLE_BL_MASK;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_K];
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_Q];
}
_ => { panic!("Invalid castle.") }
}
game_state.color = opposite(game_state.color); game_state.color = opposite(game_state.color);
changes ^= ZOBRIST_BLACK_TURN;
return changes
} else {
// If the king moved from starting square, remove it from castling options.
if self.source == E1 {
game_state.castling &= !CASTLE_WH_MASK;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_K];
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_Q];
}
else if self.source == E8 {
game_state.castling &= !CASTLE_BL_MASK;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_K];
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_Q];
}
}
}
// If the move is a castle, remove it from castling options. // Record captured piece if any.
if let Some(castle) = get_castle(m) { if !board.is_empty(self.dest) {
let captured_piece = board.get_piece_on(self.dest);
changes ^= get_piece_hash(opposite(game_state.color), captured_piece, self.dest);
self.capture = Some(captured_piece);
}
// Move the piece and apply promotion if any.
board.move_square(self.source, self.dest);
changes ^= get_piece_hash(game_state.color, piece, self.source);
if let Some(promotion_piece) = self.promotion {
board.set_piece(self.dest, PAWN, promotion_piece);
changes ^= get_piece_hash(game_state.color, promotion_piece, self.dest);
} else {
changes ^= get_piece_hash(game_state.color, piece, self.dest);
}
// If a rook moved, remove the castle side.
if self.source == A1 || self.dest == A1 {
game_state.castling &= !CASTLE_WH_Q;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_Q];
}
else if self.source == H1 || self.dest == H1 {
game_state.castling &= !CASTLE_WH_K;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_K];
}
else if self.source == A8 || self.dest == A8 {
game_state.castling &= !CASTLE_BL_Q;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_Q];
}
else if self.source == H8 || self.dest == H8 {
game_state.castling &= !CASTLE_BL_K;
changes ^= ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_K];
}
// Finally, switch to the opposing player in the game state.
game_state.color = opposite(game_state.color);
changes ^= ZOBRIST_BLACK_TURN;
// Return changes as a Zobrist hash.
changes
}
/// Unmake a move.
pub fn unmake(&self, board: &mut Board, game_state: &mut GameState) {
// Always restore previous castle options.
game_state.castling = self.old_castles;
// If the move is a castle, unmake it properly.
let piece = board.get_piece_on(self.dest);
if piece == KING {
if let Some(castle) = self.get_castle() {
match castle { match castle {
CASTLING_WH_K | CASTLING_WH_Q => game_state.castling &= !CASTLING_WH_MASK, CASTLE_WH_K => { board.move_square(G1, E1); board.move_square(F1, H1); }
CASTLING_BL_K | CASTLING_BL_Q => game_state.castling &= !CASTLING_BL_MASK, CASTLE_WH_Q => { board.move_square(C1, E1); board.move_square(D1, A1); }
_ => {} CASTLE_BL_K => { board.move_square(G8, E8); board.move_square(F8, H8); }
}; CASTLE_BL_Q => { board.move_square(C8, E8); board.move_square(D8, A8); }
_ => { panic!("Invalid castle.") }
} }
// Else, check if the king or a rook moved to update castling options. game_state.color = opposite(game_state.color);
else { return
let piece = get_square(board, &m.1);
if is_white(piece) && game_state.castling & CASTLING_WH_MASK != 0 {
match get_type(piece) {
SQ_K => {
if m.0 == pos("e1") {
game_state.castling &= !CASTLING_WH_MASK;
} }
} }
SQ_R => {
if m.0 == pos("a1") {
game_state.castling &= !CASTLING_WH_Q;
} else if m.0 == pos("h1") {
game_state.castling &= !CASTLING_WH_K;
}
}
_ => {}
}
} else if is_black(piece) && game_state.castling & CASTLING_BL_MASK != 0 {
match get_type(piece) {
SQ_K => {
if m.0 == pos("e8") {
game_state.castling &= !CASTLING_BL_MASK;
}
}
SQ_R => {
if m.0 == pos("a8") {
game_state.castling &= !CASTLING_BL_Q;
} else if m.0 == pos("h8") {
game_state.castling &= !CASTLING_BL_K;
}
}
_ => {}
}
}
}
}
/// Apply a move `m` into `board`. // Move the piece back.
pub fn apply_move_to_board(board: &mut Board, m: &Move) { board.move_square(self.dest, self.source);
if let Some(castle) = get_castle(m) {
// Cancel the promotion.
if let Some(piece) = self.promotion {
board.set_piece(self.source, piece, PAWN);
}
// Restore captured piece.
if let Some(piece) = self.capture {
board.set_square(self.dest, game_state.color, piece);
}
// And switch back to previous player.
game_state.color = opposite(game_state.color);
}
/// Get the corresponding castling flag for this move.
pub fn get_castle(&self) -> Option<Castle> {
match (self.source, self.dest) {
(E1, C1) => Some(CASTLE_WH_Q),
(E1, G1) => Some(CASTLE_WH_K),
(E8, C8) => Some(CASTLE_BL_Q),
(E8, G8) => Some(CASTLE_BL_K),
_ => None,
}
}
/// Get the move for this castle.
pub fn get_castle_move(castle: u8) -> Move {
match castle { match castle {
CASTLING_WH_K => { CASTLE_WH_Q => Move::new(E1, C1),
move_piece(board, &START_WH_K_POS, &pos("g1")); CASTLE_WH_K => Move::new(E1, G1),
move_piece(board, &pos("h1"), &pos("f1")); CASTLE_BL_Q => Move::new(E8, C8),
} CASTLE_BL_K => Move::new(E8, G8),
CASTLING_WH_Q => {
move_piece(board, &START_WH_K_POS, &pos("c1"));
move_piece(board, &pos("a1"), &pos("d1"));
}
CASTLING_BL_K => {
move_piece(board, &START_BL_K_POS, &pos("g8"));
move_piece(board, &pos("h8"), &pos("f8"));
}
CASTLING_BL_Q => {
move_piece(board, &START_BL_K_POS, &pos("c8"));
move_piece(board, &pos("a8"), &pos("d8"));
}
_ => {}
}
} else {
move_piece(board, &m.0, &m.1);
if let Some(prom_type) = m.2 {
let color = get_color(get_square(board, &m.1));
set_square(board, &m.1, color|prom_type);
}
}
}
/// Get the corresponding castling flag for this move.
pub fn get_castle(m: &Move) -> Option<u8> {
if m.0 == pos("e1") {
if m.1 == pos("c1") {
Some(CASTLING_WH_Q)
} else if m.1 == pos("g1") {
Some(CASTLING_WH_K)
} else {
None
}
} else if m.0 == pos("e8") {
if m.1 == pos("c8") {
Some(CASTLING_BL_Q)
} else if m.1 == pos("g8") {
Some(CASTLING_BL_K)
} else {
None
}
} else {
None
}
}
/// Get the move for this castle.
pub fn get_castle_move(castle: u8) -> Move {
match castle {
CASTLING_WH_Q => (pos("e1"), pos("c1"), None),
CASTLING_WH_K => (pos("e1"), pos("g1"), None),
CASTLING_BL_Q => (pos("e8"), pos("c8"), None),
CASTLING_BL_K => (pos("e8"), pos("g8"), None),
_ => panic!("Illegal castling requested: {:08b}", castle), _ => panic!("Illegal castling requested: {:08b}", castle),
} }
}
/// Parse an UCI move algebraic notation string to a Move.
pub fn from_uci_string(m_str: &str) -> Move {
Move {
source: sq_from_string(&m_str[0..2]),
dest: sq_from_string(&m_str[2..4]),
promotion: if m_str.len() == 5 {
Some(match m_str.as_bytes()[4] {
b'b' => BISHOP,
b'n' => KNIGHT,
b'r' => ROOK,
b'q' => QUEEN,
_ => panic!("What is the opponent doing? This is illegal, I'm out."),
})
} else {
None
},
capture: None,
old_castles: 0,
}
}
/// Create a string containing the UCI algebraic notation of this move.
pub fn to_uci_string(&self) -> String {
let mut move_string = String::new();
move_string.push_str(&sq_to_string(self.source));
move_string.push_str(&sq_to_string(self.dest));
if let Some(piece) = self.promotion {
move_string.push(match piece {
QUEEN => 'q',
BISHOP => 'b',
KNIGHT => 'n',
ROOK => 'r',
_ => panic!("What are you doing? Promote to a legal piece.")
});
}
move_string
}
/// Debug only: create a space-separated string of moves.
pub(crate) fn list_to_uci_string(moves: &Vec<Move>) -> String {
moves.iter().map(|m| m.to_uci_string()).collect::<Vec<_>>().join(" ")
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::notation::parse_move;
#[test] #[test]
fn test_apply_move_to_board() { fn test_apply_to_board() {
let mut b = new_empty(); let mut b = Board::new_empty();
let mut gs = GameState::new();
// Put 2 enemy knights on board. // Put 2 enemy knights on board.
set_square(&mut b, &pos("d4"), SQ_WH_N); b.set_square(D4, WHITE, KNIGHT);
set_square(&mut b, &pos("f4"), SQ_BL_N); b.set_square(F4, BLACK, KNIGHT);
// Move white knight in a position attacked by black knight. // Move white knight in a position attacked by black knight.
apply_move_to_board(&mut b, &(pos("d4"), pos("e6"), None)); let mut m = Move::new(D4, E6);
assert_eq!(get_square(&b, &pos("d4")), SQ_E); m.apply_to(&mut b, &mut gs);
assert_eq!(get_square(&b, &pos("e6")), SQ_WH_N); assert!(b.is_empty(D4));
assert_eq!(num_pieces(&b), 2); assert_eq!(b.get_color_on(E6), WHITE);
assert_eq!(b.get_piece_on(E6), KNIGHT);
assert_eq!(count_bits(b.combined()), 2);
assert!(m.capture.is_none());
// Sack it with black knight // Sack it with black knight
apply_move_to_board(&mut b, &(pos("f4"), pos("e6"), None)); let mut m = Move::new(F4, E6);
assert_eq!(get_square(&b, &pos("e6")), SQ_BL_N); m.apply_to(&mut b, &mut gs);
assert_eq!(num_pieces(&b), 1); assert_eq!(b.get_color_on(E6), BLACK);
assert_eq!(b.get_piece_on(E6), KNIGHT);
assert_eq!(count_bits(b.combined()), 1);
assert_eq!(m.capture.unwrap(), KNIGHT);
} }
#[test] #[test]
fn test_apply_move_to_castling() { fn test_apply_to_castling() {
let mut b = new(); let mut b = Board::new();
let mut gs = rules::GameState::new(); let mut gs = GameState::new();
assert_eq!(gs.castling, CASTLING_MASK); assert_eq!(gs.castling, CASTLE_MASK);
// On a starting board, start by making place for all castles. // On a starting board, start by making place for all castles.
clear_square(&mut b, &pos("b1")); b.clear_square(B1, WHITE, KNIGHT);
clear_square(&mut b, &pos("c1")); b.clear_square(C1, WHITE, BISHOP);
clear_square(&mut b, &pos("d1")); b.clear_square(D1, WHITE, QUEEN);
clear_square(&mut b, &pos("f1")); b.clear_square(F1, WHITE, BISHOP);
clear_square(&mut b, &pos("g1")); b.clear_square(G1, WHITE, KNIGHT);
clear_square(&mut b, &pos("b8")); b.clear_square(B8, BLACK, KNIGHT);
clear_square(&mut b, &pos("c8")); b.clear_square(C8, BLACK, BISHOP);
clear_square(&mut b, &pos("d8")); b.clear_square(D8, BLACK, QUEEN);
clear_square(&mut b, &pos("f8")); b.clear_square(F8, BLACK, BISHOP);
clear_square(&mut b, &pos("g8")); b.clear_square(G8, BLACK, KNIGHT);
// White queen-side castling. // White queen-side castling.
apply_move_to(&mut b, &mut gs, &parse_move("e1c1")); Move::new(E1, C1).apply_to(&mut b, &mut gs);
assert!(is_piece(get_square(&b, &pos("c1")), SQ_WH_K)); assert_eq!(b.get_color_on(C1), WHITE);
assert!(is_piece(get_square(&b, &pos("d1")), SQ_WH_R)); assert_eq!(b.get_piece_on(C1), KING);
assert!(is_empty(&b, &pos("a1"))); assert_eq!(b.get_color_on(D1), WHITE);
assert!(is_empty(&b, &pos("e1"))); assert_eq!(b.get_piece_on(D1), ROOK);
assert_eq!(gs.castling, CASTLING_BL_MASK); assert!(b.is_empty(A1));
assert!(b.is_empty(E1));
assert_eq!(gs.castling, CASTLE_BL_MASK);
// Black king-side castling. // Black king-side castling.
apply_move_to(&mut b, &mut gs, &parse_move("e8g8")); Move::new(E8, G8).apply_to(&mut b, &mut gs);
assert!(is_piece(get_square(&b, &pos("g8")), SQ_BL_K)); assert_eq!(b.get_color_on(G8), BLACK);
assert!(is_piece(get_square(&b, &pos("f8")), SQ_BL_R)); assert_eq!(b.get_piece_on(G8), KING);
assert!(is_empty(&b, &pos("h8"))); assert_eq!(b.get_color_on(F8), BLACK);
assert!(is_empty(&b, &pos("e8"))); assert_eq!(b.get_piece_on(F8), ROOK);
assert!(b.is_empty(H8));
assert!(b.is_empty(E8));
// At the end, no more castling options for both sides.
assert_eq!(gs.castling, 0); assert_eq!(gs.castling, 0);
} }
#[test]
fn test_unmake() {
let mut b = Board::new_empty();
let mut gs = GameState::new();
// On unmaking a move, the white pawn is back to its original square.
b.set_square(D4, WHITE, PAWN);
let mut m = Move::new(D4, D5);
m.apply_to(&mut b, &mut gs);
m.unmake(&mut b, &mut gs);
assert!(b.is_empty(D5));
assert_eq!(b.get_color_on(D4), WHITE);
assert_eq!(b.get_piece_on(D4), PAWN);
// Castle options should be properly unmade.
b.set_square(E1, WHITE, KING);
b.set_square(H1, WHITE, ROOK);
let mut m = Move::new(E1, G1);
m.apply_to(&mut b, &mut gs);
assert!(!b.is_empty(G1));
assert!(!b.is_empty(F1));
assert_eq!(gs.castling, CASTLE_MASK ^ CASTLE_WH_MASK);
m.unmake(&mut b, &mut gs);
assert!(b.is_empty(G1));
assert!(b.is_empty(F1));
assert_eq!(b.get_piece_on(E1), KING);
assert_eq!(b.get_piece_on(H1), ROOK);
assert_eq!(gs.castling, CASTLE_MASK);
}
#[test] #[test]
fn test_get_castle() { fn test_get_castle() {
assert_eq!(get_castle(&parse_move("e1c1")), Some(CASTLING_WH_Q)); assert_eq!(Move::new(E1, C1).get_castle(), Some(CASTLE_WH_Q));
assert_eq!(get_castle(&parse_move("e1g1")), Some(CASTLING_WH_K)); assert_eq!(Move::new(E1, G1).get_castle(), Some(CASTLE_WH_K));
assert_eq!(get_castle(&parse_move("e8c8")), Some(CASTLING_BL_Q)); assert_eq!(Move::new(E8, C8).get_castle(), Some(CASTLE_BL_Q));
assert_eq!(get_castle(&parse_move("e8g8")), Some(CASTLING_BL_K)); assert_eq!(Move::new(E8, G8).get_castle(), Some(CASTLE_BL_K));
assert_eq!(get_castle(&parse_move("d2d4")), None); assert_eq!(Move::new(D2, D4).get_castle(), None);
}
#[test]
fn test_to_uci_string() {
assert_eq!(Move::new(A1, D4).to_uci_string(), "a1d4");
assert_eq!(Move::new(H8, A8).to_uci_string(), "h8a8");
assert_eq!(Move::new_promotion(H7, H8, QUEEN).to_uci_string(), "h7h8q");
assert_eq!(Move::new_promotion(H7, H8, KNIGHT).to_uci_string(), "h7h8n");
}
#[test]
fn test_from_uci_string() {
assert_eq!(Move::from_uci_string("a1d4"), Move::new(A1, D4));
assert_eq!(Move::from_uci_string("a7a8q"), Move::new_promotion(A7, A8, QUEEN));
assert_eq!(Move::from_uci_string("a7a8r"), Move::new_promotion(A7, A8, ROOK));
} }
} }

View file

@ -1,42 +1,52 @@
use std::fmt; use std::fmt;
use std::hash::{Hash, Hasher};
use crate::board; use crate::board;
use crate::movement::{self, Move}; use crate::movement::Move;
use crate::rules; use crate::rules;
use crate::stats; use crate::stats;
use crate::zobrist;
/// Analysis node: a board along with the game state. /// Analysis node: a board along with the game state.
#[derive(Clone)] #[derive(Clone, PartialEq)]
pub struct Node { pub struct Node {
/// Board for this node. /// Board for this node.
pub board: board::Board, pub board: board::Board,
/// Game state. /// Game state.
pub game_state: rules::GameState, pub game_state: rules::GameState,
/// Zobrist hash of the node.
pub hash: zobrist::ZobristHash,
} }
impl Node { impl Node {
/// Create a new node for an empty board and a new game state. /// Create a new node for an empty board and a new game state.
pub fn new() -> Node { pub fn new() -> Node {
Node { Node {
board: board::new_empty(), board: board::Board::new_empty(),
game_state: rules::GameState::new(), game_state: rules::GameState::new(),
hash: zobrist::get_new_game_hash(),
} }
} }
/// Apply a move to this node. /// Apply a move to this node.
pub fn apply_move(&mut self, m: &Move) { pub fn apply_move(&mut self, m: &mut Move) -> zobrist::ZobristHash {
movement::apply_move_to(&mut self.board, &mut self.game_state, m); let changes = m.apply_to(&mut self.board, &mut self.game_state);
self.hash ^= changes;
changes
}
pub fn unmake_move(&mut self, m: &Move, changes: zobrist::ZobristHash) {
m.unmake(&mut self.board, &mut self.game_state);
self.hash ^= changes;
} }
/// Return player moves from this node. /// Return player moves from this node.
pub fn get_player_moves(&self, commit: bool) -> Vec<Move> { pub fn get_player_moves(&mut self) -> Vec<Move> {
rules::get_player_moves(&self.board, &self.game_state, commit) rules::get_player_moves(&mut self.board, &mut self.game_state)
} }
/// Compute stats for both players for this node. /// Compute stats for both players for this node.
pub fn compute_stats(&self) -> (stats::BoardStats, stats::BoardStats) { pub fn compute_stats(&mut self) -> (stats::BoardStats, stats::BoardStats) {
stats::compute_stats(&self.board, &self.game_state) stats::BoardStats::new_from(&mut self.board, &mut self.game_state)
} }
} }
@ -53,29 +63,8 @@ impl fmt::Debug for Node {
impl fmt::Display for Node { impl fmt::Display for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = vec!(); let mut s = vec!();
board::draw(&self.board, &mut s); self.board.draw_to(&mut s);
let board_drawing = String::from_utf8_lossy(&s).to_string(); let board_drawing = String::from_utf8_lossy(&s).to_string();
write!( write!(f, "{}{}", board_drawing, self.game_state)
f,
"* Board:\n{}\n\
* Game state:\n{}",
board_drawing, self.game_state
)
}
}
impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool {
self.board.iter().zip(other.board.iter()).all(|(a, b)| a == b)
&& self.game_state == other.game_state
}
}
impl Eq for Node {}
impl Hash for Node {
fn hash<H: Hasher>(&self, state: &mut H) {
self.board.iter().for_each(|square| state.write_u8(*square));
self.game_state.hash(state);
} }
} }

View file

@ -1,111 +0,0 @@
//! Functions using various notations.
use crate::board::*;
use crate::movement::Move;
pub const NULL_MOVE: &str = "0000";
/// Create a string containing the UCI algebraic notation of this move.
pub fn move_to_string(m: &Move) -> String {
let mut move_string = String::new();
move_string.push_str(&pos_string(&m.0));
move_string.push_str(&pos_string(&m.1));
if let Some(prom) = m.2 {
move_string.push(match prom {
SQ_Q => 'q',
SQ_B => 'b',
SQ_N => 'n',
SQ_R => 'r',
_ => panic!("What are you doing? Promote to a legal piece.")
});
}
move_string
}
/// Parse an UCI move algebraic notation string to a Move.
pub fn parse_move(m_str: &str) -> Move {
let prom = if m_str.len() == 5 {
Some(match m_str.as_bytes()[4] {
b'b' => SQ_B,
b'n' => SQ_N,
b'r' => SQ_R,
b'q' => SQ_Q,
_ => panic!("What is the opponent doing? This is illegal, I'm out."),
})
} else {
None
};
(pos(&m_str[0..2]), pos(&m_str[2..4]), prom)
}
/// Create a space-separated string of moves. Used for debugging.
pub fn move_list_to_string(moves: &Vec<Move>) -> String {
moves.iter().map(|m| move_to_string(m)).collect::<Vec<_>>().join(" ")
}
pub const FEN_START: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
/// FEN notation for positions, split into fields.
#[derive(Debug, Clone)]
pub struct Fen {
pub placement: String,
pub color: String,
pub castling: String,
pub en_passant: String,
pub halfmove: String,
pub fullmove: String,
}
pub fn parse_fen(i: &str) -> Option<Fen> {
let fields: Vec<&str> = i.split_whitespace().collect();
parse_fen_fields(&fields)
}
pub fn parse_fen_fields(fields: &[&str]) -> Option<Fen> {
if fields.len() < 6 {
return None
}
Some(Fen {
placement: fields[0].to_string(),
color: fields[1].to_string(),
castling: fields[2].to_string(),
en_passant: fields[3].to_string(),
halfmove: fields[4].to_string(),
fullmove: fields[5].to_string(),
})
}
pub fn en_passant_to_string(ep: Option<Pos>) -> String {
ep.and_then(|p| Some(pos_string(&p))).unwrap_or("-".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_move_to_string() {
assert_eq!(move_to_string(&((0, 0), (3, 3), None)), "a1d4");
assert_eq!(move_to_string(&((7, 7), (0, 7), None)), "h8a8");
assert_eq!(move_to_string(&((7, 6), (7, 7), Some(SQ_Q))), "h7h8q");
assert_eq!(move_to_string(&((7, 6), (7, 7), Some(SQ_N))), "h7h8n");
}
#[test]
fn test_parse_move() {
assert_eq!(parse_move("a1d4"), ((0, 0), (3, 3), None));
assert_eq!(parse_move("a7a8q"), ((0, 6), (0, 7), Some(SQ_Q)));
assert_eq!(parse_move("a7a8r"), ((0, 6), (0, 7), Some(SQ_R)));
}
#[test]
fn test_parse_fen() {
let fen_start = parse_fen(FEN_START).unwrap();
assert_eq!(&fen_start.placement, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR");
assert_eq!(&fen_start.color, "w");
assert_eq!(&fen_start.castling, "KQkq");
assert_eq!(&fen_start.en_passant, "-");
assert_eq!(&fen_start.halfmove, "0");
assert_eq!(&fen_start.fullmove, "1");
}
}

1308
src/precomputed.rs Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
//! Board statistics used for heuristics. //! Board statistics used for heuristics.
use crate::board::*; use crate::board::*;
use crate::rules; use crate::rules::{GameState, get_player_moves};
/// Storage for board pieces stats. /// Storage for board pieces stats.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -27,6 +27,20 @@ impl BoardStats {
} }
} }
/// Create two new BoardStats objects from the board, for both sides.
///
/// The playing color will have its stats filled in the first
/// BoardStats object, its opponent in the second.
pub fn new_from(board: &mut Board, game_state: &mut GameState) -> (BoardStats, BoardStats) {
let mut stats = (BoardStats::new(), BoardStats::new());
let mut gs = game_state.clone();
stats.0.compute(board, &mut gs);
gs.color = opposite(gs.color);
stats.1.compute(board, &mut gs);
stats
}
/// Reset all stats to 0.
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.num_pawns = 0; self.num_pawns = 0;
self.num_bishops = 0; self.num_bishops = 0;
@ -39,6 +53,88 @@ impl BoardStats {
self.num_isolated_pawns = 0; self.num_isolated_pawns = 0;
self.mobility = 0; self.mobility = 0;
} }
/// Fill `stats` from given `board` and `game_state`.
///
/// Only the current playing side stats are created,
/// prepare the game_state accordingly.
pub fn compute(&mut self, board: &mut Board, game_state: &mut GameState) {
self.reset();
let color = game_state.color;
// Compute mobility for all pieces.
self.mobility = get_player_moves(board, game_state).len() as i32;
// Compute amount of each piece.
for file in 0..8 {
for rank in 0..8 {
let square = sq(file, rank);
if board.is_empty(square) || board.get_color_on(square) != color {
continue
}
match board.get_piece_on(square) {
ROOK => self.num_rooks += 1,
KNIGHT => self.num_knights += 1,
BISHOP => self.num_bishops += 1,
QUEEN => self.num_queens += 1,
KING => self.num_kings += 1,
PAWN => {
self.num_pawns += 1;
let pawn_bb = board.by_color_and_piece(color, PAWN);
// Check for doubled pawns.
let file_bb = FILES[file as usize];
if (pawn_bb ^ bit_pos(square)) & file_bb != 0 {
self.num_doubled_pawns += 1;
}
// Check for isolated and backward pawns.
let (iso_on_prev_file, bw_on_prev_file) = if file > FILE_A {
self.find_isolated_and_backward(pawn_bb, square, color, file - 1)
} else {
(true, true)
};
let (iso_on_next_file, bw_on_next_file) = if file < FILE_H {
self.find_isolated_and_backward(pawn_bb, square, color, file + 1)
} else {
(true, true)
};
if iso_on_prev_file && iso_on_next_file {
self.num_isolated_pawns += 1;
}
if bw_on_prev_file && bw_on_next_file {
self.num_backward_pawns += 1;
}
},
_ => {}
}
}
}
}
/// Find isolated and backward pawns from `square` perspective.
///
/// `bb` is the bitboard of `color`. `square` is only used to have
/// the reference rank. `file` is the file to inspect. To detect
/// isolated and backward pawns, `bb` should be the bitboard of
/// pawns of `color`.
fn find_isolated_and_backward(
&mut self,
bb: Bitboard,
square: Square,
color: Color,
file: i8
) -> (bool, bool) {
if bb & FILES[file as usize] == 0 {
// If the piece is isolated for this file, it's backward as well.
(true, true)
} else {
let backward_file_bb = if color == WHITE {
before_on_file(file, sq_rank(square)) | bit_pos(sq(file, sq_rank(square)))
} else {
after_on_file(file, sq_rank(square)) | bit_pos(sq(file, sq_rank(square)))
};
(false, bb & backward_file_bb == 0)
}
}
} }
impl std::fmt::Display for BoardStats { impl std::fmt::Display for BoardStats {
@ -54,126 +150,6 @@ impl std::fmt::Display for BoardStats {
} }
} }
/// Create two new BoardStats objects from the board, for both sides.
///
/// See `compute_stats_into` for details.
pub fn compute_stats(board: &Board, game_state: &rules::GameState) -> (BoardStats, BoardStats) {
let mut stats = (BoardStats::new(), BoardStats::new());
compute_stats_into(board, game_state, &mut stats);
stats
}
/// Compute stats for both the current player and its opponent.
///
/// The playing color will have its stats filled in the first
/// BoardStats object, its opponent in the second.
pub fn compute_stats_into(
board: &Board,
game_state: &rules::GameState,
stats: &mut (BoardStats, BoardStats)
) {
let mut gs = game_state.clone();
compute_color_stats_into(board, &gs, &mut stats.0);
gs.color = opposite(gs.color);
compute_color_stats_into(board, &gs, &mut stats.1);
}
/// Fill `stats` from given `board` and `game_state`.
///
/// Only the current playing side stats are created,
/// prepare the game_state accordingly.
pub fn compute_color_stats_into(
board: &Board,
game_state: &rules::GameState,
stats: &mut BoardStats,
) {
stats.reset();
let color = game_state.color;
// Compute mobility for all pieces.
stats.mobility = rules::get_player_moves(board, game_state, true).len() as i32;
// Compute amount of each piece.
for (piece, p) in get_piece_iterator(board) {
let (pos_f, pos_r) = p;
if piece == SQ_E || !is_color(piece, color) {
continue
}
match get_type(piece) {
SQ_R => stats.num_rooks += 1,
SQ_N => stats.num_knights += 1,
SQ_B => stats.num_bishops += 1,
SQ_Q => stats.num_queens += 1,
SQ_K => stats.num_kings += 1,
SQ_P => {
stats.num_pawns += 1;
let mut doubled = false;
let mut isolated = true;
let mut backward = true;
for r in 0..8 {
// Check for doubled pawns.
if
!doubled &&
is_piece(get_square(board, &(pos_f, r)), color|SQ_P) && r != pos_r
{
doubled = true;
}
// Check for isolated pawns.
if
isolated &&
(
// Check on the left file if not on a-file...
(
pos_f > POS_MIN &&
is_piece(get_square(board, &(pos_f - 1, r)), color|SQ_P)
) ||
// Check on the right file if not on h-file...
(
pos_f < POS_MAX &&
is_piece(get_square(board, &(pos_f + 1, r)), color|SQ_P)
)
)
{
isolated = false;
}
// Check for backward pawns.
if backward {
if color == SQ_WH && r <= pos_r {
if (
pos_f > POS_MIN &&
is_type(get_square(board, &(pos_f - 1, r)), SQ_P)
) || (
pos_f < POS_MAX &&
is_type(get_square(board, &(pos_f + 1, r)), SQ_P)
) {
backward = false;
}
} else if color == SQ_BL && r >= pos_r {
if (
pos_f > POS_MIN &&
is_type(get_square(board, &(pos_f - 1, r)), SQ_P)
) || (
pos_f < POS_MAX &&
is_type(get_square(board, &(pos_f + 1, r)), SQ_P)
) {
backward = false;
}
}
}
}
if doubled {
stats.num_doubled_pawns += 1;
}
if isolated {
stats.num_isolated_pawns += 1;
}
if backward {
stats.num_backward_pawns += 1;
}
},
_ => {}
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -181,8 +157,8 @@ mod tests {
#[test] #[test]
fn test_compute_stats() { fn test_compute_stats() {
// Check that initial stats are correct. // Check that initial stats are correct.
let b = new(); let mut b = Board::new();
let gs = rules::GameState::new(); let mut gs = GameState::new();
let initial_stats = BoardStats { let initial_stats = BoardStats {
num_pawns: 8, num_pawns: 8,
num_bishops: 2, num_bishops: 2,
@ -195,56 +171,54 @@ mod tests {
num_isolated_pawns: 0, num_isolated_pawns: 0,
mobility: 20, mobility: 20,
}; };
let mut stats = compute_stats(&b, &gs); let mut stats = BoardStats::new_from(&mut b, &mut gs);
eprintln!("{}", stats.0);
eprintln!("{}", stats.1);
assert!(stats.0 == stats.1); assert!(stats.0 == stats.1);
assert!(stats.0 == initial_stats); assert!(stats.0 == initial_stats);
// Check that doubled pawns are correctly counted. // Check that doubled pawns are correctly counted.
let mut b = new_empty(); let mut b = Board::new_empty();
set_square(&mut b, &pos("d4"), SQ_WH_P); b.set_square(D4, WHITE, PAWN);
set_square(&mut b, &pos("d6"), SQ_WH_P); b.set_square(D6, WHITE, PAWN);
compute_color_stats_into(&b, &gs, &mut stats.0); stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 2); assert_eq!(stats.0.num_doubled_pawns, 2);
// Add a pawn on another file, no changes expected. // Add a pawn on another file, no changes expected.
set_square(&mut b, &pos("e6"), SQ_WH_P); b.set_square(E6, WHITE, PAWN);
compute_color_stats_into(&b, &gs, &mut stats.0); stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 2); assert_eq!(stats.0.num_doubled_pawns, 2);
// Add a pawn backward in the d-file: there are now 3 doubled pawns. // Add a pawn backward in the d-file: there are now 3 doubled pawns.
set_square(&mut b, &pos("d2"), SQ_WH_P); b.set_square(D2, WHITE, PAWN);
compute_color_stats_into(&b, &gs, &mut stats.0); stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 3); assert_eq!(stats.0.num_doubled_pawns, 3);
// Check that isolated and backward pawns are correctly counted. // Check that isolated and backward pawns are correctly counted.
assert_eq!(stats.0.num_isolated_pawns, 0); assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 2); // A bit weird? assert_eq!(stats.0.num_backward_pawns, 2); // A bit weird?
// Protect d4 pawn with a friend in e3: it is not isolated nor backward anymore. // Protect d4 pawn with a friend in e3: it is not isolated nor backward anymore.
set_square(&mut b, &pos("e3"), SQ_WH_P); b.set_square(E3, WHITE, PAWN);
compute_color_stats_into(&b, &gs, &mut stats.0); stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 5); assert_eq!(stats.0.num_doubled_pawns, 5);
assert_eq!(stats.0.num_isolated_pawns, 0); assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 1); assert_eq!(stats.0.num_backward_pawns, 1);
// Add an adjacent friend to d2 pawn: no pawns are left isolated or backward. // Add an adjacent friend to d2 pawn: no pawns are left isolated or backward.
set_square(&mut b, &pos("c2"), SQ_WH_P); b.set_square(C2, WHITE, PAWN);
compute_color_stats_into(&b, &gs, &mut stats.0); stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 5); assert_eq!(stats.0.num_doubled_pawns, 5);
assert_eq!(stats.0.num_isolated_pawns, 0); assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 0); assert_eq!(stats.0.num_backward_pawns, 0);
// Add an isolated/backward white pawn in a far file. // Add an isolated/backward white pawn in a far file.
set_square(&mut b, &pos("a2"), SQ_WH_P); b.set_square(A2, WHITE, PAWN);
compute_color_stats_into(&b, &gs, &mut stats.0); stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 5); assert_eq!(stats.0.num_doubled_pawns, 5);
assert_eq!(stats.0.num_isolated_pawns, 1); assert_eq!(stats.0.num_isolated_pawns, 1);
assert_eq!(stats.0.num_backward_pawns, 1); assert_eq!(stats.0.num_backward_pawns, 1);
// Check for pawns that are backward but not isolated. // Check for pawns that are backward but not isolated.
let mut b = new_empty(); let mut b = Board::new_empty();
// Here, d4 pawn protects both e5 and e3, but it is backward. // Here, d4 pawn protects both e5 and e3, but it is backward.
set_square(&mut b, &pos("d4"), SQ_WH_P); b.set_square(D4, WHITE, PAWN);
set_square(&mut b, &pos("e5"), SQ_WH_P); b.set_square(E5, WHITE, PAWN);
set_square(&mut b, &pos("e3"), SQ_WH_P); b.set_square(E3, WHITE, PAWN);
compute_color_stats_into(&b, &gs, &mut stats.0); stats.0.compute(&mut b, &mut gs);
assert_eq!(stats.0.num_doubled_pawns, 2); assert_eq!(stats.0.num_doubled_pawns, 2);
assert_eq!(stats.0.num_isolated_pawns, 0); assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 1); assert_eq!(stats.0.num_backward_pawns, 1);

View file

@ -7,8 +7,8 @@ use std::thread;
use crate::analysis::AnalysisInfo; use crate::analysis::AnalysisInfo;
use crate::engine; use crate::engine;
use crate::movement::Move; use crate::fen;
use crate::notation; use crate::movement::{Move, UCI_NULL_MOVE_STR};
const VATU_NAME: &str = env!("CARGO_PKG_NAME"); const VATU_NAME: &str = env!("CARGO_PKG_NAME");
const VATU_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); const VATU_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
@ -57,6 +57,10 @@ pub enum UciCmd {
Position(Vec<PositionArgs>), Position(Vec<PositionArgs>),
Go(Vec<GoArgs>), Go(Vec<GoArgs>),
Quit, Quit,
// Unofficial commands mostly for debugging.
VatuNode,
Unknown(String), Unknown(String),
} }
@ -64,7 +68,7 @@ pub enum UciCmd {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum PositionArgs { pub enum PositionArgs {
Startpos, Startpos,
Fen(notation::Fen), Fen(fen::Fen),
Moves(Vec<Move>), Moves(Vec<Move>),
} }
@ -198,6 +202,9 @@ impl Uci {
self.send_engine_command(engine::Cmd::Stop); self.send_engine_command(engine::Cmd::Stop);
}, },
UciCmd::Quit => return false, UciCmd::Quit => return false,
UciCmd::VatuNode => {
self.send_engine_command(engine::Cmd::LogNode);
}
UciCmd::Unknown(c) => { self.log(format!("Unknown command: {}", c)); } UciCmd::Unknown(c) => { self.log(format!("Unknown command: {}", c)); }
} }
true true
@ -271,7 +278,7 @@ impl Uci {
s.push_str(&format!(" nps {}", n)); s.push_str(&format!(" nps {}", n));
} }
AnalysisInfo::CurrentMove(m) => { AnalysisInfo::CurrentMove(m) => {
s.push_str(&format!(" currmove {}", notation::move_to_string(m))); s.push_str(&format!(" currmove {}", m.to_uci_string()));
} }
} }
} }
@ -280,11 +287,10 @@ impl Uci {
/// Send best move. /// Send best move.
fn send_bestmove(&mut self, m: &Option<Move>) { fn send_bestmove(&mut self, m: &Option<Move>) {
let move_str = match m { self.send(&format!(
Some(m) => notation::move_to_string(m), "bestmove {}",
None => notation::NULL_MOVE.to_string(), if let Some(m) = m { m.to_uci_string() } else { UCI_NULL_MOVE_STR.to_string() }
}; ));
self.send(&format!("bestmove {}", move_str));
} }
} }
@ -292,6 +298,9 @@ impl Uci {
// UCI command parsers // UCI command parsers
/// Parse an UCI command. /// Parse an UCI command.
///
/// Handle main UCI commands: position, go, etc. The command "p" is an
/// alias to "position".
fn parse_command(s: &str) -> UciCmd { fn parse_command(s: &str) -> UciCmd {
if s.len() == 0 { if s.len() == 0 {
return UciCmd::Unknown("Empty command.".to_string()); return UciCmd::Unknown("Empty command.".to_string());
@ -303,9 +312,10 @@ fn parse_command(s: &str) -> UciCmd {
"isready" => UciCmd::IsReady, "isready" => UciCmd::IsReady,
"ucinewgame" => UciCmd::UciNewGame, "ucinewgame" => UciCmd::UciNewGame,
"stop" => UciCmd::Stop, "stop" => UciCmd::Stop,
"position" => parse_position_command(&fields[1..]), "position" | "p" => parse_position_command(&fields[1..]),
"go" => parse_go_command(&fields[1..]), "go" => parse_go_command(&fields[1..]),
"quit" => UciCmd::Quit, "quit" => UciCmd::Quit,
"vatunode" => UciCmd::VatuNode,
c => UciCmd::Unknown(c.to_string()), c => UciCmd::Unknown(c.to_string()),
} }
} }
@ -319,7 +329,7 @@ fn parse_position_command(fields: &[&str]) -> UciCmd {
match fields[i] { match fields[i] {
// Subcommand "fen" is followed by a FEN string. // Subcommand "fen" is followed by a FEN string.
"fen" => { "fen" => {
if let Some(fen) = notation::parse_fen_fields(&fields[i + 1 .. i + 7]) { if let Some(fen) = fen::parse_fen_fields(&fields[i + 1 .. i + 7]) {
subcommands.push(PositionArgs::Fen(fen)) subcommands.push(PositionArgs::Fen(fen))
} else { } else {
return UciCmd::Unknown(format!("Bad format for position fen")) return UciCmd::Unknown(format!("Bad format for position fen"))
@ -332,7 +342,7 @@ fn parse_position_command(fields: &[&str]) -> UciCmd {
"moves" => { "moves" => {
let mut moves = vec!(); let mut moves = vec!();
while i + 1 < num_fields { while i + 1 < num_fields {
moves.push(notation::parse_move(fields[i + 1])); moves.push(Move::from_uci_string(fields[i + 1]));
i += 1; i += 1;
} }
subcommands.push(PositionArgs::Moves(moves)); subcommands.push(PositionArgs::Moves(moves));

49
src/zobrist.rs Normal file
View file

@ -0,0 +1,49 @@
//! Functions related to Zobrist hashes.
use crate::board::*;
pub type ZobristHash = u64;
pub fn get_new_game_hash() -> ZobristHash {
return
get_piece_hash(WHITE, ROOK, A1)
^ get_piece_hash(WHITE, KNIGHT, B1)
^ get_piece_hash(WHITE, BISHOP, C1)
^ get_piece_hash(WHITE, QUEEN, D1)
^ get_piece_hash(WHITE, KING, E1)
^ get_piece_hash(WHITE, BISHOP, F1)
^ get_piece_hash(WHITE, KNIGHT, G1)
^ get_piece_hash(WHITE, ROOK, H1)
^ get_piece_hash(WHITE, PAWN, A2)
^ get_piece_hash(WHITE, PAWN, B2)
^ get_piece_hash(WHITE, PAWN, C2)
^ get_piece_hash(WHITE, PAWN, D2)
^ get_piece_hash(WHITE, PAWN, E2)
^ get_piece_hash(WHITE, PAWN, F2)
^ get_piece_hash(WHITE, PAWN, G2)
^ get_piece_hash(WHITE, PAWN, H2)
^ get_piece_hash(BLACK, PAWN, A7)
^ get_piece_hash(BLACK, PAWN, B7)
^ get_piece_hash(BLACK, PAWN, C7)
^ get_piece_hash(BLACK, PAWN, D7)
^ get_piece_hash(BLACK, PAWN, E7)
^ get_piece_hash(BLACK, PAWN, F7)
^ get_piece_hash(BLACK, PAWN, G7)
^ get_piece_hash(BLACK, PAWN, H7)
^ get_piece_hash(BLACK, ROOK, A8)
^ get_piece_hash(BLACK, KNIGHT, B8)
^ get_piece_hash(BLACK, BISHOP, C8)
^ get_piece_hash(BLACK, QUEEN, D8)
^ get_piece_hash(BLACK, KING, E8)
^ get_piece_hash(BLACK, BISHOP, F8)
^ get_piece_hash(BLACK, KNIGHT, G8)
^ get_piece_hash(BLACK, ROOK, H8)
^ ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_K]
^ ZOBRIST_CASTLES[ZOBRIST_CASTLE_WH_Q]
^ ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_K]
^ ZOBRIST_CASTLES[ZOBRIST_CASTLE_BL_Q]
}
pub fn get_piece_hash(color: Color, piece: Piece, square: Square) -> ZobristHash {
ZOBRIST_PIECES[color][piece][square as usize]
}