Summary
- Introduced a blitz-specific slow mover adjustment that conservatively reduces time usage for short controls with minimal increments, helping maintain search stability under fast time limits
- Documented the new blitz time heuristic in the README to inform users about its effect in rapid games
- Recorded the addition in the changelog for visibility in upcoming release notes.
Strategy sprt test
Perfect—here’s a step-by-step, one-patch-at-a-time plan for Codex.
Each step:
- adds one switchable change (UCI option defaults to off)
- gives exact file edits (minimal, copy-paste)
- includes a quick test you can run to compare before/after
Pre-condition for fair A/B (do this once before XP-1)
Build both BASE and DEV as x86-64-sse41-popcnt on Ivy Bridge.CMake (add to target):
-O3 -DNDEBUG -fno-exceptions -fno-rtti -msse4.1 -mpopcnt -mno-avx -mno-avx2 -mno-bmi2 -mtune=ivybridge
Makefile profile (ARCH=x86-64-sse41-popcnt):-msse4.1 -mpopcnt -mno-avx -mno-avx2 -mno-bmi2 -mtune=ivybridge
Experiment Harness (add once)
FILES: src/ucioption.cpp, src/search.h, src/search.cpp (or your nearest equivalents)
- Define a small config struct & globals
// search.h
struct XPToggles {
bool XP1_TimeClamp = false;
bool XP2_AspWide = false;
bool XP3_LMRCons = false;
bool XP4_NMPSoft = false;
bool XP5_LMPFutil = false;
bool XP6_OrderHist = false;
bool XP7_TTPolicy = false;
bool XP8_QSCap = false;
bool XP9_EvalSafe = false;
bool XP10_RootBias = false;
};
extern XPToggles XP;
- Declare options (default OFF) and mirror into
XPon init
// ucioption.cpp (near other options)
UCI::Option<bool> XP1_TimeClamp("XP1 TimeClamp", false);
UCI::Option<bool> XP2_AspWide("XP2 AspWide", false);
UCI::Option<bool> XP3_LMRCons("XP3 LMRCons", false);
UCI::Option<bool> XP4_NMPSoft("XP4 NMPSoft", false);
UCI::Option<bool> XP5_LMPFutil("XP5 LMPFutil", false);
UCI::Option<bool> XP6_OrderHist("XP6 OrderHist", false);
UCI::Option<bool> XP7_TTPolicy("XP7 TTPolicy", false);
UCI::Option<bool> XP8_QSCap("XP8 QSCap", false);
UCI::Option<bool> XP9_EvalSafe("XP9 EvalSafe", false);
UCI::Option<bool> XP10_RootBias("XP10 RootBias", false);
// after options are created (engine init):
XP.XP1_TimeClamp = XP1_TimeClamp;
XP.XP2_AspWide = XP2_AspWide;
XP.XP3_LMRCons = XP3_LMRCons;
XP.XP4_NMPSoft = XP4_NMPSoft;
XP.XP5_LMPFutil = XP5_LMPFutil;
XP.XP6_OrderHist = XP6_OrderHist;
XP.XP7_TTPolicy = XP7_TTPolicy;
XP.XP8_QSCap = XP8_QSCap;
XP.XP9_EvalSafe = XP9_EvalSafe;
XP.XP10_RootBias = XP10_RootBias;
XP-1 — Time clamp (fast-TC stabiliser)
Goal: Minimum think time + panic margin + adaptive overhead.
FILES: src/timeman.h, src/timeman.cpp
// timeman.h
struct TimeModel {
int64_t min_think_ms = 30;
int64_t move_overhead_ms = 20;
int64_t panic_margin_ms = 80;
};
extern TimeModel GTime;
// timeman.cpp (in compute_move_time or equivalent)
if (XP.XP1_TimeClamp) {
int64_t base = std::max<int64_t>(GTime.min_think_ms, alloc_for_this_move);
int64_t dyn_overhead = GTime.move_overhead_ms;
if (inc_ms < 200) dyn_overhead += 10;
if (nodes_last_move == 0 || bestmove_changed_often) dyn_overhead += 10;
int64_t budget = std::max<int64_t>(GTime.min_think_ms, base - dyn_overhead);
budget = std::min<int64_t>(budget, std::max<int64_t>(0, time_left_ms - GTime.panic_margin_ms));
return budget;
}
Quick test:fastchess ... -each tc=10+0.1 -games 300 -sprt elo0=0 elo1=6 "opt.XP1 TimeClamp"=true
XP-2 — Aspiration window predictable widening
FILE: src/search.cpp (root iteration)
if (XP.XP2_AspWide) {
int alpha = bestScore - 16, beta = bestScore + 16;
int fh = 0, fl = 0;
while (true) {
Score s = searchRoot(depth, alpha, beta);
if (s <= alpha) { alpha = s - (32 << std::min(++fl, 3)); continue; }
if (s >= beta ) { beta = s + (32 << std::min(++fh, 3)); continue; }
break;
}
} else {
// existing code path
}
Test: same as above; enable only XP2.
XP-3 — Conservative LMR schedule
FILE: src/search.cpp (or lmr.cpp)
inline int xp3_reduction(bool pv, bool cut, bool improving, int depth, int moveCount) {
int r = (moveCount > 3 && depth > 2) ? int(0.75 + log(moveCount)*log(depth)) : 0;
if (pv) r -= 1;
if (improving) r -= 1;
if (depth <= 6) r = std::max(0, r - 1);
return std::clamp(r, 0, depth - 1);
}
// use:
int r = XP.XP3_LMRCons ? xp3_reduction(pv, cut, improving, depth, moveCount)
: currentReduction(...);
// Also: guard non-reduction cases when XP3 is on:
if (XP.XP3_LMRCons) {
if (isTTMove || isCheck || isCapture || isEvasion || (firstQuietAfterTT && cutNode))
r = 0;
}
Test: enable XP3 only.
XP-4 — Softer Null-Move Pruning, zugzwang guard
FILE: src/search.cpp
if (XP.XP4_NMPSoft && !pv && !inCheck && depth >= 3 && !inZugzwangLike(pos)) {
int R = 3 + depth / 8;
Score margin = 80 + 8 * depth;
if (eval >= beta + margin) {
makeNullMove(pos);
Score s = -search(pos, depth - 1 - R, -beta, -beta + 1, NON_PV);
undoNullMove(pos);
if (s >= beta) return s;
}
}
Test: enable XP4 only.
XP-5 — LMP/Futility gates (SEE/history-aware)
FILE: src/search.cpp
// LMP for quiets
if (XP.XP5_LMPFutil && !pv && !inCheck && !isCapture(m) && !givesCheck(m)) {
if (depth <= 8 && moveCount > lmpLimit[depth] && see(m) < 0 && hist(m) < histCutoff)
continue;
}
// Futility
if (XP.XP5_LMPFutil && !pv && !inCheck && depth <= 7 && !isCapture(m) && !isPromotion(m)) {
Score margin = 120 + 80 * depth;
if (eval + margin <= alpha) continue;
}
Test: enable XP5 only.
XP-6 — Move ordering + bounded history updates
FILES: src/movepick.cpp, src/history.{h,cpp}
- Ordering when XP6 is ON: TT → good captures (SEE≥0) → killers(2) → countermove → quiets(hist/CMH) → bad captures.
- Bound history updates:
if (XP.XP6_OrderHist) {
// on cutoff for a quiet
int bonus = std::min(2000, 32 * depth * depth);
history[from][to] = std::clamp(history[from][to] + bonus, -32767, 32767);
// on quiet fail-low
int malus = std::min(2000, 16 * depth);
if (quiet_fail_low)
history[from][to] = std::clamp(history[from][to] - malus, -32767, 32767);
// killers: store only if quiet and != TT move
}
Test: XP6 only.
XP-7 — TT replacement: deeper/EXACT/newer preferred
FILES: src/tt.h, src/tt.cpp
// tt.h
struct TTEntry { Key key; int16_t score; uint8_t depth, bound, gen; Move best; };
extern uint8_t TTGen; // ++ at each root
// tt.cpp
static TTEntry* select_victim(Bucket& b, int depth, uint8_t bound, uint8_t gen) {
TTEntry* v = &b[0];
for (auto& e : b) {
bool empty = (e.key == 0);
bool older = (e.gen != gen);
bool shallower = (e.depth < v->depth);
bool preferExact = (v->bound != 0 && bound == 0);
if (empty || older || shallower || preferExact) v = &e;
}
return v;
}
void store(...) {
if (XP.XP7_TTPolicy) {
auto& bucket = table[index(k)];
TTEntry* repl = select_victim(bucket, depth, (uint8_t)b, TTGen);
*repl = {k, packScore(s), (uint8_t)std::min(depth,255), (uint8_t)b, m, TTGen};
} else {
// existing policy
}
}
Test: XP7 only.
XP-8 — QSearch caps (check-ext +1; safe delta)
FILE: src/search.cpp (qsearch)
if (XP.XP8_QSCap) {
// limit giving-check extension to +1 ply total
if (givesCheck(m)) extension = std::min(extension + 1, 1);
constexpr Score Delta = 925; // queen − small slack
bool keepPassedPush = isPassedPawnPush(m);
if (!keepPassedPush && eval + Delta <= alpha && see(m) < 0)
continue; // delta prune
}
Test: XP8 only.
XP-9 — Small eval de-risk (tempo +16; calmer passed pawns; KS fade)
FILES: src/evaluate.cpp, src/pawns.cpp
if (XP.XP9_EvalSafe) {
// tempo
score += SCORE(16, 16);
// passed pawns (where you compute their bonus):
if (blockedByMinorOrRook) bonus /= 2;
if (!supportedByPawn) bonus = bonus * 3 / 4;
// king safety scaling with phase (ensure you apply a 0..1 factor)
ks = scaleByGamePhase(ks, phase); // linear fade
contempt = 0; // unless user overrides via UCI
}
Test: XP9 only.
XP-10 — Root-only bias (experience/NNUE/book hints)
FILE: wherever bias is injected
if (XP.XP10_RootBias) {
if (nodeType != ROOT) bias = std::clamp(bias, -10, +10);
// or simply: if (nodeType != ROOT) bias = 0;
}
Test: XP10 only.
How to run each A/B (minimal)
For each XPi:
- Set all XP options to
false, measure vs BASE. - Toggle only that XPi to
true, re-measure vs BASE. - Log Elo delta and key stats (TT hit, FH rate, nodes/s).
Example command (blitz 10+0.1; 1t; 32MB):
fastchess -concurrency 8 \
-engine cmd=.\revolution_dev.exe name=DEV opt.Hash=32 opt.Threads=1 \
-engine cmd=.\revolution_base.exe name=BASE opt.Hash=32 opt.Threads=1 \
-each tc=10+0.1 proto=uci \
-openings file=UHO_2024_8mvs_+085_+094.pgn format=pgn order=sequential \
-games 300 -sprt elo0=0 elo1=6 \
-pgn out_XP{N}_blitz.pgn
Rapid 60+0.6 sanity (200–300 games):
fastchess -concurrency 8 \
-engine cmd=.\revolution_dev.exe name=DEV opt.Hash=32 opt.Threads=1 \
-engine cmd=.\revolution_base.exe name=BASE opt.Hash=32 opt.Threads=1 \
-each tc=60+0.6 proto=uci \
-openings file=UHO_2024_8mvs_+085_+094.pgn format=pgn order=sequential \
-games 250 -sprt elo0=0 elo1=4 \
-pgn out_XP{N}_rapid.pgn
Tip: keep a CSV log: XP name, on/off, Elo, LOS, TT hit, FH rate, nps. Promote only XPs that are non-negative at blitz and do not harm rapid.
Expected signs (rule-of-thumb)
- XP-1 TimeClamp: + at blitz, neutral/+ at rapid
- XP-2 AspWide: + stability, fewer re-search storms
- XP-3 LMRCons: + at both, esp. tactical stability
- XP-4 NMPSoft: + at tricky endgames; neutral otherwise
- XP-5 LMP/Futil: + at fast, neutral at slow if margins sane
- XP-6 Ordering/History bounds: + both; lower variance
- XP-7 TT Policy: + both; better reuse, fewer horizon traps
- XP-8 QSCap: + vs tactical blow-ups at fast
- XP-9 EvalSafe: small +/neutral; avoid fast-TC drift
- XP-10 RootBias: + if you had deep-node bias hurting pruning
Merge strategy after testing
- Keep all XPs OFF by default in
main. - Permanently enable only those with non-negative blitz and rapid deltas on your machine (Ivy Bridge).
- Leave the rest as optional UCI switches for future hardware.
