diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index e2cbd21..8b1a018 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; + + // 4PC-oriented positional HCE terms. These intentionally start + // conservative so each can be strength-tested and tuned. + int bishopPairBonus = 30; + int supportedPawnBonus = 6; + int kingPawnShieldBonus = 6; + int checkPenalty = 40; + int kingZoneAttackPenalty = 5; + int partnerSupportBonus = 4; + int tempoBonus = 8; }; [[nodiscard]] SearchParameters& search() noexcept; diff --git a/src/eval.cpp b/src/eval.cpp index f0706bf..745d5df 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -34,9 +34,12 @@ namespace ironphoenix::Eval { } } + IRONPHOENIX_FORCE_INLINE Color partnerOf(Color c) noexcept { + return static_cast(static_cast(c) ^ 2u); + } + 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); + return pos.occupancy(c) | pos.occupancy(partnerOf(c)); } int materialForColor(const Position& pos, Color c) noexcept { @@ -93,9 +96,108 @@ 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 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); + + // 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; + } + + return score; + } + + int kingSafetyForColor(const Position& pos, Color c) noexcept { + if (!pos.isAlive(c)) + 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; + + // 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 partnerCoordinationForColor(const Position& pos, Color c) noexcept { + const Color partner = partnerOf(c); + if (!pos.isAlive(partner)) + return 0; + + const auto& params = Tuning::eval(); + const Bitboard occ = pos.occupancy(); + int score = 0; + + // 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; + } + 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) + + partnerCoordinationForColor(pos, c); } int handcraftedEvaluate(const Position& pos) noexcept { @@ -113,7 +215,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; } } diff --git a/src/tuning.cpp b/src/tuning.cpp index bffd563..6ef1085 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 == "EvalSupportedPawnBonus" + || name == "EvalKingPawnShieldBonus" + || name == "EvalCheckPenalty" + || name == "EvalKingZoneAttackPenalty" + || name == "EvalPartnerSupportBonus" + || 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 == "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 == "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 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 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 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"; } @@ -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 + << " EvalSupportedPawnBonus=" << e.supportedPawnBonus + << " EvalKingPawnShieldBonus=" << e.kingPawnShieldBonus + << " EvalCheckPenalty=" << e.checkPenalty + << " EvalKingZoneAttackPenalty=" << e.kingZoneAttackPenalty + << " EvalPartnerSupportBonus=" << e.partnerSupportBonus + << " EvalTempoBonus=" << e.tempoBonus << '\n'; } } // namespace ironphoenix::Tuning