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", "windows-sys",
] ]
[[package]]
name = "anyhow"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@@ -194,9 +200,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.77" version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -298,6 +304,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
name = "zeal" name = "zeal"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"clap", "clap",
"rand", "rand",
] ]

View File

@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.93"
clap = { version = "4.5.20", features = ["derive"] } clap = { version = "4.5.20", features = ["derive"] }
rand = { version = "0.8.5", features = ["small_rng"] } rand = { version = "0.8.5", features = ["small_rng"] }

View File

@@ -10,7 +10,7 @@ use crate::{
use super::psqt::piece_square_score_endgame; 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 { match piece_type {
PieceType::Pawn => 100, PieceType::Pawn => 100,
PieceType::Knight => 320, PieceType::Knight => 320,
@@ -23,11 +23,11 @@ const fn piece_score(piece_type: PieceType) -> i32 {
fn is_end_game(board: &Board) -> bool { fn is_end_game(board: &Board) -> bool {
let white_pieces = board.white_pieces.iter().fold(0, |acc, p| { 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| { 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 (white_pieces + black_pieces) < 2000
@@ -55,7 +55,7 @@ fn evaluate_side_for(board: &Board, color: Color) -> i32 {
Color::Black => mirror_index(lsb(bitboard)), Color::Black => mirror_index(lsb(bitboard)),
}; };
score += piece_score(piece_type); score += material_score(piece_type);
score += psqt(piece_type, psqt_index); score += psqt(piece_type, psqt_index);
bitboard &= bitboard - 1; bitboard &= bitboard - 1;
} }
@@ -81,7 +81,7 @@ mod tests {
board::{Color, PieceType}, board::{Color, PieceType},
fen::from_fen, 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] = [ const FEN_QUIET: [&str; 2] = [
@@ -90,13 +90,13 @@ mod tests {
]; ];
#[test] #[test]
fn test_piece_score() -> Result<(), String> { fn test_material_score() -> Result<(), String> {
assert_eq!(100, piece_score(PieceType::Pawn)); assert_eq!(100, material_score(PieceType::Pawn));
assert_eq!(320, piece_score(PieceType::Knight)); assert_eq!(320, material_score(PieceType::Knight));
assert_eq!(330, piece_score(PieceType::Bishop)); assert_eq!(330, material_score(PieceType::Bishop));
assert_eq!(500, piece_score(PieceType::Rook)); assert_eq!(500, material_score(PieceType::Rook));
assert_eq!(900, piece_score(PieceType::Queen)); assert_eq!(900, material_score(PieceType::Queen));
assert_eq!(0, piece_score(PieceType::King)); assert_eq!(0, material_score(PieceType::King));
Ok(()) Ok(())
} }

View File

@@ -1,8 +1,11 @@
use std::{ use std::{
io::{BufRead, Write}, io::{BufRead, Write},
str::SplitWhitespace, str::SplitWhitespace,
time::Instant,
}; };
use anyhow::{anyhow, bail};
use crate::{ use crate::{
board::{board::Color, game::Game}, board::{board::Color, game::Game},
movegen::r#move::Move, movegen::r#move::Move,
@@ -22,7 +25,7 @@ pub enum Command {
Quit, Quit,
} }
fn parse_command(parts: &mut SplitWhitespace) -> Result<Command, String> { fn parse_command(parts: &mut SplitWhitespace) -> anyhow::Result<Command> {
match parts.next() { match parts.next() {
Some("uci") => Ok(Command::Uci), Some("uci") => Ok(Command::Uci),
Some("isready") => Ok(Command::IsReady), Some("isready") => Ok(Command::IsReady),
@@ -30,7 +33,7 @@ fn parse_command(parts: &mut SplitWhitespace) -> Result<Command, String> {
Some("position") => Ok(Command::Position), Some("position") => Ok(Command::Position),
Some("go") => Ok(Command::Go), Some("go") => Ok(Command::Go),
Some("quit") => Ok(Command::Quit), Some("quit") => Ok(Command::Quit),
_ => Err("Unrecognised command".to_string()), _ => bail!("Unrecognised command"),
} }
} }
@@ -41,9 +44,9 @@ pub enum Response {
Info(String), Info(String),
} }
fn write_response(handle_out: &mut impl Write, response: &Response) -> Result<(), String> { fn write_response(handle_out: &mut impl Write, response: &Response) -> anyhow::Result<()> {
writeln!(handle_out, "{response}").map_err(|e| e.to_string())?; writeln!(handle_out, "{response}").map_err(|e| anyhow!(e))?;
handle_out.flush().map_err(|e| e.to_string())?; handle_out.flush().map_err(|e| anyhow!(e))?;
Ok(()) Ok(())
} }
@@ -107,16 +110,18 @@ impl Default for UciParameters {
} }
} }
pub fn uci_position(position: &mut SplitWhitespace) -> Result<Game, String> { pub fn uci_position(position: &mut SplitWhitespace) -> anyhow::Result<Game> {
let state = position.next().ok_or("Expected startpos or fen")?; let state = position
.next()
.ok_or_else(|| anyhow!("Expected startpos or fen"))?;
let mut game = match state { let mut game = match state {
"startpos" => Game::new(), "startpos" => Game::new(),
"fen" => { "fen" => {
let fen_parts: Vec<&str> = position.take_while(|&part| part != "moves").collect(); let fen_parts: Vec<&str> = position.take_while(|&part| part != "moves").collect();
let fen = fen_parts.join(" "); 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() { if Some("moves") != position.next() {
@@ -124,7 +129,8 @@ pub fn uci_position(position: &mut SplitWhitespace) -> Result<Game, String> {
} }
for mv_str in position { 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); game.make_move(&mv);
} }
@@ -135,7 +141,7 @@ pub fn uci_go(
go_iter: &mut SplitWhitespace, go_iter: &mut SplitWhitespace,
game: &mut Game, game: &mut Game,
tt: &mut TranspositionTable, tt: &mut TranspositionTable,
) -> Result<Move, String> { ) -> anyhow::Result<Move> {
let mut params = UciParameters::new(); let mut params = UciParameters::new();
while let Some(subcommand) = go_iter.next() { while let Some(subcommand) = go_iter.next() {
match subcommand { match subcommand {
@@ -147,23 +153,30 @@ pub fn uci_go(
} }
} }
let time = Instant::now();
let remaining_time = match game.current_player() { let remaining_time = match game.current_player() {
Color::White => params.wtime.unwrap_or(REMAINING_TIME_DEFAULT), Color::White => params.wtime.unwrap_or(REMAINING_TIME_DEFAULT),
Color::Black => params.btime.unwrap_or(REMAINING_TIME_DEFAULT), Color::Black => params.btime.unwrap_or(REMAINING_TIME_DEFAULT),
}; };
iterative_deepening::iterative_deepening(game, MAX_DEPTH, remaining_time, tt) iterative_deepening::iterative_deepening(game, MAX_DEPTH, remaining_time, tt)?.ok_or_else(
.ok_or_else(|| "No move selected".to_string()) || {
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 go_iter
.next() .next()
.ok_or_else(|| format!("Expected {val}")) .ok_or_else(|| anyhow!("Expected {val}"))
.and_then(|v| v.parse::<T>().map_err(|_| format!("Invalid {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 params = UciParameters::new();
let mut tt = TranspositionTable::new(MAX_TT_SIZE); 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)?); params.add_game(uci_position(&mut parts)?);
Response::Info("Initialized position".to_string()) Response::Info("Initialized position".to_string())
} }
Command::Go => { Command::Go => params.game.as_mut().map_or_else(
if let Some(ref mut game) = params.game { || Response::Info("Failed to unwrap from UciParameter".to_string()),
let best_move = uci_go(&mut parts, game, &mut tt)?; |game| match uci_go(&mut parts, game, &mut tt) {
Response::BestMove(best_move.parse_into_str()) Ok(best_move) => Response::BestMove(best_move.parse_into_str()),
} else { Err(e) => Response::Info(e.to_string()),
Response::Info("Going?".to_string()) },
} ),
} // TODO: Command::Stop => (),
Command::Quit => break, Command::Quit => break,
}; };
write_response(&mut output, &response)?; write_response(&mut output, &response)?;
output.flush().map_err(|e| e.to_string())?; output.flush().map_err(|e| anyhow!(e))?
} }
Ok(()) Ok(())
@@ -221,7 +234,7 @@ mod tests {
]; ];
#[test] #[test]
fn test_uci_position() -> Result<(), String> { fn test_uci_position() -> anyhow::Result<()> {
let command_position = "position startpos"; let command_position = "position startpos";
let mut parts = command_position.split_whitespace(); let mut parts = command_position.split_whitespace();
let command = parse_command(&mut parts)?; 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"; const FEN_MATE_IN_1: &str = "8/8/8/8/8/4q1k1/8/5K2 b - - 0 1";
#[test] #[test]
fn test_uci_go() -> Result<(), String> { fn test_uci_go() -> anyhow::Result<()> {
init_attacks(); init_attacks();
let mut tt = TranspositionTable::new(MAX_TT_SIZE); 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 command_go = "go depth 2";
let mut parts = command_go.split_whitespace(); 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, &mut tt)?;
@@ -256,7 +269,7 @@ mod tests {
} }
#[test] #[test]
fn test_uci_loop() -> Result<(), String> { fn test_uci_loop() -> anyhow::Result<()> {
init_attacks(); init_attacks();
let commands = "uci\n\ let commands = "uci\n\
ucinewgame\n\ ucinewgame\n\
@@ -282,7 +295,7 @@ mod tests {
} }
#[test] #[test]
fn test_cute_chess_bug() -> Result<(), String> { fn test_cute_chess_bug() -> anyhow::Result<()> {
init_attacks(); init_attacks();
let commands = "uci\n\ let commands = "uci\n\
ucinewgame\n\ ucinewgame\n\

View File

@@ -7,8 +7,8 @@ use crate::{
}; };
use super::{ use super::{
negamax, transposition_table::TranspositionTable, HARD_LIMIT_DIVISION, SOFT_EVAL_THRESHOLD, negamax, time::TimeInfo, transposition_table::TranspositionTable, HARD_LIMIT_DIVISION,
SOFT_LIMIT_DIVISION, SOFT_EVAL_THRESHOLD, SOFT_LIMIT_DIVISION,
}; };
pub fn iterative_deepening( pub fn iterative_deepening(
@@ -16,30 +16,47 @@ pub fn iterative_deepening(
max_depth: u8, max_depth: u8,
remaining_time: u128, remaining_time: u128,
tt: &mut TranspositionTable, tt: &mut TranspositionTable,
) -> Option<Move> { ) -> anyhow::Result<Option<Move>> {
let (mut best_move, mut best_eval): (Option<Move>, i32) = (None, MIN_SCORE); let (mut best_move, mut best_score) = (None, MIN_SCORE);
let time_now = std::time::Instant::now(); let time = std::time::Instant::now();
for depth in 1..=max_depth { for depth in 1..=max_depth {
if time_limit_reached(&time_now, remaining_time, best_eval) { if time_limit_reached(&time, remaining_time, best_score) {
return best_move; 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 { fn time_limit_reached(time: &Instant, remaining_time: u128, eval: i32) -> bool {
hard_limit(time, remaining_time) || soft_limit(time, remaining_time, eval) 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 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 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"; const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1";
#[test] #[test]
fn test_iterative_deepening() -> Result<(), String> { fn test_iterative_deepening() -> anyhow::Result<()> {
init_attacks(); init_attacks();
let mut game = from_fen(FEN)?; let mut game = from_fen(FEN).unwrap();
let mut tt = TranspositionTable::new(MAX_TT_SIZE); let mut tt = TranspositionTable::new(MAX_TT_SIZE);
let time_now = std::time::Instant::now(); let time_now = std::time::Instant::now();
@@ -68,7 +85,7 @@ mod tests {
MAX_DEPTH, MAX_DEPTH,
REMAINING_TIME_DEFAULT, REMAINING_TIME_DEFAULT,
&mut tt, &mut tt,
); )?;
dbg!(time_now.elapsed()); dbg!(time_now.elapsed());

View File

@@ -1,14 +1,30 @@
use crate::movegen::r#move::Move;
pub mod iterative_deepening; pub mod iterative_deepening;
pub mod move_ordering; pub mod move_ordering;
pub mod negamax; pub mod negamax;
pub mod perft; pub mod perft;
pub mod quiescence; pub mod quiescence;
pub mod time;
pub mod transposition_table; pub mod transposition_table;
pub const MAX_DEPTH: u8 = 7; pub const MAX_DEPTH: u8 = 7;
pub const QUIESCENCE_DEPTH: u8 = 3;
pub const REMAINING_TIME_DEFAULT: u128 = 100000; // in ms pub const REMAINING_TIME_DEFAULT: u128 = 100000; // in ms
pub const HARD_LIMIT_DIVISION: u128 = 10; 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 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::{ use crate::{
board::game::Game, board::game::Game,
evaluation::{MATE_SCORE, MIN_SCORE}, evaluation::{MATE_SCORE, MIN_SCORE},
movegen::r#move::Move,
}; };
use super::{ use super::{
move_ordering::score_by_mvv_lva, iterative_deepening::hard_limit,
move_ordering::mvv_lva,
quiescence::quiescence, quiescence::quiescence,
time::TimeInfo,
transposition_table::{Bound, TTEntry, TranspositionTable}, transposition_table::{Bound, TTEntry, TranspositionTable},
QUIESCENCE_DEPTH, SearchResult,
}; };
pub fn negamax( pub fn negamax(
@@ -17,11 +20,16 @@ pub fn negamax(
beta: i32, beta: i32,
depth: u8, depth: u8,
plies: u8, plies: u8,
time_info: &TimeInfo,
tt: &mut TranspositionTable, 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 { if depth == 0 {
let q = quiescence(game, alpha, beta, QUIESCENCE_DEPTH).1; let q_score = quiescence(game, alpha, beta, time_info).map_err(|e| anyhow!("{e}"))?;
return (None, q); return Ok(SearchResult::new(None, q_score));
} }
let mut tt_move = None; let mut tt_move = None;
@@ -32,7 +40,7 @@ pub fn negamax(
|| (matches!(entry.bound, Bound::Lower) && entry.score >= beta) || (matches!(entry.bound, Bound::Lower) && entry.score >= beta)
|| (matches!(entry.bound, Bound::Upper) && entry.score <= alpha)) || (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; 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 best_move, mut best_score, mate_score) = (None, MIN_SCORE, -MATE_SCORE + plies as i32);
let mut legal_moves = 0; let mut legal_moves = 0;
let mut pseudo_legal_moves = game.board.pseudo_moves_all(); 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) = tt_move {
if let Some(tt_move_index) = pseudo_legal_moves.iter().position(|&mv| mv == 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; 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(); game.unmake_move();
if move_score > best_score { if move_score > best_score {
@@ -75,7 +84,7 @@ pub fn negamax(
best_move, best_move,
Bound::Lower, Bound::Lower,
)); ));
return (best_move, beta); return Ok(SearchResult::new(best_move, beta));
} }
alpha = alpha.max(move_score); alpha = alpha.max(move_score);
@@ -90,10 +99,10 @@ pub fn negamax(
best_move, best_move,
Bound::Exact, 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)); 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 { if best_score < alpha {
@@ -114,7 +123,7 @@ pub fn negamax(
)); ));
} }
(best_move, best_score) Ok(SearchResult::new(best_move, best_score))
} }
#[cfg(test)] #[cfg(test)]
@@ -124,6 +133,7 @@ mod tests {
use crate::movegen::attack_generator::init_attacks; use crate::movegen::attack_generator::init_attacks;
use crate::movegen::r#move::Move; use crate::movegen::r#move::Move;
use crate::search::negamax::negamax; use crate::search::negamax::negamax;
use crate::search::time::TimeInfo;
use crate::search::transposition_table::TranspositionTable; use crate::search::transposition_table::TranspositionTable;
use crate::search::MAX_TT_SIZE; use crate::search::MAX_TT_SIZE;
@@ -133,24 +143,29 @@ mod tests {
]; ];
#[test] #[test]
fn test_negamax() -> Result<(), String> { fn test_negamax() -> anyhow::Result<()> {
init_attacks(); 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 mut tt = TranspositionTable::new(MAX_TT_SIZE);
let e3f2 = Move::new(Square::E3, Square::F2); let e3f2 = Move::new(Square::E3, Square::F2);
let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &mut tt) let time_info = TimeInfo::new(std::time::Instant::now(), 1000);
.0 let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut tt)
.unwrap(); .expect("Expected a search result")
.best_move
.expect("Expected a move");
assert_eq!(e3f2, anointed_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 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, &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(()) 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) { use super::{iterative_deepening::hard_limit, move_ordering::mvv_lva, time::TimeInfo};
if depth == 0 {
return (None, evaluate_position(&game.board)); 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 color = game.current_player();
let stand_pat = evaluate_position(&game.board); let stand_pat = evaluate_position(&game.board);
if stand_pat >= beta { if stand_pat >= beta {
return (None, beta); return Ok(beta);
} }
if alpha < stand_pat { 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(); 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 { for mv in captures {
game.make_move(&mv); 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(); game.unmake_move();
continue; continue;
} }
let move_score = -quiescence(game, -beta, -alpha, time_info)?;
let move_score = -quiescence(game, -beta, -alpha, depth - 1).1;
game.unmake_move(); game.unmake_move();
if move_score >= beta { 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, board::fen::from_fen,
evaluation::{MAX_SCORE, MIN_SCORE}, evaluation::{MAX_SCORE, MIN_SCORE},
movegen::attack_generator::init_attacks, 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: &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_POSSIBLE: &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_IMPOSSIBLE: &str = "1Q2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B4/1P1PNPP1/R3K2R b KQk - 0 1";
#[test] #[test]
fn test_transposition_table() -> Result<(), String> { fn test_transposition_table() -> anyhow::Result<()> {
init_attacks(); init_attacks();
let mut game = from_fen(FEN)?; let mut game = from_fen(FEN).unwrap();
let mut tt = TranspositionTable::new(MAX_TT_SIZE); 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 negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut tt)
search::negamax::negamax(&mut game, MIN_SCORE, MAX_SCORE, 4, 0, &mut tt); .expect("Expected a search result")
dbg!(time_now.elapsed()); .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); let tt_entry = tt.lookup(will_be_hash);
assert!(tt_entry.is_some()); assert!(tt_entry.is_some());
// needs a bigger tt size because it could be a collision entry let wont_be_hash = from_fen(FEN_IMPOSSIBLE).unwrap().hash;
let wont_be_hash = from_fen(FEN_WONT_BE)?.hash;
let tt_entry = tt.lookup(wont_be_hash); let tt_entry = tt.lookup(wont_be_hash);
assert!(tt_entry.is_none()); assert!(tt_entry.is_none());