Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions include/ironphoenix/tuning.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
111 changes: 107 additions & 4 deletions src/eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ namespace ironphoenix::Eval {
}
}

IRONPHOENIX_FORCE_INLINE Color partnerOf(Color c) noexcept {
return static_cast<Color>(static_cast<unsigned>(c) ^ 2u);
}

IRONPHOENIX_FORCE_INLINE Bitboard teamOccupancy(const Position& pos, Color c) noexcept {
const Color partner = static_cast<Color>(static_cast<unsigned>(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 {
Expand Down Expand Up @@ -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<bool>(Geometry::PawnAttackers[c][sq] & ownPawns);
const bool partnerSupport = static_cast<bool>(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<PieceType>(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 {
Expand All @@ -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;
}

}
Expand Down
39 changes: 37 additions & 2 deletions src/tuning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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";
}

Expand Down Expand Up @@ -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
Loading