14#ifndef XBITBULLY__BOARD_H_
15#define XBITBULLY__BOARD_H_
42#define uint64_t_popcnt __builtin_popcountll
47#define uint64_t_popcnt __popcnt64
50#define uint64_t_popcnt popCountBoard
69 _BitScanForward64(&index, x);
70 return static_cast<int>(index);
71#elif defined(__GNUC__) || defined(__clang__)
72 return __builtin_ctzll(x);
75 while ((x & 1u) == 0u) {
94 std::vector<int> result;
98 result.push_back(bit);
124static constexpr uint64_t getMask(
const std::initializer_list<int> bits) {
125 uint64_t bb{UINT64_C(0)};
126 for (
const auto i : bits) {
128 if (i < 0 || i >= 64) {
131 bb |= (UINT64_C(1) << i);
148static constexpr bool isIllegalBit(
const int bitIdx) {
149 constexpr int COLUMN_BIT_OFFSET = 9;
150 constexpr int N_ROWS = 6;
151 constexpr int COLUMNS = 7;
152 return bitIdx >= COLUMN_BIT_OFFSET * COLUMNS ||
153 (bitIdx % COLUMN_BIT_OFFSET) / N_ROWS;
165static constexpr uint64_t illegalBitMask() {
166 uint64_t bb{UINT64_C(0)};
167 for (
size_t i = 0; i <
CHAR_BIT *
sizeof(uint64_t); ++i) {
168 bb ^= (isIllegalBit(i) ? UINT64_C(1) << i : UINT64_C(0));
205 friend class BoardTest;
247 b.playMoveFastBB(mv);
289 [[nodiscard]] std::vector<int>
legalMoves(
bool nonLosing,
290 bool orderMoves)
const;
301 count +=
static_cast<int>(x & 1);
334 [[nodiscard]]
static uint64_t
hash(uint64_t x) {
335 x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
336 x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
350 [[nodiscard]] uint64_t
uid()
const {
return m_bActivePTokens + m_bAllTokens; }
358 [[nodiscard]] uint64_t
hash()
const {
359 return hash(
hash(m_bActivePTokens) ^ (
hash(m_bAllTokens) << 1));
377 for (
const auto p : BB_MOVES_PRIO_LIST) {
378 if (
const TBitBoard pvMv = allMoves & p) {
383 return lsb(allMoves);
388 const bool equal = (b.m_bAllTokens == m_bAllTokens &&
389 b.m_bActivePTokens == m_bActivePTokens);
392 assert((equal && (b.m_movesLeft == m_movesLeft)) || !equal);
431 [[nodiscard]]
bool setBoard(
const std::vector<int>& moveSequence);
438 bool play(
int column);
445 [[nodiscard]]
bool play(
const std::vector<int>& moveSequence);
455 [[nodiscard]]
bool play(
const std::string& moveSequence);
458 [[nodiscard]]
bool setBoard(
const std::string& moveSequence);
478 [[nodiscard]]
bool canWin()
const;
485 [[nodiscard]]
bool canWin(
int column)
const;
492 [[nodiscard]]
bool hasWin()
const;
500 [[nodiscard]] std::string
toString()
const;
571 const auto mvMask = x - UINT64_C(1);
599 winningPositions(m_bActivePTokens ^ m_bAllTokens,
true);
600 if (
const TBitBoard directThreats = threats & moves) {
602 moves = directThreats & (directThreats - 1) ? UINT64_C(0) : directThreats;
606 return moves & ~(threats >> 1);
620 const TBitBoard ownThreats = winningPositions(m_bActivePTokens,
false);
622 winningPositions(m_bActivePTokens ^ m_bAllTokens,
true);
623 return moves & (ownThreats >> 1) & (ownThreats >> 2) & ~(otherThreats >> 1);
653 if (m_movesLeft < 30 || m_movesLeft & 1) {
656 int huff = INT64_C(0);
659 auto all = m_bAllTokens;
660 auto active = m_bActivePTokens;
663 for (
int j = 0; j <
N_ROWS && (all & 1); j++) {
665 huff |= (active & 1) ? 2 : 3;
690 const int nPly,
const bool forbidDirectWin =
true) {
695 auto [b, mvList] = randomBoardInternal(nPly);
697 while (mvList.size() !=
static_cast<decltype(mvList.size())
>(nPly) ||
698 (forbidDirectWin && b.canWin())) {
699 std::tie(b, mvList) = randomBoardInternal(nPly);
702 return std::make_pair(b, std::move(mvList));
720 bool exactlyN)
const {
722 std::map<uint64_t, Board> positions;
723 positions.insert({
uid(), *
this});
724 addAfterStates(positions, *
this, upToNPly);
726 std::vector<Board> boardVector;
727 boardVector.reserve(positions.size());
729 for (
const auto& [key, board] : positions) {
730 if (!exactlyN || board.countTokens() == upToNPly)
731 boardVector.push_back(board);
750 return RawState{m_bAllTokens, m_bActivePTokens, m_movesLeft};
761 m_bAllTokens = s.all_tokens;
762 m_bActivePTokens = s.active_tokens;
763 m_movesLeft = s.moves_left;
777 static constexpr auto BOTTOM_ROW_BITS = {54, 45, 36, 27, 18, 9, 0};
778 static constexpr TBitBoard BB_BOTTOM_ROW = getMask(BOTTOM_ROW_BITS);
779 static constexpr auto TOP_ROW_BITS = {59, 50, 41, 32, 23, 14, 5};
780 static constexpr TBitBoard BB_TOP_ROW = getMask(TOP_ROW_BITS);
781 static constexpr TBitBoard BB_ILLEGAL = illegalBitMask();
782 static constexpr TBitBoard BB_ALL_LEGAL_TOKENS = ~BB_ILLEGAL;
783 static constexpr TBitBoard BB_EMPTY{UINT64_C(0)};
786 static constexpr TBitBoard BB_MOVES_PRIO1 = getMask({29, 30});
789 static constexpr TBitBoard BB_MOVES_PRIO2 = getMask({31, 21, 20, 28, 38, 39});
790 static constexpr TBitBoard BB_MOVES_PRIO3 = getMask({40, 32, 22, 19, 27, 37});
791 static constexpr TBitBoard BB_MOVES_PRIO4 = getMask({47, 48, 11, 12});
792 static constexpr TBitBoard BB_MOVES_PRIO5 =
793 getMask({49, 41, 23, 13, 10, 18, 36, 46});
794 static constexpr TBitBoard BB_MOVES_PRIO6 = getMask({45, 50, 14, 9});
795 static constexpr auto BB_MOVES_PRIO_LIST = {BB_MOVES_PRIO1, BB_MOVES_PRIO2,
796 BB_MOVES_PRIO3, BB_MOVES_PRIO4,
797 BB_MOVES_PRIO5, BB_MOVES_PRIO6};
829 auto static inline constexpr getColumnMask(
const int column) {
830 assert(column >= 0 && column <
N_COLUMNS);
835 auto static inline constexpr getRowMask(
const int row) {
836 assert(row >= 0 && row <
N_ROWS);
854 auto static constexpr mirrorBitBoard(
const TBitBoard x) {
872 return y | (x & getColumnMask(3));
875 static constexpr uint64_t getMaskColRow(
const int column,
const int row) {
876 assert(column >= 0 && column <
N_COLUMNS);
877 assert(row >= 0 && row <
N_ROWS);
882 return static_cast<Player>(3 - p);
885 void inline playMoveFastBB(
const TBitBoard mv) {
886 assert(mv != BB_EMPTY);
887 assert((mv & BB_ILLEGAL) == BB_EMPTY);
888 assert((m_bAllTokens & mv) == BB_EMPTY);
889 m_bActivePTokens ^= m_bAllTokens;
897 void inline playMoveFast(
const int column) {
898 assert(column >= 0 && column <
N_COLUMNS);
899 const TBitBoard columnMask = getColumnMask(column);
901 const auto mvMask = (m_bAllTokens + BB_BOTTOM_ROW) & columnMask;
902 playMoveFastBB(mvMask);
905 static void addAfterStates(std::map<uint64_t, Board>& boardCollection,
906 const Board& b,
const int nPly) {
907 if (b.countTokens() >= nPly) {
911 auto moves = b.legalMovesMask();
914 const auto mv = b.nextMove(moves);
916 if (
auto newB = b.playBitMaskOnCopy(mv);
917 boardCollection.find(newB.uid()) == boardCollection.end() &&
920 boardCollection.insert({newB.uid(), newB});
921 addAfterStates(boardCollection, newB, nPly);
928 static std::pair<Board, std::vector<int>> randomBoardInternal(
936 static std::random_device rd;
939 static std::mt19937 gen(rd());
942 static std::uniform_int_distribution<> nextUniform(0,
N_COLUMNS);
944 std::vector<int> mvSequence;
945 static constexpr int MAX_TRIES = 20;
946 for (
int j = 0; j < nPly; ++j) {
947 int randColumn, tries = 0;
949 randColumn = nextUniform(gen);
951 }
while (tries < MAX_TRIES &&
952 (!b.isLegalMove(randColumn) || b.canWin(randColumn)));
953 if (tries >= MAX_TRIES) {
957 mvSequence.emplace_back(randColumn);
960 assert(b.countTokens() == nPly);
962 return {std::move(b), std::move(mvSequence)};
967 std::vector<int> orderedLegalMovesFromMask(
TBitBoard mvBits)
const;
969 std::vector<int> legalMovesFromMask(
TBitBoard mvBits)
const;
#define uint64_t_popcnt
Portable fallback popcount when no compiler intrinsic is available.
std::vector< int > bits_set(uint64_t x)
Decompose a bitboard into the indices of its set bits.
int ctz_u64(uint64_t x)
Count trailing zero bits of a 64-bit integer.
Tiny priority queue used by the engine for move ordering.
Connect-4 position represented as a pair of 64-bit bitboards.
bool operator!=(const Board &b) const
Inequality counterpart of operator==.
int getColumnHeight(const int column) const
Number of stones currently stacked in column.
MoveList sortMoves(TBitBoard moves) const
Sort candidate moves into a MoveList using a threat-based heuristic.
void setRawState(const RawState &s) noexcept
Restore an internal state previously captured with rawState().
Board()
Construct an empty board with player Yellow to move.
static constexpr int N_ROWS
Number of rows in a Connect-4 grid.
uint64_t TBitBoard
64-bit type used for any bitboard value.
TBitBoard legalMovesMask() const
Bitboard whose set bits mark the next reachable cell of every non-full column.
bool isLegalMove(int column) const
Check whether dropping a stone in column is legal.
TBitBoard findOddThreats(TBitBoard moves)
Identify candidate moves that introduce a new "odd" threat.
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).
static constexpr size_t N_VALID_BOARD_VALUES
Number of distinct cell values: empty, yellow, red.
static uint64_t hash(uint64_t x)
Splittable-mix-style hash for a 64-bit integer.
bool canWin() const
Does the player to move have an immediately winning move?
int TMovesCounter
Type used to count remaining moves on a position.
bool play(int column)
Drop a stone of the player to move into column.
Player
Player identifiers used by the board's array view.
@ P_RED
Red stone (player who moves second).
@ P_YELLOW
Yellow stone (player who moves first).
Board copy() const
Return a deep copy of this board.
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.
bool hasWin() const
Did the player who just moved form a four-in-a-row?
static bool isValid(const TBoardArray &board)
Validate a board layout.
bool operator==(const Board &b) const
Compare two positions for equality (same stones, same side to move).
bool setBoard(const TBoardArray &board)
Replace the current position with the one described by board.
TBitBoard generateNonLosingMoves() const
Compute moves that do not lose immediately.
auto popCountBoard() const
Number of stones currently on the board.
Board mirror() const
Mirror the board around its central column.
std::string toString() const
Pretty-printed ASCII representation of the board.
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.
std::vector< int > legalMoves(bool nonLosing, bool orderMoves) const
Enumerate legal moves as column indices.
uint64_t hash() const
64-bit hash of the position derived from uid().
Board playMoveOnCopy(const int mv) const
Apply a move given by column index on a copy of this board.
static TBitBoard lsb(const TBitBoard x)
Isolate the least significant set bit of a bitboard.
TMovesCounter movesLeft() const
Number of plies remaining until the board is full.
static std::pair< Board, std::vector< int > > randomBoard(const int nPly, const bool forbidDirectWin=true)
Generate a random reachable position with a fixed number of plies.
std::array< std::array< int32_t, N_ROWS >, N_COLUMNS > TBoardArray
Column-major 2D representation of the board (board[col][row]).
RawState rawState() const noexcept
Snapshot the internal state.
std::vector< Board > allPositions(const int upToNPly, bool exactlyN) const
Enumerate every reachable position from this board within a depth.
uint64_t uid() const
Unique 64-bit identifier of the position.
std::array< std::array< int32_t, N_COLUMNS >, N_ROWS > TBoardArrayT
Row-major 2D representation of the board (board[row][col]).
int toHuffman() const
Encode the position as a compact Huffman-like integer.
static constexpr int COLUMN_BIT_OFFSET
Bit-offset between two columns inside the 64-bit board representation.
TBoardArray toArray() const
Convert the bitboard representation into a column-major array.
Fixed-capacity priority queue tailored for Connect-4 move ordering.
Top-level namespace for the BitBully Connect-4 engine.
constexpr int CHAR_BIT
Number of bits per byte (defined locally if not provided by the platform).
Plain-data snapshot of the board's internal state.
TBitBoard all_tokens
Bitboard of all occupied cells.
TBitBoard active_tokens
Bitboard of cells held by the side to move.
TMovesCounter moves_left
Plies left until the board is full.