From a1c76c3df858e01d22183b23f16aa9832db633d3 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 16:20:58 -0500 Subject: [PATCH 1/6] Add team-aware king pressure evaluation --- include/ironphoenix/tuning.hpp | 10 ++++ src/eval.cpp | 83 +++++++++++++++++++++++++++++++++- src/tuning.cpp | 34 +++++++++++++- 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index e2cbd21..c6a827d 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; + + // King-pressure weights are applied for every square an attacking + // piece controls in an enemy king's adjacent ring. A direct attack + // on the king receives the additional check bonus. + int kingPressurePawn = 2; + int kingPressureKnight = 4; + int kingPressureBishop = 4; + int kingPressureRook = 5; + int kingPressureQueen = 6; + int kingPressureCheckBonus = 16; }; [[nodiscard]] SearchParameters& search() noexcept; diff --git a/src/eval.cpp b/src/eval.cpp index f0706bf..434e860 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -34,11 +34,45 @@ namespace ironphoenix::Eval { } } + IRONPHOENIX_FORCE_INLINE int kingPressureWeight(PieceType pt) noexcept { + const auto& params = Tuning::eval(); + + switch (pt) { + case PAWN: return params.kingPressurePawn; + case KNIGHT: return params.kingPressureKnight; + case BISHOP: return params.kingPressureBishop; + case ROOK: return params.kingPressureRook; + case QUEEN: return params.kingPressureQueen; + default: return 0; + } + } + 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 Bitboard attacksFrom( + Color c, + PieceType pt, + Square sq, + Bitboard occ) noexcept { + switch (pt) { + case PAWN: + return Geometry::PawnAttacks[c][sq]; + case KNIGHT: + return Geometry::KnightAttacks[sq]; + case BISHOP: + return Geometry::bishopAttacks(sq, occ); + case ROOK: + return Geometry::rookAttacks(sq, occ); + case QUEEN: + return Geometry::queenAttacks(sq, occ); + default: + return Bitboard{}; + } + } + int materialForColor(const Position& pos, Color c) noexcept { int material = 0; @@ -93,9 +127,56 @@ namespace ironphoenix::Eval { return score; } + int kingPressureForColor(const Position& pos, Color c) noexcept { + const Bitboard occ = pos.occupancy(); + const auto& params = Tuning::eval(); + const int ourTeam = teamOf(c); + + int score = 0; + + // In Teams, each player contributes pressure against both enemy + // kings. Summing playerScore() for the two partners naturally + // combines their attacks into one team score. + for (unsigned enemyIndex = 0; enemyIndex < COLOR_NB; ++enemyIndex) { + const Color enemy = static_cast(enemyIndex); + if (teamOf(enemy) == ourTeam) + continue; + + const Square king = pos.kingSquare(enemy); + if (king == SQ_NONE) + continue; + + const Bitboard kingRing = Geometry::KingAttacks[king]; + + for (unsigned ptIndex = PAWN; ptIndex <= QUEEN; ++ptIndex) { + const PieceType pt = static_cast(ptIndex); + const int weight = kingPressureWeight(pt); + Bitboard pieces = pos.pieces(c, pt); + + while (pieces) { + const Square sq = popLsb(pieces); + const Bitboard attacks = attacksFrom(c, pt, sq, occ); + + // Every controlled escape/adjacent square adds pressure. + // Multiple attackers deliberately stack: coordinated + // attacks from partners are especially important in 4PC. + score += (attacks & kingRing).popcount() * weight; + + // A direct attack on the king is much more urgent in + // IronPhoenix because an enemy king capture is terminal. + if (attacks.test(king)) + score += params.kingPressureCheckBonus; + } + } + } + + return score; + } + int playerScore(const Position& pos, Color c) noexcept { return materialForColor(pos, c) - + mobilityForColor(pos, c); + + mobilityForColor(pos, c) + + kingPressureForColor(pos, c); } int handcraftedEvaluate(const Position& pos) noexcept { diff --git a/src/tuning.cpp b/src/tuning.cpp index bffd563..b22ea59 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -58,7 +58,13 @@ namespace ironphoenix::Tuning { || name == "EvalKnightMobility" || name == "EvalBishopMobility" || name == "EvalRookMobility" - || name == "EvalQueenMobility"; + || name == "EvalQueenMobility" + || name == "EvalKingPressurePawn" + || name == "EvalKingPressureKnight" + || name == "EvalKingPressureBishop" + || name == "EvalKingPressureRook" + || name == "EvalKingPressureQueen" + || name == "EvalKingPressureCheckBonus"; } bool isLmrParameter(std::string_view name) noexcept { @@ -155,6 +161,18 @@ namespace ironphoenix::Tuning { p.rookMobility = value; else if (name == "EvalQueenMobility" && inRange(value, -100, 100)) p.queenMobility = value; + else if (name == "EvalKingPressurePawn" && inRange(value, -100, 100)) + p.kingPressurePawn = value; + else if (name == "EvalKingPressureKnight" && inRange(value, -100, 100)) + p.kingPressureKnight = value; + else if (name == "EvalKingPressureBishop" && inRange(value, -100, 100)) + p.kingPressureBishop = value; + else if (name == "EvalKingPressureRook" && inRange(value, -100, 100)) + p.kingPressureRook = value; + else if (name == "EvalKingPressureQueen" && inRange(value, -100, 100)) + p.kingPressureQueen = value; + else if (name == "EvalKingPressureCheckBonus" && inRange(value, -500, 500)) + p.kingPressureCheckBonus = value; else return false; @@ -228,6 +246,12 @@ 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 EvalKingPressurePawn type spin default 2 min -100 max 100\n" + << "option name EvalKingPressureKnight type spin default 4 min -100 max 100\n" + << "option name EvalKingPressureBishop type spin default 4 min -100 max 100\n" + << "option name EvalKingPressureRook type spin default 5 min -100 max 100\n" + << "option name EvalKingPressureQueen type spin default 6 min -100 max 100\n" + << "option name EvalKingPressureCheckBonus type spin default 16 min -500 max 500\n" << "option name ResetTuning type button\n"; } @@ -256,7 +280,13 @@ namespace ironphoenix::Tuning { << " EvalKnightMobility=" << e.knightMobility << " EvalBishopMobility=" << e.bishopMobility << " EvalRookMobility=" << e.rookMobility - << " EvalQueenMobility=" << e.queenMobility << '\n'; + << " EvalQueenMobility=" << e.queenMobility + << " EvalKingPressurePawn=" << e.kingPressurePawn + << " EvalKingPressureKnight=" << e.kingPressureKnight + << " EvalKingPressureBishop=" << e.kingPressureBishop + << " EvalKingPressureRook=" << e.kingPressureRook + << " EvalKingPressureQueen=" << e.kingPressureQueen + << " EvalKingPressureCheckBonus=" << e.kingPressureCheckBonus << '\n'; } } // namespace ironphoenix::Tuning From d23ea33246c5406e84c2ae7119b00ee2cffbfa52 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 16:41:49 -0500 Subject: [PATCH 2/6] Refine king pressure evaluation --- src/eval.cpp | 84 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/src/eval.cpp b/src/eval.cpp index 434e860..75bf2bd 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 { @@ -127,19 +129,15 @@ namespace ironphoenix::Eval { return score; } - int kingPressureForColor(const Position& pos, Color c) noexcept { + int kingPressureForTeam(const Position& pos, int attackingTeam) noexcept { const Bitboard occ = pos.occupancy(); const auto& params = Tuning::eval(); - const int ourTeam = teamOf(c); int score = 0; - // In Teams, each player contributes pressure against both enemy - // kings. Summing playerScore() for the two partners naturally - // combines their attacks into one team score. for (unsigned enemyIndex = 0; enemyIndex < COLOR_NB; ++enemyIndex) { const Color enemy = static_cast(enemyIndex); - if (teamOf(enemy) == ourTeam) + if (teamOf(enemy) == attackingTeam) continue; const Square king = pos.kingSquare(enemy); @@ -147,27 +145,59 @@ namespace ironphoenix::Eval { continue; const Bitboard kingRing = Geometry::KingAttacks[king]; + Bitboard coveredRing{}; + int attackerCount = 0; + int attackUnits = 0; + int directAttackers = 0; + + // Evaluate the two partners together. This prevents the same + // king-ring square from being rewarded twice just because both + // partners attack it, while still rewarding real coordination. + for (unsigned ci = 0; ci < COLOR_NB; ++ci) { + const Color c = static_cast(ci); + if (teamOf(c) != attackingTeam) + continue; + + for (unsigned ptIndex = PAWN; ptIndex <= QUEEN; ++ptIndex) { + const PieceType pt = static_cast(ptIndex); + Bitboard pieces = pos.pieces(c, pt); + + while (pieces) { + const Square sq = popLsb(pieces); + const Bitboard attacks = attacksFrom(c, pt, sq, occ); + const Bitboard ringHits = attacks & kingRing; + const bool directAttack = attacks.test(king); + + if (!ringHits && !directAttack) + continue; + + ++attackerCount; + attackUnits += kingPressureWeight(pt); + coveredRing |= ringHits; + + if (directAttack) + ++directAttackers; + } + } + } - for (unsigned ptIndex = PAWN; ptIndex <= QUEEN; ++ptIndex) { - const PieceType pt = static_cast(ptIndex); - const int weight = kingPressureWeight(pt); - Bitboard pieces = pos.pieces(c, pt); - - while (pieces) { - const Square sq = popLsb(pieces); - const Bitboard attacks = attacksFrom(c, pt, sq, occ); - - // Every controlled escape/adjacent square adds pressure. - // Multiple attackers deliberately stack: coordinated - // attacks from partners are especially important in 4PC. - score += (attacks & kingRing).popcount() * weight; + // A single piece hovering near the king is usually tactical + // noise that search can resolve. Require coordinated pressure + // before awarding the positional king-zone term. + if (attackerCount >= 2) { + const int coverage = coveredRing.popcount(); + const int coordinatedAttackers = std::min(attackerCount, 4); - // A direct attack on the king is much more urgent in - // IronPhoenix because an enemy king capture is terminal. - if (attacks.test(king)) - score += params.kingPressureCheckBonus; - } + score += attackUnits; + score += coverage * 2; + score += (coordinatedAttackers - 1) * 2; } + + // Direct king attacks still matter because king capture is + // terminal in 4PC, but cap repeated bonuses so the evaluation + // does not drown out material and mobility. + if (directAttackers > 0) + score += std::min(directAttackers, 2) * params.kingPressureCheckBonus; } return score; @@ -175,8 +205,7 @@ namespace ironphoenix::Eval { int playerScore(const Position& pos, Color c) noexcept { return materialForColor(pos, c) - + mobilityForColor(pos, c) - + kingPressureForColor(pos, c); + + mobilityForColor(pos, c); } int handcraftedEvaluate(const Position& pos) noexcept { @@ -193,6 +222,9 @@ namespace ironphoenix::Eval { team1 += score; } + team0 += kingPressureForTeam(pos, 0); + team1 += kingPressureForTeam(pos, 1); + const int score = team0 - team1; return teamOf(pos.sideToMove()) == 0 ? score : -score; } From 4dbfa34e5566672f891dc6967f1a68fcb52e54bd Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 16:42:00 -0500 Subject: [PATCH 3/6] Document coordinated king pressure weights --- include/ironphoenix/tuning.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index c6a827d..b9b1cdc 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -35,9 +35,10 @@ namespace ironphoenix { int rookMobility = 2; int queenMobility = 1; - // King-pressure weights are applied for every square an attacking - // piece controls in an enemy king's adjacent ring. A direct attack - // on the king receives the additional check bonus. + // King-pressure piece weights are applied once per attacking piece + // that reaches an enemy king zone. The evaluator separately rewards + // unique ring coverage and coordinated attacks from the two partners. + // Direct attacks on the king receive the additional check bonus. int kingPressurePawn = 2; int kingPressureKnight = 4; int kingPressureBishop = 4; From 0251b33cd3adc7226f88d02d4a8ce0971a6312a6 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 17:19:09 -0500 Subject: [PATCH 4/6] Make king pressure tempo-aware --- src/eval.cpp | 68 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/src/eval.cpp b/src/eval.cpp index 75bf2bd..380e1d6 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -132,6 +132,7 @@ namespace ironphoenix::Eval { int kingPressureForTeam(const Position& pos, int attackingTeam) noexcept { const Bitboard occ = pos.occupancy(); const auto& params = Tuning::eval(); + const Color sideToMove = pos.sideToMove(); int score = 0; @@ -144,15 +145,22 @@ namespace ironphoenix::Eval { if (king == SQ_NONE) continue; - const Bitboard kingRing = Geometry::KingAttacks[king]; - Bitboard coveredRing{}; + // Only score king-ring squares the defending king could at least + // potentially use. Friendly pieces around the defending king are + // shields, not escape squares, so do not reward pseudo-pressure + // against those occupied squares. + const Bitboard kingZone = Geometry::KingAttacks[king] + & ~teamOccupancy(pos, enemy); + + Bitboard coveredZone{}; int attackerCount = 0; int attackUnits = 0; - int directAttackers = 0; + bool immediateDirectAttack = false; - // Evaluate the two partners together. This prevents the same - // king-ring square from being rewarded twice just because both - // partners attack it, while still rewarding real coordination. + // Evaluate the two partners together so overlapping pressure is + // counted once. Positional king pressure may come from either + // partner, but a direct king attack is only immediately urgent + // when it belongs to the actual side to move. for (unsigned ci = 0; ci < COLOR_NB; ++ci) { const Color c = static_cast(ci); if (teamOf(c) != attackingTeam) @@ -160,44 +168,48 @@ namespace ironphoenix::Eval { for (unsigned ptIndex = PAWN; ptIndex <= QUEEN; ++ptIndex) { const PieceType pt = static_cast(ptIndex); + const int weight = kingPressureWeight(pt); Bitboard pieces = pos.pieces(c, pt); while (pieces) { const Square sq = popLsb(pieces); const Bitboard attacks = attacksFrom(c, pt, sq, occ); - const Bitboard ringHits = attacks & kingRing; + const Bitboard zoneHits = attacks & kingZone; const bool directAttack = attacks.test(king); - if (!ringHits && !directAttack) - continue; - - ++attackerCount; - attackUnits += kingPressureWeight(pt); - coveredRing |= ringHits; - - if (directAttack) - ++directAttackers; + // A zero weight cleanly disables this piece type from + // the positional king-pressure term while preserving + // genuinely immediate king attacks. + if (weight != 0 && zoneHits) { + ++attackerCount; + attackUnits += weight; + coveredZone |= zoneHits; + } + + if (c == sideToMove && directAttack) + immediateDirectAttack = true; } } } - // A single piece hovering near the king is usually tactical - // noise that search can resolve. Require coordinated pressure - // before awarding the positional king-zone term. - if (attackerCount >= 2) { - const int coverage = coveredRing.popcount(); + const int coverage = coveredZone.popcount(); + + // Require both multiple attackers and multiple useful king-zone + // squares. This filters out single-ray and one-square pressure + // that search already handles well. + if (attackerCount >= 2 && coverage >= 2) { const int coordinatedAttackers = std::min(attackerCount, 4); score += attackUnits; - score += coverage * 2; - score += (coordinatedAttackers - 1) * 2; + score += coverage; + score += coordinatedAttackers - 1; } - // Direct king attacks still matter because king capture is - // terminal in 4PC, but cap repeated bonuses so the evaluation - // does not drown out material and mobility. - if (directAttackers > 0) - score += std::min(directAttackers, 2) * params.kingPressureCheckBonus; + // King capture is terminal, but extra checking pieces do not make + // one available king capture more terminal. Award this once per + // enemy king and only to the color that can move right now. + if (immediateDirectAttack) + score += params.kingPressureCheckBonus; } return score; From d55e666326162ac18b550705122e57b223dedbf8 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 17:19:17 -0500 Subject: [PATCH 5/6] Retune king pressure defaults --- include/ironphoenix/tuning.hpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index b9b1cdc..89ffc0c 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -35,16 +35,17 @@ namespace ironphoenix { int rookMobility = 2; int queenMobility = 1; - // King-pressure piece weights are applied once per attacking piece - // that reaches an enemy king zone. The evaluator separately rewards - // unique ring coverage and coordinated attacks from the two partners. - // Direct attacks on the king receive the additional check bonus. - int kingPressurePawn = 2; - int kingPressureKnight = 4; - int kingPressureBishop = 4; - int kingPressureRook = 5; - int kingPressureQueen = 6; - int kingPressureCheckBonus = 16; + // King-pressure piece weights are applied once per meaningful + // attacker that reaches usable squares around an enemy king. + // Pawn pressure starts disabled because pawn pseudo-attacks were + // especially noisy in early SPRT runs. Direct attacks on the king + // receive a smaller tempo-aware bonus only for the side to move. + int kingPressurePawn = 0; + int kingPressureKnight = 3; + int kingPressureBishop = 3; + int kingPressureRook = 4; + int kingPressureQueen = 5; + int kingPressureCheckBonus = 12; }; [[nodiscard]] SearchParameters& search() noexcept; From 331ce4ca32a4045ed2158fd2e6dbc8b09fdff624 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 9 Oct 2026 17:19:45 -0500 Subject: [PATCH 6/6] Update king pressure UCI defaults --- src/tuning.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tuning.cpp b/src/tuning.cpp index b22ea59..840a208 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -246,12 +246,12 @@ 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 EvalKingPressurePawn type spin default 2 min -100 max 100\n" - << "option name EvalKingPressureKnight type spin default 4 min -100 max 100\n" - << "option name EvalKingPressureBishop type spin default 4 min -100 max 100\n" - << "option name EvalKingPressureRook type spin default 5 min -100 max 100\n" - << "option name EvalKingPressureQueen type spin default 6 min -100 max 100\n" - << "option name EvalKingPressureCheckBonus type spin default 16 min -500 max 500\n" + << "option name EvalKingPressurePawn type spin default 0 min -100 max 100\n" + << "option name EvalKingPressureKnight type spin default 3 min -100 max 100\n" + << "option name EvalKingPressureBishop type spin default 3 min -100 max 100\n" + << "option name EvalKingPressureRook type spin default 4 min -100 max 100\n" + << "option name EvalKingPressureQueen type spin default 5 min -100 max 100\n" + << "option name EvalKingPressureCheckBonus type spin default 12 min -500 max 500\n" << "option name ResetTuning type button\n"; }