BitBully 0.0.75
Loading...
Searching...
No Matches
Board.h
1#ifndef XBITBULLY__BOARD_H_
2#define XBITBULLY__BOARD_H_
3
4#include <array>
5#include <cassert>
6#include <cstddef>
7#include <cstdint>
8#include <iostream>
9#include <map>
10#include <random>
11#include <sstream>
12#include <vector>
13
14#include "MoveList.h"
15
16// TODO: Move function definitions to .cpp file!
17/*
18 * // https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
19 * A generalization of the best bit counting method to integers of bit-widths
20upto 128 (parameterized by type T) is this:
21
22v = v - ((v >> 1) & (T)~(T)0/3); // temp
23v = (v & (T)~(T)0/15*3) + ((v >> 2) & (T)~(T)0/15*3); // temp
24v = (v + (v >> 4)) & (T)~(T)0/255*15; // temp
25c = (T)(v * ((T)~(T)0/255)) >> (sizeof(T) - 1) * CHAR_BIT; // count
26*/
27#if __GNUC__
28#define uint64_t_popcnt __builtin_popcountll
29#else
30#if _MSC_VER
31#include <intrin.h>
32#define uint64_t_popcnt __popcnt64
33#else
34#define uint64_t_popcnt popCountBoard
35#endif
36#endif
37
38inline int ctz_u64(uint64_t x) {
39#if defined(_MSC_VER)
40 unsigned long index;
41 _BitScanForward64(&index, x);
42 return static_cast<int>(index);
43#elif defined(__GNUC__) || defined(__clang__)
44 return __builtin_ctzll(x);
45#else
46 int idx = 0;
47 while ((x & 1u) == 0u) {
48 x >>= 1u;
49 ++idx;
50 }
51 return idx;
52#endif
53}
54
55inline std::vector<int> bits_set(uint64_t x) {
56 std::vector<int> result;
57 result.reserve(uint64_t_popcnt(x));
58 while (x) {
59 int bit = ctz_u64(x);
60 result.push_back(bit);
61 x &= x - UINT64_C(1);
62 }
63 return result;
64}
65
66namespace BitBully {
67
68#ifndef CHAR_BIT
69constexpr int CHAR_BIT = 8;
70#endif
71
72static constexpr uint64_t getMask(const std::initializer_list<int> bits) {
73 uint64_t bb{UINT64_C(0)};
74 for (const auto i : bits) {
75 // return 0, if index is out of range (0-63)
76 if (i < 0 || i >= 64) {
77 return UINT64_C(0);
78 }
79 bb |= (UINT64_C(1) << i);
80 }
81 return bb;
82}
83
84static constexpr bool isIllegalBit(const int bitIdx) {
85 constexpr int COLUMN_BIT_OFFSET = 9; // TODO: redundant in class below. Fix??
86 constexpr int N_ROWS = 6; // TODO: redundant in class below. Fix??
87 constexpr int COLUMNS = 7; // TODO: redundant in class below. Fix??
88 return bitIdx >= COLUMN_BIT_OFFSET * COLUMNS ||
89 (bitIdx % COLUMN_BIT_OFFSET) / N_ROWS;
90}
91
92static constexpr uint64_t illegalBitMask() {
93 uint64_t bb{UINT64_C(0)};
94 for (size_t i = 0; i < CHAR_BIT * sizeof(uint64_t); ++i) {
95 bb ^= (isIllegalBit(i) ? UINT64_C(1) << i : UINT64_C(0));
96 }
97 return bb;
98}
99
100class Board {
101 friend class BoardTest;
102
103 public:
104 Board();
105 static constexpr int N_COLUMNS = 7;
106 static constexpr int N_ROWS = 6;
107 static constexpr int COLUMN_BIT_OFFSET = 9;
108 enum Player { P_EMPTY = 0, P_YELLOW = 1, P_RED = 2 };
109 static constexpr size_t N_VALID_BOARD_VALUES = 3; // P_EMPTY, P_YELLOW, P_RED
110 using TBitBoard = uint64_t;
111 using TMovesCounter = int;
112 using TBoardArray = std::array<std::array<int32_t, N_ROWS>, N_COLUMNS>;
113 using TBoardArrayT = std::array<std::array<int32_t, N_COLUMNS>, N_ROWS>;
114
115 [[nodiscard]] Board inline playBitMaskOnCopy(const TBitBoard mv) const {
116 Board b = *this;
117 b.playMoveFastBB(mv);
118 return b;
119 }
120
121 [[nodiscard]] Board inline playMoveOnCopy(const int mv) const {
122 // Returns an empty board in case the move is illegal.
123 Board b = *this;
124 return b.play(mv) ? b : Board();
125 }
126
127 [[nodiscard]] Board inline copy() const {
128 Board b = *this;
129 return b;
130 }
131
132 [[nodiscard]] TBitBoard legalMovesMask() const;
133
134 [[nodiscard]] std::vector<int> legalMoves(bool nonLosing,
135 bool orderMoves) const;
136
137 [[nodiscard]] static constexpr int popCountBoard(uint64_t x) {
138 int count = 0;
139 while (x) {
140 count += static_cast<int>(x & 1);
141 x >>= 1;
142 }
143 return count;
144 }
145
146 [[nodiscard]] inline auto popCountBoard() const {
147 return uint64_t_popcnt(m_bAllTokens);
148 }
149
150 [[nodiscard]] bool isLegalMove(int column) const;
151
152 [[nodiscard]] static uint64_t hash(uint64_t x) {
153 x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
154 x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
155 x = x ^ (x >> 31);
156 return x;
157 }
158
159 [[nodiscard]] uint64_t uid() const {
160 // the resulting 64-bit integer is a unique identifier for each board
161 // Can be used to store a position in a transposition table
162 return m_bActivePTokens + m_bAllTokens;
163 }
164
165 [[nodiscard]] uint64_t hash() const {
166 return hash(hash(m_bActivePTokens) ^ (hash(m_bAllTokens) << 1));
167 }
168
169 [[nodiscard]] static TBitBoard nextMove(TBitBoard allMoves) {
170 for (const auto p : BB_MOVES_PRIO_LIST) {
171 if (const TBitBoard pvMv = allMoves & p) {
172 allMoves = pvMv;
173 break;
174 }
175 }
176 return lsb(allMoves);
177 }
178
179 [[nodiscard]] bool operator==(const Board& b) const {
180 const bool equal = (b.m_bAllTokens == m_bAllTokens &&
181 b.m_bActivePTokens == m_bActivePTokens);
182
183 // Assert that if board is equal that also movesLeft are equal
184 assert((equal && (b.m_movesLeft == m_movesLeft)) || !equal);
185 return equal;
186 }
187
188 [[nodiscard]] bool operator!=(const Board& b) const { return !(b == *this); }
189
190 [[nodiscard]] TBitBoard findOddThreats(TBitBoard moves);
191
192 [[nodiscard]] bool setBoard(const TBoardArray& board);
193
194 [[nodiscard]] bool setBoard(const TBoardArrayT& board);
195
196 [[nodiscard]] bool setBoard(const std::vector<int>& moveSequence);
197
198 bool play(int column);
199 [[nodiscard]] bool play(const std::vector<int>& moveSequence);
200 [[nodiscard]] bool play(const std::string& moveSequence);
201
202 [[nodiscard]] bool setBoard(const std::string& moveSequence);
203
204 [[nodiscard]] TBoardArray toArray() const;
205
206 [[nodiscard]] static bool isValid(const TBoardArray& board);
207
208 [[nodiscard]] bool canWin() const;
209
210 [[nodiscard]] bool canWin(int column) const;
211
212 [[nodiscard]] bool hasWin() const;
213
214 [[nodiscard]] std::string toString() const;
215
216 [[nodiscard]] inline TMovesCounter movesLeft() const { return m_movesLeft; }
217
218 [[nodiscard]] inline TMovesCounter countTokens() const {
219 return N_ROWS * N_COLUMNS - m_movesLeft;
220 }
221
222 [[nodiscard]] Board mirror() const;
223
224 [[nodiscard]] MoveList sortMoves(TBitBoard moves) const;
225
226 TBitBoard findThreats(TBitBoard moves);
227
228 /*
229 * [ *, *, *, *, *, *, *]
230 * [ *, *, *, *, *, *, *]
231 * [ *, *, *, *, *, *, *]
232 * [ 5, 14, 23, 32, 41, 50, 59],
233 * [ 4, 13, 22, 31, 40, 49, 58],
234 * [ 3, 12, 21, 30, 39, 48, 57],
235 * [ 2, 11, 20, 29, 38, 47, 56],
236 * [ 1, 10, 19, 28, 37, 46, 55],
237 * [ 0, 9, 18, 27, 36, 45, 54]
238 */
239 [[nodiscard]] int getColumnHeight(const int column) const;
240
241 static inline TBitBoard lsb(const TBitBoard x) {
242 const auto mvMask = x - UINT64_C(1);
243 return ~mvMask & x;
244 }
245
246 [[nodiscard]] TBitBoard generateNonLosingMoves() const {
247 // Mostly inspired by Pascal's Code
248 // This function might return an empty bitboard. In this case, the active
249 // player will lose, since all possible moves will lead to a defeat.
250 // NOTE: This function will not return immediate winning moves in those
251 // cases where the opposing player has a double threat (or threat)
252 TBitBoard moves = legalMovesMask();
253 const TBitBoard threats =
254 winningPositions(m_bActivePTokens ^ m_bAllTokens, true);
255 if (const TBitBoard directThreats = threats & moves) {
256 // no way we can neutralize more than one direct threat...
257 moves = directThreats & (directThreats - 1) ? UINT64_C(0) : directThreats;
258 }
259
260 // No token under an opponent's threat.
261 return moves & ~(threats >> 1);
262 }
263
264 [[nodiscard]] TBitBoard doubleThreat(const TBitBoard moves) const {
265 const TBitBoard ownThreats = winningPositions(m_bActivePTokens, false);
266 const TBitBoard otherThreats =
267 winningPositions(m_bActivePTokens ^ m_bAllTokens, true);
268 return moves & (ownThreats >> 1) & (ownThreats >> 2) & ~(otherThreats >> 1);
269 }
270
271 /* [ *, *, *, *, *, *, *]
272 * [ *, *, *, *, *, *, *]
273 * [ *, *, *, *, *, *, *]
274 * [ 5, 14, 23, 32, 41, 50, 59],
275 * [ 4, 13, 22, 31, 40, 49, 58],
276 * [ 3, 12, 21, 30, 39, 48, 57],
277 * [ 2, 11, 20, 29, 38, 47, 56],
278 * [ 1, 10, 19, 28, 37, 46, 55],
279 * [ 0, 9, 18, 27, 36, 45, 54]
280 */
281 [[nodiscard]] int toHuffman() const {
282 // This function is only defined for positions with an even number of tokens
283 // and for positions with less or equal than 12 tokens.
284 if (m_movesLeft < 30 || m_movesLeft & 1) {
285 return 0;
286 }
287 int huff = INT64_C(0);
288
289 for (int i = 0; i < N_COLUMNS; ++i) {
290 auto all = m_bAllTokens;
291 auto active = m_bActivePTokens;
292 all >>= (i * COLUMN_BIT_OFFSET);
293 active >>= (i * COLUMN_BIT_OFFSET);
294 for (int j = 0; j < N_ROWS && (all & 1); j++) {
295 huff <<= 2; // we will insert 2 bits for yellow or red
296 huff |= (active & 1) ? 2 : 3; // yellow-> 10b, red -> 11b
297 all >>= 1;
298 active >>= 1;
299 }
300 huff <<= 1; // insert 0 to indicate the end of the column
301 }
302 // length until here (for 12-ply position): 12*2+7 = 31
303 return huff << 1; // add one 0-bit to fill up to a full byte
304 }
305
306 static std::pair<Board, std::vector<int>> randomBoard(
307 const int nPly, const bool forbidDirectWin = true) {
308 if (nPly < 0 || nPly > N_COLUMNS * N_ROWS) {
309 return {};
310 }
311
312 auto [b, mvList] = randomBoardInternal(nPly);
313
314 while (mvList.size() != static_cast<decltype(mvList.size())>(nPly) ||
315 (forbidDirectWin && b.canWin())) {
316 std::tie(b, mvList) = randomBoardInternal(nPly);
317 }
318
319 return std::make_pair(b, std::move(mvList));
320 }
321
322 [[nodiscard]] std::vector<Board> allPositions(const int upToNPly,
323 bool exactlyN) const {
324 // https://oeis.org/A212693
325 std::map<uint64_t, Board> positions;
326 positions.insert({uid(), *this}); // add empty board
327 addAfterStates(positions, *this, upToNPly);
328
329 std::vector<Board> boardVector;
330 boardVector.reserve(positions.size()); // Optimize memory allocation
331
332 for (const auto& [key, board] : positions) {
333 if (!exactlyN || board.countTokens() == upToNPly)
334 boardVector.push_back(board); // Copy each board into the vector
335 }
336 return boardVector;
337 }
338
339 struct RawState {
340 TBitBoard all_tokens;
341 TBitBoard active_tokens;
342 TMovesCounter moves_left;
343 };
344
345 [[nodiscard]] inline RawState rawState() const noexcept {
346 return RawState{m_bAllTokens, m_bActivePTokens, m_movesLeft};
347 }
348
349 inline void setRawState(const RawState& s) noexcept {
350 m_bAllTokens = s.all_tokens;
351 m_bActivePTokens = s.active_tokens;
352 m_movesLeft = s.moves_left;
353 }
354
355 private:
356 /* [ *, *, *, *, *, *, *]
357 * [ *, *, *, *, *, *, *]
358 * [ *, *, *, *, *, *, *]
359 * [ 5, 14, 23, 32, 41, 50, 59],
360 * [ 4, 13, 22, 31, 40, 49, 58],
361 * [ 3, 12, 21, 30, 39, 48, 57],
362 * [ 2, 11, 20, 29, 38, 47, 56],
363 * [ 1, 10, 19, 28, 37, 46, 55],
364 * [ 0, 9, 18, 27, 36, 45, 54]
365 */
366 static constexpr auto BOTTOM_ROW_BITS = {54, 45, 36, 27, 18, 9, 0};
367 static constexpr TBitBoard BB_BOTTOM_ROW = getMask(BOTTOM_ROW_BITS);
368 static constexpr auto TOP_ROW_BITS = {59, 50, 41, 32, 23, 14, 5};
369 static constexpr TBitBoard BB_TOP_ROW = getMask(TOP_ROW_BITS);
370 static constexpr TBitBoard BB_ILLEGAL = illegalBitMask();
371 static constexpr TBitBoard BB_ALL_LEGAL_TOKENS = ~BB_ILLEGAL;
372 static constexpr TBitBoard BB_EMPTY{UINT64_C(0)};
373
374 // These two center fields generally are the most promising ones:
375 static constexpr TBitBoard BB_MOVES_PRIO1 = getMask({29, 30});
376
377 // After {29, 30}, we should consider these moves, and so on:
378 static constexpr TBitBoard BB_MOVES_PRIO2 = getMask({31, 21, 20, 28, 38, 39});
379 static constexpr TBitBoard BB_MOVES_PRIO3 = getMask({40, 32, 22, 19, 27, 37});
380 static constexpr TBitBoard BB_MOVES_PRIO4 = getMask({47, 48, 11, 12});
381 static constexpr TBitBoard BB_MOVES_PRIO5 =
382 getMask({49, 41, 23, 13, 10, 18, 36, 46});
383 static constexpr TBitBoard BB_MOVES_PRIO6 = getMask({45, 50, 14, 9});
384 static constexpr auto BB_MOVES_PRIO_LIST = {BB_MOVES_PRIO1, BB_MOVES_PRIO2,
385 BB_MOVES_PRIO3, BB_MOVES_PRIO4,
386 BB_MOVES_PRIO5, BB_MOVES_PRIO6};
387
388 /* Having a bitboard that contains all stones and another one representing the
389 * current active player has the advantage that we do not have to do any
390 * branching to figure out which player's turn it is. After each move we
391 * simply apply an XOR-operation to switch players. */
392 /* [ *, *, *, *, *, *, *]
393 * [ *, *, *, *, *, *, *]
394 * [ *, *, *, *, *, *, *]
395 * [ 5, 14, 23, 32, 41, 50, 59],
396 * [ 4, 13, 22, 31, 40, 49, 58],
397 * [ 3, 12, 21, 30, 39, 48, 57],
398 * [ 2, 11, 20, 29, 38, 47, 56],
399 * [ 1, 10, 19, 28, 37, 46, 55],
400 * [ 0, 9, 18, 27, 36, 45, 54]
401 */
402 TBitBoard m_bAllTokens, m_bActivePTokens;
403 TMovesCounter m_movesLeft;
404
405 static TBitBoard winningPositions(TBitBoard x, bool verticals);
406
407 auto static inline constexpr getColumnMask(const int column) {
408 assert(column >= 0 && column < N_COLUMNS);
409 return (UINT64_C(1) << (column * COLUMN_BIT_OFFSET + N_ROWS)) -
410 (UINT64_C(1) << (column * COLUMN_BIT_OFFSET));
411 }
412
413 auto static inline constexpr getRowMask(const int row) {
414 assert(row >= 0 && row < N_ROWS);
415 TBitBoard mask{0};
416 for (int i = 0; i < N_COLUMNS; ++i) {
417 mask |= (UINT64_C(1) << (i * COLUMN_BIT_OFFSET + row));
418 }
419 return mask;
420 }
421
422 /* [ *, *, *, *, *, *, *]
423 * [ *, *, *, *, *, *, *]
424 * [ *, *, *, *, *, *, *]
425 * [ 5, 14, 23, 32, 41, 50, 59],
426 * [ 4, 13, 22, 31, 40, 49, 58],
427 * [ 3, 12, 21, 30, 39, 48, 57],
428 * [ 2, 11, 20, 29, 38, 47, 56],
429 * [ 1, 10, 19, 28, 37, 46, 55],
430 * [ 0, 9, 18, 27, 36, 45, 54]
431 */
432 auto static constexpr mirrorBitBoard(const TBitBoard x) {
433 // TODO: It should be possible to do it in x only (using XORS). But,
434 // premature optimization is the root of all evil. Try this later.
435 // TODO: Any difference using XOR instead of OR? (probably not)...
436 TBitBoard y{UINT64_C(0)};
437 // move left-most column to right-most and vice versa:
438 y |= ((x & getColumnMask(6)) >> 6 * COLUMN_BIT_OFFSET);
439 y |= ((x & getColumnMask(0)) << 6 * COLUMN_BIT_OFFSET);
440
441 // Same with columns 1 & 5...
442 y |= ((x & getColumnMask(5)) >> 4 * COLUMN_BIT_OFFSET);
443 y |= ((x & getColumnMask(1)) << 4 * COLUMN_BIT_OFFSET);
444
445 // Same with columns 2 & 4
446 y |= ((x & getColumnMask(4)) >> 2 * COLUMN_BIT_OFFSET);
447 y |= ((x & getColumnMask(2)) << 2 * COLUMN_BIT_OFFSET);
448
449 // column 3 stays where it is...
450 return y | (x & getColumnMask(3));
451 }
452
453 static constexpr uint64_t getMaskColRow(const int column, const int row) {
454 assert(column >= 0 && column < N_COLUMNS);
455 assert(row >= 0 && row < N_ROWS);
456 return UINT64_C(1) << (column * COLUMN_BIT_OFFSET + row);
457 }
458
459 static constexpr Player opponent(Player p) {
460 return static_cast<Player>(3 - p);
461 }
462
463 void inline playMoveFastBB(const TBitBoard mv) {
464 assert(mv != BB_EMPTY);
465 assert((mv & BB_ILLEGAL) == BB_EMPTY);
466 assert((m_bAllTokens & mv) == BB_EMPTY);
467 m_bActivePTokens ^= m_bAllTokens; // Already, switch player
468
469 // However, move is performed for current player (assuming, above switch is
470 // not yet performed)
471 m_bAllTokens ^= mv; // bitwise xor and bitwise or are equivalent here
472 m_movesLeft--;
473 }
474
475 void inline playMoveFast(const int column) {
476 assert(column >= 0 && column < N_COLUMNS);
477 const TBitBoard columnMask = getColumnMask(column);
478 assert(uint64_t_popcnt(columnMask) == N_ROWS);
479 const auto mvMask = (m_bAllTokens + BB_BOTTOM_ROW) & columnMask;
480 playMoveFastBB(mvMask);
481 }
482
483 static void addAfterStates(std::map<uint64_t, Board>& boardCollection,
484 const Board& b, const int nPly) {
485 if (b.countTokens() >= nPly) {
486 return;
487 }
488
489 auto moves = b.legalMovesMask();
490
491 while (moves) {
492 const auto mv = b.nextMove(moves);
493 assert(uint64_t_popcnt(mv) == 1);
494 if (auto newB = b.playBitMaskOnCopy(mv);
495 boardCollection.find(newB.uid()) == boardCollection.end() &&
496 !b.hasWin()) {
497 // We have not reached this position yet
498 boardCollection.insert({newB.uid(), newB});
499 addAfterStates(boardCollection, newB, nPly);
500 }
501
502 moves ^= mv;
503 }
504 }
505
506 static std::pair<Board, std::vector<int>> randomBoardInternal(
507 const int nPly) {
508 if (nPly < 0 || nPly > N_COLUMNS * N_ROWS) {
509 return {};
510 }
511 Board b;
512
513 // Create a random device to seed the random number generator
514 static std::random_device rd;
515
516 // Create a Mersenne Twister random number generator
517 static std::mt19937 gen(rd());
518
519 // Create a uniform integer distribution for the desired range
520 static std::uniform_int_distribution<> nextUniform(0, N_COLUMNS);
521
522 std::vector<int> mvSequence;
523 static constexpr int MAX_TRIES = 20;
524 for (int j = 0; j < nPly; ++j) {
525 int randColumn, tries = 0;
526 do {
527 randColumn = nextUniform(gen);
528 tries++;
529 } while (tries < MAX_TRIES &&
530 (!b.isLegalMove(randColumn) || b.canWin(randColumn)));
531 if (tries >= MAX_TRIES) {
532 return {};
533 }
534 b.play(randColumn);
535 mvSequence.emplace_back(randColumn);
536 }
537
538 assert(b.countTokens() == nPly);
539
540 return {std::move(b), std::move(mvSequence)};
541 }
542
543 static TBoardArray transpose(const TBoardArrayT& board);
544
545 std::vector<int> orderedLegalMovesFromMask(TBitBoard mvBits) const;
546
547 std::vector<int> legalMovesFromMask(TBitBoard mvBits) const;
548};
549
550} // namespace BitBully
551
552#endif // XBITBULLY__BOARD_H_