diff --git a/Cargo.lock b/Cargo.lock index 095f1cf..dfee2a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + [[package]] name = "cfg-if" version = "1.0.0" @@ -194,9 +200,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -298,6 +304,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" name = "zeal" version = "0.1.0" dependencies = [ + "anyhow", "clap", "rand", ] diff --git a/Cargo.toml b/Cargo.toml index 8855e75..32227ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.93" clap = { version = "4.5.20", features = ["derive"] } rand = { version = "0.8.5", features = ["small_rng"] } diff --git a/src/evaluation/evaluation.rs b/src/evaluation/evaluation.rs index c156a9f..c019690 100644 --- a/src/evaluation/evaluation.rs +++ b/src/evaluation/evaluation.rs @@ -10,7 +10,7 @@ use crate::{ use super::psqt::piece_square_score_endgame; -const fn piece_score(piece_type: PieceType) -> i32 { +pub const fn material_score(piece_type: PieceType) -> i32 { match piece_type { PieceType::Pawn => 100, PieceType::Knight => 320, @@ -23,11 +23,11 @@ const fn piece_score(piece_type: PieceType) -> i32 { fn is_end_game(board: &Board) -> bool { let white_pieces = board.white_pieces.iter().fold(0, |acc, p| { - acc + bitboard::bit_count(p.bitboard) * piece_score(p.piece_type) as usize + acc + bitboard::bit_count(p.bitboard) * material_score(p.piece_type) as usize }); let black_pieces = board.black_pieces.iter().fold(0, |acc, p| { - acc + bitboard::bit_count(p.bitboard) * piece_score(p.piece_type) as usize + acc + bitboard::bit_count(p.bitboard) * material_score(p.piece_type) as usize }); (white_pieces + black_pieces) < 2000 @@ -55,7 +55,7 @@ fn evaluate_side_for(board: &Board, color: Color) -> i32 { Color::Black => mirror_index(lsb(bitboard)), }; - score += piece_score(piece_type); + score += material_score(piece_type); score += psqt(piece_type, psqt_index); bitboard &= bitboard - 1; } @@ -81,7 +81,7 @@ mod tests { board::{Color, PieceType}, fen::from_fen, }, - evaluation::evaluation::{evaluate_position, evaluate_side_for, piece_score}, + evaluation::evaluation::{evaluate_position, evaluate_side_for, material_score}, }; const FEN_QUIET: [&str; 2] = [ @@ -90,13 +90,13 @@ mod tests { ]; #[test] - fn test_piece_score() -> Result<(), String> { - assert_eq!(100, piece_score(PieceType::Pawn)); - assert_eq!(320, piece_score(PieceType::Knight)); - assert_eq!(330, piece_score(PieceType::Bishop)); - assert_eq!(500, piece_score(PieceType::Rook)); - assert_eq!(900, piece_score(PieceType::Queen)); - assert_eq!(0, piece_score(PieceType::King)); + fn test_material_score() -> Result<(), String> { + assert_eq!(100, material_score(PieceType::Pawn)); + assert_eq!(320, material_score(PieceType::Knight)); + assert_eq!(330, material_score(PieceType::Bishop)); + assert_eq!(500, material_score(PieceType::Rook)); + assert_eq!(900, material_score(PieceType::Queen)); + assert_eq!(0, material_score(PieceType::King)); Ok(()) } diff --git a/src/interface/uci.rs b/src/interface/uci.rs index 32d6ca4..216ec5e 100644 --- a/src/interface/uci.rs +++ b/src/interface/uci.rs @@ -1,8 +1,11 @@ use std::{ io::{BufRead, Write}, str::SplitWhitespace, + time::Instant, }; +use anyhow::{anyhow, bail}; + use crate::{ board::{board::Color, game::Game}, movegen::r#move::Move, @@ -22,7 +25,7 @@ pub enum Command { Quit, } -fn parse_command(parts: &mut SplitWhitespace) -> Result { +fn parse_command(parts: &mut SplitWhitespace) -> anyhow::Result { match parts.next() { Some("uci") => Ok(Command::Uci), Some("isready") => Ok(Command::IsReady), @@ -30,7 +33,7 @@ fn parse_command(parts: &mut SplitWhitespace) -> Result { Some("position") => Ok(Command::Position), Some("go") => Ok(Command::Go), Some("quit") => Ok(Command::Quit), - _ => Err("Unrecognised command".to_string()), + _ => bail!("Unrecognised command"), } } @@ -41,9 +44,9 @@ pub enum Response { Info(String), } -fn write_response(handle_out: &mut impl Write, response: &Response) -> Result<(), String> { - writeln!(handle_out, "{response}").map_err(|e| e.to_string())?; - handle_out.flush().map_err(|e| e.to_string())?; +fn write_response(handle_out: &mut impl Write, response: &Response) -> anyhow::Result<()> { + writeln!(handle_out, "{response}").map_err(|e| anyhow!(e))?; + handle_out.flush().map_err(|e| anyhow!(e))?; Ok(()) } @@ -107,16 +110,18 @@ impl Default for UciParameters { } } -pub fn uci_position(position: &mut SplitWhitespace) -> Result { - let state = position.next().ok_or("Expected startpos or fen")?; +pub fn uci_position(position: &mut SplitWhitespace) -> anyhow::Result { + let state = position + .next() + .ok_or_else(|| anyhow!("Expected startpos or fen"))?; let mut game = match state { "startpos" => Game::new(), "fen" => { let fen_parts: Vec<&str> = position.take_while(|&part| part != "moves").collect(); let fen = fen_parts.join(" "); - Game::from_fen(&fen)? + Game::from_fen(&fen).map_err(|e| anyhow!("Failed to parse FEN: {fen}, {e}"))? } - _ => return Err("Expected startpos or fen".to_string()), + _ => bail!("Expected startpos or fen"), }; if Some("moves") != position.next() { @@ -124,7 +129,8 @@ pub fn uci_position(position: &mut SplitWhitespace) -> Result { } for mv_str in position { - let mv = Move::parse_from_str(&game, mv_str)?; + let mv = Move::parse_from_str(&game, mv_str) + .map_err(|e| anyhow!("Failed to parse move: {e}"))?; game.make_move(&mv); } @@ -135,7 +141,7 @@ pub fn uci_go( go_iter: &mut SplitWhitespace, game: &mut Game, tt: &mut TranspositionTable, -) -> Result { +) -> anyhow::Result { let mut params = UciParameters::new(); while let Some(subcommand) = go_iter.next() { match subcommand { @@ -147,23 +153,30 @@ pub fn uci_go( } } + let time = Instant::now(); let remaining_time = match game.current_player() { Color::White => params.wtime.unwrap_or(REMAINING_TIME_DEFAULT), Color::Black => params.btime.unwrap_or(REMAINING_TIME_DEFAULT), }; - iterative_deepening::iterative_deepening(game, MAX_DEPTH, remaining_time, tt) - .ok_or_else(|| "No move selected".to_string()) + 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() + ) + }, + ) } -fn parse_next(go_iter: &mut SplitWhitespace, val: &str) -> Result { +fn parse_next(go_iter: &mut SplitWhitespace, val: &str) -> anyhow::Result { go_iter .next() - .ok_or_else(|| format!("Expected {val}")) - .and_then(|v| v.parse::().map_err(|_| format!("Invalid {val}"))) + .ok_or_else(|| anyhow!("Expected {val}")) + .and_then(|v| v.parse::().map_err(|_| anyhow!("Invalid {val}"))) } -pub fn uci_loop(input: R, mut output: W) -> Result<(), String> { +pub fn uci_loop(input: R, mut output: W) -> anyhow::Result<()> { let mut params = UciParameters::new(); let mut tt = TranspositionTable::new(MAX_TT_SIZE); @@ -182,19 +195,19 @@ pub fn uci_loop(input: R, mut output: W) -> Result<(), Str params.add_game(uci_position(&mut parts)?); Response::Info("Initialized position".to_string()) } - Command::Go => { - if let Some(ref mut game) = params.game { - let best_move = uci_go(&mut parts, game, &mut tt)?; - Response::BestMove(best_move.parse_into_str()) - } else { - Response::Info("Going?".to_string()) - } - } + 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) { + Ok(best_move) => Response::BestMove(best_move.parse_into_str()), + Err(e) => Response::Info(e.to_string()), + }, + ), + // TODO: Command::Stop => (), Command::Quit => break, }; write_response(&mut output, &response)?; - output.flush().map_err(|e| e.to_string())?; + output.flush().map_err(|e| anyhow!(e))? } Ok(()) @@ -221,7 +234,7 @@ mod tests { ]; #[test] - fn test_uci_position() -> Result<(), String> { + fn test_uci_position() -> anyhow::Result<()> { let command_position = "position startpos"; let mut parts = command_position.split_whitespace(); let command = parse_command(&mut parts)?; @@ -242,10 +255,10 @@ mod tests { const FEN_MATE_IN_1: &str = "8/8/8/8/8/4q1k1/8/5K2 b - - 0 1"; #[test] - fn test_uci_go() -> Result<(), String> { + 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)?; + 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)?; @@ -256,7 +269,7 @@ mod tests { } #[test] - fn test_uci_loop() -> Result<(), String> { + fn test_uci_loop() -> anyhow::Result<()> { init_attacks(); let commands = "uci\n\ ucinewgame\n\ @@ -282,7 +295,7 @@ mod tests { } #[test] - fn test_cute_chess_bug() -> Result<(), String> { + fn test_cute_chess_bug() -> anyhow::Result<()> { init_attacks(); let commands = "uci\n\ ucinewgame\n\ diff --git a/src/search/iterative_deepening.rs b/src/search/iterative_deepening.rs index ab4bac1..630cba9 100644 --- a/src/search/iterative_deepening.rs +++ b/src/search/iterative_deepening.rs @@ -7,8 +7,8 @@ use crate::{ }; use super::{ - negamax, transposition_table::TranspositionTable, HARD_LIMIT_DIVISION, SOFT_EVAL_THRESHOLD, - SOFT_LIMIT_DIVISION, + negamax, time::TimeInfo, transposition_table::TranspositionTable, HARD_LIMIT_DIVISION, + SOFT_EVAL_THRESHOLD, SOFT_LIMIT_DIVISION, }; pub fn iterative_deepening( @@ -16,30 +16,47 @@ pub fn iterative_deepening( max_depth: u8, remaining_time: u128, tt: &mut TranspositionTable, -) -> Option { - let (mut best_move, mut best_eval): (Option, i32) = (None, MIN_SCORE); - let time_now = std::time::Instant::now(); +) -> 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_now, remaining_time, best_eval) { - return best_move; + if time_limit_reached(&time, remaining_time, best_score) { + return Ok(best_move); } - (best_move, best_eval) = negamax::negamax(game, MIN_SCORE, MAX_SCORE, depth, 0, tt); + let search_result = negamax::negamax( + game, + MIN_SCORE, + MAX_SCORE, + depth, + 0, + &TimeInfo::new(time, remaining_time), + tt, + ); + + if let Ok(search_result) = search_result { + if search_result.best_move.is_some() { + best_move = search_result.best_move; + } + best_score = search_result.best_score; + } else { + return Ok(best_move); + } } - best_move + Ok(best_move) } fn time_limit_reached(time: &Instant, remaining_time: u128, eval: i32) -> bool { hard_limit(time, remaining_time) || soft_limit(time, remaining_time, eval) } -fn hard_limit(time_now: &Instant, remaining_time: u128) -> bool { +pub fn hard_limit(time_now: &Instant, remaining_time: u128) -> bool { time_now.elapsed().as_millis() >= remaining_time / HARD_LIMIT_DIVISION } -fn soft_limit(time: &Instant, remaining_time: u128, eval: i32) -> bool { +pub fn soft_limit(time: &Instant, remaining_time: u128, eval: i32) -> bool { time.elapsed().as_millis() >= remaining_time / SOFT_LIMIT_DIVISION && eval > SOFT_EVAL_THRESHOLD } @@ -57,9 +74,9 @@ mod tests { const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1"; #[test] - fn test_iterative_deepening() -> Result<(), String> { + fn test_iterative_deepening() -> anyhow::Result<()> { init_attacks(); - let mut game = from_fen(FEN)?; + let mut game = from_fen(FEN).unwrap(); let mut tt = TranspositionTable::new(MAX_TT_SIZE); let time_now = std::time::Instant::now(); @@ -68,7 +85,7 @@ mod tests { MAX_DEPTH, REMAINING_TIME_DEFAULT, &mut tt, - ); + )?; dbg!(time_now.elapsed()); diff --git a/src/search/mod.rs b/src/search/mod.rs index e763baf..f4dc3c0 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -1,14 +1,30 @@ +use crate::movegen::r#move::Move; + pub mod iterative_deepening; pub mod move_ordering; pub mod negamax; pub mod perft; pub mod quiescence; +pub mod time; pub mod transposition_table; pub const MAX_DEPTH: u8 = 7; -pub const QUIESCENCE_DEPTH: u8 = 3; pub const REMAINING_TIME_DEFAULT: u128 = 100000; // in ms pub const HARD_LIMIT_DIVISION: u128 = 10; -pub const SOFT_LIMIT_DIVISION: u128 = 2; +pub const SOFT_LIMIT_DIVISION: u128 = 20; pub const SOFT_EVAL_THRESHOLD: i32 = 500; -pub const MAX_TT_SIZE: u64 = 1000000; +pub const MAX_TT_SIZE: u64 = 2000000; + +pub struct SearchResult { + pub best_move: Option, + pub best_score: i32, +} + +impl SearchResult { + pub const fn new(best_move: Option, best_score: i32) -> Self { + Self { + best_move, + best_score, + } + } +} diff --git a/src/search/negamax.rs b/src/search/negamax.rs index d1434d0..cbe7d40 100644 --- a/src/search/negamax.rs +++ b/src/search/negamax.rs @@ -1,14 +1,17 @@ +use anyhow::{anyhow, bail, Result}; + use crate::{ board::game::Game, evaluation::{MATE_SCORE, MIN_SCORE}, - movegen::r#move::Move, }; use super::{ - move_ordering::score_by_mvv_lva, + iterative_deepening::hard_limit, + move_ordering::mvv_lva, quiescence::quiescence, + time::TimeInfo, transposition_table::{Bound, TTEntry, TranspositionTable}, - QUIESCENCE_DEPTH, + SearchResult, }; pub fn negamax( @@ -17,11 +20,16 @@ pub fn negamax( beta: i32, depth: u8, plies: u8, + time_info: &TimeInfo, tt: &mut TranspositionTable, -) -> (Option, i32) { +) -> Result { + if hard_limit(&time_info.time, time_info.remaining_time_in_ms) { + bail!("Time is up! In Negamax"); + } + if depth == 0 { - let q = quiescence(game, alpha, beta, QUIESCENCE_DEPTH).1; - return (None, q); + let q_score = quiescence(game, alpha, beta, time_info).map_err(|e| anyhow!("{e}"))?; + return Ok(SearchResult::new(None, q_score)); } let mut tt_move = None; @@ -32,7 +40,7 @@ pub fn negamax( || (matches!(entry.bound, Bound::Lower) && entry.score >= beta) || (matches!(entry.bound, Bound::Upper) && entry.score <= alpha)) { - return (entry.mv, entry.score); + return Ok(SearchResult::new(entry.mv, entry.score)); } tt_move = entry.mv; } @@ -41,7 +49,7 @@ pub fn negamax( 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)); + pseudo_legal_moves.sort_unstable_by_key(|mv| 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) { @@ -59,7 +67,8 @@ pub fn negamax( } legal_moves += 1; - let move_score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, tt).1; + let move_score = + -negamax(game, -beta, -alpha, depth - 1, plies + 1, time_info, tt)?.best_score; game.unmake_move(); if move_score > best_score { @@ -75,7 +84,7 @@ pub fn negamax( best_move, Bound::Lower, )); - return (best_move, beta); + return Ok(SearchResult::new(best_move, beta)); } alpha = alpha.max(move_score); @@ -90,10 +99,10 @@ pub fn negamax( best_move, Bound::Exact, )); - return (None, mate_score); + return Ok(SearchResult::new(None, mate_score)); } tt.insert(TTEntry::new(game.hash, depth, 0, best_move, Bound::Exact)); - return (None, 0); + return Ok(SearchResult::new(None, 0)); } if best_score < alpha { @@ -114,7 +123,7 @@ pub fn negamax( )); } - (best_move, best_score) + Ok(SearchResult::new(best_move, best_score)) } #[cfg(test)] @@ -124,6 +133,7 @@ mod tests { use crate::movegen::attack_generator::init_attacks; 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; @@ -133,24 +143,29 @@ mod tests { ]; #[test] - fn test_negamax() -> Result<(), String> { + fn test_negamax() -> anyhow::Result<()> { init_attacks(); - let mut game = from_fen(FEN_MATE_IN_1[0])?; + 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 anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &mut tt) - .0 - .unwrap(); + 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) + .expect("Expected a search result") + .best_move + .expect("Expected a move"); assert_eq!(e3f2, anointed_move); - // let mut game = from_fen(FEN_MATE_IN_1[1])?; + 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).0.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) + .expect("Expected a search result") + .best_move + .expect("Expected a move"); - // assert_eq!(e3f2, anointed_move); + assert_eq!(e3f2, anointed_move); Ok(()) } diff --git a/src/search/quiescence.rs b/src/search/quiescence.rs index 41a2f3f..7ba52dd 100644 --- a/src/search/quiescence.rs +++ b/src/search/quiescence.rs @@ -1,17 +1,19 @@ -use crate::{board::game::Game, evaluation::evaluation::evaluate_position, movegen::r#move::Move}; +use anyhow::{bail, Result}; -use super::move_ordering::score_by_mvv_lva; +use crate::{board::game::Game, evaluation::evaluation::evaluate_position}; -pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, depth: u8) -> (Option, i32) { - if depth == 0 { - return (None, evaluate_position(&game.board)); +use super::{iterative_deepening::hard_limit, move_ordering::mvv_lva, time::TimeInfo}; + +pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, time_info: &TimeInfo) -> Result { + if hard_limit(&time_info.time, time_info.remaining_time_in_ms) { + bail!("Time is up! In Quiescence"); } let color = game.current_player(); let stand_pat = evaluate_position(&game.board); if stand_pat >= beta { - return (None, beta); + return Ok(beta); } if alpha < stand_pat { @@ -19,7 +21,7 @@ pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, depth: u8) -> (Opt } let mut captures: Vec<_> = game.board.pseudo_moves_all_captures(); - captures.sort_unstable_by_key(|mv| score_by_mvv_lva(&game.mailbox, *mv)); + captures.sort_unstable_by_key(|mv| mvv_lva(&game.mailbox, *mv)); for mv in captures { game.make_move(&mv); @@ -28,16 +30,17 @@ pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, depth: u8) -> (Opt game.unmake_move(); continue; } - - let move_score = -quiescence(game, -beta, -alpha, depth - 1).1; + let move_score = -quiescence(game, -beta, -alpha, time_info)?; game.unmake_move(); if move_score >= beta { - return (Some(mv), beta); + return Ok(beta); } - alpha = alpha.max(move_score); + if alpha > move_score { + alpha = move_score + } } - (None, alpha) + Ok(alpha) } diff --git a/src/search/time.rs b/src/search/time.rs new file mode 100644 index 0000000..e8f5246 --- /dev/null +++ b/src/search/time.rs @@ -0,0 +1,15 @@ +use std::time::Instant; + +pub struct TimeInfo { + pub time: Instant, + pub remaining_time_in_ms: u128, +} + +impl TimeInfo { + pub const fn new(time: Instant, remaining_time_in_ms: u128) -> Self { + Self { + time, + remaining_time_in_ms, + } + } +} diff --git a/src/search/transposition_table.rs b/src/search/transposition_table.rs index 9e9e31c..95af6d5 100644 --- a/src/search/transposition_table.rs +++ b/src/search/transposition_table.rs @@ -75,31 +75,35 @@ mod tests { board::fen::from_fen, evaluation::{MAX_SCORE, MIN_SCORE}, movegen::attack_generator::init_attacks, - search::{self, transposition_table::TranspositionTable, MAX_TT_SIZE}, + search::{ + negamax::negamax, time::TimeInfo, transposition_table::TranspositionTable, MAX_TT_SIZE, + }, }; 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"; + const FEN_POSSIBLE: &str = "1r2k2r/2P1pq1p/2npb3/1B3ppP/p3P3/P4Q2/1P1PNPP1/R3K2R b KQk - 0 1"; + const FEN_IMPOSSIBLE: &str = "1Q2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B4/1P1PNPP1/R3K2R b KQk - 0 1"; #[test] - fn test_transposition_table() -> Result<(), String> { + fn test_transposition_table() -> anyhow::Result<()> { init_attacks(); - let mut game = from_fen(FEN)?; + let mut game = from_fen(FEN).unwrap(); let mut tt = TranspositionTable::new(MAX_TT_SIZE); - let time_now = std::time::Instant::now(); + let time_info = TimeInfo::new(std::time::Instant::now(), 30000); - // fill Transposition Table - search::negamax::negamax(&mut game, MIN_SCORE, MAX_SCORE, 4, 0, &mut tt); - dbg!(time_now.elapsed()); + negamax(&mut game, MIN_SCORE, MAX_SCORE, 3, 0, &time_info, &mut tt) + .expect("Expected a search result") + .best_move + .expect("Expected a move"); - let will_be_hash = from_fen(FEN_WILL_BE)?.hash; + dbg!(time_info.time.elapsed()); + + let will_be_hash = from_fen(FEN_POSSIBLE).unwrap().hash; let tt_entry = tt.lookup(will_be_hash); 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 wont_be_hash = from_fen(FEN_IMPOSSIBLE).unwrap().hash; let tt_entry = tt.lookup(wont_be_hash); assert!(tt_entry.is_none());