diff --git a/.github/workflows/arm_compilation.yml b/.github/workflows/arm_compilation.yml index 3934ac2d6..5bf2a93e5 100644 --- a/.github/workflows/arm_compilation.yml +++ b/.github/workflows/arm_compilation.yml @@ -26,6 +26,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - name: Download required linux packages if: runner.os == 'Linux' @@ -91,4 +92,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ matrix.config.simple_name }} ${{ matrix.binaries }} - path: . + path: | + . + !.git + !.output diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 630edbf93..637cfc0d8 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -19,6 +19,7 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Run clang-format style check uses: jidicula/clang-format-action@f62da5e3d3a2d88ff364771d9d938773a618ab5e # @v4.11.0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d949a5a76..d01ed41fe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,6 +30,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/compilation.yml b/.github/workflows/compilation.yml index 3524d5e9f..5878adecb 100644 --- a/.github/workflows/compilation.yml +++ b/.github/workflows/compilation.yml @@ -25,6 +25,8 @@ jobs: shell: ${{ matrix.config.shell }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Install fixed GCC on Linux if: runner.os == 'Linux' @@ -86,4 +88,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ matrix.config.simple_name }} ${{ matrix.binaries }} - path: . + path: | + . + !.git + !.output diff --git a/.github/workflows/games.yml b/.github/workflows/games.yml new file mode 100644 index 000000000..f0bca442f --- /dev/null +++ b/.github/workflows/games.yml @@ -0,0 +1,43 @@ +# This workflow will play games with a debug enabled SF using the PR + +name: Games +on: + workflow_call: +jobs: + Matetrack: + name: Games + runs-on: ubuntu-22.04 + steps: + - name: Checkout SF repo + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: Stockfish + persist-credentials: false + + - name: build debug enabled version of SF + working-directory: Stockfish/src + run: make -j build debug=yes + + - name: Checkout fast-chess repo + uses: actions/checkout@v4 + with: + repository: Disservin/fast-chess + path: fast-chess + ref: d54af1910d5479c669dc731f1f54f9108a251951 + persist-credentials: false + + - name: fast-chess build + working-directory: fast-chess + run: make -j + + - name: Run games + working-directory: fast-chess + run: | + ./fast-chess -rounds 4 -games 2 -repeat -concurrency 4 -openings file=app/tests/data/openings.epd format=epd order=random -srand $RANDOM\ + -engine name=sf1 cmd=/home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish\ + -engine name=sf2 cmd=/home/runner/work/Stockfish/Stockfish/Stockfish/src/stockfish\ + -ratinginterval 1 -report penta=true -each proto=uci tc=4+0.04 -log file=fast.log | tee fast.out + cat fast.log + ! grep "Assertion" fast.log > /dev/null + ! grep "disconnect" fast.out > /dev/null diff --git a/.github/workflows/iwyu.yml b/.github/workflows/iwyu.yml index 0552a598c..f8898b1c9 100644 --- a/.github/workflows/iwyu.yml +++ b/.github/workflows/iwyu.yml @@ -14,6 +14,7 @@ jobs: uses: actions/checkout@v4 with: path: Stockfish + persist-credentials: false - name: Checkout include-what-you-use uses: actions/checkout@v4 @@ -21,6 +22,7 @@ jobs: repository: include-what-you-use/include-what-you-use ref: f25caa280dc3277c4086ec345ad279a2463fea0f path: include-what-you-use + persist-credentials: false - name: Download required linux packages run: | diff --git a/.github/workflows/matetrack.yml b/.github/workflows/matetrack.yml index dd81f334d..de65209fb 100644 --- a/.github/workflows/matetrack.yml +++ b/.github/workflows/matetrack.yml @@ -13,6 +13,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} path: Stockfish + persist-credentials: false - name: build SF working-directory: Stockfish/src @@ -24,6 +25,7 @@ jobs: repository: vondele/matetrack path: matetrack ref: 20287a1a145f30a166b7ef251eddb611e4e44fbf + persist-credentials: false - name: matetrack install deps working-directory: matetrack diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index b75c06cfb..554592921 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -40,6 +40,8 @@ jobs: shell: ${{ matrix.config.shell }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Download required linux packages run: | diff --git a/.github/workflows/stockfish.yml b/.github/workflows/stockfish.yml index fcaa3f6b8..1f87e061b 100644 --- a/.github/workflows/stockfish.yml +++ b/.github/workflows/stockfish.yml @@ -15,8 +15,12 @@ jobs: Prerelease: if: github.repository == 'official-stockfish/Stockfish' && (github.ref == 'refs/heads/master' || (startsWith(github.ref_name, 'sf_') && github.ref_type == 'tag')) runs-on: ubuntu-latest + permissions: + contents: write # For deleting/creating a prerelease steps: - uses: actions/checkout@v4 + with: + persist-credentials: false # returns null if no pre-release exists - name: Get Commit SHA of Latest Pre-release @@ -66,6 +70,8 @@ jobs: arm_matrix: ${{ steps.set-arm-matrix.outputs.arm_matrix }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - id: set-matrix run: | TASKS=$(echo $(cat .github/ci/matrix.json) ) @@ -92,15 +98,25 @@ jobs: uses: ./.github/workflows/tests.yml Matetrack: uses: ./.github/workflows/matetrack.yml + Games: + uses: ./.github/workflows/games.yml Binaries: if: github.repository == 'official-stockfish/Stockfish' needs: [Matrix, Prerelease, Compilation] uses: ./.github/workflows/upload_binaries.yml with: matrix: ${{ needs.Matrix.outputs.matrix }} + permissions: + contents: write # For deleting/creating a (pre)release + secrets: + token: ${{ secrets.GITHUB_TOKEN }} ARM_Binaries: if: github.repository == 'official-stockfish/Stockfish' needs: [Matrix, Prerelease, ARMCompilation] uses: ./.github/workflows/upload_binaries.yml with: matrix: ${{ needs.Matrix.outputs.arm_matrix }} + permissions: + contents: write # For deleting/creating a (pre)release + secrets: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 328c9cf94..836555e61 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -106,6 +106,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - name: Download required linux packages if: runner.os == 'Linux' diff --git a/.github/workflows/upload_binaries.yml b/.github/workflows/upload_binaries.yml index acf91a8f3..c5a2cd105 100644 --- a/.github/workflows/upload_binaries.yml +++ b/.github/workflows/upload_binaries.yml @@ -5,6 +5,9 @@ on: matrix: type: string required: true + secrets: + token: + required: true jobs: Artifacts: @@ -25,6 +28,8 @@ jobs: shell: ${{ matrix.config.shell }} steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Download artifact from compilation uses: actions/download-artifact@v4 @@ -78,6 +83,7 @@ jobs: uses: softprops/action-gh-release@4634c16e79c963813287e889244c50009e7f0981 with: files: stockfish-${{ matrix.config.simple_name }}-${{ matrix.binaries }}.${{ matrix.config.archive_ext }} + token: ${{ secrets.token }} - name: Get last commit sha id: last_commit @@ -104,3 +110,4 @@ jobs: tag_name: stockfish-dev-${{ env.COMMIT_DATE }}-${{ env.COMMIT_SHA }} prerelease: true files: stockfish-${{ matrix.config.simple_name }}-${{ matrix.binaries }}.${{ matrix.config.archive_ext }} + token: ${{ secrets.token }} diff --git a/src/engine.cpp b/src/engine.cpp index 0003b1c22..a2e3ca44f 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -300,16 +300,20 @@ std::string Engine::get_numa_config_as_string() const { std::string Engine::numa_config_information_as_string() const { auto cfgStr = get_numa_config_as_string(); - return "Available Processors: " + cfgStr; + return "Available processors: " + cfgStr; } std::string Engine::thread_binding_information_as_string() const { - auto boundThreadsByNode = get_bound_thread_count_by_numa_node(); - if (boundThreadsByNode.empty()) - return ""; - + auto boundThreadsByNode = get_bound_thread_count_by_numa_node(); std::stringstream ss; - ss << "NUMA Node Thread Binding: "; + + size_t threadsSize = threads.size(); + ss << "Using " << threadsSize << (threadsSize > 1 ? " threads" : " thread"); + + if (boundThreadsByNode.empty()) + return ss.str(); + + ss << " with NUMA node thread binding: "; bool isFirst = true; diff --git a/src/misc.cpp b/src/misc.cpp index e97d58b93..b68c12b97 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -281,13 +281,21 @@ template struct DebugInfo { std::atomic data[N] = {0}; - constexpr inline std::atomic& operator[](int index) { return data[index]; } + constexpr std::atomic& operator[](int index) { return data[index]; } }; -DebugInfo<2> hit[MaxDebugSlots]; -DebugInfo<2> mean[MaxDebugSlots]; -DebugInfo<3> stdev[MaxDebugSlots]; -DebugInfo<6> correl[MaxDebugSlots]; +struct DebugExtremes: public DebugInfo<3> { + DebugExtremes() { + data[1] = std::numeric_limits::min(); + data[2] = std::numeric_limits::max(); + } +}; + +DebugInfo<2> hit[MaxDebugSlots]; +DebugInfo<2> mean[MaxDebugSlots]; +DebugInfo<3> stdev[MaxDebugSlots]; +DebugInfo<6> correl[MaxDebugSlots]; +DebugExtremes extremes[MaxDebugSlots]; } // namespace @@ -311,6 +319,18 @@ void dbg_stdev_of(int64_t value, int slot) { stdev[slot][2] += value * value; } +void dbg_extremes_of(int64_t value, int slot) { + ++extremes[slot][0]; + + int64_t current_max = extremes[slot][1].load(); + while (current_max < value && !extremes[slot][1].compare_exchange_weak(current_max, value)) + {} + + int64_t current_min = extremes[slot][2].load(); + while (current_min > value && !extremes[slot][2].compare_exchange_weak(current_min, value)) + {} +} + void dbg_correl_of(int64_t value1, int64_t value2, int slot) { ++correl[slot][0]; @@ -345,6 +365,13 @@ void dbg_print() { std::cerr << "Stdev #" << i << ": Total " << n << " Stdev " << r << std::endl; } + for (int i = 0; i < MaxDebugSlots; ++i) + if ((n = extremes[i][0])) + { + std::cerr << "Extremity #" << i << ": Total " << n << " Min " << extremes[i][2] + << " Max " << extremes[i][1] << std::endl; + } + for (int i = 0; i < MaxDebugSlots; ++i) if ((n = correl[i][0])) { diff --git a/src/misc.h b/src/misc.h index bdc7c864d..0184ab88c 100644 --- a/src/misc.h +++ b/src/misc.h @@ -67,6 +67,8 @@ std::optional read_file_to_string(const std::string& path); void dbg_hit_on(bool cond, int slot = 0); void dbg_mean_of(int64_t value, int slot = 0); void dbg_stdev_of(int64_t value, int slot = 0); +void dbg_extremes_of(int64_t value, int slot); + void dbg_correl_of(int64_t value1, int64_t value2, int slot = 0); void dbg_print(); diff --git a/src/position.cpp b/src/position.cpp index b46ba0299..d374b1c07 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -1156,9 +1156,9 @@ bool Position::has_repeated() const { } -// Tests if the position has a move which draws by repetition, -// or an earlier position has a move that directly reaches the current position. -bool Position::has_game_cycle(int ply) const { +// Tests if the position has a move which draws by repetition. +// This function accurately matches the outcome of is_draw() over all legal moves. +bool Position::upcoming_repetition(int ply) const { int j; @@ -1169,10 +1169,16 @@ bool Position::has_game_cycle(int ply) const { Key originalKey = st->key; StateInfo* stp = st->previous; + Key other = originalKey ^ stp->key ^ Zobrist::side; for (int i = 3; i <= end; i += 2) { - stp = stp->previous->previous; + stp = stp->previous; + other ^= stp->key ^ stp->previous->key ^ Zobrist::side; + stp = stp->previous; + + if (other != 0) + continue; Key moveKey = originalKey ^ stp->key; if ((j = H1(moveKey), cuckoo[j] == moveKey) || (j = H2(moveKey), cuckoo[j] == moveKey)) @@ -1188,12 +1194,6 @@ bool Position::has_game_cycle(int ply) const { // For nodes before or at the root, check that the move is a // repetition rather than a move to the current position. - // In the cuckoo table, both moves Rc1c5 and Rc5c1 are stored in - // the same location, so we have to select which square to check. - if (color_of(piece_on(empty(s1) ? s2 : s1)) != side_to_move()) - continue; - - // For repetitions before or at the root, require one more if (stp->repetition) return true; } diff --git a/src/position.h b/src/position.h index 154ed6529..3cfb87d06 100644 --- a/src/position.h +++ b/src/position.h @@ -156,7 +156,7 @@ class Position { int game_ply() const; bool is_chess960() const; bool is_draw(int ply) const; - bool has_game_cycle(int ply) const; + bool upcoming_repetition(int ply) const; bool has_repeated() const; int rule50_count() const; Value non_pawn_material(Color c) const; diff --git a/src/search.cpp b/src/search.cpp index 5f607ef83..c8ee51387 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -63,7 +63,7 @@ static constexpr double EvalLevel[10] = {0.981, 0.956, 0.895, 0.949, 0.913, Value futility_margin(Depth d, bool noTtCutNode, bool improving, bool oppWorsening) { Value futilityMult = 109 - 40 * noTtCutNode; Value improvingDeduction = 59 * improving * futilityMult / 32; - Value worseningDeduction = 328 * oppWorsening * futilityMult / 1024; + Value worseningDeduction = oppWorsening * futilityMult / 3; return futilityMult * d - improvingDeduction - worseningDeduction; } @@ -550,7 +550,7 @@ void Search::Worker::iterative_deepening() { void Search::Worker::clear() { counterMoves.fill(Move::none()); mainHistory.fill(0); - captureHistory.fill(0); + captureHistory.fill(-700); pawnHistory.fill(-1193); correctionHistory.fill(0); @@ -582,9 +582,8 @@ Value Search::Worker::search( // Limit the depth if extensions made it too large depth = std::min(depth, MAX_PLY - 1); - // Check if we have an upcoming move that draws by repetition, or - // if the opponent had an alternative move earlier to this position. - if (!rootNode && alpha < VALUE_DRAW && pos.has_game_cycle(ss->ply)) + // Check if we have an upcoming move that draws by repetition. + if (!rootNode && alpha < VALUE_DRAW && pos.upcoming_repetition(ss->ply)) { alpha = value_draw(this->nodes); if (alpha >= beta) @@ -800,8 +799,7 @@ Value Search::Worker::search( // Use static evaluation difference to improve quiet move ordering (~9 Elo) if (((ss - 1)->currentMove).is_ok() && !(ss - 1)->inCheck && !priorCapture) { - int bonus = std::clamp(-10 * int((ss - 1)->staticEval + ss->staticEval), -1590, 1371); - bonus = bonus > 0 ? 2 * bonus : bonus / 2; + int bonus = std::clamp(-10 * int((ss - 1)->staticEval + ss->staticEval), -1590, 1371) + 800; thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] << bonus; if (type_of(pos.piece_on(prevSq)) != PAWN && ((ss - 1)->currentMove).type_of() != PROMOTION) thisThread->pawnHistory[pawn_structure_index(pos)][pos.piece_on(prevSq)][prevSq] @@ -835,8 +833,9 @@ Value Search::Worker::search( && eval - futility_margin(depth, cutNode && !ss->ttHit, improving, opponentWorsening) - (ss - 1)->statScore / 263 >= beta - && eval >= beta && eval < VALUE_TB_WIN_IN_MAX_PLY && (!ttData.move || ttCapture)) - return beta > VALUE_TB_LOSS_IN_MAX_PLY ? beta + (eval - beta) / 3 : eval; + && eval >= beta && (!ttData.move || ttCapture) && beta > VALUE_TB_LOSS_IN_MAX_PLY + && eval < VALUE_TB_WIN_IN_MAX_PLY) + return beta + (eval - beta) / 3; // Step 9. Null move search with verification search (~35 Elo) if (!PvNode && (ss - 1)->currentMove != Move::null() && (ss - 1)->statScore < 14369 @@ -880,9 +879,12 @@ Value Search::Worker::search( } // Step 10. Internal iterative reductions (~9 Elo) - // For PV nodes without a ttMove, we decrease depth by 3. + // For PV nodes without a ttMove, we decrease depth. + // Additionally, if the current position is found in the TT + // and the stored depth in the TT is greater than or equal to + // current search depth, we decrease search depth even further. if (PvNode && !ttData.move) - depth -= 3; + depth -= 3 + (ss->ttHit && ttData.depth >= depth); // Use qsearch if depth <= 0. if (depth <= 0) @@ -909,12 +911,19 @@ Value Search::Worker::search( assert(probCutBeta < VALUE_INFINITE && probCutBeta > beta); MovePicker mp(pos, ttData.move, probCutBeta - ss->staticEval, &thisThread->captureHistory); + Move probcutCapturesSearched[32]; + int probcutCaptureCount = 0; + Piece captured; while ((move = mp.next_move()) != Move::none()) if (move != excludedMove && pos.legal(move)) { assert(pos.capture_stage(move)); + movedPiece = pos.moved_piece(move); + captured = pos.piece_on(move.to_sq()); + + // Prefetch the TT entry for the resulting position prefetch(tt.first_entry(pos.key_after(move))); @@ -938,6 +947,19 @@ Value Search::Worker::search( if (value >= probCutBeta) { + thisThread->captureHistory[movedPiece][move.to_sq()][type_of(captured)] + << stat_bonus(depth - 2); + + for (int i = 0; i < probcutCaptureCount; i++) + { + movedPiece = pos.moved_piece(probcutCapturesSearched[i]); + captured = pos.piece_on(probcutCapturesSearched[i].to_sq()); + + thisThread->captureHistory[movedPiece][probcutCapturesSearched[i].to_sq()] + [type_of(captured)] + << -stat_malus(depth - 3); + } + // Save ProbCut data into transposition table Distributed::save(tt, threads, thisThread, ttWriter, posKey, value_to_tt(value, ss->ply), ss->ttPv, BOUND_LOWER, depth - 3, @@ -945,6 +967,9 @@ Value Search::Worker::search( return std::abs(value) < VALUE_TB_WIN_IN_MAX_PLY ? value - (probCutBeta - beta) : value; } + + if (probcutCaptureCount < 32) + probcutCapturesSearched[probcutCaptureCount++] = move; } Eval::NNUE::hint_common_parent_position(pos, networks[numaAccessToken], refreshTable); @@ -954,9 +979,8 @@ moves_loop: // When in check, search starts here // Step 12. A small Probcut idea, when we are in check (~4 Elo) probCutBeta = beta + 388; - if (ss->inCheck && !PvNode && ttCapture && (ttData.bound & BOUND_LOWER) - && ttData.depth >= depth - 4 && ttData.value >= probCutBeta - && std::abs(ttData.value) < VALUE_TB_WIN_IN_MAX_PLY + if (ss->inCheck && (ttData.bound & BOUND_LOWER) && ttData.depth >= depth - 4 + && ttData.value >= probCutBeta && std::abs(ttData.value) < VALUE_TB_WIN_IN_MAX_PLY && std::abs(beta) < VALUE_TB_WIN_IN_MAX_PLY) return probCutBeta; @@ -1042,7 +1066,7 @@ moves_loop: // When in check, search starts here // Futility pruning for captures (~2 Elo) if (!givesCheck && lmrDepth < 7 && !ss->inCheck) { - Value futilityValue = ss->staticEval + 287 + 248 * lmrDepth + Value futilityValue = ss->staticEval + 294 + 246 * lmrDepth + PieceValue[capturedPiece] + captHist / 7; if (futilityValue <= alpha) continue; @@ -1050,7 +1074,7 @@ moves_loop: // When in check, search starts here // SEE based pruning for captures and checks (~11 Elo) int seeHist = std::clamp(captHist / 32, -180 * depth, 163 * depth); - if (!pos.see_ge(move, -160 * depth - seeHist)) + if (!pos.see_ge(move, -163 * depth - seeHist)) continue; } else @@ -1061,15 +1085,15 @@ moves_loop: // When in check, search starts here + thisThread->pawnHistory[pawn_structure_index(pos)][movedPiece][move.to_sq()]; // Continuation history based pruning (~2 Elo) - if (lmrDepth < 6 && history < -4151 * depth) + if (lmrDepth < 6 && history < -3899 * depth) continue; history += 2 * thisThread->mainHistory[us][move.from_to()]; - lmrDepth += history / 3678; + lmrDepth += history / 4040; Value futilityValue = - ss->staticEval + (bestValue < ss->staticEval - 51 ? 138 : 54) + 140 * lmrDepth; + ss->staticEval + (bestValue < ss->staticEval - 51 ? 135 : 56) + 140 * lmrDepth; // Futility pruning: parent node (~13 Elo) if (!ss->inCheck && lmrDepth < 12 && futilityValue <= alpha) @@ -1135,8 +1159,8 @@ moves_loop: // When in check, search starts here // and if after excluding the ttMove with a reduced search we fail high over the original beta, // we assume this expected cut-node is not singular (multiple moves fail high), // and we can prune the whole subtree by returning a softbound. - else if (singularBeta >= beta) - return singularBeta; + else if (value >= beta && std::abs(value) < VALUE_TB_WIN_IN_MAX_PLY) + return value; // Negative extensions // If other moves failed high over (ttValue - margin) without the ttMove on a reduced search, @@ -1185,7 +1209,8 @@ moves_loop: // When in check, search starts here // Decrease reduction if position is or has been on the PV (~7 Elo) if (ss->ttPv) - r -= 1 + (ttData.value > alpha) + (ttData.depth >= depth); + r -= 1 + (ttData.value > alpha) + (ttData.depth >= depth) + - (PvNode && ttData.value < alpha && ttData.depth >= depth); // Decrease reduction for PvNodes (~0 Elo on STC, ~2 Elo on LTC) if (PvNode) @@ -1204,7 +1229,7 @@ moves_loop: // When in check, search starts here // Increase reduction if next ply has a lot of fail high (~5 Elo) if ((ss + 1)->cutoffCnt > 3) - r++; + r += 1 + !(PvNode || cutNode); // For first picked move (ttMove) reduce reduction // but never allow it to go below 0 (~3 Elo) @@ -1293,7 +1318,7 @@ moves_loop: // When in check, search starts here rm.effort += nodes - nodeCount; rm.averageScore = - rm.averageScore != -VALUE_INFINITE ? (2 * value + rm.averageScore) / 3 : value; + rm.averageScore != -VALUE_INFINITE ? (value + rm.averageScore) / 2 : value; // PV move or new best move? if (moveCount == 1 || value > alpha) @@ -1333,11 +1358,17 @@ moves_loop: // When in check, search starts here rm.score = -VALUE_INFINITE; } - if (value > bestValue) + // In case we have an alternative move equal in eval to the current bestmove, + // promote it to bestmove by pretending it just exceeds alpha (but not beta). + int inc = (value == bestValue && (int(nodes) & 15) == 0 + && ss->ply + 2 + ss->ply / 32 >= thisThread->rootDepth + && std::abs(value) + 1 < VALUE_TB_WIN_IN_MAX_PLY); + + if (value + inc > bestValue) { bestValue = value; - if (value > alpha) + if (value + inc > alpha) { bestMove = move; @@ -1396,10 +1427,16 @@ moves_loop: // When in check, search starts here // Bonus for prior countermove that caused the fail low else if (!priorCapture && prevSq != SQ_NONE) { - int bonus = (113 * (depth > 5) + 118 * (PvNode || cutNode) - + 191 * ((ss - 1)->statScore < -14396) + 119 * ((ss - 1)->moveCount > 8) + int bonus = (113 * (depth > 5) + 118 * (PvNode || cutNode) + 119 * ((ss - 1)->moveCount > 8) + 64 * (!ss->inCheck && bestValue <= ss->staticEval - 107) + 147 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 75)); + + + // proportional to "how much damage we have to undo" + if ((ss - 1)->statScore < -8000) + bonus += std::clamp(-(ss - 1)->statScore / 100, 0, 250); + + update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, stat_bonus(depth) * bonus / 100); thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] @@ -1461,9 +1498,8 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, assert(PvNode || (alpha == beta - 1)); assert(depth <= 0); - // Check if we have an upcoming move that draws by repetition, or if - // the opponent had an alternative move earlier to this position. (~1 Elo) - if (alpha < VALUE_DRAW && pos.has_game_cycle(ss->ply)) + // Check if we have an upcoming move that draws by repetition. (~1 Elo) + if (alpha < VALUE_DRAW && pos.upcoming_repetition(ss->ply)) { alpha = value_draw(this->nodes); if (alpha >= beta) @@ -1633,7 +1669,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, // If static exchange evaluation is much worse than what is needed to not // fall below alpha we can prune this move. - if (futilityBase > alpha && !pos.see_ge(move, (alpha - futilityBase) * 2 - 30)) + if (futilityBase > alpha && !pos.see_ge(move, (alpha - futilityBase) * 4)) { bestValue = alpha; continue; diff --git a/src/thread.h b/src/thread.h index 1f48c98bf..e76269cf0 100644 --- a/src/thread.h +++ b/src/thread.h @@ -40,10 +40,10 @@ namespace Stockfish { class OptionsMap; using Value = int; -// Sometimes we don't want to actually bind the threads, but the recipent still +// Sometimes we don't want to actually bind the threads, but the recipient still // needs to think it runs on *some* NUMA node, such that it can access structures // that rely on NUMA node knowledge. This class encapsulates this optional process -// such that the recipent does not need to know whether the binding happened or not. +// such that the recipient does not need to know whether the binding happened or not. class OptionalThreadToNumaNodeBinder { public: OptionalThreadToNumaNodeBinder(NumaIndex n) : @@ -88,7 +88,7 @@ class Thread { // this name is no longer correct. However, this class (and ThreadPool) // require further work to make them properly generic while maintaining // appropriate specificity regarding search, from the point of view of an - // outside user, so renaming of this function in left for whenever that happens. + // outside user, so renaming of this function is left for whenever that happens. void wait_for_search_finished(); size_t id() const { return idx; } diff --git a/src/tt.cpp b/src/tt.cpp index 763e2c9b3..4b55e53fd 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -55,6 +55,7 @@ struct TTEntry { Bound(genBound8 & 0x3), bool(genBound8 & 0x4)}; } + bool is_occupied() const; void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8); // The returned age is a multiple of TranspositionTable::GENERATION_DELTA uint8_t relative_age(const uint8_t generation8) const; @@ -84,7 +85,8 @@ static constexpr int GENERATION_MASK = (0xFF << GENERATION_BITS) & 0xFF; // DEPTH_ENTRY_OFFSET exists because 1) we use `bool(depth8)` as the occupancy check, but // 2) we need to store negative depths for QS. (`depth8` is the only field with "spare bits": -// we sacrifice the ability to store depths greater than 1<<8 less the offset, as asserted below.) +// we sacrifice the ability to store depths greater than 1<<8 less the offset, as asserted in `save`.) +bool TTEntry::is_occupied() const { return bool(depth8); } // Populates the TTEntry with a new node's data, possibly // overwriting an old position. The update is not atomic and can be racy. @@ -196,7 +198,7 @@ int TranspositionTable::hashfull() const { int cnt = 0; for (int i = 0; i < 1000; ++i) for (int j = 0; j < ClusterSize; ++j) - cnt += table[i].entry[j].depth8 + cnt += table[i].entry[j].is_occupied() && (table[i].entry[j].genBound8 & GENERATION_MASK) == generation8; return cnt / ClusterSize; @@ -227,7 +229,7 @@ std::tuple TranspositionTable::probe(const Key key) cons if (tte[i].key16 == key16) // This gap is the main place for read races. // After `read()` completes that copy is final, but may be self-inconsistent. - return {bool(tte[i].depth8), tte[i].read(), TTWriter(&tte[i])}; + return {tte[i].is_occupied(), tte[i].read(), TTWriter(&tte[i])}; // Find an entry to be replaced according to the replacement strategy TTEntry* replace = tte; @@ -236,7 +238,7 @@ std::tuple TranspositionTable::probe(const Key key) cons > tte[i].depth8 - tte[i].relative_age(generation8) * 2) replace = &tte[i]; - return {false, replace->read(), TTWriter(replace)}; + return {false, TTData(), TTWriter(replace)}; } diff --git a/src/uci.cpp b/src/uci.cpp index 17dbf6c10..8a26d36a0 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -110,16 +110,18 @@ void UCIEngine::loop() { sync_cout << "id name " << engine_info(true) << "\n" << engine.get_options() << sync_endl; - print_info_string(engine.numa_config_information_as_string()); - print_info_string(engine.thread_binding_information_as_string()); - sync_cout << "uciok" << sync_endl; } else if (token == "setoption") setoption(is); else if (token == "go") + { + // send info strings after the go command is sent for old GUIs and python-chess + print_info_string(engine.numa_config_information_as_string()); + print_info_string(engine.thread_binding_information_as_string()); go(is); + } else if (token == "position") position(is); else if (token == "ucinewgame")