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