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_pawn_history tests/pawn_history.cpp)
target_link_libraries(ironphoenix_pawn_history PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_pawn_history PRIVATE cxx_std_20)
add_test(NAME ironphoenix_pawn_history COMMAND ironphoenix_pawn_history)

set_target_properties(ironphoenix_smoke ironphoenix_property ironphoenix_search_stack ironphoenix_mate_stop ironphoenix_reverse_futility ironphoenix_attacking_pieces ironphoenix_pawn_history PROPERTIES
FOLDER "Tests"
)
endif()
Expand Down
31 changes: 31 additions & 0 deletions docs/pawn-history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Pawn-structure history in IronPhoenix

This is a **move-ordering heuristic**, not pawn evaluation or a history
restricted to pawn moves. It learns preferences for quiet moves by any
piece, conditioned on the four-player pawn structure.

- Incremental Zobrist signature includes pawn locations for **Red, Blue,
Yellow, and Green**, alive-player mask, and ruleset. Excludes castling
rights, en passant markers, and the side to move.
- History indexed by **pawn hash bucket + moving piece (color and type)
+ destination square**, so Red and Yellow retain separate histories.
- Bounded table: **256 pawn contexts × 32 piece codes × 160 board squares**
with signed 16-bit entries (~2.5 MiB). Context collisions only impact
move ordering hints, not correctness of board state or legal moves.
- Successful quiet beta cutoffs add history bonuses, while failed quiet
moves receive negative feedback. Existing bounded history updates are
reused. TT moves, captures and promotions retain existing precedence.
- Existing LMP/HMP use combined quiet history scores; a positive Elo or NPS
impact is not guaranteed and must be tested against main.

## UCI tuning

setoption name SearchPawnHistoryWeight value 50

Range **0–200%**, default **50%**. At 0 the table is neither queried nor
updated. Use 'ucinewgame' or restart the engine before comparing runs.

