Skip to content

Commit 0372cd2

Browse files
committed
Complete implementation of transposition table. Currently performing
worse than just AB-pruning for some reason
1 parent 2c198c6 commit 0372cd2

File tree

5 files changed

+147
-72
lines changed

5 files changed

+147
-72
lines changed

src/algorithms/the_algorithm.rs

Lines changed: 106 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
use std::collections::HashMap;
2-
use std::time::Instant;
2+
use std::time::{Duration, Instant};
33

44
use chess::{Action, Board, BoardStatus, ChessMove, Color, MoveGen};
55

66
use crate::common::constants::modules::*;
7-
use crate::common::utils::{self, Stats};
7+
use crate::common::utils::{self, module_enabled, Stats};
88

99
use super::utils::{Evaluation, TranspositionEntry};
1010

1111
pub(crate) struct Algorithm {
1212
pub(crate) modules: u32,
1313
transposition_table: HashMap<Board, TranspositionEntry>,
14+
pub(crate) time_per_move: Duration,
1415
}
1516

1617
impl Algorithm {
17-
pub(crate) fn new(modules: u32) -> Self {
18+
pub(crate) fn new(modules: u32, time_per_move: Duration) -> Self {
1819
Self {
1920
modules,
2021
transposition_table: HashMap::new(),
22+
time_per_move,
2123
}
2224
}
2325

@@ -34,84 +36,100 @@ impl Algorithm {
3436
) -> Evaluation {
3537
if depth == 0 {
3638
stats.leaves_visited += 1;
37-
return Evaluation::new(self.eval(board), None, None);
39+
let eval = self.eval(board);
40+
// if module_enabled(self.modules, TRANSPOSITION_TABLE) {
41+
// let start = Instant::now();
42+
// self.transposition_table
43+
// .insert(*board, TranspositionEntry::new(depth, eval, None));
44+
// stats.time_for_transposition_access += Instant::now() - start;
45+
// stats.transposition_table_entries += 1
46+
// }
47+
return Evaluation::new(Some(eval), None, None);
3848
}
3949

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

4554
let legal_moves = MoveGen::new_legal(board);
4655
let num_legal_moves = legal_moves.len();
4756
if num_legal_moves == 0 {
4857
if board.checkers().popcnt() == 0 {
4958
// Is Stalemate, no checking pieces
50-
best_evaluation.eval = 0.;
59+
best_evaluation.eval = Some(0.);
5160
}
61+
5262
// 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");
63+
// has been checkmated.
64+
65+
best_evaluation.eval = Some(if board.side_to_move() == Color::White {
66+
f32::MIN
67+
} else {
68+
f32::MAX
69+
});
5770
return best_evaluation;
5871
}
5972

6073
let mut boards = legal_moves
6174
.map(|chess_move| {
6275
let board = board.make_move_new(chess_move);
6376
let mut transposition_entry = None;
64-
if self.modules & TRANSPOSITION_TABLE != 0 {
65-
transposition_entry = self.transposition_table.get(&board);
77+
if module_enabled(self.modules, TRANSPOSITION_TABLE) {
78+
let start = Instant::now();
79+
80+
transposition_entry = self.transposition_table.get(&board).copied();
81+
82+
let time_for_transposition_access = Instant::now() - start;
83+
stats.time_for_transposition_access += time_for_transposition_access;
6684
}
6785
(chess_move, board, transposition_entry)
6886
})
69-
.collect::<Vec<(ChessMove, Board, Option<&TranspositionEntry>)>>();
87+
.collect::<Vec<(ChessMove, Board, Option<TranspositionEntry>)>>();
7088

7189
// 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-
});
90+
// boards.sort_by(|board1, board2| {
91+
// let eval1 = board1.2.map_or(self.eval(&board1.1), |entry| entry.eval);
92+
// let eval2 = board2.2.map_or(self.eval(&board2.1), |entry| entry.eval);
93+
// let ordering = eval1.partial_cmp(&eval2).expect("Eval is a valid value");
94+
95+
// if maximise {
96+
// return ordering.reverse();
97+
// }
98+
// ordering
99+
// });
84100

85101
for (i, (chess_move, new_board, transposition_entry)) in boards.iter().enumerate() {
86-
if deadline.map_or(false, |deadline| {
87-
!Instant::now().saturating_duration_since(deadline).is_zero()
88-
}) {
102+
if deadline.is_some_and(utils::passed_deadline) {
89103
// The previous value of progress_on_next_layer comes from deeper layers returning.
90104
// We want these contributions to be proportional to the contribution from a single
91105
// node on our layer
92106
stats.progress_on_next_layer *= 1. / num_legal_moves as f32;
93-
stats.progress_on_next_layer += i as f32 / num_legal_moves as f32;
107+
stats.progress_on_next_layer +=
108+
(i.saturating_sub(1)) as f32 / num_legal_moves as f32;
94109
return best_evaluation;
95110
};
96111

97-
let evaluation = if transposition_entry.is_none()
98-
|| transposition_entry.unwrap().depth < depth
112+
let evaluation = if transposition_entry.is_some()
113+
&& transposition_entry.unwrap().depth >= depth
99114
{
100-
self.node_eval_recursive(new_board, depth - 1, alpha, beta, false, deadline, stats)
101-
} else {
115+
stats.transposition_table_accesses += 1;
102116
Evaluation::new(
103-
transposition_entry.unwrap().eval,
117+
Some(transposition_entry.unwrap().eval),
104118
transposition_entry.unwrap().next_action,
105119
None,
106120
)
121+
} else {
122+
self.node_eval_recursive(new_board, depth - 1, alpha, beta, false, deadline, stats)
107123
};
108124
stats.nodes_visited += 1;
109125

110126
// Replace best_eval if ours is better
111-
if maximise && evaluation.eval > best_evaluation.eval
112-
|| !maximise && evaluation.eval < best_evaluation.eval
127+
if evaluation.eval.is_some()
128+
&& (best_evaluation.eval.is_none()
129+
|| maximise && evaluation.eval.unwrap() > best_evaluation.eval.unwrap()
130+
|| !maximise && evaluation.eval.unwrap() < best_evaluation.eval.unwrap())
113131
{
114-
if original && self.modules & ANALYZE != 0 {
132+
if original && module_enabled(self.modules, ANALYZE) {
115133
let mut vec = Vec::new();
116134
let new_best_move = chess_move.to_string();
117135
let new_best_eval = evaluation.eval;
@@ -135,11 +153,13 @@ impl Algorithm {
135153
best_evaluation.next_action = Some(Action::MakeMove(*chess_move));
136154
}
137155

138-
if self.modules & ALPHA_BETA != 0 {
139-
if maximise {
140-
alpha = alpha.max(evaluation.eval);
141-
} else {
142-
beta = beta.min(evaluation.eval);
156+
if module_enabled(self.modules, ALPHA_BETA) {
157+
if let Some(eval) = evaluation.eval {
158+
if maximise {
159+
alpha = alpha.max(eval);
160+
} else {
161+
beta = beta.min(eval);
162+
}
143163
}
144164

145165
if alpha > beta {
@@ -149,10 +169,25 @@ impl Algorithm {
149169
}
150170
}
151171

152-
self.transposition_table.insert(
153-
*board,
154-
TranspositionEntry::new(depth, best_evaluation.eval, best_evaluation.next_action),
155-
);
172+
if module_enabled(self.modules, TRANSPOSITION_TABLE) && depth >= 3 {
173+
if let Some(best_eval) = best_evaluation.eval {
174+
let start = Instant::now();
175+
self.transposition_table.insert(
176+
*board,
177+
TranspositionEntry::new(depth, best_eval, best_evaluation.next_action),
178+
);
179+
stats.time_for_transposition_access += Instant::now() - start;
180+
}
181+
stats.transposition_table_entries += 1
182+
}
183+
184+
if best_evaluation.debug_data.is_some() {
185+
let mut debug_data = best_evaluation.debug_data.take().unwrap();
186+
if let Some(Action::MakeMove(next_move)) = best_evaluation.next_action {
187+
utils::vector_push_debug!(debug_data, best_evaluation.eval, next_move.to_string(),);
188+
best_evaluation.debug_data = Some(debug_data);
189+
}
190+
}
156191
best_evaluation
157192
}
158193

@@ -161,23 +196,12 @@ impl Algorithm {
161196
board: &Board,
162197
depth: u32,
163198
deadline: Option<Instant>,
164-
) -> (chess::Action, Vec<String>, Stats) {
199+
) -> (Option<chess::Action>, Vec<String>, Stats) {
165200
let mut stats = Stats::default();
166201
let out =
167202
self.node_eval_recursive(board, depth, f32::MIN, f32::MAX, true, deadline, &mut stats);
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-
};
179203
let analyzer_data = out.debug_data.unwrap_or_default();
180-
(action, analyzer_data, stats)
204+
(out.next_action, analyzer_data, stats)
181205
}
182206

183207
pub(crate) fn next_action(
@@ -203,7 +227,24 @@ impl Algorithm {
203227
}
204228
}
205229
deepest_complete_output.2.depth = deepest_complete_depth;
206-
deepest_complete_output
230+
deepest_complete_output.2.tt_size = self.transposition_table.len() as u32;
231+
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+
},
244+
},
245+
deepest_complete_output.1,
246+
deepest_complete_output.2,
247+
)
207248
}
208249

209250
pub(crate) fn eval(&self, board: &Board) -> f32 {
@@ -224,4 +265,8 @@ impl Algorithm {
224265
let diff = material_each_side.0 as i32 - material_each_side.1 as i32;
225266
diff as f32
226267
}
268+
269+
pub(crate) fn reset(&mut self) {
270+
self.transposition_table = HashMap::new();
271+
}
227272
}

src/algorithms/utils.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@ impl TranspositionEntry {
1717
}
1818
}
1919

20+
#[derive(Debug)]
2021
pub(super) struct Evaluation {
2122
pub(super) debug_data: Option<Vec<String>>,
22-
pub(super) eval: f32,
23+
pub(super) eval: Option<f32>,
2324
pub(super) next_action: Option<Action>,
2425
}
2526

2627
impl Evaluation {
2728
pub(super) fn new(
28-
eval: f32,
29+
eval: Option<f32>,
2930
next_action: Option<Action>,
3031
debug_data: Option<Vec<String>>,
3132
) -> Evaluation {

src/common/utils.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ pub(crate) struct Stats {
8585
pub(crate) num_plies: u32,
8686
pub(crate) time_spent: Duration,
8787
pub(crate) progress_on_next_layer: f32,
88+
pub(crate) transposition_table_entries: u32,
89+
pub(crate) transposition_table_accesses: u32,
90+
pub(crate) time_for_transposition_access: Duration,
91+
pub(crate) tt_size: u32,
8892
}
8993

9094
impl AddAssign for Stats {
@@ -96,6 +100,10 @@ impl AddAssign for Stats {
96100
self.num_plies += rhs.num_plies;
97101
self.time_spent += rhs.time_spent;
98102
self.progress_on_next_layer += rhs.progress_on_next_layer;
103+
self.transposition_table_entries += rhs.transposition_table_entries;
104+
self.transposition_table_accesses += rhs.transposition_table_accesses;
105+
self.time_for_transposition_access += rhs.time_for_transposition_access;
106+
self.tt_size += rhs.tt_size;
99107
}
100108
}
101109

@@ -109,6 +117,10 @@ impl Div<u32> for Stats {
109117
num_plies: self.num_plies as f32 / rhs as f32,
110118
time_spent: self.time_spent / rhs,
111119
progress_on_next_layer: self.progress_on_next_layer / rhs as f32,
120+
transposition_table_entries: self.transposition_table_entries as f32 / rhs as f32,
121+
transposition_table_accesses: self.transposition_table_accesses as f32 / rhs as f32,
122+
time_for_transposition_access: self.time_for_transposition_access / rhs,
123+
tt_size: self.tt_size as f32 / rhs as f32,
112124
}
113125
}
114126

@@ -125,9 +137,17 @@ pub(crate) struct StatsAverage {
125137
pub(crate) num_plies: f32,
126138
pub(crate) time_spent: Duration,
127139
pub(crate) progress_on_next_layer: f32,
140+
pub(crate) transposition_table_entries: f32,
141+
pub(crate) transposition_table_accesses: f32,
142+
pub(crate) time_for_transposition_access: Duration,
143+
pub(crate) tt_size: f32,
128144
}
129145

130146
pub(crate) fn passed_deadline(deadline: Instant) -> bool {
131147
let time_since_deadline = Instant::now().saturating_duration_since(deadline);
132-
time_since_deadline.is_zero()
148+
!time_since_deadline.is_zero()
149+
}
150+
151+
pub(crate) fn module_enabled(modules: u32, module_to_test: u32) -> bool {
152+
modules & module_to_test != 0
133153
}

src/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::time::Duration;
2+
13
use crate::algorithms::the_algorithm::Algorithm;
24
use crate::common::constants::modules::{ALPHA_BETA, ANALYZE, TRANSPOSITION_TABLE};
35

@@ -10,10 +12,15 @@ mod pitter;
1012
fn main() {
1113
let modules1 = TRANSPOSITION_TABLE | ALPHA_BETA | ANALYZE;
1214
let modules2 = ALPHA_BETA | ANALYZE;
15+
let time_per_move1 = Duration::from_micros(2000);
16+
let time_per_move2 = Duration::from_micros(2000);
1317

14-
let mut competition = Competition::new(Algorithm::new(modules1), Algorithm::new(modules2));
18+
let mut competition = Competition::new(
19+
Algorithm::new(modules1, time_per_move1),
20+
Algorithm::new(modules2, time_per_move2),
21+
);
1522

1623
// let results = competition.analyze_algorithm_choices(|_, _| true);
17-
let results = competition.start_competition();
24+
let results = competition.start_competition(200);
1825
dbg!(results);
1926
}

0 commit comments

Comments
 (0)