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