diff --git a/engine/src/aspiration_window.rs b/engine/src/aspiration_window.rs new file mode 100644 index 0000000..984e6cd --- /dev/null +++ b/engine/src/aspiration_window.rs @@ -0,0 +1,78 @@ +use crate::{ + score::{Score, ScoreType}, + tuneable::{ASPIRATION_WINDOW, MIN_ASPIRATION_DEPTH}, +}; + +pub(crate) struct AspirationWindow { + alpha: Score, + beta: Score, + alpha_fails: u32, + beta_fails: u32, +} + +impl AspirationWindow { + pub(crate) fn infinite() -> Self { + Self { + alpha: -Score::INF, + beta: Score::INF, + alpha_fails: 0, + beta_fails: 0, + } + } + + pub(crate) fn alpha(&self) -> Score { + self.alpha + } + + pub(crate) fn beta(&self) -> Score { + self.beta + } + + pub(crate) fn failed_low(&self, score: Score) -> bool { + score != -Score::INF && score <= self.alpha + } + + pub(crate) fn failed_high(&self, score: Score) -> bool { + score != Score::INF && score >= self.beta + } + + /// Create a new [`AspirationWindow`] centered around the given score. + pub(crate) fn around(score: Score, depth: ScoreType) -> Self { + if depth <= MIN_ASPIRATION_DEPTH || score.is_mate() { + // If the score is mate, we can't use the window as we would expect search results to fluctuate. + // Set it to a full window and search again. + // We also want to do a full search on the first iteration (i.e. depth == 1); + Self::infinite() + } else { + let window = Self::window_size(depth); + Self { + alpha: (score - window).max(-Score::INF), + beta: (score + window).min(Score::INF), + alpha_fails: 0, + beta_fails: 0, + } + } + } + + pub(crate) fn widen_down(&mut self, score: Score, depth: ScoreType) { + // Note that we do not alter beta here, as we are widening the window downwards. + let margin = Self::window_size(depth) + self.alpha_fails as ScoreType * ASPIRATION_WINDOW; + self.alpha = (score - margin).max(-Score::INF); + // save that this was a fail low + self.alpha_fails += 1; + } + + pub(crate) fn widen_up(&mut self, score: Score, depth: ScoreType) { + // Note that we do not alter alpha here, as we are widening the window upwards. + let margin = Self::window_size(depth) + self.beta_fails as ScoreType * ASPIRATION_WINDOW; + let new_beta = (score.0 as i32 + margin.0 as i32).min(Score::INF.0 as i32); + self.beta = Score::new(new_beta as ScoreType); + // save that this was a fail high + self.beta_fails += 1; + } + + fn window_size(_depth: ScoreType) -> Score { + // TODO(PT): Scale the window to depth + Score::new(ASPIRATION_WINDOW) + } +} diff --git a/engine/src/defs.rs b/engine/src/defs.rs index b2b0d91..33a8279 100644 --- a/engine/src/defs.rs +++ b/engine/src/defs.rs @@ -30,3 +30,5 @@ impl About { pub const AUTHORS: &'static str = "Paul T. (DeveloperPaul123)"; pub const BANNER: &'static str = BANNER; } + +pub(crate) const MAX_DEPTH: u8 = 128; diff --git a/engine/src/lib.rs b/engine/src/lib.rs index ea38d02..3d1d268 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -1,3 +1,4 @@ +pub mod aspiration_window; pub mod defs; pub mod engine; pub mod evaluation; @@ -8,3 +9,4 @@ pub mod score; pub mod search; pub mod search_thread; pub mod ttable; +pub mod tuneable; diff --git a/engine/src/score.rs b/engine/src/score.rs index ca333fd..3bf24dc 100644 --- a/engine/src/score.rs +++ b/engine/src/score.rs @@ -4,7 +4,7 @@ * Created Date: Thursday, November 14th 2024 * Author: Paul Tsouchlos (DeveloperPaul123) (developer.paul.123@gmail.com) * ----- - * Last Modified: Tue Dec 10 2024 + * Last Modified: Thu Dec 12 2024 * ----- * Copyright (c) 2024 Paul Tsouchlos (DeveloperPaul123) * GNU General Public License v3.0 or later @@ -12,13 +12,15 @@ * */ -use std::ops::{Sub, SubAssign}; +use std::ops::{Div, DivAssign, Mul, MulAssign, Shl, Sub, SubAssign}; use std::{ fmt::{self, Display, Formatter}, ops::{Add, AddAssign, Neg}, }; use uci_parser::UciScore; +use crate::defs::MAX_DEPTH; + pub(crate) type ScoreType = i16; pub(crate) type MoveOrderScoreType = i32; /// Represents a score in centipawns. @@ -28,6 +30,8 @@ pub struct Score(pub ScoreType); impl Score { pub const DRAW: Score = Score(0); pub const MATE: Score = Score(ScoreType::MAX as ScoreType); + /// The minimum mate score. This is the maximum score minus the maximum depth. + pub const MINIMUM_MATE: Score = Score(Score::MATE.0 - MAX_DEPTH as ScoreType); pub const INF: Score = Score(ScoreType::MAX as ScoreType); // Max/min score for history heuristic @@ -41,6 +45,16 @@ impl Score { pub fn clamp(&self, min: ScoreType, max: ScoreType) -> Score { Score(self.0.clamp(min, max)) } + + /// Returns true if the score is a mate score. + /// This is the case if the absolute value of the score is greater than or equal to `Score::MINIMUM_MATE`. + pub fn is_mate(&self) -> bool { + self.0.abs() >= Score::MINIMUM_MATE.0.abs() + } + + pub fn pow(&self, exp: u32) -> Score { + Score(self.0.pow(exp)) + } } impl From for UciScore { @@ -120,3 +134,62 @@ impl SubAssign for Score { self.0 -= rhs; } } + +impl Div for Score { + type Output = Score; + fn div(self, rhs: ScoreType) -> Score { + Score(self.0 / rhs) + } +} + +impl Div for Score { + type Output = Score; + fn div(self, rhs: Score) -> Score { + Score(self.0 / rhs.0) + } +} + +impl DivAssign for Score { + fn div_assign(&mut self, rhs: ScoreType) { + self.0 /= rhs; + } +} + +impl DivAssign for Score { + fn div_assign(&mut self, rhs: Score) { + self.0 /= rhs.0; + } +} + +impl Mul for Score { + type Output = Score; + fn mul(self, rhs: ScoreType) -> Score { + Score(self.0 * rhs) + } +} + +impl Mul for Score { + type Output = Score; + fn mul(self, rhs: Score) -> Score { + Score(self.0 * rhs.0) + } +} + +impl MulAssign for Score { + fn mul_assign(&mut self, rhs: ScoreType) { + self.0 *= rhs; + } +} + +impl MulAssign for Score { + fn mul_assign(&mut self, rhs: Score) { + self.0 *= rhs.0; + } +} + +impl Shl for Score { + type Output = Score; + fn shl(self, rhs: u32) -> Score { + Score(self.0 << rhs) + } +} diff --git a/engine/src/search.rs b/engine/src/search.rs index 220eff8..ee243d3 100644 --- a/engine/src/search.rs +++ b/engine/src/search.rs @@ -26,6 +26,8 @@ use itertools::Itertools; use uci_parser::{UciInfo, UciResponse, UciSearchOptions}; use crate::{ + aspiration_window::AspirationWindow, + defs::MAX_DEPTH, evaluation::Evaluation, history_table::HistoryTable, score::{MoveOrderScoreType, Score, ScoreType}, @@ -33,8 +35,6 @@ use crate::{ }; use ttable::TranspositionTable; -const MAX_DEPTH: u8 = 128; - /// Result for a search. #[derive(Clone, Copy, Debug)] pub struct SearchResult { @@ -224,28 +224,47 @@ impl<'a> Search<'a> { // initialize the best result let mut best_result = SearchResult::default(); let mut move_list = MoveList::new(); + self.move_gen.generate_legal_moves(board, &mut move_list); if !move_list.is_empty() { best_result.best_move = Some(*move_list.at(0).unwrap()) } - while self.parameters.start_time.elapsed() <= self.parameters.soft_timeout + 'deepening: while self.parameters.start_time.elapsed() <= self.parameters.soft_timeout && best_result.depth <= self.parameters.max_depth { - // search the tree, starting at the current depth (starts at 1) - let score = self.negamax( - board, - best_result.depth as ScoreType, - 0, - -Score::INF, - Score::INF, - ); + // create an aspiration window around the best result so far + let mut aspiration_window = + AspirationWindow::around(best_result.score, best_result.depth as ScoreType); + + let mut score: Score; + 'aspiration_window: loop { + // search the tree, starting at the current depth (starts at 1) + score = self.negamax( + board, + best_result.depth as ScoreType, + 0, + aspiration_window.alpha(), + aspiration_window.beta(), + ); + + if aspiration_window.failed_low(score) { + // fail low, widen the window + aspiration_window.widen_down(score, best_result.depth as ScoreType); + } else if aspiration_window.failed_high(score) { + // fail high, widen the window + aspiration_window.widen_up(score, best_result.depth as ScoreType); + } else { + // we have a valid score, break the loop + break 'aspiration_window; + } - // check stop conditions - if self.should_stop_searching() { - // we have to stop searching now, use the best result we have - // no score update - break; + // check stop conditions + if self.should_stop_searching() { + // we have to stop searching now, use the best result we have + // no score update + break 'deepening; + } } // update the best result diff --git a/engine/src/tuneable.rs b/engine/src/tuneable.rs new file mode 100644 index 0000000..fd5fea8 --- /dev/null +++ b/engine/src/tuneable.rs @@ -0,0 +1,18 @@ +/* + * tuneable.rs + * Part of the byte-knight project + * Created Date: Wednesday, December 11th 2024 + * Author: Paul Tsouchlos (DeveloperPaul123) (developer.paul.123@gmail.com) + * ----- + * Last Modified: Thu Dec 12 2024 + * ----- + * Copyright (c) 2024 Paul Tsouchlos (DeveloperPaul123) + * GNU General Public License v3.0 or later + * https://www.gnu.org/licenses/gpl-3.0-standalone.html + * + */ + +use crate::score::ScoreType; + +pub(crate) const MIN_ASPIRATION_DEPTH: ScoreType = 1; +pub(crate) const ASPIRATION_WINDOW: ScoreType = 50;