Add time limits, use anyhow crate to improve error handling, remove depth limit on quiescence search

This commit is contained in:
stefiosif
2024-11-13 21:16:49 +02:00
parent 30d90cb3a0
commit 39322ff0e1
10 changed files with 199 additions and 108 deletions

11
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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"] }

View File

@@ -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(())
}

View File

@@ -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<Command, String> {
fn parse_command(parts: &mut SplitWhitespace) -> anyhow::Result<Command> {
match parts.next() {
Some("uci") => Ok(Command::Uci),
Some("isready") => Ok(Command::IsReady),
@@ -30,7 +33,7 @@ fn parse_command(parts: &mut SplitWhitespace) -> Result<Command, String> {
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<Game, String> {
let state = position.next().ok_or("Expected startpos or fen")?;
pub fn uci_position(position: &mut SplitWhitespace) -> anyhow::Result<Game> {
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<Game, String> {
}
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<Move, String> {
) -> anyhow::Result<Move> {
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<T: std::str::FromStr>(go_iter: &mut SplitWhitespace, val: &str) -> Result<T, String> {
fn parse_next<T: std::str::FromStr>(go_iter: &mut SplitWhitespace, val: &str) -> anyhow::Result<T> {
go_iter
.next()
.ok_or_else(|| format!("Expected {val}"))
.and_then(|v| v.parse::<T>().map_err(|_| format!("Invalid {val}")))
.ok_or_else(|| anyhow!("Expected {val}"))
.and_then(|v| v.parse::<T>().map_err(|_| anyhow!("Invalid {val}")))
}
pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> Result<(), String> {
pub fn uci_loop<R: BufRead, W: Write>(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<R: BufRead, W: Write>(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\

View File

@@ -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<Move> {
let (mut best_move, mut best_eval): (Option<Move>, i32) = (None, MIN_SCORE);
let time_now = std::time::Instant::now();
) -> anyhow::Result<Option<Move>> {
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());

View File

@@ -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<Move>,
pub best_score: i32,
}
impl SearchResult {
pub const fn new(best_move: Option<Move>, best_score: i32) -> Self {
Self {
best_move,
best_score,
}
}
}

View File

@@ -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<Move>, i32) {
) -> Result<SearchResult> {
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(())
}

View File

@@ -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<Move>, 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<i32> {
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)
}

15
src/search/time.rs Normal file
View File

@@ -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,
}
}
}

View File

@@ -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, 2, 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());