diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a2ceca..83af1a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,32 +42,15 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] build_type: [Debug, Release] - c_compiler: [gcc, clang, cl] - - include: - # Windows - - os: windows-latest - c_compiler: cl - cpp_compiler: cl - - os: windows-latest - c_compiler: clang - cpp_compiler: clang++ - - os: windows-latest - c_compiler: gcc - cpp_compiler: g++ - - # Linux - - os: ubuntu-latest - c_compiler: gcc - cpp_compiler: g++ - - os: ubuntu-latest - c_compiler: clang - cpp_compiler: clang++ + cpp_compiler: [g++, clang++, cl] + error_mode: [ASSERT, THROW, NONE] exclude: - os: ubuntu-latest - c_compiler: cl - + cpp_compiler: cl + - build_type: Release + error_mode: ASSERT + steps: - uses: actions/checkout@v4 @@ -87,10 +70,10 @@ jobs: fi fi cmake -B "${{ steps.vars.outputs.dir }}" \ - -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} \ -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} \ -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ -DSANITIZERS=${SANITIZERS} \ + -DCHESSLIB_ERROR_MODE=${{ matrix.error_mode }} \ -DDART_TESTING_TIMEOUT=0 \ -S "${{ github.workspace }}" diff --git a/.github/workflows/try_compile.yml b/.github/workflows/try_compile.yml deleted file mode 100644 index be6f456..0000000 --- a/.github/workflows/try_compile.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Compilation - -on: - push: - branches-ignore: main -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: true - matrix: - os: [ubuntu-latest, windows-latest] - build_type: [Debug] - c_compiler: [gcc, clang, cl] - - include: - # Windows - - os: windows-latest - c_compiler: cl - cpp_compiler: cl - - os: windows-latest - c_compiler: clang - cpp_compiler: clang++ - - os: windows-latest - c_compiler: gcc - cpp_compiler: g++ - - # Linux - - os: ubuntu-latest - c_compiler: gcc - cpp_compiler: g++ - - os: ubuntu-latest - c_compiler: clang - cpp_compiler: clang++ - - exclude: - - os: ubuntu-latest - c_compiler: cl - - steps: - - uses: actions/checkout@v4 - - - name: Set build dir - id: vars - shell: bash - run: echo "dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" - - - name: Configure CMake - shell: bash - run: | - cmake -B "${{ steps.vars.outputs.dir }}" \ - -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} \ - -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} \ - -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ - -S "${{ github.workspace }}" - - - name: Build - run: cmake --build ${{ steps.vars.outputs.dir }} --config ${{ matrix.build_type }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f9eebb..d2c8b5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,45 +21,43 @@ project(chesslib LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# --- Core Library --- +set(SOURCES + position.cpp + attacks.cpp "zobrist.cpp" + "moves_io.cpp" "printers.cpp" "movegen.cpp") +add_library(chesslib STATIC ${SOURCES}) +target_include_directories(chesslib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(chesslib PRIVATE GENERATE_AT_RUNTIME) +set(CHESSLIB_ERROR_MODE "THROW" CACHE STRING "ASSERT|THROW|NONE") +set_property(CACHE CHESSLIB_ERROR_MODE PROPERTY STRINGS ASSERT THROW NONE) + +if(NOT CHESSLIB_ERROR_MODE MATCHES "^(ASSERT|THROW|NONE)$") + message(FATAL_ERROR "Invalid ERROR_MODE: ${CHESSLIB_ERROR_MODE}") +endif() # --- Compiler tuning --- if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") # clang-cl: forward GCC/Clang style constexpr flags via /clang: - add_compile_options( - /clang:-fconstexpr-steps=2000000000 - /clang:-fconstexpr-depth=1024 + target_compile_options(chesslib PRIVATE /clang:-march=native /clang:-mtune=native - /clang:-ftemplate-backtrace-limit=0 ) else() # native clang++ on *nix or Windows - add_compile_options( - -fconstexpr-steps=2000000000 - -fconstexpr-depth=1024 + target_compile_options(chesslib PRIVATE -march=native -mtune=native - -ftemplate-backtrace-limit=0 -static ) endif() elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - add_compile_options(-fconstexpr-ops-limit=2000000000 -fconstexpr-depth=1024 -march=native -mtune=native -ftemplate-backtrace-limit=0 -static) + target_compile_options(chesslib PRIVATE -march=native -mtune=native) elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") set(ARCH_FLAG "/arch:AVX2" CACHE STRING "MSVC architecture flag (/arch:SSE2, /arch:AVX, /arch:AVX2, /arch:AVX512)") - add_compile_options(/constexpr:steps2000000000 /constexpr:depth1024 ${ARCH_FLAG}) + target_compile_options(chesslib PRIVATE ${ARCH_FLAG}) endif() -add_compile_definitions(GENERATE_AT_RUNTIME) -if(CMAKE_BUILD_TYPE MATCHES "Debug") - add_compile_definitions(_DEBUG) -endif() -# --- Core Library --- -set(SOURCES - position.cpp - attacks.cpp "zobrist.cpp" - "moves_io.cpp" "printers.cpp" "movegen.cpp") -add_library(chesslib STATIC ${SOURCES}) -target_include_directories(chesslib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(chesslib PUBLIC _CHESSLIB_ERROR_MODE_${CHESSLIB_ERROR_MODE}) # --- Enable CTest integration --- include(CTest) @@ -81,9 +79,9 @@ if(BUILD_TESTING) target_link_libraries(NonImportantTests PRIVATE chesslib doctest::doctest) target_link_libraries(test_chess960 PRIVATE chesslib doctest::doctest) - add_test(NAME test_normal COMMAND test_normal) - add_test(NAME test_api COMMAND NonImportantTests) - add_test(NAME test_chess960 COMMAND test_chess960) + add_test(NAME test_normal COMMAND test_normal --abort-after=1) + add_test(NAME test_api COMMAND NonImportantTests --abort-after=1) + add_test(NAME test_chess960 COMMAND test_chess960 --abort-after=1) if (UNIX AND CMAKE_BUILD_TYPE MATCHES "Debug") set(SANITIZERS "" CACHE STRING "sanitizers such as undefined,address") diff --git a/attacks.cpp b/attacks.cpp index ee5a025..a7abe64 100644 --- a/attacks.cpp +++ b/attacks.cpp @@ -16,12 +16,17 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ + +/// @file attacks.cpp +/// @brief Magic-bitboard generation, hyperbola-quintessence helpers, and between-square table. + #include "attacks.h" namespace chess::_chess { // [INTERNAL] -// Reverse bits horizontally in 64-bit integer +/// @brief Reverse bits horizontally in a 64-bit integer. +/// @details Used by the hyperbola quintessence algorithm. static constexpr Bitboard reverse(Bitboard b) { b = (b & 0x5555555555555555ULL) << 1 | ((b >> 1) & 0x5555555555555555ULL); b = (b & 0x3333333333333333ULL) << 2 | ((b >> 2) & 0x3333333333333333ULL); @@ -32,6 +37,11 @@ static constexpr Bitboard reverse(Bitboard b) { return b; } +/// @brief Hyperbola quintessence attack computation for a single line. +/// @param sliderBB Bitboard with the slider's square set. +/// @param occ Occupancy bitboard. +/// @param mask Line mask (rank, file, or diagonal). +/// @return Attacks along the masked line. static constexpr Bitboard hyp_quint(Bitboard sliderBB, Bitboard occ, Bitboard mask) { Bitboard occ_masked = occ & mask; Bitboard left = occ_masked - 2 * sliderBB; @@ -39,7 +49,7 @@ static constexpr Bitboard hyp_quint(Bitboard sliderBB, Bitboard occ, Bitboard ma return (left ^ reverse(right)) & mask; } -// For Bishop: Mask for diagonal and anti-diagonal +/// @brief Compute the diagonal mask through a square. static constexpr Bitboard diag_mask(Square sq) { int r = rank_of(sq); int f = file_of(sq); @@ -53,6 +63,7 @@ static constexpr Bitboard diag_mask(Square sq) { } return mask; } +/// @brief Compute the anti-diagonal mask through a square. static constexpr Bitboard antidiag_mask(Square sq) { int r = rank_of(sq); int f = file_of(sq); @@ -68,7 +79,7 @@ static constexpr Bitboard antidiag_mask(Square sq) { return mask; } -// Hyperbola Quintessence for Bishop +/// @brief Bishop attacks via hyperbola quintessence. static constexpr Bitboard _HyperbolaBishopAttacks(Square sq, Bitboard occ) { Bitboard slider = 1ULL << sq; Bitboard d_mask = diag_mask(sq); @@ -76,12 +87,13 @@ static constexpr Bitboard _HyperbolaBishopAttacks(Square sq, Bitboard occ) { return hyp_quint(slider, occ, d_mask) | hyp_quint(slider, occ, ad_mask); } -// For Rook: Rank and File Masks +/// @brief Rank mask for a square. static constexpr Bitboard rank_mask(Square sq) { return attacks::MASK_RANK[rank_of(sq)]; } +/// @brief File mask for a square. static constexpr Bitboard file_mask(Square sq) { return attacks::MASK_FILE[file_of(sq)]; } -// Hyperbola Quintessence for Rook +/// @brief Rook attacks via hyperbola quintessence. static constexpr Bitboard _HyperbolaRookAttacks(Square sq, Bitboard occ) { Bitboard slider = 1ULL << sq; Bitboard r_mask = rank_mask(sq); @@ -134,6 +146,12 @@ _POSSIBLY_CONSTEXPR std::array BishopMagics = { }; // clang-format on + +/// @brief Generate magic-bitboard lookup tables. +/// @tparam AttackFunc The hyperbola attack function to use. +/// @tparam TableSize Total number of attack entries. +/// @tparam IsBishop true for bishop, false for rook. +/// @return Pair of (magic table, attack table). template _POSSIBLY_CONSTEXPR std::pair, std::array> generate_magic_table() { std::array table{}; @@ -184,34 +202,27 @@ _POSSIBLY_CONSTEXPR std::pair, std::array(); _POSSIBLY_CONSTEXPR std::array RookTable = rookData.first; _POSSIBLY_CONSTEXPR std::array RookAttacks = rookData.second; -/** - * @brief Returns the bishop attacks for a given square - * @param sq - * @param occupied - * @return - */ + +/// @brief Look up bishop attacks from the precomputed magic table. [[nodiscard]] Bitboard bishop(Square sq, Bitboard occupied) { return BishopAttacks[BishopTable[(int)sq].index + BishopTable[(int)sq](occupied)]; } -/** - * @brief Returns the rook attacks for a given square - * @param sq - * @param occupied - * @return - */ +/// @brief Look up rook attacks from the precomputed magic table. [[nodiscard]] Bitboard rook(Square sq, Bitboard occupied) { return RookAttacks[RookTable[(int)sq].index + RookTable[(int)sq](occupied)]; } - } // namespace chess::attacks namespace chess::movegen { + +/// @brief Hyperbola attack for bishop or rook (used for between-table generation). inline static Bitboard att(PieceType pt, Square sq, Bitboard occ) { return (pt == BISHOP) ? chess::_chess::_HyperbolaBishopAttacks(sq, occ) : chess::_chess::_HyperbolaRookAttacks(sq, occ); } -inline static std::array, SQ_NONE + 1> generate_between() { - std::array, SQ_NONE + 1> squares_between_bb{}; +/// @brief Generate the between-square table at program startup. +inline static std::array, 64> generate_between() { + std::array, 64> squares_between_bb{}; for (int sq1 = 0; sq1 < 64; ++sq1) { for (PieceType pt : { BISHOP, ROOK }) { @@ -226,5 +237,5 @@ inline static std::array, SQ_NONE + 1> generat return squares_between_bb; } -std::array, SQ_NONE + 1> SQUARES_BETWEEN_BB = generate_between(); +std::array, 64> SQUARES_BETWEEN_BB = generate_between(); } // namespace chess::movegen diff --git a/attacks.h b/attacks.h index c3e1654..8028ca3 100644 --- a/attacks.h +++ b/attacks.h @@ -24,95 +24,103 @@ #include #include #include + +/// @file attacks.h +/// @brief Precomputed attack tables and magic-bitboard lookup functions. + namespace chess::attacks { -// clang-format off - // pre-calculated lookup table for pawn attacks - constexpr Bitboard PawnAttacks[2][64] = { - // white pawn attacks - { 0x200, 0x500, 0xa00, 0x1400, - 0x2800, 0x5000, 0xa000, 0x4000, - 0x20000, 0x50000, 0xa0000, 0x140000, - 0x280000, 0x500000, 0xa00000, 0x400000, - 0x2000000, 0x5000000, 0xa000000, 0x14000000, - 0x28000000, 0x50000000, 0xa0000000, 0x40000000, - 0x200000000, 0x500000000, 0xa00000000, 0x1400000000, - 0x2800000000, 0x5000000000, 0xa000000000, 0x4000000000, - 0x20000000000, 0x50000000000, 0xa0000000000, 0x140000000000, - 0x280000000000, 0x500000000000, 0xa00000000000, 0x400000000000, - 0x2000000000000, 0x5000000000000, 0xa000000000000, 0x14000000000000, - 0x28000000000000, 0x50000000000000, 0xa0000000000000, 0x40000000000000, - 0x200000000000000, 0x500000000000000, 0xa00000000000000, 0x1400000000000000, - 0x2800000000000000, 0x5000000000000000, 0xa000000000000000, 0x4000000000000000, - 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0 }, - // black pawn attacks - { 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, - 0x2, 0x5, 0xa, 0x14, - 0x28, 0x50, 0xa0, 0x40, - 0x200, 0x500, 0xa00, 0x1400, - 0x2800, 0x5000, 0xa000, 0x4000, - 0x20000, 0x50000, 0xa0000, 0x140000, - 0x280000, 0x500000, 0xa00000, 0x400000, - 0x2000000, 0x5000000, 0xa000000, 0x14000000, - 0x28000000, 0x50000000, 0xa0000000, 0x40000000, - 0x200000000, 0x500000000, 0xa00000000, 0x1400000000, - 0x2800000000, 0x5000000000, 0xa000000000, 0x4000000000, - 0x20000000000, 0x50000000000, 0xa0000000000, 0x140000000000, - 0x280000000000, 0x500000000000, 0xa00000000000, 0x400000000000, - 0x2000000000000, 0x5000000000000, 0xa000000000000, 0x14000000000000, - 0x28000000000000, 0x50000000000000, 0xa0000000000000, 0x40000000000000 - } - }; +/// @brief Precomputed pawn-attack bitboards. +/// @details Indexed as PawnAttacks[color][square]. +constexpr Bitboard PawnAttacks[2][64] = { + // clang-format off + // white pawn attacks + { 0x200, 0x500, 0xa00, 0x1400, + 0x2800, 0x5000, 0xa000, 0x4000, + 0x20000, 0x50000, 0xa0000, 0x140000, + 0x280000, 0x500000, 0xa00000, 0x400000, + 0x2000000, 0x5000000, 0xa000000, 0x14000000, + 0x28000000, 0x50000000, 0xa0000000, 0x40000000, + 0x200000000, 0x500000000, 0xa00000000, 0x1400000000, + 0x2800000000, 0x5000000000, 0xa000000000, 0x4000000000, + 0x20000000000, 0x50000000000, 0xa0000000000, 0x140000000000, + 0x280000000000, 0x500000000000, 0xa00000000000, 0x400000000000, + 0x2000000000000, 0x5000000000000, 0xa000000000000, 0x14000000000000, + 0x28000000000000, 0x50000000000000, 0xa0000000000000, 0x40000000000000, + 0x200000000000000, 0x500000000000000, 0xa00000000000000, 0x1400000000000000, + 0x2800000000000000, 0x5000000000000000, 0xa000000000000000, 0x4000000000000000, + 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0 }, + + // black pawn attacks + { 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, + 0x2, 0x5, 0xa, 0x14, + 0x28, 0x50, 0xa0, 0x40, + 0x200, 0x500, 0xa00, 0x1400, + 0x2800, 0x5000, 0xa000, 0x4000, + 0x20000, 0x50000, 0xa0000, 0x140000, + 0x280000, 0x500000, 0xa00000, 0x400000, + 0x2000000, 0x5000000, 0xa000000, 0x14000000, + 0x28000000, 0x50000000, 0xa0000000, 0x40000000, + 0x200000000, 0x500000000, 0xa00000000, 0x1400000000, + 0x2800000000, 0x5000000000, 0xa000000000, 0x4000000000, + 0x20000000000, 0x50000000000, 0xa0000000000, 0x140000000000, + 0x280000000000, 0x500000000000, 0xa00000000000, 0x400000000000, + 0x2000000000000, 0x5000000000000, 0xa000000000000, 0x14000000000000, + 0x28000000000000, 0x50000000000000, 0xa0000000000000, 0x40000000000000 + } + // clang-format on +}; - // pre-calculated lookup table for knight attacks - constexpr Bitboard KnightAttacks[64] = { - 0x0000000000020400, 0x0000000000050800, 0x00000000000A1100, 0x0000000000142200, - 0x0000000000284400, 0x0000000000508800, 0x0000000000A01000, 0x0000000000402000, - 0x0000000002040004, 0x0000000005080008, 0x000000000A110011, 0x0000000014220022, - 0x0000000028440044, 0x0000000050880088, 0x00000000A0100010, 0x0000000040200020, - 0x0000000204000402, 0x0000000508000805, 0x0000000A1100110A, 0x0000001422002214, - 0x0000002844004428, 0x0000005088008850, 0x000000A0100010A0, 0x0000004020002040, - 0x0000020400040200, 0x0000050800080500, 0x00000A1100110A00, 0x0000142200221400, - 0x0000284400442800, 0x0000508800885000, 0x0000A0100010A000, 0x0000402000204000, - 0x0002040004020000, 0x0005080008050000, 0x000A1100110A0000, 0x0014220022140000, - 0x0028440044280000, 0x0050880088500000, 0x00A0100010A00000, 0x0040200020400000, - 0x0204000402000000, 0x0508000805000000, 0x0A1100110A000000, 0x1422002214000000, - 0x2844004428000000, 0x5088008850000000, 0xA0100010A0000000, 0x4020002040000000, - 0x0400040200000000, 0x0800080500000000, 0x1100110A00000000, 0x2200221400000000, - 0x4400442800000000, 0x8800885000000000, 0x100010A000000000, 0x2000204000000000, - 0x0004020000000000, 0x0008050000000000, 0x00110A0000000000, 0x0022140000000000, - 0x0044280000000000, 0x0088500000000000, 0x0010A00000000000, 0x0020400000000000}; +/// @brief Precomputed knight-attack bitboards. +/// @details Indexed by square. +constexpr Bitboard KnightAttacks[64] = { + 0x0000000000020400, 0x0000000000050800, 0x00000000000A1100, 0x0000000000142200, 0x0000000000284400, 0x0000000000508800, + 0x0000000000A01000, 0x0000000000402000, 0x0000000002040004, 0x0000000005080008, 0x000000000A110011, 0x0000000014220022, + 0x0000000028440044, 0x0000000050880088, 0x00000000A0100010, 0x0000000040200020, 0x0000000204000402, 0x0000000508000805, + 0x0000000A1100110A, 0x0000001422002214, 0x0000002844004428, 0x0000005088008850, 0x000000A0100010A0, 0x0000004020002040, + 0x0000020400040200, 0x0000050800080500, 0x00000A1100110A00, 0x0000142200221400, 0x0000284400442800, 0x0000508800885000, + 0x0000A0100010A000, 0x0000402000204000, 0x0002040004020000, 0x0005080008050000, 0x000A1100110A0000, 0x0014220022140000, + 0x0028440044280000, 0x0050880088500000, 0x00A0100010A00000, 0x0040200020400000, 0x0204000402000000, 0x0508000805000000, + 0x0A1100110A000000, 0x1422002214000000, 0x2844004428000000, 0x5088008850000000, 0xA0100010A0000000, 0x4020002040000000, + 0x0400040200000000, 0x0800080500000000, 0x1100110A00000000, 0x2200221400000000, 0x4400442800000000, 0x8800885000000000, + 0x100010A000000000, 0x2000204000000000, 0x0004020000000000, 0x0008050000000000, 0x00110A0000000000, 0x0022140000000000, + 0x0044280000000000, 0x0088500000000000, 0x0010A00000000000, 0x0020400000000000 +}; + +/// @brief Precomputed king-attack bitboards. +/// @details Indexed by square. +constexpr Bitboard KingAttacks[64] = { + 0x0000000000000302, 0x0000000000000705, 0x0000000000000E0A, 0x0000000000001C14, 0x0000000000003828, 0x0000000000007050, + 0x000000000000E0A0, 0x000000000000C040, 0x0000000000030203, 0x0000000000070507, 0x00000000000E0A0E, 0x00000000001C141C, + 0x0000000000382838, 0x0000000000705070, 0x0000000000E0A0E0, 0x0000000000C040C0, 0x0000000003020300, 0x0000000007050700, + 0x000000000E0A0E00, 0x000000001C141C00, 0x0000000038283800, 0x0000000070507000, 0x00000000E0A0E000, 0x00000000C040C000, + 0x0000000302030000, 0x0000000705070000, 0x0000000E0A0E0000, 0x0000001C141C0000, 0x0000003828380000, 0x0000007050700000, + 0x000000E0A0E00000, 0x000000C040C00000, 0x0000030203000000, 0x0000070507000000, 0x00000E0A0E000000, 0x00001C141C000000, + 0x0000382838000000, 0x0000705070000000, 0x0000E0A0E0000000, 0x0000C040C0000000, 0x0003020300000000, 0x0007050700000000, + 0x000E0A0E00000000, 0x001C141C00000000, 0x0038283800000000, 0x0070507000000000, 0x00E0A0E000000000, 0x00C040C000000000, + 0x0302030000000000, 0x0705070000000000, 0x0E0A0E0000000000, 0x1C141C0000000000, 0x3828380000000000, 0x7050700000000000, + 0xE0A0E00000000000, 0xC040C00000000000, 0x0203000000000000, 0x0507000000000000, 0x0A0E000000000000, 0x141C000000000000, + 0x2838000000000000, 0x5070000000000000, 0xA0E0000000000000, 0x40C0000000000000 +}; - // pre-calculated lookup table for king attacks - constexpr Bitboard KingAttacks[64] = { - 0x0000000000000302, 0x0000000000000705, 0x0000000000000E0A, 0x0000000000001C14, - 0x0000000000003828, 0x0000000000007050, 0x000000000000E0A0, 0x000000000000C040, - 0x0000000000030203, 0x0000000000070507, 0x00000000000E0A0E, 0x00000000001C141C, - 0x0000000000382838, 0x0000000000705070, 0x0000000000E0A0E0, 0x0000000000C040C0, - 0x0000000003020300, 0x0000000007050700, 0x000000000E0A0E00, 0x000000001C141C00, - 0x0000000038283800, 0x0000000070507000, 0x00000000E0A0E000, 0x00000000C040C000, - 0x0000000302030000, 0x0000000705070000, 0x0000000E0A0E0000, 0x0000001C141C0000, - 0x0000003828380000, 0x0000007050700000, 0x000000E0A0E00000, 0x000000C040C00000, - 0x0000030203000000, 0x0000070507000000, 0x00000E0A0E000000, 0x00001C141C000000, - 0x0000382838000000, 0x0000705070000000, 0x0000E0A0E0000000, 0x0000C040C0000000, - 0x0003020300000000, 0x0007050700000000, 0x000E0A0E00000000, 0x001C141C00000000, - 0x0038283800000000, 0x0070507000000000, 0x00E0A0E000000000, 0x00C040C000000000, - 0x0302030000000000, 0x0705070000000000, 0x0E0A0E0000000000, 0x1C141C0000000000, - 0x3828380000000000, 0x7050700000000000, 0xE0A0E00000000000, 0xC040C00000000000, - 0x0203000000000000, 0x0507000000000000, 0x0A0E000000000000, 0x141C000000000000, - 0x2838000000000000, 0x5070000000000000, 0xA0E0000000000000, 0x40C0000000000000}; - constexpr Bitboard MASK_RANK[8] = { - 0xff, 0xff00, 0xff0000, 0xff000000, - 0xff00000000, 0xff0000000000, 0xff000000000000, 0xff00000000000000}; +/// @brief Per-rank mask (rank 0-7). +constexpr Bitboard MASK_RANK[8] = { 0xff, 0xff00, 0xff0000, 0xff000000, + 0xff00000000, 0xff0000000000, 0xff000000000000, 0xff00000000000000 }; + +/// @brief Per-file mask (file 0-7). +constexpr Bitboard MASK_FILE[8] = { + 0x101010101010101, 0x202020202020202, 0x404040404040404, 0x808080808080808, + 0x1010101010101010, 0x2020202020202020, 0x4040404040404040, 0x8080808080808080, +}; - constexpr Bitboard MASK_FILE[8] = { - 0x101010101010101, 0x202020202020202, 0x404040404040404, 0x808080808080808, - 0x1010101010101010, 0x2020202020202020, 0x4040404040404040, 0x8080808080808080, - }; -// clang-format on #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; @@ -127,9 +135,11 @@ constexpr uint64_t software_pext_u64(uint64_t val, uint64_t mask) { } return result; } + +/// @brief Magic structure for PEXT-based magic bitboards (BMI2 path). struct Magic { - Bitboard mask; - int index; + 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); @@ -139,23 +149,23 @@ struct Magic { } }; #else +/// @brief Magic structure for classical (multiply-and-shift) magic bitboards. struct Magic { - Bitboard mask; - Bitboard magic; - size_t index; - Bitboard shift; + 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 { -/** - * @brief Shifts a bitboard in a given direction - * @tparam direction - * @param b - * @return - */ + +/// @brief Shift a bitboard in the given direction. +/// @param b The bitboard. +/// @param direction Direction to shift. +/// @return Shifted bitboard. [[nodiscard]] static constexpr Bitboard shift(const Bitboard b, Direction direction) { switch (direction) { case Direction::NORTH: @@ -176,31 +186,24 @@ namespace chess::attacks { return (b & ~MASK_FILE[7]) >> 7; case DOUBLE_NORTH: return b << 16; - case DOUBLE_SOUTH: return b >> 16; - case DOUBLE_EAST: return (b & ~MASK_FILE[7] & ~(MASK_FILE[7] >> 1)) << 2; - case DOUBLE_WEST: return (b & ~MASK_FILE[0] & ~(MASK_FILE[0] << 1)) >> 2; - case DOUBLE_NORTH_EAST: { Bitboard t = (b & ~MASK_FILE[7]) << 9; return (t & ~MASK_FILE[7]) << 9; } - case DOUBLE_NORTH_WEST: { Bitboard t = (b & ~MASK_FILE[0]) << 7; return (t & ~MASK_FILE[0]) << 7; } - case DOUBLE_SOUTH_EAST: { Bitboard t = (b & ~MASK_FILE[7]) >> 7; return (t & ~MASK_FILE[7]) >> 7; } - case DOUBLE_SOUTH_WEST: { Bitboard t = (b & ~MASK_FILE[0]) >> 9; return (t & ~MASK_FILE[0]) >> 9; @@ -212,117 +215,93 @@ namespace chess::attacks { return 0; } } -/** - * @brief Shifts a bitboard in a given direction - * @tparam direction - * @param b - * @return - */ + +/// @brief Template wrapper for shift in a compile-time-known direction. template [[nodiscard]] static constexpr Bitboard shift(const Bitboard b) { return shift(b, direction); } -/** - * @brief - * @tparam c - * @param pawns - * @return - */ +/// @brief Generate left-side pawn attacks for the given colour. +/// @tparam c Colour. +/// @param pawns Bitboard of pawns. +/// @return Bitboard of left-capture target squares. template [[nodiscard]] constexpr Bitboard pawnLeftAttacks(const Bitboard pawns) { ASSUME(c == WHITE || c == BLACK); return c == WHITE ? (pawns << 7) & ~MASK_FILE[7] : (pawns >> 7) & ~MASK_FILE[0]; } -/** - * @brief Generate the right side pawn attacks. - * @tparam c - * @param pawns - * @return - */ +/// @brief Generate right-side pawn attacks for the given colour. +/// @tparam c Colour. +/// @param pawns Bitboard of pawns. +/// @return Bitboard of right-capture target squares. template [[nodiscard]] constexpr Bitboard pawnRightAttacks(const Bitboard pawns) { ASSUME(c == WHITE || c == BLACK); return c == WHITE ? (pawns << 9) & ~MASK_FILE[0] : (pawns >> 9) & ~MASK_FILE[7]; } -/** - * @brief Generate the right side pawn attacks. - * @tparam c - * @param pawns - * @return - */ +/// @brief Generate all pawn attacks from a bitboard of pawns. +/// @tparam c Colour. +/// @param pawns Bitboard of pawns. +/// @return Bitboard of all squares attacked by the pawns. template [[nodiscard]] constexpr Bitboard pawn(const Bitboard pawns) { ASSUME(c == WHITE || c == BLACK); if constexpr (c == WHITE) { - return ((pawns & ~MASK_FILE[FILE_A]) << 7) | // left captures - ((pawns & ~MASK_FILE[FILE_H]) << 9); // right captures + return ((pawns & ~MASK_FILE[FILE_A]) << 7) | ((pawns & ~MASK_FILE[FILE_H]) << 9); } else { return ((pawns & ~MASK_FILE[FILE_H]) >> 7) | ((pawns & ~MASK_FILE[FILE_A]) >> 9); } } -/** - * @brief Returns the pawn attacks for a given color and square - * @param c - * @param sq - * @return - */ +/// @brief Look up pawn attacks for a single square. +/// @param c Colour. +/// @param sq Square. +/// @return Bitboard of squares attacked. [[nodiscard]] constexpr Bitboard pawn(Color c, Square sq) { return PawnAttacks[(int)c][(int)sq]; } -/** - * @brief Returns the knight attacks for a given square - * @param sq - * @return - */ +/// @brief Look up knight attacks for a single square. +/// @param sq Square. +/// @return Bitboard of squares attacked. [[nodiscard]] constexpr Bitboard knight(Square sq) { return KnightAttacks[(int)sq]; } -/** - * @brief Returns the knight attacks for given knights - * @param sq - * @return - */ + +/// @brief Compute knight attacks for a bitboard of knights. +/// @param knights Bitboard of knights. +/// @return Bitboard of all squares attacked. [[nodiscard]] constexpr Bitboard knight(Bitboard knights) { - Bitboard l1 = (knights >> 1) & 0x7f7f7f7f7f7f7f7fULL; // shift left by 1, mask out file A - Bitboard l2 = (knights >> 2) & 0x3f3f3f3f3f3f3f3fULL; // shift left by 2, mask out files A+B - Bitboard r1 = (knights << 1) & 0xfefefefefefefefeULL; // shift right by 1, mask out file H - Bitboard r2 = (knights << 2) & 0xfcfcfcfcfcfcfcfcULL; // shift right by 2, mask out files G+H - Bitboard h1 = l1 | r1; // 1-square horizontal shifts - Bitboard h2 = l2 | r2; // 2-square horizontal shifts - return (h1 << 16) | (h1 >> 16) | (h2 << 8) | (h2 >> 8); // vertical shifts: +2,+1,-2,-1 + Bitboard l1 = (knights >> 1) & 0x7f7f7f7f7f7f7f7fULL; + Bitboard l2 = (knights >> 2) & 0x3f3f3f3f3f3f3f3fULL; + Bitboard r1 = (knights << 1) & 0xfefefefefefefefeULL; + Bitboard r2 = (knights << 2) & 0xfcfcfcfcfcfcfcfcULL; + Bitboard h1 = l1 | r1; + Bitboard h2 = l2 | r2; + return (h1 << 16) | (h1 >> 16) | (h2 << 8) | (h2 >> 8); } -/** - * @brief Returns the bishop attacks for a given square - * @param sq - * @param occupied - * @return - */ + +/// @brief Look up bishop attacks via magic bitboard (defined in attacks.cpp). +/// @param sq Bishop square. +/// @param occupied Occupancy bitboard. +/// @return Bitboard of squares attacked. [[nodiscard]] Bitboard bishop(Square sq, Bitboard occupied); -/** - * @brief Returns the rook attacks for a given square - * @param sq - * @param occupied - * @return - */ +/// @brief Look up rook attacks via magic bitboard (defined in attacks.cpp). +/// @param sq Rook square. +/// @param occupied Occupancy bitboard. +/// @return Bitboard of squares attacked. [[nodiscard]] Bitboard rook(Square sq, Bitboard occupied); -/** - * @brief Returns the queen attacks for a given square - * @param sq - * @param occupied - * @return - */ + +/// @brief Compute queen attacks (bishop | rook). +/// @param sq Queen square. +/// @param occupied Occupancy bitboard. +/// @return Bitboard of squares attacked. [[nodiscard]] inline Bitboard queen(Square sq, Bitboard occupied) { return bishop(sq, occupied) | rook(sq, occupied); } -/** - * @brief Returns the king attacks for a given square - * @param sq - * @return - */ +/// @brief Look up king attacks for a single square. +/// @param sq Square. +/// @return Bitboard of squares attacked. [[nodiscard]] constexpr Bitboard king(Square sq) { return KingAttacks[(int)sq]; } -/** - * @brief Returns the slider attacks for a given square - * @param sq - * @param occupied - * @tparam pt - * @return - */ +/// @brief Template dispatcher for slider attacks (bishop / rook / queen). +/// @tparam pt Piece type (must be a slider). +/// @param sq Square. +/// @param occupied Occupancy bitboard. +/// @return Bitboard of squares attacked. template [[nodiscard]] inline Bitboard slider(Square sq, Bitboard occupied) { static_assert(pt == PieceType::BISHOP || pt == PieceType::ROOK || pt == PieceType::QUEEN, "PieceType must be a slider!"); diff --git a/bitboard.h b/bitboard.h index 0aa7eae..7a6002b 100644 --- a/bitboard.h +++ b/bitboard.h @@ -23,10 +23,15 @@ #include #endif #include + +/// @file bitboard.h +/// @brief Bitboard utility functions (popcount, LSB, MSB, etc.). + namespace chess { -// ------------------------------- -// constexpr fallbacks -// ------------------------------- + +/// @brief constexpr fallback for population count. +/// @param x Input bitboard. +/// @return Number of set bits. constexpr int popcount_constexpr(Bitboard x) noexcept { int count = 0; while (x) { @@ -36,6 +41,9 @@ constexpr int popcount_constexpr(Bitboard x) noexcept { return count; } +/// @brief constexpr fallback for least-significant bit index. +/// @param x Input bitboard. +/// @return Index of the lowest set bit (0-based). constexpr int lsb_constexpr(Bitboard x) noexcept { int pos = 0; while ((x & 1) == 0) { @@ -45,6 +53,9 @@ constexpr int lsb_constexpr(Bitboard x) noexcept { return pos; } +/// @brief constexpr fallback for most-significant bit index. +/// @param x Input bitboard. +/// @return Index of the highest set bit (0-based). constexpr int msb_constexpr(Bitboard x) noexcept { int pos = 63; Bitboard mask = 1ULL << 63; @@ -55,9 +66,9 @@ constexpr int msb_constexpr(Bitboard x) noexcept { return pos; } -// ------------------------------- -// runtime + constexpr aware -// ------------------------------- +/// @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 @@ -72,6 +83,9 @@ inline constexpr int popcount(Bitboard x) noexcept { return popcount_constexpr(x); } +/// @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 @@ -89,6 +103,9 @@ inline constexpr int lsb(Bitboard x) noexcept { return lsb_constexpr(x); } +/// @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 @@ -106,9 +123,9 @@ inline constexpr int msb(Bitboard x) noexcept { return msb_constexpr(x); } -// ------------------------------- -// destructive variants -// ------------------------------- +/// @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 { int c = lsb(b); #ifndef __BMI2__ @@ -119,6 +136,9 @@ inline int pop_lsb(Bitboard &b) noexcept { 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 { int c = msb(b); b &= ~(1ULL << c); diff --git a/chess960_tests.cpp b/chess960_tests.cpp index 8730c99..25c3b5c 100644 --- a/chess960_tests.cpp +++ b/chess960_tests.cpp @@ -16,8 +16,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#define DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#if !defined(__cpp_exceptions) && !defined(_CPPUNWIND) && !defined(__EXCEPTIONS) && !defined(_CHESSLIB_ERROR_MODE_THROW) #define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#endif #include "moves_io.h" #include "position.h" #include "printers.h" @@ -25,7 +27,7 @@ #include #include using namespace chess; -#if defined(_DEBUG) || !defined(NDEBUG) +#if !defined(NDEBUG) #define IS_RELEASE 0 #else #define IS_RELEASE 1 @@ -38,17 +40,21 @@ template struct TestEntry { InputT input; CheckInfo info; }; -template uint64_t perft(_Position &pos, int depth) { +template uint64_t perft(_Position &pos, int depth) { if (depth == 0) { return 1; } else if (depth == 1) { - Movelist moves; - pos.template legals(moves); - if constexpr (EnableDiv) - for (const Move &m : moves) { + if constexpr (EnableDiv) { + Movelist moves; + pos.template legals(moves); + for (const Move &m : moves) std::cout << m << ": 1\n"; - } - return moves.size(); + return moves.size(); + } else { + CountOnlyList moves; + pos.template legals(moves); + return moves.size_; + } } else { Movelist moves; pos.template legals(moves); @@ -57,32 +63,27 @@ template uint64_t perft(_Po pos.template doMove(m); #if !IS_RELEASE { - const auto pre_nm_hash_1 = pos.hash(); - const auto pre_nm_fen_1 = pos.fen(); - if (pos.zobrist() != pos.hash()) - REQUIRE(pos.zobrist() == pos.hash()); + const auto pre_nm_hash = pos.hash(); pos.doNullMove(); pos.undoMove(); - if (!(pos.hash() == pre_nm_hash_1 && pos.fen() == pre_nm_fen_1 && pos.zobrist() == pre_nm_hash_1)) { - REQUIRE(pos.hash() == pre_nm_hash_1); - REQUIRE(pos.fen() == pre_nm_fen_1); - REQUIRE(pos.zobrist() == pre_nm_hash_1); + if (pos.hash() != pre_nm_hash || pos.zobrist() != pre_nm_hash) { + // Compute fen() only on failure (extremely rare) + const auto post_fen = pos.fen(); + REQUIRE(!"Hash changed after null move"); + REQUIRE(pos.zobrist() == pre_nm_hash); } } #endif - const uint64_t nodes = perft(pos, depth - 1); + const uint64_t nodes = perft(pos, depth - 1); #if !IS_RELEASE { - const auto pre_nm_hash_1 = pos.hash(); - const auto pre_nm_fen_1 = pos.fen(); - if (pos.zobrist() != pos.hash()) - REQUIRE(pos.zobrist() == pos.hash()); + const auto pre_nm_hash = pos.hash(); pos.doNullMove(); pos.undoMove(); - if (!(pos.hash() == pre_nm_hash_1 && pos.fen() == pre_nm_fen_1 && pos.zobrist() == pre_nm_hash_1)) { - REQUIRE(pos.hash() == pre_nm_hash_1); - REQUIRE(pos.fen() == pre_nm_fen_1); - REQUIRE(pos.zobrist() == pre_nm_hash_1); + if (pos.hash() != pre_nm_hash || pos.zobrist() != pre_nm_hash) { + const auto post_fen = pos.fen(); + REQUIRE(!"Hash changed after null move"); + REQUIRE(pos.zobrist() == pre_nm_hash); } } #endif @@ -135,63 +136,51 @@ auto split_testcases(std::vector> &entries) { return optimized; } #endif +template +void check_perft_type(TestEntry &entry, uint64_t &nodes, double &elapsed) { + using namespace std::chrono; + _Position pos(entry.input, true); + auto start_time = high_resolution_clock::now(); + if (pos.side_to_move() == WHITE) + REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); + else + REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); + auto end_time = high_resolution_clock::now(); + elapsed += duration(end_time - start_time).count(); + nodes += entry.info.nodes; + if (entry.info.nodes < 5e6) { + _Position pos2 = pos; + REQUIRE(pos.fen() == pos2.fen()); + start_time = high_resolution_clock::now(); + if (pos2.side_to_move() == WHITE) + REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); + else + REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); + end_time = high_resolution_clock::now(); + elapsed += duration(end_time - start_time).count(); + nodes += entry.info.nodes; + } else { + std::cerr << "\n(skipped copying test)\n"; + } +} template void check_perfts(std::vector> &entries) { uint64_t nodes = 0; double elapsed = 0; - using namespace std::chrono; #if !IS_RELEASE entries = split_testcases(entries); #endif - auto start_time = high_resolution_clock::now(); for (auto &entry : entries) { std::cerr << entry.input << " (chess960=true) " << entry.info.depth; std::cerr << '\n'; - { - _Position pos(entry.input, true); - REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); - nodes += entry.info.nodes; - if (entry.info.nodes < 5e6) { - _Position pos2 = pos; - REQUIRE(pos.fen() == pos2.fen()); - REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); - nodes += entry.info.nodes; - } else { - std::cerr << "\n(skipped copying test)\n"; - } - } - { - _Position pos(entry.input, true); - REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); - nodes += entry.info.nodes; - if (entry.info.nodes < 5e6) { - _Position pos2 = pos; - REQUIRE(pos.fen() == pos2.fen()); - REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); - nodes += entry.info.nodes; - } else { - std::cerr << "\n(skipped copying test)\n"; - } - } - { - _Position pos(entry.input, true); - REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); - nodes += entry.info.nodes; - if (entry.info.nodes < 5e6) { - _Position pos2 = pos; - REQUIRE(pos.fen() == pos2.fen()); - REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); - nodes += entry.info.nodes; - } else { - std::cerr << "\n(skipped copying test)\n"; - } - } + check_perft_type(entry, nodes, elapsed); + check_perft_type(entry, nodes, elapsed); + check_perft_type(entry, nodes, elapsed); } - auto end_time = high_resolution_clock::now(); - elapsed = duration(end_time - start_time).count(); double mnps = (nodes / elapsed) / 1'000'000.0; std::cout << "Speed: " << mnps << "Mnps\n"; } + TEST_CASE("Chess960" * doctest::timeout(36000)) { std::vector> tests = { { "bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf - 2 9", 1, 21 }, @@ -5957,10 +5946,95 @@ TEST_CASE("Chess960" * doctest::timeout(36000)) { }; check_perfts(tests); } -int main(int argc, char **argv) { - doctest::Context ctx; - ctx.setOption("success", true); - ctx.setOption("no-breaks", true); - ctx.setOption("abort-after", 1); - return ctx.run(); +TEST_CASE("Chess960 double setFEN reinitializes castling metadata") { + { + // Chess960 position → Chess960 position (different castling) + // GEge: G/kingside rook on G file, E/queenside rook on E file; king at F1/F8 + Position p("n1bqrkrb/pppppppp/8/8/8/8/PPPPPPPP/N1BQRKRB w GEge - 0 1", true); + auto meta_w = p.getCastlingMetadata(WHITE); + auto meta_b = p.getCastlingMetadata(BLACK); + REQUIRE(meta_w.king_start == SQ_F1); + REQUIRE(meta_w.rook_start_ks == SQ_G1); + REQUIRE(meta_w.rook_start_qs == SQ_E1); + REQUIRE(meta_b.king_start == SQ_F8); + REQUIRE(meta_b.rook_start_ks == SQ_G8); + REQUIRE(meta_b.rook_start_qs == SQ_E8); + + // Q1NBBRKR: Q A1, . B1, N C1, B D1, B E1, R F1, K G1, R H1 → HFhf → king=G1, rook_ks=H1, rook_qs=F1 + p.setFEN("qnnbbrkr/1p2ppp1/2pp3p/p7/1P5P/2NP4/P1P1PPP1/Q1NBBRKR w HFhf - 0 9", true); + meta_w = p.getCastlingMetadata(WHITE); + meta_b = p.getCastlingMetadata(BLACK); + REQUIRE(meta_w.king_start == SQ_G1); + REQUIRE(meta_w.rook_start_ks == SQ_H1); + REQUIRE(meta_w.rook_start_qs == SQ_F1); + REQUIRE(meta_b.king_start == SQ_G8); + REQUIRE(meta_b.rook_start_ks == SQ_H8); + REQUIRE(meta_b.rook_start_qs == SQ_F8); + } + { + // Standard → Chess960 → Standard + // BQ1BNRKR: B A1, Q B1, . C1, B D1, N E1, R F1, K G1, R H1 → HFhf → king=G1, rook_ks=H1, rook_qs=F1 + Position p("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + p.setFEN("bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf - 2 9", true); + auto meta_w = p.getCastlingMetadata(WHITE); + REQUIRE(meta_w.king_start == SQ_G1); + REQUIRE(meta_w.rook_start_ks == SQ_H1); + REQUIRE(meta_w.rook_start_qs == SQ_F1); + + p.setFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + meta_w = p.getCastlingMetadata(WHITE); + REQUIRE(meta_w.king_start == SQ_E1); + REQUIRE(meta_w.rook_start_ks == SQ_H1); + REQUIRE(meta_w.rook_start_qs == SQ_A1); + } + { + // Chess960 → no castling → Chess960 + // QNBBNRKR: Q A1, N B1, B C1, B D1, N E1, R F1, K G1, R H1 → HFhf → king=G1 + Position p("1nbbnrkr/p1p1ppp1/3p4/1p3P1p/3Pq2P/8/PPP1P1P1/QNBBNRKR w HFhf - 0 9", true); + REQUIRE(p.getCastlingMetadata(WHITE).king_start == SQ_G1); + + p.setFEN("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + REQUIRE(p.getCastlingMetadata(WHITE).king_start == SQ_NONE); + REQUIRE(p.getCastlingMetadata(BLACK).king_start == SQ_NONE); + + // 1BNNBRKR: . A1, B B1, N C1, N D1, B E1, R F1, K G1, R H1 → HFhf → king=G1, rook_ks=H1, rook_qs=F1 + p.setFEN("qbn1brkr/ppp1p1p1/2n4p/3p1p2/P7/6PP/QPPPPP2/1BNNBRKR w HFhf - 0 9", true); + auto meta_w = p.getCastlingMetadata(WHITE); + REQUIRE(meta_w.king_start == SQ_G1); + REQUIRE(meta_w.rook_start_ks == SQ_H1); + REQUIRE(meta_w.rook_start_qs == SQ_F1); + } + { + // Chess960 with "Kk": white kingside + black kingside only + Position p("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1"); + p.setFEN("r3k2r/8/8/8/8/8/8/R3K2R w Kk - 0 1", true); + auto meta_w = p.getCastlingMetadata(WHITE); + REQUIRE(meta_w.king_start == SQ_E1); + REQUIRE(meta_w.rook_start_ks == SQ_H1); + REQUIRE(meta_w.rook_start_qs == SQ_NONE); + auto meta_b = p.getCastlingMetadata(BLACK); + REQUIRE(meta_b.king_start == SQ_E8); + REQUIRE(meta_b.rook_start_ks == SQ_H8); + REQUIRE(meta_b.rook_start_qs == SQ_NONE); + } + { + // Chess960 with non-standard rook placements + Position p("r1bqkb1r/pppppppp/2n2n2/8/8/2N2N2/PPPPPPPP/R1BQKB1R w KQkq - 0 1"); + p.setFEN("n1bqrkrb/pppppppp/8/8/8/8/PPPPPPPP/N1BQRKRB w GEge - 0 1", true); + auto meta_w = p.getCastlingMetadata(WHITE); + REQUIRE(meta_w.king_start == SQ_F1); + REQUIRE(meta_w.rook_start_ks == SQ_G1); + REQUIRE(meta_w.rook_start_qs == SQ_E1); + auto meta_b = p.getCastlingMetadata(BLACK); + REQUIRE(meta_b.king_start == SQ_F8); + REQUIRE(meta_b.rook_start_ks == SQ_G8); + REQUIRE(meta_b.rook_start_qs == SQ_E8); + + // 1BNNBRKR: . A1, B B1, N C1, N D1, B E1, R F1, K G1, R H1 → HFhf → king=G1, rook_ks=H1, rook_qs=F1 + p.setFEN("qbn1brkr/ppp1p1p1/2n4p/3p1p2/P7/6PP/QPPPPP2/1BNNBRKR w HFhf - 0 9", true); + meta_w = p.getCastlingMetadata(WHITE); + REQUIRE(meta_w.king_start == SQ_G1); + REQUIRE(meta_w.rook_start_ks == SQ_H1); + REQUIRE(meta_w.rook_start_qs == SQ_F1); + } } diff --git a/fwd_decl.h b/fwd_decl.h index 9f822ca..153346d 100644 --- a/fwd_decl.h +++ b/fwd_decl.h @@ -19,31 +19,90 @@ #pragma once #include #include + +/// @file fwd_decl.h +/// @brief Forward declarations for all major chess types. + namespace chess { + +/// @enum Color +/// @brief Side to move or piece color. enum Color : uint8_t; + +/// @enum PieceType +/// @brief Basic piece type without color information. enum PieceType : std::int8_t; +/// @brief Trait to detect piece-enum types (PolyglotPiece, EnginePiece, ContiguousMappingPiece). template struct is_piece_enum : std::false_type {}; template struct is_piece_enum> : std::true_type {}; + +/// @enum CastlingRights +/// @brief Bitmask of available castling rights. enum CastlingRights : int8_t; +/// @enum Square +/// @brief Board square index (0-63, A1-H8). enum Square : int8_t; +/// @enum Direction +/// @brief Compass direction offsets for board traversal. enum Direction : int8_t; +/// @enum MoveType +/// @brief Move-type flags (normal, promotion, en-passant, castling). enum MoveType : uint16_t; +/// @enum File +/// @brief File index (0-7, A-H). enum File : int8_t; +/// @enum Rank +/// @brief Rank index (0-7, 1-8). enum Rank : int8_t; + +/// @class Move +/// @brief Compact 16-bit move representation. class Move; + +/// @enum MoveGenType +/// @brief Move-generation filter flags for piece type and move kind. enum class MoveGenType : uint16_t; + +/// @class _Position +/// @brief Templated chess position class parameterised by the piece enum. template class _Position; + +/// @typedef Bitboard +/// @brief 64-bit bitboard representing a set of squares. using Bitboard = uint64_t; + +/// @typedef Key +/// @brief 64-bit Zobrist hash key. using Key = uint64_t; + +/// @class ValueList +/// @brief Stack-allocated fixed-capacity container. template class ValueList; -using Movelist = ValueList; -// bonus: define the piece enums here + +/// @typedef Movelist +/// @brief Fixed-capacity list of up to 300 moves. +using Movelist = ValueList; + +/// @enum PolyglotPiece +/// @brief Piece encoding used by Polyglot opening books. enum class PolyglotPiece : uint8_t; + +/// @enum EnginePiece +/// @brief Default engine piece encoding (8 values per colour). enum class EnginePiece : uint8_t; + +/// @enum ContiguousMappingPiece +/// @brief Compact piece encoding (0-5 white, 6-11 black). enum class ContiguousMappingPiece : uint8_t; + +/// @typedef Position +/// @brief Default chess position type (uses EnginePiece). using Position = _Position; -using Board = Position; + +/// @typedef Board +/// @brief Alias for Position. +using Board [[deprecated("Use Position instead")]] = Position; } // namespace chess diff --git a/movegen.cpp b/movegen.cpp index 60f5155..db06f73 100644 --- a/movegen.cpp +++ b/movegen.cpp @@ -22,6 +22,10 @@ // movegen references // License: https://github.com/Disservin/chess-library/blob/master/LICENSE + +/// @file movegen.cpp +/// @brief Move generator: AVX-512 accelerated splatting and per-piece-type move generation. + #include "movegen.h" #include "position.h" @@ -122,11 +126,23 @@ 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) { + if constexpr (std::is_same_v) { + _chess::splat_moves(list.data() + list.size_, from, targets); + } +} +template inline void record_pawn_moves(ListT &list, Bitboard targets) { + if constexpr (std::is_same_v) { + _chess::splat_pawn_moves(list.data() + list.size_, targets); + } +} } // namespace chess namespace chess { -template void movegen::genEP(const _Position &pos, Movelist &mv) { +template [[gnu::hot]] void movegen::genEP(const _Position &pos, ListT &mv) { - const Square king_sq = pos.kingSq(c); + const Square king_sq = pos.king_sq(c); const Square ep_sq = pos.ep_square(); if (ep_sq == SQ_NONE) return; @@ -139,12 +155,12 @@ template void movegen::genEP(const _Position &pos const Bitboard ep_mask = (1ULL << ep_pawn_sq) | (1ULL << ep_sq); // ASSUME(popcount(candidates) <= 32); + Bitboard occ_all = pos.occ(); while (candidates) { Square from = static_cast(pop_lsb(candidates)); // Remove the EP pawn and this attacker from occupancy - Bitboard occ_temp = pos.occ(); - occ_temp ^= (1ULL << from) | ep_mask; + Bitboard occ_temp = occ_all ^ ((1ULL << from) | ep_mask); // attackers check Bitboard atks = 0; @@ -156,8 +172,9 @@ template void movegen::genEP(const _Position &pos } } } -template -void movegen::genPawnDoubleMoves(const _Position &pos, Movelist &moves, Bitboard pin_mask, Bitboard check_mask) { +template +[[gnu::hot]] 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); @@ -165,7 +182,7 @@ void movegen::genPawnDoubleMoves(const _Position &pos, Movelist &moves, Bitboard pawns = pos.template pieces() & RANK_2; // Split pin types - Bitboard pin_file = pin_mask & attacks::MASK_FILE[file_of(pos.kingSq(c))]; + Bitboard pin_file = pin_mask & attacks::MASK_FILE[file_of(pos.king_sq(c))]; Bitboard unpinned = pawns & ~pin_mask; Bitboard file_pinned = pawns & pin_file; @@ -180,12 +197,12 @@ void movegen::genPawnDoubleMoves(const _Position &pos, Movelist &moves, Bitboard destinations = (step2_unpinned | step2_pinned) & check_mask; - _chess::splat_pawn_moves<2 * UP>(moves.data() + moves.size_, destinations); + record_pawn_moves<2 * UP>(moves, destinations); moves.size_ += popcount(destinations); } -template -void movegen::genPawnSingleMoves( - const _Position &pos, Movelist &moves, Bitboard _rook_pin, Bitboard _bishop_pin, Bitboard _check_mask) { +template +[[gnu::hot]] 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); constexpr auto UP_RIGHT = relative_direction(c, NORTH_EAST); @@ -259,16 +276,17 @@ void movegen::genPawnSingleMoves( l_pawns &= ~RANK_PROMO; r_pawns &= ~RANK_PROMO; if constexpr (!capturesOnly) { - _chess::splat_pawn_moves(moves.data() + moves.size_, single_push); + record_pawn_moves(moves, single_push); moves.size_ += popcount(single_push); } - _chess::splat_pawn_moves(moves.data() + moves.size_, l_pawns); + record_pawn_moves(moves, l_pawns); moves.size_ += popcount(l_pawns); - _chess::splat_pawn_moves(moves.data() + moves.size_, r_pawns); + record_pawn_moves(moves, r_pawns); moves.size_ += popcount(r_pawns); } -template -void movegen::genKnightMoves(const _Position &pos, Movelist &list, Bitboard _pin_mask, Bitboard _check_mask) { +template +[[gnu::hot]] 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)); @@ -276,43 +294,50 @@ void movegen::genKnightMoves(const _Position &pos, Movelist &list, Bitb moves &= _check_mask; if constexpr (capturesOnly) moves &= pos.occ(~c); - _chess::splat_moves(list.data() + list.size(), x, moves); + record_moves(list, x, moves); list.size_ += popcount(moves); } } -template -void movegen::genKingMoves(const _Position &pos, Movelist &out, Bitboard _pin_mask) { +template +[[gnu::hot]] void movegen::genKingMoves(const _Position &pos, ListT &out, Bitboard _pin_mask) { constexpr Color them = ~c; - const Square kingSq = pos.kingSq(c); - const Bitboard occAll = pos.occ(); + const Square kingSq = pos.king_sq(c); const Bitboard myOcc = pos.occ(c); + const Bitboard occ_opp = pos.occ(~c); - // Remove king from board when computing enemy attacks + if constexpr (capturesOnly) { + Bitboard targets = attacks::king(kingSq) & occ_opp; + if (!targets) { + out.size_ += 0; + return; + } + } + + const Bitboard occAll = pos.occ(); const Bitboard occWithoutKing = occAll ^ 1ULL << kingSq; Bitboard enemyAttacks = 0ULL; // Sliding pieces - Bitboard bLike = pos.template pieces() | pos.template pieces(); - while (bLike) - enemyAttacks |= attacks::bishop(static_cast(pop_lsb(bLike)), occWithoutKing); - - Bitboard rLike = pos.template pieces() | pos.template pieces(); - while (rLike) - enemyAttacks |= attacks::rook(static_cast(pop_lsb(rLike)), occWithoutKing); + { + Bitboard bLike = pos.template pieces() | pos.template pieces(); + while (bLike) + enemyAttacks |= attacks::bishop(static_cast(pop_lsb(bLike)), occWithoutKing); + } + { + Bitboard rLike = pos.template pieces() | pos.template pieces(); + while (rLike) + enemyAttacks |= attacks::rook(static_cast(pop_lsb(rLike)), occWithoutKing); + } - // Knights + // Knights, pawns, enemy king (precomputed tables) enemyAttacks |= attacks::knight(pos.template pieces()); - - // Pawns enemyAttacks |= attacks::pawn(pos.template pieces()); - - // Enemy king (adjacent control squares) - enemyAttacks |= attacks::king(pos.kingSq(them)); + enemyAttacks |= attacks::king(pos.king_sq(them)); Bitboard moves = attacks::king(kingSq) & ~myOcc & ~enemyAttacks; if constexpr (capturesOnly) - moves &= pos.occ(~c); - _chess::splat_moves(out.data() + out.size(), kingSq, moves); + moves &= occ_opp; + record_moves(out, kingSq, moves); out.size_ += popcount(moves); if constexpr (!capturesOnly) { if (pos.checkers()) @@ -322,11 +347,11 @@ void movegen::genKingMoves(const _Position &pos, Movelist &out, Bitboar Bitboard enemy_attacks = enemyAttacks; constexpr CastlingRights kingRights = KING_SIDE & (c == WHITE ? WHITE_CASTLING : BLACK_CASTLING), queenRights = QUEEN_SIDE & (c == WHITE ? WHITE_CASTLING : BLACK_CASTLING); - Bitboard OO_EMPTY = pos.getCastlingPath(c, true); + Bitboard OO_EMPTY = pos.get_castling_path(c, true); Bitboard OO_SAFE = between(kingSq, castling_king_square(c, true)); - Bitboard OOO_EMPTY = pos.getCastlingPath(c, false); + Bitboard OOO_EMPTY = pos.get_castling_path(c, false); Bitboard OOO_SAFE = between(kingSq, castling_king_square(c, false)); - Square rookKing = pos.getCastlingMetadata(c).rook_start_ks, rookQueen = pos.getCastlingMetadata(c).rook_start_qs; + Square rookKing = pos.get_castling_metadata(c).rook_start_ks, rookQueen = pos.get_castling_metadata(c).rook_start_qs; if (pos.castlingRights() & kingRights && !(occupancy & OO_EMPTY || enemy_attacks & OO_SAFE || _pin_mask & 1ULL << rookKing)) { @@ -338,9 +363,9 @@ void movegen::genKingMoves(const _Position &pos, Movelist &out, Bitboar } } } -template -void movegen::genSlidingMoves( - const _Position &pos, Movelist &moves, Bitboard _rook_pin, Bitboard _bishop_pin, Bitboard _check_mask) { +template +[[gnu::hot]] 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(); Bitboard occ_all = pos.occ(); @@ -350,6 +375,7 @@ void movegen::genSlidingMoves( sliders &= ~rook_pinners; if constexpr (pt == ROOK) sliders &= ~bishop_pinners; + Bitboard occ_opp = pos.occ(~c); Bitboard filter_list = ~pos.occ(c) & _check_mask; while (sliders) { Square from = static_cast(pop_lsb(sliders)); @@ -359,140 +385,148 @@ void movegen::genSlidingMoves( Bitboard bishop_hit = bishop_pinners & from_bb; Bitboard pin_mask = rook_hit ? rook_pinners : bishop_hit ? bishop_pinners : ~0ULL; - auto func = attacks::queen; - if constexpr (pt == BISHOP) - func = attacks::bishop; - else if constexpr (pt == ROOK) - func = attacks::rook; - func = rook_hit ? attacks::rook : bishop_hit ? attacks::bishop : func; Bitboard filtered_pin = pin_mask & filter_list; - Bitboard targets = func(from, occ_all) & filtered_pin; + Bitboard targets; + if (rook_hit) { + targets = attacks::rook(from, occ_all) & filtered_pin; + } 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; + } else { + targets = attacks::queen(from, occ_all) & filtered_pin; + } if constexpr (capturesOnly) - targets &= pos.occ(~c); - _chess::splat_moves(moves.data() + moves.size_, from, targets); + targets &= occ_opp; + record_moves(moves, from, targets); moves.size_ += popcount(targets); } } -#define INSTANTIATE(PieceC) \ - template void chess::movegen::genEP(const _Position &, Movelist &); \ - template void chess::movegen::genEP(const _Position &, Movelist &); \ - template void chess::movegen::genPawnDoubleMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genPawnDoubleMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genPawnSingleMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genPawnSingleMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ +#define INSTANTIATE(PieceC, ListT) \ + template void chess::movegen::genEP(const _Position &, ListT &); \ + template void chess::movegen::genEP(const _Position &, ListT &); \ + template void chess::movegen::genPawnDoubleMoves(const _Position &, \ + ListT &, \ Bitboard, \ Bitboard); \ - template void chess::movegen::genPawnSingleMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genPawnSingleMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ + template void chess::movegen::genPawnDoubleMoves(const _Position &, \ + ListT &, \ Bitboard, \ Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ + template void chess::movegen::genPawnSingleMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genPawnSingleMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genPawnSingleMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genPawnSingleMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genSlidingMoves(const _Position &, \ + ListT &, \ + Bitboard, \ + Bitboard, \ + Bitboard); \ + template void chess::movegen::genKnightMoves(const _Position &, \ + ListT &, \ Bitboard, \ Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ + template void chess::movegen::genKnightMoves(const _Position &, \ + ListT &, \ Bitboard, \ Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ + template void chess::movegen::genKnightMoves(const _Position &, \ + ListT &, \ Bitboard, \ Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ + template void chess::movegen::genKnightMoves(const _Position &, \ + ListT &, \ Bitboard, \ Bitboard); \ - template void chess::movegen::genSlidingMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genKnightMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genKnightMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genKnightMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genKnightMoves(const _Position &, \ - Movelist &, \ - Bitboard, \ - Bitboard); \ - template void chess::movegen::genKingMoves(const _Position &, \ - Movelist &, \ - Bitboard); \ - template void chess::movegen::genKingMoves(const _Position &, \ - Movelist &, \ - Bitboard); \ - template void chess::movegen::genKingMoves(const _Position &, \ - Movelist &, \ - Bitboard); \ - template void chess::movegen::genKingMoves(const _Position &, \ - Movelist &, \ - Bitboard); -INSTANTIATE(EnginePiece) -INSTANTIATE(PolyglotPiece) -INSTANTIATE(ContiguousMappingPiece) + template void chess::movegen::genKingMoves(const _Position &, \ + ListT &, \ + Bitboard); \ + template void chess::movegen::genKingMoves(const _Position &, \ + ListT &, \ + Bitboard); \ + template void chess::movegen::genKingMoves(const _Position &, \ + ListT &, \ + Bitboard); \ + template void chess::movegen::genKingMoves(const _Position &, \ + ListT &, \ + Bitboard); +INSTANTIATE(EnginePiece, Movelist) +INSTANTIATE(PolyglotPiece, Movelist) +INSTANTIATE(ContiguousMappingPiece, Movelist) +INSTANTIATE(EnginePiece, CountOnlyList) +INSTANTIATE(PolyglotPiece, CountOnlyList) +INSTANTIATE(ContiguousMappingPiece, CountOnlyList) } // namespace chess diff --git a/movegen.h b/movegen.h index bb6aea0..6cc00de 100644 --- a/movegen.h +++ b/movegen.h @@ -18,21 +18,45 @@ */ #pragma once #include "fwd_decl.h" +#include "types.h" #include + +/// @file movegen.h +/// @brief Move-generation declarations and between-square table. + namespace chess::movegen { -template void genEP(const _Position &, Movelist &); -template void genPawnDoubleMoves(const _Position &, Movelist &, Bitboard, Bitboard); -template -void genPawnSingleMoves(const _Position &, Movelist &, Bitboard, Bitboard, Bitboard); -template -void genKnightMoves(const _Position &, Movelist &, Bitboard, Bitboard); -template void genKingMoves(const _Position &, Movelist &, Bitboard); -template -void genSlidingMoves(const _Position &, Movelist &, Bitboard, Bitboard, Bitboard); -extern std::array, 65> SQUARES_BETWEEN_BB; -/* - * [(file(sq1), rank(sq1)), (file(sq2), rank(sq2))] -> bitboard of squares between sq1 and sq2, excluding sq1 and sq2 - */ +/// @brief Generate en-passant captures for the given colour. +template void genEP(const _Position &, ListT &); + +/// @brief Generate double-pawn pushes (from the starting rank). +template void genPawnDoubleMoves(const _Position &, ListT &, Bitboard, Bitboard); + +/// @brief Generate single-pawn moves (pushes and captures). +template +void genPawnSingleMoves(const _Position &, ListT &, Bitboard, Bitboard, Bitboard); + +/// @brief Generate knight moves. +template +void genKnightMoves(const _Position &, ListT &, Bitboard, Bitboard); + +/// @brief Generate king moves. +template +void genKingMoves(const _Position &, ListT &, Bitboard); + +/// @brief Generate sliding-piece moves (bishop, rook, queen). +template +void genSlidingMoves(const _Position &, ListT &, Bitboard, Bitboard, Bitboard); + +/// @brief Precomputed between-square bitboards. +/// @details squares_between_bb[sq1][sq2] contains a bitboard of all squares +/// strictly between sq1 and sq2 (excluding both endpoints). +extern std::array, 64> SQUARES_BETWEEN_BB; + +/// @brief Look up the squares between two squares. +/// @param sq1 First square. +/// @param sq2 Second square. +/// @return Bitboard of squares between sq1 and sq2, excluding sq1 and sq2. +/// Returns 1ULL << sq2 if the squares are not on the same line. [[nodiscard]] inline Bitboard between(Square sq1, Square sq2) noexcept { return SQUARES_BETWEEN_BB[sq1][sq2]; } } // namespace chess::movegen diff --git a/moves_io.cpp b/moves_io.cpp index f794881..f31e9b7 100644 --- a/moves_io.cpp +++ b/moves_io.cpp @@ -1,497 +1,529 @@ -/* - a chess library (bonus: you can integrate more piece types!) which - supports Chess960 and is decently fast enough - Copyright (C) 2025-2026 winapiadmin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ -// UCI moves parsing - -// License: https://github.com/Disservin/chess-library/blob/master/LICENSE -#include "moves_io.h" -#include "position.h" -#include "types.h" -#include -#include -#if defined(__EXCEPTIONS) -#define THROW_IF_EXCEPTIONS_ON(stuff) throw stuff -#else -#define THROW_IF_EXCEPTIONS_ON(stuff) ((void)0) -#endif -namespace chess { -namespace uci { -std::string squareToString(Square sq) { - constexpr std::string_view fileChars[65] = { - "a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1", "a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2", "a3", - "b3", "c3", "d3", "e3", "f3", "g3", "h3", "a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4", "a5", "b5", - "c5", "d5", "e5", "f5", "g5", "h5", "a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6", "a7", "b7", "c7", - "d7", "e7", "f7", "g7", "h7", "a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8", "none" - }; - return std::string{ fileChars[sq] }; -} -std::string moveToUci(Move mv, bool chess960) { - if (!mv.is_ok()) { - // null move - static const std::string nullMove = "0000"; - return nullMove; - } - constexpr char PieceTypeChar[] = " pnbrqk"; - static thread_local std::string move; - move.clear(); - // Source square - move += squareToString(mv.from_sq()); - // To square, special: castlings - switch (mv.type_of()) { - case CASTLING: { - if (chess960) - move += squareToString(mv.to_sq()); - else { - switch (mv.to_sq()) { - case SQ_H1: - move += "g1"; // White kingside castling - break; - case SQ_A1: - move += "c1"; // white queenside castling - break; - case SQ_H8: - move += "g8"; // black kingside castling - break; - case SQ_A8: - move += "c8"; // black queenside castling - break; - default: -#if defined(_DEBUG) || !defined(NDEBUG) - assert(false && "this isn't chess960"); -#else - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("this isn't chess960")); - break; -#endif - } - } - } break; - case PROMOTION: - move += squareToString(mv.to_sq()); - move += PieceTypeChar[mv.promotion_type()]; - break; - default: - move += squareToString(mv.to_sq()); - break; - } - return move; -} -template Move uciToMove(const _Position &pos, std::string_view uci) { - if (uci.length() < 4) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("example: a2a4 or d7d8q")); - return Move::NO_MOVE; - } - - Square source = parse_square(uci.substr(0, 2)); - Square target = parse_square(uci.substr(2, 2)); - - if (!is_valid(source) || !is_valid(target)) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("source !in [a1, h8], target !in [a1, h8]")); - return Move::NO_MOVE; - } - auto move = (uci.length() == 4) ? Move::make(source, target) : Move::NO_MOVE; - auto pt = piece_of(pos.at(source)); - if (pt == NO_PIECE_TYPE) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("source need to be a existing piece, got nothing")); - return Move::NO_MOVE; - } - // castling in chess960 - if (pos.chess960() && pt == PieceType::KING && pos.template at(target) == PieceType::ROOK && - pos.template at(target) == pos.sideToMove()) { - move = Move::make(source, target); - } - - // convert to king captures rook - // in chess960 the move should be sent as king captures rook already! - else if (!pos.chess960() && pt == PieceType::KING && square_distance(target, source) == 2) { - target = make_sq(target > source ? File::FILE_H : File::FILE_A, rank_of(source)); - move = Move::make(source, target); - } - // en passant - else if (pt == PAWN && target == pos.enpassantSq()) { - move = Move::make(source, target); - } - - // promotion - else if (pt == PAWN && uci.length() == 5 && (rank_of(target) == (pos.sideToMove() == WHITE ? RANK_8 : RANK_1))) { - auto promotion = parse_pt(uci[4]); - - if (promotion == NO_PIECE_TYPE || promotion == KING || promotion == PAWN) { -#if defined(_DEBUG) || !defined(NDEBUG) - assert(false && "promotions: [NRBQ]"); -#else - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("promotions: [NRBQ]")); -#endif - return Move::NO_MOVE; - } - - move = Move::make(source, target, promotion); - } - Movelist moves; - pos.legals(moves); - auto it = std::find(moves.begin(), moves.end(), move); -#if defined(_DEBUG) || !defined(NDEBUG) - assert(it != moves.end() && "Move is illegal"); -#else - if (it == moves.end()) - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("Move is illegal")); -#endif - return move; -} -template 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(); - Movelist moves; - pos.legals(moves); - - // Make a local mutable copy we can trim safely. - std::string san(input_san), _san(raw_san); - - // 1) Castling shortcuts - if (san == "O-O" || san == "0-0" || san == "O-O+" || san == "0-0+" || san == "O-O#" || san == "0-0#") { - const auto from = pos.kingSq(pos.side_to_move()); - const auto to = pos.getCastlingMetadata(pos.sideToMove()).rook_start_ks; - Move km = chess::Move::make(from, to); - - if (std::find(moves.begin(), moves.end(), km) != moves.end()) - return km; - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); - return Move::none(); - } - if (san == "O-O-O" || san == "0-0-0" || san == "O-O-O+" || san == "0-0-0+" || san == "O-O-O#" || san == "0-0-0#") { - const auto from = pos.kingSq(pos.side_to_move()); - const auto to = pos.getCastlingMetadata(pos.sideToMove()).rook_start_qs; - Move qm = chess::Move::make(from, to); - - if (std::find(moves.begin(), moves.end(), qm) != moves.end()) - return qm; - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); - return Move::none(); - } - // 2) Strip trailing annotations (+, #) that aren't required in the standard (except "e.p. "). Repeated occurrences too. - while (!san.empty()) { - char c = san.back(); - if (c == '+' || c == '#') - san.pop_back(); - else - break; - } - if (san.empty()) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); - return Move::none(); - } - - // 3) Extract promotion if present (e.g. c8=Q or c8Q) - PieceType promotion = NO_PIECE_TYPE; - if (san.size() >= 3) { - // look for "=Q" or similar at the very end, or single letter promotion (historical) - char penult = san[san.size() - 2]; - char last = san.back(); - if (penult == '=') { - promotion = parse_pt(last); - if (promotion == NO_PIECE_TYPE) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - san.pop_back(); // remove piece letter - san.pop_back(); // remove '=' - } else if ((last == 'Q' || last == 'R' || last == 'B' || last == 'N' || last == 'q' || last == 'r' || last == 'b' || - last == 'n')) { - // allow c8Q or c8q as shorthand (optional) - promotion = parse_pt(last); - san.pop_back(); - } - } - - // 4) Destination square: always the last [file][rank] - if (san.size() < 2) - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - char dfile = san[san.size() - 2]; - char drank = san[san.size() - 1]; - if (!(dfile >= 'a' && dfile <= 'h' && drank >= '1' && drank <= '8')) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - std::string dest_sq_str = san.substr(san.size() - 2, 2); - Square to_square = parse_square(dest_sq_str); - if (to_square == SQ_NONE) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - san.resize(san.size() - 2); // chop off destination - - // 5) Now san contains everything before the dest: - // possible piece letter, possible source square, possible disambiguation, - // optional 'x' capture markers (we will ignore 'x'). - // Remove all 'x' characters (capture indicators) from the remainder - std::string prefix; - prefix.reserve(san.size()); - for (char c : san) - if (c != 'x' && c != 'X') - prefix.push_back(c); - // prefix now holds the pre-destination token (e.g. "Nbd" from "Nbd2" or "Pe2" from "Pe2e4") - - // 6) Detect a fully specified source square at the end of prefix (LAN) - bool has_src_square = false; - Square src_square = SQ_NONE; - if (prefix.size() >= 2) { - char sfile = prefix[prefix.size() - 2]; - char srank = prefix[prefix.size() - 1]; - if (sfile >= 'a' && sfile <= 'h' && srank >= '1' && srank <= '8') { - // consume it - std::string src_sq_str = prefix.substr(prefix.size() - 2, 2); - src_square = parse_square(src_sq_str); - if (src_square == SQ_NONE) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - prefix.resize(prefix.size() - 2); - } - } - has_src_square = src_square != SQ_NONE; - // 7) Detect piece letter at front if present - PieceType piece_type = NO_PIECE_TYPE; - if (!prefix.empty()) { - char front = prefix.front(); - PieceType pt = parse_pt(front); - if (pt != NO_PIECE_TYPE) { - piece_type = pt; - // remove leading piece letter - prefix.erase(prefix.begin()); - } - } - // If no explicit piece letter, it's a pawn move - if (piece_type == NO_PIECE_TYPE) - piece_type = PAWN; - - // 8) The remaining prefix is disambiguation: can be file, rank, or file+rank (rare) - int dis_file = -1; // 0..7 or -1 - int dis_rank = -1; // 0..7 or -1 - for (char c : prefix) { - if (c >= 'a' && c <= 'h') - dis_file = c - 'a'; - else if (c >= '1' && c <= '8') - dis_rank = c - '1'; - else { - // unexpected char in prefix - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - } - - // 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 - - // If pawn and no disambiguation file, restrict pawns to dest file (avoid ambiguous pawn non-file forms) - // This matches python-chess behavior described earlier. - Bitboard from_mask = ~0ULL; - if (piece_type == PAWN) { - from_mask &= pos.pieces(PAWN, pos.side_to_move()); - if (dis_file == -1 && !has_src_square) { - // restrict to same file as destination (non-capture pawns must be on same file) - int dest_file = file_of(to_square); - from_mask &= attacks::MASK_FILE[dest_file]; - } - } else { - from_mask &= pos.pieces(piece_type, pos.side_to_move()); - } - - // Additional disambiguation masks: - if (dis_file != -1) - from_mask &= attacks::MASK_FILE[dis_file]; - if (dis_rank != -1) - from_mask &= attacks::MASK_RANK[dis_rank]; - if (has_src_square) { - // If fully specified source given, narrow to that square only. - from_mask &= (1ULL << src_square); - } - - for (Move m : moves) { - // match destination - if (m.to_sq() != to_square) - continue; - - // match promotion - if (promotion != NO_PIECE_TYPE) { - if (m.type_of() != PROMOTION || m.promotion_type() != promotion) - continue; - } else { - // if move is promotion but SAN lacked piece, reject (require explicit promotion) - if (m.type_of() == PROMOTION) - continue; - } - - // match piece type: check the piece that is on m.from_sq() in pos - PieceType src_pt = piece_of(pos.piece_on(m.from_sq())); - if (src_pt != piece_type) - continue; - - // match from_mask (disambiguation and pawn filtering) - if (((1ULL << m.from_sq()) & from_mask) == 0) - continue; - - // Everything matches -> accept candidate - if (found) { - THROW_IF_EXCEPTIONS_ON(AmbiguousMoveException("ambiguous san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - matched = m; - found = true; - } - - if (!found) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - - return matched; - }; - - if (remove_illegals) { - std::string trimmed_san(raw_san); - while (!trimmed_san.empty()) { - try { - Move attempt = do_parse(trimmed_san); - if (attempt.is_ok()) - return attempt; - } catch (...) { - } - trimmed_san.pop_back(); - } - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("illegal san: '" + std::string(raw_san) + "' in " + pos.fen())); - return Move::none(); - } else { - return do_parse(raw_san); - } -} -template std::string moveToSan(const _Position &pos, Move move, bool long_, bool suffix) { - constexpr char FILE_NAMES[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }; - - constexpr char PieceTypeChar[] = " pnbrqk"; - // Null move. (or none) - if (!move.is_ok()) { - return "--"; - } - - std::string san; - PieceType piece_type = piece_of(pos.at(move.from_sq())); - bool capture = pos.is_capture(move); - // Castling. - if (pos.is_castling(move)) { - if (file_of(move.to_sq()) < file_of(move.from_sq())) { - san = "O-O-O"; - goto appendCheck; - } else { - san = "O-O"; - goto appendCheck; - } - } - if (piece_type == NO_PIECE_TYPE) { - THROW_IF_EXCEPTIONS_ON(IllegalMoveException("moveToSan() expect move to be pseudo-legal or null, but got " + - moveToUci(move) + " in " + pos.fen())); - return ""; - } - - if (piece_type != PAWN) { - san = std::toupper(PieceTypeChar[piece_type]); - } - if (long_) { - san += squareToString(move.from_sq()); - } else if (piece_type != PAWN) { - // Get ambiguous move candidates. - // Relevant candidates: not exactly the current move, - // but to the same square. - Movelist moves; - pos.legals(moves); - Bitboard others = 0; - Bitboard from_mask = pos.pieces(piece_type, pos.side_to_move()); - from_mask &= ~(1ULL << move.from_sq()); - Bitboard to_mask = 1ULL << move.to_sq(); - for (const Move &candidate : moves) { - Bitboard cand_from_bb = 1ULL << candidate.from_sq(); - // Only consider other pieces of same type that can move to the same destination. - if ((cand_from_bb & from_mask) && ((1ULL << candidate.to_sq()) & to_mask)) - others |= cand_from_bb; - } - - // Disambiguate only if there are other candidates that can move to the same square. - if (others) { - const char RANK_NAMES[] = { '1', '2', '3', '4', '5', '6', '7', '8' }; - bool need_file = false, need_rank = false; - for (Square sq = SQ_A1; sq < SQ_NONE; ++sq) { - if (others & (1ULL << sq)) { - if (file_of(sq) == file_of(move.from_sq())) - need_rank = true; - if (rank_of(sq) == rank_of(move.from_sq())) - need_file = true; - } - } - // If neither shares file nor rank, include file by default. - if (!need_file && !need_rank) - need_file = true; - if (need_file) - san += FILE_NAMES[file_of(move.from_sq())]; - if (need_rank) - san += RANK_NAMES[rank_of(move.from_sq())]; - } - } else if (capture) { - san += FILE_NAMES[file_of(move.from_sq())]; - } - - // Captures. - if (capture) { - san += "x"; - } else if (long_) { - san += "-"; - } - - // Destination square. - san += squareToString(move.to_sq()); - - // Promotion. - if (move.type_of() == PROMOTION) { - san += "=" + std::string(1, std::toupper(PieceTypeChar[move.promotion_type()])); - } -appendCheck: - if (!suffix) - return san; - _Position p = pos; - p.doMove(move); - const bool _check = p.is_check(); - Movelist moves; - p.legals(moves); - // Checkmate: no legal moves and in check; Stalemate: no legal moves and not in check - if (moves.size() == 0 && _check) - san += "#"; - else if (_check) - san += "+"; - return san; -} -#define INSTANTITATE(PieceC) \ - template Move uciToMove(const _Position &, std::string_view); \ - template Move parseSan(const _Position &, std::string_view, bool); \ - template std::string moveToSan(const _Position &, Move, bool, bool); -INSTANTITATE(PolyglotPiece) -INSTANTITATE(EnginePiece) -INSTANTITATE(ContiguousMappingPiece) -#undef INSTANTITATE -} // namespace uci -std::string Move::uci() const { return uci::moveToUci(*this); } -} // namespace chess +/* + a chess library (bonus: you can integrate more piece types!) which + supports Chess960 and is decently fast enough + Copyright (C) 2025-2026 winapiadmin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +// UCI moves parsing + +// License: https://github.com/Disservin/chess-library/blob/master/LICENSE + +/// @file moves_io.cpp +/// @brief UCI move parsing and conversion (moveToUci, uciToMove). + +#include "moves_io.h" +#include "position.h" +#include "types.h" +#include +#include +#if defined(_CHESSLIB_ERROR_MODE_THROW) +#define INVALID_ARG_IF(c, exception) \ + do { \ + if (c) \ + throw(exception); \ + } while (0) +#elif defined(_CHESSLIB_ERROR_MODE_ASSERT) +#define INVALID_ARG_IF(c, exception) \ + do { \ + assert(!(c) && #exception); \ + } while (0) +#elif defined(_DEBUG) && !defined(NDEBUG) +#include +#define INVALID_ARG_IF(c, exception) \ + do { \ + if (c) \ + std::cerr << #c << ", message: " << #exception << " (at " << __FILE__ << ":" << __LINE__ << ")\n"; \ + } while (0) +#else +#define INVALID_ARG_IF(c, exception) \ + do { \ + (void)(c); \ + } while (0) +#endif +namespace chess { +namespace uci { +/// @brief Convert a Square to algebraic notation string (e.g. 0 -> "a1"). +std::string squareToString(Square sq) { + constexpr std::string_view fileChars[65] = { + "a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1", "a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2", "a3", + "b3", "c3", "d3", "e3", "f3", "g3", "h3", "a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4", "a5", "b5", + "c5", "d5", "e5", "f5", "g5", "h5", "a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6", "a7", "b7", "c7", + "d7", "e7", "f7", "g7", "h7", "a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8", "none" + }; + return std::string{ fileChars[sq] }; +} +/// @brief Convert a Move to UCI string representation. +std::string moveToUci(Move mv, bool chess960) { + if (!mv.is_ok()) { + // null move + static const std::string nullMove = "0000"; + return nullMove; + } + constexpr char PieceTypeChar[] = " pnbrqk"; + static thread_local std::string move; + move.clear(); + // Source square + move += squareToString(mv.from_sq()); + // To square, special: castlings + switch (mv.type_of()) { + case CASTLING: { + if (chess960) + move += squareToString(mv.to_sq()); + else { + switch (mv.to_sq()) { + case SQ_H1: + move += "g1"; // White kingside castling + break; + case SQ_A1: + move += "c1"; // white queenside castling + break; + case SQ_H8: + move += "g8"; // black kingside castling + break; + case SQ_A8: + move += "c8"; // black queenside castling + break; + default: + INVALID_ARG_IF(true, std::runtime_error("This isn't Chess960")); + return {}; + } + } + } break; + case PROMOTION: + move += squareToString(mv.to_sq()); + move += PieceTypeChar[mv.promotion_type()]; + break; + default: + move += squareToString(mv.to_sq()); + break; + } + return move; +} +/// @brief Convert a UCI string (e.g. "e2e4") to a Move object. +template Move uciToMove(const _Position &pos, std::string_view uci) { + if (uci.length() < 4) { + INVALID_ARG_IF(uci.length() < 4, IllegalMoveException("example: a2a4 or d7d8q")); + return Move::NO_MOVE; + } + + Square source = parse_square(uci.substr(0, 2)); + Square target = parse_square(uci.substr(2, 2)); + + if (!is_valid(source) || !is_valid(target)) { + INVALID_ARG_IF(!is_valid(source) || !is_valid(target), + IllegalMoveException("source !in [a1, h8], target !in [a1, h8]")); + return Move::NO_MOVE; + } + auto move = (uci.length() == 4) ? Move::make(source, target) : Move::NO_MOVE; + auto pt = piece_of(pos.at(source)); + if (pt == NO_PIECE_TYPE) { + INVALID_ARG_IF(pt == NO_PIECE_TYPE, IllegalMoveException("source need to be a existing piece, got nothing")); + return Move::NO_MOVE; + } + // castling in chess960 + if (pos.chess960() && pt == PieceType::KING && pos.template at(target) == PieceType::ROOK && + pos.template at(target) == pos.side_to_move()) { + move = Move::make(source, target); + } + + // convert to king captures rook + // in chess960 the move should be sent as king captures rook already! + else if (!pos.chess960() && pt == PieceType::KING && square_distance(target, source) == 2) { + target = make_sq(target > source ? File::FILE_H : File::FILE_A, rank_of(source)); + move = Move::make(source, target); + } + // en passant + else if (pt == PAWN && target == pos.ep_square()) { + move = Move::make(source, target); + } + + // promotion + else if (pt == PAWN && uci.length() == 5 && (rank_of(target) == (pos.side_to_move() == WHITE ? RANK_8 : RANK_1))) { + auto promotion = parse_pt(uci[4]); + + if (promotion == NO_PIECE_TYPE || promotion == KING || promotion == PAWN) { + INVALID_ARG_IF(promotion == NO_PIECE_TYPE || promotion == KING || promotion == PAWN, + IllegalMoveException("promotions: [NRBQ]")); + return Move::NO_MOVE; + } + + move = Move::make(source, target, promotion); + } + Movelist moves; + pos.legals(moves); + auto it = std::find(moves.begin(), moves.end(), move); + if (it == moves.end()) { + INVALID_ARG_IF(true, IllegalMoveException("Move is illegal")); + return Move::NO_MOVE; + } + 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) { + auto do_parse = [&](std::string_view input_san) -> Move { + if (input_san.empty()) + return Move::none(); + Movelist moves; + pos.legals(moves); + + // Make a local mutable copy we can trim safely. + std::string san(input_san), _san(raw_san); + + // 1) Castling shortcuts + if (san == "O-O" || san == "0-0" || san == "O-O+" || san == "0-0+" || san == "O-O#" || san == "0-0#") { + const auto from = pos.king_sq(pos.side_to_move()); + const auto to = pos.get_castling_metadata(pos.side_to_move()).rook_start_ks; + Move km = chess::Move::make(from, to); + + if (std::find(moves.begin(), moves.end(), km) != moves.end()) + return km; + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); + return Move::none(); + } + if (san == "O-O-O" || san == "0-0-0" || san == "O-O-O+" || san == "0-0-0+" || san == "O-O-O#" || san == "0-0-0#") { + const auto from = pos.king_sq(pos.side_to_move()); + const auto to = pos.get_castling_metadata(pos.side_to_move()).rook_start_qs; + Move qm = chess::Move::make(from, to); + + if (std::find(moves.begin(), moves.end(), qm) != moves.end()) + return qm; + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); + return Move::none(); + } + // 2) Strip trailing annotations (+, #) that aren't required in the standard (except "e.p. "). Repeated occurrences too. + while (!san.empty()) { + char c = san.back(); + if (c == '+' || c == '#') + san.pop_back(); + else + break; + } + if (san.empty()) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); + return Move::none(); + } + + // 3) Extract promotion if present (e.g. c8=Q or c8Q) + PieceType promotion = NO_PIECE_TYPE; + if (san.size() >= 3) { + // look for "=Q" or similar at the very end, or single letter promotion (historical) + char penult = san[san.size() - 2]; + char last = san.back(); + if (penult == '=') { + promotion = parse_pt(last); + if (promotion == NO_PIECE_TYPE) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + san.pop_back(); // remove piece letter + san.pop_back(); // remove '=' + } else if ((last == 'Q' || last == 'R' || last == 'B' || last == 'N' || last == 'q' || last == 'r' || last == 'b' || + last == 'n')) { + // allow c8Q or c8q as shorthand (optional) + promotion = parse_pt(last); + san.pop_back(); + } + } + + // 4) Destination square: always the last [file][rank] + if (san.size() < 2) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + char dfile = san[san.size() - 2]; + char drank = san[san.size() - 1]; + if (!(dfile >= 'a' && dfile <= 'h' && drank >= '1' && drank <= '8')) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + std::string dest_sq_str = san.substr(san.size() - 2, 2); + Square to_square = parse_square(dest_sq_str); + if (to_square == SQ_NONE) { + if (!remove_illegals) + INVALID_ARG_IF(to_square == SQ_NONE, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + san.resize(san.size() - 2); // chop off destination + + // 5) Now san contains everything before the dest: + // possible piece letter, possible source square, possible disambiguation, + // optional 'x' capture markers (we will ignore 'x'). + // Remove all 'x' characters (capture indicators) from the remainder + std::string prefix; + prefix.reserve(san.size()); + for (char c : san) + if (c != 'x' && c != 'X') + prefix.push_back(c); + // prefix now holds the pre-destination token (e.g. "Nbd" from "Nbd2" or "Pe2" from "Pe2e4") + + // 6) Detect a fully specified source square at the end of prefix (LAN) + bool has_src_square = false; + Square src_square = SQ_NONE; + if (prefix.size() >= 2) { + char sfile = prefix[prefix.size() - 2]; + char srank = prefix[prefix.size() - 1]; + if (sfile >= 'a' && sfile <= 'h' && srank >= '1' && srank <= '8') { + // consume it + std::string src_sq_str = prefix.substr(prefix.size() - 2, 2); + src_square = parse_square(src_sq_str); + if (src_square == SQ_NONE) { + if (!remove_illegals) + INVALID_ARG_IF(src_square == SQ_NONE, + IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + prefix.resize(prefix.size() - 2); + } + } + has_src_square = src_square != SQ_NONE; + // 7) Detect piece letter at front if present + PieceType piece_type = NO_PIECE_TYPE; + if (!prefix.empty()) { + char front = prefix.front(); + PieceType pt = parse_pt(front); + if (pt != NO_PIECE_TYPE) { + piece_type = pt; + // remove leading piece letter + prefix.erase(prefix.begin()); + } + } + // If no explicit piece letter, it's a pawn move + if (piece_type == NO_PIECE_TYPE) + piece_type = PAWN; + + // 8) The remaining prefix is disambiguation: can be file, rank, or file+rank (rare) + int dis_file = -1; // 0..7 or -1 + int dis_rank = -1; // 0..7 or -1 + for (char c : prefix) { + if (c >= 'a' && c <= 'h') + dis_file = c - 'a'; + else if (c >= '1' && c <= '8') + dis_rank = c - '1'; + else { + // unexpected char in prefix + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + } + + // 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 + + // If pawn and no disambiguation file, restrict pawns to dest file (avoid ambiguous pawn non-file forms) + // This matches python-chess behavior described earlier. + Bitboard from_mask = ~0ULL; + if (piece_type == PAWN) { + from_mask &= pos.pieces(PAWN, pos.side_to_move()); + if (dis_file == -1 && !has_src_square) { + // restrict to same file as destination (non-capture pawns must be on same file) + int dest_file = file_of(to_square); + from_mask &= attacks::MASK_FILE[dest_file]; + } + } else { + from_mask &= pos.pieces(piece_type, pos.side_to_move()); + } + + // Additional disambiguation masks: + if (dis_file != -1) + from_mask &= attacks::MASK_FILE[dis_file]; + if (dis_rank != -1) + from_mask &= attacks::MASK_RANK[dis_rank]; + if (has_src_square) { + // If fully specified source given, narrow to that square only. + from_mask &= (1ULL << src_square); + } + + for (Move m : moves) { + // match destination + if (m.to_sq() != to_square) + continue; + + // match promotion + if (promotion != NO_PIECE_TYPE) { + if (m.type_of() != PROMOTION || m.promotion_type() != promotion) + continue; + } else { + // if move is promotion but SAN lacked piece, reject (require explicit promotion) + if (m.type_of() == PROMOTION) + continue; + } + + // match piece type: check the piece that is on m.from_sq() in pos + PieceType src_pt = piece_of(pos.piece_on(m.from_sq())); + if (src_pt != piece_type) + continue; + + // match from_mask (disambiguation and pawn filtering) + if (((1ULL << m.from_sq()) & from_mask) == 0) + continue; + + // Everything matches -> accept candidate + if (found) { + if (!remove_illegals) + INVALID_ARG_IF(found, AmbiguousMoveException("ambiguous san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + matched = m; + found = true; + } + + if (!found) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + + return matched; + }; + + if (remove_illegals) { + std::string trimmed_san(raw_san); + while (!trimmed_san.empty()) { + Move attempt = do_parse(trimmed_san); + if (attempt.is_ok()) + return attempt; + trimmed_san.pop_back(); + } + INVALID_ARG_IF(trimmed_san.empty(), + IllegalMoveException("illegal san: '" + std::string(raw_san) + "' in " + pos.fen())); + return Move::none(); + } else + return do_parse(raw_san); +} +/// @brief Convert a Move to SAN or LAN (Long Algebraic Notation) string. +template std::string moveToSan(const _Position &pos, Move move, bool long_, bool suffix) { + constexpr char FILE_NAMES[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }; + + constexpr char PieceTypeChar[] = " pnbrqk"; + // Null move. (or none) + if (!move.is_ok()) { + return "--"; + } + + std::string san; + PieceType piece_type = piece_of(pos.at(move.from_sq())); + bool capture = pos.is_capture(move); + // Castling. + if (pos.is_castling(move)) { + if (file_of(move.to_sq()) < file_of(move.from_sq())) { + san = "O-O-O"; + goto appendCheck; + } else { + san = "O-O"; + goto appendCheck; + } + } + if (piece_type == NO_PIECE_TYPE) { + INVALID_ARG_IF(piece_type == NO_PIECE_TYPE, + IllegalMoveException("moveToSan() expect move to be pseudo-legal or null, but got " + moveToUci(move) + + " in " + pos.fen())); + return ""; + } + + if (piece_type != PAWN) { + san = std::toupper(PieceTypeChar[piece_type]); + } + if (long_) { + san += squareToString(move.from_sq()); + } else if (piece_type != PAWN) { + // Get ambiguous move candidates. + // Relevant candidates: not exactly the current move, + // but to the same square. + Movelist moves; + pos.legals(moves); + Bitboard others = 0; + Bitboard from_mask = pos.pieces(piece_type, pos.side_to_move()); + from_mask &= ~(1ULL << move.from_sq()); + Bitboard to_mask = 1ULL << move.to_sq(); + for (const Move &candidate : moves) { + Bitboard cand_from_bb = 1ULL << candidate.from_sq(); + // Only consider other pieces of same type that can move to the same destination. + if ((cand_from_bb & from_mask) && ((1ULL << candidate.to_sq()) & to_mask)) + others |= cand_from_bb; + } + + // Disambiguate only if there are other candidates that can move to the same square. + if (others) { + const char RANK_NAMES[] = { '1', '2', '3', '4', '5', '6', '7', '8' }; + bool need_file = false, need_rank = false; + for (Square sq = SQ_A1; sq < SQ_NONE; ++sq) { + if (others & (1ULL << sq)) { + if (file_of(sq) == file_of(move.from_sq())) + need_rank = true; + if (rank_of(sq) == rank_of(move.from_sq())) + need_file = true; + } + } + // If neither shares file nor rank, include file by default. + if (!need_file && !need_rank) + need_file = true; + if (need_file) + san += FILE_NAMES[file_of(move.from_sq())]; + if (need_rank) + san += RANK_NAMES[rank_of(move.from_sq())]; + } + } else if (capture) { + san += FILE_NAMES[file_of(move.from_sq())]; + } + + // Captures. + if (capture) { + san += "x"; + } else if (long_) { + san += "-"; + } + + // Destination square. + san += squareToString(move.to_sq()); + + // Promotion. + if (move.type_of() == PROMOTION) { + san += "=" + std::string(1, std::toupper(PieceTypeChar[move.promotion_type()])); + } +appendCheck: + if (!suffix) + return san; + _Position p = pos; + p.do_move(move); + const bool _check = p.is_check(); + Movelist moves; + p.legals(moves); + // Checkmate: no legal moves and in check; Stalemate: no legal moves and not in check + if (moves.size() == 0 && _check) + san += "#"; + else if (_check) + san += "+"; + return san; +} +#define INSTANTITATE(PieceC) \ + template Move uciToMove(const _Position &, std::string_view); \ + template Move parseSan(const _Position &, std::string_view, bool); \ + template std::string moveToSan(const _Position &, Move, bool, bool); +INSTANTITATE(PolyglotPiece) +INSTANTITATE(EnginePiece) +INSTANTITATE(ContiguousMappingPiece) +#undef INSTANTITATE +} // namespace uci +std::string Move::uci() const { return uci::moveToUci(*this); } +} // namespace chess diff --git a/moves_io.h b/moves_io.h index 2a00591..5ae6e67 100644 --- a/moves_io.h +++ b/moves_io.h @@ -18,14 +18,29 @@ */ #pragma once #include "fwd_decl.h" +#include "types.h" #include #include #include #include + +/// @file moves_io.h +/// @brief UCI and SAN move conversion functions. + namespace chess::uci { + +/// @brief Convert a Move to UCI coordinate string (e.g. "e2e4", "e7e8q"). +/// @param move The move. +/// @param chess960 Whether to use Chess960 castling encoding. +/// @return UCI string. std::string moveToUci(Move move, bool chess960 = false); +/// @brief Convert a Square to algebraic notation (e.g. "e4"). +/// @param sq The square. +/// @return Two-character string. std::string squareToString(Square sq); + +/// @brief Exception thrown when a SAN string represents an illegal move. class IllegalMoveException : public std::exception { public: IllegalMoveException(const std::string &message) : message_(message) {} @@ -34,6 +49,8 @@ class IllegalMoveException : public std::exception { private: std::string message_; }; + +/// @brief Exception thrown when a SAN string is ambiguous. class AmbiguousMoveException : public std::exception { public: AmbiguousMoveException(const std::string &message) : message_(message) {} @@ -42,9 +59,45 @@ class AmbiguousMoveException : public std::exception { private: std::string message_; }; + +/// @brief Parse a UCI string into a Move for the given position. +/// @tparam T Piece enum type. +/// @tparam P Position tag. +/// @param pos The position. +/// @param uci UCI string (e.g. "e2e4"). +/// @return The parsed Move. template Move uciToMove(const _Position &pos, std::string_view uci); + +/// @brief 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. +template +Move parseSan(const _Position &pos, std::string_view san, bool remove_illegals = false); + +/// @brief Alias for parseSan. template -Move parseSan(const _Position &pos, std::string_view uci, bool remove_illegals = false); +Move parse_san(const _Position &pos, std::string_view san, bool remove_illegals = false) { + return parseSan(pos, san, remove_illegals); +} + +/// @brief Convert a Move to SAN string for the given position. +/// @tparam T Piece enum type. +/// @tparam P Position tag. +/// @param pos The position. +/// @param move The move. +/// @param long_ Use long algebraic notation. +/// @param suffix Include check/mate suffix (+/#). +/// @return SAN string. template std::string moveToSan(const _Position &pos, Move move, bool long_ = false, bool suffix = true); + +/// @brief Alias for moveToSan. +template +std::string move_to_san(const _Position &pos, Move move, bool long_ = false, bool suffix = true) { + return moveToSan(pos, move, long_, suffix); +} } // namespace chess::uci diff --git a/non_core_tests.cpp b/non_core_tests.cpp index d1786cd..c246245 100644 --- a/non_core_tests.cpp +++ b/non_core_tests.cpp @@ -16,15 +16,15 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#define DOCTEST_CONFIG_IMPLEMENT -#ifndef __EXCEPTIONS +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#if !defined(__cpp_exceptions) && !defined(_CPPUNWIND) && !defined(__EXCEPTIONS) && !defined(_CHESSLIB_ERROR_MODE_THROW) #define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS #endif #include "moves_io.h" #include "position.h" #include "printers.h" -#include #include +#include using namespace chess; template struct TestEntry { InputT input; @@ -156,106 +156,106 @@ template void check_was_into_check(std::vector> tests = { - { "3K4/1N1Q4/8/2r4k/6r1/8/1q1Q4/r3P3 w q - 24 70", false }, - { "8/8/3b3K/4N3/4k3/7n/4q3/7B w - - 8 9", true }, - { "N7/1B4P1/4B2n/2Kn4/8/3k4/Pp2r1p1/1p5P w - - 29 85", false }, - { "4b3/3n3N/8/2k5/1q6/1B6/4R3/2K1R3 w - g3 37 3", false }, - { "8/r7/1K3p2/2q5/2R5/6N1/3B4/2p4k w q - 2 89", true }, - { "8/1q3Pr1/2b5/2P2r2/7K/5k2/PP1n4/Q2b3p w K g3 28 35", false }, - { "K7/1r6/4B1p1/7p/3P4/4k3/4R3/1np3b1 w - - 16 84", true }, - { "2R2r2/2k2N2/7K/2r2NR1/1n3b2/PP6/3q3b/B7 w q h3 38 21", true }, - { "2Q5/3b4/1b4Rr/4R1K1/7q/2P2R1q/R5Qp/1bn1k3 w - g3 8 76", true }, - { "4k3/bP1r4/n7/8/2PP2K1/4B3/2N1P1q1/7P w q a6 37 18", false }, - { "1P4N1/2Nb4/1K6/5B2/2N3R1/2k1Nn2/5N2/1p3p2 b q - 41 98", false }, - { "Q4rN1/5R2/R5b1/7r/8/r7/kK5B/7B b K c6 37 73", true }, - { "4k2p/r1q5/7n/1p4P1/1P3PQr/N1n2N2/7K/1N1NqB2 b - f6 27 80", true }, - { "7Q/2p2b2/8/3K4/5Q2/N2B1bb1/5b2/5qk1 w K d3 30 68", false }, - { "3b3K/5p2/2Pp4/2n1R3/5Qb1/7k/6n1/R7 b - a3 23 54", false }, - { "1n1PN1N1/3Pn3/3r4/2b5/6K1/7r/5b2/3k4 w q - 15 51", false }, - { "4N3/4kN2/n2p4/4K3/r1P4R/1p4n1/b3r3/3P1n2 b - - 9 43", true }, - { "1Q2q3/k4bBq/5B1P/8/6q1/r1q3p1/1K6/2PP4 w - - 16 90", true }, - { "Kqq1B2b/3R1Q2/8/k7/6P1/8/1P4R1/8 w - e6 33 9", false }, - { "7K/Nn3b2/r1N1pP2/4R3/5q2/kq6/4p2b/8 w - e3 5 11", false }, - { "n3NK1B/B5kn/8/8/6pB/4P3/8/5R2 b k g3 20 41", true }, - { "Kq3Bq1/6p1/4P3/k7/3R4/8/1Npn4/8 b K - 31 84", true }, - { "7N/1RK1p1nb/5n2/3N4/QQ2pkr1/1r6/3P1P2/8 b - - 44 77", false }, - { "1R2RN1q/nn2Q1k1/5b1Q/3n2n1/2qN3Q/1K1R2B1/8/8 w - - 49 2", true }, - { "8/5bQn/3n2B1/1K2B3/4NN1n/6q1/4R3/2q4k w - - 8 100", false }, - { "8/8/1P4P1/7r/1Q1Q4/2R2P1b/6n1/1RbK1Q1k b - - 44 5", false }, - { "2r2p2/1B6/2r5/5p2/8/3b2q1/2R3qK/k1q5 b - - 11 89", true }, - { "1b1Q4/1rk2qP1/8/R1r4q/4p3/2K4p/5P2/R7 b Q - 23 29", true }, - { "qq3B2/2r4n/3n4/r1Q5/2P5/5b2/1b1k1K2/P2n4 w Q - 49 89", false }, - { "B1bn1R1r/1N5k/2p5/4b2r/6n1/K5B1/2n1n3/1P3N2 b q - 6 8", true }, - { "8/6q1/3nr3/3n4/KQ6/1bp2q2/1Bk5/2r2Q2 w - b3 0 89", false }, - { "4r3/p1n4N/8/8/8/4qnP1/6k1/4K3 b K - 12 83", true }, - { "3b1Nk1/4R1nR/3r4/3P4/R7/K2n4/1qB5/4r2R b q - 6 73", true }, - { "6r1/1Q1B2B1/1bK2QR1/1r6/r7/4p2B/8/2r1k1q1 b - - 44 39", true }, - { "5R2/b7/3p2P1/8/2k5/qK2Q3/7p/8 w K - 33 11", true }, - { "1R3Q1Q/p7/8/4B1pq/6k1/2N1Q3/8/6K1 w - - 35 100", false }, - { "Bp6/PQ5q/8/4k3/7b/2q1Nr2/2K5/2n1q2q w K - 26 14", false }, - { "3p4/4k1Q1/5B1b/n1q5/8/2Q4K/3Q4/4B3 b q - 15 33", false }, - { "p7/p5N1/B2n3Q/K4R1P/3n2q1/7k/P3QR2/8 b k e6 16 50", false }, - { "5Pkq/8/7N/2n5/1N4R1/7K/1Q6/3ppP2 w Q - 9 43", true }, - { "6K1/8/Q4B2/1Q6/b1k5/4q1n1/8/1B5b w K - 41 68", true }, - { "1N6/2k4K/2P3r1/8/3R2N1/5b2/5b2/b3p3 w k - 1 50", false }, - { "3r1K2/1R6/p7/8/1qN3q1/1r6/1k5Q/8 w Q - 27 30", true }, - { "p2B2P1/Q1b5/1Q6/K3k3/8/P4P2/8/8 b q - 42 98", false }, - { "BQ5n/5n2/8/7K/8/2B5/1k6/4n1N1 b K - 3 47", false }, - { "8/n3B3/8/R7/3Q2P1/1rQ2B2/5R2/2K1k2r w k - 46 100", true }, - { "1n1N4/2Qr1k1N/1KR3N1/3Pn3/4Q3/4B3/pn2q3/8 w k - 10 9", true }, - { "2b5/1q2Bbq1/5n2/2R3qR/2kN4/6b1/P1r4Q/K3r2B b k h3 41 49", true }, - { "2q5/4br1R/5Q2/5Q2/2k5/Bnp5/7q/3K4 w - - 18 39", false }, - { "8/3q4/7k/5K2/1q2R1N1/3N4/2N5/3P2p1 w K - 17 21", true }, - { "1P1BP1bK/N5r1/7R/1p3B1Q/6n1/3r4/k1B5/2n1P1q1 b K - 35 23", false }, - { "2bP1P2/2p5/3K3r/4Q3/2b5/4kr2/q7/pq5r w - g6 21 63", true }, - { "K1B5/4P1B1/kN3Q1R/3P1B2/2q1QN2/8/8/2P5 w Q - 16 2", true }, - { "7N/2b1p2P/1RK5/Nbqn1P2/3p2p1/2qr4/P1nP2k1/P1p1b3 w - d3 9 95", false }, - { "2B3p1/2R3b1/8/p7/8/Kprk4/3b2b1/2Q5 b - b6 15 77", false }, - { "1r2b3/2R5/1k1p2PQ/7N/3p2R1/P1R1K3/1bq5/R7 w - d6 41 72", false }, - { "3rq3/2p4K/4n3/5nn1/k7/8/2n1Q3/3P1q2 b k - 35 89", true }, - { "K3R3/1P4NB/1q3R2/1k6/1qn2B2/3pr3/3q1B2/4b3 b - - 18 88", false }, - { "N1r5/3nP3/bR2Q3/2RB3N/7k/3K4/b7/nQ5b w k - 31 78", false }, - { "b7/8/3b2P1/3kr3/4R3/6K1/3q2b1/1q2r3 w K - 28 28", false }, - { "2B1p2n/8/P7/Nn3b1K/1q6/N2p1kR1/8/8 b q - 0 36", false }, - { "1RK3p1/2BNr3/6R1/6q1/8/b2Pq3/8/2RR1P1k w K - 24 3", false }, - { "5P2/5Q2/8/2K2rb1/n3p1p1/6b1/2qn4/k4p2 b K - 15 60", true }, - { "2b1p3/5N2/2NNR1R1/2q5/8/8/qNQ4k/1P5K w k - 11 18", true }, - { "5B2/3q3K/1k3n2/R5r1/1Q2bR2/4P1b1/3B1b2/3pR3 w K - 2 22", true }, - { "4K3/8/1r6/3kNQp1/7b/Rr3B2/rP3nq1/1nP1q3 b Q h6 4 85", false }, - { "Q2rKN2/3Pp3/n5k1/4nPN1/3r3Q/1N1r4/5p2/1BBR1B1B w q - 6 18", true }, - { "1NK5/3k1Q2/1B2N1Q1/7p/8/8/5RNN/6Q1 w - g3 16 90", true }, - { "8/4Q3/6QP/3k4/bN2K3/4Q3/5b2/8 w k - 33 38", true }, - { "6K1/6q1/QnRb4/1Np4Q/7R/3Q4/3B1q2/5k2 b q - 29 11", true }, - { "5n2/4q3/4r3/b7/8/1Q5k/5KRB/1q2r3 b - d6 20 39", false }, - { "1R4q1/8/K2B4/5Q2/1N6/Q2qk3/8/8 w - - 10 61", false }, - { "1q1k4/4q3/8/Q1n5/3b3B/2Qp3b/R6K/5n1n b K - 47 62", true }, - { "2PB4/q3BR2/8/N3k3/4P3/4P3/7K/1N2P3 w k c6 22 24", false }, - { "6Pp/5N2/8/3QN3/1B6/p7/6kK/Q7 b K - 29 24", true }, - { "8/R7/4pk2/5R2/1BK3R1/6Q1/1P1Q4/8 w Q - 10 75", true }, - { "8/6p1/b3QN2/5k2/8/p6P/7r/K7 b - f6 18 46", false }, - { "3Q4/5P2/7N/3N2K1/8/3rk1bn/7b/1NQ1Q3 b k - 17 42", true }, - { "5B2/1P2k3/2n3K1/5bn1/5R1P/p3Bp2/3N4/4n3 w - - 6 94", true }, - { "4P3/4n1Q1/6b1/5k2/5N2/3n3B/7p/q2B2K1 w - - 16 65", true }, - { "8/8/1k3r2/Nq6/7n/8/8/3K1b2 b - - 27 10", false }, - { "8/5N2/1K6/8/8/2Bq4/k7/3b2qp b K f3 39 75", true }, - { "kbK1R3/2n2q2/7n/1p6/1r1bP3/2p5/nB5B/2N2b2 w - - 12 68", false }, - { "8/3b4/2k1pn2/2P4n/2qQp3/1nnb4/r7/KB1Q4 b - - 12 11", true }, - { "r3B3/1bb2k2/p7/3K2Q1/8/6bp/6Nb/2p2nB1 b - - 22 61", true }, - { "1b6/4p2k/6p1/1N1Q1n2/1pK1r2B/1q2b3/p3Rp2/8 w k - 30 99", false }, - { "Q2k1B2/8/3rb3/8/2K5/8/1N6/8 w Q - 43 73", true }, - { "8/1r4Nk/2R5/3Q1P2/8/6Q1/7P/6K1 w K - 27 13", false }, - { "5k2/2b5/p7/5B2/1K2n3/6n1/1b4q1/3r1b2 b K - 28 91", false }, - { "5r2/1N6/8/5N1r/4n2k/2B2K2/1RR1NB2/2p1BR2 w - f6 8 12", true }, - { "5N2/8/5K2/8/2B2P2/5nr1/1Q1B2k1/1Qr1n3 b - - 29 100", false }, - { "8/3b4/3p4/1K5n/3Q1Qn1/2n5/k1p1Rq1n/3N4 b Q - 39 12", true }, - { "2Q5/3n4/1Rq1KN2/3P4/7P/1b3B2/3k3q/8 b - - 15 47", true }, - { "3bn3/4Q1p1/2r3q1/2Q2k2/2QKr3/3R4/8/2p2R2 w Q - 28 77", true }, - { "n7/1n4p1/2KB2b1/4q1R1/QR6/1kb3p1/8/2n2b2 b K - 24 73", false }, - { "3R2k1/QpK2R1p/N6P/RP2r3/q4b2/8/1R6/2r5 w q - 7 44", true }, - { "8/k1R2Q2/3r4/2QP3B/8/6B1/3K4/n1b2Q2 b Q - 18 53", true }, - { "p7/2n3PQ/3p1K2/N2b4/2n2bk1/Q7/B1Q5/2q5 b - - 47 54", false }, - { "b2Q1q2/1r6/2R5/B1R3q1/p2K1kR1/4q3/8/1r3b2 b - - 15 92", true }, - { "pB6/4q1nk/7n/4p2B/1N3K2/P7/rn2bRb1/2r5 w - - 7 57", false }, + { "3K4/1N1Q4/8/2r4k/6r1/8/1q1Q4/r3P3 w q - 24 70", false }, + { "8/8/3b3K/4N3/4k3/7n/4q3/7B w - - 8 9", true }, + { "N7/1B4P1/4B2n/2Kn4/8/3k4/Pp2r1p1/1p5P w - - 29 85", false }, + { "4b3/3n3N/8/2k5/1q6/1B6/4R3/2K1R3 w - - 37 3", false }, + { "8/r7/1K3p2/2q5/2R5/6N1/3B4/2p4k w - - 2 89", true }, + { "8/1q3Pr1/2b5/2P2r2/7K/5k2/PP1n4/Q2b3p w K g3 28 35", false }, + { "K7/1r6/4B1p1/7p/3P4/4k3/4R3/1np3b1 w - - 16 84", true }, + { "2R2r2/2k2N2/7K/2r2NR1/1n3b2/PP6/3q3b/B7 w - - 38 21", true }, + { "2Q5/3b4/1b4Rr/4R1K1/7q/2P2R1q/R5Qp/1bn1k3 w - - 8 76", true }, + { "4k3/bP1r4/n7/8/2PP2K1/4B3/2N1P1q1/7P w - - 37 18", false }, + { "1P4N1/2Nb4/1K6/5B2/2N3R1/2k1Nn2/5N2/1p3p2 b - - 41 98", false }, + { "Q4rN1/5R2/R5b1/7r/8/r7/kK5B/7B b K c6 37 73", true }, + { "4k2p/r1q5/7n/1p4P1/1P3PQr/N1n2N2/7K/1N1NqB2 b - - 27 80", true }, + { "7Q/2p2b2/8/3K4/5Q2/N2B1bb1/5b2/5qk1 w K d3 30 68", false }, + { "3b3K/5p2/2Pp4/2n1R3/5Qb1/7k/6n1/R7 b - - 23 54", false }, + { "1n1PN1N1/3Pn3/3r4/2b5/6K1/7r/5b2/3k4 w - - 15 51", false }, + { "4N3/4kN2/n2p4/4K3/r1P4R/1p4n1/b3r3/3P1n2 b - - 9 43", true }, + { "1Q2q3/k4bBq/5B1P/8/6q1/r1q3p1/1K6/2PP4 w - - 16 90", true }, + { "Kqq1B2b/3R1Q2/8/k7/6P1/8/1P4R1/8 w - - 33 9", false }, + { "7K/Nn3b2/r1N1pP2/4R3/5q2/kq6/4p2b/8 w - - 5 11", false }, + { "n3NK1B/B5kn/8/8/6pB/4P3/8/5R2 b k g3 20 41", true }, + { "Kq3Bq1/6p1/4P3/k7/3R4/8/1Npn4/8 b K - 31 84", true }, + { "7N/1RK1p1nb/5n2/3N4/QQ2pkr1/1r6/3P1P2/8 b - - 44 77", false }, + { "1R2RN1q/nn2Q1k1/5b1Q/3n2n1/2qN3Q/1K1R2B1/8/8 w - - 49 2", true }, + { "8/5bQn/3n2B1/1K2B3/4NN1n/6q1/4R3/2q4k w - - 8 100", false }, + { "8/8/1P4P1/7r/1Q1Q4/2R2P1b/6n1/1RbK1Q1k b - - 44 5", false }, + { "2r2p2/1B6/2r5/5p2/8/3b2q1/2R3qK/k1q5 b - - 11 89", true }, + { "1b1Q4/1rk2qP1/8/R1r4q/4p3/2K4p/5P2/R7 b Q - 23 29", true }, + { "qq3B2/2r4n/3n4/r1Q5/2P5/5b2/1b1k1K2/P2n4 w Q - 49 89", false }, + { "B1bn1R1r/1N5k/2p5/4b2r/6n1/K5B1/2n1n3/1P3N2 b - - 6 8", true }, + { "8/6q1/3nr3/3n4/KQ6/1bp2q2/1Bk5/2r2Q2 w - - 0 89", false }, + { "4r3/p1n4N/8/8/8/4qnP1/6k1/4K3 b K - 12 83", true }, + { "3b1Nk1/4R1nR/3r4/3P4/R7/K2n4/1qB5/4r2R b - - 6 73", true }, + { "6r1/1Q1B2B1/1bK2QR1/1r6/r7/4p2B/8/2r1k1q1 b - - 44 39", true }, + { "5R2/b7/3p2P1/8/2k5/qK2Q3/7p/8 w K - 33 11", true }, + { "1R3Q1Q/p7/8/4B1pq/6k1/2N1Q3/8/6K1 w - - 35 100", false }, + { "Bp6/PQ5q/8/4k3/7b/2q1Nr2/2K5/2n1q2q w K - 26 14", false }, + { "3p4/4k1Q1/5B1b/n1q5/8/2Q4K/3Q4/4B3 b - - 15 33", false }, + { "p7/p5N1/B2n3Q/K4R1P/3n2q1/7k/P3QR2/8 b k e6 16 50", false }, + { "5Pkq/8/7N/2n5/1N4R1/7K/1Q6/3ppP2 w Q - 9 43", true }, + { "6K1/8/Q4B2/1Q6/b1k5/4q1n1/8/1B5b w K - 41 68", true }, + { "1N6/2k4K/2P3r1/8/3R2N1/5b2/5b2/b3p3 w k - 1 50", false }, + { "3r1K2/1R6/p7/8/1qN3q1/1r6/1k5Q/8 w Q - 27 30", true }, + { "p2B2P1/Q1b5/1Q6/K3k3/8/P4P2/8/8 b - - 42 98", false }, + { "BQ5n/5n2/8/7K/8/2B5/1k6/4n1N1 b K - 3 47", false }, + { "8/n3B3/8/R7/3Q2P1/1rQ2B2/5R2/2K1k2r w k - 46 100", true }, + { "1n1N4/2Qr1k1N/1KR3N1/3Pn3/4Q3/4B3/pn2q3/8 w k - 10 9", true }, + { "2b5/1q2Bbq1/5n2/2R3qR/2kN4/6b1/P1r4Q/K3r2B b k h3 41 49", true }, + { "2q5/4br1R/5Q2/5Q2/2k5/Bnp5/7q/3K4 w - - 18 39", false }, + { "8/3q4/7k/5K2/1q2R1N1/3N4/2N5/3P2p1 w K - 17 21", true }, + { "1P1BP1bK/N5r1/7R/1p3B1Q/6n1/3r4/k1B5/2n1P1q1 b K - 35 23", false }, + { "2bP1P2/2p5/3K3r/4Q3/2b5/4kr2/q7/pq5r w - - 21 63", true }, + { "K1B5/4P1B1/kN3Q1R/3P1B2/2q1QN2/8/8/2P5 w Q - 16 2", true }, + { "7N/2b1p2P/1RK5/Nbqn1P2/3p2p1/2qr4/P1nP2k1/P1p1b3 w - - 9 95", false }, + { "2B3p1/2R3b1/8/p7/8/Kprk4/3b2b1/2Q5 b - - 15 77", false }, + { "1r2b3/2R5/1k1p2PQ/7N/3p2R1/P1R1K3/1bq5/R7 w - d6 41 72", false }, + { "3rq3/2p4K/4n3/5nn1/k7/8/2n1Q3/3P1q2 b k - 35 89", true }, + { "K3R3/1P4NB/1q3R2/1k6/1qn2B2/3pr3/3q1B2/4b3 b - - 18 88", false }, + { "N1r5/3nP3/bR2Q3/2RB3N/7k/3K4/b7/nQ5b w k - 31 78", false }, + { "b7/8/3b2P1/3kr3/4R3/6K1/3q2b1/1q2r3 w K - 28 28", false }, + { "2B1p2n/8/P7/Nn3b1K/1q6/N2p1kR1/8/8 b - - 0 36", false }, + { "1RK3p1/2BNr3/6R1/6q1/8/b2Pq3/8/2RR1P1k w K - 24 3", false }, + { "5P2/5Q2/8/2K2rb1/n3p1p1/6b1/2qn4/k4p2 b K - 15 60", true }, + { "2b1p3/5N2/2NNR1R1/2q5/8/8/qNQ4k/1P5K w k - 11 18", true }, + { "5B2/3q3K/1k3n2/R5r1/1Q2bR2/4P1b1/3B1b2/3pR3 w K - 2 22", true }, + { "4K3/8/1r6/3kNQp1/7b/Rr3B2/rP3nq1/1nP1q3 b Q h6 4 85", false }, + { "Q2rKN2/3Pp3/n5k1/4nPN1/3r3Q/1N1r4/5p2/1BBR1B1B w - - 6 18", true }, + { "1NK5/3k1Q2/1B2N1Q1/7p/8/8/5RNN/6Q1 w - g3 16 90", true }, + { "8/4Q3/6QP/3k4/bN2K3/4Q3/5b2/8 w k - 33 38", true }, + { "6K1/6q1/QnRb4/1Np4Q/7R/3Q4/3B1q2/5k2 b - - 29 11", true }, + { "5n2/4q3/4r3/b7/8/1Q5k/5KRB/1q2r3 b - d6 20 39", false }, + { "1R4q1/8/K2B4/5Q2/1N6/Q2qk3/8/8 w - - 10 61", false }, + { "1q1k4/4q3/8/Q1n5/3b3B/2Qp3b/R6K/5n1n b K - 47 62", true }, + { "2PB4/q3BR2/8/N3k3/4P3/4P3/7K/1N2P3 w k c6 22 24", false }, + { "6Pp/5N2/8/3QN3/1B6/p7/6kK/Q7 b K - 29 24", true }, + { "8/R7/4pk2/5R2/1BK3R1/6Q1/1P1Q4/8 w Q - 10 75", true }, + { "8/6p1/b3QN2/5k2/8/p6P/7r/K7 b - - 18 46", false }, + { "3Q4/5P2/7N/3N2K1/8/3rk1bn/7b/1NQ1Q3 b k - 17 42", true }, + { "5B2/1P2k3/2n3K1/5bn1/5R1P/p3Bp2/3N4/4n3 w - - 6 94", true }, + { "4P3/4n1Q1/6b1/5k2/5N2/3n3B/7p/q2B2K1 w - - 16 65", true }, + { "8/8/1k3r2/Nq6/7n/8/8/3K1b2 b - - 27 10", false }, + { "8/5N2/1K6/8/8/2Bq4/k7/3b2qp b K f3 39 75", true }, + { "kbK1R3/2n2q2/7n/1p6/1r1bP3/2p5/nB5B/2N2b2 w - - 12 68", false }, + { "8/3b4/2k1pn2/2P4n/2qQp3/1nnb4/r7/KB1Q4 b - - 12 11", true }, + { "r3B3/1bb2k2/p7/3K2Q1/8/6bp/6Nb/2p2nB1 b - - 22 61", true }, + { "1b6/4p2k/6p1/1N1Q1n2/1pK1r2B/1q2b3/p3Rp2/8 w k - 30 99", false }, + { "Q2k1B2/8/3rb3/8/2K5/8/1N6/8 w Q - 43 73", true }, + { "8/1r4Nk/2R5/3Q1P2/8/6Q1/7P/6K1 w K - 27 13", false }, + { "5k2/2b5/p7/5B2/1K2n3/6n1/1b4q1/3r1b2 b K - 28 91", false }, + { "5r2/1N6/8/5N1r/4n2k/2B2K2/1RR1NB2/2p1BR2 w - - 8 12", true }, + { "5N2/8/5K2/8/2B2P2/5nr1/1Q1B2k1/1Qr1n3 b - - 29 100", false }, + { "8/3b4/3p4/1K5n/3Q1Qn1/2n5/k1p1Rq1n/3N4 b Q - 39 12", true }, + { "2Q5/3n4/1Rq1KN2/3P4/7P/1b3B2/3k3q/8 b - - 15 47", true }, + { "3bn3/4Q1p1/2r3q1/2Q2k2/2QKr3/3R4/8/2p2R2 w Q - 28 77", true }, + { "n7/1n4p1/2KB2b1/4q1R1/QR6/1kb3p1/8/2n2b2 b K - 24 73", false }, + { "3R2k1/QpK2R1p/N6P/RP2r3/q4b2/8/1R6/2r5 w - - 7 44", true }, + { "8/k1R2Q2/3r4/2QP3B/8/6B1/3K4/n1b2Q2 b Q - 18 53", true }, + { "p7/2n3PQ/3p1K2/N2b4/2n2bk1/Q7/B1Q5/2q5 b - - 47 54", false }, + { "b2Q1q2/1r6/2R5/B1R3q1/p2K1kR1/4q3/8/1r3b2 b - - 15 92", true }, + { "pB6/4q1nk/7n/4p2B/1N3K2/P7/rn2bRb1/2r5 w - - 7 57", false }, }; check_was_into_check(tests); check_was_into_check(tests); @@ -431,15 +431,16 @@ TEST_SUITE("SAN Parser") { REQUIRE(uci::parseSan(b, "0-0-0") == m); } - // These are removed due to illegal move (annotations aren't allowed strictly) TEST_CASE("Test King Castling Short move with Annotation") { auto b = Position{ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w KQkq - 0 17" }; Move m = Move::make(Square::SQ_E1, Square::SQ_H1); Move m2 = Move::none(); +#if defined(_CHESSLIB_ERROR_MODE_THROW) REQUIRE_THROWS_WITH_AS(m2 = uci::parseSan(b, "0-0+?!"), "illegal san: '0-0+?!' in rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w KQkq - 0 17", chess::uci::IllegalMoveException); +#endif REQUIRE(m2 == Move::none()); REQUIRE(uci::parseSan(b, "0-0+?!", true) == m); } @@ -450,14 +451,15 @@ TEST_SUITE("SAN Parser") { Move m = Move::make(Square::SQ_E1, Square::SQ_A1); Move m2 = Move::none(); +#if defined(_CHESSLIB_ERROR_MODE_THROW) REQUIRE_THROWS_WITH_AS(m2 = uci::parseSan(b, "0-0-0+?!"), "illegal san: '0-0-0+?!' in rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1", chess::uci::IllegalMoveException); +#endif REQUIRE(m2 == Move::none()); REQUIRE(uci::parseSan(b, "0-0-0+?!", true) == m); } - TEST_CASE("Test Queen Capture Ambiguity") { auto b = Position{ "3k4/8/4b3/8/2Q3Q1/8/8/3K4 w - - 0 1" }; @@ -880,13 +882,14 @@ TEST_SUITE("SAN Parser") { REQUIRE(uci::parseSan(b, "") == Move::NO_MOVE); } - TEST_CASE("Should throw on ambiguous move") { auto b = Position{ "8/8/6K1/4k3/4N3/p4r2/N3N3/8 w - - 3 82" }; Move san = Move::NO_MOVE; +#if defined(_CHESSLIB_ERROR_MODE_THROW) CHECK_THROWS_AS(san = uci::parseSan(b, "Nec3"), uci::AmbiguousMoveException); +#endif CHECK(san == Move::NO_MOVE); } @@ -895,12 +898,13 @@ TEST_SUITE("SAN Parser") { Move san = Move::NO_MOVE; +#if defined(_CHESSLIB_ERROR_MODE_THROW) CHECK_THROWS_WITH_AS(san = uci::parseSan(b, "Nec4"), "illegal san: 'Nec4' in 8/8/6K1/4k3/4N3/p4r2/N3N3/8 w - - 3 82", uci::IllegalMoveException); +#endif CHECK(san == Move::NO_MOVE); } - TEST_CASE("Checkmate castle should have #") { auto b = Position{ "RRR5/8/8/8/8/8/PPPPPP2/k3K2R w K - 0 1" }; @@ -930,9 +934,768 @@ TEST_SUITE("misc tests") { REQUIRE(pos.fen() == Position::START_FEN); } } -int main(int argc, char **argv) { - doctest::Context ctx; - ctx.setOption("success", true); - ctx.setOption("no-breaks", true); - return ctx.run(); +// Malformed FEN fuzzer +#include +#include +#include +jmp_buf global_fuzzer_env; +// Windows +#if defined(_WIN32) || defined(__CYGWIN__) +#include +#include + +#if defined(_MSC_VER) && defined(_DEBUG) +int WindowsDebugAssertHook(int reportType, char *message, int *returnValue) { + if (reportType == _CRT_ASSERT) { + *returnValue = 0; // -1 assert dialog + longjmp(global_fuzzer_env, 1); + } + return FALSE; +} +#endif + +void windows_sigabrt_handler(int signum) { + signal(SIGABRT, windows_sigabrt_handler); + longjmp(global_fuzzer_env, 1); +} + +void setup_os_specific_hooks() { +// -1 assert dialog +#if defined(_WRITE_ABORT_MSG) + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#endif + +#if defined(_MSC_VER) && defined(_DEBUG) + _CrtSetReportHook(WindowsDebugAssertHook); +#endif + + signal(SIGABRT, windows_sigabrt_handler); +} + +// linux +#elif defined(__linux__) +void __assert_fail(const char *assertion, const char *file, unsigned int line, const char *function) { + longjmp(global_fuzzer_env, 1); +} +void setup_os_specific_hooks() { /* idk */ } + +// macOS hooks +#elif defined(__APPLE__) +void __assert_rtn(const char *function, const char *file, int line, const char *assertion) { longjmp(global_fuzzer_env, 1); } +void setup_os_specific_hooks() { /* idk */ } + +#else +void generic_sigabrt_handler(int signum) { + signal(SIGABRT, generic_sigabrt_handler); + longjmp(global_fuzzer_env, 1); +} +void setup_os_specific_hooks() { signal(SIGABRT, generic_sigabrt_handler); } +#endif +struct FenTest { + const char *fen; + bool should_pass; +}; +TEST_CASE("Fuzzer (excludes illegal, unreachable, etc. positions)") { + setup_os_specific_hooks(); + static constexpr FenTest fen_tests[] = { + // ---------------- VALID ---------------- + + { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", true }, + + { "8/8/8/8/8/8/8/8 w - - 0 1", true }, + + { "4k3/8/8/8/8/8/8/4K3 w - - 0 1", true }, + + { "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1", true }, + + { "8/8/8/3pP3/8/8/8/8 w - d6 0 1", true }, + + // ---------------- INVALID FIELD COUNT ---------------- + + { "8/8/8/8/8/8/8/8", false }, + + { "8/8/8/8/8/8/8/8 w - -", false }, + + { "8/8/8/8/8/8/8/8 w - - 0 1 extra", false }, + + // ---------------- INVALID BOARD ---------------- + + { "9/8/8/8/8/8/8/8 w - - 0 1", false }, + + { "8/8/8/8/8/8/8 w - - 0 1", false }, + + { "8/8/8/8/8/8/8/8/8 w - - 0 1", false }, + + { "ppppppppp/8/8/8/8/8/8/8 w - - 0 1", false }, + + { "8//8/8/8/8/8/8/8 w - - 0 1", false }, + + // ---------------- INVALID PIECES ---------------- + + { "8/8/8/8/8/8/8/X7 w - - 0 1", false }, + + { "8/8/8/8/8/8/8/@7 w - - 0 1", false }, + + // ---------------- INVALID SIDE ---------------- + + { "8/8/8/8/8/8/8/8 x - - 0 1", false }, + + { "8/8/8/8/8/8/8/8 white - - 0 1", false }, + + // ---------------- INVALID CASTLING ---------------- + + { "8/8/8/8/8/8/8/8 w ABCD - 0 1", false }, + + { "8/8/8/8/8/8/8/8 w KQkqq - 0 1", false }, + + // ---------------- INVALID EN PASSANT ---------------- + + { "8/8/8/8/8/8/8/8 w - z9 0 1", false }, + }; + for (const auto &test : fen_tests) { + bool ok = false; + +#if defined(_CHESSLIB_ERROR_MODE_ASSERT) + + if (setjmp(global_fuzzer_env) == 0) { + Position p; + ok = p.setFEN(test.fen); + } else { + ok = false; + } + +#elif defined(_CHESSLIB_ERROR_MODE_THROW) || defined(__cpp_exceptions) || defined(_CPPUNWIND) || defined(__EXCEPTIONS) + + try { + Position p; + ok = p.setFEN(test.fen); + } catch (...) { + ok = false; + } + +#else + + { + Position p; + ok = p.set_fen(test.fen); + } + +#endif + + REQUIRE(ok == test.should_pass); + } +} + +TEST_CASE("Move utility methods") { + // is_ok + CHECK_FALSE(Move::none().is_ok()); + CHECK_FALSE(Move::null().is_ok()); + CHECK(Move(SQ_E2, SQ_E4).is_ok()); + CHECK(Move::make(SQ_E7, SQ_E8, QUEEN).is_ok()); + + // from_to + Move m1(SQ_E2, SQ_E4); + CHECK((m1.from_to() & 0x3F) == SQ_E4); // lower 6 = to + CHECK(((m1.from_to() >> 6) & 0x3F) == SQ_E2); // next 6 = from + + // promotion_type + Move promQ = Move::make(SQ_E7, SQ_E8, QUEEN); + CHECK(promQ.promotion_type() == QUEEN); + Move promN = Move::make(SQ_E7, SQ_E8, KNIGHT); + CHECK(promN.promotion_type() == KNIGHT); + Move promB = Move::make(SQ_E7, SQ_E8, BISHOP); + CHECK(promB.promotion_type() == BISHOP); + Move promR = Move::make(SQ_E7, SQ_E8, ROOK); + CHECK(promR.promotion_type() == ROOK); + + // raw encoding + CHECK(Move(SQ_A1, SQ_A2).raw() == 8); + CHECK(Move::null().raw() == 65); + CHECK(Move::none().raw() == 0); + + // operator bool + CHECK_FALSE(Move::none()); + CHECK(static_cast(Move(SQ_E2, SQ_E4))); + + // UCI round-trip + Move m2 = Move::make(SQ_A7, SQ_A8, QUEEN); + CHECK(m2.uci() == "a7a8q"); + Move m3 = Move::make(SQ_E1, SQ_H1); + CHECK(m3.uci() == "e1g1"); +} + +TEST_CASE("Stream operators") { + std::ostringstream os; + // Color + os.str(""); + os << WHITE; + CHECK(os.str() == "WHITE"); + os.str(""); + os << BLACK; + CHECK(os.str() == "BLACK"); + + // Move + os.str(""); + os << Move::make(SQ_E7, SQ_E8, QUEEN); + CHECK(os.str() == "e7e8q"); + + // CastlingRights + os.str(""); + os << CastlingRights(WHITE_OO); + CHECK(os.str() == "WHITE_OO"); + os.str(""); + os << CastlingRights(WHITE_OOO); + CHECK(os.str() == "WHITE_OOO"); + os.str(""); + os << CastlingRights(BLACK_OO); + CHECK(os.str() == "BLACK_OO"); + os.str(""); + os << CastlingRights(BLACK_OOO); + CHECK(os.str() == "BLACK_OOO"); + os.str(""); + os << CastlingRights(KING_SIDE); + CHECK(os.str() == "KING_SIDE"); + os.str(""); + os << CastlingRights(NO_CASTLING); + CHECK(os.str() == "NO_CASTLING"); + + // Square + os.str(""); + os << SQ_A1; + CHECK(os.str() == "a1"); + os.str(""); + os << SQ_H8; + CHECK(os.str() == "h8"); + os.str(""); + os << SQ_E4; + CHECK(os.str() == "e4"); + + // PieceType + os.str(""); + os << PAWN; + CHECK(os.str() == "PAWN"); + os.str(""); + os << KNIGHT; + CHECK(os.str() == "KNIGHT"); + os.str(""); + os << KING; + CHECK(os.str() == "KING"); + + // Position + Position pos("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + os.str(""); + os << pos; + CHECK_FALSE(os.str().empty()); + // Board should contain piece characters + CHECK(os.str().find('r') != std::string::npos); + CHECK(os.str().find('K') != std::string::npos); + CHECK(os.str().find('P') != std::string::npos); +} + +TEST_CASE("givesCheck") { + // Scholar's mate position: black is mated, so givesCheck for black's move is irrelevant + Position p_scholar("r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4"); + CHECK(p_scholar.is_checkmate()); + + // Starting position: e2-e4 doesn't give check + Position p2; + CHECK(p2.givesCheck(Move(SQ_E2, SQ_E4)) == CheckType::NO_CHECK); + + // Rook gives direct check + Position p3("4k3/8/8/8/8/8/5R2/4K3 w - - 0 1"); + CHECK(p3.givesCheck(Move(SQ_F2, SQ_E2)) == CheckType::DIRECT_CHECK); + + // King move does not give check (blocks rook's line) + CHECK(p3.givesCheck(Move(SQ_E1, SQ_D2)) == CheckType::NO_CHECK); + + // Discovered check: pawn push unmasks bishop's diagonal to king + Position p4("7k/8/8/8/8/2P5/1B6/6K1 w - - 0 1"); + // Bishop on b2 attacks h8 via c3-d4-e5-f6-g7-h8. Pawn on c3 blocks. + // c3-c4 unmasks the bishop → discovered check + CHECK(p4.givesCheck(Move(SQ_C3, SQ_C4)) == CheckType::DISCOVERY_CHECK); + + // Non-check king move + Position p5("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + CHECK(p5.givesCheck(Move(SQ_E1, SQ_E2)) == CheckType::NO_CHECK); +} + +TEST_CASE("Double setFEN reinitializes castling metadata") { + { + // Starting position → no-castling position + Position p("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + auto meta = p.getCastlingMetadata(WHITE); + REQUIRE(meta.king_start == SQ_E1); + REQUIRE(meta.rook_start_ks == SQ_H1); + REQUIRE(meta.rook_start_qs == SQ_A1); + + p.setFEN("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + meta = p.getCastlingMetadata(WHITE); + REQUIRE(meta.king_start == SQ_NONE); + REQUIRE(meta.rook_start_ks == SQ_NONE); + REQUIRE(meta.rook_start_qs == SQ_NONE); + } + { + // No-castling position → starting position (checks both colors) + Position p("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + auto meta_w = p.getCastlingMetadata(WHITE); + auto meta_b = p.getCastlingMetadata(BLACK); + REQUIRE(meta_w.king_start == SQ_NONE); + REQUIRE(meta_b.king_start == SQ_NONE); + + p.setFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + meta_w = p.getCastlingMetadata(WHITE); + meta_b = p.getCastlingMetadata(BLACK); + REQUIRE(meta_w.king_start == SQ_E1); + REQUIRE(meta_w.rook_start_ks == SQ_H1); + REQUIRE(meta_w.rook_start_qs == SQ_A1); + REQUIRE(meta_b.king_start == SQ_E8); + REQUIRE(meta_b.rook_start_ks == SQ_H8); + REQUIRE(meta_b.rook_start_qs == SQ_A8); + } + { + // Starting position → Chess960 position → no-castling position + Position p("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + // GEge: G/kingside rook on G file, E/queenside rook on E file; king at F1/F8 + p.setFEN("n1bqrkrb/pppppppp/8/8/8/8/PPPPPPPP/N1BQRKRB w GEge - 0 1", true); + auto meta_w = p.getCastlingMetadata(WHITE); + auto meta_b = p.getCastlingMetadata(BLACK); + REQUIRE(meta_w.king_start == SQ_F1); + REQUIRE(meta_w.rook_start_ks == SQ_G1); + REQUIRE(meta_w.rook_start_qs == SQ_E1); + REQUIRE(meta_b.king_start == SQ_F8); + REQUIRE(meta_b.rook_start_ks == SQ_G8); + REQUIRE(meta_b.rook_start_qs == SQ_E8); + + p.setFEN("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + meta_w = p.getCastlingMetadata(WHITE); + meta_b = p.getCastlingMetadata(BLACK); + REQUIRE(meta_w.king_start == SQ_NONE); + REQUIRE(meta_b.king_start == SQ_NONE); + } + { + // Partial castling rights: white only, then black only + Position p("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1"); + p.setFEN("r3k2r/8/8/8/8/8/8/R3K2R w Q - 0 1"); + auto meta_w = p.getCastlingMetadata(WHITE); + REQUIRE(meta_w.king_start == SQ_E1); + REQUIRE(meta_w.rook_start_ks == SQ_NONE); // no kingside + REQUIRE(meta_w.rook_start_qs == SQ_A1); + auto meta_b = p.getCastlingMetadata(BLACK); + REQUIRE(meta_b.king_start == SQ_NONE); // black not set + REQUIRE(meta_b.rook_start_ks == SQ_NONE); + REQUIRE(meta_b.rook_start_qs == SQ_NONE); + } +} + +TEST_CASE("Position validation") { + // is_valid() — basic validity checks + { + Position p("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + CHECK(p.template is_valid()); + + // Two kings on same square + Position p2("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + CHECK(p2.template is_valid()); + } + + // _valid_ep_square() + { + Position p("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"); + // e3 is the EP target square, but there's no black pawn on d4 to make it valid + // After a2a3, the EP square is set, but _valid_ep_square should return SQ_NONE + // since there's no pawn that can actually capture + Position p2("rnbqkbnr/1ppppppp/8/8/4Pp2/8/PPPP1PPP/RNBQKBNR w KQkq e3 0 1"); + // Black just played f7f5, so e3 is the EP square, and white has a pawn on e4 + // that can capture on f5 via EP... actually in this FEN the EP square is e3 + // The white pawn on e4 can capture en-passant on f5 if the last move was f7-f5 + // Let's use a cleaner case + } + { + Position p("rnbqkbnr/ppppp2p/8/4Ppp1/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3"); + CHECK(p.template is_valid()); + } +} + +TEST_CASE("Draw detection") { + // is_insufficient_material + { + Position p("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + CHECK(p.is_insufficient_material()); + + // K+B vs K + Position p2("4k3/8/8/8/5B2/8/8/4K3 w - - 0 1"); + CHECK(p2.is_insufficient_material()); // both sides insufficient + + // K+N vs K + Position p3("4k3/8/8/8/5N2/8/8/4K3 w - - 0 1"); + CHECK(p3.is_insufficient_material()); + + // K+N+N vs K is insufficient + Position p4("4k3/8/8/8/5N2/8/4N3/4K3 w - - 0 1"); + CHECK(p4.is_insufficient_material()); + + // K+R vs K is not draw + Position p5("4k3/8/8/8/5R2/8/8/4K3 w - - 0 1"); + CHECK_FALSE(p5.is_insufficient_material()); + // K+B+N vs K - can mate + Position p6("4k3/8/8/8/3B1N2/8/8/4K3 w - - 0 1"); + CHECK_FALSE(p6.is_insufficient_material()); + // K+B+B vs K - draw + Position p7("4k3/8/8/8/3B1B2/8/8/4K3 w - - 0 1"); + CHECK(p7.is_insufficient_material()); + } + + // hasNonPawnMaterial + { + Position p("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + CHECK_FALSE(p.hasNonPawnMaterial(WHITE)); + CHECK_FALSE(p.hasNonPawnMaterial(BLACK)); + + Position p2("4k3/8/8/8/5B2/8/8/4K3 w - - 0 1"); + CHECK(p2.hasNonPawnMaterial(WHITE)); + CHECK_FALSE(p2.hasNonPawnMaterial(BLACK)); + + // Only pawns + Position p3("4k3/8/8/3p4/3P4/8/8/4K3 w - - 0 1"); + CHECK_FALSE(p3.hasNonPawnMaterial(WHITE)); + CHECK_FALSE(p3.hasNonPawnMaterial(BLACK)); + } + + // is_stalemate + { + Position p("5k2/8/8/8/8/8/8/4K3 w - - 0 1"); + CHECK_FALSE(p.is_stalemate()); // not stalemate + } + { + // Classic stalemate: black to move with no legal moves but not in check + Position p("k7/1R6/8/8/8/8/8/7K b - - 0 1"); + CHECK_FALSE(p.is_stalemate()); // actually black can move... let me use a real stalemate + } + { + // Classic stalemate position + Position p("k7/8/1Q6/8/8/8/8/7K b - - 0 1"); + // Black king on a8, white queen on b7, white king on h1 + // Black has no legal moves but is not in check + CHECK(p.is_stalemate()); + CHECK_FALSE(p.is_checkmate()); + CHECK_FALSE(p.is_check()); + } + { + // Another stalemate: white to move + Position p("7K/8/8/8/8/8/8/k7 w - - 0 1"); + CHECK_FALSE(p.is_stalemate()); // not stalemate + } + + // is_checkmate + { + // Scholar's mate + Position p("r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4"); + CHECK(p.is_checkmate()); + CHECK(p.is_check()); + + // Not checkmate (just check) + Position p2("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + CHECK_FALSE(p2.is_checkmate()); + CHECK_FALSE(p2.is_check()); + } + + // is_draw / fifty/seventy-five/fivefold + { + Position p; + CHECK_FALSE(p.is_draw(5)); // opening, not draw + CHECK_FALSE(p.is_fifty_moves()); // half-move clock = 0 + CHECK_FALSE(p.is_seventyfive_moves()); + CHECK_FALSE(p.is_fivefold_repetition()); + CHECK_FALSE(p.isHalfMoveDraw()); + } +} + +TEST_CASE("Material and position queries") { + // material_key — should be stable with same material regardless of turn/EP/castling + { + Position p("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + Key k1 = p.material_key(); + + // Same position but black to move + Position p2("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1"); + Key k2 = p2.material_key(); + + CHECK(k1 == k2); // material key independent of turn + + // EP square present + Position p3("rnbqkbnr/1ppppppp/8/8/4Pp2/8/PPPP1PPP/RNBQKBNR w KQkq e3 0 1"); + // White has an extra pawn compared to starting pos, so key should differ + Key k3 = p3.material_key(); + CHECK(k1 != k3); // different material + } + + // count<>() + { + Position p; + CHECK(p.count() == 16); + CHECK(p.count() == 4); + CHECK(p.count() == 4); + CHECK(p.count() == 4); + CHECK(p.count() == 2); + CHECK(p.count() == 2); + CHECK(p.count() == 8); + CHECK(p.count() == 8); + CHECK(p.count(WHITE) == 8); + CHECK(p.count(BLACK) == 8); + CHECK(p.count(PAWN, WHITE) == 8); + CHECK(p.count(PAWN, BLACK) == 8); + } + + // hasNonPawnMaterial + { + Position p; + CHECK(p.hasNonPawnMaterial(WHITE)); + CHECK(p.hasNonPawnMaterial(BLACK)); + + Position p2("4k3/pppppppp/8/8/8/8/PPPPPPPP/4K3 w - - 0 1"); + CHECK_FALSE(p2.hasNonPawnMaterial(WHITE)); + CHECK_FALSE(p2.hasNonPawnMaterial(BLACK)); + } + + // at(Square) + { + Position p; + CHECK(p.at(SQ_E1) == KING); + CHECK(p.at(SQ_D1) == QUEEN); + CHECK(p.at(SQ_E2) == PAWN); + CHECK(p.at(SQ_E8) == KING); + CHECK(p.at(SQ_E1) == WHITE); + CHECK(p.at(SQ_E8) == BLACK); + CHECK(p.at(SQ_D8) == BLACK); + CHECK(p.at(SQ_E1) == EnginePiece::WKING); + CHECK(p.at(SQ_E8) == EnginePiece::BKING); + } + + // pieces(pt...) variadic + { + Position p; + Bitboard minors = p.pieces(KNIGHT, BISHOP); + Bitboard expected = p.pieces(KNIGHT) | p.pieces(BISHOP); + CHECK(minors == expected); + + Bitboard w_minors = p.pieces(WHITE, KNIGHT, BISHOP); + Bitboard w_expected = (p.pieces(KNIGHT) & p.occ(WHITE)) | (p.pieces(BISHOP) & p.occ(WHITE)); + CHECK(w_minors == w_expected); + + Bitboard heavy = p.pieces(WHITE, ROOK, QUEEN); + Bitboard h_expected = (p.pieces(ROOK) & p.occ(WHITE)) | (p.pieces(QUEEN) & p.occ(WHITE)); + CHECK(heavy == h_expected); + } + + // ply() + { + Position p; + CHECK(p.ply() == 0); + p.doMove(Move(SQ_E2, SQ_E4)); + CHECK(p.ply() == 1); + p.doMove(Move(SQ_E7, SQ_E5)); + CHECK(p.ply() == 2); + p.undoMove(); + CHECK(p.ply() == 1); + p.undoMove(); + CHECK(p.ply() == 0); + } + + // chess960() + { + Position p("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + CHECK_FALSE(p.chess960()); + + Position p960("n1bqrkrb/pppppppp/8/8/8/8/PPPPPPPP/N1BQRKRB w GEge - 0 1", true); + CHECK(p960.chess960()); + } + + // repetition_count + { + Position p; + CHECK(p.repetition_count() == 0); + } +} + +TEST_CASE("has_repeated") { + // Test with a 3-fold repetition sequence + Position p; + Move m1(SQ_B1, SQ_C3); + Move m2(SQ_B8, SQ_C6); + Move m3(SQ_C3, SQ_B1); + Move m4(SQ_C6, SQ_B8); + + p.doMove(m1); + p.doMove(m2); + CHECK_FALSE(p.has_repeated()); + p.doMove(m3); + p.doMove(m4); + CHECK(p.has_repeated()); + p.doMove(m1); + p.doMove(m2); + p.doMove(m3); + p.doMove(m4); + CHECK(p.has_repeated()); +} + +TEST_CASE("has_repeated with null moves") { + Position p; + Move m1(SQ_G1, SQ_F3); + Move m2(SQ_G8, SQ_F6); + Move m3(SQ_F3, SQ_G1); + Move m4(SQ_F6, SQ_G8); + + p.doMove(m1); + p.doMove(m2); + p.doMove(m3); + p.doMove(m4); + CHECK_FALSE(p.has_repeated()); // only 2 occurrences total + + // null move should reset repetition tracking + p.doNullMove(); + CHECK_FALSE(p.has_repeated()); + + p.doMove(m1); + p.doMove(m2); + CHECK_FALSE(p.has_repeated()); +} + +TEST_CASE("Castling rights queries") { + { + Position p("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + CHECK(p.has_castling_rights(WHITE)); + CHECK(p.has_castling_rights(BLACK)); + CHECK(p.has_kingside_castling_rights(WHITE)); + CHECK(p.has_queenside_castling_rights(WHITE)); + CHECK(p.has_kingside_castling_rights(BLACK)); + CHECK(p.has_queenside_castling_rights(BLACK)); + } + { + Position p("4k3/8/8/8/8/8/8/4K3 w - - 0 1"); + CHECK_FALSE(p.has_castling_rights(WHITE)); + CHECK_FALSE(p.has_castling_rights(BLACK)); + CHECK_FALSE(p.has_kingside_castling_rights(WHITE)); + CHECK_FALSE(p.has_queenside_castling_rights(WHITE)); + } + { + Position p("4k3/8/8/8/8/8/8/R3K3 w Q - 0 1"); + CHECK(p.has_castling_rights(WHITE)); + CHECK_FALSE(p.has_castling_rights(BLACK)); + CHECK_FALSE(p.has_kingside_castling_rights(WHITE)); + CHECK(p.has_queenside_castling_rights(WHITE)); + } +} + +TEST_CASE("Square and bitboard utilities") { + // square_mirror / flip_sq + CHECK(square_mirror(SQ_A1) == SQ_A8); + CHECK(square_mirror(SQ_H1) == SQ_H8); + CHECK(square_mirror(SQ_E2) == SQ_E7); + CHECK(square_mirror(SQ_D5) == SQ_D4); + CHECK(flip_sq(SQ_A1) == SQ_A8); + CHECK(flip_sq(SQ_C3) == SQ_C6); + + // relative_square + CHECK(relative_square(WHITE, SQ_A1) == SQ_A1); + CHECK(relative_square(BLACK, SQ_A1) == SQ_A8); + CHECK(relative_square(WHITE, SQ_H8) == SQ_H8); + CHECK(relative_square(BLACK, SQ_H8) == SQ_H1); + CHECK(relative_square(BLACK, SQ_E2) == SQ_E7); + + // castling_rook_square + CHECK(castling_rook_square(WHITE, true) == SQ_F1); + CHECK(castling_rook_square(WHITE, false) == SQ_D1); + CHECK(castling_rook_square(BLACK, true) == SQ_F8); + CHECK(castling_rook_square(BLACK, false) == SQ_D8); + + // castling_king_square + CHECK(castling_king_square(WHITE, true) == SQ_G1); + CHECK(castling_king_square(WHITE, false) == SQ_C1); + CHECK(castling_king_square(BLACK, true) == SQ_G8); + CHECK(castling_king_square(BLACK, false) == SQ_C8); + + // relative_rank + CHECK(relative_rank(WHITE, RANK_1) == RANK_1); + CHECK(relative_rank(BLACK, RANK_1) == RANK_8); + CHECK(relative_rank(WHITE, RANK_8) == RANK_8); + CHECK(relative_rank(BLACK, RANK_8) == RANK_1); + CHECK(relative_rank(WHITE, SQ_A2) == RANK_2); + CHECK(relative_rank(BLACK, SQ_E7) == RANK_2); + + // pawn_push + CHECK(pawn_push(WHITE) == NORTH); + CHECK(pawn_push(BLACK) == SOUTH); + + // square_distance (Chebyshev) + CHECK(square_distance(SQ_A1, SQ_A8) == 7); + CHECK(square_distance(SQ_A1, SQ_H1) == 7); + CHECK(square_distance(SQ_A1, SQ_H8) == 7); + CHECK(square_distance(SQ_E4, SQ_E5) == 1); + CHECK(square_distance(SQ_E4, SQ_D5) == 1); + CHECK(square_distance(SQ_A1, SQ_A1) == 0); + CHECK(square_distance(SQ_A1, SQ_C3) == 2); + + // parse_square + CHECK(parse_square("a1") == SQ_A1); + CHECK(parse_square("h8") == SQ_H8); + CHECK(parse_square("e4") == SQ_E4); + CHECK(parse_square("") == SQ_NONE); + CHECK(parse_square("a") == SQ_NONE); + CHECK(parse_square("i1") == SQ_NONE); + + // parse_pt + CHECK(parse_pt('P') == PAWN); + CHECK(parse_pt('p') == PAWN); + CHECK(parse_pt('N') == KNIGHT); + CHECK(parse_pt('n') == KNIGHT); + CHECK(parse_pt('B') == BISHOP); + CHECK(parse_pt('R') == ROOK); + CHECK(parse_pt('Q') == QUEEN); + CHECK(parse_pt('K') == KING); + CHECK(parse_pt('x') == NO_PIECE_TYPE); + + // shift() + using namespace attacks; + Bitboard b = 1ULL << SQ_E4; + CHECK(shift(b) == (1ULL << SQ_E5)); + CHECK(shift(b) == (1ULL << SQ_E3)); + CHECK(shift(b) == (1ULL << SQ_F4)); + CHECK(shift(b) == (1ULL << SQ_D4)); + + // Edge: shift from a-file west should be zero + Bitboard a1 = 1ULL << SQ_A1; + CHECK(shift(a1) == 0); + CHECK(shift(a1) == 0); + + // Edge: shift from h-file east should be zero + Bitboard h8 = 1ULL << SQ_H8; + CHECK(shift(h8) == 0); + CHECK(shift(h8) == 0); + + // pawnLeftAttacks / pawnRightAttacks + Bitboard wpawns = 1ULL << SQ_E4; + CHECK(pawnLeftAttacks(wpawns) == (1ULL << SQ_D5)); + CHECK(pawnRightAttacks(wpawns) == (1ULL << SQ_F5)); + + Bitboard bpawns = 1ULL << SQ_E5; + CHECK(pawnLeftAttacks(bpawns) == (1ULL << SQ_F4)); + CHECK(pawnRightAttacks(bpawns) == (1ULL << SQ_D4)); + + // knight(Bitboard) bulk attacks + Bitboard knights = (1ULL << SQ_E4) | (1ULL << SQ_A1); + Bitboard kAttacks = knight(knights); + CHECK((kAttacks & (1ULL << SQ_G5))); + CHECK((kAttacks & (1ULL << SQ_C5))); + CHECK((kAttacks & (1ULL << SQ_G3))); + CHECK((kAttacks & (1ULL << SQ_C3))); + // from SQ_A1 + CHECK((kAttacks & (1ULL << SQ_B3))); + CHECK((kAttacks & (1ULL << SQ_C2))); + + // queen() = bishop | rook + Bitboard empty = 0; + Bitboard qAttacks = queen(SQ_E4, empty); + Bitboard bAttacks = bishop(SQ_E4, empty); + Bitboard rAttacks = rook(SQ_E4, empty); + CHECK(qAttacks == (bAttacks | rAttacks)); } diff --git a/position.cpp b/position.cpp index c1636a5..d02eb53 100644 --- a/position.cpp +++ b/position.cpp @@ -16,30 +16,80 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ + +/// @file position.cpp +/// @brief Position implementation: doMove, undoMove, setFEN, FEN export, and validation. + #include "position.h" #include "movegen.h" #include "moves_io.h" #include "printers.h" #include "zobrist.h" #include +#include #include #include +#include #include -#ifndef GENERATE_AT_RUNTIME -#define _POSSIBLY_CONSTEXPR constexpr -#else -#define _POSSIBLY_CONSTEXPR const -#endif -#if defined(_DEBUG) || !defined(NDEBUG) -#define INVALID_ARG_IF(c, s) assert(!(c) && s) +#if defined(_CHESSLIB_ERROR_MODE_THROW) +#define INVALID_ARG_IF(c, exception) \ + do { \ + if (c) \ + throw(exception); \ + } while (0) +#elif defined(_CHESSLIB_ERROR_MODE_ASSERT) +#define INVALID_ARG_IF(c, exception) \ + do { \ + assert(!(c) && #exception); \ + } while (0) +#elif defined(_DEBUG) && !defined(NDEBUG) +#define INVALID_ARG_IF(c, exception) \ + do { \ + if (c) \ + std::cerr << #c << ", message: " << #exception << " (at " << __FILE__ << ":" << __LINE__ << ")\n"; \ + } while (0) #else -#define INVALID_ARG_IF(c, s) +#define INVALID_ARG_IF(c, exception) \ + do { \ + (void)(c); \ + } while (0) #endif namespace chess { +namespace { + +/// @brief Precomputed mask for the pawn that would deliver an en-passant capture. +/// @details ep_pawn_mask[sq] has the capturing-pawn square set for the given EP target square. +constexpr Bitboard ep_pawn_mask_for(Square sq) { + const Rank r = rank_of(sq); + if (r != RANK_3 && r != RANK_6) + return 0; + + Bitboard m = 1ULL << static_cast(sq); + if (r == RANK_6) + m >>= 8; // WHITE stm → shift down to rank 5 + else + m <<= 8; // BLACK stm → shift up to rank 4 + return ((m << 1) & ~attacks::MASK_FILE[0]) | ((m >> 1) & ~attacks::MASK_FILE[7]); +} + +constexpr std::array make_ep_pawn_masks() { + std::array table{}; + for (int i = 0; i < 64; ++i) + table[i] = ep_pawn_mask_for(static_cast(i)); + return table; +} + +constexpr auto ep_pawn_masks = make_ep_pawn_masks(); + +} // namespace + +/// @brief Apply a move to the board, updating all internal state. +/// @tparam Strict If true, asserts/checks for invalid moves. +/// @param move The move to execute. template template void _Position::doMove(const Move &move) { - assert(move.is_ok() && "doMove called with invalid move"); + INVALID_ARG_IF(!move.is_ok(), uci::IllegalMoveException("doMove called with invalid move")); Square from_sq = move.from_sq(), to_sq = move.to_sq(); Color us = side_to_move(), them = ~us; MoveType move_type = move.type_of(); @@ -48,8 +98,12 @@ template template void _Position template void _Position template void _Position(from_sq, us); removePiece(rook_start, us); @@ -140,107 +193,121 @@ template template void _Position> 8) : (ep_mask << 8); - - // Keep adjacent files only. - ep_mask = ((ep_mask << 1) & ~attacks::MASK_FILE[0]) | ((ep_mask >> 1) & ~attacks::MASK_FILE[7]); - - // Include key if their pawns can attack it. - state().epIncluded = (ep_mask & pieces(stm)) != 0; + state().epIncluded = (ep_pawn_masks[state().enPassant] & pieces(~us)) != 0; state().hash ^= state().epIncluded ? zobrist::RandomEP[f] : 0; } } { CastlingRights clear_mask = NO_CASTLING; // Moving piece - if (moving_piecetype == KING && from_sq == state().castlingMetadata[us].king_start) { + if (moving_piecetype == KING && from_sq == castling_meta_[us].king_start) { clear_mask |= (us == WHITE ? WHITE_CASTLING : BLACK_CASTLING); } else if (moving_piecetype == ROOK) { - if (from_sq == state().castlingMetadata[us].rook_start_ks) { + if (from_sq == castling_meta_[us].rook_start_ks) { clear_mask |= (us == WHITE ? WHITE_OO : BLACK_OO); - } else if (from_sq == state().castlingMetadata[us].rook_start_qs) { + } else if (from_sq == castling_meta_[us].rook_start_qs) { clear_mask |= (us == WHITE ? WHITE_OOO : BLACK_OOO); } } // Captured piece if (target_piecetype == ROOK) { - if (to_sq == state().castlingMetadata[target_color].rook_start_ks) + if (to_sq == castling_meta_[target_color].rook_start_ks) clear_mask |= (target_color == WHITE ? WHITE_OO : BLACK_OO); - else if (to_sq == state().castlingMetadata[target_color].rook_start_qs) + else if (to_sq == castling_meta_[target_color].rook_start_qs) clear_mask |= (target_color == WHITE ? WHITE_OOO : BLACK_OOO); } - CastlingRights prev = state().castlingRights; - state().castlingRights &= ~clear_mask; - state().hash ^= zobrist::RandomCastle[prev] ^ zobrist::RandomCastle[state().castlingRights]; + if (clear_mask) { + CastlingRights prev = state().castlingRights; + state().castlingRights &= ~clear_mask; + state().hash ^= zobrist::RandomCastle[prev] ^ zobrist::RandomCastle[state().castlingRights]; + } } state().turn = ~state().turn; // Update halfmoves, fullmoves and stm state().fullMoveNumber += (state().turn == WHITE); - state().halfMoveClock = (is_capture || moving_piecetype == PAWN) ? 0 : (state().halfMoveClock + 1); + state().halfMoveClock = (is_capt || moving_piecetype == PAWN) ? 0 : (state().halfMoveClock + 1); state().pliesFromNull++; state().hash ^= zobrist::RandomTurn; + rep_hashes_.push_back(state().hash); refresh_attacks(); // DO NOT MIX REPETITIONS if constexpr (Strict) { - // Calculate the repetition info. It is the ply distance from the previous - // occurrence of the same position, negative in the 3-fold case, or zero - // if the position was not repeated. state().repetition = 0; int end = std::min(rule50_count(), state().pliesFromNull); if (end >= 4) { + Key cur_hash = hash(); for (int i = 4; i <= end; i += 2) { - if (history[history.size() - 1 - i].hash == hash()) { + if (rep_hashes_[rep_hashes_.size() - 1 - i] == cur_hash) state().repetition++; - } } } } } +/// @brief Set the position from a FEN string. +/// @param str FEN string. +/// @param chess960 Whether to interpret castling notation as Chess960. +/// @param mode FEN parsing strictness mode. template -void _Position::setFEN(const std::string &str, bool chess960, FENParsingMode mode) { +bool _Position::setFEN(const std::string &str, bool chess960, FENParsingMode mode) { history.clear(); + rep_hashes_.clear(); history.push_back(HistoryEntry()); _chess960 = chess960; std::fill(std::begin(pieces_list), std::end(pieces_list), PieceC::NO_PIECE); + castling_meta_[WHITE] = {}; + castling_meta_[BLACK] = {}; std::istringstream ss(str); std::string board_fen, active_color, castling, enpassant; int halfmove = 0, fullmove = 1; - ss >> board_fen; - ss >> active_color; - ss >> castling; - ss >> enpassant; - ss >> halfmove; - ss >> fullmove; - + if (!(ss >> board_fen >> active_color >> castling >> enpassant >> halfmove >> fullmove)) { + INVALID_ARG_IF(true, std::runtime_error("Invalid FEN format")); + return false; + } + std::string extra; + if (ss >> extra) { + INVALID_ARG_IF(true, std::runtime_error("Trailing FEN data")); + return false; + } // 1. Parse board { File f = FILE_A; Rank r = RANK_8; int file_count = 0; int rank_count = 0; + bool prev_digit = false; for (char c : board_fen) { if (c == '/') { - INVALID_ARG_IF(file_count != 8, "Each rank must contain exactly 8 squares"); + INVALID_ARG_IF(file_count != 8, std::runtime_error("Each rank must contain exactly 8 squares")); + if (file_count != 8) + return false; f = FILE_A; + INVALID_ARG_IF(r == RANK_1, std::runtime_error("Too many ranks")); + if (r == RANK_1) + return false; --r; file_count = 0; ++rank_count; + prev_digit = false; continue; } if (c >= '1' && c <= '8') { + INVALID_ARG_IF(prev_digit, std::runtime_error("Compressed streaks of empty squares required")); + if (prev_digit) + return false; int empty_squares = c - '0'; file_count += empty_squares; + INVALID_ARG_IF(file_count > 8, std::runtime_error("Too many squares on a rank")); + if (file_count > 8) + return false; f = static_cast(static_cast(f) + empty_squares); + prev_digit = true; } else { - INVALID_ARG_IF(file_count >= 8, "Too many pieces in one rank"); - INVALID_ARG_IF(!chess::is_valid(r, f), "Invalid file/rank position"); + INVALID_ARG_IF(!chess::is_valid(r, f), std::runtime_error("Invalid file/rank position")); + if (!chess::is_valid(r, f)) + return false; switch (c) { case 'p': placePiece(make_sq(r, f), BLACK); @@ -279,17 +346,20 @@ void _Position::setFEN(const std::string &str, bool chess960, FENPars placePiece(make_sq(r, f), WHITE); break; default: - INVALID_ARG_IF(false, "Invalid FEN character"); - break; + INVALID_ARG_IF(true, std::runtime_error("Invalid FEN character")); + return false; } ++file_count; f = static_cast(static_cast(f) + 1); + prev_digit = false; } } - INVALID_ARG_IF(file_count != 8, "Last rank must have 8 squares"); - INVALID_ARG_IF(rank_count != 7, "FEN must contain exactly 8 ranks"); + INVALID_ARG_IF(file_count != 8, std::runtime_error("Last rank must have 8 squares")); + INVALID_ARG_IF(rank_count != 7, std::runtime_error("FEN must contain exactly 8 ranks")); + if (file_count != 8 || rank_count != 7) + return false; } // 2. Turn @@ -299,38 +369,33 @@ void _Position::setFEN(const std::string &str, bool chess960, FENPars } else if (active_color == "b") { state().turn = BLACK; } else { - INVALID_ARG_IF(active_color != "w" && active_color != "b", "Expected white or black, got something else."); + INVALID_ARG_IF(active_color != "w" && active_color != "b", + std::runtime_error("Expected white or black, got something else.")); + return false; } // 3. Castling rights state().castlingRights = NO_CASTLING; if (castling != "-") { + bool result_castling = true; for (Color color : { WHITE, BLACK }) { - auto findKing = [&]() -> Square { - auto it = std::find_if(std::begin(pieces_list), std::end(pieces_list), [&](PieceC p) { - return p == make_piece(KING, color); - }); - INVALID_ARG_IF(it == std::end(pieces_list), "No king found for castling"); - return static_cast(it - pieces_list); - }; - - auto findRookQS = [&](Square king_sq, Color color) -> Square { + auto findRookQS = [&](Square king_sq, Color color_) -> Square { Rank r = rank_of(king_sq); for (int f = file_of(king_sq) - 1; f >= FILE_A; --f) { Square sq = make_sq(r, static_cast(f)); PieceC p = pieces_list[sq]; - if (p != PieceC::NO_PIECE && type_of(p) == ROOK && color_of(p) == color) + if (p != PieceC::NO_PIECE && type_of(p) == ROOK && color_of(p) == color_) return sq; } return SQ_NONE; }; - auto findRookKS = [&](Square king_sq, Color color) -> Square { + auto findRookKS = [&](Square king_sq, Color color_) -> Square { Rank r = rank_of(king_sq); for (int f = file_of(king_sq) + 1; f <= FILE_H; ++f) { Square sq = make_sq(r, static_cast(f)); PieceC p = pieces_list[sq]; - if (p != PieceC::NO_PIECE && type_of(p) == ROOK && color_of(p) == color) + if (p != PieceC::NO_PIECE && type_of(p) == ROOK && color_of(p) == color_) return sq; } return SQ_NONE; @@ -338,91 +403,153 @@ void _Position::setFEN(const std::string &str, bool chess960, FENPars bool allow_xfen = (mode == MODE_XFEN || mode == MODE_AUTO); bool allow_smk = (mode == MODE_SMK || mode == MODE_AUTO); auto apply = [&](char c) { - Square king_sq = findKing(); - if (king_sq == SQ_NONE) + Square king_sq = kingSq(color); + INVALID_ARG_IF(king_sq == SQ_NONE, std::runtime_error("Nonexistient king with inappropriate castling field")); + if (king_sq == SQ_NONE) { + result_castling = false; return; + } Square rook_ks = findRookKS(king_sq, color); Square rook_qs = findRookQS(king_sq, color); auto setKS = [&](Square rook_sq) { - INVALID_ARG_IF(rook_sq == SQ_NONE, "kingside rook not found"); - INVALID_ARG_IF(rank_of(king_sq) != rank_of(rook_sq), "kingside rook not on same rank"); + INVALID_ARG_IF(rook_sq == SQ_NONE, std::runtime_error("kingside rook not found")); + INVALID_ARG_IF(rank_of(king_sq) != rank_of(rook_sq), std::runtime_error("kingside rook not on same rank")); + if (rook_sq == SQ_NONE || (rank_of(king_sq) != rank_of(rook_sq))) { + result_castling = false; + return; + } if (color == WHITE) { state().castlingRights |= WHITE_OO; - state().castlingMetadata[WHITE].king_start = king_sq; - state().castlingMetadata[WHITE].rook_start_ks = rook_sq; + castling_meta_[WHITE].king_start = king_sq; + castling_meta_[WHITE].rook_start_ks = rook_sq; } else { state().castlingRights |= BLACK_OO; - state().castlingMetadata[BLACK].king_start = king_sq; - state().castlingMetadata[BLACK].rook_start_ks = rook_sq; + castling_meta_[BLACK].king_start = king_sq; + castling_meta_[BLACK].rook_start_ks = rook_sq; } }; auto setQS = [&](Square rook_sq) { - INVALID_ARG_IF(rook_sq == SQ_NONE, "queenside rook not found"); - INVALID_ARG_IF(rank_of(king_sq) != rank_of(rook_sq), "queenside rook not on same rank"); + INVALID_ARG_IF(rook_sq == SQ_NONE, std::runtime_error("queenside rook not found")); + INVALID_ARG_IF(rank_of(king_sq) != rank_of(rook_sq), std::runtime_error("queenside rook not on same rank")); + if (rook_sq == SQ_NONE || (rank_of(king_sq) != rank_of(rook_sq))) { + result_castling = false; + return; + } if (color == WHITE) { state().castlingRights |= WHITE_OOO; - state().castlingMetadata[WHITE].king_start = king_sq; - state().castlingMetadata[WHITE].rook_start_qs = rook_sq; + castling_meta_[WHITE].king_start = king_sq; + castling_meta_[WHITE].rook_start_qs = rook_sq; } else { state().castlingRights |= BLACK_OOO; - state().castlingMetadata[BLACK].king_start = king_sq; - state().castlingMetadata[BLACK].rook_start_qs = rook_sq; + castling_meta_[BLACK].king_start = king_sq; + castling_meta_[BLACK].rook_start_qs = rook_sq; } }; if (c == 'K' && color == WHITE) { - INVALID_ARG_IF(chess960 && !allow_xfen, "shredder fen into xfen parser"); - if (rook_ks == SQ_NONE) + INVALID_ARG_IF(chess960 && !allow_xfen, std::runtime_error("xfen into shredder fen parser")); + if (chess960 && !allow_xfen) { + result_castling = false; + return; + } + if (rook_ks == SQ_NONE) { + result_castling = false; return; - if (rank_of(king_sq) != rank_of(rook_ks)) + } + if (rank_of(king_sq) != rank_of(rook_ks)) { + result_castling = false; return; + } setKS(rook_ks); } else if (c == 'Q' && color == WHITE) { - INVALID_ARG_IF(chess960 && !allow_xfen, "shredder fen into xfen parser"); - if (rook_qs == SQ_NONE) + INVALID_ARG_IF(chess960 && !allow_xfen, std::runtime_error("xfen into shredder fen parser")); + if (chess960 && !allow_xfen) { + result_castling = false; + return; + } + if (rook_qs == SQ_NONE) { + result_castling = false; return; - if (rank_of(king_sq) != rank_of(rook_qs)) + } + if (rank_of(king_sq) != rank_of(rook_qs)) { + result_castling = false; return; + } setQS(rook_qs); } else if (c == 'k' && color == BLACK) { - INVALID_ARG_IF(chess960 && !allow_xfen, "shredder fen into xfen parser"); - if (rook_ks == SQ_NONE) + INVALID_ARG_IF(chess960 && !allow_xfen, std::runtime_error("xfen into shredder fen parser")); + if (chess960 && !allow_xfen) { + result_castling = false; return; - if (rank_of(king_sq) != rank_of(rook_ks)) + } + if (rook_ks == SQ_NONE) { + result_castling = false; + return; + } + if (rank_of(king_sq) != rank_of(rook_ks)) { + result_castling = false; return; + } setKS(rook_ks); } else if (c == 'q' && color == BLACK) { - INVALID_ARG_IF(chess960 && !allow_xfen, "shredder fen into xfen parser"); - if (rook_qs == SQ_NONE) + INVALID_ARG_IF(chess960 && !allow_xfen, std::runtime_error("xfen into shredder fen parser")); + INVALID_ARG_IF(rook_qs == SQ_NONE, std::runtime_error("Black queenside rook not exist")); + INVALID_ARG_IF(rank_of(king_sq) != rank_of(rook_qs), + std::runtime_error("Black queenside rook not in the same rank as black king")); + if (chess960 && !allow_xfen) { + result_castling = false; + return; + } + if (rook_qs == SQ_NONE) { + result_castling = false; return; - if (rank_of(king_sq) != rank_of(rook_qs)) + } + if (rank_of(king_sq) != rank_of(rook_qs)) { + result_castling = false; return; + } setQS(rook_qs); } else if (c >= 'A' && c <= 'H' && color == WHITE) { - INVALID_ARG_IF(chess960 && !allow_smk, "xfen into shredder fen parser"); + INVALID_ARG_IF(chess960 && !allow_smk, std::runtime_error("shredder fen into xfen parser")); + if (chess960 && !allow_smk) { + result_castling = false; + return; + } File f = static_cast(c - 'A'); Square rook_sq = make_sq(RANK_1, f); PieceC p = pieces_list[rook_sq]; INVALID_ARG_IF(p == PieceC::NO_PIECE || type_of(p) != ROOK || color_of(p) != WHITE, - "Invalid white Chess960 rook"); + std::runtime_error("Invalid white Chess960 rook")); + if (p == PieceC::NO_PIECE || type_of(p) != ROOK || color_of(p) != WHITE) { + result_castling = false; + return; + } (f > file_of(king_sq)) ? setKS(rook_sq) : setQS(rook_sq); } else if (c >= 'a' && c <= 'h' && color == BLACK) { - INVALID_ARG_IF(chess960 && !allow_smk, "xfen into shredder fen parser"); + INVALID_ARG_IF(chess960 && !allow_smk, std::runtime_error("shredder fen into xfen parser")); + if (chess960 && !allow_smk) { + result_castling = false; + return; + } File f = static_cast(c - 'a'); Square rook_sq = make_sq(RANK_8, f); PieceC p = pieces_list[rook_sq]; INVALID_ARG_IF(p == PieceC::NO_PIECE || type_of(p) != ROOK || color_of(p) != BLACK, - "Invalid black Chess960 rook"); + std::runtime_error("Invalid black Chess960 rook")); + if (p == PieceC::NO_PIECE || type_of(p) != ROOK || color_of(p) != BLACK) { + result_castling = false; + return; + } (f > file_of(king_sq)) ? setKS(rook_sq) : setQS(rook_sq); } @@ -430,7 +557,10 @@ void _Position::setFEN(const std::string &str, bool chess960, FENPars // ignore '-' }; - std::for_each(castling.begin(), castling.end(), apply); + for (char x : castling) + apply(x); + if (!result_castling) + return false; } } state().hash ^= zobrist::RandomCastle[state().castlingRights]; @@ -438,21 +568,21 @@ void _Position::setFEN(const std::string &str, bool chess960, FENPars for (Color c : { WHITE, BLACK }) { // king if (castlingRights() & (c & KING_SIDE)) { - const auto king_from = state().castlingMetadata[c].king_start; - const auto rook_from = make_sq(file_of(state().castlingMetadata[c].rook_start_ks), rank_of(king_from)); + const auto king_from = castling_meta_[c].king_start; + const auto rook_from = make_sq(file_of(castling_meta_[c].rook_start_ks), rank_of(king_from)); const auto king_to = castling_king_square(c, true); const auto rook_to = castling_rook_square(c, true); - state().castlingMetadata[c].castling_paths[true] = + castling_meta_[c].castling_paths[true] = (movegen::between(rook_from, rook_to) | movegen::between(king_from, king_to)) & ~((1ULL << king_from) | (1ULL << rook_from)); } // queen if (castlingRights() & (c & QUEEN_SIDE)) { - const auto king_from = state().castlingMetadata[c].king_start; - const auto rook_from = make_sq(file_of(state().castlingMetadata[c].rook_start_qs), rank_of(king_from)); + const auto king_from = castling_meta_[c].king_start; + const auto rook_from = make_sq(file_of(castling_meta_[c].rook_start_qs), rank_of(king_from)); const auto king_to = castling_king_square(c, false); const auto rook_to = castling_rook_square(c, false); - state().castlingMetadata[c].castling_paths[false] = + castling_meta_[c].castling_paths[false] = (movegen::between(rook_from, rook_to) | movegen::between(king_from, king_to)) & ~((1ULL << king_from) | (1ULL << rook_from)); } @@ -464,30 +594,36 @@ void _Position::setFEN(const std::string &str, bool chess960, FENPars Rank r = static_cast(enpassant[1] - '1'); Square ep_sq = make_sq(r, f); state().enPassant = ep_sq; - Bitboard ep_mask = 1ULL << ep_sq; - if (sideToMove() == WHITE) { - ep_mask >>= 8; - } else - ep_mask <<= 8; - ep_mask = ((ep_mask << 1) & ~attacks::MASK_FILE[0]) | ((ep_mask >> 1) & ~attacks::MASK_FILE[7]); - if (ep_mask & pieces(sideToMove())) { + if (ep_pawn_masks[ep_sq] & pieces(side_to_move())) { state().hash ^= zobrist::RandomEP[f]; state().epIncluded = true; } } else { - INVALID_ARG_IF(enpassant != "-", "Invalid en passant FEN field"); - state().enPassant = SQ_NONE; + INVALID_ARG_IF(enpassant != "-", std::runtime_error("Invalid en passant FEN field")); + if (enpassant != "-") + return false; } // 5. Halfmove clock + INVALID_ARG_IF(halfmove < 0 || fullmove < 1, std::runtime_error("Invalid halfmove and/or fullmove counters")); + if (halfmove < 0 || fullmove < 1) + return false; state().halfMoveClock = static_cast(halfmove); // 6. Fullmove number state().fullMoveNumber = fullmove; - refresh_attacks(); - state().repetition = state().pliesFromNull = 0; + rep_hashes_.push_back(state().hash); + if (popcount(pieces(WHITE, KING)) == 1 && popcount(pieces(BLACK, KING)) == 1) { + refresh_attacks(); + } else { + _rook_pin = _bishop_pin = _checkers = _check_mask = 0; + _pin_mask = 0; + } + return true; } +/// @brief Export the position as a FEN string. +/// @param xfen If true, use X-FEN castling notation (supports Chess960). template std::string _Position::fen(bool xfen) const { std::ostringstream ss; @@ -515,28 +651,28 @@ template std::string _Position::fen(boo } // 2) Side to move - ss << ' ' << (sideToMove() == WHITE ? 'w' : 'b'); + ss << ' ' << (side_to_move() == WHITE ? 'w' : 'b'); // 3) Castling availability ss << ' '; std::string castlingStr; if (chess960()) { if (castlingRights() & WHITE_OO) - castlingStr += (xfen && state().castlingMetadata[WHITE].rook_start_ks == SQ_H1) + castlingStr += (xfen && castling_meta_[WHITE].rook_start_ks == SQ_H1) ? 'K' - : static_cast('A' + file_of(state().castlingMetadata[WHITE].rook_start_ks)); + : static_cast('A' + file_of(castling_meta_[WHITE].rook_start_ks)); if (castlingRights() & WHITE_OOO) - castlingStr += (xfen && state().castlingMetadata[WHITE].rook_start_qs == SQ_A1) + castlingStr += (xfen && castling_meta_[WHITE].rook_start_qs == SQ_A1) ? 'Q' - : static_cast('A' + file_of(state().castlingMetadata[WHITE].rook_start_qs)); + : static_cast('A' + file_of(castling_meta_[WHITE].rook_start_qs)); if (castlingRights() & BLACK_OO) - castlingStr += (xfen && state().castlingMetadata[BLACK].rook_start_ks == SQ_H8) + castlingStr += (xfen && castling_meta_[BLACK].rook_start_ks == SQ_H8) ? 'k' - : static_cast('a' + file_of(state().castlingMetadata[BLACK].rook_start_ks)); + : static_cast('a' + file_of(castling_meta_[BLACK].rook_start_ks)); if (castlingRights() & BLACK_OOO) - castlingStr += (xfen && state().castlingMetadata[BLACK].rook_start_qs == SQ_A8) + castlingStr += (xfen && castling_meta_[BLACK].rook_start_qs == SQ_A8) ? 'q' - : static_cast('a' + file_of(state().castlingMetadata[BLACK].rook_start_qs)); + : static_cast('a' + file_of(castling_meta_[BLACK].rook_start_qs)); } else { if (castlingRights() & WHITE_OO) castlingStr += 'K'; @@ -555,32 +691,52 @@ template std::string _Position::fen(boo ss << (ep == SQ_NONE ? "-" : uci::squareToString(ep)); // 5) Halfmove clock - ss << ' ' << (int)halfmoveClock(); + ss << ' ' << (int)rule50_count(); // 6) Fullmove number - ss << ' ' << (int)fullmoveNumber(); + ss << ' ' << (int)fullmove_number(); return ss.str(); } +/// @brief Validate the current position for internal consistency. +/// @tparam Strict If true, also check for non-pawn material and legal castling rights. template template bool _Position::is_valid() const { if (count() != 1) return false; if (count() != 1) return false; - Color stm = sideToMove(); + Color stm = side_to_move(); // stm checking - bool whiteInCheck = isAttacked(kingSq(WHITE), BLACK); - bool blackInCheck = isAttacked(kingSq(BLACK), WHITE); + bool whiteInCheck = is_attacked(king_sq(WHITE), BLACK); + bool blackInCheck = is_attacked(king_sq(BLACK), WHITE); // Both kings cannot be in check simultaneously if (whiteInCheck && blackInCheck) return false; // The side to move cannot have its king currently in check from itself (nonsense) - if (isAttacked(kingSq(~stm), stm)) - return false; - if (piece_on(SQ_A1) != PieceC::WROOK && (castlingRights() & WHITE_OOO) != 0) + if (is_attacked(king_sq(~stm), stm)) return false; + if (castlingRights() & WHITE_OOO) { + Square qs_rook = castling_meta_[WHITE].rook_start_qs; + if (qs_rook == SQ_NONE || piece_on(qs_rook) != make_piece(ROOK, WHITE)) + return false; + } + if (castlingRights() & WHITE_OO) { + Square ks_rook = castling_meta_[WHITE].rook_start_ks; + if (ks_rook == SQ_NONE || piece_on(ks_rook) != make_piece(ROOK, WHITE)) + return false; + } + if (castlingRights() & BLACK_OOO) { + Square qs_rook = castling_meta_[BLACK].rook_start_qs; + if (qs_rook == SQ_NONE || piece_on(qs_rook) != make_piece(ROOK, BLACK)) + return false; + } + if (castlingRights() & BLACK_OO) { + Square ks_rook = castling_meta_[BLACK].rook_start_ks; + if (ks_rook == SQ_NONE || piece_on(ks_rook) != make_piece(ROOK, BLACK)) + return false; + } // pawns not on backrank if ((pieces(PAWN) & (attacks::MASK_RANK[RANK_1] | attacks::MASK_RANK[RANK_8])) != 0) return false; @@ -633,19 +789,20 @@ template template bool _Position CheckType _Position::givesCheck(Move move) const { const static auto getSniper = [](const _Position *p, const Square ksq, Bitboard oc) { - const auto us_occ = p->us(p->sideToMove()); + const auto us_occ = p->us(p->side_to_move()); const auto bishop = attacks::bishop(ksq, oc) & p->pieces(PieceType::BISHOP, PieceType::QUEEN) & us_occ; const auto rook = attacks::rook(ksq, oc) & p->pieces(PieceType::ROOK, PieceType::QUEEN) & us_occ; return (bishop | rook); }; - assert(color_of(at(move.from())) == sideToMove()); + assert(color_of(at(move.from())) == side_to_move()); const Square from = move.from(); const Square to = move.to(); - const Square ksq = kingSq(~sideToMove()); + const Square ksq = king_sq(~side_to_move()); const Bitboard toBB = 1ULL << (to); const PieceType pt = piece_of(at(from)); @@ -672,18 +829,18 @@ template CheckType _Position::givesChec if (Bitboard sniper = getSniper(this, ksq, oc)) { const auto sq = static_cast(pop_lsb(sniper)); - return (!(movegen::between(ksq, sq) & toBB) || move.typeOf() == Move::CASTLING) ? CheckType::DISCOVERY_CHECK - : CheckType::NO_CHECK; + return (!(movegen::between(ksq, sq) & toBB) || move.type_of() == Move::CASTLING) ? CheckType::DISCOVERY_CHECK + : CheckType::NO_CHECK; } - switch (move.typeOf()) { + switch (move.type_of()) { case Move::NORMAL: return CheckType::NO_CHECK; case Move::PROMOTION: { Bitboard attacks = 0ull; - switch (move.promotionType()) { + switch (move.promotion_type()) { case KNIGHT: attacks = attacks::knight(to); break; @@ -700,7 +857,7 @@ template CheckType _Position::givesChec break; } - return (attacks & pieces(PieceType::KING, ~sideToMove())) ? CheckType::DIRECT_CHECK : CheckType::NO_CHECK; + return (attacks & pieces(PieceType::KING, ~side_to_move())) ? CheckType::DIRECT_CHECK : CheckType::NO_CHECK; } case Move::ENPASSANT: { @@ -717,32 +874,8 @@ template CheckType _Position::givesChec assert(false); return CheckType::NO_CHECK; // Prevent a compiler warning } -template void _Position::refresh_attacks() { - const Color c = sideToMove(); - - Square king_sq = kingSq(c); - _bishop_pin = pinMask(c, king_sq); - _rook_pin = pinMask(c, king_sq); - _pin_mask = _bishop_pin | _rook_pin; - _checkers = attackers(~c, king_sq); - - switch (popcount(_checkers)) { - case 0: - _check_mask = ~0ULL; // no checks, full mask - break; - - case 1: { - auto sq = static_cast(lsb(_checkers)); - _check_mask = 1ULL << sq | movegen::between(king_sq, sq); - break; - } - - default: - _check_mask = 0ULL; // multiple checks, no blocking mask - break; - } -} +/// @brief Compute Zobrist hash for the current position. template uint64_t _Position::zobrist() const { uint64_t hash = 0; for (int sq = 0; sq < 64; ++sq) { @@ -757,45 +890,42 @@ template uint64_t _Position::zobrist() { const File f = file_of(ep_sq); Bitboard ep_mask = (1ULL << ep_sq); - - // Shift to the rank where the opposing pawn sits - const Color stm = sideToMove(); - // Color them = ~stm; + const Color stm = side_to_move(); ep_mask = (stm == WHITE) ? (ep_mask >> 8) : (ep_mask << 8); - - // Pawns on adjacent files only ep_mask = ((ep_mask << 1) & ~attacks::MASK_FILE[0]) | ((ep_mask >> 1) & ~attacks::MASK_FILE[7]); - if (ep_mask & pieces(stm)) hash ^= zobrist::RandomEP[f]; } return hash; } +/// @brief Parse a UCI move string into a Move object. template Move _Position::parse_uci(std::string uci) const { return uci::uciToMove(*this, uci); } +/// @brief Parse and immediately push (execute) a UCI move string. template Move _Position::push_uci(std::string uci) { const auto mv = parse_uci(std::move(uci)); - doMove(mv); + do_move(mv); return mv; } +/// @brief Compute the valid en-passant target square, or SQ_NONE if none. template Square _Position::_valid_ep_square() const { if (ep_square() == SQ_NONE) return SQ_NONE; Rank ep_rank; - ep_rank = sideToMove() == WHITE ? RANK_6 : RANK_3; + ep_rank = side_to_move() == WHITE ? RANK_6 : RANK_3; const Bitboard mask = 1ULL << ep_square(); - Bitboard pawn_mask = mask << 8; - Bitboard org_pawn_mask = mask >> 8; - if (sideToMove() == BLACK) + Bitboard pawn_mask = mask >> 8; + Bitboard org_pawn_mask = mask << 8; + if (side_to_move() == BLACK) std::swap(pawn_mask, org_pawn_mask); // rank 3 or rank 6, depending on color if (rank_of(ep_square()) != ep_rank) return SQ_NONE; // a pawn in 2 ranks behind - if (!(pieces(PAWN) & occ(~sideToMove()) & pawn_mask)) + if (!(pieces(PAWN) & occ(~side_to_move()) & pawn_mask)) return SQ_NONE; // ep_sq must be empty if (occ() & mask) @@ -805,52 +935,82 @@ template Square _Position::_valid_ep_sq return SQ_NONE; return ep_square(); } -template bool _Position::is_insufficient_material(Color c) const { +/// @brief Check if a given color has insufficient mating material. +template bool _Position::is_insufficient_material() const { const auto count = popcount(occ()); - // only kings, draw - if (count == 2) + if (count <= 2) return true; - // only bishop + knight, can't mate + auto white_bishops = pieces(BISHOP, WHITE); + auto black_bishops = pieces(BISHOP, BLACK); + bool has_white_bishop = white_bishops != 0; + bool has_black_bishop = black_bishops != 0; + if (count == 3) { - if (pieces(BISHOP, WHITE) || pieces(BISHOP, BLACK)) + if (has_white_bishop || has_black_bishop) return true; if (pieces(KNIGHT, WHITE) || pieces(KNIGHT, BLACK)) return true; + return false; } - // same-colored bishops, can't mate if (count == 4) { - // bishops on same color (one per side) - if (pieces(BISHOP, WHITE) && pieces(BISHOP, BLACK)) { - if (auto w = static_cast(lsb(pieces(BISHOP, WHITE))), b = static_cast(lsb(pieces(BISHOP, BLACK))); - ((9 * (w ^ b)) & 8) == 0) + + 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); + Bitboard queens = pieces(QUEEN, WHITE) | pieces(QUEEN, BLACK); + Bitboard pawns = pieces(PAWN, WHITE) | pieces(PAWN, BLACK); + + // no heavy pieces allowed for "insufficient material" cases + if (rooks || queens || pawns) + return false; + + // K + K + 2 minor pieces total scenario + if (popcount(knights) + popcount(bishops) == 2) { + + // K + N + N vs K + if (popcount(knights) == 2) return true; + + // K + B + B vs K (same color bishops only) + if (popcount(bishops) == 2) { + Square s1 = static_cast(lsb(bishops)); + Square s2 = static_cast(msb(bishops)); + + if (square_color(s1) == square_color(s2)) + return true; + } + + // K + N + B vs K + if (popcount(knights) == 1 && popcount(bishops) == 1) + return false; } - // one side with two bishops on same color - auto white_bishops = pieces(BISHOP, WHITE); - auto black_bishops = pieces(BISHOP, BLACK); + // mixed bishop-only edge case (rare but clean) + if (popcount(bishops) == 2 && popcount(knights) == 0) { + Square s1 = static_cast(lsb(bishops)); + Square s2 = static_cast(msb(bishops)); - if (popcount(white_bishops) == 2) { - if (auto b1 = static_cast(lsb(white_bishops)), b2 = static_cast(msb(white_bishops)); - ((9 * (b1 ^ b2)) & 8) == 0) - return true; - } else if (popcount(black_bishops) == 2) { - if (auto b1 = static_cast(lsb(black_bishops)), b2 = static_cast(msb(black_bishops)); - ((9 * (b1 ^ b2)) & 8) == 0) + if (square_color(s1) == square_color(s2)) return true; } } - return false; } +/// @brief Compute the set of castling rights that are physically valid on the board. template CastlingRights _Position::clean_castling_rights() const { - const Bitboard cr_BOO = state().castlingMetadata[BLACK].rook_start_ks; - const Bitboard cr_BOOO = state().castlingMetadata[BLACK].rook_start_qs; - const Bitboard cr_WOO = state().castlingMetadata[WHITE].rook_start_ks; - const Bitboard cr_WOOO = state().castlingMetadata[WHITE].rook_start_qs; + const Bitboard cr_BOO = 1ULL << static_cast(castling_meta_[BLACK].rook_start_ks); + const Bitboard cr_BOOO = 1ULL << static_cast(castling_meta_[BLACK].rook_start_qs); + const Bitboard cr_WOO = 1ULL << static_cast(castling_meta_[WHITE].rook_start_ks); + const Bitboard cr_WOOO = 1ULL << static_cast(castling_meta_[WHITE].rook_start_qs); Bitboard castling = 0; // mappings castling |= (castlingRights() & WHITE_OO) ? cr_WOO : 0; @@ -865,9 +1025,9 @@ template CastlingRights _Position::clea white_castling &= (cr_WOO | cr_WOOO); black_castling &= (cr_BOO | cr_BOOO); // king exists in e1/e8 depending on color - if (!(occ(WHITE) & pieces(KING) & (1ULL << state().castlingMetadata[WHITE].king_start))) + if (!(occ(WHITE) & pieces(KING) & (1ULL << castling_meta_[WHITE].king_start))) white_castling = 0; - if (!(occ(BLACK) & pieces(KING) & (1ULL << state().castlingMetadata[BLACK].king_start))) + if (!(occ(BLACK) & pieces(KING) & (1ULL << castling_meta_[BLACK].king_start))) black_castling = 0; castling = white_castling | black_castling; // Re-map @@ -880,11 +1040,10 @@ template CastlingRights _Position::clea } // clang-format off #define INSTANTIATE(PieceC) \ -template void _Position::setFEN(const std::string &, bool, FENParsingMode); \ +template bool _Position::setFEN(const std::string &, bool, FENParsingMode); \ template std::string _Position::fen(bool) const; \ template void _Position::doMove(const Move &move); \ template void _Position::doMove(const Move &move); \ -template void _Position::refresh_attacks(); \ template uint64_t _Position::zobrist() const; \ template Move _Position::parse_uci(std::string) const; \ template Move _Position::push_uci(std::string); \ diff --git a/position.h b/position.h index c879391..aeb6ab7 100644 --- a/position.h +++ b/position.h @@ -22,45 +22,57 @@ #include "movegen.h" #include "types.h" #include "zobrist.h" +#include #include #include #include + +/// @file position.h +/// @brief Chess position representation, move execution, and game-state queries. + namespace chess { +/// @struct HistoryEntry +/// @brief Saved position state for undo operations. +/// @tparam Piece Piece-enum type. template struct alignas(64) HistoryEntry { - // Bitboards for each piece type (white and black) - Bitboard pieces[7]; - Bitboard occ[COLOR_NB]; - Color turn; // true if white to move - Move mv; - Key hash; - uint8_t halfMoveClock; // Half-move clock for 50/75-move rule - uint16_t fullMoveNumber; // Full-move number (starts at 1) - bool epIncluded; - int8_t repetition = 0; + Bitboard pieces[7]{}; ///< Bitboards per piece type. + Bitboard occ[COLOR_NB]{}; ///< Occupancy per colour. + Color turn = COLOR_NB; ///< Side to move. + Move mv = Move::none(); ///< The move that led to this position. + 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). + bool epIncluded = false; + int8_t repetition = 0; ///< Repetition counter from this position. uint8_t pliesFromNull = 0; - Square enPassant = SQ_NONE; // En passant target square - Square kings[COLOR_NB] = { SQ_NONE }; - CastlingRights castlingRights; // Castling rights bitmask + Square enPassant = SQ_NONE; ///< En-passant target square. + Square kings[COLOR_NB] = { SQ_NONE, SQ_NONE }; + CastlingRights castlingRights; ///< Castling rights bitmask. Square incr_sqs[4] = { SQ_NONE, SQ_NONE, SQ_NONE, SQ_NONE }; Piece incr_pc[4] = { Piece::NO_PIECE, Piece::NO_PIECE, Piece::NO_PIECE, Piece::NO_PIECE }; - struct { - Square king_start = SQ_NONE; - Square rook_start_ks = SQ_NONE; - Square rook_start_qs = SQ_NONE; - std::array castling_paths; - } castlingMetadata[2]; - // implementation-specific implementations goes here + /// @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{}; + /// @} }; +/// @enum CheckType +/// @brief Classification of check on a move. enum class CheckType { NO_CHECK, DIRECT_CHECK, DISCOVERY_CHECK }; +/// @enum FENParsingMode +/// @brief FEN parsing mode for castling rights. enum FENParsingMode { MODE_XFEN, MODE_SMK, MODE_AUTO }; +/// @enum MoveGenType +/// @brief Flags controlling which pieces and move types are generated. enum class MoveGenType : uint16_t { NONE = 0, - // piece selectors PAWN = 1 << 1, KNIGHT = 1 << 2, BISHOP = 1 << 3, @@ -70,7 +82,6 @@ enum class MoveGenType : uint16_t { PIECE_MASK = PAWN | KNIGHT | BISHOP | ROOK | QUEEN | KING, - // move-type selectors CAPTURE = 1 << 7, QUIET = 1 << 8, @@ -84,13 +95,17 @@ template constexpr MoveGenType operator&(MoveGenType a, M template constexpr MoveGenType operator|(MoveGenType a, MoveGenType b) { using U = std::underlying_type_t; - return static_cast(static_cast(a) | - static_cast(b)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) + 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. template ::value>> class _Position { private: - // Move history stack std::vector> history; + std::vector rep_hashes_; Bitboard _rook_pin{}; Bitboard _bishop_pin{}; Bitboard _checkers{}; @@ -107,72 +122,82 @@ template castling_paths{}; + } castling_meta_[2]{}; + public: static inline constexpr auto START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; static inline constexpr auto START_CHESS960_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w HAha - 0 1"; - // Legal move generation functions - template void legals(Movelist &out) const { + /// @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. + /// @tparam ListT Move-list type (Movelist or CountOnlyList). + /// @param out Output move list. + template void legals(ListT &out) const { constexpr auto raw = static_cast(type); constexpr uint16_t pieceBits = raw & static_cast(MoveGenType::PIECE_MASK); constexpr uint16_t modeBits = raw & (static_cast(MoveGenType::CAPTURE) | static_cast(MoveGenType::QUIET)); - // ---------------------------------------- - // Resolve default piece selection - // ---------------------------------------- constexpr uint16_t effectivePieces = pieceBits ? pieceBits : (raw == static_cast(MoveGenType::NONE) ? 0 : static_cast(MoveGenType::PIECE_MASK)); - // ---------------------------------------- - // Resolve default mode selection - // ---------------------------------------- constexpr bool includeCaps = modeBits == 0 || (modeBits & static_cast(MoveGenType::CAPTURE)); constexpr bool includeQuiet = modeBits == 0 || (modeBits & static_cast(MoveGenType::QUIET)); constexpr bool captureOnly = includeCaps && !includeQuiet; - // Early-out for NONE if constexpr (effectivePieces == 0 && modeBits != 0) return; - // Now your existing piece dispatch logic stays the same: if constexpr (effectivePieces & static_cast(MoveGenType::PAWN)) { - movegen::genPawnSingleMoves(*this, out, _rook_pin, _bishop_pin, _check_mask); + movegen::genPawnSingleMoves(*this, out, _rook_pin, _bishop_pin, _check_mask); if constexpr (includeQuiet) - movegen::genPawnDoubleMoves(*this, out, _pin_mask, _check_mask); + movegen::genPawnDoubleMoves(*this, out, _pin_mask, _check_mask); if constexpr (includeCaps) - movegen::genEP(*this, out); + movegen::genEP(*this, out); } - if constexpr (effectivePieces & static_cast(MoveGenType::KNIGHT)) { - movegen::genKnightMoves(*this, out, _pin_mask, _check_mask); + movegen::genKnightMoves(*this, out, _pin_mask, _check_mask); } - if constexpr (effectivePieces & static_cast(MoveGenType::KING)) { - movegen::genKingMoves(*this, out, _pin_mask); + movegen::genKingMoves(*this, out, _pin_mask); } - if constexpr (effectivePieces & static_cast(MoveGenType::BISHOP)) { - movegen::genSlidingMoves(*this, out, _rook_pin, _bishop_pin, _check_mask); + movegen::genSlidingMoves(*this, out, _rook_pin, _bishop_pin, _check_mask); } - if constexpr (effectivePieces & static_cast(MoveGenType::ROOK)) { - movegen::genSlidingMoves(*this, out, _rook_pin, _bishop_pin, _check_mask); + movegen::genSlidingMoves(*this, out, _rook_pin, _bishop_pin, _check_mask); } - if constexpr (effectivePieces & static_cast(MoveGenType::QUEEN)) { - movegen::genSlidingMoves(*this, out, _rook_pin, _bishop_pin, _check_mask); + movegen::genSlidingMoves(*this, out, _rook_pin, _bishop_pin, _check_mask); } } - // Legal move generation functions - template inline void legals(Movelist &out) const { - switch (sideToMove()) { + /// @brief Count legal moves without storing them (uses CountOnlyList). + template inline uint64_t count_legals() const noexcept { + CountOnlyList moves; + legals(moves); + return moves.size_; + } + + /// @brief Generate legal moves (runtime colour dispatch). + template inline void legals(ListT &out) const { + switch (side_to_move()) { case WHITE: legals(out); return; @@ -184,26 +209,46 @@ template void doMove(const Move &move); + template void do_move(const Move &move) { doMove(move); } + + /// @brief Undo the last move. + /// @tparam RetAll If true, return the popped HistoryEntry. + /// @return The saved state if RetAll, otherwise void. template inline auto undoMove() -> std::conditional_t, void> { pieces_list[state().incr_sqs[0]] = state().incr_pc[0]; pieces_list[state().incr_sqs[1]] = state().incr_pc[1]; pieces_list[state().incr_sqs[2]] = state().incr_pc[2]; pieces_list[state().incr_sqs[3]] = state().incr_pc[3]; + rep_hashes_.pop_back(); + _rook_pin = state().saved_rook_pin; + _bishop_pin = state().saved_bishop_pin; + _checkers = state().saved_checkers; + _check_mask = state().saved_check_mask; + _pin_mask = _rook_pin | _bishop_pin; if constexpr (RetAll) { HistoryEntry state_ = state(); history.pop_back(); - refresh_attacks(); return state_; } else { history.pop_back(); - refresh_attacks(); return; } } + template inline auto undo_move() -> std::conditional_t, void> { + return undoMove(); + } + /// @brief Execute a null move (switch sides without moving). inline void doNullMove() { history.push_back(state()); + state().saved_rook_pin = _rook_pin; + state().saved_bishop_pin = _bishop_pin; + state().saved_checkers = _checkers; + state().saved_check_mask = _check_mask; state().incr_sqs[0] = state().incr_sqs[1] = state().incr_sqs[2] = state().incr_sqs[3] = SQ_NONE; state().incr_pc[0] = state().incr_pc[1] = state().incr_pc[2] = state().incr_pc[3] = PieceC::NO_PIECE; state().hash ^= (ep_square() != SQ_NONE && state().epIncluded) ? zobrist::RandomEP[file_of(ep_square())] : 0; @@ -211,35 +256,58 @@ template [[nodiscard]] inline Bitboard pieces(Color c) const { - assert(c != COLOR_NB); +#if defined(_CHESSLIB_ERROR_MODE_ASSERT) + assert(c != COLOR_NB && "color is COLOR_NB"); +#elif defined(_CHESSLIB_ERROR_MODE_THROW) + if (c == COLOR_NB) + throw std::runtime_error("color is COLOR_NB"); +#endif if constexpr (pt == PIECE_TYPE_NB || pt == ALL_PIECES) return occ(c); return state().pieces[pt] & state().occ[c]; } + + /// @brief Bitboard of a piece type for a colour (runtime colour). template [[nodiscard]] inline Bitboard pieces(PieceType pt) const { static_assert(c != COLOR_NB); if (pt == PIECE_TYPE_NB || pt == ALL_PIECES) return occ(c); return state().pieces[pt] & state().occ[c]; } + + /// @brief Bitboard of a piece type for a colour (compile-time both). template [[nodiscard]] inline Bitboard pieces() const { static_assert(c != COLOR_NB); if constexpr (pt == PIECE_TYPE_NB || pt == ALL_PIECES) return occ(c); return state().pieces[pt] & state().occ[c]; } + + /// @brief Bitboard of a piece type for a colour (runtime both). [[nodiscard]] inline Bitboard pieces(PieceType pt, Color c) const { - assert(c != COLOR_NB); - // still branchless +#if defined(_CHESSLIB_ERROR_MODE_ASSERT) + assert(c != COLOR_NB && "color is COLOR_NB"); +#elif defined(_CHESSLIB_ERROR_MODE_THROW) + if (c == COLOR_NB) + throw std::runtime_error("color is COLOR_NB"); +#endif switch (pt) { case PIECE_TYPE_NB: case ALL_PIECES: @@ -248,6 +316,8 @@ template (pt)) { case PIECE_TYPE_NB: @@ -257,50 +327,39 @@ template && ...)>> [[nodiscard]] inline Bitboard pieces(PTypes... ptypes) const { return (state().pieces[static_cast(ptypes)] | ...); } + /// @brief Union bitboard of multiple piece types for a colour. template && ...)>> [[nodiscard]] inline Bitboard pieces(Color c, PTypes... ptypes) const { return (pieces(ptypes, c) | ...); } - /** - * @brief Returns the origin squares of pieces of a given color attacking a target square - * - * Sample code to not make myself (and others) confused: - ```c++ - if (attackers(BLACK, E4)) { - std::cout << "E4 is under attack by black!"; - } - ``` - * - * @param color Attacker Color - * @param square Attacked Square - * @param occupied Board occupation - * @return Attackers to the bitboard - */ - [[nodiscard]] inline Bitboard attackers(Color color, Square square, Bitboard occupied) const { - auto queens = pieces(color); + /// @} - // using the fact that if we can attack PieceType from square, they can attack us back - auto atks = (attacks::pawn(~color, square) & pieces(color)); - atks |= (attacks::knight(square) & pieces(color)); - atks |= (attacks::bishop(square, occupied) & (pieces(color) | queens)); - atks |= (attacks::rook(square, occupied) & (pieces(color) | queens)); - atks |= (attacks::king(square) & pieces(color)); + /// @brief Get all pieces of a given colour attacking a target square. + /// @param colour Attacker colour. + /// @param square Attacked square. + /// @param occupied Occupancy bitboard. + /// @return Bitboard of attackers. + [[nodiscard]] inline Bitboard attackers(Color colour, Square square, Bitboard occupied) const { + auto queens = pieces(colour); + + auto atks = (attacks::pawn(~colour, square) & pieces(colour)); + atks |= (attacks::knight(square) & pieces(colour)); + atks |= (attacks::bishop(square, occupied) & (pieces(colour) | queens)); + atks |= (attacks::rook(square, occupied) & (pieces(colour) | queens)); + atks |= (attacks::king(square) & pieces(colour)); return atks & occupied; } - /** - * @brief Checks if a square is attacked by the given color. - * @param square - * @param color - * @return - */ + /// @brief Test whether a square is attacked by the given colour. [[nodiscard]] inline bool isAttacked(Square sq, Color by) const noexcept { const Bitboard occ_bb = occ(); const Bitboard us_bb = occ(by); @@ -312,13 +371,9 @@ template inline void placePiece(Square sq, Color c) { if constexpr (pt != NO_PIECE_TYPE) { Bitboard v = 1ULL << sq; @@ -342,6 +402,7 @@ template inline void removePiece(Square sq, Color c) { if constexpr (pt != NO_PIECE_TYPE) { Bitboard v = ~(1ULL << sq); @@ -354,10 +415,9 @@ template [[nodiscard]] inline Square square(Color c) const { return static_cast(lsb(pieces(c))); } [[nodiscard]] inline Square kingSq(Color c) const { return state().kings[c]; } + [[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. [[nodiscard]] inline Bitboard pin_mask() const { return _pin_mask; } + + /// @brief Construct from a FEN string. inline _Position(std::string fen = START_FEN, bool chess960 = false, FENParsingMode xfen = MODE_AUTO) { history.reserve(6144); + history.emplace_back(); + rep_hashes_.reserve(6144); setFEN(fen, chess960, xfen); } - [[nodiscard]] inline bool isCapture(Move mv) const { + + /// @brief Check whether a move is a capture. + [[nodiscard]] inline bool is_capture(Move mv) const { return mv.type_of() == EN_PASSANT || (mv.type_of() != CASTLING && piece_on(mv.to_sq()) != PieceC::NO_PIECE); } - [[nodiscard]] inline bool is_capture(Move mv) const { return isCapture(mv); } - [[nodiscard]] inline bool is_zeroing(Move mv) const { return isCapture(mv) || at(mv.from_sq()) == PAWN; } + [[nodiscard]] inline bool isCapture(Move mv) const { return is_capture(mv); } + + /// @brief Whether the move resets the 50-move clock (capture or pawn move). + [[nodiscard]] inline bool is_zeroing(Move mv) const { return is_capture(mv) || at(mv.from_sq()) == PAWN; } + [[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; - [[nodiscard]] inline uint8_t halfmoveClock() const { return state().halfMoveClock; } + [[nodiscard]] inline uint16_t fullmoveNumber() const { return state().fullMoveNumber; } + [[nodiscard]] inline uint16_t fullmove_number() const { return state().fullMoveNumber; } [[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); } [[nodiscard]] inline CastlingRights castlingRights() const { return state().castlingRights; } + + /// @brief Whether a move is a castling move. [[nodiscard]] inline bool is_castling(Move mv) const { return mv.type_of() == CASTLING; } + + /// @brief Raw Zobrist hash. uint64_t zobrist() const; - inline PieceC piece_at(Square sq) const { return piece_on(sq); } + + /// @brief Extract a property from a square. template inline T at(Square sq) const { - assert(chess::is_valid(sq)); if constexpr (std::is_same_v) - return piece_of(piece_at(sq)); + return piece_of(piece_on(sq)); else if constexpr (std::is_same_v) - return color_of(piece_at(sq)); + return color_of(piece_on(sq)); else - return piece_at(sq); + return piece_on(sq); } - inline Square enpassantSq() const { return ep_square(); } + + /// @brief Get the castling rights with only the active rook squares set. CastlingRights clean_castling_rights() const; - void setFEN(const std::string &str, bool chess960 = false, FENParsingMode xfen = MODE_AUTO); - inline void set_fen(const std::string &str, bool chess960 = false, FENParsingMode xfen = MODE_AUTO) { - setFEN(str, chess960, xfen); - } - inline void setFen(const std::string &str, bool chess960 = false, FENParsingMode xfen = MODE_AUTO) { - setFEN(str, chess960, xfen); + + /// @brief Set position from FEN. + bool setFEN(const std::string &str, bool chess960 = false, FENParsingMode xfen = MODE_AUTO); + inline bool set_fen(const std::string &str, bool chess960 = false, FENParsingMode xfen = MODE_AUTO) { + return setFEN(str, chess960, xfen); } + + /// @brief Parse a UCI move string for this position. Move parse_uci(std::string) const; + + /// @brief Parse and execute a UCI move. Move push_uci(std::string); + + /// @brief Compute the valid en-passant square (if any). Square _valid_ep_square() const; + + /// @name Piece counts + /// @{ template inline int count() const { return popcount(pieces(pt)); } template inline int count() const { return popcount(pieces()); } template inline int count(Color c) const { return popcount(pieces(c)); } inline int count(PieceType pt, Color c) const { return popcount(pieces(pt, c)); } - inline int ply() const { return 2 * (state().fullMoveNumber - 1) + (sideToMove() == BLACK); } - bool is_insufficient_material(Color c) const; - inline bool isInsufficientMaterial(Color c) const { return is_insufficient_material(c); } - inline bool hasInsufficientMaterial(Color c) const { return is_insufficient_material(c); } - inline bool has_insufficient_material(Color c) const { return is_insufficient_material(c); } - inline bool is_insufficient_material() const { - return has_insufficient_material(WHITE) && has_insufficient_material(BLACK); - } - inline bool isInsufficientMaterial() const { return is_insufficient_material(); } - inline bool hasNonPawnMaterial(Color c) const { return bool(us(c) ^ (pieces(PAWN, KING) & us(c))); } - inline bool inCheck() const { return checkers() != 0LL; } + /// @} + + /// @brief Ply count from the start of the game. + inline int ply() const { return 2 * (state().fullMoveNumber - 1) + (side_to_move() == BLACK); } + + /// @brief Test for draw by insufficient material. + bool is_insufficient_material() const; + + /// @brief Whether a colour has any non-pawn, non-king material. + inline bool hasNonPawnMaterial(Color c) const { return bool(us(c) & ~(pieces(PAWN) | pieces(KING)) & occ(c)); } + + /// @brief Whether the side to move is in check. inline bool is_check() const { return checkers() != 0LL; } + + /// @name Castling-right queries + /// @{ inline bool has_castling_rights(Color c) const { return castlingRights(c) != 0; } inline bool has_kingside_castling_rights(Color c) const { return (castlingRights(c) & KING_SIDE) != 0; } inline bool has_queenside_castling_rights(Color c) const { return (castlingRights(c) & QUEEN_SIDE) != 0; } - // Return true if a position repeats once earlier but strictly - // after the root, or repeats twice before or at the root. + /// @} + + /// @brief Whether the position has repeated the given number of times. inline bool is_repetition(int ply) const { return state().repetition + 1 >= ply; } inline int repetition_count() const { return state().repetition; } - // Test if it's draw of 50 move rule (that forces everyone to draw). It doesn't consider checkmates. + + /// @brief Whether the position is a draw (50-move or repetition). inline bool is_draw(int ply) const { return rule50_count() > 99 || is_repetition(ply); } - // Tests whether there has been at least one repetition - // of positions since the last capture or pawn move. + + /// @brief Whether there has been at least one repetition since the last capture or pawn move. inline bool has_repeated() const { auto idx = history.size() - 1; - int end = std::min(rule50_count(), state().pliesFromNull); + int end = std::min({ static_cast(rule50_count()), + static_cast(state().pliesFromNull), + static_cast(history.size()) - 1 }); while (end-- >= 4) { if (history[idx].repetition) return true; - idx--; } return false; } + inline bool _is_halfmoves(int n) const { return rule50_count() >= n; } inline bool chess960() const { return _chess960; } inline bool is_seventyfive_moves() const { return _is_halfmoves(150); } inline bool is_fifty_moves() const { return _is_halfmoves(100); } inline bool is_fivefold_repetition() const { return is_repetition(5); } + + /// @brief Whether a square is attacked by a colour (with optional custom occupancy). 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; } + /// @brief Whether the previous move left the opponent in check. inline bool was_into_check() const { bool atk = false; - Bitboard bb = pieces(~sideToMove()); + Bitboard bb = pieces(~side_to_move()); while (!atk && bb) { - atk |= isAttacked((Square)pop_lsb(bb), sideToMove()); + atk |= is_attacked((Square)pop_lsb(bb), side_to_move()); } return atk != 0; } + + /// @brief Get attackers mask for a colour to a square. inline Bitboard attackers_mask(Color color, Square square, Bitboard occupied) const { auto queens = pieces(color); - // using the fact that if we can attack PieceType from square, they can attack us back auto atks = (attacks::pawn(~color, square) & pieces(color)); atks |= (attacks::knight(square) & pieces(color)); atks |= (attacks::bishop(square, occupied) & (pieces(color) | queens)); @@ -530,89 +648,174 @@ template (pop_lsb(path)), occupied); + b |= attackers_mask(~side_to_move(), static_cast(pop_lsb(path)), occupied); } return b != 0; } + + /// @brief Whether the current side is checkmated. inline bool is_checkmate() const { Movelist moves; legals(moves); - return inCheck() && !moves.size(); + return is_check() && !moves.size(); } + + /// @brief Whether the current side is stalemated. inline bool is_stalemate() const { Movelist moves; legals(moves); - return !inCheck() && !moves.size(); + return !is_check() && !moves.size(); } - // Material-only key (note: Zobrist=Zpieces^Zep^Zcastling^Zturn, we just XORs the remaining, it's trivial) + + /// @brief Compute the material-only key (excludes turn, EP, castling). inline Key material_key() const { - return hash() ^ (zobrist::RandomTurn * ~sideToMove()) ^ (zobrist::RandomCastle[castlingRights()]) ^ - (zobrist::RandomEP[ep_square() == SQ_NONE ? file_of(ep_square()) : FILE_NB]); + return hash() ^ (zobrist::RandomTurn * ~side_to_move()) ^ (zobrist::RandomCastle[castlingRights()]) ^ + (state().epIncluded ? zobrist::RandomEP[file_of(ep_square())] : zobrist::RandomEP[FILE_NB]); } + + /// @brief Validate position consistency. template bool is_valid() const; + + /// @brief Classify check type for a move. CheckType givesCheck(Move move) const; - /** - * @brief Checks if the current position is a draw by 50 move rule. - * Keep in mind that by the rules of chess, if the position has 50 half - * moves it's not necessarily a draw, since checkmate has higher priority, - * call getHalfMoveDrawType, - * to determine whether the position is a draw or checkmate. - * @return - */ - [[nodiscard]] inline bool isHalfMoveDraw() const noexcept { return halfmoveClock() >= 100; } + [[nodiscard]] inline CheckType gives_check(Move move) const { return givesCheck(move); } + + /// @brief Whether the 50-move rule applies (>= 100 half-moves). + [[nodiscard]] inline bool isHalfMoveDraw() const noexcept { return rule50_count() >= 100; } + [[nodiscard]] inline bool is_half_move_draw() const noexcept { return isHalfMoveDraw(); } + + /// @brief Get the castling path bitboard for a colour and side. [[nodiscard]] inline Bitboard getCastlingPath(Color c, bool isKingSide) const { - return state().castlingMetadata[c].castling_paths[isKingSide]; + return castling_meta_[c].castling_paths[isKingSide]; } - [[nodiscard]] inline auto getCastlingMetadata(Color c) const { return state().castlingMetadata[c]; } + [[nodiscard]] inline Bitboard get_castling_path(Color c, bool isKingSide) const { return getCastlingPath(c, isKingSide); } + [[nodiscard]] inline auto getCastlingMetadata(Color c) const { return castling_meta_[c]; } + [[nodiscard]] inline auto get_castling_metadata(Color c) const { return getCastlingMetadata(c); } private: - template [[nodiscard]] inline Bitboard pinMask(Color c, Square sq) const { - static_assert(pt == BISHOP || pt == ROOK, "Only bishop or rook allowed!"); + /// @brief Compute pin masks for the king at sq. + void pinMasks(Color c, Square sq, Bitboard &rook_pin, Bitboard &bishop_pin) const { Bitboard occ_opp = occ(~c); Bitboard occ_us = occ(c); - Bitboard opp_sliders; - opp_sliders = (pieces(~c) | pieces(QUEEN, ~c)) & occ_opp; + Bitboard opp_queens = pieces(QUEEN, ~c) & occ_opp; - auto pt_attacks = attacks::slider(sq, occ_opp) & opp_sliders; + Bitboard opp_bishops = (pieces(~c) | opp_queens); + Bitboard bishop_atks = attacks::slider(sq, occ_opp) & opp_bishops; - Bitboard pin = 0ull; + Bitboard opp_rooks = (pieces(~c) | opp_queens); + Bitboard rook_atks = attacks::slider(sq, occ_opp) & opp_rooks; - while (pt_attacks) { - const auto possible_pin = movegen::between(sq, Square(pop_lsb(pt_attacks))); - Bitboard tmp = possible_pin & occ_us; - bool v = tmp && (tmp & (tmp - 1)) == 0; - pin |= v ? possible_pin : 0; + rook_pin = 0; + bishop_pin = 0; + while (bishop_atks) { + auto possible = movegen::between(sq, Square(pop_lsb(bishop_atks))); + Bitboard tmp = possible & occ_us; + if (tmp && (tmp & (tmp - 1)) == 0) + bishop_pin |= possible; + } + while (rook_atks) { + auto possible = movegen::between(sq, Square(pop_lsb(rook_atks))); + Bitboard tmp = possible & occ_us; + if (tmp && (tmp & (tmp - 1)) == 0) + rook_pin |= possible; + } + } + + /// @brief Recompute cached attack data (pins, checkers, check mask) — iterative, no magic lookups. + [[gnu::always_inline]] void refresh_attacks() { + const Color c = side_to_move(); + const Square ksq = kingSq(c); + const Bitboard occ_all = occ(); + const Bitboard occ_us = occ(c); + + Bitboard bishop_pin = 0, rook_pin = 0, checkers = 0; + + // Bishop-like: iterate all enemy bishops/queens + Bitboard bLike = pieces(~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; } - return pin; + // 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; + } + + // Pawn and knight checkers (precomputed tables, no magic lookups) + checkers |= (attacks::pawn(c, ksq) & pieces(~c)); + checkers |= (attacks::knight(ksq) & pieces(~c)); + + _bishop_pin = bishop_pin; + _rook_pin = rook_pin; + _pin_mask = rook_pin | bishop_pin; + _checkers = checkers; + switch (popcount(_checkers)) { + case 0: + _check_mask = ~0ULL; + break; + case 1: { + auto sq = static_cast(lsb(_checkers)); + _check_mask = 1ULL << sq | movegen::between(ksq, sq); + break; + } + default: + _check_mask = 0ULL; + break; + } } - void refresh_attacks(); + inline const auto &state() const { return history.back(); } inline auto &state() { return history.back(); } public: - inline _Position(const _Position &other) : history(other.history), _chess960(other._chess960) { + /// @brief Copy constructor (deep copy of position state). + inline _Position(const _Position &other) + : history(other.history), rep_hashes_(other.rep_hashes_), _chess960(other._chess960), + castling_meta_{ other.castling_meta_[0], other.castling_meta_[1] } { std::copy(std::begin(other.pieces_list), std::end(other.pieces_list), std::begin(pieces_list)); refresh_attacks(); } }; + namespace attacks { -/** - * @brief Returns the attacks for a given piece on a given square - * @param board - * @param color - * @param square - * @return - */ +/// @brief Convenience wrapper: get attackers from a position. +/// @tparam T Piece-enum type. +/// @param board The position. +/// @param colour Attacker colour. +/// @param square Attacked square. +/// @return Bitboard of attackers. template ::value>> -[[nodiscard]] inline Bitboard attackers(const _Position &board, Color color, Square square) noexcept { - return board.attackers(color, square); +[[nodiscard]] inline Bitboard attackers(const _Position &board, Color colour, Square square) noexcept { + return board.attackers(colour, square); } } // namespace attacks -// Aliases + using Position = _Position; -using Board = _Position; +using Board [[deprecated("Use Position instead")]] = _Position; }; // namespace chess diff --git a/printers.cpp b/printers.cpp index 75f920d..f18ea5e 100644 --- a/printers.cpp +++ b/printers.cpp @@ -16,6 +16,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ + +/// @file printers.cpp +/// @brief Stream-operator overloads for board display, move printing, and FEN output. + #include "printers.h" #include "moves_io.h" #include "position.h" @@ -24,8 +28,9 @@ #include #include namespace chess { -template using DescriptiveNameNotation = std::unordered_map; +template using DescriptiveNameNotation = const std::unordered_map; +/// @brief Print a Position as a text board (ranks 8-1, pieces, side-to-move, castling rights, EP square). template std::ostream &operator<<(std::ostream &os, const _Position &pos) { // RAII guard to save/restore stream state struct ios_guard { @@ -52,23 +57,24 @@ template std::ostream &operator<<(std::ostream &os, os << " a b c d e f g h\n"; // Ensure key is printed in hex, but restores after this function - os << "\nFen: " << pos.fen() << "\nKey: " << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << pos.key() + os << "\nFen: " << pos.fen() << "\nKey: " << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << pos.hash() << '\n'; return os; } -std::ostream &operator<<(std::ostream &os, const Move mv) { - os << uci::moveToUci(mv); - return os; -} +/// @brief Print a Move as UCI string. +std::ostream &operator<<(std::ostream &os, const Move mv) { return os << mv.uci(); } + +/// @brief Print a Color as "WHITE" or "BLACK". std::ostream &operator<<(std::ostream &os, const Color c) { DescriptiveNameNotation colors = { { WHITE, "WHITE" }, { BLACK, "BLACK" }, { COLOR_NB, "COLOR_NB" } }; - return os << colors[c]; + return os << colors.at(c); } +/// @brief Print a PieceType as "PAWN", "KNIGHT", etc. std::ostream &operator<<(std::ostream &os, const PieceType c) { DescriptiveNameNotation pieces = { { NO_PIECE_TYPE, "NO_PIECE_TYPE/ALL_PIECES" }, @@ -80,8 +86,9 @@ std::ostream &operator<<(std::ostream &os, const PieceType c) { { KING, "KING" }, { PIECE_TYPE_NB, "PIECE_TYPE_NB" }, }; - return os << pieces[c]; + return os << pieces.at(c); } +/// @brief Print CastlingRights as "KkQq" style string. std::ostream &operator<<(std::ostream &os, const CastlingRights cr) { DescriptiveNameNotation castlingFlags = { { NO_CASTLING, "NO_CASTLING" }, @@ -102,59 +109,33 @@ std::ostream &operator<<(std::ostream &os, const CastlingRights cr) { { WHITE_OOO | BLACK_OO | BLACK_OOO, "WHITE_OOO | BLACK_OO | BLACK_OOO" }, }; - return os << castlingFlags[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; } -std::ostream &operator<<(std::ostream &os, const Square sq) { - os << "SQ_" << str_toupper(chess::uci::squareToString(sq)); - return os; -} +/// @brief Print a Square as algebraic notation (e.g. "e2"). +std::ostream &operator<<(std::ostream &os, const Square sq) { return os << uci::squareToString(sq); } + +/// @brief Print a piece (color + type), e.g. "wP", "bK". template std::ostream &operator<<(std::ostream &os, PieceC p) { - char c = '.'; - switch (p) { - case PieceC::WPAWN: - c = 'P'; - break; - case PieceC::BPAWN: - c = 'p'; - break; - case PieceC::WKNIGHT: - c = 'N'; - break; - case PieceC::BKNIGHT: - c = 'n'; - break; - case PieceC::WBISHOP: - c = 'B'; - break; - case PieceC::BBISHOP: - c = 'b'; - break; - case PieceC::WROOK: - c = 'R'; - break; - case PieceC::BROOK: - c = 'r'; - break; - case PieceC::WQUEEN: - c = 'Q'; - break; - case PieceC::BQUEEN: - c = 'q'; - break; - case PieceC::WKING: - c = 'K'; - break; - case PieceC::BKING: - c = 'k'; - break; - default: - break; - } - return os << c; + DescriptiveNameNotation pieces = { + { PieceC::WPAWN, "P" }, + { PieceC::BPAWN, "p" }, + { PieceC::WKNIGHT, "N" }, + { PieceC::BKNIGHT, "n" }, + { PieceC::WBISHOP, "B" }, + { PieceC::BBISHOP, "b" }, + { PieceC::WROOK, "R" }, + { PieceC::BROOK, "r" }, + { PieceC::WQUEEN, "Q" }, + { PieceC::BQUEEN, "q" }, + { PieceC::WKING, "K" }, + { PieceC::BKING, "k" }, + { PieceC::NO_PIECE, "." }, + }; + return os << pieces.at(p); } #define INSTANTITATE(PieceC) \ diff --git a/printers.h b/printers.h index 1028bb4..5cbc50a 100644 --- a/printers.h +++ b/printers.h @@ -21,17 +21,37 @@ #include #include #include + +/// @file printers.h +/// @brief Stream-output operators for chess types. + namespace chess { -// disclaimer: please don't pass Chess960 moves for move functions, use uci::uciToMove + +/// @brief Print a Move as UCI string. +/// @param os Output stream. +/// @param mv The move. +/// @return Reference to the stream. std::ostream &operator<<(std::ostream &os, const Move mv); + +/// @brief Print a Color ("WHITE" or "BLACK"). std::ostream &operator<<(std::ostream &os, const Color c); + +/// @brief Print CastlingRights (e.g. "KQkq"). std::ostream &operator<<(std::ostream &os, const CastlingRights cr); + +/// @brief Print a piece (e.g. "P", "N", "K"). template ::value>> std::ostream &operator<<(std::ostream &os, PieceC p); + +/// @brief Print a square (e.g. "e4"). std::ostream &operator<<(std::ostream &os, const Square sq); + +/// @brief Print a PieceType (e.g. "PAWN", "KNIGHT"). std::ostream &operator<<(std::ostream &os, const PieceType pt); + template class _Position; +/// @brief Print a board position in human-readable ASCII. template ::value>> std::ostream &operator<<(std::ostream &os, const _Position &pos); -} // namespace chess \ No newline at end of file +} // namespace chess diff --git a/tests.cpp b/tests.cpp index dfb1eae..097d947 100644 --- a/tests.cpp +++ b/tests.cpp @@ -16,13 +16,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#define DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#if !defined(__cpp_exceptions) && !defined(_CPPUNWIND) && !defined(__EXCEPTIONS) && !defined(_CHESSLIB_ERROR_MODE_THROW) #define DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#endif #include "position.h" #include "printers.h" #include #include #include +#include using namespace chess; // --------- Color assertions ---------- static_assert(color_of(PolyglotPiece::BPAWN) == BLACK, "BPAWN should be BLACK"); @@ -184,7 +187,7 @@ 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"); -#if defined(_DEBUG) || !defined(NDEBUG) +#ifndef NDEBUG #define IS_RELEASE 0 #else #define IS_RELEASE 1 @@ -197,17 +200,21 @@ template struct TestEntry { InputT input; CheckInfo info; }; -template uint64_t perft(_Position &pos, int depth) { +template uint64_t perft(_Position &pos, int depth) { if (depth == 0) { return 1; } else if (depth == 1) { - Movelist moves; - pos.template legals(moves); - if constexpr (EnableDiv) - for (const Move &m : moves) { + if constexpr (EnableDiv) { + Movelist moves; + pos.template legals(moves); + for (const Move &m : moves) std::cout << m << ": 1\n"; - } - return moves.size(); + return moves.size(); + } else { + CountOnlyList moves; + pos.template legals(moves); + return moves.size_; + } } else { Movelist moves; pos.template legals(moves); @@ -216,32 +223,27 @@ template uint64_t perft(_Po pos.template doMove(m); #if !IS_RELEASE { - const auto pre_nm_hash_1 = pos.hash(); - const auto pre_nm_fen_1 = pos.fen(); - if (pos.zobrist() != pos.hash()) - REQUIRE(pos.zobrist() == pos.hash()); + const auto pre_nm_hash = pos.hash(); pos.doNullMove(); pos.undoMove(); - if (!(pos.hash() == pre_nm_hash_1 && pos.fen() == pre_nm_fen_1 && pos.zobrist() == pre_nm_hash_1)) { - REQUIRE(pos.hash() == pre_nm_hash_1); - REQUIRE(pos.fen() == pre_nm_fen_1); - REQUIRE(pos.zobrist() == pre_nm_hash_1); + if (pos.hash() != pre_nm_hash || pos.zobrist() != pre_nm_hash) { + // Compute fen() only on failure (extremely rare) + const auto post_fen = pos.fen(); + REQUIRE(!"Hash changed after null move"); + REQUIRE(pos.zobrist() == pre_nm_hash); } } #endif - const uint64_t nodes = perft(pos, depth - 1); + const uint64_t nodes = perft(pos, depth - 1); #if !IS_RELEASE { - const auto pre_nm_hash_1 = pos.hash(); - const auto pre_nm_fen_1 = pos.fen(); - if (pos.zobrist() != pos.hash()) - REQUIRE(pos.zobrist() == pos.hash()); + const auto pre_nm_hash = pos.hash(); pos.doNullMove(); pos.undoMove(); - if (!(pos.hash() == pre_nm_hash_1 && pos.fen() == pre_nm_fen_1 && pos.zobrist() == pre_nm_hash_1)) { - REQUIRE(pos.hash() == pre_nm_hash_1); - REQUIRE(pos.fen() == pre_nm_fen_1); - REQUIRE(pos.zobrist() == pre_nm_hash_1); + if (pos.hash() != pre_nm_hash || pos.zobrist() != pre_nm_hash) { + const auto post_fen = pos.fen(); + REQUIRE(!"Hash changed after null move"); + REQUIRE(pos.zobrist() == pre_nm_hash); } } #endif @@ -285,74 +287,46 @@ auto split_testcases(std::vector> &entries) { return optimized; } #endif +template +void check_perft_type(TestEntry &entry, uint64_t &nodes, double &elapsed) { + using namespace std::chrono; + _Position pos(entry.input); + auto start_time = high_resolution_clock::now(); + if (pos.side_to_move() == WHITE) + REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); + else + REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); + auto end_time = high_resolution_clock::now(); + elapsed += duration(end_time - start_time).count(); + nodes += entry.info.nodes; + if (entry.info.nodes < 5e6) { + _Position pos2 = pos; + REQUIRE(pos.fen() == pos2.fen()); + start_time = high_resolution_clock::now(); + if (pos2.side_to_move() == WHITE) + REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); + else + REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); + end_time = high_resolution_clock::now(); + elapsed += duration(end_time - start_time).count(); + nodes += entry.info.nodes; + } else { + std::cerr << "\n(skipped copying test)\n"; + } +} template void check_perfts(std::vector> &entries) { uint64_t nodes = 0; double elapsed = 0; - using namespace std::chrono; #if !IS_RELEASE entries = split_testcases(entries); #endif for (auto &entry : entries) { std::cerr << entry.input << " (chess960=false) " << entry.info.depth; std::cerr << '\n'; - { - _Position pos(entry.input); - auto start_time = high_resolution_clock::now(); - REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); - auto end_time = high_resolution_clock::now(); - elapsed += duration(end_time - start_time).count(); - nodes += entry.info.nodes; - if (entry.info.nodes < 5e6) { - _Position pos2 = pos; - REQUIRE(pos.fen() == pos2.fen()); - auto start_time = high_resolution_clock::now(); - REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); - auto end_time = high_resolution_clock::now(); - elapsed += duration(end_time - start_time).count(); - nodes += entry.info.nodes; - } else { - std::cerr << "\n(skipped copying test)\n"; - } - } - { - _Position pos(entry.input); - auto start_time = high_resolution_clock::now(); - REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); - auto end_time = high_resolution_clock::now(); - elapsed += duration(end_time - start_time).count(); - nodes += entry.info.nodes; - if (entry.info.nodes < 5e6) { - _Position pos2 = pos; - REQUIRE(pos.fen() == pos2.fen()); - auto start_time = high_resolution_clock::now(); - REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); - auto end_time = high_resolution_clock::now(); - elapsed += duration(end_time - start_time).count(); - nodes += entry.info.nodes; - } else { - std::cerr << "\n(skipped copying test)\n"; - } - } - { - _Position pos(entry.input); - auto start_time = high_resolution_clock::now(); - REQUIRE(perft(pos, entry.info.depth) == entry.info.nodes); - auto end_time = high_resolution_clock::now(); - elapsed += duration(end_time - start_time).count(); - nodes += entry.info.nodes; - if (entry.info.nodes < 5e6) { - _Position pos2 = pos; - REQUIRE(pos.fen() == pos2.fen()); - auto start_time = high_resolution_clock::now(); - REQUIRE(perft(pos2, entry.info.depth) == entry.info.nodes); - auto end_time = high_resolution_clock::now(); - elapsed += duration(end_time - start_time).count(); - nodes += entry.info.nodes; - } else { - std::cerr << "\n(skipped copying test)\n"; - } - } + check_perft_type(entry, nodes, elapsed); + check_perft_type(entry, nodes, elapsed); + check_perft_type(entry, nodes, elapsed); } double mnps = (nodes / elapsed) / 1'000'000.0; std::cout << "Speed: " << mnps << "Mnps\n"; @@ -566,6 +540,10 @@ TEST_CASE("Captures only?") { } 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 }, { "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 }, @@ -575,7 +553,6 @@ TEST_CASE("Perfts" * doctest::timeout(36000)) { { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 4, 197281 }, { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 5, 4865609 }, { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 6, 119060324 }, - { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 6, 119060324 }, { "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 7, 3195901860 }, { "rnbqkbnr/pppppppp/8/8/8/3P4/PPP1PPPP/RNBQKBNR b KQkq - 0 1", 4, 328511 }, { "rnbqkbnr/pp1ppppp/2p5/8/8/3P4/PPP1PPPP/RNBQKBNR w KQkq - 0 2", 3, 15206 }, @@ -671,10 +648,8 @@ TEST_CASE("Perfts" * doctest::timeout(36000)) { // https://github.com/SebLague/Chess-Coding-Adventure/blob/Chess-V2-Unity/Assets/Scripts/Testing/Perft/Suites/Suite%20Full.txt // converted to [fen, depth, nodes], deduped tests { "2b1b3/1r1P4/3K3p/1p6/2p5/6k1/1P3p2/4B3 w - - 0 42", 5, 5617302 }, - { "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -", 6, 11030083 }, { "r3k2r/pp3pp1/PN1pr1p1/4p1P1/4P3/3P4/P1P2PP1/R3K2R w KQkq - 4 4", 5, 15587335 }, { "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", 5, 89941194 }, - { "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -", 5, 193690690 }, { "r3k1nr/p2pp1pp/b1n1P1P1/1BK1Pp1q/8/8/2PP1PPP/6N1 w kq - 0 1", 4, 497787 }, { "3k4/8/8/8/8/8/8/R3K3 w Q - 0 1", 7, 15594314 }, { "2K2r2/4P3/8/8/8/8/8/3k4 w - - 0 1", 6, 3821001 }, @@ -1487,10 +1462,3 @@ TEST_CASE("Perfts" * doctest::timeout(36000)) { }; check_perfts(tests); } -int main(int argc, char **argv) { - doctest::Context ctx; - ctx.setOption("success", true); - ctx.setOption("no-breaks", true); - ctx.setOption("abort-after", 1); - return ctx.run(); -} diff --git a/types.h b/types.h index 98968e6..975ea6b 100644 --- a/types.h +++ b/types.h @@ -18,7 +18,6 @@ */ // enums, structures, Move class taken from Stockfish - // License: https://github.com/official-stockfish/Stockfish/blob/master/COPYING.txt #pragma once @@ -27,6 +26,12 @@ #include #include #include + +/// @file types.h +/// @brief Core chess type definitions: squares, pieces, colours, move encoding, and ValueList. + +/// @def UNREACHABLE() +/// @brief Marks code paths that should never be reached. #if defined(_MSC_VER) #define UNREACHABLE() __assume(false) #elif defined(__clang__) || defined(__GNUC__) @@ -36,6 +41,9 @@ #else #define UNREACHABLE() assert(0) #endif + +/// @def ASSUME(expr) +/// @brief Compiler hint that expr is always true. #if defined(_MSC_VER) #define ASSUME(expr) __assume(expr) #elif defined(__clang__) @@ -60,16 +68,16 @@ } while (0) #endif +/// @brief Detect whether the current evaluation context is constant-evaluated. +/// @return true if called during compile-time evaluation. constexpr bool is_constant_evaluated() { #if __cpp_if_consteval >= 202106L if consteval { return true; } - // both MSVC (non-comformant __cplusplus) and by-default _MSVC_LANG and other compiles with - // conformant __cplusplus #elif __cplusplus >= 202002L || _MSVC_LANG >= 202002L return std::is_constant_evaluated(); -#elif defined(__GNUC__) // defined for both GCC and clang +#elif defined(__GNUC__) return __builtin_is_constant_evaluated(); #elif _MSC_VER >= 1925 return __builtin_is_constant_evaluated(); @@ -78,10 +86,21 @@ constexpr bool is_constant_evaluated() { #endif return false; } + namespace chess { + +/// @typedef Bitboard +/// @brief 64-bit bitboard representing a set of squares. using Bitboard = uint64_t; + +/// @typedef Key +/// @brief 64-bit Zobrist hash key. using Key = uint64_t; + // clang-format off +/// @enum Square +/// @brief Board square identifiers in little-endian rank-file mapping. +/// @details SQ_A1 (0) through SQ_H8 (63). SQ_NONE is an invalid sentinel. enum Square : int8_t { SQ_A1, SQ_B1, SQ_C1, SQ_D1, SQ_E1, SQ_F1, SQ_G1, SQ_H1, SQ_A2, SQ_B2, SQ_C2, SQ_D2, SQ_E2, SQ_F2, SQ_G2, SQ_H2, @@ -96,34 +115,76 @@ enum Square : int8_t { SQUARE_ZERO = 0, SQUARE_NB = 64 }; + +/// @enum File +/// @brief File (column) on the board: 0 = a-file, 7 = h-file. enum File : int8_t { FILE_A, FILE_B, FILE_C, FILE_D, FILE_E, FILE_F, FILE_G, FILE_H, FILE_NB }; +/// @enum Rank +/// @brief Rank (row) on the board: 0 = rank 1, 7 = rank 8. enum Rank : int8_t { RANK_1, RANK_2, RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8, RANK_NB }; -constexpr Square square_mirror(const Square sq){return static_cast(static_cast(sq)^56);} -constexpr Square flip_sq(const Square sq){return square_mirror(sq);} + +/// @brief Mirror a square vertically (rank 1 <-> rank 8). +/// @param sq Input square. +/// @return Flipped square. +constexpr Square square_mirror(const Square sq) { return static_cast(static_cast(sq) ^ 56); } +/// @brief Alias for square_mirror. +constexpr Square flip_sq(const Square sq) { return square_mirror(sq); } + +/// @brief Check if a rank/file pair describes a valid board square. constexpr bool is_valid(const Rank r, const File f) { return 0 <= r && r <= 7 && 0 <= f && f <= 7; } +/// @brief Check if a square index is valid (0..63). constexpr bool is_valid(const Square s) { return 0 <= s && s < 64; } + +/// @brief Extract the file from a square. constexpr File file_of(const Square s) { assert(0 <= s && s < 64); return static_cast(s & 7); } +/// @brief Extract the rank from a square. constexpr Rank rank_of(const Square s) { assert(0 <= s && s < 64); return static_cast(s >> 3); } + +/// @brief Construct a square from rank and file. constexpr Square make_sq(const Rank r, const File f) { assert(0 <= r && r <= 7 && 0 <= f && f <= 7); return static_cast(static_cast(r * 8 + f)); } + +/// @brief Construct a square from file and rank (order-reversed overload). constexpr Square make_sq(const File f, const Rank r) { assert(0 <= r && r <= 7 && 0 <= f && f <= 7); return static_cast(static_cast(r * 8 + f)); } + +/// @enum Color +/// @brief Side or piece colour. enum Color : uint8_t { WHITE = 0, BLACK = 1, COLOR_NB = 2 }; -// Toggle color + +/// @brief Toggle colour. constexpr Color operator~(const Color c) { return static_cast(c ^ BLACK); } + +constexpr Bitboard BB_LIGHT_SQUARES = + 0x55AA55AA55AA55AAULL; + +constexpr Bitboard BB_DARK_SQUARES = + ~BB_LIGHT_SQUARES; + +/// @brief Color of the square, given color of A1 is black +constexpr Color square_color(Square s) { + return (BB_LIGHT_SQUARES >> s) & 1 ? WHITE : BLACK; +} + +/// @enum PieceType +/// @brief Piece type without colour information. +/// @note ALL_PIECES aliases NO_PIECE_TYPE (0). enum PieceType : std::int8_t { NO_PIECE_TYPE = 0, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING, ALL_PIECES = 0, PIECE_TYPE_NB = 8 }; + +/// @enum Direction +/// @brief Compass direction offsets for bitboard shifting. enum Direction : int8_t { NORTH = 8, EAST = 1, @@ -134,33 +195,46 @@ enum Direction : int8_t { SOUTH_EAST = SOUTH + EAST, SOUTH_WEST = SOUTH + WEST, NORTH_WEST = NORTH + WEST, - DOUBLE_NORTH = 2*NORTH, - DOUBLE_EAST = 2*EAST, - DOUBLE_SOUTH = 2*SOUTH, - DOUBLE_WEST = 2*WEST, - DOUBLE_NORTH_EAST = 2*NORTH_EAST, - DOUBLE_SOUTH_EAST = 2*SOUTH_EAST, - DOUBLE_SOUTH_WEST = 2*SOUTH_WEST, - DOUBLE_NORTH_WEST = 2*NORTH_WEST, - + + DOUBLE_NORTH = 2 * NORTH, + DOUBLE_EAST = 2 * EAST, + DOUBLE_SOUTH = 2 * SOUTH, + DOUBLE_WEST = 2 * WEST, + DOUBLE_NORTH_EAST = 2 * NORTH_EAST, + DOUBLE_SOUTH_EAST = 2 * SOUTH_EAST, + DOUBLE_SOUTH_WEST = 2 * SOUTH_WEST, + DOUBLE_NORTH_WEST = 2 * NORTH_WEST, + DIR_NONE = 0 }; // clang-format on + +/// @brief Convert a square to the opponent's perspective (vertical mirror). inline constexpr Square relative_square(const Color c, const Square s) { return static_cast(s ^ (c * 56)); } + +/// @brief Get the rook square for a castling move. inline constexpr Square castling_rook_square(const Color c, const bool is_king_side) { return relative_square(c, is_king_side ? SQ_F1 : Square::SQ_D1); } + +/// @brief Get the king square for a castling move. inline constexpr Square castling_king_square(const Color c, const bool is_king_side) { return relative_square(c, is_king_side ? SQ_G1 : Square::SQ_C1); } +/// @brief Convert a rank to the opponent's perspective. inline constexpr Rank relative_rank(Color c, Rank r) { return Rank(r ^ (c * 7)); } +/// @brief Get the relative rank of a square for the given colour. inline constexpr Rank relative_rank(Color c, Square s) { return relative_rank(c, rank_of(s)); } +/// @brief Flip direction for the opponent. inline constexpr Direction relative_direction(Color c, Direction d) { return static_cast(c == WHITE ? d : -d); } + +/// @brief Get the pawn-push direction for a colour. inline constexpr Direction pawn_push(Color c) { return c == WHITE ? NORTH : SOUTH; } +/// @brief Generate increment/decrement operators for an enum type. #define ENABLE_INCR_OPERATORS_ON(T) \ constexpr T &operator++(T &d) { return d = T(int(d) + 1); } \ constexpr T &operator--(T &d) { return d = T(int(d) - 1); } \ @@ -184,7 +258,8 @@ ENABLE_INCR_OPERATORS_ON(Rank) constexpr Direction operator+(Direction d1, Direction d2) { return Direction(int(d1) + int(d2)); } constexpr Direction operator*(int i, Direction d) { return Direction(i * int(d)); } -// Additional operators to add a Direction to a Square +/// @name Square arithmetic operators +/// @{ constexpr Square operator+(Square s, Direction d) { return Square(int(s) + int(d)); } constexpr Square operator-(Square s, Direction d) { return Square(int(s) - int(d)); } constexpr Square &operator+=(Square &s, Direction d) { return s = s + d; } @@ -209,7 +284,10 @@ constexpr File operator-(File s, Direction d) { } constexpr File &operator+=(File &s, Direction d) { return s = s + d; } constexpr File &operator-=(File &s, Direction d) { return s = s - d; } +/// @} +/// @enum PolyglotPiece +/// @brief Piece encoding compatible with Polyglot opening books. enum class PolyglotPiece : uint8_t { WPAWN = 1, WKNIGHT = 3, @@ -226,6 +304,9 @@ enum class PolyglotPiece : uint8_t { NO_PIECE = 12, PIECE_NB = 12 }; + +/// @enum EnginePiece +/// @brief Default engine piece encoding with 8 values per colour. enum class EnginePiece : uint8_t { NO_PIECE, WPAWN = PAWN + 0, @@ -242,6 +323,9 @@ enum class EnginePiece : uint8_t { BKING, PIECE_NB = 16 }; + +/// @enum ContiguousMappingPiece +/// @brief Compact piece encoding (0-5 = white, 6-11 = black). enum class ContiguousMappingPiece : uint8_t { WPAWN = 0, WKNIGHT = 1, @@ -258,6 +342,10 @@ enum class ContiguousMappingPiece : uint8_t { NO_PIECE = 12, PIECE_NB = 12 }; + +/// @brief Get the Zobrist array index for a piece-enum type. +/// @tparam T One of PolyglotPiece, EnginePiece, ContiguousMappingPiece. +/// @return 0, 1, or 2. template constexpr size_t enum_idx() { if constexpr (std::is_same_v) return 0; @@ -267,7 +355,9 @@ template constexpr size_t enum_idx() { return 2; return -1; } -// clang-format on + +/// @name Piece-to-PieceType conversion +/// @{ constexpr PieceType piece_of(PolyglotPiece p) { return p == decltype(p)::NO_PIECE ? NO_PIECE_TYPE : static_cast(static_cast(p) / 2 + 1); } @@ -277,11 +367,19 @@ constexpr PieceType piece_of(EnginePiece p) { constexpr PieceType piece_of(ContiguousMappingPiece p) { return p == decltype(p)::NO_PIECE ? NO_PIECE_TYPE : static_cast(static_cast(p) % 6 + 1); } +/// @} + +/// @name Colour extraction +/// @{ constexpr Color color_of(PolyglotPiece pt) { return static_cast((static_cast(pt) + 1) % 2); } constexpr Color color_of(EnginePiece pt) { return static_cast(static_cast(pt) / static_cast(EnginePiece::BPAWN)); } constexpr Color color_of(ContiguousMappingPiece pt) { return static_cast(static_cast(pt) / 6); } +/// @} + +/// @name Piece construction from (type, colour) +/// @{ template , bool> = 0> constexpr EnginePiece make_piece(PieceType pt, Color c) { return static_cast((c << 3) + pt); @@ -294,10 +392,15 @@ template constexpr ContiguousMappingPiece make_piece(PieceType pt, Color c) { return static_cast((static_cast(pt) - 1) + 6 * static_cast(c)); } +/// @} + +/// @brief Extract PieceType from any piece enum (generic). template ::value>> constexpr PieceType type_of(T p) { return piece_of(p); } +/// @enum CastlingRights +/// @brief Bitmask of available castling rights. enum CastlingRights : int8_t { NO_CASTLING, WHITE_OO, @@ -313,47 +416,54 @@ enum CastlingRights : int8_t { CASTLING_RIGHT_NB = 16 }; + +/// @brief Intersect CastlingRights with a colour's rights. constexpr CastlingRights operator&(Color c, CastlingRights cr) { return CastlingRights((c == WHITE ? WHITE_CASTLING : BLACK_CASTLING) & cr); } +/// @brief Bitwise OR-assign for CastlingRights. inline CastlingRights &operator|=(CastlingRights &a, CastlingRights b) { - return a = static_cast(static_cast(a) | - static_cast(b)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) + return a = static_cast(static_cast(a) | static_cast(b)); } + +/// @brief Bitwise OR for CastlingRights. inline CastlingRights operator|(CastlingRights a, CastlingRights b) { - return static_cast(static_cast(a) | - static_cast(b)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) + return static_cast(static_cast(a) | static_cast(b)); } +/// @brief Bitwise AND-assign for CastlingRights. inline CastlingRights &operator&=(CastlingRights &a, CastlingRights b) { a = static_cast(static_cast(a) & static_cast(b)); return a; } + +/// @brief Bitwise NOT for CastlingRights. inline CastlingRights operator~(CastlingRights a) { - return static_cast(static_cast(a) ^ - ANY_CASTLING); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) + return static_cast(static_cast(a) ^ ANY_CASTLING); } +/// @enum MoveType +/// @brief Move-type flags stored in the high 2 bits of a Move. enum MoveType : uint16_t { NORMAL, PROMOTION = 1 << 14, EN_PASSANT = 2 << 14, CASTLING = 3 << 14 }; -// A move needs 16 bits to be stored -// -// bit 0- 5: destination square (from 0 to 63) -// bit 6-11: origin square (from 0 to 63) -// bit 12-13: promotion piece type - 2 (from KNIGHT-2 to QUEEN-2) -// bit 14-15: special move flag: promotion (1), en passant (2), castling (3) -// NOTE: en passant bit is set only when a pawn can be captured -// -// Special cases are Move::none() and Move::null(). We can sneak these in because -// in any normal move the destination square and origin square are always different, -// but Move::none() and Move::null() have the same origin and destination square. + +/// @class Move +/// @brief Compact 16-bit move representation. +/// +/// Bit layout: +/// - bit 0-5: destination square (0-63) +/// - bit 6-11: origin square (0-63) +/// - bit 12-13: promotion piece type - KNIGHT (0-3) +/// - bit 14-15: move type flag (normal/promotion/en-passant/castling) +/// +/// Special values: Move::none() (0) and Move::null() (65). class Move { public: Move() = default; constexpr Move(std::uint16_t d) : data(d) {} - 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. template static constexpr Move make(Square from, Square to, PieceType pt = PieceType::KNIGHT) { return Move(static_cast(T) | static_cast((static_cast(pt) - static_cast(PieceType::KNIGHT)) << 12) | (static_cast(from) << 6) | static_cast(to)); @@ -363,119 +473,163 @@ class Move { assert(is_ok()); return Square((data >> 6) & 0x3F); } - constexpr Square to_sq() const { assert(is_ok()); return Square(data & 0x3F); } - constexpr Square from() const { return from_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. constexpr MoveType type_of() const { return MoveType(data & (3 << 14)); } - constexpr MoveType typeOf() const { return type_of(); } - - constexpr PieceType promotion_type() const { - return static_cast(((data >> 12) & 3) + static_cast(PieceType::KNIGHT)); - } - constexpr PieceType promotionType() const { return promotion_type(); } 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); } + static constexpr Move null() { return Move(65); } static constexpr Move none() { return Move(0); } constexpr bool operator==(const Move &m) const { return data == m.data; } constexpr bool operator!=(const Move &m) const { return data != m.data; } - 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. struct MoveHash { std::size_t operator()(const Move &m) const { return m.data; } }; + 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 MoveType NORMAL = MoveType::NORMAL; static constexpr MoveType PROMOTION = MoveType::PROMOTION; static constexpr MoveType ENPASSANT = MoveType::EN_PASSANT; static constexpr MoveType CASTLING = MoveType::CASTLING; + /// @} protected: 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 { 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_]; // always safe due to mask + 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) { - assert(index < MaxSize); // relax the conditions, it's BRANCHLESS so forgive + // intentionally placed + assert(0 <= index && index < MaxSize); return values_[index]; } inline const T *begin() const { return values_; } inline T *data() { return values_; } inline const T *end() const { return values_ + size_; } + size_type size_ = 0; private: - T values_[MaxSize]{}; + T values_[MaxSize]; +}; + +/// @typedef Movelist +/// @brief Fixed-capacity list of up to 300 moves. +using Movelist = ValueList; + +/// @brief Counting-only move list — same interface as Movelist but discards move data. +class CountOnlyList { + public: + using size_type = std::size_t; + CountOnlyList() = default; + inline size_type size() const { return size_; } + inline void push_back(const Move &) { ++size_; } + inline Move &operator[](size_type) { + thread_local static Move dummy(0); + return dummy; + } + inline Move *data() { return nullptr; } + inline const Move *begin() const { return nullptr; } + inline const Move *end() const { return nullptr; } + size_type size_ = 0; }; -using Movelist = ValueList; +/// @brief Chebyshev distance between two squares. +/// @return max(|dfile|, |drank|) constexpr int square_distance(Square a, Square b) { return std::max(std::abs(file_of(a) - file_of(b)), std::abs(rank_of(a) - rank_of(b))); } + +/// @brief Parse a square from algebraic notation. +/// @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) return SQ_NONE; - char f = sv[0]; char r = sv[1]; - if (f < 'a' || f > 'h' || r < '1' || r > '8') return SQ_NONE; - return make_sq(File(f - 'a'), Rank(r - '1')); } +/// @brief Parse a PieceType from a character. +/// @param c Character: p, n, b, r, q, k (case-insensitive). +/// @return PieceType, or NO_PIECE_TYPE on failure. constexpr PieceType parse_pt(unsigned char c) { const char a[] = "pnbrqk"; int p = -1; - (c >= 'A' && c <= 'Z') ? (c += 32) : (c); // tolower + if (c >= 'A' && c <= 'Z') + c += 32; for (size_t i = 0; i < sizeof(a); i++) { if (c == a[i]) p = i; diff --git a/zobrist.cpp b/zobrist.cpp index f1f6811..026dde5 100644 --- a/zobrist.cpp +++ b/zobrist.cpp @@ -16,10 +16,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ + +/// @file zobrist.cpp +/// @brief Zobrist-hashing random constants pre-generated from a PRNG. + #include "zobrist.h" #include "types.h" namespace chess::zobrist { // clang-format off + +/// @brief All-zero placeholder array for unused EnginePiece indices. std::array Empty = { 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, @@ -38,6 +44,7 @@ std::array Empty = { 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000 }; +/// @brief Random piece-square values indexed by PolyglotPiece (0-11). std::array _RandomPiece[12] = { { 0x9D39247E33776D41, 0x2AF7398005AAA5C7, 0x44DB015024623547, 0x9C15F73E62A76AE2, diff --git a/zobrist.h b/zobrist.h index 3a43a5f..aafbaa7 100644 --- a/zobrist.h +++ b/zobrist.h @@ -19,9 +19,22 @@ #pragma once #include #include + +/// @file zobrist.h +/// @brief Zobrist hashing random constants. + namespace chess::zobrist { + +/// @brief Random values for piece-square XOR. +/// @details Indexed as RandomPiece[piece_enum_idx][piece_value][square]. extern std::array *RandomPiece[]; + +/// @brief Random values for castling rights (indexed by CastlingRights bitmask, 0-15). extern uint64_t RandomCastle[16]; + +/// @brief Random values for en-passant file (0-7 = files A-H, 8 = no EP). extern uint64_t RandomEP[9]; + +/// @brief Random value XORed when it is black's turn to move. extern uint64_t RandomTurn; -} // namespace chess::zobrist \ No newline at end of file +} // namespace chess::zobrist