/** Copyright (c) Scott Gasch Module Name: eval.c Abstract: Position evaluation routines. Author: Scott Gasch (scott.gasch@gmail.com) 14 Jun 2004 Revision History: $Id: eval.c 357 2008-07-03 16:18:11Z scott $ **/ #include "chess.h" // // CountBits and the CoorFromBitBoard{Rank8ToRank1,Rank1ToRank8} // wrappers (bitboard.c) are real out-of-line asm calls on every one of // eval.c's ~20 production call sites (passed-pawn detection, bishop- // pair logic, etc.), hit on every real Eval() call. CountBits' asm // body (x64.asm) isn't even O(1) popcnt, it's a Kernighan bit-clearing // loop -- O(popcount) iterations plus call overhead. Redirected via // #define, the same mechanism chess.h already uses for the OTHER // direction (#ifdef CROUTINES routes CountBits/FirstBit/LastBit to the // slow C fallbacks) -- this routes them to fast inline compiler // builtins instead, for every existing call site below with no further // edits. Gated on !CROUTINES so the CROUTINES debug/comparison build // still gets the real (slow, cross-checked) implementations. // // _FastCoorFromBitBoardRank1ToRank8 also bakes in the wrong-bit-clear // fix applied to bitboard.c's real CoorFromBitBoardRank1ToRank8 (that // function used to clear the LOWEST set bit via `*pbb &= (*pbb - 1)` // regardless of uLastBit, correct only when a single bit was set) -- // this version clears the bit it actually just reported, via BBSQUARE. // #ifndef CROUTINES static ULONG INLINE _FastCountBits(IN BITBOARD bb) { return (ULONG)__builtin_popcountll(bb); } 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; } #define CountBits _FastCountBits #define CoorFromBitBoardRank8ToRank1 _FastCoorFromBitBoardRank8ToRank1 #define CoorFromBitBoardRank1ToRank8 _FastCoorFromBitBoardRank1ToRank8 #endif // !CROUTINES // // To simplify code / maintenance I use the same loop for both colors // in some places. These globals coorespond to "ahead of the piece" // or "behind the piece" for each color. // const int g_iAhead[2] = { +1, -1 }; const int g_iBehind[2] = { -1, +1 }; // // General eval terms // --------------------------------------------------------------------------- // //static SCORE TRADE_PIECES[3][17] = //{// 0 1 2 3 4 5 6 7 8 | 9..15 -- down piece count // { -1, 90, 81, 73, 67, 62, 57, 52, 50, 50,50,50,50,50,50,50,-1}, // { -1, 125, 117, 109, 100, 91, 84, 78, 71, 66,66,66,66,66,66,66,-1}, // { -1, 150, 132, 124, 116, 109, 102, 94, 87, 77,77,77,77,77,77,77,-1}, //}; //static SCORE DONT_TRADE_PAWNS[3][9] = //{// 0 1 2 3 4 5 6 7 8 -- up pawn count // { -43, -15, 0, +3, +6, +10, +15, +21, +28 }, // // 0 // { -63, -25, 0, +4, +9, +14, +19, +25, +32 }, // // 1 // { -10, 0, 0, +7, +15, +20, +24, +29, +36 }, // // 2 //}; static ULONG REDUCED_MATERIAL_DOWN_SCALER[32] = { // none na na m na R 2m na 0, 0, 0, 0, 0, 0, 0, 0, // Rm 3m/Q 2R R2m 4m/Qm 2Rm R3m/QR Q2m 0, 1, 1, 1, 1, 1, 2, 3, // 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m // Q2R Q2Rm 4, 5, 6, 6, 7, 7, 7, 7, // na Q2R2m QR4m na Q2R3m na na full 8, 8, 8, 8, 8, 8, 8, 8, }; // // Same shape as REDUCED_MATERIAL_DOWN_SCALER (0 with little material left, // ramping to 8 with a full army) but decays to 0 much sooner -- "haven't // castled yet" / "minors still at home" are opening-specific concerns that // stop mattering well before the point where real king danger fades, so // this table's zero region extends much further up the index range than // REDUCED_MATERIAL_DOWN_SCALER's does. // static ULONG CASTLE_AND_DEVELOPMENT_SCALER[32] = { // none na na m na R 2m na 0, 0, 0, 0, 0, 0, 0, 0, // Rm 3m/Q 2R R2m 4m/Qm 2Rm R3m/QR Q2m 0, 0, 0, 0, 0, 0, 0, 0, // 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m // Q2R Q2Rm 0, 1, 2, 3, 4, 5, 6, 7, // na Q2R2m QR4m na Q2R3m na na full 7, 8, 8, 8, 8, 8, 8, 8, }; static ULONG REDUCED_MATERIAL_UP_SCALER[32] = { // none na na m na R 2m na 8, 8, 8, 8, 8, 8, 8, 8, // Rm 3m/Q 2R R2m 4m/Qm 2Rm R3m/QR Q2m 8, 7, 7, 7, 7, 6, 6, 5, // 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m // Q2R Q2Rm 4, 3, 3, 3, 2, 2, 2, 1, // na Q2R2m QR4m na Q2R3m na na full 1, 1, 0, 0, 0, 0, 0, 0, }; static ULONG PASSER_MATERIAL_UP_SCALER[32] = { // none na na m na R 2m na 8, 8, 8, 8, 8, 8, 7, 7, // Rm 3m/Q 2R R2m 4m/Qm 2Rm R3m/QR Q2m 7, 6, 7, 5, 4, 5, 4, 3, // 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m // Q2R Q2Rm 2, 2, 1, 1, 0, 0, 0, 0, // na Q2R2m QR4m na Q2R3m na na full 0, 0, 0, 0, 0, 0, 0, 0, }; COOR QUEENING_RANK[2] = { 1, 8 }; COOR JUMPING_RANK[2] = { 7, 2 }; // // Pawn eval terms // --------------------------------------------------------------------------- // // "There is one case which can be treated as positional or material, // namely the rook's pawn, which differs from other pawns in that it // can only capture one way instead of two. Since this handicap cannot // be corrected without the opponent's help, I teach my students to // regard the rook's pawn as a different piece type, a crippled // pawn. Database statistics indicate that it is on average worth // about 15% less than a normal pawn. The difference is enough so that // it is usually advantageous to make a capture with a rook's pawn, // promoting it to a knights pawn, even if that produces doubled pawns // and even if there is no longer a rook on the newly opened rook's // file." // --Larry Kaufman, IM // "Evaluation of Material Imbalance" // static SCORE PAWN_CENTRALITY_BONUS[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0, -8, 0, 0, 0, 0, 0, 0, -8, 0,0,0,0,0,0,0,0, -8, 0, 5, 5, 5, 5, 0, -8, 0,0,0,0,0,0,0,0, -8, 0, 5, 9, 9, 5, 0, -8, 0,0,0,0,0,0,0,0, // ------------------------------------------------------------------- -8, 0, 5, 9, 9, 5, 0, -8, 0,0,0,0,0,0,0,0, -8, 0, 5, 5, 5, 5, 0, -8, 0,0,0,0,0,0,0,0, -8, 0, 0, 0, 0, 0, 0, -8, 0,0,0,0,0,0,0,0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0 }; // Hand-restored 2026-08-30: the DNA-tuned values here had drifted to // mostly-positive across the board (a shielded backward pawn was scoring // as a +12..+17 bonus on most files, and several EXPOSED entries were // less negative than the corresponding SHIELDED ones on the same file -- // i.e. "more exposed" scored better than "less exposed"). Both the // ASSERT(BACKWARD_SHIELDED_BY_LOCATION[c] < 0) and the analogous EXPOSED // assert below had been silently commented out rather than investigated. // Replaced with a monotonic, strictly-negative, rank-mirrored shape: // backward pawns are always a defect, exposed is always worse than // shielded at the same rank, and the penalty grows as the pawn advances // toward the more contested center ranks (4/5) where a backward pawn is // most exploitable. Table is indexed by absolute square (not // color-relative), hence the rank2/rank7, rank3/rank6, rank4/rank5 // mirroring -- same as the original layout. static SCORE BACKWARD_SHIELDED_BY_LOCATION[128] = { +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0, -7, -5, -5, -7, -7, -5, -5, -7, 0,0,0,0,0,0,0,0, -5, -4, -4, -5, -5, -4, -4, -5, 0,0,0,0,0,0,0,0, -4, -3, -3, -4, -4, -3, -3, -4, 0,0,0,0,0,0,0,0, // ------------------------------------------------------------------- -4, -3, -3, -4, -4, -3, -3, -4, 0,0,0,0,0,0,0,0, -5, -4, -4, -5, -5, -4, -4, -5, 0,0,0,0,0,0,0,0, -7, -5, -5, -7, -7, -5, -5, -7, 0,0,0,0,0,0,0,0, +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0 }; static SCORE BACKWARD_EXPOSED_BY_LOCATION[128] = { +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0, -12, -9, -9, -11, -11, -9, -9, -12, 0,0,0,0,0,0,0,0, -9, -7, -7, -9, -9, -7, -7, -9, 0,0,0,0,0,0,0,0, -7, -6, -6, -7, -7, -6, -6, -7, 0,0,0,0,0,0,0,0, // ------------------------------------------------------------------- -7, -6, -6, -7, -7, -6, -6, -7, 0,0,0,0,0,0,0,0, -9, -7, -7, -9, -9, -7, -7, -9, 0,0,0,0,0,0,0,0, -12, -9, -9, -11, -11, -9, -9, -12, 0,0,0,0,0,0,0,0, +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0 }; // // "The first statement I can confirm is that doubled pawns are indeed // on average undesirable. However this statistic needs to be broken // down to be useful. Doubled pawns themselves are really more // serious than this generally, but when your pawns are doubled you // automatically get an extra half-open file for your rooks (or queen, // but the queen can also use diagonals). So it follows logically // that the net cost of doubled pawns is much greater in the absence // of major pieces. The database shows that with all rooks present, // doubled pawns "cost" only about 1/16th of a pawn on average. With // one rook each the cost rises to 1/4th of a pawn and with no rooks // present to 3/8ths of a pawn. With the queens present the cost is // again only 1/16th of a pawn; without them it's a quarter pawn. So // the lessons are clear; beware of doubled pawns when major pieces // have been exchanged, and beware of exchanging major pieces when you // are the one with the doubled pawns." // --Larry Kaufman, IM // "All About Doubled Pawns" // // Note: indexed by number of files with 1+ pawn on them. static SCORE DOUBLED_PAWN_PENALTY_BY_COUNT[4][9] = { // ------------ count of doubled+ pawns ------------ {// 0 1 2 3 4 5 6 7 8 : no majors alive +0, -32, -65, -99, -134, -170, -207, -222, -250 }, {// : 1 rook +0, -23, -47, -76, -108, -144, -184, -200, -216 }, {// : 2 rooks or 1 queen +0, -13, -23, -34, -46, -64, -86, -111, -138 }, {// : all majors alive +0, -7, -13, -25, -39, -55, -73, -95, -121 }, }; static SCORE ISOLATED_PAWN_BY_PAWNFILE[9] = { 0, -7, -8, -9, -10, -10, -9, -8, -7 }; static SCORE ISOLATED_EXPOSED_PAWN = -5; // Turned down (2026-08-30): a per-pawn modifier that stacks on top of // DOUBLED_PAWN_PENALTY_BY_COUNT's whole-position doubled-pawn aggregate // for the specific case of a pawn that's both isolated and doubled -- // a different angle (single-worst-case flag vs. whole-position // severity), not the same fact counted twice, so scaled down rather // than removed. static SCORE ISOLATED_DOUBLED_PAWN = -7; // // Note: -25% to -33% if the enemy occupies or controls the next sq. // static SCORE PASSER_BY_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +0,+162,+111, +62, +36, +18, +13, +0 }, // black { +0, +0, +13, +18, +36, +62,+111,+162, +0 } // white }; static SCORE CANDIDATE_PASSER_BY_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +0, +0, +48, +34, +22, +13, +9, +0 }, // black { +0, +0, +9, +13, +22, +34, +48, +0, +0 } // white }; // Note: x2 // Scaled to 1/3 of the original hand-tuned magnitude (2026-08-30): these // three terms, plus PASSER_BY_RANK and OUTSIDE_PASSER_BY_DISTANCE, all // separately price "how good is this passer" from different angles // (connected to a partner passer, backed by an ordinary pawn, outside // the opposing pawn majority) and can stack for the same pawn -- a // rank-7 connected+outside passer could hit ~370cp in this pawn-hash- // cached family alone, ~9x Crafty's ~40cp ceiling for the equivalent // concept. Rather than remove any of these (they're each pricing a // genuinely distinct structural fact, not re-counting the same one -- // and pawn-hash caching makes the compute cost of keeping all of them // free), turned the magnitude down on the three secondary/modifier // terms so the stack lands in a saner range; PASSER_BY_RANK (the core // "how far advanced" signal) is left as the primary, least-reduced // term. static SCORE CONNECTED_PASSERS_BY_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +0, +32, +25, +16, +7, +3, +2, +0 }, // black { +0, +0, +2, +3, +7, +16, +25, +32, +0 } // white }; static SCORE SUPPORTED_PASSER_BY_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +0, +20, +13, +4, +2, +1, +1, +0 }, // black { +0, +0, +1, +1, +2, +4, +13, +20, +0 } // white }; // Split out 2026-08-30 from SUPPORTED_PASSER_BY_RANK, which _EvalKing's // "kings in front of passers" endgame bonus was silently reusing -- // pawn-support and king-escort are different concepts (this fires when // the KING stands next to its own passer, not when a pawn does), so // scaling one for its real purpose was silently also scaling the // other. Seeded with SUPPORTED_PASSER_BY_RANK's original (pre-scaling) // hand-tuned magnitude, since that's what this use case was actually // getting before today's pawn-hash pass touched the shared table for // an unrelated reason. static SCORE KING_SUPPORTING_OWN_PASSER_BY_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +0, +60, +40, +13, +6, +3, +1, +0 }, // black { +0, +0, +1, +3, +6, +13, +40, +60, +0 } // white }; static SCORE OUTSIDE_PASSER_BY_DISTANCE[9] = {// 0 1 2 3 4 5 6 7 8 +0, +0, +2, +5, +7, +9, +11, +14, +18 }; static SCORE PASSER_BONUS_AS_MATERIAL_COMES_OFF[32] = { // none na na m na R 2m na +60, -1, -1, +37, -1, +43, +19, -1, // Rm 3m/Q 2R R2m 4m/Qm 2Rm R3m/QR Q2m +14, +9, +20, +4, +3, +3, 0, 0, // 2R2m R4m/QRm Q3m 2R3m/ QR2m Q4m 2R4m/ QR3m // Q2R Q2Rm 0, 0, 0, 0, 0, 0, 0, 0, // na Q2R2m QR4m na Q2R3m na na full 0, 0, 0, 0, 0, 0, 0, 0, }; SCORE RACER_WINS_RACE = +800; static SCORE UNDEVELOPED_MINORS_IN_OPENING[5] = {// 0 1 2 3 4 0, -6, -10, -16, -22 }; // // Bishop eval terms // --------------------------------------------------------------------------- // static SCORE BISHOP_OVER_KNIGHT_IN_ENDGAME = +33; // // "The bishop pair has an average value of half a pawn (more when the // opponent has no minor pieces to exchange for one of the bishops), // enough to regard it as part of the material evaluation of the // position, and enough to overwhelm most positional considerations. // Moreover, this substantial bishop pair value holds up in all // situations tested, regardless of what else is on the board... // // ...One rule which I often teach to students is that if you have the // bishop pair, and your opponent's single bishop is a bad bishop // (hemmed in by his own pawns), you already have full compensation // for a pawn... Kasparov has said something similar... // // As noted before, the bishop pair is worth more with fewer pawns on // the board. Aside from this factor, the half pawn value of the // bishop pair is remarkably constant, applying even when there are no // pieces on the board except two minors each." // --Larry Kaufman, IM // "Evaluation of Material Imbalance" static SCORE BISHOP_PAIR_BONUS = 40; static SCORE STATIONARY_PAWN_ON_BISHOP_COLOR[128] = { +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0, -4, -6, -7, -7, -7, -7, -6, -4, 0,0,0,0,0,0,0,0, -6, -7, -9, -8, -8, -9, -7, -6, 0,0,0,0,0,0,0,0, -7, -8, -10, -11, -11, -10, -8, -7, 0,0,0,0,0,0,0,0, // ------------------------------------------------------------------- -7, -8, -10, -11, -11, -10, -8, -7, 0,0,0,0,0,0,0,0, -6, -7, -9, -8, -8, -9, -7, -6, 0,0,0,0,0,0,0,0, -4, -6, -7, -7, -7, -7, -6, -4, 0,0,0,0,0,0,0,0, +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0 }; static SCORE TRANSIENT_PAWN_ON_BISHOP_COLOR[128] = { +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0, -1, -1, -2, -2, -2, -2, -1, -1, 0,0,0,0,0,0,0,0, -3, -4, -4, -4, -4, -4, -4, -3, 0,0,0,0,0,0,0,0, -4, -5, -7, -8, -8, -7, -5, -4, 0,0,0,0,0,0,0,0, // ------------------------------------------------------------------- -4, -5, -7, -8, -8, -7, -5, -4, 0,0,0,0,0,0,0,0, -3, -4, -4, -4, -4, -4, -4, -3, 0,0,0,0,0,0,0,0, -1, -1, -2, -2, -2, -2, -1, -1, 0,0,0,0,0,0,0,0, +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0 }; // // A bishop can move to between 0..13 squares. // static SCORE BISHOP_MOBILITY_BY_SQUARES[14] = {// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 -22, -14, -10, -5, -1, 0, +1, +3, +4, +5, +6, +7, +8, +9 };// ^ | static SCORE BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING[9] = {// 0 1 2 3 4 5 6 7 8 +0, +33, +28, +18, +8, +4, +0, -10, -16 }; // // Knight eval terms // --------------------------------------------------------------------------- // static SCORE KNIGHT_CENTRALITY_BONUS[128] = { -12, -8, -8, -8, -8, -8, -8, -12, 0,0,0,0,0,0,0,0, -8, -2, -2, 0, 0, -2, -2, -8, 0,0,0,0,0,0,0,0, -8, -2, +4, +5, +5, +4, -2, -8, 0,0,0,0,0,0,0,0, -8, -2, +5, +8, +8, +5, -2, -8, 0,0,0,0,0,0,0,0, // ------------------------------------------------------------------- -8, -2, +5, +8, +8, +5, -2, -8, 0,0,0,0,0,0,0,0, -8, -2, +4, +5, +5, +4, -2, -8, 0,0,0,0,0,0,0,0, -8, -2, -2, 0, 0, -2, -2, -8, 0,0,0,0,0,0,0,0, -12, -8, -8, -8, -8, -8, -8, -12, 0,0,0,0,0,0,0,0 }; static SCORE KNIGHT_KING_TROPISM_BONUS[9] = {// 0 1 2 3 4 5 6 7 8 0, +15, +12, +9, +4, +0, +0, +0, +0 }; static SCORE KNIGHT_UNASSAILABLE_BY_DIST_FROM_EKING[9] = {// 0 1 2 3 4 5 6 7 8 +0, +25, +19, +14, +6, +0, +0, +0, +0 }; static SCORE KNIGHT_ON_INTERESTING_SQUARE_BY_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +12, +11, +9, +6, +3, +0, +0, +0 }, // black { +0, +0, +0, +0, +3, +6, +9, +11, +12 } // white }; // // A knight can move to between 0..8 squares // ----------------------------------------- // 0 = supported enemy pawn // 1 = unsupported enemy pawn, friend pawn, enemy B/N // enemy controlled empty sq. // 2 = enemy >B, friend controlled empty sq, no one controlled empty sq // // Total: 16 mobility max // static SCORE KNIGHT_MOBILITY_BY_COUNT[9] = {// 0 1 2 3 4 5 6 7 8 -17, -10, -6, 0, +3, +5, +7, +8, +8 };// ^ | static SCORE KNIGHT_WITH_N_PAWNS_SUPPORTING[3] = { +0, +4, +8 }; static SCORE KNIGHT_IN_CLOSED_POSITION[33] = {// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 4, 6, 9, 11, 12, 13, 15, 17, 19, 21, 21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 27 // 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 }; // // Rook eval terms // --------------------------------------------------------------------------- // // Simplified 2026-09-06 (board_representation/EVAL.md section 9): flat // average bonus, not scaled by distance from the enemy king -- by direct // instruction. Each was a [8]-by-distance table; these are that table's // simple mean, rounded. static SCORE ROOK_ON_FULL_OPEN = 16; static SCORE ROOK_ON_HALF_OPEN_WITH_ENEMY = 9; static SCORE ROOK_ON_HALF_OPEN_WITH_FRIEND = 10; // Cache of the three constants above, indexed [friend pawn on file?] // [enemy pawn on file?] so _EvalRook's hot path is a single array read // instead of rebuilding a local array every call. Not `static const` // -- the three source constants are DNA-tunable globals (plain mutable // SCORE, not compile-time constants), so this has to be a plain // mutable array, rebuilt by InitEval() whenever they change (startup, // and after every `evaldna read`), not frozen at compile time. static SCORE ROOK_FULL_HALF_OPEN_BONUS[2][2] = { { 16, 9 }, { 10, 0 } }; // Same treatment for the enemy/friend-passer-on-file bonuses -- was // PASSER_BY_RANK[xColor][rank]/4 (enemy case) and a rank-scaled // behind/leads-passer split (friend case, +1..+25 behind, -3..-22 in // front); flat means of the meaningful (non-padding) entries. static SCORE ROOK_WITH_ENEMY_PASSER = 17; static SCORE ROOK_WITH_FRIEND_PASSER = 10; static SCORE ROOK_BEHIND_PASSER_BY_PASSER_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +0, +25, +17, +12, +6, +1, +0, +0 }, // black { +0, +0, +0, +1, +6, +12, +17, +25, +0 } // white }; static SCORE ROOK_LEADS_PASSER_BY_PASSER_RANK[2][9] = {// 0 1 2 3 4 5 6 7 8 { +0, +0, -22, -16, -13, -9, -5, -3, +0 }, // black { +0, +0, -3, -5, -9, -13, -16, -22, +0 } // white }; static SCORE KING_TRAPPING_ROOK = -40; static SCORE ROOK_VALUE_AS_PAWNS_COME_OFF[17] = {// 0 1 2 3 4 5 6 7 8 +55, +51, +44, +38, +33, +27, +22, +16, +7, // 9 10 11 12 13 14 15 16 +1, -3, -6, -9, -12, -18, -22, -25 }; // // Note: these are multiplied by two (one per rook). // static SCORE ROOK_CONNECTED_VERT = +7; // x2 static SCORE ROOK_CONNECTED_HORIZ = +4; // x2 // // A rook can move to between 0..14 squares // ---------------------------------------- // 0 = supported enemy P/N/B, friend piece // 1 = unsupported enemy piece, enemy controlled empty sq, enemy R // 2 = enemy >R, friend controlled empty sq, no one controlled empty sq // // Total: 28 mobility max // static SCORE ROOK_MOBILITY_BY_SQUARES[15] = {// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 -28, -24, -20, -14, -7, -2, +0, +4, +8, +12, +15, +17, +19, +21, +22 };// ^ | // // Queen eval terms // --------------------------------------------------------------------------- // // // A queen can move to between 0..27 squares // ----------------------------------------- // 0 = supported enemy P/N/B/R, friend piece // 1 = unsupported enemy piece, enemy controlled empty sq, enemy Q // 2 = enemy K, no one controlled empty sq, friend controlled empty sq // // Total: 54 mobility max // static SCORE QUEEN_MOBILITY_BY_SQUARES[28] = {// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 -30, -26, -22, -17, -11, -8, -4, -2, -1, +2, +4, +7, +10, +12, +14, // | //15 16 17 18 19 20 21 22 23 24 25 26 27 +15, +16, +17, +18, +19, +20, +20, +21, +21, +22, +22, +23, +23 }; // // In "Evaluation of Material Imbalance", IM Larry Kaufman makes the // point that queens are worth more then the standard nine "points". // Thus, this scale is biased upwards by about 0.15 pawn. // static SCORE QUEEN_KING_TROPISM[8] = {// 0 1 2 3 4 5 6 7 0, +34, +28, +22, +19, +17, +16, +15 }; // // King eval terms // --------------------------------------------------------------------------- // static ULONG KING_INITIAL_COUNTER_BY_LOCATION[2][128] = { { 1, 0, 0, 0, 1, 0, 0, 1, 0,0,0,0,0,0,0,0, 1, 1, 1, 1, 1, 1, 1, 1, 0,0,0,0,0,0,0,0, 3, 3, 3, 3, 3, 3, 3, 3, 0,0,0,0,0,0,0,0, 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0, // ------------------------------------------------------------------- 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0, 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0, 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0, 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0 }, { 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0, 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0, 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0, 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0, // ------------------------------------------------------------------- 4, 4, 4, 4, 4, 4, 4, 4, 0,0,0,0,0,0,0,0, 3, 3, 3, 3, 3, 3, 3, 3, 0,0,0,0,0,0,0,0, 1, 1, 1, 1, 1, 1, 1, 1, 0,0,0,0,0,0,0,0, 1, 0, 0, 0, 1, 0, 0, 1, 0,0,0,0,0,0,0,0 } }; static SCORE KING_TO_CENTER[128] = { +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0, +1, +3, +5, +7, +7, +5, +3, +1, 0,0,0,0,0,0,0,0, +3, +5, +13, +15, +15, +13, +5, +3, 0,0,0,0,0,0,0,0, +5, +7, +17, +23, +23, +17, +7, +5, 0,0,0,0,0,0,0,0, // ------------------------------------------------------------------ +5, +7, +17, +23, +23, +17, +7, +5, 0,0,0,0,0,0,0,0, +3, +5, +13, +15, +15, +13, +5, +3, 0,0,0,0,0,0,0,0, +1, +3, +5, +7, +7, +5, +3, +1, 0,0,0,0,0,0,0,0, +0, +0, +0, +0, +0, +0, +0, +0, 0,0,0,0,0,0,0,0 }; // // If a side has less than this much material we don't bother scoring // the other side's king safety. Note: this doesn't incl pawn // material. // #define DO_KING_SAFETY_THRESHOLD \ (VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1) // // If a side has less than this much material the other side's king // can come out. Note: this doesn't incl pawn material. // #define KEEP_KING_AT_HOME_THRESHOLD \ (VALUE_ROOK + VALUE_ROOK + VALUE_BISHOP + VALUE_KING + 1) static ULONG KING_COUNTER_BY_ATTACK_PATTERN[32] = { //p 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 //m 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 //r 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 //q 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 //k 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0,0,2,2,1,1,3,3,1,1,3,3,2,2,4,4,2,2,3,3,2,2,4,4,2,2,4,4,3,4,5,5 }; static SCORE KING_SAFETY_BY_COUNTER[42] = { -0, -4, -8, -12, -16, -20, -25, -31, -38, -50, -63, -76, -90, -105,-120,-135,-150,-165,-180,-195,-210,-225,-240,-255,-270,-290, -310,-330,-350,-370,-390,-410,-440,-470,-500,-500,-500,-500,-500, -500,-500,-500 }; // Added 2026-08-30: penalty for the enemy queen directly attacking or // x-raying squares near this king, indexed by count (capped at 6). // Replaces the old per-queen "pointing near enemy K" bolt-on // (QUEEN_ATTACKS_SQ_NEXT_TO_KING, computed via the queen's own // mobility ray-cast, direct-attacks only) with a version computed from // data this loop already reads for king safety (no extra attack-table // work) and that also catches x-ray/latent queen threats the old // version missed. Calibrated against the removed term's own magnitude // (uNearKing * 8, capped at 6 -> max 48) rather than guessed, since // that's what was empirically shown to matter for real solve rate -- // see the 180-position king-safety-term distribution sampled // 2026-08-30 (median -27, p75 -16, only ~1% worse than -200) for the // scale this needs to slot into. static SCORE KING_QUEEN_PROXIMITY_DANGER[7] = {// 0 1 2 3 4 5 6 +0, -8, -16, -24, -32, -40, -48 }; static SCORE KING_MISSING_ONE_CASTLE_OPTION = -23; // Flat "this is bad" flags for _EvalTrappedPieces -- not an attempt to // price the material outcome (search resolves that), just a nudge away // from positions with a piece that looks stuck. ENPRISE_AND_TRAPPED is // larger because that case is opponent-to-move / imminently capturable; // TRAPPED_WITH_MOVE is our own move, so there's still a chance to // wriggle out. static SCORE ENPRISE_AND_TRAPPED_PENALTY = -50; static SCORE TRAPPED_WITH_MOVE_PENALTY = -10; typedef struct _DNA_BASE_SIZE { SCORE *pBase; ULONG uCount; } DNA_BASE_SIZE; #define DNA_VAR(x) {(SCORE *)&(x), 1} #define DNA_ARRAY(x) {(SCORE *)(x), ARRAY_LENGTH(x)} #define DNA_MATRIX(x) {(SCORE *)(x), ARRAY_LENGTH(x) * ARRAY_LENGTH((x)[0])} static DNA_BASE_SIZE g_EvalDNA[] = { DNA_ARRAY(REDUCED_MATERIAL_DOWN_SCALER), DNA_ARRAY(CASTLE_AND_DEVELOPMENT_SCALER), DNA_ARRAY(REDUCED_MATERIAL_UP_SCALER), DNA_ARRAY(PASSER_MATERIAL_UP_SCALER), DNA_ARRAY(PAWN_CENTRALITY_BONUS), DNA_ARRAY(BACKWARD_SHIELDED_BY_LOCATION), DNA_ARRAY(BACKWARD_EXPOSED_BY_LOCATION), DNA_MATRIX(DOUBLED_PAWN_PENALTY_BY_COUNT), DNA_ARRAY(ISOLATED_PAWN_BY_PAWNFILE), DNA_VAR(ISOLATED_EXPOSED_PAWN), DNA_VAR(ISOLATED_DOUBLED_PAWN), DNA_MATRIX(PASSER_BY_RANK), DNA_MATRIX(CANDIDATE_PASSER_BY_RANK), DNA_MATRIX(CONNECTED_PASSERS_BY_RANK), DNA_MATRIX(SUPPORTED_PASSER_BY_RANK), DNA_MATRIX(KING_SUPPORTING_OWN_PASSER_BY_RANK), DNA_ARRAY(OUTSIDE_PASSER_BY_DISTANCE), DNA_ARRAY(PASSER_BONUS_AS_MATERIAL_COMES_OFF), DNA_VAR(RACER_WINS_RACE), DNA_ARRAY(UNDEVELOPED_MINORS_IN_OPENING), DNA_VAR(BISHOP_OVER_KNIGHT_IN_ENDGAME), DNA_VAR(BISHOP_PAIR_BONUS), DNA_ARRAY(STATIONARY_PAWN_ON_BISHOP_COLOR), DNA_ARRAY(TRANSIENT_PAWN_ON_BISHOP_COLOR), DNA_ARRAY(BISHOP_MOBILITY_BY_SQUARES), DNA_ARRAY(BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING), DNA_ARRAY(KNIGHT_CENTRALITY_BONUS), DNA_ARRAY(KNIGHT_KING_TROPISM_BONUS), DNA_ARRAY(KNIGHT_UNASSAILABLE_BY_DIST_FROM_EKING), DNA_MATRIX(KNIGHT_ON_INTERESTING_SQUARE_BY_RANK), DNA_ARRAY(KNIGHT_MOBILITY_BY_COUNT), DNA_ARRAY(KNIGHT_WITH_N_PAWNS_SUPPORTING), DNA_ARRAY(KNIGHT_IN_CLOSED_POSITION), DNA_VAR(ROOK_ON_FULL_OPEN), DNA_VAR(ROOK_ON_HALF_OPEN_WITH_ENEMY), DNA_VAR(ROOK_ON_HALF_OPEN_WITH_FRIEND), DNA_VAR(ROOK_WITH_ENEMY_PASSER), DNA_VAR(ROOK_WITH_FRIEND_PASSER), DNA_MATRIX(ROOK_BEHIND_PASSER_BY_PASSER_RANK), DNA_MATRIX(ROOK_LEADS_PASSER_BY_PASSER_RANK), DNA_VAR(KING_TRAPPING_ROOK), DNA_ARRAY(ROOK_VALUE_AS_PAWNS_COME_OFF), DNA_VAR(ROOK_CONNECTED_VERT), DNA_VAR(ROOK_CONNECTED_HORIZ), DNA_ARRAY(ROOK_MOBILITY_BY_SQUARES), DNA_ARRAY(QUEEN_MOBILITY_BY_SQUARES), DNA_ARRAY(QUEEN_KING_TROPISM), DNA_MATRIX(KING_INITIAL_COUNTER_BY_LOCATION), DNA_ARRAY(KING_TO_CENTER), DNA_ARRAY(KING_SAFETY_BY_COUNTER), DNA_ARRAY(KING_QUEEN_PROXIMITY_DANGER), DNA_VAR(KING_MISSING_ONE_CASTLE_OPTION) }; ULONG DNABufferSizeBytes() { ULONG u; ULONG uSize = 0; for (u = 0; u < ARRAY_LENGTH(g_EvalDNA); u++) { uSize += 10 * g_EvalDNA[u].uCount; } return uSize + 1; } char * ExportEvalDNA() { ULONG uSize = DNABufferSizeBytes(); char *p = malloc(uSize); ULONG u, v; SCORE *q; FLAG fFirst = TRUE; memset(p, 0, uSize); for (u = 0; u < ARRAY_LENGTH(g_EvalDNA); u++) { q = g_EvalDNA[u].pBase; for (v = 0; v < g_EvalDNA[u].uCount; v++) { if (!fFirst) { sprintf(p, "%s,%d", p, *q); } else { sprintf(p, "%s%d", p, *q); fFirst = FALSE; } q++; } strcat(p, "\n"); fFirst = TRUE; } return p; // caller must free } FLAG WriteEvalDNA(char *szFilename) { FILE *p = NULL; char *q = NULL; FLAG fRet = FALSE; if (SystemDoesFileExist(szFilename)) goto end; p = fopen(szFilename, "a+b"); if (!p) goto end; q = ExportEvalDNA(); if (!q) goto end; fprintf(p, "%s", q); fRet = TRUE; end: if (p) fclose(p); if (q) free(q); return fRet; } FLAG ImportEvalDNA(char *p) { ULONG u, v; for (u = 0; u < ARRAY_LENGTH(g_EvalDNA); u++) { for (v = 0; v < g_EvalDNA[u].uCount; v++) { while(*p && (!isdigit(*p) && (*p != '-'))) p++; if (*p == '\0') return FALSE; *(g_EvalDNA[u].pBase + v) = atoi(p); while(*p && (isdigit(*p) || (*p == '-'))) p++; } } return TRUE; } FLAG ReadEvalDNA(char *szFilename) { FILE *p = NULL; ULONG uSize = DNABufferSizeBytes(); char *q = malloc(uSize); FLAG fRet = FALSE; static char line[1024]; char *c; if (!q) goto end; memset(q, 0, uSize); p = fopen(szFilename, "rb"); if (!p) { Trace("Failed to open file \"%s\"\n", szFilename); goto end; } while(fgets(line, 1024, p) != NULL) { c = strchr(line, '#'); if (c) *c = '\0'; c = line; strcat(q, c); } fRet = ImportEvalDNA(q); end: if (q) free(q); if (p) fclose(p); return fRet; } // // Misc stuff // --------------------------------------------------------------------------- // // // The three ranks "around" a rank (indexed by rank, used in eval.c) // BITBOARD BBADJACENT_RANKS[9] = { 0, BBRANK11 | BBRANK22, BBRANK11 | BBRANK22 | BBRANK33, BBRANK22 | BBRANK33 | BBRANK44, BBRANK33 | BBRANK44 | BBRANK55, BBRANK44 | BBRANK55 | BBRANK66, BBRANK55 | BBRANK66 | BBRANK77, BBRANK66 | BBRANK77 | BBRANK88, BBRANK77 | BBRANK88 }; // // The files "around" one, indexed by file (used in eval.c) // BITBOARD BBADJACENT_FILES[8] = { // A = 0 BBFILEB, // B = 1 BBFILEA | BBFILEC, // C = 2 BBFILEB | BBFILED, // D = 3 BBFILEC | BBFILEE, // E = 4 BBFILED | BBFILEF, // F = 5 BBFILEE | BBFILEG, // G = 6 BBFILEF | BBFILEH, // H = 7 BBFILEG }; // // The ranks preceeding one, indexed by rank/color of pawn (used in // eval.c) // BITBOARD BBPRECEEDING_RANKS[8][2] = { // BLACK WHITE // ----- ----- { // 0 == RANK8 BBRANK72, 0 }, { // 1 == RANK7 BBRANK62, 0 }, { // 2 == RANK6 BBRANK52, BBRANK77 }, { // 3 == RANK5 BBRANK42, BBRANK67 }, { // 4 == RANK4 BBRANK32, BBRANK57 }, { // 5 == RANK3 BBRANK22, BBRANK47 }, { // 6 == RANK2 0, BBRANK37 }, { // 7 == RANK1 0, BBRANK27 } }; static FLAG _IsSquareSafeFromEnemyPawn(IN POSITION *pos, IN COOR c, IN BITBOARD bb) /** Routine description: Determine if a piece in square c is "outposted". A piece is outposted if no enemy pawns can advance to attack it / drive it away. Parameters: POSITION *pos : the board COOR c : the square (must not be empty!) BITBOARD bb : the bitboard of pawns you want to consider Return value: FLAG : TRUE if the piece is safe/outposted, FALSE otherwise **/ { ULONG uColor = GET_COLOR(pos->rgSquare[c].pPiece); ULONG uFile, uRank; #ifdef DEBUG COOR cSquare; PIECE p = pos->rgSquare[c].pPiece; BITBOARD dbb = bb; ASSERT(IS_ON_BOARD(c)); ASSERT(p && (IS_BISHOP(p) || IS_KNIGHT(p) || IS_PAWN(p))); ASSERT(IS_VALID_COLOR(uColor)); #endif uFile = FILE(c); ASSERT(uFile < 8); bb &= BBADJACENT_FILES[uFile]; ASSERT(((c & 0x70) >> 4) == (c >> 4)); uRank = c >> 4; ASSERT(uRank < 8); bb &= BBPRECEEDING_RANKS[uRank][uColor]; #ifdef DEBUG if (bb != 0) { while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&dbb))) { ASSERT(pos->rgSquare[cSquare].pPiece); ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); if ((FILE(cSquare) == (FILE(c) - 1)) || (FILE(cSquare) == (FILE(c) + 1))) { switch(uColor) { case WHITE: if (RANK(cSquare) > RANK(c)) { return(FALSE); } break; case BLACK: if (RANK(cSquare) < RANK(c)) { return(FALSE); } break; } } } ASSERT(FALSE); } else { ASSERT(bb == 0); while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&dbb))) { ASSERT(pos->rgSquare[cSquare].pPiece); ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); if ((FILE(cSquare) == (FILE(c) - 1)) || (FILE(cSquare) == (FILE(c) + 1))) { switch(uColor) { case WHITE: if (RANK(cSquare) > RANK(c)) { ASSERT(FALSE); } break; case BLACK: if (RANK(cSquare) < RANK(c)) { ASSERT(FALSE); } break; } } } } #endif return(bb == 0); } static ULONG _WhoControlsSquareFast(IN POSITION *pos, IN COOR c) /** Routine description: Determine which side controls a board square. Parameters: POSITION *pos : the board COOR c : the square in question Return value: static ULONG : (ULONG)-1 if neither side controls it or the sides control it evenly, WHITE if white controls the square, or BLACK if black controls the square. **/ { // // board_representation/EVAL.md section 9: bvAttacks/ATTACK_BITV // retired entirely now that king (the last piece to write it) has // converted -- every side's presence at this square is read // straight off its own bbXAttacks accumulator instead. Bit // positions (PAWN_BIT/MINOR_BIT/ROOK_BIT/QUEEN_BIT/KING_BIT) are // unchanged so g_SwapTable's indexing below still sees the same // shape it always has; x-ray-only presence (bbMinorXrayAttacks/ // bbRookXrayAttacks/bbQueenXrayAttacks) ORs into the same // byte-scale bit as its direct-attack counterpart, matching the // old struct's .uSmall/.uXray sharing one bit position for // "attacks or x-rays" (see g_SwapTable's own construction). King // has no x-ray (can't move through a blocker). This function is // only ever called after *both* kings finish evaluating (the // passed-pawn re-check and trapped-piece/danger passes all run // after Eval()'s king-eval block), so pos->bbKingAttacks is always // fully populated for both colors here -- unlike _EvalKing's own, // deliberately asymmetric internal bvAttack (see that function's // comment), this is a plain, symmetric fact query. // BITBOARD sq = COOR_TO_BB(c); ULONG uWhite = ((pos->bbPawnAttacks[WHITE] & sq) ? PAWN_BIT : 0) | ((pos->bbMinorAttacks[WHITE] & sq) ? MINOR_BIT : 0) | ((pos->bbMinorXrayAttacks[WHITE] & sq) ? MINOR_BIT : 0) | ((pos->bbRookAttacks[WHITE] & sq) ? ROOK_BIT : 0) | ((pos->bbRookXrayAttacks[WHITE] & sq) ? ROOK_BIT : 0) | ((pos->bbQueenAttacks[WHITE] & sq) ? QUEEN_BIT : 0) | ((pos->bbQueenXrayAttacks[WHITE] & sq) ? QUEEN_BIT : 0) | ((pos->bbKingAttacks[WHITE] & sq) ? KING_BIT : 0); ULONG uBlack = ((pos->bbPawnAttacks[BLACK] & sq) ? PAWN_BIT : 0) | ((pos->bbMinorAttacks[BLACK] & sq) ? MINOR_BIT : 0) | ((pos->bbMinorXrayAttacks[BLACK] & sq) ? MINOR_BIT : 0) | ((pos->bbRookAttacks[BLACK] & sq) ? ROOK_BIT : 0) | ((pos->bbRookXrayAttacks[BLACK] & sq) ? ROOK_BIT : 0) | ((pos->bbQueenAttacks[BLACK] & sq) ? QUEEN_BIT : 0) | ((pos->bbQueenXrayAttacks[BLACK] & sq) ? QUEEN_BIT : 0) | ((pos->bbKingAttacks[BLACK] & sq) ? KING_BIT : 0); ULONG u; PIECE p; CHAR ch; // g_SwapTable is only [14][32][32] (data.c) -- this is the bound // that actually matters, not just "no garbage above bit 7" below. // PAWN_BIT..KING_BIT briefly didn't fit in bits 0-4 earlier this // session (when _WhoControlsSquareFast was converted to read them // directly instead of going through the old ATTACK_BITV struct) // and silently indexed out of bounds on every attacked square -- // the (& 0xFFFFFF00) checks below never caught it (board_ // representation/EVAL.md section 9). ASSERT(uWhite < 32); ASSERT(uBlack < 32); ASSERT((uWhite & 0xFFFFFF00) == 0); ASSERT((uBlack & 0xFFFFFF00) == 0); p = pos->rgSquare[c].pPiece; ch = g_SwapTable[p][uWhite][uBlack]; if (ch != 0) { u = ch; u >>= 7; u = u & 1; ASSERT(((u == 1) && (ch < 0)) || ((u == 0) && (ch > 0))); return(FLIP(u)); } return((ULONG)-1); } /** Routine description: Zero out the Eval()-scoped attack-bitboard accumulators before building them for this call. Used to clear the old per-square rgSquare[c|8].bvAttacks/ATTACK_BITV structure too (a full-board macro-unrolled loop, since every square's storage needed zeroing); retired along with that structure (2026-09-05, board_ representation/EVAL.md section 9) -- nothing left to clear but the bbXAttacks accumulators themselves. Parameters: POSITION *pos Return value: void **/ static void _ClearAttackTables(IN OUT POSITION *pos) { pos->bbMinorAttacks[WHITE] = pos->bbMinorAttacks[BLACK] = 0; pos->bbMinorXrayAttacks[WHITE] = pos->bbMinorXrayAttacks[BLACK] = 0; pos->bbRookAttacks[WHITE] = pos->bbRookAttacks[BLACK] = 0; pos->bbRookXrayAttacks[WHITE] = pos->bbRookXrayAttacks[BLACK] = 0; pos->bbQueenAttacks[WHITE] = pos->bbQueenAttacks[BLACK] = 0; pos->bbQueenXrayAttacks[WHITE] = pos->bbQueenXrayAttacks[BLACK] = 0; pos->bbKingAttacks[WHITE] = pos->bbKingAttacks[BLACK] = 0; } static INLINE void _InitializePawnHashEntry(IN OUT PAWN_HASH_ENTRY *pHash, IN POSITION *pos) /** Routine description: Parameters: PAWN_HASH_ENTRY *pHash, POSITION *pos Return value: static INLINE void **/ { memset(pHash, 0, sizeof(PAWN_HASH_ENTRY)); pHash->u64Key = pos->u64PawnSig; } static INLINE void _EvaluateCandidatePasser(IN POSITION *pos, IN OUT PAWN_HASH_ENTRY *pHash, IN COOR c) /** Routine description: Parameters: POSITION *pos, PAWN_HASH_ENTRY *pHash, COOR c Return value: static INLINE **/ { COOR c1, cSquare; PIECE pSentry, pHelper; ULONG uPawnFile = FILE(c) + 1; BITBOARD bb; ULONG uSentries, uHelpers; ULONG uColor; int d1; #ifdef DEBUG COOR cVerifySquare; ULONG uVerifySentries; #endif ASSERT(IS_ON_BOARD(c)); ASSERT(RANK(c) >= 2); ASSERT(RANK(c) <= 7); pHelper = pos->rgSquare[c].pPiece; ASSERT(IS_PAWN(pHelper)); uColor = GET_COLOR(pHelper); ASSERT(IS_VALID_COLOR(uColor)); if (pHash->uCountPerFile[FLIP(uColor)][uPawnFile] != 0) { ASSERT((pos->bbPawns[FLIP(uColor)] & BBFILE[FILE(c)]) != 0); // // The only way a pawn can be a passer/candidate if the other // side has a pawn on its same file is if that enemy pawn is // behind it. This is a corner case but it is important to // detect all passers. // switch(uColor) { case WHITE: bb = pos->bbPawns[BLACK] & BBFILE[FILE(c)]; ASSERT(bb); while(IS_ON_BOARD(c1 = CoorFromBitBoardRank8ToRank1(&bb))) { ASSERT(FILE(c1) == FILE(c)); if (c1 < c) return; } break; case BLACK: bb = pos->bbPawns[WHITE] & BBFILE[FILE(c)]; ASSERT(bb); while(IS_ON_BOARD(c1 = CoorFromBitBoardRank1ToRank8(&bb))) { ASSERT(FILE(c1) == FILE(c)); if (c1 > c) return; } break; } } // // Count FLIP(uColor)'s sentries and determine the location of the // critical square. Note if there are no sentries then this // pawn (on square c) is a passer already, not a candidate. // bb = pos->bbPawns[FLIP(uColor)] & BBADJACENT_FILES[FILE(c)]; bb &= BBPRECEEDING_RANKS[(c & 0x70) >> 4][uColor]; if (!bb) { ASSERT(CountBits(bb) == 0); // // There are no sentries so this pawn is a passer. // pHash->bbPasserLocations[uColor] |= COOR_TO_BB(c); ASSERT(CountBits(pHash->bbPasserLocations[uColor]) > 0); ASSERT(CountBits(pHash->bbPasserLocations[uColor]) <= 8); EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], PASSER_BY_RANK[uColor][RANK(c)], "passed pawn"); // // However, don't give doubled passers such a big bonus. // if (IS_PAWN(pos->rgSquare[c - 16 * g_iAhead[uColor]].pPiece)) { EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], -(PASSER_BY_RANK[uColor][RANK(c)] / 2), "doubled passer"); } return; } uSentries = CountBits(bb); // // There's one or more sentry pawns so we'll look for helpers to // decide if this pawn is a candidate passer. // if (uColor == WHITE) { pSentry = BLACK_PAWN; cSquare = CoorFromBitBoardRank1ToRank8(&bb); cSquare += 0x10; } else { pSentry = WHITE_PAWN; cSquare = CoorFromBitBoardRank8ToRank1(&bb); cSquare -= 0x10; } cSquare &= 0xF0; cSquare |= FILE(c); ASSERT(IS_ON_BOARD(cSquare)); ASSERT(FILE(cSquare) == FILE(c)); #ifdef DEBUG uVerifySentries = 0; cVerifySquare = 0; d1 = 16 * g_iAhead[uColor]; c1 = c + d1; do { if (IS_ON_BOARD(c1 - 1)) { if (pos->rgSquare[c1 - 1].pPiece == pSentry) { uVerifySentries++; if (0 == cVerifySquare) { cVerifySquare = c1 - d1; ASSERT(cVerifySquare == cSquare); } } } if (IS_ON_BOARD(c1 + 1)) { if (pos->rgSquare[c1 + 1].pPiece == pSentry) { uVerifySentries++; if (0 == cVerifySquare) { cVerifySquare = c1 - d1; ASSERT(cVerifySquare == cSquare); } } } c1 = c1 + d1; } while(!RANK8(c1) && !RANK1(c1)); ASSERT(uVerifySentries == uSentries); #endif // // Note: we don't do this with bitboards because we want to not // consider a pawn to be a potential passer if it can't safely // advance to get into helper position. // // IDEA: scale the candidate passer bonus based on rank AND on // the distance the helper(s) have to go to get into position. // // FIXED 2026-09-05 (found while removing rgSquare[c|8].bvAttacks // entirely, board_representation/EVAL.md section 9; landed as its // own change with its own before/after check, not bundled into // that mechanical cleanup commit). This function runs from // _EvalPawns, which is the *first* piece type evaluated each // Eval() call, so the only attack data that actually exists yet is // pos->bbPawnAttacks -- every non-pawn piece, and (since commit // 57502d6) pawns' own old bvAttacks write, happens later in the // same call. The original condition here read the old combined // bvAttacks word (all piece types), which had been silently // always-zero -- and this gate silently always-true -- since // 57502d6; narrowed to what's actually valid at this point: // pos->bbPawnAttacks only. This is narrower than the original // presumably intended (any piece type, not just pawns), but it's // a real, correct check instead of a fake one, and pawns are the // dominant real-world case for contesting a helper-pawn square // anyway. // uHelpers = 0; d1 = 16 * g_iAhead[uColor]; ASSERT(-d1 == 16 * g_iBehind[uColor]); c1 = cSquare + 1 - d1; if ((IS_ON_BOARD(c1)) && (pHash->uCountPerFile[uColor][FILE(c1) + 1])) { ASSERT(pos->bbPawns[uColor] & BBFILE[FILE(c1)]); if (!(pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(c1)) || (pos->bbPawnAttacks[uColor] & COOR_TO_BB(c1))) { // // The square c1 the place a helper pawn must get to in // order to aide the candidate past a sentry. // if (pos->rgSquare[c1].pPiece == pHelper) { uHelpers = 1; goto do_left; } // // There is no helper pawn in the support position yet. // See if one can get there. // c1 = c1 - d1; while (IS_ON_BOARD(c1) && ((!(pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(c1))) || (pos->bbPawnAttacks[uColor] & COOR_TO_BB(c1)))) { if (pos->rgSquare[c1].pPiece == pHelper) { uHelpers = 1; break; } else if (pos->rgSquare[c1].pPiece == pSentry) { break; } c1 = c1 - d1; } } } do_left: c1 = cSquare - 1 - d1; if ((IS_ON_BOARD(c1)) && (pHash->uCountPerFile[uColor][FILE(c1) + 1])) { ASSERT(pos->bbPawns[uColor] & BBFILE[FILE(c1)]); if (!(pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(c1)) || (pos->bbPawnAttacks[uColor] & COOR_TO_BB(c1))) { // // The square c1 is the place a helper pawn must get to in // order to aide the candidate. // if (pos->rgSquare[c1].pPiece == pHelper) { uHelpers++; goto done_helpers; } // // There is no pawn in the left support position yet. See // if one can get there. // c1 -= d1; while (IS_ON_BOARD(c1) && ((!(pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(c1))) || (pos->bbPawnAttacks[uColor] & COOR_TO_BB(c1)))) { if (pos->rgSquare[c1].pPiece == pHelper) { uHelpers++; break; } else if (pos->rgSquare[c1].pPiece == pSentry) { break; } c1 -= d1; } } } done_helpers: if ((uHelpers >= uSentries) || ((pHash->uCountPerFile[uColor][uPawnFile + 1] + pHash->uCountPerFile[uColor][uPawnFile - 1]) && (((WHITE == uColor) && (RANK(c) > 5)) || ((BLACK == uColor) && (RANK(c) < 4))))) { ASSERT(CANDIDATE_PASSER_BY_RANK[uColor][RANK(c)] > 0); EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], CANDIDATE_PASSER_BY_RANK[uColor][RANK(c)], "candidate passer"); // // If the other side has no pieces then give this candidate an // extra bonus -- used to be a second full copy of the same // CANDIDATE_PASSER_BY_RANK value (an exact duplicate of the // term just added above, not a different angle on it), now a // fractional modifier. // if (pos->uNonPawnCount[FLIP(uColor)][0] == 1) { EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], CANDIDATE_PASSER_BY_RANK[uColor][RANK(c)] / 2, "candidate passer in endgame"); } } } static void _EvaluateConnectedSupportedOutsidePassers(IN POSITION *pos, IN OUT PAWN_HASH_ENTRY *pHash) /** Routine description: Parameters: POSITION *pos, PAWN_HASH_ENTRY *pHash Return value: void **/ { ULONG uColor; ULONG u, v; COOR c; COOR cLeftmostPasser, cRightmostPasser; PIECE pFriend; COOR cSupport; BITBOARD bb; static const INT iDelta[5] = { -1, +1, +17, +15, 0 }; FOREACH_COLOR(uColor) { ASSERT(IS_VALID_COLOR(uColor)); if (pHash->bbPasserLocations[uColor]) { ASSERT(pos->uPawnCount[uColor] > 0); pFriend = BLACK_PAWN | uColor; cLeftmostPasser = cRightmostPasser = ILLEGAL_COOR; for (u = A; u <= H; u++) { bb = pHash->bbPasserLocations[uColor] & BBFILE[u]; if (bb != 0) { ASSERT(pHash->uCountPerFile[uColor][u+1] != 0); while(IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb))) { // // Keep track of leftmost/rightmost passer for // outside passer code later on. // ASSERT(FILE(c) == u); ASSERT(RANK(c) > 1); ASSERT(RANK(c) < 8); if (cLeftmostPasser == ILLEGAL_COOR) { cLeftmostPasser = c; } cRightmostPasser = c; v = 0; while(iDelta[v] != 0) { cSupport = c + iDelta[v] * g_iBehind[uColor]; if (IS_ON_BOARD(cSupport)) { if (pHash->bbPasserLocations[uColor] & COOR_TO_BB(cSupport)) { ASSERT(CONNECTED_PASSERS_BY_RANK[uColor] [RANK(c)] > 0); EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], CONNECTED_PASSERS_BY_RANK[uColor] [RANK(c)], "connected passers"); // // TODO: connected passers vs a R is // strong. // } else if (pos->rgSquare[cSupport].pPiece == pFriend) { ASSERT(SUPPORTED_PASSER_BY_RANK[uColor] [RANK(c)] > 0); EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], SUPPORTED_PASSER_BY_RANK[uColor] [RANK(c)], "supported passer"); } } v++; } } } } #ifdef DEBUG ASSERT(IS_ON_BOARD(cLeftmostPasser)); ASSERT(IS_ON_BOARD(cRightmostPasser)); ASSERT(RANK(cLeftmostPasser) > 1); ASSERT(RANK(cRightmostPasser) > 1); ASSERT(RANK(cLeftmostPasser) < 8); ASSERT(RANK(cRightmostPasser) < 8); if (CountBits(pHash->bbPasserLocations[uColor]) == 1) { ASSERT(cLeftmostPasser == cRightmostPasser); } #endif for (u = A; u <= D; u++) { if (pHash->uCountPerFile[FLIP(uColor)][u + 1] != 0) { ASSERT(pos->bbPawns[FLIP(uColor)] & BBFILE[u]); if (!(pHash->bbPasserLocations[FLIP(uColor)] & BBFILE[u])) { break; } } } if (u > FILE(cLeftmostPasser)) { EVAL_TERM(uColor, PAWN, ILLEGAL_COOR, pHash->iScore[uColor], OUTSIDE_PASSER_BY_DISTANCE[u-FILE(cLeftmostPasser)], "left outside passer"); } for (u = H; u >= E; u--) { if (pHash->uCountPerFile[FLIP(uColor)][u + 1] != 0) { ASSERT(pos->bbPawns[FLIP(uColor)] & BBFILE[u]); if (!(pHash->bbPasserLocations[FLIP(uColor)] & BBFILE[u])) { break; } } } if (u < FILE(cRightmostPasser)) { EVAL_TERM(uColor, PAWN, ILLEGAL_COOR, pHash->iScore[uColor], OUTSIDE_PASSER_BY_DISTANCE[FILE(cRightmostPasser)-u], "right outside passer"); } } } } // board_representation/EVAL.md section 2/0d: bitboard replacement for // the old per-pawn mailbox delta+IS_ON_BOARD population this function // used to do. pos->bbPawns[2] already has every pawn location // (incrementally maintained, chess.h) with zero per-pawn iteration // needed to query it -- the exact shift-and-mask technique // generate.c's _GenerateAllPawnMovesBB already uses to bulk-generate a // whole side's pawn captures (its bbCapLeft/bbCapRight, before being // masked down to actual enemy-occupied squares) is exactly "every // square this side's pawns attack" -- reused here verbatim, minus the // enemy-occupancy mask, since attack-bit population doesn't care what // (if anything) sits on the attacked square. See that function's // header comment for the full square-numbering/shift-direction // derivation (WHITE forward = bb >> 8, BLACK forward = bb << 8, // diagonals need the *opposite* file excluded to prevent same-row // wraparound) -- not repeated here. // // Unlike the old version, this does NOT write PAWN_BIT into the old // rgSquare[c|8].bvAttacks/ATTACK_BITV mechanism (retired entirely as // of 2026-09-05, once king -- the last piece writing it -- converted // too) -- pos->bbPawnAttacks[2] (chess.h) is the single source of // truth for "does a pawn attack this square", read directly by every // consumer. _ClearAttackTables(pos) still needs to run here first, to // zero the other bbXAttacks accumulators knight/bishop/rook/queen/king // populate as they each evaluate. static void _PopulatePawnAttackBits(IN OUT POSITION *pos) /** Routine description: Populate the attack table with pawn bits. Parameters: POSITION *pos : the board Return value: void **/ { _ClearAttackTables(pos); pos->bbPawnAttacks[BLACK] = ((pos->bbPawns[BLACK] & ~BBFILE[0]) << 7) | ((pos->bbPawns[BLACK] & ~BBFILE[7]) << 9); pos->bbPawnAttacks[WHITE] = ((pos->bbPawns[WHITE] & ~BBFILE[0]) >> 9) | ((pos->bbPawns[WHITE] & ~BBFILE[7]) >> 7); } static PAWN_HASH_ENTRY * _EvalPawns(IN OUT SEARCHER_THREAD_CONTEXT *ctx, OUT FLAG *pfDeferred) /** Routine description: Evaluate pawn structures; return a ptr to a pawn hash entry. Parameters: SEARCHER_THREAD_CONTEXT *ctx : the searcher thread context Return value: PAWN_HASH_ENTRY * **/ { static ULONG uUnmovedRank[2] = { 0x10, 0x60 }; POSITION *pos = &ctx->sPosition; PAWN_HASH_ENTRY *pHash; COOR c, cSquare; PIECE p; ULONG uIsolated[2]; ULONG uDoubled[2]; SCORE iDuos[2]; ULONG u, uPawnFile; ULONG uColor; ULONG uUnsupportable; BITBOARD bb; int d1; #ifdef DEBUG SCORE t; #endif // // Now, look up the hash entry for this position. // INC(ctx->sCounters.pawnhash.u64Probes); pHash = PawnHashLookup(ctx); ASSERT(NULL != pHash); if (pHash->u64Key == pos->u64PawnSig) { *pfDeferred = TRUE; INC(ctx->sCounters.pawnhash.u64Hits); return(pHash); } // // We need the attack table to evaluate the pawns... and we missed // the hash table so we have to populate it now. Can't defer this // one. :( // *pfDeferred = FALSE; _InitializePawnHashEntry(pHash, pos); _PopulatePawnAttackBits(pos); uIsolated[BLACK] = uIsolated[WHITE] = 0; uDoubled[BLACK] = uDoubled[WHITE] = 0; iDuos[BLACK] = iDuos[WHITE] = 0; // // First pass // FOREACH_COLOR(uColor) { ASSERT(IS_VALID_COLOR(uColor)); ASSERT(pos->uPawnCount[uColor] <= 8); d1 = 16 * g_iAhead[uColor]; for (u = 0; u < pos->uPawnCount[uColor]; u++) { c = pos->cPawns[uColor][u]; ASSERT(IS_ON_BOARD(c)); ASSERT(pos->rgSquare[c].pPiece); ASSERT(IS_PAWN(pos->rgSquare[c].pPiece)); uPawnFile = FILE(c) + 1; ASSERT((1 <= uPawnFile) && (uPawnFile <= 8)); // // Update files counter // ASSERT(pHash->uCountPerFile[uColor][uPawnFile] < 6); pHash->uCountPerFile[uColor][uPawnFile]++; // // pos->bbPawns[uColor] is plain POSITION state, maintained // incrementally by move.c on every pawn move -- already // has this pawn's bit set by the time we get here, nothing // to build. (No running-count cross-check against // uCountPerFile here: unlike the old bit-by-bit // pHash->bbPawnLocations this replaces, bbPawns already // holds every pawn on the board up front, not just the // ones this loop has visited so far, so a per-iteration // "counts so far agree" comparison isn't meaningful // against it -- only a post-loop total would be.) // ASSERT(pos->bbPawns[uColor] & COOR_TO_BB(c)); // // Count unmoved pawns // pHash->uNumUnmovedPawns[uColor] += ((c & 0xF0) == uUnmovedRank[uColor]); ASSERT(pHash->uNumUnmovedPawns[uColor] <= 8); // // Detect rammed and other stationary pawns. // cSquare = c + d1; ASSERT(IS_ON_BOARD(cSquare)); p = pos->rgSquare[cSquare].pPiece; if (IS_PAWN(p)) { pHash->bbStationaryPawns[uColor] |= COOR_TO_BB(c); ASSERT(CountBits(pHash->bbStationaryPawns[uColor]) <= 8); pHash->uNumRammedPawns += OPPOSITE_COLORS(p, uColor); ASSERT(pHash->uNumRammedPawns <= 16); } // // Give central pawns a small bonus; penalize rook pawns: // EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], PAWN_CENTRALITY_BONUS[c], "centrality"); } } // // We counted rammed pawns twice (once for the black pawn and once // for the white one). Fix this now. // ASSERT(!(pHash->uNumRammedPawns & 1)); pHash->uNumRammedPawns /= 2; // // Second pass // FOREACH_COLOR(uColor) { ASSERT(IS_VALID_COLOR(uColor)); ASSERT(pos->uPawnCount[uColor] <= 8); ASSERT(CountBits(pos->bbPawns[uColor]) <= 8); d1 = 16 * g_iAhead[uColor]; for (u = 0; u < pos->uPawnCount[uColor]; u++) { c = pos->cPawns[uColor][u]; ASSERT(IS_ON_BOARD(c)); ASSERT(IS_PAWN(pos->rgSquare[c].pPiece)); ASSERT((1 < RANK(c)) && (RANK(c) < 8)); // // Find weak pawns... pawns that are isolated are weak as // are pawns that have advanced beyond their support. // Both of these can tie up one or more pieces in order to // defend them. // uUnsupportable = 0; uPawnFile = FILE(c) - 1; if (pHash->uCountPerFile[uColor][uPawnFile + 1] > 0) { bb = (pos->bbPawns[uColor] & BBFILE[uPawnFile] & BBADJACENT_RANKS[RANK(c)]); if (!bb) { bb = (pos->bbPawns[FLIP(uColor)] & BBFILE[uPawnFile]); while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) { ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); if (uColor == WHITE) { if ((cSquare & 0xF0) >= (c & 0xF0)) { ASSERT(RANK(cSquare) <= RANK(c)); uUnsupportable = 1; break; } } else { ASSERT(uColor == BLACK); if ((cSquare & 0xF0) <= (c & 0xF0)) { ASSERT(RANK(cSquare) >= RANK(c)); uUnsupportable = 1; break; } } } } } else { uUnsupportable = 1; // no friend pawn on that file } if (0 == uUnsupportable) { goto fast_skip; } ASSERT(1 == uUnsupportable); uPawnFile = FILE(c) + 1; if (pHash->uCountPerFile[uColor][uPawnFile + 1] > 0) { bb = (pos->bbPawns[uColor] & BBFILE[uPawnFile] & BBADJACENT_RANKS[RANK(c)]); if (!bb) { bb = (pos->bbPawns[FLIP(uColor)] & BBFILE[uPawnFile]); while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) { ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); if (uColor == WHITE) { if ((cSquare & 0xF0) >= (c & 0xF0)) { ASSERT(RANK(cSquare) <= RANK(c)); uUnsupportable = 2; break; } } else { ASSERT(uColor == BLACK); if ((cSquare & 0xF0) <= (c & 0xF0)) { ASSERT(RANK(cSquare) >= RANK(c)); uUnsupportable = 2; break; } } } } } else { uUnsupportable = 2; } // // If this pawn is not supportable from either side then // it is either isolated or has been pushed too far ahead // of its support. Either way it's a target -- penalize // it. // if (2 == uUnsupportable) { // // Single consolidated isolated-pawn penalty: base // severity by file, plus modifiers if the pawn is also // exposed and/or doubled. These used to be three // separate additive EVAL_TERM calls that all fired for // the same pawn, plus a fourth, whole-position // "exponential isolated" aggregate below that // double-counted the same uIsolated[] count this loop // already prices in one pawn at a time -- that // aggregate is removed entirely below rather than just // consolidated (see the comment there). // uPawnFile = FILE(c) + 1; uIsolated[uColor] += 1; ASSERT(uIsolated[uColor] > 0); ASSERT(uIsolated[uColor] <= 8); EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], (ISOLATED_PAWN_BY_PAWNFILE[uPawnFile] + ((pHash->uCountPerFile[FLIP(uColor)][uPawnFile]==0) * ISOLATED_EXPOSED_PAWN) + ((pHash->uCountPerFile[uColor][uPawnFile] > 1) * ISOLATED_DOUBLED_PAWN)), "isolated pawn"); } fast_skip: uPawnFile = FILE(c) + 1; ASSERT((1 <= uPawnFile) && (uPawnFile <= 8)); ASSERT(CountBits(pos->bbPawns[uColor] & BBFILE[FILE(c)]) == pHash->uCountPerFile[uColor][uPawnFile]); // // Keep count of doubled pawns // uDoubled[uColor] += (pHash->uCountPerFile[uColor][uPawnFile] > 1); ASSERT((0 <= uDoubled[uColor]) && (uDoubled[uColor] <= 8)); // // We can detect pawn duos (triads, etc...) and backward pawns // by considering the control of the pawn's stopsquare. // cSquare = c + d1; ASSERT(IS_ON_BOARD(cSquare)); // // At this point in Eval()'s sequence (inside _EvalPawns, // before any other piece type has run this call), pawns // are the only thing that could possibly have marked an // attack -- bvAttacks itself no longer carries the pawn // bit at all (see _PopulatePawnAttackBits), so this reads // pos->bbPawnAttacks directly instead of the (permanently // zero, at this point in execution) bvAttacks word. // if (pos->bbPawnAttacks[uColor] & COOR_TO_BB(cSquare)) { // // Count pawn duos. See "Pawn Power in Chess" pp 10-16 // #ifdef DEBUG t = ((uColor * RANK(cSquare)) + (FLIP(uColor) * (9 - RANK(cSquare)))); if (uColor == WHITE) { ASSERT(t == RANK(cSquare)); } else { ASSERT(uColor == BLACK); ASSERT(t == (9 - RANK(cSquare))); } #endif iDuos[uColor] += ((uColor * RANK(cSquare)) + (FLIP(uColor) * (9 - RANK(cSquare)))); ASSERT(iDuos[uColor] <= (7 * 8)); } else if (pos->bbPawnAttacks[FLIP(uColor)] & COOR_TO_BB(cSquare)) { // // Detect backwards pawns. See "Pawn Power in Chess" pp 25-27 // if (!IS_PAWN(pos->rgSquare[cSquare].pPiece)) { p = (BLACK_PAWN | uColor); if (((WHITE == uColor) && (RANK(c) < 4)) || ((BLACK == uColor) && (RANK(c) > 5))) { if ((IS_ON_BOARD(cSquare - 1) && pos->rgSquare[cSquare - 1].pPiece == p) || (IS_ON_BOARD(cSquare + 1) && pos->rgSquare[cSquare + 1].pPiece == p)) { // // The pawn at c is backward. Determine // whether it is shielded or exposed. // pHash->bbStationaryPawns[uColor] |= COOR_TO_BB(c); ASSERT(CountBits(pHash->bbStationaryPawns[uColor]) <= 8); if (pHash->uCountPerFile[FLIP(uColor)][uPawnFile]) { // Tuning can (and has) flipped this sign; not a real invariant. //ASSERT(BACKWARD_SHIELDED_BY_LOCATION[c] < 0); EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], BACKWARD_SHIELDED_BY_LOCATION[c], "shielded backward pawn"); } else { //ASSERT(BACKWARD_EXPOSED_BY_LOCATION[c] < 0); EVAL_TERM(uColor, PAWN, c, pHash->iScore[uColor], BACKWARD_EXPOSED_BY_LOCATION[c], "exposed backward pawn"); } } } } // // TODO: Think about backward doubled pawns; read Kauffman's // paper "All About Doubled Pawns". // } // // Handle passers / candidate passers // _EvaluateCandidatePasser(pos, pHash, c); } } // // Reward pawn duos, see "Pawn Power in Chess" pp. 10-16 // ASSERT(iDuos[WHITE] >= 0); ASSERT(iDuos[WHITE] <= 56); ASSERT(iDuos[BLACK] >= 0); ASSERT(iDuos[BLACK] <= 56); EVAL_TERM(WHITE, PAWN, ILLEGAL_COOR, pHash->iScore[WHITE], iDuos[WHITE] / 2, "pawn duos"); EVAL_TERM(BLACK, PAWN, ILLEGAL_COOR, pHash->iScore[BLACK], iDuos[BLACK] / 2, "pawn duos"); // // The penalty for doubled pawns is scaled based on two primary // factors: the presence of major pieces for the side with doubled // pawns and the number of doubled pawns on the board. The // presence of major pieces makes the doubled pawns less severe // while the presence of many doubled pawns makes the penalty for // each more severe. // ASSERT((uDoubled[WHITE] >= 0) && (uDoubled[WHITE] <= 8)); ASSERT((uDoubled[BLACK] >= 0) && (uDoubled[BLACK] <= 8)); u = pos->uNonPawnCount[BLACK][ROOK] + pos->uNonPawnCount[BLACK][QUEEN] * 2; u = MINU(u, 3); EVAL_TERM(BLACK, PAWN, ILLEGAL_COOR, pHash->iScore[BLACK], DOUBLED_PAWN_PENALTY_BY_COUNT[u][uDoubled[BLACK]], "exponential doubled"); u = pos->uNonPawnCount[WHITE][ROOK] + pos->uNonPawnCount[WHITE][QUEEN] * 2; u = MINU(u, 3); EVAL_TERM(WHITE, PAWN, ILLEGAL_COOR, pHash->iScore[WHITE], DOUBLED_PAWN_PENALTY_BY_COUNT[u][uDoubled[WHITE]], "exponential doubled"); ASSERT(uIsolated[WHITE] >= 0); ASSERT(uIsolated[WHITE] <= 8); ASSERT(uIsolated[BLACK] >= 0); ASSERT(uIsolated[BLACK] <= 8); // // Removed: a redundant "exponential isolated" whole-position term // keyed by uIsolated[color] used to be added here on top of the // per-pawn "isolated pawn" term above, which already sums once per // isolated pawn and so already scales with the same count. Having // both double-counted the same structural fact. // // // Look for connected, supported and outside passed pawns to give // extra bonuses. // _EvaluateConnectedSupportedOutsidePassers(pos, pHash); // // TODO: recognize quartgrips and stonewalls // return(pHash); } ULONG CountKingSafetyDefects(IN OUT POSITION *pos, IN ULONG uColor) /** Routine description: Determine how many defects uColor's king position has _quickly_. TODO: add more knowledge as cheaply as possible... Parameters: POSITION *pos, ULONG uColor Return value: ULONG **/ { ULONG uCounter = 0; ULONG xColor = FLIP(uColor); COOR cKing; BITBOARD bbKingZone; BITBOARD bbBlockers; BITBOARD bb; COOR c; // // Don't count king safety defects if the real eval code in // _EvalKing would not... // if (pos->uNonPawnMaterial[xColor] < DO_KING_SAFETY_THRESHOLD) { return 0; } cKing = pos->cNonPawns[uColor][0]; uCounter = KING_INITIAL_COUNTER_BY_LOCATION[uColor][cKing] >> 1; ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); ASSERT(pos->rgSquare[cKing].uIndex == 0); ASSERT(IS_ON_BOARD(cKing)); ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) == uColor); // // board_representation/EVAL.md section 9 follow-up (2026-09-05): // real, blocker-aware attack tests against the king's actual // 8-neighbor zone (the same zone _EvalKing's own KingSafetyDeltas // loop tests, now that its own bug -- testing 11 squares including // two bogus same-rank-2-away entries -- is fixed), replacing the // old CHECK_VECTOR_WITH_INDEX geometry-only lookup. That lookup // tested three hypothetical king positions (cKing-1, cKing, // cKing+1 -- always on the same rank, driven by CHECK_VECTOR's // one-dimensional index arithmetic) via a table with no board- // occupancy awareness at all -- it couldn't tell a rook on an open // file from one blocked by its own pawn, and never tested whether // a piece threatened a square one rank away from the king. This // version can't reuse pos->bbXAttacks (Eval()-scoped, and this // function is also called directly from search.c/searchsup.c as // an extension/reduction gate, independent of whether Eval() ran // on the current position at all) -- but pos->bbOccupied is a // real, always-current POSITION field regardless, so a fresh, // per-piece magic-bitboard lookup here is both cheap (same O(1) // shape as the old table lookups) and genuinely blocker-aware. // X-rays deliberately not considered via chain-following (that // would double the per-slider cost of a function called at every // lazy-eval check across the whole search tree, for what's already // a rough magnitude estimate, not an exact score) -- but the // occupancy fed to the slider lookups below is deliberately just // xColor's own pieces (_BuildFriendlySideBB(pos, xColor)), not the // whole board: a piece belonging to uColor (a defending pawn, the // king itself) is transparent to these rays, only xColor's own // pieces actually block. This gets latent-threat detection (a // rook aimed at the king zone but currently shielded by the // king's own pawn still counts) for free, at zero extra cost -- // still one magic lookup per slider, just against a different // occupancy bitboard -- by direct instruction: we don't count our // own pieces as blocking the enemy king. // bbKingZone = g_KingAttacksBB[cKing] | COOR_TO_BB(cKing); bbBlockers = _BuildFriendlySideBB(pos, xColor); bb = pos->bbPieces[xColor][KNIGHT]; while (bb) { c = CoorFromBitBoardRank8ToRank1(&bb); uCounter += (g_KnightAttacksBB[c] & bbKingZone) != 0; } bb = pos->bbPieces[xColor][BISHOP]; while (bb) { c = CoorFromBitBoardRank8ToRank1(&bb); uCounter += (_BishopAttacksBB(c, bbBlockers) & bbKingZone) != 0; } bb = pos->bbPieces[xColor][ROOK]; while (bb) { c = CoorFromBitBoardRank8ToRank1(&bb); uCounter += (_RookAttacksBB(c, bbBlockers) & bbKingZone) != 0; } // Queen hits count double -- by direct instruction: a queen near // the king is a real, distinct danger source _EvalKing's own // KING_QUEEN_PROXIMITY_DANGER term prices separately from generic // attack presence, and this counter had no way to reflect that // (a queen hitting the zone counted exactly the same as a lone // knight doing so). Raises the bound below accordingly (up to 8 // queens are legal via promotion, each now worth 2). bb = pos->bbPieces[xColor][QUEEN]; while (bb) { c = CoorFromBitBoardRank8ToRank1(&bb); uCounter += 2 * (((_RookAttacksBB(c, bbBlockers) | _BishopAttacksBB(c, bbBlockers)) & bbKingZone) != 0); } ASSERT(uCounter < 30); return uCounter; } static ULONG _ComputeFileStormDefects(IN POSITION *pos, IN const PAWN_HASH_ENTRY *pHash, IN ULONG uColor) /** Routine description: Real, exact open-file/storming-pawn defect count for uColor's king -- king's own file plus the two adjacent ones (2026-08-30: collapsed from three copy-pasted blocks that differed only in c-1/c/c+1). Extracted unchanged from _EvalKing (2026-09-05, board_representation/EVAL.md section 9's king-safety-hash prototype) so it can be called once per side from _GetFileStormDefects's cache-miss path instead of inline. Parameters: POSITION *pos PAWN_HASH_ENTRY *pHash ULONG uColor Return value: ULONG **/ { static ULONG KingStormingPawnDefects[8] = { +0, +4, +3, +1, +0, +0, +0, +0 }; static ULONG KingFileDefects[3] = { +2, +1, +0 }; ULONG ufColor = FLIP(uColor); COOR c = pos->cNonPawns[uColor][0]; ULONG uCounter = 0; COOR cFileSq, cSquare; ULONG u, v; BITBOARD bb; int w; for (w = -1; w <= 1; w++) { cFileSq = c + w; if (!IS_ON_BOARD(cFileSq)) { continue; } u = FILE(cFileSq) + 1; v = (pHash->uCountPerFile[WHITE][u] > 0) + (pHash->uCountPerFile[BLACK][u] > 0); ASSERT((v >= 0) && (v <= 2)); uCounter += (KingFileDefects[v] + ((v < 2) && ((u == 1) || (u == 8)))); bb = pos->bbPawns[ufColor] & BBFILE[u - 1]; if (bb) { if (uColor == WHITE) { cSquare = CoorFromBitBoardRank1ToRank8(&bb); } else { cSquare = CoorFromBitBoardRank8ToRank1(&bb); } ASSERT(IS_ON_BOARD(cSquare)); uCounter += KingStormingPawnDefects[DISTANCE(cSquare, c)]; } } return uCounter; } static ULONG _GetFileStormDefects(IN SEARCHER_THREAD_CONTEXT *ctx, IN POSITION *pos, IN const PAWN_HASH_ENTRY *pHash, IN ULONG uColor) /** Routine description: EXPERIMENTAL, prototype-only (2026-09-05, board_representation/ EVAL.md section 9's king-safety-hash discussion). Cache front end for _ComputeFileStormDefects, keyed on (pos->u64PawnSig, both kings' squares) -- deliberately covers only the DO_KING_SAFETY_ THRESHOLD-gated "safety branch" term (measured ~96% hit rate in general/middlegame positions), not the u<8-gated "endgame branch" terms (king-supporting-own-passer, in/out-of-action), which measured a much worse ~75-85% hit rate in bare K+P endgames and are cheap enough there that caching them isn't expected to pay off -- those stay inline in _EvalKing, uncached. On a miss, computes *both* sides' defect counts and stores them together, so whichever king _EvalKing evaluates second within the same Eval() call (kings are always evaluated black-then-white) is a guaranteed hit -- only one real miss per position, not one per king. Parameters: SEARCHER_THREAD_CONTEXT *ctx POSITION *pos PAWN_HASH_ENTRY *pHash ULONG uColor Return value: ULONG **/ { UINT64 u64Key = pos->u64PawnSig ^ g_u64SigSeeds[pos->cNonPawns[WHITE][0]][KING][WHITE] ^ g_u64SigSeeds[pos->cNonPawns[BLACK][0]][KING][BLACK]; ULONG uSlot = (ULONG)u64Key & (KING_SAFETY_HASH_TABLE_SIZE - 1); KING_SAFETY_HASH_ENTRY *pEntry = &ctx->rgKingSafetyHash[uSlot]; #ifdef PERF_COUNTERS UINT64 u64Start = SystemReadTimeStampCounter(); #endif INC(ctx->sCounters.kingsafetyhash.u64Probes); if (pEntry->u64Key == u64Key) { INC(ctx->sCounters.kingsafetyhash.u64Hits); #ifdef PERF_COUNTERS ctx->sCounters.kingsafetyhash.u64CyclesHit += (SystemReadTimeStampCounter() - u64Start); #endif return pEntry->uFileStormDefects[uColor]; } pEntry->u64Key = u64Key; pEntry->uFileStormDefects[WHITE] = _ComputeFileStormDefects(pos, pHash, WHITE); pEntry->uFileStormDefects[BLACK] = _ComputeFileStormDefects(pos, pHash, BLACK); #ifdef PERF_COUNTERS ctx->sCounters.kingsafetyhash.u64CyclesMiss += (SystemReadTimeStampCounter() - u64Start); #endif return pEntry->uFileStormDefects[uColor]; } #ifdef CALIBRATE_POSITIONAL // // board_representation/EVAL.md section 9 recalibration (2026-09-05): // histogram of (cheap defect index -> real KING_SAFETY_BY_COUNTER value), // one row per possible EstimatePositionalScore bucket (the same // MINU(10, CountKingSafetyDefects + _GetFileStormDefects) index it looks // up), so a p90-per-row read after a calibration run over a large sample // (curated suites or a slice of ecm.ep_, at real search depth so the // distribution matches what the search tree actually sees, not just root // positions) gives a fresh iKingSwingP90 table -- coarse cp bins, not raw // samples, to keep this cheap enough to leave compiled into a whole search // run rather than needing to buffer millions of individual values. // #define CALIBRATE_NUM_BUCKETS 11 #define CALIBRATE_BIN_WIDTH 5 #define CALIBRATE_NUM_BINS 200 // covers raw KING_SAFETY_BY_COUNTER 0..999 static UINT64 g_CalibrateHist[CALIBRATE_NUM_BUCKETS][CALIBRATE_NUM_BINS]; UINT64 g_uCalibrateGateChecked = 0; UINT64 g_uCalibrateGateTrue = 0; UINT64 g_uCalibrateEvalCalls = 0; static void _RecordPositionalCalibration(IN SEARCHER_THREAD_CONTEXT *ctx, IN POSITION *pos, IN const PAWN_HASH_ENTRY *pHash, IN ULONG uColor, IN ULONG uRawCounterValue) /** Routine description: Bucket one real _EvalKing sample (this side's raw, pre-material-scaled KING_SAFETY_BY_COUNTER lookup) by the same cheap defect index EstimatePositionalScore would have used to estimate it, so a later DumpPositionalCalibration can compute a fresh p90-per-bucket table. Parameters: SEARCHER_THREAD_CONTEXT *ctx POSITION *pos PAWN_HASH_ENTRY *pHash ULONG uColor ULONG uRawCounterValue : KING_SAFETY_BY_COUNTER[uCounter], before the REDUCED_MATERIAL_DOWN_SCALER scaling _EvalKing applies next -- EstimatePositionalScore's table is looked up then scaled the same way, so the table itself should be fit against the unscaled value. Return value: void **/ { ULONG uIndex; ULONG uBin; // Only sample nodes Eval()'s own base-margin ("super lazy") gate // would actually pass through to EstimatePositionalScore in a real // build -- see ctx->fCalibrateCandidate's comment in chess.h. if (FALSE == ctx->fCalibrateCandidate) { return; } uIndex = MINU(10, CountKingSafetyDefects(pos, uColor) + _GetFileStormDefects(ctx, pos, pHash, uColor)); uBin = MINU(CALIBRATE_NUM_BINS - 1, uRawCounterValue / CALIBRATE_BIN_WIDTH); g_CalibrateHist[uIndex][uBin]++; } void DumpPositionalCalibration(void) /** Routine description: Print a fresh iKingSwingP90[] table (p90 of the real, unscaled KING_SAFETY_BY_COUNTER value observed at each cheap defect-index bucket) from the histogram _RecordPositionalCalibration has been filling for the duration of this run. Only meaningful in a CALIBRATE_POSITIONAL build, after running a representative workload (the curated suites, or an ecm.ep_ slice, at real search depth -- root-only positions won't exercise the distribution of positions the search tree actually evaluates). Parameters: void Return value: void **/ { ULONG uIndex; ULONG uBin; UINT64 u64Total; UINT64 u64Running; SCORE iP90; Trace("Positional calibration: Eval() called %" COMPILER_LONGLONG_UNSIGNED_FORMAT " times\n", g_uCalibrateEvalCalls); Trace("Positional calibration: gate checked %" COMPILER_LONGLONG_UNSIGNED_FORMAT " times, true %" COMPILER_LONGLONG_UNSIGNED_FORMAT " times\n", g_uCalibrateGateChecked, g_uCalibrateGateTrue); Trace("Positional calibration (p90 per bucket, sample counts):\n"); for (uIndex = 0; uIndex < CALIBRATE_NUM_BUCKETS; uIndex++) { u64Total = 0; for (uBin = 0; uBin < CALIBRATE_NUM_BINS; uBin++) { u64Total += g_CalibrateHist[uIndex][uBin]; } if (0 == u64Total) { Trace(" bucket %2u: no samples\n", uIndex); continue; } u64Running = 0; iP90 = (SCORE)((CALIBRATE_NUM_BINS - 1) * CALIBRATE_BIN_WIDTH); for (uBin = 0; uBin < CALIBRATE_NUM_BINS; uBin++) { u64Running += g_CalibrateHist[uIndex][uBin]; if (u64Running * 10 >= u64Total * 9) { iP90 = (SCORE)(uBin * CALIBRATE_BIN_WIDTH); break; } } Trace(" bucket %2u: p90=%d (n=%" COMPILER_LONGLONG_UNSIGNED_FORMAT ")\n", uIndex, iP90, u64Total); } } #endif // CALIBRATE_POSITIONAL #if defined(CALIBRATE_BASE_MARGIN) || defined(CALIBRATE_MARGIN_SAFETY) // // board_representation/EVAL.md section 9: is LAZY_EVAL_BASE_MARGIN (the // flat, un-widened margin Eval() screens with before ever calling // EstimatePositionalScore) well-sized? Two distinct questions, tracked // separately, neither requiring the lazy-exit machinery to be disabled // (unlike the CALIBRATE_POSITIONAL king-safety work above) -- real // exits still happen normally here, so this runs at normal speed: // // 1. Once the gate passes and EstimatePositionalScore has widened the // margin, does a real exit actually happen, broken out by the // worse (max) of the two sides' cheap defect-index buckets? This // answers "does the 'very dangerous' bucket (10) ever actually // exit, or does its wide margin effectively rule it out". // 2. When the gate comes back *false* (we skip straight to full eval, // no lazy attempt tried at all) -- would a real exit have been // available anyway, had we bothered to compute the widened margin? // A "yes" here is a false negative: LAZY_EVAL_BASE_MARGIN was // tight enough to force full eval on a node that genuinely could // have taken the cheap path. // // 2026-09-06: added a material dimension -- pos->uArmyScaler[WHITE] + // pos->uArmyScaler[BLACK] (0-62), bucketed into 8 coarse bands, since // the lazy/full cost ratio measured directly (opening/middlegame vs a // bare K+P endgame) varies ~7x by material, and LAZY_EVAL_BASE_MARGIN // is currently one flat constant across all of it. #define BASE_MARGIN_MAT_BUCKETS 8 #define BASE_MARGIN_MAT_WIDTH 8 static UINT64 g_uBaseMarginExitAttempted[BASE_MARGIN_MAT_BUCKETS][11]; static UINT64 g_uBaseMarginExitTaken[BASE_MARGIN_MAT_BUCKETS][11]; static UINT64 g_uBaseMarginFalseNegative[BASE_MARGIN_MAT_BUCKETS]; static UINT64 g_uBaseMarginCorrectlyFull[BASE_MARGIN_MAT_BUCKETS]; static ULONG _MaxKingDangerBucket(IN SEARCHER_THREAD_CONTEXT *ctx, IN POSITION *pos, IN PAWN_HASH_ENTRY *pHash) /** Routine description: Cheap helper for CALIBRATE_BASE_MARGIN instrumentation only: the worse of the two sides' combined cheap defect-index buckets, same definition EstimatePositionalScore's iKingSwingP90 lookup uses. Re-reads CountKingSafetyDefects/_GetFileStormDefects rather than threading extra output params through EstimatePositionalScore -- both hit the king-safety hash on a repeat call within the same Eval(), so this is cheap (see rgKingSafetyHash's "guaranteed intra-call hit" comment). Parameters: SEARCHER_THREAD_CONTEXT *ctx POSITION *pos PAWN_HASH_ENTRY *pHash Return value: ULONG : 0-10 **/ { ULONG uWhite = MINU(10, CountKingSafetyDefects(pos, WHITE) + _GetFileStormDefects(ctx, pos, pHash, WHITE)); ULONG uBlack = MINU(10, CountKingSafetyDefects(pos, BLACK) + _GetFileStormDefects(ctx, pos, pHash, BLACK)); return MAXU(uWhite, uBlack); } static ULONG _MaterialBucket(IN POSITION *pos) /** Routine description: CALIBRATE_BASE_MARGIN instrumentation only: coarse combined-material bucket, 0 (bare kings) to BASE_MARGIN_MAT_BUCKETS-1 (full army). Parameters: POSITION *pos Return value: ULONG : 0..BASE_MARGIN_MAT_BUCKETS-1 **/ { ULONG uCombined = pos->uArmyScaler[WHITE] + pos->uArmyScaler[BLACK]; return MINU(BASE_MARGIN_MAT_BUCKETS - 1, uCombined / BASE_MARGIN_MAT_WIDTH); } void DumpBaseMarginCalibration(void) /** Routine description: Print LAZY_EVAL_BASE_MARGIN calibration stats gathered so far this run: per-(material bucket, king-danger bucket) exit-attempted/ exit-taken counts (command.c's "calibrate basemargin"), and the false-negative rate of the flat pre-EstimatePositionalScore gate, broken out by material bucket. Parameters: void Return value: void **/ { ULONG m, u; UINT64 u64FN, u64CF; for (m = 0; m < BASE_MARGIN_MAT_BUCKETS; m++) { Trace("Base margin calibration -- material bucket %u " "(combined army scaler %u-%u):\n", m, m * BASE_MARGIN_MAT_WIDTH, (m * BASE_MARGIN_MAT_WIDTH) + BASE_MARGIN_MAT_WIDTH - 1); for (u = 0; u <= 10; u++) { if (0 == g_uBaseMarginExitAttempted[m][u]) { continue; } Trace(" king-danger bucket %2u: %" COMPILER_LONGLONG_UNSIGNED_FORMAT " attempted, %" COMPILER_LONGLONG_UNSIGNED_FORMAT " exited (%.2f%%)\n", u, g_uBaseMarginExitAttempted[m][u], g_uBaseMarginExitTaken[m][u], (100.0 * (double)g_uBaseMarginExitTaken[m][u] / (double)g_uBaseMarginExitAttempted[m][u])); } u64FN = g_uBaseMarginFalseNegative[m]; u64CF = g_uBaseMarginCorrectlyFull[m]; Trace(" false negatives: %" COMPILER_LONGLONG_UNSIGNED_FORMAT " correctly full: %" COMPILER_LONGLONG_UNSIGNED_FORMAT " rate: %.4f%%\n", u64FN, u64CF, (u64FN + u64CF > 0) ? (100.0 * (double)u64FN / (double)(u64FN + u64CF)) : 0.0); } } #endif // CALIBRATE_BASE_MARGIN #ifdef CALIBRATE_MARGIN_SAFETY // // board_representation/EVAL.md section 9: for every node that actually // took a lazy exit, how far off was the real (full-eval) score from // the lazy-path score that got returned? The *maximum* observed swing, // not just its p90, is what answers "how large would LAZY_EVAL_BASE_ // MARGIN have had to be before some exit here would have been // unsound" -- unlike iKingSwingP90 (an estimate that's allowed to be // wrong ~10% of the time by design), a used margin being exceeded even // once is a genuine soundness question, so max is the right statistic // here, not a percentile. Bucketed by combined material (same bucketing // as CALIBRATE_BASE_MARGIN) since the earlier lazy/full cost-ratio // measurement showed the two regimes are very different. // static UINT64 g_uMarginSwingMax[BASE_MARGIN_MAT_BUCKETS]; static UINT64 g_uMarginSwingCount[BASE_MARGIN_MAT_BUCKETS]; static UINT64 g_uMarginSwingSum[BASE_MARGIN_MAT_BUCKETS]; // Genuine soundness check: did the swing ever exceed the *actual* // (already-widened) margin that was used to justify this specific // exit? Unlike the raw max above (which just says "how far things // moved"), this directly answers "was any real exit actually unsound". static UINT64 g_uMarginExceeded[BASE_MARGIN_MAT_BUCKETS]; static void RecordMarginSafetySwing(IN POSITION *pos, IN SCORE iSwing, IN SCORE iActualMarginUsed) /** Routine description: Record one (material bucket, |real - lazy| swing) sample for DumpBaseMarginCalibration's margin-safety report, and check whether this specific exit's swing exceeded the actual margin that was used to justify it (a genuine unsoundness event, not just "large"). Parameters: POSITION *pos SCORE iSwing : >= 0 SCORE iActualMarginUsed : the real (post-EstimatePositionalScore) alpha or beta margin this exit was taken under Return value: void **/ { ULONG uMatBucket = _MaterialBucket(pos); if (iSwing > iActualMarginUsed) { g_uMarginExceeded[uMatBucket]++; } ASSERT(iSwing >= 0); g_uMarginSwingCount[uMatBucket]++; g_uMarginSwingSum[uMatBucket] += (UINT64)iSwing; if ((UINT64)iSwing > g_uMarginSwingMax[uMatBucket]) { g_uMarginSwingMax[uMatBucket] = (UINT64)iSwing; } } void DumpMarginSafetyCalibration(void) /** Routine description: Print, per material bucket, the max and average |real - lazy| swing observed among nodes that actually took a lazy exit this run (command.c's "calibrate marginsafety"). The max is the number that tells you how large LAZY_EVAL_BASE_MARGIN would need to be, at that material level, before an exit could have been unsound. Parameters: void Return value: void **/ { ULONG m; Trace("Margin safety -- max/avg |real - lazy| swing among exits taken, " "by material bucket:\n"); for (m = 0; m < BASE_MARGIN_MAT_BUCKETS; m++) { if (0 == g_uMarginSwingCount[m]) { Trace(" material bucket %u (scaler %u-%u): no exits taken\n", m, m * BASE_MARGIN_MAT_WIDTH, (m * BASE_MARGIN_MAT_WIDTH) + BASE_MARGIN_MAT_WIDTH - 1); continue; } Trace(" material bucket %u (scaler %u-%u): max=%" COMPILER_LONGLONG_UNSIGNED_FORMAT " avg=%.1f (n=%" COMPILER_LONGLONG_UNSIGNED_FORMAT ") exceeded=%" COMPILER_LONGLONG_UNSIGNED_FORMAT "\n", m, m * BASE_MARGIN_MAT_WIDTH, (m * BASE_MARGIN_MAT_WIDTH) + BASE_MARGIN_MAT_WIDTH - 1, g_uMarginSwingMax[m], (double)g_uMarginSwingSum[m] / (double)g_uMarginSwingCount[m], g_uMarginSwingCount[m], g_uMarginExceeded[m]); } } #endif // CALIBRATE_MARGIN_SAFETY static void EstimatePositionalScore(IN SEARCHER_THREAD_CONTEXT *ctx, IN POSITION *pos, IN PAWN_HASH_ENTRY *pHash, IN OUT SCORE *piAlphaMargin, IN OUT SCORE *piBetaMargin) /** Routine description: Before doing an early lazy eval, look at the position and widen the alpha/beta margins to account for the positional terms that have not been computed yet at this point in Eval() -- king safety plus a flat residual covering mobility, passers, and everything else that genuinely requires attack-generation to know exactly. Both terms below are calibrated from measured data (CALIBRATE_ POSITIONAL instrumentation, ~1.6M full-eval samples over an ECM slice), not guessed: for each value, p90 of the *actual* score swing between this point in Eval() and full-eval completion, so the resulting margin is wrong (too narrow) on at most ~10% of calls, in either bucket. See conversation history for the percentile tables -- if re-tuning, regenerate them, don't hand- edit these numbers. Note: king safety and "everything else" turned out to be close to independent of piece count and of each other, so summing their two p90s (rather than deriving one joint p90) is a deliberately conservative (wider than strictly necessary) combination. O(1) -- no attack bitboard generation -- so it's safe to call whenever the cheap material-only lazy check (the caller's first-pass margin) wasn't enough to resolve the cutoff on its own. Parameters: SEARCHER_THREAD_CONTEXT *ctx : unused as of 2026-09-06 (_GetFileStormDefects, the only user, was pulled out of this hot path -- see the iKingSwingP90 staleness comment below); kept in the signature for call-site stability. POSITION *pos PAWN_HASH_ENTRY *pHash : unused as of 2026-09-06, same reason as ctx above. pHash->iScore is already folded into pos->iScore by this point regardless, and the passer-specific estimate this used to compute turned out to be negligible next to the residual term below. SCORE *piAlphaMargin, *piBetaMargin : widened in place, identically (no measured basis for an asymmetric alpha/beta split) Return value: void **/ { // p90 of the real, unscaled |KING_SAFETY_BY_COUNTER| magnitude // _EvalKing computes, indexed per-side by MINU(10, // CountKingSafetyDefects(side)). // // STALE as of 2026-09-06 (board_representation/EVAL.md section 9): // fit while this index also included _GetFileStormDefects(side) // (~1.28B samples, see command.c's "calibrate dump" and git history // for the fit-time numbers). _GetFileStormDefects was pulled out of // this hot path the same day -- it cost ~181 of ~1619 avg eval // cycles (measured via EVAL_TIME), more than the "cheap cached // lookup" it was assumed to be, running on ~90% of all Eval() calls // regardless of whether an exit ever resulted. Recalibration against // the now-narrower (CountKingSafetyDefects-only) index deliberately // deferred; until then this table is a reasonable but unverified // approximation. Worst-case error from dropping the storm term: the // index it fed into is clamped to 10 either way, so the max possible // per-side swing this can introduce is iKingSwingP90[10] - // iKingSwingP90[0] = 300, scaled by REDUCED_MATERIAL_DOWN_SCALER/8 // (0-1.0) -- only reachable when CountKingSafetyDefects alone is 0 // but a pawn storm alone would have maxed the old combined index. // _EvalKing's real, exact score is unaffected either way -- this // only widens/narrows the lazy-eval margin estimate. // // search.c's ">2" and searchsup.c's ">1" fixed-threshold extension // gates still read CountKingSafetyDefects' raw, unscaled return // value directly and remain equally stale -- not yet recalibrated. // static const SCORE iKingSwingP90[11] = { 10, 20, 35, 60, 90, 120, 135, 165, 195, 225, 310 }; // p90 of |mobility + passers + everything else combined|, measured // directly (no useful correlation found with piece count). static const SCORE iResidualP90 = 154; // Per-side lookup-then-scale, deliberately mirroring _EvalKing's // own shape exactly (look the raw counter up in a table first, // *then* scale that resulting estimate by the attacker's army, // rather than scaling the raw counter before the lookup) -- by // direct instruction, so this stays as symmetric as possible with // the real term it's estimating. CountKingSafetyDefects itself is // untouched by this: it keeps returning a small, unscaled count, // used as-is by its other two callers. SCORE iWhiteKingSwing; SCORE iBlackKingSwing; SCORE iKingTerm; ULONG uWhiteDefects, uBlackDefects; #ifdef EVAL_TIME UINT64 uSubTimer; #endif #ifdef EVAL_TIME uSubTimer = SystemReadTimeStampCounter(); #endif uWhiteDefects = CountKingSafetyDefects(pos, WHITE); uBlackDefects = CountKingSafetyDefects(pos, BLACK); #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalCountKingSafetyDefects += (SystemReadTimeStampCounter() - uSubTimer); #endif iWhiteKingSwing = iKingSwingP90[MINU(10, uWhiteDefects)]; iBlackKingSwing = iKingSwingP90[MINU(10, uBlackDefects)]; iWhiteKingSwing = (iWhiteKingSwing * (SCORE)REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[BLACK]]) / 8; iBlackKingSwing = (iBlackKingSwing * (SCORE)REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[WHITE]]) / 8; iKingTerm = iWhiteKingSwing + iBlackKingSwing; *piAlphaMargin += iKingTerm + iResidualP90; *piBetaMargin += iKingTerm + iResidualP90; } // // ====================================================================== // static void _RecordTrappedCandidate(IN OUT POSITION *pos, IN ULONG uColor, IN COOR c) /** Routine description: Record a zero-mobility piece as a trapped-piece candidate for uColor. Used by _EvalBishop/_EvalKnight/_EvalRook/_EvalQueen; the actual attacked-or-not check happens later, in _EvalTrappedPieces. Capped at ARRAY_LENGTH(pos->cTrapped[0]) candidates per side -- extras are just dropped, not a real concern in practice. Parameters: POSITION *pos, ULONG uColor, COOR c Return value: void **/ { if (pos->uNumTrapped[uColor] < ARRAY_LENGTH(pos->cTrapped[uColor])) { pos->cTrapped[uColor][pos->uNumTrapped[uColor]++] = c; } } static void _EvalBishop(IN OUT POSITION *pos, IN COOR c, IN PAWN_HASH_ENTRY *pHash) /** Routine description: Parameters: POSITION *pos, COOR c, PAWN_HASH_ENTRY *pHash, Return value: FLAG **/ { static const BITBOARD bbColorSq[2] = { 0x55aa55aa55aa55aaULL, 0xaa55aa55aa55aa55ULL }; static const COOR cBishopAtHome[2][2] = { { C8, F8 }, // BLACK { C1, F1 } // WHITE }; ULONG uColor; ULONG xColor; BITBOARD bb; BITBOARD bbMask; BITBOARD bbPc; COOR cSquare; SCORE i; ULONG u; ULONG uTotalMobility; PIECE p; ASSERT(IS_ON_BOARD(c)); p = pos->rgSquare[c].pPiece; ASSERT(p && IS_BISHOP(p)); uColor = GET_COLOR(p); xColor = FLIP(uColor); // Undeveloped minor piece; maybe penalized later in EvalKing. pos->uMinorsAtHome[uColor] += (c == cBishopAtHome[uColor][0]); pos->uMinorsAtHome[uColor] += (c == cBishopAtHome[uColor][1]); ASSERT(pos->uMinorsAtHome[uColor] <= 4); // Good and bad bishops given stationary pawn structure. i = 16; bbMask = bbColorSq[IS_SQUARE_WHITE(c)]; bb = pHash->bbStationaryPawns[uColor] & bbMask; ASSERT(CountBits(bb) <= 16); while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) { // Consider stationary (rammed or backward) pawns of the same // color as the bishop. ASSERT(pos->rgSquare[cSquare].pPiece); ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); ASSERT(GET_COLOR(pos->rgSquare[cSquare].pPiece) == uColor); i += STATIONARY_PAWN_ON_BISHOP_COLOR[cSquare]; } EVAL_TERM(uColor, BISHOP, c, pos->iScore[uColor], i, "good/bad stationary pawns ++"); i = 0; bbPc = ~(pHash->bbStationaryPawns[WHITE] | pHash->bbStationaryPawns[BLACK]); bbPc &= bbMask; bb = pos->bbPawns[uColor] & bbPc; while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) { ASSERT(pos->rgSquare[cSquare].pPiece); ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); ASSERT(GET_COLOR(pos->rgSquare[cSquare].pPiece) == uColor); ASSERT(pos->bbPawns[uColor] & COOR_TO_BB(cSquare)); i += TRANSIENT_PAWN_ON_BISHOP_COLOR[cSquare]; } bb = pos->bbPawns[xColor] & bbPc; while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) { // N.B. Only count enemy pawns that are supported by another // pawn. ASSERT(pos->rgSquare[cSquare].pPiece); ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); ASSERT(GET_COLOR(pos->rgSquare[cSquare].pPiece) == xColor); ASSERT(pos->bbPawns[xColor] & COOR_TO_BB(cSquare)); i += ((pos->bbPawnAttacks[xColor] & COOR_TO_BB(cSquare)) != 0) * TRANSIENT_PAWN_ON_BISHOP_COLOR[cSquare] / 2; } EVAL_TERM(uColor, BISHOP, c, pos->iScore[uColor], i, "good/bad transient pawns"); // // Removed 2026-08-30: BISHOP_IN_CLOSED_POSITION[pos->uClosedScaler] // duplicated bishop mobility (computed just below) rather than // adding a distinct angle on it. uClosedScaler is a coarse, // whole-board openness proxy for exactly the same fact bishop // mobility measures directly and per-bishop via ray-casting -- how // much this specific bishop's diagonals are blocked by pawns. The // mobility term is strictly more precise (opposite-bishop-color // endgames are the clearest case where "position is open overall" // and "this bishop's diagonal is blocked" diverge), so cut the // redundant whole-board proxy rather than scale it down. // // // Bishop mobility (and update attack tables) -- board_representation/ // EVAL.md section 1b: _BishopAttacksBB(c, pos->bbOccupied) // (generate.c's magic-bitboard slider lookup, already used by move // generation) gives the whole ray-to-first-blocker attack set in // one shot -- no per-square delta walk, no mailbox read, no switch // dispatch. The old BMobCaseTable's seven cases collapse to: // - terminal enemy non-pawn (opposing minor/rook/queen/king): // counts unconditionally -- covers both BMOB_ENEMY_SAME (no // x-ray) and BMOB_ENEMY_GREATER (x-rays past too); the // x-ray/no-x-ray distinction only matters for attack-bit // population below, not the mobility count itself. // - empty or terminal enemy pawn: counts unless pawn-unsafe // (BMOB_EMPTY / BMOB_ENEMY_PAWN) -- both terminal-square // categories together are exactly "not friend-occupied and not // enemy-non-pawn-occupied," the same partition knight's // reduction already uses. // - terminal friendly, non-stationary pawn on this bishop's own // color complex (BMOB_FRIEND_PAWN): still counts, the one case // that isn't a pure occupancy mask -- bbPc (already computed // above for the good/bad transient-pawn scoring) is exactly // "non-stationary pawns of either color on this color complex"; // ANDing with pos->bbPawns[uColor] and the attack set isolates // just this bishop's own transient pawns. // - any other friendly piece (knight/rook/king): blocks, no // count, no x-ray -- simply excluded by the friendly-occupied // mask, nothing else needed. // pos->bb = bbPc; { BITBOARD bbAttack = _BishopAttacksBB(c, pos->bbOccupied); BITBOARD bbEnemySame = pos->bbPieces[xColor][BISHOP] | pos->bbPieces[xColor][KNIGHT]; BITBOARD bbEnemyGEContinue = pos->bbPieces[xColor][ROOK] | pos->bbPieces[xColor][QUEEN] | COOR_TO_BB(pos->cNonPawns[xColor][0]); BITBOARD bbFriendBQ = pos->bbPieces[uColor][BISHOP] | pos->bbPieces[uColor][QUEEN]; BITBOARD bbUnsafeForMinor = pos->bbPawnAttacks[xColor]; BITBOARD bbExclude = 0; BITBOARD bbSeen = 0; BITBOARD bbLayer = bbAttack; BITBOARD bbMobility = 0; BITBOARD bbXrayAccum = 0; BITBOARD bbFirstLayerMask = 0; pos->bbMinorAttacks[uColor] |= bbAttack; // // Unified chain walk over both mobility and x-ray population // in one pass -- same technique and same reason as rook's // identical rewrite (board_representation/EVAL.md section 9): // the old mailbox walk's BMOB_FRIEND_XRAY/BMOB_ENEMY_GREATER // cases don't just x-ray past their blocker for attack-bit // purposes, they keep walking (fStop=FALSE) and keep crediting // mobility for whatever lies beyond. The originally-committed // bitboard version (and its single-hop-then-chain-following // x-ray fix) only ever handled the near side for *mobility*, // an under-count caught the same way rook's was: a two-bishop // battery on one diagonal with open squares beyond (old // mailbox: 12/8 mobility for the two bishops; near-side-only // bitboard version: 11/5). // for (;;) { BITBOARD bbNew = bbLayer & ~bbSeen; BITBOARD bbBlockersHere = bbNew & pos->bbOccupied; BITBOARD bbEmptyHere = bbNew & ~pos->bbOccupied; BITBOARD bbFriendBQHere = bbBlockersHere & bbFriendBQ; BITBOARD bbEnemySameHere = bbBlockersHere & bbEnemySame; BITBOARD bbEnemyGEHere = bbBlockersHere & bbEnemyGEContinue; BITBOARD bbFriendPawnHere = bbBlockersHere & pos->bbPawns[uColor]; BITBOARD bbEnemyPawnHere = bbBlockersHere & pos->bbPawns[xColor]; BITBOARD bbTransientHere = bbFriendPawnHere & bbPc; BITBOARD bbContinueHere; bbSeen |= bbNew; bbXrayAccum |= (bbNew & bbFirstLayerMask); bbFirstLayerMask = ~(BITBOARD)0; bbMobility |= (bbEmptyHere & ~bbUnsafeForMinor) | bbEnemySameHere | bbEnemyGEHere | (bbEnemyPawnHere & ~bbUnsafeForMinor) | bbTransientHere; bbContinueHere = bbFriendBQHere | bbEnemyGEHere; if (!bbContinueHere) { break; } bbExclude |= bbContinueHere; bbLayer = _BishopAttacksBB(c, pos->bbOccupied & ~bbExclude); } pos->bbMinorXrayAttacks[uColor] |= bbXrayAccum; uTotalMobility = CountBits(bbMobility); } ASSERT(uTotalMobility <= 13); EVAL_TERM(uColor, BISHOP, c, pos->iScore[uColor], BISHOP_MOBILITY_BY_SQUARES[uTotalMobility], "bishop mobility"); // // Look for bishops with no mobility, they are trapped and, later, // we'll see if they are also under attack too. // if (uTotalMobility == 0) { _RecordTrappedCandidate(pos, uColor, c); } // // Bonus for a bishop that's securely placed -- safe from ever // being challenged by an enemy pawn, and (checked below) defended // by a friendly one -- near the enemy king. This is a // bishop-specific king-tropism/outpost bonus. // bb = pos->bbPawns[xColor] & (~pHash->bbStationaryPawns[xColor]); if (TRUE == _IsSquareSafeFromEnemyPawn(pos, c, bb)) { // Defended (and defending) a friendly pawn. if (pos->bbPawnAttacks[uColor] & COOR_TO_BB(c)) { #ifdef DEBUG p = BLACK_PAWN | uColor; ASSERT(((IS_ON_BOARD(c + 17 * g_iBehind[uColor]) && (pos->rgSquare[c + 17 * g_iBehind[uColor]].pPiece==p)) || ((IS_ON_BOARD(c + 15 * g_iBehind[uColor])) && (pos->rgSquare[c + 15 * g_iBehind[uColor]].pPiece==p)))); #endif u = DISTANCE(c, pos->cNonPawns[xColor][0]); i = BISHOP_UNASSAILABLE_BY_DIST_FROM_EKING[u]; EVAL_TERM(uColor, BISHOP, c, pos->iReducedMaterialDownScaler[uColor], i, "[scaled] unassailable"); } } } static void _EvalKnight(IN OUT POSITION *pos, IN COOR c, IN PAWN_HASH_ENTRY *pHash) /** Routine description: Parameters: POSITION *pos, COOR c, PAWN_HASH_ENTRY *pHash, Return value: FLAG **/ { static const int iPawnStart[2] = { -17, +15 }; static const COOR cKnightAtHome[2][2] = { { B8, G8 }, // BLACK { B1, G1 } // WHITE }; PIECE p; COOR cSquare; BITBOARD bb; ULONG uColor; ULONG xColor; ULONG uPawnsSupporting; ULONG uMobilitySquares; SCORE i; ULONG uDist; p = pos->rgSquare[c].pPiece; ASSERT(p && IS_KNIGHT(p)); uColor = GET_COLOR(p); ASSERT(IS_VALID_COLOR(uColor)); xColor = FLIP(uColor); // Unmoved piece; potentially penalized in EvalKing. pos->uMinorsAtHome[uColor] += (c == cKnightAtHome[uColor][0]); pos->uMinorsAtHome[uColor] += (c == cKnightAtHome[uColor][1]); ASSERT(pos->uMinorsAtHome[uColor] <= 4); // Centrality bonus EVAL_TERM(uColor, KNIGHT, c, pos->iScore[uColor], KNIGHT_CENTRALITY_BONUS[c], "board centrality"); // Give a bonus to knights on a closed / busy board. ASSERT(pos->uClosedScaler <= 32); EVAL_TERM(uColor, KNIGHT, c, pos->iScore[uColor], KNIGHT_IN_CLOSED_POSITION[pos->uClosedScaler], "in closed/open position"); // Outposted knights. See if square c is safe from enemy pawns; // if so give bonus for outposted knight which increases the // closer it is to the enemy king and the more pawns it has // supporting it. uDist = DISTANCE(c, pos->cNonPawns[xColor][0]); ASSERT((uDist > 0) && (uDist <= 8)); bb = pos->bbPawns[xColor] & (~pHash->bbStationaryPawns[xColor]); if (TRUE == _IsSquareSafeFromEnemyPawn(pos, c, bb)) { // Count the number of supporting pawns the knight has uPawnsSupporting = 0; p = BLACK_PAWN | uColor; cSquare = c + iPawnStart[uColor]; uPawnsSupporting = (IS_ON_BOARD(cSquare) && (pos->rgSquare[cSquare].pPiece == p)); cSquare += 2; uPawnsSupporting += (IS_ON_BOARD(cSquare) && (pos->rgSquare[cSquare].pPiece == p)); ASSERT(uPawnsSupporting <= 2); // Give a bonus based on distance from enemy king i = KNIGHT_UNASSAILABLE_BY_DIST_FROM_EKING[uDist] + KNIGHT_WITH_N_PAWNS_SUPPORTING[uPawnsSupporting]; EVAL_TERM(uColor, KNIGHT, c, pos->iReducedMaterialDownScaler[uColor], i, "[scaled] unassailable/support"); // Now, if there's zero or one supporter see if the enemy has a // bishop the color of the knight it could capture with. if (uPawnsSupporting != 2) { if (IS_SQUARE_WHITE(c)) { if (pos->uWhiteSqBishopCount[xColor]) { EVAL_TERM(uColor, KNIGHT, c, pos->iScore[uColor], -(i / 2), "not safe from enemy B"); } } else { if (pos->uNonPawnCount[xColor][BISHOP] - pos->uWhiteSqBishopCount[xColor]) { EVAL_TERM(uColor, KNIGHT, c, pos->iScore[uColor], -(i / 2), "not safe from enemy B"); } } } } else // not outposted { // Still good to be close to the enemy king. i = KNIGHT_KING_TROPISM_BONUS[uDist]; EVAL_TERM(uColor, KNIGHT, c, pos->iReducedMaterialDownScaler[uColor], i, "[scaled] enemy king tropism"); } // Give a bonus for blockading an enemy backward pawn. cSquare = c + 16 * g_iAhead[uColor]; bb = pHash->bbStationaryPawns[xColor]; if (bb & COOR_TO_BB(cSquare)) { ASSERT(pos->rgSquare[cSquare].pPiece); ASSERT(IS_PAWN(pos->rgSquare[cSquare].pPiece)); EVAL_TERM(uColor, KNIGHT, c, pos->iScore[uColor], KNIGHT_ON_INTERESTING_SQUARE_BY_RANK[uColor][RANK(c)], "blockades enemy pawn"); } // // Removed 2026-08-30 ("A knight with an open file behind it is // good"): dubious chess reasoning on its own (open-file bonuses // are classically a rook concept -- knights don't use files the // way rooks do) compounded by reusing KNIGHT_ON_INTERESTING_SQUARE // _BY_RANK, the *same* table as the backward-pawn-blockade bonus // just above, for a completely unrelated condition. That reuse // muddied what the table means and made it untunable // independently for either concept. // // // Do mobility and piece relevance. Also update attack tables. // // board_representation/EVAL.md section 1b/2: g_KnightAttacksBB[c] // (generate.c's precomputed table, already used by move // generation) is exactly the old per-square g_iNDeltas walk's // destination set, IS_ON_BOARD baked in at table-build time -- no // per-square branch, no mailbox read, no switch. Knights never // x-ray or have a battery partner, so the old NMobCaseTable's four // cases collapse to two bitboard masks: NMOB_ENEMY_OTHER (any // enemy non-pawn -- count unconditionally) and NMOB_MOBILE_SQUARE // (empty or enemy pawn -- count unless pawn-unsafe). NMOB_FRIEND // needs no term at all, it's just "neither of the above." // { BITBOARD bbAttack = g_KnightAttacksBB[c]; BITBOARD bbFriendOcc = _BuildFriendlySideBB(pos, uColor); BITBOARD bbEnemyNonPawnOcc = _BuildFriendlySideBB(pos, xColor) & ~pos->bbPawns[xColor]; BITBOARD bbUnsafeForMinor = pos->bbPawnAttacks[xColor]; BITBOARD bbMobility; pos->bbMinorAttacks[uColor] |= bbAttack; bbMobility = (bbAttack & bbEnemyNonPawnOcc) | (bbAttack & ~bbFriendOcc & ~bbEnemyNonPawnOcc & ~bbUnsafeForMinor); uMobilitySquares = CountBits(bbMobility); } ASSERT(uMobilitySquares >= 0); ASSERT(uMobilitySquares <= 8); EVAL_TERM(uColor, KNIGHT, c, pos->iScore[uColor], KNIGHT_MOBILITY_BY_COUNT[uMobilitySquares], "knight mobility"); if (uMobilitySquares == 0) { _RecordTrappedCandidate(pos, uColor, c); } } static void _EvalRook(IN OUT POSITION *pos, IN COOR c, IN PAWN_HASH_ENTRY *pHash) /** Routine description: Parameters: IN POSITION *pos, IN COOR c, IN PAWN_HASH_ENTRY *pHash, Return value: FLAG **/ { PIECE p; ULONG uColor; ULONG uTotalMobility; COOR cSquare; SCORE i; ASSERT(IS_ON_BOARD(c)); p = pos->rgSquare[c].pPiece; ASSERT(p && IS_ROOK(p)); uColor = GET_COLOR(p); ASSERT(IS_VALID_COLOR(uColor)); ULONG xColor = FLIP(uColor); ASSERT(IS_VALID_COLOR(xColor)); BITBOARD bbFile = BBFILE[FILE(c)]; BITBOARD bbFriendPawns = pos->bbPawns[uColor] & bbFile; BITBOARD bbEnemyPawns = pos->bbPawns[xColor] & bbFile; // Collapsed 2026-09-06: friend-passer bonus used to distinguish // rook-behind-passer (good, rank-scaled +1..+25) from rook-in- // front-of-passer (bad, -3..-22) -- now a single flat bonus // regardless of which side of the pawn the rook is on, by // direct instruction. SCORE bonus = ROOK_FULL_HALF_OPEN_BONUS[bbFriendPawns != 0][bbEnemyPawns != 0]; bonus += (SCORE)((bbEnemyPawns & pHash->bbPasserLocations[xColor]) != 0) * ROOK_WITH_ENEMY_PASSER; bonus += (SCORE)((bbFriendPawns & pHash->bbPasserLocations[uColor]) != 0) * ROOK_WITH_FRIEND_PASSER; EVAL_TERM(uColor, ROOK, c, pos->iScore[uColor], bonus, "file/passer bonus"); // // Removed 2026-08-30 ("rook trapping enemy king"): a rook on the // 7th/8th rank aligned with the enemy king is exactly the // geometric pattern CountKingSafetyDefects' CHECK_VECTOR // line-of-sight scan already picks up into // uPiecesPointingAtKing/king-safety -- this was pricing the same // king-danger fact a second time via a rook-specific bonus. Belongs // in king safety, not here. (pFriendRook, only ever assigned in // this block and never read, is gone too.) // // // Rooks increase in value as pawns come off: // // "A further refinement would be to raise the knight's value by // 1/16 and lower the rook's value by 1/8 for each pawn above five // of the side being valued, with the opposite adjustment for each // pawn short of five." // --Larry Kaufman, IM // "Evaluation of Material Imbalance" // EVAL_TERM(uColor, ROOK, c, pos->iScore[uColor], ROOK_VALUE_AS_PAWNS_COME_OFF[pos->uPawnCount[uColor] + pos->uPawnCount[FLIP(uColor)]], "increase in value/pawns"); // // Rook mobility (and update attack tables) -- board_representation/ // EVAL.md section 1b/9: _RookAttacksBB(c, pos->bbOccupied) // (generate.c's magic-bitboard slider lookup) gives the whole // ray-to-first-blocker attack set in one shot, same technique // already landed for knight/bishop. The old RMobCaseTable's six // live cases collapse to: // - terminal enemy rook/queen/king (RMOB_ENEMY_SAME/_GREATER): // counts unconditionally, no unsafe check -- capturing an // equal-or-higher piece is never "unsafe" in the mobility // sense. Queen/king additionally x-ray past (_GREATER); rook // does not (_SAME). // - empty or terminal enemy pawn/knight/bishop (RMOB_EMPTY / // RMOB_ENEMY_LESS): counts unless the square is attacked by an // enemy pawn or minor -- both terminal-square categories // together are exactly "not friend-occupied and not // enemy-rook/queen/king-occupied," the same partition // bishop/knight's reductions already use with their own value // thresholds. // - any friendly piece (RMOB_FRIEND_BLOCK/_ROOK/_QUEEN): never // counts, simply excluded by the friendly-occupied mask. // Friendly rook/queen additionally x-ray past (no mobility // credit at the blocker itself, matching the old table); a // friendly rook blocker also earns the connected-rook bonus. // { BITBOARD bbAttack = _RookAttacksBB(c, pos->bbOccupied); BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, FLIP(uColor)); BITBOARD bbEnemySame = pos->bbPieces[FLIP(uColor)][ROOK]; BITBOARD bbEnemyGEContinue = pos->bbPieces[FLIP(uColor)][QUEEN] | COOR_TO_BB(pos->cNonPawns[FLIP(uColor)][0]); BITBOARD bbFriendRQ = pos->bbPieces[uColor][ROOK] | pos->bbPieces[uColor][QUEEN]; BITBOARD bbUnsafeForRook = pos->bbPawnAttacks[FLIP(uColor)] | pos->bbMinorAttacks[FLIP(uColor)]; BITBOARD bbExclude = 0; BITBOARD bbSeen = 0; BITBOARD bbLayer = bbAttack; BITBOARD bbMobility = 0; BITBOARD bbXrayAccum = 0; BITBOARD bbFirstLayerMask = 0; pos->bbRookAttacks[uColor] |= bbAttack; // // Unified chain walk over both mobility and x-ray population // in one pass, board_representation/EVAL.md section 9. The // old mailbox walk's RMOB_FRIEND_ROOK/_QUEEN and // RMOB_ENEMY_GREATER cases don't just x-ray past their blocker // for attack-bit purposes -- they keep walking (fStop=FALSE) // and keep crediting mobility for whatever safe/empty squares // and further captures lie beyond, however many such blockers // are stacked on one ray. A first version of this conversion // (committed, since fixed) only handled the near side (up to // the first blocker) for mobility and treated the chain purely // as an attack-bit population exercise -- caught by comparing // against the old mailbox walk directly on a battery position // (two same-color rooks on an open file: old code credited 13 // mobility to the far rook's near companion via squares beyond // it, the near-side-only version credited only 9). Fixed by // reusing the exact same "recompute with blockers excluded" // chain used for x-ray population to also accumulate mobility // credit at every layer, not just the first. // // Each iteration classifies the newly-revealed blockers on // each still-open ray: friendly rook/queen and enemy queen/ // king continue the chain (recompute with them excluded too); // everything else (friendly non-R/Q, enemy pawn/knight/bishop, // enemy rook, or the board edge) stops that ray. Bounded: each // iteration either terminates or consumes at least one new // piece into the exclusion set, so this can loop at most // once per piece on the board. // for (;;) { BITBOARD bbNew = bbLayer & ~bbSeen; BITBOARD bbBlockersHere = bbNew & pos->bbOccupied; BITBOARD bbEmptyHere = bbNew & ~pos->bbOccupied; BITBOARD bbFriendRQHere = bbBlockersHere & bbFriendRQ; BITBOARD bbEnemySameHere = bbBlockersHere & bbEnemySame; BITBOARD bbEnemyGEHere = bbBlockersHere & bbEnemyGEContinue; BITBOARD bbEnemyLEHere = bbBlockersHere & bbEnemyOcc & ~bbEnemySameHere & ~bbEnemyGEHere; BITBOARD bbFriendRookHere = bbBlockersHere & pos->bbPieces[uColor][ROOK]; BITBOARD bbContinueHere; bbSeen |= bbNew; bbXrayAccum |= (bbNew & bbFirstLayerMask); bbFirstLayerMask = ~(BITBOARD)0; bbMobility |= (bbEmptyHere & ~bbUnsafeForRook) | bbEnemySameHere | bbEnemyGEHere | (bbEnemyLEHere & ~bbUnsafeForRook); // // Connected-rook bonus fires at every friendly rook found // along the chain, not just the first (matches the old // walk: each RMOB_FRIEND_ROOK hit its own EVAL_TERM call). // while (bbFriendRookHere) { COOR cBlocker = CoorFromBitBoardRank8ToRank1(&bbFriendRookHere); FLAG fHoriz = (RANK(cBlocker) == RANK(c)); EVAL_TERM(uColor, ROOK, cBlocker, pos->iScore[uColor], (ROOK_CONNECTED_HORIZ * fHoriz + ROOK_CONNECTED_VERT * FLIP(fHoriz)), "rook connected"); } bbContinueHere = bbFriendRQHere | bbEnemyGEHere; if (!bbContinueHere) { break; } bbExclude |= bbContinueHere; bbLayer = _RookAttacksBB(c, pos->bbOccupied & ~bbExclude); } pos->bbRookXrayAttacks[uColor] |= bbXrayAccum; uTotalMobility = CountBits(bbMobility); } ASSERT(uTotalMobility <= 14); ASSERT(pos->uArmyScaler[FLIP(uColor)] <= 31); i = ROOK_MOBILITY_BY_SQUARES[uTotalMobility]; i *= (int)(REDUCED_MATERIAL_UP_SCALER[pos->uArmyScaler[FLIP(uColor)]] + 1); i /= 8; EVAL_TERM(uColor, ROOK, c, pos->iScore[uColor], i, "rook mobility"); if (uTotalMobility < 3) { // Look for rooks with no mobility who are under attack. These // pieces are trapped! if (uTotalMobility == 0) { _RecordTrappedCandidate(pos, uColor, c); } // Rook trapped in the corner by a stupid friendly king? -- // color-indexed instead of a WHITE/BLACK branch over two // mirrored blocks (board_representation/EVAL.md section 9). ASSERT(IS_VALID_COLOR(uColor)); { static const ULONG BACK_RANK_MASK[2] = { 0x00, 0x70 }; // BLACK, WHITE static const COOR E_CORNER[2] = { E8, E1 }; static const COOR D_CORNER[2] = { D8, D1 }; ASSERT(IS_ON_BOARD(c)); if ((c & 0xF0) == BACK_RANK_MASK[uColor]) { cSquare = pos->cNonPawns[uColor][0]; ASSERT(IS_ON_BOARD(cSquare)); if ((cSquare & 0xF0) == BACK_RANK_MASK[uColor]) { if (((cSquare > E_CORNER[uColor]) && (c > cSquare)) || ((cSquare < D_CORNER[uColor]) && (c < cSquare))) { EVAL_TERM(uColor, ROOK, c, pos->iScore[uColor], KING_TRAPPING_ROOK, "king trapping rook"); } } } } } // if low mobility } static void _EvalQueen(IN OUT POSITION *pos, IN COOR c, IN PAWN_HASH_ENTRY *pHash) /** Parameters: POSITION *pos, COOR c, PAWN_HASH_ENTRY *pHash, Return value: FLAG **/ { PIECE p = pos->rgSquare[c].pPiece; ULONG uColor; ULONG uTotalMobility; ULONG u; COOR cKing; ASSERT(IS_ON_BOARD(c)); ASSERT(p && IS_QUEEN(p)); uColor = GET_COLOR(p); ASSERT(IS_VALID_COLOR(uColor)); cKing = pos->cNonPawns[FLIP(uColor)][0]; ASSERT(IS_ON_BOARD(cKing)); ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); // Encourage enemy king tropism, but not too early. if ((TRUE == pos->fCastled[uColor]) || (pos->uMinorsAtHome[uColor] <= 1)) { u = DISTANCE(c, cKing); ASSERT((u <= 7) && (u > 0)); EVAL_TERM(uColor, QUEEN, c, pos->iScore[uColor], QUEEN_KING_TROPISM[u], "enemy king tropism"); } // // Do queen mobility (and update attack tables) -- two-pass // rook-direction/bishop-direction magic lookups, board_ // representation/EVAL.md section 1b/9: MOVEGEN_MIGRATION.md // already found (in the stashed first bitboard-eval attempt's // _EvalQueenOccupancyBB PoC) that a combined 8-ray table measures // *slower* than reusing the rook/bishop tables in two passes -- // reuse that structure here too, don't rediscover the regression. // Each pass is its own unified chain walk, identical technique to // rook's/bishop's own conversions (including their fix for // crediting mobility beyond a battery partner, not just x-ray // attack bits): continue through a friendly queen in both passes, // a friendly rook only in the rook-direction pass, a friendly // bishop only in the bishop-direction pass (QMOB_FRIEND_ROOK/ // _BISHOP's old per-ray-family split falls out for free from // *which* lookup a blocker shows up in -- no fOrthogonalRay flag // needed). Unlike rook/bishop, queen never x-rays through *any* // enemy piece (QMOB_ENEMY_GE always stopped in the old table, the // one case that's a *fewer*-cases asymmetry versus bishop, not // more), so the enemy side of each pass's continue-set is empty. // { BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, FLIP(uColor)); BITBOARD bbEnemyGE = pos->bbPieces[FLIP(uColor)][QUEEN] | COOR_TO_BB(cKing); BITBOARD bbUnsafeForQueen = pos->bbPawnAttacks[FLIP(uColor)] | pos->bbMinorAttacks[FLIP(uColor)] | pos->bbRookAttacks[FLIP(uColor)]; BITBOARD bbAttackTotal = 0; BITBOARD bbXrayAccum = 0; BITBOARD bbMobility = 0; ULONG uPass; for (uPass = 0; uPass < 2; uPass++) { BITBOARD bbFriendContinue = (uPass == 0) ? (pos->bbPieces[uColor][ROOK] | pos->bbPieces[uColor][QUEEN]) : (pos->bbPieces[uColor][BISHOP] | pos->bbPieces[uColor][QUEEN]); BITBOARD bbInitial = (uPass == 0) ? _RookAttacksBB(c, pos->bbOccupied) : _BishopAttacksBB(c, pos->bbOccupied); BITBOARD bbExclude = 0; BITBOARD bbSeen = 0; BITBOARD bbLayer = bbInitial; BITBOARD bbFirstLayerMask = 0; bbAttackTotal |= bbInitial; for (;;) { BITBOARD bbNew = bbLayer & ~bbSeen; BITBOARD bbBlockersHere = bbNew & pos->bbOccupied; BITBOARD bbEmptyHere = bbNew & ~pos->bbOccupied; BITBOARD bbFriendContinueHere = bbBlockersHere & bbFriendContinue; BITBOARD bbEnemyGEHere = bbBlockersHere & bbEnemyGE; BITBOARD bbEnemyLEHere = bbBlockersHere & bbEnemyOcc & ~bbEnemyGEHere; bbSeen |= bbNew; bbXrayAccum |= (bbNew & bbFirstLayerMask); bbFirstLayerMask = ~(BITBOARD)0; bbMobility |= (bbEmptyHere & ~bbUnsafeForQueen) | bbEnemyGEHere | (bbEnemyLEHere & ~bbUnsafeForQueen); if (!bbFriendContinueHere) { break; } bbExclude |= bbFriendContinueHere; bbLayer = (uPass == 0) ? _RookAttacksBB(c, pos->bbOccupied & ~bbExclude) : _BishopAttacksBB(c, pos->bbOccupied & ~bbExclude); } } pos->bbQueenAttacks[uColor] |= bbAttackTotal; pos->bbQueenXrayAttacks[uColor] |= bbXrayAccum; uTotalMobility = CountBits(bbMobility); } ASSERT(uTotalMobility <= 27); EVAL_TERM(uColor, QUEEN, c, pos->iScore[uColor], QUEEN_MOBILITY_BY_SQUARES[uTotalMobility], "queen mobility"); // Look for queens with no mobility who are under attack. These // pieces are trapped! if (uTotalMobility == 0) { _RecordTrappedCandidate(pos, uColor, c); } } static void _EvalKing(IN OUT POSITION *pos, IN const COOR c, IN const PAWN_HASH_ENTRY *pHash, IN SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: Parameters: POSITION *pos, COOR c, PAWN_HASH_ENTRY *pHash, Return value: FLAG **/ { PIECE p; ULONG uColor, xColor; COOR cSquare; ULONG u, v; ULONG uCounter; BITV bvAttack; BITV bvXray; BITV bvDefend; BITV bvPattern = 0; ULONG uFlightSquares; ULONG uQueenNearKing = 0; BITBOARD bb; SCORE i; SCORE iKingScore = 0; static ULONG KingFlightDefects[9] = { +4, +2, +1, 0, 0, 0, 0, 0, 0 }; // Trimmed from 11 entries to the real 8 king-move squares // (2026-09-05, by direct instruction): the dropped -2/+2 entries // (two squares away on the same rank) were never a king move -- // confirmed intentional signal, not a bug, but judged not worth // keeping once decoupled from the actual bug (KingSafetyDeltas' // wrong square set leaking into pos->bbKingAttacks' population, // fixed separately) on two grounds: CountKingSafetyDefects' own // rewrite doesn't count them either now, and they're "just kinda // weird" -- not clearly earning their keep as their own thing. static INT KingSafetyDeltas[9] = { -17, -16, -15, -1, +1, +15, +16, +17, 0 }; static ULONG EmptyAttackedSquare[2][8] = { { 1, 1, 1, 1, 1, 2, 2, 2 }, { 2, 2, 2, 1, 1, 1, 1, 1 }, }; static ULONG EmptyUnattackedSquare[2][8] = { { 0, 0, 0, 0, 0, 1, 1, 1 }, { 1, 1, 1, 0, 0, 0, 0, 0 }, }; static ULONG OccupiedAttackedSquare[2] = { 1, 2 }; ASSERT(IS_ON_BOARD(c)); p = pos->rgSquare[c].pPiece; ASSERT(p && IS_KING(p)); uColor = GET_COLOR(p); xColor = FLIP(uColor); ASSERT(IS_VALID_COLOR(uColor)); pos->bbKingAttacks[uColor] |= g_KingAttacksBB[c]; // Don't bother with king safety when the other side has no mating // material. u = pos->uNonPawnMaterial[xColor]; ASSERT(u >= VALUE_KING); if (u < DO_KING_SAFETY_THRESHOLD) { goto skip_safety; } // Prepare to do king safety uCounter = (u >= KEEP_KING_AT_HOME_THRESHOLD) * KING_INITIAL_COUNTER_BY_LOCATION[uColor][c]; #ifdef EVAL_DUMP Trace("%s Initial KS Counter: %u\n", COLOR_NAME(uColor), uCounter); #endif BITBOARD bbEnemyOcc = _BuildFriendlySideBB(pos, xColor); uFlightSquares = 0; u = 0; ASSERT(KingSafetyDeltas[u] != 0); do { cSquare = c + KingSafetyDeltas[u]; if (IS_ON_BOARD(cSquare)) { // // board_representation/EVAL.md section 9: bvAttacks/ // ATTACK_BITV retired entirely -- every bit here now // comes straight from a bbXAttacks accumulator read, // no more c|8 shadow-index struct storage or // per-square writes. No enemy-king contribution in // bvAttack, by direct instruction (see this function's // header comment on the black-then-white evaluation- // order asymmetry this sidesteps). bvDefend's own-king // bit is real, load-bearing signal (see the bvDefend // &= ~8 below, which strips it back out when the // square is x-rayed or multiply attacked -- "a lone // king isn't adequate defense against that") -- not // just self-consistency noise, so it keeps its // own-color check. // BITBOARD sq = COOR_TO_BB(cSquare); bvAttack = ((pos->bbPawnAttacks[xColor] & sq) ? PAWN_BIT : 0) | ((pos->bbMinorAttacks[xColor] & sq) ? MINOR_BIT : 0) | ((pos->bbRookAttacks[xColor] & sq) ? ROOK_BIT : 0) | ((pos->bbQueenAttacks[xColor] & sq) ? QUEEN_BIT : 0); bvXray = ((pos->bbMinorXrayAttacks[xColor] & sq) ? MINOR_BIT : 0) | ((pos->bbRookXrayAttacks[xColor] & sq) ? ROOK_BIT : 0) | ((pos->bbQueenXrayAttacks[xColor] & sq) ? QUEEN_BIT : 0); bvDefend = ((pos->bbPawnAttacks[uColor] & sq) ? PAWN_BIT : 0) | ((pos->bbMinorAttacks[uColor] & sq) ? MINOR_BIT : 0) | ((pos->bbRookAttacks[uColor] & sq) ? ROOK_BIT : 0) | ((pos->bbQueenAttacks[uColor] & sq) ? QUEEN_BIT : 0) | ((pos->bbKingAttacks[uColor] & sq) ? KING_BIT : 0); // // Count squares near the king the enemy queen // specifically attacks or x-rays (2026-08-30, replaces // the old per-queen "pointing near enemy K" bolt-on // this loop's own bvAttack/bvXray data already made // redundant). Reuses data already read above -- no // extra attack-table work -- and, unlike the old term, // catches x-ray/latent queen threats too, not just // direct ray-cast hits. // // Queen no longer writes small.uQueen/xray.uQueen at // all (2026-09-05 conversion) -- bvAttack/bvXray above // already fold its contribution back in via QUEEN_BIT // (same byte position .small.uQueen used), so just // read those instead of the now-permanently-zero raw // struct fields. // uQueenNearKing += (((bvAttack | bvXray) & QUEEN_BIT) != 0); if (bvAttack != 0) { bvPattern |= (bvAttack | bvXray); if (bvXray || (bvAttack & (bvAttack - 1))) { bvDefend &= ~KING_BIT; } if (!(pos->bbOccupied & sq)) { ASSERT(u < 8); uCounter += EmptyAttackedSquare[uColor][u]; } else { // OccupiedAttackedSquare is indexed purely by // color (is the occupant ufColor's own piece, // i.e. an enemy from uColor's perspective) -- // no need for the mailbox piece type at all. uCounter += OccupiedAttackedSquare[(bbEnemyOcc & sq) != 0]; } uCounter += (bvDefend == 0); } else { uCounter += (bvXray != 0); ASSERT(u < 8); uCounter += EmptyUnattackedSquare[uColor][u]; uFlightSquares += ((pos->bbOccupied & sq) == 0); } } u++; } while(KingSafetyDeltas[u] != 0); #ifdef EVAL_DUMP Trace("%s KS Counter post-squares: %u\n", COLOR_NAME(uColor), uCounter); #endif // // King's own file plus the two adjacent ones. EXPERIMENTAL, // prototype-only (2026-09-05, board_representation/EVAL.md // section 9's king-safety-hash discussion): now routed through // _GetFileStormDefects's cache instead of computed inline -- // _ComputeFileStormDefects holds the exact same logic this used // to be (collapsed 2026-08-30 from three copy-pasted blocks that // differed only in c-1/c/c+1; confirmed behaviorally neutral by // isolated sd10 suite testing before that landing). // uCounter += _GetFileStormDefects(ctx, pos, pHash, uColor); #ifdef EVAL_DUMP Trace("%s KS Counter post-open file/stormers: %u\n", COLOR_NAME(uColor), uCounter); #endif ASSERT(bvPattern >= 0); ASSERT(bvPattern < 32); v = KING_COUNTER_BY_ATTACK_PATTERN[bvPattern]; uCounter += v; #ifdef EVAL_DUMP Trace("%s KS Counter post-attack pattern: %u\n", COLOR_NAME(uColor), uCounter); #endif ASSERT(uFlightSquares <= 8); uCounter += KingFlightDefects[uFlightSquares] * (v > 1); #ifdef EVAL_DUMP Trace("%s KS Counter post-flight sq: %u\n", COLOR_NAME(uColor), uCounter); #endif // Note: can't use pos->iReducedMaterialDownScaler here because we // are scaling it based on the _other_ side's material. uCounter = MINU(uCounter, 41); i = KING_SAFETY_BY_COUNTER[uCounter]; #ifdef CALIBRATE_POSITIONAL // KING_SAFETY_BY_COUNTER is <= 0 throughout (a penalty); the // calibration histogram bins magnitude, matching iKingSwingP90's // own all-positive-magnitude shape. ASSERT(i <= 0); _RecordPositionalCalibration(ctx, pos, pHash, uColor, (ULONG)(-i)); #endif i *= REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[xColor]]; i /= 8; EVAL_TERM(uColor, KING, c, iKingScore, i, "king safety"); ASSERT(uQueenNearKing <= 10); EVAL_TERM(uColor, KING, c, iKingScore, KING_QUEEN_PROXIMITY_DANGER[MINU(uQueenNearKing, 6)], "queen proximity danger"); // Bonus for castling / penalty for loss of castle. Also, if side has // not yet castled, be concerned with undeveloped minor pieces. if (FALSE == pos->fCastled[uColor]) { ULONG uDevelopmentScaler = CASTLE_AND_DEVELOPMENT_SCALER[pos->uArmyScaler[uColor]]; if (uDevelopmentScaler > 0) { ASSERT(pos->uMinorsAtHome[uColor] <= 4); EVAL_TERM(uColor, 0, ILLEGAL_COOR, pos->iScore[uColor], (SCORE)((UNDEVELOPED_MINORS_IN_OPENING[pos->uMinorsAtHome[uColor]] * (SCORE)uDevelopmentScaler) / 8), "development"); // Color-indexed instead of a WHITE/BLACK branch over // two copy-pasted blocks -- one fewer per-piece branch // for the processor to mispredict (board_representation/ // EVAL.md section 9). static const BITV CASTLE_SHORT[2] = { CASTLE_BLACK_SHORT, CASTLE_WHITE_SHORT }; static const BITV CASTLE_LONG[2] = { CASTLE_BLACK_LONG, CASTLE_WHITE_LONG }; EVAL_TERM(uColor, KING, c, iKingScore, (SCORE)((KING_MISSING_ONE_CASTLE_OPTION * ((CASTLE_SHORT[uColor] & pos->bvCastleInfo) == 0) * (SCORE)uDevelopmentScaler) / 8), "can't castle short"); EVAL_TERM(uColor, KING, c, iKingScore, (SCORE)((KING_MISSING_ONE_CASTLE_OPTION * ((CASTLE_LONG[uColor] & pos->bvCastleInfo) == 0) * (SCORE)uDevelopmentScaler) / 8), "can't castle long"); } } skip_safety: // // Special code for kings in the endgame // u = pos->uNonPawnCount[WHITE][0] + pos->uNonPawnCount[BLACK][0]; if (u < 8) { // // Encourage kings to come to the center // i = KING_TO_CENTER[c]; EVAL_TERM(uColor, KING, c, iKingScore, i, "centralize king"); // // Kings in front of passers in the late endgame are strong... // cSquare = FILE(c); bb = pHash->bbPasserLocations[uColor] & (BBADJACENT_FILES[cSquare] | BBFILE[cSquare]); if (bb) { while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) { if (DISTANCE(cSquare, c) == 1) { ASSERT(abs((INT)FILE(c) - (INT)FILE(cSquare)) <= 1); ASSERT(abs((INT)RANK(c) - (INT)RANK(cSquare)) <= 1); i = 2; i += KING_SUPPORTING_OWN_PASSER_BY_RANK[uColor][RANK(cSquare)]; switch(uColor) { case WHITE: i += 8 * (c < (cSquare - 1)); break; case BLACK: i += 8 * (c > (cSquare + 1)); break; } EVAL_TERM(uColor, KING, c, iKingScore, i, "supporting own passer"); } } } // // Detect kings way out of the action in KP endgames: // // 8/8/1p6/p6k/P3K3/8/1P6/8 w - - 0 11 // if ((u == 2) && (uColor == WHITE)) { i = 0; bb = (pos->bbPawns[WHITE] | pos->bbPawns[BLACK]); while(IS_ON_BOARD(cSquare = CoorFromBitBoardRank8ToRank1(&bb))) { i += (DISTANCE(cSquare, pos->cNonPawns[BLACK][0]) - DISTANCE(cSquare, pos->cNonPawns[WHITE][0])); } EVAL_TERM(WHITE, KING, c, iKingScore, i * 12, "in/out of \"action\""); } } pos->iScore[uColor] += iKingScore; ctx->sPlyInfo[ctx->uPly].iKingScore[uColor] = iKingScore; } FLAG _EvalPasserRacesAgainstLoneKings(IN OUT POSITION *pos, IN PAWN_HASH_ENTRY *pHash) /** Routine description: Determine if one side has a pawn that is unstoppable. This is broken for positions like: Parameters: POSITION *pos, PAWN_HASH_ENTRY *pHash, Return value: void **/ { BITBOARD bb[2]; ULONG uColor; int d1; COOR cQueen; COOR cKing, c; ULONG uKingDist, uPawnDist, uFriendDist; ULONG uRacerDist[2]; FLAG fDontCountMeOut[2]; bb[BLACK] = pHash->bbPasserLocations[BLACK]; bb[WHITE] = pHash->bbPasserLocations[WHITE]; if (!(bb[BLACK] | bb[WHITE])) { return(FALSE); } ASSERT((pos->uNonPawnCount[WHITE][0] == 1) || (pos->uNonPawnCount[BLACK][0] == 1)) // Both deferred past the no-passer early return above -- no need to // pay for this on the common case (most positions have no passer at // all, and _EvalPasserRacesAgainstLoneKings itself is only even called when a side // is down to a bare king, so this function runs unconditionally in // the pre-lazy-exit segment whenever that's true). uRacerDist[BLACK] = 99; uRacerDist[WHITE] = 99; fDontCountMeOut[BLACK] = ((RANK(pos->cNonPawns[BLACK][0]) <= 3) && (pos->uPawnCount[BLACK] != 0)); fDontCountMeOut[WHITE] = ((RANK(pos->cNonPawns[WHITE][0]) >= 6) && (pos->uPawnCount[WHITE] != 0)); FOREACH_COLOR(uColor) { if (pos->uNonPawnCount[FLIP(uColor)][0] == 1) { d1 = 16 * g_iAhead[uColor]; ASSERT((d1 * 2) == (32 * g_iAhead[uColor])); if (bb[uColor]) { while(IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1( &(bb[uColor])))) { // If the enemy king can take this passer and is // on the move, all bets are off! cKing = pos->cNonPawns[FLIP(uColor)][0]; ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) != uColor); if (pos->uToMove == FLIP(uColor) && (DISTANCE(cKing, c) == 1)) { // TODO: something clever here about protected pawns continue; } cQueen = FILE_RANK_TO_COOR(FILE(c), QUEENING_RANK[uColor]); ASSERT(FILE(cQueen) == FILE(c)); uPawnDist = RANK_DISTANCE(cQueen, c); if ((RANK(c) == JUMPING_RANK[uColor]) && (IS_EMPTY(pos->rgSquare[c + d1].pPiece) && IS_EMPTY(pos->rgSquare[c + d1 * 2].pPiece))) { uPawnDist--; } ASSERT((uPawnDist >= 1) && (uPawnDist <= 6)); uKingDist = DISTANCE(cKing, cQueen); if (uKingDist > 0) { uKingDist -= (pos->uToMove == FLIP(uColor)); } ASSERT((uKingDist >= 0) && (uPawnDist <= 7)); if (uPawnDist < uKingDist) { uRacerDist[uColor] = MINU(uRacerDist[uColor], uPawnDist); } cKing = pos->cNonPawns[uColor][0]; ASSERT(IS_KING(pos->rgSquare[cKing].pPiece)); ASSERT(GET_COLOR(pos->rgSquare[cKing].pPiece) == uColor); uFriendDist = DISTANCE(cKing, cQueen); if ((uFriendDist <= 1) && (DISTANCE(cKing, c) <= 1)) { if (FILE(c) != FILE(cKing)) { uRacerDist[uColor] = MINU(uRacerDist[uColor], uPawnDist); } else if ((FILE(cKing) != A) && (FILE(cKing) != H)) { uRacerDist[uColor] = MINU(uRacerDist[uColor], (uPawnDist + 1)); } } fDontCountMeOut[uColor] = TRUE; // // TODO: winning connected passers // } } } } if ((uRacerDist[WHITE] < uRacerDist[BLACK]) && (FALSE == fDontCountMeOut[BLACK])) { EVAL_TERM(WHITE, PAWN, ILLEGAL_COOR, pos->iScore[WHITE], RACER_WINS_RACE - uRacerDist[WHITE] * 4, "winning racer pawn"); return(TRUE); } else if ((uRacerDist[BLACK] < uRacerDist[WHITE]) && (FALSE == fDontCountMeOut[WHITE])) { EVAL_TERM(BLACK, PAWN, ILLEGAL_COOR, pos->iScore[BLACK], RACER_WINS_RACE - uRacerDist[BLACK] * 4, "winning racer pawn"); return(TRUE); } return(FALSE); } static void _ReEvalPassers(IN OUT POSITION *pos, IN PAWN_HASH_ENTRY *pHash) /** Routine description: Parameters: POSITION *pos, PAWN_HASH_ENTRY *pHash, Return value: void **/ { ULONG u; ULONG uColor; COOR c; COOR cSquare; BITBOARD bb; PIECE p; SCORE i; ULONG uNumPassers[2] = { 0, 0 }; int d1; ASSERT(pHash->bbPasserLocations[WHITE] | pHash->bbPasserLocations[BLACK]); FOREACH_COLOR(uColor) { bb = pHash->bbPasserLocations[uColor]; if (bb != 0) { uNumPassers[uColor] = CountBits(bb); ASSERT(uNumPassers[uColor] > 0); d1 = 16 * g_iAhead[uColor]; ASSERT((d1 * 2) == (32 * g_iAhead[uColor])); while(IS_ON_BOARD(c = CoorFromBitBoardRank8ToRank1(&bb))) { // // Consider the control of the square in front of the passer // cSquare = c + d1; ASSERT(IS_ON_BOARD(cSquare)); u = _WhoControlsSquareFast(pos, cSquare); if (u == FLIP(uColor)) { EVAL_TERM(uColor, PAWN, c, pos->iScore[uColor], -((PASSER_BY_RANK[uColor][RANK(c)] / 4) + (PASSER_BY_RANK[uColor][RANK(c)] / 16)), "enemy controls sq ahead"); } else if (u == (ULONG)-1) { p = pos->rgSquare[cSquare].pPiece; if (!IS_EMPTY(p) && (OPPOSITE_COLORS(p, uColor))) { ASSERT(GET_COLOR(p) != uColor); EVAL_TERM(uColor, PAWN, c, pos->iScore[uColor], -(PASSER_BY_RANK[uColor][RANK(c)] / 4), "enemy occupies sq ahead"); } } #ifdef DEBUG else { ASSERT(u == uColor); } #endif // // Consider distance from friend king / enemy king // i = (8 - DISTANCE(c, pos->cNonPawns[uColor][0])); if (((uColor == WHITE) && ((pos->cNonPawns[BLACK][0] >> 4) <= (c >> 4))) || ((uColor == BLACK) && ((pos->cNonPawns[WHITE][0] >> 4) >= (c >> 4)))) { // // The enemy king is ahead of the passer // i += FILE_DISTANCE(c, pos->cNonPawns[FLIP(uColor)][0]) * 4; } else { // // The enemy king is behind the passer // i += 10 + DISTANCE(c, pos->cNonPawns[FLIP(uColor)][0]) * 4; } i *= PASSER_MATERIAL_UP_SCALER[pos->uArmyScaler[FLIP(uColor)]]; i /= 8; EVAL_TERM(uColor, PAWN, c, pos->iScore[uColor], i, "passer distance to kings"); // // If a side is a piece down then the other side's passers // are even more dangerous. // if (pos->uNonPawnCount[uColor][0] > pos->uNonPawnCount[FLIP(uColor)][0]) { i = (PASSER_BY_RANK[uColor][RANK(c)] / 2); EVAL_TERM(uColor, PAWN, c, pos->iScore[uColor], i, "passer and piece up"); } // // TODO: For connected passers vs KR, consult the oracle // } } } // // Game stage bonus // u = pos->uArmyScaler[BLACK]; EVAL_TERM(WHITE, PAWN, ILLEGAL_COOR, pos->iScore[WHITE], uNumPassers[WHITE] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u], "passer value material"); // ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0); u = pos->uArmyScaler[WHITE]; EVAL_TERM(BLACK, PAWN, ILLEGAL_COOR, pos->iScore[BLACK], uNumPassers[BLACK] * PASSER_BONUS_AS_MATERIAL_COMES_OFF[u], "passer value material"); // ASSERT(PASSER_BONUS_AS_MATERIAL_COMES_OFF[u] >= 0); } static void _EvalBadTrades(IN OUT POSITION *pos) /** Routine description: Parameters: POSITION *pos, Return value: void **/ { ASSERT(pos->uNonPawnMaterial[WHITE] != pos->uNonPawnMaterial[BLACK]); ULONG uAhead = (pos->uNonPawnMaterial[WHITE] > pos->uNonPawnMaterial[BLACK]); ULONG uBehind = FLIP(uAhead); #ifdef DEBUG if (pos->uNonPawnMaterial[WHITE] > pos->uNonPawnMaterial[BLACK]) { ASSERT(uAhead == WHITE); ASSERT(uBehind == BLACK); } else { ASSERT(pos->uNonPawnMaterial[BLACK] > pos->uNonPawnMaterial[WHITE]); ASSERT(uAhead == BLACK); ASSERT(uBehind == WHITE); } #endif // // Encourage the side that is ahead in piece material to continue // trading pieces and not to trade pawns. This also has the // effect of making the side that is behind in material want to // trade pawns and not pieces. This also gives the side that is // ahead a bit of a "bad trade" bonus. Note that the bonus is not // given if the side "ahead" has no pawns; this is so that the // engine will not like positions like KNB vs KRP - the side with // the pawn has the winning chances. // ULONG uAheadPawnCount = pos->uPawnCount[uAhead]; ULONG uInverseBehindPieceCount = 9 - MINU(9, pos->uNonPawnCount[uBehind][0]); EVAL_TERM(uAhead, 0, ILLEGAL_COOR, pos->iScore[uAhead], (uInverseBehindPieceCount * 16) * (uAheadPawnCount != 0), "trade pieces"); // Zero pawns left for the ahead side is the classic false-positive // (KNB vs KRP, minor-up-no-pawns endings that are often drawn or // even lost) -- the old DONT_TRADE_PAWNS table existed specifically // to punish it (-43/-10 depending on lead size) rather than just // trailing off toward a small positive number. Keep that guard as // a cheap branch rather than a lookup table; the positive slope // for 1+ pawns doesn't need the same care. EVAL_TERM(uAhead, 0, ILLEGAL_COOR, pos->iScore[uAhead], (0 == uAheadPawnCount) ? -30 : (SCORE)(uAheadPawnCount * 2), "don't trade pawns"); } static void _EvalLookForDanger(IN OUT SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: This routine looks for squares on the board where the side to move has a piece that is en prise. So far in eval we will have tagged pieces in situations where they are attacked by lesser-valued enemy pieces as "in danger". However, because of the order in which the attack table was generated, we will have missed situations where a piece is undefended (or underdefended) and attacked by an enemy piece that is the same (or greater) value. Note: This routine only considers side to move. Also Note: this routine does not find pawns that are en prise, only pieces. Parameters: SEARCHER_THREAD_CONTEXT *ctx Return value: void **/ { COOR c; ULONG u; POSITION *pos = &ctx->sPosition; ULONG uSide = pos->uToMove; #ifdef DEBUG PIECE p; #endif for (u = 1; u < pos->uNonPawnCount[uSide][0]; u++) { c = pos->cNonPawns[uSide][u]; #ifdef DEBUG ASSERT(IS_ON_BOARD(c)); p = pos->rgSquare[c].pPiece; ASSERT(p); ASSERT(GET_COLOR(p) == uSide); ASSERT(!IS_PAWN(p)); #endif if (_WhoControlsSquareFast(pos, c) == FLIP(uSide)) { RecordEnprisePiece(ctx, c); } } } static void _EvalTrappedPieces(IN OUT SEARCHER_THREAD_CONTEXT *ctx) /** Routine description: Parameters: SEARCHER_THREAD_CONTEXT *ctx Return value: static void **/ { COOR c; COOR cBestOwnTrapped; ULONG uBestOwnTrappedValue; ULONG uColor; ULONG u; POSITION *pos = &ctx->sPosition; PIECE p; FOREACH_COLOR(uColor) { cBestOwnTrapped = ILLEGAL_COOR; uBestOwnTrappedValue = 0; for (u = 0; u < pos->uNumTrapped[uColor]; u++) { c = pos->cTrapped[uColor][u]; ASSERT(IS_ON_BOARD(c)); if (_WhoControlsSquareFast(pos, c) == FLIP(uColor)) { #ifdef DEBUG p = pos->rgSquare[c].pPiece; ASSERT(p); ASSERT(!IS_PAWN(p)); ASSERT(GET_COLOR(p) == uColor); #endif // See if the side who created the trap and controls // the square the trapped piece is sitting on has the // move too. If so, uColor moved at ply-1. if (OPPOSITE_COLORS(uColor, pos->uToMove)) { if (ctx->uPly > 0) { RecordEnprisePieceAtPly(ctx, ctx->uPly - 1, c); } EVAL_TERM(uColor, p, c, pos->iScore[uColor], ENPRISE_AND_TRAPPED_PENALTY, "en prise and trapped"); } // ctx->cTrapped[uPly] (RecordTrappedPiece's target) // is a single slot, not a list -- if more than one // of our own pieces is genuinely trapped this ply, // only report the most valuable one. else { if (PIECE_VALUE(pos->rgSquare[c].pPiece) > uBestOwnTrappedValue) { uBestOwnTrappedValue = PIECE_VALUE(pos->rgSquare[c].pPiece); cBestOwnTrapped = c; } } } } // uColor has the move but we found at least one piece that // seems to have no safe place to move and is actively under // attack now. It may not be lost, at least uColor has the // move. But this is still a bad thing. if (IS_ON_BOARD(cBestOwnTrapped)) { RecordTrappedPiece(ctx, cBestOwnTrapped); p = pos->rgSquare[cBestOwnTrapped].pPiece; EVAL_TERM(uColor, p, cBestOwnTrapped, pos->iScore[uColor], TRAPPED_WITH_MOVE_PENALTY, "trapped piece, our move"); } } } static void _EvalBishopPairs(IN POSITION *pos) /*++ Routine description: Give a flat bonus to sides that have a viable bishop pair (one light-squared, one dark-squared) -- not scaled by pawn count, by direct instruction (2026-09-06). Parameters: IN POSITION *pos - position Return value: static void --*/ { ULONG uColor; FOREACH_COLOR(uColor) { ULONG uBishopCount = pos->uNonPawnCount[uColor][BISHOP]; ASSERT(uBishopCount <= 10); ULONG uWhiteSqBishopCount = pos->uWhiteSqBishopCount[uColor]; ASSERT(uWhiteSqBishopCount <= 10); FLAG fPair = ((uBishopCount > 1) & (uWhiteSqBishopCount != 0) & (uWhiteSqBishopCount != uBishopCount)); EVAL_TERM(uColor, 0, ILLEGAL_COOR, pos->iScore[uColor], fPair * BISHOP_PAIR_BONUS, "bishop pair"); } } static FLAG _SideHasWinningChances(IN POSITION *pos, IN ULONG uSide) /** Routine description: Cheap, material-count-only classifier (the same idea as Crafty's EvaluateWinningChances): can uSide possibly force a win from this material alone, ignoring the actual position entirely? Used only to soften (scale toward drawscore) an otherwise-misleading raw material lead in Eval() below -- unlike recogn.c's interior-node recognizers (RECOGN_EXACT/UPPER/LOWER), this is never a hard claim fed into a search bound, so a wrong answer here just biases the eval a little; it can't corrupt alpha-beta the way a wrong hard recognizer bound can (see recogn.c's _RecognizeKNKP/_RecognizeKBNK incident). Deliberately conservative: only covers material shapes where "no, this can't be forced" is basic, textbook chess knowledge (insufficient mating material, up the exchange with nothing else changed, bare knights vs a bare king), not trickier exceptions (fortress draws, wrong-bishop-pawn-with-king-in-time geometry) that need real board information -- those are recogn.c's hard-recognizer territory (already exact for tiny material there) or plain search's job, not this cheap pre-check's. Parameters: POSITION *pos, ULONG uSide Return value: static FLAG : TRUE if uSide has any winning chances at all, FALSE if this exact material can provably never be forced to a win regardless of position. **/ { ULONG uEnemy = FLIP(uSide); INT iMajorDiff; INT iMinorDiff; // // A pawn always gives some winning chances (it can always try to // queen with support) -- nothing below applies. // if (pos->uPawnCount[uSide] > 0) { return(TRUE); } // // No pawns and the only piece besides the king is a single minor: // never enough material to force mate (K+N or K+B vs anything is // never a forced win on material alone). // if ((2 == pos->uNonPawnCount[uSide][0]) && (1 == (pos->uNonPawnCount[uSide][KNIGHT] + pos->uNonPawnCount[uSide][BISHOP]))) { return(FALSE); } // // No pawns and up exactly the exchange (one extra rook/queen, // balanced by one extra enemy minor elsewhere): not enough to // force a win either -- e.g. KRB vs KR can be held by the // defender with correct play. // iMajorDiff = (INT)(pos->uNonPawnCount[uSide][ROOK] + 2 * pos->uNonPawnCount[uSide][QUEEN]) - (INT)(pos->uNonPawnCount[uEnemy][ROOK] + 2 * pos->uNonPawnCount[uEnemy][QUEEN]); if ((1 == iMajorDiff) || (-1 == iMajorDiff)) { iMinorDiff = (INT)(pos->uNonPawnCount[uEnemy][KNIGHT] + pos->uNonPawnCount[uEnemy][BISHOP]) - (INT)(pos->uNonPawnCount[uSide][KNIGHT] + pos->uNonPawnCount[uSide][BISHOP]); if (iMajorDiff == iMinorDiff) { return(FALSE); } } // // No pawns, exactly two bare knights (no bishops/rooks/queens) for // uSide, and the enemy has nothing left at all: two knights can't // force mate against a bare king. Two bishops CAN (excluded here // by requiring BISHOP count == 0) and knight+bishop CAN too (also // excluded, since that has one bishop, not zero) -- both are // genuine, if sometimes technique-heavy, forced wins. // if ((2 == pos->uNonPawnCount[uSide][KNIGHT]) && (0 == pos->uNonPawnCount[uSide][BISHOP]) && (0 == pos->uNonPawnCount[uSide][ROOK]) && (0 == pos->uNonPawnCount[uSide][QUEEN]) && (1 == pos->uNonPawnCount[uEnemy][0]) && (0 == pos->uPawnCount[uEnemy])) { return(FALSE); } return(TRUE); } void InitEval(void) { ROOK_FULL_HALF_OPEN_BONUS[0][0] = ROOK_ON_FULL_OPEN; ROOK_FULL_HALF_OPEN_BONUS[0][1] = ROOK_ON_HALF_OPEN_WITH_ENEMY; ROOK_FULL_HALF_OPEN_BONUS[1][0] = ROOK_ON_HALF_OPEN_WITH_FRIEND; ROOK_FULL_HALF_OPEN_BONUS[1][1] = 0; } SCORE Eval(IN SEARCHER_THREAD_CONTEXT *ctx, IN SCORE iAlpha, IN SCORE iBeta, OUT SCORE *piPositional) /** Routine description: Parameters: SEARCHER_THREAD_CONTEXT *ctx, SCORE iAlpha, SCORE iBeta, SCORE *piPositional : if non-NULL, filled in with a magnitude (always >= 0) estimating the non-material component of the score -- exact if a full eval ran, a cheap estimate otherwise. Callers must treat it as an estimate either way; it's only ever used to size a pruning margin, never as a hard fact. Return value: SCORE **/ { POSITION *pos = &(ctx->sPosition); SCORE iScoreForSideToMove; SCORE iAlphaMargin, iBetaMargin; PAWN_HASH_ENTRY *pHash; COOR c; ULONG u; ULONG uColor; ULONG xColor; BITBOARD bb; FLAG fDeferred; #ifdef EVAL_TIME UINT64 uTimer = SystemReadTimeStampCounter(); #endif #ifdef CALIBRATE_POSITIONAL g_uCalibrateEvalCalls++; #endif #ifdef CALIBRATE_MARGIN_SAFETY // Set when a real exit would have been taken; the rest of Eval() // still runs (for measurement), but the saved values get restored // just before returning so real search behavior is bit-for-bit // unaffected -- see the swing recorded near `end:` below. FLAG fWouldHaveExited = FALSE; SCORE iSavedLazyScore = 0; SCORE iSavedLazyPositional = 0; #endif ASSERT(IS_VALID_SCORE(iAlpha)); ASSERT(IS_VALID_SCORE(iBeta)); ASSERT(iAlpha < iBeta); ASSERT((pos->iMaterialBalance[WHITE] * -1) == pos->iMaterialBalance[BLACK]); pos->uNumTrapped[BLACK] = pos->uNumTrapped[WHITE] = 0; pos->iScore[BLACK] = (pos->uPawnMaterial[BLACK] + pos->uNonPawnMaterial[BLACK]); pos->iScore[WHITE] = (pos->uPawnMaterial[WHITE] + pos->uNonPawnMaterial[WHITE]); #ifdef EVAL_DUMP EvalTraceClear(); Trace("Material:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif // // Pawn eval. Note: if fDeferred comes back as TRUE then we have // neither cleared nor initialized the attack tables. This is // because we hope that we can get a lazy eval cutoff here and // save some time. BEFORE ANY CODE BELOW TOUCHES THE ATTACK // TABLES IT NEEDS TO CLEAR/POPULATE THEM THOUGH!!! // #ifdef EVAL_TIME { UINT64 u64PawnTimer = SystemReadTimeStampCounter(); pHash = _EvalPawns(ctx, &fDeferred); { UINT64 u64Elapsed = SystemReadTimeStampCounter() - u64PawnTimer; ctx->sCounters.tree.u64CyclesEvalPawns += u64Elapsed; if (fDeferred) { ctx->sCounters.tree.u64CyclesEvalPawnsHit += u64Elapsed; ctx->sCounters.tree.u64CountEvalPawnsHit++; } else { ctx->sCounters.tree.u64CyclesEvalPawnsMiss += u64Elapsed; ctx->sCounters.tree.u64CountEvalPawnsMiss++; } } } #else pHash = _EvalPawns(ctx, &fDeferred); #endif ASSERT(NULL != pHash); ASSERT(IS_VALID_FLAG(fDeferred)); pos->iScore[WHITE] += pHash->iScore[WHITE]; pos->iScore[BLACK] += pHash->iScore[BLACK]; ASSERT(IS_VALID_SCORE(pos->iScore[WHITE] - pos->iScore[BLACK])); #ifdef EVAL_DUMP Trace("After pawns:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif // // Look for won passers races. Note: This code cannot trust the // state of the attack tables yet! // if ((pos->uNonPawnCount[WHITE][0] == 1) || (pos->uNonPawnCount[BLACK][0] == 1)) { (void)_EvalPasserRacesAgainstLoneKings(pos, pHash); #ifdef EVAL_DUMP Trace("After passer races:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // // Bad trade code. Right. Note: this code cannot trust the state // of the attack tables... // if (pos->uNonPawnMaterial[WHITE] != pos->uNonPawnMaterial[BLACK]) { _EvalBadTrades(pos); #ifdef EVAL_DUMP Trace("After bad trades:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // // Bishop pairs. Again... Note: this code cannot trust the state // of the attack tables. // _EvalBishopPairs(pos); #ifdef EVAL_DUMP Trace("After bishop pair bonus:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif #ifdef LAZY_EVAL // // Below LAZY_EVAL_MIN_MATERIAL combined army scaler, skip lazy eval // entirely (see its chess.h comment) -- measured directly via // CALIBRATE_MARGIN_SAFETY, the regular lazy exit's real swing // exceeded its own assumed margin 20.6% of the time in near-bare- // king endgames, far above the ~0.01-0.36% rate everywhere else. // if (pos->uArmyScaler[WHITE] + pos->uArmyScaler[BLACK] >= LAZY_EVAL_MIN_MATERIAL) { #ifdef EVAL_TIME UINT64 uLazyDecisionTimer = SystemReadTimeStampCounter(); #endif // // Compute an estimate of the score for the side to move based on the // eval terms we have already considered: // // 1. Material balance // 2. Pawn structure bonuses/penalties (incl passers/candidates) // 3. Passer races are detected already // 4. "Bad trade" code has already run // 5. We've already detected unwinnable endgames // 6. Bishop pairs // iScoreForSideToMove = (pos->iScore[pos->uToMove] - pos->iScore[FLIP(pos->uToMove)]); ASSERT(IS_VALID_SCORE(iScoreForSideToMove)); // // Eval has not considered several potentially large terms: // // 1. Piece positional components (such as mobility, trapped) // 2. King safety penalties // 3. Other miscellaneous bonuses/penalties // // We build two "margins" to account for these components of the // score. // iAlphaMargin = iBetaMargin = LAZY_EVAL_BASE_MARGIN; // // If (score + alpha_margin) is already > alpha -OR- // (score - beta_margin) is already < beta // // ...then we can stop thinking about lazy eval; the rest of the // computation only increases the margin so we know LE will fail // and can save some work here. // #ifdef CALIBRATE_POSITIONAL ctx->fCalibrateCandidate = FALSE; g_uCalibrateGateChecked++; #endif if ((iScoreForSideToMove + iAlphaMargin < iAlpha) || (iScoreForSideToMove - iBetaMargin > iBeta)) { #ifdef CALIBRATE_POSITIONAL g_uCalibrateGateTrue++; #endif // // Ok, we can't say for sure that we won't take a lazy exit // yet. So do the expensive part of lazy eval estimation and // widen the margin further for king safety issues and passed // pawns. // #ifdef CALIBRATE_POSITIONAL // This node is one where a real (uninstrumented) build would // actually call EstimatePositionalScore and consider a lazy // exit -- the population _RecordPositionalCalibration should // be sampling from, per _EvalKing's calls below. ctx->fCalibrateCandidate = TRUE; #endif EstimatePositionalScore(ctx, pos, pHash, &iAlphaMargin, &iBetaMargin); #ifdef CALIBRATE_BASE_MARGIN { ULONG uBucket = _MaxKingDangerBucket(ctx, pos, pHash); ULONG uMatBucket = _MaterialBucket(pos); g_uBaseMarginExitAttempted[uMatBucket][uBucket]++; if ((iScoreForSideToMove + iAlphaMargin <= iAlpha) || (iScoreForSideToMove - iBetaMargin >= iBeta)) { g_uBaseMarginExitTaken[uMatBucket][uBucket]++; } } #endif // // CALIBRATE_POSITIONAL builds never take the lazy exit here -- // the whole point of a calibration run is to compute the cheap // defect-count estimate *and* let Eval() continue on to // _EvalKing's real computation on every node, so the two can be // logged and compared. See _RecordPositionalCalibration below // (called from _EvalKing) and DumpPositionalCalibration // (command.c's "calibrate dump"). // #ifndef CALIBRATE_POSITIONAL if (iScoreForSideToMove + iAlphaMargin <= iAlpha) { INC(ctx->sCounters.tree.u64LazyEvals); if (NULL != piPositional) { *piPositional = iAlphaMargin; } #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalPreLazy += (SystemReadTimeStampCounter() - uTimer); #endif #ifdef CALIBRATE_MARGIN_SAFETY // Don't exit yet -- let the real full eval run below so we // can measure the true swing, then restore these values // right before `end:` so the caller sees exactly what a // normal build would have returned. fWouldHaveExited = TRUE; iSavedLazyScore = iScoreForSideToMove; iSavedLazyPositional = iAlphaMargin; #else #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalLazyDecision += (SystemReadTimeStampCounter() - uLazyDecisionTimer); #endif goto end; #endif } else if (iScoreForSideToMove - iBetaMargin >= iBeta) { INC(ctx->sCounters.tree.u64LazyEvals); if (NULL != piPositional) { *piPositional = iBetaMargin; } #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalPreLazy += (SystemReadTimeStampCounter() - uTimer); #endif #ifdef CALIBRATE_MARGIN_SAFETY fWouldHaveExited = TRUE; iSavedLazyScore = iScoreForSideToMove; iSavedLazyPositional = iBetaMargin; #else #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalLazyDecision += (SystemReadTimeStampCounter() - uLazyDecisionTimer); #endif goto end; #endif } #endif // !CALIBRATE_POSITIONAL #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalLazyDecision += (SystemReadTimeStampCounter() - uLazyDecisionTimer); #endif } #ifdef CALIBRATE_BASE_MARGIN else { // // The flat gate said "don't even try" -- measure whether the // real, widened margin would have allowed an exit anyway had // we bothered to compute it. A "yes" here is a false negative // attributable to LAZY_EVAL_BASE_MARGIN being tighter than it // needs to be. This extra EstimatePositionalScore call is // measurement-only overhead, real exit behavior is unaffected // (this is the branch where a real build already skips lazy // eval entirely). // SCORE iMeasuredAlphaMargin = LAZY_EVAL_BASE_MARGIN; SCORE iMeasuredBetaMargin = LAZY_EVAL_BASE_MARGIN; ULONG uMatBucket = _MaterialBucket(pos); EstimatePositionalScore(ctx, pos, pHash, &iMeasuredAlphaMargin, &iMeasuredBetaMargin); if ((iScoreForSideToMove + iMeasuredAlphaMargin <= iAlpha) || (iScoreForSideToMove - iMeasuredBetaMargin >= iBeta)) { g_uBaseMarginFalseNegative[uMatBucket]++; } else { g_uBaseMarginCorrectlyFull[uMatBucket]++; } } #endif } // LAZY_EVAL_MIN_MATERIAL #endif #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalPreLazy += (SystemReadTimeStampCounter() - uTimer); #endif // // If we have to clear/populate the attack table, do it now that // we know we aren't taking a lazy exit. // if (TRUE == fDeferred) { TIMED_EVAL_CALL(ctx, u64CyclesEvalAttackTablePop, _PopulatePawnAttackBits(pos)); } // // Pre-compute some common terms used in per-piece evals: // // This is a scaler based on the size of the army for each side. // pos->uArmyScaler[BLACK] = pos->uNonPawnMaterial[BLACK] - VALUE_KING; pos->uArmyScaler[WHITE] = pos->uNonPawnMaterial[WHITE] - VALUE_KING; pos->uArmyScaler[BLACK] /= VALUE_PAWN; pos->uArmyScaler[WHITE] /= VALUE_PAWN; ASSERT(!(pos->uArmyScaler[BLACK] & 0x80000000)); ASSERT(!(pos->uArmyScaler[WHITE] & 0x80000000)); pos->uArmyScaler[BLACK] = MINU(31, pos->uArmyScaler[BLACK]); pos->uArmyScaler[WHITE] = MINU(31, pos->uArmyScaler[WHITE]); ASSERT(pos->uArmyScaler[BLACK] >= 0); ASSERT(pos->uArmyScaler[BLACK] <= 31); ASSERT(pos->uArmyScaler[WHITE] >= 0); ASSERT(pos->uArmyScaler[WHITE] <= 31); pos->iReducedMaterialDownScaler[BLACK] = pos->iReducedMaterialDownScaler[WHITE] = 0; // // This is a "position is closed|open" number. // pos->uClosedScaler = (pos->uPawnCount[WHITE] + pos->uPawnCount[BLACK]) - (pHash->uNumUnmovedPawns[WHITE] + pHash->uNumUnmovedPawns[BLACK]) + (pHash->uNumRammedPawns); ASSERT(pos->uClosedScaler >= 0); ASSERT(pos->uClosedScaler <= 32); // // Evaluate individual pieces. // // board_representation/EVAL.md section 0/2: replaces the old // cNonPawns[color][1..N] walk + per-piece mailbox lookup + p&0x4/ // IS_KNIGHT branch dispatch (a hard-to-predict branch per piece, // on top of a redundant mailbox read the callee already redoes // for its own ASSERT) with a direct per-type location-bitboard // walk. pos->bbPieces[color][PIECE_TYPE] already exists, // incrementally maintained by move.c, and was simply unused by // Eval() until now -- no new infrastructure, purely a consumer // change. Also retires the cDefer/uDefer "remember the rooks and // queens for later" bookkeeping entirely: since each type is now // its own direct bitboard walk, "evaluate rooks after minors" is // just "do the rook-bitboard walk after the minor-bitboard walks" // -- nothing to defer. // // board_representation/EVAL.md section 9: the real dependency // isn't "side to move first" -- it's "both colors' minors done // before any rook or queen, both rooks done before any queen, // everything above done before either king" (_EvalRook/_EvalQueen // read the enemy's pos->bbMinorAttacks; _EvalKing reads both // colors' bbMinorAttacks/bbQueenAttacks; _ReEvalPassers further // down needs the king scores). Interleaving ours-then-enemy within // each type (rather than both colors' minors, then both colors' // rooks, then both colors' queens as separate phases) still // satisfies that same ordering -- knight-vs-bishop order *within* // a color, and encounter order within a single type's own bitboard // walk, were never meaningful (cNonPawns' order is arbitrary // swap-with-last-on-removal, not stable) and stay that way -- // EVAL_TERM's plain score accumulation doesn't care. // pos->uMinorsAtHome[BLACK] = pos->uMinorsAtHome[WHITE] = 0; uColor = pos->uToMove; xColor = FLIP(uColor); // Knights. bb = pos->bbPieces[uColor][KNIGHT]; while (bb) { u = FastFirstBit(bb) - 1; bb &= (bb - 1); c = BIT_NUMBER_TO_COOR(u); TIMED_EVAL_CALL(ctx, u64CyclesEvalKnight, _EvalKnight(pos, c, pHash)); #ifdef EVAL_DUMP Trace("After our N at %s:\n%d\t\t%d\n", CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // Enemy knights. ASSERT(xColor != pos->uToMove); bb = pos->bbPieces[xColor][KNIGHT]; while (bb) { u = FastFirstBit(bb) - 1; bb &= (bb - 1); c = BIT_NUMBER_TO_COOR(u); TIMED_EVAL_CALL(ctx, u64CyclesEvalKnight, _EvalKnight(pos, c, pHash)); #ifdef EVAL_DUMP Trace("After enemy N at %s:\n%d\t\t%d\n", CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // Bishops. bb = pos->bbPieces[uColor][BISHOP]; while (bb) { u = FastFirstBit(bb) - 1; bb &= (bb - 1); c = BIT_NUMBER_TO_COOR(u); TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop, _EvalBishop(pos, c, pHash)); #ifdef EVAL_DUMP Trace("After our B at %s:\n%d\t\t%d\n", CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // Enemy bishops. bb = pos->bbPieces[xColor][BISHOP]; while (bb) { u = FastFirstBit(bb) - 1; bb &= (bb - 1); c = BIT_NUMBER_TO_COOR(u); TIMED_EVAL_CALL(ctx, u64CyclesEvalBishop, _EvalBishop(pos, c, pHash)); #ifdef EVAL_DUMP Trace("After enemy B at %s:\n%d\t\t%d\n", CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // Rooks. bb = pos->bbPieces[uColor][ROOK]; while (bb) { u = FastFirstBit(bb) - 1; bb &= (bb - 1); c = BIT_NUMBER_TO_COOR(u); TIMED_EVAL_CALL(ctx, u64CyclesEvalRook, _EvalRook(pos, c, pHash)); #ifdef EVAL_DUMP Trace("After our R at %s:\n%d\t\t%d\n", CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // Enemy rooks. bb = pos->bbPieces[xColor][ROOK]; while (bb) { u = FastFirstBit(bb) - 1; bb &= (bb - 1); c = BIT_NUMBER_TO_COOR(u); TIMED_EVAL_CALL(ctx, u64CyclesEvalRook, _EvalRook(pos, c, pHash)); #ifdef EVAL_DUMP Trace("After enemy R at %s:\n%d\t\t%d\n", CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // Our queen(s). bb = pos->bbPieces[uColor][QUEEN]; while (bb) { u = FastFirstBit(bb) - 1; bb &= (bb - 1); c = BIT_NUMBER_TO_COOR(u); TIMED_EVAL_CALL(ctx, u64CyclesEvalQueen, _EvalQueen(pos, c, pHash)); #ifdef EVAL_DUMP Trace("After our Q at %s:\n%d\t\t%d\n", CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // Enemy queen(s). bb = pos->bbPieces[xColor][QUEEN]; while (bb) { u = FastFirstBit(bb) - 1; bb &= (bb - 1); c = BIT_NUMBER_TO_COOR(u); TIMED_EVAL_CALL(ctx, u64CyclesEvalQueen, _EvalQueen(pos, c, pHash)); #ifdef EVAL_DUMP Trace("After enemy Q at %s:\n%d\t\t%d\n", CoorToString(c), pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // // Evaluate the two kings last. // c = pos->cNonPawns[BLACK][0]; #ifdef DEBUG ASSERT(IS_ON_BOARD(c)); { PIECE pDebugKing = pos->rgSquare[c].pPiece; ASSERT(IS_VALID_PIECE(pDebugKing)); ASSERT(GET_COLOR(pDebugKing) == BLACK); ASSERT(IS_KING(pDebugKing)); } #endif TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash, ctx)); #ifdef EVAL_DUMP Trace("After *k:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif c = pos->cNonPawns[WHITE][0]; #ifdef DEBUG ASSERT(IS_ON_BOARD(c)); { PIECE pDebugKing = pos->rgSquare[c].pPiece; ASSERT(IS_VALID_PIECE(pDebugKing)); ASSERT(GET_COLOR(pDebugKing) == WHITE); ASSERT(IS_KING(pDebugKing)); } #endif TIMED_EVAL_CALL(ctx, u64CyclesEvalKing, _EvalKing(pos, c, pHash, ctx)); #ifdef EVAL_DUMP Trace("After .k:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif // // Now that we have the whole attack table generated, think about // passed pawns identified by the pawn eval routine again. Also // see if the side not on move has a trapped piece. // #ifdef EVAL_TIME UINT64 u64PostLazyMiscTimer = SystemReadTimeStampCounter(); #endif bb = (pHash->bbPasserLocations[WHITE] | pHash->bbPasserLocations[BLACK]); if (0 != bb) { _ReEvalPassers(pos, pHash); #ifdef EVAL_DUMP Trace("After passers:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif } // // _EvalLookForDanger/_EvalTrappedPieces only ever *add* a hint // when they find one; a full eval that finds nothing this time // would otherwise leave a stale, unrelated hint from an earlier // visit to this ply sitting here (still piece-identity-valid by // coincidence). Clear this ply's self-danger slots first so a // clean full eval reliably means a clean slate. // ctx->cEnprise[ctx->uPly][0] = ctx->cEnprise[ctx->uPly][1] = ILLEGAL_COOR; ctx->cTrapped[ctx->uPly] = ILLEGAL_COOR; _EvalLookForDanger(ctx); _EvalTrappedPieces(ctx); // TODO: endgame-specific knowledge? e.g. B over N in an endgame // with 2 pawn wings? // // Roll in the reduced material down scaler terms. // ASSERT(pos->iReducedMaterialDownScaler[BLACK] > -200); ASSERT(pos->iReducedMaterialDownScaler[BLACK] < +200); iAlphaMargin = (pos->iReducedMaterialDownScaler[BLACK] * (SCORE)REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[BLACK]]) / 8; pos->iScore[BLACK] += iAlphaMargin; ASSERT(pos->iReducedMaterialDownScaler[WHITE] > -200); ASSERT(pos->iReducedMaterialDownScaler[WHITE] < +200); iAlphaMargin = (pos->iReducedMaterialDownScaler[WHITE] * (SCORE)REDUCED_MATERIAL_DOWN_SCALER[pos->uArmyScaler[WHITE]]) / 8; pos->iScore[WHITE] += iAlphaMargin; #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesEvalPostLazyMisc += (SystemReadTimeStampCounter() - u64PostLazyMiscTimer); #endif // // Almost done // iScoreForSideToMove = (pos->iScore[pos->uToMove] - pos->iScore[FLIP(pos->uToMove)]); #ifdef EVAL_DUMP Trace("At the end:\n%d\t\t%d\n", pos->iScore[WHITE], pos->iScore[BLACK]); #endif // // TODO: detect and discourage blocked positions? // // If the side who's ahead by raw material/positional score can't // actually force a win with what it has left on the board (see // _SideHasWinningChances), squash the score hard towards draw -- // same idiom as the 50-move dampening just below (and the same // signed-arithmetic care: g_iDrawScore is currently always 0, but // write this relative to it rather than assuming that, matching // the 50-move code's own convention). /16 (not a full collapse to // drawscore) deliberately mirrors Crafty's EvaluateDraws -- e.g. // KRB vs KR is still theoretically losable by the run-of-the-mill // defender with the checks _SideHasWinningChances gates on, so // some residual signal survives. if ((iScoreForSideToMove > g_iDrawScore[pos->uToMove]) && (FALSE == _SideHasWinningChances(pos, pos->uToMove))) { iScoreForSideToMove = g_iDrawScore[pos->uToMove] + ((iScoreForSideToMove - g_iDrawScore[pos->uToMove]) / 16); } else if ((iScoreForSideToMove < g_iDrawScore[pos->uToMove]) && (FALSE == _SideHasWinningChances(pos, FLIP(pos->uToMove)))) { iScoreForSideToMove = g_iDrawScore[pos->uToMove] + ((iScoreForSideToMove - g_iDrawScore[pos->uToMove]) / 16); } // Drive the score towards draw as we approach a 50 move w/o // progress draw. if (pos->uFifty > 84) { ULONG uDrawDist = 101 - pos->uFifty; ASSERT(uDrawDist > 0); // uDrawDist is ULONG -- multiplying a negative SCORE by it // directly promotes the SCORE to unsigned first (usual // arithmetic conversions, same rank), wrapping a negative // iScoreForSideToMove into a huge positive garbage value // instead of scaling it down. Cast uDrawDist to SCORE so the // multiply happens in signed arithmetic; its range (1-16) is // always representable. iScoreForSideToMove = g_iDrawScore[pos->uToMove] + (iScoreForSideToMove * (SCORE)uDrawDist / 16); } // // Adjust dynamic positional component. // iAlphaMargin = abs(pos->iMaterialBalance[pos->uToMove] - iScoreForSideToMove); if (NULL != piPositional) { *piPositional = iAlphaMargin; } g_Options.iLastEvalScore = iScoreForSideToMove; #ifdef CALIBRATE_MARGIN_SAFETY if (TRUE == fWouldHaveExited) { // // The real swing: how far the true, full-eval score actually // ended up from the lazy-exit score this node would have // returned. RecordMarginSafetySwing buckets this by material; // its max (not just p90) is the number that answers "how // large would LAZY_EVAL_BASE_MARGIN have had to be before an // exit here would have been unsound." // RecordMarginSafetySwing(pos, abs(iScoreForSideToMove - iSavedLazyScore), iSavedLazyPositional); // // Restore exactly what a normal build would have returned -- // this measurement must not change real search behavior. // iScoreForSideToMove = iSavedLazyScore; if (NULL != piPositional) { *piPositional = iSavedLazyPositional; } } #endif #ifdef EVAL_TIME ctx->sCounters.tree.u64CyclesInEval += (SystemReadTimeStampCounter() - uTimer); #endif INC(ctx->sCounters.tree.u64FullEvals); end: ASSERT(IS_VALID_SCORE(iScoreForSideToMove)); ASSERT((iScoreForSideToMove > -NMATE) && (iScoreForSideToMove < +NMATE)); ASSERT(abs(ctx->sPlyInfo[ctx->uPly].iKingScore[BLACK]) < 700); ASSERT(abs(ctx->sPlyInfo[ctx->uPly].iKingScore[WHITE]) < 700); return(iScoreForSideToMove); }