Skip to content

Commit cd257f3

Browse files
committed
Implement 3-fold repitition rule for algos and logic. Implement search
extension module
1 parent 0372cd2 commit cd257f3

File tree

4 files changed

+137
-60
lines changed

4 files changed

+137
-60
lines changed

src/algorithms/the_algorithm.rs

Lines changed: 85 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ pub(crate) struct Algorithm {
1212
pub(crate) modules: u32,
1313
transposition_table: HashMap<Board, TranspositionEntry>,
1414
pub(crate) time_per_move: Duration,
15+
/// Number of times that a given board has been played
16+
pub(crate) board_played_times: HashMap<Board, u32>,
1517
}
1618

1719
impl Algorithm {
1820
pub(crate) fn new(modules: u32, time_per_move: Duration) -> Self {
1921
Self {
2022
modules,
21-
transposition_table: HashMap::new(),
23+
transposition_table: HashMap::with_capacity(45),
2224
time_per_move,
25+
board_played_times: HashMap::new(),
2326
}
2427
}
2528

@@ -33,6 +36,7 @@ impl Algorithm {
3336
original: bool,
3437
deadline: Option<Instant>,
3538
stats: &mut Stats,
39+
num_extensions: u32,
3640
) -> Evaluation {
3741
if depth == 0 {
3842
stats.leaves_visited += 1;
@@ -70,7 +74,7 @@ impl Algorithm {
7074
return best_evaluation;
7175
}
7276

73-
let mut boards = legal_moves
77+
let boards = legal_moves
7478
.map(|chess_move| {
7579
let board = board.make_move_new(chess_move);
7680
let mut transposition_entry = None;
@@ -109,18 +113,35 @@ impl Algorithm {
109113
return best_evaluation;
110114
};
111115

112-
let evaluation = if transposition_entry.is_some()
113-
&& transposition_entry.unwrap().depth >= depth
114-
{
115-
stats.transposition_table_accesses += 1;
116-
Evaluation::new(
117-
Some(transposition_entry.unwrap().eval),
118-
transposition_entry.unwrap().next_action,
119-
None,
120-
)
121-
} else {
122-
self.node_eval_recursive(new_board, depth - 1, alpha, beta, false, deadline, stats)
123-
};
116+
let extend_by =
117+
if !module_enabled(self.modules, SEARCH_EXTENSIONS) || num_extensions > 3 {
118+
0
119+
} else if num_legal_moves <= 3 || new_board.checkers().popcnt() != 0 {
120+
1
121+
} else {
122+
0
123+
};
124+
125+
let evaluation =
126+
if transposition_entry.is_some() && transposition_entry.unwrap().depth >= depth {
127+
stats.transposition_table_accesses += 1;
128+
Evaluation::new(
129+
Some(transposition_entry.unwrap().eval),
130+
transposition_entry.unwrap().next_action,
131+
None,
132+
)
133+
} else {
134+
self.node_eval_recursive(
135+
new_board,
136+
depth - 1 + extend_by,
137+
alpha,
138+
beta,
139+
false,
140+
deadline,
141+
stats,
142+
num_extensions + extend_by,
143+
)
144+
};
124145
stats.nodes_visited += 1;
125146

126147
// Replace best_eval if ours is better
@@ -191,31 +212,44 @@ impl Algorithm {
191212
best_evaluation
192213
}
193214

194-
fn next_action_internal(
215+
fn next_action(
195216
&mut self,
196217
board: &Board,
197218
depth: u32,
198219
deadline: Option<Instant>,
199220
) -> (Option<chess::Action>, Vec<String>, Stats) {
200221
let mut stats = Stats::default();
201-
let out =
202-
self.node_eval_recursive(board, depth, f32::MIN, f32::MAX, true, deadline, &mut stats);
222+
let out = self.node_eval_recursive(
223+
board,
224+
depth,
225+
f32::MIN,
226+
f32::MAX,
227+
true,
228+
deadline,
229+
&mut stats,
230+
0,
231+
);
203232
let analyzer_data = out.debug_data.unwrap_or_default();
204233
(out.next_action, analyzer_data, stats)
205234
}
206235

207-
pub(crate) fn next_action(
236+
pub(crate) fn next_action_iterative_deepening(
208237
&mut self,
209238
board: &Board,
210239
deadline: Instant,
211240
) -> (chess::Action, Vec<String>, Stats) {
241+
self.board_played_times.insert(
242+
*board,
243+
*self.board_played_times.get(board).unwrap_or(&0) + 1,
244+
);
245+
212246
// Guarantee that at least the first layer gets done.
213247
const START_DEPTH: u32 = 1;
214-
let mut deepest_complete_output = self.next_action_internal(board, START_DEPTH, None);
248+
let mut deepest_complete_output = self.next_action(board, START_DEPTH, None);
215249
let mut deepest_complete_depth = START_DEPTH;
216250

217251
for depth in (deepest_complete_depth + 1)..=10 {
218-
let latest_output = self.next_action_internal(board, depth, Some(deadline));
252+
let latest_output = self.next_action(board, depth, Some(deadline));
219253
if utils::passed_deadline(deadline) {
220254
// The cancelled layer is the one with this data
221255
deepest_complete_output.2.progress_on_next_layer =
@@ -229,22 +263,31 @@ impl Algorithm {
229263
deepest_complete_output.2.depth = deepest_complete_depth;
230264
deepest_complete_output.2.tt_size = self.transposition_table.len() as u32;
231265

232-
(
233-
match deepest_complete_output.0 {
234-
Some(action) => action,
235-
None => match board.status() {
236-
BoardStatus::Ongoing => {
237-
println!("{}", board);
238-
println!("{:#?}", deepest_complete_output.1);
239-
panic!("No action returned by algorithm even though game is still ongoing")
240-
}
241-
BoardStatus::Stalemate => Action::DeclareDraw,
242-
BoardStatus::Checkmate => Action::Resign(board.side_to_move()),
243-
},
266+
let mut action = match deepest_complete_output.0 {
267+
Some(action) => action,
268+
None => match board.status() {
269+
BoardStatus::Ongoing => {
270+
println!("{}", board);
271+
println!("{:#?}", deepest_complete_output.1);
272+
panic!("No action returned by algorithm even though game is still ongoing")
273+
}
274+
BoardStatus::Stalemate => Action::DeclareDraw,
275+
BoardStatus::Checkmate => Action::Resign(board.side_to_move()),
244276
},
245-
deepest_complete_output.1,
246-
deepest_complete_output.2,
247-
)
277+
};
278+
279+
if let Action::MakeMove(chess_move) = action {
280+
let new_board = board.make_move_new(chess_move);
281+
let old_value = *self.board_played_times.get(&new_board).unwrap_or(&0);
282+
if old_value >= 3 {
283+
// Oh no! We should declare draw by three-fold repetition. This is not checked
284+
// unless we do this.
285+
action = Action::DeclareDraw;
286+
}
287+
self.board_played_times.insert(new_board, old_value + 1);
288+
}
289+
290+
(action, deepest_complete_output.1, deepest_complete_output.2)
248291
}
249292

250293
pub(crate) fn eval(&self, board: &Board) -> f32 {
@@ -259,6 +302,12 @@ impl Algorithm {
259302
f32::MAX
260303
};
261304
}
305+
if let Some(&board_played_times) = self.board_played_times.get(board) {
306+
if board_played_times >= 2 {
307+
// This is third time this is played. Draw by three-fold repetition
308+
return 0.;
309+
}
310+
}
262311
let material_each_side = utils::material_each_side(board);
263312

264313
// Negative when black has advantage
@@ -268,5 +317,6 @@ impl Algorithm {
268317

269318
pub(crate) fn reset(&mut self) {
270319
self.transposition_table = HashMap::new();
320+
self.board_played_times = HashMap::new();
271321
}
272322
}

src/common/constants.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
pub(crate) mod modules {
22
pub(crate) const ANALYZE: u32 = 0b1;
3-
/// What bit the AB feature is in the modules integer
43
pub(crate) const ALPHA_BETA: u32 = 0b10;
5-
64
pub(crate) const TRANSPOSITION_TABLE: u32 = 0b100;
5+
pub(crate) const SEARCH_EXTENSIONS: u32 = 0b1000;
76
}

src/main.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
use std::time::Duration;
22

33
use crate::algorithms::the_algorithm::Algorithm;
4-
use crate::common::constants::modules::{ALPHA_BETA, ANALYZE, TRANSPOSITION_TABLE};
4+
use crate::common::constants::modules::{
5+
ALPHA_BETA, ANALYZE, SEARCH_EXTENSIONS, TRANSPOSITION_TABLE,
6+
};
57

6-
use self::pitter::logic::Competition;
8+
use self::pitter::logic::{Competition, GameOutcome};
79

810
mod algorithms;
911
mod common;
1012
mod pitter;
1113

1214
fn main() {
13-
let modules1 = TRANSPOSITION_TABLE | ALPHA_BETA | ANALYZE;
15+
let modules1 = SEARCH_EXTENSIONS | ALPHA_BETA | ANALYZE;
1416
let modules2 = ALPHA_BETA | ANALYZE;
1517
let time_per_move1 = Duration::from_micros(2000);
1618
let time_per_move2 = Duration::from_micros(2000);
@@ -20,7 +22,9 @@ fn main() {
2022
Algorithm::new(modules2, time_per_move2),
2123
);
2224

23-
// let results = competition.analyze_algorithm_choices(|_, _| true);
24-
let results = competition.start_competition(200);
25+
// competition.analyze_algorithm_choices(|(game_info, _), _| {
26+
// game_info.outcome == GameOutcome::InconclusiveTooLong
27+
// });
28+
let results = competition.start_competition(500);
2529
dbg!(results);
2630
}

src/pitter/logic.rs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ impl GamePairOutcome {
4343
(Draw, BlackWin) => Algo1HalfWin,
4444
(Draw, Draw) => GamePairOutcome::Draw,
4545
// Consider changing this in the future, this is an arbitrary choice
46-
(Inconclusive, _) => GamePairOutcome::InconclusiveTooLong,
47-
(_, Inconclusive) => GamePairOutcome::InconclusiveTooLong,
46+
(InconclusiveTooLong, _) => GamePairOutcome::InconclusiveTooLong,
47+
(_, InconclusiveTooLong) => GamePairOutcome::InconclusiveTooLong,
4848
}
4949
}
5050
}
@@ -55,7 +55,7 @@ pub(crate) enum GameOutcome {
5555
BlackWin,
5656
Draw,
5757
#[default]
58-
Inconclusive,
58+
InconclusiveTooLong,
5959
}
6060

6161
#[allow(unused_assignments)]
@@ -121,9 +121,9 @@ impl Competition {
121121
) -> GameInfo {
122122
let mut game_info = GameInfo::default();
123123
let mut algo1 = &mut self.algo1;
124-
// algo1.reset();
124+
algo1.reset();
125125
let mut algo2 = &mut self.algo2;
126-
// algo2.reset();
126+
algo2.reset();
127127
if reversed {
128128
mem::swap(&mut algo1, &mut algo2);
129129
};
@@ -136,14 +136,14 @@ impl Competition {
136136
let mut next_action = match side_to_move {
137137
Color::White => {
138138
analyze = algo1.modules & ANALYZE != 0;
139-
algo1.next_action(
139+
algo1.next_action_iterative_deepening(
140140
&game.current_position(),
141141
Instant::now() + algo1.time_per_move,
142142
)
143143
}
144144
Color::Black => {
145145
analyze = algo1.modules & ANALYZE != 0;
146-
algo2.next_action(
146+
algo2.next_action_iterative_deepening(
147147
&game.current_position(),
148148
Instant::now() + algo2.time_per_move,
149149
)
@@ -167,14 +167,29 @@ impl Competition {
167167
game_info.stats.0 += next_action.2;
168168
}
169169

170-
match next_action.0 {
170+
let mut declared_draw = false;
171+
let success = match next_action.0 {
171172
Action::MakeMove(chess_move) => game.make_move(chess_move),
172173
Action::OfferDraw(color) => game.offer_draw(color),
173174
Action::AcceptDraw => game.accept_draw(),
174-
Action::DeclareDraw => game.declare_draw(),
175+
Action::DeclareDraw => {
176+
// The chess crate one is really bad and wrong
177+
declared_draw = true;
178+
true
179+
}
175180
Action::Resign(color) => game.resign(color),
176181
};
177182

183+
if !success {
184+
dbg!(utils::to_pgn(&game));
185+
panic!("Algorithm made illegal action");
186+
}
187+
188+
if declared_draw {
189+
game_info.outcome = GameOutcome::Draw;
190+
break;
191+
}
192+
178193
if let Some(result) = game.result() {
179194
game_info.outcome = match result {
180195
GameResult::WhiteCheckmates => GameOutcome::WhiteWin,
@@ -188,7 +203,7 @@ impl Competition {
188203
break;
189204
}
190205
if num_plies >= max_plies {
191-
game_info.outcome = GameOutcome::Inconclusive;
206+
game_info.outcome = GameOutcome::InconclusiveTooLong;
192207
break;
193208
}
194209
num_plies += 1
@@ -222,8 +237,8 @@ impl Competition {
222237
game_pair_info.1.outcome,
223238
);
224239

225-
// println!("Game pair played. Outcome: {:?}", combined_outcome);
226-
// println!("{}", utils::to_pgn(&game_pair_info.0.game.unwrap()));
240+
println!("Game pair played. Outcome: {:?}", combined_outcome);
241+
println!("{}", utils::to_pgn(&game_pair_info.0.game.unwrap()));
227242

228243
results.register_game_outcome(combined_outcome);
229244

@@ -275,6 +290,8 @@ impl Competition {
275290
if i > 500 {
276291
return None;
277292
}
293+
self.algo1.reset();
294+
self.algo2.reset();
278295
}
279296
}
280297

@@ -290,19 +307,23 @@ impl Competition {
290307
println!("{}", utils::to_pgn(game.0.game.as_ref().unwrap()));
291308
let mut board = Board::default();
292309

310+
self.algo1.reset();
311+
self.algo2.reset();
293312
for chess_move in game.0.game.as_ref().unwrap().actions() {
294313
let Action::MakeMove(chess_move) = chess_move else {continue};
295314

296-
// PROBLEM!! This will be unreliable if the algorithms have persistent data over moves
297-
// in the future, we will have to make new instances of the algos somehow then
298315
let start = Instant::now();
299316
let mut algo_out = if i % 2 == 1 {
300317
// White's turn
301-
self.algo1
302-
.next_action(&board, Instant::now() + Duration::from_micros(2000))
318+
self.algo1.next_action_iterative_deepening(
319+
&board,
320+
Instant::now() + Duration::from_micros(2000),
321+
)
303322
} else {
304-
self.algo2
305-
.next_action(&board, Instant::now() + Duration::from_micros(2000))
323+
self.algo2.next_action_iterative_deepening(
324+
&board,
325+
Instant::now() + Duration::from_micros(2000),
326+
)
306327
};
307328
let end = Instant::now();
308329
algo_out.2.time_spent = end - start;
@@ -320,5 +341,8 @@ impl Competition {
320341
board = board.make_move_new(*chess_move);
321342
i += 1;
322343
}
344+
345+
dbg!(&self.algo1.board_played_times.values());
346+
dbg!(&self.algo2.board_played_times.values());
323347
}
324348
}

0 commit comments

Comments
 (0)