diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml new file mode 100644 index 0000000..b4eb127 --- /dev/null +++ b/.github/workflows/doxygen.yml @@ -0,0 +1,32 @@ +name: Doxygen docs + +on: + push: + branches: [ main ] + +jobs: + build-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y doxygen graphviz + + - name: Build docs + working-directory: ${{ github.workspace }} + run: | + if [ ! -f Doxyfile ]; then echo "Doxyfile not found in repo root"; exit 1; fi + doxygen Doxyfile + + - name: Upload docs artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: doxygen-html + path: doc/html + retention-days: 7 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83af1a3..5525b77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,10 +18,10 @@ jobs: commit_sha: ${{ steps.auto-commit.outputs.commit_hash }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with: ref: ${{ github.head_ref }} - + persist-credentials: false - name: Install clang-format run: sudo apt-get update && sudo apt-get install -y clang-format @@ -31,7 +31,7 @@ jobs: - name: Commit and push changes id: auto-commit if: github.actor != 'github-actions[bot]' && github.event.pull_request.head.repo.full_name == github.repository - uses: stefanzweifel/git-auto-commit-action@v7 + uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 build: needs: format @@ -52,7 +52,9 @@ jobs: error_mode: ASSERT steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + with: + persist-credentials: false - name: Set build dir id: vars diff --git a/.gitignore b/.gitignore index 27f2772..fe2d04a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ !.gitattributes !.github/** !.github/ +!Doxyfile +!doc/ # Ignore build/editor junk build/ out/ diff --git a/CMakeLists.txt b/CMakeLists.txt index d2c8b5d..f417367 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,11 @@ elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") endif() target_compile_definitions(chesslib PUBLIC _CHESSLIB_ERROR_MODE_${CHESSLIB_ERROR_MODE}) +target_compile_definitions(chesslib INTERFACE + "$<$:NDEBUG>" + "$<$:NDEBUG>" + "$<$:NDEBUG>" +) # --- Enable CTest integration --- include(CTest) diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..fe00865 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,20 @@ +# Doxyfile 1.9.1 +PROJECT_NAME = "chesslib" +OUTPUT_DIRECTORY = doc +INPUT = . +RECURSIVE = YES +EXTRACT_ALL = YES +EXTRACT_PRIVATE = YES +GENERATE_HTML = YES +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_OUTPUT = xml +QUIET = YES +GENERATE_TREEVIEW = YES +INLINE_SOURCES = YES +STRIP_FROM_PATH = "$(PWD)" +FILE_PATTERNS = *.h *.hpp *.cpp +EXCLUDE = build +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_AS_ERROR = YES \ No newline at end of file diff --git a/attacks.cpp b/attacks.cpp index a7abe64..032b882 100644 --- a/attacks.cpp +++ b/attacks.cpp @@ -93,7 +93,13 @@ static constexpr Bitboard rank_mask(Square sq) { return attacks::MASK_RANK[rank_ /// @brief File mask for a square. static constexpr Bitboard file_mask(Square sq) { return attacks::MASK_FILE[file_of(sq)]; } -/// @brief Rook attacks via hyperbola quintessence. +/** + * @brief Computes all squares a rook can attack from a given position. + * + * @param sq The rook's square. + * @param occ Board occupancy. + * @return Bitboard of attacked squares. + */ static constexpr Bitboard _HyperbolaRookAttacks(Square sq, Bitboard occ) { Bitboard slider = 1ULL << sq; Bitboard r_mask = rank_mask(sq); @@ -102,6 +108,115 @@ static constexpr Bitboard _HyperbolaRookAttacks(Square sq, Bitboard occ) { } } // namespace chess::_chess namespace chess::attacks { + +// Precompute rays for each square and each of 8 directions. +const std::array, 8> RAYS = []() { + std::array, 8> r{}; + for (int dir = 0; dir < 8; ++dir) { + for (Square sq = SQ_A1; sq < SQ_NONE; ++sq) { + Bitboard cur = 1ULL << sq; + Bitboard accum = 0ULL; + while (true) { + switch (dir) { + case RD_NORTH: + cur = cur << 8; + break; + case RD_SOUTH: + cur = cur >> 8; + break; + case RD_EAST: + cur = (cur & ~MASK_FILE[FILE_H]) << 1; + break; + case RD_WEST: + cur = (cur & ~MASK_FILE[FILE_A]) >> 1; + break; + case RD_NE: + cur = (cur & ~MASK_FILE[FILE_H]) << 9; + break; + case RD_NW: + cur = (cur & ~MASK_FILE[FILE_A]) << 7; + break; + case RD_SE: + cur = (cur & ~MASK_FILE[FILE_H]) >> 7; + break; + case RD_SW: + cur = (cur & ~MASK_FILE[FILE_A]) >> 9; + break; + } + if (!cur) + break; + accum |= cur; + } + r[dir][sq] = accum; + } + } + return r; +}(); + +#ifdef __BMI2__ +/// @brief Software fallback for the PEXT instruction. +/// @details Used during constant evaluation when BMI2 is unavailable. +/// @param val The value to compress. +/// @param mask The bit mask. +/** + * @brief Extracts bits from a value according to a mask and compacts them. + * + * For each set bit position in `mask`, extracts the corresponding bit from `val` + * and places it into the result at consecutive positions, starting from bit 0. + * + * @param val The value to extract bits from. + * @param mask A mask indicating which bit positions in `val` to extract. + * @return A compacted bitboard containing only the extracted bits. + */ +constexpr uint64_t software_pext_u64(uint64_t val, uint64_t mask) { + uint64_t result = 0; + uint64_t bit_position = 0; + + for (uint64_t bit = 1; bit != 0; bit <<= 1) { + if (mask & bit) { + if (val & bit) { + result |= 1ULL << bit_position; + } + ++bit_position; + } + } + return result; +} + +/// @brief Magic structure for PEXT-based magic bitboards (BMI2 path). +struct Magic { + Bitboard mask; ///< Relevant occupancy mask. + int index; ///< Starting index into the attack table. + /** + * @brief Extracts relevant occupancy bits for magic bitboard indexing. + * + * @param b Occupancy bitboard. + * @return Compressed index into the magic attack table. + */ + constexpr Bitboard operator()(Bitboard b) const { + if (is_constant_evaluated()) { + return software_pext_u64(b, mask); + } else { + return _pext_u64(b, mask); + } + } +}; +#else +/// @brief Magic structure for classical (multiply-and-shift) magic bitboards. +struct Magic { + Bitboard mask; ///< Relevant occupancy mask. + Bitboard magic; ///< Magic multiplier. + size_t index; ///< Starting index into the attack table. + Bitboard shift; ///< Right-shift amount. + /** + * @brief Converts an occupancy pattern to an attack table index. + * + * @return Index for accessing the precomputed attack bitboard. + */ + constexpr Bitboard operator()(Bitboard b) const { return (((b & mask)) * magic) >> shift; } +}; +#endif + #ifndef GENERATE_AT_RUNTIME #define _POSSIBLY_CONSTEXPR constexpr #else @@ -153,6 +268,12 @@ _POSSIBLY_CONSTEXPR std::array BishopMagics = { /// @tparam IsBishop true for bishop, false for rook. /// @return Pair of (magic table, attack table). template +/** + * @brief Generates magic bitboard tables for fast attack computation. + * + * @return A pair containing the Magic entry table (64 entries, one per square) and the corresponding precomputed attack + * bitboards table. + */ _POSSIBLY_CONSTEXPR std::pair, std::array> generate_magic_table() { std::array table{}; std::array attacks{}; @@ -165,7 +286,6 @@ _POSSIBLY_CONSTEXPR std::pair, std::array(sq), 0) & ~edges; int bits = popcount(mask); - int shift = 64 - bits; Bitboard magic = 0; if constexpr (IsBishop) magic = BishopMagics[sq]; @@ -176,7 +296,7 @@ _POSSIBLY_CONSTEXPR std::pair, std::array, std::array RookTable = rookData.first; _POSSIBLY_CONSTEXPR std::array RookAttacks = rookData.second; -/// @brief Look up bishop attacks from the precomputed magic table. +/** + * @brief Returns the attack bitboard for a bishop on the given square. + * + * @param sq The square where the bishop is located. + * @param occupied The occupied squares that block the bishop's attack paths. + * @return Bitboard with bits set for each square the bishop attacks. + */ [[nodiscard]] Bitboard bishop(Square sq, Bitboard occupied) { - return BishopAttacks[BishopTable[(int)sq].index + BishopTable[(int)sq](occupied)]; + const auto &entry = BishopTable[(int)sq]; + return BishopAttacks[entry.index + entry(occupied)]; } -/// @brief Look up rook attacks from the precomputed magic table. +/** + * @brief Look up rook attacks from the precomputed magic table. + * @param sq The square where the rook is located. + * @param occupied A bitboard representing occupied squares. + * @return A bitboard of squares the rook can attack. + */ [[nodiscard]] Bitboard rook(Square sq, Bitboard occupied) { - return RookAttacks[RookTable[(int)sq].index + RookTable[(int)sq](occupied)]; + const auto &entry = RookTable[(int)sq]; + return RookAttacks[entry.index + entry(occupied)]; } } // namespace chess::attacks namespace chess::movegen { diff --git a/attacks.h b/attacks.h index 8028ca3..6a2fa3f 100644 --- a/attacks.h +++ b/attacks.h @@ -114,51 +114,6 @@ constexpr Bitboard MASK_FILE[8] = { 0x101010101010101, 0x202020202020202, 0x404040404040404, 0x808080808080808, 0x1010101010101010, 0x2020202020202020, 0x4040404040404040, 0x8080808080808080, }; - -#ifdef __BMI2__ -/// @brief Software fallback for the PEXT instruction. -/// @details Used during constant evaluation when BMI2 is unavailable. -/// @param val The value to compress. -/// @param mask The bit mask. -/// @return Compressed bits. -constexpr uint64_t software_pext_u64(uint64_t val, uint64_t mask) { - uint64_t result = 0; - uint64_t bit_position = 0; - - for (uint64_t bit = 1; bit != 0; bit <<= 1) { - if (mask & bit) { - if (val & bit) { - result |= 1ULL << bit_position; - } - ++bit_position; - } - } - return result; -} - -/// @brief Magic structure for PEXT-based magic bitboards (BMI2 path). -struct Magic { - Bitboard mask; ///< Relevant occupancy mask. - int index; ///< Starting index into the attack table. - constexpr Bitboard operator()(Bitboard b) const { - if (is_constant_evaluated()) { - return software_pext_u64(b, mask); - } else { - return _pext_u64(b, mask); - } - } -}; -#else -/// @brief Magic structure for classical (multiply-and-shift) magic bitboards. -struct Magic { - Bitboard mask; ///< Relevant occupancy mask. - Bitboard magic; ///< Magic multiplier. - size_t index; ///< Starting index into the attack table. - Bitboard shift; ///< Right-shift amount. - constexpr Bitboard operator()(Bitboard b) const { return (((b & mask)) * magic) >> shift; } -}; -#endif - } // namespace chess::attacks namespace chess::attacks { @@ -302,7 +257,13 @@ template [[nodiscard]] constexpr Bitboard pawn(const Bitboard pawns) { /// @param sq Square. /// @param occupied Occupancy bitboard. /// @return Bitboard of squares attacked. -template [[nodiscard]] inline Bitboard slider(Square sq, Bitboard occupied) { +template /** + * Computes attack squares for a slider piece. + * @param sq Square the piece occupies. + * @param occupied Squares currently occupied on the board. + * @return Bitboard of squares attacked by the piece. + */ +[[nodiscard]] inline Bitboard slider(Square sq, Bitboard occupied) { static_assert(pt == PieceType::BISHOP || pt == PieceType::ROOK || pt == PieceType::QUEEN, "PieceType must be a slider!"); if constexpr (pt == PieceType::BISHOP) @@ -313,4 +274,10 @@ template [[nodiscard]] inline Bitboard slider(Square sq, Bitboard return queen(sq, occupied); } +// Ray direction indices for precomputed ray bitboards +enum RayDir : int { RD_NORTH = 0, RD_SOUTH = 1, RD_EAST = 2, RD_WEST = 3, RD_NE = 4, RD_NW = 5, RD_SE = 6, RD_SW = 7 }; + +/// @brief Precomputed rays from each square in 8 directions. +extern const std::array, 8> RAYS; + } // namespace chess::attacks diff --git a/bitboard.h b/bitboard.h index 7a6002b..9609a6b 100644 --- a/bitboard.h +++ b/bitboard.h @@ -45,6 +45,8 @@ constexpr int popcount_constexpr(Bitboard x) noexcept { /// @param x Input bitboard. /// @return Index of the lowest set bit (0-based). constexpr int lsb_constexpr(Bitboard x) noexcept { + if (x == 0) + return 0; int pos = 0; while ((x & 1) == 0) { x >>= 1; @@ -57,6 +59,8 @@ constexpr int lsb_constexpr(Bitboard x) noexcept { /// @param x Input bitboard. /// @return Index of the highest set bit (0-based). constexpr int msb_constexpr(Bitboard x) noexcept { + if (x == 0) + return 0; int pos = 63; Bitboard mask = 1ULL << 63; while ((x & mask) == 0) { @@ -69,10 +73,7 @@ constexpr int msb_constexpr(Bitboard x) noexcept { /// @brief Population count (uses hardware POPCNT when available). /// @param x Input bitboard. /// @return Number of set bits. -#if defined(__GNUG__) || defined(__clang__) -[[gnu::const]] -#endif -inline constexpr int popcount(Bitboard x) noexcept { +NO_SIDE_EFFECTS FORCEINLINE FLATTEN constexpr int popcount(Bitboard x) noexcept { #if defined(__GNUG__) || defined(__clang__) if (!is_constant_evaluated()) return __builtin_popcountll(x); @@ -86,10 +87,7 @@ inline constexpr int popcount(Bitboard x) noexcept { /// @brief Least-significant bit index (uses hardware BSF when available). /// @param x Input bitboard (must be non-zero). /// @return Index of the lowest set bit. -#if defined(__GNUG__) || defined(__clang__) -[[gnu::const]] -#endif -inline constexpr int lsb(Bitboard x) noexcept { +NO_SIDE_EFFECTS FORCEINLINE FLATTEN constexpr int lsb(Bitboard x) noexcept { #if defined(__GNUG__) || defined(__clang__) if (!is_constant_evaluated()) return __builtin_ctzll(x); @@ -106,10 +104,10 @@ inline constexpr int lsb(Bitboard x) noexcept { /// @brief Most-significant bit index (uses hardware BSR when available). /// @param x Input bitboard (must be non-zero). /// @return Index of the highest set bit. -#if defined(__GNUG__) || defined(__clang__) -[[gnu::const]] -#endif -inline constexpr int msb(Bitboard x) noexcept { +NO_SIDE_EFFECTS FORCEINLINE FLATTEN constexpr int msb(Bitboard x) noexcept { + ASSUME(x != 0); + if (x == 0) + return 0; #if defined(__GNUG__) || defined(__clang__) if (!is_constant_evaluated()) return 63 - __builtin_clzll(x); @@ -126,20 +124,23 @@ inline constexpr int msb(Bitboard x) noexcept { /// @brief Extract and pop the least-significant bit (destructive). /// @param b Bitboard reference; modified in place. /// @return Index of the lowest set bit before removal. -inline int pop_lsb(Bitboard &b) noexcept { +FORCEINLINE FLATTEN constexpr int pop_lsb(Bitboard &b) noexcept { int c = lsb(b); + if (!is_constant_evaluated()) { #ifndef __BMI2__ - b &= b - 1; + b &= b - 1; #else - b = _blsr_u64(b); + b = _blsr_u64(b); #endif + } else + b &= b - 1; return c; } /// @brief Extract and pop the most-significant bit (destructive). /// @param b Bitboard reference; modified in place. /// @return Index of the highest set bit before removal. -inline int pop_msb(Bitboard &b) noexcept { +FORCEINLINE FLATTEN constexpr int pop_msb(Bitboard &b) noexcept { int c = msb(b); b &= ~(1ULL << c); return c; diff --git a/fwd_decl.h b/fwd_decl.h index 153346d..0379e4d 100644 --- a/fwd_decl.h +++ b/fwd_decl.h @@ -21,7 +21,13 @@ #include /// @file fwd_decl.h -/// @brief Forward declarations for all major chess types. +/** + * @brief Default trait for type detection. + */ + +/** + * @brief Specialization that detects piece-enum types by matching types that expose PIECE_NB. + */ namespace chess { @@ -36,6 +42,7 @@ enum PieceType : std::int8_t; /// @brief Trait to detect piece-enum types (PolyglotPiece, EnginePiece, ContiguousMappingPiece). template struct is_piece_enum : std::false_type {}; +/// @brief Specialisation: detects types that expose PIECE_NB (piece-enum types). template struct is_piece_enum> : std::true_type {}; /// @enum CastlingRights diff --git a/movegen.cpp b/movegen.cpp index db06f73..c64adca 100644 --- a/movegen.cpp +++ b/movegen.cpp @@ -33,7 +33,7 @@ namespace chess { namespace _chess { -#if defined(USE_AVX512ICL) +#if defined(__AVX512F__) && defined(__AVX512VNNI__) && defined(__AVX512VBMI2__) // clang-format off const __m512i AllSquares = _mm512_set_epi8( @@ -42,7 +42,15 @@ const __m512i AllSquares = _mm512_set_epi8( 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0); // clang-format on -template inline Move *splat_pawn_moves(Move *moveList, Bitboard to_bb) { +template /** + * @brief Packs pawn destination squares into move objects. + * + * @tparam offset Direction offset from destination to origin squares. + * @param moveList Output buffer where move objects are written. + * @param to_bb Bitboard of destination squares (at most 8 bits set). + * @return Pointer advanced by popcount(to_bb). + */ +inline Move *splat_pawn_moves(Move *moveList, Bitboard to_bb) { assert(popcount(to_bb) <= 8); // <= 8 pawns per side const __m128i toSquares = _mm_cvtepi8_epi16(_mm512_castsi512_si128(_mm512_maskz_compress_epi8(to_bb, AllSquares))); @@ -53,12 +61,20 @@ template inline Move *splat_pawn_moves(Move *moveList, Bitboa return moveList + popcount(to_bb); } +/** + * @brief Convert a destination bitboard into move objects from a fixed source square. + * + * @param moveList Output array where moves are stored. + * @param from Source square for all moves. + * @param to_bb Bitboard of destination squares (popcount must not exceed 32). + * @return Pointer advanced by the number of moves written. + */ inline Move *splat_moves(Move *moveList, Square from, Bitboard to_bb) { assert(popcount(to_bb) <= 32); // Q can attack up to 27 squares const __m512i fromVec = _mm512_set1_epi16(Move(from, SQUARE_ZERO).raw()); const __m512i toSquares = _mm512_cvtepi8_epi16(_mm512_castsi512_si256(_mm512_maskz_compress_epi8(to_bb, AllSquares))); - const __m512i moves = _mm512_or_si512(fromVec, _mm512_slli_epi16(toSquares, Move::ToSqShift)); + const __m512i moves = _mm512_or_si512(fromVec, _mm512_slli_epi16(toSquares, 0)); _mm512_storeu_si512(moveList, moves); return moveList + popcount(to_bb); @@ -80,6 +96,17 @@ template struct alignas(64) SplatTable { constexpr SplatTable<> SPLAT_TABLE{}; template constexpr SplatTable SPLAT_PAWN_TABLE{}; // AVX-512 (32 lanes of uint16_t) +/** + * @brief Compresses and stores selected moves from a vectorized batch. + * + * Stores only the moves from the vector at positions indicated by the mask, + * compressing them into the output buffer and advancing the output pointer. + * + * @param moveList Output buffer for move storage. + * @param mask Bitmask indicating which vector lanes contain valid moves. + * @param vector 512-bit vector of move data in 16-bit lanes. + * @return Pointer to the next available position in the output buffer. + */ static inline Move *write_moves(Move *moveList, uint32_t mask, __m512i vector) { // Avoid _mm512_mask_compressstoreu_epi16() as it's 256 uOps on Zen4 _mm512_storeu_si512(reinterpret_cast<__m512i *>(moveList), _mm512_maskz_compress_epi16(mask, vector)); @@ -127,20 +154,80 @@ inline Move *splat_moves(Move *moveList, Square from, Bitboard to_bb) { #endif } // namespace _chess -// Count-only dispatch helpers — splat_moves/splat_pawn_moves when storing is needed, no-op when counting. -template inline void record_moves(ListT &list, Square from, Bitboard targets) { +template /** + * @brief Appends moves from a source square to destination squares, or counts them. + * + * For Movelist, generates and stores all moves efficiently. For CountOnlyList, + * only increments the count. For other list types, appends placeholder moves. + * + * @param from Source square for all moves. + * @param targets Bitboard of destination squares. + */ +inline void record_moves(ListT &list, Square from, Bitboard targets) { if constexpr (std::is_same_v) { _chess::splat_moves(list.data() + list.size_, from, targets); + list.size_ += popcount(targets); + } else if constexpr (std::is_same_v) { + list.size_ += popcount(targets); + } else { + while (targets) { + list.push_back(Move::none()); + pop_lsb(targets); + } + } +} + +template /** + * @brief Records pawn promotion moves for each destination square. + * + * For each destination in `dests`, records four promotion moves: knight, bishop, rook, and queen. + * The source square is computed by subtracting the `offset` template parameter from the destination. + * + * @param list Move list to accumulate promotions, or a count-only list. + * @param dests Bitboard of destination squares where pawns promote. + */ +inline void record_promotions(ListT &list, Bitboard dests) { + if constexpr (std::is_same_v) { + while (dests) { + Square to = static_cast(pop_lsb(dests)); + Square from = static_cast(to - offset); + list[list.size_] = Move::make(from, to, KNIGHT); + list[list.size_ + 1] = Move::make(from, to, BISHOP); + list[list.size_ + 2] = Move::make(from, to, ROOK); + list[list.size_ + 3] = Move::make(from, to, QUEEN); + list.size_ += 4; + } + } else if constexpr (std::is_same_v) { + list.size_ += 4 * popcount(dests); + } else { + UNREACHABLE(); } } -template inline void record_pawn_moves(ListT &list, Bitboard targets) { +template /** + * @brief Records or counts pawn moves from destination squares. + * + * For `Movelist`, stores pawn moves with origin squares derived from the + * destinations via the compile-time `offset` parameter. For `CountOnlyList`, + * increments the move counter without storing moves. + */ +inline void record_pawn_moves(ListT &list, Bitboard targets) { if constexpr (std::is_same_v) { _chess::splat_pawn_moves(list.data() + list.size_, targets); + list.size_ += popcount(targets); + } else if constexpr (std::is_same_v) { + // CountOnlyList doesn't store moves; just increase the counter. + list.size_ += popcount(targets); + } else { + UNREACHABLE(); } } } // namespace chess namespace chess { -template [[gnu::hot]] void movegen::genEP(const _Position &pos, ListT &mv) { +template /** + * @brief Generates all legal en passant captures for the moving side. + */ +HOTFUNC void movegen::genEP(const _Position &pos, ListT &mv) { const Square king_sq = pos.king_sq(c); const Square ep_sq = pos.ep_square(); @@ -153,7 +240,6 @@ template [[gnu::hot]] void movegen::genEP( const Square ep_pawn_sq = ep_sq - pawn_push(c); const Bitboard ep_mask = (1ULL << ep_pawn_sq) | (1ULL << ep_sq); - // ASSUME(popcount(candidates) <= 32); Bitboard occ_all = pos.occ(); while (candidates) { @@ -173,8 +259,12 @@ template [[gnu::hot]] void movegen::genEP( } } template -[[gnu::hot]] void -movegen::genPawnDoubleMoves(const _Position &pos, ListT &moves, Bitboard pin_mask, Bitboard check_mask) { +/** + * @brief Generates pawn double-step pushes from the starting rank. + * + * Respects pin constraints and check evasion requirements. + */ +HOTFUNC void movegen::genPawnDoubleMoves(const _Position &pos, ListT &moves, Bitboard pin_mask, Bitboard check_mask) { constexpr Bitboard RANK_2 = (c == WHITE) ? attacks::MASK_RANK[1] : attacks::MASK_RANK[6]; constexpr Direction UP = pawn_push(c); @@ -198,10 +288,23 @@ movegen::genPawnDoubleMoves(const _Position &pos, ListT &moves, Bitboar Bitboard destinations = (step2_unpinned | step2_pinned) & check_mask; record_pawn_moves<2 * UP>(moves, destinations); - moves.size_ += popcount(destinations); } template -[[gnu::hot]] void movegen::genPawnSingleMoves( +/** + * @brief Generates pawn single-step pushes and captures, including promotions, while respecting pin and check constraints. + * + * Generates all legal single-square pawn moves in the forward direction and diagonal captures. + * Handles promotions when pawns reach the promotion rank. Respects piece pinning constraints + * (rook and bishop pins) and check evasion mask filtering. For `capturesOnly` mode, omits + * non-capturing forward moves. + * + * @param pos The position to generate moves from. + * @param moves The move list to append generated moves to. + * @param _rook_pin Bitmask of pawns pinned along rook lines (vertical/horizontal). + * @param _bishop_pin Bitmask of pawns pinned along bishop lines (diagonal). + * @param _check_mask Bitmask of squares that moves must target to be legal (check evasion). + */ +HOTFUNC void movegen::genPawnSingleMoves( const _Position &pos, ListT &moves, Bitboard _rook_pin, Bitboard _bishop_pin, Bitboard _check_mask) { constexpr auto UP = relative_direction(c, NORTH); constexpr auto UP_LEFT = relative_direction(c, NORTH_WEST); @@ -240,36 +343,11 @@ template Bitboard promo_push = single_push & RANK_PROMO; if constexpr (!capturesOnly) { - while (promo_push) { - Square to = static_cast(pop_lsb(promo_push)); - Square from = static_cast(to - UP); - moves[moves.size_] = Move::make(from, to, KNIGHT); - moves[moves.size_ + 1] = Move::make(from, to, BISHOP); - moves[moves.size_ + 2] = Move::make(from, to, ROOK); - moves[moves.size_ + 3] = Move::make(from, to, QUEEN); - moves.size_ += 4; - } - } - - while (promo_left) { - Square to = static_cast(pop_lsb(promo_left)); - Square from = static_cast(to - UP_LEFT); // correct - moves[moves.size_] = Move::make(from, to, KNIGHT); - moves[moves.size_ + 1] = Move::make(from, to, BISHOP); - moves[moves.size_ + 2] = Move::make(from, to, ROOK); - moves[moves.size_ + 3] = Move::make(from, to, QUEEN); - moves.size_ += 4; + record_promotions(moves, promo_push); } - while (promo_right) { - Square to = static_cast(pop_lsb(promo_right)); - Square from = static_cast(to - UP_RIGHT); // correct - moves[moves.size_] = Move::make(from, to, KNIGHT); - moves[moves.size_ + 1] = Move::make(from, to, BISHOP); - moves[moves.size_ + 2] = Move::make(from, to, ROOK); - moves[moves.size_ + 3] = Move::make(from, to, QUEEN); - moves.size_ += 4; - } + record_promotions(moves, promo_left); + record_promotions(moves, promo_right); } single_push &= ~RANK_PROMO; @@ -277,16 +355,21 @@ template r_pawns &= ~RANK_PROMO; if constexpr (!capturesOnly) { record_pawn_moves(moves, single_push); - moves.size_ += popcount(single_push); } record_pawn_moves(moves, l_pawns); - moves.size_ += popcount(l_pawns); record_pawn_moves(moves, r_pawns); - moves.size_ += popcount(r_pawns); } template -[[gnu::hot]] void -movegen::genKnightMoves(const _Position &pos, ListT &list, Bitboard _pin_mask, Bitboard _check_mask) { +/** + * @brief Generates legal knight moves for the given color. + * + * Generates all knight moves subject to pin and check constraints. + * If `capturesOnly` is true, restricts to capture moves only. + * + * @param _pin_mask Bitboard of pinned pieces; pinned knights are excluded. + * @param _check_mask Bitboard indicating squares that resolve checks. + */ +HOTFUNC void movegen::genKnightMoves(const _Position &pos, ListT &list, Bitboard _pin_mask, Bitboard _check_mask) { Bitboard knights = pos.template pieces() & ~_pin_mask; while (knights) { Square x = static_cast(pop_lsb(knights)); @@ -295,11 +378,21 @@ movegen::genKnightMoves(const _Position &pos, ListT &list, Bitboard _pi if constexpr (capturesOnly) moves &= pos.occ(~c); record_moves(list, x, moves); - list.size_ += popcount(moves); } } template -[[gnu::hot]] void movegen::genKingMoves(const _Position &pos, ListT &out, Bitboard _pin_mask) { +/** + * @brief Generates legal king moves and castling. + * + * Computes all legal king destination squares by excluding occupied friendly squares and squares attacked by enemy pieces. + * When `capturesOnly` is true, only captures are generated. Otherwise, also generates castling moves if the king is not in + * check, the castling path is unobstructed, and all squares the king passes through are not under attack. + * + * @param pos The position. + * @param out The move list to record moves into. + * @param _pin_mask Bitboard of pinned pieces; used to filter illegal castling moves. + */ +HOTFUNC void movegen::genKingMoves(const _Position &pos, ListT &out, Bitboard _pin_mask) { constexpr Color them = ~c; const Square kingSq = pos.king_sq(c); const Bitboard myOcc = pos.occ(c); @@ -307,8 +400,7 @@ template if constexpr (capturesOnly) { Bitboard targets = attacks::king(kingSq) & occ_opp; - if (!targets) { - out.size_ += 0; + if (UNLIKELY(!targets)) { return; } } @@ -338,9 +430,8 @@ template if constexpr (capturesOnly) moves &= occ_opp; record_moves(out, kingSq, moves); - out.size_ += popcount(moves); if constexpr (!capturesOnly) { - if (pos.checkers()) + if (UNLIKELY(pos.checkers())) return; Bitboard occupancy = pos.occ(); @@ -364,7 +455,18 @@ template } } template -[[gnu::hot]] void movegen::genSlidingMoves( +/** + * @brief Generates legal moves for sliding pieces (bishop, rook, or queen). + * + * Computes all valid destination squares for each sliding piece on the board, applying pin + * constraints and check restrictions. Pieces pinned along rook lines are confined to those lines; + * pieces pinned along bishop lines are confined to those diagonals. + * + * @param _rook_pin Bitboard of squares pinned along rook lines (vertical/horizontal). + * @param _bishop_pin Bitboard of squares pinned along bishop lines (diagonals). + * @param _check_mask Bitboard of legal destination squares when in check. + */ +HOTFUNC void movegen::genSlidingMoves( const _Position &pos, ListT &moves, Bitboard _rook_pin, Bitboard _bishop_pin, Bitboard _check_mask) { static_assert(pt == BISHOP || pt == ROOK || pt == QUEEN, "Sliding pieces only."); Bitboard sliders = pos.template pieces(); @@ -387,21 +489,25 @@ template Bitboard filtered_pin = pin_mask & filter_list; Bitboard targets; + // Choose attack function without std::function to avoid indirect call overhead. + decltype(&attacks::rook) func; if (rook_hit) { - targets = attacks::rook(from, occ_all) & filtered_pin; + func = attacks::rook; } else if (bishop_hit) { - targets = attacks::bishop(from, occ_all) & filtered_pin; - } else if constexpr (pt == BISHOP) { - targets = attacks::bishop(from, occ_all) & filtered_pin; - } else if constexpr (pt == ROOK) { - targets = attacks::rook(from, occ_all) & filtered_pin; + func = attacks::bishop; } else { - targets = attacks::queen(from, occ_all) & filtered_pin; + if constexpr (pt == BISHOP) { + func = attacks::bishop; + } else if constexpr (pt == ROOK) { + func = attacks::rook; + } else { + func = attacks::queen; + } } + targets = func(from, occ_all) & filtered_pin; if constexpr (capturesOnly) targets &= occ_opp; record_moves(moves, from, targets); - moves.size_ += popcount(targets); } } #define INSTANTIATE(PieceC, ListT) \ diff --git a/moves_io.cpp b/moves_io.cpp index f31e9b7..5cca8fa 100644 --- a/moves_io.cpp +++ b/moves_io.cpp @@ -171,7 +171,30 @@ template Move uciToMove(const _Position &pos, std return move; } /// @brief Parse a SAN (Standard Algebraic Notation) move string. -template Move parseSan(const _Position &pos, std::string_view raw_san, bool remove_illegals) { +template /** + * @brief Parses a SAN move string into a Move, validating against legal moves. + * + * Handles castling + notations (`O-O`, `0-0`, `O-O-O`, `0-0-0`), check/checkmate suffixes, + * promotions (`c8=Q` or `c8Q`), + and disambiguates moves using piece letters, file/rank hints, + * or full source squares (LAN + notation). + * + * @param pos The position context for validating legality and resolving ambiguity. + * + @param raw_san The SAN move string to parse (e.g., "e4", "Nf3", "exd5", "e8=Q+"). + * @param + remove_illegals If `true`, progressively removes trailing characters from the input + * until a legal + move is found or the string is empty; if `false`, + * parses the full string and + returns `Move::none()` on any error. + * @return The parsed `Move`, or `Move::none()` if parsing fails + or no legal move matches. + */ +Move parseSan(const _Position &pos, std::string_view raw_san, bool remove_illegals) { auto do_parse = [&](std::string_view input_san) -> Move { if (input_san.empty()) return Move::none(); @@ -328,7 +351,7 @@ template Move parseSan(const _Position &pos, std: // 9) Build candidate filter and scan legal moves Move matched = Move::null(); bool found = false; - Bitboard to_mask = (1ULL << to_square) & ~pos.occ(pos.side_to_move()); // mask excluding own pieces on destination + // Bitboard to_mask = (1ULL << to_square) & ~pos.occ(pos.side_to_move()); // mask excluding own pieces on destination // If pawn and no disambiguation file, restrict pawns to dest file (avoid ambiguous pawn non-file forms) // This matches python-chess behavior described earlier. diff --git a/moves_io.h b/moves_io.h index 5ae6e67..630ff90 100644 --- a/moves_io.h +++ b/moves_io.h @@ -25,7 +25,17 @@ #include /// @file moves_io.h -/// @brief UCI and SAN move conversion functions. +/** + * Parse a SAN string into a Move for the given position. + * @tparam T Piece enum type. + * @tparam P Position tag. + * @param pos The position. + * @param san SAN string (e.g. "Nf3", "O-O"). + * @param remove_illegals If true, return Move::NO_MOVE instead of throwing. + * @return The parsed Move. + * @throws IllegalMoveException if the SAN string represents an illegal move and remove_illegals is false. + * @throws AmbiguousMoveException if the SAN string is ambiguous. + */ namespace chess::uci { @@ -43,17 +53,30 @@ std::string squareToString(Square sq); /// @brief Exception thrown when a SAN string represents an illegal move. class IllegalMoveException : public std::exception { public: + /// @brief Construct with an explanatory message. IllegalMoveException(const std::string &message) : message_(message) {} + /** + * Provides the exception's message. + * @returns A C-string containing the exception message. + */ const char *what() const noexcept override { return message_.c_str(); } private: std::string message_; }; -/// @brief Exception thrown when a SAN string is ambiguous. +/** + * @brief Create an exception for an ambiguous SAN move. + * @param message The exception message. + */ class AmbiguousMoveException : public std::exception { public: + /// @brief Construct ambiguous-move exception with message. AmbiguousMoveException(const std::string &message) : message_(message) {} + /** + * Provides the exception's message. + * @returns A C-string containing the exception message. + */ const char *what() const noexcept override { return message_.c_str(); } private: diff --git a/position.cpp b/position.cpp index d02eb53..1e16dd3 100644 --- a/position.cpp +++ b/position.cpp @@ -250,10 +250,25 @@ template template void _Position +/** + * @brief Loads a position from a FEN string. + * + * Parses and applies a FEN string to reset the position. The FEN must contain + * piece placement, side to move, castling rights, and en-passant target fields; + * halfmove and fullmove counters are optional and default to 0 and 1 respectively. + * + * @param str The FEN string to parse. + * @param chess960 Whether to parse Chess960 castling notation. + * @param mode The FEN parsing mode, controlling which castling notations are accepted. + * @return `true` if parsing succeeds, `false` otherwise. + */ bool _Position::setFEN(const std::string &str, bool chess960, FENParsingMode mode) { history.clear(); rep_hashes_.clear(); history.push_back(HistoryEntry()); + std::fill(std::begin(state().pieces), std::end(state().pieces), 0ULL); + state().occ[0] = state().occ[1] = 0; + state().kings[0] = state().kings[1] = SQ_NONE; _chess960 = chess960; std::fill(std::begin(pieces_list), std::end(pieces_list), PieceC::NO_PIECE); castling_meta_[WHITE] = {}; @@ -261,10 +276,29 @@ bool _Position::setFEN(const std::string &str, bool chess960, FENPars std::istringstream ss(str); std::string board_fen, active_color, castling, enpassant; int halfmove = 0, fullmove = 1; - if (!(ss >> board_fen >> active_color >> castling >> enpassant >> halfmove >> fullmove)) { - INVALID_ARG_IF(true, std::runtime_error("Invalid FEN format")); + if (!(ss >> board_fen >> active_color >> castling >> enpassant)) { + INVALID_ARG_IF(true, std::runtime_error("Invalid FEN format (lack of required fields)")); return false; } + // Halfmove clock and fullmove number (required per FEN spec) + { + int temp_halfmove = 0; + int temp_fullmove = 0; + + if (ss >> temp_halfmove) { + if (ss >> temp_fullmove) { + halfmove = temp_halfmove; + fullmove = temp_fullmove; + } else { + INVALID_ARG_IF(true, std::runtime_error("Invalid FEN format (has halfmove but lacks fullmove)")); + return false; + } + } else { + INVALID_ARG_IF(true, std::runtime_error("Invalid FEN format (expected halfmove clock)")); + return false; + } + } + std::string extra; if (ss >> extra) { INVALID_ARG_IF(true, std::runtime_error("Trailing FEN data")); @@ -936,7 +970,12 @@ template Square _Position::_valid_ep_sq return ep_square(); } /// @brief Check if a given color has insufficient mating material. -template bool _Position::is_insufficient_material() const { +template /** + * @brief Determines whether the position has insufficient material to achieve checkmate. + * + * @return `true` if the position has insufficient mating material, `false` otherwise. + */ +bool _Position::is_insufficient_material() const { const auto count = popcount(occ()); if (count <= 2) @@ -960,9 +999,6 @@ template bool _Position::is_insufficien Bitboard wb = white_bishops; Bitboard bb = black_bishops; - int wb_cnt = popcount(wb); - int bb_cnt = popcount(bb); - Bitboard bishops = wb | bb; Bitboard knights = pieces(KNIGHT, WHITE) | pieces(KNIGHT, BLACK); Bitboard rooks = pieces(ROOK, WHITE) | pieces(ROOK, BLACK); diff --git a/position.h b/position.h index aeb6ab7..68695f5 100644 --- a/position.h +++ b/position.h @@ -28,13 +28,58 @@ #include /// @file position.h -/// @brief Chess position representation, move execution, and game-state queries. - namespace chess { +namespace attacks { -/// @struct HistoryEntry -/// @brief Saved position state for undo operations. -/// @tparam Piece Piece-enum type. +/// @brief Scan for attacks along a ray and identify checkers and pins. +/// @tparam RayDir Direction index of the ray to scan. +/// @tparam FirstIncreases Whether the ray direction corresponds to increasing square indices (e.g. north/east) or decreasing +/// (south/west). +/// @param ksq King's square. +/// @param occ_masked Occupancy bitboard masked to the ray (i.e. only squares on the ray are considered occupied). +/// @param slider_mask Bitboard of potential slider attackers (rooks for orthogonal rays, bishops for diagonal rays). +/// @param occ_us Occupancy bitboard of the attacking side (used to detect pinned pieces). +/// @param checkers Output bitboard to accumulate discovered checkers. +/// @param pin_bb Output bitboard to accumulate discovered pinned pieces (bits set for squares of pinned pieces, not the +/// attackers). +/// @details This function uses the precomputed ray bitboards to efficiently find the first occupied square along the ray and +/// determine if it's a checker or a pinned piece. If the first occupied square is an enemy slider, it's a checker. If it's a +/// friendly piece, we check if there's another enemy slider behind it on the same ray, which would indicate that the friendly +/// piece is pinned. +/// @note This function assumes that the occupancy bitboards have already been masked to only include pieces on the relevant +/// ray, which allows it to use simple bit operations to find the first blocker and potential attackers without needing to +/// iterate over squares. +/// @return nothing (modified via refs) +template +inline void +scan_attacks_ray(Square ksq, Bitboard occ_masked, Bitboard slider_mask, Bitboard occ_us, Bitboard &checkers, Bitboard &pin_bb) { + const auto &ray = attacks::RAYS[RayDir][ksq]; + Bitboard occ_on_ray = ray & occ_masked; + if (!occ_on_ray) + return; + + int first_sq = FirstIncreases ? lsb(occ_on_ray) : msb(occ_on_ray); + Bitboard first_bb = 1ULL << first_sq; + if (first_bb & slider_mask) { + checkers |= first_bb; + } else if (first_bb & occ_us) { + Bitboard after = FirstIncreases ? occ_on_ray & ~((first_bb) | (first_bb - 1)) : occ_on_ray & (first_bb - 1); + if (after) { + int attacker_sq = FirstIncreases ? lsb(after) : msb(after); + if ((1ULL << attacker_sq) & slider_mask) + pin_bb |= movegen::between(ksq, static_cast(attacker_sq)); + } + } +} +} // namespace attacks +/** + * Stores complete and incremental position state for supporting undo operations. + * + * Captures all necessary board state including piece placement, per-color occupancy, + * game rules (castling, en-passant, move counters), and incremental undo information + * (changed squares and pieces). Cached attack masks are saved to avoid recomputation + * on undo. + */ template struct alignas(64) HistoryEntry { Bitboard pieces[7]{}; ///< Bitboards per piece type. Bitboard occ[COLOR_NB]{}; ///< Occupancy per colour. @@ -43,25 +88,32 @@ template struct alignas(64) HistoryEntry { Key hash = 0; ///< Zobrist hash. uint8_t halfMoveClock = 0; ///< Half-move clock for 50/75-move rule. uint16_t fullMoveNumber = 1; ///< Full-move number (starts at 1). + /// @brief Whether en-passant presence was included in the Zobrist hash. bool epIncluded = false; + /// @brief Repetition counter originating from this saved state. int8_t repetition = 0; ///< Repetition counter from this position. + /// @brief Number of plies since last null move. uint8_t pliesFromNull = 0; + /// @brief En-passant target square. Square enPassant = SQ_NONE; ///< En-passant target square. + /// @brief King's square for each colour in this saved state. Square kings[COLOR_NB] = { SQ_NONE, SQ_NONE }; + /// @brief Castling rights bitmask at this saved state. CastlingRights castlingRights; ///< Castling rights bitmask. + /// @brief Incremental squares changed by the move (for undo). Square incr_sqs[4] = { SQ_NONE, SQ_NONE, SQ_NONE, SQ_NONE }; + /// @brief Incremental piece values for undo (parallel to incr_sqs). Piece incr_pc[4] = { Piece::NO_PIECE, Piece::NO_PIECE, Piece::NO_PIECE, Piece::NO_PIECE }; /// @name Cached attack data (saved to avoid recomputation on undo) /// @{ - Bitboard saved_rook_pin{}; - Bitboard saved_bishop_pin{}; - Bitboard saved_checkers{}; - Bitboard saved_check_mask{}; + Bitboard saved_rook_pin{}; ///< Saved rook pin mask. + Bitboard saved_bishop_pin{}; ///< Saved bishop pin mask. + Bitboard saved_checkers{}; ///< Saved checkers bitboard. + Bitboard saved_check_mask{}; ///< Saved check mask. /// @} }; /// @enum CheckType -/// @brief Classification of check on a move. enum class CheckType { NO_CHECK, DIRECT_CHECK, DISCOVERY_CHECK }; /// @enum FENParsingMode @@ -69,7 +121,14 @@ enum class CheckType { NO_CHECK, DIRECT_CHECK, DISCOVERY_CHECK }; enum FENParsingMode { MODE_XFEN, MODE_SMK, MODE_AUTO }; /// @enum MoveGenType -/// @brief Flags controlling which pieces and move types are generated. +/** + * @brief Bitmask flags controlling which pieces and move types to generate. + * + * Compile-time and runtime flags for filtering legal move generation. + * Piece flags (PAWN through KING) select which piece types to include. + * Move type flags (CAPTURE, QUIET) select move categories. + * PIECE_MASK combines all piece flags; ALL combines all flags. + */ enum class MoveGenType : uint16_t { NONE = 0, @@ -88,20 +147,38 @@ enum class MoveGenType : uint16_t { ALL = PIECE_MASK | CAPTURE | QUIET }; +/** + * @brief Bitwise AND operation for MoveGenType flags. + * @param a First operand. + * @param b Second operand. + * @return Result of bitwise AND. + */ template constexpr MoveGenType operator&(MoveGenType a, MoveGenType b) { using U = std::underlying_type_t; return static_cast(static_cast(a) & static_cast(b)); } +/** + * @brief Bitwise OR operation for MoveGenType flags. + * @param a First operand. + * @param b Second operand. + * @return Result of bitwise OR. + */ template constexpr MoveGenType operator|(MoveGenType a, MoveGenType b) { using U = std::underlying_type_t; return static_cast(static_cast(a) | static_cast(b)); } -/// @class _Position -/// @brief Templated chess position. -/// @tparam PieceC Piece-enum type (EnginePiece, PolyglotPiece, or ContiguousMappingPiece). -/// @tparam (unused) Position tag parameter. +/** + * @class _Position + * @brief Chess position representation and move execution system. + * @tparam PieceC Piece-enum type (EnginePiece, PolyglotPiece, or ContiguousMappingPiece). + * + * Maintains board state including piece placement, Zobrist hashing, move history for undo, + * castling rights, en-passant state, and cached attack/pin/check masks. Supports both + * standard chess and Chess960 variants. + */ + template ::value>> class _Position { private: std::vector> history; @@ -129,19 +206,18 @@ template castling_paths{}; + Square king_start = SQ_NONE; ///< King's start square for castling. + Square rook_start_ks = SQ_NONE; ///< Rook start for kingside castling. + Square rook_start_qs = SQ_NONE; ///< Rook start for queenside castling. + std::array castling_paths{}; ///< Castling path bitboards [ks, qs]. } castling_meta_[2]{}; public: + /// @brief Standard starting FEN for classical chess. static inline constexpr auto START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + /// @brief Default FEN stub used for Chess960 tests (special castling format HA/ha). static inline constexpr auto START_CHESS960_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w HAha - 0 1"; - /// @brief Generate legal moves filtered by type. - /// @tparam type Bitmask of MoveGenType flags. - /// @tparam c Colour to move. /// @brief Generate legal moves filtered by type. /// @tparam type Bitmask of MoveGenType flags. /// @tparam c Colour to move. @@ -213,9 +289,10 @@ template void doMove(const Move &move); + /// @brief Snake-case alias for doMove(). template void do_move(const Move &move) { doMove(move); } - /// @brief Undo the last move. + /// @brief Undo the last move. Returns saved HistoryEntry when RetAll=true. /// @tparam RetAll If true, return the popped HistoryEntry. /// @return The saved state if RetAll, otherwise void. template inline auto undoMove() -> std::conditional_t, void> { @@ -238,11 +315,15 @@ template inline auto undo_move() -> std::conditional_t, void> { return undoMove(); } - /// @brief Execute a null move (switch sides without moving). + /** + * Execute a null move, switching the side to move without placing any piece. + * Resets repetition and null-move tracking, and refreshes cached attack data. + */ inline void doNullMove() { history.push_back(state()); state().saved_rook_pin = _rook_pin; @@ -263,6 +344,7 @@ template [[nodiscard]] inline Square square(Color c) const { return static_cast(lsb(pieces(c))); } + /** + * Retrieve the square occupied by the king for the given color. + * @param c The color. + * @returns The square of the king for color `c`. + */ [[nodiscard]] inline Square kingSq(Color c) const { return state().kings[c]; } + /** + * Returns the king's square for the given color. + */ [[nodiscard]] inline Square king_sq(Color c) const { return kingSq(c); } - /// @brief Current checkers. [[nodiscard]] inline Bitboard checkers() const { return _checkers; } /// @brief Combined pin mask. @@ -513,23 +633,47 @@ template (mv.from_sq()) == PAWN; } + /** + * Queries the piece at a square. + * @return The piece type at the given square. + */ [[nodiscard]] inline PieceC piece_at(Square sq) const { return piece_on(sq); } /// @brief Export position to FEN. [[nodiscard]] std::string fen(bool xfen = true) const; + /** + * @brief Full move number, starting from 1. + */ + + /** + * @brief Full move number, starting from 1. + */ + + /** + * @brief Half-move clock for the 50/75-move rule. + */ [[nodiscard]] inline uint16_t fullmoveNumber() const { return state().fullMoveNumber; } + /// @brief Full move number (snake_case wrapper). [[nodiscard]] inline uint16_t fullmove_number() const { return state().fullMoveNumber; } + /// @brief Half-move clock for 50/75-move rule. [[nodiscard]] inline uint8_t rule50_count() const { return state().halfMoveClock; } /// @brief Castling rights for a specific colour. [[nodiscard]] inline CastlingRights castlingRights(Color c) const { return state().castlingRights & (c == WHITE ? WHITE_CASTLING : BLACK_CASTLING); } + /** + * Castling rights bitmask for both colors. + */ [[nodiscard]] inline CastlingRights castlingRights() const { return state().castlingRights; } /// @brief Whether a move is a castling move. @@ -538,7 +682,17 @@ template inline T at(Square sq) const { if constexpr (std::is_same_v) return piece_of(piece_on(sq)); @@ -551,8 +705,10 @@ template inline int count() const { return popcount(pieces(pt)); } + /// @brief Count pieces of compile-time piece type `pt` for colour `c`. template inline int count() const { return popcount(pieces()); } + /// @brief Count pieces of piece type `pt` for runtime colour `c`. template inline int count(Color c) const { return popcount(pieces(c)); } + /// @brief Count pieces of runtime piece type `pt` for colour `c`. inline int count(PieceType pt, Color c) const { return popcount(pieces(pt, c)); } /// @} @@ -593,8 +753,13 @@ template = ply; } + /// @brief Repetition counter for current position. inline int repetition_count() const { return state().repetition; } /// @brief Whether the position is a draw (50-move or repetition). @@ -614,16 +779,26 @@ template = n; } + /// @brief Whether the position uses Chess960 castling rules. inline bool chess960() const { return _chess960; } + /// @brief Whether the seventy-five move rule applies. inline bool is_seventyfive_moves() const { return _is_halfmoves(150); } + /// @brief Whether the fifty-move rule applies. inline bool is_fifty_moves() const { return _is_halfmoves(100); } + /// @brief Whether fivefold repetition has occurred. inline bool is_fivefold_repetition() const { return is_repetition(5); } /// @brief Whether a square is attacked by a colour (with optional custom occupancy). + [[deprecated("Future migration to isAttacked due to incompatible API")]] inline bool is_attacked_by(Color color, Square sq, Bitboard occupied = 0) const { - Bitboard occ_bb = occupied ? occupied : this->occ(); - return attackers_mask(color, sq, occ_bb) != 0; + Bitboard occ_bb = occupied ? occupied : occ(); + return isAttacked(sq, color, occ_bb); } /// @brief Whether the previous move left the opponent in check. @@ -683,18 +858,43 @@ template = 100 half-moves). + /** + * @brief Checks if a draw is available under the 50-move rule. + */ [[nodiscard]] inline bool isHalfMoveDraw() const noexcept { return rule50_count() >= 100; } + /// @brief Whether the 50-move rule draw applies (snake_case wrapper). [[nodiscard]] inline bool is_half_move_draw() const noexcept { return isHalfMoveDraw(); } - /// @brief Get the castling path bitboard for a colour and side. + /** + * Returns the castling path bitboard for the specified color and side. + * + * @param c The color to query castling information for. + * @param isKingSide `true` for kingside castling, `false` for queenside. + * @returns A bitboard representing the squares involved in the castling path for the given color and side. + */ [[nodiscard]] inline Bitboard getCastlingPath(Color c, bool isKingSide) const { return castling_meta_[c].castling_paths[isKingSide]; } + /** + * Returns the castling path bitboard for the specified color and side. + * @returns Bitboard of squares along the castling path. + */ [[nodiscard]] inline Bitboard get_castling_path(Color c, bool isKingSide) const { return getCastlingPath(c, isKingSide); } + /** + * Retrieve the castling metadata for a color. + * @return The castling metadata for the specified color. + */ [[nodiscard]] inline auto getCastlingMetadata(Color c) const { return castling_meta_[c]; } + /** + * Castling metadata for a color. + * @param c Color. + */ [[nodiscard]] inline auto get_castling_metadata(Color c) const { return getCastlingMetadata(c); } private: @@ -726,8 +926,12 @@ template (~c) | pieces(~c); - while (bLike) { - Square s = static_cast(pop_lsb(bLike)); - int fd = (ksq & 7) - (s & 7); - int rd = (ksq >> 3) - (s >> 3); - if (fd != rd && fd != -rd) - continue; - Bitboard possible = movegen::between(ksq, s); - Bitboard blockers = (possible & ~(1ULL << s)) & occ_all; - int n = popcount(blockers); - if (n == 0) - checkers |= 1ULL << s; - else if (n == 1 && (blockers & occ_us)) - bishop_pin |= possible; - } - - // Rook-like: iterate all enemy rooks/queens - Bitboard rLike = pieces(~c) | pieces(~c); - while (rLike) { - Square s = static_cast(pop_lsb(rLike)); - if ((ksq ^ s) & 7 && (ksq ^ s) & 56) - continue; - Bitboard possible = movegen::between(ksq, s); - Bitboard blockers = (possible & ~(1ULL << s)) & occ_all; - int n = popcount(blockers); - if (n == 0) - checkers |= 1ULL << s; - else if (n == 1 && (blockers & occ_us)) - rook_pin |= possible; - } + // Directional scan from the king: check each ray for first/second occupied squares. + // This avoids iterating over all enemy sliders and calling movegen::between() per piece. + const Bitboard diag_sliders = pieces(~c) | pieces(~c); + const Bitboard ortho_sliders = pieces(~c) | pieces(~c); + + // Use precomputed rays and direction-aware nearest-blocker extraction. + const Bitboard occ_masked = occ_all; + // Diagonals: NE,NW,SE,SW + attacks::scan_attacks_ray(ksq, occ_masked, diag_sliders, occ_us, checkers, bishop_pin); + attacks::scan_attacks_ray(ksq, occ_masked, diag_sliders, occ_us, checkers, bishop_pin); + attacks::scan_attacks_ray(ksq, occ_masked, diag_sliders, occ_us, checkers, bishop_pin); + attacks::scan_attacks_ray(ksq, occ_masked, diag_sliders, occ_us, checkers, bishop_pin); + + // Orthogonals: N,S,E,W + attacks::scan_attacks_ray(ksq, occ_masked, ortho_sliders, occ_us, checkers, rook_pin); + attacks::scan_attacks_ray(ksq, occ_masked, ortho_sliders, occ_us, checkers, rook_pin); + attacks::scan_attacks_ray(ksq, occ_masked, ortho_sliders, occ_us, checkers, rook_pin); + attacks::scan_attacks_ray(ksq, occ_masked, ortho_sliders, occ_us, checkers, rook_pin); // Pawn and knight checkers (precomputed tables, no magic lookups) checkers |= (attacks::pawn(c, ksq) & pieces(~c)); @@ -775,18 +966,13 @@ template (lsb(_checkers)); _check_mask = 1ULL << sq | movegen::between(ksq, sq); - break; - } - default: + } else { _check_mask = 0ULL; - break; } } @@ -799,7 +985,14 @@ template castlingFlags = { { NO_CASTLING, "NO_CASTLING" }, @@ -111,10 +117,6 @@ std::ostream &operator<<(std::ostream &os, const CastlingRights cr) { return os << castlingFlags.at(cr); } -static std::string str_toupper(std::string s) { - std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::toupper(c); }); - return s; -} /// @brief Print a Square as algebraic notation (e.g. "e2"). std::ostream &operator<<(std::ostream &os, const Square sq) { return os << uci::squareToString(sq); } diff --git a/tests.cpp b/tests.cpp index 097d947..df658d0 100644 --- a/tests.cpp +++ b/tests.cpp @@ -187,10 +187,10 @@ static_assert(make_sq(RANK_8, FILE_A) == SQ_A8, "incorrect indexing"); static_assert(make_sq(RANK_1, FILE_H) == SQ_H1, "incorrect indexing"); static_assert(file_of(SQ_H7) == FILE_H, "incorrect indexing"); static_assert(rank_of(SQ_C3) == RANK_3, "incorrect indexing"); -#ifndef NDEBUG -#define IS_RELEASE 0 -#else +#if defined(NDEBUG) #define IS_RELEASE 1 +#else +#define IS_RELEASE 0 #endif struct perft_t { int depth; @@ -542,8 +542,8 @@ TEST_CASE("Perfts" * doctest::timeout(36000)) { std::vector> tests = { { "Q1Q2QQQ/3Q4/1Q4Q1/4Q3/2Q4Q/Q4Q1Q/pp1Q3Q/kBQQ1KQ1 w - - 0 1", 1, 240 }, { "Q1Q2QQQ/3Q4/1Q4Q1/4Q3/2Q4Q/Q4Q1Q/pp1Q3Q/kBQQ1KQ1 w - - 0 1", 2, 0 }, - { "Q1Q2QQQ/3Q4/1Q4Q1/4Q3/2Q4Q/Q4Q1Q/pp1Q3Q/kBQQ1KQ1 w - - 0 1", 2, 0 }, { "QQQQQQQK/Q6Q/Q6Q/Q6Q/Q6Q/Q6Q/BR5Q/kBQQQQQQ w - - 0 1", 1, 271 }, + { "QQQQQQQK/Q6Q/Q6Q/Q6Q/Q6Q/Q6Q/BR5Q/kBQQQQQQ w - - 0 1", 2, 0 }, { "5k2/8/8/8/3K4/8/8/8 w - - 0 1", 1, 8 }, { "5k2/8/8/8/3K4/8/8/8 w - - 0 1", 3, 310 }, { "5k2/8/8/8/3K4/8/8/8 w - - 0 1", 6, 95366 }, diff --git a/types.h b/types.h index 975ea6b..68c5f9d 100644 --- a/types.h +++ b/types.h @@ -29,7 +29,48 @@ /// @file types.h /// @brief Core chess type definitions: squares, pieces, colours, move encoding, and ValueList. - +#if defined(__clang__) || defined(__GNUC__) +#define LIKELY(k) __builtin_expect(!!(k), 1) +#define UNLIKELY(k) __builtin_expect(!!(k), 1) +#elif __has_cpp_attribute(likely) +#define LIKELY(k) [[likely(k)]] +#define UNLIKELY(k) [[unlikely(k)]] +#else +#define LIKELY(k) k +#define UNLIKELY(k) k +#endif +#if defined(__GNUC__) || defined(__clang__) +/// @def HOT +/// @brief Marks a function as hot (frequently called). +#define HOTFUNC __attribute__((hot)) +/// @def COLD +/// @brief Marks a function as cold (rarely called). +#define COLDFUNC __attribute__((cold)) +/// @def FLATTEN +/// @brief Make subcalls forceinlined +#define FLATTEN __attribute__((flatten)) +/// @def FORCEINLINE +/// @brief Make callers inline this function +#define FORCEINLINE __attribute__((always_inline)) +/// @def NO_SIDE_EFFECTS +/// @brief Marks a function has no side effects +#define NO_SIDE_EFFECTS __attribute__((const)) +#else +#define HOTFUNC +#define COLDFUNC +#define FLATTEN +#if defined(_MSC_VER) +/// @def FORCEINLINE +/// @brief Make callers inline this function +#define FORCEINLINE __forceinline +/// @def NO_SIDE_EFFECTS +/// @brief Make a function has no side effects +#define NO_SIDE_EFFECTS __declspec(noalias) +#else +#define FORCEINLINE +#define NO_SIDE_EFFECTS +#endif +#endif /// @def UNREACHABLE() /// @brief Marks code paths that should never be reached. #if defined(_MSC_VER) @@ -460,7 +501,9 @@ enum MoveType : uint16_t { NORMAL, PROMOTION = 1 << 14, EN_PASSANT = 2 << 14, CA class Move { public: Move() = default; + /// @brief Construct from raw 16-bit encoding. constexpr Move(std::uint16_t d) : data(d) {} + /// @brief Construct from origin and destination squares. constexpr Move(Square from, Square to) : data((static_cast(from) << 6) | static_cast(to)) {} /// @brief Construct a move with an explicit type and optional promotion piece. @@ -469,51 +512,80 @@ class Move { (static_cast(from) << 6) | static_cast(to)); } + /** + * @brief Origin square of the move. + */ constexpr Square from_sq() const { assert(is_ok()); return Square((data >> 6) & 0x3F); } + /** + * @brief Destination square of the move. + */ constexpr Square to_sq() const { assert(is_ok()); return Square(data & 0x3F); } + /// @brief Alias for from_sq(). constexpr Square from() const { return from_sq(); } + /// @brief Alias for to_sq(). constexpr Square to() const { return to_sq(); } /// @brief Get the packed from|to field (lower 12 bits). constexpr int from_to() const { return data & 0xFFF; } - /// @brief Get the move type. + /// @brief Get the move type (normal/promotion/en-passant/castling). constexpr MoveType type_of() const { return MoveType(data & (3 << 14)); } + /// @brief True if move is neither none() nor null(). constexpr bool is_ok() const { return none().data != data && null().data != data; } - /// @brief Get the promotion piece type. - constexpr PieceType promotion_type() const { return PieceType(((data >> 12) & 3) + KNIGHT); } + /** + * Determines the piece type this move promotes to. + * @returns The promotion piece type encoded in this move, in the range [KNIGHT, QUEEN]. + */ + PieceType promotion_type() const { return PieceType(((data >> 12) & 3) + KNIGHT); } + /** + * Creates a null move sentinel used to pass without changing the board state. + * @returns A sentinel move with encoding 65. + */ static constexpr Move null() { return Move(65); } + /// @brief No-move sentinel (represents absence of a move). static constexpr Move none() { return Move(0); } + /** + * Checks if two moves are equal. + * @returns `true` if the moves are equal, `false` otherwise. + */ constexpr bool operator==(const Move &m) const { return data == m.data; } + /// @brief Inequality comparison of moves. constexpr bool operator!=(const Move &m) const { return data != m.data; } + /// @brief Boolean conversion: true for valid move. constexpr explicit operator bool() const { return data != 0; } /// @brief Get the raw 16-bit encoding. constexpr std::uint16_t raw() const { return data; } /// @brief Hash functor for use in unordered containers. + /// @brief Hash functor for Move suitable for unordered containers. struct MoveHash { std::size_t operator()(const Move &m) const { return m.data; } }; + /// @brief Return the UCI string representation of the move (e.g., "e2e4"). std::string uci() const; /// @name Convenience constants /// @{ - static constexpr std::uint16_t NO_MOVE = 0; - static constexpr std::uint16_t NULL_MOVE = 65; + static constexpr std::uint16_t NO_MOVE = 0; ///< Constant for no move. + static constexpr std::uint16_t NULL_MOVE = 65; ///< Constant for null move. + /// @brief Move type: normal. static constexpr MoveType NORMAL = MoveType::NORMAL; + /// @brief Move type: promotion. static constexpr MoveType PROMOTION = MoveType::PROMOTION; + /// @brief Move type: en-passant capture. static constexpr MoveType ENPASSANT = MoveType::EN_PASSANT; + /// @brief Move type: castling. static constexpr MoveType CASTLING = MoveType::CASTLING; /// @} @@ -521,56 +593,48 @@ class Move { std::uint16_t data; }; -/// @brief Trait: check that all types in a pack are the same. -template struct is_all_same { - static constexpr bool value = (std::is_same_v && ...); -}; -template constexpr auto is_all_same_v = is_all_same::value; - /// @class ValueList /// @brief Stack-allocated fixed-capacity vector. /// @tparam T Element type. /// @tparam MaxSize Maximum number of elements. -template class ValueList { +template +class ValueList { static_assert(MaxSize, "what are you doing with 0 items"); public: using size_type = std::size_t; ValueList() = default; - /// @brief Number of elements currently stored. inline size_type size() const { return size_; } - /// @brief Append an element. inline void push_back(const T &value) { assert(size_ < MaxSize); values_[size_++] = value; } - /// @brief Remove and return the last element. inline T pop() { assert(size_ > 0); return values_[--size_]; } - /// @brief Remove the last element without returning it. inline void pop_back() { assert(size_ > 0); size_--; } - - /// @brief Access the first element. inline T front() const { assert(size_ > 0); return values_[0]; } - /// @brief Indexed access. - inline T &operator[](int index) { - // intentionally placed - assert(0 <= index && index < MaxSize); - return values_[index]; - } + /// @brief Indexed access. UB if index >= MaxSize. + inline T &operator[](size_type index) { return values_[index]; } inline const T *begin() const { return values_; } inline T *data() { return values_; } @@ -589,17 +653,32 @@ using Movelist = ValueList; /// @brief Counting-only move list — same interface as Movelist but discards move data. class CountOnlyList { public: + public: + /// @brief Size type for CountOnlyList. using size_type = std::size_t; + /// @brief Default constructor. CountOnlyList() = default; + /// @brief Current size (number of moves counted). inline size_type size() const { return size_; } + /// @brief Increment the move count (discards move payload). inline void push_back(const Move &) { ++size_; } + /// @brief Index access — returns a dummy Move for compatibility. inline Move &operator[](size_type) { thread_local static Move dummy(0); return dummy; } + /// @brief No backing array for CountOnlyList; data() returns nullptr. inline Move *data() { return nullptr; } + /** + * @brief Provides no iteration support for count-only move lists. + * @returns `nullptr`. + */ inline const Move *begin() const { return nullptr; } + /** + * Returns nullptr; CountOnlyList does not support iteration. + */ inline const Move *end() const { return nullptr; } + /// @brief Internal size counter. size_type size_ = 0; }; @@ -613,7 +692,7 @@ constexpr int square_distance(Square a, Square b) { /// @param sv e.g. "e4", "a1". /// @return Square, or SQ_NONE on parse failure. constexpr Square parse_square(std::string_view sv) { - if (sv.size() < 2) + if (sv.size() != 2) return SQ_NONE; char f = sv[0]; char r = sv[1]; @@ -628,9 +707,10 @@ constexpr Square parse_square(std::string_view sv) { constexpr PieceType parse_pt(unsigned char c) { const char a[] = "pnbrqk"; int p = -1; + // tolower if (c >= 'A' && c <= 'Z') c += 32; - for (size_t i = 0; i < sizeof(a); i++) { + for (int i = 0; i < static_cast(sizeof(a)); i++) { if (c == a[i]) p = i; }