Skip to content

Commit 2c198c6

Browse files
committed
Implement transposition table. Currently battling borrow checker
1 parent 8f3b9c1 commit 2c198c6

File tree

9 files changed

+162
-72
lines changed

9 files changed

+162
-72
lines changed

src/algorithms/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub(crate) mod the_algorithm;
2+
pub(crate) mod utils;

src/algorithms/the_algorithm.rs

Lines changed: 99 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,88 @@
1+
use std::collections::HashMap;
12
use std::time::Instant;
23

3-
use chess::{Action, Board, BoardStatus, Color, MoveGen};
4+
use chess::{Action, Board, BoardStatus, ChessMove, Color, MoveGen};
45

56
use crate::common::constants::modules::*;
67
use crate::common::utils::{self, Stats};
78

9+
use super::utils::{Evaluation, TranspositionEntry};
10+
811
pub(crate) struct Algorithm {
912
pub(crate) modules: u32,
13+
transposition_table: HashMap<Board, TranspositionEntry>,
1014
}
1115

1216
impl Algorithm {
1317
pub(crate) fn new(modules: u32) -> Self {
14-
Self { modules }
18+
Self {
19+
modules,
20+
transposition_table: HashMap::new(),
21+
}
1522
}
1623

24+
#[allow(clippy::too_many_arguments)]
1725
fn node_eval_recursive(
18-
&self,
26+
&mut self,
1927
board: &Board,
2028
depth: u32,
2129
mut alpha: f32,
2230
mut beta: f32,
2331
original: bool,
2432
deadline: Option<Instant>,
2533
stats: &mut Stats,
26-
) -> (Option<Action>, f32, Option<Vec<String>>) {
34+
) -> Evaluation {
2735
if depth == 0 {
2836
stats.leaves_visited += 1;
29-
return (None, self.eval(board), None);
37+
return Evaluation::new(self.eval(board), None, None);
3038
}
3139

3240
// Whether we should try to maximise the eval
3341
let maximise: bool = board.side_to_move() == Color::White;
34-
let mut best_eval = (None, if maximise { f32::MIN } else { f32::MAX }, None);
42+
let mut best_evaluation =
43+
Evaluation::new(if maximise { f32::MIN } else { f32::MAX }, None, None);
3544

3645
let legal_moves = MoveGen::new_legal(board);
3746
let num_legal_moves = legal_moves.len();
38-
if num_legal_moves == 0 && board.checkers().popcnt() == 0 {
39-
// Is Stalemate, no checking pieces
40-
best_eval = (None, 0., None)
47+
if num_legal_moves == 0 {
48+
if board.checkers().popcnt() == 0 {
49+
// Is Stalemate, no checking pieces
50+
best_evaluation.eval = 0.;
51+
}
52+
// If we arrive at here and it is checkmate, then we know that the side playing
53+
// has been checkmated, and therefore the current `best_eval` is correct. Because if we tried to
54+
// maximise, we failed, and if trying to minimise, we failed and therefore get the
55+
// lowest/highest eval
56+
println!("The thing happened");
57+
return best_evaluation;
4158
}
42-
// If we arrive at here and it is checkmate, then we know that the side playing
43-
// has been checkmated, and therefore the current `best_eval` is correct. Because if we tried to
44-
// maximise, we failed, and if trying to minimise, we failed and therefore get the
45-
// lowest/highest eval
4659

47-
for (i, chess_move) in legal_moves.enumerate() {
60+
let mut boards = legal_moves
61+
.map(|chess_move| {
62+
let board = board.make_move_new(chess_move);
63+
let mut transposition_entry = None;
64+
if self.modules & TRANSPOSITION_TABLE != 0 {
65+
transposition_entry = self.transposition_table.get(&board);
66+
}
67+
(chess_move, board, transposition_entry)
68+
})
69+
.collect::<Vec<(ChessMove, Board, Option<&TranspositionEntry>)>>();
70+
71+
// Sort by eval
72+
boards.sort_by(|board1, board2| {
73+
let ordering = board1
74+
.2
75+
.map_or(0., |entry| entry.eval)
76+
.partial_cmp(&board2.2.map_or(0., |entry| entry.eval))
77+
.expect("Eval is a valid value");
78+
79+
if !maximise {
80+
return ordering.reverse();
81+
}
82+
ordering
83+
});
84+
85+
for (i, (chess_move, new_board, transposition_entry)) in boards.iter().enumerate() {
4886
if deadline.map_or(false, |deadline| {
4987
!Instant::now().saturating_duration_since(deadline).is_zero()
5088
}) {
@@ -53,57 +91,55 @@ impl Algorithm {
5391
// node on our layer
5492
stats.progress_on_next_layer *= 1. / num_legal_moves as f32;
5593
stats.progress_on_next_layer += i as f32 / num_legal_moves as f32;
56-
return best_eval;
94+
return best_evaluation;
5795
};
5896

59-
let new_position = board.make_move_new(chess_move);
60-
let eval = self.node_eval_recursive(
61-
&new_position,
62-
depth - 1,
63-
alpha,
64-
beta,
65-
false,
66-
deadline,
67-
stats,
68-
);
97+
let evaluation = if transposition_entry.is_none()
98+
|| transposition_entry.unwrap().depth < depth
99+
{
100+
self.node_eval_recursive(new_board, depth - 1, alpha, beta, false, deadline, stats)
101+
} else {
102+
Evaluation::new(
103+
transposition_entry.unwrap().eval,
104+
transposition_entry.unwrap().next_action,
105+
None,
106+
)
107+
};
69108
stats.nodes_visited += 1;
70109

71-
if maximise && eval.1 > best_eval.1 || !maximise && eval.1 < best_eval.1 {
110+
// Replace best_eval if ours is better
111+
if maximise && evaluation.eval > best_evaluation.eval
112+
|| !maximise && evaluation.eval < best_evaluation.eval
113+
{
72114
if original && self.modules & ANALYZE != 0 {
73115
let mut vec = Vec::new();
74116
let new_best_move = chess_move.to_string();
75-
let new_best_eval = eval.1;
117+
let new_best_eval = evaluation.eval;
76118
utils::vector_push_debug!(
77119
vec,
78120
self.modules,
79121
maximise,
80-
best_eval.1,
122+
best_evaluation.eval,
81123
new_best_move,
82124
new_best_eval,
83125
);
84-
if let Some(Action::MakeMove(previous_best_move)) = best_eval.0 {
126+
if let Some(Action::MakeMove(previous_best_move)) = best_evaluation.next_action
127+
{
85128
let previous_best_move = previous_best_move.to_string();
86129
utils::vector_push_debug!(vec, previous_best_move);
87130
}
88-
best_eval.2 = Some(vec);
131+
best_evaluation.debug_data = Some(vec);
89132
}
90133

91-
best_eval.1 = eval.1;
92-
best_eval.0 = Some(Action::MakeMove(chess_move));
134+
best_evaluation.eval = evaluation.eval;
135+
best_evaluation.next_action = Some(Action::MakeMove(*chess_move));
93136
}
94137

95138
if self.modules & ALPHA_BETA != 0 {
96139
if maximise {
97-
// if eval.1 > alpha {
98-
// println!("Alpha changed. {} -> {}. Beta: {}", alpha, eval.1, beta);
99-
// }
100-
alpha = alpha.max(eval.1);
140+
alpha = alpha.max(evaluation.eval);
101141
} else {
102-
// if eval.1 < beta {
103-
// println!("Beta changed. {} -> {}. Alpha: {}", beta, eval.1, alpha);
104-
// }
105-
106-
beta = beta.min(eval.1);
142+
beta = beta.min(evaluation.eval);
107143
}
108144

109145
if alpha > beta {
@@ -113,35 +149,50 @@ impl Algorithm {
113149
}
114150
}
115151

116-
best_eval
152+
self.transposition_table.insert(
153+
*board,
154+
TranspositionEntry::new(depth, best_evaluation.eval, best_evaluation.next_action),
155+
);
156+
best_evaluation
117157
}
118158

119159
fn next_action_internal(
120-
&self,
160+
&mut self,
121161
board: &Board,
122162
depth: u32,
123163
deadline: Option<Instant>,
124164
) -> (chess::Action, Vec<String>, Stats) {
125165
let mut stats = Stats::default();
126166
let out =
127167
self.node_eval_recursive(board, depth, f32::MIN, f32::MAX, true, deadline, &mut stats);
128-
let action = out.0.unwrap_or(Action::Resign(board.side_to_move()));
129-
let analyzer_data = out.2.unwrap_or_default();
168+
let action = if out.next_action.is_none() {
169+
match board.status() {
170+
BoardStatus::Ongoing => {
171+
panic!("No action returned by algorithm even though game is still ongoing")
172+
}
173+
BoardStatus::Stalemate => Action::DeclareDraw,
174+
BoardStatus::Checkmate => Action::Resign(board.side_to_move()),
175+
}
176+
} else {
177+
out.next_action.unwrap()
178+
};
179+
let analyzer_data = out.debug_data.unwrap_or_default();
130180
(action, analyzer_data, stats)
131181
}
132182

133183
pub(crate) fn next_action(
134-
&self,
184+
&mut self,
135185
board: &Board,
136186
deadline: Instant,
137187
) -> (chess::Action, Vec<String>, Stats) {
188+
// Guarantee that at least the first layer gets done.
138189
const START_DEPTH: u32 = 1;
139190
let mut deepest_complete_output = self.next_action_internal(board, START_DEPTH, None);
140191
let mut deepest_complete_depth = START_DEPTH;
192+
141193
for depth in (deepest_complete_depth + 1)..=10 {
142194
let latest_output = self.next_action_internal(board, depth, Some(deadline));
143-
let time_since_deadline = Instant::now().saturating_duration_since(deadline);
144-
if !time_since_deadline.is_zero() {
195+
if utils::passed_deadline(deadline) {
145196
// The cancelled layer is the one with this data
146197
deepest_complete_output.2.progress_on_next_layer =
147198
latest_output.2.progress_on_next_layer;

src/algorithms/utils.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use chess::Action;
2+
3+
#[derive(Debug, Copy, Clone)]
4+
pub(super) struct TranspositionEntry {
5+
pub(super) depth: u32,
6+
pub(super) eval: f32,
7+
pub(super) next_action: Option<Action>,
8+
}
9+
10+
impl TranspositionEntry {
11+
pub(super) fn new(depth: u32, eval: f32, next_action: Option<Action>) -> Self {
12+
TranspositionEntry {
13+
depth,
14+
eval,
15+
next_action,
16+
}
17+
}
18+
}
19+
20+
pub(super) struct Evaluation {
21+
pub(super) debug_data: Option<Vec<String>>,
22+
pub(super) eval: f32,
23+
pub(super) next_action: Option<Action>,
24+
}
25+
26+
impl Evaluation {
27+
pub(super) fn new(
28+
eval: f32,
29+
next_action: Option<Action>,
30+
debug_data: Option<Vec<String>>,
31+
) -> Evaluation {
32+
Evaluation {
33+
eval,
34+
next_action,
35+
debug_data,
36+
}
37+
}
38+
}

src/common/algorithm.rs

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/common/constants.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ pub(crate) mod modules {
22
pub(crate) const ANALYZE: u32 = 0b1;
33
/// What bit the AB feature is in the modules integer
44
pub(crate) const ALPHA_BETA: u32 = 0b10;
5+
6+
pub(crate) const TRANSPOSITION_TABLE: u32 = 0b100;
57
}

src/common/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
pub(crate) mod algorithm;
21
pub(crate) mod constants;
32
pub(crate) mod utils;

src/common/utils.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::ops::{AddAssign, Div};
2-
use std::time::Duration;
2+
use std::time::{Duration, Instant};
33

44
use chess::{Board, ChessMove, Color, Game, MoveGen, Piece};
55
use rand::Rng;
@@ -115,6 +115,8 @@ impl Div<u32> for Stats {
115115
type Output = StatsAverage;
116116
}
117117
#[derive(Default, Debug)]
118+
// These fields are used through Debug
119+
#[allow(dead_code)]
118120
pub(crate) struct StatsAverage {
119121
pub(crate) alpha_beta_breaks: f32,
120122
pub(crate) depth: f32,
@@ -124,3 +126,8 @@ pub(crate) struct StatsAverage {
124126
pub(crate) time_spent: Duration,
125127
pub(crate) progress_on_next_layer: f32,
126128
}
129+
130+
pub(crate) fn passed_deadline(deadline: Instant) -> bool {
131+
let time_since_deadline = Instant::now().saturating_duration_since(deadline);
132+
time_since_deadline.is_zero()
133+
}

src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::algorithms::the_algorithm::Algorithm;
2-
use crate::common::constants::modules::{ALPHA_BETA, ANALYZE};
2+
use crate::common::constants::modules::{ALPHA_BETA, ANALYZE, TRANSPOSITION_TABLE};
33

44
use self::pitter::logic::Competition;
55

@@ -8,8 +8,8 @@ mod common;
88
mod pitter;
99

1010
fn main() {
11-
let modules1 = ALPHA_BETA | ANALYZE;
12-
let modules2 = ANALYZE;
11+
let modules1 = TRANSPOSITION_TABLE | ALPHA_BETA | ANALYZE;
12+
let modules2 = ALPHA_BETA | ANALYZE;
1313

1414
let mut competition = Competition::new(Algorithm::new(modules1), Algorithm::new(modules2));
1515

src/pitter/logic.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,15 @@ impl Competition {
113113
}
114114
}
115115

116-
pub(crate) fn play_game(&self, mut game: Game, reversed: bool, max_plies: usize) -> GameInfo {
116+
pub(crate) fn play_game(
117+
&mut self,
118+
mut game: Game,
119+
reversed: bool,
120+
max_plies: usize,
121+
) -> GameInfo {
117122
let mut game_info = GameInfo::default();
118-
let mut algo1 = &self.algo1;
119-
let mut algo2 = &self.algo2;
123+
let mut algo1 = &mut self.algo1;
124+
let mut algo2 = &mut self.algo2;
120125
if reversed {
121126
mem::swap(&mut algo1, &mut algo2);
122127
};
@@ -191,7 +196,7 @@ impl Competition {
191196
game_info
192197
}
193198

194-
fn play_game_pair(&self, game: Game) -> (GameInfo, GameInfo) {
199+
fn play_game_pair(&mut self, game: Game) -> (GameInfo, GameInfo) {
195200
let outcome1 = self.play_game(game.clone(), false, 150);
196201
let outcome2 = self.play_game(game, true, 150);
197202

@@ -247,7 +252,7 @@ impl Competition {
247252
self.algo1.eval(&board) + self.algo2.eval(&board) / 2.
248253
}
249254

250-
pub(crate) fn find_game<P>(&self, predicate: P) -> Option<(GameInfo, GameInfo)>
255+
pub(crate) fn find_game<P>(&mut self, predicate: P) -> Option<(GameInfo, GameInfo)>
251256
where
252257
P: Fn(&(GameInfo, GameInfo), GamePairOutcome) -> bool,
253258
{
@@ -271,7 +276,7 @@ impl Competition {
271276
}
272277
}
273278

274-
pub(crate) fn analyze_algorithm_choices<P>(&self, predicate: P)
279+
pub(crate) fn analyze_algorithm_choices<P>(&mut self, predicate: P)
275280
where
276281
P: Fn(&(GameInfo, GameInfo), GamePairOutcome) -> bool,
277282
{

0 commit comments

Comments
 (0)