From b6fa45613b3f40b694cc35583da984571414ddb6 Mon Sep 17 00:00:00 2001 From: Kirill Bobyrev Date: Sun, 16 Jun 2024 21:51:37 -0700 Subject: [PATCH] chore: Store progress --- .github/workflows/build.yml | 2 +- Cargo.lock | 2 +- Cargo.toml | 5 +- build.rs | 37 ++- justfile | 3 + src/chess/attacks.rs | 3 +- src/chess/bitboard.rs | 186 +-------------- src/chess/core.rs | 54 +++-- src/chess/generated.rs | 27 ++- src/chess/mod.rs | 3 +- src/chess/position.rs | 361 +++++++++++++++++++++++------- src/chess/state.rs | 0 src/chess/transposition.rs | 37 --- src/chess/zobrist_keys.rs | 37 --- src/engine/mod.rs | 30 ++- src/evaluation/mod.rs | 16 +- src/lib.rs | 19 +- src/search/minimax.rs | 20 +- src/search/mod.rs | 12 +- tests/chess.rs | 82 +++++-- tools/src/bin/extract_lc0_data.rs | 19 +- 21 files changed, 486 insertions(+), 469 deletions(-) delete mode 100644 src/chess/state.rs delete mode 100644 src/chess/transposition.rs delete mode 100644 src/chess/zobrist_keys.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c87f62f5..133210b8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - toolchain: [stable, "1.78.0"] + toolchain: [stable, "1.79.0"] experimental: [false] include: - os: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 44a5c7372..0d1c819d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,7 +811,7 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "pabi" -version = "0.1.0" +version = "2024.6.16" dependencies = [ "anyhow", "arrayvec", diff --git a/Cargo.toml b/Cargo.toml index 5e6b6ca6f..c7d95ebb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ license = "Apache-2.0" name = "pabi" readme = "README.md" repository = "https://github.com/kirillbobyrev/pabi" -rust-version = "1.78" -version = "0.1.0" +rust-version = "1.79" +version = "2024.6.16" include = [ "/src/", "/generated/", @@ -62,7 +62,6 @@ panic = "abort" # Needed for cargo flamegraph. debug = true -# TODO: Mention this in BUILDING.md. # TODO: Test this out once the benchmarks are available and tweak specific # values. So far, this gives around -8% on parsing FEN/EPD positions. [profile.fast] diff --git a/build.rs b/build.rs index 8db8fc1b7..45d8c6166 100644 --- a/build.rs +++ b/build.rs @@ -21,30 +21,23 @@ fn generate_build_info() { generate_file("features", &features); } +type ZobristKey = u64; + fn generate_zobrist_keys() { + const NUM_COLORS: usize = 2; + const NUM_PIECES: usize = 6; + const NUM_SQUARES: usize = 64; + let mut rng = rand::thread_rng(); - for piece in [ - "white_king", - "white_queen", - "white_rook", - "white_bishop", - "white_knight", - "white_pawn", - "black_king", - "black_queen", - "black_rook", - "black_bishop", - "black_knight", - "black_pawn", - ] { - let piece_keys: [u64; 64] = std::array::from_fn(|_| rand::Rng::gen(&mut rng)); - generate_file( - &format!("{piece}_zobrist_keys"), - &format!("{:?}", piece_keys), - ); - } - - let en_passant_keys: [u64; 8] = std::array::from_fn(|_| rand::Rng::gen(&mut rng)); + + let piece_keys: [ZobristKey; NUM_COLORS * NUM_PIECES * NUM_SQUARES] = + std::array::from_fn(|_| rand::Rng::gen(&mut rng)); + generate_file( + &format!("pieces_zobrist_keys"), + &format!("{:?}", piece_keys), + ); + + let en_passant_keys: [ZobristKey; 8] = std::array::from_fn(|_| rand::Rng::gen(&mut rng)); generate_file("en_passant_zobrist_keys", &format!("{:?}", en_passant_keys)); } diff --git a/justfile b/justfile index 0a6408f5a..2e7527a41 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,9 @@ build: run: {{ compile_flags}} cargo run --profile=fast +fmt: + cargo +nightly fmt --all + # Checks the code for bad formatting, errors and warnings. lint: cargo +nightly fmt --all -- --check diff --git a/src/chess/attacks.rs b/src/chess/attacks.rs index 31081491d..d112185f1 100644 --- a/src/chess/attacks.rs +++ b/src/chess/attacks.rs @@ -11,11 +11,10 @@ // TODO: This code is probably by far the least appealing in the project. // Refactor it and make it nicer. +use super::generated; use crate::chess::bitboard::{Bitboard, Pieces}; use crate::chess::core::{Player, Square, BOARD_SIZE}; -use super::generated; - pub(super) fn king_attacks(from: Square) -> Bitboard { generated::KING_ATTACKS[from as usize] } diff --git a/src/chess/bitboard.rs b/src/chess/bitboard.rs index 170f526a1..056ee9e0b 100644 --- a/src/chess/bitboard.rs +++ b/src/chess/bitboard.rs @@ -19,23 +19,12 @@ //! [BitboardCalculator]: https://gekomad.github.io/Cinnamon/BitboardCalculator/ //! [PEXT Bitboards]: https://www.chessprogramming.org/BMI2#PEXTBitboards -use std::fmt::Write; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, Not, Shl, Shr, Sub, SubAssign}; use std::{fmt, mem}; use itertools::Itertools; -use crate::chess::core::{ - Direction, - File, - Piece, - PieceKind, - Player, - Rank, - Square, - BOARD_SIZE, - BOARD_WIDTH, -}; +use crate::chess::core::{Direction, PieceKind, Square, BOARD_SIZE, BOARD_WIDTH}; /// Represents a set of squares and provides common operations (e.g. AND, OR, /// XOR) over these sets. Each bit corresponds to one of 64 squares of the chess @@ -118,7 +107,7 @@ impl Bitboard { } #[must_use] - pub(super) fn has_any(self) -> bool { + pub(super) const fn has_any(self) -> bool { self.bits != 0 } @@ -138,8 +127,12 @@ impl Bitboard { } impl fmt::Debug for Bitboard { + /// The board is printed from A1 to H8, starting from bottom left corner to + /// the top right corner, just like on the normal chess board from the + /// perspective of White. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // TODO: This is quite verbose. Refactor or explain what is happening. + const LINE_SEPARATOR: &str = "\n"; + const SQUARE_SEPARATOR: &str = " "; write!( f, "{}", @@ -315,7 +308,7 @@ pub(crate) struct Pieces { pub(super) queens: Bitboard, pub(super) rooks: Bitboard, pub(super) bishops: Bitboard, - // TODO: Store "all" instead. + // TODO: Store "all" instead? pub(super) knights: Bitboard, pub(super) pawns: Bitboard, } @@ -436,125 +429,6 @@ impl Pieces { } } -/// Piece-centric implementation of the chess board. This is the "back-end" of -/// the chess engine, an efficient board representation is crucial for -/// performance. An alternative implementation would be Square-Piece table but -/// both have different trade-offs and scenarios where they are efficient. It is -/// likely that the best overall performance can be achieved by keeping both to -/// complement each other. -#[derive(Clone, PartialEq, Eq)] -pub(crate) struct Board { - pub(super) white_pieces: Pieces, - pub(super) black_pieces: Pieces, -} - -impl Board { - #[must_use] - pub(super) fn starting() -> Self { - Self { - white_pieces: Pieces::new_white(), - black_pieces: Pieces::new_black(), - } - } - - // Constructs an empty Board to be filled by the board and position builder. - #[must_use] - pub(super) const fn empty() -> Self { - Self { - white_pieces: Pieces::empty(), - black_pieces: Pieces::empty(), - } - } - - #[must_use] - pub(crate) const fn player_pieces(&self, player: Player) -> &Pieces { - match player { - Player::White => &self.white_pieces, - Player::Black => &self.black_pieces, - } - } - - // WARNING: This is slow and inefficient for Bitboard-based piece-centric - // representation. Use with caution. - // TODO: Completely disallow bitboard.at()? - #[must_use] - pub(super) fn at(&self, square: Square) -> Option { - if let Some(kind) = self.white_pieces.at(square) { - return Some(Piece { - owner: Player::White, - kind, - }); - } - if let Some(kind) = self.black_pieces.at(square) { - return Some(Piece { - owner: Player::Black, - kind, - }); - } - None - } -} - -impl fmt::Display for Board { - /// Prints board representation in FEN format. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for rank_idx in (0..BOARD_WIDTH).rev() { - let rank: Rank = unsafe { mem::transmute(rank_idx) }; - let mut empty_squares = 0i32; - for file_idx in 0..BOARD_WIDTH { - let file: File = unsafe { mem::transmute(file_idx) }; - let square = Square::new(file, rank); - if let Some(piece) = self.at(square) { - if empty_squares != 0 { - write!(f, "{empty_squares}")?; - empty_squares = 0; - } - write!(f, "{piece}")?; - } else { - empty_squares += 1; - } - } - if empty_squares != 0 { - write!(f, "{empty_squares}")?; - } - if rank != Rank::One { - const RANK_SEPARATOR: char = '/'; - write!(f, "{RANK_SEPARATOR}")?; - } - } - Ok(()) - } -} - -impl fmt::Debug for Board { - /// Dumps the board in a human readable format ('.' for empty square, FEN - /// algebraic symbol for piece). - /// - /// Useful for debugging purposes. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for rank_idx in (0..BOARD_WIDTH).rev() { - let rank: Rank = unsafe { mem::transmute(rank_idx) }; - for file_idx in 0..BOARD_WIDTH { - let file: File = unsafe { mem::transmute(file_idx) }; - match self.at(Square::new(file, rank)) { - Some(piece) => write!(f, "{piece}"), - None => f.write_char('.'), - }?; - if file != File::H { - write!(f, "{SQUARE_SEPARATOR}")?; - } - } - if rank != Rank::One { - write!(f, "{LINE_SEPARATOR}")?; - } - } - Ok(()) - } -} - -const LINE_SEPARATOR: &str = "\n"; -const SQUARE_SEPARATOR: &str = " "; - #[cfg(test)] mod test { use pretty_assertions::assert_eq; @@ -824,48 +698,4 @@ mod test { . . . . . . . ." ); } - - #[test] - fn starting_board() { - let starting_board = Board::starting(); - assert_eq!( - format!("{:?}", starting_board), - "r n b q k b n r\n\ - p p p p p p p p\n\ - . . . . . . . .\n\ - . . . . . . . .\n\ - . . . . . . . .\n\ - . . . . . . . .\n\ - P P P P P P P P\n\ - R N B Q K B N R" - ); - assert_eq!( - starting_board.white_pieces.all() | starting_board.black_pieces.all(), - Rank::One.mask() | Rank::Two.mask() | Rank::Seven.mask() | Rank::Eight.mask() - ); - assert_eq!( - !(starting_board.white_pieces.all() | starting_board.black_pieces.all()), - Rank::Three.mask() | Rank::Four.mask() | Rank::Five.mask() | Rank::Six.mask() - ); - assert_eq!( - starting_board.to_string(), - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" - ); - } - - #[test] - fn empty_board() { - assert_eq!( - format!("{:?}", Board::empty()), - ". . . . . . . .\n\ - . . . . . . . .\n\ - . . . . . . . .\n\ - . . . . . . . .\n\ - . . . . . . . .\n\ - . . . . . . . .\n\ - . . . . . . . .\n\ - . . . . . . . ." - ); - assert_eq!(Board::empty().to_string(), "8/8/8/8/8/8/8/8"); - } } diff --git a/src/chess/core.rs b/src/chess/core.rs index c24470988..49cb473d7 100644 --- a/src/chess/core.rs +++ b/src/chess/core.rs @@ -20,8 +20,9 @@ pub const BOARD_SIZE: u8 = BOARD_WIDTH * BOARD_WIDTH; /// representation. The moves can also be indexed and fed as an input to the /// Neural Network evaluators that would be able assess their potential without /// evaluating post-states. -// TODO: Implement bijection for a move and a numeric index. +// TODO: Implement bijection for a move and a numeric index for lc0 purposes. // TODO: Switch this to an enum representation (regular, en passant, castling) or add flag. +// TODO: Switch to a more compact representation so that Transposition Table can grow larger. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Move { pub(super) from: Square, @@ -31,7 +32,7 @@ pub struct Move { impl Move { #[must_use] - pub const fn new(from: Square, to: Square, promotion: Option) -> Self { + pub(super) const fn new(from: Square, to: Square, promotion: Option) -> Self { Self { from, to, @@ -39,6 +40,9 @@ impl Move { } } + /// Converts the move from UCI format to the internal representation. This + /// is important for the communication between the engine and UCI server in + /// `position` command. #[must_use] pub fn from_uci(uci: &str) -> anyhow::Result { Self::try_from(uci) @@ -119,7 +123,7 @@ pub type MoveList = arrayvec::ArrayVec; #[rustfmt::skip] #[allow(missing_docs)] pub enum Square { - A1 = 0, B1, C1, D1, E1, F1, G1, H1, + A1, B1, C1, D1, E1, F1, G1, H1, A2, B2, C2, D2, E2, F2, G2, H2, A3, B3, C3, D3, E3, F3, G3, H3, A4, B4, C4, D4, E4, F4, G4, H4, @@ -227,14 +231,14 @@ impl fmt::Display for Square { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[allow(missing_docs)] pub enum File { - A = 0, - B = 1, - C = 2, - D = 3, - E = 4, - F = 5, - G = 6, - H = 7, + A, + B, + C, + D, + E, + F, + G, + H, } impl fmt::Display for File { @@ -274,14 +278,14 @@ impl TryFrom for File { #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[allow(missing_docs)] pub enum Rank { - One = 0, - Two = 1, - Three = 2, - Four = 3, - Five = 4, - Six = 5, - Seven = 6, - Eight = 7, + One, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, } impl Rank { @@ -649,7 +653,7 @@ impl fmt::Display for CastleRights { /// A pawn can be promoted to a queen, rook, bishop or a knight. #[allow(missing_docs)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)] -pub enum Promotion { +pub(crate) enum Promotion { Queen, Rook, Bishop, @@ -827,8 +831,8 @@ mod test { assert_eq!(size_of::(), 1); // Primitives will have small size thanks to the niche optimizations: // https://rust-lang.github.io/unsafe-code-guidelines/layout/enums.html#layout-of-a-data-carrying-enums-without-a-repr-annotation - assert_eq!(size_of::(), size_of::>()); - // This is going to be very useful for square-centric board implementation. + assert_eq!(size_of::(), 1); + assert_eq!(size_of::>(), 1); let square_to_pieces: [Option; BOARD_SIZE as usize] = [None; BOARD_SIZE as usize]; assert_eq!(size_of_val(&square_to_pieces), BOARD_SIZE as usize); @@ -857,10 +861,4 @@ mod test { Move::new(Square::E7, Square::E8, Some(Promotion::Queen)) ); } - - // #[test] - // #[should_panic(expected = "square index should be in 0..BOARD_SIZE, got - // 64")] fn square_from_incorrect_index() { - // let _ = Square::try_from(BOARD_SIZE).unwrap(); - // } } diff --git a/src/chess/generated.rs b/src/chess/generated.rs index 63a8a7675..80d4bafb4 100644 --- a/src/chess/generated.rs +++ b/src/chess/generated.rs @@ -1,8 +1,29 @@ +/// Arrays and values generated at or before build time. use crate::chess::bitboard::Bitboard; -use crate::chess::core::BOARD_SIZE; +use crate::chess::core::{Piece, Square, BOARD_SIZE}; +use crate::chess::zobrist::Key; + +// All keys required for Zobrist hashing of a chess position. +pub(super) const BLACK_TO_MOVE: Key = 0x9E06BAD39D761293; + +pub(super) const WHITE_CAN_CASTLE_SHORT: Key = 0xF05AC573DD61D323; +pub(super) const WHITE_CAN_CASTLE_LONG: Key = 0x41D8B55BA5FEB78B; + +pub(super) const BLACK_CAN_CASTLE_SHORT: Key = 0x680988787A43D289; +pub(super) const BLACK_CAN_CASTLE_LONG: Key = 0x2F941F8DFD3E3D1F; + +pub(super) const EN_PASSANT_FILES: [Key; 8] = + include!(concat!(env!("OUT_DIR"), "/en_passant_zobrist_keys")); + +const PIECES_ZOBRIST_KEYS: [Key; 768] = include!(concat!(env!("OUT_DIR"), "/pieces_zobrist_keys")); + +pub(super) fn get_piece_key(piece: Piece, square: Square) -> Key { + const NUM_PIECES: usize = 6; + PIECES_ZOBRIST_KEYS[piece.owner as usize * NUM_PIECES * BOARD_SIZE as usize + + piece.kind as usize * BOARD_SIZE as usize + + square as usize] +} -// Generated in build.rs. -// TODO: Document PEXT bitboards. const BISHOP_ATTACKS_COUNT: usize = 5248; pub(super) const BISHOP_ATTACKS: [Bitboard; BISHOP_ATTACKS_COUNT] = include!(concat!( env!("CARGO_MANIFEST_DIR"), diff --git a/src/chess/mod.rs b/src/chess/mod.rs index c407bac45..837de7b1e 100644 --- a/src/chess/mod.rs +++ b/src/chess/mod.rs @@ -5,7 +5,6 @@ pub mod attacks; pub mod bitboard; pub mod core; pub mod position; -pub mod transposition; +pub mod zobrist; -mod zobrist_keys; mod generated; diff --git a/src/chess/position.rs b/src/chess/position.rs index 135e3b360..99cab22c4 100644 --- a/src/chess/position.rs +++ b/src/chess/position.rs @@ -7,18 +7,142 @@ //! //! [Chess Position]: https://www.chessprogramming.org/Chess_Position -use std::fmt; +use std::fmt::{self, Write}; use anyhow::{bail, Context}; -use crate::chess::attacks; -use crate::chess::bitboard::{Bitboard, Board, Pieces}; +use crate::chess::bitboard::{Bitboard, Pieces}; use crate::chess::core::{ - CastleRights, File, Move, MoveList, Piece, Player, Promotion, Rank, Square, BOARD_WIDTH, + CastleRights, + File, + Move, + MoveList, + Piece, + Player, + Promotion, + Rank, + Square, + BOARD_WIDTH, }; -use crate::chess::transposition; -use crate::chess::zobrist_keys; +use crate::chess::{attacks, generated, zobrist}; +/// Piece-centric implementation of the chess board. This is the "back-end" of +/// the chess engine, an efficient board representation is crucial for +/// performance. An alternative implementation would be Square-Piece table but +/// both have different trade-offs and scenarios where they are efficient. It is +/// likely that the best overall performance can be achieved by keeping both to +/// complement each other. +#[derive(Clone, PartialEq, Eq)] +struct Board { + white_pieces: Pieces, + black_pieces: Pieces, +} + +impl Board { + #[must_use] + fn starting() -> Self { + Self { + white_pieces: Pieces::new_white(), + black_pieces: Pieces::new_black(), + } + } + + // Constructs an empty Board to be filled by the board and position builder. + #[must_use] + const fn empty() -> Self { + Self { + white_pieces: Pieces::empty(), + black_pieces: Pieces::empty(), + } + } + + #[must_use] + const fn player_pieces(&self, player: Player) -> &Pieces { + match player { + Player::White => &self.white_pieces, + Player::Black => &self.black_pieces, + } + } + + // WARNING: This is slow and inefficient for Bitboard-based piece-centric + // representation. Use with caution. + // TODO: Completely disallow bitboard.at()? + #[must_use] + fn at(&self, square: Square) -> Option { + if let Some(kind) = self.white_pieces.at(square) { + return Some(Piece { + owner: Player::White, + kind, + }); + } + if let Some(kind) = self.black_pieces.at(square) { + return Some(Piece { + owner: Player::Black, + kind, + }); + } + None + } +} + +impl fmt::Display for Board { + /// Prints board representation in FEN format. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for rank_idx in (0..BOARD_WIDTH).rev() { + let rank: Rank = unsafe { std::mem::transmute(rank_idx) }; + let mut empty_squares = 0i32; + for file_idx in 0..BOARD_WIDTH { + let file: File = unsafe { std::mem::transmute(file_idx) }; + let square = Square::new(file, rank); + if let Some(piece) = self.at(square) { + if empty_squares != 0 { + write!(f, "{empty_squares}")?; + empty_squares = 0; + } + write!(f, "{piece}")?; + } else { + empty_squares += 1; + } + } + if empty_squares != 0 { + write!(f, "{empty_squares}")?; + } + if rank != Rank::One { + const RANK_SEPARATOR: char = '/'; + write!(f, "{RANK_SEPARATOR}")?; + } + } + Ok(()) + } +} + +impl fmt::Debug for Board { + /// Dumps the board in a human readable format ('.' for empty square, FEN + /// algebraic symbol for piece). + /// + /// Useful for debugging purposes. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + const LINE_SEPARATOR: &str = "\n"; + const SQUARE_SEPARATOR: &str = " "; + for rank_idx in (0..BOARD_WIDTH).rev() { + let rank: Rank = unsafe { std::mem::transmute(rank_idx) }; + for file_idx in 0..BOARD_WIDTH { + let file: File = unsafe { std::mem::transmute(file_idx) }; + match self.at(Square::new(file, rank)) { + Some(piece) => write!(f, "{piece}"), + None => f.write_char('.'), + }?; + if file != File::H { + write!(f, "{SQUARE_SEPARATOR}")?; + } + } + if rank != Rank::One { + write!(f, "{LINE_SEPARATOR}")?; + } + } + Ok(()) + } +} /// State of the chess game: board, half-move counters and castling rights, /// etc. It has 1:1 relationship with [Forsyth-Edwards Notation] (FEN). @@ -37,10 +161,11 @@ use crate::chess::zobrist_keys; /// [Extended Position Description]: https://www.chessprogramming.org/Extended_Position_Description /// [Operations]: https://www.chessprogramming.org/Extended_Position_Description#Operations // TODO: Make the fields private, expose appropriate assessors. -// TODO: Store Zobrist hash, possibly other info. +// TODO: Store Zobrist hash, possibly other info such as repetition count, +// in_check (might be useful for move_gen and other things). #[derive(Clone)] pub struct Position { - pub(crate) board: Board, + board: Board, castling: CastleRights, side_to_move: Player, /// [Halfmove Clock][^ply] keeps track of the number of halfmoves since the @@ -58,10 +183,6 @@ pub struct Position { // TODO: Mark more functions as const. impl Position { - pub(crate) fn board(&self) -> &Board { - &self.board - } - /// Creates the starting position of the standard chess. /// /// ``` @@ -78,15 +199,6 @@ impl Position { Self { board: Board::starting(), castling: CastleRights::ALL, - ..Self::empty() - } - } - - #[must_use] - pub fn empty() -> Self { - Self { - board: Board::empty(), - castling: CastleRights::NONE, side_to_move: Player::White, halfmove_clock: 0, fullmove_counter: 1, @@ -146,9 +258,10 @@ impl Position { /// extra symbols. // TODO: Add support for Shredder FEN and Chess960. pub fn from_fen(input: &str) -> anyhow::Result { + let mut board = Board::empty(); + let mut parts = input.split(' '); // Parse Piece Placement. - let mut result = Self::empty(); let pieces_placement = match parts.next() { Some(placement) => placement, None => bail!("incorrect FEN: missing pieces placement"), @@ -177,8 +290,8 @@ impl Position { match Piece::try_from(symbol) { Ok(piece) => { let owner = match piece.owner { - Player::White => &mut result.board.white_pieces, - Player::Black => &mut result.board.black_pieces, + Player::White => &mut board.white_pieces, + Player::Black => &mut board.black_pieces, }; let square = Square::new(file.try_into()?, rank); *owner.bitboard_for_mut(piece.kind) |= Bitboard::from(square); @@ -188,90 +301,83 @@ impl Position { file += 1; } if file != BOARD_WIDTH { - bail!("incorrect FEN: rank size should be exactly {BOARD_WIDTH}, got {rank_fen} of length {file}"); + bail!( + "incorrect FEN: rank size should be exactly {BOARD_WIDTH}, + got {rank_fen} of length {file}" + ); } } if rank_id != 0 { bail!("incorrect FEN: there should be 8 ranks, got {pieces_placement}"); } - result.side_to_move = match parts.next() { + let side_to_move = match parts.next() { Some(value) => value.try_into()?, None => bail!("incorrect FEN: missing side to move"), }; - result.castling = match parts.next() { + let castling = match parts.next() { Some(value) => value.try_into()?, None => bail!("incorrect FEN: missing castling rights"), }; - result.en_passant_square = match parts.next() { + let en_passant_square = match parts.next() { Some("-") => None, Some(value) => Some(value.try_into()?), None => bail!("incorrect FEN: missing en passant square"), }; - result.halfmove_clock = match parts.next() { - Some(value) => { - // TODO: Here and below: parse manually just by getting through - // ASCII digits since we're already checking them. - if !value.bytes().all(|c| c.is_ascii_digit()) { - bail!("halfmove clock can not contain anything other than digits"); - } - match value.parse::() { - Ok(num) => num, - Err(e) => { - return Err(e).with_context(|| { - format!("incorrect FEN: halfmove clock can not be parsed {value}") - }); - }, - } + let halfmove_clock = match parts.next() { + Some(value) => match value.parse::() { + Ok(num) => Some(num), + Err(e) => { + return Err(e).with_context(|| { + format!("incorrect FEN: halfmove clock can not be parsed {value}") + }); + }, }, // This is a correct EPD: exit early. - None => { - return match validate(&result) { - Ok(()) => Ok(result), - Err(e) => Err(e.context("illegal position")), - } - }, + None => None, }; - result.fullmove_counter = match parts.next() { - Some(value) => { - if !value.bytes().all(|c| c.is_ascii_digit()) { - bail!("fullmove counter clock can not contain anything other than digits"); - } - match value.parse::() { - Ok(0) => { - bail!("fullmove counter can not be 0") - }, - Ok(num) => num, - Err(e) => { - return Err(e).with_context(|| { - format!("incorrect FEN: fullmove counter can not be parsed {value}") - }); - }, - } + let fullmove_counter = match parts.next() { + Some(value) => match value.parse::() { + Ok(0) => { + bail!("fullmove counter can not be 0") + }, + Ok(num) => Some(num), + Err(e) => { + return Err(e).with_context(|| { + format!("incorrect FEN: fullmove counter can not be parsed {value}") + }); + }, }, - None => bail!("incorrect FEN: missing halfmove clock"), - }; - match parts.next() { - None => match validate(&result) { - Ok(()) => Ok(result), - Err(e) => Err(e.context("illegal position")), + None => match halfmove_clock { + Some(_) => bail!("incorrect FEN: missing halfmove clock"), + None => None, }, - Some(_) => bail!("trailing symbols are not allowed in FEN"), - } - } + }; - /// Returns a string representation of the position in FEN format. - #[must_use] - pub fn fen(&self) -> String { - self.to_string() - } + if parts.next().is_some() { + bail!("trailing symbols are not allowed in FEN"); + } - #[must_use] - pub fn has_insufficient_material(&self) -> bool { - todo!() + let halfmove_clock = halfmove_clock.unwrap_or(0); + let fullmove_counter = fullmove_counter.unwrap_or(1); + let result = Self { + board, + castling, + side_to_move, + halfmove_clock, + fullmove_counter, + en_passant_square, + }; + match validate(&result) { + Ok(()) => Ok(result), + Err(e) => Err(e.context("illegal position")), + } } + /// Checks whether a position is pseudo-legal. This is a simple check to + /// ensure that the state is not corrupted and is safe to work with. It + /// doesn't handle all corner cases and is simply used to as a sanity check. #[must_use] - pub fn is_legal(&self) -> bool { + pub(crate) fn is_legal(&self) -> bool { validate(self).is_ok() } @@ -352,7 +458,7 @@ impl Position { // Double checks can only be evaded by the king moves to safety: no // need to consider other moves. 2 => return moves, - _ => unreachable!("more than two pieces can not check the king"), + _ => unreachable!("checks can't be given by more than two pieces at once"), }; generate_knight_moves( our_pieces.knights, @@ -540,9 +646,14 @@ impl Position { self.in_check() && self.generate_moves().is_empty() } + /// Returns true if the player to move has no legal moves and is not + /// checkmated (i.e. the game is a draw) or if 50-move rule is in effect. + /// + /// Note that because position does not keep track of the 3-fold repetition + /// it is not taken into account. #[must_use] pub fn is_stalemate(&self) -> bool { - !self.in_check() && self.generate_moves().is_empty() + self.halfmove_clock >= 100 || (!self.in_check() && self.generate_moves().is_empty()) } #[must_use] @@ -555,10 +666,37 @@ impl Position { todo!() } - pub fn compute_hash(&self) -> transposition::Key { + /// Computes standard Zobrist hash of the using pseudo-random numbers + /// generated during the build stage. + pub fn compute_hash(&self) -> zobrist::Key { let mut key = 0; + if self.side_to_move == Player::Black { - key ^= zobrist_keys::BLACK_TO_MOVE; + key ^= generated::BLACK_TO_MOVE; + } + + if self.castling.contains(CastleRights::WHITE_SHORT) { + key ^= generated::WHITE_CAN_CASTLE_SHORT; + } + if self.castling.contains(CastleRights::WHITE_LONG) { + key ^= generated::WHITE_CAN_CASTLE_LONG; + } + if self.castling.contains(CastleRights::BLACK_SHORT) { + key ^= generated::BLACK_CAN_CASTLE_SHORT; + } + if self.castling.contains(CastleRights::BLACK_LONG) { + key ^= generated::BLACK_CAN_CASTLE_LONG; + } + + if let Some(square) = self.en_passant_square { + let en_passant_file = square.file(); + key ^= generated::EN_PASSANT_FILES[en_passant_file as usize]; + } + + let occupied_squares = self.board.white_pieces.all() | self.board.black_pieces.all(); + for square in occupied_squares.iter() { + let piece = self.board.at(square).expect("the square"); + key ^= generated::get_piece_key(piece, square); } key @@ -996,3 +1134,52 @@ fn generate_castle_moves( } } } + +mod test { + use crate::chess::core::Rank; + use crate::chess::position::Board; + + #[test] + fn empty_board() { + assert_eq!( + format!("{:?}", Board::empty()), + ". . . . . . . .\n\ + . . . . . . . .\n\ + . . . . . . . .\n\ + . . . . . . . .\n\ + . . . . . . . .\n\ + . . . . . . . .\n\ + . . . . . . . .\n\ + . . . . . . . ." + ); + assert_eq!(Board::empty().to_string(), "8/8/8/8/8/8/8/8"); + } + + #[test] + fn starting_board() { + let starting_board = Board::starting(); + assert_eq!( + format!("{:?}", starting_board), + "r n b q k b n r\n\ + p p p p p p p p\n\ + . . . . . . . .\n\ + . . . . . . . .\n\ + . . . . . . . .\n\ + . . . . . . . .\n\ + P P P P P P P P\n\ + R N B Q K B N R" + ); + assert_eq!( + starting_board.white_pieces.all() | starting_board.black_pieces.all(), + Rank::One.mask() | Rank::Two.mask() | Rank::Seven.mask() | Rank::Eight.mask() + ); + assert_eq!( + !(starting_board.white_pieces.all() | starting_board.black_pieces.all()), + Rank::Three.mask() | Rank::Four.mask() | Rank::Five.mask() | Rank::Six.mask() + ); + assert_eq!( + starting_board.to_string(), + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" + ); + } +} diff --git a/src/chess/state.rs b/src/chess/state.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/chess/transposition.rs b/src/chess/transposition.rs deleted file mode 100644 index 040409f33..000000000 --- a/src/chess/transposition.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Implements Zobrist hashing and [Transposition Table] functionality. -//! -//! [Transposition Table](https://www.chessprogramming.org/Transposition_Table - -use super::position::Position; -use core::hash; -use std::hash::{Hash, Hasher}; - -pub type Key = u64; - -pub struct Entry {} - -pub struct TranspositionTable {} - -impl TranspositionTable { - fn new() -> Self { - todo!() - } - - fn clear(&mut self) { - todo!() - } - - fn probe(&self, key: u64) -> Option<&Entry> { - todo!() - } - - fn store(&mut self, key: u64, entry: Entry) { - todo!() - } -} - -impl Hash for Position { - fn hash(&self, state: &mut H) { - todo!() - } -} diff --git a/src/chess/zobrist_keys.rs b/src/chess/zobrist_keys.rs deleted file mode 100644 index bd9db8fda..000000000 --- a/src/chess/zobrist_keys.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::chess::transposition::Key; - -pub(super) const BLACK_TO_MOVE: Key = 0x9e06bad39d761293; - -pub(super) const WHITE_CAN_CASTLE_KINGSIDE: Key = 0xf05ac573dd61d323; -pub(super) const BLACK_CAN_CASTLE_KINGSIDE: Key = 0x680988787a43d289; -pub(super) const WHITE_CAN_CASTLE_QUEENSIDE: Key = 0x41d8b55ba5feb78b; -pub(super) const BLACK_CAN_CASTLE_QUEENSIDE: Key = 0x2f941f8dfd3e3d1f; - -pub(super) const EN_PASSANT_FILES: [Key; 8] = - include!(concat!(env!("OUT_DIR"), "/en_passant_zobrist_keys")); - -pub(super) const WHITE_KING: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const WHITE_QUEEN: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const WHITE_ROOK: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const WHITE_BISHOP: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const WHITE_KNIGHT: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const WHITE_PAWN: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); - -pub(super) const BLACK_KING: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const BLACK_QUEEN: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const BLACK_ROOK: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const BLACK_BISHOP: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const BLACK_KNIGHT: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); -pub(super) const BLACK_PAWN: [Key; 64] = - include!(concat!(env!("OUT_DIR"), "/white_king_zobrist_keys")); diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 86de79140..80a9f7cc4 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -24,10 +24,12 @@ pub struct Engine<'a, R: BufRead, W: Write> { } impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { + /// Creates a new instance of the engine with starting position and provided + /// I/O. #[must_use] pub fn new(input: &'a mut R, output: &'a mut W) -> Self { Self { - position: Position::empty(), + position: Position::starting(), input, output, } @@ -45,13 +47,14 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { /// while writing the responses to the output stream. /// /// The minimal set of supported commands should be: - /// - uci - /// - isready - /// - setoption - /// - ucinewgame - /// - go wtime btime winc binc - /// - quit - /// - stop + /// + /// - `uci` + /// - `isready` + /// - `setoption` + /// - `ucinewgame` + /// - `go wtime btime winc binc` + /// - `quit` + /// - `stop` /// /// NOTE: The assumption is that the UCI input stream is **correct**. It is /// tournament manager's responsibility to send uncorrupted input and make @@ -91,23 +94,28 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { Ok(()) } + /// Responds to the `uci` handshake command by identifying the engine. fn handle_uci(&mut self) -> anyhow::Result<()> { writeln!( self.output, "id name {} {}", env!("CARGO_PKG_NAME"), - crate::get_version() + crate::engine_version() )?; writeln!(self.output, "id author {}", env!("CARGO_PKG_AUTHORS"))?; writeln!(self.output, "uciok")?; Ok(()) } + /// Syncs with the UCI server by responding with `readyok`. fn handle_isready(&mut self) -> anyhow::Result<()> { writeln!(self.output, "readyok")?; Ok(()) } + /// Sets the engine options. This is a no-op for now. In the future this + /// should at least support setting the transposition table size and search + /// thread count. fn handle_setoption(&mut self, line: String) -> anyhow::Result<()> { writeln!( self.output, @@ -121,6 +129,7 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { Ok(()) } + /// Changes the position of the board to the one specified in the command. fn handle_position( &mut self, stream: &mut std::slice::Iter<&str>, @@ -198,6 +207,9 @@ impl<'a, R: BufRead, W: Write> Engine<'a, R, W> { Ok(()) } + /// Stops the search immediately. + /// + /// NOTE: This is a no-op for now. fn handle_stop(&mut self) -> anyhow::Result<()> { // TODO: Implement this method. Ok(()) diff --git a/src/evaluation/mod.rs b/src/evaluation/mod.rs index 11789a779..053c3ea6f 100644 --- a/src/evaluation/mod.rs +++ b/src/evaluation/mod.rs @@ -10,24 +10,25 @@ use std::ops::Neg; pub(crate) mod material; -// TODO: Document: a thin wrapper around i32, same size and ergonomics -// for performance reasons. +/// A thin wrapper around i32 for ergonomics and type safety. +// TODO: Support "Mate in X" by using the unoccupied range of i32. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Score { +pub(crate) struct Score { /// Evaluation relative value in centipawn (100 CP = 1 "pawn") units. value: i32, } impl Score { - pub const LOSE: Self = Self { value: -32_000 }; - pub const MAX: Self = Self::WIN; - pub const MIN: Self = Self::LOSE; - pub const WIN: Self = Self { value: 32_000 }; + /// Corresponds to checkmating the opponent. + pub(crate) const MAX: Self = Self { value: 32_000 }; + /// Corresponds to being checkmated by opponent. + pub(crate) const MIN: Self = Self { value: -32_000 }; } impl Neg for Score { type Output = Self; + /// Mirrors evaluation to other player's perspective. fn neg(self) -> Self::Output { Self { value: self.value.neg(), @@ -42,6 +43,7 @@ impl From for Score { } impl Display for Score { + /// Formats the score as centipawn units for UCI interface. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "cp {}", self.value) } diff --git a/src/lib.rs b/src/lib.rs index 344f9122d..d984e0470 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,9 +54,11 @@ use shadow_rs::shadow; shadow!(build); /// Build type and target. Produced by `build.rs`. -pub const FEATURES: &str = include_str!(concat!(env!("OUT_DIR"), "/features")); +const FEATURES: &str = include_str!(concat!(env!("OUT_DIR"), "/features")); -pub(crate) fn get_version() -> String { +/// Returns the full engine version that can be used to identify how it was +/// built in the first place. +fn engine_version() -> String { format!( "{} (commit {}, branch {})", build::PKG_VERSION, @@ -65,16 +67,17 @@ pub(crate) fn get_version() -> String { ) } -/// Prints main information about the engine to standard output. +/// Prints informations about the engine version, author and GitHub repository +/// on engine startup. pub fn print_engine_info() { - println!("Pabi Chess Engine"); - println!("Version {}", get_version()); - println!("https://github.com/kirillbobyrev/pabi"); + println!("Pabi chess engine {}", engine_version()); + println!(""); } -/// Prints information about how the binary was built to the standard output. +/// Prints information the build type, features and whether the build is clean +/// on engine startup. pub fn print_binary_info() { - println!("Debug: {}", shadow_rs::is_debug()); + println!("Release build: {}", !shadow_rs::is_debug()); println!("Features: {FEATURES}"); if !shadow_rs::git_clean() { println!("Warning: built with uncommitted changes"); diff --git a/src/search/minimax.rs b/src/search/minimax.rs index 9b839988f..cbd98665d 100644 --- a/src/search/minimax.rs +++ b/src/search/minimax.rs @@ -4,8 +4,6 @@ //! [Minimax]: https://en.wikipedia.org/wiki/Minimax //! [Negamax]: https://en.wikipedia.org/wiki/Negamax //! [Alpha-Beta pruning]: https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning -// TODO: Implement iterative deepening. -// TODO: Implement alpha-beta pruning. // TODO: Implement move ordering. use crate::evaluation::material::material_advantage; @@ -21,7 +19,7 @@ pub(super) fn negamax(context: &mut Context, depth: u8, alpha: Score, beta: Scor if position.is_checkmate() { // The player to move is in checkmate. - return Score::LOSE; + return Score::MIN; } // TODO: is_draw: stalemate + 50 move rule + 3 repetitions. @@ -88,13 +86,13 @@ mod test { ); } - #[test] - fn losing_position() { - todo!() - } + // #[test] + // fn losing_position() { + // todo!() + // } - #[test] - fn winning_position() { - todo!() - } + // #[test] + // fn winning_position() { + // todo!() + // } } diff --git a/src/search/mod.rs b/src/search/mod.rs index 26eb13e4d..5bb4ce4fa 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -16,6 +16,9 @@ use crate::chess::position::Position; use crate::evaluation::Score; pub(crate) mod minimax; +mod transposition; + +type Depth = u8; /// The search depth does not grow fast and an upper limit is set for improving /// performance. @@ -27,7 +30,7 @@ const MAX_SEARCH_DEPTH: usize = 256; struct Context { position_history: ArrayVec, num_nodes: u64, - // TODO: num_pruned: u64, + // TODO: num_pruned for debugging } impl Context { @@ -41,6 +44,9 @@ impl Context { } } +/// Adding reserve time to ensure that the engine does not exceed the time +/// limit. +// TODO: Tweak this. const RESERVE: Duration = Duration::from_millis(500); /// Runs the search algorithm to find the best move under given time @@ -52,7 +58,7 @@ pub fn go(position: &Position, limit: Duration, output: &mut impl Write) -> Move let mut best_move = None; - const MAX_DEPTH: u8 = 8; + const MAX_DEPTH: Depth = 8; for depth in 1..MAX_DEPTH { let (next_move, score) = find_best_move_and_score(position, depth, &mut context); @@ -86,7 +92,7 @@ pub fn go(position: &Position, limit: Duration, output: &mut impl Write) -> Move fn find_best_move_and_score( position: &Position, - depth: u8, + depth: Depth, context: &mut Context, ) -> (Move, Score) { context.num_nodes += 1; diff --git a/tests/chess.rs b/tests/chess.rs index 1579a6f85..fbfa2a24d 100644 --- a/tests/chess.rs +++ b/tests/chess.rs @@ -1,7 +1,7 @@ use std::fs; use itertools::Itertools; -use pabi::chess::core::{Move, Promotion, Square}; +use pabi::chess::core::Move; use pabi::chess::position::{perft, Position}; use pretty_assertions::assert_eq; @@ -23,7 +23,7 @@ pub fn sanitize_fen(position: &str) -> String { fn expect_legal_position(input: &str) { let position = Position::from_fen(input).expect("we are parsing valid position: {input}"); - assert_eq!(position.fen(), sanitize_fen(input)); + assert_eq!(position.to_string(), sanitize_fen(input)); } #[test] @@ -158,7 +158,6 @@ fn clean_board_str() { .is_ok()); } -// TODO: Test precise error messages. #[test] fn no_crash() { assert!(Position::try_from("3k2p1N/82/8/8/7B/6K1/3R4/8 b - - 0 1").is_err()); @@ -166,7 +165,6 @@ fn no_crash() { assert!(Position::try_from("3kn3/R4N2/8/8/7B/6K1/3R4/8 b - - 0 48 b - - 0 4/8 b").is_err()); assert!(Position::try_from("\tfen3kn3/R2p1N2/8/8/7B/6K1/3R4/8 b - - 0 23").is_err()); assert!(Position::try_from("fen3kn3/R2p1N2/8/8/7B/6K1/3R4/8 b - - 0 23").is_err()); - assert!(Position::try_from("3kn3/R4N2/8/8/7B/6K1/3r4/8 b - - +8 1").is_err()); assert!(Position::from_fen( "\n epd rnbqkb1r/ppp1pp1p/5np1/3p4/3P1B2/5N2/PPP1PPPP/RN1QKB1R w KQkq -\n" ) @@ -184,7 +182,7 @@ fn arbitrary_positions() { .lines() { let position = Position::try_from(serialized_position).unwrap(); - assert_eq!(position.fen(), sanitize_fen(serialized_position)); + assert_eq!(position.to_string(), sanitize_fen(serialized_position)); } } @@ -474,7 +472,7 @@ fn make_moves() { position.make_move(&Move::from_uci("h2h4").unwrap()); assert_eq!( - position.fen(), + position.to_string(), "rnbqkbnr/pp4pp/2p1p3/3p1p2/PP5P/2P5/1B1PPPP1/RN1QKBNR b KQkq - 0 5" ); @@ -487,7 +485,7 @@ fn make_moves() { position.make_move(&Move::from_uci("f5g4").unwrap()); assert_eq!( - position.fen(), + position.to_string(), "rnbqkbnr/pp6/2p1p1p1/3p4/PP4pP/2P5/1B1PP3/RN1QKBNR w KQkq - 0 9" ); @@ -497,7 +495,7 @@ fn make_moves() { position.make_move(&Move::from_uci("g5h4").unwrap()); assert_eq!( - position.fen(), + position.to_string(), "rnbqkbnr/pp6/2p1p3/3p4/PP4pp/2PP4/1B1KP3/RN1Q1BNR w kq - 0 11" ); @@ -514,7 +512,7 @@ fn make_moves() { position.make_move(&Move::from_uci("e4d5").unwrap()); assert_eq!( - position.fen(), + position.to_string(), "r1bqkbnr/3n4/p1p5/Pp1P4/1P4pp/2P5/1BKNP3/R3QBNR b kq - 0 16" ); } @@ -554,24 +552,24 @@ fn random_positions() { #[test] fn basic_moves() { let mut position = setup("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); - position.make_move(&Move::new(Square::E2, Square::E4, None)); + position.make_move(&Move::from_uci("e2e4").expect("valid move")); assert_eq!( - position.fen(), + position.to_string(), "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1" ); - position.make_move(&Move::new(Square::E7, Square::E5, None)); + position.make_move(&Move::from_uci("e7e5").expect("valid move")); assert_eq!( - position.fen(), + position.to_string(), "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2" ); - position.make_move(&Move::new(Square::G1, Square::F3, None)); + position.make_move(&Move::from_uci("g1f3").expect("valid move")); assert_eq!( - position.fen(), + position.to_string(), "rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2" ); - position.make_move(&Move::new(Square::E8, Square::E7, None)); + position.make_move(&Move::from_uci("e8e7").expect("valid move")); assert_eq!( - position.fen(), + position.to_string(), "rnbq1bnr/ppppkppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQ - 2 3" ); } @@ -579,9 +577,9 @@ fn basic_moves() { #[test] fn promotion_moves() { let mut position = setup("2n4k/1PP5/6K1/3Pp1Q1/3N4/3P4/P3R3/8 w - - 0 1"); - position.make_move(&Move::new(Square::B7, Square::C8, Some(Promotion::Queen))); + position.make_move(&Move::from_uci("b7c8q").expect("valid move")); assert_eq!( - position.fen(), + position.to_string(), "2Q4k/2P5/6K1/3Pp1Q1/3N4/3P4/P3R3/8 b - - 0 1" ); } @@ -589,8 +587,8 @@ fn promotion_moves() { #[test] fn castling_reset() { let mut position = setup("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1"); - position.make_move(&Move::new(Square::A1, Square::A8, None)); - assert_eq!(position.fen(), "R3k2r/8/8/8/8/8/8/4K2R b Kk - 0 1"); + position.make_move(&Move::from_uci("a1a8").expect("valid move")); + assert_eq!(position.to_string(), "R3k2r/8/8/8/8/8/8/4K2R b Kk - 0 1"); } #[test] @@ -790,3 +788,45 @@ fn perft_cpw_challenge() { let position = setup("rnb1kbnr/pp1pp1pp/1qp2p2/8/Q1P5/N7/PP1PPPPP/1RB1KBNR b Kkq - 2 4"); assert_eq!(perft(&position, 7), 14794751816); } + +#[test] +fn repetition_hash() { + let mut position = setup("8/5k2/6p1/8/8/8/1p3P2/5K2 w - - 0 1"); + let initial_hash = position.compute_hash(); + position.make_move(&Move::from_uci("f1e2").expect("valid move")); + assert_ne!(initial_hash, position.compute_hash()); + position.make_move(&Move::from_uci("f7f6").expect("valid move")); + assert_ne!(initial_hash, position.compute_hash()); + position.make_move(&Move::from_uci("e2f1").expect("valid move")); + assert_ne!(initial_hash, position.compute_hash()); + position.make_move(&Move::from_uci("f6f7").expect("valid move")); + assert_eq!(position.to_string(), "8/5k2/6p1/8/8/8/1p3P2/5K2 w - - 4 3"); + assert_eq!(initial_hash, position.compute_hash()); +} + +#[test] +fn en_passant_hash() { + assert_ne!( + setup("6qk/8/8/3Pp3/8/8/K7/8 w - e6 0 1").compute_hash(), + setup("6qk/8/8/3Pp3/8/8/K7/8 w - - 0 1").compute_hash() + ); +} + +#[test] +fn castling_hash() { + let mut position = setup("rnbqk1nr/p3bppp/1p2p3/2ppP3/3P4/P7/1PP1NPPP/R1BQKBNR w KQkq - 0 7"); + let initial_hash = position.compute_hash(); + assert_ne!( + initial_hash, + setup("rnbqk1nr/p3bppp/1p2p3/2ppP3/3P4/P7/1PP1NPPP/R1BQKBNR w Qkq - 0 7").compute_hash(), + ); + position.make_move(&Move::from_uci("e1d2").expect("valid move")); + position.make_move(&Move::from_uci("e8d7").expect("valid move")); + position.make_move(&Move::from_uci("d2e1").expect("valid move")); + position.make_move(&Move::from_uci("d7e8").expect("valid move")); + assert_eq!( + position.to_string(), + "rnbqk1nr/p3bppp/1p2p3/2ppP3/3P4/P7/1PP1NPPP/R1BQKBNR w - - 4 9" + ); + assert_ne!(initial_hash, position.compute_hash()); +} diff --git a/tools/src/bin/extract_lc0_data.rs b/tools/src/bin/extract_lc0_data.rs index 8414c01cb..2b4bcb689 100644 --- a/tools/src/bin/extract_lc0_data.rs +++ b/tools/src/bin/extract_lc0_data.rs @@ -25,9 +25,10 @@ struct Args { /// Maximum number of samples to extract. #[arg(long)] limit: Option, - /// Only positions with |eval| <= q_threshold will be kept. Practically, distinguishing between - /// very high evals shouldn't be very important, because if an engine reaches that position, it - /// is likely to be winning/losing anyway. + /// Only positions with |eval| <= q_threshold will be kept. Practically, + /// distinguishing between very high evals shouldn't be very important, + /// because if an engine reaches that position, it is likely to be + /// winning/losing anyway. /// /// Q-value to CP conversion formula: /// @@ -36,8 +37,8 @@ struct Args { /// q = 0.9 corresponds to cp = 900 #[arg(long, default_value_t = 0.9)] q_threshold: f32, - /// Remove positions with less than min_pieces pieces on the board. This is useful, because - /// most tournaments allow using 6 man tablebases. + /// Remove positions with less than min_pieces pieces on the board. This is + /// useful, because most tournaments allow using 6 man tablebases. #[arg(long, default_value_t = 6)] min_pieces: u8, /// Drop positions where the best move is capturing a piece. It is likely to @@ -181,10 +182,10 @@ fn keep_sample(sample: &V6TrainingData, q_threshold: f32, filter_captures: bool) // TODO: Filter the capturing moves, positions in check and stalemates. - let board = pabi::chess::position::Position::empty(); - let best_move = - pabi::chess::core::Move::from_uci(pabi_tools::IDX_TO_MOVE[sample.best_idx as usize]); - // TODO: Just check the target square manually? + // let board = pabi::chess::position::Position::empty(); + // let best_move = + // pabi::chess::core::Move::from_uci(pabi_tools::IDX_TO_MOVE[sample.best_idx + // as usize]); TODO: Just check the target square manually? // TODO: Set the bitboards... // for &color in &[Color::White, Color::Black] {