BitBully 0.0.39
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);
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
48 // TODO: firstGuess is a parameter which could be tuned!
49 int mtdf(const Board &b, const int firstGuess) {
50 // MTD(f) algorithm by Aske Plaat: Plaat, Aske; Jonathan Schaeffer; Wim
51 // Pijls; Arie de Bruin (November 1996). "Best-first Fixed-depth Minimax
52 // Algorithms". Artificial Intelligence. 87 (1–2): 255–293.
53 // doi:10.1016/0004-3702(95)00126-3
54 auto g = firstGuess;
55 int upperBound = INT32_MAX;
56 int lowerBound = INT32_MIN;
57
58 while (lowerBound < upperBound) {
59 const auto beta = std::max(g, lowerBound + 1);
60 g = negamax(b, beta - 1, beta, 0);
61 if (g < beta) {
62 upperBound = g;
63 } else {
64 lowerBound = g;
65 }
66 }
67 return g;
68 }
69
70 // generally, appears to be slower than mtdf
71 int nullWindow(const Board &b) {
72 int min = -b.movesLeft() / 2;
73 int max = (b.movesLeft() + 1) / 2;
74
75 while (min < max) {
76 int mid = min + (max - min) / 2;
77 if (mid <= 0 && min / 2 < mid)
78 mid = min / 2;
79 else if (mid >= 0 && max / 2 > mid)
80 mid = max / 2;
81 int r = negamax(b, mid, mid + 1, 0);
82 if (r <= mid) {
83 max = r;
84 } else {
85 min = r;
86 }
87 }
88 return min;
89 }
90
91 void resetTranspositionTable() {
92 transpositionTable = TranspositionTable{
93 USE_TRANSPOSITION_TABLE ? DEFAULT_LOG_TRANSPOSITION_SIZE : 0};
94 }
95
96 [[nodiscard]] auto getNodeCounter() const { return nodeCounter; }
97
98 void resetNodeCounter() { nodeCounter = 0ULL; }
99
100 int negamax(Board b, int alpha, int beta, int depth) {
101 // In many aspects inspired by Pascal's code
102 assert(alpha < beta);
103 nodeCounter++;
104
105 if (isBookLoaded() && b.countTokens() == m_openingBook->getNPly()) {
106 return m_openingBook->getBoardValue(b);
107 }
108
109 // It appears as if this check is not necessary. Below we check, if we
110 // have any non-losing moves left. If not, we return with a negative
111 // score.
112 // TODO: move this outside negamax:
113 if (!depth && b.canWin()) {
114 return (b.movesLeft() + 1) / 2;
115 }
116
117 if (alpha >= (b.movesLeft() + 1) / 2) {
118 // We cannot get better than this (alpha) anymore (with every additional
119 // move, our potential score gets lower since we have a later win).
120 return alpha;
121 }
122
123 // lower bound of score as opponent cannot win next move:
124 if (const int min = -b.movesLeft() / 2; alpha < min) {
125 alpha = min;
126 if (alpha >= beta) return alpha;
127 }
128 if (const int max = (b.movesLeft() - 1) / 2; beta > max) {
129 beta = max;
130 if (alpha >= beta) return beta;
131 }
132
133 if (!b.movesLeft()) {
134 assert(!b.generateMoves());
135 assert(b.popCountBoard() == Board::N_COLUMNS * Board::N_ROWS);
136 return 0;
137 }
138
139 int oldAlpha = alpha;
140
141 auto moves = b.generateNonLosingMoves();
142 if (!moves) {
143 return -b.movesLeft() / 2;
144 }
145
146 assert(uint64_t_popcnt(moves) <= Board::N_COLUMNS);
147 assert(uint64_t_popcnt(moves) > 0);
148
149 if (depth < 20 && b.doubleThreat(moves)) {
150 return (b.movesLeft() - 1) / 2;
151 }
152
153 // Transposition cutoff: TODO: Pretty ugly...
154 TranspositionTable::Entry *ttEntry = nullptr;
155 if constexpr (USE_TRANSPOSITION_TABLE) {
156 if (b.movesLeft() > 6 && b.movesLeft() % 2 == 0) {
157 ttEntry = transpositionTable.get(b);
158 if (ttEntry && ttEntry->b == b.uid()) {
159 if (ttEntry->flag == TranspositionTable::Entry::EXACT) {
160 return ttEntry->value;
161 } else if (ttEntry->flag == TranspositionTable::Entry::LOWER) {
162 alpha = std::max(alpha, ttEntry->value);
163 } else if (ttEntry->flag == TranspositionTable::Entry::UPPER) {
164 beta = std::min(beta, ttEntry->value);
165 }
166 if (alpha >= beta) {
167 return ttEntry->value;
168 }
169 }
170 }
171 // Enhanced Transposition Cutoff
172 else if (depth < 22 && b.movesLeft() % 2) {
173 auto etcMoves = b.generateMoves();
174 while (etcMoves) {
175 auto mv = b.nextMove(etcMoves);
176 assert(uint64_t_popcnt(mv) == 1);
177 auto bETC = b.playMoveOnCopy(mv);
178 auto etcEntry = transpositionTable.get(bETC);
179
180 if (etcEntry->b == bETC.uid() &&
181 etcEntry->flag != TranspositionTable::Entry::LOWER &&
182 -etcEntry->value >= beta) {
183 return -etcEntry->value;
184 }
185
186 etcMoves ^= mv;
187 }
188 }
189
190 // Check symmetric positions
191 // Symmetries get rare at some point in the game, so do not check them
192 // on almost-full boards
193 if (b.movesLeft() > 20) {
194 const auto bMirror = b.mirror();
195 auto ttEntryMirror = transpositionTable.get(bMirror);
196 if (ttEntryMirror && ttEntryMirror->b == bMirror.uid()) {
197 if (ttEntryMirror->flag == TranspositionTable::Entry::EXACT) {
198 return ttEntryMirror->value;
199 } else if (ttEntryMirror->flag == TranspositionTable::Entry::LOWER) {
200 alpha = std::max(alpha, ttEntryMirror->value);
201 } else if (ttEntryMirror->flag == TranspositionTable::Entry::UPPER) {
202 beta = std::min(beta, ttEntryMirror->value);
203 }
204 if (alpha >= beta) {
205 return ttEntryMirror->value;
206 }
207 }
208 }
209 }
210
211 /*
212 if (alpha >= (b.movesLeft() + 1) / 2) {
213 // We cannot get better than this any more (with every additional move,
214 // our potential score gets lower since we have a later win).
215 return alpha;
216 }
217 */
218
219 int value = -(1 << 10);
220 if (depth < 20) {
221 auto mvList = b.sortMoves(moves);
222
223 // while (const auto mv = mvList.getNext() && alpha < beta) {
224 auto mv = mvList.pop();
225 for (; mv && alpha < beta; mv = mvList.pop()) {
226 // const auto mv = (threats ? b.nextMove(threats) : b.nextMove(moves));
227 assert(uint64_t_popcnt(mv) == 1);
228 auto moveValue =
229 -negamax(b.playMoveOnCopy(mv), -beta, -alpha, depth + 1);
230 value = std::max(value, moveValue);
231 alpha = std::max(alpha, value);
232 }
233 } else {
234 auto threats = depth < 22 ? b.findThreats(moves) : UINT64_C(0);
235 assert((threats & moves) == threats);
236
237 // int value = -(1 << 10);
238 while (moves && alpha < beta) {
239 // auto mvList = (movesFirst ? movesFirst : moves);
240 const auto mv = (threats ? b.nextMove(threats) : b.nextMove(moves));
241 assert(uint64_t_popcnt(mv) == 1);
242 auto moveValue =
243 -negamax(b.playMoveOnCopy(mv), -beta, -alpha, depth + 1);
244 value = std::max(value, moveValue);
245 alpha = std::max(alpha, value);
246 threats &= ~mv;
247 moves ^= mv;
248 }
249 }
250
251 if constexpr (USE_TRANSPOSITION_TABLE) {
252 if (!ttEntry) return value;
253 assert(ttEntry != nullptr);
254 // Do not allow high-depth nodes to override low-depth nodes (low-depth
255 // nodes achieve higher cut-offs): Does not help!
256 // if ( ttEntry->flag == TranspositionTable::Entry::EXACT &&
257 // ttEntry->b.movesLeft() < 42 &&
258 // ttEntry->b.movesLeft() > b.movesLeft() + 16)
259 // return value;
260
261 // Store node result in Transposition value
262 ttEntry->b = b.uid();
263 ttEntry->value = value;
264
265 if (value <= oldAlpha) {
266 ttEntry->flag = TranspositionTable::Entry::UPPER;
267 } else if (value >= beta) {
268 ttEntry->flag = TranspositionTable::Entry::LOWER;
269 } else {
270 ttEntry->flag = TranspositionTable::Entry::EXACT;
271 }
272 }
273 return value;
274 }
275
276 auto scoreMoves(const Board &b) {
277 std::vector scores(Board::N_COLUMNS, -1000);
278 for (auto col = 0UL; col < scores.size(); col++) {
279 if (auto afterB = b; afterB.playMove(col)) {
280 if (afterB.hasWin()) {
281 scores[col] = (afterB.movesLeft()) / 2 + 1;
282 continue;
283 }
284 // TODO: Get first guess from hash table if possible
285 scores[col] = -mtdf(afterB, !col ? 0 : scores.at(col - 1));
286 }
287 }
288
289 return scores;
290 }
291};
292} // namespace BitBully
293
294#endif // BITBULLY__BITBULLY_H_