Skip to content
Open
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
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,12 @@ if(IRONPHOENIX_BUILD_TESTS)
target_link_libraries(ironphoenix_attacking_pieces PRIVATE IronPhoenix::Core)
add_test(NAME ironphoenix_attacking_pieces COMMAND ironphoenix_attacking_pieces)

set_target_properties(ironphoenix_smoke ironphoenix_property ironphoenix_search_stack ironphoenix_mate_stop ironphoenix_reverse_futility ironphoenix_attacking_pieces PROPERTIES
add_executable(ironphoenix_queen_infiltration tests/queen_infiltration.cpp)
target_link_libraries(ironphoenix_queen_infiltration PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_queen_infiltration PRIVATE cxx_std_20)
add_test(NAME ironphoenix_queen_infiltration COMMAND ironphoenix_queen_infiltration)

set_target_properties(ironphoenix_smoke ironphoenix_property ironphoenix_search_stack ironphoenix_mate_stop ironphoenix_reverse_futility ironphoenix_attacking_pieces ironphoenix_queen_infiltration PROPERTIES
FOLDER "Tests"
)
endif()
Expand Down
40 changes: 40 additions & 0 deletions docs/queen-infiltration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Queen Infiltration (Teams handcrafted evaluation)

`queen-infiltration-v2` starts from current `main` and ports the old
four-orientation queen-infiltration feature without discarding newer
material, mobility, attacked-piece evaluation, search or web GUI code.

A queen earns a **small positional bonus** when:

- It has left its own back/home camp.
- It has penetrated at least two squares past the central boundary
towards an **alive enemy** king's side of the four-player cross board.
- It is within Chebyshev distance six of that enemy king.
- Its square is **not attacked by the opposing team**.

Each enemy color is evaluated using its own home-edge direction: RED
from the bottom, BLUE from the left, YELLOW from the top, GREEN from the
right. Teammates' kings do not count as infiltration targets.

For each eligible enemy king:

```text
bonus = (depth_into_enemy_half - 1) * (7 - chebyshev_distance) * EvalQueenInfiltration
```

The default `EvalQueenInfiltration` is **2**, configurable through UCI
from **0** (disabled) to **20**. Example:

```text
setoption name EvalQueenInfiltration value 0
```

The attack-map safety check is made only for queens with a candidate
infiltration bonus. This limits overhead when no enemy camp is reached.

**HCE only:** a loaded NNUE remains authoritative and is unchanged.
The positional bonus is experimental, can influence RFP/NMP static
evaluations, and may or may not improve Elo. Test against `main`
with a matched build and a paired four-player opening suite.

CTest: `ironphoenix_queen_infiltration`.
2 changes: 2 additions & 0 deletions include/ironphoenix/tuning.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ namespace ironphoenix {
int queenMobility = 1;
// 100 = default pressure; 0 = disabled, for A/B tests.
int attackingPiecesWeight = 100;
// Experimental enemy-king camp penetration bonus. Zero disables.
int queenInfiltration = 2;
};

[[nodiscard]] SearchParameters& search() noexcept;
Expand Down
81 changes: 80 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 @@ -126,14 +129,90 @@ namespace ironphoenix::Eval {
return pressure * weight / 100;
}

// 4PC home camps point towards four different board edges.
// A queen that reaches an enemy's half of the cross may develop
// useful pressure against that side's king. This is deliberately
// not the same as an attack on the king or a guaranteed tactic.
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 || !pos.isAlive(c))
return 0;

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

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

// Staying in the queen's own camp is not infiltration.
if (inHomeCamp(c, file, rank))
continue;

int potential = 0;
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;

// Require material penetration beyond the centre,
// not just a queen occupying a central square.
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;

potential += (depth - 1) * (7 - distance);
}

// Avoid expensive attack-map queries for queens without any
// eligible infiltration bonus. Never reward a hanging queen.
if (potential > 0 && !pos.isAttackedByEnemy(queen, c, occupancy))
score += potential * weight;
}
return score;
}

