/** Copyright (c) Scott Gasch Module Name: chess.h Abstract: Author: Scott Gasch (scott.gasch@gmail.com) 7 Apr 2004 Revision History: $Id: chess.h 354 2008-06-30 05:10:08Z scott $ **/ #ifndef CHESS_ #define CHESS_ #include "compiler.h" // // Datatype wrappers // #define MIN_CHAR (0x80) #define MAX_CHAR (0x7f) typedef char CHAR; #define MIN_BYTE (0x00) #define MAX_BYTE (0xff) typedef unsigned char BYTE; #define MIN_UCHAR (0x00) #define MAX_UCHAR (0xff) typedef unsigned char UCHAR; #define CAN_FIT_IN_UCHAR(x) ((x) <= MAX_UCHAR) #define MIN_SHORT (0x8000) #define MAX_SHORT (0x7fff) typedef signed short SHORT; #define MIN_USHORT (0x0000) #define MAX_USHORT (0xffff) typedef unsigned short USHORT; #define CAN_FIT_IN_USHORT(x) ((x) <= MAX_USHORT) #define MIN_WORD (0x0000) #define MAX_WORD (0xffff) typedef unsigned short WORD; #define MIN_INT (0x80000000) #define MAX_INT (0x7fffffff) typedef signed int INT; #define MIN_DWORD (0x00000000) #define MAX_DWORD (0xffffffff) typedef unsigned int DWORD; #define MIN_UINT (0x00000000) #define MAX_UINT (0xffffffff) typedef unsigned int UINT; #define MIN_ULONG (0x00000000) #define MAX_ULONG (0xffffffff) typedef unsigned int ULONG; #define MIN_INT64 (0x8000000000000000) #define MAX_INT64 (0x7fffffffffffffff) typedef signed COMPILER_LONGLONG INT64; #define MIN_UINT64 (0x0000000000000000) #define MAX_UINT64 (0xffffffffffffffff) typedef unsigned COMPILER_LONGLONG UINT64; #define MIN_BITBOARD (0x0000000000000000) #define MAX_BITBOARD (0xffffffffffffffff) typedef UINT64 BITBOARD; #define MIN_BITV MIN_UINT #define MAX_BITV MAX_UINT typedef unsigned int BITV; #define MIN_FLAG MIN_UINT #define MAX_FLAG MAX_UINT typedef unsigned int FLAG; #define IS_VALID_FLAG(x) (((x) == TRUE) || ((x) == FALSE)) typedef unsigned int COOR; typedef unsigned int PIECE; typedef signed int SCORE; #define BIT1 (0x1) #define BIT2 (0x2) #define BIT3 (0x4) #define BIT4 (0x8) #define BIT5 (0x10) #define BIT6 (0x20) #define BIT7 (0x40) #define BIT8 (0x80) #define BIT9 (0x100) #define BIT10 (0x200) #define BIT11 (0x400) #define BIT12 (0x800) #define BIT13 (0x1000) #define BIT14 (0x2000) #define BIT15 (0x4000) #define BIT16 (0x8000) #define BIT17 (0x10000) #define BIT18 (0x20000) #define BIT19 (0x40000) #define BIT20 (0x80000) #define BIT21 (0x100000) #define BIT22 (0x200000) #define BIT23 (0x400000) #define BIT24 (0x800000) #define BIT25 (0x1000000) #define BIT26 (0x2000000) #define BIT27 (0x4000000) #define BIT28 (0x8000000) #define BIT29 (0x10000000) #define BIT30 (0x20000000) #define BIT31 (0x40000000) #define BIT32 (0x80000000) typedef struct _DLIST_ENTRY { struct _DLIST_ENTRY *pFlink; struct _DLIST_ENTRY *pBlink; } DLIST_ENTRY; // // Constants // #define YES (1) #define NO (0) #ifndef TRUE #define TRUE (YES) #endif #ifndef FALSE #define FALSE (NO) #endif // // Calculate the length of an array (i.e. number of elements) // #define ARRAY_LENGTH(x) (sizeof(x) / sizeof((x)[0])) #define COMMAND(x) \ void (x)(CHAR *szInput, ULONG argc, CHAR *argv[], POSITION *pos) #define MB (1024 * 1024) // // Calculate the address of the base of the structure given its type, and an // address of a field within the structure. // #ifndef OFFSET_OF #define OFFSET_OF(field, type) \ (size_t)(&((type *)0)->field) #endif #ifndef CONTAINING_STRUCT #define CONTAINING_STRUCT(address, type, field) \ ((type *)((BYTE *)(address) - (BYTE *)(OFFSET_OF(field, type)))) #endif #define WHITE (1) #define BLACK (0) #define BAD_COLOR (2) #define IS_VALID_COLOR(x) (((x) == WHITE) || ((x) == BLACK)) #define FOREACH_COLOR(x) for((x) = BLACK; (x) < BAD_COLOR; (x)++) #define RANDOM_COLOR (rand() & 1) #define FLIP(color) ((color) ^ 1) #define COLOR_NAME(color) (((color) == WHITE) ? "white" : "black") #define MAX_MOVES_PER_PLY (218) #define MAX_PLY_PER_SEARCH (64) #define MAX_MOVES_PER_GAME (1024) #define SMALL_STRING_LEN_CHAR (256) #define MEDIUM_STRING_LEN_CHAR (8192) #define BIG_STRING_LEN_CHAR (16384) #define QUARTER_PLY 16 #define HALF_PLY 32 #define THREE_QUARTERS_PLY 48 #define ONE_PLY 64 #define TWO_PLY 128 #define THREE_PLY 192 #define FOUR_PLY 256 #define MAX_DEPTH_PER_SEARCH (MAX_PLY_PER_SEARCH * ONE_PLY) #define IS_VALID_DEPTH(x) (((x) >= 0) && \ ((x) <= MAX_DEPTH_PER_SEARCH) && \ (((x) & 0xfffff00f) == 0)) #define INFINITY_SCORE (MAX_SHORT) #define INVALID_SCORE (INFINITY_SCORE + 1) #define IS_VALID_SCORE(x) (((x) >= -INFINITY_SCORE) && \ ((x) <= +INFINITY_SCORE)) #define NMATE (+INFINITY_SCORE - 200) #define MATED_SCORE(ply) (-INFINITY_SCORE + (ply)) #define IS_A_POWER_OF_2(x) (((x) & (x - 1)) == 0) // // Program version number // #define VERSION "2.00" // GIT_COMMIT is normally injected by GNUmakefile via -DGIT_COMMIT (short // hash, plus a -dirty suffix if the tree had uncommitted changes at build // time); this is just a fallback for any build path that doesn't set it. #ifndef GIT_COMMIT #define GIT_COMMIT "unknown" #endif // // Function decorators // #define IN #define OUT #define INOUT #define UNUSED #define NOTHING // ---------------------------------------------------------------------- // // PIECE: // // a piece (4 bits) = type (3 bits) + color (1 bit) // // 3 1 0 // . . . . // | | | // type C // #define KING (6) // 110X #define QUEEN (5) // 101X #define ROOK (4) // 100X #define BISHOP (3) // 011X #define KNIGHT (2) // 010X #define PAWN (1) // 001X #define EMPTY (0) // 000X // (WHATEVER << 1) #define BLACK_PAWN (2) // 0010 #define BLACK_KNIGHT (4) // 0100 #define BLACK_BISHOP (6) // 0110 #define BLACK_ROOK (8) // 1000 #define BLACK_QUEEN (10) // 1010 #define BLACK_KING (12) // 1100 // (WHATEVER << 1) | WHITE #define WHITE_PAWN (3) // 0011 #define WHITE_KNIGHT (5) // 0101 #define WHITE_BISHOP (7) // 0111 #define WHITE_ROOK (9) // 1001 #define WHITE_QUEEN (11) // 1011 #define WHITE_KING (13) // 1101 #define PIECE_TYPE(p) ((p) >> 1) #define PIECE_COLOR(p) ((p) & WHITE) #define RANDOM_PIECE (rand() % 12) + 2; #define IS_PAWN(p) (((p) & 0xE) == BLACK_PAWN) #define IS_KNIGHT(p) (((p) & 0xE) == BLACK_KNIGHT) #define IS_BISHOP(p) (((p) & 0xE) == BLACK_BISHOP) #define IS_ROOK(p) (((p) & 0xE) == BLACK_ROOK) #define IS_QUEEN(p) (((p) & 0xE) == BLACK_QUEEN) #define IS_KING(p) (((p) & 0xE) == BLACK_KING) #define IS_KNIGHT_OR_KING(p) (((p) & 0x6) == 0x4) #define IS_VALID_PIECE(p) ((PIECE_TYPE((p)) >= PAWN) && \ (PIECE_TYPE((p)) <= KING)) #define IS_WHITE_PIECE(p) ((p) & WHITE) #define IS_BLACK_PIECE(p) (!IS_WHITE(p)) #define OPPOSITE_COLORS(p, q) (((p) ^ (q)) & WHITE) #define SAME_COLOR(p,q) (!OPPOSITE_COLORS(p, q)) #define GET_COLOR(p) ((p) & WHITE) // ---------------------------------------------------------------------- // // a coordinate (8 bits) = rank + file (0x88) // // 7 4 3 0 // . . . . . . . . // | | | // rank file // | | // | +--- 0-7 = file A thru file H // | // +------------ 0-7 = rank 1 thru rank 8 // // given a coordinate, C, if C & 0x88 then it is illegal (off board) // // // 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 // +---------------------------------------+ // 8 | 00 |:01:| 02 |:03:| 04 |:05:| 06 |:07:| 0x00 // 7 |:10:| 11 |:12:| 13 |:14:| 15 |:16:| 17 | 0x10 // 6 | 20 |:21:| 22 |:23:| 24 |:25:| 26 |:27:| 0x20 // 5 |:30:| 31 |:32:| 33 |:34:| 35 |:36:| 37 | 0x30 // 4 | 40 |:41:| 42 |:43:| 44 |:45:| 46 |:47:| 0x40 // 3 |:50:| 51 |:52:| 53 |:54:| 55 |:56:| 57 | 0x50 // 2 | 60 |:61:| 62 |:63:| 64 |:65:| 66 |:67:| 0x60 // 1 |:70:| 71 |:72:| 73 |:74:| 75 |:76:| 77 | 0x70 // +---------------------------------------+ // A B C D E F G H // #define A (0) #define B (1) #define C (2) #define D (3) #define E (4) #define F (5) #define G (6) #define H (7) #define A8 (0x00) #define B8 (0x01) #define C8 (0x02) #define D8 (0x03) #define E8 (0x04) #define F8 (0x05) #define G8 (0x06) #define H8 (0x07) #define A7 (0x10) #define B7 (0x11) #define C7 (0x12) #define D7 (0x13) #define E7 (0x14) #define F7 (0x15) #define G7 (0x16) #define H7 (0x17) #define A6 (0x20) #define B6 (0x21) #define C6 (0x22) #define D6 (0x23) #define E6 (0x24) #define F6 (0x25) #define G6 (0x26) #define H6 (0x27) #define A5 (0x30) #define B5 (0x31) #define C5 (0x32) #define D5 (0x33) #define E5 (0x34) #define F5 (0x35) #define G5 (0x36) #define H5 (0x37) #define A4 (0x40) #define B4 (0x41) #define C4 (0x42) #define D4 (0x43) #define E4 (0x44) #define F4 (0x45) #define G4 (0x46) #define H4 (0x47) #define A3 (0x50) #define B3 (0x51) #define C3 (0x52) #define D3 (0x53) #define E3 (0x54) #define F3 (0x55) #define G3 (0x56) #define H3 (0x57) #define A2 (0x60) #define B2 (0x61) #define C2 (0x62) #define D2 (0x63) #define E2 (0x64) #define F2 (0x65) #define G2 (0x66) #define H2 (0x67) #define A1 (0x70) #define B1 (0x71) #define C1 (0x72) #define D1 (0x73) #define E1 (0x74) #define F1 (0x75) #define G1 (0x76) #define H1 (0x77) // TODO: if use this anywhere that matters, consider double loop // and unrolling the inside loop to prevent the multiple // continue for invalid squares #define FOREACH_SQUARE(x) for((x) = (A8); (x) <= (H1); (x)++) #define IS_ON_BOARD(c) (!((c) & 0x88)) #define RANDOM_COOR (rand() & 0x77) #define FILE_RANK_TO_COOR(f, r) (((8 - (r)) << 4) | (f)) #define RANK(c) (8 - ((c) >> 4)) #define RANK1(c) (((c) & 0xF0) == 0x70) #define RANK2(c) (((c) & 0xF0) == 0x60) #define RANK3(c) (((c) & 0xF0) == 0x50) #define RANK4(c) (((c) & 0xF0) == 0x40) #define RANK5(c) (((c) & 0xF0) == 0x30) #define RANK6(c) (((c) & 0xF0) == 0x20) #define RANK7(c) (((c) & 0xF0) == 0x10) #define RANK8(c) (((c) & 0xF0) == 0x00) #define FILE(c) ((c) & 0x0F) #define FILEA(c) (((c) & 0x0F) == 0x00) #define FILEB(c) (((c) & 0x0F) == 0x01) #define FILEC(c) (((c) & 0x0F) == 0x02) #define FILED(c) (((c) & 0x0F) == 0x03) #define FILEE(c) (((c) & 0x0F) == 0x04) #define FILEF(c) (((c) & 0x0F) == 0x05) #define FILEG(c) (((c) & 0x0F) == 0x06) #define FILEH(c) (((c) & 0x0F) == 0x07) #define IS_WHITE_SQUARE_FR(f, r) (((f) + (r) - 1) & 1) #define IS_WHITE_SQUARE_COOR(c) (IS_WHITE_SQUARE_FR(FILE(c), RANK(c))) #define VALID_EP_SQUARE(c) (((c) == ILLEGAL_COOR) || ((c) == 0) || \ (RANK3(c)) || \ (RANK6(c))) // ---------------------------------------------------------------------- // // a move (4 bytes) = from/to, moved, captured, promoted and special // // 31 24 23 16 15 12 11 8 7 4 3 0 // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . // | from | to | moved | capt | prom | flags | // | | | | | // | | | | | // | | | | | // | | +-------+-------+-- type and // | | color of // | | piece moved // | | captured and // | | promote targ // +---------------+-- board location where // move begins and ends // // // Special flag asserted on special move types: // // if (special) // { // if (moved == PAWN) // { // if (promotion) // { // move is a promote (w/ or w/o capture) // } // else if (capture) // { // move is an en passant catpure // } // else // { // move is a double jump // } // } // else if (moved == KING) // { // move is a castle (long or short) // } // else // { // invalid use of special flag // } // } // typedef union _MOVE { ULONG uMove; struct { UCHAR cFrom : 8; UCHAR cTo : 8; UCHAR pMoved : 4; UCHAR pCaptured : 4; UCHAR pPromoted : 4; UCHAR bvFlags : 4; }; } MOVE; #define IS_SAME_MOVE(a, b) \ (((a).uMove & 0x0FFFFFFF) == (((b).uMove & 0x0FFFFFFF))) #define MAKE_MOVE(from,to,piece,cap,prom,flags) \ (((ULONG)(from)) | \ ((ULONG)(to) << 8) | \ ((ULONG)(piece) << 16) | \ ((ULONG)(cap) << 20) | \ ((ULONG)(prom) << 24) | \ ((ULONG)(flags) << 28)) #define MAKE_MOVE_WITH_NO_PROM_OR_FLAGS(from,to,piece,cap) \ (((ULONG)(from)) | \ ((ULONG)(to) << 8) | \ ((ULONG)(piece) << 16) | \ ((ULONG)(cap) << 20)) // // Move flag values: // #define MOVE_FLAG_SPECIAL (1) // en pass, prom, castle, double #define IS_SPECIAL_MOVE(mv) \ ((mv).uMove & 0x10000000) #define IS_CASTLE(mv) \ (IS_SPECIAL_MOVE(mv) && IS_KING((mv).pMoved)) #ifdef DEBUG #define IS_PROMOTION(mv) \ (IS_SPECIAL_MOVE(mv) && ((mv).pPromoted)) #else #define IS_PROMOTION(mv) \ ((mv).pPromoted) #endif #define IS_CAPTURE_OR_PROMOTION(mv) \ ((mv).uMove & 0x0FF00000) #define IS_ENPASSANT(mv) \ (IS_SPECIAL_MOVE(mv) && ((mv).pCaptured) && !IS_PROMOTION(mv)) #define IS_DOUBLE_JUMP(mv) \ (IS_SPECIAL_MOVE(mv) && !IS_CAPTURE_OR_PROMOTION(mv)) #define MOVE_FLAG_KILLERMATE (2) // killer mate #define IS_KILLERMATE_MOVE(mv) ((mv).uMove & 0x20000000) #define MOVE_FLAG_ESCAPING_CHECK (4) // escaping check #define IS_ESCAPING_CHECK(mv) ((mv).uMove & 0x40000000) #define MOVE_FLAG_CHECKING (8) // checking move #define IS_CHECKING_MOVE(mv) ((mv).uMove & 0x80000000) // ---------------------------------------------------------------------- // // Move list // #define MAX_MOVE_LIST (MAX_MOVES_PER_GAME + MAX_PLY_PER_SEARCH) // ---------------------------------------------------------------------- // ATTACK_BITV/bvAttacks retired entirely 2026-09-05 (board_ // representation/EVAL.md section 9): the per-square, per-color // attack-bit union that used to live at rgSquare[c|8] (the "c|8" // shadow-index trick -- reusing the same 128-entry array the 0x88 // board layout already needs for its own off-board sentinels, at // the indices real piece coordinates never use) is gone now that // king -- the last piece type writing it -- has converted to a // bitboard accumulator like every other piece type before it. Every // consumer reads a pos->bbXAttacks[color] bitboard directly instead. // // PAWN_BIT/MINOR_BIT/ROOK_BIT/QUEEN_BIT/KING_BIT below are NOT part // of that retired mechanism -- they're a separate, still-live // byte-scale bit-packing scheme _EvalKing/_WhoControlsSquareFast use // locally (BITV-typed variables, not struct fields) to build a // per-square "which piece types attack/defend/x-ray this square" // pattern from those same bbXAttacks reads, then index // KING_COUNTER_BY_ATTACK_PATTERN/g_SwapTable with it. Kept exactly as // before; only their old *storage* mechanism (ATTACK_BITV) is gone. // Bits 1-4 (2026-09-06: were 4-7, a leftover from a retired count field // that used to live in bits 0-2 -- closing that gap removes the // bvPattern >>= 3 that used to be needed before the // KING_COUNTER_BY_ATTACK_PATTERN lookup, eval.c). #define PAWN_BIT 0x00000010UL #define MINOR_BIT 0x00000008UL #define ROOK_BIT 0x00000004UL #define QUEEN_BIT 0x00000002UL // King never x-rays (it can't move through a blocker), so there's no // KING_XRAY_BIT to go with this. #define KING_BIT 0x00000001UL #define INVALID_PIECE_INDEX (17) #define IS_VALID_PIECE_INDEX(x) ((x) < INVALID_PIECE_INDEX) typedef struct _SQUARE { PIECE pPiece; ULONG uIndex; } SQUARE; // // POSITION // typedef struct _POSITION { SQUARE rgSquare[128]; // where the pieces are // (0x88 board layout -- the // upper half of each rank's // 16-entry span is off-board // sentinel space, no longer // double-purposed as attack- // bit storage now that // ATTACK_BITV is retired) UINT64 u64NonPawnSig; // hash signature UINT64 u64PawnSig; // pawn hash signature ULONG uToMove; // whose turn? ULONG uFifty; // 50 moves w/o progress = draw FLAG fCastled[2]; // record when sides have castled BITV bvCastleInfo; // who can castle how? COOR cEpSquare; // en-passant capture square // Pawn material of each side -- every pawn is worth the same, so // this (like uPawnCount/uNonPawnCount before it) is just // FastCountBits(bbPawns[color]) * VALUE_PAWN (see PawnMaterial() // below), not a stored field. ULONG uNonPawnMaterial[2]; // piece material of each side // (counts: FastCountBits( // bbPieces[2][KNIGHT/BISHOP/ // ROOK/QUEEN]) per type; // FastCountBits( // NON_KING_PIECES_BB(pos, // color)) + 1 for the total; // always exactly 1 for KING, // no backing bitboard exists) // King location, per color -- a plain scalar read/write. Used to // carry the location the old cNonPawns[color][0] convention slot // held (2026-09-16 board_representation/MIGRATION.md-style // retirement of the cNonPawns[2][16] piece-location list: every // reader outside move.c's own list-maintenance code turned out to // be either this fixed king slot or a full-population walk, both // servable directly by bbPieces[2][8] below via // NON_KING_PIECES_BB/CoorFromBitBoardRank8ToRank1 -- see git log // for the migration). NOT redundant with bbPieces[color][KING]: // that slot is deliberately never populated (fen.c's IS_KING // branch skips the bbPieces OR, board.c's // VerifyPositionConsistency rebuild loop guards `v < KING` / // `!IS_KING(p)` to match) -- and even if it were, every read would // cost a CTZ + COOR conversion versus this field's one load. // Maintained by SlidePiece (move.c) and fen.c's/testsup.c's setup // code. COOR cKingSq[2]; // Per-color, per-piece-type location bitboards -- "where are my // knights/bishops/rooks/queens" -- indexed by PIECE_TYPE exactly // like uNonPawnCount above (slots 0/1/PAWN/KING unused, same // convention). Maintained incrementally in move.c at every site // that already updates uNonPawnCount[] (see // board_representation/MIGRATION.md), not rebuilt -- O(1) per // move. Pawns use bbPawns[2] below instead; king uses cKingSq[2] // above, not this array (see cKingSq's own comment for why). BITBOARD bbPieces[2][8]; // Per-color pawn location bitboard -- same incremental-maintenance // idea as bbPieces above, but for pawns (which bbPieces // deliberately excludes). Exists so full-board occupancy // (pos->bbOccupied below) can be built via two ORs instead of looping // cPawns[2][8] (up to 16 iterations) on every call -- see // board_representation/MIGRATION.md section 3. Also the single // source of truth for pawn locations used by pawn eval // (board_representation/EVAL.md section 0c) -- PAWN_HASH_ENTRY // used to carry its own redundant bbPawnLocations[2], rebuilt // bit-by-bit on every pawn-hash miss even though this field // already had the answer; removed 2026-09-05, eval.c now reads // this field directly instead. BITBOARD bbPawns[2]; // Full-board occupancy (both colors, every piece including pawns // and kings) -- same incremental-maintenance idea as bbPieces/ // bbPawns above, added 2026-09-05 per board_representation/ // EVAL.md section 0 (was previously only on MOVE_STACK, rebuilt // on demand via _BuildFullOccupiedBB at many independent call // sites in generate.c/movesup.c/see.c, and needed again by the // planned eval.c mobility rewrite -- one incrementally-maintained // copy here replaces all of that). Maintained by move.c's // SlidePiece/SlidePawn/LiftPiece/PlacePiece (and their // WithoutSigs variants) at the same sites that already maintain // bbPieces/bbPawns, unconditionally (no per-color/per-type branch // needed, unlike those). _BuildFullOccupiedBB(pos) (generate.c) // remains as the from-scratch ground truth used to verify this // field stays in sync (see VerifyPositionConsistency, board.c), // not as something callers should call directly anymore. BITBOARD bbOccupied; // Per-side occupancy, same incremental-maintenance treatment as // bbOccupied above -- _BuildFriendlySideBB (generate.c) used to // rebuild this from scratch (a 6-term OR of bbPieces+bbPawns+king) // on every call; read far more often per node than a position // changes per move, so incremental beats on-the-fly OR-ing here // for the same reason it already does for bbOccupied. BITBOARD bbOccupiedSide[2]; // Eval()-scoped attack-bitboard accumulators, one pair per piece // family, populated by Eval()'s piece-by-piece walk (pawns, then // knights/bishops, then rooks, then queens, then king) and read by // _EvalKing/_WhoControlsSquareFast to answer "which side attacks // this square, and with what." Landed piece-by-piece as // board_representation/EVAL.md's bitboard-eval migration replaced // the old per-square, per-color rgSquare[c|8] ATTACK_BITV // mechanism (2026-09-04 through 2026-09-05, pawns first, king // last) -- see EVAL.md for that history. All are cleared once per // Eval() call in _ClearAttackTables and accumulated via |= as each // piece evaluates (not incrementally maintained across moves, the // way bbPieces/bbPawns/bbOccupied above are). // // bbPawnAttacks is the odd one out: computed in one shot from // bbPawns via a shift-and-mask (_PopulatePawnAttackBits, same // technique as generate.c's _GenerateAllPawnMovesBB), not // per-piece, and assigned wholesale rather than OR-accumulated. // // Only bishop/rook/queen have a matching bbXXrayAttacks: squares // seen *through* a friendly same-or-greater-value battery partner // (and, for rook/bishop specifically, through an x-rayable enemy // piece too -- queen's own case never x-rays past an enemy piece). // Knight and king can't x-ray at all (no blocker-piercing move). // Bishop's x-ray chains arbitrarily deep, same as rook's/queen's, // not the single-hop approximation it originally shipped with (see // EVAL.md's progress log for why that was revisited). // // REQUIRES SIG MATCH before any reader outside eval.c trusts these: // compare ctx->sPlyInfo[ctx->uPly].u64FullEvalSig (PLY_INFO, chess.h) // against this position's own live (u64NonPawnSig ^ u64PawnSig). // On a lazy/super-lazy Eval() exit these are left completely // unpopulated -- not merely stale, genuinely never cleared or // written that call (see Eval()'s own pawn-eval-deferred comment) -- // and even on a full eval they describe whatever position is // current in ctx->sPosition at the moment Eval() returns, which // stops being true the instant the next MakeMove runs. Currently // only read from inside the same Eval() call that wrote them // (_EvalKing, _WhoControlsSquareFast); any future reader elsewhere // (search.c, see.c, generate.c) must sig-check first. BITBOARD bbPawnAttacks[2]; BITBOARD bbMinorAttacks[2]; BITBOARD bbMinorXrayAttacks[2]; BITBOARD bbRookAttacks[2]; BITBOARD bbRookXrayAttacks[2]; BITBOARD bbQueenAttacks[2]; BITBOARD bbQueenXrayAttacks[2]; // bbKingAttacks reproduces, for free, an existing order-dependent // asymmetry _EvalKing already had before this bitboard conversion: // kings are evaluated black-then-white (Eval()'s fixed order), so // by the time white's king-safety computation runs it can see // black's already-written attacks but black's own computation // never could (white hadn't run yet). _EvalKing's own bvAttack // deliberately keeps this asymmetric (drops the enemy king's // contribution entirely, matching the side that already couldn't // see it, by direct instruction) -- but every other bbKingAttacks // reader (_WhoControlsSquareFast, only ever called after *both* // kings finish evaluating) sees a fully populated, symmetric // bitboard for both colors. BITBOARD bbKingAttacks[2]; SCORE iMaterialBalance[2]; // material balance // temporary storage space for use in eval // // Small fixed-capacity list of zero-mobility (or, for a deferred // bishop/knight/rook -- see EVAL_MOBILITY_CAREFULLY_BELOW in // eval.c -- low-mobility) piece candidates per color, not just the // single most-recently-evaluated one -- a single COOR here used to // silently let a later piece's candidacy overwrite an earlier one // on the same side, discarding it even if it was a genuinely // trapped (zero mobility + actually attacked) piece. Capacity of 6 // is deliberately generous; if a position somehow has more than 6 // simultaneously trapped-candidate non-pawn pieces on one side, the // rest are just dropped, not a real concern. // // bbTrappedEscapes[side][u] is the nominating bishop/knight/rook's // own bbMobility snapshot at nomination time, for a genuinely // deferred candidate (see eval.c's _EvalBishop/_EvalKnight/ // _EvalRook) -- necessarily computed from incomplete attack-table // information (the enemy king's, and for bishop/knight any // not-yet-evaluated enemy slider's, attacks aren't known yet). Zero // for every non-deferred candidate (queen, which never has this // partial-information problem in the first place, or a // bishop/knight/rook nominated at already-zero mobility -- nothing // left to re-derive there, see eval.c's comment on why 0-mobility // pieces are never deferred) -- _EvalTrappedPieces uses this being // nonzero as the signal that a deferred re-check (and its own // "mobility (deferred)" EVAL_TERM, skipped at nomination time) is // owed to this candidate. COOR cTrapped[2][6]; BITBOARD bbTrappedEscapes[2][6]; ULONG uNumTrapped[2]; ULONG uArmyScaler[2]; SCORE iScore[2]; SCORE iReducedMaterialDownScaler[2]; ULONG uMinorsAtHome[2]; } POSITION; // // Castling permission bitvector flags. // #define CASTLE_WHITE_SHORT (1) #define CASTLE_WHITE_LONG (2) #define WHITE_CAN_CASTLE \ (CASTLE_WHITE_SHORT | CASTLE_WHITE_LONG) #define CASTLE_BLACK_SHORT (4) #define CASTLE_BLACK_LONG (8) #define BLACK_CAN_CASTLE \ (CASTLE_BLACK_SHORT | CASTLE_BLACK_LONG) #define CASTLE_NONE_POSSIBLE (0) #define CASTLE_ALL_POSSIBLE \ (WHITE_CAN_CASTLE | BLACK_CAN_CASTLE) // ---------------------------------------------------------------------- // // Move stack // #define MAX_MOVE_STACK (MAX_PLY_PER_SEARCH * \ MAX_MOVES_PER_PLY) // // Main part of the move stack, triads of moves, their values and flags // that denote where the values came from // #define MVF_MOVE_SEARCHED (1) #define MVF_EXTEND_MOVE (2) #define MVF_REDUCE_MOVE (4) #define MVF_PRUNE_SUBTREE (8) #define MVF_SEE_KNOWN (16) // SEE(pos, mv) was already computed #define MVF_SEE_NONNEGATIVE (32) // ...and its sign was >= 0 typedef struct _MOVE_STACK_MOVE_VALUE_FLAGS { SCORE iValue; MOVE mv; BITV bvFlags; } MOVE_STACK_MOVE_VALUE_FLAGS; typedef struct _KEY_POINTER { int iPointer; ULONG uKey; } KEY_POINTER; typedef union _GENERATOR_FLAGS { struct { UCHAR uMoveCount; UCHAR uKingMoveCount; UCHAR uCheckingPieces; // Snapshot of MOVE_COUNT taken right after _GenerateEscapes // finishes populating the move list for a check-evasion ply, // before _ScoreAllEscapes swaps the hash move to the end and // shrinks uEnd[ply] to keep the search from re-trying it. The // live uEnd[ply]-uBegin[ply] count that trick leaves behind is // "untried moves remaining," not "legal replies to check" -- // using it for ONE_LEGAL_MOVE meant a position with two legal // replies, one of which happened to be the hash move, read as // having only one. This field is captured before that shrink // so it always reflects the true legal-escape count. UCHAR uTrueEscapeCount; }; ULONG uAllGenFlags; } GENERATOR_FLAGS; // // A move stack // typedef struct _MOVE_STACK { #ifdef DEBUG POSITION board[MAX_PLY_PER_SEARCH]; #endif // // Unblocked squares map, used for check detection // 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 // flag bits to tell search what the values are based upon. // MOVE_STACK_MOVE_VALUE_FLAGS mvf[MAX_MOVE_STACK]; // // uBegin[ply] and uEnd[ply] specify the start and end of moves gen- // erated for a position at ply distance from the root. // ULONG uPly; ULONG uBegin[MAX_PLY_PER_SEARCH]; ULONG uEnd[MAX_PLY_PER_SEARCH]; MOVE mvHash[MAX_PLY_PER_SEARCH]; GENERATOR_FLAGS sGenFlags[MAX_PLY_PER_SEARCH]; // Scratch, computed once per _GenerateAllMoves call (not per // generator-function call) when any GENERATE_*_BITBOARD toggle is // active -- every bitboard-backed piece-type generator for the // side to move needs "which squares does pos->uToMove already // occupy" (to AND off as illegal destinations), and rebuilding it // from pos->bbPieces/bbPawns freshly inside each generator call // was measured as real, avoidable overhead (board_representation/ // MOVEGEN_MIGRATION.md section 5's knight benchmark showed a net // slowdown vs. mailbox until this was hoisted out). Direct callers // of a _Generate*BB function outside of _GenerateAllMoves (e.g. // testgenerate.c's benchmark/correctness harness) must set this // themselves first -- it is not implicitly valid. BITBOARD bbFriendlyOccupied; // Same idea as bbFriendlyOccupied, for the slider (rook/bishop/ // queen) magic-bitboard generators: full-board occupancy, both // sides, needed to index into g_RookAttackTable/g_BishopAttackTable // (the magic lookup answers "attacks given this exact occupancy," // not "attacks given only my own pieces"). Computed once per node // by _GenerateAllMoves when any GENERATE_ROOK_BITBOARD/ // GENERATE_BISHOP_BITBOARD toggle is active, same amortization // reasoning as bbFriendlyOccupied. BITBOARD bbOccupied; } MOVE_STACK; #define GENERATE_NO_MOVES \ ctx->sMoveStack.uEnd[ctx->uPly] = \ ctx->sMoveStack.uBegin[ctx->uPly] + 1; \ ctx->sMoveStack.uBegin[ctx->uPly + 1] = \ ctx->sMoveStack.uEnd[ctx->uPly] #define GENERATE_ALL_MOVES (1) #define GENERATE_ESCAPES (2) #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 #define MOVE_COUNT(ctx, x) \ (((ctx)->sMoveStack.uEnd[(x)]) - (ctx)->sMoveStack.uBegin[(x)]) #define ONE_LEGAL_MOVE(ctx, x) \ ((ctx)->sMoveStack.sGenFlags[(x)].uTrueEscapeCount == 1) #define NUM_KING_MOVES(ctx, x) \ ((ctx)->sMoveStack.sGenFlags[(x)].uKingMoveCount) #define NUM_CHECKING_PIECES(ctx, x) \ ((ctx)->sMoveStack.sGenFlags[(x)].uCheckingPieces) // EXPERIMENT: see the fuller comment near COUNTERMOVE_TIER_BONUS below. // Needs to be visible before COUNTERS/GAME_OPTIONS, which use it to // size the evidence-bucket counter arrays. #define CM_EVIDENCE_BUCKETS (7) // 2026-09-28: EstimateHangingMaterialCp calibration (search.c) -- does // "how much material does this move look like it hangs" actually // predict FH%? Bucket 0 == not dumb at all; buckets 1..9 are 100cp- // wide bins covering 1-900cp; bucket 10 is the 901cp+ overflow (a // hung queen tops out around 975). Deliberately even-width, NOT // piece-value-shaped -- the previous version of this signal bucketed // by which specific piece-pair produced the cp value (e.g. "pawn // takes rook" and "rook attacks queen" both landing in one bucket // purely because 400 and 475 are both <500), silently pooling // unrelated motifs. Even-width bins can't make that mistake; if two // different motifs really do carry different risk, they'll only end // up in the same bin here when their real cp magnitudes are actually // close together. #define HANG_CP_BUCKET_WIDTH (100) #define HANG_CP_BUCKETS (11) // 2026-09-28: the "down axis" -- does (iAlpha - iEval), the node's // static-eval deficit below alpha, predict FH%? Same even-width // bucket shape as HANG_CP_BUCKETS, same reasoning. Bucket 0 == gap // <= 0 (this node's own static eval already meets or beats alpha -- // a fundamentally different, and presumably much safer, regime from // "we're statically behind and need this leftover move to catch up", // so it gets its own bucket exactly like HANG's "not dumb" does). // Buckets 1..9 are 100cp-wide bins covering a positive gap of // 1-900cp; bucket 10 is the 901cp+ overflow. #define GAP_CP_BUCKET_WIDTH (100) #define GAP_CP_BUCKETS (11) // 2026-10-10: "improving" axis -- does (iEval - iEval two plies ago, // same side to move) predict FH%? Signed and genuinely two-sided, so // a symmetric 100cp-wide-bin shape around a center "no real change" // bucket (index 5), with overflow buckets at both tails, matching // HANG/GAP's own 11-bucket count. Added as g_uJointFHThousandths's // 5th (trailing) axis -- see data.c. #define IMPROVE_BUCKET_WIDTH (100) #define IMPROVE_BUCKETS (11) #define IMPROVE_CP_CENTER_WIDTH (100) // center bucket covers (-100, 100) // 2026-10-10: gap-above-beta, for the beta-side static-NM table -- // gated on iEval > iBeta, so always >= 0, no symmetric/center bucket // needed. Plain 100cp-wide bins from 0, wide enough (2000cp) to find // where FH% actually saturates toward ~100%, with an overflow bucket // past that. #define GAP_BETA_BUCKET_WIDTH (100) #define GAP_BETA_BUCKETS (21) // 0-99 .. 1900-1999, 20 == 2000cp+ overflow // 2026-09-28: the "late axis" -- this move's own 1-based ordinal // position in the list of moves actually tried at this node // (uLegalMoves, right after MakeMove succeeds). Direct, unpaired // buckets -- move number 1..(MOVE_NUM_BUCKETS-2) each get their own // exact bucket, MOVE_NUM_BUCKETS-1 is the "at least that many" // overflow. No pairing/fraction-of-list-exhausted cleverness here; // the volume from a real collection run supports exact per-move- // number resolution directly. Index 0 is unused (move numbers start // at 1) -- wastes one slot, keeps uLegalMoves usable as the array // index with no off-by-one arithmetic anywhere it's consumed. #define MOVE_NUM_BUCKETS (33) // 2026-09-28: the "depth axis" -- remaining search depth (uDepth / // ONE_PLY, whole plies) at THIS node, before this move's own // extension/reduction is applied -- same convention as the old EFP // code's uCandDepth. Direct, unpaired buckets like MOVE_NUM_BUCKETS; // unlike move number, depth legitimately starts at 0 (a heavily- // reduced move can arrive at remaining depth 0), so index 0 IS used // here, not wasted. #define SEARCH_DEPTH_BUCKETS (25) // 2026-09-28: how many extra whole plies the graduated LMR mechanism // stacked on top of GetLMRReduction's own baseline -ONE_PLY. Must stay // in lockstep with searchsup.c's JOINT_EXTRA_MAX_PLIES (private to that // file, so not literally shared via a #define): this must be // JOINT_EXTRA_MAX_PLIES + 1, the +1 being the "tier 0 == no extra" slot. // // 2026-10-03: was (4) while JOINT_EXTRA_MAX_PLIES had already been // raised 3 -> 5 (searchsup.c, the section 8 "Attempt B" payout bump). // The lockstep this comment demands was silently broken, so // GetLMRReduction's OUT *puExtraTier could hand back 4 or 5 and both // consumers -- search.c's INC(u64JointExtraTries[uExtraTier]) and // INC(u64JointExtraResearch[uThisMoveJointExtraTier]) -- indexed these // 4-element arrays out of bounds, landing on u64JointExtraResearch[0] // and on u64LMPPruned respectively. Counters only (nothing reads them // to make a search decision, and root.c's memcpys are sizeof-bounded), // but it corrupted tier 0's re-search rate -- the calibration signal // those counters exist to provide -- and could make u64LMPPruned read // nonzero while LMP is disabled at its call site. Invisible to every // sd10 suite run (tier 4 needs remaining depth >= ~14, unreachable // there) and only live in searches deep enough to reach it, i.e. real // self-play. Raised to 6 rather than clamping MAX_PLIES back to 3, // which would have been a search-behavior change rather than a fix. #define JOINT_EXTRA_TIER_COUNT (6) // Null-move R-factor breakdown (post-move dump, root.c) -- bucketed by // SelectNullmoveRFactor's whole-ply R value (searchsup.c). R is // currently TWO_PLY (sparse material) or THREE_PLY + depth/6 (Crafty- // style growth), so real traffic never gets near this bound; sized // with headroom and clamped at the top bucket rather than tied exactly // to a formula that might grow later. #define NULL_R_BUCKETS (12) // 2026-09-28: JOINT_EXTRA_VERIFY silent-miss calibration, bucketed by // how much remaining depth the FULLY-reduced (base + extra) search // actually got, in whole plies: 0/1/2/3/4+. Tests the "reducing an // already-shallow search into a stunted one is what's actually driving // the silent-miss rate, not the axis combination itself" hypothesis -- // see search.c's JOINT_EXTRA_MIN_REMAINING_DEPTH comment. #define POST_REDUCE_DEPTH_BUCKETS (5) // TEST_NULL calibration harness (search.c's null-move precision/recall/ // EV measurement -- see _NullCalibBucketIndex's comment in search.c for // the full design). Revised 2026-09-24: dropped fThreat/fSkipNull/ // en-prise-or-trapped as bucket dimensions -- all three are already // validated, non-tunable binary production gates (fThreat/fSkipNull // gate the call to WeShouldTryNullmovePruning in search.c; en-prise/ // trapped was shown NOT to predict false-positive risk, only recall), // so the calibration probe now just requires fThreat==FALSE && // fSkipNull==FALSE as part of structural eligibility instead of // spending bucket space re-measuring them. Added a second depth // dimension (outer, pre-reduction uDepth, alongside the null probe's // own post-reduction uNullDepth) and a second material dimension // (mover-only piece count -- TotalNonPawnCount(pos, pos->uToMove) -- // alongside the existing MAX(white,black) count) -- both deliberately // independent of the current SelectNullmoveRFactor R schedule, so a // cost model fit from this data stays valid if R changes. // // Per-ply, 0..15 directly plus one "16+" overflow bucket -- fine enough // to fit a cost-vs-depth growth curve (EBF-style) and extrapolate to // depths never directly observed at the sd used for a given run. Used // for both depth dimensions (same banding, reused). #define NC_DEPTH_BUCKETS (17) #define NC_DEFICIT_BUCKETS (6) // (iEval-iBeta) cp bands // Non-pawn piece count bands: 0-1,2,3-5,6+. Used for both material // dimensions (same banding, reused). #define NC_MATERIAL_BUCKETS (4) #define NC_TOTAL_BUCKETS \ (NC_DEPTH_BUCKETS * NC_DEPTH_BUCKETS * NC_DEFICIT_BUCKETS * \ NC_MATERIAL_BUCKETS * NC_MATERIAL_BUCKETS) // ---------------------------------------------------------------------- // // Accumulators // typedef struct _COUNTERS { struct { UINT64 u64Probes; UINT64 u64OverallHits; UINT64 u64UsefulHits; UINT64 u64UpperBoundHits; UINT64 u64LowerBoundHits; UINT64 u64ExactScoreHits; } hash; struct { UINT64 u64Probes; UINT64 u64Hits; } pawnhash; // EXPERIMENTAL, prototype-only -- see rgKingSafetyHash and // _GetFileStormDefects (eval.c). Cycle counters let this be // measured with a plain PERF_COUNTERS build, no separate // EVAL_TIME rebuild needed. struct { UINT64 u64Probes; UINT64 u64Hits; UINT64 u64CyclesHit; UINT64 u64CyclesMiss; } 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; UINT64 u64QNodeCount; // CommonSearchInit's "too deep" bailout (searchsup.c), split by // which of its two triggers fired -- the MAX_PLY_PER_SEARCH-1 // array-safety ceiling (should be a rare, pathological corner // case) vs. g_uHardExtendLimit (a real, tunable limit that a // tighter extension taper makes reachable far more often, in // exactly the long forced-check-chain positions the 2026-09-10/11 // extension-taper/qsearch-prune work is about). UINT64 u64TooDeepPlyLimit; UINT64 u64TooDeepQsearchExtendLimit; UINT64 u64LeafCount; UINT64 u64TerminalPositionCount; UINT64 u64BetaCutoffs; UINT64 u64BetaCutoffsOnFirstMove; UINT64 u64CounterMoveTries; // valid counter-move slot existed UINT64 u64CounterMoveHits; // ...and it was the move that won UINT64 u64LeftoverTries; // leftover (sub-GOOD_MOVE) move tried UINT64 u64LeftoverAlpha; // ...raised alpha (didn't fail high) UINT64 u64LeftoverFH; // ...failed high UINT64 u64CMEvidenceTries[CM_EVIDENCE_BUCKETS]; UINT64 u64CMEvidenceFH[CM_EVIDENCE_BUCKETS]; UINT64 u64HangEstimateTries[HANG_CP_BUCKETS]; UINT64 u64HangEstimateFH[HANG_CP_BUCKETS]; // 2026-09-28: same population as u64LeftoverTries/FH above, // but further restricted by fPruneReduceEligible (search.c) -- // the real comparison baseline for the hang-estimate buckets, // which are gated the same way. u64LeftoverTries/FH themselves // stay broad (fIsLeftoverMove only) since they're used // elsewhere with that existing meaning. UINT64 u64EligibleLeftoverTries; UINT64 u64EligibleLeftoverFH; UINT64 u64GapEstimateTries[GAP_CP_BUCKETS]; UINT64 u64GapEstimateFH[GAP_CP_BUCKETS]; UINT64 u64MoveNumEstimateTries[MOVE_NUM_BUCKETS]; UINT64 u64MoveNumEstimateFH[MOVE_NUM_BUCKETS]; UINT64 u64DepthEstimateTries[SEARCH_DEPTH_BUCKETS]; UINT64 u64DepthEstimateFH[SEARCH_DEPTH_BUCKETS]; // 2026-09-28: joint 4D table, same fPruneReduceEligible population, // marginal (uncoarsened) resolution -- coarsened to the shipped // [12][11][11][15] shape at bake time, not at collection time, so // a different final bucket shape never requires re-collecting. UINT64 u64JointTries[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS]; UINT64 u64JointFH[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS]; // 2026-09-28: phase 2 -- joint-table-driven graduated LMR // (search.c). Index 0 == GetLMRReduction's baseline only (no // extra plies added), 1..3 == that many extra plies the table // added. Tries = how many moves were assigned each tier; // Research = how many of those triggered the existing // re-search-on-fail-high path (search.c:~1739) -- the // calibration signal for whether the table's ordering survives // actual deployment, not just offline collection. UINT64 u64JointExtraTries[JOINT_EXTRA_TIER_COUNT]; UINT64 u64JointExtraResearch[JOINT_EXTRA_TIER_COUNT]; // 2026-10-02: late-move pruning (ShouldLatePrune, searchsup.c) // -- overt skip, not reduction, for the safest tail of the // leftover pool near the leaves. Counts every move this was // applied to. UINT64 u64LMPPruned; // 2026-10-05: null-move R-factor breakdown -- which whole-ply R // SelectNullmoveRFactor chose, and how often the null-move // search at that R actually failed high. Indexed by R/ONE_PLY, // clamped to NULL_R_BUCKETS - 1. UINT64 u64NullMoveTriesByR[NULL_R_BUCKETS]; UINT64 u64NullMoveSuccessByR[NULL_R_BUCKETS]; #ifdef JOINT_EXTRA_VERIFY // Full-coverage shadow-search "silent miss" calibration -- see // GNUmakefile's JOINT_EXTRA_VERIFY comment. Tries = extra- // reduced moves whose real score was < beta (no real re-search // fired) and so got a throwaway shadow search at the // extra-plies-removed depth; SilentMiss = how many of those // shadow searches found >= beta anyway -- a fail-high the // extra reduction actually caused us to miss, undetected by // any mechanism a normal (non-JOINT_EXTRA_VERIFY) build has. UINT64 u64JointExtraVerifyTries[JOINT_EXTRA_TIER_COUNT][POST_REDUCE_DEPTH_BUCKETS]; UINT64 u64JointExtraVerifySilentMiss[JOINT_EXTRA_TIER_COUNT][POST_REDUCE_DEPTH_BUCKETS]; // 2026-09-29: of the confirmed silent misses above, how many // have iShadowScore == iBeta + 1 EXACTLY? That's the // deterministic signature of searchsup.c's null-move // "history-reduced bailout" branch (Tord Romstad idea) firing // at the top of our shadow call -- it fabricates // *piNullScore = iAlpha-1 and returns TRUE instead of actually // re-searching, which negates up to exactly iBeta+1 in our // frame. In REAL (non-shadow) search this correctly triggers // search.c's own re-search-on-fail-high machinery one level // up; our bespoke one-off shadow Search() call has no such // wrapper, so we misread the fabricated "please re-search me" // signal as a genuinely-computed fail-high. Not a perfectly // airtight test (a real score could coincidentally land on // beta+1), but this mechanism produces that exact value every // time it fires, so false positives should be rare. UINT64 u64JointExtraVerifyNullBailoutSignature[JOINT_EXTRA_TIER_COUNT]; #endif UINT64 u64NullMoves; UINT64 u64NullMoveSuccess; // 2026-10-10: static (score-only, no verification search) // null-move cutoff, gated on iEval > iBeta and the beta-side // FH% table (g_uBetaFHThousandths) predicting a cutoff with // >95% confidence -- see search.c's static-NM block. UINT64 u64QuickNullMoves; UINT64 u64QuickNullMoveSuccess; // 2026-10-13: opposite-direction (iEval <= iAlpha) check -- // reuses the alpha/leftover-move table (g_uJointFHThousandths) // as a proxy for "would a free tempo even reach beta here," to // decide whether trying the real null-move probe is worth its // own cost. EV-neutral: the real move loop always still runs // and finds the correct score regardless -- this only skips // the probe attempt on the (hopefully rare) occasion it would // have succeeded. No companion "success" counter for the same // reason u64NullMoveSkippedLowFH never had one before: skipping // is never wrong in the score sense, only in the opportunity- // cost sense, which isn't separately observable here. UINT64 u64NullMoveSkippedLowFH; #ifdef TEST_NULL // Precision/recall calibration for the null-move try/avoid // decision. Indexed by _NullCalibBucketIndex() (search.c). // Per cell: how often we attempted a (diagnostic, always-run) // null-move probe, how often the probe itself failed high, how // often the real move loop at that node *actually* failed high // (ground truth, read off Search()'s own iBestScore vs iBeta at // its "end:" label), and how often both agreed. From these four // counts: precision = uBothFH/uNullFH, recall = uBothFH/uRealFH. UINT64 u64NCAttempts[NC_TOTAL_BUCKETS]; UINT64 u64NCNullFH[NC_TOTAL_BUCKETS]; UINT64 u64NCRealFH[NC_TOTAL_BUCKETS]; UINT64 u64NCBothFH[NC_TOTAL_BUCKETS]; // Cost side of the try/skip decision: sum (not average -- divide // by u64NCAttempts at report time) of the node cost of the // diagnostic null probe itself, and of the immediately-following // real first move's full-depth search (the "R=0" alternative), // per bucket. Lets a report compute avg-nodes-under-null vs. // avg-nodes-under-first-move and the try-vs-skip break-even // this implies, given the bucket's own nullFH rate. UINT64 u64NCNullProbeNodeSum[NC_TOTAL_BUCKETS]; UINT64 u64NCFirstMoveNodeSum[NC_TOTAL_BUCKETS]; // True total cost of however the real search actually resolved // this node (one move, several moves, or production's own // separate null-move success) -- node count from right after // the diagnostic probe returns through this node's "end:". // Strictly more honest than u64NCFirstMoveNodeSum for the // try-vs-skip EV calc, which only approximates "cost of the // real alternative" as move 1 alone. UINT64 u64NCRealLoopNodeSum[NC_TOTAL_BUCKETS]; #endif UINT64 u64LazyEvals; UINT64 u64SuperLazyEvals; UINT64 u64FullEvals; UINT64 u64CyclesInEval; // // Per-eval-term cycle breakdown (EVAL_TIME only) -- board_ // representation/EVAL.md section 0b's "profile before // rewriting" step. Sum of these plus "everything else" // (u64CyclesInEval minus this sum, computed at print time) // equals u64CyclesInEval; kept as separate counters rather // than an array so each term's call sites in eval.c can name // its own accumulator directly. // // 2026-09-09: split out of the old "pre-lazy other" bucket -- // u64CyclesEvalMaterialSetup (material balance + army-scaler // calc) is paid unconditionally by *every* Eval() call // (super-lazy, lazy, and full alike), while // u64CyclesEvalPasserBadtradeBishop (passer races vs. a bare // king, bad-trade detection, bishop pairs) is gated/rare on // the first two and only unconditional (bishop pairs) inside // calls that get this far. Bundling them together made the // combined bucket's percentage read as "this work is // expensive per call" when it was really "this bucket is // paid by every call, not just full ones." UINT64 u64CyclesEvalMaterialSetup; UINT64 u64CyclesEvalPasserBadtradeBishop; UINT64 u64CyclesEvalPawns; // // Split of the counter above by pawn-hash outcome (board_ // representation/EVAL.md section 0c/0d's "is the hash still // worth it now that attack-bit population is cheap" question) // -- Hit = _EvalPawns returned early via the hash (fDeferred // TRUE); Miss = full recompute, including // _PopulatePawnAttackBits and the isolated/doubled/duo/ // backward-pawn scoring loops (fDeferred FALSE). Hit+Miss // calls sum to u64FullEvals-ish call count, not to // u64CyclesEvalPawns exactly (that's still measured as one // contiguous window; these are the same window, just bucketed // by outcome after the fact). // UINT64 u64CyclesEvalPawnsHit; UINT64 u64CyclesEvalPawnsMiss; UINT64 u64CountEvalPawnsHit; UINT64 u64CountEvalPawnsMiss; UINT64 u64CyclesEvalKnight; UINT64 u64CyclesEvalBishop; UINT64 u64CyclesEvalRook; UINT64 u64CyclesEvalQueen; UINT64 u64CyclesEvalKing; // // u64CyclesEvalPreLazy covers the "always paid, even on a // lazy exit" segment -- material setup through the lazy- // margin decision (passer races, bad trades, bishop pairs, // EstimatePositionalScore/CountKingSafetyDefects) -- and // *includes* u64CyclesEvalPawns as a subset (pawns is nested // inside this segment, not sequential with it); print sites // must subtract pawns back out rather than summing both. // u64CyclesEvalPostLazyMisc covers what's left of the "only // paid on a full eval" segment once the named piece-type // buckets above are excluded: _EvalPassers, _EvalLookForDanger, // _EvalTrappedPieces, the B-over-N/reduced-material endgame // scalers. // UINT64 u64CyclesEvalPreLazy; UINT64 u64CyclesEvalPostLazyMisc; UINT64 u64CyclesEvalAttackTablePop; // // 2026-09-06 (board_representation/EVAL.md section 9): finer // breakdown of what's inside u64CyclesEvalPreLazy, to find out // exactly what got more expensive there this session. // u64CyclesEvalLazyDecision times the whole material-gate + // margin-check + EstimatePositionalScore block; the other two // further split EstimatePositionalScore's own two calibrated // sub-calls (each already summed over both colors). Everything // else in "pre-lazy" (material/passers/bad-trades/bishop-pairs) // is u64CyclesEvalPreLazy minus u64CyclesEvalLazyDecision. // UINT64 u64CyclesEvalLazyDecision; UINT64 u64CyclesEvalCountKingSafetyDefects; UINT64 u64CyclesEvalFileStormDefects; // // 2026-09-07: the super-lazy exit (eval.c, before the regular // LAZY_EVAL block) is a much cheaper material-only check than // the regular lazy decision, so it gets its own cycle counter // rather than folding into u64CyclesEvalLazyDecision -- a hit // here never reaches the regular lazy-decision code at all. // UINT64 u64CyclesEvalSuperLazy; // 2026-09-09: EvalLookForDrawishSituations runs after // u64CyclesEvalPostLazyMisc's window closes -- previously fell // into "unaccounted" in root.c's eval breakdown report. // 2026-09-17: no longer full-eval-only -- also called from the // regular-lazy pre-exit check (eval.c), so this counter now // accumulates both call sites' cost. The two calls' real // per-position-only work (DRAWISH_FACTS) only happens once // per Eval() call regardless -- see _EnsureDrawishFactsComputed. UINT64 u64CyclesEvalDrawish; // Per-exit-path *total call cost*, i.e. the full entry-to-exit // elapsed time for a call that left via that path -- not to be // confused with the sub-timers above, which measure a single // segment's cost regardless of how the call eventually exited. // These three are mutually exclusive by construction (each // Eval() call adds to exactly one) and sum to u64CyclesInEval, // so u64CyclesInEval / (u64SuperLazyEvals + u64LazyEvals + // u64FullEvals) is a real overall average, and each bucket // divided by its own matching count (u64SuperLazyEvals, // u64LazyEvals, u64FullEvals) gives a clean per-path average -- // see root.c's eval-exit-breakdown report. UINT64 u64CyclesSuperLazyExit; UINT64 u64CyclesLazyExit; UINT64 u64CyclesFullEvalExit; // 2026-10-20: direct per-call latency of the two NNUE eval // primitives (nnueeval.c), as opposed to the HCE-oriented // whole-path-cost counters above (which NNUE's Eval() branch // doesn't feed -- it's gated on g_fUseNnueEval at runtime, not // #ifdef LAZY_EVAL). u64CyclesNnueProbeCall/u64CyclesNnueForwardCall // time just the TIMED_EVAL_CALL-wrapped NnueProbeScore()/ // NnueForward() calls themselves (eval.c's production NNUE // branch); u64NnueProbeAttempts counts every probe call made // (hit or miss) -- unlike u64LazyEvals, which only counts // hits -- needed as the denominator for a true avg-cost-per- // attempt (the question that actually matters for judging // whether the gate pays for itself: cost is paid on every // attempt, the saving only on a hit). u64FullEvals already // counts NnueForward calls 1:1 on this path, so no separate // attempt counter is needed for it. UINT64 u64CyclesNnueProbeCall; UINT64 u64CyclesNnueForwardCall; UINT64 u64NnueProbeAttempts; } tree; struct { ULONG uNumSplits; ULONG uNumSplitsTerminated; } parallel; struct { ULONG uPawnPush; ULONG uPawnPushByQuarterPly[5]; // indexed by granted amount / QUARTER_PLY ULONG uCheck; ULONG uCheckByQuarterPly[5]; // indexed by granted amount / QUARTER_PLY ULONG uMateThreat; ULONG uRecapture; ULONG uOneLegalMove; ULONG uOneLegalMoveByQuarterPly[5]; // indexed by granted amount / QUARTER_PLY ULONG uNoLegalKingMoves; ULONG uMultiCheck; ULONG uSingularMove; ULONG uZugzwang; ULONG uSingularReplyToCheck; ULONG uEndgame; ULONG uBotvinnikMarkoff; ULONG uQExtend; ULONG uQExtendChaseNodes; // qsearch nodes where the side to move // is in check (every QSearch call that // delegates to QSearchFromCheckNoStandPat) // -- compare against // sCounters.tree.u64QNodeCount for the // fraction of all qsearch nodes spent // on forced check evasions } extension; struct { ULONG uProbes; ULONG uHits; } egtb; } COUNTERS; typedef struct _CUMULATIVE_SEARCH_FLAGS { // search ULONG uNumChecks[2]; // not used FLAG fInSuspiciousBranch; // not used FLAG fInReducedDepthBranch; // not used FLAG fAvoidNullmove; // restore FLAG fVerifyNullmove; // restore INT iCumulativeExtend; // restore; total extension // plies spent so far on // this line, root to here // qsearch ULONG uQsearchDepth; // restore ULONG uQsearchNodes; // incremental ULONG uQsearchCheckDepth; // restore FLAG fCouldStandPat[2]; // restore } CUMULATIVE_SEARCH_FLAGS; // Classic PVS node typing. PV: this node's own window was wide // (iBeta != iAlpha + 1), computed for free from the window it was // called with. CUT/ALL only matter when NOT PV, and can't be derived // from window width alone (both are null-window scouts) -- set // centrally in CommonSearchInit (searchsup.c, covers both Search() and // QSearch() from one place) per the standard alternation: the first // child of a PV node is PV; every other child of a PV node is CUT // (expected to fail high); the first child of a CUT node is ALL // (expected to fail low, must search everything); children of an ALL // node are CUT again. A narrow window's type therefore depends only on // the PARENT's own type, not on which move number got us here -- no // need to thread "was this the first move" through the caller. typedef enum { NODE_PV, NODE_CUT, NODE_ALL } NODE_TYPE; // Sentinels for PLY_INFO.uParentMoveFHEstimate (below). A node's // PARENT stashes, before recursing into it, its own prediction of // "how likely is this specific move to fail high" -- which is // identically this node's own predicted FAIL-LOW probability (for mv // to fail high at the parent, every move here, including mv's // continuation, must resolve to a fail-low). Two cases produce no // real number: // - MOVE_FH_ESTIMATE_NONLEFTOVER: mv was NOT a leftover move (hash // move, good capture, killer, etc.) -- the dynamic-ordering/joint // tables are only ever consulted for leftover moves // (searchsup.c's SearchOneMove), so there is no table lookup to // report. This is NOT "no information": trying a move specifically // BECAUSE it's expected to be strong is itself evidence the parent // expects a HIGHER fail-high chance than even the best leftover // move would get, so this sentinel should bucket as a // high-confidence band, not as "unknown". // - MOVE_FH_ESTIMATE_NO_DATA: mv WAS a leftover move and the table // was consulted, but the cell was below the sample floor (same // value as searchsup.c's file-local JOINT_TABLE_SENTINEL -- kept // numerically identical on purpose, see that macro's own comment). // This case really is "no information". #define MOVE_FH_ESTIMATE_NONLEFTOVER (0xFFFFFFFE) #define MOVE_FH_ESTIMATE_NO_DATA (0xFFFF) // Band count for FHThousandthsToBand (searchsup.c) and // g_NodeTypeGuessTable (data.c) -- see both for the banding scheme // and how the table was baked. #define NODE_TYPE_FH_BANDS (7) ULONG FHThousandthsToBand(IN ULONG uFHThousandths); extern const NODE_TYPE g_NodeTypeGuessTable[2][NODE_TYPE_FH_BANDS][NODE_TYPE_FH_BANDS]; // ---------------------------------------------------------------------- // // Searcher thread record // typedef struct _SPLIT_INFO { // locks / counters volatile ULONG uLock; // lock for this split node volatile ULONG uNumThreadsHelping; // num threads in this node volatile FLAG fTerminate; // signal helpers to terminate // moves in the split node ULONG uRemainingMoves; // num moves remaining to do ULONG uOnDeckMove; // next move to do ULONG uNumMoves; ULONG uAlreadyDone; MOVE_STACK_MOVE_VALUE_FLAGS mvf[MAX_MOVES_PER_PLY]; // the moves to search // input to the split node POSITION sRootPosition; // the root position MOVE mvPathToHere[MAX_PLY_PER_SEARCH]; // path from root to split MOVE mvLast; // last move before split ULONG uDepth; // remaining depth at split INT iPositionExtend; // positional extension SCORE iAlpha; // original alpha at split SCORE iBeta; // beta at split SCORE iEval; // static eval at split SCORE iImprovement; // eval improvement since ply-2 (may be negative) FLAG fThreat; // node-wide threat extension in play at // the split point -- CanWeReduceOrPruneMove // needs this; populated by the originating // thread (it already has it, computed // before the split-launch decision), not // recomputed by helper threads BITBOARD bbMinorAttacksSnapshot[2]; // attack-table snapshots from the BITBOARD bbRookAttacksSnapshot[2]; // originating thread's own Search() call, // for EstimateHangingMaterialCp -- helper // threads have no access to the // originating node's pos->bbMinorAttacks/ // bbRookAttacks (those are this-node-local, // not replayed via mvPathToHere), so without // this they'd degrade to all-zero snapshots NODE_TYPE eNodeType; // the split point's own node type -- // copied here rather than read off a // helper thread's own ctx->sPlyInfo, // since a helper thread never actually // calls Search()/CommonSearchInit for // the split-point ply itself (it just // replays MakeMove along mvPathToHere // to reconstruct board state), so that // slot in ITS OWN persistent context is // always stale leftover data from // whatever unrelated node that thread // last helped with CUMULATIVE_SEARCH_FLAGS sSearchFlags; // flags at split time // output from the split node MOVE mvBest; // the best move SCORE iBestScore; // it's score COUNTERS sCounters; // updated counters MOVE PV[MAX_PLY_PER_SEARCH]; #ifdef DEBUG ULONG uSplitPly; POSITION sSplitPosition; #endif } SPLIT_INFO; // Which of several distinct move-scoring conventions currently // populates this ply's sMoveStack[].iValue -- generate.c's four // schemes each pack tier-flag bits (SORT_THESE_FIRST, FIRST_KILLER, // etc.) the same way, so a raw tier/leftover classification // (iValue < GOOD_MOVE) already works correctly regardless of which of // those four is active. What does NOT generalize is treating iValue // as a material/eval-axis estimate: each scheme bakes in different // ordering-only bias terms on top of the real SEE/material value (see // ComputeMoveScore's DecodeMoveValueEstimate, searchsup.c), so any // caller that needs an actual value (not just a tier) must know which // scheme it's decoding. MOVE_SCORING_ROOT and MOVE_SCORING_IID_RESCORED // aren't tier-flag-encoded at all -- both are already real quantities // (a node-count/score composite, or a genuine searched eval-axis // score respectively), never meaningful as a generate.c-style tier. typedef enum { MOVE_SCORING_NORMAL, // _ScoreAllMoves MOVE_SCORING_ESCAPE, // _ScoreAllEscapes MOVE_SCORING_QSEARCH_CHECKS, // _ScoreQSearchMovesInclChecks MOVE_SCORING_QSEARCH_NO_CHECKS, // _ScoreQSearchMovesNoChecks MOVE_SCORING_IID_RESCORED, // RescoreMovesViaSearch -- a real // eval-axis score already, trust // iValue as-is MOVE_SCORING_ROOT // root.c's own next-iteration // move-ordering composite // ((subtree node count >> 9, // capped) + iScore) -- only ever // populates ply 0's move stack } MOVE_SCORING_SCHEME; // NNUE accumulator -- the feature-transformer's output for ONE color's // perspective (own-king-relative HalfKAv2_hm features summed through // the single shared [22528, 256] embedding table -- see // docs/NNUE.md's "Canonical HalfKAv2_hm feature-indexing spec" and // annotate_pgns/nnue_model.py's L1). Stored PRE-activation (matches // Stockfish convention and nnue_model.py's own torch.relu() applied // AFTER this sum, not baked into it) so incremental MakeMove/ // UnmakeMove updates stay purely additive -- the activation is only // ever applied where the accumulator is actually read (the FC head), // never where it's maintained. // // Int32, not float: the embedding table (g_rgfNnueWeight) is // quantized to SHORT at FT_SCALE (see export_nnue_weights.py / // g_fNnueFtScale), and summing up to 32 active features' SHORT rows // can exceed SHORT's range even though each individual row value // fits -- measured worst-case real accumulator magnitude (NNUE.md's // "Quantization safety-checked" note) is ~26 in float-equivalent // units, i.e. ~106,500 at the chosen scale, well past MAX_SHORT. // INT headroom makes this a non-issue. Converted back to float // (divided by g_fNnueFtScale) only where the accumulator is actually // read (the FC head, nnueeval.c's NnueForward) -- never where it's // maintained, so incremental MakeMove/UnmakeMove updates stay purely // additive integer arithmetic (exact, no float drift). #define NNUE_ACCUMULATOR_SIZE (256) // L1, nnue_model.py #define NNUE_L2 (32) // nnue_model.py's L2 #define NNUE_L3 (32) // nnue_model.py's L3 typedef struct _NNUE_ACCUMULATOR { INT rgiValue[NNUE_ACCUMULATOR_SIZE]; } NNUE_ACCUMULATOR; // // NNUE feature-index formula and accumulator-maintenance prototypes // live further down in this header (after ASSERT is defined) -- see // the block near ASSERT's own definition. typedef struct _PLY_INFO { #ifdef DEBUG POSITION sPosition; SCORE iAlpha; SCORE iBeta; #endif SCORE iEval; // 2026-09-17: per-color iPositional from the SAME Eval() call that // set iEval above -- previously discarded (Search() called Eval() // with piPositional==NULL). Stored per-ply, right next to iEval, // so any code walking the ply stack (up or down) can read both the // score and its positional-bias estimate for a given node together // -- e.g. a cleaner cross-ply "improving" signal that corrects for // a tier switch (super-lazy vs full) between the two Eval() calls // being compared, instead of just diffing raw iEval values. Not // yet consumed that way anywhere; its one reader (search.c's EFP // gate, VALUE_KNIGHT/VALUE_ROOK margins) was removed along with // EFP, so this is currently write-only -- flagged, not deleted, // since removing it means touching Eval()'s piPositional parameter // at every call site, a bigger change than this cleanup pass. SCORE iPositional[2]; INT iExtensionAmount; FLAG fInCheck; FLAG fInQsearch; NODE_TYPE eNodeType; // classic PVS node typing -- // see the enum's own comment // 2026-10-22: the PARENT's own predicted FH% for the move that // led to this node (identically this node's predicted FAIL-LOW // probability -- see the sentinel macros' comment above). Written // by searchsup.c's SearchOneMove right after MakeMove succeeds, // i.e. for THIS ply, by the ply ABOVE, before recursing in; read // back from here once this node's own Search()/CommonSearchInit // runs. One slot per ply, same convention as every other // per-ply-indexed field in this struct (iExtensionAmount, etc) -- // a child overwrites it on its own next move, so it must be read // before this ply recurses again itself. ULONG uParentMoveFHEstimate; MOVE_SCORING_SCHEME eMoveScoringScheme; // which convention scored // sMoveStack[uPly] -- see // the enum's own comment MOVE mv; MOVE mvBest; MOVE PV[MAX_PLY_PER_SEARCH]; // Stamped with (pos->u64NonPawnSig ^ pos->u64PawnSig) the instant a // full (not lazy/super-lazy) Eval() call finishes for the position // currently sitting at this ply -- see Eval()'s own comment near // where it's written. A handful of fields below are only ever // populated on that same full-eval path (iKingScore) or only // trustworthy when read back against the exact position that set // them (ctx->cEnprise/pEnprise/cTrapped/pTrapped, indexed by ply // alongside this struct -- see chess.h's SEARCHER_THREAD_CONTEXT). // Every such field is marked "REQUIRES SIG MATCH" below; before // trusting one, compare this stamp against the CURRENT position's // live signature (NOT against u64Sig just below -- that field holds // a different thing, the position last recorded as this ply was // being LEFT via SaveCurrentPositionHistoryInContext, not the // position currently occupying it while Eval() is mid-flight). // Nonzero-but-wrong is exactly as unsafe as zero: a plain FALSE // default at struct-zero-init time (sig 0) already reads as "no // match" against any real position, so no separate valid/invalid // flag is needed. UINT64 u64FullEvalSig; // Which Eval() exit tier supplied the piPositional value most // recently returned for THIS ply -- set inside Eval() itself (all // three exit points), read back in QSearch/QSearchFromCheckNoStandPat // to index FUTILITY_BASE_MARGIN_BY_SOURCE. Per-ply, not a single // per-thread scalar, precisely because it used to be one // (SEARCHER_THREAD_CONTEXT.uLastPositionalSource) and that was a // real, live bug: QSearch's own move loop recurses into QSearch() // again for earlier moves before reading this back for later ones, // and that recursive call's own nested Eval() at ctx->uPly+1 would // clobber a single shared scalar -- indexing the margin table with // whatever tier the deepest just-completed call happened to use, // not the tier that actually produced this ply's own iEval/ // rgiPositional. Per-ply storage makes that impossible by // construction: a deeper node's Eval() call writes to a different // array slot entirely. ULONG uLastPositionalSource; SCORE iKingScore[2]; // REQUIRES SIG MATCH // (u64FullEvalSig) -- // only written on // Eval()'s full path UINT64 u64NonPawnSig; UINT64 u64PawnSig; UINT64 u64Sig; ULONG uFifty; FLAG fCastled[2]; ULONG uTotalNonPawns; BITV bvCastleInfo; COOR cEpSquare; // King-move backup for the NNUE accumulator (see NNUE_ACCUMULATOR // above and SEARCHER_THREAD_CONTEXT's sNnueAccum[2] below): every // feature in HalfKAv2_hm is own-king-relative, so when a color's // OWN king moves, that color's ENTIRE accumulator needs a full // recompute, not an incremental delta -- MakeMove backs up the // pre-move accumulator here before recomputing; the MATCHING // UnmakeMove restores it from here. Indexed by ply (this struct is // already sPlyInfo[uPly]), not a single shared slot, so nested own- // king moves at different plies never collide -- same reasoning as // cTrapped/cEnprise's existing per-ply indexing. // // No "is this valid" marker needed in a release build: Make/ // UnmakeMove already decide whether to touch this purely from // whether the move being made/unmade is IS_KING(mv.pMoved) for // this ply's mover -- exactly the same information Make/UnmakeMove // already use to dispatch every other move-type-specific update, // not something inferred from this struct's own contents. A // correctly nested Make/Unmake pair guarantees this is always // freshly written by MakeMove before the matching UnmakeMove ever // reads it. NNUE_ACCUMULATOR sNnueAccumBackup; #ifdef DEBUG // Which color's accumulator sNnueAccumBackup above holds, checked // by UnmakeMove (ASSERT only -- never a real branch condition, see // above) purely to catch a hypothetical Make/Unmake mismatch, and // then the backup is memset(0)'d (poison, not a real "clear" any // release-build logic depends on) so an accidental future stale // read in some other DEBUG-only path doesn't look like valid data. ULONG uNnueAccumBackupColor; #endif } PLY_INFO; #define PAWN_HASH_TABLE_SIZE (131072) // 10Mb (per thread; sizeof(PAWN_HASH_ENTRY) == 80) typedef struct _PAWN_HASH_ENTRY { UINT64 u64Key; BITBOARD bbPasserLocations[2]; BITBOARD bbStationaryPawns[2]; SHORT iScore[2]; 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 // function of pawn placement (see // _EvalBishop's cache-miss // population in eval.c), reused by // every bishop of that color/ // complex instead of rescanned. } PAWN_HASH_ENTRY; // EXPERIMENTAL, prototype-only (2026-09-05) -- see // SEARCHER_THREAD_CONTEXT's rgKingSafetyHash comment. Same table size // as the pawn hash for a fair, direct hit-rate comparison against the // already-measured numbers. #define KING_SAFETY_HASH_TABLE_SIZE PAWN_HASH_TABLE_SIZE typedef struct _KING_SAFETY_HASH_ENTRY { UINT64 u64Key; // 2026-09-15: SCORE (direct centipawns), not a counter -- see // eval.c's _ComputeFileStormDefects. KING_DEFECT_DUMP's debug // sibling _ComputeFileStormDefectsSplit is untouched and still // 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; #ifdef KING_DEFECT_DUMP // Ring-slot indexing matches KingSafetyDeltas' order in eval.c exactly // (-17,-16,-15,-1,+1,+15,+16,+17): slots 0-7, no bucketing -- kept at // full per-square resolution rather than collapsed into front/side/back, // since the direct in-front square (delta +-16) and the two front- // diagonal squares (+-15/+-17) are genuinely different attack geometries // (e.g. a same-file rook vs. a diagonal bishop) that a bucket would // conflate. Sample size (tens of thousands of mined positions) has no // trouble supporting this many features. #define KING_RING_NUM_SLOTS 8 #define KING_RING_PAWN 0 #define KING_RING_MINOR 1 #define KING_RING_ROOK 2 #define KING_RING_QUEEN 3 #define KING_RING_NUM_PIECE_TYPES 4 // Raw, per-defect, per-square breakdown of _EvalKing's ring-square scan, // filled in (only under this build flag) instead of/alongside the normal // collapse into one scalar uCounter -- feeds a per-defect regression // against real mate outcomes instead of trusting the hand-picked weights // already baked into KING_SAFETY_BY_COUNTER / KING_COUNTER_BY_ATTACK_PATTERN. // See CLAUDE.md's king-safety discussion. typedef struct _KING_DEFECT_DUMP_INFO { // [slot][piece type]: 1 if this ring square is attacked or x-rayed by // this piece type, same bits _EvalKing's own bvAttack/bvXray already // compute per square -- just retained per-slot instead of OR'd // together into one whole-ring bvPattern. FLAG fAttackedByPieceType[KING_RING_NUM_SLOTS][KING_RING_NUM_PIECE_TYPES]; // [slot]: off-board squares (near an edge/corner) leave all four FALSE. FLAG fOnBoard[KING_RING_NUM_SLOTS]; FLAG fAttackedUndefended[KING_RING_NUM_SLOTS]; FLAG fAttackedDefended[KING_RING_NUM_SLOTS]; FLAG fXrayedUnattacked[KING_RING_NUM_SLOTS]; FLAG fEmptyFlight[KING_RING_NUM_SLOTS]; ULONG uFileStormDefects; // Split-out sub-components of uFileStormDefects, from two already- // separate hand-picked tables (KingFileDefects / KingStormingPawnDefects) // -- kept apart so a regression can weigh "open/half-open file near // the king" independently from "how far an enemy pawn has advanced up // that file", rather than only ever seeing their pre-summed total. ULONG uFileStormOpenFile; ULONG uFileStormStormingPawn; ULONG uQueenNearKing; ULONG uInitialCounterByLocation; ULONG uCounterTotal; // final uCounter, pre-clamp ULONG uSafetyTableValue; // KING_SAFETY_BY_COUNTER[MINU(uCounterTotal,41)] // Attacker's (xColor's) army strength -- pos->uArmyScaler[xColor], // the exact index REDUCED_MATERIAL_DOWN_SCALER itself uses to scale // the final king-safety score. Captured so training can control for // it directly (a defect pattern under a weak attacker converts to // mate far less often than the identical pattern under a full // attacking army, regardless of what the raw defect counts say) -- // see CLAUDE.md's king-safety discussion. ULONG uAttackerArmyScaler; FLAG fCastled; ULONG uMinorsAtHome; FLAG fMissingCastleShort; FLAG fMissingCastleLong; // 2026-09-15: the REAL, current _EvalKing output (post-rewrite linear // ring-scan sum, AFTER the REDUCED_MATERIAL_DOWN_SCALER multiply -- // i.e. exactly what gets added into this side's total eval) -- unlike // every other field in this struct, which all come from the // deliberately-frozen OLD counter mechanism (see uSafetyTableValue's // comment). Added for a magnitude sanity check against other engines' // king-safety terms (Crafty/Glaurung), not for regression mining -- // it postdates the CSVs the mining pipeline already produced. SCORE iRealSafety; } KING_DEFECT_DUMP_INFO; #endif #define NUM_SPLIT_PTRS_IN_CONTEXT (8) // // Counter-move table (Crafty-style): keyed by a hash of the *previous* // move (whatever the opponent just played to reach this node), not by // ply -- unlike killers, this generalizes across different branches // that happen to share the same preceding move, anywhere in the tree. // Sized to MOVE_TO_INDEX's full range so the index is exact, not a // lossy hash. // #define COUNTER_MOVE_TABLE_SIZE (0x20000) #ifdef FH_TABLE_COLLECTION // 2026-10-20: full dual-table (5D alpha + 4D beta) collection, // restored for the NNUE-eval re-collection run -- see docs/NNUE.md. // Deliberately NOT a member of COUNTERS (unlike every other // PERF_COUNTERS field) -- COUNTERS is embedded both in // SEARCHER_THREAD_CONTEXT *and* in SPLIT_INFO's g_SplitInfo[MAX_SPLITS] // array, so anything added there gets silently multiplied by // MAX_SPLITS. This struct alone is already ~19MB (the 5D alpha array // dominates); embedded in COUNTERS it inflated the binary's BSS to // ~1.47GB (confirmed via `size typhoon` + `nm --size-sort`) purely // from g_SplitInfo's replication -- harmless at runtime since a // --cpus 1 collection shard never touches g_SplitInfo's copies (pure // never-faulted zero pages), but it broke valgrind's fixed-address // mmap of the BSS segment outright, and would have silently dropped // any data a helper thread collected during a real MP split (no // merge-back code exists for these fields, unlike COUNTERS' other // per-split fields) if collection were ever run with --cpus > 1. // Giving it its own struct, referenced only from // SEARCHER_THREAD_CONTEXT below, fixes both: SPLIT_INFO never carries // a copy at all now. typedef struct _FH_COLLECTION_COUNTERS { // Alpha axes: [depth][dumb][gap][late][improve], same // fPruneReduceEligible population as u64JointTries/FH (COUNTERS.tree) // for late >= 2 (real per-move samples). late == 1 is the existing // "early move" extrapolation proxy used live at the null-move-skip // site (search.c) -- NOT collected here, it's a lookup-time // fiction, never a real sample. late == 0 is reserved and, before // this change, permanently unproduced (uMoveNum is 1-based and the // per-move eligibility gate requires uLegalMoves > 1) -- repurposed // here for a genuine NODE-level sample (hang pinned to 0, no // specific move) answering "will SOMETHING at this node fail // high", taken once per eligible node when iEval <= iAlpha, ground // truth = the real move loop's iBestScore >= iBeta at Search()'s // end: label. This is the alpha-side analog of the beta table's // own whole-node sample (which only covers iEval > iBeta) -- // together the two cover every null-window child's self-estimate // with no gap, since alpha_child/beta_child are adjacent integers // for a null window. See docs/NNUE.md's node-type discussion. UINT64 u64Alpha5Tries[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS][IMPROVE_BUCKETS]; UINT64 u64Alpha5FH[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS][IMPROVE_BUCKETS]; // Beta axes: [depth][gapBeta][ownImprove][enemyImprove], one // sample per eligible node (iEval > iBeta), same real-move-loop // ground truth. Collecting this requires NOT taking the // static-null-move shortcut's early `goto end` during collection // builds (same reasoning as the LMP disable in searchsup.c) -- // otherwise the current, soon-to-be-replaced data.c table would // censor exactly the population this collection is trying to // measure. UINT64 u64BetaTries[SEARCH_DEPTH_BUCKETS][GAP_BETA_BUCKETS][IMPROVE_BUCKETS][IMPROVE_BUCKETS]; UINT64 u64BetaFH[SEARCH_DEPTH_BUCKETS][GAP_BETA_BUCKETS][IMPROVE_BUCKETS][IMPROVE_BUCKETS]; // 1D marginals, independent INC() call sites (not derived from the // joints above) -- see marginal_crosscheck.py. The alpha table's // depth/dumb/gap/late marginals already exist in COUNTERS.tree // (u64DepthEstimateTries etc.) and are reused as-is for the 5D // cross-check; only the improve marginal is new, here. UINT64 u64ImproveEstimateTries[IMPROVE_BUCKETS]; UINT64 u64ImproveEstimateFH[IMPROVE_BUCKETS]; UINT64 u64BetaDepthEstimateTries[SEARCH_DEPTH_BUCKETS]; UINT64 u64BetaDepthEstimateFH[SEARCH_DEPTH_BUCKETS]; UINT64 u64BetaGapEstimateTries[GAP_BETA_BUCKETS]; UINT64 u64BetaGapEstimateFH[GAP_BETA_BUCKETS]; UINT64 u64BetaOwnImpEstimateTries[IMPROVE_BUCKETS]; UINT64 u64BetaOwnImpEstimateFH[IMPROVE_BUCKETS]; UINT64 u64BetaEnemyImpEstimateTries[IMPROVE_BUCKETS]; UINT64 u64BetaEnemyImpEstimateFH[IMPROVE_BUCKETS]; } FH_COLLECTION_COUNTERS; #endif // FH_TABLE_COLLECTION typedef struct _SEARCHER_THREAD_CONTEXT { ULONG uPly; // its distance from root POSITION sPosition; // the board // The live, always-current NNUE accumulator, one per color -- // analogous to sPosition just above (always reflects the CURRENT // position, maintained incrementally by MakeMove/UnmakeMove, not // recomputed per call). See NNUE_ACCUMULATOR's own comment and // PLY_INFO's sNnueAccumBackup (this struct's sPlyInfo[uPly] member // below) for the king-move full-recompute case. Indexed by color // (WHITE/BLACK, chess.h), not by perspective-as-concept, since // each color's own accumulator is independently maintained and // independently invalidated (only THAT color's own king moving // forces its recompute -- see Stockfish's requires_refresh(), same // semantics here). NNUE_ACCUMULATOR sNnueAccum[2]; MOVE_STACK sMoveStack; // the move stack CUMULATIVE_SEARCH_FLAGS sSearchFlags; PLY_INFO sPlyInfo[MAX_PLY_PER_SEARCH+1]; MOVE mvKiller[MAX_PLY_PER_SEARCH][2]; MOVE mvKillerEscapes[MAX_PLY_PER_SEARCH][2]; MOVE mvNullmoveRefutations[MAX_PLY_PER_SEARCH]; MOVE mvNullmoveQuietRefutations[MAX_PLY_PER_SEARCH]; MOVE mvCounter[COUNTER_MOVE_TABLE_SIZE][2]; // counter-move table // REQUIRES SIG MATCH (sPlyInfo[uPly].u64FullEvalSig, chess.h's // PLY_INFO) before trusting any of these four -- only written on // Eval()'s full path (eval.c's _EvalLookForDanger/ // _EvalTrappedPieces), by whichever node last ran a full eval at // this ply (possibly this node, possibly an earlier sibling that // visited the same ply slot for a different position -- see // ValueOfMaterialInTroubleDespiteMove's own comment on that). Was // previously guarded by _EnpriseSlotValid/_TrappedSlotValid // re-checking piece identity on the live board instead; that's a // decent but imprecise proxy (a coincidentally-matching piece on // the same square in an unrelated position at this ply reads as // "fresh") -- a direct sig compare is both cheaper and exact. COOR cEnprise[MAX_PLY_PER_SEARCH][2]; // en prise piece hints PIECE pEnprise[MAX_PLY_PER_SEARCH][2]; COOR cTrapped[MAX_PLY_PER_SEARCH]; // trapped piece hint PIECE pTrapped[MAX_PLY_PER_SEARCH]; COUNTERS sCounters; #ifdef FH_TABLE_COLLECTION // Deliberately NOT inside sCounters -- see FH_COLLECTION_COUNTERS' // own comment (just above this struct) for why: sCounters is also // embedded in SPLIT_INFO's g_SplitInfo[MAX_SPLITS] array, and // anything added there gets silently multiplied by MAX_SPLITS. FH_COLLECTION_COUNTERS sFhCollection; #endif ULONG uThreadNumber; SPLIT_INFO *pSplitInfo[NUM_SPLIT_PTRS_IN_CONTEXT]; MOVE mvRootMove; SCORE iRootScore; ULONG uRootDepth; PAWN_HASH_ENTRY rgPawnHash[PAWN_HASH_TABLE_SIZE]; // EXPERIMENTAL, prototype-only (2026-09-05): real cache for // _EvalKing's open-file/storming-pawn defect count only (the // DO_KING_SAFETY_THRESHOLD-gated "safety branch" terms -- measured // ~96% hit rate in general/middlegame positions). Deliberately // does NOT cover the u<8-gated "endgame branch" terms (king- // supporting-own-passer, in/out-of-action) -- those measured a // much worse ~75-85% hit rate in bare K+P endgames, and are cheap // enough in that low-piece-count regime that caching them isn't // expected to pay off; they stay computed directly in _EvalKing, // uncached, per board_representation/EVAL.md section 9's // king-safety-hash discussion. Keyed on (pos->u64PawnSig, both // kings' squares) -- both sides' defect counts are computed and // stored together on a miss, so whichever king _EvalKing evaluates // second within the same Eval() call is a guaranteed hit. KING_SAFETY_HASH_ENTRY rgKingSafetyHash[KING_SAFETY_HASH_TABLE_SIZE]; #ifdef CALIBRATE_POSITIONAL // Set/cleared once per Eval() call, around the cheap base-margin // ("super lazy") gate that decides whether EstimatePositionalScore // would even run in a normal build -- _RecordPositionalCalibration // only logs a sample when this is set, so the calibration histogram // matches the population EstimatePositionalScore actually sees in // real (uninstrumented) search, not every node Eval() is ever // called on. FLAG fCalibrateCandidate; #endif #ifdef CALIBRATE_QSEARCH_FUTILITY // TRUE for the duration of a diagnostic "what if we hadn't pruned // this move" re-search -- search.c's qsearch futility gate checks // this and forces iFutility wide open so the diagnostic subtree // itself isn't contaminated by the same pruning being measured. // Never set recursively: a rejection can't occur while this is // already TRUE (iFutility being wide open means nothing gets // rejected), so there's no risk of runaway nested diagnosis. FLAG fDiagUnprunedSubtree; #endif #ifdef KING_DEFECT_DUMP KING_DEFECT_DUMP_INFO sKingDefectDump[2]; // filled by _EvalKing, indexed by color #endif CHAR szLastPV[SMALL_STRING_LEN_CHAR]; } SEARCHER_THREAD_CONTEXT; // // When I added the pawn hash table and eval hash tables to searcher // thread contexts they became too heavy to just allocate on the fly // for things like checking the legality of SAN moves or seeing if an // opening book line leads to a draw. This // LIGHTWEIGHT_SEARCHER_CONTEXT structure can be cast into a full // SEARCHER_THREAD_CONTEXT and passed safely into the Generate, // MakeMove and UnMakeMove functions because they presently only // need: // // 1. uPly // 2. sPosition // 3. sNnueAccum (NNUE_ACCUMULATOR -- MakeMove/UnmakeMove touch // this unconditionally now, not just on a king move, so it // has to physically exist here too, at the same relative // position as in SEARCHER_THREAD_CONTEXT, or a lightweight- // cast caller's write becomes an out-of-bounds overflow past // this smaller struct's real allocation. 2026-10-15: this bit // a real bug the first time sNnueAccum was added to // SEARCHER_THREAD_CONTEXT without a matching mirror here -- // every field after it silently shifted out of alignment for // any lightweight-cast caller (ParseMoveSan's self-test caught // it). Any future SEARCHER_THREAD_CONTEXT field MakeMove/ // UnmakeMove/Generate touch unconditionally needs the same // treatment -- check this list before adding one.) // 4. sPlyInfo // 5. sMoveStack // typedef struct _LIGHTWEIGHT_SEARCHER_CONTEXT { ULONG uPly; POSITION sPosition; NNUE_ACCUMULATOR sNnueAccum[2]; MOVE_STACK sMoveStack; CUMULATIVE_SEARCH_FLAGS sSearchFlags; PLY_INFO sPlyInfo[MAX_PLY_PER_SEARCH+1]; } LIGHTWEIGHT_SEARCHER_CONTEXT; // ---------------------------------------------------------------------- // // Global game options // typedef struct _GAME_OPTIONS { ULONG uMyClock; ULONG uOpponentsClock; FLAG fGameIsRated; FLAG fOpponentIsComputer; ULONG uSecPerMove; ULONG uMaxDepth; UINT64 u64MaxNodeCount; FLAG fShouldPonder; FLAG fPondering; FLAG fThinking; FLAG fSuccessfulPonder; MOVE mvPonder; FLAG fShouldPost; FLAG fForceDrawWorthZero; ULONG uMyIncrement; ULONG uMovesPerTimePeriod; CHAR szAnalyzeProgressReport[SMALL_STRING_LEN_CHAR]; FLAG fShouldAnnounceOpening; SCORE iLastEvalScore; UINT64 u64NodesSearched; UINT64 u64BetaCutoffs; UINT64 u64BetaCutoffsOnFirstMove; UINT64 u64CounterMoveTries; UINT64 u64CounterMoveHits; UINT64 u64LeftoverTries; UINT64 u64LeftoverAlpha; UINT64 u64LeftoverFH; UINT64 u64CMEvidenceTries[CM_EVIDENCE_BUCKETS]; UINT64 u64CMEvidenceFH[CM_EVIDENCE_BUCKETS]; UINT64 u64HangEstimateTries[HANG_CP_BUCKETS]; UINT64 u64HangEstimateFH[HANG_CP_BUCKETS]; UINT64 u64EligibleLeftoverTries; UINT64 u64EligibleLeftoverFH; UINT64 u64GapEstimateTries[GAP_CP_BUCKETS]; UINT64 u64GapEstimateFH[GAP_CP_BUCKETS]; UINT64 u64MoveNumEstimateTries[MOVE_NUM_BUCKETS]; UINT64 u64MoveNumEstimateFH[MOVE_NUM_BUCKETS]; UINT64 u64DepthEstimateTries[SEARCH_DEPTH_BUCKETS]; UINT64 u64DepthEstimateFH[SEARCH_DEPTH_BUCKETS]; UINT64 u64JointTries[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS]; UINT64 u64JointFH[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS]; UINT64 u64JointExtraTries[JOINT_EXTRA_TIER_COUNT]; UINT64 u64JointExtraResearch[JOINT_EXTRA_TIER_COUNT]; UINT64 u64LMPPruned; UINT64 u64NullMoveTriesByR[NULL_R_BUCKETS]; UINT64 u64NullMoveSuccessByR[NULL_R_BUCKETS]; #ifdef FH_TABLE_COLLECTION // Mirrors SEARCHER_THREAD_CONTEXT's sCounters.tree fields of the // same name -- see that struct's comment for the full design. UINT64 u64Alpha5Tries[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS][IMPROVE_BUCKETS]; UINT64 u64Alpha5FH[SEARCH_DEPTH_BUCKETS][HANG_CP_BUCKETS][GAP_CP_BUCKETS][MOVE_NUM_BUCKETS][IMPROVE_BUCKETS]; UINT64 u64BetaTries[SEARCH_DEPTH_BUCKETS][GAP_BETA_BUCKETS][IMPROVE_BUCKETS][IMPROVE_BUCKETS]; UINT64 u64BetaFH[SEARCH_DEPTH_BUCKETS][GAP_BETA_BUCKETS][IMPROVE_BUCKETS][IMPROVE_BUCKETS]; UINT64 u64ImproveEstimateTries[IMPROVE_BUCKETS]; UINT64 u64ImproveEstimateFH[IMPROVE_BUCKETS]; UINT64 u64BetaDepthEstimateTries[SEARCH_DEPTH_BUCKETS]; UINT64 u64BetaDepthEstimateFH[SEARCH_DEPTH_BUCKETS]; UINT64 u64BetaGapEstimateTries[GAP_BETA_BUCKETS]; UINT64 u64BetaGapEstimateFH[GAP_BETA_BUCKETS]; UINT64 u64BetaOwnImpEstimateTries[IMPROVE_BUCKETS]; UINT64 u64BetaOwnImpEstimateFH[IMPROVE_BUCKETS]; UINT64 u64BetaEnemyImpEstimateTries[IMPROVE_BUCKETS]; UINT64 u64BetaEnemyImpEstimateFH[IMPROVE_BUCKETS]; #endif // FH_TABLE_COLLECTION #ifdef JOINT_EXTRA_VERIFY UINT64 u64JointExtraVerifyTries[JOINT_EXTRA_TIER_COUNT][POST_REDUCE_DEPTH_BUCKETS]; UINT64 u64JointExtraVerifySilentMiss[JOINT_EXTRA_TIER_COUNT][POST_REDUCE_DEPTH_BUCKETS]; UINT64 u64JointExtraVerifyNullBailoutSignature[JOINT_EXTRA_TIER_COUNT]; #endif #ifdef TEST_NULL UINT64 u64NCAttempts[NC_TOTAL_BUCKETS]; UINT64 u64NCNullFH[NC_TOTAL_BUCKETS]; UINT64 u64NCRealFH[NC_TOTAL_BUCKETS]; UINT64 u64NCBothFH[NC_TOTAL_BUCKETS]; UINT64 u64NCNullProbeNodeSum[NC_TOTAL_BUCKETS]; UINT64 u64NCFirstMoveNodeSum[NC_TOTAL_BUCKETS]; UINT64 u64NCRealLoopNodeSum[NC_TOTAL_BUCKETS]; #endif CHAR szLogfile[SMALL_STRING_LEN_CHAR]; CHAR szEGTBPath[SMALL_STRING_LEN_CHAR]; CHAR szBookName[SMALL_STRING_LEN_CHAR]; ULONG uNumProcessors; ULONG uNumHashTableEntries; FLAG fNoInputThread; FLAG fVerbosePosting; FLAG fRunningUnderXboard; FLAG fStatusLine; FLAG fFastScript; INT iResignThreshold; // Minimum elapsed time (seconds, since this move's search started) // before UtilPrintPV (util.c) will emit a PV line at all -- default // 0.35 matches the historical hardcoded constant. 2026-10-16: a // search that finishes a move's ENTIRE iterative-deepening loop // faster than this (common at low sd/st, e.g. scripted self-play // corpus generation) gets NO PV line at all, not a delayed one -- // match_play.py's "no eval captured for this move" case. Exposed as // a runtime "set PostThrottle " variable (vars.c) rather // than hand-editing the constant, specifically so a fast scripted // run can set it to 0 without changing default interactive/xboard // behavior or risking a forgotten permanent edit. double dPostThrottleSec; enum { CLOCK_NORMAL = 0, CLOCK_FIXED, CLOCK_INCREMENT, CLOCK_NONE } eClock; enum { GAME_UNKNOWN = 0, GAME_BULLET, GAME_BLITZ, GAME_STANDARD } eGameType; enum { I_PLAY_WHITE = 0, I_PLAY_BLACK, FORCE_MODE, EDIT_MODE, ANALYZE_MODE } ePlayMode; } GAME_OPTIONS; extern GAME_OPTIONS g_Options; // ---------------------------------------------------------------------- // // Move timer // #define TIMER_SEARCHING_FIRST_MOVE (0x1) #define TIMER_SEARCHING_IMPORTANT_MOVE (0x2) #define TIMER_RESOLVING_ROOT_FL (0x4) #define TIMER_RESOLVING_ROOT_FH (0x8) #define TIMER_JUST_OUT_OF_BOOK (0x10) #define TIMER_CURRENT_OBVIOUS (0x20) #define TIMER_CURRENT_WONT_UNBLOCK (0x40) #define TIMER_ROOT_POSITION_CRITICAL (0x80) #define TIMER_MOVE_IMMEDIATELY (0x100) #define TIMER_MANY_ROOT_FLS (0x200) #define TIMER_STOPPING (0x400) #define TIMER_SPLIT_FAILED (0x800) typedef struct _MOVE_TIMER { double dStartTime; double dEndTime; double dSoftTimeLimit; double dHardTimeLimit; ULONG uNodeCheckMask; volatile BITV bvFlags; } MOVE_TIMER; // ---------------------------------------------------------------------- // // Special move tag literals // #define ILLEGALMOVE ((ULONG)0x1DDD8888) // // useful macros // #ifdef DEBUG void _assert(CHAR *szFile, ULONG uLine); #define ASSERT(x) if (x) \ { ; } \ else \ { _assert(__FILE__, __LINE__); } #define VERIFY(x) ASSERT(x) #else #define ASSERT(x) ; #define VERIFY(x) x; #endif // DEBUG // HalfKAv2_hm feature-index formula, shared verbatim between the // reference-only dump path (nnuefeatures.c's DumpNnueFeatures) and the // real, hot-path MakeMove/UnmakeMove accumulator maintenance (move.c) // -- defined once, here, so the two can never silently drift apart. // See docs/NNUE.md's "Canonical HalfKAv2_hm feature-indexing spec" // for the derivation; this is just the formula, no commentary // repeated here. // #define NNUE_SQUARE_NB (64) #define NNUE_PS_W_PAWN (0 * NNUE_SQUARE_NB) #define NNUE_PS_B_PAWN (1 * NNUE_SQUARE_NB) #define NNUE_PS_W_KNIGHT (2 * NNUE_SQUARE_NB) #define NNUE_PS_B_KNIGHT (3 * NNUE_SQUARE_NB) #define NNUE_PS_W_BISHOP (4 * NNUE_SQUARE_NB) #define NNUE_PS_B_BISHOP (5 * NNUE_SQUARE_NB) #define NNUE_PS_W_ROOK (6 * NNUE_SQUARE_NB) #define NNUE_PS_B_ROOK (7 * NNUE_SQUARE_NB) #define NNUE_PS_W_QUEEN (8 * NNUE_SQUARE_NB) #define NNUE_PS_B_QUEEN (9 * NNUE_SQUARE_NB) #define NNUE_PS_KING (10 * NNUE_SQUARE_NB) #define NNUE_PS_NB (11 * NNUE_SQUARE_NB) // 704 #define NNUE_DIMENSIONS ((NNUE_SQUARE_NB * NNUE_PS_NB) / 2) // 22528 static int FORCEINLINE _NnueCoorToSq64(COOR c) { return (int)FILE(c) + 8 * ((int)RANK(c) - 1); } static int FORCEINLINE _NnueKingBucket(ULONG uPerspective, int sq64) { int file = sq64 & 7; int rank0 = sq64 >> 3; int fileQuartet = (file < 4) ? file : (7 - file); int rankGroup = (uPerspective == WHITE) ? (7 - rank0) : rank0; return rankGroup * 4 + fileQuartet; } static int FORCEINLINE _NnueOrientMask(ULONG uPerspective, int ksq64) { int file = ksq64 & 7; if (uPerspective == WHITE) { return (file < 4) ? 7 : 0; } return (file < 4) ? 63 : 56; } static int FORCEINLINE _NnuePiecePlane(ULONG uPerspective, ULONG uPieceColor, ULONG uPieceType) { FLAG fOwn; if (uPieceType == KING) { return NNUE_PS_KING; } fOwn = (uPieceColor == uPerspective); switch (uPieceType) { case PAWN: return fOwn ? NNUE_PS_W_PAWN : NNUE_PS_B_PAWN; case KNIGHT: return fOwn ? NNUE_PS_W_KNIGHT : NNUE_PS_B_KNIGHT; case BISHOP: return fOwn ? NNUE_PS_W_BISHOP : NNUE_PS_B_BISHOP; case ROOK: return fOwn ? NNUE_PS_W_ROOK : NNUE_PS_B_ROOK; case QUEEN: return fOwn ? NNUE_PS_W_QUEEN : NNUE_PS_B_QUEEN; default: ASSERT(FALSE); return 0; } } // Full feature-index formula: given perspective uPerspective, that // perspective's OWN king square cKingOwn (unoriented, the real board // square), and some piece pPiece sitting on cPieceSquare, returns the // active HalfKAv2_hm feature index. cPieceSquare == cKingOwn is valid // (every king is also a feature, via NNUE_PS_KING) and so is pPiece // being the OPPONENT's king. __attribute__((unused)) because not // every TU that includes chess.h calls this directly (some only use // the higher-level NnueAccum* wrappers in nnueaccum.c) and it's a // static-in-header function, so each such TU would otherwise warn. static int FORCEINLINE __attribute__((unused)) NnueFeatureIndex(ULONG uPerspective, COOR cKingOwn, COOR cPieceSquare, PIECE pPiece) { int ksq64 = _NnueCoorToSq64(cKingOwn); int orientMask = _NnueOrientMask(uPerspective, ksq64); int kingBucketOffset = _NnueKingBucket(uPerspective, ksq64) * NNUE_PS_NB; int sq64 = _NnueCoorToSq64(cPieceSquare); int idx = (sq64 ^ orientMask) + _NnuePiecePlane(uPerspective, PIECE_COLOR(pPiece), PIECE_TYPE(pPiece)) + kingBucketOffset; ASSERT((idx >= 0) && (idx < NNUE_DIMENSIONS)); return idx; } // Embedding table, quantized to SHORT (see docs/NNUE.md's // "Quantization plan" -- this is the per-move hot path table that // dominates memory bandwidth; halving it from float to SHORT is the // whole point). Zero-initialized by C's own static/global default // until a real exported checkpoint is loaded via NnueLoadWeights. // g_fNnueFtScale is the fixed-point scale applied by // export_nnue_weights.py's QuantizeFt before rounding to SHORT; // NnueForward divides it back out when converting the accumulator // (NNUE_ACCUMULATOR.rgiValue, INT) to float for the FC head. Defaults // to 1.0f so the all-zero placeholder table behaves sanely (0/1.0 == // 0) before any real weights are loaded. extern SHORT g_rgfNnueWeight[NNUE_DIMENSIONS][NNUE_ACCUMULATOR_SIZE]; extern SHORT g_rgfNnueBias[NNUE_ACCUMULATOR_SIZE]; extern float g_fNnueFtScale; void NnueAccumAddFeature(NNUE_ACCUMULATOR *pAccum, ULONG uPerspective, COOR cKingOwn, COOR cPieceSquare, PIECE pPiece); void NnueAccumRemoveFeature(NNUE_ACCUMULATOR *pAccum, ULONG uPerspective, COOR cKingOwn, COOR cPieceSquare, PIECE pPiece); void NnueAccumRecompute(NNUE_ACCUMULATOR *pAccum, POSITION *pos, ULONG uPerspective); // // FC head weights (nnueeval.c) -- the dense 2*L1 -> L2 -> L3 -> 1 // layers that sit on top of the two accumulator halves. Still a // placeholder (zero-initialized) until NnueLoadWeights successfully // reads a real exported checkpoint (annotate_pgns/ // export_nnue_weights.py's output format). g_fNnueWeightsLoaded is // FALSE until that happens -- NnueForward does not check it itself // (callers should, before trusting its output for anything beyond // plumbing validation). // // FC head weights, quantized to CHAR (int8, signed) per // docs/NNUE.md's "Quantization plan" step 2 -- biases stay float32 // (tiny arrays, added post-dequant, not worth quantizing). // g_rgfNnueActScale[0..2] (AS1,AS2,AS3) are the per-stage uint8 // fixed-point scales used to requantize each ReLU'd activation before // the next layer's int8 dot product; g_rgfNnueWeightScale[0..2] // (WS1,WS2,WS3) are the matching per-layer weight scales. Both are // read from the export file's header (NnueLoadWeights) -- see // annotate_pgns/export_nnue_weights.py's CalibrateActivationScales, // which derives AS1-3 from real sampled positions (not a theoretical // bound) since these activations' true range isn't knowable from the // weights alone. extern CHAR g_rgfNnueFc1Weight[NNUE_L2][2 * NNUE_ACCUMULATOR_SIZE]; extern float g_rgfNnueFc1Bias[NNUE_L2]; extern CHAR g_rgfNnueFc2Weight[NNUE_L3][NNUE_L2]; extern float g_rgfNnueFc2Bias[NNUE_L3]; extern CHAR g_rgfNnueFc3Weight[NNUE_L3]; extern float g_rgfNnueFc3Bias; extern float g_rgfNnueActScale[3]; extern float g_rgfNnueWeightScale[3]; // Affine raw-to-real-score correction (a + b*raw), re-fit and baked // into every export by CalibrateScoreMapping -- see nnueeval.c's // declaration comment and the 2026-10-20 "keeping this stable across // retrains" conversation. Defaults (0,1) are the identity transform. extern float g_fNnueCalibA; extern float g_fNnueCalibB; extern FLAG g_fNnueWeightsLoaded; extern FLAG g_fUseNnueEval; FLAG NnueLoadWeights(CHAR *szFilename); SCORE NnueForward(SEARCHER_THREAD_CONTEXT *ctx); void NnueDumpActivationStats(SEARCHER_THREAD_CONTEXT *ctx); // Calibration-only lazy-eval probe (docs/NNUE.md's "Lazy-evaluation // tiering plan") -- a linear regression fit over fc1's own input (the // ReLU'd, concatenated accumulator), separate from the production // .nnue weight file/format entirely. NnueProbeScore is cheap (one // 512-wide dot product, no fc1/fc2/fc3) and exists purely to measure // how well it tracks NnueForward's real output, via the // CALIBRATE_MARGIN_SAFETY harness in eval.c -- not yet used to skip // any real computation. extern FLAG g_fNnueProbeLoaded; // Separate from g_fUseNnueEval/g_fNnueProbeLoaded -- controls whether // Eval()'s NNUE short-circuit actually TRUSTS the probe-based gate to // skip NnueForward (vs. just measuring it under CALIBRATE_MARGIN_ // SAFETY). Defaults FALSE, same convention as g_fUseNnueEval itself -- // an explicit opt-in, not auto-enabled just because weights/probe are // loaded. See docs/NNUE.md's "Lazy-evaluation tiering plan". extern FLAG g_fUseNnueLazyProbe; // Runtime experimentation knobs -- see nnueeval.c's comment. Flat // override for the lazy probe's margin (< 0 = use the calibrated // per-bucket table) and the super-lazy (material-only, no net) // pre-filter's own on/off + margin. extern INT g_iNnueLazyMarginOverride; extern FLAG g_fUseNnueSuperLazy; extern SCORE g_iNnueSuperLazyMargin; FLAG NnueProbeLoadWeights(CHAR *szFilename); SCORE NnueProbeScore(SEARCHER_THREAD_CONTEXT *ctx); #ifdef PERF_COUNTERS #define INC(x) ((x) += 1) #else #define INC(x) #endif // Dedicated gate for the move-ordering evidence-bucket harnesses // (countermove/move-pair hit tracking, CM/MP evidence buckets -- see // MOVE_ORDERING_IDEAS.md) -- deliberately a separate flag/macro from // INC/PERF_COUNTERS above, not nested under it, so this collection's // own cost (per-move IS_SAME_MOVE checks + bucket lookups) can be // toggled independently of the general perf-counter set. #ifdef MOVE_ORDERING_EVIDENCE_COLLECTION #define MOI_INC(x) ((x) += 1) #else #define MOI_INC(x) #endif #define BREAKPOINT SystemDebugBreakpoint() #define MIN(x, y) (((x) < (y)) ? (x) : (y)) #define MAX(x, y) (((x) > (y)) ? (x) : (y)) #if defined(_X86_) || defined(_X64_) // // Note: these are 32-bit int bit tricks, not actually x86-specific -- // they only require sizeof(int) == 4, which still holds under the LP64 // data model used by 64-bit (_X64_) builds. Gated on both so a SIXTYFOUR // build (which defines _X64_, not _X86_) still gets the branchless // versions instead of falling through to the plain-C fallbacks below. // // Note: MAXU, MINU and ABS_DIFF require arguments with the high order // bit CLEAR to work right. // // These are branchless constructs. MAXU and MINU are equivalent to // MIN and MAX (with the above restriction on inputs) // // MIN0 and MAX0 are equivalent to MAX(0, x) and MIN(0, x). These // macros have no restiction on argument type. // // ABS_DIFF is equivalent to abs(x - y). Again, x and y must have // their high-order bits CLEAR for this to work. // // Note: gcc generates code with cmovs so no need for MAXU/MINU on // that compiler. // #define MINU(x, y) \ (((((int)((x)-(y)))>>31) & ((x)-(y)))+(y)) #define MAXU(x, y) \ (((((int)((x)-(y)))>>31) & ((y)-(x)))+(x)) #define MIN0(x) \ ((x) & (((int)(x)) >> 31)) #define MAX0(x) \ ((x) & ~(((int)(x)) >> 31)) #define ABS_DIFF(a, b) \ (((b)-(a)) - ((((b) - (a)) & (((int)((b) - (a))) >> 31) ) << 1)) #endif // _X86_ || _X64_ #ifndef MINU #define MINU(x, y) (MIN((x), (y))) #endif #ifndef MAXU #define MAXU(x, y) (MAX((x), (y))) #endif #ifndef MIN0 #define MIN0(x) (MIN((x), 0)) #endif #ifndef MAX0 #define MAX0(x) (MAX((x), 0)) #endif #ifndef ABS_DIFF #define ABS_DIFF(a, b) (abs((a) - (b))) #endif #define FILE_DISTANCE(a, b) (ABS_DIFF(FILE((a)), FILE((b)))) #define RANK_DISTANCE(a, b) (ABS_DIFF(((a) & 0xF0), ((b) & 0xF0)) >> 4) #define REAL_DISTANCE(a, b) (MAXU(FILE_DISTANCE((a), (b)), \ RANK_DISTANCE((a), (b)))) #ifdef DEBUG #define DISTANCE(a, b) DistanceBetweenSquares((a), (b)) #else // // (a) and (b) are COOR, i.e. unsigned; the subtraction below must be // forced into signed arithmetic before it's used as an index into // g_pDistance (which points to the middle of g_uDistance so that // negative differences work). Without the cast, (a)-(b) is computed // as unsigned and wraps instead of going negative; on a 32-bit build // that wrapped value, added to a 32-bit pointer, happens to wrap back // to the right address by accident, but on a 64-bit build the 32-bit // unsigned result gets zero-extended (not sign-extended) into the // 64-bit index and reads wildly out of bounds. // #define DISTANCE(a, b) g_pDistance[(int)(a) - (int)(b)] #endif // DEBUG #define IS_EMPTY( square ) (!(square)) #define IS_OCCUPIED( square ) ((square)) #define IS_DEAD( listindex ) ((listindex) > 119) #define IS_ALIVE( listindex ) ((listindex) <= 119) #define ILLEGAL_COOR (0x88) #define EDGE_DISTANCE(c) (MIN(MIN(abs(RANK(c) - 7), RANK(c)), \ MIN(abs(FILE(c) - 7), FILE(c)))) #define NOT_ON_EDGE(c) ((((c) & 0xF0) != 0x00) && \ (((c) & 0xF0) != 0x70) && \ (((c) & 0x0F) != 0x00) && \ (((c) & 0x0F) != 0x07)) #define ON_EDGE(c) (RANK8(c) || \ RANK1(c) || \ FILEA(c) || \ FILEH(c)) #define CORNER_DISTANCE(c) (MAX(MIN((ULONG)(RANK(c) - 7), RANK(c)),\ MIN((ULONG)(FILE(c) - 7), FILE(c)))) #define IN_CORNER(c) (((c) == A8) || \ ((c) == A1) || \ ((c) == H8) || \ ((c) == H1)) #define WHITE_CORNER_DISTANCE(c) (MIN(DISTANCE((c), A8), \ DISTANCE((c), H1))) #define BLACK_CORNER_DISTANCE(c) (MIN(DISTANCE((c), H8), \ DISTANCE((c), A1))) #define SYM_SQ(c) ((7 - (((c) & 0xF0) >> 4)) << 4) \ | (7 - ((c) & 0x0F)) #define ARRAY_SIZE(a) (sizeof((a)) / sizeof((a[0]))) #define MAKE_PSQT(a, b, c, d) (((a) << 24) | ((b) << 16) | \ ((c) << 8) | (d)) #define TO64(x) ((x) & 0x7) + ((0x7 - ((x)>>4)) << 3) #define COOR_TO_BIT_NUMBER(c) (((((c) & 0x70) >> 1) | ((c) & 0x7))) #define SLOWCOOR_TO_BB(c) (1ULL << COOR_TO_BIT_NUMBER(c)) // Was BBSQUARE[COOR_TO_BIT_NUMBER(c)] (an L1 load off a 64-entry table) // -- measured (testbitboard.c's TestBitboards, fixed to use a volatile // sink so the comparison isn't dead-code-eliminated) consistently ~5-7% // slower than the pure-ALU shift on this hardware, so switched to match // SLOWCOOR_TO_BB's expression. "SLOW" in that macro's name reflects a // stale assumption (variable-count shifts being slow) that doesn't hold // on modern silicon; BBSQUARE itself stays -- still used directly (not // via this macro) where callers already have a bit index in hand and // indexing it avoids recomputing COOR_TO_BIT_NUMBER redundantly. #define COOR_TO_BB(c) (1ULL << COOR_TO_BIT_NUMBER(c)) #define SLOW_BIT_NUMBER_TO_COOR(b) ((((b) / 8) << 4) + ((b) & 7)) #define BIT_NUMBER_TO_COOR(b) ((((b) & 0xF8) << 1) | ((b) & 7)) #define BBRANK88 \ SLOWCOOR_TO_BB(A8) | SLOWCOOR_TO_BB(B8) | SLOWCOOR_TO_BB(C8) | \ SLOWCOOR_TO_BB(D8) | SLOWCOOR_TO_BB(E8) | SLOWCOOR_TO_BB(F8) | \ SLOWCOOR_TO_BB(G8) | SLOWCOOR_TO_BB(H8) #define BBRANK77 \ SLOWCOOR_TO_BB(A7) | SLOWCOOR_TO_BB(B7) | SLOWCOOR_TO_BB(C7) | \ SLOWCOOR_TO_BB(D7) | SLOWCOOR_TO_BB(E7) | SLOWCOOR_TO_BB(F7) | \ SLOWCOOR_TO_BB(G7) | SLOWCOOR_TO_BB(H7) #define BBRANK66 \ SLOWCOOR_TO_BB(A6) | SLOWCOOR_TO_BB(B6) | SLOWCOOR_TO_BB(C6) | \ SLOWCOOR_TO_BB(D6) | SLOWCOOR_TO_BB(E6) | SLOWCOOR_TO_BB(F6) | \ SLOWCOOR_TO_BB(G6) | SLOWCOOR_TO_BB(H6) #define BBRANK55 \ SLOWCOOR_TO_BB(A5) | SLOWCOOR_TO_BB(B5) | SLOWCOOR_TO_BB(C5) | \ SLOWCOOR_TO_BB(D5) | SLOWCOOR_TO_BB(E5) | SLOWCOOR_TO_BB(F5) | \ SLOWCOOR_TO_BB(G5) | SLOWCOOR_TO_BB(H5) #define BBRANK44 \ SLOWCOOR_TO_BB(A4) | SLOWCOOR_TO_BB(B4) | SLOWCOOR_TO_BB(C4) | \ SLOWCOOR_TO_BB(D4) | SLOWCOOR_TO_BB(E4) | SLOWCOOR_TO_BB(F4) | \ SLOWCOOR_TO_BB(G4) | SLOWCOOR_TO_BB(H4) #define BBRANK33 \ SLOWCOOR_TO_BB(A3) | SLOWCOOR_TO_BB(B3) | SLOWCOOR_TO_BB(C3) | \ SLOWCOOR_TO_BB(D3) | SLOWCOOR_TO_BB(E3) | SLOWCOOR_TO_BB(F3) | \ SLOWCOOR_TO_BB(G3) | SLOWCOOR_TO_BB(H3) #define BBRANK22 \ SLOWCOOR_TO_BB(A2) | SLOWCOOR_TO_BB(B2) | SLOWCOOR_TO_BB(C2) | \ SLOWCOOR_TO_BB(D2) | SLOWCOOR_TO_BB(E2) | SLOWCOOR_TO_BB(F2) | \ SLOWCOOR_TO_BB(G2) | SLOWCOOR_TO_BB(H2) #define BBRANK11 \ SLOWCOOR_TO_BB(A1) | SLOWCOOR_TO_BB(B1) | SLOWCOOR_TO_BB(C1) | \ SLOWCOOR_TO_BB(D1) | SLOWCOOR_TO_BB(E1) | SLOWCOOR_TO_BB(F1) | \ SLOWCOOR_TO_BB(G1) | SLOWCOOR_TO_BB(H1) #define BBFILEA \ SLOWCOOR_TO_BB(A1) | SLOWCOOR_TO_BB(A2) | SLOWCOOR_TO_BB(A3) | \ SLOWCOOR_TO_BB(A4) | SLOWCOOR_TO_BB(A5) | SLOWCOOR_TO_BB(A6) | \ SLOWCOOR_TO_BB(A7) | SLOWCOOR_TO_BB(A8) #define BBFILEB \ SLOWCOOR_TO_BB(B1) | SLOWCOOR_TO_BB(B2) | SLOWCOOR_TO_BB(B3) | \ SLOWCOOR_TO_BB(B4) | SLOWCOOR_TO_BB(B5) | SLOWCOOR_TO_BB(B6) | \ SLOWCOOR_TO_BB(B7) | SLOWCOOR_TO_BB(B8) #define BBFILEC \ SLOWCOOR_TO_BB(C1) | SLOWCOOR_TO_BB(C2) | SLOWCOOR_TO_BB(C3) | \ SLOWCOOR_TO_BB(C4) | SLOWCOOR_TO_BB(C5) | SLOWCOOR_TO_BB(C6) | \ SLOWCOOR_TO_BB(C7) | SLOWCOOR_TO_BB(C8) #define BBFILED \ SLOWCOOR_TO_BB(D1) | SLOWCOOR_TO_BB(D2) | SLOWCOOR_TO_BB(D3) | \ SLOWCOOR_TO_BB(D4) | SLOWCOOR_TO_BB(D5) | SLOWCOOR_TO_BB(D6) | \ SLOWCOOR_TO_BB(D7) | SLOWCOOR_TO_BB(D8) #define BBFILEE \ SLOWCOOR_TO_BB(E1) | SLOWCOOR_TO_BB(E2) | SLOWCOOR_TO_BB(E3) | \ SLOWCOOR_TO_BB(E4) | SLOWCOOR_TO_BB(E5) | SLOWCOOR_TO_BB(E6) | \ SLOWCOOR_TO_BB(E7) | SLOWCOOR_TO_BB(E8) #define BBFILEF \ SLOWCOOR_TO_BB(F1) | SLOWCOOR_TO_BB(F2) | SLOWCOOR_TO_BB(F3) | \ SLOWCOOR_TO_BB(F4) | SLOWCOOR_TO_BB(F5) | SLOWCOOR_TO_BB(F6) | \ SLOWCOOR_TO_BB(F7) | SLOWCOOR_TO_BB(F8) #define BBFILEG \ SLOWCOOR_TO_BB(G1) | SLOWCOOR_TO_BB(G2) | SLOWCOOR_TO_BB(G3) | \ SLOWCOOR_TO_BB(G4) | SLOWCOOR_TO_BB(G5) | SLOWCOOR_TO_BB(G6) | \ SLOWCOOR_TO_BB(G7) | SLOWCOOR_TO_BB(G8) #define BBFILEH \ SLOWCOOR_TO_BB(H1) | SLOWCOOR_TO_BB(H2) | SLOWCOOR_TO_BB(H3) | \ SLOWCOOR_TO_BB(H4) | SLOWCOOR_TO_BB(H5) | SLOWCOOR_TO_BB(H6) | \ SLOWCOOR_TO_BB(H7) | SLOWCOOR_TO_BB(H8) #define BBRANK72 \ BBRANK77 | BBRANK66 | BBRANK55 | BBRANK44 | BBRANK33 | BBRANK22 #define BBRANK62 \ BBRANK66 | BBRANK55 | BBRANK44 | BBRANK33 | BBRANK22 #define BBRANK52 \ BBRANK55 | BBRANK44 | BBRANK33 | BBRANK22 #define BBRANK42 \ BBRANK44 | BBRANK33 | BBRANK22 #define BBRANK32 \ BBRANK33 | BBRANK22 #define BBRANK27 \ BBRANK22 | BBRANK33 | BBRANK44 | BBRANK55 | BBRANK66 | BBRANK77 #define BBRANK37 \ BBRANK33 | BBRANK44 | BBRANK55 | BBRANK66 | BBRANK77 #define BBRANK47 \ BBRANK44 | BBRANK55 | BBRANK66 | BBRANK77 #define BBRANK57 \ BBRANK55 | BBRANK66 | BBRANK77 #define BBRANK67 \ BBRANK66 | BBRANK77 #include #include #include #include #include #include #include #include #include // // util.c // COMMAND(LearnPsqtFromPgn); COMMAND(GeneratePositionAndBestMoveSuite); CHAR * WalkPV(SEARCHER_THREAD_CONTEXT *ctx); CHAR * ReadNextGameFromPgnFile(FILE *pf); COOR DistanceBetweenSquares(COOR a, COOR b); void CDECL Trace(CHAR *szMessage, ...); void CDECL Log(CHAR *szMessage, ...); void CDECL Bug(CHAR *szMessage, ...); char * FindChunk(char *sz, ULONG uTargetChunk); FLAG BufferIsZeroed(BYTE *p, ULONG u); char * ColorToString(ULONG u); char * CoorToString(COOR c); char * ScoreToString(SCORE iScore); char * TimeToString(double d); ULONG AcquireSpinLock(volatile ULONG *pSpinlock); FLAG TryAcquireSpinLock(volatile ULONG *pSpinlock); void ReleaseSpinLock(volatile ULONG *pSpinLock); BYTE Checksum(BYTE *p, ULONG uSize); FLAG BackupFile(CHAR *szFile); void UtilPrintPV(SEARCHER_THREAD_CONTEXT *ctx, SCORE iAlpha, SCORE iBeta, SCORE iScore, MOVE mv); #define CANNOT_INITIALIZE_SPLIT (1) #define INCONSISTENT_POSITION (2) #define UNEXPECTED_SYSTEM_CALL_FAILURE (3) #define SHOULD_NOT_GET_HERE (4) #define GOT_ILLEGAL_MOVE_WHILE_PONDERING (5) #define CANNOT_OFFICIALLY_MAKE_MOVE (6) #define DETECTED_INCORRECT_INITIALIZATION (7) #define INCONSISTENT_STATE (8) #define FATAL_ACCESS_DENIED (9) #define TESTCASE_FAILURE (10) #define INITIALIZATION_FAILURE (11) void UtilPanic(ULONG uPanicCode, POSITION *pos, void *a1, void *a2, void *a3, char *file, ULONG line); // // main.c // #define LOGFILE_NAME "/usr/local/tmp/typhoon.log" extern FILE *g_pfLogfile; void Banner(void); FLAG PreGameReset(FLAG fResetBoard); // // system dependent exports (see win32.c or fbsd.c) // typedef ULONG (THREAD_ENTRYPOINT)(ULONG); #ifdef DEBUG ULONG GetHeapMemoryUsage(void); #endif void SystemDebugBreakpoint(void); CHAR * SystemStrDup(CHAR *p); double SystemTimeStamp(void); FLAG SystemDoesFileExist(CHAR *szFilename); void SystemDeferExecution(ULONG uMs); void SystemFreeMemory(void *pMem); void * SystemAllocateMemory(ULONG dwSizeBytes); FLAG SystemMakeMemoryReadOnly(void *pMemory, ULONG dwSizeBytes); FLAG SystemMakeMemoryNoAccess(void *pMemory, ULONG dwSizeBytes); FLAG SystemMakeMemoryReadWrite(void *pMemory, ULONG dwSizeBytes); FLAG SystemDependentInitialization(void); UINT64 FASTCALL SystemReadTimeStampCounter(void); // // This routine _must_ implement a full memory fence -- it is used in // util.c to implement spinlocks. // FLAG SystemCreateThread(THREAD_ENTRYPOINT *pEntry, ULONG uParam, ULONG *puHandle); FLAG SystemWaitForThreadToExit(ULONG uThreadHandle); FLAG SystemGetThreadExitCode(ULONG uThreadHandle, ULONG *puCode); FLAG SystemDeleteThread(ULONG uThreadHandle); CHAR * SystemGetDateString(void); CHAR * SystemGetTimeString(void); FLAG SystemCopyFile(CHAR *szSource, CHAR *szDest); FLAG SystemDeleteFile(CHAR *szFile); ULONG SystemCreateLock(void); FLAG SystemDeleteLock(ULONG u); FLAG SystemBlockingWaitForLock(ULONG u); FLAG SystemReleaseLock(ULONG u); ULONG SystemCreateSemaphore(ULONG u); FLAG SystemDeleteSemaphore(ULONG u); void SystemReleaseSemaphoreResource(ULONG u); void SystemObtainSemaphoreResource(ULONG u); // // fen.c // #define STARTING_POSITION_IN_FEN \ "rnbqkbnr/pppppppp/--------/8/8/--------/PPPPPPPP/RNBQKBNR w KQkq - 0 1" FLAG LooksLikeFen(char *szFen); char * PositionToFen(POSITION *p); FLAG FenToPosition(POSITION *p, char *szFen); FLAG ParseEpdRecord(char *szLineIn, char *szFen, ULONG cbFen, char *szId, ULONG cbId, char szMoves[][SMALL_STRING_LEN_CHAR], ULONG uMaxMoves, ULONG *puNumMoves, FLAG *pfIsAvoid); // // testfen.c // #ifdef TEST void TestFenCode(void); #endif // // piece.c // // INVERTED VALUE == (VALUE_QUEEN / VALUE_PIECE) * VALUE_PAWN; #define VALUE_PAWN 100 #define INVERT_PAWN 900 #define VALUE_KNIGHT 300 #define INVERT_KNIGHT 300 #define VALUE_BISHOP 300 #define INVERT_BISHOP 300 #define VALUE_ROOK 500 #define INVERT_ROOK 180 #define VALUE_QUEEN 975 #define INVERT_QUEEN 100 #define VALUE_KING (INFINITY_SCORE) #define INVERT_KING 1 #define VALUE_FULL_ARMY (8 * VALUE_PAWN) + (2 * VALUE_KNIGHT) + \ (2 * VALUE_BISHOP) + (2 * VALUE_ROOK) + \ VALUE_QUEEN + VALUE_KING #define VALUE_MAX_ARMY (9 * VALUE_QUEEN) + (2 * VALUE_KNIGHT) + \ (2 * VALUE_BISHOP) + (2 * VALUE_ROOK) + \ VALUE_KING typedef struct _PIECE_DATA { ULONG uValue; ULONG uValueOver100; ULONG uInvertedValue; CHAR *szName; } PIECE_DATA; extern PIECE_DATA g_PieceData[8]; #define PIECE_VALUE_OVER_100(p) (g_PieceData[PIECE_TYPE(p)].uValueOver100) // _ScoreAllMoves / _ScoreQSearchMoves{InclChecks,NoChecks} (generate.c) bake // a flat +120 move-ordering bias plus small MVV-LVA tie-break nudges into // winning/even captures'/promotions' iValue -- harmless for sorting, but // wrong once the value is used on the eval axis (compared against // material-scale margins). Back it out to recover the pure SEE/ // material-diff value. Used only inside DecodeMoveValueEstimate // (searchsup.c) -- that function is the single place that knows which of // generate.c's several distinct scoring conventions (MOVE_SCORING_SCHEME) // a given iValue was packed under and decodes accordingly; don't apply // this macro to a raw iValue anywhere else. #define MOVE_SCORE_ORDERING_BIAS(mv) \ (PIECE_VALUE_OVER_100((mv).pCaptured) + 120 + \ PIECE_VALUE_OVER_100((mv).pPromoted) - \ PIECE_VALUE_OVER_100((mv).pMoved)) extern ULONG PieceValueOver100(PIECE p); #define PIECE_VALUE(p) (g_PieceData[PIECE_TYPE(p)].uValue) extern ULONG PieceValue(PIECE p); #define INVERTED_PIECE_VALUE(p) (g_PieceData[PIECE_TYPE(p)].uInvertedValue) extern ULONG PieceInvertedValue(PIECE p); CHAR * PieceAbbrev(PIECE p); // // board.c // FLAG VerifyPositionConsistency(POSITION *pos, FLAG fContinueOnError); FLAG PositionsAreEquivalent(POSITION *p1, POSITION *p2); CHAR * DrawTextBoardFromPosition(POSITION *pos); void DumpPosition(POSITION *pos); CHAR * CastleInfoString(BITV bv); void SetRootToInitialPosition(void); // // move.c // // Each takes the mover's live NNUE_ACCUMULATOR pair (normally // ctx->sNnueAccum) and keeps it incrementally up to date for whatever // square(s) this call changes, for BOTH perspectives, unconditionally // -- including when the piece involved is a king (never true for // Lift/Place, which never touch kings; see MakeMove/UnmakeMove's own // comments on why a king move's OWN-perspective accumulator needs a // full recompute/restore layered on top at the call site, not inside // these primitives). // void SlidePiece(POSITION *pos, COOR cFrom, COOR cTo, NNUE_ACCUMULATOR sNnueAccum[2]); void SlidePawn(POSITION *pos, COOR cFrom, COOR cTo, NNUE_ACCUMULATOR sNnueAccum[2]); PIECE LiftPiece(POSITION *pos, COOR cSquare, NNUE_ACCUMULATOR sNnueAccum[2]); void PlacePiece(POSITION *pos, COOR cSquare, PIECE pPiece, NNUE_ACCUMULATOR sNnueAccum[2]); FLAG MakeMove(SEARCHER_THREAD_CONTEXT *ctx, MOVE mv); void UnmakeMove(SEARCHER_THREAD_CONTEXT *ctx, MOVE mv); FLAG MakeUserMove(SEARCHER_THREAD_CONTEXT *ctx, MOVE mvUser); void DumpMove(ULONG u); // // movesup.c // #define MOVE_TO_INDEX(mv) (((mv).uMove & 0xFFFF) + \ (0x10000 * GET_COLOR(mv.pMoved))) COOR FasterExposesCheck(POSITION *pos, COOR cRemove, COOR cLocation); COOR ExposesCheck(POSITION *pos, COOR cRemove, COOR cLocation); COOR ExposesCheckEp(POSITION *pos, COOR cTest, COOR cIgnore, COOR cBlock, COOR cKing); // board_representation/MOVEGEN_MIGRATION.md section 6b COOR FasterExposesCheckBB(POSITION *pos, COOR cRemove, COOR cLocation); COOR ExposesCheckBB(POSITION *pos, COOR cRemove, COOR cLocation); COOR ExposesCheckEpBB(POSITION *pos, COOR cTest, COOR cIgnore, COOR cBlock, COOR cKing); // Three-way choice matching GetAttacks's own pattern (MIGRATION.md // section 6): EXPOSESCHECK_BITBOARD defined -> the BB versions above; // else the mailbox versions declared right above this block. #if defined(EXPOSESCHECK_BITBOARD) #define ExposesCheck ExposesCheckBB #define FasterExposesCheck FasterExposesCheckBB #define ExposesCheckEp ExposesCheckEpBB #endif // Real mailbox declarations first, unconditionally, so the plain names // "IsAttacked"/"InCheck" are always genuinely declared -- the #define // below only affects *later* call sites in files that include this // header after this point, matching GetAttacks's own three-way-choice // pattern (chess.h, `GetAttacks` block) exactly. Declaring these after // the #define instead (an earlier mistake here, caught by testsee.c's // TestIsAttackedBB needing to call the real mailbox function by name) // would macro-substitute this very declaration too, leaving the real // name never actually declared anywhere. FLAG IsAttacked(COOR cTest, POSITION *pos, ULONG uSide); FLAG InCheck(POSITION *pos, ULONG uSide); FLAG IsAttackedBB(COOR cTest, POSITION *pos, ULONG uSide); FLAG FORCEINLINE InCheckBB(POSITION *pos, ULONG uSide); // ISATTACKED_BITBOARD defined -> the BB versions above resolve plain // "IsAttacked"/"InCheck" call sites from here on; else the mailbox // versions declared just above stay live. #if defined(ISATTACKED_BITBOARD) #define IsAttacked IsAttackedBB #define InCheck InCheckBB #endif FLAG SanityCheckMove(POSITION *pos, MOVE mv); FLAG LooksLikeFile(CHAR c); FLAG LooksLikeRank(CHAR c); FLAG LooksLikeCoor(CHAR *szData); CHAR * StripMove(CHAR *szMove); ULONG LooksLikeMove(CHAR *szData); void FORCEINLINE SelectBestWithHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u); void FORCEINLINE SelectBestNoHistory(SEARCHER_THREAD_CONTEXT *ctx, ULONG u); void FASTCALL SelectMoveAtRoot(SEARCHER_THREAD_CONTEXT *ctx, ULONG u); #define NOT_MOVE 0 #define MOVE_ICS 1 #define MOVE_SAN 2 COMMAND(PerftCommand); // // testmove.c // #ifdef TEST void TestLiftPlaceSlidePiece(void); void TestExposesCheck(void); void TestIsAttacked(void); void TestMakeUnmakeMove(void); #endif // // generate.c // #define SORT_THESE_FIRST (0x40000000) #define FIRST_KILLER (0x20000000) #define SECOND_KILLER (0x10000000) #define THIRD_KILLER (0x08000000) #define FOURTH_KILLER (0x04000000) #define GOOD_MOVE (0x02000000) #define STRIP_OFF_FLAGS (0x007FFFFF) // Countermove-match tier bonus, placed above GOOD_MOVE (see // generate.c's _ScoreAllMoves quiet-move branch) so a *sufficiently // evidenced* match (see COUNTERMOVE_EVIDENCE_THRESHOLD) escapes // "leftover" classification -- and the fIsLeftoverMove-gated LMP check -- // entirely, becoming its own tier alongside killers. Conservatively // placed below both ply-2 killer slots (FOURTH_KILLER/THIRD_KILLER): // the contested-node A/B harness never found clean evidence a // qualifying match should outrank killers, only that it beats them on // average when forced to compete -- revisit with real data before // moving it, don't just nudge it up. #define COUNTERMOVE_TIER_BONUS (0x03000000) // DROPPED (MOVE_ORDERING_IDEAS.md item 4, recapture bonus): a capture // landing on the opponent's last move's destination square showed a // real, well-calibrated FH% signal at SEE >= 0 (64-65% vs. a 49-52% // all-captures-at-the-same-SEE-sign baseline, consistent across all // three curated suites, large samples -- MOVE_ORDERING_EVIDENCE_ // COLLECTION's RECAPTURE_BUCKETS/u64RecaptureTries/FH vs. // u64CaptureBaselineTries/FH, both removed along with this). Two // magnitudes tried as a same-tier nudge in generate.c's // _ScoreAllMoves (s >= 0 capture branch), neither converted into a // tree-size win: 50 (over 3x the ~16-unit-wide MVV/LVA spread this // branch actually sorts by -- PIECE_VALUE_OVER_100(captured) - // PIECE_VALUE_OVER_100(mover), ranges ~112-128 across every capture // combination -- a near-dominant override, not a tiebreak) caused a // real, confirmed tactical loss (ECM.735, see MOVE_ORDERING_IDEAS.md); // 4 (inside that band) fixed the regression but still produced no // node-count improvement on any of the three curated suites. Same // shape as the move-pair backlog entry: a real FH% edge that // SelectBestWithHistory/MVV/LVA's existing ordering apparently already // captures most of, with no mechanism tried converting the residual // signal into a leaner tree. // // Evidence threshold gating the promotion above -- the same // history+continuation sum already used to score ordinary leftovers, // evaluated for the specific countermove-matched move. Calibration // (search.c's CM_EVIDENCE_BUCKETS harness) showed a match with zero // evidence performs identically to an unprivileged leftover (~0.6- // 0.85% FH); 10,000 is where the curve first clearly exceeds killer- // ply2's own filtered/elite FH% (53.92-64.07% vs. 34.85-51.38%) on all // three curated suites, the strongest defensible bar found. #define COUNTERMOVE_EVIDENCE_THRESHOLD (10000) // DROPPED (MOVE_ORDERING_IDEAS.md item 6, flee-to-safety revival): a // quiet move whose origin matched the cached en-prise hint // (FindEnprisePiece, dynamic.c), evidence-gated with the exact same // formula/threshold as COUNTERMOVE_TIER_BONUS/COUNTERMOVE_EVIDENCE_ // THRESHOLD above. Calibration found a real signal (evidence >= 10000 // hit 56-68% FH vs. a ~1.35-1.54% unprivileged-leftover baseline, // comparable to or exceeding countermove's own bar) -- but the // qualifying population was too rare (2,444-9,209 tries across an // entire suite run, vs. countermove's tens-to-hundreds-of-thousands) // to register any measurable sd10 node-count or solve-count effect // either way once implemented as a GOOD_MOVE-tier escape. Not a // magnitude-tuning problem like move-pair/recapture -- there simply // isn't enough volume for any mechanism acting on this signal to move // the aggregate needle. u64FleeTries/FH and the scoring/precompute // code were fully reverted. // // DROPPED (MOVE_ORDERING_IDEAS.md item 3, ply-2 move-pair bonus): three // mechanisms tried against a real, strongly-calibrated signal // (butterfly-history-gated match against the move two plies back hit // 53-66% FH in its top evidence band vs. ~1.2-1.4% unprivileged- // leftover baseline, on large samples across all three curated // suites) -- a GOOD_MOVE-tier escape (+1.4-1.6% nodes at sd10), a // dominant same-tier nudge at FLEE_BONUS's old magnitude (+2-20% // nodes, worse), and a scaled-down proportionate nudge (mixed, // net flat-to-negative, worst specifically on the smallest/noisiest // curated suite). No mechanism converted the signal into a tree-size // win; SelectBestWithHistory's existing PSQT+history+continuation // scoring apparently already captures most of what it offers. The // mvMovePair table and its evidence-bucket harness were removed along // with it -- re-derive from git history if this is ever revisited // with a different approach. // // FLEE_BONUS itself (flee-to-safety same-tier nudge, a quiet leftover // move whose origin square is currently en prise) is separately dead // code in movesup.c's SelectBestWithHistory (commented out) -- retired // after the GOOD_MOVE-tier version it replaced was shown, by evidence- // calibration, to perform at the plain-leftover baseline for the vast // majority of its (huge) zero-evidence population. Revive either only // if re-measured and shown to actually help. // EXPERIMENT: is history+continuation evidence actually predictive of // a countermove match's FH%? Bucket every tried countermove-matched // move by its accumulated evidence (g_HistoryCounters[piece][to] + // g_ContinuationHistory[prev,cur], the same sum already used to score // ordinary leftovers) and track tries/FH per bucket -- see search.c. // Independent of tier placement; COUNTERMOVE_TIER_BONUS should be 0 // while this runs so the calibration reflects unmodified behavior. // (CM_EVIDENCE_BUCKETS itself is defined earlier, before COUNTERS.) extern const int g_iQKDeltas[9]; extern const int g_iNDeltas[9]; extern const int g_iBDeltas[5]; extern const int g_iRDeltas[5]; void GenerateMoves(SEARCHER_THREAD_CONTEXT *ctx, MOVE mvOrderFirst, ULONG uType); // board_representation/MOVEGEN_MIGRATION.md section 3 step 1 -- see // generate.c for the routine description. Exposed non-static so // testgenerate.c's correctness/speed harness can call it directly // regardless of whether GENERATE_KNIGHT_BITBOARD is defined for this // build. BITBOARD FORCEINLINE _BuildFriendlySideBB(POSITION *pos, ULONG uSide); BITBOARD FORCEINLINE _BuildFullOccupiedBB(POSITION *pos); // board_representation/MOVEGEN_MIGRATION.md section 6b -- exposed so // movesup.c's ExposesCheckBB family can reuse the same magic-lookup // arithmetic as the Part A/B generators instead of duplicating it. BITBOARD _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, COOR cRook); void GenerateRook(MOVE_STACK *pStack, POSITION *pos, COOR cRook); void _GenerateBishopBB(MOVE_STACK *pStack, POSITION *pos, COOR cBishop); void GenerateBishop(MOVE_STACK *pStack, POSITION *pos, COOR cBishop); void _GenerateQueenBB(MOVE_STACK *pStack, POSITION *pos, COOR cQueen); // Whole-node dispatch pair -- see _GenerateAllMovesBB's block comment // in generate.c. Exposed (not static) so testgenerate.c can benchmark // them directly against each other by name. void _GenerateAllMoves(MOVE_STACK *pStack, POSITION *pos); void _GenerateAllMovesBB(MOVE_STACK *pStack, POSITION *pos); void _GenerateAllPawnMovesBB(MOVE_STACK *pStack, POSITION *pos, ULONG uSide); // board_representation/MOVEGEN_MIGRATION.md section 6a Phase 2 -- // exposed (not static) for testgenerate.c's future speed harness, // same convention as the Part A _Generate*BB functions. void _SaveMeKnightBB(MOVE_STACK *pStack, POSITION *pos, COOR cKnight, COOR cKing, BITBOARD bbTargetMask); void _SaveMeBishopBB(MOVE_STACK *pStack, POSITION *pos, COOR cBishop, COOR cKing, BITBOARD bbTargetMask); void _SaveMeRookBB(MOVE_STACK *pStack, POSITION *pos, COOR cRook, COOR cKing, BITBOARD bbTargetMask); void _SaveMeQueenBB(MOVE_STACK *pStack, POSITION *pos, COOR cQueen, COOR cKing, BITBOARD bbTargetMask); void _SaveMeAllPawnMovesBB(MOVE_STACK *pStack, POSITION *pos, ULONG uSide, COOR cKing, COOR cAttacker, BITBOARD bbTargetMask); void GenerateQueen(MOVE_STACK *pStack, POSITION *pos, COOR cQueen); void _GenerateKnightBB(MOVE_STACK *pStack, POSITION *pos, COOR cKnight); void GenerateKnight(MOVE_STACK *pStack, POSITION *pos, COOR cKnight); void GenerateWhiteKnight(MOVE_STACK *pStack, POSITION *pos, COOR cKnight); void _GenerateKingBB(MOVE_STACK *pStack, POSITION *pos, COOR cKing); void GenerateBlackKing(MOVE_STACK *pStack, POSITION *pos, COOR cKing); void GenerateWhiteKing(MOVE_STACK *pStack, POSITION *pos, COOR cKing); FLAG FORCEINLINE WouldGiveCheck(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv); // // testgenerate.c // #ifdef TEST void PlyTest(SEARCHER_THREAD_CONTEXT *ctx, ULONG uDepth, FLAG fRootPositionInCheck); void TestMoveGenerator(void); void TestLegalMoveGenerator(void); void TestNnueAccumulator(void); void TestGenerateKnightSpeed(void); void TestGenerateKingSpeed(void); void TestGenerateRookSpeed(void); void TestGenerateBishopSpeed(void); void TestGenerateQueenSpeed(void); void TestGenerateAllMovesSpeed(void); void TestFindUnblockedSquaresSpeed(void); #endif // // sig.c // extern UINT64 g_u64SigSeeds[128][7][2]; extern UINT64 g_u64PawnSigSeeds[128][2]; extern UINT64 g_u64CastleSigSeeds[16]; extern UINT64 g_u64EpSigSeeds[9]; void InitializeSigSystem(void); UINT64 ComputePawnSig(POSITION *pos); UINT64 ComputeSig(POSITION *pos); // // mersenne.c // void seedMT(unsigned int seed); unsigned int reloadMT(void); unsigned int randomMT(void); // // data.c // typedef struct _VECTOR_DELTA { UCHAR iVector[2]; signed char iDelta; signed char iNegDelta; } VECTOR_DELTA; extern ULONG g_uDistance[256]; extern ULONG *g_pDistance; extern VECTOR_DELTA g_VectorDelta[256]; extern VECTOR_DELTA *g_pVectorDelta; extern CHAR g_SwapTable[14][32][32]; extern SCORE _PSQT[14][128]; extern MOVE NULLMOVE; 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]; extern BITBOARD BBLEFT; extern BITBOARD BBRIGHT; extern BITBOARD BBWHITESQ; extern BITBOARD BBBLACKSQ; extern BITBOARD BBLEFTSIDE; extern BITBOARD BBRIGHTSIDE; extern BITBOARD BBSQUARE[64]; extern BITBOARD BBROOK_PAWNS; extern BITBOARD BBPRECEEDING_RANKS[8][2]; extern BITBOARD BBADJACENT_FILES[8]; extern BITBOARD BBADJACENT_FILES_INCLUSIVE[8]; extern BITBOARD BBADJACENT_RANKS[9]; // Ray-to-edge / knight-attack occupancy tables (data.c) -- built once // at startup, consumed by ray-walk mobility code and (per // board_representation/MIGRATION.md) the planned bbPieces-backed // GetAttacks/CountKingSafetyDefects primitive. extern BITBOARD g_RookRayToEdge[4][128]; extern BITBOARD g_RookRayAll[128]; extern const int g_RookRayDeltas[4]; extern const FLAG g_RookRayPositiveDir[4]; extern BITBOARD g_BishopRayToEdge[4][128]; extern BITBOARD g_BishopRayAll[128]; extern const int g_BishopRayDeltas[4]; extern const FLAG g_BishopRayPositiveDir[4]; extern BITBOARD g_KnightAttacksBB[128]; extern BITBOARD g_KingAttacksBB[128]; // 2026-09-28: joint 4D FH% table, baked from a 16-shard sd16 collection // run (see lmr_testing/2026-09-28_four_axis_joint_table_plan.md and // data.c's trailing comment for provenance/coverage). Cell = FH% * 1000 // (thousandths of a percent), 0xFFFF sentinel = below this run's // 500-try sample floor, don't trust yet. [depth][dumb][gap][late], // depth outermost for cache locality -- a whole node's move loop shares // one depth-slice (11*11*17 = 2,057 cells, ~4KB). Not yet consumed by // search.c -- phase 2 (LMR replacement) is a separate, later step. // // 2026-10-21: late axis widened 16 -> 17. Shipped late bucket 0 is now // dedicated SOLELY to the whole-node sample (raw late == 0, "will // something at this node fail high" -- no specific move), not shared // with the real first-leftover-move curve the way the old 16-bucket // shape coincidentally did. See searchsup.c's _ShipLateBucket and // dual_table_run_nnue/README.md's "whole-node alpha sample" section. // g_uJointFHThousandths (HCE) carries a PLACEHOLDER in that bucket, // duplicated from g_uJointFHThousandths_NNUE's real measurement, since // this table's own collection build predates the whole-node // instrumentation -- see data.c's header comment on this array, and // dual_table_run/README.md, for how/when that gets replaced with a // real HCE-side measurement. extern USHORT g_uJointFHThousandths[12][11][11][17][11]; // 2026-10-21: NNUE-eval counterpart of g_uJointFHThousandths above -- // same shape, collected against the NNUE evaluator instead of HCE // (see ../dual_table_run_nnue/). Both tables are compiled in; which // one a lookup reads is chosen at runtime by // _JointTablePredictedFHThousandths (searchsup.c) based on // (g_fUseNnueEval && g_fNnueWeightsLoaded) -- NOT a build-time // #ifdef, since NNUE is itself already a runtime switch in this // binary (see eval.c, command.c's "nnue on"/"nnue off"). extern USHORT g_uJointFHThousandths_NNUE[12][11][11][17][11]; // 2026-10-10: beta-side (static-null-move) 4D FH% table -- see data.c's // header. [depth][gapBeta][ownImprove][enemyImprove]. Cell stores the // GAP TO 100% FH rate (thousandths of a percentage point), not FH% // itself -- see _BetaTablePredictedFHThousandths's comment for why. extern USHORT g_uBetaFHThousandths[12][21][11][11]; // 2026-10-21: NNUE-eval counterpart of g_uBetaFHThousandths above -- // same shape/encoding, collected against the NNUE evaluator. Runtime- // selected the same way as g_uJointFHThousandths_NNUE above (see // _BetaTablePredictedFHThousandths, searchsup.c). extern USHORT g_uBetaFHThousandths_NNUE[12][21][11][11]; // 2026-09-15: king-ring bitboard split for _EvalKing's linear-SCORE // rewrite -- front/back mirror EmptyAttackedSquare's old color-dependent // asymmetry and eval_tune/train_king_safety_model.py's BUCKET_SLOTS // (WHITE's front = the -17/-16/-15 delta triplet, BLACK's = +15/+16/+17); // side is the two same-rank neighbors (-1/+1), color-independent. // g_KingRingFrontBB[uColor][c] | g_KingRingBackBB[uColor][c] | // g_KingRingSideBB[c] == g_KingAttacksBB[c] always. Precomputed once at // startup (InitializeKingAttackTables, data.c) so the hot ring-scan can // intersect these with attack bitboards and CountBits() instead of // branching per square. extern BITBOARD g_KingRingFrontBB[2][128]; extern BITBOARD g_KingRingBackBB[2][128]; extern BITBOARD g_KingRingSideBB[128]; extern BITBOARD g_PawnAttackOriginBB[2][128]; extern BITBOARD g_RookOccupancyMask[128]; extern BITBOARD g_BishopOccupancyMask[128]; extern BITBOARD g_RookMagic[128]; extern BITBOARD g_BishopMagic[128]; extern ULONG g_RookMagicShift[128]; extern ULONG g_BishopMagicShift[128]; extern BITBOARD *g_RookAttackTable[128]; extern BITBOARD *g_BishopAttackTable[128]; void InitializeWhiteSquaresTable(void); void InitializeVectorDeltaTable(void); void InitializeSwapTable(void); void InitializeDistanceTable(void); void InitializeRookRayTables(void); void InitializeBishopRayTables(void); void InitializeKnightAttackTables(void); void InitializeKingAttackTables(void); void InitializePawnAttackOriginTable(void); void InitializeBitboardMagic(void); #ifdef DEBUG ULONG CheckVectorWithIndex(int i, ULONG uColor); #define CHECK_VECTOR_WITH_INDEX(i, color) \ CheckVectorWithIndex(i, color) int DirectionBetweenSquaresWithIndex(int i); #define CHECK_DELTA_WITH_INDEX(i) \ DirectionBetweenSquaresWithIndex(i) int DirectionBetweenSquaresFromTo(COOR, COOR); #define DIRECTION_BETWEEN_SQUARES(from, to) \ DirectionBetweenSquaresFromTo(from, to) int NegativeDirectionBetweenSquaresWithIndex(int i); #define NEG_DELTA_WITH_INDEX(i) \ NegativeDirectionBetweenSquaresWithIndex(i) FLAG IsSquareWhite(COOR c); #define IS_SQUARE_WHITE(c) \ IsSquareWhite(c) #else #define CHECK_VECTOR_WITH_INDEX(i, color) \ (g_pVectorDelta[(i)].iVector[(color)]) #define CHECK_DELTA_WITH_INDEX(i) \ (g_pVectorDelta[(i)].iDelta) #define DIRECTION_BETWEEN_SQUARES(cFrom, cTo) \ CHECK_DELTA_WITH_INDEX((int)(cFrom) - (int)(cTo)) #define NEG_DELTA_WITH_INDEX(i) \ (g_pVectorDelta[(i)].iNegDelta) #define IS_SQUARE_WHITE(c) \ (g_fIsWhiteSquare[(c)]) #endif // // san.c // MOVE ParseMoveSan(CHAR *szInput, POSITION *pos); CHAR * MoveToSan(MOVE mv, POSITION *pos); // // testsan.c // void TestSan(void); // // list.c // void InitializeListHead(IN DLIST_ENTRY *pListHead); FLAG IsListEmpty(IN DLIST_ENTRY *pListHead); FLAG RemoveEntryList(IN DLIST_ENTRY *pEntry); DLIST_ENTRY * RemoveHeadList(IN DLIST_ENTRY *pListHead); DLIST_ENTRY * RemoveTailList(IN DLIST_ENTRY *pListHead); void InsertTailList(IN DLIST_ENTRY *pListHead, IN DLIST_ENTRY *pEntry); void InsertHeadList(IN DLIST_ENTRY *pListHead, IN DLIST_ENTRY *pEntry); // // command.c // void ParseUserInput(FLAG fSearching); void ExecuteCommandLineDirectly(CHAR *szLine, POSITION *pos); FLAG InitializeCommandSystem(void); void CleanupCommandSystem(void); // // input.c // void InitInputSystemInBatchMode(void); ULONG InitInputSystemWithDedicatedThread(void); void PushNewInput(CHAR *buf); CHAR * PeekNextInput(void); CHAR * ReadNextInput(void); CHAR * BlockingReadInput(void); ULONG NumberOfPendingInputEvents(void); volatile extern FLAG g_fExitProgram; // // ics.c // MOVE ParseMoveIcs(CHAR *szInput, POSITION *pos); CHAR * MoveToIcs(MOVE mv); // // testics.c // void TestIcs(void); // // gamelist.c // typedef enum _ERESULT { RESULT_BLACK_WON = -1, RESULT_DRAW = 0, RESULT_WHITE_WON = 1, RESULT_IN_PROGRESS, RESULT_ABANDONED, RESULT_UNKNOWN } ERESULT; typedef struct _GAME_RESULT { ERESULT eResult; CHAR szDescription[256]; } GAME_RESULT; typedef struct _GAME_PLAYER { CHAR *szName; CHAR *szDescription; FLAG fIsComputer; ULONG uRating; } GAME_PLAYER; typedef struct _GAME_HEADER { CHAR *szGameDescription; CHAR *szLocation; GAME_PLAYER sPlayer[2]; FLAG fGameIsRated; UINT64 u64OpeningSig; GAME_RESULT result; CHAR *szInitialFen; } GAME_HEADER; typedef struct _GAME_MOVE { DLIST_ENTRY links; ULONG uNumber; MOVE mv; CHAR *szComment; CHAR *szDecoration; CHAR *szMoveInSan; CHAR *szMoveInIcs; CHAR *szUndoPositionFen; SCORE iMoveScore; UINT64 u64PositionSigAfterMove; UINT64 u64PositionSigBeforeMove; volatile FLAG fInUse; } GAME_MOVE; typedef struct _GAME_DATA { GAME_HEADER sHeader; DLIST_ENTRY sMoveList; } GAME_DATA; extern GAME_DATA g_GameData; POSITION * GetRootPosition(void); FLAG SetRootPosition(CHAR *szFen); void ResetGameList(void); ULONG GetMoveNumber(ULONG uColor); void SetGameResultAndDescription(GAME_RESULT result); GAME_RESULT GetGameResult(void); void SetMyName(void); void SetOpponentsName(CHAR *sz); void SetMyRating(ULONG u); void SetOpponentsRating(ULONG u); void DumpGameList(void); void TellGamelistThatIPlayColor(ULONG u); void DumpPgn(void); FLAG LoadPgn(CHAR *szPgn); ULONG CountOccurrancesOfSigInOfficialGameList(UINT64 u64Sig); FLAG DoesSigAppearInOfficialGameList(UINT64 u64Sig); FLAG OfficiallyTakebackMove(void); FLAG OfficiallyMakeMove(MOVE mv, SCORE iMoveScore, FLAG fFast); GAME_MOVE * GetNthOfficialMoveRecord(ULONG n); void MakeStatusLine(void); FLAG IsLegalDrawByRepetition(void); // // script.c // COMMAND(ScriptCommand); COMMAND(EpdCommand); COMMAND(SolutionCommand); COMMAND(AvoidCommand); COMMAND(IdCommand); void PostMoveTestSuiteReport(SEARCHER_THREAD_CONTEXT *); FLAG CheckTestSuiteMove(MOVE mv, SCORE iScore, ULONG uDepth); FLAG WeAreRunningASuite(void); // // vars.c // COMMAND(SetCommand); // // root.c // extern ULONG g_uSoftExtendLimit; extern ULONG g_uHardExtendLimit; extern volatile MOVE_TIMER g_MoveTimer; extern ULONG g_uExtensionReduction[MAX_PLY_PER_SEARCH]; #ifdef PERF_COUNTERS #define KEEP_TRACK_OF_FIRST_MOVE_FHs(x) \ ctx->sCounters.tree.u64BetaCutoffs++; \ if (x) \ { \ ctx->sCounters.tree.u64BetaCutoffsOnFirstMove++;\ } #else #define KEEP_TRACK_OF_FIRST_MOVE_FHs(x) #endif #define GAME_NOT_OVER 0 #define GAME_WHITE_WON 1 #define GAME_BLACK_WON 2 #define GAME_DRAW_STALEMATE 3 #define GAME_DRAW_REPETITION 4 #define GAME_ONE_LEGAL_MOVE 5 #define GAME_DRAW_FIFTY_MOVES_WO_PROGRESS 6 GAME_RESULT Think(POSITION *pos); GAME_RESULT Ponder(POSITION *pos); GAME_RESULT Iterate(SEARCHER_THREAD_CONTEXT *ctx); void SetMoveTimerForSearch(FLAG fSwitchOver, ULONG uColor); void SetMoveTimerToThinkForever(void); void ClearRootNodecountHash(void); // // draw.c // extern SCORE g_iDrawScore[2]; FLAG IsDraw(SEARCHER_THREAD_CONTEXT *ctx); // // search.c // #define QPLIES_OF_NON_CAPTURE_CHECKS (2) // Which Eval() exit tier supplied a given piPositional value -- // PLY_INFO.uLastPositionalSource above. Was diagnostic-only // (CALIBRATE_QSEARCH_FUTILITY); promoted to unconditional 2026-09-08 // once FUTILITY_BASE_MARGIN_BY_SOURCE (search.c) made it load-bearing // for real search behavior, not just measurement. #define EVAL_POSITIONAL_SOURCE_FULL (0) #define EVAL_POSITIONAL_SOURCE_LAZY (1) #define EVAL_POSITIONAL_SOURCE_SUPERLAZY (2) // Added 2026-10-16 (docs/NNUE.md's "Lazy-evaluation tiering plan"): // Eval()'s NNUE short-circuit never set uLastPositionalSource at all // before this -- every NNUE-sourced score silently inherited whatever // stale value a prior classic-Eval() call left in that ply's PLY_INFO // slot (or the C zero-init default, EVAL_POSITIONAL_SOURCE_FULL, // if none ever ran), so QSearch futility pruning was using an // unexamined, accidentally-conservative margin for every NNUE score, // with no distinction between the full forward pass (exact) and the // lazy accumulator-probe exit (real, measured error). These two give // each NNUE source its own tier, same as the three classic ones. #define EVAL_POSITIONAL_SOURCE_NNUE_FULL (3) #define EVAL_POSITIONAL_SOURCE_NNUE_LAZY (4) #define EVAL_POSITIONAL_SOURCE_COUNT (5) // Per-tier qsearch futility base margins -- see search.c's // FUTILITY_BASE_MARGIN_BY_SOURCE for the full derivation. Named here // (rather than only as array entries in search.c) so eval.c's // piPositional floor -- which only ever applies on the full-eval // exit -- can reference the matching tier's constant directly instead // of a stale flat value. #define FUTILITY_BASE_MARGIN_FULL (450) // 2026-09-17: LAZY raised 225->350, SUPERLAZY lowered 275->175, after // eval.c's lazy/super-lazy iPositional stopped being flat 0 and became // a real, signed p50-based typical-value estimate (see eval.c's // iKingP50ByAttackerArmyOnly/iResidualP50ByOwnArmy). Re-measured via // `calibrate qsearchfutility` (CALIBRATE_QSEARCH_FUTILITY build, // tests/twic_sample.ep_, 1500 real-game positions, sd 6) at the OLD // margins first, to see what the iPositional change alone had done: // generic capture/promo gate, surprise rate (wrongly-rejected-move // rate) by distance short of the futility bar, summed over piPositional // buckets: // source margin(old) dist0-25 25-50 50-100 100-200 200-400 400+ n // full 450 (n/a) 1.91% 1.72% 1.80% 1.30% 0.46% 0.11% 135,031 // regular lazy 225 6.22% 2.89% 2.07% 1.80% 1.58% 0.16% 1,024,889 // super lazy 275 1.23% 0.98% 0.90% 0.60% 0.24% 0.12% 1,813,965 // Regular lazy got MORE dangerous than its original 2026-09-08 // derivation (was 1.26% max at dist0-25; now 6.22%) -- the cross-tab // by piPositional bucket at dist0-25 showed why: piPositional<0 alone // carries an 8.9% surprise rate (n=66,040) vs. 1.9%/1.4% for the >=0 // buckets -- piPositional is genuinely predictive (validates the p50 // approach) but 225 wasn't wide enough for the negative population. // Super lazy got safer (was 7.96% at dist0-25; now 1.23%), real // headroom to tighten for speed. New values chosen by reading where // each tier's current worst bucket would land after the shift (not // guessed): LAZY +125 moves today's risky dist0-25 population into // territory today's data shows is ~1.8% (matching full-eval's own risk // level); SUPERLAZY -100 moves today's safe dist100-200 bucket (0.60%) // into the new near-boundary zone, still comfortably safe. FULL is // untouched -- its iPositional computation (full-eval exit) wasn't // changed this session, and its numbers above are consistent with the // original 2026-09-08 derivation. Re-derive with `calibrate // qsearchfutility` if iPositional's construction changes again. #define FUTILITY_BASE_MARGIN_LAZY (350) #define FUTILITY_BASE_MARGIN_SUPERLAZY (175) // Placeholder starting points, 2026-10-16 -- NOT yet calibrated via // `calibrate qsearchfutility` against real data (see // EVAL_POSITIONAL_SOURCE_NNUE_FULL/NNUE_LAZY's own comment). NNUE_FULL // starts equal to classic FULL (both are exact, non-approximate static // evals -- a reasonable prior pending real measurement); NNUE_LAZY // starts equal to classic LAZY (both are an approximate exit one tier // down from exact). Re-derive both for real before trusting them. // 2026-10-16: first real data (twic_sample.ep_, 1500 positions, sd 6) // showed NNUE_FULL's 7.41% surprise rate at 450 in the checking- // capture/promo gate. Widening to 650 was then tried and tested as // NO-OP (identical 7.41%/n) -- root-caused to iFutility's MAX0 clamp // saturating this source's population 100.00% of the time (mean // pre-clamp -589.3) -- the margin constant is never actually read for // ~all of this test pool, so neither 450 nor 650 is "validated," both // are simply untested by this data. Reverted to 450 (matches classic // FULL) as the more defensible default pending real evidence either // way: when NNUE_FULL surprises DO occur (in whatever small // non-saturated slice exists, not yet isolated), their magnitude runs // smaller than classic FULL's own surprises (mean 477.5 vs 938.7cp, // >100cp 9.88% vs 28.74%, twic_sample.ep_ sd6) -- no evidence NNUE's // rare misses are more expensive, so no reason to prefer a wider, // unvalidated number over HCE's own long-trusted one. // // TODO: find and calibrate against the actual non-saturated NNUE_FULL // population specifically (likely nodes where iEval sits close to // iAlpha rather than comfortably above it -- the saturated case is // "iEval way above iAlpha", so look at the complement) before trusting // any NNUE_FULL margin as real, NNUE-specific-validated rather than a // borrowed HCE default. // // NNUE_LAZY=200 (tightened from 350) IS genuinely validated -- real // headroom confirmed (only 24.76% of its population even hits the // clamp, unlike NNUE_FULL's 100%), surprise rate landed in a sane // 1.3-2.1% range on real data, and its own surprise magnitudes // (mean 5085.3, >100cp 19.45%) run well below classic regular lazy's // (mean 11030.2, >100cp 69.73%). Keep this one. #define FUTILITY_BASE_MARGIN_NNUE_FULL (450) #define FUTILITY_BASE_MARGIN_NNUE_LAZY (200) // Measured: disabling this entirely (see lmr_testing/RESULTS.md) is a // clear net loss across ringers/confident_quick/hard_quick, so IID itself // is load-bearing. The "is the top move crappy" gate in search.c's DO_IID // block still misclassifies ordinary killer moves as crappy (see the fix // there) -- that's the next thing being tuned, not whether IID exists. #define DO_IID #define IID_R_FACTOR (TWO_PLY + HALF_PLY) #ifndef MP #define WE_SHOULD_STOP_SEARCHING (g_MoveTimer.bvFlags & TIMER_STOPPING) #else #define WE_SHOULD_STOP_SEARCHING ((g_MoveTimer.bvFlags & TIMER_STOPPING) || \ (ThreadUnderTerminatingSplit(ctx))) #endif SCORE FASTCALL QSearch(SEARCHER_THREAD_CONTEXT *ctx, SCORE iAlpha, SCORE iBeta); SCORE FASTCALL Search(SEARCHER_THREAD_CONTEXT *ctx, SCORE iAlpha, SCORE iBeta, ULONG uDepth); // // searchsup.c // SCORE ComputeMoveScore(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv, IN ULONG uMoveNum); SCORE DecodeMoveValueEstimate(IN MOVE mv, IN SCORE iRawValue, IN MOVE_SCORING_SCHEME eScheme); FLAG ThreadUnderTerminatingSplit(SEARCHER_THREAD_CONTEXT *); // LMR reduction table -- built once at startup (InitLMRTable, dynamic.c), // no floating point in the search hot path. [depth in plies][move number, // clamped]. Value is a ply-fraction in ONE_PLY units, i.e. directly // usable as a negative iExtend. #define LMR_TABLE_MAX_MOVES (63) extern SCORE g_iLMRQuietReduction[MAX_PLY_PER_SEARCH + 1][LMR_TABLE_MAX_MOVES + 1]; void InitializeLMRTable(void); FLAG CanWeReduceOrPruneMove(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv, IN ULONG x, IN ULONG uLegalMoves, IN SCORE iExtend, IN SCORE iAlpha, IN FLAG fThreat, IN FLAG fIsLeftoverMove); INT FORCEINLINE GetLMRReduction(IN SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uRemainingDepth, IN ULONG uLegalMoves, IN MOVE mv, IN INT iExtend, IN ULONG uFHAttempts, IN ULONG uFHPctThousandths, IN ULONG uPredictedFH, OUT ULONG *puExtraTier); FLAG ShouldLatePrune(IN SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uRemainingDepth, IN ULONG uLegalMoves, IN ULONG uFHAttempts, IN ULONG uFHPctThousandths, IN ULONG uPredictedFH); SCORE EstimateHangingMaterialCp(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv, IN ULONG uMoveNum, IN BITBOARD bbMinorAttacksSnapshot[2], IN BITBOARD bbRookAttacksSnapshot[2]); ULONG HangCpBucket(SCORE iHangCp); ULONG GapCpBucket(SCORE iGapCp); ULONG MoveNumBucket(ULONG uMoveNum); ULONG DepthBucket(ULONG uCandDepth); ULONG ImproveBucket(SCORE iImprovement); ULONG GapBetaBucket(SCORE iGapBeta); USHORT _JointTablePredictedFHThousandths(IN ULONG uRawDepthBucket, IN ULONG uRawDumbBucket, IN ULONG uRawGapBucket, IN ULONG uRawLateBucket, IN ULONG uRawImproveBucket); ULONG _BetaTablePredictedFHThousandths(IN ULONG uRawDepthBucket, IN ULONG uGapBetaBucket, IN ULONG uOwnImproveBucket, IN ULONG uEnemyImproveBucket); ULONG _RefineNodeTypeGuess(IN SEARCHER_THREAD_CONTEXT *ctx, IN SCORE iEval, IN SCORE iAlpha, IN SCORE iBeta, IN ULONG uDepth, IN SCORE iEnemyImprovement, IN ULONG uRawImproveB); FLAG WeShouldTryNullmovePruning(IN SEARCHER_THREAD_CONTEXT *ctx, IN SCORE iAlpha, IN SCORE iBeta, IN SCORE iEval, IN SCORE iImprovement, IN ULONG uNullDepth); FLAG TryNullmovePruning(IN OUT SEARCHER_THREAD_CONTEXT *ctx, IN OUT FLAG *pfThreat, IN SCORE iAlpha, IN SCORE iBeta, IN ULONG uNullDepth, IN OUT INT *piOrigExtend, OUT SCORE *piNullScore, IN ULONG uNullRBucket); void UpdatePV(SEARCHER_THREAD_CONTEXT *ctx, MOVE mv); FLAG CheckInputAndTimers(IN SEARCHER_THREAD_CONTEXT *ctx); void ComputeReactionToCheckExtension(IN OUT SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uGenFlags, IN ULONG uMoveNum, IN SCORE iRoughEval, IN SCORE iAlpha, IN OUT INT *piExtend, IN OUT INT *piOrigExtend); SCORE GetMoveSee(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv, IN ULONG uMoveNum); void FORCEINLINE ComputeMoveExtension(IN OUT 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); // SearchOneMove's return value -- what Search()'s move loop (and, once // unified, HelpSearch()'s) should do next. An enum instead of the // goto/continue control flow the inline code used to use directly, // since those targets don't exist inside a called function. typedef enum _MOVE_KERNEL_RESULT { MOVE_KERNEL_SEARCHED, // move was made, searched, and unmade; *piScore valid MOVE_KERNEL_PRUNED, // late-move-pruned before searching; not made, *piScore invalid MOVE_KERNEL_ILLEGAL, // MakeMove refused mv (left mover's own king in check); // nothing else ran, *piScore invalid, *puLegalMoves unchanged MOVE_KERNEL_ABORTED // search was stopped mid-move; mv WAS made, searched, and // unmade (cleanup already ran) before this was detected; // *piScore is a partial/real result, treat as this move's score } MOVE_KERNEL_RESULT; // The per-move kernel hoisted out of Search()'s move loop (search.c) -- // see UNIFY_SEARCH.md for why and the full accounting of every // parameter below. FORCEINLINE + -flto is what makes the long // parameter list free: this is the same convention WouldGiveCheck // (generate.c) already uses to stay inlined across translation units // despite being called from half a dozen different .c files -- once // actually inlined there is no call/ret/stack-marshaling left at all, // every parameter just becomes an ordinary local in the caller's own // inlined expansion. Confirm it still inlines (not just assume) if // this is ever moved or its signature grows further. MOVE_KERNEL_RESULT FORCEINLINE SearchOneMove(IN SEARCHER_THREAD_CONTEXT *ctx, IN MOVE mv, IN ULONG x, // this move's slot in ctx->sMoveStack, // AFTER the caller's own index was // incremented past it (same convention // search.c's inline code used -- every // "this move's own index" use inside // the kernel computes (x - 1) itself) IN FLAG fIsGeneratedMoveStage, // (uStage == TRY_GENERATED_MOVES) // in search.c's own terms -- passed as // a plain FLAG since TRY_HASH_MOVE/ // TRY_GENERATED_MOVES are search.c- // private #defines, not visible here. // Needed only for GetMoveSee's index arg IN ULONG uDepth, IN SCORE iAlpha, IN SCORE iBeta, IN SCORE iEval, IN INT iPositionalExtend, IN FLAG fIsLeftoverMove, IN FLAG fThreat, IN FLAG fFullWindow, // TRUE only for the actual first move // tried at this node -- explicit, not // inferred from an iBestScore sentinel // the way search.c's inline code did // (see UNIFY_SEARCH.md on why that // matters once HelpSearch() also calls // this: its first move off the shared // pool is never the node's first move) IN ULONG uRawDepthB, // node-level hoists, loop-invariant -- IN ULONG uRawImproveB, // caller computes once, not per-move IN BITBOARD bbMinorAttacksSnapshot[2], IN BITBOARD bbRookAttacksSnapshot[2], #ifdef MOVE_ORDERING_EVIDENCE_COLLECTION IN FLAG fThisMoveIsCountermoveMatch, IN ULONG uCMEvidenceBucket, #endif #ifdef TEST_NULL IN FLAG fNullCalibEligible, IN ULONG uNullCalibBucket, OUT UINT64 *puNullCalibFirstMoveNodeCost, #endif IN OUT ULONG *puLegalMoves, OUT SCORE *piScore #ifdef PERF_COUNTERS , OUT FLAG *pfMoveIsPruneReduceEligible , OUT ULONG *puThisMoveHangBucket , OUT ULONG *puThisMoveGapBucket , OUT ULONG *puThisMoveNumBucket , OUT ULONG *puThisMoveDepthBucket #endif ); SCORE RescoreMovesViaSearch(IN SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uDepth, IN SCORE iAlpha, IN SCORE iBeta); FLAG MateDistancePruningCutoff(IN ULONG uPly, IN FLAG fInCheck, IN OUT SCORE *piBestScore, IN OUT SCORE *piAlpha, IN OUT SCORE *piBeta); FLAG FORCEINLINE CommonSearchInit(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piAlpha, IN OUT SCORE *piBeta, IN OUT SCORE *piScore); ULONG SelectNullmoveRFactor(IN SEARCHER_THREAD_CONTEXT *ctx, IN INT uDepth); #define VERIFY_BEFORE (1) #define VERIFY_AFTER (2) FLAG SanityCheckMoves(IN SEARCHER_THREAD_CONTEXT *ctx, IN ULONG uCurrent, IN ULONG uType); // // testsearch.c // FLAG TestSearch(void); FLAG TestQSearchSanity(void); // // see.c // #define SEE_HEAPS 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 FORCEINLINE SeeFastSign(POSITION *pos, MOVE mv); typedef struct _SEE_THREESOME { PIECE pPiece; COOR cLoc; ULONG uVal; } SEE_THREESOME; typedef struct _SEE_LIST { ULONG uCount; SEE_THREESOME data[16]; } SEE_LIST; SCORE ControlsSquareMinusPiece(ULONG uSide, POSITION *pos, COOR c, COOR cIgnore); // // testsee.c // SCORE DebugSEE(POSITION *pos, MOVE mv); void TestGetAttacks(void); void TestIsAttackedBB(void); void TestSeeFastSign(void); void TestWhoAttacksSquareMagic(void); // // hash.c // #define NUM_HASH_ENTRIES_PER_LINE 4 #define HASH_FLAG_EXACT 0x1 #define HASH_FLAG_LOWER 0x2 #define HASH_FLAG_UPPER 0x4 #define HASH_FLAG_VALID_BOUNDS 0x7 #define HASH_FLAG_THREAT 0x8 #define HASH_FLAG_DIRTY 0xF0 typedef struct _HASH_ENTRY { // Lockless-hash data word (Hyatt/Crafty scheme, see hash.c's // module comment): mv/uDepth/bvFlags/iValue pack into one aligned // UINT64 (u64Data) so hash.c can read/write them as a single word // alongside u64Sig, which stores (realKey ^ u64Data) rather than // the plain key. Every existing pHash->mv/uDepth/bvFlags/iValue // access keeps compiling unchanged via this anonymous union/struct // (same idiom MOVE itself already uses above). union { struct { MOVE mv; // 0 1 2 3 UCHAR uDepth; // 4 UCHAR bvFlags; // 5 ==> d d d d | thr up low exact signed short iValue; // 6 7 }; UINT64 u64Data; // 0 1 2 3 4 5 6 7, same 8 bytes }; UINT64 u64Sig; // 8 9 A B C D E F == 16 bytes total; // holds (realKey ^ u64Data), not the // plain key -- see hash.c. } HASH_ENTRY; FLAG InitializeHashSystem(void); void CleanupHashSystem(void); void ClearHashTable(void); void DirtyHashTable(void); void StoreLowerBound(MOVE mvBestMove, POSITION *pos, SCORE iValue, ULONG uDepth, FLAG fThreat); void StoreExactScore(MOVE mvBestMove, POSITION *pos, SCORE iValue, ULONG uDepth, FLAG fThreat, ULONG uPly); void StoreUpperBound(//MOVE mvBestMove, POSITION *pos, SCORE iValue, ULONG uDepth, FLAG fThreat); FLAG HashLookup(SEARCHER_THREAD_CONTEXT *ctx, ULONG uDepth, ULONG uNextDepth, SCORE iAlpha, SCORE iBeta, FLAG *pfThreat, FLAG *pfAvoidNull, MOVE *pHashMove, SCORE *piScore, HASH_ENTRY *pOut); COOR CheckHashForDangerSquare(POSITION *pos); MOVE GetPonderMove(POSITION *pos); extern ULONG g_uHashTableSizeEntries; extern ULONG g_uHashTableSizeBytes; extern HASH_ENTRY *g_pHashTable; // // testhash.c // void AnalyzeFullHashTable(void); FLAG TestLocklessHashAlgebra(void); // // dynamic.c -- enprise/trapped piece hints // // Ply-indexed, like killer moves: cheap, lock-free, thread-local hints // about which piece(s) looked en prise or trapped the last time full // Eval() (or a fail-high capture) ran at this ply. Not guaranteed // fresh for the position currently at that ply -- readers must // re-validate via the recorded PIECE still being on the recorded COOR // before trusting it (RecordEnprisePiece/RecordTrappedPiece store // both for exactly this reason). Nice-to-have only: pruning/ordering // decisions must work correctly with none of this data available. // void RecordEnprisePiece(SEARCHER_THREAD_CONTEXT *ctx, COOR cSquare); void RecordEnprisePieceAtPly(SEARCHER_THREAD_CONTEXT *ctx, ULONG uPly, COOR cSquare); void RecordTrappedPiece(SEARCHER_THREAD_CONTEXT *ctx, COOR cSquare); COOR FindEnprisePiece(SEARCHER_THREAD_CONTEXT *ctx, ULONG uSide); ULONG ValueOfMaterialInTroubleDespiteMove(SEARCHER_THREAD_CONTEXT *ctx, ULONG uSide); // // pawnhash.c // #define COOR_TO_BIT(x) ((x) & 0x7) + ((0x7 - ((x) >> 4)) << 3) void ClearPawnHashStats(void); void ReportPawnHashStats(void); PAWN_HASH_ENTRY * PawnHashLookup(SEARCHER_THREAD_CONTEXT *ctx); // // eval.c // #define LAZY_EVAL #define LAZE_EVAL_BASE_SCORE 10 #define LAZY_EVAL_BASE_MARGIN (75) // cheap material-only lazy exit margin; // widened by EstimatePositionalScore // if this isn't enough on its own // board_representation/EVAL.md section 9 (2026-09-06): below this // combined pos->uArmyScaler[WHITE]+pos->uArmyScaler[BLACK] total, both // stages of lazy eval are skipped entirely (always full eval). Measured // directly via CALIBRATE_MARGIN_SAFETY: the regular (widened-margin) // lazy exit's real swing exceeded its own assumed margin 20.6% of the // time in the 0-7 combined-scaler bucket (n=1229) -- a near-bare-king // endgame's positional swings (opposition, zugzwang, breakthrough // timing) are too discontinuous for the margin's "small and bounded" // assumption to hold. Set at 16 (start of the material bucket that // measured a clean 0.05% rate) rather than exactly at the 0-7 boundary, // since the 8-15 bucket's own 0% reading was too thin (n=50) to trust. #define LAZY_EVAL_MIN_MATERIAL 16 extern const int g_iAhead[2]; extern const int g_iBehind[2]; ULONG DNABufferSizeBytes(); char * ExportEvalDNA(); FLAG WriteEvalDNA(char *szFilename); FLAG ImportEvalDNA(char *p); void DumpNnueFeatures(POSITION *pos); FLAG ReadEvalDNA(char *szFilename); void InitializeEval(); SCORE Eval(SEARCHER_THREAD_CONTEXT *, SCORE, SCORE, SCORE (*)[2]); // Exposed (non-static) so testrecogn.c can EGTB-cross-check the // wrong-bishop-rook-pawn-fortress shortcut inside it -- see this // function's own header comment in eval.c for why that specific case // is fine to be occasionally wrong here (a soft eval nudge) in a way // it would not be as a recogn.c hard bound. FLAG SideHasWinningChances(POSITION *pos, ULONG uSide); FLAG _EvalPasserRacesAgainstLoneKings(SEARCHER_THREAD_CONTEXT *, PAWN_HASH_ENTRY *); // Exposed (non-static) so testrecogn.c can build a real, // fully-populated PAWN_HASH_ENTRY (bbPasserLocations included) the // same way Eval() itself does, in order to EGTB-cross-check // _EvalPasserRacesAgainstLoneKings directly rather than guessing at // its accuracy. PAWN_HASH_ENTRY * EvalPawns(SEARCHER_THREAD_CONTEXT *ctx, FLAG *pfDeferred); ULONG CountKingSafetyDefects(POSITION *pos, ULONG uSide); #ifdef CALIBRATE_POSITIONAL void DumpPositionalCalibration(void); #endif #ifdef CALIBRATE_BASE_MARGIN void DumpBaseMarginCalibration(void); #endif #ifdef CALIBRATE_MARGIN_SAFETY void DumpMarginSafetyCalibration(CHAR *szHistogramPathOverride); #endif #ifdef DUMP_RESIDUAL_SAMPLES void DumpResidualAndKingSafetyHistograms(void); #endif // Always-on (not gated behind a CALIBRATE_* build flag) -- see // eval.c's g_u64SuperLazyEvalsByBucket/g_u64LazyEvalsByBucket/ // g_u64FullEvalsByBucket and the five INC-site call pairs in Eval(). void DumpEvalExitRateByBucket(void); #ifdef CALIBRATE_QSEARCH_FUTILITY // Which futility gate in _ShouldWeConsiderThisMove rejected a move -- // see search.c's _QFutRecordSample. The recapture gate (used to be // QFUT_GATE_RECAPTURE) was retired 2026-09-08 along with the +100 // recapture bonus itself -- see the "recapture-shaped" comment in // _ShouldWeConsiderThisMove for why (fRecaptureShaped never checked // mv.cTo == mvLast.cTo, so it wasn't testing real recaptures). #define QFUT_GATE_GENERIC_CAPTURE (0) #define QFUT_GATE_CHECK_ROOK (1) #define QFUT_GATE_CHECK_BISHOP (2) #define QFUT_GATE_COUNT (3) void DumpQSearchFutilityCalibration(void); #endif // // testeval.c // #ifdef EVAL_DUMP void TestEval(void); void EvalTraceReport(void); void EvalTrace(ULONG uColor, PIECE p, COOR c, SCORE iVal, CHAR *szMessage); SCORE EvalSigmaForPiece(POSITION *pos, COOR c); void EvalTraceClear(void); void TestEvalWithSymmetry(void); #define EVAL_TERM(COL, PIE, COO, VAL, ADJ, MESS) \ (VAL) += (ADJ); \ EvalTrace((COL), (PIE), (COO), (ADJ), (MESS)); #else #define EVAL_TERM(COL, PIE, COO, VAL, ADJ, MESS) \ (VAL) += (ADJ); #endif // // Per-eval-term cycle accounting, EVAL_TIME only -- see // board_representation/EVAL.md section 0b. CTX must be the // SEARCHER_THREAD_CONTEXT* in scope at the call site; COUNTER names // one of the ctx->sCounters.tree.u64CyclesEval* fields. Wraps CALL // (an expression, e.g. a function call whose return value is // discarded or assigned outside the macro) with a start/stop // timestamp pair, accumulating elapsed cycles into COUNTER. A no-op // wrapper (CALL alone) outside EVAL_TIME builds so call sites don't // need their own #ifdef. // #ifdef EVAL_TIME #define TIMED_EVAL_CALL(CTX, COUNTER, CALL) \ do { \ UINT64 u64EvalTimerStart = SystemReadTimeStampCounter(); \ CALL; \ (CTX)->sCounters.tree.COUNTER += \ (SystemReadTimeStampCounter() - u64EvalTimerStart); \ } while (0) #else #define TIMED_EVAL_CALL(CTX, COUNTER, CALL) CALL #endif // // bitboard.c // void InitializeBitboards(void); ULONG CDECL SlowCountBits(BITBOARD bb); ULONG CDECL DeBruijnFirstBit(BITBOARD bb); ULONG CDECL SlowFirstBit(BITBOARD bb); ULONG CDECL SlowLastBit(BITBOARD bb); // Compiler-builtin (__builtin_ctzll/__builtin_clzll) bsf/bsr -- same // 1-based/0-for-empty contract as FirstBit/LastBit below, and the // exact same bsf/bsr instruction the asm FirstBit/LastBit use (this // build passes no -mbmi, so __builtin_ctzll still lowers to bsf, not // tzcnt) -- but static inline, so a call site pays for the // instruction itself and nothing else, no CDECL call/ret/arg-marshal // overhead. That overhead is exactly what makes it worth having a // second copy instead of just calling FirstBit/LastBit everywhere: // worthwhile in a per-move-generated, per-search-node hot path, // pointless as a blanket replacement elsewhere. static (not extern) // deliberately -- a plain non-static C99 "inline" definition with no // out-of-line instantiation anywhere is a link-time trap, not just a // style choice. static ULONG INLINE FastFirstBit(IN BITBOARD bb) { return bb ? ((ULONG)__builtin_ctzll(bb) + 1) : 0; } static ULONG INLINE FastLastBit(IN BITBOARD bb) { return bb ? (ULONG)(64 - __builtin_clzll(bb)) : 0; } // Same rationale as FastFirstBit/FastLastBit above: __builtin_popcountll // compiles to a single POPCNT instruction (verified against this // build's actual object code, not assumed -- GNUmakefile's -mpopcnt is // required for this; without it clang silently falls back to a // ~15-instruction SWAR sequence, still faster than the extern CDECL // CountBits below -- x64.asm's real implementation is a Kernighan // bit-clearing loop, O(popcount) iterations plus call overhead, // measured at ~23 cycles/op by gmake TEST=1's own bitboard benchmark // -- but not the ~1-cycle single instruction this function gets with // -mpopcnt). Measured (rdtsc, -O3 -mpopcnt, inlined): statistically // identical cost to a plain scalar field read for a single popcnt, and // ~2.5 cycles for an OR-several-bitboards-then-popcnt compound count -- // this is what retired uPawnCount[2]/uNonPawnCount[2][8]'s stored // counts in favor of computing them from bbPawns/bbPieces on read. static ULONG INLINE FastCountBits(IN BITBOARD bb) { ULONG u = (ULONG)__builtin_popcountll(bb); ASSERT(u == SlowCountBits(bb)); return u; } // Fast, globally-available equivalents of CoorFromBitBoardRank8ToRank1/ // CoorFromBitBoardRank1ToRank8 (bitboard.c) -- those are real // out-of-line extern CDECL calls from every translation unit except // eval.c (which has had its own private copy of exactly this pair // since the cNonPawns/cPawns/uPawnCount/uNonPawnCount retirement work // started adding multi-bit bitboard-scan loops elsewhere -- confirmed // by disassembly that recogn.o/board.o/generate.o/movesup.o/probe.o/ // testmove.o were still emitting real callq+PLT relocations to the // slow extern version). Promoted here so every caller gets the fast // path, not just eval.c. _FastCoorFromBitBoardRank1ToRank8 bakes in // the same wrong-bit-clear fix as bitboard.c's real // CoorFromBitBoardRank1ToRank8 (clears the bit it actually reported, // via BBSQUARE, not unconditionally the lowest set bit). static COOR INLINE FastCoorFromBitBoardRank8ToRank1(IN OUT BITBOARD *pbb) { COOR c = ILLEGAL_COOR; ULONG uBitIndex; if (*pbb) { uBitIndex = (ULONG)__builtin_ctzll(*pbb); c = BIT_NUMBER_TO_COOR(uBitIndex); *pbb &= (*pbb - 1); } return c; } static COOR INLINE FastCoorFromBitBoardRank1ToRank8(IN OUT BITBOARD *pbb) { COOR c = ILLEGAL_COOR; ULONG uBitIndex; if (*pbb) { uBitIndex = (ULONG)(63 - __builtin_clzll(*pbb)); c = BIT_NUMBER_TO_COOR(uBitIndex); *pbb &= ~BBSQUARE[uBitIndex]; } return c; } #ifdef CROUTINES #define CountBits SlowCountBits #define FirstBit SlowFirstBit #define LastBit SlowLastBit #endif COOR CoorFromBitBoardRank8ToRank1(BITBOARD *pbb); COOR CoorFromBitBoardRank1ToRank8(BITBOARD *pbb); // King location -- reads the dedicated cKingSq[2] scalar (see its // declaration in POSITION) instead of the old cNonPawns[color][0] // convention. static COOR INLINE KingPosition(IN POSITION *pos, IN ULONG color) { return pos->cKingSq[color]; } // Combined "every non-pawn, non-king piece of this color" bitboard -- // replaces a cNonPawns[color][1..count-1] list walk (slot 0 is always // the king, see cKingSq above) with a scan over this bitboard. Bit // order (rank8->rank1 via CoorFromBitBoardRank8ToRank1) differs from // the old list's insertion order; fine for callers that just // sum/count/search, not for callers that depended on insertion order // specifically. // // bbOccupiedSide[color] & ~bbPawns[color] is every non-pawn piece of // this color INCLUDING the king (bbOccupiedSide covers pawns, king, // and every other piece type; subtracting bbPawns leaves king + // knight/bishop/rook/queen) -- clearing the king's own bit on top of // that is cheaper (one AND, one ANDNOT, one more ANDNOT) than the // previous OR of all four per-type bitboards (three ORs), and doesn't // need bbPieces[color][KING] to exist (it deliberately doesn't -- see // cKingSq's comment). #define NON_KING_PIECES_BB(pos, color) \ ((pos)->bbOccupiedSide[(color)] & \ ~(pos)->bbPawns[(color)] & \ ~COOR_TO_BB((pos)->cKingSq[(color)])) // Non-destructive single-bit-to-COOR conversion -- unlike // CoorFromBitBoardRank8ToRank1, doesn't clear the bit or take a // pointer, so it's safe to call directly on a POSITION field. Callers // must know the bitboard has exactly one bit set (e.g. bbPieces[color] // [KNIGHT] in a position with only one knight of that color) -- // behavior on an empty or multi-bit board is the same // "first/lowest set bit, or ILLEGAL_COOR-shaped garbage if none" // contract FastFirstBit already has. static COOR INLINE CoorOfSingleBit(IN BITBOARD bb) { return BIT_NUMBER_TO_COOR(FastFirstBit(bb) - 1); } // Total non-pawn piece count (including the king), replacing the old // stored uNonPawnCount[color][0] convention slot -- see chess.h's // uNonPawnMaterial comment for why this became a popcnt instead of a // stored field. bbOccupiedSide[color] & ~bbPawns[color] already // includes the king (unlike NON_KING_PIECES_BB, which excludes it) -- // cheaper than FastCountBits(NON_KING_PIECES_BB(pos, color)) + 1, // same reasoning as NON_KING_PIECES_BB's own comment. static ULONG INLINE TotalNonPawnCount(IN POSITION *pos, IN ULONG color) { return FastCountBits(pos->bbOccupiedSide[color] & ~pos->bbPawns[color]); } // Number of this color's bishops on white squares, replacing the old // stored uWhiteSqBishopCount[2] field -- one AND against the constant // BBWHITESQ bitboard (bitboard.c) plus a popcnt, same cost class as // the other counts retired alongside it. static ULONG INLINE WhiteSqBishopCount(IN POSITION *pos, IN ULONG color) { return FastCountBits(pos->bbPieces[color][BISHOP] & BBWHITESQ); } // Pawn material of this color, replacing the old stored // uPawnMaterial[2] field -- every pawn is worth VALUE_PAWN, so this is // just a popcnt and a multiply. static ULONG INLINE PawnMaterial(IN POSITION *pos, IN ULONG color) { return FastCountBits(pos->bbPawns[color]) * VALUE_PAWN; } // // x86.asm // ULONG CDECL CountBits(BITBOARD bb); ULONG CDECL FirstBit(BITBOARD bb); ULONG CDECL LastBit(BITBOARD bb); ULONG CDECL LockCompareExchange(volatile void *pDest, ULONG uExch, ULONG uComp); ULONG CDECL LockIncrement(volatile void *pDest); ULONG CDECL LockDecrement(volatile void *pDest); FLAG CDECL CanUseParallelOpcodes(); ULONG CDECL ParallelCompareUlong(ULONG uComparand, void *pComparators); ULONG CDECL ParallelCompareVector(void *pComparand, void *pComparators); // board_representation/MIGRATION.md sections 3/6/7: bbPieces/bbPawns- // backed GetAttacks primitive. Verified correct (20,000-position // sweep) and faster than the old mailbox implementation (0.53-0.89x // cycles/call across opening/middlegame/endgame). Now the only // GetAttacks implementation -- the old asm/CROUTINES mailbox versions // (SlowGetAttacks, asm GetAttacks) were retired 2026-09-06 once all of // section 7's retirement criteria cleared. void CDECL _GetAttacksBB(SEE_LIST *pList, POSITION *pos, COOR cSquare, ULONG uSide); // Magic-bitboard implementation (as of 2026-09-19) -- exposed (not // static) so generate.c's Part B king-flight code and movesup.c can // 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 FORCEINLINE _WhoAttacksSquareBB(POSITION *pos, COOR cSquare, ULONG uSide, BITBOARD bbOccupied); // Ray-walk reference implementation, superseded by the magic version // above -- see its own header comment in see.c. Exposed non-static so // testsee.c can diff it against the magic version. BITBOARD _WhoAttacksSquareRayBB(POSITION *pos, COOR cSquare, ULONG uSide, BITBOARD bbOccupied); // _GetAttacksBB is the only implementation as of 2026-09-06 (see // MIGRATION.md section 7) -- this macro exists only so call sites // written against the name "GetAttacks" didn't need touching when the // old asm/CROUTINES mailbox versions were retired. #define GetAttacks _GetAttacksBB #ifdef _X86_ // // Note: this is most of the stuff that x86.asm assumes about the // internal data structures. If any of this fails then either the // assembly language code needs to be updated or you need to use the C // version of the routine in see.c instead. // #define ASSERT_ASM_ASSUMPTIONS \ ASSERT(VALUE_PAWN == 100); \ ASSERT(OFFSET_OF(uCount, SEE_LIST) == 0); \ ASSERT(OFFSET_OF(data, SEE_LIST) == 4); \ ASSERT(sizeof(SEE_THREESOME) == 12); \ ASSERT(OFFSET_OF(pPiece, SEE_THREESOME) == 0); \ ASSERT(OFFSET_OF(cLoc, SEE_THREESOME) == 4); \ ASSERT(OFFSET_OF(uVal, SEE_THREESOME) == 8); \ ASSERT(OFFSET_OF(rgSquare, POSITION) == 0); \ ASSERT(sizeof(SQUARE) == 8); \ ASSERT(sizeof(VECTOR_DELTA) == 4); \ ASSERT(sizeof(g_VectorDelta) == 256 * 4); \ ASSERT(OFFSET_OF(iVector, VECTOR_DELTA) == 0); \ ASSERT(OFFSET_OF(iDelta, VECTOR_DELTA) == 2); \ ASSERT(OFFSET_OF(iNegDelta, VECTOR_DELTA) == 3); \ ASSERT(sizeof(g_PieceData) == 8 * 4 * 4); \ ASSERT(OFFSET_OF(uValue, PIECE_DATA) == 0); #else #define ASSERT_ASM_ASSUMPTIONS #endif // // testbitboard.c // void TestBitboards(void); // // dynamic.c // // EXPERIMENT: flat (depth-independent) saturating history update, after // Crafty's history.c -- *pu += (HISTORY_MAX - *pu) >> HISTORY_UPDATE_SHIFT // on fail-high, *pu -= *pu >> HISTORY_UPDATE_SHIFT on a miss. Replaces the // old (depth+1)^2-scaled growth (which biases toward root-near nodes, // where depth is large, over the far more numerous near-tip nodes where // it's small) with a flat update whose magnitude never depends on depth // at all. Self-bounding by construction (asymptotically approaches // HISTORY_MAX, never exceeds it), so the old overflow-triggered global // >>=4 rescale is no longer needed for this table. #define HISTORY_MAX (8192) #define HISTORY_UPDATE_SHIFT (5) extern ULONG g_HistoryCounters[14][128]; // Continuation history: same growth/decay math as g_HistoryCounters // (see dynamic.c), but keyed additionally by the previous move, at the // same [piece][to] coarseness -- not full from/to -- to keep the table // a manageable size (14*128 * 14*128 = ~3.2M ULONGs, ~12MB). "Did a // move of this piece-type to this square tend to fail high right after // a move like that?" // EXPERIMENT: read-time-only multiplier on the continuation-history // contribution to a leftover move's selection score. Write-side growth/ // decay math is untouched (still self-calibrating by evidence), this // just scales how much weight that accumulated evidence carries // relative to PSQT+history. Swept empirically via the zero-selection- // budget diagnostic binary to trace best-leftover FH% vs. scale. #define CONTINUATION_SCALE (1) #define CONT_KEY_RANGE (14 * 128) #define MOVE_TO_CONT_KEY(mv) (((mv).pMoved * 128) + (mv).cTo) #define CONTINUATION_TABLE_SIZE (CONT_KEY_RANGE * CONT_KEY_RANGE) extern ULONG g_ContinuationHistory[CONTINUATION_TABLE_SIZE]; ULONG GetMoveFailHighPctThousandths(MOVE mv, ULONG *puAttempts); void GetMoveFailHighRawCounts(MOVE mv, ULONG *puFailHighs, ULONG *puAttempts); void UpdateDynamicMoveOrdering(SEARCHER_THREAD_CONTEXT *ctx, ULONG uRemainingDepth, MOVE mvBest, SCORE iScore, ULONG uCurrent); void NewKillerMove(SEARCHER_THREAD_CONTEXT *ctx, MOVE mv, SCORE iScore); FLAG InitializeDynamicMoveOrdering(void); void CleanupDynamicMoveOrdering(void); void ClearDynamicMoveOrdering(void); void MaintainDynamicMoveOrdering(void); void IncrementMoveHistoryCounter(MOVE mv, ULONG u); void DecrementMoveHistoryCounter(MOVE mv, ULONG u); // // split.c // extern volatile ULONG g_uNumHelpersAvailable; extern ULONG g_uNumHelperThreads; FLAG InitializeParallelSearch(void); FLAG CleanupParallelSearch(void); void ClearHelperThreadIdleness(void); void DumpHelperIdlenessReport(void); SCORE StartParallelSearch(IN SEARCHER_THREAD_CONTEXT *ctx, IN OUT SCORE *piAlpha, IN SCORE iBeta, IN SCORE iImprovement, IN OUT SCORE *piBestScore, IN OUT MOVE *pmvBest, IN ULONG uMoveNum, IN INT iPositionExtend, IN ULONG uDepth, IN FLAG fThreat, IN BITBOARD bbMinorAttacksSnapshot[2], IN BITBOARD bbRookAttacksSnapshot[2]); void InitializeSearcherContext(POSITION *pos, SEARCHER_THREAD_CONTEXT *ctx); void ReInitializeSearcherContext(POSITION *pos, SEARCHER_THREAD_CONTEXT *ctx); void InitializeLightweightSearcherContext(POSITION *pos, LIGHTWEIGHT_SEARCHER_CONTEXT *ctx); // // book.c // #define BOOK_PROBE_MISS_LIMIT (7) #define FLAG_DISABLED (1) #define FLAG_ALWAYSPLAY (2) #define FLAG_DELETED (4) #define BOOKMOVE_SELECT_MOVE (1) #define BOOKMOVE_DUMP (2) #pragma pack(4) typedef struct _BOOK_ENTRY { UINT64 u64Sig; // 8 bytes UINT64 u64NextSig; // 8 bytes MOVE mvNext; // 4 bytes ULONG uWins; // 4 bytes ULONG uDraws; // 4 bytes ULONG uLosses; // 4 bytes BITV bvFlags; // 4 bytes } BOOK_ENTRY; #pragma pack() typedef struct _OPENING_NAME_MAPPING { UINT64 u64Sig; CHAR *szString; } OPENING_NAME_MAPPING; #define BOOK_EDITING_RECORD "bkedit.edt" #define OPENING_LEARNING_FILENAME "bklearn.bin" typedef struct _OPENING_LEARNING_ENTRY { UINT64 u64Sig; ULONG uWhiteWins; ULONG uDraws; ULONG uBlackWins; } OPENING_LEARNING_ENTRY; FLAG InitializeOpeningBook(void); void ResetOpeningBook(void); void CleanupOpeningBook(void); MOVE BookMove(POSITION *pos, BITV bvFlags); COMMAND(BookCommand); extern ULONG g_uBookProbeFailures; extern FLAG g_fTournamentMode; extern CHAR *g_szBookName; // // bench.c // COMMAND(BenchCommand); // // testdraw.c // void TestDraw(void); void TestRecogn(void); void TestRecognExhaustiveKNKP(void); void TestRecognExhaustiveKQKP(void); void TestRecognExhaustiveKPK(void); void TestRecognExhaustiveKPvKP(void); void TestRecognSampleKPvKP(void); void TestEvalWrongBishopFortress(void); void TestEvalPasserRacesAgainstLoneKings(void); void TestEvalSamplePasserRacesKPPKPP(void); void TestEvalSamplePasserRacesKPKPPP(void); // // probe.c // FLAG ProbeEGTB(SEARCHER_THREAD_CONTEXT *ctx, SCORE *score); FLAG ProbeEGTBRoot(SEARCHER_THREAD_CONTEXT *ctx, MOVE *pmv, SCORE *piScore); FLAG EgtbProbeLikelyResident(SEARCHER_THREAD_CONTEXT *ctx); void InitializeEGTB(void); void CleanupEGTB(void); // // dumptree.c: TREEDUMP -- flat, line-oriented, path-keyed node/move // trace of the search tree, meant to be diffed against an // identically-instrumented Stockfish build on the same FEN -- see // TREEDUMP_FORMAT.md. Build-time-only ("gmake TREEDUMP=1 ..."), // always-on-when-built. There is no runtime toggle/filter -- keep a // dump small by using a shallow "sd N"/"go depth N", not by // restricting what gets written. // // Replaces a 2004-era, long-unused XML tree dump written for a Windows // GUI (typhoonui.exe) that this project no longer has; that mechanism // (DUMP_TREE, InitializeTreeDump/CleanupTreeDump/DTEnterNode/ // DTLeaveNode/DTTrace) has been removed outright rather than kept // alongside this one, on the assumption that a mechanism nothing has // exercised in two decades is broken. // // TDMove logs one decision about a move already made at the CURRENT // (child) ply -- i.e. call it AFTER MakeMove and BEFORE UnmakeMove, so // the path builder (which walks ctx->sPlyInfo[0..ctx->uPly-1].mv) picks // up this move as the path's last element for free. TDResult logs the // node's own final outcome and must be called at the node's OWN ply // (e.g. search.c's "end:"), before any further move has been made. #ifdef TREEDUMP void TDOpen(void); void TDClose(void); void TDMove(SEARCHER_THREAD_CONTEXT *ctx, ULONG uDepth, SCORE iAlpha, SCORE iBeta, CHAR *szKind, CHAR *szAct, double dReducedPlies, double dExtendedPlies, SCORE iScore, FLAG fSkipped); void TDResult(SEARCHER_THREAD_CONTEXT *ctx, ULONG uDepth, SCORE iAlpha, SCORE iBeta, SCORE iEval, SCORE iBestScore); #else #define TDOpen(...) #define TDClose(...) #define TDMove(...) #define TDResult(...) #endif // // testsup.c // FLAG IsBoardLegal(POSITION *pos); void GenerateRandomLegalPosition(POSITION *pos); void GenerateRandomLegalSymetricPosition(POSITION *pos); // // recogn.c // #define UNRECOGNIZED (0) #define RECOGN_EXACT (1) #define RECOGN_UPPER (2) #define RECOGN_LOWER (3) #define RECOGN_EGTB (4) // // EGTB probing mode for RecognLookup's 3rd argument -- trinary rather // than a plain FLAG so "probe only if we believe the page is resident" // is a distinct option from "always probe" and "never probe". Letting // RecognLookup itself decide (rather than making the caller compute // EgtbProbeLikelyResident up front, unconditionally, as a function // argument) means the residency check -- which can now involve real // mincore() syscalls for 6-man tables, see EgtbProbeLikelyResident's // own comment -- is only ever paid on the branch that's actually about // to decide whether to call ProbeEGTB, i.e. skipped entirely at every // node one of the cheap hand-written interior recognizers already // resolves. See CLAUDE.md's EGTB residency section. // #define EGTB_PROBE_NEVER (0) #define EGTB_PROBE_ALWAYS (1) #define EGTB_PROBE_IF_RESIDENT (2) void InitializeInteriorNodeRecognizers(void); ULONG FORCEINLINE RecognLookup(SEARCHER_THREAD_CONTEXT *ctx, SCORE *piScore, ULONG uEgtbProbeMode); // // Shared, ctx-free KP+KP* "unstoppable passer" static analysis -- // deliberately touches no move generation (see recogn.c's // IsStaticSafeQueeningPawn for why) so it's safe to call from eval.c's // Eval() as well as recogn.c's own hard-bound recognizer. See that // function's comment for the full rationale and the trust boundary // between recogn.c (must never be wrong) and eval.c (a soft bonus, // allowed to occasionally miss what recogn.c's fuller, // move-generation-assisted version catches). // FLAG IsStaticSafeQueeningPawn(POSITION *pos, ULONG uColor, COOR cPawn); FLAG HasStaticUnstoppablePasser(POSITION *pos, ULONG uColor, COOR *pcPawn); // // True-lone-king (weak side has zero pawns) "rule of the square + king // escort" analysis -- exhaustively verified via TestRecognExhaustiveKPK // (0 wrong across all 165,676 EGTB-covered legal single-pawn KPK // positions). Also pure geometry, no move generation, so also safe for // eval.c to call directly. // FLAG IsUnstoppableLoneKingPasser(POSITION *pos, ULONG uStrong, COOR cPawn); // // Unregistered/experimental recognizers, exposed only so testrecogn.c // can exhaustively validate a candidate before it's ever re-registered // in InitializeInteriorNodeRecognizers. Not reachable from the normal // RecognLookup dispatch path. // ULONG _RecognizeKNKP(SEARCHER_THREAD_CONTEXT *ctx, SCORE *piScore); ULONG _RecognizeKQKP(SEARCHER_THREAD_CONTEXT *ctx, SCORE *piScore); #endif // CHESS