Improving Kaggriculture Bot

Competition Bots · Self-Play

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.

Third revision, August 2026 · 542 ranked episodes · 10 payoff-matrix solves · engine kaggle-environments 1.32.6 · every figure recomputed from the project's own data files. Team names anonymised.

Third revision, and the ground has moved twice. The first version led with a deficit against strong opponents that turned out to be a selection artefact (§7). This version is written after the project measured two things it had never looked at: the actual leaderboard, and its own two agents against each other. The Go rewrite has since been reverted, and the "remaining gap is one number" thesis of the previous version is withdrawn. A full list of what changed is in §12.

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

Leaderboard scores: top 3193 against this project's best of 1117

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.

Leaderboard top
3193
our best ever: 1117.6
Ladder win rate
47.8%
542 episodes, ±2.1pp
Rewrite vs predecessor
0 / 16
games won, −$40k a game
Best measured gain
+0.5%
in a full day of held-out work

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.

#whatwhy nowstatus
1Gate every build against an external reference before shippingThe 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
2Stop ranking on head-to-head win rate, in self-play as well as on the ladderIt 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
3Treat "tune the policy constants" as closedTwo architectures both stalled near 1100. The largest held-out effect measured is +444 ± 313. This line of work is finished, not unlucky.decided
4Spend the unused compute: ~100 full rollouts per turn sit idleactTimeout 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
5Find why one swapped assignment costs 90% of a seasonThe 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
6Port the predecessor's thesis, not this executor'sLivestock 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
The through-line. Every wrong turn in this project has the same shape: a measurement taken against itself. Self-play against clones of itself, matrices against a library of near-copies, a rewrite against its own earlier builds, a win rate against opponents matchmaking chose to be its equals. Each was internally consistent and each was uninformative about the only question that mattered.

1. The game

In one line: two farms, 30 days, one shared market, most cash at the end wins — and winning by $1 scores the same as winning by $100,000.

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.

Cost of the n-th hire, following the Fibonacci sequence

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.

Season demand by product, measured over 2000 episodes

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.

Version matters more than it sounds. The engine changed between 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.

internal/pyrand/pyrand.go
// 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.

internal/engine/obs.go
// 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.

internal/engine/defs.go
// 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.

internal/macro/macro.go
// 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.

internal/macro/player.go
// 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:

internal/macro/player.go
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.

internal/macro/assign.go
// 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.

A refuted claim is embedded in this file's justification. The comment above it cites the predecessor as beating "opponents banking $88k+ about 40% of the time where this one is 0 for 37" — the same selection artefact Section 7 withdraws. The fix was right and the measurement that confirmed it (66–81% head-to-head) is sound, but the motivating statistic in the source is not. Artefacts propagate into comments, where nothing re-checks them.

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.

internal/arena/arena.go
// 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.

submission/main.py
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.

internal/macro/mutate.go
// 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

The unit of measurement is a seed-paired duel. Two macros play the same seed twice, once in each seating. The engine is deterministic, so on a fixed seed the only difference between the two runs is the plans.

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:

answerwhat it meanshow it misleads
fieldmean win rate against the whole librarysix near-copies of one popular notebook count six times
worstthe maximin row — the best worst-case planpessimistic if no opponent actually plays your worst matchup
nashthe unexploitable mixture, and its supportsays 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.

Best macro's self-play win rate across eight solves, against the flat ladder result
solvemacrosepisodes / cell95% CIbest field winNash supportexploitability
matrix v134128±8.7%84.9%1 macro0.00%
matrix v236160±7.7%87.9%1 macro0.00%
matrix v338160±7.7%87.4%1 macro0.00%
matrix v440160±7.7%92.8%1 macro0.00%
matrix v541160±7.7%92.3%1 macro0.00%
matrix v641480±4.5%93.9%1 macro0.00%
tournament v181000±3.1%84.0%1 macro0.00%
tournament 08-1112600±4.0%78.3%1 macro0.00%
tournament, post-assignment121000±3.1%66.2%3 macros0.00%
tournament, current12480±4.5%72.3%4 macros0.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.

