From 60ad601057c0eba5125c79134ece132b7ba52761 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 14:14:18 -0500 Subject: [PATCH] Add runtime-adjustable search and evaluation parameters --- CMakeLists.txt | 2 + include/ironphoenix/eval.hpp | 7 +- include/ironphoenix/lmr.hpp | 5 +- include/ironphoenix/tuning.hpp | 57 +++++++ src/eval.cpp | 40 +++-- src/lmr.cpp | 22 ++- src/search.cpp | 26 ++-- src/tuning.cpp | 262 +++++++++++++++++++++++++++++++++ src/uci.cpp | 13 +- 9 files changed, 397 insertions(+), 37 deletions(-) create mode 100644 include/ironphoenix/tuning.hpp create mode 100644 src/tuning.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4285f73..d6961a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ add_library(ironphoenix_core STATIC src/tt.cpp src/eval.cpp src/nnue.cpp + src/tuning.cpp src/search.cpp src/multipv.cpp src/fen.cpp @@ -33,6 +34,7 @@ add_library(ironphoenix_core STATIC include/ironphoenix/eval.hpp include/ironphoenix/lmr.hpp include/ironphoenix/nnue.hpp + include/ironphoenix/tuning.hpp ) add_library(IronPhoenix::Core ALIAS ironphoenix_core) diff --git a/include/ironphoenix/eval.hpp b/include/ironphoenix/eval.hpp index 9c036bd..6cdbc56 100644 --- a/include/ironphoenix/eval.hpp +++ b/include/ironphoenix/eval.hpp @@ -1,15 +1,16 @@ #pragma once +#include "tuning.hpp" + namespace ironphoenix { class Position; namespace Eval { - struct Parameters final { - - }; + using Parameters = Tuning::EvalParameters; + [[nodiscard]] Parameters& parameters() noexcept; [[nodiscard]] int evaluate(const Position& pos) noexcept; } diff --git a/include/ironphoenix/lmr.hpp b/include/ironphoenix/lmr.hpp index 3f24e74..2026968 100644 --- a/include/ironphoenix/lmr.hpp +++ b/include/ironphoenix/lmr.hpp @@ -12,7 +12,10 @@ namespace ironphoenix { std::array, LMR_MAX_DEPTH + 1>; - extern const LmrReductionTable lmrReduction; + // The table is rebuilt whenever an LMR tuning parameter changes. + extern LmrReductionTable lmrReduction; + + void rebuildLmrReductionTable() noexcept; inline int lmrBaseReduction(int depth, int moveCount) noexcept { if (depth <= 0 || moveCount <= 0) diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp new file mode 100644 index 0000000..e2cbd21 --- /dev/null +++ b/include/ironphoenix/tuning.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +namespace ironphoenix { + + class SearchEngine; + + namespace Tuning { + + struct SearchParameters final { + int aspirationStartDepth = 4; + int aspirationInitialDelta = 50; + int seePruneMaxDepth = 4; + int seePruneMarginPerDepth = 80; + int historyBonusScale = 64; + int historyBonusMax = 2048; + int counterMoveBonus = 32'000; + int checkMoveBonus = 10'000'000; + int lmrMinDepth = 3; + int lmrFullMoves = 3; + int lmrBaseX100 = 75; + int lmrDivisorX100 = 275; + }; + + struct EvalParameters final { + int pawnValue = 100; + int knightValue = 300; + int bishopValue = 400; + int rookValue = 500; + int queenValue = 1000; + int knightMobility = 4; + int bishopMobility = 4; + int rookMobility = 2; + int queenMobility = 1; + }; + + [[nodiscard]] SearchParameters& search() noexcept; + [[nodiscard]] EvalParameters& eval() noexcept; + + void resetSearch() noexcept; + void resetEval() noexcept; + void resetAll() noexcept; + + [[nodiscard]] bool setSearchParameter(std::string_view name, int value) noexcept; + [[nodiscard]] bool setEvalParameter(std::string_view name, int value) noexcept; + + // UCI integration helpers. handleSetOption returns true when the option + // belongs to the tuning system, including recognized-but-invalid input. + [[nodiscard]] bool handleSetOption(std::string_view args, SearchEngine& searchEngine, std::ostream& out); + void printUciOptions(std::ostream& out); + void printCurrent(std::ostream& out); + + } // namespace Tuning + +} // namespace ironphoenix diff --git a/src/eval.cpp b/src/eval.cpp index 6dddaa8..f0706bf 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -3,20 +3,36 @@ #include "ironphoenix/geometry.hpp" #include "ironphoenix/nnue.hpp" #include "ironphoenix/position.hpp" - -#include +#include "ironphoenix/tuning.hpp" namespace ironphoenix::Eval { namespace { - constexpr std::array EVAL_VALUE = { - 0, 100, 300, 400, 500, 1000, 0 - }; + IRONPHOENIX_FORCE_INLINE int evalValue(PieceType pt) noexcept { + const auto& params = Tuning::eval(); - constexpr std::array MOBILITY_WEIGHT = { - 0, 0, 4, 4, 2, 1, 0 - }; + switch (pt) { + case PAWN: return params.pawnValue; + case KNIGHT: return params.knightValue; + case BISHOP: return params.bishopValue; + case ROOK: return params.rookValue; + case QUEEN: return params.queenValue; + default: return 0; + } + } + + IRONPHOENIX_FORCE_INLINE int mobilityWeight(PieceType pt) noexcept { + const auto& params = Tuning::eval(); + + switch (pt) { + case KNIGHT: return params.knightMobility; + case BISHOP: return params.bishopMobility; + case ROOK: return params.rookMobility; + case QUEEN: return params.queenMobility; + default: return 0; + } + } IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { const Color partner = static_cast(static_cast(c) ^ 2u); @@ -28,7 +44,7 @@ namespace ironphoenix::Eval { for (unsigned pt = PAWN; pt <= QUEEN; ++pt) { material += pos.pieces(c, static_cast(pt)).popcount() - * EVAL_VALUE[pt]; + * evalValue(static_cast(pt)); } return material; @@ -65,7 +81,7 @@ namespace ironphoenix::Eval { } const int mobility = (attacks & ~blockedByTeam).popcount(); - score += mobility * MOBILITY_WEIGHT[static_cast(pt)]; + score += mobility * mobilityWeight(pt); } }; @@ -102,6 +118,10 @@ namespace ironphoenix::Eval { } + Parameters& parameters() noexcept { + return Tuning::eval(); + } + int evaluate(const Position& pos) noexcept { if (NNUE::loaded()) return NNUE::evaluate(pos); diff --git a/src/lmr.cpp b/src/lmr.cpp index c603b0f..d34d1f0 100644 --- a/src/lmr.cpp +++ b/src/lmr.cpp @@ -1,4 +1,5 @@ #include "ironphoenix/lmr.hpp" +#include "ironphoenix/tuning.hpp" #include #include @@ -7,26 +8,29 @@ namespace ironphoenix { namespace { - LmrReductionTable buildLmrReductionTable() { + LmrReductionTable buildLmrReductionTable() noexcept { LmrReductionTable table{}; + const auto& params = Tuning::search(); + + const double base = static_cast(params.lmrBaseX100) / 100.0; + const double divisor = static_cast(std::max(1, params.lmrDivisorX100)) / 100.0; for (int depth = 1; depth <= LMR_MAX_DEPTH; ++depth) { for (int moveCount = 1; moveCount <= LMR_MAX_MOVES; ++moveCount) { - if (depth < 3 || moveCount <= 3) { + if (depth < params.lmrMinDepth || moveCount <= params.lmrFullMoves) { table[depth][moveCount] = 0; continue; } - + const double raw = - 0.75 + base + (std::log(static_cast(depth)) - * std::log(static_cast(moveCount))) / 2.75; + * std::log(static_cast(moveCount))) / divisor; int reduction = static_cast(raw); reduction = std::max(reduction, 1); - reduction = std::min(reduction, std::max(1, depth - 2)); table[depth][moveCount] = @@ -39,6 +43,10 @@ namespace ironphoenix { } - const LmrReductionTable lmrReduction = buildLmrReductionTable(); + LmrReductionTable lmrReduction = buildLmrReductionTable(); + + void rebuildLmrReductionTable() noexcept { + lmrReduction = buildLmrReductionTable(); + } } diff --git a/src/search.cpp b/src/search.cpp index 393c9b7..c47792f 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -2,6 +2,7 @@ #include "ironphoenix/eval.hpp" #include "ironphoenix/lmr.hpp" +#include "ironphoenix/tuning.hpp" #include "ironphoenix/movegen.hpp" #include "ironphoenix/see.hpp" @@ -15,13 +16,6 @@ namespace ironphoenix { namespace { - constexpr int ASPIRATION_START_DEPTH = 4; - constexpr int ASPIRATION_INITIAL_DELTA = 50; - - constexpr int SEE_PRUNE_MAX_DEPTH = 4; - constexpr int SEE_PRUNE_MARGIN_PER_DEPTH = 80; - constexpr int QSEARCH_SEE_THRESHOLD = -50; - IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { const Color partner = static_cast(static_cast(c) ^ 2u); return pos.occupancy(c) | pos.occupancy(partner); @@ -124,7 +118,8 @@ namespace ironphoenix { } int SearchEngine::historyBonus(int depth) const noexcept { - return std::min(2048, 64 * depth * depth); + const auto& params = Tuning::search(); + return std::min(params.historyBonusMax, params.historyBonusScale * depth * depth); } int SearchEngine::moveScore(const Position& pos, Move move, Move ttMove, int ply) const noexcept { @@ -133,6 +128,7 @@ namespace ironphoenix { if (isTerminalKingCapture(pos, move)) return 45'000'000; + const auto& tuning = Tuning::search(); const Color mover = pos.sideToMove(); const Piece movingPiece = pos.pieceAt(move.from()); @@ -158,7 +154,7 @@ namespace ironphoenix { score += history_.continuationScore(ContinuationKind::PreviousPly, searchStack_[ply], movingPiece, move.to()); if (history_.counterMove(mover, searchStack_[ply]) == move) - score += 32'000; + score += tuning.counterMoveBonus; } if (ply >= 4 && searchStack_[ply - 3].valid() @@ -167,7 +163,7 @@ namespace ironphoenix { } if (pos.givesCheck(move)) - score += 10'000'000; + score += tuning.checkMoveBonus; return score; } @@ -413,6 +409,7 @@ namespace ironphoenix { } const bool inCheckNode = pos.inCheck(); + const auto& tuning = Tuning::search(); MoveList moves; generatePseudoLegalMoves(pos, moves); @@ -446,14 +443,14 @@ namespace ironphoenix { if (!pvNode && !inCheckNode - && depth <= SEE_PRUNE_MAX_DEPTH + && depth <= tuning.seePruneMaxDepth && legalMoves > 0 && move.isCapture() && !move.isPromotion() && move != ttMove && !givesCheckMove && !mateWindow) { - const int seeThreshold = -SEE_PRUNE_MARGIN_PER_DEPTH * depth; + const int seeThreshold = -tuning.seePruneMarginPerDepth * depth; if (!seeGE(pos, move, seeThreshold, SeeMode::Legal)) continue; } @@ -654,6 +651,7 @@ namespace ironphoenix { return; } + const auto& tuning = Tuning::search(); const int requestedDepth = limits_.depth > 0 ? std::min(limits_.depth, MAX_PLY - 1) : 0; int depth = 1; @@ -669,7 +667,7 @@ namespace ironphoenix { int score = 0; - if (depth < ASPIRATION_START_DEPTH || + if (depth < tuning.aspirationStartDepth || bestScore >= MATE_THRESHOLD || bestScore <= -MATE_THRESHOLD) { @@ -686,7 +684,7 @@ namespace ironphoenix { } else { - int delta = ASPIRATION_INITIAL_DELTA; + int delta = tuning.aspirationInitialDelta; int alpha = std::max(-INF, bestScore - delta); int beta = std::min(INF, bestScore + delta); diff --git a/src/tuning.cpp b/src/tuning.cpp new file mode 100644 index 0000000..bffd563 --- /dev/null +++ b/src/tuning.cpp @@ -0,0 +1,262 @@ +#include "ironphoenix/tuning.hpp" + +#include "ironphoenix/lmr.hpp" +#include "ironphoenix/search.hpp" + +#include +#include +#include + +namespace ironphoenix::Tuning { + + namespace { + + std::string_view trim(std::string_view text) noexcept { + while (!text.empty() && (text.front() == ' ' || text.front() == '\t')) + text.remove_prefix(1); + while (!text.empty() && (text.back() == ' ' || text.back() == '\t')) + text.remove_suffix(1); + return text; + } + + bool parseInt(std::string_view text, int& value) noexcept { + text = trim(text); + if (text.empty()) + return false; + + const char* first = text.data(); + const char* last = text.data() + text.size(); + const auto [ptr, ec] = std::from_chars(first, last, value); + return ec == std::errc{} && ptr == last; + } + + bool inRange(int value, int minimum, int maximum) noexcept { + return value >= minimum && value <= maximum; + } + + bool isSearchParameter(std::string_view name) noexcept { + return name == "SearchAspirationStartDepth" + || name == "SearchAspirationDelta" + || name == "SearchSEEPruneMaxDepth" + || name == "SearchSEEPruneMargin" + || name == "SearchHistoryBonusScale" + || name == "SearchHistoryBonusMax" + || name == "SearchCounterMoveBonus" + || name == "SearchCheckMoveBonus" + || name == "SearchLMRMinDepth" + || name == "SearchLMRFullMoves" + || name == "SearchLMRBaseX100" + || name == "SearchLMRDivisorX100"; + } + + bool isEvalParameter(std::string_view name) noexcept { + return name == "EvalPawnValue" + || name == "EvalKnightValue" + || name == "EvalBishopValue" + || name == "EvalRookValue" + || name == "EvalQueenValue" + || name == "EvalKnightMobility" + || name == "EvalBishopMobility" + || name == "EvalRookMobility" + || name == "EvalQueenMobility"; + } + + bool isLmrParameter(std::string_view name) noexcept { + return name == "SearchLMRMinDepth" + || name == "SearchLMRFullMoves" + || name == "SearchLMRBaseX100" + || name == "SearchLMRDivisorX100"; + } + + } + + SearchParameters& search() noexcept { + // Function-local statics avoid cross-translation-unit initialization + // order problems with the LMR table's startup construction. + static SearchParameters parameters{}; + return parameters; + } + + EvalParameters& eval() noexcept { + static EvalParameters parameters{}; + return parameters; + } + + void resetSearch() noexcept { + search() = SearchParameters{}; + rebuildLmrReductionTable(); + } + + void resetEval() noexcept { + eval() = EvalParameters{}; + } + + void resetAll() noexcept { + search() = SearchParameters{}; + eval() = EvalParameters{}; + rebuildLmrReductionTable(); + } + + bool setSearchParameter(std::string_view name, int value) noexcept { + auto& p = search(); + bool changed = true; + + if (name == "SearchAspirationStartDepth" && inRange(value, 1, 32)) + p.aspirationStartDepth = value; + else if (name == "SearchAspirationDelta" && inRange(value, 1, 2000)) + p.aspirationInitialDelta = value; + else if (name == "SearchSEEPruneMaxDepth" && inRange(value, 0, 32)) + p.seePruneMaxDepth = value; + else if (name == "SearchSEEPruneMargin" && inRange(value, 0, 2000)) + p.seePruneMarginPerDepth = value; + else if (name == "SearchHistoryBonusScale" && inRange(value, 0, 2048)) + p.historyBonusScale = value; + else if (name == "SearchHistoryBonusMax" && inRange(value, 1, 32767)) + p.historyBonusMax = value; + else if (name == "SearchCounterMoveBonus" && inRange(value, 0, 50'000'000)) + p.counterMoveBonus = value; + else if (name == "SearchCheckMoveBonus" && inRange(value, 0, 50'000'000)) + p.checkMoveBonus = value; + else if (name == "SearchLMRMinDepth" && inRange(value, 1, 32)) + p.lmrMinDepth = value; + else if (name == "SearchLMRFullMoves" && inRange(value, 0, 128)) + p.lmrFullMoves = value; + else if (name == "SearchLMRBaseX100" && inRange(value, 0, 1000)) + p.lmrBaseX100 = value; + else if (name == "SearchLMRDivisorX100" && inRange(value, 25, 5000)) + p.lmrDivisorX100 = value; + else + changed = false; + + if (changed && isLmrParameter(name)) + rebuildLmrReductionTable(); + + return changed; + } + + bool setEvalParameter(std::string_view name, int value) noexcept { + auto& p = eval(); + + if (name == "EvalPawnValue" && inRange(value, 0, 5000)) + p.pawnValue = value; + else if (name == "EvalKnightValue" && inRange(value, 0, 5000)) + p.knightValue = value; + else if (name == "EvalBishopValue" && inRange(value, 0, 5000)) + p.bishopValue = value; + else if (name == "EvalRookValue" && inRange(value, 0, 5000)) + p.rookValue = value; + else if (name == "EvalQueenValue" && inRange(value, 0, 5000)) + p.queenValue = value; + else if (name == "EvalKnightMobility" && inRange(value, -100, 100)) + p.knightMobility = value; + else if (name == "EvalBishopMobility" && inRange(value, -100, 100)) + p.bishopMobility = value; + else if (name == "EvalRookMobility" && inRange(value, -100, 100)) + p.rookMobility = value; + else if (name == "EvalQueenMobility" && inRange(value, -100, 100)) + p.queenMobility = value; + else + return false; + + return true; + } + + bool handleSetOption(std::string_view args, SearchEngine& searchEngine, std::ostream& out) { + args = trim(args); + + if (args == "name ResetTuning") { + resetAll(); + searchEngine.clearHash(); + out << "info string tuning parameters reset to defaults\n"; + return true; + } + + constexpr std::string_view namePrefix = "name "; + constexpr std::string_view valueDelimiter = " value "; + + if (!args.starts_with(namePrefix)) + return false; + + const std::size_t valuePos = args.find(valueDelimiter, namePrefix.size()); + if (valuePos == std::string_view::npos) + return false; + + const std::string_view name = trim(args.substr(namePrefix.size(), valuePos - namePrefix.size())); + if (!isSearchParameter(name) && !isEvalParameter(name)) + return false; + + const std::string_view text = trim(args.substr(valuePos + valueDelimiter.size())); + int value = 0; + if (!parseInt(text, value)) { + out << "info string invalid tuning value for " << name << '\n'; + return true; + } + + const bool accepted = isSearchParameter(name) + ? setSearchParameter(name, value) + : setEvalParameter(name, value); + + if (!accepted) { + out << "info string tuning value out of range for " << name << ": " << value << '\n'; + return true; + } + + searchEngine.clearHash(); + out << "info string tuning parameter " << name << " set to " << value << '\n'; + return true; + } + + void printUciOptions(std::ostream& out) { + out << "option name SearchAspirationStartDepth type spin default 4 min 1 max 32\n" + << "option name SearchAspirationDelta type spin default 50 min 1 max 2000\n" + << "option name SearchSEEPruneMaxDepth type spin default 4 min 0 max 32\n" + << "option name SearchSEEPruneMargin type spin default 80 min 0 max 2000\n" + << "option name SearchHistoryBonusScale type spin default 64 min 0 max 2048\n" + << "option name SearchHistoryBonusMax type spin default 2048 min 1 max 32767\n" + << "option name SearchCounterMoveBonus type spin default 32000 min 0 max 50000000\n" + << "option name SearchCheckMoveBonus type spin default 10000000 min 0 max 50000000\n" + << "option name SearchLMRMinDepth type spin default 3 min 1 max 32\n" + << "option name SearchLMRFullMoves type spin default 3 min 0 max 128\n" + << "option name SearchLMRBaseX100 type spin default 75 min 0 max 1000\n" + << "option name SearchLMRDivisorX100 type spin default 275 min 25 max 5000\n" + << "option name EvalPawnValue type spin default 100 min 0 max 5000\n" + << "option name EvalKnightValue type spin default 300 min 0 max 5000\n" + << "option name EvalBishopValue type spin default 400 min 0 max 5000\n" + << "option name EvalRookValue type spin default 500 min 0 max 5000\n" + << "option name EvalQueenValue type spin default 1000 min 0 max 5000\n" + << "option name EvalKnightMobility type spin default 4 min -100 max 100\n" + << "option name EvalBishopMobility type spin default 4 min -100 max 100\n" + << "option name EvalRookMobility type spin default 2 min -100 max 100\n" + << "option name EvalQueenMobility type spin default 1 min -100 max 100\n" + << "option name ResetTuning type button\n"; + } + + void printCurrent(std::ostream& out) { + const auto& s = search(); + const auto& e = eval(); + + out << "info string SearchAspirationStartDepth=" << s.aspirationStartDepth + << " SearchAspirationDelta=" << s.aspirationInitialDelta + << " SearchSEEPruneMaxDepth=" << s.seePruneMaxDepth + << " SearchSEEPruneMargin=" << s.seePruneMarginPerDepth + << " SearchHistoryBonusScale=" << s.historyBonusScale + << " SearchHistoryBonusMax=" << s.historyBonusMax + << " SearchCounterMoveBonus=" << s.counterMoveBonus + << " SearchCheckMoveBonus=" << s.checkMoveBonus + << " SearchLMRMinDepth=" << s.lmrMinDepth + << " SearchLMRFullMoves=" << s.lmrFullMoves + << " SearchLMRBaseX100=" << s.lmrBaseX100 + << " SearchLMRDivisorX100=" << s.lmrDivisorX100 << '\n'; + + out << "info string EvalPawnValue=" << e.pawnValue + << " EvalKnightValue=" << e.knightValue + << " EvalBishopValue=" << e.bishopValue + << " EvalRookValue=" << e.rookValue + << " EvalQueenValue=" << e.queenValue + << " EvalKnightMobility=" << e.knightMobility + << " EvalBishopMobility=" << e.bishopMobility + << " EvalRookMobility=" << e.rookMobility + << " EvalQueenMobility=" << e.queenMobility << '\n'; + } + +} // namespace ironphoenix::Tuning diff --git a/src/uci.cpp b/src/uci.cpp index 2d99bd7..8617882 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -3,6 +3,7 @@ #include "ironphoenix/movegen.hpp" #include "ironphoenix/nnue.hpp" #include "ironphoenix/perft.hpp" +#include "ironphoenix/tuning.hpp" #include #include @@ -244,6 +245,7 @@ namespace ironphoenix { " setoption name EvalFile value \n" " setoption name UseNNUE value \n" " setoption name ReloadNNUE - reload the current EvalFile\n" + " tuneparams - print current search/HCE tuning values\n" " position fen [moves ...]\n" " position startpos [moves ...] (Modern is built in by default)\n" " d - display board\n" @@ -423,6 +425,9 @@ namespace ironphoenix { return true; } + if (Tuning::handleSetOption(args, search_, out)) + return true; + out << "info string unsupported option\n"; return false; } @@ -615,8 +620,9 @@ namespace ironphoenix { << "option name EvalFile type string default " << nnueFile_ << "\n" << "option name UseNNUE type check default " << (useNNUE_ ? "true" : "false") << "\n" << "option name ReloadNNUE type button\n" - << "option name Clear Hash type button\n" - << "uciok\n"; + << "option name Clear Hash type button\n"; + Tuning::printUciOptions(out); + out << "uciok\n"; } else if (line == "isready") { out << "readyok\n"; @@ -624,6 +630,9 @@ namespace ironphoenix { else if (line == "help") { printHelp(out); } + else if (line == "tuneparams") { + Tuning::printCurrent(out); + } else if (line == "ucinewgame") { search_.newGame(); pos_.setRulesetId(setupRulesetId(setup_));