Skip to content

Commit 8349c34

Browse files
committed
Fix draw-by-repetition detection
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.
1 parent 2b1a182 commit 8349c34

File tree

4 files changed

+104
-27
lines changed

4 files changed

+104
-27
lines changed

Diff for: engine/src/board.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ pub trait BoardExt {
2323

2424
impl BoardExt for Board {
2525
fn halfmove_reset(&self, mv: ChessMove) -> bool {
26-
self.piece_on(mv.get_source()).unwrap() == Piece::Pawn
26+
self.piece_on(mv.get_source())
27+
.unwrap_or_else(|| panic!("error at {self} for {mv}"))
28+
== Piece::Pawn
2729
|| self.piece_on(mv.get_dest()).is_some()
2830
}
2931

Diff for: engine/src/lib.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,14 @@ impl Engine {
108108
.join(" ")
109109
);
110110
self.transposition_table.new_search();
111-
let (bm, logger) = Searcher::best_move(
111+
let (bm, logger) = Searcher::new(
112112
&position,
113113
&go_options,
114114
interface.stop.clone(),
115115
&mut self.transposition_table,
116116
self.tablebase,
117-
);
117+
)
118+
.best_move();
118119

119120
println!("bestmove {bm}");
120121

Diff for: engine/src/search/helpers.rs

+68-7
Original file line numberDiff line numberDiff line change
@@ -117,31 +117,31 @@ impl Searcher<'_> {
117117
// Check for repetition.
118118
// Check positions from the last halfmove clock reset, and return true if we've seen the
119119
// same position twice before. Positions are treated as equal by their Zobrist keys.
120-
let ply = ply.as_usize();
121-
self.ss[ply.saturating_sub(self.ss[ply].halfmove_clock)..ply]
120+
let ply = (self.root_position_ply + ply).as_usize();
121+
let position_count = self.ss[ply.saturating_sub(self.ss[ply].halfmove_clock)..ply]
122122
.iter()
123123
.filter({
124124
let latest_zobrist = self.ss[ply].zobrist;
125125
move |s| s.zobrist == latest_zobrist
126126
})
127-
.count()
128-
>= 2
127+
.count();
128+
position_count >= 2
129129
}
130130

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

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

146146
if let Some((wdl, filter)) = self
147147
.tablebase
@@ -174,3 +174,64 @@ impl From<Result<Value, Value>> for Value {
174174
}
175175
}
176176
}
177+
178+
#[cfg(test)]
179+
mod tests {
180+
use std::sync::Arc;
181+
182+
use uci::Position;
183+
184+
use crate::tt::TranspositionTable;
185+
186+
use super::*;
187+
188+
#[test]
189+
fn test_is_draw_by_50_move_rule() {
190+
let board = Board::default();
191+
let position = Position {
192+
start_pos: board,
193+
moves: vec![],
194+
starting_halfmove_clock: 0,
195+
};
196+
let mut tt = TranspositionTable::default();
197+
let mut searcher = Searcher::new(&position, &[], Arc::default(), &mut tt, None);
198+
199+
searcher.stack_state_mut(Ply::ZERO).halfmove_clock = 100;
200+
searcher.stack_state_mut(Ply::ONE).halfmove_clock = 99;
201+
assert!(searcher.is_draw(&board, Ply::ZERO));
202+
assert!(!searcher.is_draw(&board, Ply::ONE));
203+
}
204+
205+
#[test]
206+
fn test_is_draw_by_insufficient_material() {
207+
for fen in [
208+
"k7/8/8/8/8/8/8/KB6 w - - 0 1",
209+
"8/8/nk6/8/8/8/8/K7 b - - 0 1",
210+
] {
211+
let board: Board = fen.parse().unwrap();
212+
assert!(board.has_insufficient_material());
213+
let position = Position {
214+
start_pos: board,
215+
moves: vec![],
216+
starting_halfmove_clock: 0,
217+
};
218+
let mut tt = TranspositionTable::default();
219+
let searcher = Searcher::new(&position, &[], Arc::default(), &mut tt, None);
220+
assert!(searcher.is_draw(&board, Ply::ZERO));
221+
}
222+
}
223+
224+
#[test]
225+
fn test_is_draw_by_repetition() {
226+
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();
227+
let mut tt = TranspositionTable::default();
228+
let mut searcher = Searcher::new(&position, &[], Arc::default(), &mut tt, None);
229+
assert!(!searcher.is_draw(&position.start_pos, Ply::ZERO));
230+
231+
let board = searcher.root_position;
232+
let mut new_board = board;
233+
searcher.make_move(&board, "c6b8".parse().unwrap(), &mut new_board, Ply::ZERO);
234+
235+
assert!(searcher.is_draw(&board, Ply::ONE));
236+
}
237+
}

