diff --git a/src/board/game.rs b/src/board/game.rs index 9e07a3c..37a07c7 100644 --- a/src/board/game.rs +++ b/src/board/game.rs @@ -166,7 +166,9 @@ impl Game { board.state.halfmove_clock = new_halfmove_clock; } - let mv = move_parameters.mv.unwrap(); + let mv = move_parameters + .mv + .expect("Expected move parameters from history stack"); let piece_at_dst = mailbox.piece_at(mv.dst).expect("Expected set piece"); match &mv.move_type { MoveType::Quiet | MoveType::DoublePush => { diff --git a/src/board/mailbox.rs b/src/board/mailbox.rs index ce7fcbd..7530439 100644 --- a/src/board/mailbox.rs +++ b/src/board/mailbox.rs @@ -1,7 +1,6 @@ use super::{ bitboard::{have_common_bit, square_to_bitboard}, board::{Board, PieceType}, - square::Square, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -12,8 +11,9 @@ pub struct Mailbox { impl Mailbox { pub fn from_board(board: &Board) -> Self { let mut mailbox: [Option; 64] = [None; 64]; - for square in Square::A1..=Square::H8 { - mailbox[square] = board + + for (square, mailbox_square) in mailbox.iter_mut().enumerate() { + *mailbox_square = board .white_pieces .iter() .chain(board.black_pieces.iter()) diff --git a/src/board/mod.rs b/src/board/mod.rs index 16dd948..4d0775b 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -6,5 +6,4 @@ pub mod history; pub mod mailbox; pub mod square; pub mod state; -pub mod transposition_table; pub mod zobrist; diff --git a/src/interface/uci.rs b/src/interface/uci.rs index 76cd1f9..c7ac581 100644 --- a/src/interface/uci.rs +++ b/src/interface/uci.rs @@ -4,10 +4,9 @@ use std::{ }; use crate::{ - board::{game::Game, transposition_table::TranspositionTable}, - evaluation::{MAX_SCORE, MIN_SCORE}, + board::game::Game, movegen::r#move::Move, - search, + search::{self, MAX_DEPTH, MOVE_TIME}, }; #[derive(PartialEq, Eq, Debug)] @@ -117,8 +116,6 @@ pub fn uci_position(position: &mut SplitWhitespace) -> Result { Ok(game) } -const MAX_DEPTH: u8 = 3; - pub fn uci_go(go: &mut SplitWhitespace, game: &mut Game) -> Result { let mut params = UciParameters::new(); while let Some(subcommand) = go.next() { @@ -142,18 +139,11 @@ 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, - &mut 0, + + Ok( + search::iterative_deepening::iterative_deepening(game, MAX_DEPTH, MOVE_TIME) + .expect("No move selected"), ) - .0 - .expect("No move selected")) } pub fn uci_loop(input: R, mut output: W) -> Result<(), String> { diff --git a/src/movegen/move.rs b/src/movegen/move.rs index 0d0b006..4d050f1 100644 --- a/src/movegen/move.rs +++ b/src/movegen/move.rs @@ -83,6 +83,7 @@ impl Move { } } + #[allow(clippy::unwrap_used)] pub fn parse_promotion(mut mv_chars: Chars<'_>) -> Result { match mv_chars.next().unwrap() { 'n' => Ok(Promote::Knight), @@ -93,6 +94,7 @@ impl Move { } } + #[allow(clippy::unwrap_used)] pub fn parse_from_str(game: &Game, mv: &str) -> Result { if mv.len() != 4 && mv.len() != 5 { return Err("Invalid move characters length".to_string()); @@ -107,11 +109,9 @@ impl Move { let src = from_coords(src_rank, src_file); let dst = from_coords(dst_rank, dst_file); - let promote_into = if mv.len() == 5 { - Some(Self::parse_promotion(mv_chars)?) - } else { - None - }; + let promote_into = (mv.len() == 5) + .then(|| Self::parse_promotion(mv_chars)) + .transpose()?; Ok(Self::build_with_type(&game.mailbox, src, dst, promote_into)) } diff --git a/src/search/iterative_deepening.rs b/src/search/iterative_deepening.rs index e624a49..7f82ece 100644 --- a/src/search/iterative_deepening.rs +++ b/src/search/iterative_deepening.rs @@ -1,40 +1,30 @@ use crate::{ - board::{game::Game, transposition_table::TranspositionTable}, + board::game::Game, evaluation::{MAX_SCORE, MIN_SCORE}, movegen::r#move::Move, }; -use super::negamax; +use super::{negamax, transposition_table::TranspositionTable, MAX_TT_SIZE}; -pub fn iterative_deepening( - game: &mut Game, - max_depth: u8, - move_time: u128, - mut tt: TranspositionTable, -) -> Result { - let mut nodes = 0; - let (mut mv, _): (Option, i32) = (None, 0); +pub fn iterative_deepening(game: &mut Game, max_depth: u8, move_time: u128) -> Option { + let mut tt = TranspositionTable::new(MAX_TT_SIZE); + let mut mv: Option = None; let time_now = std::time::Instant::now(); - for depth in 0..max_depth { - if move_time < time_now.elapsed().as_millis() { - return Ok(mv.expect("No move selected")); + for depth in 1..=max_depth { + if time_now.elapsed().as_millis() >= move_time { + return mv; } - (mv, _) = negamax::negamax(game, MIN_SCORE, MAX_SCORE, depth, 0, &mut tt, &mut nodes); - dbg!(depth, nodes); + (mv, _) = negamax::negamax(game, MIN_SCORE, MAX_SCORE, depth, 0, &mut tt); } - Ok(mv.expect("No move selected")) + mv } #[cfg(test)] mod tests { - use crate::{ - board::{fen::from_fen, transposition_table::TranspositionTable}, - movegen::attack_generator::init_attacks, - search, - }; + use crate::{board::fen::from_fen, movegen::attack_generator::init_attacks, search}; const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1"; @@ -42,10 +32,9 @@ mod tests { fn test_iterative_deepening() -> Result<(), String> { init_attacks(); let mut game = from_fen(FEN)?; - let tt = TranspositionTable::new(1000000); let time_now = std::time::Instant::now(); - let _ = search::iterative_deepening::iterative_deepening(&mut game, 6, 100000, tt); + let _ = search::iterative_deepening::iterative_deepening(&mut game, 6, 1000); dbg!(time_now.elapsed()); Ok(()) diff --git a/src/search/mod.rs b/src/search/mod.rs index 35eecbb..356b936 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -3,3 +3,9 @@ pub mod move_ordering; pub mod negamax; pub mod perft; pub mod quiescence; +pub mod transposition_table; + +pub const MAX_DEPTH: u8 = 4; +pub const QUIESCENCE_DEPTH: u8 = 3; +pub const MOVE_TIME: u128 = 1000; +pub const MAX_TT_SIZE: u64 = 1000000; diff --git a/src/search/negamax.rs b/src/search/negamax.rs index d1601ca..35e5c5f 100644 --- a/src/search/negamax.rs +++ b/src/search/negamax.rs @@ -1,15 +1,10 @@ use crate::{ - board::{ - game::Game, - transposition_table::{Bound, TTEntry, TranspositionTable}, - }, + board::game::Game, evaluation::{MATE_SCORE, MIN_SCORE}, movegen::r#move::Move, }; -use super::{move_ordering::score_by_mvv_lva, quiescence::quiescence}; - -const QUIESCENCE_DEPTH: u8 = 3; +use super::{move_ordering::score_by_mvv_lva, quiescence::quiescence, transposition_table::{Bound, TTEntry, TranspositionTable}, QUIESCENCE_DEPTH}; pub fn negamax( game: &mut Game, @@ -18,15 +13,14 @@ pub fn negamax( depth: u8, plies: u8, tt: &mut TranspositionTable, - nodes: &mut u64, ) -> (Option, i32) { - *nodes += 1; - if depth == 0 { - let q = quiescence(game, alpha, beta, nodes, QUIESCENCE_DEPTH).1; + let q = quiescence(game, alpha, beta, QUIESCENCE_DEPTH).1; return (None, q); } + let mut tt_move = None; + if let Some(entry) = tt.lookup(game.hash) { if entry.depth >= depth && (matches!(entry.bound, Bound::Exact) @@ -35,6 +29,7 @@ pub fn negamax( { return (entry.mv, entry.score); } + tt_move = entry.mv; } let color = game.current_player(); @@ -43,6 +38,13 @@ pub fn negamax( 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)); + if let Some(tt_move) = tt_move { + if let Some(tt_move_index) = pseudo_legal_moves.iter().position(|&mv| mv == tt_move) { + pseudo_legal_moves.remove(tt_move_index); + pseudo_legal_moves.insert(0, tt_move); + } + } + for mv in pseudo_legal_moves { game.make_move(&mv); @@ -52,7 +54,7 @@ pub fn negamax( } legal_moves += 1; - let move_score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, tt, nodes).1; + let move_score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, tt).1; game.unmake_move(); if move_score > best_score { @@ -65,7 +67,7 @@ pub fn negamax( game.hash, depth, best_score, - None, + best_move, Bound::Lower, )); return (best_move, beta); @@ -80,12 +82,12 @@ pub fn negamax( game.hash, depth, mate_score, - None, + best_move, Bound::Exact, )); return (None, mate_score); } - tt.insert(TTEntry::new(game.hash, depth, 0, None, Bound::Exact)); + tt.insert(TTEntry::new(game.hash, depth, 0, best_move, Bound::Exact)); return (None, 0); } @@ -104,12 +106,13 @@ pub fn negamax( #[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; use crate::movegen::r#move::Move; use crate::search::negamax::negamax; + 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", @@ -121,13 +124,11 @@ mod tests { init_attacks(); let mut game = from_fen(FEN_MATE_IN_1[0])?; - let mut tt = TranspositionTable::new(1000000); + let mut tt = TranspositionTable::new(MAX_TT_SIZE); let e3f2 = Move::new(Square::E3, Square::F2); - let mut nodes = 0; - let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &mut tt, &mut nodes) + let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &mut tt) .0 .unwrap(); - dbg!(nodes); assert_eq!(e3f2, anointed_move); diff --git a/src/search/perft.rs b/src/search/perft.rs index f998a94..4d0667f 100644 --- a/src/search/perft.rs +++ b/src/search/perft.rs @@ -80,23 +80,22 @@ mod tests { let mut nodes = 0; driver(&mut game, &mut nodes, depth); - dbg!(time_now.elapsed()); + dbg!(nodes, time_now.elapsed()); Ok(nodes) } const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1"; - #[ignore] + // #[ignore] #[test] fn test_nodes() -> Result<(), String> { init_attacks(); - assert_eq!(perft(FEN, 6)?, 0); + perft(FEN, 4)?; Ok(()) } - #[test] fn test_perft_1() -> Result<(), String> { init_attacks(); diff --git a/src/search/quiescence.rs b/src/search/quiescence.rs index 6ee9247..41a2f3f 100644 --- a/src/search/quiescence.rs +++ b/src/search/quiescence.rs @@ -2,15 +2,7 @@ use crate::{board::game::Game, evaluation::evaluation::evaluate_position, movege use super::move_ordering::score_by_mvv_lva; -pub fn quiescence( - game: &mut Game, - mut alpha: i32, - beta: i32, - nodes: &mut u64, - depth: u8, -) -> (Option, i32) { - *nodes += 1; - +pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, depth: u8) -> (Option, i32) { if depth == 0 { return (None, evaluate_position(&game.board)); } @@ -37,7 +29,7 @@ pub fn quiescence( continue; } - let move_score = -quiescence(game, -beta, -alpha, nodes, depth - 1).1; + let move_score = -quiescence(game, -beta, -alpha, depth - 1).1; game.unmake_move(); if move_score >= beta { diff --git a/src/board/transposition_table.rs b/src/search/transposition_table.rs similarity index 88% rename from src/board/transposition_table.rs rename to src/search/transposition_table.rs index ff521c3..9e9e31c 100644 --- a/src/board/transposition_table.rs +++ b/src/search/transposition_table.rs @@ -1,6 +1,4 @@ -use crate::movegen::r#move::Move; - -use super::zobrist::ZobristHash; +use crate::{board::zobrist::ZobristHash, movegen::r#move::Move}; pub struct TranspositionTable { positions: Vec>, @@ -74,10 +72,10 @@ pub enum Bound { #[cfg(test)] mod tests { use crate::{ - board::{fen::from_fen, transposition_table::TranspositionTable}, + board::fen::from_fen, evaluation::{MAX_SCORE, MIN_SCORE}, movegen::attack_generator::init_attacks, - search, + search::{self, transposition_table::TranspositionTable, MAX_TT_SIZE}, }; const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1"; @@ -88,13 +86,11 @@ mod tests { fn test_transposition_table() -> Result<(), String> { init_attacks(); let mut game = from_fen(FEN)?; - let mut tt = TranspositionTable::new(1000000); - let mut nodes = 0; + let mut tt = TranspositionTable::new(MAX_TT_SIZE); let time_now = std::time::Instant::now(); // fill Transposition Table - search::negamax::negamax(&mut game, MIN_SCORE, MAX_SCORE, 4, 0, &mut tt, &mut nodes); - dbg!(nodes); + search::negamax::negamax(&mut game, MIN_SCORE, MAX_SCORE, 4, 0, &mut tt); dbg!(time_now.elapsed()); let will_be_hash = from_fen(FEN_WILL_BE)?.hash; @@ -102,6 +98,7 @@ mod tests { assert!(tt_entry.is_some()); + // needs a bigger tt size because it could be a collision entry let wont_be_hash = from_fen(FEN_WONT_BE)?.hash; let tt_entry = tt.lookup(wont_be_hash);