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