Skip to content

Commit

Permalink
feat: implement aspiration windows
Browse files Browse the repository at this point in the history
```
Elo   | 51.79 +- 20.20 (95%)
SPRT  | 8.0+0.08s Threads=1 Hash=16MB
LLR   | 3.28 (-2.94, 2.94) [0.00, 10.00]
Games | N: 588 W: 216 L: 129 D: 243
Penta | [7, 54, 114, 83, 36]
https://pyronomy.pythonanywhere.com/test/385/
```

bench: 1583604
  • Loading branch information
DeveloperPaul123 authored Dec 12, 2024
2 parents 8232498 + 7fcc427 commit a78d965
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 18 deletions.
78 changes: 78 additions & 0 deletions engine/src/aspiration_window.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions engine/src/defs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 2 additions & 0 deletions engine/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod aspiration_window;
pub mod defs;
pub mod engine;
pub mod evaluation;
Expand All @@ -8,3 +9,4 @@ pub mod score;
pub mod search;
pub mod search_thread;
pub mod ttable;
pub mod tuneable;
77 changes: 75 additions & 2 deletions engine/src/score.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@
* 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
* https://www.gnu.org/licenses/gpl-3.0-standalone.html
*
*/

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.
Expand All @@ -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
Expand All @@ -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<Score> for UciScore {
Expand Down Expand Up @@ -120,3 +134,62 @@ impl SubAssign<ScoreType> for Score {
self.0 -= rhs;
}
}

impl Div<ScoreType> for Score {
type Output = Score;
fn div(self, rhs: ScoreType) -> Score {
Score(self.0 / rhs)
}
}

impl Div<Score> for Score {
type Output = Score;
fn div(self, rhs: Score) -> Score {
Score(self.0 / rhs.0)
}
}

impl DivAssign<ScoreType> for Score {
fn div_assign(&mut self, rhs: ScoreType) {
self.0 /= rhs;
}
}

impl DivAssign<Score> for Score {
fn div_assign(&mut self, rhs: Score) {
self.0 /= rhs.0;
}
}

impl Mul<ScoreType> for Score {
type Output = Score;
fn mul(self, rhs: ScoreType) -> Score {
Score(self.0 * rhs)
}
}

impl Mul<Score> for Score {
type Output = Score;
fn mul(self, rhs: Score) -> Score {
Score(self.0 * rhs.0)
}
}

impl MulAssign<ScoreType> for Score {
fn mul_assign(&mut self, rhs: ScoreType) {
self.0 *= rhs;
}
}

impl MulAssign<Score> for Score {
fn mul_assign(&mut self, rhs: Score) {
self.0 *= rhs.0;
}
}

impl Shl<u32> for Score {
type Output = Score;
fn shl(self, rhs: u32) -> Score {
Score(self.0 << rhs)
}
}
51 changes: 35 additions & 16 deletions engine/src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ 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},
ttable::{self, TranspositionTableEntry},
};
use ttable::TranspositionTable;

const MAX_DEPTH: u8 = 128;

/// Result for a search.
#[derive(Clone, Copy, Debug)]
pub struct SearchResult {
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions engine/src/tuneable.rs
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit a78d965

Please sign in to comment.