BitBully 0.0.59-a2
Loading...
Searching...
No Matches
Board.cpp
1#include "Board.h"
2
3#include <algorithm>
4#include <array>
5#include <cassert>
6
7namespace BitBully {
8
9Board::Board()
10 : m_bAllTokens{BB_EMPTY},
11 m_bActivePTokens{BB_EMPTY},
12 m_movesLeft{N_COLUMNS * N_ROWS} {
13 // asserts will be turned off in Release mode
14 assert(uint64_t_popcnt(BB_ALL_LEGAL_TOKENS) == N_COLUMNS * N_ROWS);
15 assert(uint64_t_popcnt(BB_ILLEGAL) ==
16 CHAR_BIT * sizeof(TBitBoard) - N_COLUMNS * N_ROWS);
17 assert(getRowMask(0) == getMask({0, 9, 18, 27, 36, 45, 54}));
18 assert(getRowMask(5) == getMask({5, 14, 23, 32, 41, 50, 59}));
19}
20
21bool Board::setBoard(const std::vector<int>& moveSequence) {
22 Board b; // First operate on fresh board
23
24 if (!b.play(moveSequence)) {
25 return false;
26 }
27
28 *this = b;
29 return true;
30}
31
32bool Board::setBoard(const std::string& moveSequence) {
33 Board b; // start with an empty board
34
35 // attempt to play the move sequence
36 if (!b.play(moveSequence)) return false;
37
38 // only modify the current board on success
39 *this = b;
40 return true;
41}
42
43Board::TBoardArray Board::transpose(const TBoardArrayT& board) {
44 TBoardArray result{};
45 for (std::size_t c = 0; c < N_COLUMNS; ++c) {
46 for (std::size_t r = 0; r < N_ROWS; ++r) {
47 result[c][N_ROWS - r - 1] = board[r][c];
48 }
49 }
50 return result;
51}
52
53bool Board::setBoard(const TBoardArrayT& board) {
54 const auto boardT = transpose(board);
55 return setBoard(boardT);
56}
57
58bool Board::setBoard(const TBoardArray& board) {
59 if (!isValid(board)) {
60 return false;
61 }
62 TBitBoard allTokens{BB_EMPTY}, yellowTokens{BB_EMPTY};
63 for (auto c = 0; c < N_COLUMNS; ++c) {
64 for (auto r = 0; r < N_ROWS; ++r) {
65 const auto val = board[c][r];
66 const auto mask = getMaskColRow(c, r);
67 if (val == P_YELLOW) {
68 yellowTokens ^= mask;
69 } else if (val != P_RED) {
70 assert(val == P_EMPTY);
71 continue;
72 }
73 assert((allTokens & mask) == BB_EMPTY);
74 allTokens ^= mask;
75 }
76 }
77
78 m_movesLeft =
79 N_COLUMNS * N_ROWS - static_cast<int>(uint64_t_popcnt(allTokens));
80
81 // if m_movesLeft is odd that means that its players RED turn
82 m_bActivePTokens = yellowTokens ^ (m_movesLeft & 1 ? allTokens : BB_EMPTY);
83 m_bAllTokens = allTokens;
84
85 return true;
86}
87
88bool Board::isValid(const TBoardArray& board) {
89 // Counter for P_YELLOW, P_RED and P_EMPTY
90 std::array<int, N_VALID_BOARD_VALUES> valCounts = {0UL};
91 for (auto c = static_cast<TBoardArray::size_type>(0); c < N_COLUMNS; ++c) {
92 bool columnComplete = false;
93 for (auto r = static_cast<TBoardArray::size_type>(0); r < N_ROWS; ++r) {
94 const auto val = board[c][r];
95 // There are only 3 options for a cell:
96 if (val != P_RED && val != P_YELLOW && val != P_EMPTY) {
97 return false;
98 }
99 if (val == P_EMPTY) {
100 columnComplete = true; // rest of column should be empty
101 } else {
102 if (columnComplete) return false; // Unexpected bYellow/red stone
103 }
104 valCounts[val]++;
105 }
106 }
107
108 // Yellow cannot have fewer pieces than Red:
109 if (const auto diff = valCounts[P_YELLOW] - valCounts[P_RED];
110 diff < 0 || diff > 1) {
111 return false;
112 }
113
114 // TODO: Check for multiple Wins...
115 return true;
116}
117
118/* [ *, *, *, *, *, *, *]
119 * [ *, *, *, *, *, *, *]
120 * [ *, *, *, *, *, *, *]
121 * [ 5, 14, 23, 32, 41, 50, 59],
122 * [ 4, 13, 22, 31, 40, 49, 58],
123 * [ 3, 12, 21, 30, 39, 48, 57],
124 * [ 2, 11, 20, 29, 38, 47, 56],
125 * [ 1, 10, 19, 28, 37, 46, 55],
126 * [ 0, 9, 18, 27, 36, 45, 54]
127 */
128bool Board::play(const int column) {
129 // Check, if column full...
130 if (!isLegalMove(column)) return false;
131
132 playMoveFast(column);
133
134 return true;
135}
136
137bool Board::play(const std::vector<int>& moveSequence) {
138 Board b = *this; // First operate on copy
139
140 for (const auto mv : moveSequence) {
141 if (!b.play(mv)) return false;
142 }
143 *this = b;
144 return true;
145}
146
147bool Board::play(const std::string& moveSequence) {
148 for (const char c : moveSequence) {
149 if (c < '0' || c >= '0' + N_COLUMNS) return false;
150 }
151
152 std::vector<int> moveSequenceInt;
153 moveSequenceInt.reserve(
154 moveSequence.size()); // optional, avoids reallocations
155 std::transform(moveSequence.begin(), moveSequence.end(),
156 std::back_inserter(moveSequenceInt),
157 [](const char c) { return c - '0'; });
158 return play(moveSequenceInt);
159}
160
161bool Board::isLegalMove(const int column) const {
162 if (column < 0 || column >= N_COLUMNS) return false;
163
164 TBitBoard columnMask = getColumnMask(column);
165 columnMask -= (UINT64_C(1) << (column * COLUMN_BIT_OFFSET));
166 return !(m_bAllTokens & columnMask & BB_TOP_ROW);
167}
168
169/*
170 * Simplified case: Position x is a nibble (4 bit): x = abcd
171 *
172 * a b c d | A B C D
173 * --------+--------
174 * 0 0 0 0 | 0 0 0 0
175 * 0 0 0 1 | 0 0 0 0
176 * 0 0 1 0 | 0 0 0 0
177 * 0 0 1 1 | 0 0 0 0
178 * 0 1 0 0 | 0 0 0 0
179 * 0 1 0 1 | 0 0 0 0
180 * 0 1 1 0 | 0 0 0 0
181 * 0 1 1 1 | 1 0 0 0 < A = bcd
182 * 1 0 0 0 | 0 0 0 0
183 * 1 0 0 1 | 0 0 0 0
184 * 1 0 1 0 | 0 0 0 0
185 * 1 0 1 1 | 0 1 0 0 < B = acd
186 * 1 1 0 0 | 0 0 0 0
187 * 1 1 0 1 | 0 0 1 0 < C = abd
188 * 1 1 1 0 | 0 0 0 1 < D = abc
189 * 1 1 1 1 | * * * * < Should not happen (since it already contains a win)
190 *
191 * In order to check if C = abd is true (putting a token in "c" would win), we
192 * have to shift the remaining bits "a", "b", "d" onto the position "c" and then
193 * perform a bitwise "AND". In the simplified tabular case above, we could do
194 * something like " C = (x >> 2) & (x >> 1) & ( x << 1)
195 * Per direction (vertical is an exception) we have 4 possible winning positions
196 * A, B, C and, D. We have to check all 4 of them. However, in order to win, the
197 * target position has to be empty and reachable. This has to be checked
198 * subsequently.
199 * TODO: inspired by John Tromp:
200 * https://github.com/gamesolver/fhourstones/blob/master/Game.c
201 */
202/*
203 * [ *, *, *, *, *, *, *]
204 * [ *, *, *, *, *, *, *]
205 * [ *, *, *, *, *, *, *]
206 * [ 5, 14, 23, 32, 41, 50, 59],
207 * [ 4, 13, 22, 31, 40, 49, 58],
208 * [ 3, 12, 21, 30, 39, 48, 57],
209 * [ 2, 11, 20, 29, 38, 47, 56],
210 * [ 1, 10, 19, 28, 37, 46, 55],
211 * [ 0, 9, 18, 27, 36, 45, 54]
212 */
213Board::TBitBoard Board::winningPositions(const TBitBoard x,
214 const bool verticals) {
215 // Vertical wins are fairly simple:
216 TBitBoard wins = verticals ? (x << 1) & (x << 2) & (x << 3) : UINT64_C(0);
217
218 for (auto b = COLUMN_BIT_OFFSET - 1; b <= COLUMN_BIT_OFFSET + 1; ++b) {
219 // tokens shifted by 1 & 2 positions left, up-left and down-left (this
220 // avoids some redundant computations below):
221 auto tmp = (x << b) & (x << 2 * b);
222 wins |= tmp & (x << 3 * b); // A = bcd
223 wins |= tmp & (x >> b); // B = bcd
224
225 // tokens shifted by 1 & 2 positions right, up-right and down-right:
226 tmp = (x >> b) & (x >> 2 * b);
227 wins |= tmp & (x << b); // C = abd
228 wins |= tmp & (x >> 3 * b); // D = abc
229 }
230
231 return wins & BB_ALL_LEGAL_TOKENS;
232}
233
234/*
235 * [ *, *, *, *, *, *, *]
236 * [ *, *, *, *, *, *, *]
237 * [ *, *, *, *, *, *, *]
238 * [ 5, 14, 23, 32, 41, 50, 59],
239 * [ 4, 13, 22, 31, 40, 49, 58],
240 * [ 3, 12, 21, 30, 39, 48, 57],
241 * [ 2, 11, 20, 29, 38, 47, 56],
242 * [ 1, 10, 19, 28, 37, 46, 55],
243 * [ 0, 9, 18, 27, 36, 45, 54]
244 */
245bool Board::hasWin() const {
246 // Vertical win:
247 const auto y = m_bActivePTokens ^ m_bAllTokens;
248 auto x = y & (y << 2);
249 if (x & (x << 1)) return true;
250
251 // Horizontal win:
252 x = y & (y << 2 * COLUMN_BIT_OFFSET);
253 if (x & (x << COLUMN_BIT_OFFSET)) return true;
254
255 // Diagonal-1 win:
256 x = y & (y << 2 * (COLUMN_BIT_OFFSET - 1));
257 if (x & (x << (COLUMN_BIT_OFFSET - 1))) return true;
258
259 // Diagonal-2 win:
260 x = y & (y << 2 * (COLUMN_BIT_OFFSET + 1));
261 if (x & (x << (COLUMN_BIT_OFFSET + 1))) return true;
262 return false;
263}
264
265/*
266 * [ *, *, *, *, *, *, *]
267 * [ *, *, *, *, *, *, *]
268 * [ *, *, *, *, *, *, *]
269 * [ 5, 14, 23, 32, 41, 50, 59],
270 * [ 4, 13, 22, 31, 40, 49, 58],
271 * [ 3, 12, 21, 30, 39, 48, 57],
272 * [ 2, 11, 20, 29, 38, 47, 56],
273 * [ 1, 10, 19, 28, 37, 46, 55],
274 * [ 0, 9, 18, 27, 36, 45, 54]
275 */
276MoveList Board::sortMoves(TBitBoard moves) const {
277 MoveList mvList;
278
279 // own threats
280 const TBitBoard ownThreats = winningPositions(m_bActivePTokens, false);
281
282 while (moves) {
283 const auto mv = nextMove(moves); // TODO: not yet efficient enough
284 assert(uint64_t_popcnt(mv) == 1);
285
286 // How many threats (indirect & direct) will I have after this move?
287 const auto threats =
288 winningPositions(m_bActivePTokens ^ mv, true) & ~(m_bAllTokens ^ mv);
289 auto numThreats = static_cast<int>(uint64_t_popcnt(threats));
290
291 // Usually, try to avoid moving under own threat since opponent will
292 // neutralize it...
293 if (ownThreats & (mv << 1)) numThreats--;
294
295 mvList.insert(mv, numThreats);
296
297 moves ^= mv;
298 }
299 return mvList;
300}
301
302Board::TBitBoard Board::findThreats(TBitBoard moves) {
303 auto threats = winningPositions(m_bActivePTokens, true) & ~m_bAllTokens;
304
305 // TODO: The pop couting function seems to matter a lot
306 auto curNumThreats = uint64_t_popcnt(threats);
307 auto threatMoves = UINT64_C(0);
308 while (moves) {
309 auto mv = lsb(moves);
310
311 // Threats are winning positions which are generated by performing move mv.
312 // However, we do not care about winning positions which are not achievable
313 // any more (since the positions are already occupied). Hence, we have to
314 // only select those positions as threats, which are still empty.
315 // It seems that all kind of threats (immediate and indirect threats) are
316 // beneficial for the move ordering!
317 threats =
318 winningPositions(m_bActivePTokens ^ mv, true) & ~(m_bAllTokens ^ mv);
319
320 if (uint64_t_popcnt(threats & (moves ^ mv)) > 1)
321 return mv; // at least 2 immediate threats
322
323 if (const auto numThreats = uint64_t_popcnt(threats);
324 numThreats > curNumThreats) {
325 threatMoves ^= mv;
326 }
327 moves ^= mv;
328 }
329
330 return threatMoves;
331}
332
333Board::TBitBoard Board::findOddThreats(TBitBoard moves) {
334 constexpr auto ODD_ROWS = getRowMask(2) | getRowMask(4);
335 auto threats = winningPositions(m_bActivePTokens, true) & ~m_bAllTokens;
336
337 // TODO: The pop couting function seems to matter a lot
338 const auto curNumThreats = uint64_t_popcnt(threats & ODD_ROWS);
339 auto threatMoves = UINT64_C(0);
340 while (moves) {
341 const auto mv = lsb(moves);
342
343 threats =
344 winningPositions(m_bActivePTokens ^ mv, true) & ~(m_bAllTokens ^ mv);
345 if (const auto numThreats = uint64_t_popcnt(threats & ODD_ROWS);
346 numThreats > curNumThreats) {
347 threatMoves ^= mv;
348 }
349 moves ^= mv;
350 }
351
352 return threatMoves;
353}
354
355/*
356std::pair<unsigned long, unsigned long> Board::findOddEvenThreats() {
357 constexpr auto ODD_ROWS = getRowMask(2) | getRowMask(4);
358 constexpr auto EVEN_ROWS = getRowMask(1) | getRowMask(3) | getRowMask(5);
359 auto threats = winningPositions(m_bActive) & ~m_bAll;
360 auto oddThreats = threats & ODD_ROWS;
361 auto evenThreats = threats & EVEN_ROWS;
362
363 int curNumOddThreats = uint64_t_popcnt(oddThreats);
364 int curNumEvenThreats = uint64_t_popcnt(evenThreats);
365 auto threatOddMoves = UINT64_C(0);
366 auto threatEvenMoves = UINT64_C(0);
367 auto moves = legalMovesMask();
368 while (moves) {
369 auto mvMask = moves - UINT64_C(1);
370 auto mv = ~mvMask & moves;
371
372 // Threats are winning positions which are generated by performing move mv.
373 // However, we do not care about winning positions which are not achievable
374 // any more (since the positions are already occupied). Hence, we have to
375 // only select those positions as threats, which are still empty.
376 threats = winningPositions(m_bActive ^ mv) & ~(m_bAll ^ mv);
377 oddThreats = threats & ODD_ROWS;
378 int numOddThreats = uint64_t_popcnt(oddThreats);
379 if (numOddThreats > curNumOddThreats) {
380 threatOddMoves ^= mv;
381 }
382
383 evenThreats = threats & ODD_ROWS;
384 int numEvenThreats = uint64_t_popcnt(oddThreats);
385 if (numEvenThreats > curNumEvenThreats) {
386 threatEvenMoves ^= mv;
387 }
388
389 moves ^= mv;
390 }
391
392 return std::pair{threatOddMoves, threatEvenMoves};
393}
394 */
395
396bool Board::canWin() const {
397 return winningPositions(m_bActivePTokens, true) &
398 (m_bAllTokens + BB_BOTTOM_ROW);
399}
400
401bool Board::canWin(int column) const {
402 return isLegalMove(column) &&
403 (winningPositions(m_bActivePTokens, true) &
404 (m_bAllTokens + BB_BOTTOM_ROW) & getColumnMask(column));
405}
406
407Board::TBitBoard Board::legalMovesMask() const {
408 return (m_bAllTokens + BB_BOTTOM_ROW) & BB_ALL_LEGAL_TOKENS;
409}
410
411// In Board.cpp
412
413std::vector<int> Board::orderedLegalMovesFromMask(TBitBoard mvBits) const {
414 auto sortedMoves = sortMoves(mvBits); // MoveList
415
416 std::vector<int> cols;
417 cols.reserve(sortedMoves.getSize());
418
419 while (auto mv = sortedMoves.pop()) {
420 int bit = ctz_u64(mv); // bit index of the move
421 cols.push_back(bit / COLUMN_BIT_OFFSET); // convert to column index
422 }
423
424 return cols;
425}
426
427std::vector<int> Board::legalMovesFromMask(TBitBoard mvBits) const {
428 auto bits = bits_set(mvBits); // bit indices of legal moves
429
430 std::transform(bits.begin(), bits.end(), bits.begin(),
431 [](int bit) { return bit / COLUMN_BIT_OFFSET; });
432
433 return bits; // now contains column indices
434}
435
436std::vector<int> Board::legalMoves(bool nonLosing, bool orderMoves) const {
437 // TODO: Google Unit tests are missing!
438 TBitBoard mvBits = nonLosing ? generateNonLosingMoves() : legalMovesMask();
439 return orderMoves ? orderedLegalMovesFromMask(mvBits)
440 : legalMovesFromMask(mvBits);
441}
442
443Board::TBoardArray Board::toArray() const {
444 TBoardArray board{{0}};
445 const auto activePlayer = (m_movesLeft & 1 ? P_RED : P_YELLOW);
446 const auto inactivePlayer = opponent(activePlayer);
447 for (auto c = 0; c < N_COLUMNS; ++c) {
448 for (auto r = 0; r < N_ROWS; ++r) {
449 if (const auto mask = getMaskColRow(c, r); m_bActivePTokens & mask) {
450 board[c][r] = activePlayer;
451 } else if (m_bAllTokens & mask) {
452 board[c][r] = inactivePlayer;
453 } else {
454 board[c][r] = P_EMPTY;
455 }
456 }
457 }
458 return board;
459}
460
461std::string Board::toString() const {
462 std::stringstream ss;
463 ss << "\n ";
464 const auto arr = toArray();
465 for (int r = N_ROWS - 1; r >= 0; r--) {
466 for (int c = 0; c < N_COLUMNS; c++) {
467 if (arr[c][r] == P_RED) {
468 ss << "O ";
469 } else if (arr[c][r] == P_YELLOW) {
470 ss << "X ";
471 } else {
472 assert(arr[c][r] == P_EMPTY);
473 ss << "_ ";
474 }
475 }
476 ss << "\n ";
477 }
478 return ss.str();
479}
480
481Board Board::mirror() const {
482 Board mB;
483 mB.m_movesLeft = m_movesLeft;
484 mB.m_bActivePTokens = mirrorBitBoard(m_bActivePTokens);
485 mB.m_bAllTokens = mirrorBitBoard(m_bAllTokens);
486 assert(uint64_t_popcnt(mB.m_bActivePTokens) ==
487 uint64_t_popcnt(m_bActivePTokens));
488 assert(uint64_t_popcnt(mB.m_bAllTokens) == uint64_t_popcnt(m_bAllTokens));
489 return mB;
490}
491
492} // namespace BitBully