The falling numbers are the honest ones. The later solves score lower because the field was re-recorded after the top of the leaderboard turned over — wheat moved from 40 tiles to 54–61, strawberry from 42 down to 30–36. Any "versus the meta" figure older than a few days is measuring a field that has stopped existing.

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.

Distribution of final banks across 542 ranked episodes

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.

Final banks over the whole archive, oldest to newest, with rating eras marked

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.

Read those two paragraphs together. The bank decline is largely the era, not the agent. The rating decline is the agent. Two measurements of the same period disagree in what they attribute, and only one of them controls for who you were playing.

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.

Self-play win rates of four candidate variants against the ladder result

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.

Real quartile gradient against the gradient from randomly re-paired episodes

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%).

The project made the same error twice more, in a different costume. Its 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%.
Harvest distributions, unselected, us against opponents

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.

Public leaderboard score against win rate for six submissions

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.

Smallest detectable win rate against episodes collected

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.

tools/vs_predecessor.py
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.

The general form: an instrument that compares a system to itself will report progress under any conditions, including a regression, and will never report distance from anything that matters. At least one measurement in a research loop has to be against something the project does not control.

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.

Clone shortfall by dimension, before and after the recorder fixes Real opponent versus cloned opponent, spending by category

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.

One metric here is confounded and two are not. In a clone-versus-clone replay both seats are clones, so any change that makes clones sell more freely crashes the shared price curve and depresses both banks. Two better-looking rules for inferring an opponent's selling floor both made the measured bank deficit worse — the metric failing, not the fixes. Peak field and peak herd are trustworthy; bank is not.

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.

measurebeforeafter
head-to-head against the greedy66–81% (+$3,481 to +$5,196/game)
absolute mirror bank$65,013$72,399 (+11%)
clone peak-crop deficit10.0%−4.4% (now exceeds the original)
clone peak-herd deficit3.2%0.0%
field realisation85–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.

Read this win at its true size. It is real, it is the largest execution improvement the project has measured, and the build it improved still lost 16–0 to the agent it had replaced (§9). A change can be the best thing in a research programme and still be far too small to matter, and nothing internal to that programme can tell you which.

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.

The Go rewrite against the Python predecessor, head to head

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.

Why this was invisible for an entire rewrite. Every instrument compares the Go build to itself, to an earlier Go build, or to opponent macros replayed through the Go executor. None of them had ever run the predecessor. And the one external signal — ladder win rate — is equalised by rating-paired matchmaking, so 47.8% meant "converged against 750-rated opponents", not "competitive".

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.

variantfitting bankheld-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 largest macro lever available moves about 1% of the gap. $400 of $40,011. The predecessor's advantage is its policy, not its trade floors or its crop mix, which means porting it is porting an agent rather than tuning this one — a much larger job than the open items assumed.

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.

Steps per productive action across four agents

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.

Adaptive hiring: harvest rises, bank falls

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.

Bank against the number of candidate assignments evaluated

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.

changeclaimresult
min-cost assignmentsolve the matching instead of approximating it66–81%, ceiling +$7,386
animal feed/care DPexact 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 marginalsprovably correct valuationsall lost
absorption-rate meteringsell against measured demandlost
graded crop yieldwheat is worth 4 units, not 6 — and it isworse on every axis
late planting deadlinethe same fix, minus the re-valuationshipped
continuation-value lookaheadthe myopic matcher is the problemloses monotonically
sequence assignmentreserve a unit a run of jobsworst result in the project
route commitment (v1, v2)imitate the top agent's movement profile65–74% against
smoothing the hire curvethe meta's erratic roster looks like noiselost 300 of 300 episodes
safety wateringa small floor, not a binary ruleshipped
demand-gated herdsize the herd from the unlocked shop list75.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.

The rule the ledger supports: this farm is action-bound, so a valuation's job is to rank jobs, not to be correct. A number that is wrong pointwise but right in rank order costs nothing. A number that is right pointwise and re-orders the queue costs a great deal. The one big win came from fixing matching, which is the part no valuation can reach.

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

  1. 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 residual now does this automatically.
  2. 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.640 shows 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.