Improving Kaggriculture Bot
A bot that wins 97% of its practice games, 48% of its real ones, and scores a third of what the leader does. This is a study of four ways a measurement can lie, one of which took in the first version of this article.
Key points
- The score is 1117 at its best. The leaderboard leader scores 3193. That figure appeared nowhere in the repository until now, and its absence shaped roughly twenty experiments run against a target nobody had located.
- Self-play says 97.5%. The ladder says 47.8% across 542 ranked episodes. The benchmark is saturated — every candidate scores 79–97% against the recorded field, so it cannot rank anything.
- Win rate cannot measure strength on this ladder, at any sample size. Submissions spanning 427 rating points all sit within a few points of 50%, because rating-paired matchmaking finds every build opponents at its own level. Win rate says whether a build has converged, not how good it is.
- Every instrument compared the build to itself. Self-play, the payoff matrices, the clone-deficit tools — all of them measure the Go build against the Go build, or against opponents replayed through its own executor. Nothing ever ran an external reference, which is how a 370-point regression survived an entire rewrite.
- The rewrite lost to the agent it replaced, 16 games to nil. $98,490 against $57,285 through the real interpreter. It has been reverted, and the score is climbing back through 928.8.
- ~1100 looks like the ceiling of hand-tuned rules here. Two agents of different architecture both stalled near it. The largest carefully measured effect in a full day of work was worth about 0.5%; the gap to the frontier is 2000 points.
The score, against the rest of the board
This is the number that reframes everything below it. The project spent its effort on questions of the form "is this change better than that change", all of which were answerable, and never asked "how far away is the top", which was answerable too — the leaderboard is a public page. A 2,000-point gap is not reachable by tuning: every positive result this project has ever recorded, stacked together, does not approach it.
The roadmap
In priority order, with the reasoning for each in the sections that follow. The first two are about not being lied to again; the rest are about the only gap that now matters.
| # | what | why now | status |
|---|---|---|---|
| 1 | Gate every build against an external reference before shipping | The 370-point regression was invisible to every internal instrument. tools/vs_predecessor.py runs the real interpreter at ~6s an episode — slow and authoritative, not a sweep tool. | built |
| 2 | Stop ranking on head-to-head win rate, in self-play as well as on the ladder | It is equalised by matchmaking on the ladder and by twin-vs-twin dynamics in the mirror. One knob read −71.7% against a clone and neutral against a fixed opponent. Rank on absolute output. | partly |
| 3 | Treat "tune the policy constants" as closed | Two architectures both stalled near 1100. The largest held-out effect measured is +444 ± 313. This line of work is finished, not unlucky. | decided |
| 4 | Spend the unused compute: ~100 full rollouts per turn sit idle | actTimeout is 1s; the executor uses 4.5ms and the engine simulates a whole episode in 8ms. Both agents evaluate a hand-written expression once and commit. This is the one asset the 1117 agent cannot replicate. | blocked |
| 5 | Find why one swapped assignment costs 90% of a season | The rollout searcher has real ordering signal and still collapses the bank from $70,284 to $6,867 at K=2. Until this is understood, no rollout tuning means anything. | open |
| 6 | Port the predecessor's thesis, not this executor's | Livestock income on a ratcheting curve, CARE as the multiplier. Its advantage is its policy, though — the largest macro lever moves 1% of the $40,011 gap — so this means porting the agent, not tuning knobs. | costed, not started |
1. The game
Kaggriculture is a two-player simulation competition. Each episode runs 30 days of 24 turns, 720 steps in total. You control a farmer plus any hands you hire; every unit takes one action per turn. Both farms sell into the same market, so the price of wheat depends on how much wheat the pair of you have already sold.
Three rules do most of the strategic work.
Only banked cash counts. Produce sitting in the shed, in a hand's inventory, or on the ground at the final step is worth zero. So is anything produced on the last night: the reward is captured at step 718 and the day-29 refresh would run at step 719, which never happens.
Labour is a recurring cost, not a capital one. Every hand is
dismissed overnight and the roster is re-hired each morning, where the
n-th hire of a day costs fib(n). This is the most commonly
mis-modelled number on the board, and Section 9 shows it setting the price of
every capacity decision the bot makes.
The market has a hard appetite, and it is smaller than it looks. Shops unlock on a schedule, drawn independently with replacement, so a whole product can go a season with no buyer at all.
Two entries decide a lot. Wool has a standard deviation nearly equal to its mean, because its only shop is drawn in just 65% of seasons — a sheep-heavy plan is a bet on the draw, and one season in three the bet is simply lost. Melon's entire season appetite is 30 units, all of it from the town centre.
kaggle-environments 1.32.4 and 1.32.6, and
town-centre demand fell from 140 units per product per season to 30 — a 79% cut
to the only demand melon ever sees. Any inherited number without a version stamp
is unusable.2. What the bot is
The competition ships its engine as Python. Running one mirror episode takes about five seconds, which caps how much you can measure. The first thing this project did was port that engine to Go, bit for bit, including CPython's Mersenne Twister and Python's half-to-even rounding. One episode now takes about half a millisecond.
That is a factor of ten thousand, and it changes what questions are affordable. A 40-seed experiment bank becomes a 4,000-seed bank. The port only keeps its value while it stays exact, so a differential test compares the whole state — every tile, both sheds, the insertion order of every inventory — against the real interpreter after every step, and a second test replays real ranked episodes and requires the banks to match to the dollar.
On top of the engine sits the design decision that shapes everything after it: the plan and the player are separate objects.
- A macro is the season-long commitment schedule — when to buy land, how many hands, which crops on which days, how many animals, what to sell and above what price. A few dozen numbers.
- The executor turns any macro into per-turn actions, and it is identical for every macro.
The payoff is interpretability: when macro A beats macro B, the difference is a difference in plan and nothing else. The cost is that every measurement is taken through one executor. Sections 7 and 8 are the bill for that; Section 8 is also where it finally paid.
3. A tour of the code
The arguments above are all about measurement, which makes it easy to forget there is a machine underneath. Seven places carry most of the design, and each one is load-bearing for a reason that is written down beside it.
The random number generator, ported bit for bit
The Go engine is only useful while it is identical to the Python one, and the only nondeterminism in an episode is one generator per day. So the project reimplements the exact subset of CPython's Mersenne Twister the engine touches.
// The engine's only nondeterminism is one generator per day:
//
// rng = random.Random((seed * 1_000_003) ^ day)
//
// which drives `rng.random()` once per empty tile (weed spawn, both farms) and
// then `rng.choice(sorted(remaining))` for the shop unlock. Reproducing those
// draws exactly is what lets the Go engine be diffed against the Python one
// instead of merely resembling it.
That last clause is the whole justification for the rewrite. "Resembling" would have been worthless: a benchmark that diverges from the real engine produces confident numbers about a game nobody is playing.
The type that makes cheating impossible
In self-play it is trivially easy to read state a real agent could never see, and the resulting numbers evaporate on the ladder. Rather than writing that down as a rule, the project makes it unrepresentable.
// Obs is exactly what a policy is allowed to see, and the type exists to make
// cheating impossible rather than merely discouraged.
//
// `farms` is shared — both players' tiles, money, unit positions, unlocked
// quadrants and hire counts are public. `private` is not: the opponent's shed,
// seeds and per-unit inventories are hidden. A self-play policy that reads the
// rival's shed would produce research numbers that evaporate on the ladder, so
// Obs simply does not carry it.
type Obs struct {
Farms *[2]Farm // public
Private *Private // this player's only
Market *Market
Town *Town
}
This is the cheapest possible version of a discipline that is otherwise very hard to maintain. Nothing has to remember the rule, because the field is not there to read.
One rounding mode, worth a whole dollar
Python's round() is half-to-even, not half-away-from-zero. Using
Go's math.Round would differ by exactly one dollar on every exact
.5, and one dollar of divergence in a price compounds into two
engines playing different games.
// math.RoundToEven and not math.Round — the difference is a whole dollar on
// every exact .5 and would desynchronise the two engines.
v := int(math.RoundToEven(price))
The differential test checks all 36,009 reachable price quotes, which is how a detail this small stays fixed.
The plan as data
The macro/executor split described in Section 2 is enforced by making a plan a plain serialisable struct — a few dozen numbers with no behaviour of its own.
// A macro is small (a few dozen numbers), serialisable, hashable and
// mutable — which is what makes recording, best-response search and an
// equilibrium solve possible at all.
// SchemaVersion is bumped whenever a field changes meaning. A payoff matrix
// computed under one version cannot be compared with one computed under
// another, and results files record it so that mistake is catchable.
const SchemaVersion = 1
Every downstream capability follows from that shape. You can record an opponent's plan out of a replay because it is data; you can mutate it because it is small; you can put 41 of them in a matrix because they are hashable. The version stamp exists because the project has already been burned by comparing numbers produced under different rules.
The executor's constants, and what they cost to learn
The micro layer is held identical across every macro, which is what makes a matrix cell mean anything. The constants live in a struct rather than as consts for a specific reason, and the comments record what each one bought.
// They live in a struct rather than as consts for one reason: without it there
// is no way to A/B an executor change against the same seed-paired machinery
// that judges macros, and executor changes were being eyeballed on three mirror
// seeds.
// DistDamp is the offset in value/(DistDamp+steps). Scaling the numerator
// lifts near and far candidates alike and changes nobody's mind; the
// denominator is what re-ranks.
//
// It was 2.0 and is now 0.5, worth 96% of 200 held-out episodes and
// +$9,767. The farm is job-rich and action-poor — every job type sat at
// 15-25% claimed, and 2,250 WATER jobs were claimed against 590 executed —
// so what pays is not choosing *better* jobs but choosing *nearer* ones.
DistDamp float64
That comment is the entire thesis of Section 10 in miniature, written a month before the ledger confirmed it. Scaling a job's value changes no decisions; changing how distance discounts it changes all of them. Five later attempts to make job values more accurate all lost, and this is why.
The scoring expression itself is three lines:
d := abs(job.pos.X-pos.X) + abs(job.pos.Y-pos.Y)
score := job.value / (p.o.DistDamp + float64(d))
switch {
case d == 0:
score *= p.o.StayBonus
case p.hasTgt[u] && p.targets[u] == job.pos:
score *= p.o.CommitBonus
}
The change that actually moved the ceiling
For most of the project's life those scores were consumed by a greedy: each unit takes its own best job in turn. Replacing it with a real min-cost assignment is the one change that raised the absolute bank.
// Neither of those is a min-cost assignment. Greedy on sorted pairs is at best a
// 2-approximation and its failure mode is specific: it hands the best job to the
// unit that happens to score highest on it, stranding that unit's *other*
// near-optimal jobs with nobody to do them. The optimum has to be solved, not
// approximated, and at 15 units it is cheap to solve exactly.
//
// This is the rectangular assignment problem ... Jonker-Volgenant
// shortest-augmenting-path, which for 15 units against ~200 jobs is a few tens
// of microseconds — affordable inside a 1-second turn budget that currently
// spends 0.5 ms.
func hungarian(cost [][]float64, nRows, nCols int) []int {
Two things are worth noticing. The first is the cost argument: solving it exactly takes microseconds against a one-second budget, so the greedy was never buying anything. The second is where the idea came from — the predecessor agent's own public submission description, which names its core as a min-cost assignment. Reading a competitor's prose was worth more than five rounds of search.
The doctrine, written into the arena
Package-level comments in this codebase carry the reasoning, not just the description. The arena opens by naming the one choice everything else depends on.
// The single most important choice in this file: **rank on win rate, not on
// mean margin.** The competition's win condition is binary — winning by $1
// scores the same as winning by $100,000 — and matchmaking is rating-paired, so
// the objective is the rating at which drift is zero, which is P(margin>0)=0.5,
// which is median margin = 0. Win rate *is* the estimator of median margin.
// When the win column and the mean margin disagree, the mean is the artefact.
The reasoning is correct and the conclusion is now half-retired. Win rate is the right estimator of median margin against a fixed opponent, which is what the arena measures. It is not a measure of strength against a rating-paired field, which is what the ladder is, and Section 7 shows 427 rating points collapsing into no win-rate separation at all. The same sentence is right in one place and wrong in the other, which is a hard thing to notice when it is written down once and inherited everywhere.
The project has since scoped it explicitly. CLAUDE.md now carries
the identity with a rider: it "governs matchmaking equilibrium, and it
says nothing about absolute strength. A build converges to ~50% against opponents
at its own level ... only the score can" say where you sit.
The five rules each bought by a dead submission
The Kaggle entry point is 189 lines of shim around a static Go binary, and its docstring is a list of failures.
1. **Never raise.** An unhandled fault marks the seat ERROR and the agent is
never called again. Every turn is wrapped; the fallback is PASS.
2. **No module-level `__file__`.** Kaggle runs `exec(compile(src, path, "exec"),
{})` in a bare namespace, so `__file__` is undefined. A submission was
rejected at load for this.
3. **Never `stderr=sys.stderr`.** Kaggle wraps each turn in
`redirect_stderr(StringIO())`, and StringIO has no `fileno()`. A submission
ran 719 turns of PASS because of this and banked exactly startingMoney.
4. **Spawn at import, not on turn 0.** Startup is ~0.8s against a 1s actTimeout.
Paying it inside the first turn threw the opening turn away (858ms -> 1ms).
5. **The fallback shares no code with the engine path.** `_fallback` is
deliberately dumb and independent; a fallback built on the machinery that just
failed is not a fallback.
None of this is strategy and all of it cost real submissions. Rule 3 is the one worth staring at: the agent loaded, answered all 719 turns, reported a healthy status, and banked exactly its starting money. A clean status line is not success.
One operator per mutation
The search deliberately moves one axis at a time.
// One operator per call, deliberately. A mutation that changes five things at
// once is untraceable: when it wins you cannot say which change won, and when
// it loses you have condemned four innocent changes with it. The search is
// cheap enough (about a millisecond an episode) that it can afford to move one
// axis at a time.
The reasoning is sound and it has a blind spot, which Section 11 argues is now the binding one: an interaction where neither half wins alone cannot be sampled by a search that only ever moves one knob. Roster and throughput are exactly that shape — hands lose money at the current steps-per-action, and throughput gains are worth little without more hands to spend them.
4. How self-play works here
Pairing removes the map, the shop draw and the weather from the comparison for free. Playing both seatings removes the small asymmetry that comes from weed spawning resolving for player 0 first. The two seatings of one seed are correlated, so the seed-pair rather than the episode is treated as the unit of independence when computing error bars.
Run every macro in a library against every other and you get a payoff matrix. The project reads three different answers off it, deliberately:
| answer | what it means | how it misleads |
|---|---|---|
| field | mean win rate against the whole library | six near-copies of one popular notebook count six times |
| worst | the maximin row — the best worst-case plan | pessimistic if no opponent actually plays your worst matchup |
| nash | the unexploitable mixture, and its support | says the search converged, never that the plan is safe |
Statistical power is taken seriously, and had to be. At 40 seeds (80 episodes per cell) the standard error is ±5.6%, which cannot separate two macros six points apart. It did not: the equilibrium picked the weaker plan and flipped when the same matrix was re-run at 120 seeds.
5. What self-play said
Ten solves are now stored in the repository. The best macro's win rate against the recorded field climbs from 84.9% to 93.9%, then falls back as the field is re-recorded against a leaderboard that has moved.
| solve | macros | episodes / cell | 95% CI | best field win | Nash support | exploitability |
|---|---|---|---|---|---|---|
| matrix v1 | 34 | 128 | ±8.7% | 84.9% | 1 macro | 0.00% |
| matrix v2 | 36 | 160 | ±7.7% | 87.9% | 1 macro | 0.00% |
| matrix v3 | 38 | 160 | ±7.7% | 87.4% | 1 macro | 0.00% |
| matrix v4 | 40 | 160 | ±7.7% | 92.8% | 1 macro | 0.00% |
| matrix v5 | 41 | 160 | ±7.7% | 92.3% | 1 macro | 0.00% |
| matrix v6 | 41 | 480 | ±4.5% | 93.9% | 1 macro | 0.00% |
| tournament v1 | 8 | 1000 | ±3.1% | 84.0% | 1 macro | 0.00% |
| tournament 08-11 | 12 | 600 | ±4.0% | 78.3% | 1 macro | 0.00% |
| tournament, post-assignment | 12 | 1000 | ±3.1% | 66.2% | 3 macros | 0.00% |
| tournament, current | 12 | 480 | ±4.5% | 72.3% | 4 macros | 0.02% |
For eight consecutive solves a single macro carried 100% of the Nash support with zero exploitability, which reads like a decisive result. It was not: the library was 29 copies of one shared notebook out of 41 macros, so the equilibrium was a mixture over a room with one person in it. The last two rows are the first with genuine mixtures, and they arrive only after structural alternatives were hand-seeded into the library.
6. What the ladder says
542 ranked episodes are now on disk, up from 131 when this was first written. The record is 259 wins and 283 losses: 47.8%, with a standard error of 2.1 points.
The two distributions sit on top of each other. Median bank $87,201 against the field's $90,151; mean $89,008 against $86,583. There is no seat bias (medians $88,178 and $86,490), and no episode ended early, so the shipped path is sound.
The interesting structure is over time, and it is not what the raw banks suggest.
Both banks fall together across the archive. That is mostly common-mode:
corr(us, opponent) = 0.640, so about two-thirds of the variation
between episodes is shared richness — the map, the shop draw, the era — rather
than anything either agent did. Bank alone cannot tell you whether the agent
got worse, which is exactly the trap the next section is about.
The leaderboard score can, because it is measured against the field rather than against one opponent. It says the rewrite is a regression: the three most recent submissions score 690.8, 752.4 and 758.1, where the three Python submissions they replaced scored 1091.7, 1114.6 and 1117.6.
7. Four ways to fool yourself
This project has produced four confidently wrong readings of its own data. They are worth separating, because they fail for different reasons, only one is fixed by collecting more episodes, and the last one contains the other three.
Trap 1 — a benchmark on which everything wins
Self-play here does not play arbitrary opponents. It plays recorded ones: real ranked episodes are downloaded, the opponent's plan is reconstructed into a macro, and that macro is replayed through this bot's own executor. Every candidate scored 79–97% against that field.
A benchmark on which everything scores above 90% has no resolving power left. Worse, the direction of travel was misleading: re-recording the whole field with a fixed recorder moved the shipped bot's score from 95.0% to 97.5%.
Trap 2 — selecting on the outcome
This is the one that took in the first version of this article. Sort the episodes by the opponent's final bank, split into quartiles, and report the win rate in each. On 131 episodes that gave 93.8%, 65.6%, 37.5% and 0.0%, and a headline: zero wins in 35 games against the strongest opponents.
The statistic is real and the inference is worthless. Winning means our bank exceeded theirs, so conditioning on their bank being high conditions directly on the outcome. To show the size of the effect, take the same 542 episodes and pair each of our banks with an opponent bank drawn from a different episode. Any genuine "we lose to strong players" relationship is destroyed by that shuffle. The gradient is not.
Randomly re-paired data produces 90% / 61% / 38% / 16% — a steeper slope than the real episodes' 72% / 41% / 36% / 42%. The v1 finding was a measurement of arithmetic, not of the bot. Against genuinely high-banking opponents the bot in fact does better than chance pairing would predict (42% against 16%).
lab residual instrument ranked episodes by margin
and compared the tails, reporting that opponents harvest 936 units against our
548 — a 2.1x gap. But the worst bucket selects episodes where they did unusually
well and we did unusually badly. On the unselected distribution the gap is
1.32x at the median, and every figure derived from it was overstated by about
60%. The tell was visible in the original table: our harvest moved 10% across the
buckets while theirs moved 62%.The rule both errors violate: a tail is for finding candidate mechanisms; a gap must be measured on the full distribution.
Trap 3 — a metric that cannot express the answer
The first version of this article argued that "ship it and watch the ladder" is a weak A/B instrument because 50 episodes can only detect a jump to about 70%. That was right about the arithmetic and wrong about the reason. The deeper problem is that no sample size helps.
Nine submissions span 427 rating points. Their win rates are 52.5%, 38.5%, 47.5%, 51.4%, 50.8% and 46.6% — no relationship to strength at all. Matchmaking is rating-paired, so every build is fed opponents at its own level and converges to 50% wherever it happens to sit. Win rate measures convergence, not strength. A build still above 50% is still climbing; below, still falling.
So the corrected loop is: ship, let the score settle over 50–100 episodes, and compare scores. Win rate is a convergence check and nothing more.
Trap 4 — measuring against yourself
The three above are specific errors. This one is the shape they share, and it is the expensive one.
Every instrument this project built compares the Go agent to the Go agent.
Self-play duels it against its own macros. The payoff matrices rank plans that
all run through its own executor. clonegap and clonetrace
measure reconstructed opponents replayed through that same executor.
lab prod holds a fixed opponent — which is better — but the fixed
opponent is also this executor. Each of those is internally valid. Not one of
them can answer "is this agent any good".
The cost was a 370-point regression that survived an entire rewrite. It was found by running the two agents against each other for the first time, which took one afternoon and had been possible from the first day. The gap was not subtle: sixteen games, sixteen losses, 42% less money.
Why this exists. Every other instrument in this project compares the Go build to
itself, or to macros recorded from replays and replayed through the Go executor.
That made a ~370-point regression invisible for an entire rewrite: the Go build
scored 746.7 where the agent it replaced scored 1117.6, and lost 16-0 head to
head. Ladder win rate could not show it either, because rating-paired matchmaking
equalises win rate at every strength.
So this is deliberately the slow, authoritative gate rather than a sweep tool.
~6s an episode against `lab prod`'s 8ms; run it once before shipping, not in a
loop.
The same blindness hid the frontier. Locating it required fetching a public web page, and the reason nobody did is that no internal instrument implied it was missing — every one of them was returning a confident, well-calibrated, self-referential number.
8. The clone deficit, and what closed it
The saturated benchmark had a concrete cause. Opponents reconstructed from real episodes played at roughly 70% of their real strength through this bot's executor, and the worst-cloned seats were the strongest players — one seat that banked $115,305 in its own episode managed $28,164 as a clone. Self-play was scoring candidates against weakened copies of exactly the agents worth beating.
Measured on quantities the market cannot move, the reconstruction was good at building and bad at earning.
The clone earned more than its original and banked 30% less; 91% of the over-spend was buying wheat, traced to the recorder storing the highest quote a seat was ever seen paying rather than its median. Under it sat a loop: the clone grows about 11% fewer crop tiles, so it grows less wheat, so it buys more, so it has less cash for seed.
The fix was in matching, not in scoring
Five separate attempts had tried to make the executor's job values more accurate, and all five lost (Section 10). The defect was somewhere else. The executor assigned jobs greedily — each unit takes its own best-scoring job in turn — which means the first unit can take the one job a later unit could have done well, and that unit is then sent walking.
Replacing the greedy with a genuine min-cost assignment (Hungarian algorithm, about 15 units against 200 jobs) was found by reading the predecessor agent's own submission description, which names its core as exactly that.
| measure | before | after |
|---|---|---|
| head-to-head against the greedy | — | 66–81% (+$3,481 to +$5,196/game) |
| absolute mirror bank | $65,013 | $72,399 (+11%) |
| clone peak-crop deficit | 10.0% | −4.4% (now exceeds the original) |
| clone peak-herd deficit | 3.2% | 0.0% |
| field realisation | 85–93% | 89–97% |
The production half of the clone deficit is gone — the thing that had been saturating the entire benchmark. It closed not by tuning a valuation but by solving the assignment the executor had always been approximating. Every previous attempt attacked which job is worth more, and the scoring was already fine; the defect was in who does which job, and no amount of re-valuing can fix a bad matching.
9. Where it actually stands
The previous version of this article ended this section with "what remains is one number" — steps per productive action, 1.30 against the rank-1 agent's 1.02. That was written before anyone ran the two agents in the same episode.
Through the real interpreter, seed-paired and seat-balanced, the Go rewrite lost sixteen games out of sixteen and banked 42% less. A second run through the new gate on twelve fresh seed pairs returned a margin of −$40,011 ± 3,302 and the verdict "significantly WORSE than the predecessor. Do not ship."
It has been reverted. The pre-rewrite Python agent was reshipped with one
hardening fix — a latent NameError on __file__ in its
import fallback, the same fault that got this project's first submission
rejected, which had never fired because nothing had ever taken that branch. The
score is climbing back: 776 → 928.8 and still moving toward the 1117.6 the same
agent reached before.
What the better agent was doing
The predecessor's advantage is not a crop mix. Its own header describes a different economy: livestock income on a curve that ratchets upward, because almost nobody farms milk and wool, so the town and the dairy and yarn shops drain the starting inventory and quotes climb all season. It realises $332 for milk and $252 for wool against bases of $160 and $200. This project realises $156 and $161 — roughly half — because it dumps mid-season into a curve it crushes itself.
The immediate cause was visible at once: sell floors of $3 for milk and $11 for wool, so produce goes out from day 7. Raising them is a real gain and a small one.
| variant | fitting bank | held-out |
|---|---|---|
| floors 90 / 80 | — | +428 ± 280 |
| floors 120 / 110 | +874 ± 281 | +444 ± 313 |
| floors 200 / 180 | −2,066 ± 605 | — |
| drop melon | −9,237 ± 1,300 | — |
The best of those does not survive as significant on held-out seeds. Dropping melon — which no shop demands, and which looked like the meta's most obvious unexamined habit — is a $9,237 disaster; the predecessor farms it deliberately and calls it "the melon pot".
The throughput number, in its proper place
It is still true that the rank-1 agent takes 1.02 steps per productive action where this executor takes 1.30, and it is still the cleanest single measure of execution quality anyone here has found.
What changed is its standing. Closing 1.30 to 1.02 is worth a fraction of a 2,000-point gap, and the agent it would improve is the one that just lost 16–0. The same applies to the capacity economics: buying hands converts to output at +17% harvest and the Fibonacci wage curve eats all of it, which remains true and remains a small effect.
The one asset nobody has spent
actTimeout is one second. The executor uses 4.5 milliseconds of
it, and the bit-exact engine simulates an entire 720-step episode in 8. That is
roughly a hundred full rollouts available every turn, sitting idle, and it is the
one thing the 1117-rated Python agent cannot replicate — it runs an episode in
five seconds.
Both agents, at both ratings, evaluate a hand-written expression once and commit. The obvious objection is the branching factor, about 1813 per turn, but that rules out enumeration rather than sampling. The first attempt at spending it does not work yet.
A rollout searcher that evaluates K whole-turn assignments and plays the best collapses the bank from $70,284 to $6,867 at K=2 — one swapped assignment per five turns. The evaluator is not blind: forced to play its worst-ranked candidate it scores 1,650 against 4,538 for its best, a 2.75x separation, so the ranking carries real signal. Executor state corruption was found, fixed, and changed nothing; recording side effects are ruled out by K=12 playing bit-identically to K=0; and cumulative small losses are ruled out because K=2 already collapses.
So each departure from the heuristic is structurally destructive, and the reason is not yet known. The leading hypothesis is that the rollout under-prices a bad assignment because it continues with a fresh policy that re-plans from wherever the units end up, while the real executor carries the consequences — but the last several confident hypotheses in this project's log were wrong, and it is recorded as a hypothesis.
10. The ledger
The changelog records every measured attempt, and it has a shape: on this farm, making a number more correct almost always lost.
| change | claim | result |
|---|---|---|
| min-cost assignment | solve the matching instead of approximating it | 66–81%, ceiling +$7,386 |
| animal feed/care DP | exact solve; a meal costs a wheat, a second priced currency | −71.7% vs a clone; neutral (+296 ± 1228) vs a fixed opponent |
| exact CARE / HARVEST / WATER marginals | provably correct valuations | all lost |
| absorption-rate metering | sell against measured demand | lost |
| graded crop yield | wheat is worth 4 units, not 6 — and it is | worse on every axis |
| late planting deadline | the same fix, minus the re-valuation | shipped |
| continuation-value lookahead | the myopic matcher is the problem | loses monotonically |
| sequence assignment | reserve a unit a run of jobs | worst result in the project |
| route commitment (v1, v2) | imitate the top agent's movement profile | 65–74% against |
| smoothing the hire curve | the meta's erratic roster looks like noise | lost 300 of 300 episodes |
| safety watering | a small floor, not a binary rule | shipped |
| demand-gated herd | size the herd from the unlocked shop list | 75.3% ±2.5 vs ungated |
The graded-yield experiment is the clearest case, because its successor separated the variables. Pricing wheat at its true four units instead of six is strictly more accurate, and it lost on every axis. Splitting the change showed why: the half that extended the planting deadline was good and shipped; the half that corrected the value destroyed it, because a more accurate wheat price makes wheat lose the internal job auction and diverts units to the herd.
Note also what the multi-turn hypothesis has cost. Five expressions of "look further ahead" — continuation values, sequence assignment, route commitment twice, territories — have all lost, and sequence assignment lost worst because a reserved sequence constrains exactly the matching freedom that made the Hungarian win.
11. Ways to improve
The roadmap at the top of this article is the short form. This is the reasoning, including what the previous version got right and what it got wrong.
Status: built. The previous version of this article recommended, as its first item, naming the 350 points by diffing the two agents on something other than throughput. That happened, and it was the decisive action: the two agents were run against each other, the rewrite lost 16–0, and it was reverted.
What generalises. The fix is not "run the predecessor" but "hold one
measurement outside the system". tools/vs_predecessor.py is
deliberately slow — about six seconds an episode against lab prod's
eight milliseconds — because it is a gate rather than a sweep. That trade is the
right one: an authoritative measurement you run once before shipping is worth
more than a fast one you run continuously and cannot interpret.
The second half of this is still outstanding. Head-to-head win rate is
compromised inside the mirror as well as on the ladder: AnimalDP
measured −71.7% against a clone and neutral (+296 ± 1228) on absolute bank
against a fixed opponent. Every clone-duel win rate in the archive carries that
distortion.
Status: decided. Two agents of genuinely different architecture, built by different reasoning, both stalled near or below 1100 against a frontier of 3193. The largest effect measured in a full day of careful held-out work was +444 ± 313, about half a percent, and it did not survive as significant.
That is evidence about the method rather than about either agent. A 2,000-point gap is not a knob, and every positive result this project has recorded, stacked together, does not approach it. Tuning policy constants is finished as a line of work — not unlucky, finished.
This retires most of the previous version's forward-looking section, including its recommendation to run a pre-registered two-knob experiment on roster × throughput. The interaction argument was sound and the prize was too small to matter.
Status: the real opportunity, and currently blocked. The turn budget is one second and the executor spends 4.5 milliseconds. The bit-exact engine — the most expensive thing this project built, and the piece that survives the revert intact — runs a whole episode in 8 milliseconds. That is about a hundred rollouts per turn going unused, and the 1117-rated Python agent cannot copy it at five seconds an episode.
My read: this is the only asset here that is shaped like a 2,000-point answer, and it is worth saying why. Every other avenue on the list is a refinement of hand-written evaluation, which two independent attempts have now shown tops out near 1100. Search is a different kind of agent, which is what the gap calls for, and the engine that makes it affordable already exists and is already verified exact.
It does not work yet, and the failure is interesting rather than discouraging: the evaluator ranks candidates with real signal (2.75x between its best and worst picks) and playing its top pick still destroys the agent. Until that is understood, no amount of rollout tuning means anything — which is the correct place to stop, and the instrumentation that establishes it (departure rate, signal test, K-sweep) caught a 90% regression before any A/B was run.
The two habits worth keeping
- Print the unselected distribution beneath every tail comparison. Two of
this project's wrong answers and one of this article's were tails read as gaps.
lab residualnow does this automatically. - State what an instrument cannot see, next to its output. The
measurements here that held up were the ones whose write-ups named their own
confound — the confounded bank metric in
clonegap, the handicapped opponent in the rollout world. The ones that failed were the confident ones.
12. Limitations and corrections
Corrected in this revision
- "What remains is one number" is withdrawn. The previous version closed on steps per productive action, 1.30 against 1.02. The frontier was measured afterwards at 3193 against a best-ever 1117.6, and the build that number described lost 16–0 to its predecessor and has been reverted.
- The rating gap was −350; it is −370, and the individual scores quoted are snapshots. A submission's public score drifts as episodes accumulate — the same builds appear at 1091.7 / 1105.4 and at 746.7 / 752.4 / 755.3 / 776.1 at different times. The 427-point spread the chart in §7 rests on survives, but the points are not fixed quantities.
- The ledger's "animal feed/care DP lost 71.7%" is retracted upstream. That figure came from a clone duel; against a fixed opponent the same knob reads neutral. It is still not a win, and "lost 71.7%" was the wrong epitaph — an instance of this article's own Trap 3, inside this article's own table.
- The recommendation to run a two-knob roster × throughput experiment is withdrawn as too small to matter, though the interaction argument behind it stands.
Corrected since the first version
- "0 wins in 35 against opponents banking $88k+" is withdrawn. It was selection on the outcome; shuffled data reproduces a steeper gradient. There is no measured deficit against strong opponents.
- The win rate is 47.8% over 542 episodes, not 48.1% over 131. Both are indistinguishable from the 50% that rating-pairing forces.
- "The ladder is a weak A/B instrument" understated the problem. It is not a matter of sample size: win rate cannot express strength at any n.
Where this article may still be wrong
- The archive's decline is attributed to the era, not the agent, on the
strength of a correlation.
corr(us, opp) = 0.640shows most variation is common-mode, but it does not prove none of the decline is ours. The rating drop is the independent evidence, and it is only six data points. - The submission-to-score mapping is inferred. The project's notes record six scores with their episode counts; I matched those counts to submission identifiers in the episode index. The counts are unique, so the mapping is determined, but it was not read from a labelled field.
- (resolved) The previous revision flagged a disagreement with the project's notes, which reported the archive trending the other way and concluded that a low score was lag rather than weakness. That is now settled in the direction this article took: the project retracted the conclusion — "I concluded from a 47.8% win rate that 'there is no evidence of a large deficit'. That conclusion was wrong" — after the head-to-head showed a 370-point regression.
- Clone-deficit and throughput figures are the project's own measurements, cited as recorded, and inherit the recorder's approximations.
- Matchmaking is rating-paired, so the opponent distribution is not a sample of the leaderboard.
Sources: the project's data/ours (542 ranked episodes),
data/results (10 payoff-matrix and tournament solves),
knowledge.md and CHANGELOG.md. Win rates, bank
distributions, the quartile permutation test, the score/win-rate comparison and
the power curves were recomputed from the raw episode JSON. Clone-deficit,
throughput, capacity and season-appetite figures are the project's own
measurements. Team names have been removed throughout.