diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index e2cbd21..89ffc0c 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -34,6 +34,18 @@ namespace ironphoenix { int bishopMobility = 4; int rookMobility = 2; int queenMobility = 1; + + // 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; diff --git a/src/eval.cpp b/src/eval.cpp index f0706bf..380e1d6 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 { @@ -34,11 +36,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,6 +129,92 @@ namespace ironphoenix::Eval { return score; } + 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; + + for (unsigned enemyIndex = 0; enemyIndex < COLOR_NB; ++enemyIndex) { + const Color enemy = static_cast(enemyIndex); + if (teamOf(enemy) == attackingTeam) + continue; + + const Square king = pos.kingSquare(enemy); + if (king == SQ_NONE) + continue; + + // 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; + bool immediateDirectAttack = false; + + // 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) + continue; + + 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 zoneHits = attacks & kingZone; + const bool directAttack = attacks.test(king); + + // 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; + } + } + } + + 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; + score += coordinatedAttackers - 1; + } + + // 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; + } + int playerScore(const Position& pos, Color c) noexcept { return materialForColor(pos, c) + mobilityForColor(pos, c); @@ -112,6 +234,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; } diff --git a/src/tuning.cpp b/src/tuning.cpp index bffd563..840a208 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 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"; } @@ -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