Diff for: engine/src/search/mod.rs

+30-17
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ use super::newtypes::Ply;
1818
use super::tt::TranspositionTable;
1919
use crate::board::BoardExt;
2020
use crate::evaluate::Evaluate;
21+
use crate::newtypes::Depth;
2122
use fathom::Tablebase;
2223
use stackstate::StackState;
2324
use uci::Position;
2425

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

4244
impl<'a> Searcher<'a> {
4345
/// Create a new [`Searcher`].
44-
pub fn best_move(
46+
pub fn new(
4547
position: &Position,
4648
go_options: &[uci::GoOption],
4749
stop_signal: Arc<AtomicBool>,
4850
transposition_table: &'a mut TranspositionTable,
4951
tablebase: Option<&'static Tablebase>,
50-
) -> (ChessMove, Logger) {
51-
let mut board = position.start_pos;
52-
let mut root_position = board;
52+
) -> Self {
53+
let mut root_position = position.start_pos;
5354
let mut halfmove_clock = position.starting_halfmove_clock;
54-
for mv in &position.moves {
55-
if board.halfmove_reset(*mv) {
56-
halfmove_clock = 0;
57-
} else {
58-
halfmove_clock += 1;
59-
}
60-
board.make_move(*mv, &mut root_position);
61-
board = root_position;
62-
}
55+
let mut root_position_ply = Ply::ZERO;
6356

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

62+
for (i, mv) in position.moves.iter().enumerate() {
63+
if root_position.halfmove_reset(*mv) {
64+
halfmove_clock = 0;
65+
} else {
66+
halfmove_clock += 1;
67+
}
68+
root_position = root_position.make_move_new(*mv);
69+
stack_states[i + 1].eval = Some(root_position.evaluate());
70+
stack_states[i + 1].zobrist = root_position.get_hash();
71+
stack_states[i + 1].halfmove_clock = halfmove_clock;
72+
73+
// Remember pre-root moves, but leave enough stack states for a max search depth.
74+
if position.moves.len() - i < Depth::MAX.as_usize() {
75+
root_position_ply = root_position_ply.increment();
76+
}
77+
}
78+
6979
let root_moves: MoveVec = MoveGen::new_legal(&root_position).into();
7080
let logger = Logger::new().silent(go_options.iter().any(|o| *o == uci::GoOption::Silent));
7181
let limits = Limits::from(go_options).with_time_control(&root_position);
7282

73-
let mut searcher = Self {
83+
Self {
7484
root_position,
85+
root_position_ply,
7586
ss: stack_states,
7687
limits,
7788
logger,
@@ -80,9 +91,11 @@ impl<'a> Searcher<'a> {
8091
transposition_table,
8192
tablebase,
8293
history_stats: [[0; 64]; 64],
83-
};
94+
}
95+
}
8496

85-
let bm = searcher.run();
86-
(bm, searcher.logger)
97+
pub fn best_move(mut self) -> (ChessMove, Logger) {
98+
let bm = self.run();
99+
(bm, self.logger)
87100
}
88101
}

0 commit comments

Comments
 (0)