From 30e20d1f6634ca0e5c22bf1d06633e3282d6d054 Mon Sep 17 00:00:00 2001 From: dogeystamp Date: Sat, 16 Nov 2024 21:36:39 -0500 Subject: [PATCH] stub: zobrist hash no transposition table yet, but if the hash works it's coming --- src/fen.rs | 2 + src/hash.rs | 129 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 10 ++++ src/movegen.rs | 11 +++++ src/random.rs | 76 +++++++++++++++++++++++++++++ 5 files changed, 228 insertions(+) create mode 100644 src/hash.rs create mode 100644 src/random.rs diff --git a/src/fen.rs b/src/fen.rs index b609fa0..7990e1f 100644 --- a/src/fen.rs +++ b/src/fen.rs @@ -12,6 +12,7 @@ Copyright © 2024 dogeystamp */ use crate::{Board, ColPiece, Color, Square, SquareIdx, BOARD_HEIGHT, BOARD_WIDTH}; +use crate::hash::Zobrist; pub const START_POSITION: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; @@ -239,6 +240,7 @@ impl FromFen for Board { // parser is always ready to receive another full move digit, // so there is no real "stop" state if matches!(parser_state, FenState::FullMove) { + Zobrist::toggle_board_info(&mut pos); Ok(pos) } else { Err(FenError::MissingFields) diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..24d1b5b --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,129 @@ +/* + +This file is part of chess_inator. + +chess_inator is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. + +chess_inator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with chess_inator. If not, see https://www.gnu.org/licenses/. + +Copyright © 2024 dogeystamp +*/ + +//! Zobrist hash implementation. + +use crate::random::{random_arr_2d_64, random_arr_64}; +use crate::{ + Board, CastleRights, ColPiece, Color, Square, BOARD_WIDTH, N_COLORS, N_PIECES, N_SQUARES, +}; + +const PIECE_KEYS: [[[u64; N_SQUARES]; N_PIECES]; N_COLORS] = + [random_arr_2d_64(11), random_arr_2d_64(22)]; + +// 4 bits in castle perms -> 16 keys +const CASTLE_KEYS: [u64; 16] = random_arr_64(33); + +// ep can be specified by the file +const EP_KEYS: [u64; BOARD_WIDTH] = random_arr_64(44); + +// current turn +const COL_KEY: [u64; N_COLORS] = random_arr_64(55); + +/// Zobrist hash state. +/// +/// This is not synced to board state, so ensure that all changes made are reflected in the hash +/// too. +#[derive(PartialEq, Eq, Clone, Copy, Default, Debug)] +pub(crate) struct Zobrist { + hash: u64, +} + +impl Zobrist { + /// Toggle a piece. + pub(crate) fn toggle_pc(&mut self, pc: &ColPiece, sq: &Square) { + let key = PIECE_KEYS[pc.col as usize][pc.pc as usize][usize::from(sq.0)]; + self.hash ^= key; + } + + /// Toggle an en-passant target square (only square file is used). + pub(crate) fn toggle_ep(&mut self, sq: Option) { + if let Some(sq) = sq { + let (_r, c) = sq.to_row_col(); + self.hash ^= EP_KEYS[c]; + } + } + + /// Toggle castle rights key. + pub(crate) fn toggle_castle(&mut self, castle: &CastleRights) { + let bits = ((0x1) & castle.0[0].k as u8) + | ((0x2) & castle.0[0].q as u8) + | (0x4) & castle.0[1].k as u8 + | (0x8) & castle.0[1].q as u8; + + self.hash ^= CASTLE_KEYS[bits as usize]; + } + + /// Toggle player to move. + pub(crate) fn toggle_turn(&mut self, turn: Color) { + self.hash ^= COL_KEY[turn as usize]; + } + + /// Toggle all of castling rights, en passant and player to move. + /// + /// This is done because it's simpler to do this every time at the start and end of a + /// move/unmove rather than keep track of when castling and ep square and whatever rights + /// change. Piece moves, unlike this information, have a centralized implementation. + pub(crate) fn toggle_board_info(pos: &mut Board) { + pos.zobrist.toggle_ep(pos.ep_square); + pos.zobrist.toggle_castle(&pos.castle); + pos.zobrist.toggle_turn(pos.turn); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fen::FromFen; + use crate::movegen::{FromUCIAlgebraic, Move}; + + /// Zobrist hashes of the same positions should be the same. (basic sanity test) + #[test] + fn test_zobrist_equality() { + let test_cases = [ + ( + "4k2r/8/8/8/8/8/8/R3K3 w Qk - 0 1", + "4k2r/8/8/8/8/8/8/2KR4 b k - 1 1", + "e1c1", + ), + ( + "4k2r/8/8/8/8/8/8/R3K3 b Qk - 0 1", + "5rk1/8/8/8/8/8/8/R3K3 w Q - 1 2", + "e8g8", + ), + ( + "4k3/8/8/8/3p4/8/4P3/4K3 w - - 0 1", + "4k3/8/8/8/3pP3/8/8/4K3 b - e3 0 1", + "e2e4", + ), + ( + "4k3/8/8/8/3pP3/8/8/4K3 b - e3 0 1", + "4k3/8/8/8/8/4p3/8/4K3 w - - 0 2", + "d4e3", + ), + ]; + for (pos1_fen, pos2_fen, mv_uci) in test_cases { + eprintln!("tc: {}", pos1_fen); + let mut pos1 = Board::from_fen(pos1_fen).unwrap(); + let hash1_orig = pos1.zobrist; + eprintln!("refreshing board 2 '{}'", pos2_fen); + let pos2 = Board::from_fen(pos2_fen).unwrap(); + eprintln!("making mv {}", mv_uci); + let mv = Move::from_uci_algebraic(mv_uci).unwrap(); + let anti_mv = mv.make(&mut pos1); + assert_eq!(pos1.zobrist, pos2.zobrist); + anti_mv.unmake(&mut pos1); + assert_eq!(pos1.zobrist, hash1_orig); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index e0f5ecd..25dcc89 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,10 +19,13 @@ use std::str::FromStr; pub mod eval; pub mod fen; +mod hash; pub mod movegen; +mod random; pub mod search; use crate::fen::{FromFen, ToFen, START_POSITION}; +use crate::hash::Zobrist; use eval::eval_score::EvalScores; const BOARD_WIDTH: usize = 8; @@ -457,6 +460,9 @@ pub struct Board { /// Counters for evaluation. eval: EvalScores, + + /// Hash state to incrementally update. + zobrist: Zobrist, } impl Board { @@ -477,6 +483,7 @@ impl Board { pl[pc.into()].on_sq(sq); *self.mail.sq_mut(sq) = Some(pc); self.eval.add_piece(&pc, &sq); + self.zobrist.toggle_pc(&pc, &sq); dest_pc } @@ -495,6 +502,7 @@ impl Board { pl[pc.into()].off_sq(sq); *self.mail.sq_mut(sq) = None; self.eval.del_piece(&pc, &sq); + self.zobrist.toggle_pc(&pc, &sq); Some(pc) } else { None @@ -529,9 +537,11 @@ impl Board { ep_square: self.ep_square.map(|sq| sq.mirror_vert()), castle: CastleRights(self.castle.0), eval: Default::default(), + zobrist: Zobrist::default(), }; new_board.castle.0.reverse(); + Zobrist::toggle_board_info(&mut new_board); for sq in Board::squares() { let opt_pc = self.get_piece(sq.mirror_vert()).map(|pc| ColPiece { diff --git a/src/movegen.rs b/src/movegen.rs index 0bb9fad..3a8b597 100644 --- a/src/movegen.rs +++ b/src/movegen.rs @@ -13,6 +13,7 @@ Copyright © 2024 dogeystamp //! Move generation. +use crate::hash::Zobrist; use crate::fen::ToFen; use crate::{ Board, CastleRights, ColPiece, Color, Piece, Square, SquareError, BOARD_HEIGHT, BOARD_WIDTH, @@ -97,6 +98,8 @@ pub struct AntiMove { impl AntiMove { /// Undo the move. pub fn unmake(self, pos: &mut Board) { + Zobrist::toggle_board_info(pos); + pos.move_piece(self.dest, self.src); pos.half_moves = self.half_moves; pos.castle = self.castle; @@ -146,6 +149,8 @@ impl AntiMove { pos.move_piece(rook_dest, rook_src); } } + + Zobrist::toggle_board_info(pos); } } @@ -179,6 +184,9 @@ impl Move { ep_square: pos.ep_square, }; + // undo hashes (we will update them at the end of this function) + Zobrist::toggle_board_info(pos); + // reset en passant let ep_square = pos.ep_square; pos.ep_square = None; @@ -360,6 +368,9 @@ impl Move { pos.turn = pos.turn.flip(); + // redo hashes (we undid them at the start of this function) + Zobrist::toggle_board_info(pos); + anti_move } } diff --git a/src/random.rs b/src/random.rs new file mode 100644 index 0000000..77c844d --- /dev/null +++ b/src/random.rs @@ -0,0 +1,76 @@ +//! Rust port by dogeystamp of +//! the pcg64 dxsm random number generator (https://dotat.at/@/2023-06-21-pcg64-dxsm.html) + +struct Pcg64Random { + state: u128, + inc: u128, +} + +/// Generates an array of random numbers. +/// +/// The `rng` parameter only sets the initial state. This function is deterministic and pure. +/// +/// # Returns +/// +/// The array of random numbers, plus the RNG state at the end. +const fn pcg64_dxsm(mut rng: Pcg64Random) -> ([u64; N], Pcg64Random) { + let mut ret = [0; N]; + + const MUL: u64 = 15750249268501108917; + + let mut i = 0; + while i < N { + let state: u128 = rng.state; + rng.state = state.wrapping_mul(MUL as u128).wrapping_add(rng.inc); + let mut hi: u64 = (state >> 64) as u64; + let lo: u64 = (state | 1) as u64; + hi ^= hi >> 32; + hi &= MUL; + hi ^= hi >> 48; + hi = hi.wrapping_mul(lo); + ret[i] = hi; + + i += 1; + } + + (ret, rng) +} + +/// Make an RNG state "sane". +const fn pcg64_seed(mut rng: Pcg64Random) -> Pcg64Random { + // ensure rng.inc is odd + rng.inc = (rng.inc << 1) | 1; + rng.state += rng.inc; + // one iteration of random + let (_, rng) = pcg64_dxsm::<1>(rng); + rng +} + +/// Generate array of random numbers, based on a seed. +/// +/// This function is pure and deterministic, and also works at compile-time rather than at runtime. +/// +/// Example (generate 10 random numbers): +/// +///```rust +/// const ARR: [u64; 10] = random_arr_64(123456); +///``` +pub const fn random_arr_64(seed: u128) -> [u64; N] { + let rng = pcg64_seed(Pcg64Random { + // chosen by fair dice roll + state: 24437033748623976104561743679864923857, + inc: seed, + }); + pcg64_dxsm(rng).0 +} + +/// Generate 2D array of random numbers based on a seed. +pub const fn random_arr_2d_64(seed: u128) -> [[u64; N]; M] { + let mut ret = [[0; N]; M]; + let mut i = 0; + while i < M { + ret[i] = random_arr_64(seed); + i += 1; + } + ret +}