diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index e2cbd21..da4ca3d 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -34,6 +34,14 @@ namespace ironphoenix { int bishopMobility = 4; int rookMobility = 2; int queenMobility = 1; + + // Space rewards unique control of empty forward squares. Infiltration + // rewards non-pawn pieces that penetrate deeply toward the enemy side. + int spaceControl = 1; + int infiltrationKnight = 3; + int infiltrationBishop = 3; + int infiltrationRook = 2; + int infiltrationQueen = 1; }; [[nodiscard]] SearchParameters& search() noexcept; diff --git a/src/eval.cpp b/src/eval.cpp index f0706bf..7485a1a 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -5,6 +5,9 @@ #include "ironphoenix/position.hpp" #include "ironphoenix/tuning.hpp" +#include +#include + namespace ironphoenix::Eval { namespace { @@ -34,11 +37,82 @@ namespace ironphoenix::Eval { } } + IRONPHOENIX_FORCE_INLINE int infiltrationWeight(PieceType pt) noexcept { + const auto& params = Tuning::eval(); + + switch (pt) { + case KNIGHT: return params.infiltrationKnight; + case BISHOP: return params.infiltrationBishop; + case ROOK: return params.infiltrationRook; + case QUEEN: return params.infiltrationQueen; + 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{}; + } + } + + // Normalize a square into forward progress for each seat. The pawn home + // line is progress 0 and the promotion line is progress 9 for all colors. + IRONPHOENIX_FORCE_INLINE int forwardProgress(Color c, Square sq) noexcept { + const int file = static_cast(Geometry::fileOf(sq)); + const int rank = static_cast(Geometry::rankOf(sq)); + + switch (c) { + case RED: return rank - 1; + case BLUE: return file - 1; + case YELLOW: return 12 - rank; + case GREEN: return 12 - file; + } + return 0; + } + + const Bitboard& spaceMask(Color c) noexcept { + // Build the four directional masks once, after Geometry::init() has + // initialized FileOf/RankOf. Progress 4..9 represents useful forward + // space from the middle of the board through the promotion line. + static const std::array masks = [] { + std::array result{}; + + for (unsigned ci = 0; ci < COLOR_NB; ++ci) { + const Color color = static_cast(ci); + for (unsigned si = 0; si < SQUARE_NB; ++si) { + const Square sq = static_cast(si); + const int progress = forwardProgress(color, sq); + if (progress >= 4 && progress <= 9) + result[ci].set(sq); + } + } + + return result; + }(); + + return masks[static_cast(c)]; + } + int materialForColor(const Position& pos, Color c) noexcept { int material = 0; @@ -93,9 +167,59 @@ namespace ironphoenix::Eval { return score; } + int spaceAndInfiltrationForColor(const Position& pos, Color c) noexcept { + const Bitboard occ = pos.occupancy(); + const auto& params = Tuning::eval(); + + // Space is unique control of empty squares in the player's forward + // half. Using a union keeps several pieces attacking the same square + // from artificially inflating the score. + Bitboard controlled{}; + 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); + controlled |= attacksFrom(c, pt, sq, occ); + } + } + + controlled &= spaceMask(c); + controlled &= ~occ; + + int score = controlled.popcount() * params.spaceControl; + + // Infiltration rewards minor/major pieces that physically penetrate + // into the opponent-facing side of the board. Pawns are intentionally + // excluded here because promotion potential belongs in a pawn term. + for (unsigned ptIndex = KNIGHT; ptIndex <= QUEEN; ++ptIndex) { + const PieceType pt = static_cast(ptIndex); + const int weight = infiltrationWeight(pt); + if (weight == 0) + continue; + + Bitboard pieces = pos.pieces(c, pt); + while (pieces) { + const Square sq = popLsb(pieces); + const int progress = forwardProgress(c, sq); + if (progress < 6) + continue; + + // Depth 1 begins at progress 6. Cap the depth component so a + // piece deep in an outer arm does not overwhelm the HCE. + const int depth = std::min(progress - 5, 6); + score += depth * weight; + } + } + + return score; + } + int playerScore(const Position& pos, Color c) noexcept { return materialForColor(pos, c) - + mobilityForColor(pos, c); + + mobilityForColor(pos, c) + + spaceAndInfiltrationForColor(pos, c); } int handcraftedEvaluate(const Position& pos) noexcept { diff --git a/src/tuning.cpp b/src/tuning.cpp index bffd563..5587bff 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -58,7 +58,12 @@ namespace ironphoenix::Tuning { || name == "EvalKnightMobility" || name == "EvalBishopMobility" || name == "EvalRookMobility" - || name == "EvalQueenMobility"; + || name == "EvalQueenMobility" + || name == "EvalSpaceControl" + || name == "EvalInfiltrationKnight" + || name == "EvalInfiltrationBishop" + || name == "EvalInfiltrationRook" + || name == "EvalInfiltrationQueen"; } bool isLmrParameter(std::string_view name) noexcept { @@ -155,6 +160,16 @@ namespace ironphoenix::Tuning { p.rookMobility = value; else if (name == "EvalQueenMobility" && inRange(value, -100, 100)) p.queenMobility = value; + else if (name == "EvalSpaceControl" && inRange(value, -100, 100)) + p.spaceControl = value; + else if (name == "EvalInfiltrationKnight" && inRange(value, -100, 100)) + p.infiltrationKnight = value; + else if (name == "EvalInfiltrationBishop" && inRange(value, -100, 100)) + p.infiltrationBishop = value; + else if (name == "EvalInfiltrationRook" && inRange(value, -100, 100)) + p.infiltrationRook = value; + else if (name == "EvalInfiltrationQueen" && inRange(value, -100, 100)) + p.infiltrationQueen = value; else return false; @@ -228,6 +243,11 @@ 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 EvalSpaceControl type spin default 1 min -100 max 100\n" + << "option name EvalInfiltrationKnight type spin default 3 min -100 max 100\n" + << "option name EvalInfiltrationBishop type spin default 3 min -100 max 100\n" + << "option name EvalInfiltrationRook type spin default 2 min -100 max 100\n" + << "option name EvalInfiltrationQueen type spin default 1 min -100 max 100\n" << "option name ResetTuning type button\n"; } @@ -256,7 +276,12 @@ namespace ironphoenix::Tuning { << " EvalKnightMobility=" << e.knightMobility << " EvalBishopMobility=" << e.bishopMobility << " EvalRookMobility=" << e.rookMobility - << " EvalQueenMobility=" << e.queenMobility << '\n'; + << " EvalQueenMobility=" << e.queenMobility + << " EvalSpaceControl=" << e.spaceControl + << " EvalInfiltrationKnight=" << e.infiltrationKnight + << " EvalInfiltrationBishop=" << e.infiltrationBishop + << " EvalInfiltrationRook=" << e.infiltrationRook + << " EvalInfiltrationQueen=" << e.infiltrationQueen << '\n'; } } // namespace ironphoenix::Tuning