Skip to content

Commit

Permalink
Fix draw-by-repetition detection
Browse files Browse the repository at this point in the history
It would not count pre-root moves in the detection, meaning it only
found repetitions in-search. This should hopefully cause a _lot_ less
draw-by-repetition in play tests, and less/no drawing in won positions.

Edit: Still a lot of draw-by-repetition in play tests. But spot-checking
them, they all make sense. Positions are entirely equal, and the side
with slightly less control semi-forces the repetition with checks. Time
will show if this still draws won positions. We probably get a lot of
repetition because the evaluation is so simple, and nobody knows how to
make progress in early endgames.
  • Loading branch information
bsamseth committed Aug 27, 2024
1 parent 2b1a182 commit 8349c34
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 27 deletions.
4 changes: 3 additions & 1 deletion engine/src/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ pub trait BoardExt {

impl BoardExt for Board {
fn halfmove_reset(&self, mv: ChessMove) -> bool {
self.piece_on(mv.get_source()).unwrap() == Piece::Pawn
self.piece_on(mv.get_source())
.unwrap_or_else(|| panic!("error at {self} for {mv}"))
== Piece::Pawn
|| self.piece_on(mv.get_dest()).is_some()
}

Expand Down
5 changes: 3 additions & 2 deletions engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,14 @@ impl Engine {
.join(" ")
);
self.transposition_table.new_search();
let (bm, logger) = Searcher::best_move(
let (bm, logger) = Searcher::new(
&position,
&go_options,
interface.stop.clone(),
&mut self.transposition_table,
self.tablebase,
);
)
.best_move();

println!("bestmove {bm}");

Expand Down
75 changes: 68 additions & 7 deletions engine/src/search/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,31 +117,31 @@ impl Searcher<'_> {
// Check for repetition.
// Check positions from the last halfmove clock reset, and return true if we've seen the
// same position twice before. Positions are treated as equal by their Zobrist keys.
let ply = ply.as_usize();
self.ss[ply.saturating_sub(self.ss[ply].halfmove_clock)..ply]
let ply = (self.root_position_ply + ply).as_usize();
let position_count = self.ss[ply.saturating_sub(self.ss[ply].halfmove_clock)..ply]
.iter()
.filter({
let latest_zobrist = self.ss[ply].zobrist;
move |s| s.zobrist == latest_zobrist
})
.count()
>= 2
.count();
position_count >= 2
}

/// Return a reference to the stack state for the given ply.
pub fn stack_state(&self, ply: Ply) -> &StackState {
&self.ss[ply.as_usize()]
&self.ss[(self.root_position_ply + ply).as_usize()]
}
/// Return a mutable reference to the stack state for the given ply.
pub fn stack_state_mut(&mut self, ply: Ply) -> &mut StackState {
&mut self.ss[ply.as_usize()]
&mut self.ss[(self.root_position_ply + ply).as_usize()]
}

/// Remove moves from the root move list that don't preserve the WDL value (if available).
///
/// A tablebase must be initialized, and the root position must be in the tablebase.
pub fn filter_root_moves_using_tb(&mut self) {
let hmc = self.stack_state(Ply::new(0)).halfmove_clock;
let hmc = self.stack_state(Ply::ZERO).halfmove_clock;

if let Some((wdl, filter)) = self
.tablebase
Expand Down Expand Up @@ -174,3 +174,64 @@ impl From<Result<Value, Value>> for Value {
}
}
}

