14#ifndef BITBULLY__BITBULLY_H_
15#define BITBULLY__BITBULLY_H_
47 unsigned long long int nodeCounter;
49 static bool constexpr USE_TRANSPOSITION_TABLE =
true;
51 static auto constexpr DEFAULT_LOG_TRANSPOSITION_SIZE = 22;
54 std::unique_ptr<OpeningBook> m_openingBook;
64 explicit BitBully(
const std::filesystem::path& bookPath =
"")
67 USE_TRANSPOSITION_TABLE ? DEFAULT_LOG_TRANSPOSITION_SIZE : 0} {
72 inline bool isBookLoaded()
const {
return m_openingBook !=
nullptr; }
87 inline bool loadBook(
const std::filesystem::path& bookPath =
"") {
91 if (!bookPath.empty()) {
92 m_openingBook = std::make_unique<OpeningBook>(bookPath);
117 return b.movesLeft();
119 const int p = (b.movesLeft() + 1) % 2;
120 const int sgnScore = score < 0 ? 1 : 0;
121 const int absScore = score < 0 ? -score : score;
122 const int mvFinalLeft = 2 * (absScore - 1) + (sgnScore ^ p);
123 return b.movesLeft() - mvFinalLeft;
144 const int score = (b.movesLeft() + 1) / 2;
145 return (ply % 2 == 0) ? score : -score;
148 if (!b.movesLeft()) {
152 auto moves = b.generateNonLosingMoves();
154 const int score = -(b.movesLeft() / 2);
155 return (ply % 2 == 0) ? score : -score;
165 b = b.playBitMaskOnCopy(mv);
189 const int maxDepth = -1) noexcept {
195 int upperBound = INT32_MAX;
196 int lowerBound = INT32_MIN;
198 while (lowerBound < upperBound) {
199 const auto beta = std::max(g, lowerBound + 1);
200 g =
negamax(b, beta - 1, beta, 0, maxDepth);
226 int mid = min + (max - min) / 2;
227 if (mid <= 0 && min / 2 < mid)
229 else if (mid >= 0 && max / 2 > mid)
231 int r =
negamax(b, mid, mid + 1, 0, maxDepth);
244 USE_TRANSPOSITION_TABLE ? DEFAULT_LOG_TRANSPOSITION_SIZE : 0};
275 const int maxDepth = -1) noexcept {
277 assert(alpha < beta);
280 if (maxDepth >= 0 && depth >= maxDepth) {
284 const int8_t remainingBudget =
285 (maxDepth < 0) ? INT8_MAX : static_cast<int8_t>(maxDepth - depth);
288 return m_openingBook->getBoardValue(b);
295 if (!depth && b.
canWin()) {
306 if (
const int min = -b.
movesLeft() / 2; alpha < min) {
308 if (alpha >= beta)
return alpha;
310 if (
const int max = (b.
movesLeft() - 1) / 2; beta > max) {
312 if (alpha >= beta)
return beta;
321 int oldAlpha = alpha;
337 if constexpr (USE_TRANSPOSITION_TABLE) {
339 ttEntry = transpositionTable.get(b);
340 if (ttEntry && ttEntry->
b == b.
uid() &&
343 return ttEntry->
value;
345 alpha = std::max(alpha, ttEntry->
value);
347 beta = std::min(beta, ttEntry->
value);
350 return ttEntry->
value;
355 else if (depth < 22 && b.
movesLeft() % 2) {
361 auto etcEntry = transpositionTable.get(bETC);
363 if (etcEntry->b == bETC.uid() &&
364 etcEntry->searchDepth >= remainingBudget &&
366 -etcEntry->value >= beta) {
367 return -etcEntry->value;
378 const auto bMirror = b.
mirror();
379 auto ttEntryMirror = transpositionTable.get(bMirror);
380 if (ttEntryMirror && ttEntryMirror->b == bMirror.uid() &&
381 ttEntryMirror->searchDepth >= remainingBudget) {
383 return ttEntryMirror->value;
385 alpha = std::max(alpha, ttEntryMirror->value);
387 beta = std::min(beta, ttEntryMirror->value);
390 return ttEntryMirror->value;
404 int value = -(1 << 10);
409 auto mv = mvList.
pop();
410 for (; mv && alpha < beta; mv = mvList.pop()) {
414 depth + 1, maxDepth);
415 value = std::max(value, moveValue);
416 alpha = std::max(alpha, value);
419 auto threats = depth < 22 ? b.
findThreats(moves) : UINT64_C(0);
420 assert((threats & moves) == threats);
423 while (moves && alpha < beta) {
428 depth + 1, maxDepth);
429 value = std::max(value, moveValue);
430 alpha = std::max(alpha, value);
436 if constexpr (USE_TRANSPOSITION_TABLE) {
437 if (!ttEntry)
return value;
438 assert(ttEntry !=
nullptr);
447 ttEntry->
b = b.
uid();
448 ttEntry->
value = value;
451 if (value <= oldAlpha) {
453 }
else if (value >= beta) {
477 const int maxDepth = -1) {
479 if (
auto afterB = b; afterB.
play(column)) {
480 if (afterB.hasWin()) {
481 return (afterB.movesLeft()) / 2 + 1;
484 score = -
mtdf(afterB, firstGuess, maxDepth);
501 for (
auto col = 0UL; col < scores.size(); col++) {
513 scores[col] =
scoreMove(b,
static_cast<int>(col),
514 !col ? 0 : scores.at(col - 1), maxDepth);
Bitboard-based representation of a Connect-4 position.
#define uint64_t_popcnt
Portable fallback popcount when no compiler intrinsic is available.
Tiny priority queue used by the engine for move ordering.
Read-only access to BitBully's pre-computed Connect-4 opening books.
Direct-mapped, replacement-free transposition table used by the solver.
bool loadBook(const std::filesystem::path &bookPath="")
Load an opening book from disk.
bool isBookLoaded() const
Was an opening book successfully loaded?
void resetNodeCounter()
Reset the visited-node counter to zero.
static int scoreToMovesLeft(const int score, const Board &b) noexcept
Convert a solver score to the number of moves until the game ends.
int mtdf(const Board &b, const int firstGuess, const int maxDepth=-1) noexcept
Solve a position using the MTD(f) driver.
void resetTranspositionTable()
Discard the contents of the transposition table.
auto scoreMove(const Board &b, const int column, const int firstGuess, const int maxDepth=-1)
Evaluate the move that drops a stone in column.
auto getNodeCounter() const
Number of nodes visited since resetNodeCounter().
int nullWindow(const Board &b, const int maxDepth=-1) noexcept
Alternative driver based on a binary search of zero-width windows.
BitBully(const std::filesystem::path &bookPath="")
Construct a solver, optionally loading an opening book.
auto scoreMoves(const Board &b, const int maxDepth=-1)
Evaluate every column move from b.
int negamax(Board b, int alpha, int beta, const int depth, const int maxDepth=-1) noexcept
Negamax search with alpha-beta pruning, transposition table and opening book consultation.
static int rollout(Board b) noexcept
Cheap evaluation: play out the game using safe heuristic moves.
void resetBook()
Discard the currently loaded opening book (if any).
Connect-4 position represented as a pair of 64-bit bitboards.
MoveList sortMoves(TBitBoard moves) const
Sort candidate moves into a MoveList using a threat-based heuristic.
static constexpr int N_ROWS
Number of rows in a Connect-4 grid.
TBitBoard legalMovesMask() const
Bitboard whose set bits mark the next reachable cell of every non-full column.
TMovesCounter countTokens() const
Number of stones currently placed on the board.
TBitBoard doubleThreat(const TBitBoard moves) const
Identify moves that create a "double threat" (forced win).
bool canWin() const
Does the player to move have an immediately winning move?
bool play(int column)
Drop a stone of the player to move into column.
Board playBitMaskOnCopy(const TBitBoard mv) const
Apply a move (already encoded as a single-bit mask) on a copy of this board.
static constexpr int N_COLUMNS
Number of columns in a Connect-4 grid.
static TBitBoard nextMove(TBitBoard allMoves)
Pick the best-ranked move from a candidate set using a fixed priority order.
TBitBoard generateNonLosingMoves() const
Compute moves that do not lose immediately.
Board mirror() const
Mirror the board around its central column.
static constexpr int popCountBoard(uint64_t x)
Software popcount fallback used when no intrinsic is available.
TBitBoard findThreats(TBitBoard moves)
Identify candidate moves that yield a tactical advantage.
TMovesCounter movesLeft() const
Number of plies remaining until the board is full.
uint64_t uid() const
Unique 64-bit identifier of the position.
TBitBoard pop()
Remove and return the highest-priority move.
Power-of-two-sized direct-mapped cache for negamax search results.
Top-level namespace for the BitBully Connect-4 engine.
One transposition-table slot.
int value
Cached search score.
NodeType flag
Type of bound stored in value.
int8_t searchDepth
Remaining search budget when the entry was stored.
uint64_t b
Position UID (see Board::uid()).
@ LOWER
Lower bound (fail-high cut node).
@ UPPER
Upper bound (fail-low all node).
@ EXACT
Exact score (PV node).