int handcraftedEvaluate(const Position& pos) noexcept {
int team0 = 0, team1 = 0;
Bitboard attacks[2]{};

for (unsigned ci = 0; ci < COLOR_NB; ++ci) {
const Color c = static_cast<Color>(ci);
const int score = materialForColor(pos, c)
+ mobilityForColor(pos, c, attacks[teamOf(c)]);
+ mobilityForColor(pos, c, attacks[teamOf(c)])
+ queenInfiltrationForColor(pos, c);
if (teamOf(c) == 0)
team0 += score;
else
Expand Down
9 changes: 7 additions & 2 deletions src/tuning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ namespace ironphoenix::Tuning {
|| name == "EvalBishopMobility"
|| name == "EvalRookMobility"
|| name == "EvalQueenMobility"
|| name == "EvalAttackingPiecesWeight";
|| name == "EvalAttackingPiecesWeight"
|| name == "EvalQueenInfiltration";
}

bool isLmrParameter(std::string_view name) noexcept {
Expand Down Expand Up @@ -158,6 +159,8 @@ namespace ironphoenix::Tuning {
p.queenMobility = value;
else if (name == "EvalAttackingPiecesWeight" && inRange(value, 0, 300))
p.attackingPiecesWeight = value;
else if (name == "EvalQueenInfiltration" && inRange(value, 0, 20))
p.queenInfiltration = value;
else
return false;

Expand Down Expand Up @@ -232,6 +235,7 @@ namespace ironphoenix::Tuning {
<< "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 EvalAttackingPiecesWeight type spin default 100 min 0 max 300\n"
<< "option name EvalQueenInfiltration type spin default 2 min 0 max 20\n"
<< "option name ResetTuning type button\n";
}

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

} // namespace ironphoenix::Tuning
94 changes: 94 additions & 0 deletions tests/queen_infiltration.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include "ironphoenix/eval.hpp"
#include "ironphoenix/geometry.hpp"
#include "ironphoenix/nnue.hpp"
#include "ironphoenix/position.hpp"
#include "ironphoenix/tuning.hpp"

#include <iostream>

using namespace ironphoenix;

namespace {
int failures = 0;

void expect(bool condition, const char* description) {
if (!condition) {
std::cerr << "FAIL: " << description << '\n';
++failures;
}
}

Square sq(char file, int rank) {
return Geometry::square(file - 'a', rank - 1);
}

Position queenPosition(Color queenColor, char file, int rank,
bool attacked = false) {
Position pos;
pos.clear();
pos.placePiece(sq('g', 1), makePiece(RED, KING));
pos.placePiece(sq('a', 7), makePiece(BLUE, KING));
pos.placePiece(sq('g', 14), makePiece(YELLOW, KING));
pos.placePiece(sq('n', 7), makePiece(GREEN, KING));
pos.placePiece(sq(file, rank), makePiece(queenColor, QUEEN));
if (attacked)
pos.placePiece(sq('d', 9), makePiece(BLUE, ROOK));
pos.setSideToMove(queenColor);
pos.finalizeSetup();
return pos;
}

// Subtract an evaluation of the exact same position with the new term
// disabled. This isolates infiltration from material, mobility and the
// existing attacked-piece score without disabling any other features.
int bonus(const Position& pos, int weight = 2) {
const auto saved = Tuning::eval();
Tuning::eval().queenInfiltration = 0;
const int baseline = Eval::evaluate(pos);
Tuning::eval().queenInfiltration = weight;
const int updated = Eval::evaluate(pos);
Tuning::eval() = saved;
return updated - baseline;
}
}

int main() {
Geometry::init();
expect(!NNUE::loaded(), "tests run in HCE mode");
Tuning::resetEval();
expect(Tuning::eval().queenInfiltration == 2, "default infiltration weight");

const Position red = queenPosition(RED, 'd', 7);
const Position yellow = queenPosition(YELLOW, 'k', 7);
const Position blue = queenPosition(BLUE, 'g', 4);
const Position green = queenPosition(GREEN, 'g', 11);
expect(bonus(red) > 0, "RED infiltrates BLUE's camp");
expect(bonus(yellow) > 0, "YELLOW infiltrates GREEN's camp");
expect(bonus(blue) > 0, "BLUE infiltrates RED's camp");
expect(bonus(green) > 0, "GREEN infiltrates YELLOW's camp");

expect(bonus(red) == 24, "RED infiltration score accounts for depth and king distance");
expect(bonus(red, 4) == 48, "infiltration scales with configured weight");
expect(bonus(red, 0) == 0, "disabled infiltration adds no score");
expect(bonus(queenPosition(RED, 'g', 4)) == 0, "centre is not infiltration");
expect(bonus(queenPosition(RED, 'g', 11)) == 0, "allied king camp adds no bonus");
expect(bonus(queenPosition(RED, 'g', 1)) == 0, "queen in own home camp adds no bonus");
expect(bonus(queenPosition(RED, 'd', 7, true)) == 0, "hanging infiltrating queen is not rewarded");

Position oppositeTurn = red;
oppositeTurn.setSideToMove(BLUE);
oppositeTurn.finalizeSetup();
expect(bonus(oppositeTurn) == -24, "bonus follows team-relative score perspective");

expect(Tuning::setEvalParameter("EvalQueenInfiltration", 0), "UCI setting permits disable");
expect(Tuning::eval().queenInfiltration == 0, "zero stored");
expect(Tuning::setEvalParameter("EvalQueenInfiltration", 20), "maximum weight accepted");
expect(!Tuning::setEvalParameter("EvalQueenInfiltration", -1), "negative weight rejected");
expect(!Tuning::setEvalParameter("EvalQueenInfiltration", 21), "weight beyond maximum rejected");
Tuning::resetEval();
expect(Tuning::eval().queenInfiltration == 2, "reset restores default");

if (!failures)
std::cout << "Queen infiltration tests: PASS\n";
return failures ? 1 : 0;
}
Loading