forked from tseip/fourinarow
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbfs_node.h
350 lines (314 loc) · 11.4 KB
/
bfs_node.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
#ifndef BFS_NODE_H_INCLUDED
#define BFS_NODE_H_INCLUDED
#include <limits>
#include <memory>
#include "game_tree_node.h"
#include "player.h"
/**
* Represents a single node in the game tree.
*
* @tparam Board The board representation used by the game.
*/
template <class Board>
class BFSNode : public Node<Board> {
private:
static std::shared_ptr<BFSNode<Board>> downcast(
std::shared_ptr<Node<Board>> node) {
return std::dynamic_pointer_cast<BFSNode<Board>>(node);
}
static std::shared_ptr<const BFSNode<Board>> downcast(
std::shared_ptr<const Node<Board>> node) {
return std::dynamic_pointer_cast<const BFSNode<Board>>(node);
}
/**
* Heuristic values for a black win and white win, respectively. This is the
* maximum tree depth, which is the maximum number of moves plus one, plus
* one.
* @{
*/
static constexpr int BLACK_WINS = Board::get_max_num_moves() + 1 + 1;
static constexpr int WHITE_WINS = -BLACK_WINS;
/**
* @}
*/
/**
* The child of this node with the best known heuristic value.
*/
std::shared_ptr<BFSNode> best_known_child;
/**
* The heuristic value of this node.
*/
double val;
/**
* A pessimistic estimate of the true expected value of this node.
*/
int pess;
/**
* An optimistic estimate of the true expected value of this node.
*/
int opt;
/**
* Creates a node on the heap that is a child of the current node and returns
* a pointer to it.
*
* @param move The move to be represented by the child.
*
* @return A pointer to the newly created node.
*/
std::shared_ptr<BFSNode> create_child(const typename Board::MoveT &move) {
// Validate that the parent doesn't already have this child.
for (const auto &child : this->children) {
if (downcast(child)->move.board_position == move.board_position) {
throw std::logic_error(
"Given move already exists as a child of this node!");
}
}
return std::shared_ptr<BFSNode>(
new BFSNode(downcast(this->shared_from_this()), move));
}
/**
* Private node constructor for nodes without meaningful move history (i.e.,
* with no parents).
*
* @param board The board state prior to the move being represented by this
* node is made.
* @param val The heuristic value of the move this node represents.
*/
BFSNode(const Board &board, double val)
: Node<Board>(board), val(val), pess(0), opt(0) {
setup_pess_opt();
}
/**
* Private node constructor for nodes with meaningful move history (i.e., with
* parents).
*
* @note Checking for nullity of parent is presumed to have been done outside
* of this function.
*
* @param board The board state prior to the move being represented by this
* node is made.
* @param move The move represented by this node.
* @param val The heuristic value of the move this node represents.
* @param parent The parent of this node, if any.
*/
BFSNode(const std::shared_ptr<BFSNode> parent,
const typename Board::MoveT &move)
: Node<Board>(parent, move),
val(parent->board.active_player() == Player::Player1
? parent->val + move.val
: parent->val - move.val),
pess(0),
opt(0) {
setup_pess_opt();
}
/**
* Establishes initial values for pess and opt based on the board state.
*/
void setup_pess_opt() {
if (this->board.player_has_won(Player::Player1))
pess = opt = BLACK_WINS - this->depth,
val = std::numeric_limits<double>::infinity();
else if (this->board.player_has_won(Player::Player2))
pess = opt = WHITE_WINS + this->depth,
val = -std::numeric_limits<double>::infinity();
else if (this->board.game_is_drawn())
pess = opt = 0, val = 0.0;
else
pess = WHITE_WINS + this->depth, opt = BLACK_WINS - this->depth;
}
/**
* Updates the opt field of this node by scanning over all of its children.
* Assumes all of this node's children are themselves updated.
*/
void update_opt() {
opt = (this->board.active_player() == Player::Player1
? WHITE_WINS + this->depth
: BLACK_WINS - this->depth);
for (const auto &child : this->children) {
this->update_field_against_child(downcast(child)->opt, opt);
}
}
/**
* Updates the pess field of this node by scanning over all of its children.
* Assumes all of this node's children are themselves updated.
*/
void update_pess() {
pess = (this->board.active_player() == Player::Player1
? WHITE_WINS + this->depth
: BLACK_WINS - this->depth);
for (const auto &child : this->children) {
this->update_field_against_child(downcast(child)->pess, pess);
}
}
/**
* Updates the val field of this node by scanning over all of its children.
* Assumes all of this node's children are themselves updated.
*/
void update_val() {
val = (this->board.active_player() == Player::Player1
? -std::numeric_limits<double>::infinity()
: std::numeric_limits<double>::infinity());
for (const auto &child : this->children) {
if (!downcast(child)->determined() &&
this->update_field_against_child(downcast(child)->val, val)) {
best_known_child = downcast(child);
}
}
for (const auto &child : this->children) {
if (downcast(child)->determined()) {
this->update_field_against_child(downcast(child)->val, val);
}
}
}
/**
* Takes a (presumably) updated child of this node, updates the current node
* with any new game state information in the child, and then reports that
* information recursively to the parent of this node up to the root of the
* game tree.
*
* @param child The child node that has been updated and whose new values
* should be propagated to the root of the tree.
*/
void backpropagate(const std::shared_ptr<BFSNode> &child) {
if (!this->update_field_against_child(child->opt, opt)) update_opt();
if (!this->update_field_against_child(child->pess, pess)) update_pess();
if (!child->determined() &&
this->update_field_against_child(child->val, val)) {
best_known_child = child;
} else {
update_val();
update_best_determined();
}
if (auto locked_parent = this->get_parent())
downcast(locked_parent)
->backpropagate(downcast(this->shared_from_this()));
}
/**
* Scans through the children of this node to find the first child whose
* pessimistic or optimistic estimate matches our own, and chooses that to be
* the best known child. This is only valid if we ourselves are determined
* (i.e., if our optimistic/pessimistic estimates for the heuristic value of
* this node have converged).
*/
void update_best_determined() {
// Nothing to do if we aren't ourselves determined.
if (!determined()) {
return;
}
if (this->board.active_player() == Player::Player1) {
for (const auto &child : this->children) {
if (downcast(child)->pess == pess) {
best_known_child = downcast(child);
break;
}
}
} else {
for (const auto &child : this->children) {
if (downcast(child)->opt == opt) {
best_known_child = downcast(child);
break;
}
}
}
}
public:
/**
* Creates a node with no move history on the heap and returns a pointer to
* it.
*
* @param board The board state prior to the move being represented by this
* node is made.
* @param val The heuristic value of the move this node represents.
*
* @return A pointer to the newly created node.
*/
static std::shared_ptr<BFSNode> create(const Board &board, double val) {
return std::shared_ptr<BFSNode>(new BFSNode(board, val));
}
virtual double get_value() const override { return val; }
/**
* @return True if the heuristic value of this node has converged, i.e. if the
* pessimistic and optimistic bounds on the value of the node are equal.
*/
virtual bool determined() const override { return pess == opt; }
/**
* @return A string representing the state of this node.
*/
std::string to_string() const override {
std::stringstream stream;
stream << Node<Board>::to_string() << ", Heuristic value: " << val
<< ", Opt: " << opt << ", Pess: " << pess;
return stream.str();
}
/**
* Accepts a list of moves that can be played from this position represented
* by this node and adds them to the game tree.
*
* @param moves A list of moves that can be played from this position.
*/
void expand(const std::vector<typename Board::MoveT> &moves) override {
if (moves.empty()) return;
for (const typename Board::MoveT &move : moves) {
this->children.push_back(create_child(move));
}
update_opt();
update_pess();
update_val();
if (determined()) update_best_determined();
if (auto locked_parent = this->get_parent())
downcast(locked_parent)
->backpropagate(downcast(this->shared_from_this()));
}
/**
* @return The number of moves between us and our recursively best known
* child.
*/
std::size_t get_depth_of_pv() const override {
const auto selected_node = this->select();
if (selected_node == this->shared_from_this()) return 0;
return downcast(selected_node)->depth - this->depth - 1;
}
/**
* @return The best known move from the current position for the current
* player.
*/
typename Board::MoveT get_best_move() const override {
if (!best_known_child)
throw std::logic_error(
"No best known child has been determined for this board:\n" +
this->board.to_string());
if (determined()) {
return typename Board::MoveT(best_known_child->move.board_position, val,
this->board.active_player());
}
double val_temp = (this->board.active_player() == Player::Player1
? -std::numeric_limits<double>::infinity()
: std::numeric_limits<double>::infinity());
if (this->children.empty()) return this->move;
std::size_t best_position =
downcast(this->children[0])->move.board_position;
for (const auto &child : this->children) {
if (this->update_field_against_child(downcast(child)->val, val_temp)) {
best_position = downcast(child)->move.board_position;
}
}
return typename Board::MoveT(best_position, val_temp,
this->board.active_player());
}
protected:
/**
* Select the best move from among our children. If we have no good child,
* return ourselves. Note that the returned move may be multiple moves in the
* future, i.e. we may return a move that represents an entire line of play.
*
* @return The best move from amongst our children and ourselves.
*/
virtual std::shared_ptr<const Node<Board>> virtual_select() const override {
if (best_known_child) {
return best_known_child->select();
} else {
return downcast(this->shared_from_this());
}
}
};
#endif // BFS_NODE_H_INCLUDED