From 92e2c4f685f2538233449e455071a00654907f1c Mon Sep 17 00:00:00 2001 From: William Speirs Date: Sun, 28 Mar 2021 15:29:47 -0400 Subject: [PATCH 1/3] Init start at adding Zobrist hashes --- Cargo.toml | 6 +++- src/board.rs | 16 ++++++++- src/build.rs | 75 ++++++++++++++++++++++++++++++++++---- src/lib.rs | 1 + src/movelist.rs | 4 +-- src/position.rs | 17 ++++++++- src/types.rs | 29 +++++++++++++++ src/zobrist.rs | 95 +++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 src/zobrist.rs diff --git a/Cargo.toml b/Cargo.toml index 2c411332..e068aa99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,12 +23,16 @@ harness = false bench = false [dependencies] +arrayvec = "0.6" bitflags = "1.2" btoi = "0.4" -arrayvec = "0.5" [dev-dependencies] iai = "0.1" +rand = "0.8" + +[build-dependencies] +rand = { version="0.8", features=["std_rng"] } [package.metadata.docs.rs] all-features = true diff --git a/src/board.rs b/src/board.rs index 36c5e725..7b09ae9a 100644 --- a/src/board.rs +++ b/src/board.rs @@ -18,7 +18,7 @@ use std::fmt; use std::fmt::Write; use std::iter::FromIterator; -use crate::attacks; +use crate::{attacks, zobrist}; use crate::bitboard::Bitboard; use crate::color::{Color, ByColor}; use crate::square::{File, Rank, Square}; @@ -306,6 +306,20 @@ impl Board { white: self.material_side(Color::White), } } + + /// Computes the zobrist hash of the pieces on the board + /// This is *not* the complete hash; call Chess::zobrist + pub(crate) fn zobrist(&self) -> u64 { + let mut ret :u64 = 0; + + for square in (0..64).into_iter().map(|i| Square::new(i)) { + if let Some(piece) = self.piece_at(square) { + ret ^= zobrist::square(square, piece); + } + } + + ret + } } impl Default for Board { diff --git a/src/build.rs b/src/build.rs index 578c0f13..6137f98f 100644 --- a/src/build.rs +++ b/src/build.rs @@ -22,6 +22,9 @@ use std::io; use std::io::Write; use std::path::Path; +use rand::prelude::*; + + mod color; mod util; mod types; @@ -94,9 +97,9 @@ fn dump_slice(w: &mut W, name: &str, tname: &str, writeln!(w, "];") } -fn dump_table(w: &mut W, name: &str, tname: &str, table: &[[T; 64]; 64]) -> io::Result<()> { +fn dump_table(w: &mut W, name: &str, tname: &str, table: &[[T; COLS]; ROWS]) -> io::Result<()> { writeln!(w, "#[allow(clippy::unreadable_literal)]")?; - write!(w, "static {}: [[{}; 64]; 64] = [", name, tname)?; + write!(w, "static {}: [[{}; {}]; {}] = [", name, tname, COLS, ROWS)?; for row in table.iter() { write!(w, "[")?; for column in row.iter().cloned() { @@ -108,12 +111,18 @@ fn dump_table(w: &mut W, name: &str, tname: &str, } fn main() -> io::Result<()> { - // generate attacks.rs let out_dir = env::var("OUT_DIR").expect("got OUT_DIR"); - let dest_path = Path::new(&out_dir).join("attacks.rs"); - let mut f = File::create(&dest_path).expect("created attacks.rs"); + + // generate attacks.rs + let attacks_path = Path::new(&out_dir).join("attacks.rs"); + let mut f = File::create(&attacks_path).expect("created attacks.rs"); generate_basics(&mut f)?; - generate_sliding_attacks(&mut f) + generate_sliding_attacks(&mut f)?; + + // generate zobrist.rs + let zobrist_path = Path::new(&out_dir).join("zobrist.rs"); + let mut f = File::create(&zobrist_path).expect("error creating zobrist.rs"); + generate_zobrist(&mut f) } fn generate_basics(f: &mut W) -> io::Result<()> { @@ -166,3 +175,57 @@ fn generate_sliding_attacks(f: &mut W) -> io::Result<()> { dump_slice(f, "ATTACKS", "u64", &attacks) } + +// inspiration from this implementation taken from: https://github.com/sfleischman105/Pleco +fn generate_zobrist(f: &mut W) -> io::Result<()> { + let seed = 0x30b3_1137_bb45_7b1b_u64; + let mut rnd = StdRng::seed_from_u64(seed); + + let mut piece_square :[[u64; 16]; 64] = [[0; 16]; 64]; + + // generate random values for the piece-square table + for row in piece_square.iter_mut() { + for col in row.iter_mut() { + *col = rnd.gen::(); + } + } + + dump_table(f, "PIECE_SQUARE", "u64", &piece_square)?; + + + // generate random values for enpassant + let mut enpassant :[u64; 8] = [0; 8]; + + for file in enpassant.iter_mut() { + *file = rnd.gen::(); + } + + dump_slice(f, "ENPASSANT", "u64", &enpassant); + + + // generate random values for castling + let mut castle :[u64; 16] = [0; 16]; + + for c in 0..16_u64 { + let mut board = Bitboard(c); + + while let Some(square) = board.pop_back() { + let mut k :u64 = castle[1 << square as usize]; + + if k == 0 { + k = rnd.gen::(); + } + + castle[c as usize] ^= k; + } + } + + dump_slice(f, "CASTLE", "u64", &castle); + + + // generate two random values: side & no-pawns + writeln!(f, "const SIDE :u64 = 0x{:x}_u64;", rnd.gen::()); + writeln!(f, "const NO_PAWNS :u64 = 0x{:x}_u64;", rnd.gen::()); + + Ok( () ) +} diff --git a/src/lib.rs b/src/lib.rs index a2c241cc..7cd297dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,6 +86,7 @@ mod movelist; mod magics; mod perft; mod util; +mod zobrist; pub mod attacks; pub mod bitboard; diff --git a/src/movelist.rs b/src/movelist.rs index e8935d8c..838763f7 100644 --- a/src/movelist.rs +++ b/src/movelist.rs @@ -37,6 +37,6 @@ use arrayvec::ArrayVec; pub type MoveList = MoveListImpl; #[cfg(feature = "variant")] -type MoveListImpl = ArrayVec<[Move; 512]>; +type MoveListImpl = ArrayVec; #[cfg(not(feature = "variant"))] -type MoveListImpl = ArrayVec<[Move; 256]>; +type MoveListImpl = ArrayVec; diff --git a/src/position.rs b/src/position.rs index ac9c96b3..ba7ea0b8 100644 --- a/src/position.rs +++ b/src/position.rs @@ -433,6 +433,7 @@ pub struct Chess { ep_square: Option, halfmoves: u32, fullmoves: NonZeroU32, + zobrist: u64 // zobrist hash value of the current game/state } impl Chess { @@ -464,6 +465,9 @@ impl Chess { } }; + // compute the zobrist hash + let mut zobrist = board.zobrist(); + let pos = Chess { board, turn, @@ -471,23 +475,34 @@ impl Chess { ep_square, halfmoves: setup.halfmoves(), fullmoves: setup.fullmoves(), + zobrist }; errors |= validate(&pos); (pos, errors) } + + /// Computes the zobrist hash of the current game state + pub fn zobrist(&self) -> u64 { + self.zobrist + } } impl Default for Chess { fn default() -> Chess { + let board = Board::default(); + + let mut zobrist = board.zobrist(); + Chess { - board: Board::default(), + board, turn: White, castles: Castles::default(), ep_square: None, halfmoves: 0, fullmoves: NonZeroU32::new(1).unwrap(), + zobrist } } } diff --git a/src/types.rs b/src/types.rs index 12f33285..132e3293 100644 --- a/src/types.rs +++ b/src/types.rs @@ -203,6 +203,19 @@ impl Piece { } } +// we implement Into instead of From (or TryFrom) because we only need one-way conversion +// Piece -> usize +impl Into for Piece { + #[inline(always)] + fn into(self) -> usize { + if self.color == Color::Black { + self.role as usize + 8_usize + } else { + self.role as usize + } + } +} + /// Information about a move. #[derive(Clone, Eq, PartialEq, Hash, Debug)] #[repr(align(4))] @@ -383,6 +396,22 @@ impl CastlingSide { pub fn rook_to(self, color: Color) -> Square { Square::from_coords(self.rook_to_file(), color.backrank()) } + + // used for zobrist hashing + #[inline(always)] + pub fn to_usize(self, color: Color) -> usize { + if color == Color::White { + match self { + CastlingSide::KingSide => 0, + CastlingSide::QueenSide => 1, + } + } else { + match self { + CastlingSide::KingSide => 2, + CastlingSide::QueenSide => 3 + } + } + } } /// `Standard` or `Chess960`. diff --git a/src/zobrist.rs b/src/zobrist.rs new file mode 100644 index 00000000..17fe6a14 --- /dev/null +++ b/src/zobrist.rs @@ -0,0 +1,95 @@ +use crate::{Square, Piece, CastlingSide, Color}; + + +include!(concat!(env!("OUT_DIR"), "/zobrist.rs")); // generated by build.rs + +#[inline(always)] +pub fn square(sq: Square, piece: Piece) -> u64 { + PIECE_SQUARE[sq as usize][>::into(piece)] +} + +#[inline(always)] +pub fn enpassant(sq: Square) -> u64 { + ENPASSANT[sq.file() as usize] +} + +#[inline(always)] +pub fn castle(castle: CastlingSide, color :Color) -> u64 { + CASTLE[castle.to_usize(color)] +} + +#[inline(always)] +pub fn side() -> u64 { + SIDE +} + +#[inline(always)] +pub fn z_no_pawns() -> u64 { + NO_PAWNS +} + +#[cfg(test)] +mod zobrist_tests { + use crate::{Square, Piece, Chess, Position}; + use crate::fen::epd; + use crate::zobrist::square; + use std::collections::{HashSet, HashMap}; + use rand::Rng; + + #[test] + fn square_test() { + let mut hashes = HashSet::new(); + + // go through each square and piece combo and make sure they're unique + for sq in (0..64).into_iter().map(|i| Square::new(i)) { + for piece in ['p','n','b','r','q','k','P','N','B','R','Q','K'].iter().map(|c| Piece::from_char(*c).unwrap()) { + let h = square(sq, piece); + + if hashes.contains(&h) { + panic!("Zobrist square({}, {:?}) = {} already exists!!!", sq, piece, h); + } else { + hashes.insert(h); + } + } + } + + println!("LEN: {}", hashes.len()); + } + + #[test] + fn moves_test() { + // randomly move through a bunch of moves, ensuring we get different zobrist hashes + const MAX_MOVES :usize = 10_000; + let mut hash_fen :HashMap = HashMap::new(); + let mut chess = Chess::default(); + let mut rnd = rand::thread_rng(); + + while hash_fen.len() < MAX_MOVES { + // generate and collect all the moves + let moves = chess.legal_moves(); + let mv_i = rnd.gen_range(0..moves.len()); + + // play a random move + chess.play_unchecked(&moves[mv_i]); + + // get the zobrist hash value + let z = chess.zobrist(); + let fen = epd(&chess); + + if let Some(existing_fen) = hash_fen.get(&z) { + // found a collision!!! + if fen != *existing_fen { + panic!("ZOBRIST COLLISION AFTER {}: 0x{:016x}: {} != {}", hash_fen.len(), z, fen, existing_fen); + } + } else { + hash_fen.insert(z, fen); + } + + // check to see if the game is over, and if so restart it + if chess.is_game_over() { + chess = Chess::default(); + } + } + } + +} \ No newline at end of file From ee53fe352b1984c3a62c4e9b9a31bd9de80f5756 Mon Sep 17 00:00:00 2001 From: William Speirs Date: Wed, 31 Mar 2021 21:16:47 -0400 Subject: [PATCH 2/3] Added zobrist to do_move --- src/build.rs | 22 +++------ src/position.rs | 124 +++++++++++++++++++++++++++++++++++++++++++----- src/zobrist.rs | 100 ++++++++++++++++++++++++++++++++------ 3 files changed, 204 insertions(+), 42 deletions(-) diff --git a/src/build.rs b/src/build.rs index 6137f98f..1cea1262 100644 --- a/src/build.rs +++ b/src/build.rs @@ -200,32 +200,22 @@ fn generate_zobrist(f: &mut W) -> io::Result<()> { *file = rnd.gen::(); } - dump_slice(f, "ENPASSANT", "u64", &enpassant); + dump_slice(f, "ENPASSANT", "u64", &enpassant).expect("Error dumping enpassant slice"); // generate random values for castling - let mut castle :[u64; 16] = [0; 16]; + let mut castle :[u64; 4] = [0; 4]; - for c in 0..16_u64 { - let mut board = Bitboard(c); - - while let Some(square) = board.pop_back() { - let mut k :u64 = castle[1 << square as usize]; - - if k == 0 { - k = rnd.gen::(); - } - - castle[c as usize] ^= k; - } + for castle in castle.iter_mut() { + *castle = rnd.gen::(); } - dump_slice(f, "CASTLE", "u64", &castle); + dump_slice(f, "CASTLE", "u64", &castle).expect("Error dumping castle slice"); // generate two random values: side & no-pawns writeln!(f, "const SIDE :u64 = 0x{:x}_u64;", rnd.gen::()); - writeln!(f, "const NO_PAWNS :u64 = 0x{:x}_u64;", rnd.gen::()); + // writeln!(f, "const NO_PAWNS :u64 = 0x{:x}_u64;", rnd.gen::()); Ok( () ) } diff --git a/src/position.rs b/src/position.rs index ba7ea0b8..cad7e232 100644 --- a/src/position.rs +++ b/src/position.rs @@ -20,7 +20,7 @@ use std::num::NonZeroU32; use bitflags::bitflags; -use crate::attacks; +use crate::{attacks, File}; use crate::board::Board; use crate::bitboard::Bitboard; use crate::color::{ByColor, Color}; @@ -30,6 +30,7 @@ use crate::types::{CastlingSide, CastlingMode, Move, Piece, Role, RemainingCheck use crate::material::Material; use crate::setup::{Castles, EpSquare, Setup, SwapTurn}; use crate::movelist::MoveList; +use crate::zobrist; /// Outcome of a game. #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] @@ -465,9 +466,34 @@ impl Chess { } }; - // compute the zobrist hash + // compute the zobrist hash for the board let mut zobrist = board.zobrist(); + // set castling + if castles.has(Color::White, CastlingSide::KingSide) { + zobrist ^= zobrist::castle(Color::White, CastlingSide::KingSide); + } + + if castles.has(Color::White, CastlingSide::QueenSide) { + zobrist ^= zobrist::castle(Color::White, CastlingSide::QueenSide); + } + + if castles.has(Color::Black, CastlingSide::KingSide) { + zobrist ^= zobrist::castle(Color::Black, CastlingSide::KingSide); + } + + if castles.has(Color::Black, CastlingSide::QueenSide) { + zobrist ^= zobrist::castle(Color::Black, CastlingSide::QueenSide); + } + + if let Some(sq) = ep_square { + zobrist ^= zobrist::enpassant(sq.0); + } + + if turn == Color::Black { + zobrist ^= zobrist::side(); + } + let pos = Chess { board, turn, @@ -495,7 +521,13 @@ impl Default for Chess { let mut zobrist = board.zobrist(); - Chess { + // add in all the castling + zobrist ^= zobrist::castle(Color::White, CastlingSide::KingSide); + zobrist ^= zobrist::castle(Color::White, CastlingSide::QueenSide); + zobrist ^= zobrist::castle(Color::Black, CastlingSide::KingSide); + zobrist ^= zobrist::castle(Color::Black, CastlingSide::QueenSide); + + Chess { board, turn: White, castles: Castles::default(), @@ -528,8 +560,8 @@ impl FromSetup for Chess { impl Position for Chess { fn play_unchecked(&mut self, m: &Move) { do_move(&mut self.board, &mut self.turn, &mut self.castles, - &mut self.ep_square, &mut self.halfmoves, - &mut self.fullmoves, m); + &mut self.ep_square, &mut self.zobrist, + &mut self.halfmoves, &mut self.fullmoves, m); } fn castles(&self) -> &Castles { @@ -1740,11 +1772,16 @@ fn do_move(board: &mut Board, turn: &mut Color, castles: &mut Castles, ep_square: &mut Option, + zobrist: &mut u64, halfmoves: &mut u32, fullmoves: &mut NonZeroU32, m: &Move) { let color = *turn; - ep_square.take(); + + // we need to "remove" the old EP square if there is one + if let Some(sq) = ep_square.take() { + *zobrist ^= zobrist::enpassant(sq.0); + } *halfmoves = if m.is_zeroing() { 0 @@ -1760,36 +1797,99 @@ fn do_move(board: &mut Board, *ep_square = from.offset(-8).map(EpSquare); } + // if we have an enpassant square, add it to the hash + if let Some(sq) = ep_square { + *zobrist ^= zobrist::enpassant(sq.0); + } + if role == Role::King { + // if we have the castling ability, then need to "remove" it + if castles.has(color, CastlingSide::KingSide) { + *zobrist ^= zobrist::castle(color, CastlingSide::KingSide); + } + + if castles.has(color, CastlingSide::QueenSide) { + *zobrist ^= zobrist::castle(color, CastlingSide::QueenSide); + } + castles.discard_side(color); } else if role == Role::Rook { + let side = CastlingSide::from_queen_side(from.file() == File::A); + + if castles.has(color, side) { + *zobrist ^= zobrist::castle(color, side); + } + castles.discard_rook(from); } if capture == Some(Role::Rook) { + let side = CastlingSide::from_queen_side(to.file() == File::A); + + if castles.has(color, side) { + *zobrist ^= zobrist::castle(color, side); + } + castles.discard_rook(to); } let promoted = board.promoted().contains(from) || promotion.is_some(); + // remove the piece at the from square + *zobrist ^= zobrist::square(from, board.piece_at(from).unwrap()); board.discard_piece_at(from); - board.set_piece_at(to, promotion.map_or(role.of(color), |p| p.of(color)), promoted); + + // remove the piece at the to square if there is one + if let Some(to_piece) = board.piece_at(to) { + *zobrist ^= zobrist::square(to, to_piece); + } + + let to_piece = promotion.map_or(role.of(color), |p| p.of(color)); + board.set_piece_at(to, to_piece, promoted); + *zobrist ^= zobrist::square(to, to_piece); // add in the moving piece or promotion }, Move::Castle { king, rook } => { let side = CastlingSide::from_queen_side(rook < king); + board.discard_piece_at(king); board.discard_piece_at(rook); - board.set_piece_at(Square::from_coords(side.rook_to_file(), rook.rank()), color.rook(), false); - board.set_piece_at(Square::from_coords(side.king_to_file(), king.rank()), color.king(), false); + + *zobrist ^= zobrist::square(king, color.king()); + *zobrist ^= zobrist::square(rook, color.rook()); + + let rook_sq = Square::from_coords(side.rook_to_file(), rook.rank()); + let king_sq = Square::from_coords(side.king_to_file(), king.rank()); + board.set_piece_at(rook_sq, color.rook(), false); + board.set_piece_at(king_sq, color.king(), false); + + *zobrist ^= zobrist::square(rook_sq, color.rook()); + *zobrist ^= zobrist::square(king_sq, color.king()); + castles.discard_side(color); + + if castles.has(color, CastlingSide::KingSide) { + *zobrist ^= zobrist::castle(color, CastlingSide::KingSide); + } + + if castles.has(color, CastlingSide::QueenSide) { + *zobrist ^= zobrist::castle(color, CastlingSide::QueenSide); + } } Move::EnPassant { from, to } => { - board.discard_piece_at(Square::from_coords(to.file(), from.rank())); // captured pawn + let captured_pawn_sq = Square::from_coords(to.file(), from.rank()); + board.discard_piece_at(captured_pawn_sq); // captured pawn + *zobrist ^= zobrist::square(captured_pawn_sq, (!color).pawn()); + board.discard_piece_at(from); + *zobrist ^= zobrist::square(from, color.pawn()); + board.set_piece_at(to, color.pawn(), false); + *zobrist ^= zobrist::square(to, color.pawn()); } Move::Put { role, to } => { - board.set_piece_at(to, Piece { color, role }, false); + let piece = Piece { color, role }; + board.set_piece_at(to, piece, false); + *zobrist ^= zobrist::square(to, piece); } } @@ -1798,6 +1898,8 @@ fn do_move(board: &mut Board, } *turn = !color; + // *zobrist ^= zobrist::side(); + *zobrist ^= 0x01; } fn validate(pos: &P) -> PositionErrorKinds { diff --git a/src/zobrist.rs b/src/zobrist.rs index 17fe6a14..d25f633f 100644 --- a/src/zobrist.rs +++ b/src/zobrist.rs @@ -14,8 +14,14 @@ pub fn enpassant(sq: Square) -> u64 { } #[inline(always)] -pub fn castle(castle: CastlingSide, color :Color) -> u64 { - CASTLE[castle.to_usize(color)] +pub fn castle(color :Color, castle: CastlingSide) -> u64 { + // there are 4 values in CASTLE: WHITE_KING[0], WHITE_QUEEN[1], BLACK_KING[2], BLACK_QUEEN[3] + match (color, castle) { + (Color::White, CastlingSide::KingSide) => CASTLE[0], + (Color::White, CastlingSide::QueenSide) => CASTLE[1], + (Color::Black, CastlingSide::KingSide) => CASTLE[2], + (Color::Black, CastlingSide::QueenSide) => CASTLE[3] + } } #[inline(always)] @@ -23,18 +29,18 @@ pub fn side() -> u64 { SIDE } -#[inline(always)] -pub fn z_no_pawns() -> u64 { - NO_PAWNS -} +// #[inline(always)] +// pub fn z_no_pawns() -> u64 { +// NO_PAWNS +// } #[cfg(test)] mod zobrist_tests { - use crate::{Square, Piece, Chess, Position}; - use crate::fen::epd; + use crate::{Square, Piece, Chess, Position, CastlingMode, Move}; + use crate::fen::{epd, Fen}; use crate::zobrist::square; use std::collections::{HashSet, HashMap}; - use rand::Rng; + use rand::prelude::*; #[test] fn square_test() { @@ -56,21 +62,40 @@ mod zobrist_tests { println!("LEN: {}", hashes.len()); } + #[test] + fn fen_test() { + let setup1 :Fen = "8/8/8/8/p7/P7/6k1/2K5 w - -".parse().expect("Error parsing FEN"); + let setup2 :Fen = "8/8/8/8/p7/P7/6k1/2K5 w - -".parse().expect("Error parsing FEN"); + + let game1 :Chess = setup1.position(CastlingMode::Standard).expect("Error setting up game"); + let game2 :Chess = setup2.position(CastlingMode::Standard).expect("Error setting up game"); + + println!("0x{:x} != 0x{:x}", game1.zobrist(), game2.zobrist()); + + assert_ne!(game1.zobrist(), game2.zobrist()); + } + #[test] fn moves_test() { // randomly move through a bunch of moves, ensuring we get different zobrist hashes - const MAX_MOVES :usize = 10_000; + const MAX_MOVES :usize = 100_000; let mut hash_fen :HashMap = HashMap::new(); + let mut hash_moves :HashMap> = HashMap::new(); + let mut moves = Vec::new(); let mut chess = Chess::default(); - let mut rnd = rand::thread_rng(); + let mut rnd = StdRng::seed_from_u64(0x30b3_1137_bb45_7b1b_u64); while hash_fen.len() < MAX_MOVES { // generate and collect all the moves - let moves = chess.legal_moves(); - let mv_i = rnd.gen_range(0..moves.len()); + let legal_moves = chess.legal_moves(); + let mv_i = rnd.gen_range(0..legal_moves.len()); + let mv = legal_moves[mv_i].clone(); // play a random move - chess.play_unchecked(&moves[mv_i]); + chess.play_unchecked(&mv); + + // add to our current list of moves + moves.push(mv); // get the zobrist hash value let z = chess.zobrist(); @@ -79,17 +104,62 @@ mod zobrist_tests { if let Some(existing_fen) = hash_fen.get(&z) { // found a collision!!! if fen != *existing_fen { - panic!("ZOBRIST COLLISION AFTER {}: 0x{:016x}: {} != {}", hash_fen.len(), z, fen, existing_fen); + // check to see if the FENs are also the same + let setup1 :Fen = fen.parse().expect("Error parsing FEN"); + let setup2 :Fen = existing_fen.parse().expect("Error parsing FEN"); + + let game1 :Chess = setup1.position(CastlingMode::Standard).expect("Error setting up game"); + let game2 :Chess = setup2.position(CastlingMode::Standard).expect("Error setting up game"); + + if game1.zobrist() == game2.zobrist() { + panic!("COLLISION FOUND FOR 2 FENs: {} (0x{:x}) & {} (0x{:x})", fen, game1.zobrist(), existing_fen, game2.zobrist()); + } else { + let mvs1 = hash_moves.get(&z).unwrap(); + let mvs2 = moves; + let mut game = Chess::default(); + + let mut panic_str = format!("ZOBRIST COLLISION AFTER {}: 0x{:016x} ({} {})\n", hash_fen.len(), z, mvs1.len(), mvs2.len()); + + for (i, (mv1, mv2)) in mvs1.iter().zip(mvs2.iter()).enumerate() { + if mv1 == mv2 { + game.play_unchecked(mv1); + panic_str += format!("{:03}: {:?} -> {}\t0x{:08x}\n", i, mv1, epd(&game), game.zobrist()).as_str(); + } else { + panic_str += format!("DIFF {:03}: {:?} {:?}", i, mv1, mv2).as_str(); + break + } + } + + if mvs1.len() > mvs2.len() { + for (i, mv1) in mvs1.iter().skip(mvs2.len()).enumerate() { + game.play_unchecked(mv1); + panic_str += format!("MV1 {:03}: {:?} -> {}\t0x{:08x}\n", i + mvs2.len(), mv1, epd(&game), game.zobrist()).as_str(); + } + } else { + for (i, mv2) in mvs2.iter().skip(mvs1.len()).enumerate() { + game.play_unchecked(mv2); + panic_str += format!("MV2 {:03}: {:?} -> {}\t0x{:08x}\n", i + mvs1.len(), mv2, epd(&game), game.zobrist()).as_str(); + } + } + + panic!("{}", panic_str); + } } } else { + // keep around the FEN of the board, and also the moves that got us there hash_fen.insert(z, fen); + hash_moves.insert(z, moves.clone()); } // check to see if the game is over, and if so restart it if chess.is_game_over() { chess = Chess::default(); + moves.clear(); + println!("{} of {}", hash_fen.len(), MAX_MOVES); } } + + println!("Found {} unique hashes for boards", hash_fen.len()); } } \ No newline at end of file From 64520aed499f797dd06fccfac1cdec596fe377fb Mon Sep 17 00:00:00 2001 From: William Speirs Date: Thu, 17 Jun 2021 17:11:20 -0400 Subject: [PATCH 3/3] Changed Zobrist implementation to be a separate struct --- src/board.rs | 16 +-- src/position.rs | 137 ++--------------------- src/zobrist.rs | 281 +++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 266 insertions(+), 168 deletions(-) diff --git a/src/board.rs b/src/board.rs index 7b09ae9a..36c5e725 100644 --- a/src/board.rs +++ b/src/board.rs @@ -18,7 +18,7 @@ use std::fmt; use std::fmt::Write; use std::iter::FromIterator; -use crate::{attacks, zobrist}; +use crate::attacks; use crate::bitboard::Bitboard; use crate::color::{Color, ByColor}; use crate::square::{File, Rank, Square}; @@ -306,20 +306,6 @@ impl Board { white: self.material_side(Color::White), } } - - /// Computes the zobrist hash of the pieces on the board - /// This is *not* the complete hash; call Chess::zobrist - pub(crate) fn zobrist(&self) -> u64 { - let mut ret :u64 = 0; - - for square in (0..64).into_iter().map(|i| Square::new(i)) { - if let Some(piece) = self.piece_at(square) { - ret ^= zobrist::square(square, piece); - } - } - - ret - } } impl Default for Board { diff --git a/src/position.rs b/src/position.rs index 5167f7ea..e76eef5c 100644 --- a/src/position.rs +++ b/src/position.rs @@ -20,7 +20,7 @@ use std::num::NonZeroU32; use bitflags::bitflags; -use crate::{attacks, File}; +use crate::attacks; use crate::board::Board; use crate::bitboard::Bitboard; use crate::color::{ByColor, Color}; @@ -30,7 +30,6 @@ use crate::types::{CastlingSide, CastlingMode, Move, Piece, Role, RemainingCheck use crate::material::Material; use crate::setup::{Castles, EpSquare, Setup, SwapTurn}; use crate::movelist::MoveList; -use crate::zobrist; /// Outcome of a game. #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] @@ -434,7 +433,6 @@ pub struct Chess { ep_square: Option, halfmoves: u32, fullmoves: NonZeroU32, - zobrist: u64 // zobrist hash value of the current game/state } impl Chess { @@ -466,34 +464,6 @@ impl Chess { } }; - // compute the zobrist hash for the board - let mut zobrist = board.zobrist(); - - // set castling - if castles.has(Color::White, CastlingSide::KingSide) { - zobrist ^= zobrist::castle(Color::White, CastlingSide::KingSide); - } - - if castles.has(Color::White, CastlingSide::QueenSide) { - zobrist ^= zobrist::castle(Color::White, CastlingSide::QueenSide); - } - - if castles.has(Color::Black, CastlingSide::KingSide) { - zobrist ^= zobrist::castle(Color::Black, CastlingSide::KingSide); - } - - if castles.has(Color::Black, CastlingSide::QueenSide) { - zobrist ^= zobrist::castle(Color::Black, CastlingSide::QueenSide); - } - - if let Some(sq) = ep_square { - zobrist ^= zobrist::enpassant(sq.0); - } - - if turn == Color::Black { - zobrist ^= zobrist::side(); - } - let pos = Chess { board, turn, @@ -501,40 +471,23 @@ impl Chess { ep_square, halfmoves: setup.halfmoves(), fullmoves: setup.fullmoves(), - zobrist }; errors |= validate(&pos); (pos, errors) } - - /// Computes the zobrist hash of the current game state - pub fn zobrist(&self) -> u64 { - self.zobrist - } } impl Default for Chess { fn default() -> Chess { - let board = Board::default(); - - let mut zobrist = board.zobrist(); - - // add in all the castling - zobrist ^= zobrist::castle(Color::White, CastlingSide::KingSide); - zobrist ^= zobrist::castle(Color::White, CastlingSide::QueenSide); - zobrist ^= zobrist::castle(Color::Black, CastlingSide::KingSide); - zobrist ^= zobrist::castle(Color::Black, CastlingSide::QueenSide); - Chess { - board, + board: Board::default(), turn: White, castles: Castles::default(), ep_square: None, halfmoves: 0, fullmoves: NonZeroU32::new(1).unwrap(), - zobrist } } } @@ -560,8 +513,8 @@ impl FromSetup for Chess { impl Position for Chess { fn play_unchecked(&mut self, m: &Move) { do_move(&mut self.board, &mut self.turn, &mut self.castles, - &mut self.ep_square, &mut self.zobrist, - &mut self.halfmoves, &mut self.fullmoves, m); + &mut self.ep_square, &mut self.halfmoves, + &mut self.fullmoves, m); } fn castles(&self) -> &Castles { @@ -2001,16 +1954,11 @@ fn do_move(board: &mut Board, turn: &mut Color, castles: &mut Castles, ep_square: &mut Option, - zobrist: &mut u64, halfmoves: &mut u32, fullmoves: &mut NonZeroU32, m: &Move) { let color = *turn; - - // we need to "remove" the old EP square if there is one - if let Some(sq) = ep_square.take() { - *zobrist ^= zobrist::enpassant(sq.0); - } + ep_square.take(); *halfmoves = if m.is_zeroing() { 0 @@ -2026,99 +1974,36 @@ fn do_move(board: &mut Board, *ep_square = from.offset(-8).map(EpSquare); } - // if we have an enpassant square, add it to the hash - if let Some(sq) = ep_square { - *zobrist ^= zobrist::enpassant(sq.0); - } - if role == Role::King { - // if we have the castling ability, then need to "remove" it - if castles.has(color, CastlingSide::KingSide) { - *zobrist ^= zobrist::castle(color, CastlingSide::KingSide); - } - - if castles.has(color, CastlingSide::QueenSide) { - *zobrist ^= zobrist::castle(color, CastlingSide::QueenSide); - } - castles.discard_side(color); } else if role == Role::Rook { - let side = CastlingSide::from_queen_side(from.file() == File::A); - - if castles.has(color, side) { - *zobrist ^= zobrist::castle(color, side); - } - castles.discard_rook(from); } if capture == Some(Role::Rook) { - let side = CastlingSide::from_queen_side(to.file() == File::A); - - if castles.has(color, side) { - *zobrist ^= zobrist::castle(color, side); - } - castles.discard_rook(to); } let promoted = board.promoted().contains(from) || promotion.is_some(); - // remove the piece at the from square - *zobrist ^= zobrist::square(from, board.piece_at(from).unwrap()); board.discard_piece_at(from); - - // remove the piece at the to square if there is one - if let Some(to_piece) = board.piece_at(to) { - *zobrist ^= zobrist::square(to, to_piece); - } - - let to_piece = promotion.map_or(role.of(color), |p| p.of(color)); - board.set_piece_at(to, to_piece, promoted); - *zobrist ^= zobrist::square(to, to_piece); // add in the moving piece or promotion + board.set_piece_at(to, promotion.map_or(role.of(color), |p| p.of(color)), promoted); }, Move::Castle { king, rook } => { let side = CastlingSide::from_queen_side(rook < king); - board.discard_piece_at(king); board.discard_piece_at(rook); - - *zobrist ^= zobrist::square(king, color.king()); - *zobrist ^= zobrist::square(rook, color.rook()); - - let rook_sq = Square::from_coords(side.rook_to_file(), rook.rank()); - let king_sq = Square::from_coords(side.king_to_file(), king.rank()); - board.set_piece_at(rook_sq, color.rook(), false); - board.set_piece_at(king_sq, color.king(), false); - - *zobrist ^= zobrist::square(rook_sq, color.rook()); - *zobrist ^= zobrist::square(king_sq, color.king()); - + board.set_piece_at(Square::from_coords(side.rook_to_file(), rook.rank()), color.rook(), false); + board.set_piece_at(Square::from_coords(side.king_to_file(), king.rank()), color.king(), false); castles.discard_side(color); - - if castles.has(color, CastlingSide::KingSide) { - *zobrist ^= zobrist::castle(color, CastlingSide::KingSide); - } - - if castles.has(color, CastlingSide::QueenSide) { - *zobrist ^= zobrist::castle(color, CastlingSide::QueenSide); - } } Move::EnPassant { from, to } => { - let captured_pawn_sq = Square::from_coords(to.file(), from.rank()); - board.discard_piece_at(captured_pawn_sq); // captured pawn - *zobrist ^= zobrist::square(captured_pawn_sq, (!color).pawn()); - + board.discard_piece_at(Square::from_coords(to.file(), from.rank())); // captured pawn board.discard_piece_at(from); - *zobrist ^= zobrist::square(from, color.pawn()); - board.set_piece_at(to, color.pawn(), false); - *zobrist ^= zobrist::square(to, color.pawn()); } Move::Put { role, to } => { - let piece = Piece { color, role }; - board.set_piece_at(to, piece, false); - *zobrist ^= zobrist::square(to, piece); + board.set_piece_at(to, Piece { color, role }, false); } } @@ -2127,8 +2012,6 @@ fn do_move(board: &mut Board, } *turn = !color; - // *zobrist ^= zobrist::side(); - *zobrist ^= 0x01; } fn validate(pos: &P) -> PositionErrorKinds { diff --git a/src/zobrist.rs b/src/zobrist.rs index d25f633f..c161fec4 100644 --- a/src/zobrist.rs +++ b/src/zobrist.rs @@ -1,16 +1,255 @@ -use crate::{Square, Piece, CastlingSide, Color}; +use crate::{Square, Piece, CastlingSide, Color, Setup, Position, MoveList, Move, Outcome, Castles, RemainingChecks, Board, ByColor, Material, Bitboard, Role, File, FromSetup, CastlingMode, PositionError}; +use std::num::NonZeroU32; include!(concat!(env!("OUT_DIR"), "/zobrist.rs")); // generated by build.rs -#[inline(always)] -pub fn square(sq: Square, piece: Piece) -> u64 { - PIECE_SQUARE[sq as usize][>::into(piece)] +struct Zobrist

