01What the sea is made of
Six delegations fish one stock. Each round you choose how many licensed boats to send, how many unlicensed ones, how much of your warehouse to sell, and what catch figure to declare — and you choose all of it before you learn what anybody else chose. Then the ledger is published, one delegation may report another, and an audit may open somebody's books. After twelve rounds the treaty can conclude at any time. If the stock ever falls below the collapse line, nobody wins.
Three layers stack on top of each other. The biology and economics are Gordon-Schaefer: logistic recruitment, catch proportional to effort through a saturating function, convex costs on the legal fleet. The market layer separates catching from selling — you land fish into a warehouse and decide later when to sell into a price that falls as everyone sells at once. The institutional layer is Ostrom's: public disclosure, self-reporting that is allowed to be false, forensic audits, fines, and a bounty for the reporter who is right.
| Symbol | Value | What it does |
|---|---|---|
| K / r / B₀ | 1000 / 0.35 / 700 | Carrying capacity, intrinsic growth, opening stock |
| q / p / c | 0.05 / 10 / 60 | Catchability, base price, cost per legal boat |
| COST_GAMMA | 1.25 | Convexity on legal cost — licences and port congestion |
| C_ILL | 60 | Unlicensed cost, linear — the asymmetry that creates temptation |
| PRICE_ETA | 0.6 | Demand elasticity: everyone selling at once crashes the price |
| σ / R_NOISE | 0.12 / 0.15 | Per-boat luck and unpublished recruitment variance |
| ALLEE | 150 | Collapse line. Everybody loses, including the leader |
02The original sin is legal
The first cycle produced a result I did not want. With linear costs the myopically optimal fleet was six to eight boats, past the cap of five. Since the institutions only investigate unlicensed fishing, a delegation that simply maxes out its legal fleet is untouchable. The greediest bot took 85–100% of games. The commons was being destroyed entirely legally, and every institution I had built was pointed somewhere else.
Two changes fixed it, and both had to be there. Convex legal cost (γ = 1.25) pulls the interior optimum down to two or three boats; price elasticity (η = 0.6) means that when everyone lands a lot at once, the price collapses on all of them. Crucially the unlicensed cost stayed linear — no licence, no port fee, no congestion. That asymmetry is the whole economic engine of temptation: expanding legally is expensive and expanding illegally is cheap.
| Prescription | Measured | Verdict |
|---|---|---|
| Price elasticity alone (η 0.6) | Greediest bot still won 76–88% | not enough |
| Convex cost γ = 1.25 | Interior optimum 2–3 boats | kept |
| Convex cost γ = 1.5 | Optimum 1–2 boats, fleet choice disappears | rejected |
| Punishment = retaliate at full fleet | Extinction in 100% of games | rejected |
Two of the ten degenerate strategies in the test battery — always-max-fleet and end-game sniping — were dead in every difficulty from the first cycle onward, at roughly 0% win rate, without a single institution touching them. The sea executes them. Collective loss on collapse turned out to be a stronger defence against backward-induction unravelling than the random end date was.
03The detector was looking at the wrong thing
The moment warehouses went in, a strategy called hoarder won 42.8% of games on the hardest difficulty. The mechanism was not what I expected. Hoarding is legal. What made it dominant was that the bots' detector watched GDP, so every time the hoarder dumped a full warehouse its GDP spiked, a bot reported it, the audit found nothing, and the reporting bot paid the false-report fine. The hoarder was not evading the regulator. It was farming it — using the enforcement system as a weapon to bleed the other players.
The fix was to change what counts as an anomaly. It is not “this delegation earned a lot”. It is “this delegation's tonnage does not reconcile with what its licensed boats could plausibly have caught”. The books are public, so implied catch is recoverable:
An honest dump now reconciles and is innocent. Laundering does not reconcile, unless the round provides cover. And that last term in sd is the one I got wrong twice: an auditor has to be humble by exactly as much as recruitment might have varied. When I left R_NOISE out of the error budget, false accusations came back at 2.2 per game. Putting it in is not a fudge factor — it is the arithmetic of an observer who genuinely cannot separate a bad spawning year from a thief.
04Five alibis, pointing in five directions
A disturbance in the stock could be recruitment variance, weather, or theft, and you have to decide whether to report someone without being able to tell them apart. What makes that interesting rather than merely noisy is that each event opens a different excuse.
| Event | Channel | The excuse it buys |
|---|---|---|
| Red tide | growth ×U(.2,.6) | “The stock fell on its own” — and the quote rises the same round |
| Cold intrusion | growth ×U(.4,.8) | The same, weaker |
| Migration | B +U(.05,.15)K | A good year for everyone — the auditor's ceiling widens 35% |
| Typhoon | two named fleets halved | “We could not sail” |
| Demand surge | reference demand ×U(1.3,1.8) | “The price was good, so we emptied the store” |
The migration multiplier is the cleanest thing in the design. Because the size of the shoal is never published, an honest auditor has no choice but to widen their bound — and that widened bound is the laundering window. The alibi is not narrative flavour bolted onto the rules. It is a coefficient inside the detector.
05Twice the dial did nothing
Two separate sweeps came back byte-for-byte identical. The first was a greed floor: raising the minimum appetite of every bot from 0.55/0.66 to 0.62/0.74 changed nothing at all. The second was the restraint cap, the ceiling on how greedy a bot may be once the stock enters the danger zone: same story, no movement.
Identical output from a changed input usually means the code is not wired up. Both times it was not a bug. It was the system reporting something about itself.
The fishery organises itself just below the regulator's line. Every bot caps its appetite once its estimate falls under half of carrying capacity, and the stock converges to just under that line and stays there. So in the overwhelming majority of rounds every bot is already at the cap, and a floor on greed never gets the chance to fire. This is not a quirk of my parameters — it is what real regulated fisheries do, sitting up against whatever limit they are given. It is the reason the gauge in the finished game shows that line permanently, labelled as a political number rather than a biological one.
And on top of that line, integers build a staircase. Boats are whole. Rounding a continuous appetite into a whole fleet flattens wide ranges of the dial into the same integer — near the line, everybody sends two boats, and at one specific threshold everybody sends three and the extinction rate explodes to 56%. The dial was continuous; the game was not.
The fix is a small one that unlocked everything after it: bots round stochastically, taking the floor plus a Bernoulli draw on the fraction. Average effective fleet becomes continuous again and the cap becomes a real handle. The same four cap values that had produced identical results now spread the win curve across 53.8 / 42.0 / 35.5 / 26.2. The human player still rounds like a human, because a slider that sometimes gives you three boats when you asked for two would be a bug, not a mechanic.
06Strength is precision, not appetite
For three cycles the difficulty curve was inverted: the player won more in the harder rooms. 65 / 71 / 84 / 92. Something about the hard rooms was helping the human.
I found three culprits by running discriminating experiments rather than guessing, and cleared two suspects the same way.
| Suspect | Experiment | Verdict |
|---|---|---|
| False accusations between bots | 3.33 per game on the hardest tier vs 0.29 on the easiest; bots lost 240 points each, the human seat lost nothing | guilty |
| Bots over-buying surveys | Flattening the purchase rate moved the curve from 48/49/64/73 to 45/43/51/47 | guilty |
| The greed ladder | Setting every tier's greed multiplier to 1.0 flattened the curve to 44/44/45/47 | guilty, backwards |
| Bots' illegal-fishing coefficient | Controlled at 1.0 across tiers: curve unchanged | cleared |
| Bots' lost illegal income | Same experiment | cleared |
The greed result is the one worth sitting with. I had assumed a harder opponent is a hungrier one. Measurement said the opposite: past a point, more appetite is self-harm, because the myopic best response overshoots and the price crashes on the bot doing the overshooting. Making the hard bots greedier made them worse. I proposed a greed ladder twice, in different clothes, and it was rejected twice.
What actually separates the rooms is precision. A weak bot has a wide personality jitter, a noisy stock estimate, and a dull customs house; a strong bot has tight jitter, a clean estimate, and a sharp one. Clumsy hands, cloudy eyes, a dull customs house — against precise hands, clear eyes, a keen customs house. The accusation threshold is not part of the ladder either: it is not allowed below the floor that the catch noise itself sets, around 2.8, because a threshold under the noise is not sharpness, it is a bot that shouts at random.
07Surveillance is a public good, so nobody paid for it
By cycle five the science survey was dead weight. I measured it directly by deleting it: the reference player's win rate went from 49.5% to 63.2% when it could no longer buy surveys. Information was costing about 120 points a game and returning less than that. The CPUE estimator was already good enough that measuring the sea properly told you almost nothing you did not have.
The same experiment cleared something else. Removing the reference player's illegal channel entirely moved it 49.5% to 48.0% — inside the noise. Laundering was not where the human advantage lived, which killed a planned fix (teaching the hard bots to launder) before I wrote a line of it. The residual advantage was one thing only: the human fished more, 18.5 boats a game against 12–15 for the restrained bots.
Rather than delete the survey I changed what it is for. It stopped being a fishing tool and became an investigative one. The survey vessel now also counts the dark tonnage: last round's total catch minus what the publicly declared fleets could account for. It cannot name anyone — the deduction stays yours — but it tells you whether there is something to find. Price dropped from 30 to 12, because the free-survey experiment had measured the information's actual worth at 10–15 points and I had been charging double.
That left the deeper problem, which is Ostrom's problem and not mine. Reporting a cheat benefits everybody; paying for the survey that lets you report benefits only you. Monitoring is a public good and public goods are undersupplied, so nobody watched. The real world's answer is to pay informants, so the game pays informants: a correct report returns 40% of the fine to whoever made it. A random audit pays the treasury instead.
The intel first shipped computing dark tonnage against the current stock estimate. That estimate is taken after fishing and regrowth, so it is systematically too low, so dark tonnage was almost always positive, so suspicion was inflated every round and false accusations came back. The survey vessel has to reason about the stock as it was before last round's catch. Chronological alignment is not a detail in forensic accounting; it is the whole subject.
The final measurement on the survey is my favourite number in the project. Buying it blindly a quarter of the time costs 6–7 percentage points of win rate. Buying it only when you already suspect something breaks exactly even. The survey does not reward spending. It rewards judgement, and it charges you for the absence of it.
08The margin is a razor
Cutting the survey price by 18 points — about 3% of a typical final score — moved the hardest tier's win rate by ten percentage points. I confirmed the size of the effect by splitting it two ways, purchase rate against price, and it held.
That ratio is now a standing rule on this project: touch any economic constant and the restraint caps have to be re-tuned in the same session. It is also why the balance figures cannot be eyeballed. A change that looks like rounding error in the score moves the outcome by more than the difference between two difficulty levels.
| Cycle | EASY | NORMAL | HARD | BRUTAL | What changed |
|---|---|---|---|---|---|
| 1 | 65 | 71 | 84 | 92 | inverted — harder rooms were easier |
| 4 | 55.5 | 52.0 | 55.2 | 49.5 | inversion gone, curve flat |
| 5 | 49 | 45 | 44 | 45 | survey cost identified as dead weight |
| 6 | 51.0 | 43.8 | 32.5 | 34.3 | stochastic rounding unlocked the caps |
| 7 | 67.8 | 53.2 | 41.6 | 38.8 | survey redesigned, bounty added |
09Porting it, and what the harness caught
The reference engine is Python. The game is a single HTML file. A port like this is only trustworthy if it can be shown to produce identical games from identical seeds, so the first thing built was not the interface but the parity harness — 6,410 assertions across four layers, from the random number generator up to complete games driven by ten different seat strategies.
Four things broke it, and none of them were the economics.
Python's round() is banker's rounding. It rounds halves to even; JavaScript's Math.round rounds halves up. That function decides how many boats a cooperative player sends, so the two engines diverged on any round where the target landed exactly on a half.
Short-circuit evaluation is part of the specification. In four places a condition is checked before a random number is drawn, so the draw only happens for some personas or some states. Reordering those checks for readability — putting the cheap numeric test first, as one normally would — changes how many numbers are consumed and every subsequent round diverges.
One difficulty consumes fewer random numbers than the others. The hardest bots have zero estimation noise, and the code skips the draw entirely rather than drawing a zero-width Gaussian. Per-round random consumption is therefore tier-dependent.
An early return skips more than it looks like it skips. A bot that stops fishing because the stock is critical also skips the one-time initialisation of its selling policy, so a bot that idles early draws that number several rounds later than one that does not.
The original game loop runs to completion, which is fine for simulation and impossible for a human who has to decide inside it. The tempting fix is to write a second, asynchronous copy of the loop for the interface — and a second copy of a rule is how two versions of a rule are born. Instead the loop body became a generator that yields at the points where a human is asked something, and there are now two drivers over the same generator: a synchronous one for the simulations and an asynchronous one for the screen. Yielding consumes no randomness, so the whole parity suite was re-run after the refactor and still passes. The interface contains no rules at all; the stock gauge calls the same estimator the bots do.
10The fix that was certified dead
Back in cycle five, the greed floor produced byte-identical results and was written up as inert, with a note recommending removal. It was in the specification as a dead knob when I started porting.
It is not dead. It fires at construction time, before any game state exists, and the reason the sweep saw nothing is that the sweep only varied it inside a range where a different mechanism was already binding. Its actual effect is on the roster:
| Tier | floor | Glutton | Steward | Grim | Tit-for-tat | Opportunist |
|---|---|---|---|---|---|---|
| EASY | 0.00 | 0.780 | 0.450 | 0.620 | 0.620 | 0.650 |
| NORMAL | 0.45 | 0.780 | 0.470 | 0.620 | 0.620 | 0.650 |
| HARD | 0.60 | 0.780 | 0.600 | 0.627 | 0.627 | 0.651 |
| BRUTAL | 0.70 | 0.780 | 0.700 | 0.700 | 0.700 | 0.700 |
On the hardest difficulty the Steward — the cast's moral anchor, the delegation that is supposed to fish conservatively no matter what — has exactly the same appetite as the grudge-holder and the opportunist. The cast loses its personalities in the room where personalities should matter most. And the greed ladder that measurement rejected twice, in cycle 4 and again in cycle 5, is present anyway through a side door.
Both original measurements were correct. The floor really did nothing to the dynamics that were swept, and it really does something to the roster, which was never swept. The error was in the sentence that joined them: inert is a claim about a system, and it was written from an experiment that only touched part of one.
I ported it exactly as it stands, because the balance baseline and every parity fixture were generated with it live, and changing it means re-running everything downstream. The razor-thin margin rule applies to itself. But the specification now says it is alive, with the table above, instead of saying it is dead.
11The baseline could not police its own tolerance
The handover asked for the JavaScript win rates to match the Python ones within three percentage points. Four cells missed, by up to five points. Since the two engines use different random number generators, seeded games are not comparable and a difference of that size is not obviously wrong — but “probably sampling noise” is not a result.
So I made it comparable. Running the Python side with the portable generator instead of its own puts both engines on identical seed streams, and then the comparison stops being statistical: all 120 numbers across 40 cells matched exactly.
Which left the question of where the original five-point gap came from. Growing the JavaScript sample shrank it — 5.0, then 4.1, then 3.2 points — but it would not go to zero, so I re-measured the Python side at 8,000 games too.
| Cell | Python, 8,000 | JS, 8,000 | Δ | Shipped baseline (500) |
|---|---|---|---|---|
| easy / hoarder | 24.0% | 23.6% | −0.4 | 19.0% |
| normal / hoarder | 19.7% | 19.7% | 0.0 | 22.0% |
| hard / hoarder | 17.7% | 17.0% | −0.7 | 20.2% |
| normal / reference | 54.5% | 53.3% | −1.2 | 53.2% |
| brutal / reference | 39.0% | 38.9% | −0.1 | 38.8% |
The gap was in the baseline. At 500 games a win rate near 20% carries a standard error of 1.8 points, so its 95% interval is about ±3.5 — and near 50% it is ±4.4. The baseline was being asked to police a three-point tolerance that is narrower than its own uncertainty. Two of its cells sit five and 2.7 points away from where they converge.
This is the same mistake as the accuracy figure in the authorship write-up and the same mistake as the false-accusation rate earlier in this one: a number that was measured correctly, and then asked to answer a question it did not have the resolution to answer. The new baseline is 4,000 games, where the standard error at 20% is 0.63 points and a three-point tolerance means something.
12What I still do not know
Three figures are shipped as deliberate and flagged for real-use confirmation, because a simulated reference player is not a person.
The easiest room gives the reference player 67.8% of games. That is generous, and it is meant to be a playground, but “generous” measured against a bot is not the same as generous felt by somebody who has never seen a Gordon-Schaefer curve. Bots on the harder tiers still falsely accuse each other 1.8 to 1.9 times per game; the accusation threshold cannot go below the noise floor, so some of that is structural, but I do not know how much bleeding is too much until people watch it happen. And the sea dies in 13–16% of games on the hardest difficulty — roughly one in seven. As drama that seems about right. As an experience it may simply be discouraging.
The largest thing I do not know is whether any of the theory arrives. The design has a Gordon-Schaefer stock, an Allee threshold, a common-pool dilemma, Ostrom's graduated sanctions and her monitoring problem, and a forensic reconciliation that a real fisheries auditor would recognise. None of that is on screen. What is on screen is a slider, a ledger, and the question of whether to accuse somebody. If the physics of it never surfaces as a feeling — the sea is sitting right under the line and everybody knows it and nobody moves — then the seven cycles bought a balanced game and taught nobody anything, which was not the point.
Six delegations, one sea. Everyone can see the ledger; nobody can see the sea. It runs entirely in the tab and stores nothing.
Open Erysichthon →