From 60351b9df901ff5278f208a9cf3a40059ff54832 Mon Sep 17 00:00:00 2001 From: Shawn Xu Date: Thu, 12 Sep 2024 15:53:15 -0700 Subject: [PATCH] Introduce Various Correction histories This patch introduces three additional correction histories, namely, Major Piece Correction History, Minor Piece Correction History, and Non-Pawn Correction History. Introduced by @mcthouacbb in Sirius (https://github.com/mcthouacbb/Sirius) chess engine. The Major Piece Correction History is indexed by side-to-move and the Zobrist key representing the position of the King, Rook, and Queen of both sides. Likewise, the Minor Piece Correction History is indexed by side-to-move and the Zobrist key representing the position of the King, Knight, and Bishop of both sides. Also See: https://github.com/mcthouacbb/Sirius/commit/97b85bbaac88ff5a0f63e28776027dd3de77164e https://github.com/mcthouacbb/Sirius/commit/3099cdef2f13e29805654b5f8153e6ecd5853195 Introduced by @zzzzz151 in Starzix (https://github.com/zzzzz151/Starzix) chess engine. Non-Pawn correction history consists of side-to-move, side of Zobrist key, and a Zobrist key representing of the position of all non-pawn pieces of **one side**. The non-pawn correction values for both key sides are then summed. Also See: https://github.com/zzzzz151/Starzix/commit/34911772f178c27b3a239dda0acb79c397c3a2f0 https://github.com/zzzzz151/Starzix/commit/33e0df8dd2db1d4775974ab12e3390154697f47a The weights on the final correction value of the above correction histories, as well as existing correction histories, are then tuned in two separate SPSA sessions, totaling 75k games. SPSA1: https://tests.stockfishchess.org/tests/view/66e5243886d5ee47d953a86b (Stopped early due to some weights reaching the maximum value) SPSA2: https://tests.stockfishchess.org/tests/view/66e6a26f86d5ee47d953a965 Also thanks to @martinnovaak, (Motor https://github.com/martinnovaak/motor author) for insights and suggestions. Passed STC: LLR: 2.94 (-2.94,2.94) <0.00,2.00> Total: 23328 W: 6197 L: 5901 D: 11230 Ptnml(0-2): 82, 2582, 6041, 2876, 83 https://tests.stockfishchess.org/tests/view/66e8787b86d5ee47d953ab6f Passed LTC: LLR: 2.94 (-2.94,2.94) <0.50,2.50> Total: 10626 W: 2826 L: 2560 D: 5240 Ptnml(0-2): 4, 1054, 2941, 1300, 14 https://tests.stockfishchess.org/tests/view/66e8ab2386d5ee47d953aba8 closes https://github.com/official-stockfish/Stockfish/pull/5598 Bench: 1011161 --- src/bitboard.cpp | 4 +-- src/movepick.h | 40 ++++++++++++++++++++++++----- src/position.cpp | 66 ++++++++++++++++++++++++++++++++++++++++++++---- src/position.h | 12 +++++++++ src/search.cpp | 24 +++++++++++++----- src/search.h | 19 +++++++++----- tests/perft.sh | 2 +- 7 files changed, 140 insertions(+), 27 deletions(-) diff --git a/src/bitboard.cpp b/src/bitboard.cpp index c842ca12..a8b4e5f4 100644 --- a/src/bitboard.cpp +++ b/src/bitboard.cpp @@ -140,8 +140,8 @@ Bitboard sliding_attack(PieceType pt, Square sq, Bitboard occupied) { // Computes all rook and bishop attacks at startup. Magic // bitboards are used to look up attacks of sliding pieces. As a reference see -// www.chessprogramming.org/Magic_Bitboards. In particular, here we use the so -// called "fancy" approach. +// https://www.chessprogramming.org/Magic_Bitboards. In particular, here we use +// the so called "fancy" approach. void init_magics(PieceType pt, Bitboard table[], Magic magics[]) { // Optimal PRNG seeds to pick the correct magics in the shortest time diff --git a/src/movepick.h b/src/movepick.h index f66cdadf..13b9635b 100644 --- a/src/movepick.h +++ b/src/movepick.h @@ -34,10 +34,13 @@ namespace Stockfish { -constexpr int PAWN_HISTORY_SIZE = 512; // has to be a power of 2 -constexpr int PAWN_CORRECTION_HISTORY_SIZE = 16384; // has to be a power of 2 -constexpr int MATERIAL_CORRECTION_HISTORY_SIZE = 32768; // has to be a power of 2 -constexpr int CORRECTION_HISTORY_LIMIT = 1024; +constexpr int PAWN_HISTORY_SIZE = 512; // has to be a power of 2 +constexpr int PAWN_CORRECTION_HISTORY_SIZE = 16384; // has to be a power of 2 +constexpr int MATERIAL_CORRECTION_HISTORY_SIZE = 32768; // has to be a power of 2 +constexpr int MAJOR_PIECE_CORRECTION_HISTORY_SIZE = 32768; // has to be a power of 2 +constexpr int MINOR_PIECE_CORRECTION_HISTORY_SIZE = 32768; // has to be a power of 2 +constexpr int NON_PAWN_CORRECTION_HISTORY_SIZE = 32768; // has to be a power of 2 +constexpr int CORRECTION_HISTORY_LIMIT = 1024; static_assert((PAWN_HISTORY_SIZE & (PAWN_HISTORY_SIZE - 1)) == 0, "PAWN_HISTORY_SIZE has to be a power of 2"); @@ -59,6 +62,19 @@ inline int material_index(const Position& pos) { return pos.material_key() & (MATERIAL_CORRECTION_HISTORY_SIZE - 1); } +inline int major_piece_index(const Position& pos) { + return pos.major_piece_key() & (MAJOR_PIECE_CORRECTION_HISTORY_SIZE - 1); +} + +inline int minor_piece_index(const Position& pos) { + return pos.minor_piece_key() & (MINOR_PIECE_CORRECTION_HISTORY_SIZE - 1); +} + +template +inline int non_pawn_index(const Position& pos) { + return pos.non_pawn_key(c) & (NON_PAWN_CORRECTION_HISTORY_SIZE - 1); +} + // StatsEntry stores the stat table value. It is usually a number but could // be a move or even a nested history. We use a class instead of a naked value // to directly call history update operator<<() on the entry so to use stats @@ -120,7 +136,7 @@ enum StatsType { // ButterflyHistory records how often quiet moves have been successful or unsuccessful // during the current search, and is used for reduction and move ordering decisions. // It uses 2 tables (one for each color) indexed by the move's from and to squares, -// see www.chessprogramming.org/Butterfly_Boards (~11 elo) +// see https://www.chessprogramming.org/Butterfly_Boards (~11 elo) using ButterflyHistory = Stats; // CapturePieceToHistory is addressed by a move's [piece][to][captured piece type] @@ -138,10 +154,10 @@ using ContinuationHistory = Stats // PawnHistory is addressed by the pawn structure and a move's [piece][to] using PawnHistory = Stats; - // Correction histories record differences between the static evaluation of // positions and their search score. It is used to improve the static evaluation // used by some search heuristics. +// see https://www.chessprogramming.org/Static_Evaluation_Correction_History // PawnCorrectionHistory is addressed by color and pawn structure using PawnCorrectionHistory = @@ -151,6 +167,18 @@ using PawnCorrectionHistory = using MaterialCorrectionHistory = Stats; +// MajorPieceCorrectionHistory is addressed by color and king/major piece (Queen, Rook) positions +using MajorPieceCorrectionHistory = + Stats; + +// MinorPieceCorrectionHistory is addressed by color and king/minor piece (Knight, Bishop) positions +using MinorPieceCorrectionHistory = + Stats; + +// NonPawnCorrectionHistory is addressed by color and non-pawn material positions +using NonPawnCorrectionHistory = + Stats; + // The MovePicker class is used to pick one pseudo-legal move at a time from the // current position. The most important method is next_move(), which emits one // new pseudo-legal move on every call, until there are no moves left, when diff --git a/src/position.cpp b/src/position.cpp index df95ffef..f596b015 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -334,8 +334,10 @@ void Position::set_check_info() const { // The function is only used when a new position is set up void Position::set_state() const { - st->key = st->materialKey = 0; - st->pawnKey = Zobrist::noPawns; + st->key = st->materialKey = 0; + st->majorPieceKey = st->minorPieceKey = 0; + st->nonPawnKey[WHITE] = st->nonPawnKey[BLACK] = 0; + st->pawnKey = Zobrist::noPawns; st->nonPawnMaterial[WHITE] = st->nonPawnMaterial[BLACK] = VALUE_ZERO; st->checkersBB = attackers_to(square(sideToMove)) & pieces(~sideToMove); @@ -350,8 +352,27 @@ void Position::set_state() const { if (type_of(pc) == PAWN) st->pawnKey ^= Zobrist::psq[pc][s]; - else if (type_of(pc) != KING) - st->nonPawnMaterial[color_of(pc)] += PieceValue[pc]; + else + { + st->nonPawnKey[color_of(pc)] ^= Zobrist::psq[pc][s]; + + if (type_of(pc) != KING) + { + st->nonPawnMaterial[color_of(pc)] += PieceValue[pc]; + + if (type_of(pc) == QUEEN || type_of(pc) == ROOK) + st->majorPieceKey ^= Zobrist::psq[pc][s]; + + else + st->minorPieceKey ^= Zobrist::psq[pc][s]; + } + + else + { + st->majorPieceKey ^= Zobrist::psq[pc][s]; + st->minorPieceKey ^= Zobrist::psq[pc][s]; + } + } } if (st->epSquare != SQ_NONE) @@ -707,6 +728,8 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { do_castling(us, from, to, rfrom, rto); k ^= Zobrist::psq[captured][rfrom] ^ Zobrist::psq[captured][rto]; + st->majorPieceKey ^= Zobrist::psq[captured][rfrom] ^ Zobrist::psq[captured][rto]; + st->nonPawnKey[us] ^= Zobrist::psq[captured][rfrom] ^ Zobrist::psq[captured][rto]; captured = NO_PIECE; } @@ -732,7 +755,16 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { st->pawnKey ^= Zobrist::psq[captured][capsq]; } else + { st->nonPawnMaterial[them] -= PieceValue[captured]; + st->nonPawnKey[them] ^= Zobrist::psq[captured][capsq]; + + if (type_of(pc) == QUEEN || type_of(pc) == ROOK) + st->majorPieceKey ^= Zobrist::psq[captured][capsq]; + + else + st->minorPieceKey ^= Zobrist::psq[captured][capsq]; + } dp.dirty_num = 2; // 1 piece moved, 1 piece captured dp.piece[1] = captured; @@ -790,7 +822,8 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { else if (m.type_of() == PROMOTION) { - Piece promotion = make_piece(us, m.promotion_type()); + Piece promotion = make_piece(us, m.promotion_type()); + PieceType promotionType = type_of(promotion); assert(relative_rank(us, to) == RANK_8); assert(type_of(promotion) >= KNIGHT && type_of(promotion) <= QUEEN); @@ -811,6 +844,12 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { st->materialKey ^= Zobrist::psq[promotion][pieceCount[promotion] - 1] ^ Zobrist::psq[pc][pieceCount[pc]]; + if (promotionType == QUEEN || promotionType == ROOK) + st->majorPieceKey ^= Zobrist::psq[promotion][to]; + + else + st->minorPieceKey ^= Zobrist::psq[promotion][to]; + // Update material st->nonPawnMaterial[us] += PieceValue[promotion]; } @@ -822,6 +861,23 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { st->rule50 = 0; } + else + { + st->nonPawnKey[us] ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; + + if (type_of(pc) == KING) + { + st->majorPieceKey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; + st->minorPieceKey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; + } + + else if (type_of(pc) == QUEEN || type_of(pc) == ROOK) + st->majorPieceKey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; + + else + st->minorPieceKey ^= Zobrist::psq[pc][from] ^ Zobrist::psq[pc][to]; + } + // Set capture piece st->capturedPiece = captured; diff --git a/src/position.h b/src/position.h index 6cac1731..888612da 100644 --- a/src/position.h +++ b/src/position.h @@ -43,6 +43,9 @@ struct StateInfo { // Copied when making a move Key materialKey; Key pawnKey; + Key majorPieceKey; + Key minorPieceKey; + Key nonPawnKey[COLOR_NB]; Value nonPawnMaterial[COLOR_NB]; int castlingRights; int rule50; @@ -151,6 +154,9 @@ class Position { Key key_after(Move m) const; Key material_key() const; Key pawn_key() const; + Key major_piece_key() const; + Key minor_piece_key() const; + Key non_pawn_key(Color c) const; // Other properties of the position Color side_to_move() const; @@ -298,6 +304,12 @@ inline Key Position::pawn_key() const { return st->pawnKey; } inline Key Position::material_key() const { return st->materialKey; } +inline Key Position::major_piece_key() const { return st->majorPieceKey; } + +inline Key Position::minor_piece_key() const { return st->minorPieceKey; } + +inline Key Position::non_pawn_key(Color c) const { return st->nonPawnKey[c]; } + inline Value Position::non_pawn_material(Color c) const { return st->nonPawnMaterial[c]; } inline Value Position::non_pawn_material() const { diff --git a/src/search.cpp b/src/search.cpp index 3c6da163..199b9355 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -46,7 +46,6 @@ #include "thread.h" #include "timeman.h" #include "tt.h" -#include "types.h" #include "uci.h" #include "ucioption.h" @@ -81,11 +80,16 @@ constexpr int futility_move_count(bool improving, Depth depth) { // Add correctionHistory value to raw staticEval and guarantee evaluation // does not hit the tablebase range. Value to_corrected_static_eval(Value v, const Worker& w, const Position& pos) { - const auto pcv = - w.pawnCorrectionHistory[pos.side_to_move()][pawn_structure_index(pos)]; - const auto mcv = w.materialCorrectionHistory[pos.side_to_move()][material_index(pos)]; - const auto cv = (2 * pcv + mcv) / 3; - v += 74 * cv / 512; + const Color us = pos.side_to_move(); + const auto pcv = w.pawnCorrectionHistory[us][pawn_structure_index(pos)]; + const auto mcv = w.materialCorrectionHistory[us][material_index(pos)]; + const auto macv = w.majorPieceCorrectionHistory[us][major_piece_index(pos)]; + const auto micv = w.minorPieceCorrectionHistory[us][minor_piece_index(pos)]; + const auto wnpcv = w.nonPawnCorrectionHistory[WHITE][us][non_pawn_index(pos)]; + const auto bnpcv = w.nonPawnCorrectionHistory[BLACK][us][non_pawn_index(pos)]; + const auto cv = + (98198 * pcv + 68968 * mcv + 54353 * macv + 85174 * micv + 85581 * (wnpcv + bnpcv)) / 2097152; + v += cv; return std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1); } @@ -500,6 +504,10 @@ void Search::Worker::clear() { pawnHistory.fill(-1152); pawnCorrectionHistory.fill(0); materialCorrectionHistory.fill(0); + majorPieceCorrectionHistory.fill(0); + minorPieceCorrectionHistory.fill(0); + nonPawnCorrectionHistory[WHITE].fill(0); + nonPawnCorrectionHistory[BLACK].fill(0); for (bool inCheck : {false, true}) for (StatsType c : {NoCaptures, Captures}) @@ -1403,6 +1411,10 @@ moves_loop: // When in check, search starts here -CORRECTION_HISTORY_LIMIT / 4, CORRECTION_HISTORY_LIMIT / 4); thisThread->pawnCorrectionHistory[us][pawn_structure_index(pos)] << bonus; thisThread->materialCorrectionHistory[us][material_index(pos)] << bonus; + thisThread->majorPieceCorrectionHistory[us][major_piece_index(pos)] << bonus; + thisThread->minorPieceCorrectionHistory[us][minor_piece_index(pos)] << bonus; + thisThread->nonPawnCorrectionHistory[WHITE][us][non_pawn_index(pos)] << bonus; + thisThread->nonPawnCorrectionHistory[BLACK][us][non_pawn_index(pos)] << bonus; } assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); diff --git a/src/search.h b/src/search.h index b06c7c94..d7a909a8 100644 --- a/src/search.h +++ b/src/search.h @@ -277,13 +277,18 @@ class Worker { void ensure_network_replicated(); // Public because they need to be updatable by the stats - ButterflyHistory mainHistory; - ButterflyHistory rootHistory; - CapturePieceToHistory captureHistory; - ContinuationHistory continuationHistory[2][2]; - PawnHistory pawnHistory; - PawnCorrectionHistory pawnCorrectionHistory; - MaterialCorrectionHistory materialCorrectionHistory; + ButterflyHistory mainHistory; + ButterflyHistory rootHistory; + + CapturePieceToHistory captureHistory; + ContinuationHistory continuationHistory[2][2]; + PawnHistory pawnHistory; + + PawnCorrectionHistory pawnCorrectionHistory; + MaterialCorrectionHistory materialCorrectionHistory; + MajorPieceCorrectionHistory majorPieceCorrectionHistory; + MinorPieceCorrectionHistory minorPieceCorrectionHistory; + NonPawnCorrectionHistory nonPawnCorrectionHistory[COLOR_NB]; private: void iterative_deepening(); diff --git a/tests/perft.sh b/tests/perft.sh index 545e750f..c1532c20 100755 --- a/tests/perft.sh +++ b/tests/perft.sh @@ -1,5 +1,5 @@ #!/bin/bash -# verify perft numbers (positions from www.chessprogramming.org/Perft_Results) +# verify perft numbers (positions from https://www.chessprogramming.org/Perft_Results) error() {