From 7b1ab77cf0f7127ff5f714faf41c68dbbbf0fd75 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 12:50:34 -0500 Subject: [PATCH 1/9] Add UCI state for NNUE network selection --- include/ironphoenix/uci.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/ironphoenix/uci.hpp b/include/ironphoenix/uci.hpp index a1ca94b..af053ab 100644 --- a/include/ironphoenix/uci.hpp +++ b/include/ironphoenix/uci.hpp @@ -28,8 +28,10 @@ namespace ironphoenix { Position pos_{}; Fen4State fenState_{}; std::string startFen_; + std::string nnueFile_ = "ironphoenix.nnue"; SetupType setup_ = SetupType::Modern; int multiPV_ = 1; + bool useNNUE_ = false; SearchEngine search_{}; bool handleLine(const std::string& line, std::ostream& out); From ae6a5e12b7b8821a373be242019f5d4faff37811 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 12:51:48 -0500 Subject: [PATCH 2/9] Add UCI controls for PhoenixNet NNUE --- src/uci.cpp | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/uci.cpp b/src/uci.cpp index c8d8ce4..2d99bd7 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -1,6 +1,7 @@ #include "ironphoenix/uci.hpp" #include "ironphoenix/movegen.hpp" +#include "ironphoenix/nnue.hpp" #include "ironphoenix/perft.hpp" #include @@ -217,6 +218,11 @@ namespace ironphoenix { UciShell::UciShell() : startFen_(MODERN_START_FEN), setup_(SetupType::Modern) { + if (NNUE::loaded()) { + nnueFile_ = NNUE::loadedPath(); + useNNUE_ = true; + } + pos_.setRulesetId(setupRulesetId(setup_)); std::string error; @@ -235,6 +241,9 @@ namespace ironphoenix { " setoption name Setup value \n" " setoption name StartFEN value \n" " setoption name MultiPV value <1..32>\n" + " setoption name EvalFile value \n" + " setoption name UseNNUE value \n" + " setoption name ReloadNNUE - reload the current EvalFile\n" " position fen [moves ...]\n" " position startpos [moves ...] (Modern is built in by default)\n" " d - display board\n" @@ -347,6 +356,67 @@ namespace ironphoenix { return true; } + constexpr std::string_view evalFilePrefix = "name EvalFile value "; + if (startsWith(args, evalFilePrefix)) { + const std::string candidate(trim(args.substr(evalFilePrefix.size()))); + if (candidate.empty()) { + out << "info string EvalFile requires a PhoenixNet .nnue path\n"; + return false; + } + + if (!NNUE::loadNetwork(candidate)) { + out << "info string failed to load PhoenixNet: " << candidate << '\n'; + return false; + } + + nnueFile_ = candidate; + useNNUE_ = true; + search_.clearHash(); + out << "info string PhoenixNet loaded: " << NNUE::loadedPath() << '\n'; + return true; + } + + constexpr std::string_view useNnuePrefix = "name UseNNUE value "; + if (startsWith(args, useNnuePrefix)) { + const std::string_view value = trim(args.substr(useNnuePrefix.size())); + + if (iequals(value, "true") || iequals(value, "on") || value == "1") { + if (!NNUE::loaded() && !NNUE::loadNetwork(nnueFile_)) { + useNNUE_ = false; + out << "info string failed to load PhoenixNet: " << nnueFile_ << '\n'; + return false; + } + + useNNUE_ = true; + search_.clearHash(); + out << "info string NNUE enabled: " << NNUE::loadedPath() << '\n'; + return true; + } + + if (iequals(value, "false") || iequals(value, "off") || value == "0") { + NNUE::unloadNetwork(); + useNNUE_ = false; + search_.clearHash(); + out << "info string NNUE disabled; using HCE fallback\n"; + return true; + } + + out << "info string UseNNUE must be true or false\n"; + return false; + } + + if (args == "name ReloadNNUE") { + if (!NNUE::loadNetwork(nnueFile_)) { + out << "info string failed to reload PhoenixNet: " << nnueFile_ << '\n'; + return false; + } + + useNNUE_ = true; + search_.clearHash(); + out << "info string PhoenixNet reloaded: " << NNUE::loadedPath() << '\n'; + return true; + } + if (args == "name Clear Hash") { search_.clearHash(); out << "info string Hash cleared\n"; @@ -542,6 +612,9 @@ namespace ironphoenix { << "option name StartFEN type string default \n" << "option name Hash type spin default 64 min 1 max 4096\n" << "option name MultiPV type spin default 1 min 1 max " << SearchEngine::MAX_MULTI_PV << "\n" + << "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"; } From 0fd0915e2d514528f60043a63c868e18349b1e71 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 13:06:35 -0500 Subject: [PATCH 3/9] Add history-aware Late Move Pruning --- src/search.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/search.cpp b/src/search.cpp index 393c9b7..c634e40 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -22,6 +22,9 @@ namespace ironphoenix { constexpr int SEE_PRUNE_MARGIN_PER_DEPTH = 80; constexpr int QSEARCH_SEE_THRESHOLD = -50; + constexpr int LMP_MAX_DEPTH = 4; + constexpr int LMP_HISTORY_THRESHOLD = 2'000; + 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); @@ -444,6 +447,22 @@ namespace ironphoenix { const int mateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD) && !terminalKingCapture; + // Late Move Pruning (LMP): at shallow non-PV nodes, stop spending + // search effort on late quiet moves unless history says the move is + // promising. Checks, TT moves, promotions/captures, and mate-window + // searches are never pruned here. + if (!pvNode + && !inCheckNode + && depth <= LMP_MAX_DEPTH + && legalMoves >= 3 + depth * depth + && quiet + && move != ttMove + && !givesCheckMove + && !mateWindow + && lmrHistoryScore < LMP_HISTORY_THRESHOLD) { + continue; + } + if (!pvNode && !inCheckNode && depth <= SEE_PRUNE_MAX_DEPTH From ebc25aade81281a8c8395e0c49bc49675ec574b7 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 13:09:21 -0500 Subject: [PATCH 4/9] Remove LMP depth limit --- src/search.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index c634e40..ee7ac6c 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -22,7 +22,6 @@ namespace ironphoenix { constexpr int SEE_PRUNE_MARGIN_PER_DEPTH = 80; constexpr int QSEARCH_SEE_THRESHOLD = -50; - constexpr int LMP_MAX_DEPTH = 4; constexpr int LMP_HISTORY_THRESHOLD = 2'000; IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { @@ -447,13 +446,12 @@ namespace ironphoenix { const int mateWindow = !(alpha > -MATE_THRESHOLD && beta < MATE_THRESHOLD) && !terminalKingCapture; - // Late Move Pruning (LMP): at shallow non-PV nodes, stop spending - // search effort on late quiet moves unless history says the move is + // Late Move Pruning (LMP): at non-PV nodes, stop spending search + // effort on late quiet moves unless history says the move is // promising. Checks, TT moves, promotions/captures, and mate-window - // searches are never pruned here. + // searches are never pruned here. This intentionally has no depth cap. if (!pvNode && !inCheckNode - && depth <= LMP_MAX_DEPTH && legalMoves >= 3 + depth * depth && quiet && move != ttMove From 60ad601057c0eba5125c79134ece132b7ba52761 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 14:14:18 -0500 Subject: [PATCH 5/9] 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_)); From bed052aace0e642c71e65590da94eb3493b273d2 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 15:02:27 -0500 Subject: [PATCH 6/9] Add Code of Conduct for IronPhoenix project This document outlines the Code of Conduct for the IronPhoenix project, detailing expected behaviors, unacceptable behaviors, technical discussion guidelines, and enforcement policies. --- CODE_OF_CONDUCT.md | 156 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f72d63e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,156 @@ +# ๐Ÿ”ฅ IronPhoenix Code of Conduct + +## ๐Ÿ›ก๏ธ Our Commitment + +IronPhoenix is an open-source project focused on building and improving a high-performance four-player chess engine. + +We want the project to be a place where people can collaborate, experiment, learn, review code, test ideas, and disagree constructively. Everyone participating in the IronPhoenix community is expected to help maintain a respectful, technical, and welcoming environment. + +This Code of Conduct applies to contributors, maintainers, reviewers, testers, users, and anyone participating in project spaces. + +--- + +## ๐Ÿค Expected Behavior + +Participants are expected to: + +- Communicate respectfully and professionally. +- Critique ideas, code, benchmarks, and technical decisions rather than attacking people. +- Support technical claims with testing, measurements, examples, or reasoning when practical. +- Give constructive code-review feedback that helps improve the project. +- Be patient with contributors of different experience levels. +- Ask questions when requirements, rules, benchmarks, or engine behavior are unclear. +- Respect the complexity of four-player chess and recognize that reasonable technical approaches may differ. +- Keep discussions focused on the project when using project issue trackers, pull requests, discussions, and other development channels. +- Respect private information, credentials, unpublished work, and security-sensitive information. +- Give appropriate credit for code, research, testing, ideas, and other contributions. + +--- + +## ๐Ÿšซ Unacceptable Behavior + +The following behavior is not acceptable: + +- Harassment, threats, intimidation, stalking, or targeted abuse. +- Discrimination or derogatory comments based on personal characteristics or identity. +- Personal insults, name-calling, humiliation, or repeated hostile behavior. +- Sexual harassment or unwanted sexual attention. +- Publishing another person's private information without permission. +- Deliberately disrupting project discussions, reviews, testing, or development. +- Spamming issues, pull requests, discussions, or other project channels. +- Knowingly submitting malicious code, credential stealers, destructive payloads, or other harmful content. +- Misrepresenting benchmark results, test results, authorship, or contribution history. +- Plagiarizing code or knowingly violating software licenses. +- Using the project community to organize harassment or abuse elsewhere. + +Strong disagreement about engine design, pruning techniques, evaluation, move ordering, architecture, benchmarks, or performance is allowed. Personal attacks are not. + +--- + +## ๐Ÿงช Technical Discussions and Engine Testing + +IronPhoenix development depends heavily on experimentation and strength testing. + +When discussing engine changes: + +- Treat Elo estimates, SPRT results, NPS, node counts, depth, and benchmark results as evidence rather than personal status. +- Clearly distinguish measured results from predictions or speculation. +- Prefer isolated testing when evaluating search heuristics or evaluation terms. +- Negative test results are useful information and should not be mocked or hidden. +- Reproducible tests are encouraged whenever practical. +- Do not knowingly falsify, selectively alter, or misrepresent testing data. +- Respect existing project behavior and comments when modifying established engine code unless the change intentionally replaces them. + +A failed experiment is not a failed contribution. Good testing that shows an idea loses strength can still help the project. + +--- + +## ๐Ÿง‘โ€๐Ÿ’ป Pull Requests and Code Review + +When opening or reviewing a pull request: + +- Keep the change focused when possible. +- Explain what the change is intended to accomplish. +- Include relevant testing information when available. +- Avoid unrelated rewrites in a feature-specific pull request. +- Review the implementation, not the contributor. +- Be willing to revise code when review identifies correctness, performance, maintainability, or testing concerns. +- Do not pressure maintainers to merge an experimental change before it has received appropriate review or testing. + +Maintainers may request that experimental engine-strength changes remain on separate branches until testing is complete. + +--- + +## ๐ŸŒ Scope + +This Code of Conduct applies in official IronPhoenix project spaces, including: + +- GitHub repositories +- Issues +- Pull requests +- Discussions +- Testing and benchmarking channels +- Project documentation +- Other communication spaces officially used for IronPhoenix development + +It may also apply when someone is publicly representing the IronPhoenix project. + +--- + +## ๐Ÿšจ Reporting a Problem + +If you experience or witness behavior that violates this Code of Conduct, report it to the IronPhoenix repository owner or project maintainers through an appropriate private GitHub contact method when possible. + +Please include relevant information such as: + +- What happened +- Where it happened +- When it happened +- Links, screenshots, or other evidence when available +- Any additional context that may help the maintainers understand the situation + +Reports will be reviewed as fairly and confidentially as practical. + +Do not knowingly submit false or retaliatory reports. + +--- + +## โš–๏ธ Enforcement + +Project maintainers are responsible for interpreting and enforcing this Code of Conduct. + +Depending on the severity and history of the behavior, maintainers may take actions including: + +1. **Correction** โ€” A private or public request to change inappropriate behavior. +2. **Warning** โ€” A formal warning that continued behavior may result in further action. +3. **Temporary restriction** โ€” Temporary limits on participation in issues, pull requests, discussions, or other project spaces. +4. **Permanent ban** โ€” Permanent removal from participation in IronPhoenix project spaces. + +Severe behavior may result in immediate restriction or removal without first using every intermediate step. + +Enforcement decisions should be based on protecting the project community and maintaining a productive development environment. + +--- + +## ๐Ÿงญ Maintainer Responsibilities + +Maintainers are expected to: + +- Apply this Code of Conduct consistently and in good faith. +- Avoid using enforcement as retaliation for technical disagreement. +- Distinguish heated technical debate from harassment or abuse. +- Protect confidential reports where practical. +- Remove, edit, or reject comments, commits, code, issues, or contributions that violate this Code of Conduct. +- Prioritize the safety and long-term health of the project community. + +--- + +## ๐Ÿ”ฅ Project Values + +IronPhoenix values: + +**Performance. Correctness. Experimentation. Reproducibility. Respect.** + +We can disagree aggressively about algorithms while still treating each other well. + +โ™Ÿ๏ธ **Build strong code. Test it honestly. Help the engine improve.** From f0f11b26acdf6fdbef113bc694a053c8f2eb321c Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 15:06:50 -0500 Subject: [PATCH 7/9] Add Aspiration Windows to search features list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1a29e5a..512ece1 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ IronPhoenix includes a modern alpha-beta search stack designed specifically for - Negamax / PVS-style search - Quiescence search +- Aspiration Windows - Transposition tables - History-based move ordering - Static Exchange Evaluation (SEE) From c62b8e598970879449ef785eff40ee590995e5fc Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 16:11:59 -0500 Subject: [PATCH 8/9] Add Late Move Pruning (LMP) to features list --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 512ece1..60c67fc 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ IronPhoenix includes a modern alpha-beta search stack designed specifically for - History-based move ordering - Static Exchange Evaluation (SEE) - Late Move Reductions (LMR) +- Late Move Pruning (LMP) - Search pruning and reductions - Iterative deepening - Principal variation reporting From f299b021d662388d4d5a8c3165e84ceedf4e2dc7 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 16:19:33 -0500 Subject: [PATCH 9/9] Create .gitkeep --- tunables/.gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 tunables/.gitkeep diff --git a/tunables/.gitkeep b/tunables/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tunables/.gitkeep @@ -0,0 +1 @@ +