diff options
| author | Scott Gasch <[email protected]> | 2026-10-20 09:26:44 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-20 09:26:44 -0700 |
| commit | c8c6bbc2c1a103e5b07b079a0f8bdf66c7724d80 (patch) | |
| tree | b6c2137c9ac577fa83b1e948fa0311d53dfe445d /src/eval.c | |
| parent | 3b5d3677bb14ae14728a1c4b221176bb83667d92 (diff) | |
Wire NNUE eval into search, build the training pipeline, and add a fast
move/eval-agreement benchmark against Stockfish (tests/lop)
Implements the architecture decisions in src/docs/NNUE.md: HalfKAv2_hm
feature set, int16/int8 quantization, per-ply accumulator backup for
king moves, and a lazy-eval-ready integration into search.c/eval.c.
Adds the full annotate_pgns/ training pipeline (extraction, K-fit/
labeling, quiet filtering, tensor building, training, export) plus an
end-to-end orchestrator (run_nnue_pipeline.py) and validation tooling
(sanity_check_net.py, compare_vs_stockfish_net.py). Adds tests/lop:
a 10k-position list-of-positions benchmark comparing engine eval rank
and move choice against pre-computed Stockfish/lc0 references, much
cheaper than a full self-play match for day-to-day progress checks.
First sd10-matched self-play result against classic HCE: LLR +2.28
(H1=+2.94), -72=119+169 over 360 games, confirming the net is
"smarter per node" at matched depth. A real-time (--st 4) match is
currently weaker (candidate loses ~3 plies of depth to a ~3.3-3.7x
nps gap against an unrecalibrated search), consistent with
NNUE.md's adoption-criterion section -- pruning/reduction constants
still need retuning for the new eval's score scale before a fair
real-time comparison is possible.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/eval.c')
| -rw-r--r-- | src/eval.c | 347 |
1 files changed, 322 insertions, 25 deletions
@@ -3275,6 +3275,12 @@ Return value: static UINT64 g_u64SuperLazyEvalsByBucket[BASE_MARGIN_MAT_BUCKETS]; static UINT64 g_u64LazyEvalsByBucket[BASE_MARGIN_MAT_BUCKETS]; static UINT64 g_u64FullEvalsByBucket[BASE_MARGIN_MAT_BUCKETS]; +// Same idea, for the two NNUE exits -- added 2026-10-16 alongside the +// EVAL_POSITIONAL_SOURCE_NNUE_FULL/NNUE_LAZY tiers, specifically to +// answer "what fraction of real NNUE eval calls does the lazy probe +// gate actually resolve" with a real measurement instead of a guess. +static UINT64 g_u64NnueLazyEvalsByBucket[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_u64NnueFullEvalsByBucket[BASE_MARGIN_MAT_BUCKETS]; void DumpEvalExitRateByBucket(void) @@ -3324,6 +3330,29 @@ Return value: 100.0 * (double)u64L / (double)u64Total, 100.0 * (double)u64F / (double)u64Total); } + + Trace("NNUE eval exit rate by material bucket (lazy probe gate vs full " + "forward pass):\n"); + for (m = 0; m < BASE_MARGIN_MAT_BUCKETS; m++) + { + UINT64 u64NL = g_u64NnueLazyEvalsByBucket[m]; + UINT64 u64NF = g_u64NnueFullEvalsByBucket[m]; + UINT64 u64Total = u64NL + u64NF; + if (0 == u64Total) + { + Trace(" bucket %u (scaler %u-%u): no samples\n", + m, m * BASE_MARGIN_MAT_WIDTH, + (m * BASE_MARGIN_MAT_WIDTH) + BASE_MARGIN_MAT_WIDTH - 1); + continue; + } + Trace(" bucket %u (scaler %2u-%2u): n=%12" COMPILER_LONGLONG_UNSIGNED_FORMAT + " nnue_lazy=%5.2f%% nnue_full=%5.2f%%\n", + m, m * BASE_MARGIN_MAT_WIDTH, + (m * BASE_MARGIN_MAT_WIDTH) + BASE_MARGIN_MAT_WIDTH - 1, + u64Total, + 100.0 * (double)u64NL / (double)u64Total, + 100.0 * (double)u64NF / (double)u64Total); + } } @@ -3494,6 +3523,37 @@ static UINT64 g_uSuperLazyMarginSwingSumSq[BASE_MARGIN_MAT_BUCKETS]; static UINT64 g_uSuperLazyMarginExceeded[BASE_MARGIN_MAT_BUCKETS]; static UINT64 g_uSuperLazyMarginExceededFenSamples[BASE_MARGIN_MAT_BUCKETS]; +// Same again, for the experimental NNUE lazy probe (docs/NNUE.md's +// "Lazy-evaluation tiering plan") -- |NnueForward() - NnueProbeScore()| +// swing, same bucketing (_MaterialBucket), same harness, own counter +// set so this doesn't mix with either HCE tier's numbers. No real +// margin exists for this tier yet -- this is purely measurement, not +// gating anything -- so RecordNnueLazyMarginSafetySwing is always +// called with an effectively-infinite iActualMarginUsed (see its call +// site in Eval()) until a real margin is derived from this data. +static UINT64 g_uNnueLazyMarginSwingMax[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueLazyMarginSwingCount[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueLazyMarginSwingSum[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueLazyMarginSwingHist[BASE_MARGIN_MAT_BUCKETS][SWING_HIST_BINS]; +static UINT64 g_uNnueLazyMarginSwingSumSq[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueLazyMarginExceeded[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueLazyMarginExceededFenSamples[BASE_MARGIN_MAT_BUCKETS]; + +// Material-only "super lazy" tier -- |NnueForward() - +// pos->iMaterialBalance[uToMove]|, no fit/regression at all (unlike +// the accumulator probe above): VALUE_PAWN=100 means iMaterialBalance +// is already in centipawns, the same units NnueForward returns, so +// raw material balance is directly usable as the "center" estimate -- +// exactly how classic Eval()'s own super-lazy exit already uses it. +// Same bucketing/harness as the lazy tier above. +static UINT64 g_uNnueSuperLazyMarginSwingMax[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueSuperLazyMarginSwingCount[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueSuperLazyMarginSwingSum[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueSuperLazyMarginSwingHist[BASE_MARGIN_MAT_BUCKETS][SWING_HIST_BINS]; +static UINT64 g_uNnueSuperLazyMarginSwingSumSq[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueSuperLazyMarginExceeded[BASE_MARGIN_MAT_BUCKETS]; +static UINT64 g_uNnueSuperLazyMarginExceededFenSamples[BASE_MARGIN_MAT_BUCKETS]; + static void _RecordMarginSafetySwingInto(IN OUT UINT64 *puMax, IN OUT UINT64 *puCount, @@ -3616,6 +3676,84 @@ Return value: } +static void +RecordNnueLazyMarginSafetySwing(IN POSITION *pos, + IN SCORE iSwing, + IN SCORE iActualMarginUsed) +/** + +Routine description: + + Same shape as RecordSuperLazyMarginSafetySwing, but for the + experimental NNUE accumulator-based lazy probe (docs/NNUE.md's + "Lazy-evaluation tiering plan") -- |NnueForward() - + NnueProbeScore()|, own counter set. No margin is actually gated on + yet -- iActualMarginUsed is passed as an effectively-infinite + sentinel from Eval() until a real margin is derived from this + measurement, so puExceeded stays meaningless for now; only the + histogram/sum/sumSq/max/count fields are real data at this stage. + +Parameters: + + POSITION *pos + SCORE iSwing : >= 0 + SCORE iActualMarginUsed : sentinel (no real margin chosen yet) + +Return value: + + void + +**/ +{ + _RecordMarginSafetySwingInto(g_uNnueLazyMarginSwingMax, + g_uNnueLazyMarginSwingCount, + g_uNnueLazyMarginSwingSum, + g_uNnueLazyMarginSwingSumSq, + g_uNnueLazyMarginSwingHist, + g_uNnueLazyMarginExceeded, + g_uNnueLazyMarginExceededFenSamples, + "nnue_lazy", + pos, iSwing, iActualMarginUsed); +} + + +static void +RecordNnueSuperLazyMarginSafetySwing(IN POSITION *pos, + IN SCORE iSwing, + IN SCORE iActualMarginUsed) +/** + +Routine description: + + Same shape as RecordNnueLazyMarginSafetySwing, but for the + material-only "super lazy" signal (|NnueForward() - + pos->iMaterialBalance[uToMove]|) -- own counter set, same + sentinel-until-a-real-margin-exists convention. + +Parameters: + + POSITION *pos + SCORE iSwing : >= 0 + SCORE iActualMarginUsed : sentinel (no real margin chosen yet) + +Return value: + + void + +**/ +{ + _RecordMarginSafetySwingInto(g_uNnueSuperLazyMarginSwingMax, + g_uNnueSuperLazyMarginSwingCount, + g_uNnueSuperLazyMarginSwingSum, + g_uNnueSuperLazyMarginSwingSumSq, + g_uNnueSuperLazyMarginSwingHist, + g_uNnueSuperLazyMarginExceeded, + g_uNnueSuperLazyMarginExceededFenSamples, + "nnue_superlazy", + pos, iSwing, iActualMarginUsed); +} + + // Plain Newton's-method sqrt, no <math.h> -- chess.h's own VALUE_KING // #defines INFINITY_SCORE as a board-eval sentinel, which collides with // <math.h>'s own INFINITY_SCORE macro, so libm's sqrt() isn't available @@ -3729,6 +3867,20 @@ Return value: g_uSuperLazyMarginSwingSumSq, g_uSuperLazyMarginSwingMax, g_uSuperLazyMarginSwingHist); + _PersistMarginSwingHistogramToFile(f, "nnue_lazy", + g_uNnueLazyMarginSwingCount, + g_uNnueLazyMarginExceeded, + g_uNnueLazyMarginSwingSum, + g_uNnueLazyMarginSwingSumSq, + g_uNnueLazyMarginSwingMax, + g_uNnueLazyMarginSwingHist); + _PersistMarginSwingHistogramToFile(f, "nnue_superlazy", + g_uNnueSuperLazyMarginSwingCount, + g_uNnueSuperLazyMarginExceeded, + g_uNnueSuperLazyMarginSwingSum, + g_uNnueSuperLazyMarginSwingSumSq, + g_uNnueSuperLazyMarginSwingMax, + g_uNnueSuperLazyMarginSwingHist); fclose(f); Trace("Raw margin-safety histograms written to %s\n", szPath); } @@ -3828,6 +3980,18 @@ Return value: g_uSuperLazyMarginSwingSum, g_uSuperLazyMarginSwingSumSq, g_uSuperLazyMarginExceeded); + _DumpMarginSafetyCalibrationInto("NNUE lazy probe margin safety", + g_uNnueLazyMarginSwingMax, + g_uNnueLazyMarginSwingCount, + g_uNnueLazyMarginSwingSum, + g_uNnueLazyMarginSwingSumSq, + g_uNnueLazyMarginExceeded); + _DumpMarginSafetyCalibrationInto("NNUE super lazy (material-only) margin safety", + g_uNnueSuperLazyMarginSwingMax, + g_uNnueSuperLazyMarginSwingCount, + g_uNnueSuperLazyMarginSwingSum, + g_uNnueSuperLazyMarginSwingSumSq, + g_uNnueSuperLazyMarginExceeded); PersistMarginSafetyHistogramsToFile(szHistogramPathOverride); } #endif // CALIBRATE_MARGIN_SAFETY @@ -4104,7 +4268,7 @@ static const SCORE iKingP98ByAttackerArmy[8] = // p98/p2 pessimistic/optimistic pairing margin_a/margin_b use -- // p50 is the right statistic for a bias correction, p98/p2 is the // right statistic for a soundness bound. iPositional[color] = -// iKingP50ByAttackerArmyOnly[opponent's army bucket] (opponent is the +// iKingP50ByAttackerArmy[opponent's army bucket] (opponent is the // attacker of this side's own king) + iResidualP50ByOwnArmy[this // side's own army bucket] (or the super-lazy-scoped sibling, for the // super-lazy exit). @@ -7043,22 +7207,18 @@ Return value: // intentionally the least-specific answer that's still safe, not // an attempt to approximate what a real positional estimate would // say. - if ((TRUE == g_fUseNnueEval) && (TRUE == g_fNnueWeightsLoaded)) - { - SCORE iNnueScore = NnueForward(ctx); - if (NULL != piPositional) - { - (*piPositional)[WHITE] = 0; - (*piPositional)[BLACK] = 0; - } - return iNnueScore; - } - - // Initialize army scalers here (moved ahead of the super-lazy exit - // point below, 2026-09-08 -- their only inputs, uNonPawnMaterial, - // are already available this early, and the super-lazy margin - // table needs a material bucket before it can run). We skip lazy - // eval if we're too late into an endgame. + // Initialize army scalers here -- moved ahead of BOTH the NNUE + // short-circuit above and the super-lazy exit point below, + // 2026-10-15 (was already ahead of super-lazy only, since + // 2026-09-08; moved further to also cover the NNUE path). Their + // only inputs, uNonPawnMaterial, are already available this early. + // Side effect of this move: pos->uArmyScaler is now always fresh + // after Eval() returns, including on the NNUE path -- previously + // it was left stale (whatever a prior call last set) whenever + // Eval() took the NNUE short-circuit, since that return happened + // before this block ran at all. Nothing on the NNUE path + // previously depended on uArmyScaler being stale vs fresh, so this + // is strictly safer, not a behavior change worth gating. pos->uArmyScaler[BLACK] = pos->uNonPawnMaterial[BLACK] - VALUE_KING; pos->uArmyScaler[WHITE] = pos->uNonPawnMaterial[WHITE] - VALUE_KING; pos->uArmyScaler[BLACK] /= VALUE_PAWN; @@ -7076,6 +7236,143 @@ Return value: (SystemReadTimeStampCounter() - u64MaterialSetupTimer); #endif + if ((TRUE == g_fUseNnueEval) && (TRUE == g_fNnueWeightsLoaded)) + { + // p98 of |NnueForward() - NnueProbeScore()| per material + // bucket. Superseded 2026-10-15: originally derived from a + // 91M-sample population over three curated tactical suites + // (sd 10) -- replaced same day by a far larger and more + // representative 3.09-BILLION-sample population, 16 parallel + // shards of 12,500 positions each (200,000 total) from + // tests/cross_sectional_200k.ep_ at sd 6. Per-suite tactical + // positions are a narrow, biased slice (selected for sharp + // tactics); the cross-sectional pool's much broader position + // variety surfaced real outlier swings the narrower sample + // never saw (max up to 534cp here vs ~472cp before) while + // landing close to the same per-bucket numbers overall (mostly + // 0-10cp different) -- see docs/NNUE.md's "Lazy-evaluation + // tiering plan". Same bucketing as _MaterialBucket (combined + // army scaler, width BASE_MARGIN_MAT_WIDTH). + static const SCORE iNnueLazyMarginByMatBucket[BASE_MARGIN_MAT_BUCKETS] = + { + 180, 200, 190, 180, 170, 160, 150, 150, + }; + +#ifdef CALIBRATE_MARGIN_SAFETY + // Calibration build: ALWAYS run the real forward pass, never + // skip it -- so any candidate gate decision can keep being + // measured against ground truth before it's trusted live. + // When the gate is enabled, measure against the real chosen + // margin (making "exceeded" a meaningful, ongoing validation + // signal); when disabled, still measure against an + // effectively-infinite sentinel (pure observation, matches + // this harness's original pre-gate behavior). + SCORE iNnueScore = NnueForward(ctx); + if (TRUE == g_fNnueProbeLoaded) + { + SCORE iProbeScore = NnueProbeScore(ctx); + SCORE iSwing = (SCORE)abs((int)iNnueScore - (int)iProbeScore); + SCORE iMarginUsed = (TRUE == g_fUseNnueLazyProbe) ? + iNnueLazyMarginByMatBucket[_MaterialBucket(pos)] : + INFINITY_SCORE; + RecordNnueLazyMarginSafetySwing(pos, iSwing, iMarginUsed); + } + { + // Material-only "super lazy" measurement -- no fit, no + // probe, just the already-maintained material balance + // (same cp units as NnueForward's output, see + // RecordNnueSuperLazyMarginSafetySwing). No real margin + // chosen yet, so always the infinite sentinel for now. + SCORE iSuperLazyScore = pos->iMaterialBalance[pos->uToMove]; + SCORE iSuperLazySwing = + (SCORE)abs((int)iNnueScore - (int)iSuperLazyScore); + RecordNnueSuperLazyMarginSafetySwing(pos, iSuperLazySwing, INFINITY_SCORE); + } + // This branch always returns the real forward pass (never + // skips, by design -- see the big comment above), so it's + // unconditionally the NNUE_FULL tier for futility-margin + // selection purposes. + ctx->sPlyInfo[ctx->uPly].uLastPositionalSource = EVAL_POSITIONAL_SOURCE_NNUE_FULL; + g_u64NnueFullEvalsByBucket[_MaterialBucket(pos)]++; + if (NULL != piPositional) + { + (*piPositional)[WHITE] = 0; + (*piPositional)[BLACK] = 0; + } + return iNnueScore; +#else + // Production build: if the lazy probe gate is enabled and + // confidently resolves the fail-high/fail-low question + // relative to [iAlpha, iBeta], skip the real forward pass + // entirely -- this is the actual speed win. Otherwise (gate + // disabled, probe not loaded, or inconclusive), fall through + // to the full pass exactly as before this feature existed. + // Super-lazy pre-filter, experimental -- 2026-10-16: cheapest + // possible check, pure material balance, no net evaluation at + // all. Previously measurement-only (CALIBRATE_MARGIN_SAFETY + // above records the real swing but always used an infinite + // sentinel margin, i.e. never actually gated). Deliberately + // loose/uncalibrated here per the "safety doesn't win games" + // experiment -- g_iNnueSuperLazyMargin is a flat runtime + // override (see nnueeval.c), not a per-bucket fit. + if (TRUE == g_fUseNnueSuperLazy) + { + SCORE iSuperLazyScore = pos->iMaterialBalance[pos->uToMove]; + + if (((iSuperLazyScore - g_iNnueSuperLazyMargin) > iBeta) || + ((iSuperLazyScore + g_iNnueSuperLazyMargin) < iAlpha)) + { + // Feed the same aggregate counters root.c's "Eval exit + // breakdown" line reads -- otherwise that line stays + // 0/0/0 under NNUE eval since it was only ever wired + // to the HCE's own super-lazy/lazy/full exit sites. + INC(ctx->sCounters.tree.u64SuperLazyEvals); + ctx->sPlyInfo[ctx->uPly].uLastPositionalSource = EVAL_POSITIONAL_SOURCE_NNUE_LAZY; + g_u64NnueLazyEvalsByBucket[_MaterialBucket(pos)]++; + if (NULL != piPositional) + { + (*piPositional)[WHITE] = 0; + (*piPositional)[BLACK] = 0; + } + return iSuperLazyScore; + } + } + if ((TRUE == g_fUseNnueLazyProbe) && (TRUE == g_fNnueProbeLoaded)) + { + SCORE iProbeScore = NnueProbeScore(ctx); + SCORE iMargin = (g_iNnueLazyMarginOverride >= 0) ? + (SCORE)g_iNnueLazyMarginOverride : + iNnueLazyMarginByMatBucket[_MaterialBucket(pos)]; + + if (((iProbeScore - iMargin) > iBeta) || + ((iProbeScore + iMargin) < iAlpha)) + { + INC(ctx->sCounters.tree.u64LazyEvals); + ctx->sPlyInfo[ctx->uPly].uLastPositionalSource = EVAL_POSITIONAL_SOURCE_NNUE_LAZY; + g_u64NnueLazyEvalsByBucket[_MaterialBucket(pos)]++; + if (NULL != piPositional) + { + (*piPositional)[WHITE] = 0; + (*piPositional)[BLACK] = 0; + } + return iProbeScore; + } + } + { + SCORE iNnueScore = NnueForward(ctx); + INC(ctx->sCounters.tree.u64FullEvals); + ctx->sPlyInfo[ctx->uPly].uLastPositionalSource = EVAL_POSITIONAL_SOURCE_NNUE_FULL; + g_u64NnueFullEvalsByBucket[_MaterialBucket(pos)]++; + if (NULL != piPositional) + { + (*piPositional)[WHITE] = 0; + (*piPositional)[BLACK] = 0; + } + return iNnueScore; + } +#endif + } + #ifdef DUMP_RESIDUAL_SAMPLES // Super-lazy's OWN baseline -- material only, before _EvalPawns // even runs. Separate from iResidualBaselineWhite/Black below @@ -7178,7 +7475,7 @@ Return value: // 2026-09-17: no longer flat 0 -- same p50-based typical- // value estimate as regular lazy, using the super-lazy- // scoped residual table (material-only baseline). See - // iKingP50ByAttackerArmyOnly/iSuperLazyResidualP50By- + // iKingP50ByAttackerArmy/iSuperLazyResidualP50By- // OwnArmy (file-scope, above). ULONG uWhiteBucket = MINU(7, pos->uArmyScaler[WHITE] / 4); ULONG uBlackBucket = MINU(7, pos->uArmyScaler[BLACK] / 4); @@ -7520,7 +7817,7 @@ Return value: // terms (king safety + residual), matching full-eval's // own iPositional convention (that side's own real // positional score, not a relative-to-opponent - // quantity). See iKingP50ByAttackerArmyOnly/ + // quantity). See iKingP50ByAttackerArmy/ // iResidualP50ByOwnArmy (file-scope, above) for the // full percentile charts this was derived from. ULONG uWhiteBucket = MINU(7, pos->uArmyScaler[WHITE] / 4); @@ -7546,10 +7843,10 @@ Return value: iSavedLazyScore = iScoreForSideToMove; iSavedLazyPositional = iAlphaMargin; iSavedLazyPositionalWhite = - iKingP50ByAttackerArmyOnly[MINU(7, pos->uArmyScaler[BLACK] / 4)] + + iKingP50ByAttackerArmy[MINU(7, pos->uArmyScaler[BLACK] / 4)] + iResidualP50ByOwnArmy[MINU(7, pos->uArmyScaler[WHITE] / 4)]; iSavedLazyPositionalBlack = - iKingP50ByAttackerArmyOnly[MINU(7, pos->uArmyScaler[WHITE] / 4)] + + iKingP50ByAttackerArmy[MINU(7, pos->uArmyScaler[WHITE] / 4)] + iResidualP50ByOwnArmy[MINU(7, pos->uArmyScaler[BLACK] / 4)]; #else #ifdef EVAL_TIME @@ -7591,10 +7888,10 @@ Return value: iSavedLazyScore = iScoreForSideToMove; iSavedLazyPositional = iBetaMargin; iSavedLazyPositionalWhite = - iKingP50ByAttackerArmyOnly[MINU(7, pos->uArmyScaler[BLACK] / 4)] + + iKingP50ByAttackerArmy[MINU(7, pos->uArmyScaler[BLACK] / 4)] + iResidualP50ByOwnArmy[MINU(7, pos->uArmyScaler[WHITE] / 4)]; iSavedLazyPositionalBlack = - iKingP50ByAttackerArmyOnly[MINU(7, pos->uArmyScaler[WHITE] / 4)] + + iKingP50ByAttackerArmy[MINU(7, pos->uArmyScaler[WHITE] / 4)] + iResidualP50ByOwnArmy[MINU(7, pos->uArmyScaler[BLACK] / 4)]; #else #ifdef EVAL_TIME @@ -8127,10 +8424,10 @@ Return value: ULONG uWhiteBucket = MINU(7, pos->uArmyScaler[WHITE] / 4); ULONG uBlackBucket = MINU(7, pos->uArmyScaler[BLACK] / 4); (*piPositional)[WHITE] = - iKingP50ByAttackerArmyOnly[uBlackBucket] + + iKingP50ByAttackerArmy[uBlackBucket] + iSuperLazyResidualP50ByOwnArmy[uWhiteBucket]; (*piPositional)[BLACK] = - iKingP50ByAttackerArmyOnly[uWhiteBucket] + + iKingP50ByAttackerArmy[uWhiteBucket] + iSuperLazyResidualP50ByOwnArmy[uBlackBucket]; // CALIBRATE_MARGIN_SAFETY builds: the FULL tag set above // reflects the fallthrough that just ran for measurement |