The incremental pawn hash updates on pawn moves, captures and promotions,
and is restored on undo. Null moves leave the hash unchanged. Test with
CTests 'ironphoenix_pawn_history' and 'ironphoenix_property' and use
matched 4PC Teams STC/LTC and an SPRT before merging.
11 changes: 11 additions & 0 deletions include/ironphoenix/history.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class HistoryTables {
void clear() noexcept;

[[nodiscard]] int quietScore(Color mover, Move move) const noexcept;
// Pawn-history: condition ANY quiet move on all four players' pawn
// structures. This is not history exclusive to pawn moves.
[[nodiscard]] int pawnScore(Key pawnKey, Piece movingPiece, Square to) const noexcept;
[[nodiscard]] int captureScore(Color mover, Piece attacker, Move move) const noexcept;
[[nodiscard]] int continuationScore(ContinuationKind kind,
const HistoryContext& previous,
Expand All @@ -41,6 +44,7 @@ class HistoryTables {
[[nodiscard]] Move counterMove(Color responder, const HistoryContext& previous) const noexcept;

void updateQuiet(Color mover, Move move, int bonus) noexcept;
void updatePawn(Key pawnKey, Piece movingPiece, Square to, int bonus) noexcept;
void updateCapture(Color mover, Piece attacker, Move move, int bonus) noexcept;
void updateContinuation(ContinuationKind kind,
const HistoryContext& previous,
Expand All @@ -53,6 +57,11 @@ class HistoryTables {
static constexpr std::size_t QUIET_COUNT =
static_cast<std::size_t>(COLOR_NB) * SQUARE_NB * SQUARE_NB;

// 256 structure buckets x 32 color-coded piece IDs x 160 squares,
// approximately 2.5 MiB of saturated 16-bit entries.
static constexpr std::size_t PAWN_BUCKETS = 256;
static constexpr std::size_t PAWN_COUNT = PAWN_BUCKETS * 32 * SQUARE_NB;

static constexpr std::size_t CAPTURE_COUNT =
static_cast<std::size_t>(COLOR_NB) * PIECE_TYPE_NB * SQUARE_NB
* COLOR_NB * PIECE_TYPE_NB;
Expand All @@ -65,12 +74,14 @@ class HistoryTables {
static_cast<std::size_t>(COLOR_NB) * COLOR_NB * PIECE_TYPE_NB * SQUARE_NB;

std::unique_ptr<std::int16_t[]> quiet_;
std::unique_ptr<std::int16_t[]> pawn_;
std::unique_ptr<std::int16_t[]> capture_;
std::unique_ptr<std::int16_t[]> continuationPrevious_;
std::unique_ptr<std::int16_t[]> continuationSamePlayer_;
std::unique_ptr<std::uint32_t[]> counter_;

[[nodiscard]] static std::size_t quietIndex(Color mover, Move move) noexcept;
[[nodiscard]] static std::size_t pawnIndex(Key pawnKey, Piece movingPiece, Square to) noexcept;
[[nodiscard]] static std::size_t captureIndex(Color mover, Piece attacker, Move move) noexcept;
[[nodiscard]] static std::size_t continuationIndex(const HistoryContext& previous,
Piece currentPiece,
Expand Down
5 changes: 5 additions & 0 deletions include/ironphoenix/position.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct CastleLane {

struct StateInfo {
Key key = 0;
Key pawnKey = 0;
Bitboard checkers{};
std::array<Square, COLOR_NB> enPassant{SQ_NONE, SQ_NONE, SQ_NONE, SQ_NONE};
std::array<Square, COLOR_NB> kingSq{SQ_NONE, SQ_NONE, SQ_NONE, SQ_NONE};
Expand Down Expand Up @@ -63,6 +64,8 @@ class Position {
[[nodiscard]] IRONPHOENIX_FORCE_INLINE Square kingSquare(Color c) const noexcept { return kingSq_[c]; }
[[nodiscard]] IRONPHOENIX_FORCE_INLINE Color sideToMove() const noexcept { return sideToMove_; }
[[nodiscard]] IRONPHOENIX_FORCE_INLINE Key key() const noexcept { return key_; }
// All four colored pawn structures, alive mask and ruleset; no side to move.
[[nodiscard]] IRONPHOENIX_FORCE_INLINE Key pawnKey() const noexcept { return pawnKey_; }
[[nodiscard]] IRONPHOENIX_FORCE_INLINE std::uint8_t castlingRights() const noexcept { return castlingRights_; }
[[nodiscard]] IRONPHOENIX_FORCE_INLINE Square enPassant(Color c) const noexcept { return enPassant_[c]; }
[[nodiscard]] IRONPHOENIX_FORCE_INLINE std::uint8_t aliveMask() const noexcept { return aliveMask_; }
Expand Down Expand Up @@ -130,6 +133,7 @@ class Position {
void undoNullMove(const StateInfo& st);

[[nodiscard]] Key recomputeKey() const noexcept;
[[nodiscard]] Key recomputePawnKey() const noexcept;
[[nodiscard]] bool verify() const noexcept;

private:
Expand All @@ -150,6 +154,7 @@ class Position {
std::uint8_t aliveMask_ = 0xF;
std::uint8_t rulesetId_ = 0;
Key key_ = 0;
Key pawnKey_ = 0;
Bitboard checkers_{};

IRONPHOENIX_FORCE_INLINE void addLineOccupancy(Square sq) noexcept {
Expand Down
2 changes: 2 additions & 0 deletions include/ironphoenix/tuning.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace ironphoenix {
int seePruneMarginPerDepth = 80;
int historyBonusScale = 64;
int historyBonusMax = 2048;
// 0 disables pawn-history scoring/learning for A/B testing.
int pawnHistoryWeight = 50;
int counterMoveBonus = 32'000;
int checkMoveBonus = 10'000'000;
int lmrMinDepth = 3;
Expand Down
19 changes: 19 additions & 0 deletions src/history.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace ironphoenix {

HistoryTables::HistoryTables()
: quiet_(std::make_unique<std::int16_t[]>(QUIET_COUNT)),
pawn_(std::make_unique<std::int16_t[]>(PAWN_COUNT)),
capture_(std::make_unique<std::int16_t[]>(CAPTURE_COUNT)),
continuationPrevious_(std::make_unique<std::int16_t[]>(CONTINUATION_COUNT)),
continuationSamePlayer_(std::make_unique<std::int16_t[]>(CONTINUATION_COUNT)),
Expand All @@ -17,6 +18,7 @@ HistoryTables::HistoryTables()

void HistoryTables::clear() noexcept {
std::memset(quiet_.get(), 0, QUIET_COUNT * sizeof(std::int16_t));
std::memset(pawn_.get(), 0, PAWN_COUNT * sizeof(std::int16_t));
std::memset(capture_.get(), 0, CAPTURE_COUNT * sizeof(std::int16_t));
std::memset(continuationPrevious_.get(), 0, CONTINUATION_COUNT * sizeof(std::int16_t));
std::memset(continuationSamePlayer_.get(), 0, CONTINUATION_COUNT * sizeof(std::int16_t));
Expand All @@ -27,6 +29,11 @@ std::size_t HistoryTables::quietIndex(Color mover, Move move) noexcept {
return (static_cast<std::size_t>(mover) * SQUARE_NB + move.from()) * SQUARE_NB + move.to();
}

std::size_t HistoryTables::pawnIndex(Key pawnKey, Piece movingPiece, Square to) noexcept {
return ((static_cast<std::size_t>(pawnKey) & (PAWN_BUCKETS - 1))
* 32 + static_cast<unsigned>(movingPiece)) * SQUARE_NB + to;
}

std::size_t HistoryTables::captureIndex(Color mover, Piece attacker, Move move) noexcept {
const Piece victim = move.captured();
const unsigned attackerType = static_cast<unsigned>(typeOf(attacker));
Expand Down Expand Up @@ -73,6 +80,12 @@ int HistoryTables::quietScore(Color mover, Move move) const noexcept {
return quiet_[quietIndex(mover, move)];
}

int HistoryTables::pawnScore(Key pawnKey, Piece movingPiece, Square to) const noexcept {
if (!isPiece(movingPiece) || to >= SQUARE_NB)
return 0;
return pawn_[pawnIndex(pawnKey, movingPiece, to)];
}

int HistoryTables::captureScore(Color mover, Piece attacker, Move move) const noexcept {
if (!isPiece(attacker) || !move.isCapture())
return 0;
Expand Down Expand Up @@ -101,6 +114,12 @@ void HistoryTables::updateQuiet(Color mover, Move move, int bonus) noexcept {
updateEntry(quiet_[quietIndex(mover, move)], bonus);
}

void HistoryTables::updatePawn(Key pawnKey, Piece movingPiece, Square to, int bonus) noexcept {
if (!isPiece(movingPiece) || to >= SQUARE_NB)
return;
updateEntry(pawn_[pawnIndex(pawnKey, movingPiece, to)], bonus);
}

void HistoryTables::updateCapture(Color mover, Piece attacker, Move move, int bonus) noexcept {
if (!isPiece(attacker) || !move.isCapture())
return;
Expand Down
33 changes: 30 additions & 3 deletions src/position.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace ironphoenix {
rulesetId_ = 0;
checkers_.clear();
key_ = recomputeKey();
pawnKey_ = recomputePawnKey();
}

void Position::rawPut(Square sq, Piece p) noexcept {
Expand All @@ -41,6 +42,8 @@ namespace ironphoenix {
if (pt == KING)
kingSq_[c] = sq;
key_ ^= Zobrist::PieceSquare[p][sq];
if (pt == PAWN)
pawnKey_ ^= Zobrist::PieceSquare[p][sq];
}

Piece Position::rawRemove(Square sq) noexcept {
Expand All @@ -59,6 +62,8 @@ namespace ironphoenix {
if (pt == KING && kingSq_[c] == sq)
kingSq_[c] = SQ_NONE;
key_ ^= Zobrist::PieceSquare[p][sq];
if (pt == PAWN)
pawnKey_ ^= Zobrist::PieceSquare[p][sq];
return p;
}

Expand Down Expand Up @@ -138,16 +143,20 @@ namespace ironphoenix {

const std::uint8_t changed = static_cast<std::uint8_t>(aliveMask_ ^ mask);
for (unsigned c = 0; c < COLOR_NB; ++c)
if (changed & (1u << c))
if (changed & (1u << c)) {
key_ ^= Zobrist::Alive[c];
pawnKey_ ^= Zobrist::Alive[c];
}
aliveMask_ = mask;
}

void Position::setRulesetId(std::uint8_t id) {
id &= 0xF;
if (rulesetId_ == id)
return;
key_ ^= Zobrist::Ruleset[rulesetId_] ^ Zobrist::Ruleset[id];
const Key changed = Zobrist::Ruleset[rulesetId_] ^ Zobrist::Ruleset[id];
key_ ^= changed;
pawnKey_ ^= changed;
rulesetId_ = id;
}

Expand All @@ -160,6 +169,7 @@ namespace ironphoenix {

void Position::finalizeSetup() {
key_ = recomputeKey();
pawnKey_ = recomputePawnKey();
refreshCheckers();
assert(verify());
}
Expand Down Expand Up @@ -466,6 +476,7 @@ namespace ironphoenix {
assert(us == sideToMove_);

st.key = key_;
st.pawnKey = pawnKey_;
st.checkers = checkers_;
st.enPassant = enPassant_;
st.kingSq = kingSq_;
Expand Down Expand Up @@ -562,6 +573,7 @@ namespace ironphoenix {
sideToMove_ = us;
checkers_ = st.checkers;
key_ = st.key;
pawnKey_ = st.pawnKey;

assert(verify());
}
Expand All @@ -570,6 +582,7 @@ namespace ironphoenix {
const Color us = sideToMove_;

st.key = key_;
st.pawnKey = pawnKey_;
st.checkers = checkers_;
st.enPassant = enPassant_;
st.kingSq = kingSq_;
Expand Down Expand Up @@ -601,10 +614,24 @@ namespace ironphoenix {
sideToMove_ = static_cast<Color>(st.side);
checkers_ = st.checkers;
key_ = st.key;
pawnKey_ = st.pawnKey;

assert(verify());
}

Key Position::recomputePawnKey() const noexcept {
Key k = Zobrist::Ruleset[rulesetId_];
for (unsigned c = 0; c < COLOR_NB; ++c)
if (aliveMask_ & (1u << c))
k ^= Zobrist::Alive[c];
for (unsigned s = 0; s < SQUARE_NB; ++s) {
const Piece piece = board_[s];
if (piece != NO_PIECE && typeOf(piece) == PAWN)
k ^= Zobrist::PieceSquare[piece][s];
}
return k;
}

Key Position::recomputeKey() const noexcept {
Key k = Zobrist::Side[sideToMove_] ^ Zobrist::Ruleset[rulesetId_];
for (unsigned sq = 0; sq < SQUARE_NB; ++sq) {
Expand Down Expand Up @@ -668,7 +695,7 @@ namespace ironphoenix {
if (rebuiltPieces[c][pt] != pieceBB_[c][pt])
return false;
}
return recomputeKey() == key_;
return recomputeKey() == key_ && recomputePawnKey() == pawnKey_;
}

}
9 changes: 9 additions & 0 deletions src/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ namespace ironphoenix {
}

int score = history_.quietScore(mover, move);
// Pawn-structure-conditioned quiet ordering. The pawn signature is
// maintained incrementally, so there is no scan of all pawns here.
if (tuning.pawnHistoryWeight > 0)
score += history_.pawnScore(pos.pawnKey(), movingPiece, move.to())
* tuning.pawnHistoryWeight / 100;

if (ply > 0 && searchStack_[ply].history.valid()) {
score += history_.continuationScore(ContinuationKind::PreviousPly, searchStack_[ply].history, movingPiece, move.to());
Expand Down Expand Up @@ -439,6 +444,8 @@ namespace ironphoenix {
const int penalty = -std::max(32, bonus / 2);

history_.updateQuiet(mover, cutoffMove, bonus);
if (Tuning::search().pawnHistoryWeight > 0)
history_.updatePawn(pos.pawnKey(), movingPiece, cutoffMove.to(), bonus);

if (ply > 0 && searchStack_[ply].history.valid()) {
history_.updateContinuation(ContinuationKind::PreviousPly, searchStack_[ply].history, movingPiece, cutoffMove.to(), bonus);
Expand All @@ -458,6 +465,8 @@ namespace ironphoenix {
continue;

history_.updateQuiet(mover, failed, penalty);
if (Tuning::search().pawnHistoryWeight > 0)
history_.updatePawn(pos.pawnKey(), failedPiece, failed.to(), penalty);
if (ply > 0 && searchStack_[ply].history.valid())
history_.updateContinuation(ContinuationKind::PreviousPly, searchStack_[ply].history, failedPiece, failed.to(), penalty);

Expand Down
5 changes: 5 additions & 0 deletions src/tuning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ namespace ironphoenix::Tuning {
|| name == "SearchSEEPruneMargin"
|| name == "SearchHistoryBonusScale"
|| name == "SearchHistoryBonusMax"
|| name == "SearchPawnHistoryWeight"
|| name == "SearchCounterMoveBonus"
|| name == "SearchCheckMoveBonus"
|| name == "SearchLMRMinDepth"
Expand Down Expand Up @@ -114,6 +115,8 @@ namespace ironphoenix::Tuning {
p.historyBonusScale = value;
else if (name == "SearchHistoryBonusMax" && inRange(value, 1, 32767))
p.historyBonusMax = value;
else if (name == "SearchPawnHistoryWeight" && inRange(value, 0, 200))
p.pawnHistoryWeight = value;
else if (name == "SearchCounterMoveBonus" && inRange(value, 0, 50'000'000))
p.counterMoveBonus = value;
else if (name == "SearchCheckMoveBonus" && inRange(value, 0, 50'000'000))
Expand Down Expand Up @@ -216,6 +219,7 @@ namespace ironphoenix::Tuning {
<< "option name SearchSEEPruneMargin type spin default 80 min 0 max 2000\n"
<< "option name SearchHistoryBonusScale type spin default 64 min 0 max 2048\n"
<< "option name SearchHistoryBonusMax type spin default 2048 min 1 max 32767\n"
<< "option name SearchPawnHistoryWeight type spin default 50 min 0 max 200\n"
<< "option name SearchCounterMoveBonus type spin default 32000 min 0 max 50000000\n"
<< "option name SearchCheckMoveBonus type spin default 10000000 min 0 max 50000000\n"
<< "option name SearchLMRMinDepth type spin default 3 min 1 max 32\n"
Expand Down Expand Up @@ -245,6 +249,7 @@ namespace ironphoenix::Tuning {
<< " SearchSEEPruneMargin=" << s.seePruneMarginPerDepth
<< " SearchHistoryBonusScale=" << s.historyBonusScale
<< " SearchHistoryBonusMax=" << s.historyBonusMax
<< " SearchPawnHistoryWeight=" << s.pawnHistoryWeight
<< " SearchCounterMoveBonus=" << s.counterMoveBonus
<< " SearchCheckMoveBonus=" << s.checkMoveBonus
<< " SearchLMRMinDepth=" << s.lmrMinDepth
Expand Down
Loading
Loading