stub: zobrist hash

no transposition table yet, but if the hash works it's coming
This commit is contained in:
dogeystamp 2024-11-16 21:36:39 -05:00
parent bec310c182
commit 30e20d1f66
5 changed files with 228 additions and 0 deletions

View File

@ -12,6 +12,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
*/
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)

129
src/hash.rs Normal file
View File

@ -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 <dogeystamp@disroot.org>
*/
//! 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<Square>) {
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);
}
}
}

View File

@ -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 {

View File

@ -13,6 +13,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
//! 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
}
}

76
src/random.rs Normal file
View File

@ -0,0 +1,76 @@
//! Rust port by dogeystamp <dogeystamp@disroot.org> 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<const N: usize>(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<const N: usize>(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<const N: usize, const M: usize>(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
}