diff --git a/CMakeLists.txt b/CMakeLists.txt index 2114194..875ba50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/docs/pawn-history.md b/docs/pawn-history.md new file mode 100644 index 0000000..6ddbb91 --- /dev/null +++ b/docs/pawn-history.md @@ -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. diff --git a/include/ironphoenix/history.hpp b/include/ironphoenix/history.hpp index e058ed1..db48e2b 100644 --- a/include/ironphoenix/history.hpp +++ b/include/ironphoenix/history.hpp @@ -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, @@ -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, @@ -53,6 +57,11 @@ class HistoryTables { static constexpr std::size_t QUIET_COUNT = static_cast(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(COLOR_NB) * PIECE_TYPE_NB * SQUARE_NB * COLOR_NB * PIECE_TYPE_NB; @@ -65,12 +74,14 @@ class HistoryTables { static_cast(COLOR_NB) * COLOR_NB * PIECE_TYPE_NB * SQUARE_NB; std::unique_ptr quiet_; + std::unique_ptr pawn_; std::unique_ptr capture_; std::unique_ptr continuationPrevious_; std::unique_ptr continuationSamePlayer_; std::unique_ptr 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, diff --git a/include/ironphoenix/position.hpp b/include/ironphoenix/position.hpp index 7e66e01..2c91676 100644 --- a/include/ironphoenix/position.hpp +++ b/include/ironphoenix/position.hpp @@ -24,6 +24,7 @@ struct CastleLane { struct StateInfo { Key key = 0; + Key pawnKey = 0; Bitboard checkers{}; std::array enPassant{SQ_NONE, SQ_NONE, SQ_NONE, SQ_NONE}; std::array kingSq{SQ_NONE, SQ_NONE, SQ_NONE, SQ_NONE}; @@ -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_; } @@ -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: @@ -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 { diff --git a/include/ironphoenix/tuning.hpp b/include/ironphoenix/tuning.hpp index 5ed4eaf..36730f5 100644 --- a/include/ironphoenix/tuning.hpp +++ b/include/ironphoenix/tuning.hpp @@ -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; diff --git a/src/history.cpp b/src/history.cpp index 01b7568..6be9b8a 100644 --- a/src/history.cpp +++ b/src/history.cpp @@ -8,6 +8,7 @@ namespace ironphoenix { HistoryTables::HistoryTables() : quiet_(std::make_unique(QUIET_COUNT)), + pawn_(std::make_unique(PAWN_COUNT)), capture_(std::make_unique(CAPTURE_COUNT)), continuationPrevious_(std::make_unique(CONTINUATION_COUNT)), continuationSamePlayer_(std::make_unique(CONTINUATION_COUNT)), @@ -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)); @@ -27,6 +29,11 @@ std::size_t HistoryTables::quietIndex(Color mover, Move move) noexcept { return (static_cast(mover) * SQUARE_NB + move.from()) * SQUARE_NB + move.to(); } +std::size_t HistoryTables::pawnIndex(Key pawnKey, Piece movingPiece, Square to) noexcept { + return ((static_cast(pawnKey) & (PAWN_BUCKETS - 1)) + * 32 + static_cast(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(typeOf(attacker)); @@ -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; @@ -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; diff --git a/src/position.cpp b/src/position.cpp index f4c6018..4ba67b2 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -27,6 +27,7 @@ namespace ironphoenix { rulesetId_ = 0; checkers_.clear(); key_ = recomputeKey(); + pawnKey_ = recomputePawnKey(); } void Position::rawPut(Square sq, Piece p) noexcept { @@ -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 { @@ -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; } @@ -138,8 +143,10 @@ namespace ironphoenix { const std::uint8_t changed = static_cast(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; } @@ -147,7 +154,9 @@ namespace ironphoenix { 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; } @@ -160,6 +169,7 @@ namespace ironphoenix { void Position::finalizeSetup() { key_ = recomputeKey(); + pawnKey_ = recomputePawnKey(); refreshCheckers(); assert(verify()); } @@ -466,6 +476,7 @@ namespace ironphoenix { assert(us == sideToMove_); st.key = key_; + st.pawnKey = pawnKey_; st.checkers = checkers_; st.enPassant = enPassant_; st.kingSq = kingSq_; @@ -562,6 +573,7 @@ namespace ironphoenix { sideToMove_ = us; checkers_ = st.checkers; key_ = st.key; + pawnKey_ = st.pawnKey; assert(verify()); } @@ -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_; @@ -601,10 +614,24 @@ namespace ironphoenix { sideToMove_ = static_cast(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) { @@ -668,7 +695,7 @@ namespace ironphoenix { if (rebuiltPieces[c][pt] != pieceBB_[c][pt]) return false; } - return recomputeKey() == key_; + return recomputeKey() == key_ && recomputePawnKey() == pawnKey_; } } diff --git a/src/search.cpp b/src/search.cpp index 64c37fe..e136307 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -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()); @@ -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); @@ -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); diff --git a/src/tuning.cpp b/src/tuning.cpp index d9f07aa..d3ef4da 100644 --- a/src/tuning.cpp +++ b/src/tuning.cpp @@ -41,6 +41,7 @@ namespace ironphoenix::Tuning { || name == "SearchSEEPruneMargin" || name == "SearchHistoryBonusScale" || name == "SearchHistoryBonusMax" + || name == "SearchPawnHistoryWeight" || name == "SearchCounterMoveBonus" || name == "SearchCheckMoveBonus" || name == "SearchLMRMinDepth" @@ -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)) @@ -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" @@ -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 diff --git a/tests/pawn_history.cpp b/tests/pawn_history.cpp new file mode 100644 index 0000000..51036ec --- /dev/null +++ b/tests/pawn_history.cpp @@ -0,0 +1,146 @@ +#include "ironphoenix/geometry.hpp" +#include "ironphoenix/history.hpp" +#include "ironphoenix/position.hpp" +#include "ironphoenix/search.hpp" +#include "ironphoenix/tuning.hpp" +#include +#include +#include +using namespace ironphoenix; +namespace { +int failures = 0; +void expect(bool ok, const char* s) { if (!ok) { ++failures; std::cerr << "FAIL: " << s << '\n'; } } +Square sq(char f, int r) { return Geometry::square(f - 'a', r - 1); } +Position setup() { + Position p; + p.clear(); + p.placePiece(sq('g',1),makePiece(RED,KING)); + p.placePiece(sq('a',7),makePiece(BLUE,KING)); + p.placePiece(sq('g',14),makePiece(YELLOW,KING)); + p.placePiece(sq('n',7),makePiece(GREEN,KING)); + p.placePiece(sq('f',2),makePiece(RED,PAWN)); + p.placePiece(sq('b',6),makePiece(BLUE,PAWN)); + p.placePiece(sq('h',13),makePiece(YELLOW,PAWN)); + p.placePiece(sq('m',8),makePiece(GREEN,PAWN)); + p.finalizeSetup(); + return p; +} +void valid(const Position& p) { + expect(p.pawnKey() == p.recomputePawnKey(), "pawn hash matches recomputation"); + expect(p.verify(), "full Position remains valid"); +} +} +int main() { + Geometry::init(); + Tuning::resetAll(); + Position p = setup(); + const Key base = p.pawnKey(); + const Key full = p.key(); + valid(p); + p.setSideToMove(BLUE); + expect(p.pawnKey() == base, "turn rotation does not affect pawn key"); + p.setSideToMove(RED); + p.setCastlingRights(1u); + p.setEnPassant(RED,sq('f',3)); + expect(p.pawnKey() == base, "castling/en passant flags excluded"); + p.setEnPassant(RED,SQ_NONE); + p.setCastlingRights(0); + expect(p.key() == full, "non-pawn flags fully restored"); + + p.placePiece(sq('h',4),makePiece(RED,KNIGHT)); + p.finalizeSetup(); + const Key beforeKnight = p.pawnKey(); + StateInfo knightState; + const Move knightMove(sq('h',4),sq('i',6)); + p.makeMove(knightMove,knightState); + expect(p.pawnKey() == beforeKnight, "knight movement preserves pawn signature"); + valid(p); + p.undoMove(knightMove,knightState); + expect(p.pawnKey() == beforeKnight, "knight undo restores signature"); + p.erasePiece(sq('h',4)); + p.finalizeSetup(); + + const Key beforePush = p.pawnKey(); + StateInfo pushState; + const Move push(sq('f',2),sq('f',3)); + p.makeMove(push,pushState); + expect(p.pawnKey() != beforePush, "pawn push changes structure"); + valid(p); + p.undoMove(push,pushState); + expect(p.pawnKey() == beforePush, "pawn undo restores structure"); + + p.placePiece(sq('e',3),makePiece(BLUE,PAWN)); + p.finalizeSetup(); + const Key beforeCapture = p.pawnKey(); + StateInfo captureState; + const Move capture(sq('f',2),sq('e',3),makePiece(BLUE,PAWN)); + p.makeMove(capture,captureState); + expect(p.pawnKey() != beforeCapture, "pawn capture changes structure"); + valid(p); + p.undoMove(capture,captureState); + expect(p.pawnKey() == beforeCapture, "capture undo restores structure"); + p.erasePiece(sq('e',3)); + + p.erasePiece(sq('f',2)); + p.placePiece(sq('g',10),makePiece(RED,PAWN)); + p.finalizeSetup(); + const Key beforePromotion = p.pawnKey(); + StateInfo promotionState; + const Move promotion(sq('g',10),sq('g',11),NO_PIECE,PROMO_QUEEN); + p.makeMove(promotion,promotionState); + expect(p.pawnKey() != beforePromotion, "promotion removes pawn hash entry"); + valid(p); + p.undoMove(promotion,promotionState); + expect(p.pawnKey() == beforePromotion, "promotion undo restores pawn"); + StateInfo nullState; + const Key beforeNull = p.pawnKey(); + p.makeNullMove(nullState); + expect(p.pawnKey() == beforeNull, "null move preserves signature"); + p.undoNullMove(nullState); + expect(p.pawnKey() == beforeNull, "null undo preserves signature"); + + p.setAliveMask(0xDu); + expect(p.pawnKey() != beforeNull, "alive mask changes pawn context"); + valid(p); + p.setAliveMask(0xFu); + expect(p.pawnKey() == beforeNull, "alive mask restores pawn context"); + p.setRulesetId(3); + expect(p.pawnKey() != beforeNull, "ruleset changes pawn context"); + p.setRulesetId(0); + expect(p.pawnKey() == beforeNull, "ruleset restored"); + + HistoryTables history; + const Piece knight = makePiece(RED,KNIGHT); + const Square dst = sq('h',6); + expect(history.pawnScore(beforeNull,knight,dst) == 0, "new history zero"); + history.updatePawn(beforeNull,knight,dst,600); + expect(history.pawnScore(beforeNull,knight,dst) > 0, "quiet cutoff rewarded"); + expect(history.pawnScore(beforeNull,makePiece(YELLOW,KNIGHT),dst) == 0, "partner separately indexed"); + expect(history.pawnScore(beforeNull,knight,sq('i',6)) == 0, "destination isolated"); + expect(history.pawnScore(beforeNull ^ 1u,knight,dst) == 0, "different pawn context isolated"); + history.updatePawn(beforeNull,knight,dst,-1000); + expect(history.pawnScore(beforeNull,knight,dst) < 0, "failed quiet penalized"); + for (int i=0;i<50;++i) history.updatePawn(beforeNull,knight,dst,30000); + expect(history.pawnScore(beforeNull,knight,dst) <= HistoryTables::MAX_HISTORY, "history bounded"); + history.clear(); + expect(history.pawnScore(beforeNull,knight,dst) == 0, "new game clears pawn history"); + + expect(Tuning::search().pawnHistoryWeight == 50, "default 50 pct"); + expect(Tuning::setSearchParameter("SearchPawnHistoryWeight",0), "0 accepted"); + expect(Tuning::setSearchParameter("SearchPawnHistoryWeight",200), "200 accepted"); + expect(!Tuning::setSearchParameter("SearchPawnHistoryWeight",-1), "-1 rejected"); + expect(!Tuning::setSearchParameter("SearchPawnHistoryWeight",201), "201 rejected"); + auto engine = std::make_unique(); + std::ostringstream output; + expect(Tuning::handleSetOption("name SearchPawnHistoryWeight value 75",*engine,output), "UCI recognizes option"); + expect(Tuning::search().pawnHistoryWeight == 75, "UCI stores weight"); + Tuning::printUciOptions(output); + Tuning::printCurrent(output); + expect(output.str().find("option name SearchPawnHistoryWeight type spin default 50 min 0 max 200") != std::string::npos, "option advertised"); + expect(output.str().find("SearchPawnHistoryWeight=75") != std::string::npos, "current tuning prints setting"); + Tuning::resetSearch(); + expect(Tuning::search().pawnHistoryWeight == 50, "reset restores default"); + + if (!failures) std::cout << "4PC pawn-structure history: PASS\n"; + return failures ? 1 : 0; +}