BitBully 0.0.76
Loading...
Searching...
No Matches
BitBully.h
1#ifndef BITBULLY__BITBULLY_H_
2#define BITBULLY__BITBULLY_H_
3
4#include <filesystem>
5#include <iostream>
6#include <vector>
7
8#include "Board.h"
9#include "MoveList.h"
10#include "OpeningBook.h"
11#include "TranspositionTable.h"
12
13namespace BitBully {
14class BitBully {
15 private:
16 unsigned long long int nodeCounter;
17 static bool constexpr USE_TRANSPOSITION_TABLE = true;
18 static auto constexpr DEFAULT_LOG_TRANSPOSITION_SIZE = 22;
19
20 TranspositionTable transpositionTable;
21 std::unique_ptr<OpeningBook> m_openingBook;
22
23 public:
24 // MoveList sortMoves(Board::TBitBoard moves); // implemented in Board.cpp
25
26 explicit BitBully(const std::filesystem::path& bookPath = "")
27 : nodeCounter{0},
28 transpositionTable{
29 USE_TRANSPOSITION_TABLE ? DEFAULT_LOG_TRANSPOSITION_SIZE : 0} {
30 loadBook(bookPath); // will not do anything if path is empty
31 };
32
33 inline bool isBookLoaded() const { return m_openingBook != nullptr; }
34
35 inline void resetBook() { m_openingBook.reset(); }
36
37 inline bool loadBook(const std::filesystem::path& bookPath = "") {
38 if (isBookLoaded()) {
39 return false;
40 }
41 if (!bookPath.empty()) {
42 m_openingBook = std::make_unique<OpeningBook>(bookPath);
43 assert(isBookLoaded());
44 }
45 return isBookLoaded();
46 }
47
54 static int scoreToMovesLeft(const int score, const Board& b) noexcept {
55 if (score == 0) {
56 return b.movesLeft();
57 }
58 const int p = (b.movesLeft() + 1) % 2; // 1 -> yellow, 0 -> red
59 const int sgnScore = score < 0 ? 1 : 0;
60 const int absScore = score < 0 ? -score : score;
61 const int mvFinalLeft = 2 * (absScore - 1) + (sgnScore ^ p);
62 return b.movesLeft() - mvFinalLeft;
63 }
64
65 static int rollout(Board b) noexcept {
66 // Play out the game using non-losing moves until terminal.
67 // Both players use generateNonLosingMoves() to avoid immediate losses.
68 // Among multiple non-losing moves, pick the first via nextMove()
69 // (center-priority heuristic).
70 int ply = 0;
71
72 while (true) {
73 if (b.canWin()) {
74 const int score = (b.movesLeft() + 1) / 2;
75 return (ply % 2 == 0) ? score : -score;
76 }
77
78 if (!b.movesLeft()) {
79 return 0;
80 }
81
82 auto moves = b.generateNonLosingMoves();
83 if (!moves) {
84 const int score = -(b.movesLeft() / 2);
85 return (ply % 2 == 0) ? score : -score;
86 }
87
88 const auto mv = Board::nextMove(moves);
89
90 // TODO: probably faster to do the move directly on the current
91 // board instead of copying it first and then doing the move on the copy.
92 // However, this would require some changes to the Board class (e.g., a
93 // playMoveFast() method which does not check for legality of the move
94 // since we already know that it is legal).
95 b = b.playBitMaskOnCopy(mv);
96 ply++;
97 }
98 }
99
100 // TODO: firstGuess is a parameter which could be tuned!
101 int mtdf(const Board& b, const int firstGuess,
102 const int maxDepth = -1) noexcept {
103 // MTD(f) algorithm by Aske Plaat: Plaat, Aske; Jonathan Schaeffer; Wim
104 // Pijls; Arie de Bruin (November 1996). "Best-first Fixed-depth Minimax
105 // Algorithms". Artificial Intelligence. 87 (1–2): 255–293.
106 // doi:10.1016/0004-3702(95)00126-3
107 auto g = firstGuess;
108 int upperBound = INT32_MAX;
109 int lowerBound = INT32_MIN;
110
111 while (lowerBound < upperBound) {
112 const auto beta = std::max(g, lowerBound + 1);
113 g = negamax(b, beta - 1, beta, 0, maxDepth);
114 if (g < beta) {
115 upperBound = g;
116 } else {
117 lowerBound = g;
118 }
119 }
120 return g;
121 }
122
123 // generally, appears to be slower than mtdf
124 int nullWindow(const Board& b, const int maxDepth = -1) noexcept {
125 int min = -b.movesLeft() / 2;
126 int max = (b.movesLeft() + 1) / 2;
127
128 while (min < max) {
129 int mid = min + (max - min) / 2;
130 if (mid <= 0 && min / 2 < mid)
131 mid = min / 2;
132 else if (mid >= 0 && max / 2 > mid)
133 mid = max / 2;
134 int r = negamax(b, mid, mid + 1, 0, maxDepth);
135 if (r <= mid) {
136 max = r;
137 } else {
138 min = r;
139 }
140 }
141 return min;
142 }
143
144 void resetTranspositionTable() {
145 transpositionTable = TranspositionTable{
146 USE_TRANSPOSITION_TABLE ? DEFAULT_LOG_TRANSPOSITION_SIZE : 0};
147 }
148
149 [[nodiscard]] auto getNodeCounter() const { return nodeCounter; }
150
151 void resetNodeCounter() { nodeCounter = 0ULL; }
152
153 int negamax(Board b, int alpha, int beta, const int depth,
154 const int maxDepth = -1) noexcept {
155 // In several aspects inspired by Pascal's code
156 assert(alpha < beta);
157 nodeCounter++;
158
159 if (maxDepth >= 0 && depth >= maxDepth) {
160 return rollout(b);
161 }
162
163 const int8_t remainingBudget =
164 (maxDepth < 0) ? INT8_MAX : static_cast<int8_t>(maxDepth - depth);
165
166 if (isBookLoaded() && b.countTokens() == m_openingBook->getNPly()) {
167 return m_openingBook->getBoardValue(b);
168 }
169
170 // It appears as if this check is not necessary. Below we check, if we
171 // have any non-losing moves left. If not, we return with a negative
172 // score.
173 // TODO: move this outside negamax:
174 if (!depth && b.canWin()) {
175 return (b.movesLeft() + 1) / 2;
176 }
177
178 if (alpha >= (b.movesLeft() + 1) / 2) {
179 // We cannot get better than this (alpha) anymore (with every additional
180 // move, our potential score gets lower since we have a later win).
181 return alpha;
182 }
183
184 // lower bound of score as opponent cannot win next move:
185 if (const int min = -b.movesLeft() / 2; alpha < min) {
186 alpha = min;
187 if (alpha >= beta) return alpha;
188 }
189 if (const int max = (b.movesLeft() - 1) / 2; beta > max) {
190 beta = max;
191 if (alpha >= beta) return beta;
192 }
193
194 if (!b.movesLeft()) {
195 assert(!b.legalMovesMask());
196 assert(b.popCountBoard() == Board::N_COLUMNS * Board::N_ROWS);
197 return 0;
198 }
199
200 int oldAlpha = alpha;
201
202 auto moves = b.generateNonLosingMoves();
203 if (!moves) {
204 return -b.movesLeft() / 2;
205 }
206
207 assert(uint64_t_popcnt(moves) <= Board::N_COLUMNS);
208 assert(uint64_t_popcnt(moves) > 0);
209
210 if (depth < 20 && b.doubleThreat(moves)) {
211 return (b.movesLeft() - 1) / 2;
212 }
213
214 // Transposition cutoff: TODO: Pretty ugly...
215 TranspositionTable::Entry* ttEntry = nullptr;
216 if constexpr (USE_TRANSPOSITION_TABLE) {
217 if (b.movesLeft() > 6 && b.movesLeft() % 2 == 0) {
218 ttEntry = transpositionTable.get(b);
219 if (ttEntry && ttEntry->b == b.uid() &&
220 ttEntry->searchDepth >= remainingBudget) {
221 if (ttEntry->flag == TranspositionTable::Entry::EXACT) {
222 return ttEntry->value;
223 } else if (ttEntry->flag == TranspositionTable::Entry::LOWER) {
224 alpha = std::max(alpha, ttEntry->value);
225 } else if (ttEntry->flag == TranspositionTable::Entry::UPPER) {
226 beta = std::min(beta, ttEntry->value);
227 }
228 if (alpha >= beta) {
229 return ttEntry->value;
230 }
231 }
232 }
233 // Enhanced Transposition Cutoff
234 else if (depth < 22 && b.movesLeft() % 2) {
235 auto etcMoves = b.legalMovesMask();
236 while (etcMoves) {
237 auto mv = b.nextMove(etcMoves);
238 assert(uint64_t_popcnt(mv) == 1);
239 auto bETC = b.playBitMaskOnCopy(mv);
240 auto etcEntry = transpositionTable.get(bETC);
241
242 if (etcEntry->b == bETC.uid() &&
243 etcEntry->searchDepth >= remainingBudget &&
244 etcEntry->flag != TranspositionTable::Entry::LOWER &&
245 -etcEntry->value >= beta) {
246 return -etcEntry->value;
247 }
248
249 etcMoves ^= mv;
250 }
251 }
252
253 // Check symmetric positions
254 // Symmetries get rare at some point in the game, so do not check them
255 // on almost-full boards
256 if (b.movesLeft() > 20) {
257 const auto bMirror = b.mirror();
258 auto ttEntryMirror = transpositionTable.get(bMirror);
259 if (ttEntryMirror && ttEntryMirror->b == bMirror.uid() &&
260 ttEntryMirror->searchDepth >= remainingBudget) {
261 if (ttEntryMirror->flag == TranspositionTable::Entry::EXACT) {
262 return ttEntryMirror->value;
263 } else if (ttEntryMirror->flag == TranspositionTable::Entry::LOWER) {
264 alpha = std::max(alpha, ttEntryMirror->value);
265 } else if (ttEntryMirror->flag == TranspositionTable::Entry::UPPER) {
266 beta = std::min(beta, ttEntryMirror->value);
267 }
268 if (alpha >= beta) {
269 return ttEntryMirror->value;
270 }
271 }
272 }
273 }
274
275 /*
276 if (alpha >= (b.movesLeft() + 1) / 2) {
277 // We cannot get better than this any more (with every additional move,
278 // our potential score gets lower since we have a later win).
279 return alpha;
280 }
281 */
282
283 int value = -(1 << 10);
284 if (depth < 20) {
285 auto mvList = b.sortMoves(moves);
286
287 // while (const auto mv = mvList.getNext() && alpha < beta) {
288 auto mv = mvList.pop();
289 for (; mv && alpha < beta; mv = mvList.pop()) {
290 // const auto mv = (threats ? b.nextMove(threats) : b.nextMove(moves));
291 assert(uint64_t_popcnt(mv) == 1);
292 auto moveValue = -negamax(b.playBitMaskOnCopy(mv), -beta, -alpha,
293 depth + 1, maxDepth);
294 value = std::max(value, moveValue);
295 alpha = std::max(alpha, value);
296 }
297 } else {
298 auto threats = depth < 22 ? b.findThreats(moves) : UINT64_C(0);
299 assert((threats & moves) == threats);
300
301 // int value = -(1 << 10);
302 while (moves && alpha < beta) {
303 // auto mvList = (movesFirst ? movesFirst : moves);
304 const auto mv = (threats ? b.nextMove(threats) : b.nextMove(moves));
305 assert(uint64_t_popcnt(mv) == 1);
306 auto moveValue = -negamax(b.playBitMaskOnCopy(mv), -beta, -alpha,
307 depth + 1, maxDepth);
308 value = std::max(value, moveValue);
309 alpha = std::max(alpha, value);
310 threats &= ~mv;
311 moves ^= mv;
312 }
313 }
314
315 if constexpr (USE_TRANSPOSITION_TABLE) {
316 if (!ttEntry) return value;
317 assert(ttEntry != nullptr);
318 // Do not allow high-depth nodes to override low-depth nodes (low-depth
319 // nodes achieve higher cut-offs): Does not help!
320 // if ( ttEntry->flag == TranspositionTable::Entry::EXACT &&
321 // ttEntry->b.movesLeft() < 42 &&
322 // ttEntry->b.movesLeft() > b.movesLeft() + 16)
323 // return value;
324
325 // Store node result in Transposition value
326 ttEntry->b = b.uid();
327 ttEntry->value = value;
328 ttEntry->searchDepth = remainingBudget;
329
330 if (value <= oldAlpha) {
331 ttEntry->flag = TranspositionTable::Entry::UPPER;
332 } else if (value >= beta) {
333 ttEntry->flag = TranspositionTable::Entry::LOWER;
334 } else {
335 ttEntry->flag = TranspositionTable::Entry::EXACT;
336 }
337 }
338 return value;
339 }
340
341 auto scoreMove(const Board& b, const int column, const int firstGuess,
342 const int maxDepth = -1) {
343 int score = -1000;
344 if (auto afterB = b; afterB.play(column)) {
345 if (afterB.hasWin()) {
346 return (afterB.movesLeft()) / 2 + 1;
347 }
348 // TODO: Get first guess from hash table if possible
349 score = -mtdf(afterB, firstGuess, maxDepth);
350 }
351 return score;
352 }
353
354 auto scoreMoves(const Board& b, const int maxDepth = -1) {
355 std::vector scores(Board::N_COLUMNS, -1000);
356 for (auto col = 0UL; col < scores.size(); col++) {
357 /*
358 if (auto afterB = b; afterB.play(col)) {
359 if (afterB.hasWin()) {
360 scores[col] = (afterB.movesLeft()) / 2 + 1;
361 continue;
362 }
363 // TODO: Get first guess from hash table if possible
364 scores[col] = -mtdf(afterB, !col ? 0 : scores.at(col - 1));
365 }
366 */
367 // TODO: Get first guess from hash table if possible
368 scores[col] = scoreMove(b, static_cast<int>(col),
369 !col ? 0 : scores.at(col - 1), maxDepth);
370 }
371
372 return scores;
373 }
374}; // class BitBully
375} // namespace BitBully
376
377#endif // BITBULLY__BITBULLY_H_
static int scoreToMovesLeft(const int score, const Board &b) noexcept
Definition BitBully.h:54