mirror of
https://github.com/official-stockfish/Stockfish.git
synced 2026-07-22 20:57:10 +00:00
Currently SF’s quiescence search like most alpha-beta based engines
doesn’t verify for stalemate because doing it each leaf position is to
expensive and costs elo. However in certain positions this creates a
blindspot for SF, not recognizing soon enough that the opponent can
reach a stalemate by sacrifycing his last mobile heavy piece(s). This
tactical motif & it’s measure are similar to zugzwang & verification
search: the measure itself does not gain elo, but prevents SF from
loosing/drawing games in an awkward way.
The fix consists of 3 measures:
1. Make qsearch verify for stalemate on transitions to pure KP-material
for the side to move with our last Rook/Queen just been captured. In
fact this is the scenario where stalemate happens with highest
frequency. The stalemate-verification itself is optimized by merely
checking for pawn pushes & king mobility (captures were already
tried by qssearch)
2. Another culprit for the issue figured out to be SEE based pruning for
checks in step 14. Here often the move forcing the stalemate (or
forcing the opponent to not retake) get pruned away and it need to
much time to reach enough depth. To encounter this we verify
following conditions:
- a) side to move is happy with a draw (alpha < 0)
- b) we are about to sacrify our last heavy & unique mobile piece in
this position.
- c) this piece doesn’t move away from our kingring giving the king a
new square to move.
When all 3 conditions meet we don’t prune the move, because there
is a good chance that capturing the piece means stalemate.
3. Store terminal nodes (mates & stalemates) in TT with higher depth
than searched depth. This prevents SF from:
- reanalyzing the node (=trying to generate legal moves) in vain at
each iterative deepening step.
- overwriting an already correct draw-evaluation from a previous shallow
normal search by a qsearch which doesn’t recognize stalemate and might
store a verry erratic evaluation.
This is due to the 4 constant in the TT-overwrite condition: d -
DEPTH_ENTRY_OFFSET + 2 * pv > depth8 – 4 which allows qs to override
entries made by normal searches with depth <=4.
This 3hrd measure however is not essential for fixing the issue, but
tests (one of vdv & one of mine) seem to suggest that this measure
brings some small benefit.
Another other position where SF benefits from this fix is for instance
Position FEN 8/8/8/1B6/6p1/8/3K1Ppp/3N2kr w - - 0 1 bm f4 +M9
P.S.: Also this issue higly depends on the used net, how good the net is
at evaluate such mobility restricted positions. SF16 was pretty good in
solving 2rr4/5pBk/PqP3p1/1N3pPp/1PQ1bP1P/8/3R4/R4K2 b - - 0 40 bm Rxc6
(< 1 second) while SF16_1 with introduction of the dual net needs about
1,5 minutes and SF17.1 requires 3 minutes to find the drawing move Rxc6.
P.S.2: Using more threads produces indeterminism & using high hash
pressure makes SF reevaluate explored positions more often which makes
it more likely to solve the position. To have stable meaningful results
I tested therfore with one single thread and low hash pressure.
Preliminary LTC test at 30k games
https://tests.stockfishchess.org/tests/view/67ece7a931d7cf8afdc44e18 Elo: 0.04 ± 2.0 (95%) LOS: 51.7%
Total: 24416 W: 6226 L: 6223 D: 11967
Ptnml(0-2): 12, 2497, 7185, 2504, 10
nElo: 0.09 ± 4.4 (95%) PairsRatio: 1.00
Passed LTC no-regression sprt
https://tests.stockfishchess.org/tests/view/67ee8e4631d7cf8afdc452fb
LLR: 2.94 (-2.94,2.94) <-1.75,0.25>
Total: 401556 W: 101612 L: 101776 D: 198168
Ptnml(0-2): 152, 42241, 116170, 42049, 166
closes https://github.com/official-stockfish/Stockfish/pull/5983
fixes https://github.com/official-stockfish/Stockfish/issues/5899
Bench: 1721673
347 lines
11 KiB
C++
347 lines
11 KiB
C++
/*
|
|
Stockfish, a UCI chess playing engine derived from Glaurung 2.1
|
|
Copyright (C) 2004-2025 The Stockfish developers (see AUTHORS file)
|
|
|
|
Stockfish is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Stockfish is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "movepick.h"
|
|
|
|
#include <cassert>
|
|
#include <cstddef>
|
|
#include <limits>
|
|
|
|
#include "bitboard.h"
|
|
#include "misc.h"
|
|
#include "position.h"
|
|
|
|
namespace Stockfish {
|
|
|
|
namespace {
|
|
|
|
enum Stages {
|
|
// generate main search moves
|
|
MAIN_TT,
|
|
CAPTURE_INIT,
|
|
GOOD_CAPTURE,
|
|
QUIET_INIT,
|
|
GOOD_QUIET,
|
|
BAD_CAPTURE,
|
|
BAD_QUIET,
|
|
|
|
// generate evasion moves
|
|
EVASION_TT,
|
|
EVASION_INIT,
|
|
EVASION,
|
|
|
|
// generate probcut moves
|
|
PROBCUT_TT,
|
|
PROBCUT_INIT,
|
|
PROBCUT,
|
|
|
|
// generate qsearch moves
|
|
QSEARCH_TT,
|
|
QCAPTURE_INIT,
|
|
QCAPTURE
|
|
};
|
|
|
|
// Sort moves in descending order up to and including a given limit.
|
|
// The order of moves smaller than the limit is left unspecified.
|
|
void partial_insertion_sort(ExtMove* begin, ExtMove* end, int limit) {
|
|
|
|
for (ExtMove *sortedEnd = begin, *p = begin + 1; p < end; ++p)
|
|
if (p->value >= limit)
|
|
{
|
|
ExtMove tmp = *p, *q;
|
|
*p = *++sortedEnd;
|
|
for (q = sortedEnd; q != begin && *(q - 1) < tmp; --q)
|
|
*q = *(q - 1);
|
|
*q = tmp;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
// Constructors of the MovePicker class. As arguments, we pass information
|
|
// to decide which class of moves to emit, to help sorting the (presumably)
|
|
// good moves first, and how important move ordering is at the current node.
|
|
|
|
// MovePicker constructor for the main search and for the quiescence search
|
|
MovePicker::MovePicker(const Position& p,
|
|
Move ttm,
|
|
Depth d,
|
|
const ButterflyHistory* mh,
|
|
const LowPlyHistory* lph,
|
|
const CapturePieceToHistory* cph,
|
|
const PieceToHistory** ch,
|
|
const PawnHistory* ph,
|
|
int pl) :
|
|
pos(p),
|
|
mainHistory(mh),
|
|
lowPlyHistory(lph),
|
|
captureHistory(cph),
|
|
continuationHistory(ch),
|
|
pawnHistory(ph),
|
|
ttMove(ttm),
|
|
depth(d),
|
|
ply(pl) {
|
|
|
|
if (pos.checkers())
|
|
stage = EVASION_TT + !(ttm && pos.pseudo_legal(ttm));
|
|
|
|
else
|
|
stage = (depth > 0 ? MAIN_TT : QSEARCH_TT) + !(ttm && pos.pseudo_legal(ttm));
|
|
}
|
|
|
|
// MovePicker constructor for ProbCut: we generate captures with Static Exchange
|
|
// Evaluation (SEE) greater than or equal to the given threshold.
|
|
MovePicker::MovePicker(const Position& p, Move ttm, int th, const CapturePieceToHistory* cph) :
|
|
pos(p),
|
|
captureHistory(cph),
|
|
ttMove(ttm),
|
|
threshold(th) {
|
|
assert(!pos.checkers());
|
|
|
|
stage = PROBCUT_TT
|
|
+ !(ttm && pos.capture_stage(ttm) && pos.pseudo_legal(ttm) && pos.see_ge(ttm, threshold));
|
|
}
|
|
|
|
// Assigns a numerical value to each move in a list, used for sorting.
|
|
// Captures are ordered by Most Valuable Victim (MVV), preferring captures
|
|
// with a good history. Quiets moves are ordered using the history tables.
|
|
template<GenType Type>
|
|
void MovePicker::score() {
|
|
|
|
static_assert(Type == CAPTURES || Type == QUIETS || Type == EVASIONS, "Wrong type");
|
|
|
|
[[maybe_unused]] Bitboard threatenedByPawn, threatenedByMinor, threatenedByRook,
|
|
threatenedPieces;
|
|
if constexpr (Type == QUIETS)
|
|
{
|
|
Color us = pos.side_to_move();
|
|
|
|
threatenedByPawn = pos.attacks_by<PAWN>(~us);
|
|
threatenedByMinor =
|
|
pos.attacks_by<KNIGHT>(~us) | pos.attacks_by<BISHOP>(~us) | threatenedByPawn;
|
|
threatenedByRook = pos.attacks_by<ROOK>(~us) | threatenedByMinor;
|
|
|
|
// Pieces threatened by pieces of lesser material value
|
|
threatenedPieces = (pos.pieces(us, QUEEN) & threatenedByRook)
|
|
| (pos.pieces(us, ROOK) & threatenedByMinor)
|
|
| (pos.pieces(us, KNIGHT, BISHOP) & threatenedByPawn);
|
|
}
|
|
|
|
for (auto& m : *this)
|
|
if constexpr (Type == CAPTURES)
|
|
m.value =
|
|
7 * int(PieceValue[pos.piece_on(m.to_sq())])
|
|
+ (*captureHistory)[pos.moved_piece(m)][m.to_sq()][type_of(pos.piece_on(m.to_sq()))];
|
|
|
|
else if constexpr (Type == QUIETS)
|
|
{
|
|
Piece pc = pos.moved_piece(m);
|
|
PieceType pt = type_of(pc);
|
|
Square from = m.from_sq();
|
|
Square to = m.to_sq();
|
|
|
|
// histories
|
|
m.value = 2 * (*mainHistory)[pos.side_to_move()][m.from_to()];
|
|
m.value += 2 * (*pawnHistory)[pawn_structure_index(pos)][pc][to];
|
|
m.value += (*continuationHistory[0])[pc][to];
|
|
m.value += (*continuationHistory[1])[pc][to];
|
|
m.value += (*continuationHistory[2])[pc][to];
|
|
m.value += (*continuationHistory[3])[pc][to];
|
|
m.value += (*continuationHistory[5])[pc][to];
|
|
|
|
// bonus for checks
|
|
m.value += bool(pos.check_squares(pt) & to) * 16384;
|
|
|
|
// bonus for escaping from capture
|
|
m.value += threatenedPieces & from ? (pt == QUEEN && !(to & threatenedByRook) ? 51700
|
|
: pt == ROOK && !(to & threatenedByMinor) ? 25600
|
|
: !(to & threatenedByPawn) ? 14450
|
|
: 0)
|
|
: 0;
|
|
|
|
// malus for putting piece en prise
|
|
m.value -= (pt == QUEEN && bool(to & threatenedByRook) ? 49000
|
|
: pt == ROOK && bool(to & threatenedByMinor) ? 24335
|
|
: 0);
|
|
|
|
if (ply < LOW_PLY_HISTORY_SIZE)
|
|
m.value += 8 * (*lowPlyHistory)[ply][m.from_to()] / (1 + 2 * ply);
|
|
}
|
|
|
|
else // Type == EVASIONS
|
|
{
|
|
if (pos.capture_stage(m))
|
|
m.value = PieceValue[pos.piece_on(m.to_sq())] + (1 << 28);
|
|
else
|
|
m.value = (*mainHistory)[pos.side_to_move()][m.from_to()]
|
|
+ (*continuationHistory[0])[pos.moved_piece(m)][m.to_sq()]
|
|
+ (*pawnHistory)[pawn_structure_index(pos)][pos.moved_piece(m)][m.to_sq()];
|
|
}
|
|
}
|
|
|
|
// Returns the next move satisfying a predicate function.
|
|
// This never returns the TT move, as it was emitted before.
|
|
template<typename Pred>
|
|
Move MovePicker::select(Pred filter) {
|
|
|
|
for (; cur < endMoves; ++cur)
|
|
if (*cur != ttMove && filter())
|
|
return *cur++;
|
|
|
|
return Move::none();
|
|
}
|
|
|
|
// This is the most important method of the MovePicker class. We emit one
|
|
// new pseudo-legal move on every call until there are no more moves left,
|
|
// picking the move with the highest score from a list of generated moves.
|
|
Move MovePicker::next_move() {
|
|
|
|
auto quiet_threshold = [](Depth d) { return -3560 * d; };
|
|
|
|
top:
|
|
switch (stage)
|
|
{
|
|
|
|
case MAIN_TT :
|
|
case EVASION_TT :
|
|
case QSEARCH_TT :
|
|
case PROBCUT_TT :
|
|
++stage;
|
|
return ttMove;
|
|
|
|
case CAPTURE_INIT :
|
|
case PROBCUT_INIT :
|
|
case QCAPTURE_INIT :
|
|
cur = endBadCaptures = moves;
|
|
endMoves = generate<CAPTURES>(pos, cur);
|
|
|
|
score<CAPTURES>();
|
|
partial_insertion_sort(cur, endMoves, std::numeric_limits<int>::min());
|
|
++stage;
|
|
goto top;
|
|
|
|
case GOOD_CAPTURE :
|
|
if (select([&]() {
|
|
// Move losing capture to endBadCaptures to be tried later
|
|
return pos.see_ge(*cur, -cur->value / 18) ? true
|
|
: (*endBadCaptures++ = *cur, false);
|
|
}))
|
|
return *(cur - 1);
|
|
|
|
++stage;
|
|
[[fallthrough]];
|
|
|
|
case QUIET_INIT :
|
|
if (!skipQuiets)
|
|
{
|
|
cur = endBadCaptures;
|
|
endMoves = beginBadQuiets = endBadQuiets = generate<QUIETS>(pos, cur);
|
|
|
|
score<QUIETS>();
|
|
partial_insertion_sort(cur, endMoves, quiet_threshold(depth));
|
|
}
|
|
|
|
++stage;
|
|
[[fallthrough]];
|
|
|
|
case GOOD_QUIET :
|
|
if (!skipQuiets && select([]() { return true; }))
|
|
{
|
|
if ((cur - 1)->value > -7998 || (cur - 1)->value <= quiet_threshold(depth))
|
|
return *(cur - 1);
|
|
|
|
// Remaining quiets are bad
|
|
beginBadQuiets = cur - 1;
|
|
}
|
|
|
|
// Prepare the pointers to loop over the bad captures
|
|
cur = moves;
|
|
endMoves = endBadCaptures;
|
|
|
|
++stage;
|
|
[[fallthrough]];
|
|
|
|
case BAD_CAPTURE :
|
|
if (select([]() { return true; }))
|
|
return *(cur - 1);
|
|
|
|
// Prepare the pointers to loop over the bad quiets
|
|
cur = beginBadQuiets;
|
|
endMoves = endBadQuiets;
|
|
|
|
++stage;
|
|
[[fallthrough]];
|
|
|
|
case BAD_QUIET :
|
|
if (!skipQuiets)
|
|
return select([]() { return true; });
|
|
|
|
return Move::none();
|
|
|
|
case EVASION_INIT :
|
|
cur = moves;
|
|
endMoves = generate<EVASIONS>(pos, cur);
|
|
|
|
score<EVASIONS>();
|
|
partial_insertion_sort(cur, endMoves, std::numeric_limits<int>::min());
|
|
++stage;
|
|
[[fallthrough]];
|
|
|
|
case EVASION :
|
|
case QCAPTURE :
|
|
return select([]() { return true; });
|
|
|
|
case PROBCUT :
|
|
return select([&]() { return pos.see_ge(*cur, threshold); });
|
|
}
|
|
|
|
assert(false);
|
|
return Move::none(); // Silence warning
|
|
}
|
|
|
|
void MovePicker::skip_quiet_moves() { skipQuiets = true; }
|
|
|
|
bool MovePicker::otherPieceTypesMobile(PieceType pt, ValueList<Move, 32>& capturesSearched) {
|
|
if (stage != GOOD_QUIET && stage != BAD_QUIET)
|
|
return true;
|
|
|
|
// verify good captures
|
|
for (std::size_t i = 0; i < capturesSearched.size(); i++)
|
|
if (type_of(pos.moved_piece(capturesSearched[i])) != pt)
|
|
{
|
|
if (type_of(pos.moved_piece(capturesSearched[i])) != KING)
|
|
return true;
|
|
if (pos.legal(capturesSearched[i]))
|
|
return true;
|
|
}
|
|
|
|
// now verify bad captures and quiets
|
|
for (ExtMove* c = moves; c < endBadQuiets; ++c)
|
|
if (type_of(pos.moved_piece(*c)) != pt)
|
|
{
|
|
if (type_of(pos.moved_piece(*c)) != KING)
|
|
return true;
|
|
if (pos.legal(*c))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace Stockfish
|