From 35011ae8e7ef5cc8bec0d1cf6499f741801fb2f0 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 12 Oct 2026 04:36:02 -0500 Subject: [PATCH 1/7] Add HCE tuning parameters --- include/ironphoenix/tuning.hpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index e2cbd21..36bb01f 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -34,6 +34,16 @@ namespace ironphoenix { int bishopMobility = 4; int rookMobility = 2; int queenMobility = 1; + + // Positional HCE terms. These intentionally start conservative so + // they can be strength-tested and tuned independently of material. + int bishopPairBonus = 30; + int pawnAdvanceBonus = 4; + int supportedPawnBonus = 6; + int kingPawnShieldBonus = 6; + int checkPenalty = 40; + int knightCentralization = 3; + int tempoBonus = 8; }; [[nodiscard]] SearchParameters& search() noexcept; From f901d16c3ccaac12f89a503678b5bc7958a97aa5 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 12 Oct 2026 04:36:32 -0500 Subject: [PATCH 2/7] Add positional terms to handcrafted evaluation --- src/eval.cpp | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/src/eval.cpp b/src/eval.cpp index f0706bf..499eee8 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -5,6 +5,8 @@ #include "ironphoenix/position.hpp" #include "ironphoenix/tuning.hpp" +#include + namespace ironphoenix::Eval { namespace { @@ -39,6 +41,31 @@ namespace ironphoenix::Eval { return pos.occupancy(c) | pos.occupancy(partner); } + IRONPHOENIX_FORCE_INLINE int pawnAdvance(Color c, Square sq) noexcept { + const int file = static_cast(Geometry::fileOf(sq)); + const int rank = static_cast(Geometry::rankOf(sq)); + + int advance = 0; + switch (c) { + case RED: advance = rank - 1; break; + case BLUE: advance = file - 1; break; + case YELLOW: advance = 12 - rank; break; + case GREEN: advance = 12 - file; break; + } + + return std::clamp(advance, 0, 9); + } + + IRONPHOENIX_FORCE_INLINE int centralizationUnits(Square sq) noexcept { + // The geometric center of the 14x14 board lies between coordinates + // 6 and 7 on each axis. Work in doubled coordinates to avoid floats. + const int file = static_cast(Geometry::fileOf(sq)); + const int rank = static_cast(Geometry::rankOf(sq)); + const int fileDistance = std::abs(file * 2 - 13); + const int rankDistance = std::abs(rank * 2 - 13); + return std::max(0, 12 - fileDistance - rankDistance); + } + int materialForColor(const Position& pos, Color c) noexcept { int material = 0; @@ -93,9 +120,76 @@ namespace ironphoenix::Eval { return score; } + int bishopPairForColor(const Position& pos, Color c) noexcept { + const auto& params = Tuning::eval(); + return pos.pieces(c, BISHOP).popcount() >= 2 + ? params.bishopPairBonus + : 0; + } + + int pawnStructureForColor(const Position& pos, Color c) noexcept { + const auto& params = Tuning::eval(); + const Bitboard allPawns = pos.pieces(c, PAWN); + Bitboard pawns = allPawns; + int score = 0; + + while (pawns) { + const Square sq = popLsb(pawns); + score += pawnAdvance(c, sq) * params.pawnAdvanceBonus; + + // A pawn supported by another pawn is harder to attack and forms + // a more stable chain. PawnAttackers[c][sq] are the squares from + // which one of c's pawns would attack sq. + if (Geometry::PawnAttackers[c][sq] & allPawns) + score += params.supportedPawnBonus; + } + + return score; + } + + int kingSafetyForColor(const Position& pos, Color c) noexcept { + if (!pos.isAlive(c)) + return 0; + + const auto& params = Tuning::eval(); + const Square king = pos.kingSquare(c); + if (king == SQ_NONE) + return 0; + + int score = 0; + + const int pawnShield = (Geometry::KingAttacks[king] & pos.pieces(c, PAWN)).popcount(); + score += pawnShield * params.kingPawnShieldBonus; + + if (pos.inCheck(c)) + score -= params.checkPenalty; + + return score; + } + + int knightPlacementForColor(const Position& pos, Color c) noexcept { + const auto& params = Tuning::eval(); + Bitboard knights = pos.pieces(c, KNIGHT); + int score = 0; + + while (knights) { + const Square sq = popLsb(knights); + score += centralizationUnits(sq) * params.knightCentralization; + } + + return score; + } + int playerScore(const Position& pos, Color c) noexcept { + if (!pos.isAlive(c)) + return 0; + return materialForColor(pos, c) - + mobilityForColor(pos, c); + + mobilityForColor(pos, c) + + bishopPairForColor(pos, c) + + pawnStructureForColor(pos, c) + + kingSafetyForColor(pos, c) + + knightPlacementForColor(pos, c); } int handcraftedEvaluate(const Position& pos) noexcept { @@ -113,7 +207,8 @@ namespace ironphoenix::Eval { } const int score = team0 - team1; - return teamOf(pos.sideToMove()) == 0 ? score : -score; + const int relative = teamOf(pos.sideToMove()) == 0 ? score : -score; + return relative + Tuning::eval().tempoBonus; } } From 932719d2ea49ba297179b71378a886166ddf8d2f Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 12 Oct 2026 04:37:14 -0500 Subject: [PATCH 3/7] Expose new HCE terms through UCI tuning --- src/tuning.cpp | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/tuning.cpp b/src/tuning.cpp index bffd563..d360ac8 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -58,7 +58,14 @@ namespace ironphoenix::Tuning { || name == "EvalKnightMobility" || name == "EvalBishopMobility" || name == "EvalRookMobility" - || name == "EvalQueenMobility"; + || name == "EvalQueenMobility" + || name == "EvalBishopPairBonus" + || name == "EvalPawnAdvanceBonus" + || name == "EvalSupportedPawnBonus" + || name == "EvalKingPawnShieldBonus" + || name == "EvalCheckPenalty" + || name == "EvalKnightCentralization" + || name == "EvalTempoBonus"; } bool isLmrParameter(std::string_view name) noexcept { @@ -155,6 +162,20 @@ namespace ironphoenix::Tuning { p.rookMobility = value; else if (name == "EvalQueenMobility" && inRange(value, -100, 100)) p.queenMobility = value; + else if (name == "EvalBishopPairBonus" && inRange(value, -500, 500)) + p.bishopPairBonus = value; + else if (name == "EvalPawnAdvanceBonus" && inRange(value, -100, 100)) + p.pawnAdvanceBonus = value; + else if (name == "EvalSupportedPawnBonus" && inRange(value, -200, 200)) + p.supportedPawnBonus = value; + else if (name == "EvalKingPawnShieldBonus" && inRange(value, -200, 200)) + p.kingPawnShieldBonus = value; + else if (name == "EvalCheckPenalty" && inRange(value, 0, 1000)) + p.checkPenalty = value; + else if (name == "EvalKnightCentralization" && inRange(value, -100, 100)) + p.knightCentralization = value; + else if (name == "EvalTempoBonus" && inRange(value, -500, 500)) + p.tempoBonus = value; else return false; @@ -228,6 +249,13 @@ namespace ironphoenix::Tuning { << "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 EvalBishopPairBonus type spin default 30 min -500 max 500\n" + << "option name EvalPawnAdvanceBonus type spin default 4 min -100 max 100\n" + << "option name EvalSupportedPawnBonus type spin default 6 min -200 max 200\n" + << "option name EvalKingPawnShieldBonus type spin default 6 min -200 max 200\n" + << "option name EvalCheckPenalty type spin default 40 min 0 max 1000\n" + << "option name EvalKnightCentralization type spin default 3 min -100 max 100\n" + << "option name EvalTempoBonus type spin default 8 min -500 max 500\n" << "option name ResetTuning type button\n"; } @@ -256,7 +284,14 @@ namespace ironphoenix::Tuning { << " EvalKnightMobility=" << e.knightMobility << " EvalBishopMobility=" << e.bishopMobility << " EvalRookMobility=" << e.rookMobility - << " EvalQueenMobility=" << e.queenMobility << '\n'; + << " EvalQueenMobility=" << e.queenMobility + << " EvalBishopPairBonus=" << e.bishopPairBonus + << " EvalPawnAdvanceBonus=" << e.pawnAdvanceBonus + << " EvalSupportedPawnBonus=" << e.supportedPawnBonus + << " EvalKingPawnShieldBonus=" << e.kingPawnShieldBonus + << " EvalCheckPenalty=" << e.checkPenalty + << " EvalKnightCentralization=" << e.knightCentralization + << " EvalTempoBonus=" << e.tempoBonus << '\n'; } } // namespace ironphoenix::Tuning From 61ba17f97f057f8740d464e8aad7f98c30d7d493 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 12 Oct 2026 04:37:38 -0500 Subject: [PATCH 4/7] Fix integer abs include in HCE --- src/eval.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/eval.cpp b/src/eval.cpp index 499eee8..5a91e90 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -6,6 +6,7 @@ #include "ironphoenix/tuning.hpp" #include +#include namespace ironphoenix::Eval { From 3f81d1c566f67b5c031e5df365f765a87d5d70e5 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 12 Oct 2026 04:43:50 -0500 Subject: [PATCH 5/7] Replace generic placement terms with 4PC team eval terms --- include/ironphoenix/tuning.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index 36bb01f..8b1a018 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -35,14 +35,14 @@ namespace ironphoenix { int rookMobility = 2; int queenMobility = 1; - // Positional HCE terms. These intentionally start conservative so - // they can be strength-tested and tuned independently of material. + // 4PC-oriented positional HCE terms. These intentionally start + // conservative so each can be strength-tested and tuned. int bishopPairBonus = 30; - int pawnAdvanceBonus = 4; int supportedPawnBonus = 6; int kingPawnShieldBonus = 6; int checkPenalty = 40; - int knightCentralization = 3; + int kingZoneAttackPenalty = 5; + int partnerSupportBonus = 4; int tempoBonus = 8; }; From a09c3e4a7b8ae3ef0ea6a1816f32d06927f5f773 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 12 Oct 2026 04:44:20 -0500 Subject: [PATCH 6/7] Use 4PC king pressure and partner coordination in HCE --- src/eval.cpp | 93 ++++++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/src/eval.cpp b/src/eval.cpp index 5a91e90..745d5df 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -5,9 +5,6 @@ #include "ironphoenix/position.hpp" #include "ironphoenix/tuning.hpp" -#include -#include - namespace ironphoenix::Eval { namespace { @@ -37,34 +34,12 @@ namespace ironphoenix::Eval { } } - 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); - } - - IRONPHOENIX_FORCE_INLINE int pawnAdvance(Color c, Square sq) noexcept { - const int file = static_cast(Geometry::fileOf(sq)); - const int rank = static_cast(Geometry::rankOf(sq)); - - int advance = 0; - switch (c) { - case RED: advance = rank - 1; break; - case BLUE: advance = file - 1; break; - case YELLOW: advance = 12 - rank; break; - case GREEN: advance = 12 - file; break; - } - - return std::clamp(advance, 0, 9); + IRONPHOENIX_FORCE_INLINE Color partnerOf(Color c) noexcept { + return static_cast(static_cast(c) ^ 2u); } - IRONPHOENIX_FORCE_INLINE int centralizationUnits(Square sq) noexcept { - // The geometric center of the 14x14 board lies between coordinates - // 6 and 7 on each axis. Work in doubled coordinates to avoid floats. - const int file = static_cast(Geometry::fileOf(sq)); - const int rank = static_cast(Geometry::rankOf(sq)); - const int fileDistance = std::abs(file * 2 - 13); - const int rankDistance = std::abs(rank * 2 - 13); - return std::max(0, 12 - fileDistance - rankDistance); + IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept { + return pos.occupancy(c) | pos.occupancy(partnerOf(c)); } int materialForColor(const Position& pos, Color c) noexcept { @@ -130,18 +105,22 @@ namespace ironphoenix::Eval { int pawnStructureForColor(const Position& pos, Color c) noexcept { const auto& params = Tuning::eval(); - const Bitboard allPawns = pos.pieces(c, PAWN); - Bitboard pawns = allPawns; + const Color partner = partnerOf(c); + const Bitboard ownPawns = pos.pieces(c, PAWN); + const Bitboard partnerPawns = pos.isAlive(partner) + ? pos.pieces(partner, PAWN) + : Bitboard{}; + + Bitboard pawns = ownPawns; int score = 0; while (pawns) { const Square sq = popLsb(pawns); - score += pawnAdvance(c, sq) * params.pawnAdvanceBonus; - // A pawn supported by another pawn is harder to attack and forms - // a more stable chain. PawnAttackers[c][sq] are the squares from - // which one of c's pawns would attack sq. - if (Geometry::PawnAttackers[c][sq] & allPawns) + // In teams, support from either player's pawn structure matters. + const bool ownSupport = static_cast(Geometry::PawnAttackers[c][sq] & ownPawns); + const bool partnerSupport = static_cast(Geometry::PawnAttackers[partner][sq] & partnerPawns); + if (ownSupport || partnerSupport) score += params.supportedPawnBonus; } @@ -153,29 +132,57 @@ namespace ironphoenix::Eval { return 0; const auto& params = Tuning::eval(); + const Color partner = partnerOf(c); const Square king = pos.kingSquare(c); if (king == SQ_NONE) return 0; + const Bitboard occ = pos.occupancy(); + const Bitboard teamPawns = pos.pieces(c, PAWN) + | (pos.isAlive(partner) ? pos.pieces(partner, PAWN) : Bitboard{}); + int score = 0; - const int pawnShield = (Geometry::KingAttacks[king] & pos.pieces(c, PAWN)).popcount(); + // Either teammate's pawn can physically shelter the king. + const int pawnShield = (Geometry::KingAttacks[king] & teamPawns).popcount(); score += pawnShield * params.kingPawnShieldBonus; + // 4PC king danger is more about enemy access to the king zone than + // generic board centralization. Count attacked adjacent king squares. + Bitboard kingZone = Geometry::KingAttacks[king] & Geometry::BoardMask; + int attackedZoneSquares = 0; + while (kingZone) { + const Square sq = popLsb(kingZone); + if (pos.isAttackedByEnemy(sq, c, occ)) + ++attackedZoneSquares; + } + score -= attackedZoneSquares * params.kingZoneAttackPenalty; + if (pos.inCheck(c)) score -= params.checkPenalty; return score; } - int knightPlacementForColor(const Position& pos, Color c) noexcept { + int partnerCoordinationForColor(const Position& pos, Color c) noexcept { + const Color partner = partnerOf(c); + if (!pos.isAlive(partner)) + return 0; + const auto& params = Tuning::eval(); - Bitboard knights = pos.pieces(c, KNIGHT); + const Bitboard occ = pos.occupancy(); int score = 0; - while (knights) { - const Square sq = popLsb(knights); - score += centralizationUnits(sq) * params.knightCentralization; + // Reward pieces that are directly defended by the teammate. This is + // a genuinely 4PC-specific coordination signal and is independent of + // geometric assumptions about where a piece "should" be placed. + for (unsigned pt = PAWN; pt <= QUEEN; ++pt) { + Bitboard pieces = pos.pieces(c, static_cast(pt)); + while (pieces) { + const Square sq = popLsb(pieces); + if (pos.attackersTo(sq, partner, occ)) + score += params.partnerSupportBonus; + } } return score; @@ -190,7 +197,7 @@ namespace ironphoenix::Eval { + bishopPairForColor(pos, c) + pawnStructureForColor(pos, c) + kingSafetyForColor(pos, c) - + knightPlacementForColor(pos, c); + + partnerCoordinationForColor(pos, c); } int handcraftedEvaluate(const Position& pos) noexcept { From 5c7845aea9923afb44319f7bf432648ea467cb4a Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 12 Oct 2026 04:44:56 -0500 Subject: [PATCH 7/7] Expose 4PC king pressure and partner support tuning --- src/tuning.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/tuning.cpp b/src/tuning.cpp index d360ac8..6ef1085 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -60,11 +60,11 @@ namespace ironphoenix::Tuning { || name == "EvalRookMobility" || name == "EvalQueenMobility" || name == "EvalBishopPairBonus" - || name == "EvalPawnAdvanceBonus" || name == "EvalSupportedPawnBonus" || name == "EvalKingPawnShieldBonus" || name == "EvalCheckPenalty" - || name == "EvalKnightCentralization" + || name == "EvalKingZoneAttackPenalty" + || name == "EvalPartnerSupportBonus" || name == "EvalTempoBonus"; } @@ -164,16 +164,16 @@ namespace ironphoenix::Tuning { p.queenMobility = value; else if (name == "EvalBishopPairBonus" && inRange(value, -500, 500)) p.bishopPairBonus = value; - else if (name == "EvalPawnAdvanceBonus" && inRange(value, -100, 100)) - p.pawnAdvanceBonus = value; else if (name == "EvalSupportedPawnBonus" && inRange(value, -200, 200)) p.supportedPawnBonus = value; else if (name == "EvalKingPawnShieldBonus" && inRange(value, -200, 200)) p.kingPawnShieldBonus = value; else if (name == "EvalCheckPenalty" && inRange(value, 0, 1000)) p.checkPenalty = value; - else if (name == "EvalKnightCentralization" && inRange(value, -100, 100)) - p.knightCentralization = value; + else if (name == "EvalKingZoneAttackPenalty" && inRange(value, 0, 200)) + p.kingZoneAttackPenalty = value; + else if (name == "EvalPartnerSupportBonus" && inRange(value, -200, 200)) + p.partnerSupportBonus = value; else if (name == "EvalTempoBonus" && inRange(value, -500, 500)) p.tempoBonus = value; else @@ -250,11 +250,11 @@ namespace ironphoenix::Tuning { << "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 EvalBishopPairBonus type spin default 30 min -500 max 500\n" - << "option name EvalPawnAdvanceBonus type spin default 4 min -100 max 100\n" << "option name EvalSupportedPawnBonus type spin default 6 min -200 max 200\n" << "option name EvalKingPawnShieldBonus type spin default 6 min -200 max 200\n" << "option name EvalCheckPenalty type spin default 40 min 0 max 1000\n" - << "option name EvalKnightCentralization type spin default 3 min -100 max 100\n" + << "option name EvalKingZoneAttackPenalty type spin default 5 min 0 max 200\n" + << "option name EvalPartnerSupportBonus type spin default 4 min -200 max 200\n" << "option name EvalTempoBonus type spin default 8 min -500 max 500\n" << "option name ResetTuning type button\n"; } @@ -286,11 +286,11 @@ namespace ironphoenix::Tuning { << " EvalRookMobility=" << e.rookMobility << " EvalQueenMobility=" << e.queenMobility << " EvalBishopPairBonus=" << e.bishopPairBonus - << " EvalPawnAdvanceBonus=" << e.pawnAdvanceBonus << " EvalSupportedPawnBonus=" << e.supportedPawnBonus << " EvalKingPawnShieldBonus=" << e.kingPawnShieldBonus << " EvalCheckPenalty=" << e.checkPenalty - << " EvalKnightCentralization=" << e.knightCentralization + << " EvalKingZoneAttackPenalty=" << e.kingZoneAttackPenalty + << " EvalPartnerSupportBonus=" << e.partnerSupportBonus << " EvalTempoBonus=" << e.tempoBonus << '\n'; }