diff --git a/CMakeLists.txt b/CMakeLists.txt index b5e78156..b2df89ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,16 @@ add_library(lps INTERFACE) target_include_directories(lps INTERFACE vendor/lps/include) add_dependencies(lps checkout_lps) +# Checkout Pyrrhic +add_library(pyrrhic OBJECT + vendor/Pyrrhic/stdendian.h + vendor/Pyrrhic/tbconfig.h + vendor/Pyrrhic/tbprobe.cpp + vendor/Pyrrhic/tbprobe.h) +target_include_directories(pyrrhic PUBLIC vendor/Pyrrhic) +# Pyrrhic depends on bb_attacks.hpp, which depends on util/types.h, which depends on lps +target_link_libraries(pyrrhic PRIVATE lps) + # Flags function(target_add_flags target) @@ -78,7 +88,7 @@ function(target_add_flags target) target_include_directories(${target} PUBLIC src) # Libraries - target_link_libraries(${target} PUBLIC git_hash lps) + target_link_libraries(${target} PUBLIC git_hash lps pyrrhic) # LTO message(STATUS "LTO is set to: ${lto}") @@ -89,6 +99,8 @@ endfunction() # Sorted list of source files set(srcs + src/bb_attacks.cpp + src/bb_attacks.hpp src/bench.cpp src/bench.hpp src/board.cpp @@ -119,11 +131,15 @@ set(srcs src/psqt_state.hpp src/repetition_info.cpp src/repetition_info.hpp + src/root_move.cpp + src/root_move.hpp src/search.cpp src/search.hpp src/speedtest.cpp src/speedtest.hpp src/square.hpp + src/tb.cpp + src/tb.hpp src/tm.cpp src/tm.hpp src/tt.cpp diff --git a/src/bb_attacks.cpp b/src/bb_attacks.cpp new file mode 100644 index 00000000..7a36e4d8 --- /dev/null +++ b/src/bb_attacks.cpp @@ -0,0 +1,247 @@ +#include "bb_attacks.hpp" +#include +#include + +namespace Clockwork { + +namespace { + +constexpr std::array, 2> PAWN_ATTACKS = []() { + std::array, 2> result{}; + + for (const auto color : {Color::White, Color::Black}) { + for (u8 square_idx = 0; square_idx < 64; square_idx++) { + const auto square = Square{square_idx}; + const auto bit = Bitboard::from_square(square); + + auto& bb = result[static_cast(color)][square_idx]; + + bb |= bit.shift_relative(color, Direction::NorthWest); + bb |= bit.shift_relative(color, Direction::NorthEast); + } + } + + return result; +}(); + +constexpr std::array KNIGHT_ATTACKS = []() { + std::array result{}; + + for (u8 square_idx = 0; square_idx < 64; square_idx++) { + const auto square = Square{square_idx}; + const auto bit = Bitboard::from_square(square); + + auto& bb = result[square_idx]; + + bb |= bit.shift(Direction::North).shift(Direction::NorthWest); + bb |= bit.shift(Direction::North).shift(Direction::NorthEast); + bb |= bit.shift(Direction::South).shift(Direction::SouthWest); + bb |= bit.shift(Direction::South).shift(Direction::SouthEast); + bb |= bit.shift(Direction::West).shift(Direction::NorthWest); + bb |= bit.shift(Direction::West).shift(Direction::SouthWest); + bb |= bit.shift(Direction::East).shift(Direction::NorthEast); + bb |= bit.shift(Direction::East).shift(Direction::SouthEast); + } + + return result; +}(); + +constexpr std::array KING_ATTACKS = []() { + std::array result{}; + + for (u8 square_idx = 0; square_idx < 64; square_idx++) { + const auto square = Square{square_idx}; + const auto bit = Bitboard::from_square(square); + + auto& bb = result[square_idx]; + + bb |= bit.shift(Direction::North); + bb |= bit.shift(Direction::South); + bb |= bit.shift(Direction::West); + bb |= bit.shift(Direction::East); + bb |= bit.shift(Direction::NorthWest); + bb |= bit.shift(Direction::NorthEast); + bb |= bit.shift(Direction::SouthWest); + bb |= bit.shift(Direction::SouthEast); + } + + return result; +}(); + +constexpr Bitboard DIAG = Bitboard{0x8040201008040201}; + +constexpr Bitboard FILE_A = Bitboard{0x0101010101010101}; +constexpr Bitboard FILE_H = Bitboard{0x8080808080808080}; + +struct BishopMasks { + u64 bit; + u64 diag; + u64 anti_diag; + u64 flipped; +}; + +constexpr std::array BISHOP_MASKS = []() { + std::array result{}; + + constexpr std::array DIAGS = []() { + std::array result{}; + + for (usize i = 0; i < 15; i++) { + if (i > 7) { + result[i] = DIAG.value() >> (8 * (i - 7)); + } else { + result[i] = DIAG.value() << (8 * (7 - i)); + } + } + + return result; + }(); + + for (u8 square_idx = 0; square_idx < 64; square_idx++) { + const auto square = Square{square_idx}; + const auto bit = Bitboard::from_square(square).value(); + + const auto file = static_cast(square.file()); + const auto rank = static_cast(square.rank()); + + auto& masks = result[square_idx]; + + masks.bit = bit; + masks.diag = bit ^ DIAGS[7 + file - rank]; + masks.anti_diag = bit ^ __builtin_bswap64(DIAGS[file + rank]); + masks.flipped = __builtin_bswap64(bit); + } + + return result; +}(); + +constexpr std::array RANK_SHIFTS = []() { + std::array result{}; + + for (usize square_idx = 0; square_idx < 64; square_idx++) { + result[square_idx] = (square_idx & 0b111000) + 1; + } + + return result; +}(); + +constexpr std::array, 64> RANK_ATTACKS = []() { + std::array, 64> result{}; + + constexpr std::array WEST = []() { + std::array result{}; + + for (u8 square_idx = 0; square_idx < 64; square_idx++) { + const auto square = Square{square_idx}; + const auto bit = Bitboard::from_square(square).value(); + + result[square_idx] = Bitboard{(bit - 1) & (u64{0xFF} << (square_idx & 0b111000))}; + } + + return result; + }(); + + constexpr std::array EAST = [&WEST]() { + std::array result{}; + + for (u8 square_idx = 0; square_idx < 64; square_idx++) { + const auto square = Square{square_idx}; + const auto bit = Bitboard::from_square(square).value(); + + result[square_idx] = + Bitboard{bit ^ WEST[square_idx].value() ^ (u64{0xFF} << (square_idx & 0b111000))}; + } + + return result; + }(); + + for (u8 square_idx = 0; square_idx < 64; square_idx++) { + const auto square = Square{square_idx}; + const auto file = static_cast(square.file()); + + for (u64 i = 0; i < 64; i++) { + const auto occ = i << 1; + + auto& bb = result[square_idx][i]; + + const auto east = EAST[file] + ^ EAST[static_cast( + std::countr_zero((EAST[file].value() & occ) | (u64{1} << 63)))]; + const auto west = + WEST[file] + ^ WEST[static_cast(std::countl_zero((WEST[file].value() & occ) | 1) ^ 63)]; + + bb = (east | west) << static_cast(square_idx - file); + } + } + + return result; +}(); + +constexpr std::array, 64> FILE_ATTACKS = []() { + std::array, 64> result{}; + + for (u8 square_idx = 0; square_idx < 64; square_idx++) { + const auto square = Square{square_idx}; + const auto rank = static_cast(square.rank()); + + for (u64 occ = 0; occ < 64; occ++) { + const auto rank_attacks = RANK_ATTACKS[7 - rank][occ].value(); + result[square_idx][occ] = + Bitboard{((rank_attacks * DIAG.value()) & FILE_H.value()) >> (7 - square.file())}; + } + } + + return result; +}(); + +} + +Bitboard pawn_attacks(Square square, Color color) { + return PAWN_ATTACKS[static_cast(color)][square.raw]; +} + +Bitboard knight_attacks(Square square) { + return KNIGHT_ATTACKS[square.raw]; +} + +Bitboard bishop_attacks(Square square, Bitboard occupancy) { + const auto& masks = BISHOP_MASKS[square.raw]; + + auto diag_attacks = occupancy.value() & masks.diag; + auto flipped_diag = __builtin_bswap64(diag_attacks); + diag_attacks -= masks.bit; + flipped_diag -= masks.flipped; + diag_attacks ^= __builtin_bswap64(flipped_diag); + diag_attacks &= masks.diag; + + auto anti_diag_attacks = occupancy.value() & masks.anti_diag; + auto flipped_anti_diag = __builtin_bswap64(anti_diag_attacks); + anti_diag_attacks -= masks.bit; + flipped_anti_diag -= masks.flipped; + anti_diag_attacks ^= __builtin_bswap64(flipped_anti_diag); + anti_diag_attacks &= masks.anti_diag; + + return Bitboard{diag_attacks | anti_diag_attacks}; +} + +Bitboard rook_attacks(Square square, Bitboard occupancy) { + const auto flip = ((occupancy.value() >> square.file()) & FILE_A.value()) * DIAG.value(); + const auto file_sq = (flip >> 57) & 0x3F; + const auto file_attacks = FILE_ATTACKS[square.raw][file_sq]; + + const auto rank_sq = (occupancy.value() >> RANK_SHIFTS[square.raw]) & 0x3F; + const auto rank_attacks = RANK_ATTACKS[square.raw][rank_sq]; + + return file_attacks | rank_attacks; +} + +Bitboard queen_attacks(Square square, Bitboard occupancy) { + return bishop_attacks(square, occupancy) | rook_attacks(square, occupancy); +} + +Bitboard king_attacks(Square square) { + return KING_ATTACKS[square.raw]; +} + +} diff --git a/src/bb_attacks.hpp b/src/bb_attacks.hpp new file mode 100644 index 00000000..e8223f9b --- /dev/null +++ b/src/bb_attacks.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "bitboard.hpp" +#include "common.hpp" +#include "square.hpp" +#include "util/types.hpp" + +namespace Clockwork { + +[[nodiscard]] Bitboard pawn_attacks(Square square, Color color); +[[nodiscard]] Bitboard knight_attacks(Square square); +[[nodiscard]] Bitboard bishop_attacks(Square square, Bitboard occupancy); +[[nodiscard]] Bitboard rook_attacks(Square square, Bitboard occupancy); +[[nodiscard]] Bitboard queen_attacks(Square square, Bitboard occupancy); +[[nodiscard]] Bitboard king_attacks(Square square); + +} diff --git a/src/bitboard.hpp b/src/bitboard.hpp index 0fcade53..a9dd96da 100644 --- a/src/bitboard.hpp +++ b/src/bitboard.hpp @@ -52,43 +52,43 @@ struct Bitboard { return file_mask(2) | file_mask(3) | file_mask(4) | file_mask(5); } - [[nodiscard]] static Bitboard fill_verticals(const Bitboard mask) { + [[nodiscard]] static constexpr Bitboard fill_verticals(const Bitboard mask) { Bitboard result = mask | (mask >> 8); result |= result >> 16; result |= result >> 32; return (result & Bitboard::rank_mask(0)) * Bitboard::file_mask(0); } - [[nodiscard]] bool empty() const { + [[nodiscard]] constexpr bool empty() const { return m_raw == 0; } - [[nodiscard]] usize popcount() const { + [[nodiscard]] constexpr usize popcount() const { return static_cast(std::popcount(m_raw)); } - [[nodiscard]] i32 ipopcount() const { + [[nodiscard]] constexpr i32 ipopcount() const { return static_cast(std::popcount(m_raw)); } - [[nodiscard]] Square msb() const { + [[nodiscard]] constexpr Square msb() const { return Square{static_cast(std::bit_width(m_raw) - 1)}; } - [[nodiscard]] Square lsb() const { + [[nodiscard]] constexpr Square lsb() const { return Square{static_cast(std::countr_zero(m_raw))}; } - [[nodiscard]] bool any() const { + [[nodiscard]] constexpr bool any() const { return static_cast(m_raw); } // Rank closest to player - [[nodiscard]] u8 front_rank(Color color) const { + [[nodiscard]] constexpr u8 front_rank(Color color) const { i32 color_shift = color == Color::White ? 0 : 56; return static_cast(m_raw >> color_shift); } - [[nodiscard]] Bitboard shift(Direction dir) const { + [[nodiscard]] constexpr Bitboard shift(Direction dir) const { constexpr u64 FILE_A = file_mask(0).m_raw; constexpr u64 FILE_H = file_mask(7).m_raw; switch (dir) { @@ -111,23 +111,24 @@ struct Bitboard { } } - [[nodiscard]] Square frontmost_square(Color color) const { + [[nodiscard]] constexpr Square frontmost_square(Color color) const { return color == Color::White ? msb() : lsb(); } - [[nodiscard]] static Bitboard forward_ranks(Color c, Square sq) { + [[nodiscard]] static constexpr Bitboard forward_ranks(Color c, Square sq) { return c == Color::White ? ~rank_mask(0) << (8 * sq.relative_rank(c)) : ~rank_mask(7) >> (8 * sq.relative_rank(c)); } - [[nodiscard]] Bitboard shift_relative(Color perspective, Direction dir) const { + [[nodiscard]] constexpr Bitboard shift_relative(Color perspective, Direction dir) const { if (perspective == Color::Black) { dir = static_cast((static_cast(dir) + 4) % 8); } return shift(dir); } - [[nodiscard]] Bitboard shift_relative(Color perspective, Direction dir, const i32 times) const { + [[nodiscard]] constexpr Bitboard + shift_relative(Color perspective, Direction dir, const i32 times) const { if (perspective == Color::Black) { dir = static_cast((static_cast(dir) + 4) % 8); } @@ -138,23 +139,23 @@ struct Bitboard { return result; } - [[nodiscard]] u64 value() const { + [[nodiscard]] constexpr u64 value() const { return m_raw; } - [[nodiscard]] bool is_set(Square sq) const { + [[nodiscard]] constexpr bool is_set(Square sq) const { return (m_raw >> sq.raw) & 1; } - void clear(Square sq) { + constexpr void clear(Square sq) { m_raw &= ~from_square(sq).m_raw; } - void set(Square sq) { + constexpr void set(Square sq) { m_raw |= from_square(sq).m_raw; } - void set(Square sq, bool value) { + constexpr void set(Square sq, bool value) { if (value) { set(sq); } else { diff --git a/src/board.hpp b/src/board.hpp index 80a41c25..47412943 100644 --- a/src/board.hpp +++ b/src/board.hpp @@ -211,6 +211,10 @@ struct Byteboard { & get_occupied_bitboard(); } + [[nodiscard]] usize get_piece_count() const { + return to_vector().nonzeros().popcount(); + } + [[nodiscard]] Bitboard bitboard_for(Color color, PieceType ptype) const { Place p{color, ptype, PieceId{0}}; return Bitboard{(to_vector() & u8x64::splat(0xF0)).eq(u8x64::splat(p.raw)).to_bits()}; diff --git a/src/common.hpp b/src/common.hpp index 1df18f7e..0641746f 100644 --- a/src/common.hpp +++ b/src/common.hpp @@ -8,19 +8,48 @@ namespace Clockwork { inline std::atomic g_frc = false; -constexpr i32 MAX_PLY = 256; -constexpr Value VALUE_INF = 32501; -constexpr Value VALUE_MATED = 32500; -constexpr Value VALUE_WIN = 32000; +constexpr i32 MAX_PLY = 256; +constexpr Value VALUE_INF = 32501; +constexpr Value VALUE_MATED = 32500; +constexpr Value VALUE_TB_WIN = 31500; +constexpr Value VALUE_WIN = 31000; -constexpr bool is_mate_score(Value value) { +constexpr bool is_decisive_score(Value value) { return std::abs(value) >= VALUE_WIN; } -constexpr bool is_being_mated_score(Value value) { +constexpr bool is_win_score(Value value) { + return value >= VALUE_WIN; +} + +constexpr bool is_loss_score(Value value) { return value <= -VALUE_WIN; } +constexpr bool is_mate_score(Value value) { + return std::abs(value) > VALUE_TB_WIN; +} + +constexpr bool is_mating_score(Value value) { + return value > VALUE_TB_WIN && value <= VALUE_MATED; +} + +constexpr bool is_being_mated_score(Value value) { + return value < -VALUE_TB_WIN && value >= -VALUE_MATED; +} + +constexpr bool is_tb_score(Value value) { + return is_decisive_score(value) && !is_mate_score(value); +} + +constexpr bool is_tb_win_score(Value value) { + return value >= VALUE_WIN && value <= VALUE_TB_WIN; +} + +constexpr bool is_tb_loss_score(Value value) { + return value <= -VALUE_WIN && value >= -VALUE_TB_WIN; +} + constexpr bool is_valid_score(Value value) { return value != -VALUE_INF; } diff --git a/src/move.hpp b/src/move.hpp index da914dd1..df119ac4 100644 --- a/src/move.hpp +++ b/src/move.hpp @@ -29,6 +29,15 @@ enum class MoveFlags : u16 { PromoQueenCapture = (0b1100 | (static_cast(PieceType::Queen) - 2)) << 12, }; +constexpr MoveFlags operator|(MoveFlags a, MoveFlags b) { + return static_cast(static_cast(a) | static_cast(b)); +} + +constexpr MoveFlags& operator|=(MoveFlags& a, MoveFlags b) { + a = a | b; + return a; +} + struct Move { u16 raw = 0; constexpr Move() = default; diff --git a/src/repetition_info.cpp b/src/repetition_info.cpp index 601f9f03..fc7daf89 100644 --- a/src/repetition_info.cpp +++ b/src/repetition_info.cpp @@ -4,6 +4,7 @@ #include "position.hpp" #include "rays.hpp" #include "util/types.hpp" +#include namespace Clockwork { @@ -114,4 +115,30 @@ bool RepetitionInfo::has_game_cycle(const Position& pos, usize ply) { return false; } +bool RepetitionInfo::has_repeated() { + const auto [last_key, last_reversible] = m_repetition_table.back(); + if (!last_reversible) { + return false; + } + + std::unordered_set key_set{}; + key_set.insert(last_key); + + for (usize idx = 1; idx < m_repetition_table.size(); idx++) { + const auto [key, is_reversible] = m_repetition_table[m_repetition_table.size() - idx - 1]; + + if (key_set.contains(key)) { + return true; + } + + if (!is_reversible) { + return false; + } + + key_set.insert(key); + } + + return false; +} + } // namespace Clockwork diff --git a/src/repetition_info.hpp b/src/repetition_info.hpp index fa822736..a4db20cc 100644 --- a/src/repetition_info.hpp +++ b/src/repetition_info.hpp @@ -19,6 +19,7 @@ class RepetitionInfo { bool detect_repetition(usize root_ply); bool has_game_cycle(const Position& pos, usize ply); + bool has_repeated(); private: std::vector> m_repetition_table; diff --git a/src/root_move.cpp b/src/root_move.cpp new file mode 100644 index 00000000..19e1ff57 --- /dev/null +++ b/src/root_move.cpp @@ -0,0 +1,13 @@ +#include "root_move.hpp" +#include + +namespace Clockwork::Search { + +std::ostream& operator<<(std::ostream& os, const PV& pv) { + for (Move m : pv.m_pv) { + os << m << ' '; + } + return os; +} + +} diff --git a/src/root_move.hpp b/src/root_move.hpp new file mode 100644 index 00000000..e6368d9d --- /dev/null +++ b/src/root_move.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include "move.hpp" +#include "util/static_vector.hpp" +#include "util/types.hpp" + +namespace Clockwork::Search { + +enum class WDL { + None, + Win, + Draw, + Loss, +}; + +struct PV { +public: + void clear() { + m_pv.clear(); + } + + void set(Move move) { + m_pv.clear(); + m_pv.push_back(move); + } + + void set(Move move, const PV& child_pv_line) { + m_pv.clear(); + m_pv.push_back(move); + m_pv.append(child_pv_line.m_pv); + } + + Move first_move() const { + return m_pv.empty() ? Move::none() : m_pv[0]; + } + + friend std::ostream& operator<<(std::ostream& os, const PV& pv); + +private: + StaticVector m_pv; +}; + +struct RootMove { + explicit RootMove(Move move) { + pv.set(move); + } + + Value score = -VALUE_INF; + Value window_score = -VALUE_INF; + Value previous_score = -VALUE_INF; + Value display_score = -VALUE_INF; + + bool upperbound = false; + bool lowerbound = false; + + WDL tb_wdl = WDL::None; + i32 tb_rank = 0; + + Value tb_min_score = -VALUE_INF; + Value tb_max_score = VALUE_INF; + + PV pv; + + Depth searched_depth = 1; + Depth seldepth = 0; + + void set_tb_status(WDL wdl, i32 rank) { + tb_wdl = wdl; + tb_rank = rank; + + switch (wdl) { + case WDL::Win: + tb_min_score = VALUE_TB_WIN; + break; + case WDL::Draw: + tb_min_score = 0; + tb_max_score = 0; + break; + case WDL::Loss: + tb_max_score = -VALUE_TB_WIN; + break; + default: + break; + } + } +}; + +} diff --git a/src/search.cpp b/src/search.cpp index 5ef56ecf..b5d6951f 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -7,6 +7,7 @@ #include "movegen.hpp" #include "movepick.hpp" #include "see.hpp" +#include "tb.hpp" #include "tm.hpp" #include "tuned.hpp" #include "uci.hpp" @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +30,14 @@ static Value mated_in(i32 ply) { return -VALUE_MATED + ply; } +static Value tb_win_in(i32 ply) { + return VALUE_TB_WIN - ply; +} + +static Value tb_loss_in(i32 ply) { + return -VALUE_TB_WIN + ply; +} + static i32 stat_bonus(Depth bonus_depth) { return std::min(tuned::stat_bonus_max, tuned::stat_bonus_quad * bonus_depth * bonus_depth + tuned::stat_bonus_lin * bonus_depth @@ -40,16 +50,8 @@ static i32 stat_malus(Depth malus_depth) { - tuned::stat_malus_sub); } -std::ostream& operator<<(std::ostream& os, const PV& pv) { - for (Move m : pv.m_pv) { - os << m << ' '; - } - return os; -} - -Searcher::Searcher() : - idle_barrier(std::make_unique>(1)), - started_barrier(std::make_unique>(1)) { +Searcher::Searcher() { + initialize(1); } Searcher::~Searcher() { @@ -72,6 +74,8 @@ void Searcher::launch_search(SearchSettings settings_) { settings = settings_; tt.increment_age(); + init_root_moves(m_workers[0]->root_position, m_workers[0]->repetition_info); + for (auto& worker : m_workers) { worker->prepare(); } @@ -106,8 +110,10 @@ void Searcher::initialize(size_t thread_count) { if (m_workers.size() == thread_count) { return; } - { - std::unique_lock lock_guard{mutex}; + + std::unique_lock lock_guard{mutex}; + + if (!m_workers.empty()) { for (auto& worker : m_workers) { worker->exit(); } @@ -146,6 +152,52 @@ u64 Searcher::node_count() { return nodes; } +u64 Searcher::tb_hit_count() { + u64 tb_hits = 0; + tb_hits += tb_root; + for (auto& worker : m_workers) { + tb_hits += worker->tb_hits(); + } + return tb_hits; +} + +void Searcher::init_root_moves(const Position& root_position, RepetitionInfo& repetition_info) { + root_moves.clear(); + root_moves.reserve(256); + + MoveGen movegen{root_position}; + + MoveList noisy{}; + MoveList quiet{}; + + movegen.generate_moves(noisy, quiet); + + const auto insert_root_moves = [&](const MoveList& moves) { + for (const auto move : moves) { + root_moves.emplace_back(move); + } + }; + + insert_root_moves(noisy); + insert_root_moves(quiet); + + multipv = std::min(settings.multipv, root_moves.size()); + probe_wdl = settings.tb_enabled; + + if (!settings.tb_enabled || root_moves.empty() + || root_position.board().get_piece_count() > tb::max_pieces()) { + return; + } + + const auto dtz_succeeded = tb::probe_root(root_position, repetition_info, root_moves); + const auto root_wdl = root_moves[0].tb_wdl; + + tb_root = root_wdl != WDL::None; + + // Avoid probing WDL when a successful DTZ probe says we're winning, to help matefinding. + probe_wdl = !dtz_succeeded || root_wdl != WDL::Win; +} + Worker::Worker(Searcher& searcher, ThreadType thread_type) : m_searcher(searcher), m_thread_type(thread_type) { @@ -196,6 +248,11 @@ void Worker::thread_main() { void Worker::prepare() { m_stopped = false; m_search_nodes = 0; + m_tb_hits = 0; + + m_td.root_moves.clear(); + m_td.root_moves.reserve(256); + std::ranges::copy(m_searcher.root_moves, std::back_inserter(m_td.root_moves)); } void Worker::start_searching() { @@ -238,11 +295,6 @@ Move Worker::iterative_deepening(const Position& root_position) { Value base_search_score = -VALUE_INF; - init_root_moves(root_position); - - m_pv_start = 0; - m_pv_end = m_td.root_moves.size(); - m_node_counts.fill(0); for (Depth search_depth = 1; search_depth < MAX_PLY; search_depth++) { @@ -253,7 +305,28 @@ Move Worker::iterative_deepening(const Position& root_position) { root_move.previous_score = root_move.score; } - for (m_pv_idx = 0; m_pv_idx < m_multipv; ++m_pv_idx) { + m_pv_start = 0; + m_pv_end = 0; + + for (m_pv_idx = 0; m_pv_idx < m_searcher.multipv; ++m_pv_idx) { + if (m_pv_idx == m_pv_end) { + // We've reached the end of this block of root moves (or this is the first PV). + // Find the end of the next block by scanning to the next root move with a lower + // TB rank, or the end of the list. + // When multipv == 1, this has the effect of filtering out all suboptimal root moves + // from being searched. + + m_pv_start = m_pv_idx; + + const auto& first_root_move = m_td.root_moves[m_pv_idx]; + for (m_pv_end = m_pv_idx + 1; m_pv_end < m_td.root_moves.size(); m_pv_end++) { + const auto& curr_root_move = m_td.root_moves[m_pv_end]; + if (curr_root_move.tb_rank < first_root_move.tb_rank) { + break; + } + } + } + m_seldepth = 0; const auto& root_move = m_td.root_moves[m_pv_idx]; @@ -361,7 +434,7 @@ Move Worker::iterative_deepening(const Position& root_position) { // We don't do it for too shallow depths because the node distribution is not stable enough if (IS_MAIN && search_depth >= 6) { f64 complexity = 0; - if (!is_mate_score(score)) { + if (!is_decisive_score(score)) { complexity = 0.6 * abs(base_search_score - score) * std::log(search_depth); } m_search_limits.soft_time_limit = TM::compute_soft_limit( @@ -472,6 +545,56 @@ Value Worker::search( ttpv |= tt_data->ttpv(); } + auto syzygy_min = -VALUE_INF; + auto syzygy_max = VALUE_INF; + + // TB Probing + if (!ROOT_NODE && !excluded && m_searcher.settings.tb_enabled && m_searcher.probe_wdl + && pos.board().get_piece_count() <= tb::max_pieces() && pos.get_50mr_counter() == 0 + && pos.rook_info(Color::White).is_clear() && pos.rook_info(Color::Black).is_clear()) { + const auto wdl = tb::probe_wdl(pos); + if (wdl != WDL::None) { + increment_tb_hits(); + + Value score; + Bound bound; + + switch (wdl) { + case WDL::Win: + score = tb_win_in(ply); + bound = Bound::Lower; + break; + case WDL::Draw: + score = 0; + bound = Bound::Exact; + break; + case WDL::Loss: + score = tb_loss_in(ply); + bound = Bound::Upper; + break; + default: + unreachable(); + } + + if (bound == Bound::Exact || (bound == Bound::Upper && score <= alpha) + || (bound == Bound::Lower && score >= beta)) { + m_searcher.tt.store(pos, ply, -VALUE_INF, Move::none(), score, depth, ttpv, bound); + return score; + } + + if (PV_NODE) { + if (bound == Bound::Upper) { + syzygy_max = score; + } else { // lower + if (score > alpha) { + alpha = score; + } + syzygy_min = score; + } + } + } + } + // Ensure the correct move is searched first if pv_idx > 0. const auto tt_move = ROOT_NODE && m_root_depth > 1 ? m_td.root_moves[m_pv_idx].pv.first_move() : tt_data ? tt_data->move @@ -483,8 +606,8 @@ Value Worker::search( Value raw_eval = -VALUE_INF; ss->static_eval = -VALUE_INF; if (!is_in_check) { - correction = excluded ? 0 : m_td.history.get_correction(pos); - raw_eval = tt_data && !is_mate_score(tt_data->eval) ? tt_data->eval : evaluate(pos); + correction = excluded ? 0 : m_td.history.get_correction(pos); + raw_eval = tt_data && !is_decisive_score(tt_data->eval) ? tt_data->eval : evaluate(pos); ss->static_eval = adj_shuffle(pos, raw_eval) + correction; improving = is_valid_score((ss - 2)->static_eval) && ss->static_eval > (ss - 2)->static_eval; @@ -501,7 +624,7 @@ Value Worker::search( // Reuse TT score as a better positional evaluation auto tt_adjusted_eval = ss->static_eval; - if (tt_data && tt_data->bound() != Bound::None && !is_mate_score(tt_data->score) + if (tt_data && tt_data->bound() != Bound::None && !is_decisive_score(tt_data->score) && tt_data->bound() != (tt_data->score > ss->static_eval ? Bound::Upper : Bound::Lower)) { tt_adjusted_eval = tt_data->score; } @@ -512,8 +635,8 @@ Value Worker::search( } if (cutnode && !PV_NODE && !is_in_check && !pos.is_kp_endgame() && depth >= tuned::nmp_depth - && !excluded && tt_adjusted_eval >= beta + tuned::nmp_beta_margin - && !is_being_mated_score(beta) && !m_in_nmp_verification) { + && !excluded && tt_adjusted_eval >= beta + tuned::nmp_beta_margin && !is_loss_score(beta) + && !m_in_nmp_verification) { i32 R = tuned::nmp_base_r + depth * tuned::nmp_depth_r + std::min(3 * 64, (tt_adjusted_eval - beta) * 64 / tuned::nmp_beta_diff) @@ -530,7 +653,7 @@ Value Worker::search( repetition_info.pop(); if (null_score >= beta) { - if (is_mate_score(null_score)) { + if (is_decisive_score(null_score)) { null_score = beta; } @@ -568,7 +691,7 @@ Value Worker::search( // returning the cutoff score immediately. This saves time by not searching // moves in positions that are likely to be cutoffs anyway. if (!PV_NODE && !is_in_check && depth >= tuned::probcut_min_depth && !excluded - && !is_mate_score(beta)) { + && !is_decisive_score(beta)) { const Value probcut_beta = beta + tuned::probcut_margin; const Depth probcut_depth = std::clamp(depth - 4, 1, depth - 1); @@ -632,7 +755,7 @@ Value Worker::search( auto move_history = quiet ? m_td.history.get_quiet_stats(pos, m, ply, ss) : 0; - if (!ROOT_NODE && !is_being_mated_score(best_value)) { + if (!ROOT_NODE && !is_loss_score(best_value)) { // Late Move Pruning (LMP) if (moves_played >= (tuned::lmp_depth_mult + depth * depth) / (2 - improving)) { break; @@ -663,7 +786,7 @@ Value Worker::search( // Singular extensions int extension = 0; if (!ROOT_NODE && tt_data && m == tt_move && !excluded && depth >= tuned::sing_min_depth - && is_valid_score(tt_data->score) && !is_mate_score(tt_data->score) + && is_valid_score(tt_data->score) && !is_decisive_score(tt_data->score) && tt_data->depth >= depth - tuned::sing_depth_margin && tt_data->bound() != Bound::Upper) { Value singular_beta = tt_data->score - depth * tuned::sing_beta_margin / 64; @@ -936,6 +1059,8 @@ Value Worker::search( } } + best_value = std::clamp(best_value, syzygy_min, syzygy_max); + if (!excluded) { Bound bound = best_value >= beta ? Bound::Lower : best_move != Move::none() ? Bound::Exact @@ -1022,7 +1147,7 @@ Value Worker::quiesce(const Position& pos, Stack* ss, Value alpha, Value beta, i Value static_eval = -VALUE_INF; if (!is_in_check) { correction = m_td.history.get_correction(pos); - raw_eval = tt_data && !is_mate_score(tt_data->eval) ? tt_data->eval : evaluate(pos); + raw_eval = tt_data && !is_decisive_score(tt_data->eval) ? tt_data->eval : evaluate(pos); static_eval = adj_shuffle(pos, raw_eval) + correction; if (!tt_data) { @@ -1048,12 +1173,12 @@ Value Worker::quiesce(const Position& pos, Stack* ss, Value alpha, Value beta, i // Iterate over the move list for (Move m = moves.next(); m != Move::none(); m = moves.next()) { // Bad noisies pruning - if (!is_being_mated_score(best_value) && moves.stage() == MovePicker::Stage::EmitBadNoisy) { + if (!is_loss_score(best_value) && moves.stage() == MovePicker::Stage::EmitBadNoisy) { break; } // QS SEE Pruning - if (!is_being_mated_score(best_value) && !SEE::see(pos, m, tuned::quiesce_see_threshold)) { + if (!is_loss_score(best_value) && !SEE::see(pos, m, tuned::quiesce_see_threshold)) { continue; } @@ -1131,29 +1256,6 @@ Value Worker::adj_shuffle(const Position& pos, Value value) { return value; } -void Worker::init_root_moves(const Position& root_position) { - m_td.root_moves.clear(); - m_td.root_moves.reserve(256); - - MoveGen movegen{root_position}; - - MoveList noisy{}; - MoveList quiet{}; - - movegen.generate_moves(noisy, quiet); - - const auto insert_root_moves = [&](const MoveList& moves) { - for (const auto move : moves) { - m_td.root_moves.emplace_back(move); - } - }; - - insert_root_moves(noisy); - insert_root_moves(quiet); - - m_multipv = std::min(m_searcher.settings.multipv, m_td.root_moves.size()); -} - void Worker::print_info_line(usize pv_idx) { const auto& root_move = m_td.root_moves[pv_idx]; @@ -1169,13 +1271,28 @@ void Worker::print_info_line(usize pv_idx) { lowerbound = false; } + if (score < root_move.tb_min_score || score > root_move.tb_max_score) { + score = std::clamp(score, root_move.tb_min_score, root_move.tb_max_score); + + // root TB scores are exact + upperbound = false; + lowerbound = false; + } + // Lambda to convert internal units score to uci score. TODO: add eval rescaling here once we get one auto format_score = [](Value score) { - if (score < -VALUE_WIN && score > -VALUE_MATED) { + static constexpr Value TB_DISPLAY_BASE = 30000; + if (is_mating_score(score)) { + return "mate " + std::to_string((VALUE_MATED + 1 - score) / 2); + } + if (is_being_mated_score(score)) { return "mate " + std::to_string(-(VALUE_MATED + score + 1) / 2); } - if (score > VALUE_WIN && score < VALUE_MATED) { - return "mate " + std::to_string((VALUE_MATED + 1 - score) / 2); + if (is_tb_win_score(score)) { + return "cp " + std::to_string(TB_DISPLAY_BASE + score - VALUE_TB_WIN); + } + if (is_tb_loss_score(score)) { + return "cp " + std::to_string(-TB_DISPLAY_BASE + score + VALUE_TB_WIN); } return "cp " + std::to_string(score / 4); }; @@ -1202,12 +1319,15 @@ void Worker::print_info_line(usize pv_idx) { if (root_move.searched_depth >= 16) { std::cout << " hashfull " << m_searcher.tt.hashfull(); } - std::cout << " time " << time::cast(curr_time - m_search_start).count() - << " pv " << root_move.pv << std::endl; + std::cout << " time " << time::cast(curr_time - m_search_start).count(); + if (m_searcher.settings.tb_enabled) { + std::cout << " tbhits " << m_searcher.tb_hit_count(); + } + std::cout << " pv " << root_move.pv << std::endl; } void Worker::print_info_lines() { - for (usize pv_idx = 0; pv_idx < m_multipv; ++pv_idx) { + for (usize pv_idx = 0; pv_idx < m_searcher.multipv; ++pv_idx) { print_info_line(pv_idx); } } diff --git a/src/search.hpp b/src/search.hpp index 1d5fe634..e86d7566 100644 --- a/src/search.hpp +++ b/src/search.hpp @@ -5,8 +5,8 @@ #include "position.hpp" #include "psqt_state.hpp" #include "repetition_info.hpp" +#include "root_move.hpp" #include "tt.hpp" -#include "util/static_vector.hpp" #include "util/types.hpp" #include #include @@ -30,6 +30,7 @@ struct SearchSettings { usize multipv = 1; bool silent = false; bool datagen = false; + bool tb_enabled = false; }; // Forward declare for Searcher @@ -40,33 +41,6 @@ enum class ThreadType { SECONDARY = 0, }; -struct PV { -public: - void clear() { - m_pv.clear(); - } - - void set(Move move) { - m_pv.clear(); - m_pv.push_back(move); - } - - void set(Move move, const PV& child_pv_line) { - m_pv.clear(); - m_pv.push_back(move); - m_pv.append(child_pv_line.m_pv); - } - - Move first_move() const { - return m_pv.empty() ? Move::none() : m_pv[0]; - } - - friend std::ostream& operator<<(std::ostream& os, const PV& pv); - -private: - StaticVector m_pv; -}; - struct Stack { Value static_eval = 0; Move killer = Move::none(); @@ -84,25 +58,6 @@ struct SearchLimits { Depth depth_limit; }; -struct RootMove { - explicit RootMove(Move move) { - pv.set(move); - } - - Value score = -VALUE_INF; - Value window_score = -VALUE_INF; - Value previous_score = -VALUE_INF; - Value display_score = -VALUE_INF; - - bool upperbound = false; - bool lowerbound = false; - - PV pv; - - Depth searched_depth = 1; - Depth seldepth = 0; -}; - struct ThreadData { History history; std::vector psqt_states; @@ -137,15 +92,22 @@ class Searcher { SearchSettings settings; TT tt; + // Root moves are duplicated here to avoid probing DTZ tables once for every thread, + // which is costly with many threads and DTZ tables on an HDD (TCEC). + std::vector root_moves; + usize multipv; + bool tb_root = false; + bool probe_wdl = false; + // We use a shared_mutex to ensure proper mutual thread exclusion.and avoid races. // The UCI thread only ever obtains exclusive access (using std::unique_lock); // search threads only ever obtain shared access (using std::shared_lock). // This ensures that the two classes of thread never step on each other. std::shared_mutex mutex; - using BarrierPtr = std::unique_ptr>; - BarrierPtr idle_barrier; - BarrierPtr started_barrier; + using BarrierPtr = std::unique_ptr>; + BarrierPtr idle_barrier = nullptr; + BarrierPtr started_barrier = nullptr; Searcher(); ~Searcher(); @@ -158,12 +120,15 @@ class Searcher { void exit(); u64 node_count(); + u64 tb_hit_count(); void reset(); void resize_tt(size_t mb) { tt.resize(mb, m_workers.size()); } private: + void init_root_moves(const Position& root_position, RepetitionInfo& repetition_info); + std::vector> m_workers; }; @@ -193,6 +158,9 @@ class alignas(128) Worker { [[nodiscard]] u64 search_nodes() const { return m_search_nodes.load(std::memory_order_relaxed); } + [[nodiscard]] u64 tb_hits() const { + return m_tb_hits.load(std::memory_order_relaxed); + } [[nodiscard]] const ThreadData& get_thread_data() const { return m_td; @@ -209,14 +177,18 @@ class alignas(128) Worker { m_search_nodes.fetch_add(1, std::memory_order_relaxed); } + void increment_tb_hits() { + m_tb_hits.fetch_add(1, std::memory_order_relaxed); + } + std::atomic m_search_nodes; + std::atomic m_tb_hits; time::TimePoint m_search_start; time::TimePoint m_last_info_time; Searcher& m_searcher; std::thread m_thread; ThreadType m_thread_type; SearchLimits m_search_limits; - usize m_multipv; ThreadData m_td; usize m_pv_idx; usize m_pv_start; @@ -239,8 +211,6 @@ class alignas(128) Worker { Value adj_shuffle(const Position& pos, Value value); bool check_tm_hard_limit(); - void init_root_moves(const Position& root_position); - void print_info_lines(); void print_info_line(usize pv_idx); @@ -256,7 +226,7 @@ class alignas(128) Worker { } bool is_legal_root_move(Move move) const { - for (usize i = m_pv_idx; i < m_td.root_moves.size(); ++i) { + for (usize i = m_pv_idx; i < m_pv_end; ++i) { const auto& root_move = m_td.root_moves[i]; if (root_move.pv.first_move() == move) { return true; diff --git a/src/tb.cpp b/src/tb.cpp new file mode 100644 index 00000000..ba971fbf --- /dev/null +++ b/src/tb.cpp @@ -0,0 +1,193 @@ +#include "tb.hpp" +#include "bb_attacks.hpp" +#include +#include +#include +#include + + +#include + +namespace Clockwork::tb { + +namespace { + +[[nodiscard]] u64 piece_type_bb(const Position& pos, PieceType piece_type) { + const auto bb = + pos.bitboard_for(Color::White, piece_type) | pos.bitboard_for(Color::Black, piece_type); + return bb.value(); +} + +} + +InitStatus init(std::string_view path) { + const std::string path_str{path}; + + if (!tb_init(path_str.c_str())) { + return InitStatus::Failed; + } + + if (TB_LARGEST == 0) { + return InitStatus::NoneFound; + } + + return InitStatus::Success; +} + +void free() { + tb_free(); +} + +u32 dtz_count() { + return static_cast(TB_NUM_DTZ); +} + +u32 wdl_count() { + return static_cast(TB_NUM_WDL); +} + +u32 max_pieces() { + return static_cast(TB_LARGEST); +} + +bool probe_root(const Position& pos, + RepetitionInfo& repetition_info, + std::span root_moves) { + const auto move_from_tb = [&](PyrrhicMove tb_move) { + static constexpr std::array PROMO_PIECE_FLAGS = { + MoveFlags::Normal, MoveFlags::PromoQueen, MoveFlags::PromoRook, + MoveFlags::PromoBishop, MoveFlags::PromoKnight, + }; + + const Square from{static_cast(PYRRHIC_MOVE_FROM(tb_move))}; + const Square to{static_cast(PYRRHIC_MOVE_TO(tb_move))}; + + auto flags = PROMO_PIECE_FLAGS[PYRRHIC_MOVE_FLAGS(tb_move) & PYRRHIC_MASK_PROMO_FLAGS]; + + if (PYRRHIC_MOVE_IS_ENPASS(tb_move)) { + // an ep move cannot be a promotion + flags = MoveFlags::EnPassant; + } + + if (pos.piece_at(to) != PieceType::None) { + flags |= MoveFlags::CaptureBit; + } + + return Move{from, to, flags}; + }; + + const auto wdl_from_tb = [](i32 tb_rank) { + static constexpr i32 MAX_DTZ = 262144; + + static constexpr i32 WIN_BOUND = MAX_DTZ - 100; + static constexpr i32 DRAW_BOUND = -MAX_DTZ + 101; + + if (tb_rank >= WIN_BOUND) { + return Search::WDL::Win; + } else if (tb_rank >= DRAW_BOUND) { + return Search::WDL::Draw; + } else { + return Search::WDL::Loss; + } + }; + + TbRootMoves tb_root_moves{}; + bool dtz_succeeded = true; + + const auto ep_square = pos.en_passant(); + const u32 ep_idx = ep_square.is_valid() ? ep_square.raw : 0; + + // has_repeated() test case: + // - position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 moves e2e4 e7e5 g1f3 b8c6 f1c4 g8f6 d2d4 e5d4 e1g1 f8c5 e4e5 d7d5 e5f6 d5c4 f1e1 c8e6 f3g5 d8d5 b1c3 d5f5 c3e4 e8c8 g2g4 f5e5 g5e6 f7e6 f6g7 h8g8 c1h6 d4d3 c2c3 d8d7 e4c5 e5c5 d1f3 c5d5 f3d5 e6d5 f2f4 c6d8 g1f2 d8f7 g4g5 b7b5 f4f5 d5d4 a2a3 d7d6 e1e6 d4c3 b2c3 d6e6 f5e6 f7d6 a1e1 a7a5 e1e3 b5b4 a3b4 a5b4 e3f3 c8b7 g5g6 h7g6 f3f8 d6e4 f2e3 d3d2 e6e7 d2d1q e7e8q e4d6 e8g6 d1e1 e3d4 e1d1 d4e5 d1e2 e5d5 e2d1 d5e6 d1e1 e6d7 e1d1 g6g2 d6e4 d7e6 d1d6 e6f5 d6c5 f5f4 c5d6 f4f3 d6d3 f3g4 d3d7 g4h5 d7d5 h5h4 d5e6 f8g8 e6h6 h4g4 h6e6 g4f3 e6g8 f3e4 b7b6 g2f2 b6b5 f2f8 g8e6 e4d4 e6g4 d4e5 g4e2 e5f6 e2f3 f6e7 f3e4 e7d7 e4c6 d7d8 c6d5 d8c7 d5c6 c7d8 c6b6 d8e7 b6e3 e7f7 e3f3 f7e8 f3c6 e8e7 c6e4 e7d6 e4g6 d6e7 g6e4 e7d6 e4g6 d6e5 g6g5 e5e6 g5e3 e6f7 e3f3 f7g8 f3d5 f8f7 d5d6 f7e8 b5a6 e8a4 a6b6 a4b4 d6b4 c3b4 c4c3 g8f7 c3c2 g7g8q c2c1q g8b8 b6a6 b8a8 a6b6 a8b8 b6a6 b8d6 a6b7 d6e7 b7a6 h2h4 c1d1 e7g5 d1b3 f7g7 b3b4 h4h5 b4b7 g7g6 b7b1 g5f5 b1g1 g6h7 g1a7 h7g8 a7e3 g8f7 e3a7 f7f6 a7d4 f6e7 d4a7 e7f6 a7d4 f6f7 d4a7 f7g8 a7e3 g8g7 a6b7 h5h6 e3g3 g7f7 g3c7 f7g6 c7g3 f5g5 g3d3 g5f5 d3g3 + // - f5g5 should have a lower tb rank (262110) than g6f6 (262114) + auto wdl = tb_probe_root_dtz( + pos.board().get_color_bitboard(Color::White).value(), + pos.board().get_color_bitboard(Color::Black).value(), piece_type_bb(pos, PieceType::King), + piece_type_bb(pos, PieceType::Queen), piece_type_bb(pos, PieceType::Rook), + piece_type_bb(pos, PieceType::Bishop), piece_type_bb(pos, PieceType::Knight), + piece_type_bb(pos, PieceType::Pawn), pos.get_50mr_counter(), ep_idx, + pos.active_color() == Color::White, repetition_info.has_repeated(), &tb_root_moves); + + if (!wdl) { + dtz_succeeded = false; + wdl = tb_probe_root_wdl( + pos.board().get_color_bitboard(Color::White).value(), + pos.board().get_color_bitboard(Color::Black).value(), piece_type_bb(pos, PieceType::King), + piece_type_bb(pos, PieceType::Queen), piece_type_bb(pos, PieceType::Rook), + piece_type_bb(pos, PieceType::Bishop), piece_type_bb(pos, PieceType::Knight), + piece_type_bb(pos, PieceType::Pawn), pos.get_50mr_counter(), ep_idx, + pos.active_color() == Color::White, true, &tb_root_moves); + } + + if (!wdl || tb_root_moves.size == 0) { + return dtz_succeeded; + } + + const auto get_root_move = [&](Move move) -> Search::RootMove* { + for (auto& root_move : root_moves) { + if (root_move.pv.first_move() == move) { + return &root_move; + } + } + return nullptr; + }; + + for (usize i = 0; i < tb_root_moves.size; i++) { + auto [tb_move, tb_rank] = tb_root_moves.moves[i]; + + const auto move = move_from_tb(tb_move); + + // Correct moves that immediately threefold. + // Test case: + // - position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 moves e2e4 e7e5 g1f3 b8c6 f1c4 g8f6 d2d4 e5d4 e1g1 f8c5 e4e5 d7d5 e5f6 d5c4 f1e1 c8e6 f3g5 d8d5 b1c3 d5f5 c3e4 e8c8 g2g4 f5e5 g5e6 f7e6 f6g7 h8g8 c1h6 d4d3 c2c3 d8d7 e4c5 e5c5 d1f3 c5d5 f3d5 e6d5 f2f4 c6d8 g1f2 d8f7 g4g5 b7b5 f4f5 d5d4 a2a3 d7d6 e1e6 d4c3 b2c3 d6e6 f5e6 f7d6 a1e1 a7a5 e1e3 b5b4 a3b4 a5b4 e3f3 c8b7 g5g6 h7g6 f3f8 d6e4 f2e3 d3d2 e6e7 d2d1q e7e8q e4d6 e8g6 d1e1 e3d4 e1d1 d4e5 d1e2 e5d5 e2d1 d5e6 d1e1 e6d7 e1d1 g6g2 d6e4 d7e6 d1d6 e6f5 d6c5 f5f4 c5d6 f4f3 d6d3 f3g4 d3d7 g4h5 d7d5 h5h4 d5e6 f8g8 e6h6 h4g4 h6e6 g4f3 e6g8 f3e4 b7b6 g2f2 b6b5 f2f8 g8e6 e4d4 e6g4 d4e5 g4e2 e5f6 e2f3 f6e7 f3e4 e7d7 e4c6 d7d8 c6d5 d8c7 d5c6 c7d8 c6b6 d8e7 b6e3 e7f7 e3f3 f7e8 f3c6 e8e7 c6e4 e7d6 e4g6 d6e7 g6e4 e7d6 e4g6 d6e5 g6g5 e5e6 g5e3 e6f7 e3f3 f7g8 f3d5 f8f7 d5d6 f7e8 b5a6 e8a4 a6b6 a4b4 d6b4 c3b4 c4c3 g8f7 c3c2 g7g8q c2c1q g8b8 b6a6 b8a8 a6b6 a8b8 b6a6 b8d6 a6b7 d6e7 b7a6 h2h4 c1d1 e7g5 d1b3 f7g7 b3b4 h4h5 b4b7 g7g6 b7b1 g5f5 b1g1 g6h7 g1a7 h7g8 a7e3 g8f7 e3a7 f7f6 a7d4 f6e7 d4a7 e7f6 a7d4 f6f7 d4a7 f7g8 a7e3 g8g7 a6b7 h5h6 e3g3 g7f7 g3c7 f7g6 c7g3 f5g5 g3d3 g5f5 d3g3 f5g5 g3d3 g5f5 + // - d3g3 should be corrected to rank 0, all others losing + if (pos.is_reversible(move)) { + Position pos_after = pos.move(move); + repetition_info.push(pos_after.get_hash_key(), true); + if (repetition_info.detect_repetition(0)) { + tb_rank = 0; + } + repetition_info.pop(); + } + + auto* root_move = get_root_move(move); + if (!root_move) { + continue; + } + + const auto wdl = wdl_from_tb(tb_rank); + root_move->set_tb_status(wdl, tb_rank); + } + + std::ranges::stable_sort(root_moves, [](const Search::RootMove& a, const Search::RootMove& b) { + return a.tb_rank > b.tb_rank; + }); + + return dtz_succeeded; +} + +Search::WDL probe_wdl(const Position& pos) { + const auto ep_square = pos.en_passant(); + const u32 ep_idx = ep_square.is_valid() ? ep_square.raw : 0; + + const auto wdl = tb_probe_wdl( + pos.board().get_color_bitboard(Color::White).value(), + pos.board().get_color_bitboard(Color::Black).value(), piece_type_bb(pos, PieceType::King), + piece_type_bb(pos, PieceType::Queen), piece_type_bb(pos, PieceType::Rook), + piece_type_bb(pos, PieceType::Bishop), piece_type_bb(pos, PieceType::Knight), + piece_type_bb(pos, PieceType::Pawn), ep_idx, pos.active_color() == Color::White); + + switch (wdl) { + case TB_RESULT_FAILED: + return Search::WDL::None; + case TB_WIN: + return Search::WDL::Win; + case TB_LOSS: + return Search::WDL::Loss; + default: + // Cursed wins and blessed losses are both functionally draws + return Search::WDL::Draw; + } +} + +} diff --git a/src/tb.hpp b/src/tb.hpp new file mode 100644 index 00000000..41310511 --- /dev/null +++ b/src/tb.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "position.hpp" +#include "repetition_info.hpp" +#include "root_move.hpp" +#include "util/types.hpp" +#include +#include +#include + +namespace Clockwork::tb { + +enum class InitStatus { + Failed, + NoneFound, + Success, +}; + +InitStatus init(std::string_view path); +void free(); + +[[nodiscard]] u32 dtz_count(); +[[nodiscard]] u32 wdl_count(); + +[[nodiscard]] u32 max_pieces(); + +// Returns whether the DTZ probe succeeded. +[[nodiscard]] bool probe_root(const Position& pos, + RepetitionInfo& repetition_info, + std::span root_moves); + +[[nodiscard]] Search::WDL probe_wdl(const Position& pos); + +} diff --git a/src/uci.cpp b/src/uci.cpp index 466aa747..1fe06ea6 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -7,6 +7,7 @@ #include "position.hpp" #include "search.hpp" #include "speedtest.hpp" +#include "tb.hpp" #include "tuned.hpp" #include "util/ios_fmt_guard.hpp" #include "util/parse.hpp" @@ -32,10 +33,13 @@ constexpr usize MAX_MULTIPV = 256; UCIHandler::UCIHandler() : m_position(*Position::parse(STARTPOS)) { - searcher.initialize(1); searcher.set_position(m_position, m_repetition_info); } +UCIHandler::~UCIHandler() { + tb::free(); +} + void UCIHandler::loop() { std::string input; @@ -65,6 +69,7 @@ void UCIHandler::execute_command(const std::string& line) { std::cout << "option name Threads type spin default 1 min 1 max " << MAX_THREADS << "\n"; std::cout << "option name Hash type spin default 16 min 1 max " << MAX_HASH << "\n"; std::cout << "option name MultiPV type spin default 1 min 1 max " << MAX_MULTIPV << "\n"; + std::cout << "option name SyzygyPath type string default \n"; tuned::uci_print_tunable_options(); std::cout << "uciok" << std::endl; } else if (command == "ucinewgame") { @@ -131,8 +136,9 @@ void UCIHandler::handle_debug(std::istringstream&) { void UCIHandler::handle_go(std::istringstream& is) { // Clear any previous settings - settings = {}; - settings.multipv = m_multipv; + settings = {}; + settings.multipv = m_multipv; + settings.tb_enabled = m_tb_enabled; std::string token; while (is >> token) { if (token == "depth") { @@ -286,6 +292,22 @@ void UCIHandler::handle_setoption(std::istringstream& is) { } else { std::cout << "Invalid value " << value_str << std::endl; } + } else if (name == "SyzygyPath") { + //TODO accept paths with spaces + m_tb_enabled = false; + switch (tb::init(value_str)) { + case tb::InitStatus::Failed: + std::cout << "Failed to initialize Pyrrhic" << std::endl; + break; + case tb::InitStatus::NoneFound: + std::cout << "No TB files found" << std::endl; + break; + case tb::InitStatus::Success: + std::cout << "info string Found " << tb::wdl_count() << " WDL and " << tb::dtz_count() + << " DTZ files up to " << tb::max_pieces() << "-man" << std::endl; + m_tb_enabled = true; + break; + } } else if (tuned::uci_parse_tunable(name, value_str)) { // Successfully parsed tunable } else { diff --git a/src/uci.hpp b/src/uci.hpp index 3cd75992..2c722290 100644 --- a/src/uci.hpp +++ b/src/uci.hpp @@ -14,6 +14,7 @@ namespace Clockwork::UCI { class UCIHandler { public: UCIHandler(); + ~UCIHandler(); void loop(); void handle_command_line(i32 argc, char* argv[]); @@ -25,6 +26,7 @@ class UCIHandler { TT m_tt; bool m_use_soft_nodes = false; usize m_multipv = 1; + bool m_tb_enabled = false; Search::Searcher searcher; diff --git a/src/util/bit.hpp b/src/util/bit.hpp index 34fe65f5..9c1eac0f 100644 --- a/src/util/bit.hpp +++ b/src/util/bit.hpp @@ -1,6 +1,6 @@ #pragma once -#include "util/types.hpp" +#include "types.hpp" namespace Clockwork { diff --git a/vendor/.gitignore b/vendor/.gitignore index 867b6e9c..a8249c2b 100644 --- a/vendor/.gitignore +++ b/vendor/.gitignore @@ -1,2 +1,3 @@ -# Ignore all subdirectories +# Ignore all subdirectories, except Pyrrhic */ +!Pyrrhic/ diff --git a/vendor/Pyrrhic/LICENSE b/vendor/Pyrrhic/LICENSE new file mode 100644 index 00000000..2692b48a --- /dev/null +++ b/vendor/Pyrrhic/LICENSE @@ -0,0 +1,24 @@ +The MIT License (MIT) + +(c) 2015 basil, all rights reserved, +Modifications Copyright (c) 2016-2019 by Jon Dart +Modifications Copyright (c) 2020-2024 by Andrew Grant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/Pyrrhic/README.md b/vendor/Pyrrhic/README.md new file mode 100644 index 00000000..85de7f99 --- /dev/null +++ b/vendor/Pyrrhic/README.md @@ -0,0 +1,21 @@ +Pyrrhic is a partial cleanup of the [Fathom](https://github.com/jdart1/Fathom) library for probing up-tp 7-man Syzygy Tablebases. Pyrrhic attempts to reduce the burden on the global namespace introduced by Fathom, as well as provide a more robust API for decoding the results of function calls. Pyrrhic is kept fairly up-to date with changes as they are made to Stockfish's implementation, or the Fathom repository itself. + +**Integration with Pyrrhic** + +To make use of Pyrrhic in your engine, you'll want to copy the contents of this repository into a directory for your engine's source. Once done, you will need to update the definitions in `tbconfig.h`. This file allows Pyrrhic to make use of your engine's utilities. Namely, you'll be defining macros for `popcnt`, `lsb`, and `poplsb`, which are all common in engines. You'll also be providing a basic interface for Bitboad attack generation for each piece type. + +**Compatibility with Pyrrhic** + +Pyrrhic has a miniature chess implementation via `tbconfig.h`. Each chess program picks its own conventions. Pyrrhic uses the most common Bitboard orientation, where A1 = 0, and H8 = 63. If your engine uses a different layout, you'll need to do a form of translation when crafting calls to the Pyrrhic endpoint, as well as when reading the results back. + +**Building Alongside Pyrrhic** + +The only source file that needs to be compiled is `tbprobe.c`. Any other `.c` files in the Pyrrhic project should not be explicitly compiled. The only `.h` file that should be included by a project is `tbprobe.h`. The `api.h` file is simply reference material, and should never be explicitly included. + +**Initializing Pyrrhic** + +`tb_init(const char* path)` is used to initialize the tablebases from a directory. Multiple file paths at once should be seperated by a semicolon in Windows systems, and by a colon on Unix-based systems. When finished, tb_free() is called to cleanup any remaining memory. The tablebases may be initialized once again if desired. Lastly, if the path is an empty string, or ``, then tb_init() will return without doing anything. tb_init() will also set `TB_LARGEST`, `TB_NUM_WDL`, `TB_NUM_DTM`, `TB_NUM_DTZ` to pass back information about what was loaded. Those variables have `extern` definitions, and can be referenced through `tbprobe.h` + +**Performing Tablebase Probes** + +Refer to the [Wiki](https://github.com/AndyGrant/Pyrrhic/wiki), which lays out the 4 possible Tablebase Probing functions diff --git a/vendor/Pyrrhic/api.h b/vendor/Pyrrhic/api.h new file mode 100644 index 00000000..a0aa9595 --- /dev/null +++ b/vendor/Pyrrhic/api.h @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2013-2020 Ronald de Man + * Copyright (c) 2015 Basil, all rights reserved, + * Modifications Copyright (c) 2016-2019 by Jon Dart + * Modifications Copyright (c) 2020-2026 by Andrew Grant + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +/// General compatibility for executing tablebase calls + +#define PYRRHIC_BLACK 0 +#define PYRRHIC_WHITE 1 + +/// For providing Results arrays to tb_probe_root() + +#define TB_MAX_MOVES 256 + +/// Possible return values from a successful tb_probe_wdl() + +#define TB_LOSS 0 /* LOSS */ +#define TB_BLESSED_LOSS 1 /* LOSS but 50-move draw */ +#define TB_DRAW 2 /* DRAW */ +#define TB_CURSED_WIN 3 /* WIN but 50-move draw */ +#define TB_WIN 4 /* WIN */ + +/// Possible return values from a failed tb_probe_wdl() or tb_probe_root() + +#define TB_RESULT_CHECKMATE TB_SET_WDL(0, TB_WIN) +#define TB_RESULT_STALEMATE TB_SET_WDL(0, TB_DRAW) +#define TB_RESULT_FAILED 0xFFFFFFFF + +/// Decoding Tablebase Result -> Your Engine's Move Encoding, + WDL/DTZ data + +#define TB_RESULT_WDL(_res) (((_res) & TB_RESULT_WDL_MASK ) >> TB_RESULT_WDL_SHIFT ) +#define TB_RESULT_DTZ(_res) (((_res) & TB_RESULT_DTZ_MASK ) >> TB_RESULT_DTZ_SHIFT ) + +#define TB_RESULT_TO(_res) (((_res) & TB_RESULT_TO_MASK ) >> TB_RESULT_TO_SHIFT ) +#define TB_RESULT_FROM(_res) (((_res) & TB_RESULT_FROM_MASK ) >> TB_RESULT_FROM_SHIFT ) +#define TB_RESULT_IS_ENPASS(_res) (((_res) & TB_RESULT_EP_MASK ) >> TB_RESULT_EP_SHIFT ) + +#define TB_RESULT_IS_QPROMO(_res) (TB_GET_PROMOTES((_res)) == PYRRHIC_FLAG_QPROMO) +#define TB_RESULT_IS_RPROMO(_res) (TB_GET_PROMOTES((_res)) == PYRRHIC_FLAG_RPROMO) +#define TB_RESULT_IS_BPROMO(_res) (TB_GET_PROMOTES((_res)) == PYRRHIC_FLAG_BPROMO) +#define TB_RESULT_IS_NPROMO(_res) (TB_GET_PROMOTES((_res)) == PYRRHIC_FLAG_NPROMO) + +/// Decoding PyrrhicMove -> Your Engine's Move Encoding + +#define PYRRHIC_MOVE_TO(x) (((x) >> PYRRHIC_SHIFT_TO ) & PYRRHIC_MASK_TO ) +#define PYRRHIC_MOVE_FROM(x) (((x) >> PYRRHIC_SHIFT_FROM) & PYRRHIC_MASK_FROM) + +#define PYRRHIC_MOVE_IS_ENPASS(x) (PYRRHIC_MOVE_FLAGS((x)) == PYRRHIC_FLAG_ENPASS) +#define PYRRHIC_MOVE_IS_QPROMO(x) (PYRRHIC_MOVE_FLAGS((x)) == PYRRHIC_FLAG_QPROMO) +#define PYRRHIC_MOVE_IS_RPROMO(x) (PYRRHIC_MOVE_FLAGS((x)) == PYRRHIC_FLAG_RPROMO) +#define PYRRHIC_MOVE_IS_BPROMO(x) (PYRRHIC_MOVE_FLAGS((x)) == PYRRHIC_FLAG_BPROMO) +#define PYRRHIC_MOVE_IS_NPROMO(x) (PYRRHIC_MOVE_FLAGS((x)) == PYRRHIC_FLAG_NPROMO) + +/// For tb_probe_root_dtz() and tb_probe_root_wdl() + +struct TbRootMove { + PyrrhicMove move; + int32_t tbRank; +}; + +struct TbRootMoves { + unsigned size; + struct TbRootMove moves[TB_MAX_MOVES]; +}; + +/// Init/Deinit for Pyrrhic + +bool tb_init(const char *_path); +void tb_free(void); + +/// Optional loader for non-filesystem tablebases. When set, tb_init() asks +/// the loader for any table not found on disk. Returned bytes are not freed +/// by Pyrrhic. Pass NULL to disable. + +typedef struct { + const unsigned char *data; + size_t size; +} pyrrhic_tb_blob; + +typedef bool (*tb_loader_fn)(const char *name, const char *suffix, pyrrhic_tb_blob *out); + +void tb_set_loader(tb_loader_fn loader); + +/// Pyrrhic Tablebase Probing Functions + +unsigned tb_probe_wdl( + uint64_t white, uint64_t black, + uint64_t kings, uint64_t queens, + uint64_t rooks, uint64_t bishops, + uint64_t knights, uint64_t pawns, + unsigned ep, bool turn); + +unsigned tb_probe_root( + uint64_t white, uint64_t black, + uint64_t kings, uint64_t queens, + uint64_t rooks, uint64_t bishops, + uint64_t knights, uint64_t pawns, + unsigned rule50, unsigned ep, + bool turn, unsigned *results); + +int tb_probe_root_dtz( + uint64_t white, uint64_t black, + uint64_t kings, uint64_t queens, + uint64_t rooks, uint64_t bishops, + uint64_t knights, uint64_t pawns, + unsigned rule50, unsigned ep, + bool turn, bool hasRepeated, + struct TbRootMoves *results); + +int tb_probe_root_wdl( + uint64_t white, uint64_t black, + uint64_t kings, uint64_t queens, + uint64_t rooks, uint64_t bishops, + uint64_t knights, uint64_t pawns, + unsigned rule50, unsigned ep, + bool turn, bool useRule50, + struct TbRootMoves *results); diff --git a/vendor/Pyrrhic/stdendian.h b/vendor/Pyrrhic/stdendian.h new file mode 100644 index 00000000..b8d20dbe --- /dev/null +++ b/vendor/Pyrrhic/stdendian.h @@ -0,0 +1,192 @@ +#pragma once + +/* requires C11 or C++11 */ +#if defined (__cplusplus) +#include +#elif !defined (__OPENCL_VERSION__) +#include +#endif + +/* Linux / GLIBC */ +#if defined(__linux__) || defined(__GLIBC__) || defined(__CYGWIN__) +#include +#include +#define __ENDIAN_DEFINED 1 +#define __BSWAP_DEFINED 1 +#define __HOSTSWAP_DEFINED 1 +// NDK defines _BYTE_ORDER etc +#ifndef _BYTE_ORDER +#define _BYTE_ORDER __BYTE_ORDER +#define _LITTLE_ENDIAN __LITTLE_ENDIAN +#define _BIG_ENDIAN __BIG_ENDIAN +#endif +#define bswap16(x) bswap_16(x) +#define bswap32(x) bswap_32(x) +#define bswap64(x) bswap_64(x) +#endif /* __linux__ || __GLIBC__ */ + +/* BSD */ +#if defined(__FreeBSD__) || defined(__NetBSD__) || \ + defined(__DragonFly__) || defined(__OpenBSD__) +#include +#define __ENDIAN_DEFINED 1 +#define __BSWAP_DEFINED 1 +#define __HOSTSWAP_DEFINED 1 +#endif /* BSD */ + +/* Solaris */ +#if defined (sun) +#include +/* sun headers don't set a value for _LITTLE_ENDIAN or _BIG_ENDIAN */ +#if defined(_LITTLE_ENDIAN) +#undef _LITTLE_ENDIAN +#define _LITTLE_ENDIAN 1234 +#define _BIG_ENDIAN 4321 +#define _BYTE_ORDER _LITTLE_ENDIAN +#elif defined(_BIG_ENDIAN) +#undef _BIG_ENDIAN +#define _LITTLE_ENDIAN 1234 +#define _BIG_ENDIAN 4321 +#define _BYTE_ORDER _BIG_ENDIAN +#endif +#define __ENDIAN_DEFINED 1 +#endif /* sun */ + +/* Windows */ +#if defined(_WIN32) || defined(_MSC_VER) +/* assumes all Microsoft targets are little endian */ +#define _LITTLE_ENDIAN 1234 +#define _BIG_ENDIAN 4321 +#define _BYTE_ORDER _LITTLE_ENDIAN +#define __ENDIAN_DEFINED 1 +#endif /* _MSC_VER */ + +/* OS X */ +#if defined(__APPLE__) +#include +#define _BYTE_ORDER BYTE_ORDER +#define _LITTLE_ENDIAN LITTLE_ENDIAN +#define _BIG_ENDIAN BIG_ENDIAN +#define __ENDIAN_DEFINED 1 +#endif /* __APPLE__ */ + +/* OpenCL */ +#if defined (__OPENCL_VERSION__) +#define _LITTLE_ENDIAN 1234 +#define __BIG_ENDIAN 4321 +#if defined (__ENDIAN_LITTLE__) +#define _BYTE_ORDER _LITTLE_ENDIAN +#else +#define _BYTE_ORDER _BIG_ENDIAN +#endif +#define bswap16(x) as_ushort(as_uchar2(ushort(x)).s1s0) +#define bswap32(x) as_uint(as_uchar4(uint(x)).s3s2s1s0) +#define bswap64(x) as_ulong(as_uchar8(ulong(x)).s7s6s5s4s3s2s1s0) +#define __ENDIAN_DEFINED 1 +#define __BSWAP_DEFINED 1 +#endif + +/* Unknown */ +#if !__ENDIAN_DEFINED +#error Could not determine CPU byte order +#endif + +/* POSIX - http://austingroupbugs.net/view.php?id=162 */ +#ifndef BYTE_ORDER +#define BYTE_ORDER _BYTE_ORDER +#endif +#ifndef LITTLE_ENDIAN +#define LITTLE_ENDIAN _LITTLE_ENDIAN +#endif +#ifndef BIG_ENDIAN +#define BIG_ENDIAN _BIG_ENDIAN +#endif + +/* OpenCL compatibility - define __ENDIAN_LITTLE__ on little endian systems */ +#if _BYTE_ORDER == _LITTLE_ENDIAN +#if !defined (__ENDIAN_LITTLE__) +#define __ENDIAN_LITTLE__ 1 +#endif +#endif + +/* Byte swap macros */ +#if !__BSWAP_DEFINED + +#ifndef bswap16 +/* handle missing __builtin_bswap16 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52624 */ +#if defined __GNUC__ +#define bswap16(x) __builtin_bswap16(x) +#else +inline uint16_t bswap16(uint16_t x) { + return (uint16_t)((((uint16_t) (x) & 0xff00) >> 8) | \ + (((uint16_t) (x) & 0x00ff) << 8)); +} +#endif +#endif + +#ifndef bswap32 +#if defined __GNUC__ +#define bswap32(x) __builtin_bswap32(x) +#else +inline uint32_t bswap32(uint32_t x) { + return (( x & 0xff000000) >> 24) | \ + (( x & 0x00ff0000) >> 8) | \ + (( x & 0x0000ff00) << 8) | \ + (( x & 0x000000ff) << 24); +} +#endif +#endif + +#ifndef bswap64 +#if defined __GNUC__ +#define bswap64(x) __builtin_bswap64(x) +#else +inline uint64_t bswap64(uint64_t x) { + return (( x & 0xff00000000000000ull) >> 56) | \ + (( x & 0x00ff000000000000ull) >> 40) | \ + (( x & 0x0000ff0000000000ull) >> 24) | \ + (( x & 0x000000ff00000000ull) >> 8) | \ + (( x & 0x00000000ff000000ull) << 8) | \ + (( x & 0x0000000000ff0000ull) << 24) | \ + (( x & 0x000000000000ff00ull) << 40) | \ + (( x & 0x00000000000000ffull) << 56); +} +#endif +#endif + +#endif + +/* Host swap macros */ +#ifndef __HOSTSWAP_DEFINED +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define htobe16(x) bswap16((x)) +#define htole16(x) ((uint16_t)(x)) +#define be16toh(x) bswap16((x)) +#define le16toh(x) ((uint16_t)(x)) + +#define htobe32(x) bswap32((x)) +#define htole32(x) ((uint32_t((x)) +#define be32toh(x) bswap32((x)) +#define le32toh(x) ((uint32_t)(x)) + +#define htobe64(x) bswap64((x)) +#define htole64(x) ((uint64_t)(x)) +#define be64toh(x) bswap64((x)) +#define le64toh(x) ((uint64_t)(x)) +#elif __BYTE_ORDER == __BIG_ENDIAN +#define htobe16(x) ((uint16_t)(x)) +#define htole16(x) bswap16((x)) +#define be16toh(x) ((uint16_t)(x)) +#define le16toh(x) bswap16((x)) + +#define htobe32(x) ((uint32_t)(x)) +#define htole32(x) bswap32((x)) +#define be32toh(x) ((uint32_t)(x)) +#define le64toh(x) bswap64((x)) + +#define htobe64(x) ((uint64_t)(x)) +#define htole64(x) bswap64((x)) +#define be64toh(x) ((uint64_t)(x)) +#define le32toh(x) bswap32((x)) +#endif +#endif \ No newline at end of file diff --git a/vendor/Pyrrhic/tbchess.c b/vendor/Pyrrhic/tbchess.c new file mode 100644 index 00000000..842c76fa --- /dev/null +++ b/vendor/Pyrrhic/tbchess.c @@ -0,0 +1,429 @@ +/* + * (c) 2015 basil, all rights reserved, + * Modifications Copyright (c) 2016-2019 by Jon Dart + * Modifications Copyright (c) 2020-2026 by Andrew Grant + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +enum { + + PYRRHIC_PAWN = 1, PYRRHIC_KNIGHT = 2, + PYRRHIC_BISHOP = 3, PYRRHIC_ROOK = 4, + PYRRHIC_QUEEN = 5, PYRRHIC_KING = 6, + + PYRRHIC_WPAWN = 1, PYRRHIC_BPAWN = 9, + PYRRHIC_WKNIGHT = 2, PYRRHIC_BKNIGHT = 10, + PYRRHIC_WBISHOP = 3, PYRRHIC_BBISHOP = 11, + PYRRHIC_WROOK = 4, PYRRHIC_BROOK = 12, + PYRRHIC_WQUEEN = 5, PYRRHIC_BQUEEN = 13, + PYRRHIC_WKING = 6, PYRRHIC_BKING = 14, +}; + +const uint64_t + PYRRHIC_PROMOSQS = 0XFF000000000000FFULL, + + PYRRHIC_PRIME_WKING = 00000000000000000000ULL, + PYRRHIC_PRIME_WQUEEN = 11811845319353239651ULL, + PYRRHIC_PRIME_WROOK = 10979190538029446137ULL, + PYRRHIC_PRIME_WBISHOP = 12311744257139811149ULL, + PYRRHIC_PRIME_WKNIGHT = 15202887380319082783ULL, + PYRRHIC_PRIME_WPAWN = 17008651141875982339ULL, + PYRRHIC_PRIME_BKING = 00000000000000000000ULL, + PYRRHIC_PRIME_BQUEEN = 15484752644942473553ULL, + PYRRHIC_PRIME_BROOK = 18264461213049635989ULL, + PYRRHIC_PRIME_BBISHOP = 15394650811035483107ULL, + PYRRHIC_PRIME_BKNIGHT = 13469005675588064321ULL, + PYRRHIC_PRIME_BPAWN = 11695583624105689831ULL, + PYRRHIC_PRIME_NONE = 00000000000000000000ULL; + +typedef struct PyrrhicPosition { + uint64_t white, black; + uint64_t kings, queens, rooks; + uint64_t bishops, knights, pawns; + uint8_t rule50, ep; bool turn; +} PyrrhicPosition; + +unsigned pyrrhic_move_to (PyrrhicMove move) { return (move >> PYRRHIC_SHIFT_TO ) & PYRRHIC_MASK_TO ; } +unsigned pyrrhic_move_from (PyrrhicMove move) { return (move >> PYRRHIC_SHIFT_FROM ) & PYRRHIC_MASK_FROM ; } +unsigned pyrrhic_move_promotes (PyrrhicMove move) { return (move >> PYRRHIC_SHIFT_FLAGS ) & PYRRHIC_MASK_PROMO_FLAGS; } + +int pyrrhic_colour_of_piece (uint8_t piece) { return !(piece >> 3); } +int pyrrhic_type_of_piece (uint8_t piece) { return (piece & 0x7); } + +bool pyrrhic_test_bit (uint64_t bb, int sq) { return (bb >> sq) & 0x1; } +void pyrrhic_enable_bit (uint64_t *b, int sq) { *b |= (1ull << sq); } +void pyrrhic_disable_bit (uint64_t *b, int sq) { *b &= ~(1ull << sq); } +bool pyrrhic_promo_square (int sq) { return (PYRRHIC_PROMOSQS >> sq) & 0x1; } +bool pyrrhic_pawn_start_square (int colour, int sq) { return (sq >> 3) == (colour ? 1 : 6); } + +// The only two forward-declarations that are needed +bool pyrrhic_do_move(PyrrhicPosition *pos, const PyrrhicPosition *pos0, PyrrhicMove move); +bool pyrrhic_legal_move(const PyrrhicPosition *pos, PyrrhicMove move); + + +const char pyrrhic_piece_to_char[] = " PNBRQK pnbrqk"; + +uint64_t pyrrhic_pieces_by_type(const PyrrhicPosition *pos, int colour, int piece) { + + assert(PYRRHIC_PAWN <= piece && piece <= PYRRHIC_KING); + assert(colour == PYRRHIC_WHITE || colour == PYRRHIC_BLACK); + + uint64_t side = (colour == PYRRHIC_WHITE ? pos->white : pos->black); + + switch (piece) { + case PYRRHIC_PAWN : return pos->pawns & side; + case PYRRHIC_KNIGHT : return pos->knights & side; + case PYRRHIC_BISHOP : return pos->bishops & side; + case PYRRHIC_ROOK : return pos->rooks & side; + case PYRRHIC_QUEEN : return pos->queens & side; + case PYRRHIC_KING : return pos->kings & side; + default: assert(0); return 0; + } +} + +int pyrrhic_char_to_piece_type(char c) { + + for (int i = PYRRHIC_PAWN; i <= PYRRHIC_KING; i++) + if (c == pyrrhic_piece_to_char[i]) + return i; + return 0; +} + + +uint64_t pyrrhic_calc_key(const PyrrhicPosition *pos, int mirror) { + + uint64_t white = mirror ? pos->black : pos->white; + uint64_t black = mirror ? pos->white : pos->black; + + return PYRRHIC_POPCOUNT(white & pos->queens ) * PYRRHIC_PRIME_WQUEEN + + PYRRHIC_POPCOUNT(white & pos->rooks ) * PYRRHIC_PRIME_WROOK + + PYRRHIC_POPCOUNT(white & pos->bishops) * PYRRHIC_PRIME_WBISHOP + + PYRRHIC_POPCOUNT(white & pos->knights) * PYRRHIC_PRIME_WKNIGHT + + PYRRHIC_POPCOUNT(white & pos->pawns ) * PYRRHIC_PRIME_WPAWN + + PYRRHIC_POPCOUNT(black & pos->queens ) * PYRRHIC_PRIME_BQUEEN + + PYRRHIC_POPCOUNT(black & pos->rooks ) * PYRRHIC_PRIME_BROOK + + PYRRHIC_POPCOUNT(black & pos->bishops) * PYRRHIC_PRIME_BBISHOP + + PYRRHIC_POPCOUNT(black & pos->knights) * PYRRHIC_PRIME_BKNIGHT + + PYRRHIC_POPCOUNT(black & pos->pawns ) * PYRRHIC_PRIME_BPAWN; +} + +uint64_t pyrrhic_calc_key_from_pcs(int *pieces, int mirror) { + + return pieces[PYRRHIC_WQUEEN ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_WQUEEN + + pieces[PYRRHIC_WROOK ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_WROOK + + pieces[PYRRHIC_WBISHOP ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_WBISHOP + + pieces[PYRRHIC_WKNIGHT ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_WKNIGHT + + pieces[PYRRHIC_WPAWN ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_WPAWN + + pieces[PYRRHIC_BQUEEN ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_BQUEEN + + pieces[PYRRHIC_BROOK ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_BROOK + + pieces[PYRRHIC_BBISHOP ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_BBISHOP + + pieces[PYRRHIC_BKNIGHT ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_BKNIGHT + + pieces[PYRRHIC_BPAWN ^ (mirror ? 8 : 0)] * PYRRHIC_PRIME_BPAWN; +} + +uint64_t pyrrhic_calc_key_from_pieces(uint8_t *pieces, int length) { + + static const uint64_t PyrrhicPrimes[] = { + PYRRHIC_PRIME_NONE , PYRRHIC_PRIME_WPAWN , PYRRHIC_PRIME_WKNIGHT, PYRRHIC_PRIME_WBISHOP, + PYRRHIC_PRIME_WROOK, PYRRHIC_PRIME_WQUEEN, PYRRHIC_PRIME_WKING , PYRRHIC_PRIME_NONE , + PYRRHIC_PRIME_NONE , PYRRHIC_PRIME_BPAWN , PYRRHIC_PRIME_BKNIGHT, PYRRHIC_PRIME_BBISHOP, + PYRRHIC_PRIME_BROOK, PYRRHIC_PRIME_BQUEEN, PYRRHIC_PRIME_BKING , PYRRHIC_PRIME_NONE , + }; + + uint64_t key = 0; + for (int i = 0; i < length; i++) + key += PyrrhicPrimes[pieces[i]]; + + return key; +} + + +uint64_t pyrrhic_do_bb_move(uint64_t bb, unsigned from, unsigned to) { + return (((bb >> from) & 0x1) << to) | (bb & (~(1ull << from) & ~(1ull << to))); +} + +PyrrhicMove pyrrhic_build_move(unsigned flags, unsigned from, unsigned to) { + return ((to & PYRRHIC_MASK_TO ) << PYRRHIC_SHIFT_TO ) + | ((from & PYRRHIC_MASK_FROM ) << PYRRHIC_SHIFT_FROM ) + | ((flags & PYRRHIC_MASK_FLAGS ) << PYRRHIC_SHIFT_FLAGS ); +} + +PyrrhicMove* pyrrhic_add_move(PyrrhicMove *moves, bool promotes, bool enpass, unsigned from, unsigned to) { + + if (enpass) + *moves++ = pyrrhic_build_move(PYRRHIC_FLAG_ENPASS, from, to); + + else if (promotes) { + *moves++ = pyrrhic_build_move(PYRRHIC_FLAG_QPROMO, from, to); + *moves++ = pyrrhic_build_move(PYRRHIC_FLAG_RPROMO, from, to); + *moves++ = pyrrhic_build_move(PYRRHIC_FLAG_BPROMO, from, to); + *moves++ = pyrrhic_build_move(PYRRHIC_FLAG_NPROMO, from, to); + } + + else + *moves++ = pyrrhic_build_move(PYRRHIC_FLAG_NONE, from, to); + + return moves; +} + + +PyrrhicMove* pyrrhic_gen_captures(const PyrrhicPosition *pos, PyrrhicMove *moves) { + + uint64_t us = pos->turn ? pos->white : pos->black; + uint64_t them = pos->turn ? pos->black : pos->white; + uint64_t b, att; + + // Generate captures for the King + for (b = us & pos->kings; b; PYRRHIC_POPLSB(&b)) + for (att = PYRRHIC_KING_ATTACKS(PYRRHIC_LSB(b)) & them; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, false, false, PYRRHIC_LSB(b), PYRRHIC_LSB(att)); + + // Generate captures for the Rooks & Queens + for (b = us & (pos->rooks | pos->queens); b; PYRRHIC_POPLSB(&b)) + for (att = PYRRHIC_ROOK_ATTACKS(PYRRHIC_LSB(b), us | them) & them; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, false, false, PYRRHIC_LSB(b), PYRRHIC_LSB(att)); + + // Generate captures for the Bishops & Queens + for (b = us & (pos->bishops | pos->queens); b; PYRRHIC_POPLSB(&b)) + for (att = PYRRHIC_BISHOP_ATTACKS(PYRRHIC_LSB(b), us | them) & them; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, false, false, PYRRHIC_LSB(b), PYRRHIC_LSB(att)); + + // Generate captures for the Knights + for (b = us & pos->knights; b; PYRRHIC_POPLSB(&b)) + for (att = PYRRHIC_KNIGHT_ATTACKS(PYRRHIC_LSB(b)) & them; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, false, false, PYRRHIC_LSB(b), PYRRHIC_LSB(att)); + + // Generate captures for the Pawns + for (b = us & pos->pawns; b; PYRRHIC_POPLSB(&b)) { + + unsigned from = PYRRHIC_LSB(b); + + // Generate Enpassant Captures + if (pos->ep && pyrrhic_test_bit(PYRRHIC_PAWN_ATTACKS(from, pos->turn), pos->ep)) + moves = pyrrhic_add_move(moves, false, true, from, pos->ep); + + // Generate non-Enpassant Captures + for (att = PYRRHIC_PAWN_ATTACKS(from, pos->turn) & them; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, pyrrhic_promo_square(PYRRHIC_LSB(att)), false, from, PYRRHIC_LSB(att)); + } + + return moves; +} + +PyrrhicMove* pyrrhic_gen_moves(const PyrrhicPosition *pos, PyrrhicMove *moves) { + + const unsigned Forward = (pos->turn == PYRRHIC_WHITE ? 8 : -8); + + uint64_t us = pos->turn ? pos->white : pos->black; + uint64_t them = pos->turn ? pos->black : pos->white; + uint64_t b, att; + + // Generate moves for the King + for (b = us & pos->kings; b; PYRRHIC_POPLSB(&b)) + for (att = PYRRHIC_KING_ATTACKS(PYRRHIC_LSB(b)) & ~us; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, false, false, PYRRHIC_LSB(b), PYRRHIC_LSB(att)); + + // Generate moves for the Rooks + for (b = us & (pos->rooks | pos->queens); b; PYRRHIC_POPLSB(&b)) + for (att = PYRRHIC_ROOK_ATTACKS(PYRRHIC_LSB(b), us | them) & ~us; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, false, false, PYRRHIC_LSB(b), PYRRHIC_LSB(att)); + + // Generate moves for the Bishops + for (b = us & (pos->bishops | pos->queens); b; PYRRHIC_POPLSB(&b)) + for (att = PYRRHIC_BISHOP_ATTACKS(PYRRHIC_LSB(b), us | them) & ~us; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, false, false, PYRRHIC_LSB(b), PYRRHIC_LSB(att)); + + // Generate moves for the Knights + for (b = us & pos->knights; b; PYRRHIC_POPLSB(&b)) + for (att = PYRRHIC_KNIGHT_ATTACKS(PYRRHIC_LSB(b)) & ~us; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, false, false, PYRRHIC_LSB(b), PYRRHIC_LSB(att)); + + // Generate moves for the Pawns + for (b = us & pos->pawns; b; PYRRHIC_POPLSB(&b)) { + + unsigned from = PYRRHIC_LSB(b); + + // Generate Enpassant Captures + if (pos->ep && pyrrhic_test_bit(PYRRHIC_PAWN_ATTACKS(from, pos->turn), pos->ep)) + moves = pyrrhic_add_move(moves, false, true, from, pos->ep); + + // Generate any single pawn pushes + if (!pyrrhic_test_bit(us | them, from + Forward)) + moves = pyrrhic_add_move(moves, pyrrhic_promo_square(from + Forward), false, from, from + Forward); + + // Generate any double pawn pushes + if ( pyrrhic_pawn_start_square(pos->turn, from) + && !pyrrhic_test_bit(us | them, from + Forward) + && !pyrrhic_test_bit(us | them, from + 2 * Forward)) + moves = pyrrhic_add_move(moves, false, false, from, from + 2 * Forward); + + // Generate non-Enpassant Captures + for (att = PYRRHIC_PAWN_ATTACKS(from, pos->turn) & them; att; PYRRHIC_POPLSB(&att)) + moves = pyrrhic_add_move(moves, pyrrhic_promo_square(PYRRHIC_LSB(att)), false, from, PYRRHIC_LSB(att)); + } + + return moves; +} + +PyrrhicMove* pyrrhic_gen_legal(const PyrrhicPosition *pos, PyrrhicMove *moves) { + + PyrrhicMove _moves[TB_MAX_MOVES]; + PyrrhicMove *end = pyrrhic_gen_moves(pos, _moves); + PyrrhicMove *results = moves; + + for (PyrrhicMove *m = _moves; m < end; m++) + if (pyrrhic_legal_move(pos, *m)) + *results++ = *m; + return results; +} + + +bool pyrrhic_is_pawn_move(const PyrrhicPosition *pos, PyrrhicMove move) { + uint64_t us = pos->turn ? pos->white : pos->black; + return pyrrhic_test_bit(us & pos->pawns, pyrrhic_move_from(move)); +} + +bool pyrrhic_is_en_passant(const PyrrhicPosition *pos, PyrrhicMove move) { + return pyrrhic_is_pawn_move(pos, move) + && pyrrhic_move_to(move) == pos->ep && pos->ep; +} + +bool pyrrhic_is_capture(const PyrrhicPosition *pos, PyrrhicMove move) { + uint64_t them = pos->turn ? pos->black : pos->white; + return pyrrhic_test_bit(them, pyrrhic_move_to(move)) + || pyrrhic_is_en_passant(pos, move); +} + +bool pyrrhic_is_legal(const PyrrhicPosition *pos) { + + uint64_t us = pos->turn ? pos->black : pos->white; + uint64_t them = pos->turn ? pos->white : pos->black; + unsigned sq = PYRRHIC_LSB(pos->kings & us); + + return !(PYRRHIC_KING_ATTACKS(sq) & pos->kings & them) + && !(PYRRHIC_ROOK_ATTACKS(sq, us | them) & (pos->rooks | pos->queens) & them) + && !(PYRRHIC_BISHOP_ATTACKS(sq, us | them) & (pos->bishops | pos->queens) & them) + && !(PYRRHIC_KNIGHT_ATTACKS(sq) & pos->knights & them) + && !(PYRRHIC_PAWN_ATTACKS(sq, !pos->turn) & pos->pawns & them); +} + +bool pyrrhic_is_check(const PyrrhicPosition *pos) { + + uint64_t us = pos->turn ? pos->white : pos->black; + uint64_t them = pos->turn ? pos->black : pos->white; + unsigned sq = PYRRHIC_LSB(pos->kings & us); + + return (PYRRHIC_ROOK_ATTACKS(sq, us | them) & ((pos->rooks | pos->queens) & them)) + || (PYRRHIC_BISHOP_ATTACKS(sq, us | them) & ((pos->bishops | pos->queens) & them)) + || (PYRRHIC_KNIGHT_ATTACKS(sq) & (pos->knights & them)) + || (PYRRHIC_PAWN_ATTACKS(sq, pos->turn) & (pos->pawns & them)); +} + +bool pyrrhic_is_mate(const PyrrhicPosition *pos) { + + if (!pyrrhic_is_check(pos)) return 0; + + PyrrhicPosition pos1; + PyrrhicMove moves0[TB_MAX_MOVES]; + PyrrhicMove *moves = moves0; + PyrrhicMove *end = pyrrhic_gen_moves(pos, moves); + + for (; moves < end; moves++) + if (pyrrhic_do_move(&pos1, pos, *moves)) + return 0; + return 1; +} + + +bool pyrrhic_do_move(PyrrhicPosition *pos, const PyrrhicPosition *pos0, PyrrhicMove move) { + + unsigned from = pyrrhic_move_from(move); + unsigned to = pyrrhic_move_to(move); + unsigned promotes = pyrrhic_move_promotes(move); + + // Swap the turn and update every Bitboard as needed + pos->turn = !pos0->turn; + pos->white = pyrrhic_do_bb_move(pos0->white, from, to); + pos->black = pyrrhic_do_bb_move(pos0->black, from, to); + pos->kings = pyrrhic_do_bb_move(pos0->kings, from, to); + pos->queens = pyrrhic_do_bb_move(pos0->queens, from, to); + pos->rooks = pyrrhic_do_bb_move(pos0->rooks, from, to); + pos->bishops = pyrrhic_do_bb_move(pos0->bishops, from, to); + pos->knights = pyrrhic_do_bb_move(pos0->knights, from, to); + pos->pawns = pyrrhic_do_bb_move(pos0->pawns, from, to); + pos->ep = 0; + + // Promotions reset the Fifty-Move Rule and add a piece + if (promotes) { + + pyrrhic_disable_bit(&pos->pawns, to); + + switch (promotes) { + case PYRRHIC_FLAG_QPROMO: pyrrhic_enable_bit(&pos->queens , to); break; + case PYRRHIC_FLAG_RPROMO: pyrrhic_enable_bit(&pos->rooks , to); break; + case PYRRHIC_FLAG_BPROMO: pyrrhic_enable_bit(&pos->bishops, to); break; + case PYRRHIC_FLAG_NPROMO: pyrrhic_enable_bit(&pos->knights, to); break; + } + + pos->rule50 = 0; + } + + // Pawn moves can be Enpassant, or allow a future Enpassant + else if (pyrrhic_test_bit(pos0->pawns, from)) { + + pos->rule50 = 0; // Pawn move + + // Check for a double push by White + if ( (from ^ to) == 16 + && pos0->turn == PYRRHIC_WHITE + && (PYRRHIC_PAWN_ATTACKS(from + 8, PYRRHIC_WHITE) & pos0->pawns & pos0->black)) + pos->ep = from + 8; + + // Check for a double push by Black + if ( (from ^ to) == 16 + && pos0->turn == PYRRHIC_BLACK + && (PYRRHIC_PAWN_ATTACKS(from - 8, PYRRHIC_BLACK) & pos0->pawns & pos0->white)) + pos->ep = from - 8; + + // Check for an Enpassant being played + else if (to == pos0->ep) { + pyrrhic_disable_bit(&pos->white, pos0->turn ? to - 8: to + 8); + pyrrhic_disable_bit(&pos->black, pos0->turn ? to - 8: to + 8); + pyrrhic_disable_bit(&pos->pawns, pos0->turn ? to - 8: to + 8); + } + } + + // Any other sort of capture also resets the Fifty-Move Rule + else if (pyrrhic_test_bit(pos0->white | pos0->black, to)) + pos->rule50 = 0; + + // Otherwise, carry on as normal + else + pos->rule50 = pos0->rule50 + 1; + + // Provider the caller information about legality + return pyrrhic_is_legal(pos); +} + +bool pyrrhic_legal_move(const PyrrhicPosition *pos, PyrrhicMove move) { + PyrrhicPosition pos1; + return pyrrhic_do_move(&pos1, pos, move); +} + diff --git a/vendor/Pyrrhic/tbconfig.h b/vendor/Pyrrhic/tbconfig.h new file mode 100644 index 00000000..0b33084d --- /dev/null +++ b/vendor/Pyrrhic/tbconfig.h @@ -0,0 +1,58 @@ +/* + * (c) 2015 basil, all rights reserved, + * Modifications Copyright (c) 2016-2019 by Jon Dart + * Modifications Copyright (c) 2020-2026 by Andrew Grant + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "../../src/bb_attacks.hpp" +#include + +#define PYRRHIC_POPCOUNT(x) (std::popcount(x)) +#define PYRRHIC_LSB(x) (std::countr_zero(x)) +#define PYRRHIC_POPLSB(x) \ + ([](Clockwork::u64* y) { \ + const auto lsb = std::countr_zero(*y); \ + *y &= *y - 1; \ + return lsb; \ + }(x)) + +#define PYRRHIC_PAWN_ATTACKS(sq, c) \ + (Clockwork::pawn_attacks(Clockwork::Square{static_cast(sq)}, \ + static_cast(!c)) \ + .value()) +#define PYRRHIC_KNIGHT_ATTACKS(sq) \ + (Clockwork::knight_attacks(Clockwork::Square{static_cast(sq)}).value()) +#define PYRRHIC_BISHOP_ATTACKS(sq, occ) \ + (Clockwork::bishop_attacks(Clockwork::Square{static_cast(sq)}, \ + Clockwork::Bitboard{occ}) \ + .value()) +#define PYRRHIC_ROOK_ATTACKS(sq, occ) \ + (Clockwork::rook_attacks(Clockwork::Square{static_cast(sq)}, \ + Clockwork::Bitboard{occ}) \ + .value()) +#define PYRRHIC_QUEEN_ATTACKS(sq, occ) \ + (Clockwork::queen_attacks(Clockwork::Square{static_cast(sq)}, \ + Clockwork::Bitboard{occ}) \ + .value()) +#define PYRRHIC_KING_ATTACKS(sq) \ + (Clockwork::king_attacks(Clockwork::Square{static_cast(sq)}).value()) diff --git a/vendor/Pyrrhic/tbprobe.cpp b/vendor/Pyrrhic/tbprobe.cpp new file mode 100644 index 00000000..fda9106e --- /dev/null +++ b/vendor/Pyrrhic/tbprobe.cpp @@ -0,0 +1,2145 @@ +/* + * Copyright (c) 2013-2020 Ronald de Man + * Copyright (c) 2015 Basil, all rights reserved, + * Modifications Copyright (c) 2016-2019 by Jon Dart + * Modifications Copyright (c) 2020-2026 by Andrew Grant + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus + #include +#else + #include +#endif + +#include "tbprobe.h" + +#define TB_PIECES (7) +#define TB_HASHBITS (TB_PIECES < 7 ? 11 : 12) +#define TB_MAX_DTZ (0x40000) +#define TB_MAX_PIECE (TB_PIECES < 7 ? 254 : 650) +#define TB_MAX_PAWN (TB_PIECES < 7 ? 256 : 861) +#define TB_MAX_SYMS (4096) + +#define TB_BEST_NONE (0xFFFF) +#define TB_SCORE_ILLEGAL (0x7FFF) +#define TB_MOVE_STALEMATE (0xFFFF) +#define TB_MOVE_CHECKMATE (0xFFFE) + +#ifndef _WIN32 +#include +#include +#include +#include +#include +#define SEP_CHAR ':' +#define FD int +#define FD_ERR -1 +typedef size_t map_t; +#else +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#define SEP_CHAR ';' +#define FD HANDLE +#define FD_ERR INVALID_HANDLE_VALUE +typedef HANDLE map_t; +#endif + +#ifdef __cplusplus + using namespace std; +#endif + +#define DECOMP64 + +#if defined(__cplusplus) && (__cplusplus >= 201103L) + #include + #define LOCK_T std::mutex + #define LOCK_INIT(x) + #define LOCK_DESTROY(x) + #define LOCK(x) x.lock() + #define UNLOCK(x) x.unlock() +#else + #ifndef _WIN32 + #define LOCK_T pthread_mutex_t + #define LOCK_INIT(x) pthread_mutex_init(&(x), NULL) + #define LOCK_DESTROY(x) pthread_mutex_destroy(&(x)) + #define LOCK(x) pthread_mutex_lock(&(x)) + #define UNLOCK(x) pthread_mutex_unlock(&(x)) + #else + #define LOCK_T HANDLE + #define LOCK_INIT(x) do { x = CreateMutex(NULL, FALSE, NULL); } while (0) + #define LOCK_DESTROY(x) CloseHandle(x) + #define LOCK(x) WaitForSingleObject(x, INFINITE) + #define UNLOCK(x) ReleaseMutex(x) + #endif +#endif + +#define TB_MAX(a,b) ((a) > (b) ? (a) : (b)) +#define TB_MIN(a,b) ((a) < (b) ? (a) : (b)) + +#include "stdendian.h" + +#if _BYTE_ORDER == _BIG_ENDIAN +static uint32_t from_le_u32(uint32_t x) { return bswap32(x); } +static uint16_t from_le_u16(uint16_t x) { return bswap16(x); } +static uint64_t from_be_u64(uint64_t x) { return x; } +static uint32_t from_be_u32(uint32_t x) { return x; } +#else +static uint32_t from_le_u32(uint32_t x) { return x; } +static uint16_t from_le_u16(uint16_t x) { return x; } +static uint64_t from_be_u64(uint64_t x) { return bswap64(x); } +static uint32_t from_be_u32(uint32_t x) { return bswap32(x); } +#endif + +inline static uint32_t read_le_u32(void *p) { return from_le_u32(*(uint32_t *)p); } +inline static uint16_t read_le_u16(void *p) { return from_le_u16(*(uint16_t *)p); } + +static size_t file_size(FD fd) { +#ifdef _WIN32 + LARGE_INTEGER fileSize; + if (GetFileSizeEx(fd, &fileSize)==0) { + return 0; + } + return (size_t)fileSize.QuadPart; +#else + struct stat buf; + if (fstat(fd,&buf)) { + return 0; + } else { + return buf.st_size; + } +#endif +} + +static LOCK_T tbMutex; +static int initialized = 0; +static int tb_loaded = 0; +static int numPaths = 0; +static char *pathString = NULL; +static char **paths = NULL; + +static tb_loader_fn g_loader = NULL; + +void tb_set_loader(tb_loader_fn loader) { + g_loader = loader; +} + +static FD open_tb(const char *str, const char *suffix) +{ + int i; + FD fd; + char *file; + + for (i = 0; i < numPaths; i++) { + file = (char*)malloc(strlen(paths[i]) + strlen(str) + + strlen(suffix) + 2); + strcpy(file, paths[i]); +#ifdef _WIN32 + strcat(file,"\\"); +#else + strcat(file,"/"); +#endif + strcat(file, str); + strcat(file, suffix); +#ifndef _WIN32 + fd = open(file, O_RDONLY); +#else +#ifdef _UNICODE + wchar_t ucode_name[4096]; + size_t len; + mbstowcs_s(&len, ucode_name, 4096, file, _TRUNCATE); + fd = CreateFile(ucode_name, GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); +#else + fd = CreateFile(file, GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); +#endif +#endif + free(file); + if (fd != FD_ERR) { + return fd; + } + } + return FD_ERR; +} + +static void close_tb(FD fd) +{ +#ifndef _WIN32 + close(fd); +#else + CloseHandle(fd); +#endif +} + +static void *map_file(FD fd, map_t *mapping) +{ +#ifndef _WIN32 + struct stat statbuf; + if (fstat(fd, &statbuf)) { + perror("fstat"); + close_tb(fd); + return NULL; + } + *mapping = statbuf.st_size; + void *data = mmap(NULL, statbuf.st_size, PROT_READ, + MAP_SHARED, fd, 0); + + #if defined(MADV_RANDOM) + madvise(data, statbuf.st_size, MADV_RANDOM); + #endif + + if (data == MAP_FAILED) { + perror("mmap"); + return NULL; + } +#else + DWORD size_low, size_high; + size_low = GetFileSize(fd, &size_high); + HANDLE map = CreateFileMapping(fd, NULL, PAGE_READONLY, size_high, size_low, + NULL); + if (map == NULL) { + fprintf(stderr,"CreateFileMapping() failed, error = %lu.\n", GetLastError()); + return NULL; + } + *mapping = (map_t)map; + void *data = (void *)MapViewOfFile(map, FILE_MAP_READ, 0, 0, 0); + if (data == NULL) { + fprintf(stderr,"MapViewOfFile() failed, error = %lu.\n", GetLastError()); + } +#endif + return data; +} + +#ifndef _WIN32 +static void unmap_file(void *data, map_t size) +{ + // size == 0 is a sentinel for loader-supplied bytes; owned by the caller. + if (!data || size == 0) return; + if (munmap(data, size) < 0) { + perror("munmap"); + } +} +#else +static void unmap_file(void *data, map_t mapping) +{ + // mapping == NULL is a sentinel for loader-supplied bytes; owned by the caller. + if (!data || mapping == NULL) return; + if (!UnmapViewOfFile(data)) { + fprintf(stderr, "unmap failed, error code %lu\n", GetLastError()); + } + if (!CloseHandle((HANDLE)mapping)) { + fprintf(stderr, "CloseHandle failed, error code %lu\n", GetLastError()); + } +} +#endif + +int TB_MaxCardinality = 0, TB_MaxCardinalityDTM = 0; +int TB_LARGEST = 0; +int TB_NUM_WDL = 0; +int TB_NUM_DTM = 0; +int TB_NUM_DTZ = 0; + +static const char *tbSuffix[] = { ".rtbw", ".rtbm", ".rtbz" }; +static uint32_t tbMagic[] = { 0x5d23e871, 0x88ac504b, 0xa50c66d7 }; + +enum { WDL, DTM, DTZ }; +enum { PIECE_ENC, FILE_ENC, RANK_ENC }; + +// Attack and move generation code +#include "tbchess.c" + +struct PairsData { + uint8_t *indexTable; + uint16_t *sizeTable; + uint8_t *data; + uint16_t *offset; + uint8_t *symLen; + uint8_t *symPat; + uint8_t blockSize; + uint8_t idxBits; + uint8_t minLen; + uint8_t constValue[2]; + uint64_t base[1]; +}; + +struct EncInfo { + struct PairsData *precomp; + size_t factor[TB_PIECES]; + uint8_t pieces[TB_PIECES]; + uint8_t norm[TB_PIECES]; +}; + +struct BaseEntry { + uint64_t key; + uint8_t *data[3]; + map_t mapping[3]; +#ifdef __cplusplus + atomic ready[3]; +#else + atomic_bool ready[3]; +#endif + uint8_t num; + bool symmetric, hasPawns, hasDtm, hasDtz; + union { + bool kk_enc; + uint8_t pawns[2]; + }; + bool dtmLossOnly; +}; + +struct PieceEntry { + struct BaseEntry be; + struct EncInfo ei[5]; // 2 + 2 + 1 + uint16_t *dtmMap; + uint16_t dtmMapIdx[2][2]; + void *dtzMap; + uint16_t dtzMapIdx[4]; + uint8_t dtzFlags; +}; + +struct PawnEntry { + struct BaseEntry be; + struct EncInfo ei[24]; // 4 * 2 + 6 * 2 + 4 + uint16_t *dtmMap; + uint16_t dtmMapIdx[6][2][2]; + void *dtzMap; + uint16_t dtzMapIdx[4][4]; + uint8_t dtzFlags[4]; + bool dtmSwitched; +}; + +struct TbHashEntry { + uint64_t key; + struct BaseEntry *ptr; +}; + +static int tbNumPiece, tbNumPawn; +static int numWdl, numDtm, numDtz; + +static struct PieceEntry *pieceEntry; +static struct PawnEntry *pawnEntry; +static struct TbHashEntry tbHash[1 << TB_HASHBITS]; + +static void init_indices(void); + +// Forward declarations. These functions without the tb_ +// prefix take a pos structure as input. +static int probe_wdl(PyrrhicPosition *pos, int *success); +static int probe_dtz(PyrrhicPosition *pos, int *success); +int root_probe_wdl(const PyrrhicPosition *pos, bool useRule50, struct TbRootMoves *rm); +int root_probe_dtz(const PyrrhicPosition *pos, bool hasRepeated, struct TbRootMoves *rm); +static uint16_t probe_root(PyrrhicPosition *pos, int *score, unsigned *results); + +static unsigned dtz_to_wdl(int cnt50, int dtz) { + + int wdl = 0; + if (dtz > 0) wdl = dtz + cnt50 <= 100 ? 2: 1; + else if (dtz < 0) wdl = -dtz + cnt50 <= 100 ? -2: -1; + return wdl + 2; +} + +unsigned tb_probe_wdl( + uint64_t white, uint64_t black, + uint64_t kings, uint64_t queens, + uint64_t rooks, uint64_t bishops, + uint64_t knights, uint64_t pawns, + unsigned ep, bool turn) { + + PyrrhicPosition pos = { + white, black, kings, + queens, rooks, bishops, + knights, pawns, 0, + (uint8_t)ep, turn + }; + + int success; + int v = probe_wdl(&pos, &success); + if (success == 0) + return TB_RESULT_FAILED; + return (unsigned)(v + 2); +} + +unsigned tb_probe_root( + uint64_t white, uint64_t black, + uint64_t kings, uint64_t queens, + uint64_t rooks, uint64_t bishops, + uint64_t knights, uint64_t pawns, + unsigned rule50, unsigned ep, + bool turn, unsigned *results) { + + PyrrhicPosition pos = { + white, black, kings, + queens, rooks, bishops, + knights, pawns, (uint8_t)rule50, + (uint8_t)ep, turn + }; + + int dtz; + + PyrrhicMove move = probe_root(&pos, &dtz, results); + if (move == 0) return TB_RESULT_FAILED; + if (move == TB_MOVE_CHECKMATE) return TB_RESULT_CHECKMATE; + if (move == TB_MOVE_STALEMATE) return TB_RESULT_STALEMATE; + + unsigned res = 0; + res = TB_SET_WDL(res, dtz_to_wdl(rule50, dtz)); + res = TB_SET_DTZ(res, dtz < 0 ? -dtz : dtz); + res = TB_SET_FROM(res, pyrrhic_move_from(move)); + res = TB_SET_TO(res, pyrrhic_move_to(move)); + res = TB_SET_PROMOTES(res, pyrrhic_move_promotes(move)); + res = TB_SET_EP(res, pyrrhic_is_en_passant(&pos, move)); + return res; +} + +int tb_probe_root_dtz( + uint64_t white, uint64_t black, + uint64_t kings, uint64_t queens, + uint64_t rooks, uint64_t bishops, + uint64_t knights, uint64_t pawns, + unsigned rule50, unsigned ep, + bool turn, bool hasRepeated, + struct TbRootMoves *results) { + + PyrrhicPosition pos = { + white, black, kings, + queens, rooks, bishops, + knights, pawns, (uint8_t)rule50, + (uint8_t)ep, turn + }; + + return root_probe_dtz(&pos, hasRepeated, results); +} + +int tb_probe_root_wdl( + uint64_t white, uint64_t black, + uint64_t kings, uint64_t queens, + uint64_t rooks, uint64_t bishops, + uint64_t knights, uint64_t pawns, + unsigned rule50, unsigned ep, + bool turn, bool useRule50, + struct TbRootMoves *results) { + + PyrrhicPosition pos = { + white, black, kings, + queens, rooks, bishops, + knights, pawns, (uint8_t)rule50, + (uint8_t)ep, turn + }; + + return root_probe_wdl(&pos, useRule50, results); +} + +static void prt_str(const PyrrhicPosition *pos, char *str, int flip) { + + // Given a position, produce a string of the form KQPvKRP. + // Allow flip to be set to swap White v Black to Black v White + + int color = flip ? PYRRHIC_BLACK : PYRRHIC_WHITE; + + for (int pt = PYRRHIC_KING; pt >= PYRRHIC_PAWN; pt--) + for (int i = PYRRHIC_POPCOUNT(pyrrhic_pieces_by_type(pos, color, pt)); i > 0; i--) + *str++ = pyrrhic_piece_to_char[pt]; + + *str++ = 'v'; + + for (int pt = PYRRHIC_KING; pt >= PYRRHIC_PAWN; pt--) + for (int i = PYRRHIC_POPCOUNT(pyrrhic_pieces_by_type(pos, color^1, pt)); i > 0; i--) + *str++ = pyrrhic_piece_to_char[pt]; + *str++ = 0; +} + +static int test_tb(const char *str, const char *suffix) { + + FD fd = open_tb(str, suffix); + + if (fd != FD_ERR) { + + size_t size = file_size(fd); + close_tb(fd); + + if ((size & 63) != 16) { + fprintf(stderr, "Incomplete tablebase file %s.%s\n", str, suffix); + printf("info string Incomplete tablebase file %s.%s\n", str, suffix); + fd = FD_ERR; + } + } + + // On-disk miss: ask the loader, if registered. + if (fd == FD_ERR && g_loader) { + pyrrhic_tb_blob blob; + if (g_loader(str, suffix, &blob)) + return (blob.size & 63) == 16; + } + + return fd != FD_ERR; +} + +static void *map_tb(const char *name, const char *suffix, map_t *mapping) { + + FD fd = open_tb(name, suffix); + + if (fd == FD_ERR) { + + // On-disk miss: ask the loader, if registered. + if (g_loader) { + pyrrhic_tb_blob blob; + if (g_loader(name, suffix, &blob)) { + *mapping = (map_t)0; // sentinel: loader-owned, do not unmap + return (void *)blob.data; + } + } + + return NULL; + } + + void *data = map_file(fd, mapping); + if (data == NULL) { + fprintf(stderr, "Could not map %s%s into memory.\n", name, suffix); + exit(EXIT_FAILURE); + } + + close_tb(fd); + return data; +} + +static void add_to_hash(struct BaseEntry *ptr, uint64_t key) { + + int idx; + + idx = key >> (64 - TB_HASHBITS); + while (tbHash[idx].ptr) + idx = (idx + 1) & ((1 << TB_HASHBITS) - 1); + + tbHash[idx].key = key; + tbHash[idx].ptr = ptr; +} + +#define tb_pchr(i) pyrrhic_piece_to_char[PYRRHIC_QUEEN - (i)] +#define PYRRHIC_SWAP(a,b) {int tmp=a;a=b;b=tmp;} + +static void init_tb(char *str) +{ + if (!test_tb(str, tbSuffix[WDL])) + return; + + int pcs[16]; + for (int i = 0; i < 16; i++) + pcs[i] = 0; + int color = 0; + for (char *s = str; *s; s++) + if (*s == 'v') + color = 8; + else { + int piece_type = pyrrhic_char_to_piece_type(*s); + if (piece_type) { + assert((piece_type | color) < 16); + pcs[piece_type | color]++; + } + } + + uint64_t key = pyrrhic_calc_key_from_pcs(pcs, 0); + uint64_t key2 = pyrrhic_calc_key_from_pcs(pcs, 1); + + bool hasPawns = pcs[PYRRHIC_WPAWN] || pcs[PYRRHIC_BPAWN]; + + struct BaseEntry *be = hasPawns ? &pawnEntry[tbNumPawn++].be + : &pieceEntry[tbNumPiece++].be; + be->hasPawns = hasPawns; + be->key = key; + be->symmetric = key == key2; + be->num = 0; + for (int i = 0; i < 16; i++) + be->num += pcs[i]; + + numWdl++; + numDtm += be->hasDtm = test_tb(str, tbSuffix[DTM]); + numDtz += be->hasDtz = test_tb(str, tbSuffix[DTZ]); + + if (be->num > TB_MaxCardinality) { + TB_MaxCardinality = be->num; + } + if (be->hasDtm) + if (be->num > TB_MaxCardinalityDTM) { + TB_MaxCardinalityDTM = be->num; + } + + for (int type = 0; type < 3; type++) +#ifdef __cplusplus + be->ready[type] = false; +#else + atomic_init(&be->ready[type], false); +#endif + + if (!be->hasPawns) { + int j = 0; + for (int i = 0; i < 16; i++) + if (pcs[i] == 1) j++; + be->kk_enc = j == 2; + } else { + be->pawns[0] = pcs[PYRRHIC_WPAWN]; + be->pawns[1] = pcs[PYRRHIC_BPAWN]; + if (pcs[PYRRHIC_BPAWN] && (!pcs[PYRRHIC_WPAWN] || pcs[PYRRHIC_WPAWN] > pcs[PYRRHIC_BPAWN])) + PYRRHIC_SWAP(be->pawns[0], be->pawns[1]); + } + + add_to_hash(be, key); + if (key != key2) + add_to_hash(be, key2); +} + +#define PIECEENTRY(x) ((struct PieceEntry *)(x)) +#define PAWNENTRY(x) ((struct PawnEntry *)(x)) + +int num_tables(struct BaseEntry *be, const int type) +{ + return be->hasPawns ? type == DTM ? 6 : 4 : 1; +} + +struct EncInfo *first_ei(struct BaseEntry *be, const int type) +{ + return be->hasPawns + ? &PAWNENTRY(be)->ei[type == WDL ? 0 : type == DTM ? 8 : 20] + : &PIECEENTRY(be)->ei[type == WDL ? 0 : type == DTM ? 2 : 4]; +} + +static void free_tb_entry(struct BaseEntry *be) +{ + for (int type = 0; type < 3; type++) { + if (atomic_load_explicit(&be->ready[type], memory_order_relaxed)) { + unmap_file((void*)(be->data[type]), be->mapping[type]); + int num = num_tables(be, type); + struct EncInfo *ei = first_ei(be, type); + for (int t = 0; t < num; t++) { + free(ei[t].precomp); + if (type != DTZ) + free(ei[num + t].precomp); + } + atomic_store_explicit(&be->ready[type], false, memory_order_relaxed); + } + } +} + +static void tb_unload(void) +{ + if (!tb_loaded) return; + + free(pathString); + free(paths); + pathString = NULL; + paths = NULL; + numPaths = 0; + + for (int i = 0; i < tbNumPiece; i++) + free_tb_entry((struct BaseEntry *)&pieceEntry[i]); + for (int i = 0; i < tbNumPawn; i++) + free_tb_entry((struct BaseEntry *)&pawnEntry[i]); + + LOCK_DESTROY(tbMutex); + + numWdl = numDtm = numDtz = 0; + tb_loaded = 0; +} + +bool tb_init(const char *path) +{ + if (!initialized) { + init_indices(); + initialized = 1; + } + + tb_unload(); + + TB_LARGEST = 0; + TB_NUM_WDL = 0; + TB_NUM_DTZ = 0; + TB_NUM_DTM = 0; + + // "" is a path-less sentinel for "use the loader only". + // If neither a real path nor a loader is configured, there is nothing to load. + const char *p = path; + bool have_path = !(strlen(p) == 0 || !strcmp(p, "") || !strcmp(p, "")); + if (!have_path && !g_loader) return true; + + if (have_path) { + pathString = (char*)malloc(strlen(p) + 1); + strcpy(pathString, p); + numPaths = 0; + for (int i = 0;; i++) { + if (pathString[i] != SEP_CHAR) + numPaths++; + while (pathString[i] && pathString[i] != SEP_CHAR) + i++; + if (!pathString[i]) break; + pathString[i] = 0; + } + paths = (char**)malloc(numPaths * sizeof(*paths)); + for (int i = 0, j = 0; i < numPaths; i++) { + while (!pathString[j]) j++; + paths[i] = &pathString[j]; + while (pathString[j]) j++; + } + } + + LOCK_INIT(tbMutex); + tb_loaded = 1; + + tbNumPiece = tbNumPawn = 0; + TB_MaxCardinality = TB_MaxCardinalityDTM = 0; + + if (!pieceEntry) { + pieceEntry = (struct PieceEntry*)malloc(TB_MAX_PIECE * sizeof(*pieceEntry)); + pawnEntry = (struct PawnEntry*)malloc(TB_MAX_PAWN * sizeof(*pawnEntry)); + if (!pieceEntry || !pawnEntry) { + fprintf(stderr, "Out of memory.\n"); + exit(EXIT_FAILURE); + } + } + + for (int i = 0; i < (1 << TB_HASHBITS); i++) { + tbHash[i].key = 0; + tbHash[i].ptr = NULL; + } + + char str[16]; + int i, j, k, l, m; + + for (i = 0; i < 5; i++) { + snprintf(str, 16, "K%cvK", tb_pchr(i)); + init_tb(str); + } + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) { + snprintf(str, 16, "K%cvK%c", tb_pchr(i), tb_pchr(j)); + init_tb(str); + } + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) { + snprintf(str, 16, "K%c%cvK", tb_pchr(i), tb_pchr(j)); + init_tb(str); + } + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) + for (k = 0; k < 5; k++) { + snprintf(str, 16, "K%c%cvK%c", tb_pchr(i), tb_pchr(j), tb_pchr(k)); + init_tb(str); + } + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) + for (k = j; k < 5; k++) { + snprintf(str, 16, "K%c%c%cvK", tb_pchr(i), tb_pchr(j), tb_pchr(k)); + init_tb(str); + } + + // 6- and 7-piece TBs make sense only with a 64-bit address space + if (sizeof(size_t) < 8 || TB_PIECES < 6) + goto finished; + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) + for (k = i; k < 5; k++) + for (l = (i == k) ? j : k; l < 5; l++) { + snprintf(str, 16, "K%c%cvK%c%c", tb_pchr(i), tb_pchr(j), tb_pchr(k), tb_pchr(l)); + init_tb(str); + } + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) + for (k = j; k < 5; k++) + for (l = 0; l < 5; l++) { + snprintf(str, 16, "K%c%c%cvK%c", tb_pchr(i), tb_pchr(j), tb_pchr(k), tb_pchr(l)); + init_tb(str); + } + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) + for (k = j; k < 5; k++) + for (l = k; l < 5; l++) { + snprintf(str, 16, "K%c%c%c%cvK", tb_pchr(i), tb_pchr(j), tb_pchr(k), tb_pchr(l)); + init_tb(str); + } + + if (TB_PIECES < 7) + goto finished; + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) + for (k = j; k < 5; k++) + for (l = k; l < 5; l++) + for (m = l; m < 5; m++) { + snprintf(str, 16, "K%c%c%c%c%cvK", tb_pchr(i), tb_pchr(j), tb_pchr(k), tb_pchr(l), tb_pchr(m)); + init_tb(str); + } + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) + for (k = j; k < 5; k++) + for (l = k; l < 5; l++) + for (m = 0; m < 5; m++) { + snprintf(str, 16, "K%c%c%c%cvK%c", tb_pchr(i), tb_pchr(j), tb_pchr(k), tb_pchr(l), tb_pchr(m)); + init_tb(str); + } + + for (i = 0; i < 5; i++) + for (j = i; j < 5; j++) + for (k = j; k < 5; k++) + for (l = 0; l < 5; l++) + for (m = l; m < 5; m++) { + snprintf(str, 16, "K%c%c%cvK%c%c", tb_pchr(i), tb_pchr(j), tb_pchr(k), tb_pchr(l), tb_pchr(m)); + init_tb(str); + } + +finished: + + // Set TB_LARGEST, for backward compatibility with pre-7-man Fathom + TB_LARGEST = TB_MaxCardinality; + if (TB_MaxCardinalityDTM > TB_LARGEST) { + TB_LARGEST = TB_MaxCardinalityDTM; + } + TB_NUM_WDL = numWdl; + TB_NUM_DTZ = numDtz; + TB_NUM_DTM = numDtm; + + return true; +} + +void tb_free(void) +{ + tb_unload(); + free(pieceEntry); + free(pawnEntry); + pieceEntry = NULL; + pawnEntry = NULL; +} + +static const int8_t OffDiag[] = { + 0,-1,-1,-1,-1,-1,-1,-1, + 1, 0,-1,-1,-1,-1,-1,-1, + 1, 1, 0,-1,-1,-1,-1,-1, + 1, 1, 1, 0,-1,-1,-1,-1, + 1, 1, 1, 1, 0,-1,-1,-1, + 1, 1, 1, 1, 1, 0,-1,-1, + 1, 1, 1, 1, 1, 1, 0,-1, + 1, 1, 1, 1, 1, 1, 1, 0 +}; + +static const uint8_t Triangle[] = { + 6, 0, 1, 2, 2, 1, 0, 6, + 0, 7, 3, 4, 4, 3, 7, 0, + 1, 3, 8, 5, 5, 8, 3, 1, + 2, 4, 5, 9, 9, 5, 4, 2, + 2, 4, 5, 9, 9, 5, 4, 2, + 1, 3, 8, 5, 5, 8, 3, 1, + 0, 7, 3, 4, 4, 3, 7, 0, + 6, 0, 1, 2, 2, 1, 0, 6 +}; + +static const uint8_t FlipDiag[] = { + 0, 8, 16, 24, 32, 40, 48, 56, + 1, 9, 17, 25, 33, 41, 49, 57, + 2, 10, 18, 26, 34, 42, 50, 58, + 3, 11, 19, 27, 35, 43, 51, 59, + 4, 12, 20, 28, 36, 44, 52, 60, + 5, 13, 21, 29, 37, 45, 53, 61, + 6, 14, 22, 30, 38, 46, 54, 62, + 7, 15, 23, 31, 39, 47, 55, 63 +}; + +static const uint8_t Lower[] = { + 28, 0, 1, 2, 3, 4, 5, 6, + 0, 29, 7, 8, 9, 10, 11, 12, + 1, 7, 30, 13, 14, 15, 16, 17, + 2, 8, 13, 31, 18, 19, 20, 21, + 3, 9, 14, 18, 32, 22, 23, 24, + 4, 10, 15, 19, 22, 33, 25, 26, + 5, 11, 16, 20, 23, 25, 34, 27, + 6, 12, 17, 21, 24, 26, 27, 35 +}; + +static const uint8_t Diag[] = { + 0, 0, 0, 0, 0, 0, 0, 8, + 0, 1, 0, 0, 0, 0, 9, 0, + 0, 0, 2, 0, 0, 10, 0, 0, + 0, 0, 0, 3, 11, 0, 0, 0, + 0, 0, 0, 12, 4, 0, 0, 0, + 0, 0, 13, 0, 0, 5, 0, 0, + 0, 14, 0, 0, 0, 0, 6, 0, + 15, 0, 0, 0, 0, 0, 0, 7 +}; + +static const uint8_t Flap[2][64] = { + { 0, 0, 0, 0, 0, 0, 0, 0, + 0, 6, 12, 18, 18, 12, 6, 0, + 1, 7, 13, 19, 19, 13, 7, 1, + 2, 8, 14, 20, 20, 14, 8, 2, + 3, 9, 15, 21, 21, 15, 9, 3, + 4, 10, 16, 22, 22, 16, 10, 4, + 5, 11, 17, 23, 23, 17, 11, 5, + 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 3, 2, 1, 0, + 4, 5, 6, 7, 7, 6, 5, 4, + 8, 9, 10, 11, 11, 10, 9, 8, + 12, 13, 14, 15, 15, 14, 13, 12, + 16, 17, 18, 19, 19, 18, 17, 16, + 20, 21, 22, 23, 23, 22, 21, 20, + 0, 0, 0, 0, 0, 0, 0, 0 } +}; + +static const uint8_t PawnTwist[2][64] = { + { 0, 0, 0, 0, 0, 0, 0, 0, + 47, 35, 23, 11, 10, 22, 34, 46, + 45, 33, 21, 9, 8, 20, 32, 44, + 43, 31, 19, 7, 6, 18, 30, 42, + 41, 29, 17, 5, 4, 16, 28, 40, + 39, 27, 15, 3, 2, 14, 26, 38, + 37, 25, 13, 1, 0, 12, 24, 36, + 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, + 47, 45, 43, 41, 40, 42, 44, 46, + 39, 37, 35, 33, 32, 34, 36, 38, + 31, 29, 27, 25, 24, 26, 28, 30, + 23, 21, 19, 17, 16, 18, 20, 22, + 15, 13, 11, 9, 8, 10, 12, 14, + 7, 5, 3, 1, 0, 2, 4, 6, + 0, 0, 0, 0, 0, 0, 0, 0 } +}; + +static const int16_t KKIdx[10][64] = { + { -1, -1, -1, 0, 1, 2, 3, 4, + -1, -1, -1, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, + 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, 55, 56, 57 }, + { 58, -1, -1, -1, 59, 60, 61, 62, + 63, -1, -1, -1, 64, 65, 66, 67, + 68, 69, 70, 71, 72, 73, 74, 75, + 76, 77, 78, 79, 80, 81, 82, 83, + 84, 85, 86, 87, 88, 89, 90, 91, + 92, 93, 94, 95, 96, 97, 98, 99, + 100,101,102,103,104,105,106,107, + 108,109,110,111,112,113,114,115}, + {116,117, -1, -1, -1,118,119,120, + 121,122, -1, -1, -1,123,124,125, + 126,127,128,129,130,131,132,133, + 134,135,136,137,138,139,140,141, + 142,143,144,145,146,147,148,149, + 150,151,152,153,154,155,156,157, + 158,159,160,161,162,163,164,165, + 166,167,168,169,170,171,172,173 }, + {174, -1, -1, -1,175,176,177,178, + 179, -1, -1, -1,180,181,182,183, + 184, -1, -1, -1,185,186,187,188, + 189,190,191,192,193,194,195,196, + 197,198,199,200,201,202,203,204, + 205,206,207,208,209,210,211,212, + 213,214,215,216,217,218,219,220, + 221,222,223,224,225,226,227,228 }, + {229,230, -1, -1, -1,231,232,233, + 234,235, -1, -1, -1,236,237,238, + 239,240, -1, -1, -1,241,242,243, + 244,245,246,247,248,249,250,251, + 252,253,254,255,256,257,258,259, + 260,261,262,263,264,265,266,267, + 268,269,270,271,272,273,274,275, + 276,277,278,279,280,281,282,283 }, + {284,285,286,287,288,289,290,291, + 292,293, -1, -1, -1,294,295,296, + 297,298, -1, -1, -1,299,300,301, + 302,303, -1, -1, -1,304,305,306, + 307,308,309,310,311,312,313,314, + 315,316,317,318,319,320,321,322, + 323,324,325,326,327,328,329,330, + 331,332,333,334,335,336,337,338 }, + { -1, -1,339,340,341,342,343,344, + -1, -1,345,346,347,348,349,350, + -1, -1,441,351,352,353,354,355, + -1, -1, -1,442,356,357,358,359, + -1, -1, -1, -1,443,360,361,362, + -1, -1, -1, -1, -1,444,363,364, + -1, -1, -1, -1, -1, -1,445,365, + -1, -1, -1, -1, -1, -1, -1,446 }, + { -1, -1, -1,366,367,368,369,370, + -1, -1, -1,371,372,373,374,375, + -1, -1, -1,376,377,378,379,380, + -1, -1, -1,447,381,382,383,384, + -1, -1, -1, -1,448,385,386,387, + -1, -1, -1, -1, -1,449,388,389, + -1, -1, -1, -1, -1, -1,450,390, + -1, -1, -1, -1, -1, -1, -1,451 }, + {452,391,392,393,394,395,396,397, + -1, -1, -1, -1,398,399,400,401, + -1, -1, -1, -1,402,403,404,405, + -1, -1, -1, -1,406,407,408,409, + -1, -1, -1, -1,453,410,411,412, + -1, -1, -1, -1, -1,454,413,414, + -1, -1, -1, -1, -1, -1,455,415, + -1, -1, -1, -1, -1, -1, -1,456 }, + {457,416,417,418,419,420,421,422, + -1,458,423,424,425,426,427,428, + -1, -1, -1, -1, -1,429,430,431, + -1, -1, -1, -1, -1,432,433,434, + -1, -1, -1, -1, -1,435,436,437, + -1, -1, -1, -1, -1,459,438,439, + -1, -1, -1, -1, -1, -1,460,440, + -1, -1, -1, -1, -1, -1, -1,461 } +}; + +static const uint8_t FileToFile[] = { 0, 1, 2, 3, 3, 2, 1, 0 }; +static const int WdlToMap[5] = { 1, 3, 0, 2, 0 }; +static const uint8_t PAFlags[5] = { 8, 0, 0, 0, 4 }; + +static size_t Binomial[7][64]; +static size_t PawnIdx[2][6][24]; +static size_t PawnFactorFile[6][4]; +static size_t PawnFactorRank[6][6]; + +static void init_indices(void) +{ + int i, j, k; + + // Binomial[k][n] = Bin(n, k) + for (i = 0; i < 7; i++) + for (j = 0; j < 64; j++) { + size_t f = 1; + size_t l = 1; + for (k = 0; k < i; k++) { + f *= (j - k); + l *= (k + 1); + } + Binomial[i][j] = f / l; + } + + for (i = 0; i < 6; i++) { + size_t s = 0; + for (j = 0; j < 24; j++) { + PawnIdx[0][i][j] = s; + s += Binomial[i][PawnTwist[0][(1 + (j % 6)) * 8 + (j / 6)]]; + if ((j + 1) % 6 == 0) { + PawnFactorFile[i][j / 6] = s; + s = 0; + } + } + } + + for (i = 0; i < 6; i++) { + size_t s = 0; + for (j = 0; j < 24; j++) { + PawnIdx[1][i][j] = s; + s += Binomial[i][PawnTwist[1][(1 + (j / 4)) * 8 + (j % 4)]]; + if ((j + 1) % 4 == 0) { + PawnFactorRank[i][j / 4] = s; + s = 0; + } + } + } +} + +int leading_pawn(int *p, struct BaseEntry *be, const int enc) +{ + for (int i = 1; i < be->pawns[0]; i++) + if (Flap[enc-1][p[0]] > Flap[enc-1][p[i]]) + PYRRHIC_SWAP(p[0], p[i]); + + return enc == FILE_ENC ? FileToFile[p[0] & 7] : (p[0] - 8) >> 3; +} + +size_t encode(int *p, struct EncInfo *ei, struct BaseEntry *be, + const int enc) +{ + int n = be->num; + size_t idx; + int k; + + if (p[0] & 0x04) + for (int i = 0; i < n; i++) + p[i] ^= 0x07; + + if (enc == PIECE_ENC) { + if (p[0] & 0x20) + for (int i = 0; i < n; i++) + p[i] ^= 0x38; + + for (int i = 0; i < n; i++) + if (OffDiag[p[i]]) { + if (OffDiag[p[i]] > 0 && i < (be->kk_enc ? 2 : 3)) + for (int j = 0; j < n; j++) + p[j] = FlipDiag[p[j]]; + break; + } + + if (be->kk_enc) { + idx = KKIdx[Triangle[p[0]]][p[1]]; + k = 2; + } else { + int s1 = (p[1] > p[0]); + int s2 = (p[2] > p[0]) + (p[2] > p[1]); + + if (OffDiag[p[0]]) + idx = Triangle[p[0]] * 63*62 + (p[1] - s1) * 62 + (p[2] - s2); + else if (OffDiag[p[1]]) + idx = 6*63*62 + Diag[p[0]] * 28*62 + Lower[p[1]] * 62 + p[2] - s2; + else if (OffDiag[p[2]]) + idx = 6*63*62 + 4*28*62 + Diag[p[0]] * 7*28 + (Diag[p[1]] - s1) * 28 + Lower[p[2]]; + else + idx = 6*63*62 + 4*28*62 + 4*7*28 + Diag[p[0]] * 7*6 + (Diag[p[1]] - s1) * 6 + (Diag[p[2]] - s2); + k = 3; + } + idx *= ei->factor[0]; + } else { + for (int i = 1; i < be->pawns[0]; i++) + for (int j = i + 1; j < be->pawns[0]; j++) + if (PawnTwist[enc-1][p[i]] < PawnTwist[enc-1][p[j]]) + PYRRHIC_SWAP(p[i], p[j]); + + k = be->pawns[0]; + idx = PawnIdx[enc-1][k-1][Flap[enc-1][p[0]]]; + for (int i = 1; i < k; i++) + idx += Binomial[k-i][PawnTwist[enc-1][p[i]]]; + idx *= ei->factor[0]; + + // Pawns of other color + if (be->pawns[1]) { + int t = k + be->pawns[1]; + for (int i = k; i < t; i++) + for (int j = i + 1; j < t; j++) + if (p[i] > p[j]) PYRRHIC_SWAP(p[i], p[j]); + size_t s = 0; + for (int i = k; i < t; i++) { + int sq = p[i]; + int skips = 0; + for (int j = 0; j < k; j++) + skips += (sq > p[j]); + s += Binomial[i - k + 1][sq - skips - 8]; + } + idx += s * ei->factor[k]; + k = t; + } + } + + for (; k < n;) { + int t = k + ei->norm[k]; + for (int i = k; i < t; i++) + for (int j = i + 1; j < t; j++) + if (p[i] > p[j]) PYRRHIC_SWAP(p[i], p[j]); + size_t s = 0; + for (int i = k; i < t; i++) { + int sq = p[i]; + int skips = 0; + for (int j = 0; j < k; j++) + skips += (sq > p[j]); + s += Binomial[i - k + 1][sq - skips]; + } + idx += s * ei->factor[k]; + k = t; + } + + return idx; +} + +static size_t encode_piece(int *p, struct EncInfo *ei, struct BaseEntry *be) +{ + return encode(p, ei, be, PIECE_ENC); +} + +static size_t encode_pawn_f(int *p, struct EncInfo *ei, struct BaseEntry *be) +{ + return encode(p, ei, be, FILE_ENC); +} + +static size_t encode_pawn_r(int *p, struct EncInfo *ei, struct BaseEntry *be) +{ + return encode(p, ei, be, RANK_ENC); +} + +// Count number of placements of k like pieces on n squares +static size_t subfactor(size_t k, size_t n) +{ + size_t f = n; + size_t l = 1; + for (size_t i = 1; i < k; i++) { + f *= n - i; + l *= i + 1; + } + + return f / l; +} + +static size_t init_enc_info(struct EncInfo *ei, struct BaseEntry *be, + uint8_t *tb, int shift, int t, const int enc) +{ + bool morePawns = enc != PIECE_ENC && be->pawns[1] > 0; + + for (int i = 0; i < be->num; i++) { + ei->pieces[i] = (tb[i + 1 + morePawns] >> shift) & 0x0f; + ei->norm[i] = 0; + } + + int order = (tb[0] >> shift) & 0x0f; + int order2 = morePawns ? (tb[1] >> shift) & 0x0f : 0x0f; + + int k = ei->norm[0] = enc != PIECE_ENC ? be->pawns[0] + : be->kk_enc ? 2 : 3; + + if (morePawns) { + ei->norm[k] = be->pawns[1]; + k += ei->norm[k]; + } + + for (int i = k; i < be->num; i += ei->norm[i]) + for (int j = i; j < be->num && ei->pieces[j] == ei->pieces[i]; j++) + ei->norm[i]++; + + int n = 64 - k; + size_t f = 1; + + for (int i = 0; k < be->num || i == order || i == order2; i++) { + if (i == order) { + ei->factor[0] = f; + f *= enc == FILE_ENC ? PawnFactorFile[ei->norm[0] - 1][t] + : enc == RANK_ENC ? PawnFactorRank[ei->norm[0] - 1][t] + : be->kk_enc ? 462 : 31332; + } else if (i == order2) { + ei->factor[ei->norm[0]] = f; + f *= subfactor(ei->norm[ei->norm[0]], 48 - ei->norm[0]); + } else { + ei->factor[k] = f; + f *= subfactor(ei->norm[k], n); + n -= ei->norm[k]; + k += ei->norm[k]; + } + } + + return f; +} + +static void calc_symLen(struct PairsData *d, uint32_t s, char *tmp) +{ + uint8_t *w = d->symPat + 3 * s; + uint32_t s2 = (w[2] << 4) | (w[1] >> 4); + if (s2 == 0x0fff) + d->symLen[s] = 0; + else { + uint32_t s1 = ((w[1] & 0xf) << 8) | w[0]; + if (!tmp[s1]) calc_symLen(d, s1, tmp); + if (!tmp[s2]) calc_symLen(d, s2, tmp); + d->symLen[s] = d->symLen[s1] + d->symLen[s2] + 1; + } + tmp[s] = 1; +} + +static struct PairsData *setup_pairs(uint8_t **ptr, size_t tb_size, + size_t *size, uint8_t *flags, int type) +{ + struct PairsData *d; + uint8_t *data = *ptr; + + *flags = data[0]; + if (data[0] & 0x80) { + d = (struct PairsData*)malloc(sizeof(struct PairsData)); + d->idxBits = 0; + d->constValue[0] = type == WDL ? data[1] : 0; + d->constValue[1] = 0; + *ptr = data + 2; + size[0] = size[1] = size[2] = 0; + return d; + } + + uint8_t blockSize = data[1]; + uint8_t idxBits = data[2]; + uint32_t realNumBlocks = read_le_u32(data+4); + uint32_t numBlocks = realNumBlocks + data[3]; + int maxLen = data[8]; + int minLen = data[9]; + int h = maxLen - minLen + 1; + uint32_t numSyms = (uint32_t)read_le_u16(data + 10 + 2 * h); + d = (struct PairsData*)malloc(sizeof(struct PairsData) + h * sizeof(uint64_t) + numSyms); + d->blockSize = blockSize; + d->idxBits = idxBits; + d->offset = (uint16_t *)(&data[10]); + d->symLen = (uint8_t *)d + sizeof(struct PairsData) + h * sizeof(uint64_t); + d->symPat = &data[12 + 2 * h]; + d->minLen = minLen; + *ptr = &data[12 + 2 * h + 3 * numSyms + (numSyms & 1)]; + + size_t num_indices = (tb_size + (1ULL << idxBits) - 1) >> idxBits; + size[0] = 6ULL * num_indices; + size[1] = 2ULL * numBlocks; + size[2] = (size_t)realNumBlocks << blockSize; + + assert(numSyms < TB_MAX_SYMS); + char tmp[TB_MAX_SYMS]; + memset(tmp, 0, numSyms); + for (uint32_t s = 0; s < numSyms; s++) + if (!tmp[s]) + calc_symLen(d, s, tmp); + + d->base[h - 1] = 0; + for (int i = h - 2; i >= 0; i--) + d->base[i] = (d->base[i + 1] + read_le_u16((uint8_t *)(d->offset + i)) - read_le_u16((uint8_t *)(d->offset + i + 1))) / 2; +#ifdef DECOMP64 + for (int i = 0; i < h; i++) + d->base[i] <<= 64 - (minLen + i); +#else + for (int i = 0; i < h; i++) + d->base[i] <<= 32 - (minLen + i); +#endif + d->offset -= d->minLen; + + return d; +} + +static bool init_table(struct BaseEntry *be, const char *str, int type) +{ + uint8_t *data = (uint8_t*)map_tb(str, tbSuffix[type], &be->mapping[type]); + if (!data) return false; + + if (read_le_u32(data) != tbMagic[type]) { + fprintf(stderr, "Corrupted table.\n"); + unmap_file((void*)data, be->mapping[type]); + return false; + } + + be->data[type] = data; + + bool split = type != DTZ && (data[4] & 0x01); + if (type == DTM) + be->dtmLossOnly = data[4] & 0x04; + + data += 5; + + size_t tb_size[6][2]; + int num = num_tables(be, type); + struct EncInfo *ei = first_ei(be, type); + int enc = !be->hasPawns ? PIECE_ENC : type != DTM ? FILE_ENC : RANK_ENC; + + for (int t = 0; t < num; t++) { + tb_size[t][0] = init_enc_info(&ei[t], be, data, 0, t, enc); + if (split) + tb_size[t][1] = init_enc_info(&ei[num + t], be, data, 4, t, enc); + data += be->num + 1 + (be->hasPawns && be->pawns[1]); + } + data += (uintptr_t)data & 1; + + size_t size[6][2][3]; + for (int t = 0; t < num; t++) { + uint8_t flags; + ei[t].precomp = setup_pairs(&data, tb_size[t][0], size[t][0], &flags, type); + if (type == DTZ) { + if (!be->hasPawns) + PIECEENTRY(be)->dtzFlags = flags; + else + PAWNENTRY(be)->dtzFlags[t] = flags; + } + if (split) + ei[num + t].precomp = setup_pairs(&data, tb_size[t][1], size[t][1], &flags, type); + else if (type != DTZ) + ei[num + t].precomp = NULL; + } + + if (type == DTM && !be->dtmLossOnly) { + uint16_t *map = (uint16_t *)data; + *(be->hasPawns ? &PAWNENTRY(be)->dtmMap : &PIECEENTRY(be)->dtmMap) = map; + uint16_t (*mapIdx)[2][2] = be->hasPawns ? &PAWNENTRY(be)->dtmMapIdx[0] + : &PIECEENTRY(be)->dtmMapIdx; + for (int t = 0; t < num; t++) { + for (int i = 0; i < 2; i++) { + mapIdx[t][0][i] = (uint16_t)(data + 1 - (uint8_t*)map); + data += 2 + 2 * read_le_u16(data); + } + if (split) { + for (int i = 0; i < 2; i++) { + mapIdx[t][1][i] = (uint16_t)(data + 1 - (uint8_t*)map); + data += 2 + 2 * read_le_u16(data); + } + } + } + } + + if (type == DTZ) { + void *map = data; + *(be->hasPawns ? &PAWNENTRY(be)->dtzMap : &PIECEENTRY(be)->dtzMap) = map; + uint16_t (*mapIdx)[4] = be->hasPawns ? &PAWNENTRY(be)->dtzMapIdx[0] + : &PIECEENTRY(be)->dtzMapIdx; + uint8_t *flags = be->hasPawns ? &PAWNENTRY(be)->dtzFlags[0] + : &PIECEENTRY(be)->dtzFlags; + for (int t = 0; t < num; t++) { + if (flags[t] & 2) { + if (!(flags[t] & 16)) { + for (int i = 0; i < 4; i++) { + mapIdx[t][i] = (uint16_t)(data + 1 - (uint8_t *)map); + data += 1 + data[0]; + } + } else { + data += (uintptr_t)data & 0x01; + for (int i = 0; i < 4; i++) { + mapIdx[t][i] = (uint16_t)((uint16_t*)data + 1 - (uint16_t *)map); + data += 2 + 2 * read_le_u16(data); + } + } + } + } + data += (uintptr_t)data & 0x01; + } + + for (int t = 0; t < num; t++) { + ei[t].precomp->indexTable = data; + data += size[t][0][0]; + if (split) { + ei[num + t].precomp->indexTable = data; + data += size[t][1][0]; + } + } + + for (int t = 0; t < num; t++) { + ei[t].precomp->sizeTable = (uint16_t *)data; + data += size[t][0][1]; + if (split) { + ei[num + t].precomp->sizeTable = (uint16_t *)data; + data += size[t][1][1]; + } + } + + for (int t = 0; t < num; t++) { + data = (uint8_t *)(((uintptr_t)data + 0x3f) & ~0x3f); + ei[t].precomp->data = data; + data += size[t][0][2]; + if (split) { + data = (uint8_t *)(((uintptr_t)data + 0x3f) & ~0x3f); + ei[num + t].precomp->data = data; + data += size[t][1][2]; + } + } + + if (type == DTM && be->hasPawns) + PAWNENTRY(be)->dtmSwitched = + pyrrhic_calc_key_from_pieces(ei[0].pieces, be->num) != be->key; + + return true; +} + +static uint8_t *decompress_pairs(struct PairsData *d, size_t idx) +{ + if (!d->idxBits) + return d->constValue; + + uint32_t mainIdx = (uint32_t)(idx >> d->idxBits); + int litIdx = (idx & (((size_t)1 << d->idxBits) - 1)) - ((size_t)1 << (d->idxBits - 1)); + uint32_t block; + memcpy(&block, d->indexTable + 6 * mainIdx, sizeof(block)); + block = from_le_u32(block); + + uint16_t idxOffset = *(uint16_t *)(d->indexTable + 6 * mainIdx + 4); + litIdx += from_le_u16(idxOffset); + + if (litIdx < 0) + while (litIdx < 0) + litIdx += d->sizeTable[--block] + 1; + else + while (litIdx > d->sizeTable[block]) + litIdx -= d->sizeTable[block++] + 1; + + uint32_t *ptr = (uint32_t *)(d->data + ((size_t)block << d->blockSize)); + + int m = d->minLen; + uint16_t *offset = d->offset; + uint64_t *base = d->base - m; + uint8_t *symLen = d->symLen; + uint32_t sym, bitCnt; + +#ifdef DECOMP64 + uint64_t code = from_be_u64(*(uint64_t *)ptr); + + ptr += 2; + bitCnt = 0; // number of "empty bits" in code + for (;;) { + int l = m; + while (code < base[l]) l++; + sym = from_le_u16(offset[l]); + sym += (uint32_t)((code - base[l]) >> (64 - l)); + if (litIdx < (int)symLen[sym] + 1) break; + litIdx -= (int)symLen[sym] + 1; + code <<= l; + bitCnt += l; + if (bitCnt >= 32) { + bitCnt -= 32; + uint32_t tmp = from_be_u32(*ptr++); + code |= (uint64_t)tmp << bitCnt; + } + } +#else + uint32_t next = 0; + uint32_t data = *ptr++; + uint32_t code = from_be_u32(data); + bitCnt = 0; // number of bits in next + for (;;) { + int l = m; + while (code < base[l]) l++; + sym = offset[l] + ((code - base[l]) >> (32 - l)); + if (litIdx < (int)symLen[sym] + 1) break; + litIdx -= (int)symLen[sym] + 1; + code <<= l; + if (bitCnt < l) { + if (bitCnt) { + code |= (next >> (32 - l)); + l -= bitCnt; + } + data = *ptr++; + next = from_be_u32(data); + bitCnt = 32; + } + code |= (next >> (32 - l)); + next <<= l; + bitCnt -= l; + } +#endif + uint8_t *symPat = d->symPat; + while (symLen[sym] != 0) { + uint8_t *w = symPat + (3 * sym); + int s1 = ((w[1] & 0xf) << 8) | w[0]; + if (litIdx < (int)symLen[s1] + 1) + sym = s1; + else { + litIdx -= (int)symLen[s1] + 1; + sym = (w[2] << 4) | (w[1] >> 4); + } + } + + return &symPat[3 * sym]; +} + +// p[i] is to contain the square 0-63 (A1-H8) for a piece of type +// pc[i] ^ flip, where 1 = white pawn, ..., 14 = black king and pc ^ flip +// flips between white and black if flip == true. +// Pieces of the same type are guaranteed to be consecutive. +inline static int fill_squares(const PyrrhicPosition *pos, uint8_t *pc, bool flip, int mirror, int *p, + int i) +{ + int color = pyrrhic_colour_of_piece(pc[i]); + if (flip) color = !color; + uint64_t bb = pyrrhic_pieces_by_type(pos, color, pyrrhic_type_of_piece(pc[i])); + unsigned sq; + do { + sq = PYRRHIC_POPLSB(&bb); + p[i++] = sq ^ mirror; + } while (bb); + return i; +} + +int probe_table(const PyrrhicPosition *pos, int s, int *success, const int type) +{ + // Obtain the position's material-signature key + uint64_t key = pyrrhic_calc_key(pos,false); + + // Test for KvK + // Note: Cfish has key == 2ULL for KvK but we have 0 + if (type == WDL && key == 0ULL) + return 0; + + int hashIdx = key >> (64 - TB_HASHBITS); + while (tbHash[hashIdx].key && tbHash[hashIdx].key != key) + hashIdx = (hashIdx + 1) & ((1 << TB_HASHBITS) - 1); + if (!tbHash[hashIdx].ptr) { + *success = 0; + return 0; + } + + struct BaseEntry *be = tbHash[hashIdx].ptr; + if ((type == DTM && !be->hasDtm) || (type == DTZ && !be->hasDtz)) { + *success = 0; + return 0; + } + + // Use double-checked locking to reduce locking overhead + if (!atomic_load_explicit(&be->ready[type], memory_order_acquire)) { + LOCK(tbMutex); + if (!atomic_load_explicit(&be->ready[type], memory_order_relaxed)) { + char str[16]; + prt_str(pos, str, be->key != key); + if (!init_table(be, str, type)) { + tbHash[hashIdx].ptr = NULL; // mark as deleted + *success = 0; + UNLOCK(tbMutex); + return 0; + } + atomic_store_explicit(&be->ready[type], true, memory_order_release); + } + UNLOCK(tbMutex); + } + + bool bside, flip; + if (!be->symmetric) { + flip = key != be->key; + bside = (pos->turn == PYRRHIC_WHITE) == flip; + if (type == DTM && be->hasPawns && PAWNENTRY(be)->dtmSwitched) { + flip = !flip; + bside = !bside; + } + } else { + flip = pos->turn != PYRRHIC_WHITE; + bside = false; + } + + struct EncInfo *ei = first_ei(be, type); + int p[TB_PIECES]; + size_t idx; + int t = 0; + uint8_t flags = 0; // initialize to fix GCC warning + + if (!be->hasPawns) { + if (type == DTZ) { + flags = PIECEENTRY(be)->dtzFlags; + if ((flags & 1) != bside && !be->symmetric) { + *success = -1; + return 0; + } + } + ei = type != DTZ ? &ei[bside] : ei; + for (int i = 0; i < be->num;) + i = fill_squares(pos, ei->pieces, flip, 0, p, i); + idx = encode_piece(p, ei, be); + } else { + int i = fill_squares(pos, ei->pieces, flip, flip ? 0x38 : 0, p, 0); + t = leading_pawn(p, be, type != DTM ? FILE_ENC : RANK_ENC); + if (type == DTZ) { + flags = PAWNENTRY(be)->dtzFlags[t]; + if ((flags & 1) != bside && !be->symmetric) { + *success = -1; + return 0; + } + } + ei = type == WDL ? &ei[t + 4 * bside] + : type == DTM ? &ei[t + 6 * bside] : &ei[t]; + while (i < be->num) + i = fill_squares(pos, ei->pieces, flip, flip ? 0x38 : 0, p, i); + idx = type != DTM ? encode_pawn_f(p, ei, be) : encode_pawn_r(p, ei, be); + } + + uint8_t *w = decompress_pairs(ei->precomp, idx); + + if (type == WDL) + return (int)w[0] - 2; + + int v = w[0] + ((w[1] & 0x0f) << 8); + + if (type == DTM) { + if (!be->dtmLossOnly) + v = (int)from_le_u16(be->hasPawns + ? PAWNENTRY(be)->dtmMap[PAWNENTRY(be)->dtmMapIdx[t][bside][s] + v] + : PIECEENTRY(be)->dtmMap[PIECEENTRY(be)->dtmMapIdx[bside][s] + v]); + } else { + if (flags & 2) { + int m = WdlToMap[s + 2]; + if (!(flags & 16)) + v = be->hasPawns + ? ((uint8_t *)PAWNENTRY(be)->dtzMap)[PAWNENTRY(be)->dtzMapIdx[t][m] + v] + : ((uint8_t *)PIECEENTRY(be)->dtzMap)[PIECEENTRY(be)->dtzMapIdx[m] + v]; + else + v = (int)from_le_u16(be->hasPawns + ? ((uint16_t *)PAWNENTRY(be)->dtzMap)[PAWNENTRY(be)->dtzMapIdx[t][m] + v] + : ((uint16_t *)PIECEENTRY(be)->dtzMap)[PIECEENTRY(be)->dtzMapIdx[m] + v]); + } + if (!(flags & PAFlags[s + 2]) || (s & 1)) + v *= 2; + } + + return v; +} + +static int probe_wdl_table(const PyrrhicPosition *pos, int *success) +{ + return probe_table(pos, 0, success, WDL); +} + +static int probe_dtz_table(const PyrrhicPosition *pos, int wdl, int *success) +{ + return probe_table(pos, wdl, success, DTZ); +} + +// probe_ab() is not called for positions with en passant captures. +static int probe_ab(const PyrrhicPosition *pos, int alpha, int beta, int *success) +{ + assert(pos->ep == 0); + + PyrrhicMove moves0[TB_MAX_CAPTURES]; + PyrrhicMove *m = moves0; + // Generate (at least) all legal captures including (under)promotions. + // It is OK to generate more, as long as they are filtered out below. + PyrrhicMove *end = pyrrhic_gen_captures(pos, m); + for (; m < end; m++) { + PyrrhicPosition pos1; + PyrrhicMove move = *m; + if (!pyrrhic_is_capture(pos, move)) + continue; + if (!pyrrhic_do_move(&pos1, pos, move)) + continue; // illegal move + int v = -probe_ab(&pos1, -beta, -alpha, success); + if (*success == 0) return 0; + if (v > alpha) { + if (v >= beta) + return v; + alpha = v; + } + } + + int v = probe_wdl_table(pos, success); + + return alpha >= v ? alpha : v; +} + +// Probe the WDL table for a particular position. +// +// If *success != 0, the probe was successful. +// +// If *success == 2, the position has a winning capture, or the position +// is a cursed win and has a cursed winning capture, or the position +// has an ep capture as only best move. +// This is used in probe_dtz(). +// +// The return value is from the point of view of the side to move: +// -2 : loss +// -1 : loss, but draw under 50-move rule +// 0 : draw +// 1 : win, but draw under 50-move rule +// 2 : win +int probe_wdl(PyrrhicPosition *pos, int *success) +{ + *success = 1; + + // Generate (at least) all legal captures including (under)promotions. + PyrrhicMove moves0[TB_MAX_CAPTURES]; + PyrrhicMove *m = moves0; + PyrrhicMove *end = pyrrhic_gen_captures(pos, m); + int bestCap = -3, bestEp = -3; + + // We do capture resolution, letting bestCap keep track of the best + // capture without ep rights and letting bestEp keep track of still + // better ep captures if they exist. + + for (; m < end; m++) { + PyrrhicPosition pos1; + PyrrhicMove move = *m; + if (!pyrrhic_is_capture(pos, move)) + continue; + if (!pyrrhic_do_move(&pos1, pos, move)) + continue; // illegal move + int v = -probe_ab(&pos1, -2, -bestCap, success); + if (*success == 0) return 0; + if (v > bestCap) { + if (v == 2) { + *success = 2; + return 2; + } + if (!pyrrhic_is_en_passant(pos,move)) + bestCap = v; + else if (v > bestEp) + bestEp = v; + } + } + + int v = probe_wdl_table(pos, success); + if (*success == 0) return 0; + + // Now max(v, bestCap) is the WDL value of the position without ep rights. + // If the position without ep rights is not stalemate or no ep captures + // exist, then the value of the position is max(v, bestCap, bestEp). + // If the position without ep rights is stalemate and bestEp > -3, + // then the value of the position is bestEp (and we will have v == 0). + + if (bestEp > bestCap) { + if (bestEp > v) { // ep capture (possibly cursed losing) is best. + *success = 2; + return bestEp; + } + bestCap = bestEp; + } + + // Now max(v, bestCap) is the WDL value of the position unless + // the position without ep rights is stalemate and bestEp > -3. + + if (bestCap >= v) { + // No need to test for the stalemate case here: either there are + // non-ep captures, or bestCap == bestEp >= v anyway. + *success = 1 + (bestCap > 0); + return bestCap; + } + + // Now handle the stalemate case. + if (bestEp > -3 && v == 0) { + PyrrhicMove moves[TB_MAX_MOVES]; + PyrrhicMove *end2 = pyrrhic_gen_moves(pos, moves); + // Check for stalemate in the position with ep captures. + for (m = moves; m < end2; m++) { + if (!pyrrhic_is_en_passant(pos,*m) && pyrrhic_legal_move(pos, *m)) break; + } + if (m == end2 && !pyrrhic_is_check(pos)) { + // stalemate score from tb (w/o e.p.), but an en-passant capture + // is possible. + *success = 2; + return bestEp; + } + } + // Stalemate / en passant not an issue, so v is the correct value. + + return v; +} + +static int WdlToDtz[] = { -1, -101, 0, 101, 1 }; + +// Probe the DTZ table for a particular position. +// If *success != 0, the probe was successful. +// The return value is from the point of view of the side to move: +// n < -100 : loss, but draw under 50-move rule +// -100 <= n < -1 : loss in n ply (assuming 50-move counter == 0) +// 0 : draw +// 1 < n <= 100 : win in n ply (assuming 50-move counter == 0) +// 100 < n : win, but draw under 50-move rule +// +// If the position mate, -1 is returned instead of 0. +// +// The return value n can be off by 1: a return value -n can mean a loss +// in n+1 ply and a return value +n can mean a win in n+1 ply. This +// cannot happen for tables with positions exactly on the "edge" of +// the 50-move rule. +// +// This means that if dtz > 0 is returned, the position is certainly +// a win if dtz + 50-move-counter <= 99. Care must be taken that the engine +// picks moves that preserve dtz + 50-move-counter <= 99. +// +// If n = 100 immediately after a capture or pawn move, then the position +// is also certainly a win, and during the whole phase until the next +// capture or pawn move, the inequality to be preserved is +// dtz + 50-movecounter <= 100. +// +// In short, if a move is available resulting in dtz + 50-move-counter <= 99, +// then do not accept moves leading to dtz + 50-move-counter == 100. +// +int probe_dtz(PyrrhicPosition *pos, int *success) +{ + int wdl = probe_wdl(pos, success); + if (*success == 0) return 0; + + // If draw, then dtz = 0. + if (wdl == 0) return 0; + + // Check for winning capture or en passant capture as only best move. + if (*success == 2) + return WdlToDtz[wdl + 2]; + + PyrrhicMove moves[TB_MAX_MOVES]; + PyrrhicMove *m = moves, *end = NULL; + PyrrhicPosition pos1; + + // If winning, check for a winning pawn move. + if (wdl > 0) { + // Generate at least all legal non-capturing pawn moves + // including non-capturing promotions. + // (The following call in fact generates all moves.) + end = pyrrhic_gen_legal(pos, moves); + + for (m = moves; m < end; m++) { + PyrrhicMove move = *m; + if (!pyrrhic_is_pawn_move(pos, move) || pyrrhic_is_capture(pos, move)) + continue; + if (!pyrrhic_do_move(&pos1, pos, move)) + continue; // not legal + int v = -probe_wdl(&pos1, success); + if (*success == 0) return 0; + if (v == wdl) { + assert(wdl < 3); + return WdlToDtz[wdl + 2]; + } + } + } + + // If we are here, we know that the best move is not an ep capture. + // In other words, the value of wdl corresponds to the WDL value of + // the position without ep rights. It is therefore safe to probe the + // DTZ table with the current value of wdl. + + int dtz = probe_dtz_table(pos, wdl, success); + if (*success >= 0) + return WdlToDtz[wdl + 2] + ((wdl > 0) ? dtz : -dtz); + + // *success < 0 means we need to probe DTZ for the other side to move. + int best; + if (wdl > 0) { + best = INT32_MAX; + } else { + // If (cursed) loss, the worst case is a losing capture or pawn move + // as the "best" move, leading to dtz of -1 or -101. + // In case of mate, this will cause -1 to be returned. + best = WdlToDtz[wdl + 2]; + // If wdl < 0, we still have to generate all moves. + end = pyrrhic_gen_moves(pos, m); + } + assert(end != NULL); + + for (m = moves; m < end; m++) { + PyrrhicMove move = *m; + // We can skip pawn moves and captures. + // If wdl > 0, we already caught them. If wdl < 0, the initial value + // of best already takes account of them. + if (pyrrhic_is_capture(pos, move) || pyrrhic_is_pawn_move(pos, move)) + continue; + if (!pyrrhic_do_move(&pos1, pos, move)) { + // move was not legal + continue; + } + int v = -probe_dtz(&pos1, success); + // Check for the case of mate in 1 + if (v == 1 && pyrrhic_is_mate(&pos1)) + best = 1; + else if (wdl > 0) { + if (v > 0 && v + 1 < best) + best = v + 1; + } else { + if (v - 1 < best) + best = v - 1; + } + if (*success == 0) return 0; + } + return best; +} + +// Use the DTZ tables to rank and score all root moves in the list. +// A return value of 0 means that not all probes were successful. +int root_probe_dtz(const PyrrhicPosition *pos, bool hasRepeated, struct TbRootMoves *rm) +{ + int v, success; + + // Obtain 50-move counter for the root position. + int cnt50 = pos->rule50; + + // Probe, rank and score each move. + PyrrhicMove rootMoves[TB_MAX_MOVES]; + PyrrhicMove * end = pyrrhic_gen_legal(pos,rootMoves); + rm->size = (unsigned)(end-rootMoves); + PyrrhicPosition pos1; + for (unsigned i = 0; i < rm->size; i++) { + struct TbRootMove *m = &(rm->moves[i]); + m->move = rootMoves[i]; + pyrrhic_do_move(&pos1, pos, m->move); + + // Calculate dtz for the current move counting from the root position. + if (pos1.rule50 == 0) { + // If the move resets the 50-move counter, dtz is -101/-1/0/1/101. + v = -probe_wdl(&pos1, &success); + assert(v < 3); + v = WdlToDtz[v + 2]; + } else { + // Otherwise, take dtz for the new position and correct by 1 ply. + v = -probe_dtz(&pos1, &success); + if (v > 0) v++; + else if (v < 0) v--; + } + // Make sure that a mating move gets value 1. + if (v == 2 && pyrrhic_is_mate(&pos1)) { + v = 1; + } + + if (!success) return 0; + + // Better moves are ranked higher. Guaranteed wins are ranked equally. + // Losing moves are ranked equally unless a 50-move draw is in sight. + // Note that moves ranked 900 have dtz + cnt50 == 100, which in rare + // cases may be insufficient to win as dtz may be one off (see the + // comments before TB_probe_dtz()). + int r = v > 0 ? (v + cnt50 <= 99 && !hasRepeated ? TB_MAX_DTZ : TB_MAX_DTZ - (v + cnt50)) + : v < 0 ? (-v * 2 + cnt50 < 100 ? -TB_MAX_DTZ : -TB_MAX_DTZ + (-v + cnt50)) + : 0; + m->tbRank = r; + } + return 1; +} + +// Use the WDL tables to rank all root moves in the list. +// This is a fallback for the case that some or all DTZ tables are missing. +// A return value of 0 means that not all probes were successful. +int root_probe_wdl(const PyrrhicPosition *pos, bool useRule50, struct TbRootMoves *rm) +{ + static int WdlToRank[] = { -TB_MAX_DTZ, -TB_MAX_DTZ + 101, 0, TB_MAX_DTZ - 101, TB_MAX_DTZ }; + + int v, success; + + // Probe, rank and score each move. + PyrrhicMove moves[TB_MAX_MOVES]; + PyrrhicMove *end = pyrrhic_gen_legal(pos,moves); + rm->size = (unsigned)(end-moves); + PyrrhicPosition pos1; + for (unsigned i = 0; i < rm->size; i++) { + struct TbRootMove *m = &rm->moves[i]; + m->move = moves[i]; + pyrrhic_do_move(&pos1, pos, m->move); + v = -probe_wdl(&pos1, &success); + if (!success) return 0; + if (!useRule50) + v = v > 0 ? 2 : v < 0 ? -2 : 0; + m->tbRank = WdlToRank[v + 2]; + } + + return 1; +} + + +static const int wdl_to_dtz[] = +{ + -1, -101, 0, 101, 1 +}; + +// This supports the original Fathom root probe API +static uint16_t probe_root(PyrrhicPosition *pos, int *score, unsigned *results) +{ + int success; + int dtz = probe_dtz(pos, &success); + if (!success) + return 0; + + int16_t scores[TB_MAX_MOVES]; + uint16_t moves0[TB_MAX_MOVES]; + uint16_t *moves = moves0; + uint16_t *end = pyrrhic_gen_moves(pos, moves); + size_t len = end - moves; + size_t num_draw = 0; + unsigned j = 0; + for (unsigned i = 0; i < len; i++) + { + PyrrhicPosition pos1; + if (!pyrrhic_do_move(&pos1, pos, moves[i])) + { + scores[i] = TB_SCORE_ILLEGAL; + continue; + } + int v = 0; + if (dtz > 0 && pyrrhic_is_mate(&pos1)) + v = 1; + else + { + if (pos1.rule50 != 0) + { + v = -probe_dtz(&pos1, &success); + if (v > 0) + v++; + else if (v < 0) + v--; + } + else + { + v = -probe_wdl(&pos1, &success); + v = wdl_to_dtz[v + 2]; + } + } + num_draw += (v == 0); + if (!success) + return 0; + scores[i] = v; + if (results != NULL) + { + unsigned res = 0; + res = TB_SET_WDL(res, dtz_to_wdl(pos->rule50, v)); + res = TB_SET_FROM(res, pyrrhic_move_from(moves[i])); + res = TB_SET_TO(res, pyrrhic_move_to(moves[i])); + res = TB_SET_PROMOTES(res, pyrrhic_move_promotes(moves[i])); + res = TB_SET_EP(res, pyrrhic_is_en_passant(pos, moves[i])); + res = TB_SET_DTZ(res, (v < 0? -v: v)); + results[j++] = res; + } + } + if (results != NULL) + results[j++] = TB_RESULT_FAILED; + if (score != NULL) + *score = dtz; + + // Now be a bit smart about filtering out moves. + if (dtz > 0) // winning (or 50-move rule draw) + { + int best = TB_BEST_NONE; + uint16_t best_move = 0; + for (unsigned i = 0; i < len; i++) + { + int v = scores[i]; + if (v == TB_SCORE_ILLEGAL) + continue; + if (v > 0 && v < best) + { + best = v; + best_move = moves[i]; + } + } + return (best == TB_BEST_NONE ? 0 : best_move); + } + else if (dtz < 0) // losing (or 50-move rule draw) + { + int best = 0; + uint16_t best_move = 0; + for (unsigned i = 0; i < len; i++) + { + int v = scores[i]; + if (v == TB_SCORE_ILLEGAL) + continue; + if (v < best) + { + best = v; + best_move = moves[i]; + } + } + return (best == 0? TB_MOVE_CHECKMATE: best_move); + } + else // drawing + { + // Check for stalemate: + if (num_draw == 0) + return TB_MOVE_STALEMATE; + + // Select a "random" move that preserves the draw. + // Uses calc_key as the PRNG. + size_t count = pyrrhic_calc_key(pos, !pos->turn) % num_draw; + for (unsigned i = 0; i < len; i++) + { + int v = scores[i]; + if (v == TB_SCORE_ILLEGAL) + continue; + if (v == 0) + { + if (count == 0) + return moves[i]; + count--; + } + } + return 0; + } +} + diff --git a/vendor/Pyrrhic/tbprobe.h b/vendor/Pyrrhic/tbprobe.h new file mode 100644 index 00000000..e4ad267f --- /dev/null +++ b/vendor/Pyrrhic/tbprobe.h @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2013-2020 Ronald de Man + * Copyright (c) 2015 Basil, all rights reserved, + * Modifications Copyright (c) 2016-2019 by Jon Dart + * Modifications Copyright (c) 2020-2026 by Andrew Grant + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef TBPROBE_H +#define TBPROBE_H + +#include +#include +#include + +#include "tbconfig.h" + +/// Definitions for PyrrhicMoves + +#define PYRRHIC_FLAG_NONE 0x0 +#define PYRRHIC_FLAG_QPROMO 0x1 +#define PYRRHIC_FLAG_RPROMO 0x2 +#define PYRRHIC_FLAG_BPROMO 0x3 +#define PYRRHIC_FLAG_NPROMO 0x4 +#define PYRRHIC_FLAG_ENPASS 0x8 + +#define PYRRHIC_SHIFT_TO 0 +#define PYRRHIC_SHIFT_FROM 6 +#define PYRRHIC_SHIFT_FLAGS 12 + +#define PYRRHIC_MASK_TO 0x3F +#define PYRRHIC_MASK_FROM 0x3F +#define PYRRHIC_MASK_FLAGS 0x0F +#define PYRRHIC_MASK_PROMO_FLAGS 0x07 + +#define PYRRHIC_MOVE_FLAGS(x) (((x) >> PYRRHIC_SHIFT_FLAGS) & PYRRHIC_MASK_FLAGS) + +/****************************************************************************/ +/* MAIN API */ +/****************************************************************************/ + +#define TB_MAX_CAPTURES 64 +#define TB_MAX_PLY 256 + +#define TB_RESULT_WDL_MASK 0x0000000F +#define TB_RESULT_TO_MASK 0x000003F0 +#define TB_RESULT_FROM_MASK 0x0000FC00 +#define TB_RESULT_PROMOTES_MASK 0x00070000 +#define TB_RESULT_EP_MASK 0x00080000 +#define TB_RESULT_DTZ_MASK 0xFFF00000 +#define TB_RESULT_WDL_SHIFT 0 +#define TB_RESULT_TO_SHIFT 4 +#define TB_RESULT_FROM_SHIFT 10 +#define TB_RESULT_PROMOTES_SHIFT 16 +#define TB_RESULT_EP_SHIFT 19 +#define TB_RESULT_DTZ_SHIFT 20 + +#define TB_GET_WDL(_res) \ + (((_res) & TB_RESULT_WDL_MASK) >> TB_RESULT_WDL_SHIFT) +#define TB_GET_TO(_res) \ + (((_res) & TB_RESULT_TO_MASK) >> TB_RESULT_TO_SHIFT) +#define TB_GET_FROM(_res) \ + (((_res) & TB_RESULT_FROM_MASK) >> TB_RESULT_FROM_SHIFT) +#define TB_GET_PROMOTES(_res) \ + (((_res) & TB_RESULT_PROMOTES_MASK) >> TB_RESULT_PROMOTES_SHIFT) +#define TB_GET_EP(_res) \ + (((_res) & TB_RESULT_EP_MASK) >> TB_RESULT_EP_SHIFT) +#define TB_GET_DTZ(_res) \ + (((_res) & TB_RESULT_DTZ_MASK) >> TB_RESULT_DTZ_SHIFT) + +#define TB_SET_WDL(_res, _wdl) \ + (((_res) & ~TB_RESULT_WDL_MASK) | \ + (((_wdl) << TB_RESULT_WDL_SHIFT) & TB_RESULT_WDL_MASK)) +#define TB_SET_TO(_res, _to) \ + (((_res) & ~TB_RESULT_TO_MASK) | \ + (((_to) << TB_RESULT_TO_SHIFT) & TB_RESULT_TO_MASK)) +#define TB_SET_FROM(_res, _from) \ + (((_res) & ~TB_RESULT_FROM_MASK) | \ + (((_from) << TB_RESULT_FROM_SHIFT) & TB_RESULT_FROM_MASK)) +#define TB_SET_PROMOTES(_res, _promotes) \ + (((_res) & ~TB_RESULT_PROMOTES_MASK) | \ + (((_promotes) << TB_RESULT_PROMOTES_SHIFT) & TB_RESULT_PROMOTES_MASK)) +#define TB_SET_EP(_res, _ep) \ + (((_res) & ~TB_RESULT_EP_MASK) | \ + (((_ep) << TB_RESULT_EP_SHIFT) & TB_RESULT_EP_MASK)) +#define TB_SET_DTZ(_res, _dtz) \ + (((_res) & ~TB_RESULT_DTZ_MASK) | \ + (((_dtz) << TB_RESULT_DTZ_SHIFT) & TB_RESULT_DTZ_MASK)) + + +typedef uint16_t PyrrhicMove; + +#include "api.h" + +/* + * The tablebase can be probed for any position where #pieces <= TB_LARGEST. + */ +extern int TB_LARGEST; +extern int TB_NUM_WDL; +extern int TB_NUM_DTM; +extern int TB_NUM_DTZ; + +#endif