diff --git a/src/board/board.rs b/src/board/board.rs index b44402b..9dfa20b 100644 --- a/src/board/board.rs +++ b/src/board/board.rs @@ -22,7 +22,7 @@ pub struct Board { } impl Board { - pub const fn new() -> Self { + pub const fn startpos() -> Self { Self { pieces: [ 0xff00000000ff00, @@ -37,7 +37,7 @@ impl Board { } } - pub const fn empty_board() -> Self { + pub const fn new_empty() -> Self { Self { pieces: [0x0, 0x0, 0x0, 0x0, 0x0, 0x0], color: [0x0, 0x0], @@ -45,20 +45,8 @@ impl Board { } } - fn white_occupancies(&self) -> Bitboard { - self.color[Color::White] - } - - fn black_occupancies(&self) -> Bitboard { - self.color[Color::Black] - } - - pub fn all_occupancies(&self) -> Bitboard { - self.white_occupancies() | self.black_occupancies() - } - pub fn is_attacked(&self, square: usize, opponent_color: Color) -> bool { - let all_occupancies = self.all_occupancies(); + let all_occupancies = self.color[Color::White] | self.color[Color::Black]; let own_color = opponent_color.opponent(); let opponent_color_bb = &self.color[opponent_color]; @@ -108,7 +96,7 @@ impl Board { } pub fn pseudo_moves(&self, color: Color, piece_type: PieceType) -> Vec { - let all_occupancies = self.all_occupancies(); + let all_occupancies = self.color[Color::White] | self.color[Color::Black]; let pieces = self.pieces[piece_type] & self.color[color]; let own_occupancies = self.color[color]; let opponent_occupancies = self.color[color.opponent()]; @@ -186,7 +174,7 @@ impl Board { impl Default for Board { fn default() -> Self { - Self::new() + Self::new_empty() } } @@ -277,9 +265,12 @@ mod tests { fn test_occupancies() -> Result<(), String> { let new_game = from_fen(FEN_EXAMPLE[0])?; - assert_eq!(new_game.board.white_occupancies(), 0x40000002000000); - assert_eq!(new_game.board.black_occupancies(), 0x900204401002); - assert_eq!(new_game.board.all_occupancies(), 0x40900206401002); + assert_eq!(new_game.board.color[Color::White], 0x40000002000000); + assert_eq!(new_game.board.color[Color::Black], 0x900204401002); + assert_eq!( + new_game.board.color[Color::White] | new_game.board.color[Color::Black], + 0x40900206401002 + ); Ok(()) } diff --git a/src/board/fen.rs b/src/board/fen.rs index 6c75c4e..a4c1d6f 100644 --- a/src/board/fen.rs +++ b/src/board/fen.rs @@ -1,6 +1,8 @@ use crate::board::board::{Board, Color, PieceType}; use crate::board::game::Game; use crate::board::state::{Castle, State}; +use crate::search::transposition_table::TranspositionTable; +use crate::search::MAX_TT_SIZE; use String as FenError; pub fn from_fen(fen: &str) -> Result { @@ -34,11 +36,12 @@ pub fn from_fen(fen: &str) -> Result { history: History::new(), mailbox, hash, + tt: TranspositionTable::new(MAX_TT_SIZE), }) } pub fn piece_placement(pieces: &str) -> Result { - let mut board = Board::empty_board(); + let mut board = Board::new_empty(); let (mut file, mut rank): (u8, u8) = (0, 7); for c in pieces.chars() { diff --git a/src/board/game.rs b/src/board/game.rs index 8c3b931..19d1253 100644 --- a/src/board/game.rs +++ b/src/board/game.rs @@ -1,6 +1,7 @@ use crate::{ board::fen::from_fen, movegen::r#move::{Move, MoveType}, + search::{transposition_table::TranspositionTable, MAX_TT_SIZE}, }; use String as FenError; @@ -27,15 +28,17 @@ pub struct Game { pub history: History, pub mailbox: Mailbox, pub hash: ZobristHash, + pub tt: TranspositionTable, } impl Game { pub fn new() -> Self { Self { - board: Board::new(), + board: Board::startpos(), history: History::new(), - mailbox: Mailbox::from_board(&Board::new()), - hash: zobrist_keys().calculate_hash(&Board::new()), + mailbox: Mailbox::from_board(&Board::startpos()), + hash: zobrist_keys().calculate_hash(&Board::startpos()), + tt: TranspositionTable::new(MAX_TT_SIZE), } } diff --git a/src/interface/uci.rs b/src/interface/uci.rs index 216ec5e..04701b2 100644 --- a/src/interface/uci.rs +++ b/src/interface/uci.rs @@ -7,7 +7,10 @@ use std::{ use anyhow::{anyhow, bail}; use crate::{ - board::{board::Color, game::Game}, + board::{ + board::{Board, Color}, + game::Game, + }, movegen::r#move::Move, search::{ iterative_deepening, transposition_table::TranspositionTable, MAX_DEPTH, MAX_TT_SIZE, @@ -137,11 +140,7 @@ pub fn uci_position(position: &mut SplitWhitespace) -> anyhow::Result { Ok(game) } -pub fn uci_go( - go_iter: &mut SplitWhitespace, - game: &mut Game, - tt: &mut TranspositionTable, -) -> anyhow::Result { +pub fn uci_go(go_iter: &mut SplitWhitespace, game: &mut Game) -> anyhow::Result { let mut params = UciParameters::new(); while let Some(subcommand) = go_iter.next() { match subcommand { @@ -159,14 +158,12 @@ pub fn uci_go( Color::Black => params.btime.unwrap_or(REMAINING_TIME_DEFAULT), }; - iterative_deepening::iterative_deepening(game, MAX_DEPTH, remaining_time, tt)?.ok_or_else( - || { - anyhow!( - "No stored best move found. Time: {}", - remaining_time - time.elapsed().as_millis() - ) - }, - ) + iterative_deepening::iterative_deepening(game, MAX_DEPTH, remaining_time)?.ok_or_else(|| { + anyhow!( + "No stored best move found. Time: {}", + remaining_time - time.elapsed().as_millis() + ) + }) } fn parse_next(go_iter: &mut SplitWhitespace, val: &str) -> anyhow::Result { @@ -178,7 +175,6 @@ fn parse_next(go_iter: &mut SplitWhitespace, val: &str) -> pub fn uci_loop(input: R, mut output: W) -> anyhow::Result<()> { let mut params = UciParameters::new(); - let mut tt = TranspositionTable::new(MAX_TT_SIZE); for line in input.lines() { let line_str = line.unwrap_or_else(|_| "quit".to_string()); @@ -188,7 +184,14 @@ pub fn uci_loop(input: R, mut output: W) -> anyhow::Result Command::Uci => Response::UciOk, Command::IsReady => Response::ReadyOk, Command::UciNewGame => { - tt = TranspositionTable::new(MAX_TT_SIZE); + params.game.as_mut().map_or_else( + || Response::Info("Failed to unwrap from UciParameter".to_string()), + |game| { + game.tt = TranspositionTable::new(MAX_TT_SIZE); + game.board = Board::startpos(); + Response::Info("Initialized new game".to_string()) + }, + ); Response::Info("Clear cache".to_string()) } Command::Position => { @@ -197,7 +200,7 @@ pub fn uci_loop(input: R, mut output: W) -> anyhow::Result } Command::Go => params.game.as_mut().map_or_else( || Response::Info("Failed to unwrap from UciParameter".to_string()), - |game| match uci_go(&mut parts, game, &mut tt) { + |game| match uci_go(&mut parts, game) { Ok(best_move) => Response::BestMove(best_move.parse_into_str()), Err(e) => Response::Info(e.to_string()), }, @@ -221,7 +224,6 @@ mod tests { board::{fen::from_fen, square::Square}, interface::uci::{parse_command, Command}, movegen::{attack_generator::init_attacks, r#move::Move}, - search::{transposition_table::TranspositionTable, MAX_TT_SIZE}, }; use super::uci_go; @@ -257,11 +259,10 @@ mod tests { #[test] fn test_uci_go() -> anyhow::Result<()> { init_attacks(); - let mut tt = TranspositionTable::new(MAX_TT_SIZE); let mut game = from_fen(FEN_MATE_IN_1).unwrap(); let command_go = "go depth 2"; let mut parts = command_go.split_whitespace(); - let response = uci_go(&mut parts, &mut game, &mut tt)?; + let response = uci_go(&mut parts, &mut game)?; assert_eq!(Move::new(Square::E3, Square::F2), response); diff --git a/src/search/iterative_deepening.rs b/src/search/iterative_deepening.rs index 401e071..0c80c76 100644 --- a/src/search/iterative_deepening.rs +++ b/src/search/iterative_deepening.rs @@ -6,21 +6,19 @@ use crate::{ use super::{ negamax, - time::{time_limit_reached, TimeInfo}, - transposition_table::TranspositionTable, + time::{soft_limit, TimeInfo}, }; pub fn iterative_deepening( game: &mut Game, max_depth: u8, remaining_time: u128, - tt: &mut TranspositionTable, ) -> anyhow::Result> { let (mut best_move, mut best_score) = (None, MIN_SCORE); let time = std::time::Instant::now(); for depth in 1..=max_depth { - if time_limit_reached(&time, remaining_time, best_score) { + if soft_limit(&time, remaining_time, best_score) { return Ok(best_move); } @@ -31,7 +29,6 @@ pub fn iterative_deepening( depth, 0, &TimeInfo::new(time, remaining_time), - tt, ); if let Ok(search_result) = search_result { @@ -44,6 +41,12 @@ pub fn iterative_deepening( } } + // tt.lookup(game.hash).map(|entry| { + // if entry.mv.is_some() { + // best_move = entry.mv; + // } + // }); + Ok(best_move) } @@ -52,10 +55,7 @@ mod tests { use crate::{ board::fen::from_fen, movegen::attack_generator::init_attacks, - search::{ - iterative_deepening, transposition_table::TranspositionTable, MAX_DEPTH, MAX_TT_SIZE, - REMAINING_TIME_DEFAULT, - }, + search::{iterative_deepening, MAX_DEPTH, REMAINING_TIME_DEFAULT}, }; const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1"; @@ -64,15 +64,9 @@ mod tests { fn test_iterative_deepening() -> anyhow::Result<()> { init_attacks(); let mut game = from_fen(FEN).unwrap(); - let mut tt = TranspositionTable::new(MAX_TT_SIZE); let time_now = std::time::Instant::now(); - iterative_deepening::iterative_deepening( - &mut game, - MAX_DEPTH, - REMAINING_TIME_DEFAULT, - &mut tt, - )?; + iterative_deepening::iterative_deepening(&mut game, MAX_DEPTH, REMAINING_TIME_DEFAULT)?; dbg!(time_now.elapsed()); diff --git a/src/search/negamax.rs b/src/search/negamax.rs index a3f5d54..9cb5499 100644 --- a/src/search/negamax.rs +++ b/src/search/negamax.rs @@ -9,7 +9,7 @@ use super::{ move_ordering, quiescence::quiescence, time::{hard_limit, TimeInfo}, - transposition_table::{Bound, TTEntry, TranspositionTable}, + transposition_table::TTEntry, SearchResult, }; @@ -20,7 +20,6 @@ pub fn negamax( depth: u8, plies: u8, time_info: &TimeInfo, - tt: &mut TranspositionTable, ) -> Result { if hard_limit(&time_info.time, time_info.remaining_time_in_ms) { bail!("Time is up! In Negamax"); @@ -36,10 +35,9 @@ pub fn negamax( let mut best_score = MIN_SCORE; let mate_score = -MATE_SCORE + plies as i32; let mut legal_moves = 0; - let mut bound = Bound::Alpha; let all_moves = game.board.pseudo_moves_all(); - let tt_move = tt.lookup(game.hash).and_then(|entry| entry.mv); + let tt_move = game.tt.lookup(game.hash).and_then(|entry| entry.mv); let moves = move_ordering::sort_moves(all_moves, &game.mailbox, tt_move); for mv in moves { @@ -51,7 +49,7 @@ pub fn negamax( } legal_moves += 1; - let score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, time_info, tt)?.best_score; + let score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, time_info)?.best_score; game.unmake_move(); if score > best_score { @@ -60,14 +58,12 @@ pub fn negamax( } if score >= beta { - bound = Bound::Beta; best_score = beta; best_move = Some(mv); break; } if score > alpha { - bound = Bound::Exact; alpha = score; } } @@ -79,7 +75,7 @@ pub fn negamax( return Ok(SearchResult::new(None, 0)); } - tt.insert(TTEntry::new(game.hash, depth, best_score, best_move, bound)); + game.tt.insert(TTEntry::new(game.hash, best_move)); Ok(SearchResult::new(best_move, best_score)) } @@ -91,8 +87,6 @@ mod tests { use crate::movegen::r#move::Move; use crate::search::negamax::negamax; use crate::search::time::TimeInfo; - use crate::search::transposition_table::TranspositionTable; - use crate::search::MAX_TT_SIZE; const FEN_MATE_IN_1: [&str; 2] = [ "8/8/8/8/8/4q1k1/8/5K2 b - - 0 1", @@ -104,10 +98,9 @@ mod tests { init_attacks(); let mut game = from_fen(FEN_MATE_IN_1[0]).unwrap(); - let mut tt = TranspositionTable::new(MAX_TT_SIZE); let e3f2 = Move::new(Square::E3, Square::F2); let time_info = TimeInfo::new(std::time::Instant::now(), 1000); - let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut tt) + let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info) .expect("Expected a search result") .best_move .expect("Expected a move"); @@ -117,7 +110,7 @@ mod tests { let mut game = from_fen(FEN_MATE_IN_1[1]).unwrap(); let e3f2 = Move::new(Square::E3, Square::F2); - let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut tt) + let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info) .expect("Expected a search result") .best_move .expect("Expected a move"); diff --git a/src/search/quiescence.rs b/src/search/quiescence.rs index 4be6bec..3d6f35f 100644 --- a/src/search/quiescence.rs +++ b/src/search/quiescence.rs @@ -24,7 +24,8 @@ pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, time_info: &TimeIn let color = game.current_player(); let all_moves = game.board.pseudo_moves_all_captures(); - let moves = move_ordering::sort_moves(all_moves, &game.mailbox, None); + let tt_move = game.tt.lookup(game.hash).and_then(|entry| entry.mv); + let moves = move_ordering::sort_moves(all_moves, &game.mailbox, tt_move); for mv in moves { game.make_move(&mv); @@ -33,7 +34,7 @@ pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, time_info: &TimeIn game.unmake_move(); continue; } - + let score = -quiescence(game, -beta, -alpha, time_info)?; game.unmake_move(); diff --git a/src/search/time.rs b/src/search/time.rs index 0bfde8d..75a8352 100644 --- a/src/search/time.rs +++ b/src/search/time.rs @@ -16,10 +16,6 @@ impl TimeInfo { } } -pub fn time_limit_reached(time: &Instant, remaining_time: u128, eval: i32) -> bool { - hard_limit(time, remaining_time) || soft_limit(time, remaining_time, eval) -} - pub fn hard_limit(time_now: &Instant, remaining_time: u128) -> bool { time_now.elapsed().as_millis() >= remaining_time / HARD_LIMIT_DIVISION } diff --git a/src/search/transposition_table.rs b/src/search/transposition_table.rs index 3cc79f5..d7b3962 100644 --- a/src/search/transposition_table.rs +++ b/src/search/transposition_table.rs @@ -1,5 +1,6 @@ use crate::{board::zobrist::ZobristHash, movegen::r#move::Move}; +#[derive(Clone, Debug, Eq, PartialEq)] pub struct TranspositionTable { positions: Vec>, size: u64, @@ -24,60 +25,29 @@ impl TranspositionTable { pub fn insert(&mut self, tt_entry: TTEntry) { let index = (tt_entry.hash.hash % self.size) as usize; - - 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); - } + self.positions[index] = Some(tt_entry); } } -#[derive(Clone)] +#[derive(Clone, Debug, Eq, PartialEq)] 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, - } + pub const fn new(hash: ZobristHash, mv: Option) -> Self { + Self { hash, mv } } } -#[derive(Clone)] -pub enum Bound { - Exact, - Alpha, - Beta, -} - #[cfg(test)] mod tests { use crate::{ board::fen::from_fen, evaluation::{MAX_SCORE, MIN_SCORE}, movegen::attack_generator::init_attacks, - search::{ - negamax::negamax, time::TimeInfo, transposition_table::TranspositionTable, MAX_TT_SIZE, - }, + search::{negamax::negamax, time::TimeInfo}, }; const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1"; @@ -88,10 +58,9 @@ mod tests { fn test_transposition_table() -> anyhow::Result<()> { init_attacks(); let mut game = from_fen(FEN).unwrap(); - let mut tt = TranspositionTable::new(MAX_TT_SIZE); let time_info = TimeInfo::new(std::time::Instant::now(), 30000); - negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut tt) + negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info) .expect("Expected a search result") .best_move .expect("Expected a move"); @@ -99,12 +68,12 @@ mod tests { dbg!(time_info.time.elapsed()); let will_be_hash = from_fen(FEN_POSSIBLE).unwrap().hash; - let tt_entry = tt.lookup(will_be_hash); + let tt_entry = game.tt.lookup(will_be_hash); assert!(tt_entry.is_some()); let wont_be_hash = from_fen(FEN_IMPOSSIBLE).unwrap().hash; - let tt_entry = tt.lookup(wont_be_hash); + let tt_entry = game.tt.lookup(wont_be_hash); assert!(tt_entry.is_none());