1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
|
# Move ordering experiment backlog
Goal: a leaner tree (fewer nodes for the same search quality) via move-ordering
changes, not eval or pruning changes. Methodology: per CLAUDE.md's "Dynamic
move ordering experiments" section -- measure fixed-`sd` node count /
first-move-cutoff-rate on the three curated suites (`ecm_ringers`,
`ecm_confident_quick`, `ecm_hard_quick`) as the primary signal, cross-checked
against a larger `cross_sectional_200k.ep_` sample at fixed `sn` as a tertiary
aggregate sanity check. Avoid adding throwaway per-node instrumentation code
to the tree; prefer compile-time diagnostic swaps (like the old
zero-selection-budget trick) that are never committed, or just the suite-level
before/after comparison.
Status key: TODO / IN PROGRESS / DONE (kept/discarded) / DROPPED (not pursuing)
## Queue
2. **TODO -- Qsearch check generation.** Crafty's `quiesce.c` calls
`GenerateChecks()` (movgen.c) to extend qsearch's first ply with
non-capture checking moves (direct + discovered, via attack-set
bitboard tricks), not just captures. Confirmed (per CLAUDE.md) our
QSearch is capture-only. This is a search-shape change more than a pure
ordering change -- re-confirm current QSearch behavior in search.c
before building, since CLAUDE.md notes may be stale.
5. **TODO -- Countermove fallback key (coarse, empty-slot-only).** Today
the countermove table is keyed by the *exact* previous move; if that
exact key's slots are empty/unevidenced, no countermove signal fires at
all for that node. Add a second, coarser table keyed by
`[piece-type][to-square]` of the previous move, consulted *only* when
the exact-match slots don't fire -- a genuine fallback, not a
competing signal. Different from the earlier "scaled continuation-
history" experiment (flat across a 256x range), which tested coarse
keying as an always-active competing signal, conflating key-coarseness
with "always on." This isolates key-coarseness alone, active only in
the exact-match's current blind spot.
## Dropped
6. **DROPPED -- Flee-to-safety revival.** `FLEE_BONUS`/the commented-out
`cEnprise`-match code in `movesup.c`'s `SelectBestWithHistory` (and
the matching `RETIRED` comment in `generate.c`) was dead -- retired
after the unconditional GOOD_MOVE-tier version it replaced was shown
(evidence-calibration, pre-dates this backlog) to perform at the
plain-leftover baseline for the vast majority of its population.
Reran with real evidence-bucket resolution (`MOVE_ORDERING_EVIDENCE_
COLLECTION`, reusing `CM_EVIDENCE_BUCKETS`/`_CMEvidenceBucket` --
same evidence scale as countermove, no butterfly-only workaround
needed since this legitimately reuses the ply-1-keyed continuation
table unlike move-pair/recapture): `evidence >= 10000` hit 56.32%/
62.84%/68.23% FH (ecm_ringers/ecm_confident_quick/ecm_hard_quick)
vs. a ~1.35-1.54% unprivileged-leftover baseline -- comparable to
or exceeding countermove's own calibrated bar. A real signal the
prior session's cruder zero/nonzero split never isolated.
Implemented as a `GOOD_MOVE`-tier escape at that threshold, same
shape as `COUNTERMOVE_TIER_BONUS`. Result: **no measurable effect
either way** -- 0.00%/-0.05%/0.00% node delta at sd10, zero solve
diffs on any of the three suites, sn10M depth/cutoff flat. Not a
magnitude-tuning problem like move-pair/recapture had -- the
qualifying population (en-prise origin AND evidence >= 10000) was
just too rare (2,444-9,209 tries across an entire suite run, vs.
countermove's tens-to-hundreds-of-thousands) for any mechanism
acting on it to move the aggregate needle. `u64FleeTries/FH` and
the scoring/precompute code were fully reverted.
4. **DROPPED -- Recapture bonus.** A capture landing on the same square
the opponent's last move landed on (`mv.cTo == mvLast.cTo`), nudged
within the winning/even-capture tier (`generate.c`'s `_ScoreAllMoves`,
`s >= 0` branch) rather than escaping it.
Built the comparison harness properly this time: `RECAPTURE_BUCKETS`
(2: SEE<0 losing, SEE>=0 even/winning) plus a parallel
`u64CaptureBaselineTries/FH` logging *every* capture (not just
recaptures) at the same SEE-sign buckets, so the recapture rate
could be read against an apples-to-apples baseline instead of in
isolation -- exactly the comparison move-pair's harness lacked at
first. Real signal: at SEE >= 0, recapture FH% ran 64-65% vs. a
49-52% all-captures baseline, consistent across all three curated
suites, on samples of 300K-4.4M tries.
Two magnitudes tried as a same-tier nudge, neither produced a
tree-size win:
- `RECAPTURE_BONUS = 50` (first guess, scaled like FLEE_BONUS's
old magnitude): -0.74%/+5.04%/-0.45% nodes (mixed), but a
**confirmed real tactical loss** -- ECM.735 (see `../tests/ECM.md`'s
Legit table: "HEAD's own line is clean and solid"), on
`ecm_hard_quick` (17->14/90 solved). Root cause, found by
actually computing the surrounding arithmetic rather than
guessing: the branch's real MVV/LVA differentiator --
`PIECE_VALUE_OVER_100(captured) - PIECE_VALUE_OVER_100(mover)`,
pawn=1/knight,bishop=3/rook=5/queen=9 -- only spans ~112-128
(a ~16-unit band) across *every* capture combination. 50 is
over 3x that whole band -- not a tiebreak, a near-dominant
override capable of vaulting a recapture ahead of an
objectively higher-MVV/LVA non-recapture capture at the same
node, which is exactly what cost ECM.735.
- `RECAPTURE_BONUS = 4` (rescaled inside the ~16-unit band):
fixed the regression (ECM.735 recovered, 17/90 again; the
hard_quick loss/gain pair that remained, ECM.491/ECM.443, are
both independently classified Flaky in `ECM.md` -- net real
change zero) but still no node-count win: +2.23%/+7.95%/-0.81%,
worse on `ecm_confident_quick` than the broken 50 value was.
sn10M depth-reached flat to within noise at both magnitudes
(14.76 vs. flat-history-only's 14.83), same as every move-pair
variant.
Conclusion: same shape as move-pair -- a real, well-calibrated FH%
edge that MVV/LVA's existing ordering apparently already captures
most of, with no tried mechanism converting the residual signal
into a leaner tree. `RECAPTURE_BUCKETS`/`u64RecaptureTries/FH`/
`u64CaptureBaselineTries/FH` and the scoring-branch code were fully
reverted; `MOVE_ORDERING_EVIDENCE_COLLECTION` infrastructure kept.
**Methodology note for next time**: cross-referencing actual solve
losses/gains against `../tests/ECM.md`'s per-position Legit/Flaky
classifications (rather than reading raw suite-level solve-count
deltas) is what separated "real tactical cost" (ECM.735) from "suite
noise" (ECM.491/443/496) here -- worth doing by default for any
future candidate's solve-count changes, not just when something
looks surprising.
3. **DROPPED -- Ply-2 move-pair table.** Crafty maintains a discrete
2-slot table (`move_pair`, history.c) keyed on the *side's own* move
two plies back (`curmv[ply-2]`), tried in `next.c` right after the
countermove phase. Implemented as `mvMovePair` (same shape/indexing
as `mvCounter`), evidence-gated same as countermove.
First attempt (borrowed countermove's `g_HistoryCounters +
g_ContinuationHistory` evidence sum verbatim) measured a complete
no-op: identical node counts to flat-history-only across all three
curated suites at sd10, despite the table itself populating and
predicting fine (55-70% raw hit rate). Root cause: `g_ContinuationHistory`
is only ever written keyed by the move *one* ply back -- there's no
table anywhere keyed by two-plies-back, so the borrowed slot read
unrelated noise. Fixed to use butterfly history (`g_HistoryCounters`)
alone.
Built a dedicated evidence-bucket harness (`MOVE_ORDERING_EVIDENCE_
COLLECTION` build flag, independent of `PERF_COUNTERS` per later
request; `MP_EVIDENCE_BUCKETS`/`_MPEvidenceBucket`, denser floors
than countermove's own since butterfly-only evidence is capped at
`HISTORY_MAX`) and calibrated a real threshold from it: top band
(evidence >= 6000) hit 53.41%/64.43%/66.23% FH (ecm_ringers/
ecm_confident_quick/ecm_hard_quick) on large samples (26K/177K/271K
tries) -- comparable to or exceeding countermove's own calibrated
bar. A real, strong, well-sampled signal.
Three mechanisms tried to act on that calibrated signal, none
produced a tree-size win at sd10 across the three curated suites:
- GOOD_MOVE-tier escape (same pattern as countermove's own
promotion): +1.6%/+1.4%/+1.5% nodes -- small but consistent
regression across all three suites.
- Same-tier nudge at FLEE_BONUS's old magnitude (100000, a
dominant override far exceeding the leftover pool's actual
range): +19.8%/+4.9%/+2.1% nodes -- worse than the tier escape.
A dominant override apparently inherits the escape's downside
(jumping ahead of comparably-good moves) without its upside
(EFP/pruning exemption).
- Proportionate same-tier nudge (2000, scaled to compete with
rather than override PSQT+history+continuation): mixed,
net flat-to-negative (+20.5%/+2.0%/-0.11%) -- `ecm_hard_quick`
went essentially neutral, but `ecm_ringers` (smallest, noisiest
suite) got *worse* than every other variant, suggesting it
reacts badly to this kind of reordering regardless of magnitude.
sn10M depth-reached on the 30-position cross-sectional sample
(`/tmp/typhoon/smoke30.ep_`) stayed flat to within noise (14.72-14.83
vs. flat-history-only's 14.83) across every variant.
Conclusion: `SelectBestWithHistory`'s existing PSQT+history+
continuation scoring apparently already captures most of what this
signal offers; no mechanism tried converts the calibrated FH%
advantage into fewer nodes. `mvMovePair` and its evidence-bucket
harness were fully reverted (table, update function, nudge/tier
code, bucket counters/print block) -- re-derive from git history if
revisited with a different approach. The general
`MOVE_ORDERING_EVIDENCE_COLLECTION` infrastructure (flag, `MOI_INC`
macro, countermove's own bucket harness) was kept -- reusable for
calibrating the next candidate (recapture, flee-to-safety revival).
## Done
1. **DONE (kept) -- Flat (non-depth-scaled) history increment.** Replaced
`g_HistoryCounters`/`g_ContinuationHistory`'s old `(depth+1)^2`-scaled
growth with Crafty's flat/saturating update (`HISTORY_MAX`/
`HISTORY_UPDATE_SHIFT`, chess.h; `_Increment`/`_DecrementMoveHistoryCounter`
and continuation counterparts, dynamic.c) -- depth-independent on both
growth and decay. Measured against a fresh `fbdcb9d` HEAD baseline
(`head_reference/`, refreshed same session):
- sd10, curated suites: node count -18.9% (`ecm_confident_quick`),
-6.6% (`ecm_hard_quick`), +18.9% (`ecm_ringers`, smallest/noisiest --
first-move cutoff rate barely moved there, 91.918% -> 91.914%, so
likely a re-search artifact not a real regression); first-move
cutoff rate up +0.18pp / +0.16pp on the two larger suites.
- sn10M, 30-position random sample from `cross_sectional_1.ep_`
(seed 42, `/tmp/typhoon/smoke30.ep_`): avg deepest iteration
14.52 -> 14.83 ply (+0.31), candidate reached equal-or-deeper depth
on 27/29 parsed positions, never shallower by more than 1 ply;
first-move cutoff rate 89.724% -> 89.967%.
Three independent signals (sd10 node count, sd10/sn10M cutoff rate,
sn10M depth-reached) all agree in direction. Kept, not yet committed.
## Already covered / not pursuing
- **History malus (decrement on moves tried before the fail-high move).**
Already implemented -- `UpdateDynamicMoveOrdering` (dynamic.c) calls
`_DecrementMoveHistoryCounter`/`_DecrementContinuationCounter` for every
quiet move considered before the one that beat alpha. Not a gap.
- Crafty's `movgen.c` itself (pure move generation -- captures, checks,
evasions, noncaptures, pin detection) contains no ordering/scoring logic;
the real ordering machinery is `next.c` (phase FSM) + `history.c` (killer/
countermove/move-pair/history updates).
## Protocol reminder
- One variable at a time, against a fixed reference build (see
`head_reference/` convention) -- never "working tree a few hours ago."
- `sd` (fixed depth), not `sn`/`st`, for "does this shrink the tree"
questions -- matches what all five ideas above are actually asking.
- Read node-count and first-move-cutoff-rate off the three curated suites,
not solve-count alone (too noisy at 90-ish positions per CLAUDE.md).
- `cross_sectional_200k.ep_` at fixed `sn` as a larger tertiary cross-check,
not the primary signal, and not on a 100-position sample (too small for
an aggregate depth-at-fixed-nodes metric) -- use at least a few thousand
positions if run.
|