Skip to content
Closed
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
1 change: 1 addition & 0 deletions include/ironphoenix/tuning.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ namespace ironphoenix {
int bishopMobility = 4;
int rookMobility = 2;
int queenMobility = 1;
int queenInfiltration = 2;
};

[[nodiscard]] SearchParameters& search() noexcept;
Expand Down
78 changes: 77 additions & 1 deletion src/eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#include "ironphoenix/position.hpp"
#include "ironphoenix/tuning.hpp"

#include <algorithm>
#include <cstdlib>

namespace ironphoenix::Eval {

namespace {
Expand Down Expand Up @@ -92,9 +95,82 @@ namespace ironphoenix::Eval {
return score;
}

// Distance past the board's central boundary towards an enemy home edge.
// The four home edges rotate, so ordinary two-player rank advancement
// cannot describe queen infiltration on a cross-shaped 4PC board.
IRONPHOENIX_FORCE_INLINE int enemyCampDepth(Color enemyColor, int file, int rank) noexcept {
switch (enemyColor) {
case RED: return 7 - rank;
case BLUE: return 7 - file;
case YELLOW: return rank - 6;
case GREEN: return file - 6;
}
return 0;
}

IRONPHOENIX_FORCE_INLINE bool inHomeCamp(Color color, int file, int rank) noexcept {
switch (color) {
case RED: return rank <= 2;
case BLUE: return file <= 2;
case YELLOW: return rank >= 11;
case GREEN: return file >= 11;
}
return false;
}

int queenInfiltrationForColor(const Position& pos, Color c) noexcept {
const int weight = Tuning::eval().queenInfiltration;
if (weight == 0)
return 0;

int score = 0;
Bitboard queens = pos.pieces(c, QUEEN);
const Bitboard occupancy = pos.occupancy();

while (queens) {
const Square sq = popLsb(queens);
const int file = static_cast<int>(Geometry::fileOf(sq));
const int rank = static_cast<int>(Geometry::rankOf(sq));

// Do not reward a queen that has not left its starting camp,
// or one that the opposing team can capture immediately.
if (inHomeCamp(c, file, rank)
|| pos.isAttackedByEnemy(sq, c, occupancy))
continue;

for (unsigned ci = 0; ci < COLOR_NB; ++ci) {
const Color opponent = static_cast<Color>(ci);
if (!enemy(c, opponent) || !pos.isAlive(opponent))
continue;

const Square king = pos.kingSquare(opponent);
if (king == SQ_NONE)
continue;

// A shallow central queen is not an infiltration.
const int depth = enemyCampDepth(opponent, file, rank);
if (depth < 2)
continue;

const int kingFile = static_cast<int>(Geometry::fileOf(king));
const int kingRank = static_cast<int>(Geometry::rankOf(king));
const int distance = std::max(std::abs(file - kingFile), std::abs(rank - kingRank));
if (distance > 6)
continue;

// More credit for penetrating deeper while staying close
// enough to an enemy king to create threats.
score += (depth - 1) * (7 - distance) * weight;
}
}

return score;
}

int playerScore(const Position& pos, Color c) noexcept {
return materialForColor(pos, c)
+ mobilityForColor(pos, c);
+ mobilityForColor(pos, c)
+ queenInfiltrationForColor(pos, c);
}

int handcraftedEvaluate(const Position& pos) noexcept {
Expand Down
9 changes: 7 additions & 2 deletions src/tuning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ namespace ironphoenix::Tuning {
|| name == "EvalKnightMobility"
|| name == "EvalBishopMobility"
|| name == "EvalRookMobility"
|| name == "EvalQueenMobility";
|| name == "EvalQueenMobility"
|| name == "EvalQueenInfiltration";
}

bool isLmrParameter(std::string_view name) noexcept {
Expand Down Expand Up @@ -155,6 +156,8 @@ namespace ironphoenix::Tuning {
p.rookMobility = value;
else if (name == "EvalQueenMobility" && inRange(value, -100, 100))
p.queenMobility = value;
else if (name == "EvalQueenInfiltration" && inRange(value, 0, 20))
p.queenInfiltration = value;
else
return false;

Expand Down Expand Up @@ -228,6 +231,7 @@ 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 EvalQueenInfiltration type spin default 2 min 0 max 20\n"
<< "option name ResetTuning type button\n";
}

Expand Down Expand Up @@ -256,7 +260,8 @@ namespace ironphoenix::Tuning {
<< " EvalKnightMobility=" << e.knightMobility
<< " EvalBishopMobility=" << e.bishopMobility
<< " EvalRookMobility=" << e.rookMobility
<< " EvalQueenMobility=" << e.queenMobility << '\n';
<< " EvalQueenMobility=" << e.queenMobility
<< " EvalQueenInfiltration=" << e.queenInfiltration << '\n';
}

} // namespace ironphoenix::Tuning
44 changes: 44 additions & 0 deletions tests/smoke.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "ironphoenix/eval.hpp"
#include "ironphoenix/geometry.hpp"
#include "ironphoenix/position.hpp"
#include "ironphoenix/see.hpp"
Expand All @@ -18,6 +19,26 @@ static void putKings(Position& p) {
p.placePiece(sq('n', 7), makePiece(GREEN, KING));
}

// Measure only the new queen term, holding material and mobility constant.
static int queenInfiltrationDelta(Color queenColor, char file, int rank, bool threatened = false) {
Position p;
p.clear();
putKings(p);
p.placePiece(sq(file, rank), makePiece(queenColor, QUEEN));
if (threatened)
p.placePiece(sq('d', 9), makePiece(BLUE, ROOK));

p.setSideToMove(queenColor);
p.finalizeSetup();

auto& params = Eval::parameters();
const int saved = params.queenInfiltration;
params.queenInfiltration = 0;
const int baseline = Eval::evaluate(p);
params.queenInfiltration = saved;
return Eval::evaluate(p) - baseline;
}

int main() {
Geometry::init();

Expand Down Expand Up @@ -99,6 +120,29 @@ int main() {
assert(Geometry::isPromotionSquare(YELLOW, sq('g', 4)));
assert(Geometry::isPromotionSquare(GREEN, sq('d', 7)));

// Do not hide these checks behind assert: CI runs Release with NDEBUG.
Tuning::resetEval();
const bool allOrientations =
queenInfiltrationDelta(RED, 'd', 7) > 0 // Blue home edge
&& queenInfiltrationDelta(YELLOW, 'k', 7) > 0 // Green home edge
&& queenInfiltrationDelta(BLUE, 'g', 4) > 0 // Red home edge
&& queenInfiltrationDelta(GREEN, 'g', 11) > 0; // Yellow home edge

// No reward for own/allied territory or an enemy-attacked queen.
const bool safeOnly =
queenInfiltrationDelta(RED, 'g', 4) == 0
&& queenInfiltrationDelta(RED, 'g', 11) == 0
&& queenInfiltrationDelta(RED, 'd', 7, true) == 0;

const bool tunable = Tuning::setEvalParameter("EvalQueenInfiltration", 0)
&& queenInfiltrationDelta(RED, 'd', 7) == 0;
Tuning::resetEval();

if (!allOrientations || !safeOnly || !tunable) {
std::cerr << "queen infiltration evaluation tests: FAIL\n";
return 1;
}

std::cout << "nexus_fast_board smoke tests: PASS\n";
return 0;
}
Loading