Repository navigation
feat(eval): Count the enemy pieces bearing on the king's ring, at zero weight - #258
Conversation
…o weight A fourth leaf term, in a file of its own beside the other three. For each side, how many squares of the other king's ring its knights, its bishops, its rooks and its queens attack, one count per kind and summed over the pieces of that kind. The ring is the eight squares a king attacks from where it stands, which is five on an edge and three in a corner. The square the king stands on is not in it: no quiet position carries an attack on that square, so no fit could ever price a column holding one. The attack set is the one mobility already walks, over the real occupancy, with nothing taken out of it. That is where the two terms part company. Mobility drops this side's own squares and the squares an enemy pawn covers, because a square a piece cannot go to is not scope. An attack is the other question, and a rook bears on the pawn in front of the king whether or not a bishop guards it. So a slider stops at the first piece of either colour, counts that square if the ring holds it, and sees nothing past it. A ring square two pieces attack is counted twice, once per attacker, since how many pieces bear on the king is what the count is after. Pawns and kings carry no column of their own: the storm already reads the enemy pawns on the three ranks in front of the king, and a king bearing on the other king's ring is the opposition. All eight weights are zero. They are what the fit moves, and the counts are here now so that a corpus extracted from this build carries their coefficients whatever the weights beside them say. The alternative, a nominal weight to make the term visible, would move the tree, and the claim of this commit is that it does not. At zero weight the leaf does not take the counts at all. `SCORED` is read off the weights at compile time, the way mobility's `SCORED_KINDS` is, and the summand in `sum` stands behind it, so nothing in the evaluation walks the pieces while every weight is zero. The fit turns the term on by replacing the weights, with no line in the sum to edit. That skip is not the compiler's doing, which is worth saying because the first draft of this commit said it was. Multiplying a count by zero leaves the walk that produced it standing and llvm keeps it. Built with the summand unconditional, the same 3,868,987 node tree ran 3.2% fewer nodes per second on the mean of 21 interleaved rounds and 4.0% fewer on the best of them. Behind the constant the tree runs at the speed of the commit below this, on the same measurement: -0.1% on the median of 21 interleaved rounds and +0.2% on the fastest of them, against a spread of 5.6%. Both are well inside it, so what the run says is that nothing was added to the leaf and not that something was taken off it. The term is not cached. What it reads is the whole occupancy and one side's pieces, which is the position, so the only key it could sit behind is the one the mobility cache was measured on. That measurement: 29.21% of evaluation calls repeat inside a search and 18.43% of them hit a direct mapped table of 16,384 slots, the table that was built took 1.59% off the instructions and put about 1% on the clock, and it was rejected on the memory traffic. A second table under the same key pays that latency again for a fifth of the work. The shelter's cache earns its place because pawns and kings move on a minority of plies, and nothing about this term has that property. Two tests had to move. `every_term_writes_the_counts_its_width_claims` asserted that every term is worth something at one end of the taper, which a term shipping at zero is not. It now asserts which term that is, so the fit fails there until the line goes with it. `the_table_names_the_terms_the_sum_adds` reads this term's fold against weights of its own, since the shipped fold is nothing wherever it is read. That test holds the table and the sum together for a priced term. An unpriced one stands outside it until the fit, which the test now says. `scripts/tune.py` prices the four columns at 2, 3, 4 and 6. Two and four are exact: at most two of a knight's eight targets fall in the three by three block, and a rook on d3 against a king on e4 reaches four of the ring. A bishop reaches two, since a diagonal through the block runs into the king's square and stops there, and the best queen squares reach five, so those two columns are given a square in hand. `--hold-king-attack` is new, so that a later refit of a term below this one moves that term alone. The counts are pinned by fifteen hand counts, the size of the ring on every square, a mirrored reading for black, a fold against trial weights, and three rows in the tuner's own tests. Three of the hand counts are what a mutation of the walk gets caught by and nothing else: two rooks reading one rank, which a union per kind would answer three to, and a pawn and a king standing against the ring, which carry no column. `SCORED` has a test of its own that it is false exactly when every weight is zero at both ends of the taper. At zero weight the reconstruction identity sees nothing of this term, so those are the whole of what guards it. The bench is identical by construction, and that is the claim. A term worth nothing moves no score and so moves no tree, and at zero weight it is not counted either. The eighteen default pins, the eighteen reference ones, the tactical pass count and the strategic point total all stand where they were. Bench: 3868987 Elo: not measured Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Review round 1, folded into 7cdceae. A second agent rebuilt both sides in its own build directory: 3868987 on both, all eighteen bench rows identical in every column but time, 607 to 613 release tests at the time, clippy and fmt clean. It re-derived all fourteen hand counts from the definition before reading the commit's numbers and every one agreed. It ran nine mutations of the count against the suite. Findings, one behavioural on the clock and the rest guards:
Every one of the nine mutations is caught after the fold, and a non-zero weight moves the bench to 3937024 and flips SCORED. Airgap and trailers checked. |
Speed against
|
The rest of king safety, first half: the count. For each side, four counts of how many squares of the enemy king's ring (the eight squares a king attacks from its square) its knights, bishops, rooks and queens attack, over the real occupancy. A slider stops at the first piece of either colour and counts that square if it is in the ring. A square two pieces attack counts twice. Pawns and kings carry no column: the storm already reads the pawns, and a king bearing on the other king's ring is the opposition. Folded white less black against four packed weight pairs through the existing taper, the way mobility folds.
All four weights are zero in this commit, so the bench is 3868987 and every pin is unchanged. The count is not folded away by the compiler at zero weight: measured, it cost 3.2% (mean) to 4.0% (best) of nps against a build with the summand removed, over an identical tree. So the summand sits behind a SCORED const derived from the weights, mobility's pattern, and the fit commit turns it on with no edit. speed.sh reads -0.1% median, +0.2% fastest against master over 21 interleaved rounds at a 5.6% spread.
The term is uncached. It reads every piece of one side, so its key is the position, and the mobility cost record measured that cache at 1.59% fewer instructions and about 1% slower on the clock.
Fourteen hand counts pin the definition (one per kind, a friendly and an enemy blocker, two attackers of different kinds and two of one kind, an attacker inside the ring, a pinned attacker, a corner king, a centre king on a defended square, a pawn and a king that must not count, two bare kings), plus the ring's size on every square, a mirrored reading, a fold test, and two tuner tests. The tuner lays the four columns out from the TERMS table, prints them in the layout line, and scripts/tune.py prices them at 2, 3, 4 and 6 with a hold flag.
Release tests 607 to 614, python 215 to 218. The fit, on a fresh sealed group, is the next commit.
🤖 Generated with Claude Code