summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Gasch <[email protected]>2026-10-07 08:17:51 -0700
committerScott Gasch <[email protected]>2026-10-07 08:17:51 -0700
commitd14dbe2a50c220b2adac758c3d4f1a1fa67ece0b (patch)
treecbf6d9928bcc41ef833b62b70479113f7d419925
parent447130db3df87b40d080dc26429d45e9213c19f7 (diff)
Speed up eval/move-gen/SEE; remove EvalLookForDanger, overhaul _EvalKing
SPRT-confirmed improvement (1814 games, st 4.0): candidate nps=1498118 vs head=1378448, avg depth 14.30 vs 13.87, ELO=+24.2+/-16.0, LOWER95=0.5118, LLR=+3.126 crossing the H1 bound. Most of the diff is non-semantic speed work across eval.c, generate.c, movesup.c, see.c, probe.c and friends. Two real semantic changes: removed EvalLookForDanger and overhauled _EvalKing's king-safety scoring. Co-Authored-By: Claude Sonnet 5 <[email protected]>
-rw-r--r--src/GNUmakefile10
-rwxr-xr-xsrc/chess.h135
-rw-r--r--src/data.c5
-rw-r--r--src/eval.c582
-rw-r--r--src/generate.c725
-rw-r--r--src/main.c6
-rw-r--r--src/movesup.c275
-rw-r--r--src/probe.c35
-rw-r--r--src/recogn.c4
-rw-r--r--src/root.c5
-rw-r--r--src/search.c37
-rw-r--r--src/searchsup.c29
-rw-r--r--src/see.c111
-rw-r--r--src/split.c7
-rw-r--r--src/testgenerate.c322
-rw-r--r--src/testsee.c155
-rw-r--r--src/util.c6
17 files changed, 2019 insertions, 430 deletions
diff --git a/src/GNUmakefile b/src/GNUmakefile
index 14fa65a..8a4904b 100644
--- a/src/GNUmakefile
+++ b/src/GNUmakefile
@@ -9,7 +9,6 @@
# BOUNDS_CHECKING=1: make version with bounds checking enabled
# MP=1: make version for multi-processor machine
# DUMP_TREE=1: internal tree dumping version
-# SEE_HEAPS=1: use minheaps in the SEE code
# OSX=1: build for an Intel-based Apple Mac (omit this for FreeBSD/Linux)
# USE_READLINE=1: link against the GNU readline library
# SIXTYFOUR=1: make an X64 binary
@@ -26,12 +25,12 @@
# match_play.py). The mailbox code is still fully present and
# compiled either way -- this flag only flips which side of each
# macro-swap wins, it doesn't remove anything.
-# EVERYTHING=1: everything everything everything everything
# GPROF=1: instrument with -pg for gprof(1)-based profiling (see
# PROFILE.md) -- root-free on both FreeBSD and Linux, unlike
# hardware-counter-based profiling (pmcstat/perf). Requires a
# `gmake clean` first/after, since -pg changes codegen for every
# translation unit.
+# EVERYTHING=1: everything everything everything everything
#
# $Id$
#
@@ -132,7 +131,8 @@ ifndef DISABLE_BITBOARD_MOVEGEN
-DGENERATE_ROOK_BITBOARD -DGENERATE_BISHOP_BITBOARD \
-DGENERATE_QUEEN_BITBOARD -DGENERATE_PAWN_BITBOARD \
-DGENERATE_ESCAPES_KING_BITBOARD -DGENERATE_ESCAPES_BLOCK_BITBOARD \
- -DEXPOSESCHECK_BITBOARD -DISATTACKED_BITBOARD
+ -DEXPOSESCHECK_BITBOARD -DISATTACKED_BITBOARD \
+ -DUNBLOCKED_BITBOARD
endif
ifdef EVERYTHING
@@ -324,7 +324,7 @@ FATHOM_CFLAGS = -std=gnu99 -O2 -Ifathom $(filter -m32 -m64,$(PROFILE))
#
# ---> .o, not .c! <---
-#
+#
OBJS = main.o root.o search.o searchsup.o draw.o dynamic.o \
hash.o eval.o x86.o pawnhash.o bitboard.o \
generate.o see.o move.o movesup.o command.o script.o \
@@ -334,7 +334,7 @@ OBJS = main.o root.o search.o searchsup.o draw.o dynamic.o \
ifdef TEST
# ---> .o, not .c! <---
-#
+#
OBJS += testdraw.o testmove.o testfen.o testgenerate.o \
testsan.o testics.o testeval.o testbitboard.o \
testhash.o testsee.o testsearch.o testsup.o \
diff --git a/src/chess.h b/src/chess.h
index d637905..fc2ef85 100755
--- a/src/chess.h
+++ b/src/chess.h
@@ -778,7 +778,6 @@ typedef struct _POSITION
BITBOARD bbTrappedEscapes[2][6];
ULONG uNumTrapped[2];
ULONG uArmyScaler[2];
- ULONG uClosedScaler;
SCORE iScore[2];
SCORE iReducedMaterialDownScaler[2];
ULONG uMinorsAtHome[2];
@@ -833,7 +832,7 @@ typedef struct _KEY_POINTER
}
KEY_POINTER;
-typedef union _GENERATOR_FLAGS
+typedef union _GENERATOR_FLAGS
{
struct {
UCHAR uMoveCount;
@@ -868,6 +867,31 @@ typedef struct _MOVE_STACK
//
ULONG uUnblockedKeyValue[MAX_PLY_PER_SEARCH];
KEY_POINTER sUnblocked[MAX_PLY_PER_SEARCH][128];
+ // _FindUnblockedSquaresBB's output: one bit per square unblocked
+ // for the opposing king this ply (membership only -- the
+ // direction-back-to-king half of the old sUnblocked table is
+ // recomputed on demand by DIRECTION_BETWEEN_SQUARES, a table
+ // lookup in release builds, so it needs no storage of its own).
+ // Kept unconditional (not #ifdef UNBLOCKED_BITBOARD-gated) so
+ // testgenerate.c's harness can exercise _FindUnblockedSquaresBB
+ // and compare it against the mailbox table regardless of which
+ // toggle this build was compiled with.
+ BITBOARD bbUnblocked[MAX_PLY_PER_SEARCH];
+ // Which generation (uUnblockedKeyValue[uPly] value) bbUnblocked[uPly]
+ // was last actually built for -- WouldGiveCheck (generate.c) compares
+ // this against the live uUnblockedKeyValue[uPly] to decide whether
+ // the cached bitboard is still valid for the node now occupying this
+ // ply, or needs a lazy rebuild. GenerateMoves no longer builds
+ // bbUnblocked eagerly; it just bumps uUnblockedKeyValue[uPly], same
+ // as root.c's init comment there describes -- the actual build is
+ // deferred to WouldGiveCheck's first call this generation, so a node
+ // whose every candidate move gets rejected before ever needing
+ // WouldGiveCheck (e.g. every capture futility-pruned in QSearch)
+ // never pays for it at all. Starts zeroed; every real ticket in
+ // uUnblockedKeyValue starts at 1 or higher (root.c) and only
+ // increases, so 0 is a permanently safe "never built yet" sentinel
+ // -- no special seeding needed here, unlike that field.
+ ULONG uUnblockedBuiltFor[MAX_PLY_PER_SEARCH];
//
// The main move list, a long series of moves, their values and some
@@ -922,6 +946,20 @@ MOVE_STACK;
#define GENERATE_CAPTURES_PROMS_CHECKS (3)
#define GENERATE_CAPTURES_PROMS (4)
#define GENERATE_DONT_SCORE (5)
+
+// generate.c's _GenerateCapturesAndPromotions (GENERATE_CAPTURES_PROMS's
+// direct, non-filtered generator) exists only when every piece type is
+// on its bitboard generator -- see that function's block comment in
+// generate.c. Declared here, not in generate.c, so every translation
+// unit that needs to know whether it exists (main.c's test-registration
+// list, testgenerate.c's own tests) sees the same answer generate.c's
+// GenerateMoves dispatch does.
+#if defined(GENERATE_KNIGHT_BITBOARD) && defined(GENERATE_BISHOP_BITBOARD) && \
+ defined(GENERATE_ROOK_BITBOARD) && defined(GENERATE_QUEEN_BITBOARD) && \
+ defined(GENERATE_KING_BITBOARD) && defined(GENERATE_PAWN_BITBOARD)
+#define CAPTURES_PROMS_BITBOARD_PATH 1
+#endif
+
#ifdef TEST
#define GENERATE_ALL_MOVES_CHECK_OK (6)
#endif
@@ -1138,6 +1176,18 @@ typedef struct _COUNTERS
}
kingsafetyhash;
+ // Separate from kingsafetyhash above -- _GetKingActionBonus's
+ // cache (eval.c) is keyed and populated completely independently
+ // (see KING_SAFETY_HASH_ENTRY's u64ActionKey comment), so its own
+ // hit rate needs its own counters rather than conflating with
+ // _GetFileStormDefects'.
+ struct
+ {
+ UINT64 u64Probes;
+ UINT64 u64Hits;
+ }
+ kingactionhash;
+
struct
{
UINT64 u64TotalNodeCount;
@@ -1746,6 +1796,21 @@ typedef struct _PAWN_HASH_ENTRY
UCHAR uCountPerFile[2][10];
UCHAR uNumRammedPawns;
UCHAR uNumUnmovedPawns[2];
+ // Total pawns on the board, both colors -- _EvalRook indexes
+ // ROOK_VALUE_AS_PAWNS_COME_OFF with this once per rook (up to 4x
+ // per Eval() call); also feeds uClosedScaler's own formula below,
+ // in both cases replacing a fresh FastCountBits(bbPawns[WHITE] |
+ // bbPawns[BLACK]) with a value this entry already has cached.
+ UCHAR uTotalPawnCount;
+ // "How closed/busy is the board" scaler _EvalKnight indexes
+ // KNIGHT_IN_CLOSED_POSITION with (eval.c's EvalPawns computes the
+ // formula) -- used to live as a POSITION field recomputed from
+ // scratch on every Eval() call even though every input (total
+ // pawn count, uNumUnmovedPawns, uNumRammedPawns) is already pure
+ // pawn-structure data this same entry is keyed on. Same "finish
+ // moving a pawn-only term into the pawn hash" pattern as
+ // iBishopColorScore below and 5fbd935's original move.
+ UCHAR uClosedScaler;
SHORT iBishopColorScore[2][2]; // [color][IS_SQUARE_WHITE(bishop's
// square)] -- good/bad-bishop
// pawn-structure score, pure
@@ -1771,6 +1836,17 @@ typedef struct _KING_SAFETY_HASH_ENTRY
// reports old counter units into KING_DEFECT_DUMP_INFO, unrelated
// to this cache.
SCORE iFileStormDefects[2];
+ // Independent validity tag for iActionBonus below, decoupled from
+ // u64Key above on purpose: _GetKingActionBonus (eval.c) is called
+ // from the opposite half of _EvalKing's safety/activity split from
+ // _GetFileStormDefects (only when the ENEMY lacks mating material,
+ // where file-storm's own populate-on-miss never runs), so a hit on
+ // u64Key would say nothing about whether iActionBonus was ever
+ // actually computed for this key. Per-king (keyed on pawn sig +
+ // just THIS king's own square, not both kings'), unlike
+ // iFileStormDefects -- see _GetKingActionBonus's header comment.
+ UINT64 u64ActionKey;
+ SCORE iActionBonus;
}
KING_SAFETY_HASH_ENTRY;
@@ -2799,7 +2875,7 @@ InCheck(POSITION *pos, ULONG uSide);
FLAG
IsAttackedBB(COOR cTest, POSITION *pos, ULONG uSide);
-FLAG
+FLAG FORCEINLINE
InCheckBB(POSITION *pos, ULONG uSide);
// ISATTACKED_BITBOARD defined -> the BB versions above resolve plain
@@ -2828,10 +2904,10 @@ StripMove(CHAR *szMove);
ULONG
LooksLikeMove(CHAR *szData);
-void FASTCALL
+void FORCEINLINE
SelectBestWithHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u);
-void FASTCALL
+void FORCEINLINE
SelectBestNoHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u);
void FASTCALL
@@ -2942,6 +3018,24 @@ _RookAttacksBB(COOR c, BITBOARD bbOccupied);
BITBOARD
_BishopAttacksBB(COOR c, BITBOARD bbOccupied);
+// generate.c's bitboard replacement for _FindUnblockedSquares, exposed
+// non-static so testgenerate.c's correctness/speed harness can call it
+// directly regardless of whether UNBLOCKED_BITBOARD is defined for
+// this build.
+void
+_FindUnblockedSquaresBB(MOVE_STACK *pStack,
+ POSITION *pos);
+
+// The mailbox original, exposed non-static (compiled whenever
+// !UNBLOCKED_BITBOARD, or under DEBUG/TEST for the cross-check and
+// the test harness above) under the same build conditions generate.c
+// itself uses -- see generate.c.
+#if !defined(UNBLOCKED_BITBOARD) || defined(DEBUG) || defined(TEST)
+void
+_FindUnblockedSquares(MOVE_STACK *pStack,
+ POSITION *pos);
+#endif
+
void
_GenerateRookBB(MOVE_STACK *pStack,
POSITION *pos,
@@ -3057,7 +3151,7 @@ GenerateWhiteKing(MOVE_STACK *pStack,
POSITION *pos,
COOR cKing);
-FLAG
+FLAG FORCEINLINE
WouldGiveCheck(IN SEARCHER_THREAD_CONTEXT *ctx,
IN MOVE mv);
@@ -3095,6 +3189,9 @@ TestGenerateQueenSpeed(void);
void
TestGenerateAllMovesSpeed(void);
+void
+TestFindUnblockedSquaresSpeed(void);
+
#endif
//
@@ -3148,6 +3245,7 @@ extern MOVE HASHMOVE;
extern MOVE RECOGNMOVE;
extern MOVE DRAWMOVE;
extern MOVE MATEMOVE;
+extern MOVE EGTBMOVE;
extern FLAG g_fIsWhiteSquare[128];
extern BITBOARD BBFILE[8];
extern BITBOARD BBRANK[9];
@@ -3707,7 +3805,7 @@ CanWeReduceOrPruneMove(IN SEARCHER_THREAD_CONTEXT *ctx,
IN FLAG fThreat,
IN FLAG fIsLeftoverMove);
-INT
+INT FORCEINLINE
GetLMRReduction(IN SEARCHER_THREAD_CONTEXT *ctx,
IN ULONG uRemainingDepth,
IN ULONG uLegalMoves,
@@ -3789,7 +3887,7 @@ GetMoveSee(IN SEARCHER_THREAD_CONTEXT *ctx,
IN MOVE mv,
IN ULONG uMoveNum);
-void
+void FORCEINLINE
ComputeMoveExtension(IN OUT SEARCHER_THREAD_CONTEXT *ctx,
IN SCORE iAlpha,
IN SCORE iBeta,
@@ -3813,7 +3911,7 @@ MateDistancePruningCutoff(IN ULONG uPly,
IN OUT SCORE *piAlpha,
IN OUT SCORE *piBeta);
-FLAG
+FLAG FORCEINLINE
CommonSearchInit(IN SEARCHER_THREAD_CONTEXT *ctx,
IN OUT SCORE *piAlpha,
IN OUT SCORE *piBeta,
@@ -3849,14 +3947,25 @@ TestQSearchSanity(void);
//
#define SEE_HEAPS
-SCORE
+SCORE FORCEINLINE
SEE(POSITION *pos,
MOVE mv);
+// The real exchange-walk SEE implementation, with no early exit --
+// see SEE()'s own header comment in see.c for the cheap no-defender
+// check that now sits in front of this. Exposed non-static (unlike
+// its historical merged-into-SEE status) so testsee.c's
+// TestSEENoDefenderFastPath can call it directly by name for an
+// exact-value correctness/speed comparison against SEE(), same
+// convention as _GenerateAllMoves/_GenerateAllMovesBB.
+SCORE
+_SEEFull(POSITION *pos,
+ MOVE mv);
+
// Sign-only fast path for SEE(pos, mv) >= 0 -- see its own header
// comment in see.c. Exposed non-static so GetMoveSee (searchsup.c) can
// call it and testsee.c can diff it against real SEE().
-FLAG
+FLAG FORCEINLINE
SeeFastSign(POSITION *pos,
MOVE mv);
@@ -4449,7 +4558,7 @@ _GetAttacksBB(SEE_LIST *pList,
// call it with a king-vacated occupancy bitboard. Deliberately
// excludes pawns -- see its own header comment in see.c; callers
// needing pawn attacks must check g_PawnAttackOriginBB separately.
-BITBOARD
+BITBOARD FORCEINLINE
_WhoAttacksSquareBB(POSITION *pos,
COOR cSquare,
ULONG uSide,
@@ -4807,7 +4916,7 @@ GenerateRandomLegalSymetricPosition(POSITION *pos);
void
InitializeInteriorNodeRecognizers(void);
-ULONG
+ULONG FORCEINLINE
RecognLookup(SEARCHER_THREAD_CONTEXT *ctx,
SCORE *piScore,
ULONG uEgtbProbeMode);
diff --git a/src/data.c b/src/data.c
index e1dd2c9..711e35c 100644
--- a/src/data.c
+++ b/src/data.c
@@ -40,6 +40,7 @@ MOVE HASHMOVE = {0x11118888};
MOVE RECOGNMOVE = {0x22228888};
MOVE DRAWMOVE = {0x33338888};
MOVE MATEMOVE = {0x44448888};
+MOVE EGTBMOVE = {0x55558888};
// Is a square white?
FLAG g_fIsWhiteSquare[128];
@@ -1056,7 +1057,7 @@ _MagicFindAndBuildForSquare(COOR c, BITBOARD mask, const int iDelta[4],
BITBOARD *pMagic, ULONG *pShift,
BITBOARD **ppTable)
{
- ULONG uBits = CountBits(mask);
+ ULONG uBits = FastCountBits(mask);
ULONG uSize = 1UL << uBits;
ULONG uShift = 64 - uBits;
BITBOARD *rgbbOccupancy = malloc(sizeof(BITBOARD) * uSize);
@@ -1089,7 +1090,7 @@ _MagicFindAndBuildForSquare(COOR c, BITBOARD mask, const int iDelta[4],
// when multiplied against the mask rarely yields a
// collision-free hash -- a cheap filter to skip obviously bad
// candidates before paying for the full uSize-entry pass.
- if (CountBits((UINT64)(mask * uMagic) & 0xFF00000000000000ULL) < 6)
+ if (FastCountBits((UINT64)(mask * uMagic) & 0xFF00000000000000ULL) < 6)
{
continue;
}
diff --git a/src/eval.c b/src/eval.c
index 18c3464..e102daa 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -280,13 +280,13 @@ static SCORE DOUBLED_PAWN_PENALTY_BY_COUNT[4][9] =
static SCORE ISOLATED_PAWN_BY_PAWNFILE[9] =
{
- 0, -7, -8, -9, -10, -10, -9, -8, -7
+ 0, -8, -9, -10, -10, -10, -10, -9, -8
};
static SCORE ISOLATED_EXPOSED_PAWN = -5;
// Turned down (2026-08-30): a per-pawn modifier that stacks on top of
-// DOUBLED_PAWN_PENALTY_BY_COUNT's whole-position doubled-pawn aggregate
+//< DOUBLED_PAWN_PENALTY_BY_COUNT's whole-position doubled-pawn aggregate
// for the specific case of a pawn that's both isolated and doubled --
// a different angle (single-worst-case flag vs. whole-position
// severity), not the same fact counted twice, so scaled down rather
@@ -385,7 +385,7 @@ SCORE RACER_WINS_RACE = +300;
static SCORE UNDEVELOPED_MINORS_IN_OPENING[5] =
{// 0 1 2 3 4
- 0, -6, -10, -16, -22
+ 0, -8, -14, -20, -23
};
//
@@ -722,16 +722,19 @@ static SCORE KING_TO_CENTER[128] =
// the other side's king safety. Note: this doesn't incl pawn
// material.
//
+// 2026-10-06: also now the single dividing line for _EvalKing's whole
+// safety-vs-activity split (see that function's header comment) --
+// below this, the king is actively encouraged out (KING_TO_CENTER)
+// instead of just not being penalized for leaving. The separate,
+// stricter KEEP_KING_AT_HOME_THRESHOLD used to gate the stay-home
+// bonus on its own, independently of this one, leaving a dead band
+// between the two thresholds where safety was scored but neither the
+// stay-home bonus nor any come-out encouragement applied. Retired --
+// the stay-home bonus now uses this same threshold, so "safety scored"
+// and "stay-home bonus applies" are exactly the same condition.
#define DO_KING_SAFETY_THRESHOLD \
(VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1)
-//
-// If a side has less than this much material the other side's king
-// can come out. Note: this doesn't incl pawn material.
-//
-#define KEEP_KING_AT_HOME_THRESHOLD \
- (VALUE_ROOK + VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1)
-
static ULONG KING_COUNTER_BY_ATTACK_PATTERN[32] =
{
//p 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
@@ -2269,6 +2272,17 @@ Return value:
FastCountBits(pos->bbPawns[WHITE] & (pos->bbPawns[BLACK] << 8));
ASSERT(pHash->uNumRammedPawns <= 8);
+ pHash->uTotalPawnCount = (UCHAR)FastCountBits(bbAllPawns);
+ ASSERT(pHash->uTotalPawnCount <= 16);
+
+ // _EvalKnight's closed-position scaler, computed once per unique
+ // pawn structure instead of once per Eval() call -- see this
+ // field's comment in chess.h.
+ pHash->uClosedScaler = (UCHAR)(pHash->uTotalPawnCount -
+ (pHash->uNumUnmovedPawns[WHITE] + pHash->uNumUnmovedPawns[BLACK]) +
+ pHash->uNumRammedPawns);
+ ASSERT(pHash->uClosedScaler <= 32);
+
// Good/bad bishop pawn-structure score, cached per (color, color-
// complex) instead of rescanned by every bishop of that color/
// complex in _EvalBishop (board_representation/EVAL.md-style note:
@@ -3002,6 +3016,120 @@ Return value:
}
+static SCORE
+_GetKingActivityBonus(IN SEARCHER_THREAD_CONTEXT *ctx,
+ IN POSITION *pos,
+ IN const PAWN_HASH_ENTRY *pHash,
+ IN ULONG uColor,
+ IN COOR cKing)
+/**
+
+Routine description:
+
+ Cache front end for _EvalKing's activity half (2026-10-06 safety/
+ activity split): combines "supporting own passer" and "is this
+ king out of the action" into one cached lookup, since both only
+ ever apply together (same caller, same branch) and both depend on
+ exactly the same inputs -- pawn structure plus uColor's own king
+ square, nothing else. One key computation and one hash probe
+ serve both instead of two. Only called when the ENEMY (xColor) is
+ down to at most one non-king piece -- see _EvalKing.
+
+ Per-king, not per-position: unlike _GetFileStormDefects (keyed on
+ both kings' squares, since "is the king's position safe" depends
+ on both), out-of-action's old formula subtracted the enemy king's
+ own distance to make a symmetric "who's closer" value, but that
+ only made sense paired with the old combined-both-sides gate
+ (uTotalNonPawnsBothSides==2, true or false identically for both
+ kings' calls). The new gate is per-call (keyed on xColor, each
+ king's own enemy), so the two calls can legitimately disagree --
+ one king's own position might qualify while the other's doesn't --
+ and a shared symmetric value can no longer be "added once, during
+ white's call" the way the old code assumed. Each call now computes
+ and caches its own independent value instead.
+
+ Narrower key (just this king's square, not both) than
+ _GetFileStormDefects's as a direct consequence -- only this king's
+ own moves can invalidate it, not the opponent's too, so this
+ should measure a *better* hit rate than file-storm's own ~75-85%
+ in this same low-material regime, not a worse one.
+
+ PROVISIONAL, like the formulas it combines: neither weight has
+ been through any self-play validation -- see the old out-of-action
+ formula's own removed comment (git history) for the outlier bug
+ that originally motivated shrinking its weight. Needs the same
+ calibration discipline as every other king-safety magnitude in
+ this file. Also trades away per-passer-square EVAL_TERM attribution
+ (summed into one combined value instead) -- EVAL_DUMP tracing
+ only, not used in release builds, by direct request for fewer
+ instructions in this inner loop.
+
+Parameters:
+
+ SEARCHER_THREAD_CONTEXT *ctx
+ POSITION *pos
+ PAWN_HASH_ENTRY *pHash
+ ULONG uColor : whose king this bonus is for
+ COOR cKing : uColor's own king square (caller already has it --
+ KingPosition(pos, uColor) redundantly)
+
+Return value:
+
+ SCORE
+
+**/
+{
+ UINT64 u64Key = pos->u64PawnSig ^ g_u64SigSeeds[cKing][KING][uColor];
+ ULONG uSlot = (ULONG)u64Key & (KING_SAFETY_HASH_TABLE_SIZE - 1);
+ KING_SAFETY_HASH_ENTRY *pEntry = &ctx->rgKingSafetyHash[uSlot];
+ SCORE i, iTotal;
+ COOR cSquare;
+ BITBOARD bb;
+
+ INC(ctx->sCounters.kingactionhash.u64Probes);
+ if (pEntry->u64ActionKey == u64Key)
+ {
+ INC(ctx->sCounters.kingactionhash.u64Hits);
+ return pEntry->iActionBonus;
+ }
+
+ // Supporting own passer -- bb here is exactly the own-passer
+ // squares at king-move distance 1 (g_KingAttacksBB is that mask
+ // by construction), so every popped square counts; no separate
+ // DISTANCE() filter or ASSERTs needed. The extra "leading the
+ // pawn toward promotion" bonus applies iff the passer square is
+ // in the king's own back ring (g_KingRingBackBB) -- side- and
+ // front-ring passers still get the base rank-scaled support
+ // bonus, just not this extra.
+ iTotal = 0;
+ bb = pHash->bbPasserLocations[uColor] & g_KingAttacksBB[cKing];
+ while (IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb)))
+ {
+ i = 2;
+ i += KING_SUPPORTING_OWN_PASSER_BY_RANK[uColor][RANK(cSquare)];
+ i += 8 * ((COOR_TO_BB(cSquare) & g_KingRingBackBB[uColor][cKing]) != 0);
+ iTotal += i;
+ }
+
+ // Out of action in KP-ish endgames, e.g.
+ // 8/8/1p6/p6k/P3K3/8/1P6/8 w - - 0 11 -- how close this king is
+ // to the whole pawn mass. The *2 preserves the old formula's own
+ // scale (it used to be a differential, this is now a plain sum,
+ // but the per-pawn magnitude is unchanged).
+ i = 0;
+ bb = pos->bbPawns[WHITE] | pos->bbPawns[BLACK];
+ while (IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb)))
+ {
+ i -= (SCORE)DISTANCE(cSquare, cKing);
+ }
+ iTotal += i * 2;
+
+ pEntry->u64ActionKey = u64Key;
+ pEntry->iActionBonus = iTotal;
+ return iTotal;
+}
+
+
#ifdef CALIBRATE_POSITIONAL
//
// board_representation/EVAL.md section 9 recalibration (2026-09-05):
@@ -4557,12 +4685,12 @@ Return value:
"board centrality");
// Give a bonus to knights on a closed / busy board.
- ASSERT(pos->uClosedScaler <= 32);
+ ASSERT(pHash->uClosedScaler <= 32);
EVAL_TERM(uColor,
KNIGHT,
c,
pos->iScore[uColor],
- KNIGHT_IN_CLOSED_POSITION[pos->uClosedScaler],
+ KNIGHT_IN_CLOSED_POSITION[pHash->uClosedScaler],
"in closed/open position");
// Outposted knights. See if square c is safe from enemy pawns;
@@ -4786,12 +4914,10 @@ Return value:
ROOK,
c,
pos->iScore[uColor],
- ROOK_VALUE_AS_PAWNS_COME_OFF[
- FastCountBits(pos->bbPawns[BLACK] | pos->bbPawns[WHITE])
- ],
+ ROOK_VALUE_AS_PAWNS_COME_OFF[pHash->uTotalPawnCount],
"increase in value/pawns");
- ASSERT((FastCountBits(pos->bbPawns[uColor]) + FastCountBits(pos->bbPawns[xColor])) ==
- (FastCountBits(pos->bbPawns[uColor] | pos->bbPawns[xColor])));
+ ASSERT(pHash->uTotalPawnCount ==
+ (FastCountBits(pos->bbPawns[uColor]) + FastCountBits(pos->bbPawns[xColor])));
//
// Rook mobility (and update attack tables) -- board_representation/
@@ -5104,47 +5230,52 @@ Return value:
}
-static void
-_EvalKing(IN OUT POSITION *pos,
- IN const COOR c,
- IN const PAWN_HASH_ENTRY *pHash,
- IN SEARCHER_THREAD_CONTEXT *ctx,
- IN const ULONG uTotalNonPawnsBothSides)
+static SCORE FORCEINLINE
+_EvalKingSafety(IN POSITION *pos,
+ IN const COOR c,
+ IN const PAWN_HASH_ENTRY *pHash,
+ IN SEARCHER_THREAD_CONTEXT *ctx,
+ IN const ULONG uColor,
+ IN const ULONG xColor,
+ IN const BITBOARD bbRing)
/**
Routine description:
+ Safety half of _EvalKing's 2026-10-06 safety-vs-activity split --
+ see that function's header comment. Full ring-attack/danger-
+ pattern scoring, file-storm defects (cached), stay-on-initial-
+ square bonus, castling/development. Only called when the enemy
+ has enough material (DO_KING_SAFETY_THRESHOLD) to make an attack
+ worth scoring. FORCEINLINE: this and _EvalKingActivity are split
+ out of _EvalKing purely for readability -- _EvalKing calls exactly
+ one of the two per invocation (never both), so forcing both
+ inline reproduces the exact same generated code as the old single
+ monolithic function, not a real call.
+
Parameters:
POSITION *pos,
COOR c,
PAWN_HASH_ENTRY *pHash,
- ULONG uTotalNonPawnsBothSides : TotalNonPawnCount(pos, WHITE) +
- TotalNonPawnCount(pos, BLACK), hoisted up to Eval() (chess.h's
- TotalNonPawnCount is a POPCNT now, not a stored field -- see
- its comment) since this function runs twice per Eval() call
- (once per king) and used to recompute the identical sum both
- times.
+ SEARCHER_THREAD_CONTEXT *ctx,
+ ULONG uColor,
+ ULONG xColor,
+ BITBOARD bbRing : g_KingAttacksBB[c], already computed by the caller
Return value:
- FLAG
+ SCORE
**/
{
- PIECE p;
- ULONG uColor, xColor;
- COOR cSquare;
- ULONG u;
BITV bvPattern;
ULONG uQueenNearKing;
- BITBOARD bb;
- SCORE i;
- SCORE iKingScore = 0;
-
#ifdef KING_DEFECT_DUMP
- KING_DEFECT_DUMP_INFO *pDump;
+ COOR cSquare;
+ KING_DEFECT_DUMP_INFO *pDump = &ctx->sKingDefectDump[uColor];
#endif
+ SCORE iKingScore = 0;
// 2026-09-15: linear-SCORE rewrite -- see king_safety_data/
// HANDOFF.md's 2026-09-15 addendum for the full rationale. Replaces
@@ -5185,30 +5316,6 @@ Return value:
// self-play regression, not the underlying feature signal itself.
const ULONG uKingDangerAcceleratorBucketWidth = 56;
- ASSERT(IS_ON_BOARD(c));
- p = pos->rgSquare[c].pPiece;
- ASSERT(p && IS_KING(p));
- uColor = GET_COLOR(p);
- xColor = FLIP(uColor);
- ASSERT(IS_VALID_COLOR(uColor));
-
- // Update attack bitvs for squares around the king.
- BITBOARD bbRing = g_KingAttacksBB[c];
- pos->bbKingAttacks[uColor] |= bbRing;
-#ifdef KING_DEFECT_DUMP
- pDump = &ctx->sKingDefectDump[uColor];
- memset(pDump, 0, sizeof(KING_DEFECT_DUMP_INFO));
-#endif
-
- // Don't bother with king safety when the other side has no mating
- // material.
- u = pos->uNonPawnMaterial[xColor];
- ASSERT(u >= VALUE_KING);
- if (u < DO_KING_SAFETY_THRESHOLD)
- {
- goto skip_safety;
- }
-
// Depending on whether we are assessing a black or white king,
// the "front" and "back" directions swap. Front is "towards
// the center of the board at game start time".
@@ -5377,11 +5484,6 @@ Return value:
#endif
//
- // King's own file plus the two adjacent ones. EXPERIMENTAL,
- // prototype-only (2026-09-05, board_representation/EVAL.md
- // section 9's king-safety-hash discussion): now routed through
- // _GetFileStormDefects's cache instead of computed inline.
- // Already a direct SCORE as of the 2026-09-15 rewrite -- see
// _ComputeFileStormDefects's comment.
//
iSafety += _GetFileStormDefects(ctx, pos, pHash, uColor);
@@ -5393,15 +5495,15 @@ Return value:
pDump->uFileStormStormingPawn;
#endif
- if (u >= KEEP_KING_AT_HOME_THRESHOLD)
- {
- iSafety += KING_INITIAL_LOCATION_SCALE *
- KING_EVAL_INITIAL_LOCATION[uColor][c];
- }
+ // Unconditional within this whole function now -- KEEP_KING_AT_HOME_
+ // THRESHOLD (a separate, stricter bar than DO_KING_SAFETY_THRESHOLD)
+ // used to gate this specifically, leaving a dead band between the
+ // two. Retired; see DO_KING_SAFETY_THRESHOLD's comment.
+ iSafety += KING_INITIAL_LOCATION_SCALE *
+ KING_EVAL_INITIAL_LOCATION[uColor][c];
#ifdef KING_DEFECT_DUMP
pDump->uInitialCounterByLocation =
- (u >= KEEP_KING_AT_HOME_THRESHOLD) ?
- (ULONG)KING_EVAL_INITIAL_LOCATION[uColor][c] : 0;
+ (ULONG)KING_EVAL_INITIAL_LOCATION[uColor][c];
#endif
// Danger-magnitude acceleration -- see KING_DANGER_ACCELERATOR's
@@ -5454,138 +5556,255 @@ Return value:
pDump->uAttackerArmyScaler = pos->uArmyScaler[xColor];
#endif
- // Bonus for castling / penalty for loss of castle. Also, if side has
- // not yet castled, be concerned with undeveloped minor pieces.
+ // Bonus for castling / penalty for loss of castle. Also, if
+ // side has not yet castled AND castling is still a live
+ // option, be concerned with undeveloped minor pieces.
+ //
+ // 2026-10-06: split the development penalty's gate from the
+ // missing-castle-option penalty's -- they need different
+ // conditions. Development should stop once the castling
+ // decision is permanently settled, whether that's by
+ // castling (the outer !fCastled check) or by losing both
+ // rights without ever castling (not caught by that check
+ // alone -- a side that never castles and never loses the
+ // right would otherwise be flagged "undeveloped" forever).
+ // The missing-castle-option penalty must do the opposite --
+ // persist *after* a right is lost, not stop then -- since
+ // its whole purpose is discouraging throwing the right away
+ // in the first place; gating it the same way development is
+ // gated would silently disable it exactly when it's supposed
+ // to fire.
+ //
+ // Color-indexed instead of a WHITE/BLACK branch over two
+ // copy-pasted blocks -- one fewer per-piece branch for the
+ // processor to mispredict (board_representation/EVAL.md
+ // section 9).
if (FALSE == pos->fCastled[uColor])
{
ULONG uDevelopmentScaler = CASTLE_AND_DEVELOPMENT_SCALER[pos->uArmyScaler[uColor]];
if (uDevelopmentScaler > 0)
{
+ static const BITV CASTLE_SHORT[2] =
+ { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT };
+ static const BITV CASTLE_LONG[2] =
+ { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG };
+ BITV bvBothRights = CASTLE_SHORT[uColor] | CASTLE_LONG[uColor];
+ // One popcount answers both "can this side still
+ // castle at all" (>0, gates development below) and
+ // "how many options are missing" (2-count, scales
+ // the persistent penalty below) -- replaces two
+ // separate (==0) branches/multiplies/EVAL_TERM calls
+ // with one shared count.
+ ULONG uRightsRemaining =
+ FastCountBits(pos->bvCastleInfo & bvBothRights);
+ ASSERT(uRightsRemaining <= 2);
+
+ // Branchless: (uRightsRemaining > 0) is 0 or 1, multiplied
+ // straight into the term instead of an `if` -- nothing
+ // left to mispredict.
ASSERT(pos->uMinorsAtHome[uColor] <= 4);
EVAL_TERM(uColor,
0,
ILLEGAL_COOR,
pos->iScore[uColor],
- (SCORE)((UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] *
+ (SCORE)(((uRightsRemaining > 0) *
+ UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] *
(SCORE)uDevelopmentScaler) / 8),
"development");
- // Color-indexed instead of a WHITE/BLACK branch over
- // two copy-pasted blocks -- one fewer per-piece branch
- // for the processor to mispredict (board_representation/
- // EVAL.md section 9).
- static const BITV CASTLE_SHORT[2] =
- { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT };
- static const BITV CASTLE_LONG[2] =
- { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG };
- EVAL_TERM(uColor,
- KING,
- c,
- iKingScore,
- (SCORE)((KING_MISSING_ONE_CASTLE_OPTION *
- ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0) *
- (SCORE)uDevelopmentScaler) / 8),
- "can't castle short");
+ // Deliberately NOT gated on uRightsRemaining > 0 --
+ // see this block's header comment. Fires at full
+ // weight (both options gone) even long after the
+ // rights were lost, for as long as uDevelopmentScaler
+ // itself stays nonzero.
EVAL_TERM(uColor,
KING,
c,
iKingScore,
(SCORE)((KING_MISSING_ONE_CASTLE_OPTION *
- ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0) *
+ (SCORE)(2 - uRightsRemaining) *
(SCORE)uDevelopmentScaler) / 8),
- "can't castle long");
+ "missing castle option(s)");
#ifdef KING_DEFECT_DUMP
pDump->fMissingCastleShort = ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0);
pDump->fMissingCastleLong = ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0);
#endif
}
}
+ return iKingScore;
+}
+
+
+static SCORE FORCEINLINE
+_EvalKingActivity(IN POSITION *pos,
+ IN const COOR c,
+ IN const PAWN_HASH_ENTRY *pHash,
+ IN SEARCHER_THREAD_CONTEXT *ctx,
+ IN const ULONG uColor,
+ IN const ULONG xColor)
+/**
+
+Routine description:
+
+ Activity half of _EvalKing's 2026-10-06 safety-vs-activity split --
+ see that function's header comment. KING_TO_CENTER unconditionally;
+ once the enemy is down to at most one non-king piece, the sharper
+ passer-support/in-the-action guidance too. Only called when the
+ enemy lacks mating material (below DO_KING_SAFETY_THRESHOLD).
+ FORCEINLINE -- see _EvalKingSafety's comment for why this costs
+ nothing over the old monolithic function.
+
+Parameters:
+
+ POSITION *pos,
+ COOR c,
+ PAWN_HASH_ENTRY *pHash,
+ SEARCHER_THREAD_CONTEXT *ctx,
+ ULONG uColor,
+ ULONG xColor
+
+Return value:
+
+ SCORE
+
+**/
+{
+ SCORE i;
+ SCORE iKingScore = 0;
- skip_safety:
//
- // Special code for kings in the endgame
+ // Come out. KING_TO_CENTER applies unconditionally (no longer
+ // gated by uTotalNonPawnsBothSides<8 -- that was a different,
+ // combined-both-sides unit than this whole split now uses, and
+ // left a gap between "safety stops mattering" and "centralizing
+ // is actually encouraged"; see _EvalKing's header comment).
//
- u = uTotalNonPawnsBothSides;
- if (u < 8)
+ i = KING_TO_CENTER[c];
+ EVAL_TERM(uColor,
+ KING,
+ c,
+ iKingScore,
+ i,
+ "centralize king");
+
+ // Sharper late-game guidance once the enemy is down to at most
+ // one non-king piece (lone king, K+minor, K+rook, even K+queen
+ // -- though a lone queen's own material value already exceeds
+ // DO_KING_SAFETY_THRESHOLD on its own, VALUE_QUEEN=975 >
+ // VALUE_ROOK+VALUE_BISHOP=800, so that specific case can never
+ // actually reach this function at all; the count check below is
+ // correct on its own terms regardless). Per-call/per-king
+ // (xColor's own material), not the old combined-both-sides
+ // uTotalNonPawnsBothSides==2 -- the two kings' own enemies can
+ // legitimately disagree now.
+ if (TotalNonPawnCount(pos, xColor) <= 2)
{
- // Encourage kings to come to the center when safe.
- i = KING_TO_CENTER[c];
+ // Supporting-own-passer and out-of-action-in-KP-endgames,
+ // combined into one cached lookup (_GetKingActivityBonus)
+ // -- both depend only on (pawn structure + this king's own
+ // square), both only ever apply in this same branch, so
+ // one key computation/hash probe serves both instead of
+ // two. Trades away per-passer-square EVAL_TERM attribution
+ // (EVAL_DUMP tracing only, not used in release builds) for
+ // fewer instructions in the inner loop, by direct request
+ // -- see git history for the old separated version if
+ // finer dump granularity is ever needed again.
+ i = _GetKingActivityBonus(ctx, pos, pHash, uColor, c);
EVAL_TERM(uColor,
KING,
c,
iKingScore,
i,
- "centralize king");
+ "king activity (passer support + in/out of action)");
+ }
+ return iKingScore;
+}
- // Kings in front of passers in the late endgame are strong...
- // -- bb here is exactly the own-passer squares at king-move
- // distance 1 (g_KingAttacksBB is that mask by construction), so
- // every popped square counts; no separate DISTANCE() filter or
- // ASSERTs needed. The extra "leading the pawn toward promotion"
- // bonus applies iff the passer square is in the king's own
- // back ring (g_KingRingBackBB) -- side- and front-ring passers
- // still get the base rank-scaled support bonus, just not this
- // extra.
- bb = pHash->bbPasserLocations[uColor] & g_KingAttacksBB[c];
- while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb)))
- {
- i = 2;
- i += KING_SUPPORTING_OWN_PASSER_BY_RANK[uColor][RANK(cSquare)];
- i += 8 * ((COOR_TO_BB(cSquare) & g_KingRingBackBB[uColor][c]) != 0);
- EVAL_TERM(uColor,
- KING,
- c,
- iKingScore,
- i,
- "supporting own passer");
- }
- //
- // Detect kings way out of the action in KP endgames:
- //
- // 8/8/1p6/p6k/P3K3/8/1P6/8 w - - 0 11
- //
- if ((u == 2) && (uColor == WHITE))
- {
- // 2026-09-16: this used to sum the raw per-pawn king-
- // distance differential (each term in [-7,+7]) over EVERY
- // pawn on the board, then multiply the whole sum by 12 --
- // unbounded in pawn count, so a genuine K+P endgame with
- // 10+ pawns still on the board could reach several hundred
- // centipawns from this one soft "my king is a bit more
- // active" term (confirmed directly: CALIBRATE_MARGIN_
- // SAFETY-adjacent sampling turned up real +391/-384
- // outliers traced to exactly this line). That's worth more
- // than a whole extra rook or bishop would be anywhere else
- // in this eval, for a term that only ever fires when
- // NEITHER side has a single piece left besides pawns (u==2
- // gate) -- clearly wrong on its face regardless of the
- // exact right number. Kept the raw per-pawn sum (rather
- // than normalizing to an average) deliberately -- more
- // pawns still on the board is a real reason this term
- // could matter more, not just noise to average away -- and
- // instead shrunk the per-pawn weight from 12 to 2, so even
- // an unrealistic worst case (16 pawns, every one at the
- // max +/-7 distance differential) caps at 224 and realistic
- // cases land around 40-80, versus the old 500-700+.
- // PROVISIONAL: the "2" stops the runaway magnitude but
- // isn't derived from any calibration. Needs the same self-
- // play validation discipline as every other king-safety
- // magnitude in this file before trusting the new ceiling.
- i = 0;
- bb = (pos->bbPawns[WHITE] | pos->bbPawns[BLACK]);
- while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb)))
- {
- i += (DISTANCE(cSquare, KingPosition(pos, BLACK)) -
- DISTANCE(cSquare, KingPosition(pos, WHITE)));
- }
- EVAL_TERM(WHITE,
- KING,
- c,
- iKingScore,
- i * 2,
- "in/out of \"action\"");
- }
+static void
+_EvalKing(IN OUT POSITION *pos,
+ IN const COOR c,
+ IN const PAWN_HASH_ENTRY *pHash,
+ IN SEARCHER_THREAD_CONTEXT *ctx)
+/**
+
+Routine description:
+
+ 2026-10-06: restructured into an explicit safety-vs-activity split
+ on pos->uNonPawnMaterial[xColor] (this king's own enemy's
+ material) against DO_KING_SAFETY_THRESHOLD -- above it, stay
+ hidden (_EvalKingSafety: full ring-attack/danger scoring, stay-
+ on-initial-square bonus); below it, come out (_EvalKingActivity:
+ KING_TO_CENTER unconditionally, plus, once the enemy is down to
+ at most one non-king piece, the sharper passer-support/in-the-
+ action guidance). Replaces the old `goto skip_safety` plus two
+ *separate*, independently-thresholded gates (KEEP_KING_AT_HOME_
+ THRESHOLD for the stay-home bonus, uTotalNonPawnsBothSides<8/==2
+ -- a different, combined-both-sides unit -- for the endgame
+ terms) that didn't line up with each other or with this one,
+ leaving a dead band where safety was scored but neither a stay-
+ home nor a come-out incentive applied. This single threshold now
+ drives both halves, so the two outcomes are exact mirror images
+ of one decision rather than three loosely related ones.
+
+ Split into two FORCEINLINE helpers purely for readability -- see
+ _EvalKingSafety's comment for why this costs nothing versus the
+ old single monolithic function.
+
+ No longer takes uTotalNonPawnsBothSides -- the old combined-both-
+ sides gate it fed is gone, replaced by a per-call check
+ (TotalNonPawnCount(pos, xColor), this king's own enemy only, not a
+ hoisted both-sides sum), so there's no shared value left worth
+ hoisting out to Eval().
+
+Parameters:
+
+ POSITION *pos,
+ COOR c,
+ PAWN_HASH_ENTRY *pHash,
+ SEARCHER_THREAD_CONTEXT *ctx
+
+Return value:
+
+ FLAG
+
+**/
+{
+ PIECE p;
+ ULONG uColor, xColor;
+ ULONG u;
+ SCORE iKingScore;
+
+ ASSERT(IS_ON_BOARD(c));
+ p = pos->rgSquare[c].pPiece;
+ ASSERT(p && IS_KING(p));
+ uColor = GET_COLOR(p);
+ xColor = FLIP(uColor);
+ ASSERT(IS_VALID_COLOR(uColor) && IS_VALID_COLOR(xColor));
+
+ // Update attack bitvs for squares around the king.
+ BITBOARD bbRing = g_KingAttacksBB[c];
+ pos->bbKingAttacks[uColor] |= bbRing;
+#ifdef KING_DEFECT_DUMP
+ {
+ KING_DEFECT_DUMP_INFO *pDump = &ctx->sKingDefectDump[uColor];
+ memset(pDump, 0, sizeof(KING_DEFECT_DUMP_INFO));
+ }
+#endif
+
+ // Safety-vs-activity split -- see this function's header comment
+ // (2026-10-06 restructuring). Don't bother with king safety when
+ // the other side has low material; instead, actively encourage
+ // this king out.
+ u = pos->uNonPawnMaterial[xColor];
+ ASSERT(u >= VALUE_KING);
+ if (u >= DO_KING_SAFETY_THRESHOLD)
+ {
+ iKingScore = _EvalKingSafety(pos, c, pHash, ctx, uColor, xColor, bbRing);
+ }
+ else
+ {
+ iKingScore = _EvalKingActivity(pos, c, pHash, ctx, uColor, xColor);
}
pos->iScore[uColor] += iKingScore;
ctx->sPlyInfo[ctx->uPly].iKingScore[uColor] = iKingScore;
@@ -6483,7 +6702,7 @@ Return value:
}
-FLAG
+FLAG FORCEINLINE
SideHasWinningChances(IN POSITION *pos,
IN ULONG uSide)
/**
@@ -7694,11 +7913,6 @@ Return value:
pos->uNumTrapped[BLACK] = pos->uNumTrapped[WHITE] = 0;
pos->iReducedMaterialDownScaler[BLACK] =
pos->iReducedMaterialDownScaler[WHITE] = 0;
- pos->uClosedScaler = (FastCountBits(pos->bbPawns[WHITE]) + FastCountBits(pos->bbPawns[BLACK])) -
- (pHash->uNumUnmovedPawns[WHITE] + pHash->uNumUnmovedPawns[BLACK]) +
- (pHash->uNumRammedPawns);
- ASSERT(pos->uClosedScaler >= 0);
- ASSERT(pos->uClosedScaler <= 32);
pos->uMinorsAtHome[BLACK] = pos->uMinorsAtHome[WHITE] = 0;
//
@@ -7854,13 +8068,7 @@ Return value:
#endif
}
- // Evaluate the two kings last. Compute both sides' non-pawn count
- // once here rather than in _EvalKing itself -- it's called twice
- // below (once per king) and used to recompute the identical
- // TotalNonPawnCount(WHITE)+TotalNonPawnCount(BLACK) sum both times.
- ULONG uTotalNonPawnsBothSides =
- TotalNonPawnCount(pos, WHITE) + TotalNonPawnCount(pos, BLACK);
-
+ // Evaluate the two kings last.
c = KingPosition(pos, uColor);
#ifdef DEBUG
ASSERT(IS_ON_BOARD(c));
@@ -7872,7 +8080,7 @@ Return value:
}
#endif
TIMED_EVAL_CALL(ctx, u64CyclesEvalKing,
- _EvalKing(pos, c, pHash, ctx, uTotalNonPawnsBothSides));
+ _EvalKing(pos, c, pHash, ctx));
#ifdef EVAL_DUMP
Trace("After our K at %s:\n%d\t\t%d\t\t(%d for %s)\n",
CoorToString(c),
@@ -7893,7 +8101,7 @@ Return value:
}
#endif
TIMED_EVAL_CALL(ctx, u64CyclesEvalKing,
- _EvalKing(pos, c, pHash, ctx, uTotalNonPawnsBothSides));
+ _EvalKing(pos, c, pHash, ctx));
#ifdef EVAL_DUMP
Trace("After enemy K at %s:\n%d\t\t%d\t\t(%d for %s)\n",
CoorToString(c),
@@ -7929,7 +8137,7 @@ Return value:
// slots first so a clean full eval reliably means a clean slate.
ctx->cEnprise[ctx->uPly][0] = ctx->cEnprise[ctx->uPly][1] = ILLEGAL_COOR;
ctx->cTrapped[ctx->uPly] = ILLEGAL_COOR;
- _EvalLookForDanger(ctx);
+ //_EvalLookForDanger(ctx);
_EvalTrappedPieces(ctx);
// TODO: endgame-specific knowledge? e.g. B over N in an endgame
diff --git a/src/generate.c b/src/generate.c
index a2fc9c4..89796b4 100644
--- a/src/generate.c
+++ b/src/generate.c
@@ -56,7 +56,16 @@ Revision History:
const INT g_iQKDeltas[] = {
-17, -16, -15, -1, +1, +17, +15, +16, 0 };
-static void
+// Only needed as a standalone call site when UNBLOCKED_BITBOARD is
+// off; under that toggle it's still referenced (DEBUG builds only) by
+// _FindUnblockedSquaresBB's cross-check, so it can't simply be
+// deleted yet -- see board_representation/MOVEGEN_MIGRATION.md.
+#if !defined(UNBLOCKED_BITBOARD) || defined(DEBUG) || defined(TEST)
+// Exposed non-static (like generate.c's other migration-era helpers,
+// e.g. _GenerateRookBB) whenever anything other than this file might
+// need it: the DEBUG cross-check inside _FindUnblockedSquaresBB below,
+// or testgenerate.c's correctness/speed harness under TEST.
+void
_FindUnblockedSquares(IN MOVE_STACK *pStack,
IN POSITION *pos)
/**
@@ -153,20 +162,138 @@ Return value:
ASSERT(uCount > 2);
ASSERT(uCount < 28);
}
+#endif // !defined(UNBLOCKED_BITBOARD) || defined(DEBUG)
+
+// board_representation/MOVEGEN_MIGRATION.md's never-started
+// _FindUnblockedSquares/WouldGiveCheck candidate. Membership (is this
+// square unblocked for the opposing king) is exactly the set of
+// squares the rook+bishop magic tables already reach from the king's
+// square, stopping at (and including) the first blocker in each of
+// the 8 directions -- precisely the queen-ray walk the mailbox
+// version does by hand, one lookup+OR instead of an 8-direction loop.
+// The direction-back-to-king half of the old table needs no storage
+// at all: generate.c's own ASSERT two lines below (mailbox version)
+// proves DIR_FROM_SQ_TO_KING(c) is always identical to
+// DIRECTION_BETWEEN_SQUARES(c, xKing), which is a g_pVectorDelta[]
+// table lookup in release builds (see chess.h), not a divide/branch
+// chain -- so recomputing it on demand costs the same as reading a
+// stored value would have, with no table to populate up front.
+void
+_FindUnblockedSquaresBB(IN MOVE_STACK *pStack,
+ IN POSITION *pos)
+/**
+
+Routine description:
+
+ Bitboard replacement for _FindUnblockedSquares -- see that
+ function's header comment for what "unblocked" means. Builds
+ pStack->bbUnblocked[uPly], one bit per square unblocked for the
+ king of the side not on move.
+
+Parameters:
+
+ MOVE_STACK *pStack : move stack pointer
+ POSITION *pos : position pointer
+
+Return value:
+
+ void
+
+**/
+{
+ COOR cKing = KingPosition(pos, FLIP(pos->uToMove));
+ ULONG uPly = pStack->uPly;
+
+ ASSERT(IS_ON_BOARD(cKing));
+ ASSERT(IS_KING(pos->rgSquare[cKing].pPiece));
+ ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) != pos->uToMove);
+ pStack->bbUnblocked[uPly] =
+ _RookAttacksBB(cKing, pos->bbOccupied) |
+ _BishopAttacksBB(cKing, pos->bbOccupied);
+
+#ifdef DEBUG
+ {
+ // Cross-check against the mailbox walk every single call --
+ // this is the correctness gate for bring-up, not a one-time
+ // test: membership must match exactly, and for every square
+ // the bitboard claims is unblocked, the direction back to the
+ // king recomputed via DIRECTION_BETWEEN_SQUARES must match
+ // what the mailbox ray walk actually stored.
+ COOR c;
+ BITBOARD bb = pStack->bbUnblocked[uPly];
+
+ _FindUnblockedSquares(pStack, pos);
+ FOREACH_SQUARE(c)
+ {
+ if (!IS_ON_BOARD(c)) continue;
+ FLAG fBB = (0 != (bb & COOR_TO_BB(c)));
+ FLAG fMailbox = (pStack->sUnblocked[uPly][c].uKey ==
+ pStack->uUnblockedKeyValue[uPly]);
+ ASSERT(fBB == fMailbox);
+ if (fBB)
+ {
+ ASSERT(DIRECTION_BETWEEN_SQUARES(c, cKing) ==
+ pStack->sUnblocked[uPly][c].iPointer);
+ }
+ }
+ // Restore: the mailbox call above just bumped
+ // uUnblockedKeyValue[uPly] a second time this ply, which is
+ // harmless (it's just a generation counter) but leave the
+ // real bbUnblocked value as computed above, not whatever the
+ // mailbox call's side effects might suggest.
+ pStack->bbUnblocked[uPly] = bb;
+ }
+#endif
+}
+
+#ifdef UNBLOCKED_BITBOARD
+// GenerateMoves no longer builds bbUnblocked[uPly] eagerly -- it just
+// bumps this ply's generation ticket (uUnblockedKeyValue[uPly], see
+// its comment in chess.h) so WouldGiveCheck's own lazy check (below)
+// knows a new node has arrived here and the cached bbUnblocked[uPly]
+// (built for some earlier generation, if ever) is no longer trusted.
+// The real build is deferred to WouldGiveCheck's first call this
+// generation, if there is one -- a node whose every candidate gets
+// rejected before ever needing WouldGiveCheck now never pays for it.
+#define MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly) \
+ ((void)(pos), (pStack)->uUnblockedKeyValue[(uPly)]++)
+#define SQUARE_IS_UNBLOCKED(c) \
+ (0 != (pStack->bbUnblocked[ctx->uPly] & COOR_TO_BB(c)))
+#define DIR_FROM_SQ_TO_KING(c) \
+ DIRECTION_BETWEEN_SQUARES((c), xKing)
+#else
+// Mailbox path has no lazy build to defer to -- its own per-square
+// sUnblocked[][].uKey scheme (SQUARE_IS_UNBLOCKED/DIR_FROM_SQ_TO_KING
+// below) is written directly by _FindUnblockedSquares itself, so this
+// stays eager, same as it always was.
+#define MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly) \
+ ((void)(uPly), _FindUnblockedSquares((pStack), (pos)))
#define SQUARE_IS_UNBLOCKED(c) (uUnblocked == kp[(c)].uKey)
#define DIR_FROM_SQ_TO_KING(c) (kp[(c)].iPointer)
+#endif
-FLAG
+FLAG FORCEINLINE
WouldGiveCheck(IN SEARCHER_THREAD_CONTEXT *ctx,
IN MOVE mv)
/**
Routine description:
- Determine whether a move is a checking move or not. For this
- function to work properly _FindUnblockedSquares must have been
- called at the start of generation.
+ Determine whether a move is a checking move or not.
+
+ Under UNBLOCKED_BITBOARD (this build's active path): self-
+ sufficient -- builds bbUnblocked[ctx->uPly] itself, lazily, on the
+ first call for a given generation (see the lazy-rebuild block
+ below and chess.h's uUnblockedBuiltFor comment). GenerateMoves no
+ longer builds it eagerly, only marks the generation stale
+ (MARK_UNBLOCKED_SQUARES_STALE); callers no longer need to remember
+ to call _FindUnblockedSquares[BB] first.
+
+ Under the mailbox fallback (no UNBLOCKED_BITBOARD): the older
+ precondition still holds -- _FindUnblockedSquares must have been
+ called at the start of generation (MARK_UNBLOCKED_SQUARES_STALE
+ still does this eagerly for that path, same as it always did).
Note: this is one of the most called codepaths in the engine,
speed is of the essence here.
@@ -193,8 +320,10 @@ Return value:
COOR cFrom = mv.cFrom;
PIECE pPiece = mv.pMoved;
COOR xKing = KingPosition(pos, FLIP(GET_COLOR(pPiece)));
+#ifndef UNBLOCKED_BITBOARD
KEY_POINTER *kp = &(pStack->sUnblocked[ctx->uPly][0]);
ULONG uUnblocked = pStack->uUnblockedKeyValue[ctx->uPly];
+#endif
COOR cEpSquare;
int iDelta;
@@ -206,6 +335,28 @@ Return value:
ASSERT(IS_ON_BOARD(cFrom));
ASSERT(pPiece);
+#ifdef UNBLOCKED_BITBOARD
+ // Lazy rebuild: GenerateMoves only bumps uUnblockedKeyValue[uPly]
+ // (MARK_UNBLOCKED_SQUARES_STALE above) instead of building
+ // bbUnblocked[uPly] eagerly. The first WouldGiveCheck call for a
+ // given generation discovers its mismatch here and does the real
+ // build; every later call this same node/generation matches and
+ // skips straight to SQUARE_IS_UNBLOCKED/DIR_FROM_SQ_TO_KING below.
+ // A node whose every candidate gets rejected before ever calling
+ // WouldGiveCheck (e.g. every capture futility-pruned in QSearch)
+ // now never pays for the build at all -- see chess.h's
+ // uUnblockedBuiltFor comment for why this needs its own field
+ // rather than reusing uUnblockedKeyValue for both jobs. Measured
+ // cost of the added branch itself (mock A/B, same shape as this
+ // one, three curated suites): within this box's own run-to-run
+ // noise band, no consistent direction -- see conversation.
+ if (pStack->uUnblockedBuiltFor[ctx->uPly] != pStack->uUnblockedKeyValue[ctx->uPly])
+ {
+ _FindUnblockedSquaresBB(pStack, pos);
+ pStack->uUnblockedBuiltFor[ctx->uPly] = pStack->uUnblockedKeyValue[ctx->uPly];
+ }
+#endif
+
//
// Phase 1: Does this move directly attack the king?
//
@@ -3302,6 +3453,489 @@ Return value:
}
+// --------------------------------------------------------------------
+// Captures/promotions-only generators (QSearch's GENERATE_CAPTURES_PROMS
+// path). QSearch's _ScoreQSearchMovesNoChecks assigns a score of 0 to
+// every quiet non-promoting move, and search.c's QSearch loop bails out
+// of its move loop on the first move it sees scored <= 0 -- so a quiet
+// move that _GenerateAllMoves would have produced for this uType was
+// always going to be generated, scored 0, and never searched. These
+// generators skip producing it in the first place: same mask-and-
+// serialize shape as the normal _Generate*BB functions, but masking the
+// attack bitboard with the ENEMY-occupied bitboard instead of the
+// complement of the friendly one, so quiet destinations never reach
+// _AddNormalMove at all. Bitboard-only (no mailbox fallback) -- every
+// piece type's GENERATE_*_BITBOARD flag is on in this project's only
+// build profile (GNUmakefile's PROFILE), and the mailbox generators this
+// would otherwise have to mirror are slated for removal separately.
+//
+// Castling is skipped entirely (a castle is never a capture or
+// promotion). Quiet pawn pushes are skipped except the single push onto
+// the back rank, which is a (quiet) promotion and still must be scored.
+//
+// These must stay pseudo-legal-equivalent to _GenerateAllMoves filtered
+// to IS_CAPTURE_OR_PROMOTION(mv), as a set -- see testgenerate.c's
+// TestCapturesAndPromotionsGenerator for the TEST=1 gate that checks
+// exactly this, over a corpus of positions, before any piece type here
+// is trusted.
+// --------------------------------------------------------------------
+
+// CAPTURES_PROMS_BITBOARD_PATH is declared in chess.h (not defined
+// here) so every translation unit that needs to know whether this path
+// exists sees the same answer -- see its comment there.
+#if defined(CAPTURES_PROMS_BITBOARD_PATH)
+
+void
+_GenerateKnightCapturesBB(IN MOVE_STACK *pStack,
+ IN POSITION *pos,
+ IN COOR cKnight)
+/**
+
+Routine description:
+
+ Captures-only sibling of _GenerateKnightBB: masks the knight's
+ attack bitboard with the enemy-occupied bitboard instead of the
+ complement of the friendly one, so only captures are serialized.
+ Knights never promote, so this is the whole move set this piece
+ type can contribute to GENERATE_CAPTURES_PROMS.
+
+Parameters:
+
+ MOVE_STACK *pStack : the move stack
+ POSITION *pos : the board position
+ COOR cKnight : the knight's location
+
+Return value:
+
+ void
+
+**/
+{
+ BITBOARD bbDest = g_KnightAttacksBB[cKnight] &
+ pos->bbOccupiedSide[FLIP(pos->uToMove)];
+ ULONG uBitIndex;
+ COOR c;
+ PIECE p;
+
+ ASSERT(IS_KNIGHT(pos->rgSquare[cKnight].pPiece));
+ ASSERT(GET_COLOR(pos->rgSquare[cKnight].pPiece) == pos->uToMove);
+
+ while (bbDest)
+ {
+ uBitIndex = FastFirstBit(bbDest) - 1;
+ bbDest &= (bbDest - 1);
+ c = BIT_NUMBER_TO_COOR(uBitIndex);
+ p = pos->rgSquare[c].pPiece;
+ ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cKnight].pPiece));
+ _AddNormalMove(pStack, pos, cKnight, c, p);
+ }
+}
+
+
+void
+_GenerateKingCapturesBB(IN MOVE_STACK *pStack,
+ IN POSITION *pos,
+ IN COOR cKing)
+/**
+
+Routine description:
+
+ Captures-only sibling of _GenerateKingBB. No castling here -- a
+ castle can never be a capture.
+
+Parameters:
+
+ MOVE_STACK *pStack : the move stack
+ POSITION *pos : the board position
+ COOR cKing : the king's location
+
+Return value:
+
+ void
+
+**/
+{
+ BITBOARD bbDest = g_KingAttacksBB[cKing] &
+ pos->bbOccupiedSide[FLIP(pos->uToMove)];
+ ULONG uBitIndex;
+ COOR c;
+ PIECE p;
+
+ ASSERT(IS_KING(pos->rgSquare[cKing].pPiece));
+ ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) == pos->uToMove);
+
+ while (bbDest)
+ {
+ uBitIndex = FastFirstBit(bbDest) - 1;
+ bbDest &= (bbDest - 1);
+ c = BIT_NUMBER_TO_COOR(uBitIndex);
+ p = pos->rgSquare[c].pPiece;
+ ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cKing].pPiece));
+ _AddNormalMove(pStack, pos, cKing, c, p);
+ }
+}
+
+
+void
+_GenerateBishopCapturesBB(IN MOVE_STACK *pStack,
+ IN POSITION *pos,
+ IN COOR cBishop)
+/**
+
+Routine description:
+
+ Captures-only sibling of _GenerateBishopBB.
+
+Parameters:
+
+ MOVE_STACK *pStack : the move stack (bbOccupied must already be
+ set, same precondition as _GenerateBishopBB)
+ POSITION *pos : the board position
+ COOR cBishop : the bishop's location
+
+Return value:
+
+ void
+
+**/
+{
+ ULONG uMagicIndex;
+ BITBOARD bbDest;
+ ULONG uBitIndex;
+ COOR c;
+ PIECE p;
+
+ ASSERT(IS_BISHOP(pos->rgSquare[cBishop].pPiece));
+ ASSERT(GET_COLOR(pos->rgSquare[cBishop].pPiece) == pos->uToMove);
+ ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos));
+
+ uMagicIndex = (ULONG)
+ (((pStack->bbOccupied & g_BishopOccupancyMask[cBishop]) *
+ g_BishopMagic[cBishop]) >> g_BishopMagicShift[cBishop]);
+ bbDest = g_BishopAttackTable[cBishop][uMagicIndex] &
+ pos->bbOccupiedSide[FLIP(pos->uToMove)];
+
+ while (bbDest)
+ {
+ uBitIndex = FastFirstBit(bbDest) - 1;
+ bbDest &= (bbDest - 1);
+ c = BIT_NUMBER_TO_COOR(uBitIndex);
+ p = pos->rgSquare[c].pPiece;
+ ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cBishop].pPiece));
+ _AddNormalMove(pStack, pos, cBishop, c, p);
+ }
+}
+
+
+void
+_GenerateRookCapturesBB(IN MOVE_STACK *pStack,
+ IN POSITION *pos,
+ IN COOR cRook)
+/**
+
+Routine description:
+
+ Captures-only sibling of _GenerateRookBB.
+
+Parameters:
+
+ MOVE_STACK *pStack : the move stack (bbOccupied must already be
+ set, same precondition as _GenerateRookBB)
+ POSITION *pos : the board position
+ COOR cRook : the rook's location
+
+Return value:
+
+ void
+
+**/
+{
+ ULONG uMagicIndex;
+ BITBOARD bbDest;
+ ULONG uBitIndex;
+ COOR c;
+ PIECE p;
+
+ ASSERT(IS_ROOK(pos->rgSquare[cRook].pPiece));
+ ASSERT(GET_COLOR(pos->rgSquare[cRook].pPiece) == pos->uToMove);
+ ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos));
+
+ uMagicIndex = (ULONG)
+ (((pStack->bbOccupied & g_RookOccupancyMask[cRook]) *
+ g_RookMagic[cRook]) >> g_RookMagicShift[cRook]);
+ bbDest = g_RookAttackTable[cRook][uMagicIndex] &
+ pos->bbOccupiedSide[FLIP(pos->uToMove)];
+
+ while (bbDest)
+ {
+ uBitIndex = FastFirstBit(bbDest) - 1;
+ bbDest &= (bbDest - 1);
+ c = BIT_NUMBER_TO_COOR(uBitIndex);
+ p = pos->rgSquare[c].pPiece;
+ ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cRook].pPiece));
+ _AddNormalMove(pStack, pos, cRook, c, p);
+ }
+}
+
+
+void
+_GenerateQueenCapturesBB(IN MOVE_STACK *pStack,
+ IN POSITION *pos,
+ IN COOR cQueen)
+/**
+
+Routine description:
+
+ Captures-only sibling of _GenerateQueenBB.
+
+Parameters:
+
+ MOVE_STACK *pStack : the move stack (bbOccupied must already be
+ set, same precondition as _GenerateQueenBB)
+ POSITION *pos : the board position
+ COOR cQueen : the queen's location
+
+Return value:
+
+ void
+
+**/
+{
+ ULONG uRookMagicIndex, uBishopMagicIndex;
+ BITBOARD bbDest;
+ ULONG uBitIndex;
+ COOR c;
+ PIECE p;
+
+ ASSERT(IS_QUEEN(pos->rgSquare[cQueen].pPiece));
+ ASSERT(GET_COLOR(pos->rgSquare[cQueen].pPiece) == pos->uToMove);
+ ASSERT(pStack->bbOccupied == _BuildFullOccupiedBB(pos));
+
+ uRookMagicIndex = (ULONG)
+ (((pStack->bbOccupied & g_RookOccupancyMask[cQueen]) *
+ g_RookMagic[cQueen]) >> g_RookMagicShift[cQueen]);
+ uBishopMagicIndex = (ULONG)
+ (((pStack->bbOccupied & g_BishopOccupancyMask[cQueen]) *
+ g_BishopMagic[cQueen]) >> g_BishopMagicShift[cQueen]);
+ bbDest = (g_RookAttackTable[cQueen][uRookMagicIndex] |
+ g_BishopAttackTable[cQueen][uBishopMagicIndex]) &
+ pos->bbOccupiedSide[FLIP(pos->uToMove)];
+
+ while (bbDest)
+ {
+ uBitIndex = FastFirstBit(bbDest) - 1;
+ bbDest &= (bbDest - 1);
+ c = BIT_NUMBER_TO_COOR(uBitIndex);
+ p = pos->rgSquare[c].pPiece;
+ ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cQueen].pPiece));
+ _AddNormalMove(pStack, pos, cQueen, c, p);
+ }
+}
+
+
+void
+_GenerateCapturesAndPromotionsPawnsBB(IN MOVE_STACK *pStack,
+ IN POSITION *pos,
+ IN ULONG uSide)
+/**
+
+Routine description:
+
+ Captures/promotions-only sibling of _GenerateAllPawnMovesBB: both
+ diagonal captures (promoting or not), en passant, and the single
+ quiet push onto the back rank (a promotion, so it still must be
+ scored) -- but never a double push or a non-promoting single push,
+ neither of which can be a capture or promotion.
+
+Parameters:
+
+ MOVE_STACK *pStack : the move stack
+ POSITION *pos : the board position
+ ULONG uSide : which side's pawns to generate for (pos->uToMove)
+
+Return value:
+
+ void
+
+**/
+{
+ BITBOARD bbPawns = pos->bbPawns[uSide];
+ BITBOARD bbEmpty = ~pos->bbOccupied;
+ BITBOARD bbEnemy = pos->bbOccupiedSide[FLIP(uSide)];
+ BITBOARD bbPushPromote, bbCapLeft, bbCapRight, bb;
+ ULONG uBitIndex;
+ COOR cTo, cFrom, cEp;
+ PIECE p;
+
+ if (uSide == WHITE)
+ {
+ bbPushPromote = ((bbPawns & BBRANK[7]) >> 8) & bbEmpty;
+ bbCapLeft = ((bbPawns & ~BBFILE[0]) >> 9) & bbEnemy;
+ bbCapRight = ((bbPawns & ~BBFILE[7]) >> 7) & bbEnemy;
+ }
+ else
+ {
+ ASSERT(uSide == BLACK);
+ bbPushPromote = ((bbPawns & BBRANK[2]) << 8) & bbEmpty;
+ bbCapLeft = ((bbPawns & ~BBFILE[0]) << 7) & bbEnemy;
+ bbCapRight = ((bbPawns & ~BBFILE[7]) << 9) & bbEnemy;
+ }
+
+ // Quiet promotions only -- the single push from the 7th/2nd rank.
+ bb = bbPushPromote;
+ while (bb)
+ {
+ uBitIndex = FastFirstBit(bb) - 1;
+ bb &= (bb - 1);
+ cTo = BIT_NUMBER_TO_COOR(uBitIndex);
+ cFrom = (uSide == WHITE) ?
+ BIT_NUMBER_TO_COOR(uBitIndex + 8) :
+ BIT_NUMBER_TO_COOR(uBitIndex - 8);
+ ASSERT(IS_PAWN(pos->rgSquare[cFrom].pPiece));
+ ASSERT(RANK8(cTo) || RANK1(cTo));
+ _AddPromote(pStack, pos, cFrom, cTo);
+ }
+
+ // Capture left (+ promotion if landing on the far rank).
+ bb = bbCapLeft;
+ while (bb)
+ {
+ uBitIndex = FastFirstBit(bb) - 1;
+ bb &= (bb - 1);
+ cTo = BIT_NUMBER_TO_COOR(uBitIndex);
+ cFrom = (uSide == WHITE) ?
+ BIT_NUMBER_TO_COOR(uBitIndex + 9) :
+ BIT_NUMBER_TO_COOR(uBitIndex - 7);
+ ASSERT(IS_PAWN(pos->rgSquare[cFrom].pPiece));
+ p = pos->rgSquare[cTo].pPiece;
+ ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cFrom].pPiece));
+ if (RANK8(cTo) || RANK1(cTo))
+ {
+ _AddPromote(pStack, pos, cFrom, cTo);
+ }
+ else
+ {
+ _AddNormalMove(pStack, pos, cFrom, cTo, p);
+ }
+ }
+
+ // Capture right (+ promotion if landing on the far rank).
+ bb = bbCapRight;
+ while (bb)
+ {
+ uBitIndex = FastFirstBit(bb) - 1;
+ bb &= (bb - 1);
+ cTo = BIT_NUMBER_TO_COOR(uBitIndex);
+ cFrom = (uSide == WHITE) ?
+ BIT_NUMBER_TO_COOR(uBitIndex + 7) :
+ BIT_NUMBER_TO_COOR(uBitIndex - 9);
+ ASSERT(IS_PAWN(pos->rgSquare[cFrom].pPiece));
+ p = pos->rgSquare[cTo].pPiece;
+ ASSERT(!IS_EMPTY(p) && OPPOSITE_COLORS(p, pos->rgSquare[cFrom].pPiece));
+ if (RANK8(cTo) || RANK1(cTo))
+ {
+ _AddPromote(pStack, pos, cFrom, cTo);
+ }
+ else
+ {
+ _AddNormalMove(pStack, pos, cFrom, cTo, p);
+ }
+ }
+
+ // En passant -- same per-side-per-node (not per-pawn) check as
+ // _GenerateAllPawnMovesBB.
+ cEp = pos->cEpSquare;
+ if (IS_ON_BOARD(cEp))
+ {
+ int iBehindDelta = (uSide == WHITE) ? 16 : -16;
+
+ cFrom = cEp + iBehindDelta - 1;
+ if (IS_ON_BOARD(cFrom) &&
+ IS_PAWN(pos->rgSquare[cFrom].pPiece) &&
+ (GET_COLOR(pos->rgSquare[cFrom].pPiece) == uSide))
+ {
+ _AddEnPassant(pStack, pos, cFrom, cEp);
+ }
+ cFrom = cEp + iBehindDelta + 1;
+ if (IS_ON_BOARD(cFrom) &&
+ IS_PAWN(pos->rgSquare[cFrom].pPiece) &&
+ (GET_COLOR(pos->rgSquare[cFrom].pPiece) == uSide))
+ {
+ _AddEnPassant(pStack, pos, cFrom, cEp);
+ }
+ }
+}
+
+
+// Non-static so testgenerate.c's correctness/speed harness can call it
+// directly by name, same convention as _GenerateAllMoves below.
+void
+_GenerateCapturesAndPromotions(IN MOVE_STACK *pStack,
+ IN POSITION *pos)
+/**
+
+Routine description:
+
+ Generates exactly the pseudo-legal moves that are captures or
+ promotions in this position -- the subset of _GenerateAllMoves'
+ output that IS_CAPTURE_OR_PROMOTION(mv) is true for, produced
+ directly rather than generated-then-filtered. Used by GenerateMoves
+ for GENERATE_CAPTURES_PROMS (QSearch's no-checks path) in place of
+ _GenerateAllMoves, since every quiet move _GenerateAllMoves would
+ have produced there is scored 0 by _ScoreQSearchMovesNoChecks and
+ never searched.
+
+ Relies on pStack->bbOccupied being set by the caller for the
+ sliding-piece magic lookups, same precondition as _GenerateAllMoves
+ itself.
+
+Parameters:
+
+ MOVE_STACK *pStack : the move stack
+ POSITION *pos : the board position
+
+Return value:
+
+ void
+
+**/
+{
+ COOR c;
+ static void (*JumpTable[])(MOVE_STACK *, POSITION *, COOR) =
+ {
+ InvalidGenerator, // EMPTY
+ InvalidGenerator, // EMPTY | WHITE
+ InvalidGenerator, // 2 (BLACK_PAWN)
+ InvalidGenerator, // 3 (WHITE_PAWN)
+ _GenerateKnightCapturesBB, // 4 (BLACK_KNIGHT)
+ _GenerateKnightCapturesBB, // 5 (WHITE_KNIGHT)
+ _GenerateBishopCapturesBB, // 6 (BLACK_BISHOP)
+ _GenerateBishopCapturesBB, // 7 (WHITE_BISHOP)
+ _GenerateRookCapturesBB, // 8 (BLACK_ROOK)
+ _GenerateRookCapturesBB, // 9 (WHITE_ROOK)
+ _GenerateQueenCapturesBB, // 10 (BLACK_QUEEN)
+ _GenerateQueenCapturesBB, // 11 (WHITE_QUEEN)
+ _GenerateKingCapturesBB, // 12 (BLACK_KING)
+ _GenerateKingCapturesBB // 13 (WHITE_KING)
+ };
+
+ pStack->bbOccupied = pos->bbOccupied;
+
+ {
+ BITBOARD bbNonPawns = NON_KING_PIECES_BB(pos, pos->uToMove) |
+ COOR_TO_BB(KingPosition(pos, pos->uToMove));
+ while (IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bbNonPawns)))
+ {
+ (JumpTable[pos->rgSquare[c].pPiece])(pStack, pos, c);
+ }
+ }
+
+ _GenerateCapturesAndPromotionsPawnsBB(pStack, pos, pos->uToMove);
+}
+
+#endif // captures/promotions bitboard path
+
+
// Non-static (unlike its historical internal-only status) so
// testgenerate.c's whole-node dispatch benchmark can call it directly
// by name -- see _GenerateAllMovesBB's block comment for why that
@@ -4574,9 +5208,7 @@ Routine description:
Like _ScoreSearchMoves, this function takes "hints" from the
searcher context about what squares are in danger and tries to
- order moves away from those squares sooner. Unlike
- _ScoreSearchMoves this function also attempts to capture enemies
- that are in danger sooner.
+ order moves away from those squares sooner.
Parameters:
@@ -4599,7 +5231,7 @@ Return value:
COOR cEnprise = FindEnprisePiece(ctx, uColor);
//
- // We have generate all moves or just legal escapes from check
+ // We have generated all moves or just legal escapes from check
// here. There can be a hash move -- if there is then the search
// has already tried it and we should not generate it again.
//
@@ -4611,9 +5243,31 @@ Return value:
ctx->sPlyInfo[uPly].eMoveScoringScheme = MOVE_SCORING_QSEARCH_CHECKS;
- for (u = pStack->uBegin[uPly];
- u < pStack->uEnd[uPly];
- u++)
+ // uLiveEnd shrinks as moves are rejected, instead of leaving them in
+ // place scored <= 0 for QSearch's move loop (search.c) to discover
+ // and bail on. Every move QSearch's loop will ever consider calls
+ // SelectBestNoHistory, which rescans the WHOLE remaining [u,
+ // uEnd[uPly]) range -- so a quiet, non-checking move sitting in
+ // that range gets rescanned by every single one of those calls
+ // until the bail, not generated/scored once and ignored. Instead,
+ // a rejected move (iValue <= 0) is swapped with the last live
+ // element and uLiveEnd drops by one -- same whole-struct-swap
+ // idiom GenerateMoves already uses to retire a found hash move
+ // (see its comment above) -- and pStack->uEnd[uPly] is written
+ // once at the end, after every move has been classified. The
+ // index being examined is NOT advanced on a reject, since the
+ // element just swapped into that slot hasn't been scored yet.
+ //
+ // Net effect: every move left in [uBegin[uPly], uEnd[uPly]) after
+ // this function returns has iValue > 0, so search.c's QSearch loop
+ // will never actually hit its own `iValue <= 0` bail for this
+ // scoring scheme -- that check stays (it's shared with
+ // _ScoreQSearchMovesNoChecks, which still needs it), it just
+ // becomes unreachable here, which is harmless.
+ {
+ ULONG uLiveEnd = pStack->uEnd[uPly];
+ u = pStack->uBegin[uPly];
+ while (u < uLiveEnd)
{
mv = pStack->mvf[u].mv;
ASSERT(GET_COLOR(mv.pMoved) == uColor);
@@ -4702,10 +5356,33 @@ Return value:
s += (mv.cFrom == cEnprise) * (PIECE_VALUE(mv.pMoved) / 4);
ASSERT(s > 0);
}
+ else
+ {
+ //
+ // Reject: this move will never be searched (QSearch's
+ // caller would bail on it the moment it became the best
+ // remaining candidate) -- drop it from the live range now
+ // instead of leaving it for SelectBestNoHistory to keep
+ // rescanning. Swap the last live element into this slot
+ // and shrink uLiveEnd; u is deliberately NOT advanced so
+ // the freshly-swapped-in element gets classified on the
+ // next iteration.
+ //
+ uLiveEnd--;
+ if (u != uLiveEnd)
+ {
+ pStack->mvf[u] = pStack->mvf[uLiveEnd];
+ }
+ continue;
+ }
+
pStack->mvf[u].iValue = s;
ASSERT(pStack->mvf[u].iValue != -MAX_INT);
+ u++;
} // next move
+ pStack->uEnd[uPly] = uLiveEnd;
+ }
}
@@ -4761,7 +5438,6 @@ Return value:
ASSERT(MOVE_COUNT(ctx, uPly) <= MAX_MOVES_PER_PLY);
ctx->sPlyInfo[uPly].eMoveScoringScheme = MOVE_SCORING_QSEARCH_NO_CHECKS;
-
#ifdef DEBUG
uColor = pos->uToMove;
#endif
@@ -4791,6 +5467,7 @@ Return value:
ASSERT((mv.pCaptured) || (mv.pPromoted));
s = PIECE_VALUE(mv.pCaptured) -
PIECE_VALUE(mv.pMoved);
+
//
// TODO: how does this handle positions like K *p *k ?
//
@@ -4912,27 +5589,41 @@ Return value:
case GENERATE_CAPTURES_PROMS_CHECKS:
ASSERT(!InCheck(pos, pos->uToMove));
ASSERT(mvHash.uMove == 0);
- _FindUnblockedSquares(pStack, pos);
+ MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly);
_GenerateAllMoves(pStack, pos);
_ScoreQSearchMovesInclChecks(pStack, ctx);
break;
case GENERATE_CAPTURES_PROMS:
ASSERT(!InCheck(pos, pos->uToMove));
ASSERT(mvHash.uMove == 0);
- _FindUnblockedSquares(pStack, pos);
+ // The unblocked-squares table itself is no longer built
+ // here -- see WouldGiveCheck's lazy rebuild (generate.c):
+ // this just marks the ply's generation ticket so the
+ // first WouldGiveCheck call this node (if any) knows to
+ // build it. search.c's QSearch loop calls WouldGiveCheck
+ // itself, deferred, on whichever single move it selects
+ // and decides to search (see fIncludeChecks == FALSE
+ // there) -- most nodes whose candidates are all futility-
+ // pruned away never reach that call at all, and now never
+ // pay for the table either.
+ MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly);
+#if defined(CAPTURES_PROMS_BITBOARD_PATH)
+ _GenerateCapturesAndPromotions(pStack, pos);
+#else
_GenerateAllMoves(pStack, pos);
+#endif
_ScoreQSearchMovesNoChecks(pStack, ctx);
break;
case GENERATE_ALL_MOVES:
ASSERT(!InCheck(pos, pos->uToMove));
- _FindUnblockedSquares(pStack, pos);
+ MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly);
_GenerateAllMoves(pStack, pos);
_ScoreAllMoves(pStack, ctx, mvHash);
uReturn = MOVE_COUNT(ctx, uPly);
break;
case GENERATE_ESCAPES:
ASSERT(InCheck(pos, pos->uToMove));
- _FindUnblockedSquares(pStack, pos);
+ MARK_UNBLOCKED_SQUARES_STALE(pStack, pos, uPly);
uReturn = _GenerateEscapes(pStack, pos);
ASSERT(uReturn);
pStack->sGenFlags[uPly].uKingMoveCount = uReturn >> 16;
diff --git a/src/main.c b/src/main.c
index 5135b7a..75c74c9 100644
--- a/src/main.c
+++ b/src/main.c
@@ -620,6 +620,7 @@ Return value:
TestGetAttacks();
TestIsAttackedBB();
TestSeeFastSign();
+ TestSEENoDefenderFastPath();
TestWhoAttacksSquareMagic();
TestMoveGenerator();
TestLegalMoveGenerator();
@@ -629,6 +630,11 @@ Return value:
TestGenerateBishopSpeed();
TestGenerateQueenSpeed();
TestGenerateAllMovesSpeed();
+#if defined(CAPTURES_PROMS_BITBOARD_PATH)
+ TestCapturesAndPromotionsGenerator();
+ TestGenerateCapturesAndPromotionsSpeed();
+#endif
+ TestFindUnblockedSquaresSpeed();
TestFenCode();
TestLiftPlaceSlidePiece();
TestExposesCheck();
diff --git a/src/movesup.c b/src/movesup.c
index e39e52a..d3d2a74 100644
--- a/src/movesup.c
+++ b/src/movesup.c
@@ -385,10 +385,10 @@ Return value:
ASSERT(IS_ON_BOARD(cLocation));
ASSERT(!IS_EMPTY(pos->rgSquare[cLocation].pPiece));
ASSERT(0 != (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN)));
-
+
iDelta = CHECK_DELTA_WITH_INDEX(iIndex);
ASSERT(iDelta != 0);
-
+
for (cBlockIndex = cRemove + iDelta;
IS_ON_BOARD(cBlockIndex);
cBlockIndex += iDelta)
@@ -400,12 +400,12 @@ Return value:
{
//
// Is it an enemy?
- //
+ //
if (OPPOSITE_COLORS(xPiece, uColor))
{
//
// Does it move the right way to attack?
- //
+ //
iIndex = (int)cBlockIndex - (int)cLocation;
if (0 != (CHECK_VECTOR_WITH_INDEX(iIndex, WHITE) &
(1 << (PIECE_TYPE(xPiece)))))
@@ -413,26 +413,26 @@ Return value:
return(cBlockIndex);
}
}
-
+
//
// Either we found another friendly piece or an enemy piece
// that was unable to reach us. Either way we are safe.
- //
+ //
return(ILLEGAL_COOR);
}
}
-
+
//
// We fell off the board without seeing another piece.
- //
+ //
return(ILLEGAL_COOR);
}
-COOR
-ExposesCheck(POSITION *pos,
- COOR cRemove,
+COOR
+ExposesCheck(POSITION *pos,
+ COOR cRemove,
COOR cLocation)
/**
@@ -452,7 +452,7 @@ Parameters:
Return value:
- COOR : the location of an attacker piece or 0x88 (!IS_ON_BOARD) if
+ COOR : the location of an attacker piece or 0x88 (!IS_ON_BOARD) if
the removal of cRemove does not expose check.
**/
@@ -461,13 +461,13 @@ Return value:
register COOR cBlockIndex;
int iIndex;
PIECE xPiece;
-
+
ASSERT(IS_ON_BOARD(cRemove));
ASSERT(IS_ON_BOARD(cLocation));
ASSERT(!IS_EMPTY(pos->rgSquare[cLocation].pPiece));
-
+
iIndex = (int)cLocation - (int)cRemove;
-
+
//
// If there is no way for a queen sitting at the square removed to
// reach the square we are testing (i.e. the two squares are not
@@ -479,11 +479,11 @@ Return value:
return(ILLEGAL_COOR);
}
iDelta = CHECK_DELTA_WITH_INDEX(iIndex);
- for (cBlockIndex = cLocation + iDelta;
+ for (cBlockIndex = cLocation + iDelta;
IS_ON_BOARD(cBlockIndex);
cBlockIndex += iDelta)
{
- if (cBlockIndex == cRemove)
+ if (cBlockIndex == cRemove)
{
continue;
}
@@ -492,12 +492,12 @@ Return value:
{
//
// Is it an enemy?
- //
+ //
if (OPPOSITE_COLORS(xPiece, pos->rgSquare[cLocation].pPiece))
{
//
// Does it move the right way to attack?
- //
+ //
iIndex = (int)cBlockIndex - (int)cLocation;
if (0 != (CHECK_VECTOR_WITH_INDEX(iIndex, GET_COLOR(xPiece)) &
(1 << (PIECE_TYPE(xPiece)))))
@@ -505,28 +505,28 @@ Return value:
return(cBlockIndex);
}
}
-
+
//
// Either we found another friendly piece or an enemy piece
// that was unable to reach us. Either way we are safe.
- //
+ //
return(ILLEGAL_COOR);
}
}
-
+
//
// We fell off the board without seeing another piece.
- //
+ //
return(ILLEGAL_COOR);
}
-COOR
-ExposesCheckEp(POSITION *pos,
- COOR cTest,
- COOR cIgnore,
- COOR cBlock,
- COOR cKing)
+COOR
+ExposesCheckEp(POSITION *pos,
+ COOR cTest,
+ COOR cIgnore,
+ COOR cBlock,
+ COOR cKing)
/**
Routine description:
@@ -562,13 +562,13 @@ Return value:
// reach the square we are testing (i.e. the two squares are not
// on the same rank, file, or diagonal) then no problemo.
//
- if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN)))
+ if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) & (1 << QUEEN)))
{
return(ILLEGAL_COOR);
}
-
+
iDelta = CHECK_DELTA_WITH_INDEX(iIndex);
- for (cBlockIndex = cKing + iDelta;
+ for (cBlockIndex = cKing + iDelta;
IS_ON_BOARD(cBlockIndex);
cBlockIndex += iDelta)
{
@@ -583,7 +583,7 @@ Return value:
{
//
// Does it move the right way to attack?
- //
+ //
iIndex = (int)cBlockIndex - (int)cKing;
if (0 != (CHECK_VECTOR_WITH_INDEX(iIndex, GET_COLOR(xPiece)) &
(1 << (PIECE_TYPE(xPiece)))))
@@ -595,14 +595,14 @@ Return value:
//
// Either we found another friendly piece or an enemy piece
// that was unable to reach us. Either way we are safe.
- //
+ //
return(ILLEGAL_COOR);
}
}
-
+
//
// We fell off the board without seeing another piece.
- //
+ //
return(ILLEGAL_COOR);
}
@@ -657,7 +657,7 @@ Return value:
return(0 != (g_PawnAttackOriginBB[uSide][cTest] & pos->bbPawns[uSide]));
}
-FLAG
+FLAG FORCEINLINE
InCheckBB(IN POSITION *pos, IN ULONG uSide)
/**
@@ -740,7 +740,7 @@ Return value:
{
continue;
}
-
+
//
// These pieces do not slide, we don't need to look for
// blockers. If they can get there then there is nothing
@@ -775,16 +775,16 @@ Return value:
//
// Check the locations a pawn could attack cTest from.
- //
+ //
cLocation = cTest + iPawnLoc[uSide];
- if (IS_ON_BOARD(cLocation) &&
+ if (IS_ON_BOARD(cLocation) &&
(pos->rgSquare[cLocation].pPiece == pPawn[uSide]))
{
return(TRUE);
}
-
+
cLocation += 2;
- if (IS_ON_BOARD(cLocation) &&
+ if (IS_ON_BOARD(cLocation) &&
(pos->rgSquare[cLocation].pPiece == pPawn[uSide]))
{
return(TRUE);
@@ -793,7 +793,7 @@ Return value:
}
-FLAG
+FLAG
InCheck(POSITION *pos, ULONG uSide)
/**
@@ -818,7 +818,7 @@ Return value:
#endif
ASSERT(IS_VALID_COLOR(uSide));
-
+
cKingLoc = KingPosition(pos, uSide);
#ifdef DEBUG
pKing = pos->rgSquare[cKingLoc].pPiece;
@@ -830,7 +830,7 @@ Return value:
}
-static FLAG
+static FLAG
_SanityCheckPawnMove(POSITION *pos, MOVE mv)
/**
@@ -856,15 +856,15 @@ Return value:
//
// General sanity checking...
- //
- if (!IS_ON_BOARD(cFrom) || !IS_ON_BOARD(cTo))
+ //
+ if (!IS_ON_BOARD(cFrom) || !IS_ON_BOARD(cTo))
{
return(FALSE);
}
-
+
//
// Sanity check promotions.
- //
+ //
if (mv.pPromoted)
{
if (GET_COLOR(p) == WHITE)
@@ -875,43 +875,43 @@ Return value:
{
if ((RANK(cTo) != 1) && (RANK(cFrom) != 2)) return(FALSE);
}
-
+
if (!IS_KNIGHT(mv.pPromoted) &&
!IS_BISHOP(mv.pPromoted) &&
!IS_ROOK(mv.pPromoted) &&
!IS_QUEEN(mv.pPromoted))
{
return(FALSE);
- }
+ }
}
-
+
//
// Handle capture moves.
- //
+ //
if (mv.pCaptured)
{
//
// Must be capturing something
- //
+ //
if (IS_SPECIAL_MOVE(mv) && (!mv.pPromoted))
{
//
// en passant
- //
+ //
if (!IS_EMPTY(pos->rgSquare[cTo].pPiece)) return(FALSE);
}
else
{
//
// normal capture
- //
+ //
if (IS_EMPTY(pos->rgSquare[cTo].pPiece)) return(FALSE);
if (!OPPOSITE_COLORS(mv.pMoved, mv.pCaptured)) return(FALSE);
}
//
// Must be in the capturing position.
- //
+ //
if (WHITE == GET_COLOR(p))
{
if (((cFrom - 17) != cTo) && ((cFrom - 15) != cTo)) return(FALSE);
@@ -920,27 +920,27 @@ Return value:
{
if (((cFrom + 17) != cTo) && ((cFrom + 15) != cTo)) return(FALSE);
}
-
- if ((IS_SPECIAL_MOVE(mv)) &&
+
+ if ((IS_SPECIAL_MOVE(mv)) &&
(!mv.pPromoted) &&
(pos->cEpSquare != cTo)) return(FALSE);
return(TRUE);
}
else
{
- //
+ //
// If the pawn is not capturing, the TO square better be empty.
- //
+ //
if (!IS_EMPTY(pos->rgSquare[cTo].pPiece)) return(FALSE);
-
- //
+
+ //
// A non-capturing pawn can only push forward. One or two squares
// based on whether or not its the initial move.
//
if (WHITE == GET_COLOR(p))
{
if (cTo == cFrom - 16) return(TRUE);
- if ((cTo == cFrom - 32) &&
+ if ((cTo == cFrom - 32) &&
(RANK2(cFrom)) &&
(IS_EMPTY(pos->rgSquare[cFrom - 16].pPiece))) return(TRUE);
}
@@ -982,42 +982,42 @@ Return value:
COOR cAttack;
COOR cKing;
int iDelta;
-
+
ASSERT(!IS_PAWN(p));
//
// Handle castling
- //
+ //
if (IS_CASTLE(mv))
{
ASSERT(IS_KING(mv.pMoved));
ASSERT(IS_SPECIAL_MOVE(mv));
return(TRUE);
}
-
+
//
// Make sure the piece moves in the way the move describes.
- //
+ //
iIndex = (int)cFrom - (int)cTo;
if (0 == (CHECK_VECTOR_WITH_INDEX(iIndex, BLACK) &
(1 << pPieceType)))
{
return(FALSE);
}
-
+
//
// If we get here the piece can move in the way described by the
// move but we still have to check to see if there are any other
// pieces in the way if the piece moved is not a king or knight
// (pawns handled in their own routine, see above).
- //
+ //
if ((pPieceType == QUEEN) ||
(pPieceType == ROOK) ||
(pPieceType == BISHOP))
{
iDelta = CHECK_DELTA_WITH_INDEX(iIndex);
for (cBlock = cFrom + iDelta;
- cBlock != cTo;
+ cBlock != cTo;
cBlock += iDelta)
{
if (!IS_EMPTY(pos->rgSquare[cBlock].pPiece))
@@ -1027,15 +1027,15 @@ Return value:
}
if (cBlock != cTo) return(FALSE);
}
-
+
//
// Ok the piece can move the way they asked and there are no other
// pieces in the way... but it cannot expose its own king to check
// by doing so!
//
cKing = KingPosition(pos, pos->uToMove);
- cAttack = ExposesCheck(pos,
- cFrom,
+ cAttack = ExposesCheck(pos,
+ cFrom,
cKing);
if ((ILLEGAL_COOR != cAttack) && (cTo != cAttack))
{
@@ -1044,7 +1044,7 @@ Return value:
// does expose the king to check and the move does not capture the
// checking piece. But it's still ok as long as the TO location
// still blocks the checking piece.
- //
+ //
iIndex = cAttack - cKing;
iDelta = CHECK_DELTA_WITH_INDEX(iIndex);
for (cBlock = cAttack + iDelta;
@@ -1059,12 +1059,12 @@ Return value:
//
// Legal!
- //
+ //
return(TRUE);
}
-FLAG
+FLAG
SanityCheckMove(POSITION *pos, MOVE mv)
/**
@@ -1084,7 +1084,7 @@ Return value:
**/
{
register PIECE p = mv.pMoved;
-
+
if (0 == mv.uMove)
{
return(FALSE);
@@ -1106,7 +1106,7 @@ Return value:
{
return(FALSE);
}
-
+
if (!IS_SPECIAL_MOVE(mv))
{
if (mv.pCaptured)
@@ -1122,7 +1122,7 @@ Return value:
return(FALSE);
}
}
-
+
if (!IS_EMPTY(pos->rgSquare[mv.cTo].pPiece) &&
!mv.pCaptured)
{
@@ -1136,7 +1136,7 @@ Return value:
return(FALSE);
}
}
-
+
if (IS_PAWN(p))
{
return(_SanityCheckPawnMove(pos, mv));
@@ -1148,7 +1148,7 @@ Return value:
}
-FLAG
+FLAG
LooksLikeFile(CHAR c)
/**
@@ -1198,9 +1198,9 @@ Return value:
}
return(FALSE);
}
-
-FLAG
+
+FLAG
LooksLikeCoor(char *szData)
/**
@@ -1220,7 +1220,7 @@ Return value:
{
char f = szData[0];
char r = szData[1];
-
+
if (LooksLikeFile(f) &&
LooksLikeRank(r))
{
@@ -1252,7 +1252,7 @@ Return value:
static CHAR szStripped[10];
ULONG y;
CHAR *p;
-
+
memset(szStripped, 0, sizeof(szStripped));
p = szMove;
y = 0;
@@ -1289,11 +1289,11 @@ Return value:
CHAR *szStripped = StripMove(szData);
ULONG u;
static CHAR cPieces[] = { 'P', 'N', 'B', 'R', 'Q', 'K', '\0' };
-
+
//
// A (stripped) move must be at least two characters long even in
// SAN.
- //
+ //
if ((strlen(szStripped) < 2) || (strlen(szStripped) > 7))
{
return(NOT_MOVE);
@@ -1309,7 +1309,7 @@ Return value:
{
return(MOVE_SAN);
}
-
+
//
// COOR COOR (d2d4)
//
@@ -1318,37 +1318,37 @@ Return value:
{
return(MOVE_ICS);
}
-
+
//
// COOR (d4)
- //
+ //
if (LooksLikeCoor(szStripped) &&
strlen(szStripped) == 2)
{
return(MOVE_SAN);
}
-
+
//
// RANK COOR
- //
+ //
if ((LooksLikeRank(szStripped[0]) ||
LooksLikeFile(szStripped[0])) &&
LooksLikeCoor(&szStripped[1]))
{
return(MOVE_SAN);
}
-
+
//
// PIECE COOR COOR
// PIECE COOR
// PIECE FILE COOR
// PIECE RANK COOR
- //
+ //
for (u = 0; u < ARRAY_LENGTH(cPieces); u++)
{
if (szStripped[0] == cPieces[u]) // must be uppercase!
{
- if (LooksLikeCoor(&szStripped[1]))
+ if (LooksLikeCoor(&szStripped[1]))
{
return(MOVE_SAN);
}
@@ -1367,7 +1367,7 @@ Return value:
//
// (COORD)=(PIECE)
- //
+ //
if ((strlen(szStripped) == 4) &&
LooksLikeCoor(szStripped) &&
szStripped[2] == '=')
@@ -1392,7 +1392,7 @@ Return value:
}
-void FASTCALL
+void FORCEINLINE
SelectBestWithHistory(SEARCHER_THREAD_CONTEXT *ctx,
ULONG u)
/**
@@ -1424,7 +1424,7 @@ Return value:
MOVE mvLast;
ULONG uPrevContKey = 0;
FLAG fHaveContinuation = FALSE;
- COOR cEnprise;
+ // COOR cEnprise;
ASSERT(ctx->sMoveStack.uBegin[ctx->uPly] <= uEnd);
ASSERT(u >= ctx->sMoveStack.uBegin[ctx->uPly]);
@@ -1442,7 +1442,7 @@ Return value:
// answer than an earlier one. Recomputed every call (2026-09-16:
// briefly hoisted to the caller, reverted after a DEBUG-build
// ASSERT here caught the mismatch -- see git log).
- cEnprise = FindEnprisePiece(ctx, ctx->sPosition.uToMove);
+ // cEnprise = FindEnprisePiece(ctx, ctx->sPosition.uToMove);
// Continuation history: "did a move like this tend to fail high
// right after a move like that?", keyed by (previous move, this
@@ -1468,16 +1468,14 @@ Return value:
if (!IS_CAPTURE_OR_PROMOTION(mv))
{
iBestVal += g_HistoryCounters[mv.pMoved][mv.cTo];
- if (TRUE == fHaveContinuation)
- {
- iBestVal += CONTINUATION_SCALE *
- g_ContinuationHistory[(uPrevContKey * CONT_KEY_RANGE) +
- MOVE_TO_CONT_KEY(mv)];
- }
- if (mv.cFrom == cEnprise)
- {
- iBestVal += FLEE_BONUS;
- }
+ iBestVal += (
+ fHaveContinuation *
+ CONTINUATION_SCALE *
+ g_ContinuationHistory[(uPrevContKey * CONT_KEY_RANGE) + MOVE_TO_CONT_KEY(mv)]);
+ //if (mv.cFrom == cEnprise)
+ //{
+ // iBestVal += FLEE_BONUS;
+ //}
}
uLoc = u;
@@ -1488,15 +1486,14 @@ Return value:
if (!IS_CAPTURE_OR_PROMOTION(mv))
{
iVal += g_HistoryCounters[mv.pMoved][mv.cTo];
- if (TRUE == fHaveContinuation)
- {
- iVal += g_ContinuationHistory[(uPrevContKey * CONT_KEY_RANGE) +
- MOVE_TO_CONT_KEY(mv)];
- }
- if (mv.cFrom == cEnprise)
- {
- iVal += FLEE_BONUS;
- }
+ iVal += (
+ fHaveContinuation *
+ CONTINUATION_SCALE *
+ g_ContinuationHistory[(uPrevContKey * CONT_KEY_RANGE) + MOVE_TO_CONT_KEY(mv)]);
+// if (mv.cFrom == cEnprise)
+// {
+// iVal += FLEE_BONUS;
+// }
}
if (iVal > iBestVal)
{
@@ -1514,7 +1511,7 @@ Return value:
}
-void FASTCALL
+void FORCEINLINE
SelectBestNoHistory(SEARCHER_THREAD_CONTEXT *ctx,
ULONG u)
/**
@@ -1542,11 +1539,11 @@ Return value:
ULONG uLoc;
SCORE iVal;
MOVE_STACK_MOVE_VALUE_FLAGS mvfTemp;
-
+
ASSERT(ctx->sMoveStack.uBegin[ctx->uPly] <= uEnd);
ASSERT(u >= ctx->sMoveStack.uBegin[ctx->uPly]);
ASSERT(u < uEnd);
-
+
//
// Linear search from u..ctx->sMoveStack.uEnd[ctx->uPly] for the
// move with the best value.
@@ -1571,7 +1568,7 @@ Return value:
ctx->sMoveStack.mvf[uLoc] = mvfTemp;
}
-void FASTCALL
+void FASTCALL
SelectMoveAtRoot(SEARCHER_THREAD_CONTEXT *ctx,
ULONG u)
/**
@@ -1595,12 +1592,12 @@ Return value:
ULONG uLoc = v;
SCORE iVal;
MOVE_STACK_MOVE_VALUE_FLAGS mvfTemp;
-
+
ASSERT(ctx->sMoveStack.uBegin[ctx->uPly] <= uEnd);
ASSERT(u >= ctx->sMoveStack.uBegin[ctx->uPly]);
ASSERT(u < uEnd);
ASSERT(MOVE_COUNT(ctx, ctx->uPly) >= 1);
-
+
//
// Find the first move that we have not already searched. It will
// provide our initial iBestVal.
@@ -1648,7 +1645,7 @@ static UINT64 g_uPerftNodeCount;
static UINT64 g_uPerftTotalNodes;
static UINT64 g_uPerftGenerates;
-void
+void
Perft(SEARCHER_THREAD_CONTEXT *ctx,
ULONG uDepth)
{
@@ -1658,16 +1655,16 @@ Perft(SEARCHER_THREAD_CONTEXT *ctx,
g_uPerftTotalNodes += 1;
ASSERT(uDepth < MAX_PLY_PER_SEARCH);
- if (uDepth == 0)
+ if (uDepth == 0)
{
g_uPerftNodeCount += 1;
return;
}
-
+
mv.uMove = 0;
g_uPerftGenerates += 1;
GenerateMoves(ctx, mv, GENERATE_DONT_SCORE);
-
+
uPly = ctx->uPly;
for (u = ctx->sMoveStack.uBegin[uPly];
u < ctx->sMoveStack.uEnd[uPly];
@@ -1688,13 +1685,13 @@ COMMAND(PerftCommand)
Routine description:
This function implements the 'perft' engine command. I have no
- idea what 'perft' means but some people use this command to
+ idea what 'perft' means but some people use this command to
benchmark the speed of the move generator code. Note: the way
this is implemented here does nothing whatsoever with the move
scoring code (i.e. the SEE)
Usage:
-
+
perft <depth>
Parameters:
@@ -1718,30 +1715,30 @@ Return value:
double dBegin, dTime;
double dNps;
- if (argc < 2)
+ if (argc < 2)
{
Trace("Usage: perft <required_depth>\n");
return;
}
uDepth = atoi(argv[1]);
- if (uDepth >= MAX_DEPTH_PER_SEARCH)
+ if (uDepth >= MAX_DEPTH_PER_SEARCH)
{
Trace("Error: invalid depth.\n");
return;
}
InitializeLightweightSearcherContext(pos, &ctx);
-
+
g_uPerftTotalNodes = g_uPerftGenerates = 0ULL;
dBegin = SystemTimeStamp();
- for (u = 1; u <= uDepth; u++)
+ for (u = 1; u <= uDepth; u++)
{
g_uPerftNodeCount = 0ULL;
Perft((SEARCHER_THREAD_CONTEXT *)&ctx, u);
Trace("%u. %" COMPILER_LONGLONG_UNSIGNED_FORMAT " node%s, "
"%" COMPILER_LONGLONG_UNSIGNED_FORMAT " generate%s.\n",
- u,
- g_uPerftNodeCount,
+ u,
+ g_uPerftNodeCount,
(g_uPerftNodeCount > 1) ? "s" : "",
g_uPerftGenerates,
(g_uPerftGenerates > 1) ? "s" : "");
@@ -1751,8 +1748,8 @@ Return value:
dNps /= dTime;
Trace("%" COMPILER_LONGLONG_UNSIGNED_FORMAT " total nodes, "
"%" COMPILER_LONGLONG_UNSIGNED_FORMAT " total generates "
- "in %6.2f seconds.\n",
- g_uPerftTotalNodes,
+ "in %6.2f seconds.\n",
+ g_uPerftTotalNodes,
g_uPerftGenerates,
dTime);
Trace("That's approx %.0f moves/sec\n", dNps);
diff --git a/src/probe.c b/src/probe.c
index 269a1b9..d08ef75 100644
--- a/src/probe.c
+++ b/src/probe.c
@@ -515,28 +515,21 @@ Return value:
(mv.cTo == cTo) &&
(mv.pPromoted == pPromotes))
{
- // Root-only: use the DTZ ply count tb_probe_root already
- // packed into uResult (no extra probe) to report a real,
- // ply-distance-aware mate score instead of the flat
- // EGTB_WIN_SCORE/LOSS_SCORE used by in-search probes
- // (ProbeEGTB/RecognLookup) -- those stay WDL-only and
- // untouched. util.c's SCORE-to-string formatter already
- // treats any |score| >= NMATE as a mate score and derives
- // the move count as (INFINITY_SCORE - iScore) / 2, so
- // INFINITY_SCORE -/+ uDtz round-trips through that
- // unmodified as "+MATE<n>"/"-MATE<n>". Clamp defensively:
- // a uDtz large enough to push the score below NMATE would
- // just silently fall back to plain-decimal display, not
- // corrupt anything, but keep it a real mate score at every
- // legal DTZ value we could plausibly see (TB_MAX_PLY=256
- // exceeds NMATE's 200-point margin in principle, however
- // unlikely in practice for <=7-man tables).
- ULONG uDtz = TB_GET_DTZ(uResult);
- if (uDtz >= (+INFINITY_SCORE - NMATE)) uDtz = (+INFINITY_SCORE - NMATE - 1);
+ // DTZ (distance to the next fifty-move-rule-zeroing move)
+ // is not the same thing as distance to mate -- a previous
+ // version of this code reported (INFINITY_SCORE -/+ uDtz)
+ // as a mate score, which is wrong in general: DTZ can be
+ // small in a dead-won position that's still many plies
+ // from actual mate (e.g. right as pos->uFifty climbs
+ // towards 100, a reset becomes forced soon even though
+ // mate itself is nowhere near), producing a bogus
+ // "+MATE<n>"/"-MATE<n>" display untethered from reality.
+ // The draw/cursed-win/blessed-loss cases already returned
+ // FALSE above, so uWdl here is only ever TB_WIN or
+ // TB_LOSS -- report the same flat mate-magnitude score
+ // ProbeEGTB/RecognLookup use at interior nodes instead.
*pmv = mv;
- *piScore = (TB_WIN == uWdl) ?
- (SCORE)(+INFINITY_SCORE - uDtz) :
- (SCORE)(-INFINITY_SCORE + uDtz);
+ *piScore = (TB_WIN == uWdl) ? +NMATE : -NMATE;
return(TRUE);
}
}
diff --git a/src/recogn.c b/src/recogn.c
index 427297b..3263908 100644
--- a/src/recogn.c
+++ b/src/recogn.c
@@ -2124,7 +2124,7 @@ Return value:
}
-ULONG
+ULONG FORCEINLINE
RecognLookup(IN SEARCHER_THREAD_CONTEXT *ctx,
IN OUT SCORE *piScore,
IN ULONG uEgtbProbeMode)
@@ -2245,7 +2245,7 @@ Return value:
// mate distance to adjust for the present ply here (see
// EGTB_WIN_SCORE / EGTB_LOSS_SCORE in probe.c).
*piScore = iScore;
- return(RECOGN_EXACT);
+ return(RECOGN_EGTB);
}
}
}
diff --git a/src/root.c b/src/root.c
index f9949a9..8fcace7 100644
--- a/src/root.c
+++ b/src/root.c
@@ -1453,7 +1453,7 @@ Return value:
ctx->uRootDepth = 0;
ctx->sPlyInfo[0].PV[0] = mv;
ctx->sPlyInfo[0].PV[1] = NULLMOVE;
- strcpy(ctx->szLastPV, "tb");
+ strcpy(ctx->szLastPV, "<EGTB>");
if (!g_Options.fPondering) {
Trace("TABLEBASE MOVE --> stop searching now\n");
g_MoveTimer.bvFlags |= TIMER_STOPPING;
@@ -1710,8 +1710,6 @@ Return value:
ctx->sCounters.tree.u64NCRealLoopNodeSum[u];
}
#endif
- g_MoveTimer.dEndTime = SystemTimeStamp();
-
//
// Here we are at the end of a search. If we were:
//
@@ -1737,6 +1735,7 @@ Return value:
ret.eResult = RESULT_IN_PROGRESS;
ret.szDescription[0] = '\0';
end:
+ g_MoveTimer.dEndTime = SystemTimeStamp();
return ret;
}
diff --git a/src/search.c b/src/search.c
index d8ece8d..fcf8907 100644
--- a/src/search.c
+++ b/src/search.c
@@ -725,13 +725,19 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
case UNRECOGNIZED:
break;
case RECOGN_EXACT:
- case RECOGN_EGTB:
if ((iAlpha < iScore) && (iScore < iBeta))
{
UpdatePV(ctx, RECOGNMOVE);
}
iBestScore = iScore;
goto end;
+ case RECOGN_EGTB:
+ if ((iAlpha < iScore) && (iScore < iBeta))
+ {
+ UpdatePV(ctx, EGTBMOVE);
+ }
+ iBestScore = iScore;
+ goto end;
case RECOGN_LOWER:
if (iScore >= iBeta)
{
@@ -1073,14 +1079,18 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
// leftover (matches pre-retirement behavior).
if (MOVE_SCORING_IID_RESCORED == pi->eMoveScoringScheme)
{
- SelectBestNoHistory(ctx, x);
+ SelectBestNoHistory(ctx, x); // mv's score is search-based
+ // WouldGiveCheck already computed in RescoreMovesViaSearch
+ mv = ctx->sMoveStack.mvf[x].mv;
}
else
{
SelectBestWithHistory(ctx, x);
+ mv = ctx->sMoveStack.mvf[x].mv;
+ mv.bvFlags |= WouldGiveCheck(ctx, mv);
+ ctx->sMoveStack.mvf[x].mv = mv; // re-store WouldGiveCheck's result
fIsLeftoverMove = (ctx->sMoveStack.mvf[x].iValue < GOOD_MOVE);
}
- mv = ctx->sMoveStack.mvf[x].mv;
#ifdef DEBUG
ASSERT(0 == (ctx->sMoveStack.mvf[x].bvFlags &
MVF_MOVE_SEARCHED));
@@ -1107,7 +1117,6 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx,
uCMEvidenceBucket = _CMEvidenceBucket(uEvidence);
}
#endif
- mv.bvFlags |= WouldGiveCheck(ctx, mv);
// Note: x is the index of the NEXT move to be
// considered, this move's index is (x-1).
@@ -3021,13 +3030,19 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx,
case UNRECOGNIZED:
break;
case RECOGN_EXACT:
- case RECOGN_EGTB:
if ((iAlpha < iScore) && (iScore < iBeta))
{
UpdatePV(ctx, RECOGNMOVE);
}
iBestScore = iScore;
goto end;
+ case RECOGN_EGTB:
+ if ((iAlpha < iScore) && (iScore < iBeta))
+ {
+ UpdatePV(ctx, EGTBMOVE);
+ }
+ iBestScore = iScore;
+ goto end;
case RECOGN_LOWER:
if (iScore >= iBeta)
{
@@ -3238,7 +3253,17 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx,
// If fIncludeChecks is FALSE then we still need to see if
// this move is going to check the opponent; GenerateMoves
// didn't do it for us to save time in the event of a fail
- // high.
+ // high. Tried swapping this for a post-MakeMove InCheck call
+ // instead (the theory: at most one query, nothing to amortize
+ // WouldGiveCheck's table-build against) -- measured worse on
+ // 2 of 3 curated suites (ringers +7.2%, confident_quick +5.9%,
+ // hard_quick -1.2%, sd 10, node counts identical either way).
+ // The assumption was wrong: this loop tries more than one move
+ // per node whenever an earlier candidate raises alpha without
+ // failing high, and the lazy-built bbUnblocked[uPly] (see
+ // WouldGiveCheck) gets reused cheaply across all of those,
+ // while a fresh InCheck call pays full price every time with
+ // no reuse at all. Reverted -- WouldGiveCheck stays here.
if (FALSE == fIncludeChecks)
{
mv.bvFlags |= WouldGiveCheck(ctx, mv);
diff --git a/src/searchsup.c b/src/searchsup.c
index ca75c92..726bc9f 100644
--- a/src/searchsup.c
+++ b/src/searchsup.c
@@ -51,7 +51,7 @@ Return value:
this->PV[u] = mv;
u++;
- // HASHMOVE/RECOGNMOVE/DRAWMOVE/MATEMOVE are sentinels, not real
+ // HASHMOVE/RECOGNMOVE/DRAWMOVE/MATEMOVE/EGTBMOVE are sentinels, not real
// moves -- WalkPV (util.c) treats any of them as a hard PV
// truncation point and asserts the slot right after one is
// always a zero terminator. Copying the child ply's PV tail
@@ -67,7 +67,8 @@ Return value:
if ((mv.uMove == HASHMOVE.uMove) ||
(mv.uMove == RECOGNMOVE.uMove) ||
(mv.uMove == DRAWMOVE.uMove) ||
- (mv.uMove == MATEMOVE.uMove))
+ (mv.uMove == MATEMOVE.uMove) ||
+ (mv.uMove == EGTBMOVE.uMove))
{
if (u < MAX_PLY_PER_SEARCH)
{
@@ -805,7 +806,7 @@ Return value:
return TRUE;
}
-INT
+INT FORCEINLINE
GetLMRReduction(IN SEARCHER_THREAD_CONTEXT *ctx,
IN ULONG uRemainingDepth,
IN ULONG uLegalMoves,
@@ -1282,15 +1283,16 @@ Return value:
}
-void ComputeMoveExtension(IN SEARCHER_THREAD_CONTEXT *ctx,
- IN SCORE iAlpha,
- IN SCORE iBeta,
- IN ULONG uMoveNum,
- IN SCORE iRoughEval,
- IN ULONG uDepth,
- IN SCORE iMoveSee,
- IN FLAG fIsLeftoverMove,
- IN OUT INT *piExtend)
+void FORCEINLINE
+ComputeMoveExtension(IN SEARCHER_THREAD_CONTEXT *ctx,
+ IN SCORE iAlpha,
+ IN SCORE iBeta,
+ IN ULONG uMoveNum,
+ IN SCORE iRoughEval,
+ IN ULONG uDepth,
+ IN SCORE iMoveSee,
+ IN FLAG fIsLeftoverMove,
+ IN OUT INT *piExtend)
/**
Routine description:
@@ -1629,6 +1631,7 @@ Return value:
SelectBestNoHistory(ctx, x);
mv = ctx->sMoveStack.mvf[x].mv;
mv.bvFlags |= WouldGiveCheck(ctx, mv);
+ ctx->sMoveStack.mvf[x].mv = mv; // <-- update the move's flags, WouldGiveCheck is slow
if (TRUE == MakeMove(ctx, mv))
{
if (-INFINITY_SCORE == iBestScore)
@@ -1886,7 +1889,7 @@ Return value:
}
-FLAG
+FLAG FORCEINLINE
CommonSearchInit(IN SEARCHER_THREAD_CONTEXT *ctx,
IN OUT SCORE *piAlpha,
IN OUT SCORE *piBeta,
diff --git a/src/see.c b/src/see.c
index dd60537..fa8e8db 100644
--- a/src/see.c
+++ b/src/see.c
@@ -206,7 +206,7 @@ Return value:
// single-bit bitboard of the actual king square is either that bit or
// zero, same O(1)-table-lookup shape the knight line already uses, no
// DISTANCE()==1 branch needed.
-BITBOARD
+BITBOARD FORCEINLINE
_WhoAttacksSquareBB(IN POSITION *pos,
IN COOR cSquare,
IN ULONG uSide,
@@ -258,7 +258,7 @@ Return value:
return bbAttackers;
}
-void CDECL
+void FORCEINLINE
_GetAttacksBB(IN OUT SEE_LIST *pList,
IN POSITION *pos,
IN COOR cSquare,
@@ -319,17 +319,14 @@ Return value:
// no mailbox load -- entirely replacing what iSeeDelta/pPawn[]
// used to do at runtime; only the (0-2) actual hits still need a
// COOR to populate the SEE_LIST.
+ BITBOARD bbPawnHits = g_PawnAttackOriginBB[uSide][cSquare] &
+ pos->bbPawns[uSide];
+ ULONG uPawnBit;
+ while (bbPawnHits)
{
- BITBOARD bbPawnHits = g_PawnAttackOriginBB[uSide][cSquare] &
- pos->bbPawns[uSide];
- ULONG uPawnBit;
-
- while (bbPawnHits)
- {
- uPawnBit = FastFirstBit(bbPawnHits) - 1;
- bbPawnHits &= (bbPawnHits - 1);
- ADD_ATTACKER(pPawn[uSide], BIT_NUMBER_TO_COOR(uPawnBit), VALUE_PAWN);
- }
+ uPawnBit = FastFirstBit(bbPawnHits) - 1;
+ bbPawnHits &= (bbPawnHits - 1);
+ ADD_ATTACKER(pPawn[uSide], BIT_NUMBER_TO_COOR(uPawnBit), VALUE_PAWN);
}
//
@@ -640,7 +637,6 @@ Return value:
{
pList->uCount--;
}
-
#else // !SEE_HEAPS
//
@@ -698,7 +694,7 @@ Return value:
#ifdef SEE_HEAPS
-static PIECE
+static PIECE FORCEINLINE
_MinLegalPiece(IN POSITION *pos,
IN ULONG uColor,
IN SEE_LIST *pList,
@@ -1008,7 +1004,7 @@ Return value:
}
-FLAG
+FLAG FORCEINLINE
SeeFastSign(IN POSITION *pos,
IN MOVE mv)
/**
@@ -1084,15 +1080,18 @@ Return value:
SCORE
-SEE(IN POSITION *pos,
- IN MOVE mv)
+_SEEFull(IN POSITION *pos,
+ IN MOVE mv)
/**
Routine description:
- Given a board and a move on the board, estimate the value of the
- move by considering the friend/enemy pieces that attack the move's
- destination square.
+ The real exchange-walk implementation -- see SEE()'s header
+ comment (below) for the cheap early-exit that now sits in front of
+ this, and _SEEFull's own non-static, un-inlined status so
+ testsee.c's TestSEENoDefenderFastPath can call it directly by name
+ for a correctness/speed comparison against SEE(), same convention
+ as _GenerateAllMoves/_GenerateAllMovesBB.
Parameters:
@@ -1244,3 +1243,75 @@ Return value:
#endif
return(rgiList[0]);
}
+
+SCORE FORCEINLINE
+SEE(IN POSITION *pos,
+ IN MOVE mv)
+/**
+
+Routine description:
+
+ Given a board and a move on the board, estimate the value of the
+ move by considering the friend/enemy pieces that attack the move's
+ destination square.
+
+ Cheap early exit before paying for any of _SEEFull's work (two
+ GetAttacks calls, sort/heap of both, _AddXRays): if the defending
+ side has NO attacker on mv.cTo -- checked the same x-ray-aware way
+ SeeFastSign already does (mv.cFrom removed from occupancy, so a
+ slider the mover was itself blocking gets counted) -- the exchange
+ can never continue past our own capture, so the true SEE value is
+ exactly PIECE_VALUE(mv.pCaptured) + PIECE_VALUE(mv.pPromoted). This
+ is not an approximation: it is the exact value _SEEFull's own
+ do/while loop would compute on this input (its first
+ _MinLegalPiece call for the defender would find nothing and break
+ immediately, returning rgiList[0] unchanged) -- see
+ TestSEENoDefenderFastPath (testsee.c) for the exact-value
+ correctness gate against _SEEFull, not just a sign check.
+
+ Unlike SeeFastSign (which only ever needs the sign and so can
+ throw the magnitude away), callers of SEE() need the real value --
+ _ScoreAllMoves/_ScoreQSearchMoves* add an ordering bias on top of
+ it and DecodeMoveValueEstimate (searchsup.c) later subtracts that
+ bias back out to recover this exact number for real pruning
+ decisions in _ShouldWeConsiderThisMove. So this early exit has to
+ return the true value, not just resolve a sign and hand back a
+ placeholder -- which is exactly what it does here, no different
+ from what the full exchange walk would have produced.
+
+ Pawn defenders can't be x-ray-revealed by the mover's own
+ departure (a pawn's attack squares are a fixed 2-square delta, not
+ a blockable ray), so that check alone uses the raw board, matching
+ SeeFastSign's identical reasoning.
+
+Parameters:
+
+ POSITION *pos,
+ MOVE mv
+
+Return value:
+
+ SCORE : the estimate of the move's score
+
+**/
+{
+ ULONG uDefender = FLIP(GET_COLOR(mv.pMoved));
+ BITBOARD bbOccupiedWithoutMover;
+
+ if (g_PawnAttackOriginBB[uDefender][mv.cTo] & pos->bbPawns[uDefender])
+ {
+ return _SEEFull(pos, mv);
+ }
+ bbOccupiedWithoutMover = pos->bbOccupied & ~COOR_TO_BB(mv.cFrom);
+ if (_WhoAttacksSquareBB(pos, mv.cTo, uDefender, bbOccupiedWithoutMover))
+ {
+ return _SEEFull(pos, mv);
+ }
+
+ //
+ // No defender at all -- the exchange can't continue past our own
+ // move, so the result is exactly the material gained (captured +
+ // promoted).
+ //
+ return (SCORE)(PIECE_VALUE(mv.pCaptured) + PIECE_VALUE(mv.pPromoted));
+}
diff --git a/src/split.c b/src/split.c
index abb6a11..0f6958f 100644
--- a/src/split.c
+++ b/src/split.c
@@ -649,8 +649,11 @@ Return value:
// NumLeftoverMovesToSelect budget comment).
//
SelectBestWithHistory(ctx, v);
- ctx->sMoveStack.mvf[v].mv.bvFlags |=
- WouldGiveCheck(ctx, ctx->sMoveStack.mvf[v].mv);
+ if (v != uMoveNum) // First one has WouldGiveCheck already computed
+ {
+ ctx->sMoveStack.mvf[v].mv.bvFlags |=
+ WouldGiveCheck(ctx, ctx->sMoveStack.mvf[v].mv);
+ }
ASSERT(!(ctx->sMoveStack.mvf[v].bvFlags & MVF_MOVE_SEARCHED));
g_SplitInfo[u].mvf[g_SplitInfo[u].uRemainingMoves] =
ctx->sMoveStack.mvf[v];
diff --git a/src/testgenerate.c b/src/testgenerate.c
index c6947d2..95c5000 100644
--- a/src/testgenerate.c
+++ b/src/testgenerate.c
@@ -697,4 +697,326 @@ TestGenerateAllMovesSpeed(void)
}
SystemFreeMemory(ctx);
}
+
+//
+// board_representation/MOVEGEN_MIGRATION.md's never-started
+// _FindUnblockedSquares/WouldGiveCheck candidate. Unlike every other
+// benchmark in this file, this does NOT measure the generator call in
+// isolation: _FindUnblockedSquares writes ~20 square entries that may
+// never be read, and WouldGiveCheck reads only 1-3 of them per move,
+// so a bitboard version that looks like a big win building the table
+// can give some of it back at query time. The real per-node cost is
+// the pair -- one _FindUnblockedSquares(BB) call followed by
+// WouldGiveCheck over every move actually generated at that node --
+// so that's what gets timed here, same interleaved-call methodology
+// as the rest of this file. WouldGiveCheck's own body is selected at
+// compile time by UNBLOCKED_BITBOARD (SQUARE_IS_UNBLOCKED/
+// DIR_FROM_SQ_TO_KING), so unlike TestGenerateAllMovesSpeed this can't
+// compare both implementations by name in one binary -- run this test
+// in two builds (with and without -DUNBLOCKED_BITBOARD) and compare
+// the printed cycles/pair across them.
+//
+void
+TestFindUnblockedSquaresSpeed(void)
+{
+ static const char *rgszFen[3] =
+ {
+ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1",
+ "6k1/8/8/3K4/8/2NBRQ2/8/8 w - - 0 1",
+ };
+ static const char *rgszLabel[3] =
+ {
+ "opening ", "middlegame", "endgame ",
+ };
+ POSITION posBench;
+ SEARCHER_THREAD_CONTEXT *ctx;
+ UINT64 u64Total, u64Start;
+ ULONG uIter, u, uMove, uNumMoves;
+ ULONG uPly;
+ MOVE_STACK *pStack;
+ const ULONG uCallsPerPosition = 50000;
+
+ Trace("Benchmarking _FindUnblockedSquares%s + WouldGiveCheck over "
+ "a generated move list (%lu calls/position) -- compare this "
+ "build's numbers against a build with the opposite "
+ "-DUNBLOCKED_BITBOARD setting...\n",
+#ifdef UNBLOCKED_BITBOARD
+ "BB",
+#else
+ " (mailbox)",
+#endif
+ uCallsPerPosition);
+
+ ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT));
+ ASSERT(ctx);
+ pStack = &(ctx->sMoveStack);
+ uPly = ctx->uPly;
+
+ for (u = 0; u < 3; u++)
+ {
+ FenToPosition(&posBench, (char *)rgszFen[u]);
+ InitializeSearcherContext(&posBench, ctx);
+
+ // Generate once, outside the timed loop, to get a realistic
+ // move list for this position -- WouldGiveCheck's cost is
+ // what's under test here, not generation itself.
+ pStack->uEnd[uPly] = pStack->uBegin[uPly];
+ pStack->sGenFlags[uPly].uAllGenFlags = 0;
+#ifdef UNBLOCKED_BITBOARD
+ _FindUnblockedSquaresBB(pStack, &posBench);
+#else
+ _FindUnblockedSquares(pStack, &posBench);
+#endif
+ _GenerateAllMoves(pStack, &posBench);
+ uNumMoves = MOVE_COUNT(ctx, uPly);
+ ASSERT(uNumMoves > 0);
+
+ u64Total = 0;
+ for (uIter = 0; uIter < uCallsPerPosition; uIter++)
+ {
+ u64Start = SystemReadTimeStampCounter();
+#ifdef UNBLOCKED_BITBOARD
+ _FindUnblockedSquaresBB(pStack, &posBench);
+#else
+ _FindUnblockedSquares(pStack, &posBench);
+#endif
+ for (uMove = pStack->uBegin[uPly];
+ uMove < pStack->uEnd[uPly];
+ uMove++)
+ {
+ WouldGiveCheck(ctx, pStack->mvf[uMove].mv);
+ }
+ u64Total += (SystemReadTimeStampCounter() - u64Start);
+ }
+ printf(" %s: %" COMPILER_LONGLONG_UNSIGNED_FORMAT
+ " cycles/pair (%lu moves checked per call)\n",
+ rgszLabel[u],
+ u64Total / uCallsPerPosition,
+ uNumMoves);
+ }
+ SystemFreeMemory(ctx);
+}
+
+#if defined(CAPTURES_PROMS_BITBOARD_PATH)
+
+static int
+_CompareU32(const void *pA, const void *pB)
+{
+ const ULONG *a = (const ULONG *)pA;
+ const ULONG *b = (const ULONG *)pB;
+ if (*a < *b) return -1;
+ if (*a > *b) return 1;
+ return 0;
+}
+
+//
+// Correctness gate for _GenerateCapturesAndPromotions
+// (generate.c, the GENERATE_CAPTURES_PROMS bitboard path): its output
+// must equal _GenerateAllMoves' output filtered down to
+// IS_CAPTURE_OR_PROMOTION(mv), as a SET (mv.uMove already encodes
+// cFrom/cTo/pMoved/pCaptured/pPromoted/flags, so set membership is a
+// direct ULONG compare -- no need to decode fields individually). Both
+// generators read the identical board, so a true mismatch here means
+// one of them is wrong, not a representational difference. Covers
+// ordinary captures, capture-promotions, quiet push-promotions, en
+// passant, and (implicitly, by their absence from both outputs) that
+// castling and ordinary quiet pushes never show up in either side of
+// the comparison.
+//
+void
+TestCapturesAndPromotionsGenerator(void)
+{
+ static const char *rgszFen[] =
+ {
+ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1",
+ "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
+ "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R b KQkq - 0 1",
+ "8/PPP4k/8/8/8/8/4Kppp/8 w - - 0 1",
+ "8/PPP4k/8/8/8/8/4Kppp/8 b - - 0 1",
+ "3Q4/1Q4Q1/4Q3/2Q4R/Q4Q2/3Q4/1Q4Rp/1K1BBNNk w - - 0 1",
+ "rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3",
+ "rnbqkbnr/pp1p1ppp/8/2pPp3/8/8/PPP1PPPP/RNBQKBNR w KQkq c6 0 3",
+ "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPP1PPPP/RNBQKBNR b KQkq c6 0 2",
+ "8/1P6/8/8/8/8/7p/1k4K1 w - - 0 1",
+ "8/1P6/8/8/8/8/7p/1k4K1 b - - 0 1",
+ "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
+ "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 b - - 0 1",
+ };
+ POSITION pos;
+ SEARCHER_THREAD_CONTEXT *ctx;
+ MOVE_STACK *pStack;
+ ULONG uPly;
+ ULONG u, v;
+ ULONG uRefCount, uNewCount;
+ static ULONG rgRef[MAX_MOVES_PER_PLY];
+ static ULONG rgNew[MAX_MOVES_PER_PLY];
+ MOVE mv;
+
+ Trace("Testing captures/promotions-only generator against "
+ "_GenerateAllMoves filtered to IS_CAPTURE_OR_PROMOTION...\n");
+
+ ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT));
+ ASSERT(ctx);
+ pStack = &(ctx->sMoveStack);
+ uPly = ctx->uPly;
+
+ for (u = 0; u < ARRAY_LENGTH(rgszFen); u++)
+ {
+ if (FALSE == FenToPosition(&pos, (char *)rgszFen[u]))
+ {
+ UtilPanic(TESTCASE_FAILURE,
+ NULL, "FenToPosition", NULL, NULL,
+ __FILE__, __LINE__);
+ }
+ InitializeSearcherContext(&pos, ctx);
+
+ // GENERATE_CAPTURES_PROMS' own precondition (see GenerateMoves'
+ // ASSERT) -- none of the fixtures above should trip this, but
+ // don't assume it of a future addition.
+ if (TRUE == InCheck(&pos, pos.uToMove))
+ {
+ continue;
+ }
+
+ // Reference: full generation, filtered to captures/promotions.
+ pStack->uEnd[uPly] = pStack->uBegin[uPly];
+ pStack->sGenFlags[uPly].uAllGenFlags = 0;
+ _GenerateAllMoves(pStack, &pos);
+ uRefCount = 0;
+ for (v = pStack->uBegin[uPly]; v < pStack->uEnd[uPly]; v++)
+ {
+ mv = pStack->mvf[v].mv;
+ if (IS_CAPTURE_OR_PROMOTION(mv))
+ {
+ ASSERT(uRefCount < MAX_MOVES_PER_PLY);
+ rgRef[uRefCount++] = mv.uMove;
+ }
+ }
+
+ // Candidate: the new generator, directly.
+ pStack->uEnd[uPly] = pStack->uBegin[uPly];
+ pStack->sGenFlags[uPly].uAllGenFlags = 0;
+ _GenerateCapturesAndPromotions(pStack, &pos);
+ uNewCount = 0;
+ for (v = pStack->uBegin[uPly]; v < pStack->uEnd[uPly]; v++)
+ {
+ mv = pStack->mvf[v].mv;
+ ASSERT(IS_CAPTURE_OR_PROMOTION(mv));
+ ASSERT(uNewCount < MAX_MOVES_PER_PLY);
+ rgNew[uNewCount++] = mv.uMove;
+ }
+
+ if (uRefCount != uNewCount)
+ {
+ Trace("TestCapturesAndPromotionsGenerator: FEN %s: "
+ "reference produced %lu move(s), new produced %lu\n",
+ rgszFen[u], uRefCount, uNewCount);
+ UtilPanic(TESTCASE_FAILURE, NULL,
+ "TestCapturesAndPromotionsGenerator", NULL, NULL,
+ __FILE__, __LINE__);
+ }
+
+ // Set equality -- the two generators walk pieces in different
+ // orders, so sort both before the element-by-element compare.
+ qsort(rgRef, uRefCount, sizeof(rgRef[0]), _CompareU32);
+ qsort(rgNew, uNewCount, sizeof(rgNew[0]), _CompareU32);
+ for (v = 0; v < uRefCount; v++)
+ {
+ if (rgRef[v] != rgNew[v])
+ {
+ Trace("TestCapturesAndPromotionsGenerator: FEN %s: "
+ "move set mismatch at sorted index %lu "
+ "(ref 0x%08lx, new 0x%08lx)\n",
+ rgszFen[u], v,
+ (unsigned long)rgRef[v], (unsigned long)rgNew[v]);
+ UtilPanic(TESTCASE_FAILURE, NULL,
+ "TestCapturesAndPromotionsGenerator", NULL, NULL,
+ __FILE__, __LINE__);
+ }
+ }
+ }
+ SystemFreeMemory(ctx);
+}
+
+//
+// Speed gate: _GenerateCapturesAndPromotions must be faster than
+// _GenerateAllMoves for the same position, interleaved-call
+// methodology matching every other *Speed test in this file. Unlike
+// the per-piece-type speed tests above, this isn't comparing two
+// implementations of the same move set -- the new generator produces
+// fewer moves by design -- so this is purely "does skipping the quiet
+// moves up front actually cost less than generating and discarding
+// them," the actual claim behind building this generator at all.
+//
+void
+TestGenerateCapturesAndPromotionsSpeed(void)
+{
+ static const char *rgszFen[3] =
+ {
+ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1",
+ "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
+ };
+ static const char *rgszLabel[3] =
+ {
+ "opening ", "middlegame", "tactical ",
+ };
+ POSITION posBench;
+ SEARCHER_THREAD_CONTEXT *ctx;
+ UINT64 u64AllTotal, u64CapsTotal, u64Start;
+ ULONG uIter, u;
+ ULONG uPly;
+ const ULONG uCallsPerPosition = 200000;
+
+ Trace("Benchmarking whole-node move generation: _GenerateAllMoves "
+ "vs _GenerateCapturesAndPromotions (interleaved, %lu "
+ "calls/position)...\n",
+ uCallsPerPosition);
+
+ ctx = SystemAllocateMemory(sizeof(SEARCHER_THREAD_CONTEXT));
+ ASSERT(ctx);
+ uPly = ctx->uPly;
+
+ for (u = 0; u < 3; u++)
+ {
+ FenToPosition(&posBench, (char *)rgszFen[u]);
+ InitializeSearcherContext(&posBench, ctx);
+ ASSERT(!InCheck(&posBench, posBench.uToMove));
+
+ u64AllTotal = 0;
+ u64CapsTotal = 0;
+ for (uIter = 0; uIter < uCallsPerPosition; uIter++)
+ {
+ ctx->sMoveStack.uEnd[uPly] = ctx->sMoveStack.uBegin[uPly];
+ u64Start = SystemReadTimeStampCounter();
+ _GenerateAllMoves(&ctx->sMoveStack, &posBench);
+ u64AllTotal += (SystemReadTimeStampCounter() - u64Start);
+
+ ctx->sMoveStack.uEnd[uPly] = ctx->sMoveStack.uBegin[uPly];
+ u64Start = SystemReadTimeStampCounter();
+ _GenerateCapturesAndPromotions(&ctx->sMoveStack, &posBench);
+ u64CapsTotal += (SystemReadTimeStampCounter() - u64Start);
+ }
+ printf(" %s: _GenerateAllMoves %" COMPILER_LONGLONG_UNSIGNED_FORMAT
+ " cycles/call, _GenerateCapturesAndPromotions %"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call "
+ "(captures-only is %.2fx all-moves)\n",
+ rgszLabel[u],
+ u64AllTotal / uCallsPerPosition,
+ u64CapsTotal / uCallsPerPosition,
+ (double)u64CapsTotal / (double)u64AllTotal);
+ if (u64CapsTotal >= u64AllTotal)
+ {
+ UtilPanic(TESTCASE_FAILURE, NULL,
+ "TestGenerateCapturesAndPromotionsSpeed: not faster",
+ NULL, NULL, __FILE__, __LINE__);
+ }
+ }
+ SystemFreeMemory(ctx);
+}
+
+#endif // captures/promotions bitboard path
+
#endif
diff --git a/src/testsee.c b/src/testsee.c
index bbcd7e0..240ac39 100644
--- a/src/testsee.c
+++ b/src/testsee.c
@@ -624,4 +624,159 @@ TestWhoAttacksSquareMagic(void)
}
}
}
+
+//
+// Correctness + speed gate for SEE()'s no-defender early exit (see.c):
+// SEE() must return the EXACT same value as _SEEFull() (not just the
+// same sign -- TestSeeFastSign above already covers sign-only) on
+// every move, since real callers (_ScoreAllMoves/_ScoreQSearchMoves*)
+// trust the magnitude, not just the sign, for a winning capture.
+//
+// Speed deliberately uses four benchmark positions spanning the
+// defended/undefended spectrum, not just a "best case": a dense
+// tactical middlegame where nearly every capture has a defender (the
+// new check's overhead pays for nothing -- this is the case to watch
+// for a net LOSS, since every call still pays for the early check
+// before falling through to the exact same _SEEFull work SEE() used
+// to do directly), a bare-kings-plus-one-piece endgame where almost
+// nothing is defended (the case the early exit is for), plus the
+// standard opening/middlegame pair for a realistic middle ground.
+//
+void
+TestSEENoDefenderFastPath(void)
+{
+ SEARCHER_THREAD_CONTEXT *ctx;
+ POSITION posBench;
+ MOVE rgmv[MAX_MOVE_STACK];
+ ULONG uMoveCount;
+ ULONG u, x;
+ SCORE iFull, iFast;
+ const ULONG uPositions = 3000;
+
+ Trace("Testing SEE() no-defender fast path against _SEEFull "
+ "(exact value, not just sign)...\n");
+ ctx = (SEARCHER_THREAD_CONTEXT *)malloc(sizeof(SEARCHER_THREAD_CONTEXT));
+ if (NULL == ctx)
+ {
+ return;
+ }
+
+ for (u = 0; u < uPositions; u++)
+ {
+ GenerateRandomLegalPosition(&posBench);
+ InitializeSearcherContext(&posBench, ctx);
+ GenerateMoves(ctx, (MOVE){0}, GENERATE_DONT_SCORE);
+
+ uMoveCount = 0;
+ for (x = ctx->sMoveStack.uBegin[ctx->uPly];
+ x < ctx->sMoveStack.uEnd[ctx->uPly];
+ x++)
+ {
+ rgmv[uMoveCount++] = ctx->sMoveStack.mvf[x].mv;
+ }
+
+ for (x = 0; x < uMoveCount; x++)
+ {
+ iFull = _SEEFull(&(ctx->sPosition), rgmv[x]);
+ iFast = SEE(&(ctx->sPosition), rgmv[x]);
+ if (iFull != iFast)
+ {
+ Trace("move=0x%x from=%d to=%d moved=%d captured=%d "
+ "promoted=%d _SEEFull=%d SEE=%d\n",
+ rgmv[x].uMove, rgmv[x].cFrom, rgmv[x].cTo,
+ rgmv[x].pMoved, rgmv[x].pCaptured, rgmv[x].pPromoted,
+ iFull, iFast);
+ DumpPosition(&(ctx->sPosition));
+ UtilPanic(TESTCASE_FAILURE,
+ &(ctx->sPosition),
+ "SEE/_SEEFull exact-value mismatch",
+ NULL, NULL,
+ __FILE__, __LINE__);
+ }
+ }
+ }
+ free(ctx);
+
+ //
+ // Speed: same isolated, interleaved cycles/call methodology as the
+ // other benchmarks above.
+ //
+ {
+ static const char *rgszFen[4] =
+ {
+ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
+ "r1bq1rk1/pp2bppp/2n1pn2/2pp4/3P4/2NBPN2/PP3PPP/R1BQ1RK1 w - - 0 1",
+ "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
+ "8/5k2/8/3K4/8/8/8/4R3 w - - 0 1",
+ };
+ static const char *rgszLabel[4] =
+ {
+ "opening ", "middlegame ",
+ "tactical/dense ", "sparse endgame ",
+ };
+ SEARCHER_THREAD_CONTEXT *ctx2;
+ POSITION posBench2;
+ MOVE rgmv2[MAX_MOVE_STACK];
+ ULONG uMoveCount2;
+ UINT64 u64FullTotal, u64FastTotal, u64Start;
+ ULONG uIter;
+ SCORE iSink;
+ const ULONG uCallsPerPosition = 200000;
+
+ Trace("Benchmarking SEE(): _SEEFull (no early exit) vs SEE "
+ "(no-defender fast path), interleaved, %lu calls/"
+ "position...\n",
+ uCallsPerPosition);
+ ctx2 = (SEARCHER_THREAD_CONTEXT *)malloc(sizeof(SEARCHER_THREAD_CONTEXT));
+ if (NULL == ctx2)
+ {
+ return;
+ }
+
+ for (u = 0; u < 4; u++)
+ {
+ FenToPosition(&posBench2, (char *)rgszFen[u]);
+ InitializeSearcherContext(&posBench2, ctx2);
+ GenerateMoves(ctx2, (MOVE){0}, GENERATE_DONT_SCORE);
+
+ uMoveCount2 = 0;
+ for (x = ctx2->sMoveStack.uBegin[ctx2->uPly];
+ x < ctx2->sMoveStack.uEnd[ctx2->uPly];
+ x++)
+ {
+ rgmv2[uMoveCount2++] = ctx2->sMoveStack.mvf[x].mv;
+ }
+ if (uMoveCount2 == 0)
+ {
+ continue;
+ }
+
+ u64FullTotal = 0;
+ u64FastTotal = 0;
+ for (uIter = 0; uIter < uCallsPerPosition; uIter++)
+ {
+ ULONG uPair = uIter % uMoveCount2;
+
+ u64Start = SystemReadTimeStampCounter();
+ iSink = _SEEFull(&(ctx2->sPosition), rgmv2[uPair]);
+ u64FullTotal += (SystemReadTimeStampCounter() - u64Start);
+
+ u64Start = SystemReadTimeStampCounter();
+ iSink = SEE(&(ctx2->sPosition), rgmv2[uPair]);
+ u64FastTotal += (SystemReadTimeStampCounter() - u64Start);
+ (void)iSink;
+ }
+ printf(" %s: _SEEFull %" COMPILER_LONGLONG_UNSIGNED_FORMAT
+ " cycles/call, SEE %"
+ COMPILER_LONGLONG_UNSIGNED_FORMAT " cycles/call "
+ "(SEE is %.2fx _SEEFull, %u distinct moves)\n",
+ rgszLabel[u],
+ u64FullTotal / uCallsPerPosition,
+ u64FastTotal / uCallsPerPosition,
+ (double)u64FastTotal / (double)u64FullTotal,
+ uMoveCount2);
+ }
+ free(ctx2);
+ }
+}
#endif // TEST
diff --git a/src/util.c b/src/util.c
index f3c5521..60912de 100644
--- a/src/util.c
+++ b/src/util.c
@@ -1111,6 +1111,12 @@ Return value:
ASSERT(ctx->sPlyInfo[v].PV[u+1].uMove == 0);
break;
}
+ else if (mv.uMove == EGTBMOVE.uMove)
+ {
+ strncat(buf, "<EGTB>", ARRAY_LENGTH(buf) - strlen(buf) - 1);
+ ASSERT(ctx->sPlyInfo[v].PV[u+1].uMove == 0);
+ break;
+ }
else
{
strncat(buf, MoveToSan(mv, &ctx->sPosition),