diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index e2cbd21..68da027 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; + + // Development rewards pieces that leave their player's home back + // line. The score is tapered by remaining non-pawn material so the + // term naturally fades as the game leaves the opening. + int developmentKnight = 8; + int developmentBishop = 8; + int developmentRook = 2; + int developmentQueen = 0; }; [[nodiscard]] SearchParameters& search() noexcept; diff --git a/src/eval.cpp b/src/eval.cpp index f0706bf..53b2386 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -5,10 +5,14 @@ #include "ironphoenix/position.hpp" #include "ironphoenix/tuning.hpp" +#include + namespace ironphoenix::Eval { namespace { + constexpr int STARTING_NON_PAWN_MATERIAL = 13'600; + IRONPHOENIX_FORCE_INLINE int evalValue(PieceType pt) noexcept { const auto& params = Tuning::eval(); @@ -34,11 +38,33 @@ namespace ironphoenix::Eval { } } + IRONPHOENIX_FORCE_INLINE int developmentWeight(PieceType pt) noexcept { + const auto& params = Tuning::eval(); + + switch (pt) { + case KNIGHT: return params.developmentKnight; + case BISHOP: return params.developmentBishop; + case ROOK: return params.developmentRook; + case QUEEN: return params.developmentQueen; + 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 bool onHomeBackLine(Color c, Square sq) noexcept { + switch (c) { + case RED: return Geometry::rankOf(sq) == 0; + case BLUE: return Geometry::fileOf(sq) == 0; + case YELLOW: return Geometry::rankOf(sq) == BOARD_RANKS - 1; + case GREEN: return Geometry::fileOf(sq) == BOARD_FILES - 1; + } + return false; + } + int materialForColor(const Position& pos, Color c) noexcept { int material = 0; @@ -93,18 +119,62 @@ namespace ironphoenix::Eval { return score; } - int playerScore(const Position& pos, Color c) noexcept { + int remainingNonPawnMaterial(const Position& pos) noexcept { + int material = 0; + + for (unsigned ci = 0; ci < COLOR_NB; ++ci) { + const Color c = static_cast(ci); + for (unsigned pt = KNIGHT; pt <= QUEEN; ++pt) { + const PieceType pieceType = static_cast(pt); + material += pos.pieces(c, pieceType).popcount() * pieceValue(pieceType); + } + } + + // Promotions can take the board above starting material. Development + // should never grow beyond its full opening weight. + return std::min(material, STARTING_NON_PAWN_MATERIAL); + } + + int developmentForColor(const Position& pos, Color c, int phaseMaterial) noexcept { + int score = 0; + + // Development is position based rather than move-history based: a + // surviving piece earns its bonus while it is off the player's home + // back line. A piece that is captured cannot continue earning it. + for (unsigned pt = KNIGHT; pt <= QUEEN; ++pt) { + const PieceType pieceType = static_cast(pt); + const int weight = developmentWeight(pieceType); + if (weight == 0) + continue; + + Bitboard pieces = pos.pieces(c, pieceType); + while (pieces) { + const Square sq = popLsb(pieces); + if (!onHomeBackLine(c, sq)) + score += weight; + } + } + + // Taper toward zero as non-pawn material disappears. This keeps the + // term focused on opening development instead of rewarding arbitrary + // piece placement in simplified middlegames and endgames. + return score * phaseMaterial / STARTING_NON_PAWN_MATERIAL; + } + + int playerScore(const Position& pos, Color c, int phaseMaterial) noexcept { return materialForColor(pos, c) - + mobilityForColor(pos, c); + + mobilityForColor(pos, c) + + developmentForColor(pos, c, phaseMaterial); } int handcraftedEvaluate(const Position& pos) noexcept { int team0 = 0; int team1 = 0; + const int phaseMaterial = remainingNonPawnMaterial(pos); for (unsigned ci = 0; ci < COLOR_NB; ++ci) { const Color c = static_cast(ci); - const int score = playerScore(pos, c); + const int score = playerScore(pos, c, phaseMaterial); if (teamOf(c) == 0) team0 += score; diff --git a/src/tuning.cpp b/src/tuning.cpp index bffd563..04854b7 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -58,7 +58,11 @@ namespace ironphoenix::Tuning { || name == "EvalKnightMobility" || name == "EvalBishopMobility" || name == "EvalRookMobility" - || name == "EvalQueenMobility"; + || name == "EvalQueenMobility" + || name == "EvalDevelopmentKnight" + || name == "EvalDevelopmentBishop" + || name == "EvalDevelopmentRook" + || name == "EvalDevelopmentQueen"; } bool isLmrParameter(std::string_view name) noexcept { @@ -155,6 +159,14 @@ namespace ironphoenix::Tuning { p.rookMobility = value; else if (name == "EvalQueenMobility" && inRange(value, -100, 100)) p.queenMobility = value; + else if (name == "EvalDevelopmentKnight" && inRange(value, -100, 100)) + p.developmentKnight = value; + else if (name == "EvalDevelopmentBishop" && inRange(value, -100, 100)) + p.developmentBishop = value; + else if (name == "EvalDevelopmentRook" && inRange(value, -100, 100)) + p.developmentRook = value; + else if (name == "EvalDevelopmentQueen" && inRange(value, -100, 100)) + p.developmentQueen = value; else return false; @@ -228,6 +240,10 @@ 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 EvalDevelopmentKnight type spin default 8 min -100 max 100\n" + << "option name EvalDevelopmentBishop type spin default 8 min -100 max 100\n" + << "option name EvalDevelopmentRook type spin default 2 min -100 max 100\n" + << "option name EvalDevelopmentQueen type spin default 0 min -100 max 100\n" << "option name ResetTuning type button\n"; } @@ -256,7 +272,11 @@ namespace ironphoenix::Tuning { << " EvalKnightMobility=" << e.knightMobility << " EvalBishopMobility=" << e.bishopMobility << " EvalRookMobility=" << e.rookMobility - << " EvalQueenMobility=" << e.queenMobility << '\n'; + << " EvalQueenMobility=" << e.queenMobility + << " EvalDevelopmentKnight=" << e.developmentKnight + << " EvalDevelopmentBishop=" << e.developmentBishop + << " EvalDevelopmentRook=" << e.developmentRook + << " EvalDevelopmentQueen=" << e.developmentQueen << '\n'; } } // namespace ironphoenix::Tuning