#[cfg(test)]
mod tests {
use std::sync::Arc;

use uci::Position;

use crate::tt::TranspositionTable;

use super::*;

#[test]
fn test_is_draw_by_50_move_rule() {
let board = Board::default();
let position = Position {
start_pos: board,
moves: vec![],
starting_halfmove_clock: 0,
};
let mut tt = TranspositionTable::default();
let mut searcher = Searcher::new(&position, &[], Arc::default(), &mut tt, None);

searcher.stack_state_mut(Ply::ZERO).halfmove_clock = 100;
searcher.stack_state_mut(Ply::ONE).halfmove_clock = 99;
assert!(searcher.is_draw(&board, Ply::ZERO));
assert!(!searcher.is_draw(&board, Ply::ONE));
}

#[test]
fn test_is_draw_by_insufficient_material() {
for fen in [
"k7/8/8/8/8/8/8/KB6 w - - 0 1",
"8/8/nk6/8/8/8/8/K7 b - - 0 1",
] {
let board: Board = fen.parse().unwrap();
assert!(board.has_insufficient_material());
let position = Position {
start_pos: board,
moves: vec![],
starting_halfmove_clock: 0,
};
let mut tt = TranspositionTable::default();
let searcher = Searcher::new(&position, &[], Arc::default(), &mut tt, None);
assert!(searcher.is_draw(&board, Ply::ZERO));
}
}

#[test]
fn test_is_draw_by_repetition() {
let position: Position = "fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 moves g1f3 b8c6 f3g1 c6b8 g1f3 b8c6 f3g1".parse().unwrap();
let mut tt = TranspositionTable::default();
let mut searcher = Searcher::new(&position, &[], Arc::default(), &mut tt, None);
assert!(!searcher.is_draw(&position.start_pos, Ply::ZERO));

let board = searcher.root_position;
let mut new_board = board;
searcher.make_move(&board, "c6b8".parse().unwrap(), &mut new_board, Ply::ZERO);

assert!(searcher.is_draw(&board, Ply::ONE));
}
}
47 changes: 30 additions & 17 deletions engine/src/search/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ use super::newtypes::Ply;
use super::tt::TranspositionTable;
use crate::board::BoardExt;
use crate::evaluate::Evaluate;
use crate::newtypes::Depth;
use fathom::Tablebase;
use stackstate::StackState;
use uci::Position;

#[derive(Debug)]
pub struct Searcher<'a> {
root_position: Board,
root_position_ply: Ply,
ss: [StackState; Ply::MAX.as_usize() + 1],
limits: Limits,
logger: Logger,
Expand All @@ -41,37 +43,46 @@ pub const NON_PV_NODE: bool = false;

impl<'a> Searcher<'a> {
/// Create a new [`Searcher`].
pub fn best_move(
pub fn new(
position: &Position,
go_options: &[uci::GoOption],
stop_signal: Arc<AtomicBool>,
transposition_table: &'a mut TranspositionTable,
tablebase: Option<&'static Tablebase>,
) -> (ChessMove, Logger) {
let mut board = position.start_pos;
let mut root_position = board;
) -> Self {
let mut root_position = position.start_pos;
let mut halfmove_clock = position.starting_halfmove_clock;
for mv in &position.moves {
if board.halfmove_reset(*mv) {
halfmove_clock = 0;
} else {
halfmove_clock += 1;
}
board.make_move(*mv, &mut root_position);
board = root_position;
}
let mut root_position_ply = Ply::ZERO;

let mut stack_states = [StackState::default(); Ply::MAX.as_usize() + 1];
stack_states[0].eval = Some(root_position.evaluate());
stack_states[0].zobrist = root_position.get_hash();
stack_states[0].halfmove_clock = halfmove_clock;

for (i, mv) in position.moves.iter().enumerate() {
if root_position.halfmove_reset(*mv) {
halfmove_clock = 0;
} else {
halfmove_clock += 1;
}
root_position = root_position.make_move_new(*mv);
stack_states[i + 1].eval = Some(root_position.evaluate());
stack_states[i + 1].zobrist = root_position.get_hash();
stack_states[i + 1].halfmove_clock = halfmove_clock;

// Remember pre-root moves, but leave enough stack states for a max search depth.
if position.moves.len() - i < Depth::MAX.as_usize() {
root_position_ply = root_position_ply.increment();
}
}

let root_moves: MoveVec = MoveGen::new_legal(&root_position).into();
let logger = Logger::new().silent(go_options.iter().any(|o| *o == uci::GoOption::Silent));
let limits = Limits::from(go_options).with_time_control(&root_position);

let mut searcher = Self {
Self {
root_position,
root_position_ply,
ss: stack_states,
limits,
logger,
Expand All @@ -80,9 +91,11 @@ impl<'a> Searcher<'a> {
transposition_table,
tablebase,
history_stats: [[0; 64]; 64],
};
}
}

let bm = searcher.run();
(bm, searcher.logger)
pub fn best_move(mut self) -> (ChessMove, Logger) {
let bm = self.run();
(bm, self.logger)
}
}

0 comments on commit 8349c34

Please sign in to comment.