The transposition table died on the cost of a 32-bit hash, but the number it
died on is the most useful thing anyone has measured about this engine's hot
path. The 16-bit hash already costs about 9% a node on a real C64, and
widening that one field cost a further 15.3%.
Hash maintenance is therefore the largest single identified cost in the 6502
node, it exists only to serve repetition detection, and reducing a cost is not
subject to the exchange-rate problem that sank the four pruning candidates.
Recorded as the first place section 3b should look, so a failed experiment
leaves a lead rather than only an epitaph.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On a C64 under VICE, widening the incremental key cost 16.3%, 20.8% and 14.4% at the three c64search budgets, 15.3% combined. Node counts matched exactly. Stop before table code or RAM policy; retain the shipping engine unchanged.
The note said the expensive half was already paid for, because a position hash
is maintained for repetition detection. It is - and it is 16 bits.
That is enough to scan a repetition ring, where a false match is rare and costs
a wrongly-scored draw. It is not a transposition key: 65,536 keys against a
60,000-node search collides constantly, and the index eats most of the key, so
a 1,024-entry table has about 6 bits left to verify with - roughly one accepted
false hit in 64 probes, each returning a score and a move from an unrelated
position.
So the first cost is widening the hash to 32 bits, paid in eng_Make and
eng_Unmake on every target including those with no room for a table, and the
repetition ring doubles too. Price that on a real 6502 before writing any
table code; it can kill the idea in an afternoon.
The second cost is the RAM, and at ~8 bytes an entry the Atari holds about 64
of them while the C64 holds a thousand. Sizing per target means seven targets
playing different chess, which is a portfolio decision and not a search one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The note's premise was mine and it was wrong. It argued that because strength
here is a node budget, a technique that reaches deeper within the same budget
is a direct win, and it set a 20% node pre-gate on the arithmetic of 60 Elo a
doubling. Null move saved 27% and 22% at the levels it targeted, cleared that
gate comfortably, and measured 0.04 sigma.
The error is the exchange rate. 60 Elo a doubling was measured by *granting*
the search more nodes, and those are spent at full fidelity. Nodes removed by
pruning are removed on an assumption that is sometimes wrong, so the depth they
buy is worth less. Saved nodes are not granted nodes, and only one of the two
has a measured rate against Elo.
The other three failures have a compatible cause: move ordering here is already
close to minimal at 2.28 moves a node before a cutoff, so there was very little
tree left for a pruning technique to take.
So the pre-gate stays, demoted to what it actually is - a floor for
measurability, which stopped three candidates for pennies - and the ordering of
the work changes. A transposition table returns a search that finished rather
than an assumption, which is the one remaining candidate this result does not
discount, and it is also the one that needs RAM. Node cost and then the
budgets are the lever with a measured rate, and that puts Very Easy back on the
table as a strength question rather than an ergonomic one.
The instruments and gates are kept as the agent left them, including
nodecompare.py and the Atari page price moved to the last gate. The standing
authorisations are marked spent: the unattended Sargon permission was a one-off
and the next task asks again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The conservative defended-capture test saved 12.77% at level 1 and 9.26% at level 3, with exact switch-off equivalence over 1,024 searches. One diagnostic dose that also skipped equal trades raised those cells only to 13.72% and 9.84%. The mechanism tops out below the roughly 20% gauntlet resolution threshold, so revert before target builds or matches.
Switch-off matched all 1,024 book searches exactly. Conservative middlegame-only delta pruning saved 1.57% at level 1 and 3.16% at level 3, while levels 2 and 4 consumed slightly more of their fixed budgets. The best cell is far below the roughly 20% needed for the gauntlet to resolve, so revert before target builds or matches.
Switch-off matched 2,048 UCI records across all four levels. PVS saved 5-6% at levels 3 and 4 but hurt the weak levels, exactly as the whole-book node screen predicted. That deep-level gain is roughly 5 Elo against an 18 Elo gauntlet resolution and is too small to justify the Atari page step. Record the page as a deferred portfolio price rather than a build failure, and add the mandatory 20% whole-book pre-gate.
The queen table closing pointed somewhere else: this engine is limited by
cycles rather than bytes, and search.c is missing classical techniques that
cost almost nothing in either. It already has iterative deepening, killers,
MVV-LVA and a well-ordered tree - about 2.28 moves a node before a cutoff -
which is what makes the missing ones worth having, since they pay off most when
ordering is already good.
The note names three in order of risk rather than size: PVS first because it
needs no gate at all, quiescence economies second because quiescence is most of
the tree at 400 nodes, and null-move last because it needs a gate and 5.1.6 is
the story of a gate that every desk instrument preferred and that lost the
match.
Each has its hazards written out where they are specific to this engine - the
2,700 spare in a 16-bit int above EVAL_INFINITY, the arena bail that is a
strength problem and not a crash, what a null move does to the repetition hash
- and each has a gate and an off-ramp. "Measured, rejected, recorded" is
listed as a successful outcome, because it is one and this repo has published
several.
Also records what these changes should *not* cost. Strength here is a node
budget, so a cheaper tree is deeper at the same budget and move times do not
move; the only real cost is the extra instructions per node, which has to be
priced on target rather than assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A 64-game screen at Sargon level 4 costs 4.3 hours, not the 5 to 11 the
estimate allowed. 16 games priced it: 3.79 seconds a Sargon move against a
4.79-second opening probe, so the probe is 0.79x - slightly pessimistic, as it
was at level 3. Every estimate in this file that has since been measured came
out high, twice by a lot, which is the safe direction and not an accurate one.
The probe is now good to about 20% through level 4 and half the answer at level
6, where the direction flips. Level 5 stays a range because it is unmeasured.
Those 16 games scored 6W 6L 4D with 14 distinct openings. That is recorded as
a cost measurement with the score attached, not as a result: sixteen games has
an interval of about +-25 points and this repo has a rule about quoting them.
It does make level 4 the candidate for the 64-game screen, and the 16 games are
the first 16 of it - the harness resumes at game 17.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Everything published against Sargon II was level 1, with one six-game look at
level 6. Levels 2-5 had never been played, so the competitive band for cc65's
upper levels was unmeasured and no match could be sized.
sargon/levelcost.py times a few opening replies per level: six minutes for all
seven. Level 3 is 1.5 seconds a Sargon move and level 6 is 91, so a 64-game
screen is 1.6 hours at level 3, 5 to 11 at level 4 and 93 at level 6. The wall
between an afternoon and a fortnight is between levels 4 and 6.
Two numbers here were wrong. "45 seconds per Sargon move" at level 6 was
seconds per *ply* - half those plies are cc65's, which answers in 8 ms - so it
is 91 a move, out by two in the direction that matters when sizing a match.
And "an early move is the expensive case" holds at the shallow levels and
inverts at level 6, where a whole game costs 1.9x its opening per move.
A one-factor model fitted the two known anchors within 4% across a range of 150
and then over-predicted level 3 by 87%. Three measured levels are in the table
and levels 4 and 5 are ranges, because two anchors and a line through them is
not a model.
The first screen off it - cc65 Harder vs level 3, 64 games, 1.6 hours - scores
73.4%, above the band, so level 3 is not the pairing. The numbers worth
reading are 27 distinct games in 64 against 16-17 before, worst repeat down
from fourteen to six, and one fifty-move draw where the baseline had fifteen.
The levels also refuse to sit on one scale: interpolation predicted 58%, and
reading back down from 73.4% puts level 6 somewhere cc65 Very Hard should have
scored 90% rather than the 41.7% it did.
And the queen table, recorded as the most promising untested lead in the
project, is closed. EVAL_QUEENHOME sets sc_pstQueen's d1 from -5 to +5, which
zeroes the ten centipawns the evaluation pays the queen to leave home; the
shipping binaries hash identically with it present. Over 6144 games a side it
measures -1.09 sigma, the wrong way. Tripling the incentive instead, which had
to be worse if the square mattered, measures +0.40 sigma. Both directions run
backwards: the square is inert and the mechanism is not where the queen's
opening cost comes from.
Replicating the measurement that motivated it on 64 fresh games then found the
statistic itself was fragile. Mate scores clamp at +-10000, and a plain mean
over evaluation deltas is not robust to that: a middlegame king move reads as
-289.3 against a true -23.5, and a pawn move reads as a *gain*. With the
clamps dropped the queen ties with the bishop at about twice a pawn, not four
times - and a developing bishop move is correct chess, which says this
statistic was never measuring what it was read as.
sargon/piececost.py is that computation with the clamps dropped and a median
printed beside every mean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The runbook says to supply your own copy and gives the size and SHA-256 to
check it against, but not where anyone might find one. This is the source the
results in doc/strength.md were produced from.
Written as provenance rather than as a recommendation, and pointed back at the
hash: what a given site serves under a given name changes over time, and the
hash is the only thing that says whether the harness will boot it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sargon/ was ignored in its entirety, which is right for the disk image and
wrong for everything beside it: match.py, the runbook and analyse.py have never
been in the repository, so the last two commits cited a harness that could not
be cloned, and doc/strength.md 5.1.7 and 5.2a reference flags nobody else could
run.
.gitignore now excludes sargon/*.dsk, *.nib and *.po instead of the directory.
Sargon II is a commercial program from 1978 and its image is nobody's here to
redistribute; the harness around it is ours.
The runbook said the image was "retained as the repository-local fixture" and
that "the external download is no longer needed", both of which stop being true
with this commit. Replaced with what someone actually needs: where to put their
own copy, the exact size and SHA-256 to check it against, that the commonly
circulated 153,394-byte Sargon.dsk is the same image with a 10,034-byte trailer
to trim, and that the protected .nib announces a false mate from the initial
position. match.py's not-found error now says the image is deliberately absent
and points at the runbook rather than just naming the missing path.
Two more stale claims in the runbook, both left over from the black table:
"cc65 itself has only four randomized first moves", and the paragraph saying
match.py reproduces the White table in Python because the adapter cannot reach
cpu.c. Neither has been true since fb1e81b.
analyse.py finds Stockfish on PATH or via $STOCKFISH rather than at a hardcoded
homebrew path, since it is now a file other people can run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six games, cc65 Very Hard against Sargon II level 6 - the deepest of its seven
settings, where everything else in doc/strength.md plays level 1. Three games
with each colour. Curiosity rather than a measurement, and written up because
some of what it found does not need a sample size.
cc65 2W 3L 1D (41.7%), 6 distinct openings
8.4 hours, 84 minutes a game, ~45s per Sargon move at max turbo
**Two existence claims, which six games do settle.** cc65 at Very Hard beats
Sargon at level 6. And Sargon at level 6 misses forced mates - three games,
twice in one of them, where cc65 walked into mate at move 30, was let off,
walked into another at move 38 and was let off again. Probably the same defect
class this project keeps finding in itself: at 45s a move emulated, level 6 on
period hardware was hours a move, so it is the setting Sargon's authors could
least afford to play and the least exercised code in the program.
**And one hypothesis, which they do not.** Two of the first three games turned
on an early queen sortie and the third, a win, never moved the queen. Counting
queen moves in each game's first twenty came out clean - losses 4, 4, 4; draw 2;
wins 0, 1 - with a mechanism to match: sc_pstQueen is a plain centralization
table, so d1 scores -5 and the middle squares +5 and the evaluation pays the
queen ten centipawns to leave home, against a search that reaches depth 5 or 6
where the punishment took four preparatory moves.
Measured properly it fell over, because correlating with the *result* is
worthless - a losing position invites queen moves as much as the reverse:
queen 25 moves -70.9 cp/move
rook 24 moves -134.4 cp/move twice as expensive
Except piece type is confounded with phase, rooks barely move before move 15,
and splitting on that puts the queen back where it matters:
moves 1-15 queen -85.5, rook -55.0, bishop -35.2, knight -22.1, pawn -20.1
moves 16-30 rook -145.8, knight -99.8, queen -52.3, bishop -48.2, pawn -48.2
Three statistics over one set of six games said "the queen", "not the queen,
the rook", and "the queen, but only in the opening". Only the last controls for
anything. Recorded as the most promising untested lead in the project rather
than acted on: fourteen opening queen moves is a small sample, and Stockfish
disliking an early queen sortie is nearly definitional, so its agreement is
weaker evidence than it looks. The fix would cost nothing if it works - the
table is 64 bytes already in RODATA and retuning it is free on every target.
Also here: cc65 is outplayed positionally in the games it *wins*. Its
evaluation over the first twenty moves falls by about the same amount whether it
goes on to win or lose, so these results came from Sargon's errors rather than
from cc65 playing better. Against level 1 it reaches move 13 ahead and loses it
later; against level 6 it is behind by move 13 in every game.
AGENTS.md gets the measurement trap as a general rule, since it caught the
author of the hypothesis: a per-game count correlates with the outcome for free,
and cost per move split by phase is the statistic that says something.
Note for anyone reading the references: sargon/ is gitignored in its entirety
(the .dsk is a commercial 1978 program), so match.py, its runbook and the new
analyse.py cited above are local-only and not in this repository.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit closed with "sixty points between colours is an
opening-book gap rather than first-move advantage... the rest of the deficit
is an opening problem, not an endgame one." That was a guess, it went out as
a conclusion, and sorting the games takes ten minutes.
**It was one lost game played fourteen times.** All eighteen black losses sit
in two openings; the fourteen 1.e4 Nc6 games are identical for all 103 plies,
mate included; in the other fourteen games black scores 57% and never loses.
Stockfish over the losing game says black is *winning* on move 13 at -181 and
throws it on move 17 - Sargon's Nxf7 is worth -139 to Sargon, unsound and
practically winning at 1,200 nodes. Played from book.epd where no book fires,
the engine scores 28.1% with white and 28.1% with black at level 1. There is
no colour asymmetry in the engine. The gap is a weighting: white's four-entry
table gave 32 games twelve openings, black's absence of one gave 32 games five.
**And nothing in this repository could reach the opening table at all.**
tests/uci calls search_Best and never cpu_Play; nothing in tests/ calls
search_SetSeed, which the table is gated behind; sargon/match.py had a copy of
the white table written out again in Python. The feature shipped for two
releases with no instrument able to execute it, and the rig was measuring a
replica of what the table was believed to hold. uci gets OwnBook and BookSeed,
both default off so every published game still reproduces, and match.py asks
the engine instead of its own copy.
Black now answers from five entries of two replies each, 30 bytes. Against
Sargon II, cc65 on black in every game:
games W- L- D score distinct worst repeat
no table 32 2W 18L 12D 25.0% 5 (16%) 14x
table, 1.e4 alt = d6 64 18W 34L 12D 37.5% 26 (41%) 12x
table, 1.e4 alt = e5 32 10W 13L 9D 45.3% 24 (75%) 5x
The distinct column is the result and the score is the by-product.
There are three rows because the first table rebuilt the defect it was meant to
remove: 1.e4 d6 played twenty-one games that were two games and lost all of
them. **Both desk proxies picked d6** - second of eight on score against
Stockfish, and *first* on an instrument written specifically to count distinct
games, 13 of 16. Variety against Sargon comes from Sargon's book and search
diverging and nothing here perturbs those. e5 was chosen on the only mechanism
the games showed - a reply that forces an exchange makes the opponent diverge -
and confirmed on the rig, because the proxies have now failed twice.
Left alone deliberately: 1.d4 Nf6 wins twelve of twelve as one repeated game,
and trading a 100% entry on a twice-failed proxy is not justified.
253 bytes. Atari 972 -> 716 free, crossing the DLIST page boundary for the
third time in this project; Apple II 1419 -> 1166. No node is dearer - the
table replaces a search rather than adding to one. Part IV ratings cannot
move: every rung is played from book.epd where neither table fires, OwnBook
defaults false, and match sanity reproduces 196-196-120 and 81% conversion to
the digit.
tests/opening.c drives the new table and names the moves it expects, because
its first version passed three deliberately broken tables: a dud entry does not
play an illegal move, it falls through to the search, and the search's reply is
a legal move the other seeds did not play - a broken half-entry looks exactly
like working variety. AGENTS.md gets that, the corrected memory budgets for
all seven targets (the atari and apple2 figures were stale), the plus4's
uncapped framebuffer, and the sub-second mtime that lets make skip a rebuild
and report a stale binary green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things: the full match result, an on-target benchmark that could
actually measure this term, and the number it produced.
**The match.** 64 games, cc65 Easy against Sargon II level 1, the same
settings as the recorded pre-fix baseline:
pre-fix shipped
W-L-D 10W 19L 35D 27W 20L 17D
score 42.97% 55.47%
fifty-move draws 15 2
conversion 6 of 23 26 of 28
drew still a piece up 15 0
cc65 as White 9W 3L 20D 25W 2L 5D
cc65 as Black 1W 16L 15D 2W 18L 12D
Fifteen games ended still holding a rook against a bare king, and now none.
Twenty White draws became five. Black does not move and should not: a term
that helps finish won positions does nothing for a side that is rarely
winning.
The percentage on its own is not significant - naive 95% intervals are
30.8-55.1% and 43.3-67.6% and they overlap, over 16 to 17 distinct games
rather than 64. The conversion count and the fifty-move column carry the
result because they are categorical outcomes tied to the mechanism. The
colour split is the more interesting number anyway: Sargon scores 75.0%
with White and cc65 now scores 85.9%, so on equal footing cc65 is ahead,
but sixty points between colours is an opening-book gap rather than
first-move advantage. cc65 has a book only as White and four entries deep.
The rest of the deficit is an opening problem, not an endgame one.
**The benchmark.** Neither existing on-target program could price this
term, for the same trap in two forms. c64search.c runs from the opening,
where gePhase is 6400 against the term's bound of 1100 - it cannot execute,
so that benchmark would have reported zero faithfully and uselessly.
c64evasion.c has the right shape, a fixed replay so both builds walk
identical positions, but the wrong positions: it replays a middlegame,
because check evasions cost nothing where nobody is in check. A gated term
needs a replay that spends its time inside the gate.
tests/c64drive.c is c64evasion.c's shape with the positions inverted. It
replays the pre-fix Sargon baseline game that reached king and rook against
a bare king - the defect itself - and searches only from ply 83, where
gePhase first falls to 1100, giving 148 timed searches with the term live
in about two thirds of them. eval.h gained a -DEVAL_MATEDRIVE_ON=0
override so both halves can be the shipping configuration, since
EVAL_TUNING makes every node dearer and a node's cost is what is being
measured - the same shape SEARCH_CHECK_EVASION already had.
**The number.** On a real C64 under VICE, identical positions in both
builds: 43.225 nodes/sec without the term and 42.361 with, so a node is
**2.04% dearer**. Check evasions cost 22.7% by the same method. That is
the cost where the term applies; in a middlegame it is zero, because the
phase test cannot be entered.
Charged at 2%, it still wins: match drive at equal time, 2,940 nodes
against 3,000, comes to 246-238-28 where equal nodes gave 245-240-27. That
makes it the second evaluation term in this project to survive the
equal-time test and the first to survive it comfortably; pawn structure
died there at +2.0 sigma becoming +0.6, and the endgame king table died
there twice.
AGENTS.md gains the line the last two phases earned: every defect that
mattered here was found by playing rather than by measuring, and the reason
is structural - self-play cannot see a weakness both sides share. Three
times now, and the most recent is this term's first version, which passed
every test in tests/ and scored better on the internal conversion metric
than the version that shipped.
Also recorded, because it cost twenty minutes: these benchmarks print their
results and then spin so the screen still holds them, so a run lasts until
-limitcycles fires rather than until the work is done. Size the limit from
the work - 148 level-2 searches need about 3.4e9 cycles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The term committed in 82582ff was measured only inside this repository, and
against an outside opponent it was a twenty-point regression. Played
against Sargon II at the settings of the recorded 64-game baseline, 32
games took 51.6% to 31.2%. The eight fifty-move draws went to zero exactly
as designed, and became losses.
The term was never the problem. The gate was. It fired inside
gePhase < PHASE_ENDGAME, and that constant is 3200 - two rooks and two
minors still on the board, which is a middlegame. "Walk the enemy king to
the corner" is not weak advice there, it is meaningless advice, and it was
being applied at every node of one. Diffing a game against its baseline
and asking both binaries about the position where they parted, at move 34:
pre-fix : score cp -463 bestmove Kc2
post-fix: score cp -561 bestmove b4
cc65 was a piece *down* there, at gePhase 1650. The |score| > 400 gate was
written to ask "am I winning"; it also answers "am I losing", and the term
fired for the opponent - correct in evaluation terms and disastrous in
practice.
Every instrument in tests/ missed this and it is worth recording why.
chesstest convert and the Stockfish endgame benchmark contain nothing but
bare-king endings, all below gePhase 1000, so neither ever exercised the
term outside the range it was designed for. match sanity and match drive
are self-play: both sides carry the term, the harm cancels, and self-play
conversion read 85% for the broken version against 81% for this one. The
best number in the repository belonged to the version that lost. Part VI
of strength.md and the AGENTS.md rule both already said a self-play number
is a reason to go and measure rather than a result; it still took an
outside opponent to apply them.
DRIVE_PHASE is 1100, and it sits just above what has actually been
measured rather than at a guess about what else might benefit. King and
rook is 500, bishop and knight 660, queen 900, two rooks 1000; the position
that caused the regression is 1650. Queen against a lone minor is 1220 and
is deliberately not covered, because nothing here has measured it and the
first version is what a gate set by reasoning rather than by measurement
costs.
Everything the term exists for survives the tightening and the interference
does not:
pre loose gate shipped
chesstest convert, levels 1-4 5/11/8/13 12/13/13/13 12/13/13/13
Stockfish, 100 endings, level 1 42 75 75
Stockfish, 100 endings, level 2 61 75 75
the move at the divergence -463 Kc2 -561 b4 -463 Kc2
self-play conversion 79% 85% 81%
Sargon II, 32 games 51.6% 31.2% 57.8%
Against Sargon by colour, which is where the mechanism is visible rather
than inferred: White goes from 7W 0L 9D to 14W 0L 2D - nine draws becoming
seven wins with the loss column still empty. Black moves the other way and
is noise; only four distinct Black games exist in 32 and it churns in both
directions.
Two cautions about that rig, recorded because they outlived the
measurement. Sargon is not reproducible: the harness seeds $4E before the
level is typed but the keyboard-wait counter keeps advancing, so its
opening book depends on host timing - the same seed gave 1.d4 Nf6 in one
run and 1.d4 d5 in the next with cc65's own moves identical, so no paired
replay is possible. And 64 games is 17 distinct games, four of them on the
Black side; the fifteen fifty-move draws in the pre-fix baseline were one
game played fifteen times. Percentages from it move in large steps, which
is why the colour split and the termination counts carry the argument.
Size is 325 bytes rather than 309; the Apple II keeps 1,419 spare at
optsize and all seven targets link. The superseded figures are kept in
strength.md and rework-log.md marked as such, because which of them
survived contact is the lesson.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Played against Sargon II on an Apple II, the engine scored 21% against
Sargon's level 1 - the second-weakest of seven settings a 1978 program
offers. The obvious reading is that the ratings are too generous. It is
not what the games say. Running the material balance over all twelve
games of that batch gives one fact that settles it: every single game the
engine drew, it was a clear piece or more up. Not one draw was a
balanced position. It reached a clean king and rook against a bare king
on move 66 of one game, still had it on move 115, and drew by the
fifty-move rule. In another it reached king and queen against king and
pawn on ply 129 and let the pawn promote. The whole margin was sitting
in endings it had already won.
The cause is sc_pstKingEnd. It sends a king to the centre once the
queens are off, which is right, and it is a per-piece table, so it says
that to the king being mated as well - which belongs on the edge. With
bare kings every rook move therefore scored alike, and the engine
wandered until the counter ran out. This is the Phase 8 lesson one layer
on: the endgame king table alone was nearly worthless because the pawns
had no reason to move, and both tables together are nearly worthless in a
bare-king ending because neither king has a reason to go anywhere.
mateDrive is the fix, and how it got in matters more than what it does.
It takes *both* kings, so it is not a property of a piece on a square and
eval_MoveDelta cannot carry it - the rule that has governed this file
since Phase 5, and the rule that killed two good terms in Phase 4. It is
affordable anyway because of where it runs rather than what it costs: it
sits inside the gePhase test that already existed for the endgame blend,
so it cannot execute in a middlegame at all, and the positions where it
does execute are ones the search walks at a few hundred nodes rather than
twenty thousand. The expensive place and the place this fires are
disjoint. That is the whole argument, and it is the first time here that
a term got in by being placed correctly rather than by being cheap.
AGENTS.md says so, because it must not be "corrected" into a running
total.
The weights are not the textbook ones. The literature weights corner
drive about three times king proximity; measured over the thirteen
endings now in tests/search.c, weighting them nearly equally is clearly
better, and the first attempt at tuning went the wrong way - halving
proximity on the theory that a chasing king was what the failing queen
endings looked like collapsed level 1 from 12 conversions to 7. The
cause is depth: at four plies the corner drive cannot pay off inside the
horizon, so the term that does has to be the large one.
Won endings finished before the fifty-move rule, thirteen positions, by
level: 5/11/8/13 before, 12/13/13/13 after. Level 3 scored below level 2
before the change, which is the disease stated plainly - searching deeper
into a flat evaluation finds more equally-scored ways to wander. Against
Stockfish defending 100 random won endings, level 1 goes from 42 to 75
and level 2 from 61 to 75; king and queen against a bare king from 18 of
25 at a mean of 51 plies to 25 of 25 at 17. On the 512-game self-play
match, conversion 79% to 85%, drew-still-a-piece-up 36 to 14, stalemate
draws 6 to 0.
It does not move W-L at all - 245-246-21 played against its own absence,
and only +12 games at level 1's budget. That is Phase 8's own point
coming back: W-L-D cannot see an engine that wins positions and does not
finish them, which is why the conversion metric exists and why
chesstest convert now asks for a mate rather than for a result.
The cost is below the host's noise floor: 1,500 searches of 60,000 nodes
from endgame positions, so the term firing at every node, measured 6.80s
without and 6.63s with, where the run-to-run spread is larger than that.
It has not been measured on a 6502 and that gap is left open honestly -
c64search.c and c64evasion.c both run from the opening, where this term
cannot fire by construction, so either would report zero faithfully and
uselessly. An endgame position has to go into the on-target benchmark
first.
309 bytes on the C64, Apple II, Oric and Plus/4, 302 on the X16, 256 on
the Atari. All seven targets still link at optsize; the Apple II is the
binding one and goes from 1,744 bytes spare to 1,435. The suite is
green, and its stated runtime was already stale - 35 seconds, not 21.
King, bishop and knight against a bare king is 0 of 25 either way. It
needs a table that knows which corner, and it is recorded so nobody
mistakes it for something this should have fixed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
strength.md had to estimate the one number that decided whether check
evasions were worth having, because VICE was not to hand. It is to hand,
and so is c64m, so the estimate is now a measurement - and it was low.
A node costs 22.7% more with evasions, not the 20% charged as a
conservative guess and not the 12.2% this host reports. The host
understated the target by 1.86x, against the 1.64x the position hash
showed in Phase 5. Three times now this project has been told that a
cost measured on a desktop is not the cost, and it has been wrong in the
same direction every time.
Getting there took three attempts and only the last is a measurement.
Two match runs that had played different games said 30%. The same work
with the flag on and off, on the host, said 12.2%. The target says 22.7%
- from tests/c64evasion.c, which replays a *fixed* game so that both
builds walk identical positions rather than diverging at the first
disagreement and then timing different work. That file exists because
the fixed-game property is the whole measurement; without it this is the
30% mistake again with extra steps.
Pricing it needed the shipping configuration built twice, since
-DEVAL_TUNING makes every node dearer and the cost of a node is what is
being measured. search.h's macro now honours a -D override for exactly
that, which costs the shipped build nothing.
Recharged at 22.7%, the equal-time table changes in one cell that
matters: levels 2, 3 and 4 gain +82, +49 and +31, and **level 1 comes out
a wash** - +15 at one rung, -20 at the other. At the weakest setting the
depth given up is worth about what the evasions buy. That is the honest
result and the docs now say it, along with why it is not an argument for
reverting: what level 1 gets is 54 of 60 mates in one at the reduced
budget against 27 before, and no rating table measures whether an engine
can finish a game it has already won.
The same rig turned up something the documentation had quietly stopped
being true about. Level 1 takes 13.2 seconds a move on a stock C64, not
the 8.2 recorded when the budgets were set - the position hash, the
endgame tables and check evasions together cost about 60%. The time
column in the README and the executive summary is updated: 13 seconds, 46
seconds, ~11 minutes, ~45 minutes. The last two are computed from the
measured node rate rather than measured directly, because a single level
4 move is now three quarters of an hour, and they are labelled as upper
bounds.
measuring.md gains the c64m recipe, because four things about it had to
be learned the hard way: load-prg injects but does not start, paste-text
cannot carry a RETURN on a line-based protocol, turbo 3 disables painting
so there is no screen to read, and the finish condition has to be the
program's own output rather than a fixed sleep - the two halves of an A/B
take different amounts of wall time, which is the point of running it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported by somebody playing the game on an Apple II: it blunders
checkmate, it does not see mate in one on easy or very easy. True, and
the largest defect in the project. Sixty mate-in-one positions taken
from random games and verified by playing the move: level 1 solved 27 of
them, level 2 solved 54. Levels 3 and 4 solved all sixty.
The first job was to disprove that this session caused it. The same
probe against the commit before any of today's work gives 27 and 54 to
the digit, so it has been there the whole time.
negamax at depth 0 returns straight into quiescence, before generating
anything, so "no legal move and in check" is never tested. And
quiescence stands pat on a static evaluation and looks only at captures,
both of which are illegal when the side to move is in check: there is no
declining to move, and the move that escapes may be quiet. So a mating
move looked like a quiet move and mate in one was invisible to a depth 1
search - and level 1's 400 nodes rarely reach depth 2. Sorting level 1
by the deepest iteration it finished settles it: of the positions where
depth 2 completed it found 23 of 23, and where only depth 1 completed, 4
of 36.
Quiescence now generates evasions rather than captures when in check,
does not stand pat, and returns a mate score when nothing is legal. The
abort check on that return is load bearing: without it a search that ran
out of budget part way through the evasions would report "no legal move",
which reads as checkmate. Running out of budget must degrade to a worse
move, never to a wrong claim about the position.
27 of 60 becomes 55, and 54 becomes 58. At equal nodes the ladder moves
+44, +82, +47 and +63 down the four levels, but equal nodes is the test
this project distrusts. A node costs 12.2% more, measured over identical
work - the first estimate came from comparing two matches that had played
different games and said 30%, which is not a measurement of anything.
Charged at 20% rather than 12.2%, because the position hash cost 5.5%
here and 9% on a real C64 and the target is not the host, it still wins
at every level: +28, +102, +51, +32. And the defect stays fixed under
the cut - 54 of 60 at -20%, 50 of 60 at -30%.
132 bytes. The Atari crossed its DLIST page boundary for the second time
today and pays 256 for it: 1228 free, with 128 bytes of padding left.
The ladder and the anchor are re-run, since this changes the engine.
Level 3 now draws level with Stockfish at one node - where level 4 stood
a version ago - and level 4 scores 71% there. The price of a node still
has not moved: the first-column spread was 443, then 443, now 441, across
three strength changes.
Two reasons no test caught this, and both are now in AGENTS.md. The
tactics suite searched every position with 60000 nodes, the level 4
budget, so nothing had ever asked the weak levels a question at the
budget they play with. And self-play cannot see a defect both sides
share - the harness reported "ok, balanced" throughout, exactly as it did
for the repetition defect. tests/search.c gains matein1, which runs at
each level's own budget, with generated and verified positions rather
than invented ones; the first draft used hand-written mates and two of
them were not mates. Level 1's floor is 10 of 12 rather than 12 because
400 nodes cannot always finish depth 1 in a sharp position. Turning the
fix off drops it to 5 and 11.
Not measured: the 12.2% is a host figure. VICE is not installed here so
c64search.c never ran, and the real per-node cost on a C64 is unknown.
strength.md says so rather than implying the 20% is anything but an
estimate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the randomiser on real hardware is what showed up its limit. On
the Apple II every game opened with a knight - a different knight, so the
feature worked, but still a knight. Counting on the host: two distinct
first moves, across all 255 seeds, at every skill level. Scoring all
twenty says why - b1c3 and g1f3 tie at 50 and e4 and d4 are ten
centipawns behind - and a randomiser confined to exact ties can never
reach the third one.
The reflex was to widen it to "within N centipawns", and that was wrong
twice. It would have conceded ten centipawns to reach e4, which treats
a 400-node evaluation as the authority on opening moves when it plainly
is not; and it is not a constant to change anyway, because negamax is
fail-hard and at the root an equal move and a worse one both return
exactly alpha, so near-ties cannot be told apart without re-searching
with a lowered window.
So the first move is now four hand-picked moves in cpu.c - e4, d4, Nf3,
c4 - rolled with the LFSR that was already there. It fires only when
the engine has White and nothing has been played, which undo_CanUndo
already answers, so there is no new state. Black's first move is a
reply and a fixed reply is not sound against an arbitrary opening; that
would need a real book keyed on what White played.
It lives in the game layer rather than the search on purpose. cpu_Play
is called by no harness - every test and the UCI adapter drive
search_Best directly - so the measurement apparatus is untouched by
construction rather than by a flag anyone has to set. The level 4 rung
still reads 248-129-135 to the digit.
And it measures as an improvement rather than a cost. Each first move
got its own 256-position opening set, same generator and seeds as
book.epd and differing only in the forced move, played 512 games at
level 4 against Stockfish at 100 nodes: e4 +1, Nf3 -7, d4 -12, c4 -19,
and b1c3 - the move the engine picked for itself - last at -29. All
four table moves beat it. The intervals overlap heavily so no pair is
significant alone, but the ordering is opening theory's. Worth about
20 Elo, and free at the board: no search on move one is fifteen minutes
saved at level 4 on a stock C64.
The roll is looked up in the generator's output rather than trusted,
which earned itself immediately. The first draft encoded the pawns from
the wrong end - tile 0 is a8, so tile 12 is e7 and not e2 - putting
black's pawns in three of four entries. The effect was not an illegal
move but a silent fallback to searching, with the engine playing its two
knights exactly as before. tests/opening.c had been written to check
the table plays four different legal moves and failed with "got 2,
wanted 4" on the first run.
Verified on the Atari under AltirraSDL's bridge, eighteen cold boots,
all four openings seen.
302 bytes. The Atari crossed the DLIST page-alignment boundary doing
it, so it paid 256 where the Apple II paid 302 - 1484 and 1876 free
respectively, and four bytes of that padding are left. AGENTS.md says
so, because the free-space number on its own no longer predicts what the
next change will cost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It played a knight to c3 from the start position. Every game, on every
machine, since 2014. The cause was never mysterious: the evaluation
rates several opening moves exactly equal and the root loop keeps a move
only when it beats the incumbent, so ties fell to whatever the generator
emitted first - the order of two nested loops over the board, and not
anything to do with chess.
The fix leans on the one thing alpha-beta promises about move ordering:
it does not change the answer. Order the moves any way you like and the
search returns a move with the same score. The single exception is
exactly the case above, where equal scores are decided by which move was
tried first - so perturbing the ordering score at the root varies the
opening and cannot make the engine prefer a worse move. That is a
structural property rather than a statistical one, which is why this
landed without a 512-game match behind it: there was nothing to lose and
so nothing to measure.
+0..3 rather than a shuffle, because ordering is what makes alpha-beta
cut and randomising it wholesale would cost nodes for nothing. It stops
after five of the engine's own moves - its moves, not plies, so the
promise is the same whichever colour it has - which keeps it out of the
endgames the piece-square tables were built for.
plat_GetSeed is the first addition to plat.h since it was frozen, and it
clears the bar that freeze exists to set: three lines a port, a read of a
counter that is already running, and a failure mode that looks like the
behaviour being fixed rather than like a crash. Every machine had one
and cc65's own asminc names all of them. Three of the six recipes
needed correcting - the SID readback is $D41B and not $D400, the Plus/4's
jiffy clock is at $A3 and not the C64's $A0, and the Apple II has no
clock at all but does have a counter cc65's cgetc increments while it
waits for a key, which holds how long the human took and is better
entropy than a clock. Whether those IRQs are running needed no
emulator: cgetc spins waiting for one to fill the key buffer, so there
is no way to be asking the question if it is not.
The seed is read once a game and after the menu rather than at
board_Init, because on a cold boot the menu is the only thing that has
happened that the machine could not have predicted. A zero seed is
honoured rather than replaced - it is the LFSR's dead state, so it costs
no special case, and it means "play this game the way the old engine
would". That is what the terminal build asks for.
Determinism for the harness needed no flag in the end. search_SetSeed is
called by main.c, which is not in the test build, so every figure in
strength.md was measured by an engine sitting in that dead state. The
ladder rung that anchors the claim reproduces to the digit across the
change: 248-129-135 before and after.
188 bytes of code and 2 of BSS. The Atari's free space fell by two
rather than 188, because DLIST is page-aligned and the padding in front
of it swallowed the rest - 50 bytes of that padding are left, and the
byte that exhausts them costs 256 at once. Recorded in AGENTS.md,
because it makes "there is room" a misleading thing to say about that
target.
tests/opening.c checks the two things that could go wrong quietly: that
different seeds really do produce different openings, and that a seeded
search and an unseeded one always agree on the score even when they
disagree on the move. Breaking the feature on purpose to confirm the
test noticed is how the next one turned up - tests/Makefile did not list
the engine headers as prerequisites, so make test reported green from a
binary that never saw the change. Headers are prerequisites now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Stockfish ladder was two strength changes out of date. It is now
current, and every one of the sixteen rungs improved: the endgame tables
are worth +94, +77, +65 and +52 down the four levels, monotone in how
little the engine searches, which is what knowledge substituting for
search looks like. Level 4 beats Stockfish at one node by 82 points
where it used to draw level, and the dead heat has moved two rungs along
to a hundred. c-chess-cli was built for the cross-check and agrees.
Two things did not move. The price of a node - the first-column spread
was 443 points before both strength changes and is 443 after, so 61 Elo
a doubling still holds and the ladder simply lifted. And the arithmetic
in 4.2, which now predicts all three level gaps to within a few points.
Self-play was wrong again, in the other direction. It overstated
repetition detection about threefold and understated the endgame tables
by half, so there is no correction factor, only an outside opponent.
That is now in AGENTS.md, because it is the trap most likely to be
walked into again.
The anchor disagrees with all of it, and re-running it properly turned
up why - along with two harness defects that had been sitting inside
published ratings. A clock-ignoring engine was losing games on time,
because fastchess was handed a 30ms limit and a level 4 move is 15ms
here, so host load alone forfeited four games; the limit is 5000ms now
and the node-limited ladder reproduces to the digit across the change.
And the anchor's answer depends on which rung it is read at, by 144
points at level 4: a 200-point step in UCI_Elo buys 56 points of played
strength up there, because Stockfish's limiter has run out of ways to be
weak that a four-second clock does not already impose. Each level now
plays the two rungs nearest its own strength. Level 4 against Stockfish
rated 1700 came out 122-121-13, which is as well-placed an anchor point
as this is going to get, and the +-150 that used to be an assertion is
now a measurement.
What is left is a genuine disagreement rather than a defect: the ladder
says every level gained 60 to 80 points and the rated anchor says
approximately nothing. Some of that is a saturated rung, and some of it
may be real - a change that is pure endgame knowledge is worth more
against an opponent that does not search endgames than against one that
does. So the claim in the document is the narrow one, and the way to
settle it is a rated rung near 50% measured before the next term lands
rather than after.
The levels 1 and 2 hypothesis from 5.1.2 is tested rather than argued.
make uci-tuning builds the UCI adapter with the tuning switches exposed
as UCI options, and gauntlet.py grew --uci-option to drive them; with
everything on it reproduces the shipped binary's games to the digit,
which is what makes a difference attributable. Repetition detection off
minus on is +15 at level 1 and +1 at level 2. The mechanism is real -
every rung's draw count rises with detection off and the losses fall to
match - but at level 2 the extra draws come out of the wins instead, and
+15 is a sixth of what the endgame tables gave the same level. Not a
case for removing anything; it retires an open question.
5.3 used to say no game was ever lost on time. That was true when it
was written and stopped being true without anyone noticing, so it now
says what happened and why the flag in gauntlet.py is the only reason it
was caught.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Stockfish ladder was re-run after repetition detection and the
result never got recorded. strength.md 5.1.2 now has it: the sixteen
rungs, and the finding that self-play overstated the gain about
threefold - +38 claimed at equal time, +9 to +16 delivered against an
opponent that does not share the defect. Levels 1 and 2 came out
slightly worse, concentrated against opponents far stronger than the
engine; the mechanism is written down as a hypothesis, not a finding.
measuring.md documents the conversion metric, since W-L-D cannot see an
engine that scores level and still cannot finish won games, and that is
how the largest defect here went unnoticed through a whole tuning phase.
rework-log.md gets the next work with its numbers attached: the ladder
is two strength changes out of date, levels 1 and 2 regressed on the one
re-run, and opening randomisation is parked with room now available on
both tight targets - blocked on the seed rather than the space.
Uniform optsize is recorded as a decision rather than a circumstance, in
AGENTS.md and in Decisions on record. A port built differently is a
port that behaves differently, and since the endgame tables it is also
the only setting the Atari fits in.
The README's build section is instructions again rather than
archaeology, and checking it turned up three things that were wrong: a
bare make builds every target and not the c64, the image list was
missing po and d64 so the Apple II disk was undocumented, and the
combined form needs "all" because image rules cannot build the binaries
they consume.
AGENTS.md also gains what the B display actually is - four deliberate
numbers a square, not three plus a debug leftover - because that was
called wrong today and cost an afternoon.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The engine could not finish won endings, and the reason was not the king.
The middlegame pawn table pays 50 for a pawn on the seventh and 5 for one
at home, so marching a pawn the length of the board earned 45 centipawns
- nine a move - against the 800 that promoting is worth from beyond the
horizon. With no gradient to climb, pushing and shuffling scored the
same, so it shuffled.
Reinstating the deferred endgame king table on its own measured +15 Elo
at 1.55 sigma and moved conversion not at all: 69% before, 69% after.
The king had somewhere to go and the pawns still had no reason to move.
With a steep endgame pawn table beside it: +44 Elo at equal nodes and
+30 at equal time over 1024 games, and conversion from 69% to 78% from
openings, 87% to 90% from endgame positions. Games running out the
240-ply limit halved. The two are worth three times what one was.
Carries the difference rather than a second score: geEvalScore stays the
middlegame total everything already reads, and geEvalEnd holds only what
the endgame tables would add - zero for every piece but pawns and kings.
One extra delta per move made, shift-weighted at eval time, and an
immediate exit while the queens are on. The fuzzer checks both new
totals against a full recount like the other two.
1079 bytes and 9% slower per move on a real C64, measured with identical
node counts from the opening where the blend never fires - the honest
figure, since that overhead is paid everywhere including where the term
does nothing. Atari keeps 1742 bytes under its framebuffer, Apple II
2366 in MAIN.
First evaluation term in this project to survive the equal-time test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
W-L-D does not contain the failure that matters: an engine can score
level and still turn won endings into draws. So the harness now counts
conversion - of the sides that were a clear piece up for ten plies or
more, how many actually won - and splits the failures three ways, since
they want different fixes: drew still a piece up, drew after handing the
material back, or lost outright.
Material is the yardstick rather than the engine's own score, because
the evaluation is what is under test. An engine that rates its position
+2.55 when Stockfish says +9.8 cannot referee its own conversion.
Baseline: 69% from openings (62 drew still a piece up, 38 gave it back,
20 lost) and 87% from endgame positions (26/8/20).
Also retires a comparison that measured nothing. match endgame pitted
EVAL_ALL against EVAL_MATERIAL|EVAL_PST, and those have been the same
mask since the two deferred terms were removed - a configuration playing
itself, reporting a perfect 234-234-44 that looked like a result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every piece rendered as its opponent: white pieces solid black, black
pieces hollow. Two bitmaps exist per piece - 0 hollow, 1 solid - and
which is wanted depends on the square as well as the piece, because rop
already flips polarity on a light square. A white piece is an outline
on white and a solid on black; a black piece is the reverse. That is
blackWhite ^ isWhite, and the code XORed with the negation.
Verified headless under a2m-v2 against the starting position: black
pieces read dark, white pieces read light, on both square colours.
The title screen was already right and is untouched - it draws on black
with ROP_CPY, where hollow is the black king and solid the white one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hiresAtari.s puts the GR.8 framebuffer at a hard-coded $9100 and the
linker was never told, so ld65 placed BSS inside it and reported a
build that fits. It did not fit. At optsize the static end was 1751
bytes below the screen before repetition detection and 1069 bytes
inside it after, which is a band of BSS drawn as pixels across the top
of the title screen and engine state overwritten by whatever is drawn.
At optspeed it was already 553 bytes inside before any of this work.
That is the real reason this target "needed optsize" - not the extra
kilobyte the README claimed, just an end address below $9100.
MAIN now stops at $9100, so an overrun is a link error. The start
address drops from $2000 to $1000 to pay for the cap: MEMLO is $0700
under MyPicoDOS but $0800 crashes on load, and $1000 leaves 2771 bytes
clear at optsize and 723 at optspeed, so both settings are safe.
Verified under AltirraSDL's bridge: MyPicoDOS boot, menus, an AI-vs-AI
game, clean board, PC in CODE throughout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cx16 fix made three statements false at once: the README's "every
target except cx16 builds", the Phase 6 item waiting on a Windows
machine to build atari and cx16, and engine.md's size line counting
five buildable targets.
Also measures what Phase 5 left as bookkeeping - segment totals for all
seven targets at both optimisation settings, against the recorded c64
baseline of 25883 at optsize - and corrects Phase 6's claim that the
apple2 has never been run, which stopped being true when the relocated
BSS was verified under a2m-v2.
Three boxes remain: the two deferred evaluation terms, the play-test
checklist, and the final pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Newer cc65 renamed the software stack pointer, so the inline assembly
in platCX16.c wanted (c_sp) where it said (sp). Builds and links clean
now, repetition detection included; running it still needs the Windows
machine, and AGENTS.md says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last known absence with a price attached: 62% of self-play games
drawn, all by repetition, 57% of those in positions the engine itself
scored as winning.
A 16-bit key carried by eng_Make and eng_Unmake, built as a sibling of
eval_MoveDelta - same cases, same rule that a delta reads no board
state, so the pair cannot disagree. History is a 128-entry ring pushed
by make and popped by unmake, which holds the real game and the current
search line in one array; undo, redo and the UCI replay maintain it for
free. Castling rights are folded in when a position is compared,
because a king that steps off e1 and back leaves the same pieces on the
same squares and is not the same position. negamax scores one repeat
as a draw; board_ApplyMove applies the threefold.
+44 Elo at equal nodes, +38 at equal time, 512 games, 3.7 sigma.
Self-play draws 53% to 32%. Costs 9% on a real C64 - the host said
5.5%, which would have credited the change with points it had not
earned. RODATA +1584, CODE +1038, BSS +260.
The match harness had been counting threefold draws as games that ran
out the ply limit, so it could not see its own headline result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was the tightest target in the tree by an order of magnitude - 460
bytes between the top of BSS and the stack, against 8438 on the Atari -
and the reason was layout rather than appetite. The program starts at
$4000 because HGR page 1 is at $2000-$3FFF, stranding six kilobytes at
$0800-$1FFF below the graphics page for the whole run.
BSS moves down there and the file image keeps all of $4000 up to the
stack, which gives 2128 bytes below HGR and 3877 in MAIN. Verified
under a2m-v2: boots, plays an AI-vs-AI game, draws the B visualizer,
and a write watchpoint over the unused tail saw no writes in 150
seconds of play.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Using the Brutal Deluxe Cadius 1.4.6 does understand the #xxyyyy file name type extension so just use cadius as the program name. On windows, cadius-145 can just be called cadius if 1.4.6. for Windows doesn't work the same.
Its job in a repo like this is navigation and honest framing, not
documentation. So: what the program is, that it was written with AI
assistance, how strong it actually plays, how to build and play it, and one
line on each of the six documents saying who it is for.
Carries forward the parts of readme.txt that are still true - the keys, the
cursor colours, the build targets and the porting notes - and corrects the
parts that are not. The undo stack holds 127 moves rather than 254, and the
B/A/D displays are no longer "a visual representation of the Attack DB",
because there is no attack database any more; the feature is unchanged and
only the explanation was stale.
Every link checked, and the terminal build line verified to compile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The failure is more instructive than the fix, and the original is not bad
code - which is what makes it worth reading. Two structural decisions made
it weak, both of them traps that are well known inside computer chess and
close to invisible from outside it:
The attack database became the legality oracle. It is the right structure
for the attacker/defender display it was built for, and reusing it to
answer "is my king attacked" is the natural next thought - but it meant
testing one move for legality cost a full 64-square regeneration, and
legality is the innermost loop of the whole program.
The evaluation scored move desirability rather than position value. Each
scoring function is individually sensible; the quantity is not a property
of the position, so it cannot be negated for the opponent, so accumulating
it across plies does not describe anything. This is the one that mattered
most: the database made it slow, but this made depth not help.
Also covers what followed from those two - mate detection as special-case
machinery mutating a shared structure, promotion decided during make rather
than encoded in the move - and a positional-initialiser hazard in
gPieceValues where the comments run in a different order from the enum, so
the king and queen values landed swapped.
Leads with what the original got right, because it is the reason the rework
could port the movement rules rather than rederive them: the generator was
perft-exact to depth 5 on two reference positions, with one divergence, and
that divergence was a design choice rather than a bug.
Every excerpt and figure checked against the source at v1-original-engine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
doc/strength.md deliberately describes its method without naming a single
script, so that it does not date. This is the other half: what the
instruments are, how to run them, and which one answers which question.
Structured around the point that they do not substitute for each other. A
change can pass the suite, win at equal node counts, and still lose on real
hardware because every node got slower - which is not hypothetical, it is how
two evaluation terms came to be removed after being accepted.
Includes the workflow for making a change, the three rules the match harness
exists to enforce, and a section on reproducing the published figures from a
clean clone.
One correction while writing it: the profile quoted in doc/rework-log.md was
taken with a c64profile.c that is not in the tree - it needed a modified
engine as well as a driver, so it was done on a scratch fork and lost with it.
The document now says so and describes the technique instead of pointing at a
file that does not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gauntlet needed two absolute paths on the command line, which made every
recorded invocation specific to one machine. It now looks in tools/, then on
PATH, then in the usual package-manager prefixes, and --which reports what it
found. Engine and book paths resolve relative to the script rather than to
the shell, so the whole thing runs from any directory.
tools/ is a gitignored drop zone with a tracked README explaining what belongs
in it. The tools are fetched, never vendored: they are large, platform
specific and under their own licences. The README records the one build
gotcha worth knowing, that c-chess-cli's make.py passes an x86-only flag and
fails on Apple Silicon.
Versions of both the runner and the opponent are now printed with the
results. A figure is only reproducible against the opponent that produced
it, and a different major Stockfish will move every number in
doc/strength.md.
Also: the runner is started in tests/. fastchess drops a resume file full of
absolute paths into its working directory, and it was landing in the repo
root. Disabling autosave does not stop it, and -config is a *load* directive
that clobbers the engine setup if you try to use it to redirect the write -
so the working directory is the lever, which is free to use now that every
path handed to the runner is absolute.
Verified against both runners: same games, same result, 8-14-10 either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three moves, no content rewritten:
AGENTS.md at the repo root now holds the durable constraints - the frozen
plat.h interface and the four globals every port also reads, the cc65 code
style, and the traps that have already caught someone (16-bit int width, the
256-byte local limit, arena capacity, perft costs not transferring to the
search, the opening being the optimistic case to measure from). These were
buried in the rework journal's sections 2 and 5, where nothing would find
them. AGENTS.md is the conventional place for instructions to a coding
agent, which is what they are.
doc/rework-log.md keeps everything else and gains a preamble saying what it
is: a lab notebook, published unsanitised, whose value is the measurements
that demolished the plan around them rather than the plan.
readme.txt becomes doc/readme-2014.txt, byte-identical. It describes the
original program and is kept as a historical document rather than corrected
- the parts of it that are still true belong in a new README, and the parts
that are not are part of the history.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>