1use crate::board::{Board, Direction};
2use crate::evaluators::{BoardEvaluator, MonotonicityEvaluator, PrecomputedBoardEvaluator};
3use fnv::FnvHashMap;
4use std::cmp::max;
5
6pub struct Solver {
7 board_evaluator: Box<dyn BoardEvaluator>,
8 proba_2: f32,
9 proba_4: f32,
10 base_max_search_depth: usize,
11 min_branch_proba: f32,
12 transposition_table: FnvHashMap<Board, (f32, f32)>,
13}
14
15pub struct SolverBuilder {
16 board_evaluator: Box<dyn BoardEvaluator>,
17 proba_4: f32,
18 base_max_search_depth: usize,
19 min_branch_proba: f32,
20}
21
22impl Default for SolverBuilder {
23 fn default() -> Self {
24 Self {
25 board_evaluator: Box::new(PrecomputedBoardEvaluator::new(
26 MonotonicityEvaluator::default(),
27 )),
28 proba_4: 0.1,
29 base_max_search_depth: 3,
30 min_branch_proba: 0.1 * 0.1,
31 }
32 }
33}
34
35impl SolverBuilder {
36 pub fn board_evaluator<T>(mut self, evaluator: T) -> Self
38 where
39 T: BoardEvaluator + 'static,
40 {
41 self.board_evaluator = Box::new(evaluator);
42 self
43 }
44
45 pub fn proba_4(mut self, proba_4: f32) -> Self {
47 self.proba_4 = proba_4;
48 self
49 }
50
51 pub fn base_max_search_depth(mut self, max_search_depth: usize) -> Self {
56 self.base_max_search_depth = max_search_depth;
57 self
58 }
59
60 pub fn min_branch_proba(mut self, proba: f32) -> Self {
62 self.min_branch_proba = proba;
63 self
64 }
65
66 pub fn build(self) -> Solver {
67 Solver {
68 board_evaluator: self.board_evaluator,
69 proba_2: 1. - self.proba_4,
70 proba_4: self.proba_4,
71 base_max_search_depth: self.base_max_search_depth,
72 min_branch_proba: self.min_branch_proba,
73 transposition_table: Default::default(),
74 }
75 }
76}
77
78impl Solver {
79 pub fn next_best_move(&mut self, board: Board) -> Option<Direction> {
80 let max_depth = self.compute_max_depth(board);
81 self.transposition_table = FnvHashMap::default();
82 self.eval_max(board, max_depth as usize, 1.0)
83 .map(|(d, _)| d)
84 }
85
86 fn compute_max_depth(&self, board: Board) -> usize {
87 let adjustment_factor = match board.max_value() {
88 2048 => 4,
89 4096 => 2,
90 8192 => 2,
91 16384 => 1,
92 32768 => 0,
93 _ => 7,
94 };
95 max(
96 self.base_max_search_depth as isize,
97 board.count_distinct_tiles() as isize - adjustment_factor,
98 ) as usize
99 }
100
101 fn eval_max(
102 &mut self,
103 board: Board,
104 remaining_depth: usize,
105 branch_proba: f32,
106 ) -> Option<(Direction, f32)> {
107 Direction::all()
108 .iter()
109 .filter_map(|d| {
110 let new_board = board.move_to(*d);
111 if board == new_board {
112 return None;
113 }
114 Some((
115 *d,
116 self.eval_average(new_board, remaining_depth, branch_proba),
117 ))
118 })
119 .max_by(|(_, lhs), (_, rhs)| lhs.partial_cmp(rhs).unwrap())
120 }
121
122 fn eval_average(&mut self, board: Board, remaining_depth: usize, branch_proba: f32) -> f32 {
123 if remaining_depth == 0 || branch_proba < self.min_branch_proba {
124 return self.board_evaluator.evaluate(board);
125 }
126
127 if let Some((cached_value, cached_proba)) = self.transposition_table.get(&board) {
128 if *cached_proba >= branch_proba {
129 return *cached_value;
130 }
131 }
132
133 let empty_tiles_indices = board.empty_tiles_indices();
134 let nb_empty_tiles = board.count_empty_tiles() as f32;
135 let proba_2 = self.proba_2;
136 let proba_4 = self.proba_4;
137 let scores_sum: f32 = empty_tiles_indices
138 .map(|idx| {
139 let board_with_2 = board.set_value_by_exponent(idx, 1);
140 let board_with_4 = board.set_value_by_exponent(idx, 2);
141 let max_score_2 = self
142 .eval_max(
143 board_with_2,
144 remaining_depth - 1,
145 branch_proba * proba_2 / nb_empty_tiles,
146 )
147 .map(|(_, score)| score)
148 .unwrap_or_else(|| self.board_evaluator.gameover_penalty());
149 let max_score_4 = self
150 .eval_max(
151 board_with_4,
152 remaining_depth - 1,
153 branch_proba * proba_4 / nb_empty_tiles,
154 )
155 .map(|(_, score)| score)
156 .unwrap_or_else(|| self.board_evaluator.gameover_penalty());
157 max_score_2 * proba_2 + max_score_4 * proba_4
158 })
159 .sum();
160 let average = scores_sum / nb_empty_tiles as f32;
161 self.transposition_table
162 .insert(board, (average, branch_proba));
163 average
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_next_best_move() {
173 struct DummyEvaluator;
175 impl BoardEvaluator for DummyEvaluator {
176 fn evaluate(&self, board: Board) -> f32 {
177 board.max_value() as f32 / 32768.
178 }
179
180 fn gameover_penalty(&self) -> f32 {
181 0.
182 }
183 }
184
185 let mut solver = SolverBuilder::default()
186 .board_evaluator(DummyEvaluator {})
187 .base_max_search_depth(2)
188 .build();
189
190 #[rustfmt::skip]
191 let board: Board = Board::from(vec![
192 4, 4, 0, 4,
193 16, 0, 0, 2,
194 0, 8, 0, 16,
195 0, 8, 0, 16,
196 ]);
197
198 let direction = solver.next_best_move(board);
200
201 assert_eq!(Some(Direction::Down), direction);
203 }
204}