{ + pos: P, + zobrist: u64 +} + +impl Zobrist

{ + /// Computes the zobrist hash of the current game state + pub fn zobrist(&self) -> u64 { + self.zobrist + } +} + +impl Default for Zobrist

{ + fn default() -> Self { + let pos = P::default(); + let board = pos.board(); + + // compute the zobrist hash from the pieces on the board + let mut zobrist = zobrist_from_board(board); + + // add in all the castling + zobrist ^= castle(Color::White, CastlingSide::KingSide); + zobrist ^= castle(Color::White, CastlingSide::QueenSide); + zobrist ^= castle(Color::Black, CastlingSide::KingSide); + zobrist ^= castle(Color::Black, CastlingSide::QueenSide); + + Zobrist { pos, zobrist } + } +} + +impl FromSetup for Zobrist

{ + fn from_setup(setup: &dyn Setup, mode: CastlingMode) -> Result> { + // create the underlying from the setup + let pos = match P::from_setup(setup, mode) { + Err(e) => return Err(PositionError { pos: Zobrist { pos: e.pos, zobrist: 0 }, errors: e.errors }), + Ok(p) => p + }; + + let board = pos.board(); + let turn = pos.turn(); + + let castles = pos.castles(); + let ep_square = pos.ep_square(); + + // compute the zobrist hash for the board + let mut zobrist = zobrist_from_board(&board); + + // set castling + if castles.has(Color::White, CastlingSide::KingSide) { + zobrist ^= castle(Color::White, CastlingSide::KingSide); + } + + if castles.has(Color::White, CastlingSide::QueenSide) { + zobrist ^= castle(Color::White, CastlingSide::QueenSide); + } + + if castles.has(Color::Black, CastlingSide::KingSide) { + zobrist ^= castle(Color::Black, CastlingSide::KingSide); + } + + if castles.has(Color::Black, CastlingSide::QueenSide) { + zobrist ^= castle(Color::Black, CastlingSide::QueenSide); + } + + if let Some(sq) = ep_square { + zobrist ^= ENPASSANT[sq.file() as usize]; + } + + if turn == Color::Black { + zobrist ^= SIDE; + } + + Ok(Zobrist { pos, zobrist }) + } +} + +// Simply call through to the underlying methods +impl Setup for Zobrist

{ + #[inline(always)] + fn board(&self) -> &Board { + self.pos.board() + } + + #[inline(always)] + fn pockets(&self) -> Option<&Material> { + self.pos.pockets() + } + + #[inline(always)] + fn turn(&self) -> Color { + self.pos.turn() + } + + #[inline(always)] + fn castling_rights(&self) -> Bitboard { + self.pos.castling_rights() + } + + #[inline(always)] + fn ep_square(&self) -> Option { + self.pos.ep_square() + } + + #[inline(always)] + fn remaining_checks(&self) -> Option<&ByColor> { + self.pos.remaining_checks() + } + + #[inline(always)] + fn halfmoves(&self) -> u32 { + self.pos.halfmoves() + } + + #[inline(always)] + fn fullmoves(&self) -> NonZeroU32 { + self.pos.fullmoves() + } +} + +// call thruogh to the underlying methods for everything except `play_unchecked` +impl Position for Zobrist

{ + #[inline(always)] + fn legal_moves(&self) -> MoveList { + self.pos.legal_moves() + } + + #[inline(always)] + fn castles(&self) -> &Castles { + self.pos.castles() + } + + #[inline(always)] + fn is_variant_end(&self) -> bool { + self.pos.is_variant_end() + } + + #[inline(always)] + fn has_insufficient_material(&self, color: Color) -> bool { + self.pos.has_insufficient_material(color) + } + + #[inline(always)] + fn variant_outcome(&self) -> Option { + self.pos.variant_outcome() + } + + fn play_unchecked(&mut self, m: &Move) { + let color = self.pos.turn(); + + // we need to "remove" the old EP square if there is one + if let Some(sq) = self.pos.ep_square() { + self.zobrist ^= ENPASSANT[sq.file() as usize]; + } + + match *m { + Move::Normal { role, from, capture, to, promotion } => { + // if we have an enpassant square, add it to the hash + if let Some(sq) = self.pos.ep_square() { + self.zobrist ^= ENPASSANT[sq.file() as usize]; + } + + if role == Role::King { + // if we have the castling ability, then need to "remove" it + if self.castles().has(color, CastlingSide::KingSide) { + self.zobrist ^= castle(color, CastlingSide::KingSide); + } + + if self.castles().has(color, CastlingSide::QueenSide) { + self.zobrist ^= castle(color, CastlingSide::QueenSide); + } + } else if role == Role::Rook { + let side = CastlingSide::from_queen_side(from.file() == File::A); + + if self.castles().has(color, side) { + self.zobrist ^= castle(color, side); + } + } + + if capture == Some(Role::Rook) { + let side = CastlingSide::from_queen_side(to.file() == File::A); + + if self.castles().has(color, side) { + self.zobrist ^= castle(color, side); + } + } + + // remove the piece at the from square + self.zobrist ^= square(from, self.board().piece_at(from).unwrap()); + + // remove the piece at the to square if there is one + if let Some(to_piece) = self.board().piece_at(to) { + self.zobrist ^= square(to, to_piece); + } + + let to_piece = promotion.map_or(role.of(color), |p| p.of(color)); + self.zobrist ^= square(to, to_piece); // add in the moving piece or promotion + } + Move::Castle { king, rook } => { + let side = CastlingSide::from_queen_side(rook < king); + + self.zobrist ^= square(king, color.king()); + self.zobrist ^= square(rook, color.rook()); + + self.zobrist ^= square(Square::from_coords(side.rook_to_file(), rook.rank()), color.rook()); + self.zobrist ^= square(Square::from_coords(side.king_to_file(), king.rank()), color.king()); + + if self.castles().has(color, CastlingSide::KingSide) { + self.zobrist ^= castle(color, CastlingSide::KingSide); + } + + if self.castles().has(color, CastlingSide::QueenSide) { + self.zobrist ^= castle(color, CastlingSide::QueenSide); + } + } + Move::EnPassant { from, to } => { + self.zobrist ^= square(Square::from_coords(to.file(), from.rank()), (!color).pawn()); + self.zobrist ^= square(from, color.pawn()); + self.zobrist ^= square(to, color.pawn()); + } + Move::Put { role, to } => { + self.zobrist ^= square(to, Piece { color, role }); + } + } + + self.zobrist ^= 0x01; // flip the side + } +} + +/// Computes the Zobrist hash given a board +/// This is NOT the complete hash... castling and en passant are not included +fn zobrist_from_board(board: &Board) -> u64 { + // compute the zobrist hash from the pieces on the board + let mut zobrist = 0u64; + + for sq in (0..64).into_iter().map(|i| Square::new(i)) { + if let Some(piece) = board.piece_at(sq) { + zobrist ^= square(sq, piece); + } + } + + zobrist } #[inline(always)] -pub fn enpassant(sq: Square) -> u64 { - ENPASSANT[sq.file() as usize] +pub fn square(sq: Square, piece: Piece) -> u64 { + PIECE_SQUARE[sq as usize][>::into(piece)] } #[inline(always)] @@ -24,21 +263,11 @@ pub fn castle(color :Color, castle: CastlingSide) -> u64 { } } -#[inline(always)] -pub fn side() -> u64 { - SIDE -} - -// #[inline(always)] -// pub fn z_no_pawns() -> u64 { -// NO_PAWNS -// } - #[cfg(test)] mod zobrist_tests { use crate::{Square, Piece, Chess, Position, CastlingMode, Move}; use crate::fen::{epd, Fen}; - use crate::zobrist::square; + use crate::zobrist::{square, Zobrist}; use std::collections::{HashSet, HashMap}; use rand::prelude::*; @@ -65,10 +294,10 @@ mod zobrist_tests { #[test] fn fen_test() { let setup1 :Fen = "8/8/8/8/p7/P7/6k1/2K5 w - -".parse().expect("Error parsing FEN"); - let setup2 :Fen = "8/8/8/8/p7/P7/6k1/2K5 w - -".parse().expect("Error parsing FEN"); + let setup2 :Fen = "8/8/8/8/p7/P7/6k1/2K5 b - -".parse().expect("Error parsing FEN"); - let game1 :Chess = setup1.position(CastlingMode::Standard).expect("Error setting up game"); - let game2 :Chess = setup2.position(CastlingMode::Standard).expect("Error setting up game"); + let game1 :Zobrist = setup1.position(CastlingMode::Standard).expect("Error setting up game"); + let game2 :Zobrist = setup2.position(CastlingMode::Standard).expect("Error setting up game"); println!("0x{:x} != 0x{:x}", game1.zobrist(), game2.zobrist()); @@ -78,11 +307,11 @@ mod zobrist_tests { #[test] fn moves_test() { // randomly move through a bunch of moves, ensuring we get different zobrist hashes - const MAX_MOVES :usize = 100_000; + const MAX_MOVES :usize = 10_000; let mut hash_fen :HashMap = HashMap::new(); let mut hash_moves :HashMap> = HashMap::new(); let mut moves = Vec::new(); - let mut chess = Chess::default(); + let mut chess = Zobrist::::default(); let mut rnd = StdRng::seed_from_u64(0x30b3_1137_bb45_7b1b_u64); while hash_fen.len() < MAX_MOVES { @@ -108,15 +337,15 @@ mod zobrist_tests { let setup1 :Fen = fen.parse().expect("Error parsing FEN"); let setup2 :Fen = existing_fen.parse().expect("Error parsing FEN"); - let game1 :Chess = setup1.position(CastlingMode::Standard).expect("Error setting up game"); - let game2 :Chess = setup2.position(CastlingMode::Standard).expect("Error setting up game"); + let game1 :Zobrist = setup1.position(CastlingMode::Standard).expect("Error setting up game"); + let game2 :Zobrist = setup2.position(CastlingMode::Standard).expect("Error setting up game"); if game1.zobrist() == game2.zobrist() { panic!("COLLISION FOUND FOR 2 FENs: {} (0x{:x}) & {} (0x{:x})", fen, game1.zobrist(), existing_fen, game2.zobrist()); } else { let mvs1 = hash_moves.get(&z).unwrap(); let mvs2 = moves; - let mut game = Chess::default(); + let mut game = Zobrist::::default(); let mut panic_str = format!("ZOBRIST COLLISION AFTER {}: 0x{:016x} ({} {})\n", hash_fen.len(), z, mvs1.len(), mvs2.len()); @@ -153,7 +382,7 @@ mod zobrist_tests { // check to see if the game is over, and if so restart it if chess.is_game_over() { - chess = Chess::default(); + chess = Zobrist::::default(); moves.clear(); println!("{} of {}", hash_fen.len(), MAX_MOVES); }