diff --git a/src/board/game.rs b/src/board/game.rs index 65e44d8..9e07a3c 100644 --- a/src/board/game.rs +++ b/src/board/game.rs @@ -57,10 +57,11 @@ impl Game { let color = board.state.current_player(); let pawn_move = board.is_pawn_move(mv.src); let mut en_passant_square = None; - let capture_en_passant = match color { - Color::White => mv.dst - 8, + let ep_capture = match color { + Color::White => mv.dst.saturating_sub(8), Color::Black => mv.dst + 8, }; + let old_castling_ability = board.state.castling_ability; let piece_at_src = mailbox @@ -84,11 +85,11 @@ impl Game { } MoveType::EnPassant => { board.move_piece(mv.src, mv.dst, PieceType::Pawn); - board.remove_opponent_piece(capture_en_passant, PieceType::Pawn); - hash.update_en_passant(mv.src, mv.dst, capture_en_passant, color); + board.remove_opponent_piece(ep_capture, PieceType::Pawn); + hash.update_en_passant(mv.src, mv.dst, ep_capture, color); hash.drop_en_passant_hash(board.state.en_passant_square()); mailbox.set_piece_at(mv.dst, Some(PieceType::Pawn)); - mailbox.set_piece_at(capture_en_passant, None); + mailbox.set_piece_at(ep_capture, None); } MoveType::DoublePush => { board.move_piece(mv.src, mv.dst, piece_at_src); diff --git a/src/board/transposition_table.rs b/src/board/transposition_table.rs index 12f2890..ad33b54 100644 --- a/src/board/transposition_table.rs +++ b/src/board/transposition_table.rs @@ -2,80 +2,107 @@ use crate::movegen::r#move::Move; use super::zobrist::ZobristHash; -#[derive(Clone)] pub struct TranspositionTable { - positions: Vec>, + positions: Vec>, size: u64, } impl TranspositionTable { pub fn new(size: u64) -> Self { Self { - positions: (0..size).map(|_| None).collect(), + positions: vec![None; size as usize], size, } } - pub fn lookup(&self, zobrist_hash: ZobristHash) -> &TranspositionTableEntry { - self.positions + pub fn lookup(&self, zobrist_hash: ZobristHash) -> Option<&TTEntry> { + let entry = self + .positions .get((zobrist_hash.hash % self.size) as usize) - .unwrap() - .as_ref() - .unwrap() + .and_then(|entry| entry.as_ref()); + + entry } - pub fn insert(&mut self, tt_entry: TranspositionTableEntry, hash: ZobristHash) { - let index = hash.hash % self.size; - self.positions.insert(index as usize, Some(tt_entry)); - } -} + pub fn insert(&mut self, tt_entry: TTEntry) { + let index = (tt_entry.hash.hash % self.size) as usize; -#[derive(Clone)] -pub struct TranspositionTableEntry { - hash: ZobristHash, - depth: u8, - alpha: i32, - beta: i32, - mv: Move, - node_type: NodeType, -} - -impl TranspositionTableEntry { - pub const fn new( - hash: ZobristHash, - depth: u8, - alpha: i32, - beta: i32, - mv: Move, - node_type: NodeType, - ) -> Self { - Self { - hash, - depth, - alpha, - beta, - mv, - node_type, + if let Some(stored) = &self.positions[index] { + if tt_entry.depth > stored.depth || matches!(tt_entry.bound, Bound::Exact) { + self.positions[index] = Some(tt_entry); + } + } else { + self.positions[index] = Some(tt_entry); } } } #[derive(Clone)] -pub enum NodeType { +pub struct TTEntry { + pub hash: ZobristHash, + pub depth: u8, + pub score: i32, + pub mv: Option, + pub bound: Bound, +} + +impl TTEntry { + pub const fn new( + hash: ZobristHash, + depth: u8, + score: i32, + mv: Option, + bound: Bound, + ) -> Self { + Self { + hash, + depth, + score, + mv, + bound, + } + } +} + +#[derive(Clone)] +pub enum Bound { Exact, - LowerBound, - UpperBound, + Lower, + Upper, } #[cfg(test)] mod tests { + use crate::{ + board::{fen::from_fen, transposition_table::TranspositionTable}, + evaluation::{MAX_SCORE, MIN_SCORE}, + movegen::attack_generator::init_attacks, + search, + }; + + const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1"; + const FEN_WILL_BE: &str = "1r2k2r/2P1pq1p/2npb3/1B3ppP/p3P3/P4Q2/1P1PNPP1/R3K2R b KQk - 0 1"; + const FEN_WONT_BE: &str = "1Q2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B4/1P1PNPP1/R3K2R b KQk - 0 1"; #[test] - fn test_transpotisition() -> Result<(), String> { - // TODO: - // >have specific board state - // >run a deep search for this board state - // >test some expected outcomes that should have been stored in the TT + fn test_transposition_table() -> Result<(), String> { + init_attacks(); + let mut game = from_fen(FEN)?; + let mut tt = TranspositionTable::new(1000000); + + // fill Transposition Table + search::negamax::negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &mut tt); + + let will_be_hash = from_fen(FEN_WILL_BE)?.hash; + let tt_entry = tt.lookup(will_be_hash); + + assert!(tt_entry.is_some()); + + let wont_be_hash = from_fen(FEN_WONT_BE)?.hash; + let tt_entry = tt.lookup(wont_be_hash); + + assert!(tt_entry.is_none()); + Ok(()) } } diff --git a/src/board/zobrist.rs b/src/board/zobrist.rs index a1a2bb7..41b3c62 100644 --- a/src/board/zobrist.rs +++ b/src/board/zobrist.rs @@ -151,17 +151,12 @@ impl ZobristHash { self.hash ^= keys.piece_square_color[dst][piece_at_dst.idx()][color.opponent().idx()] } - pub fn update_en_passant( - &mut self, - src: usize, - dst: usize, - capture_en_passant: usize, - color: Color, - ) { + pub fn update_en_passant(&mut self, src: usize, dst: usize, ep_capture: usize, color: Color) { let keys = zobrist_keys(); self.hash ^= keys.piece_square_color[src][PieceType::Pawn.idx()][color.idx()]; self.hash ^= keys.piece_square_color[dst][PieceType::Pawn.idx()][color.idx()]; - self.hash ^= keys.piece_square_color[capture_en_passant][PieceType::Pawn.idx()][color.opponent().idx()] + self.hash ^= + keys.piece_square_color[ep_capture][PieceType::Pawn.idx()][color.opponent().idx()] } pub fn update_double_push( diff --git a/src/interface/uci.rs b/src/interface/uci.rs index 75b37a6..3c1cf63 100644 --- a/src/interface/uci.rs +++ b/src/interface/uci.rs @@ -4,7 +4,7 @@ use std::{ }; use crate::{ - board::game::Game, + board::{game::Game, transposition_table::TranspositionTable}, evaluation::{MAX_SCORE, MIN_SCORE}, movegen::r#move::Move, search, @@ -142,12 +142,14 @@ pub fn uci_go(go: &mut SplitWhitespace, game: &mut Game) -> Result _ => (), } } + let mut tt = TranspositionTable::new(1000000); Ok(search::negamax::negamax( game, MIN_SCORE, MAX_SCORE, params.depth.unwrap_or(MAX_DEPTH), 0, + &mut tt, ) .0 .expect("No move selected")) diff --git a/src/search/negamax.rs b/src/search/negamax.rs index 7e8b1c6..1be46ff 100644 --- a/src/search/negamax.rs +++ b/src/search/negamax.rs @@ -1,5 +1,8 @@ use crate::{ - board::game::Game, + board::{ + game::Game, + transposition_table::{Bound, TTEntry, TranspositionTable}, + }, evaluation::{MATE_SCORE, MIN_SCORE}, movegen::r#move::Move, }; @@ -12,16 +15,25 @@ pub fn negamax( beta: i32, depth: u8, plies: u8, + tt: &mut TranspositionTable, ) -> (Option, i32) { - let color = game.current_player(); - if depth == 0 { return (None, quiescence(game, alpha, beta).1); } + if let Some(entry) = tt.lookup(game.hash) { + if entry.depth >= depth + && (matches!(entry.bound, Bound::Exact) + || (matches!(entry.bound, Bound::Lower) && entry.score >= beta) + || (matches!(entry.bound, Bound::Upper) && entry.score <= alpha)) + { + return (entry.mv, entry.score); + } + } + + let color = game.current_player(); let (mut best_move, mut best_score, mate_score) = (None, MIN_SCORE, -MATE_SCORE + plies as i32); let mut legal_moves = 0; - let mut pseudo_legal_moves = game.board.pseudo_moves_all(); pseudo_legal_moves.sort_unstable_by_key(|mv| score_by_mvv_lva(&game.mailbox, *mv)); @@ -32,9 +44,9 @@ pub fn negamax( game.unmake_move(); continue; } - legal_moves += 1; - let move_score = -negamax(game, -beta, -alpha, depth - 1, plies + 1).1; + + let move_score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, tt).1; game.unmake_move(); if move_score > best_score { @@ -43,6 +55,13 @@ pub fn negamax( } if move_score >= beta { + tt.insert(TTEntry::new( + game.hash, + depth, + best_score, + None, + Bound::Lower, + )); return (best_move, beta); } @@ -51,15 +70,35 @@ pub fn negamax( if legal_moves == 0 { if game.board.king_under_check(color) { + tt.insert(TTEntry::new( + game.hash, + depth, + mate_score, + None, + Bound::Exact, + )); return (None, mate_score); } + tt.insert(TTEntry::new(game.hash, depth, 0, None, Bound::Exact)); return (None, 0); } + + if best_score < alpha { + tt.insert(TTEntry::new( + game.hash, + depth, + best_score, + best_move, + Bound::Upper, + )); + } + (best_move, best_score) } #[cfg(test)] mod tests { + use crate::board::transposition_table::TranspositionTable; use crate::board::{fen::from_fen, square::Square}; use crate::evaluation::{MAX_SCORE, MIN_SCORE}; use crate::movegen::attack_generator::init_attacks; @@ -76,8 +115,11 @@ mod tests { init_attacks(); let mut game = from_fen(FEN_MATE_IN_1[0])?; + let mut tt = TranspositionTable::new(1000000); let e3f2 = Move::new(Square::E3, Square::F2); - let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0).0.unwrap(); + let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &mut tt) + .0 + .unwrap(); assert_eq!(e3f2, anointed_move);