Chapter 4
Balancing the Party
Bots that play the real rules, the curves they drew build after build, and tonight's balance pass.
Coven Rush is a game you are meant to lose. The party spreads wave by wave, the woken areas besiege their new soundsystems, and sooner or later every soundsystem is down and the music stops. That makes balance a strange job. Nobody was tuning a fight to be won; the builders were tuning how long a run lasts, how much better a good player does than a poor one, and how it feels when it ends. Over four days that job grew from one simulator into a shelf of tools and a family of bots, and the bots ended up on the start screen, playing the game for anyone who wants to watch.
This chapter follows the balance builder's work through its pull requests: the measure it settled on, the models it built, the knobs Ed turned on its evidence, and how the numbers moved as the game changed under them.
A number for strength
We need some kind of measure of how powerful we think a player can get, and how quicklyEd, 2026-10-04, recorded in DESIGN.md
The answer was fighting value, F, from Lanchester's square law. Each fighter is worth the square root of its hit points times its damage a second, and a side is worth the sum of its fighters. When two sides fight to the end, the bigger one wins with about √(Fbig² − Fsmall²) left. It lives in src/rules/power.ts, about fifty lines, and everything else in this chapter leans on it.
Babies are worth nothing because they don't fight: no animal attacks a baby, and babies attack no one. A legend is worth about two and a half adults. The square law also explains the shape of every defeat in the game. A merged army is worth the sum of its parts, and a sum is far harder to beat than the same parts one at a time, so whatever lets sieges join up is what turns pressure into a cliff.
The first balance pull request, #59 (merged on the evening of 4 October), put this number on screen: the debug overlay gained a power meter, the party's F (leashed and parked) against each siege's, and a playtest log that samples the run every ten seconds so Ed's own games could be read back afterwards (L saves it).
The balance simulator
The same pull request brought the balance simulator, src/rules/balance.ts, run with node tools/balance/sim.mjs. It plays only what decides a run: the real map, the real wave order from the party's own picker, each area's creatures marching in straight lines on their new soundsystem at their real march speed, hitting it at their level's damage, and the survivors of a fallen soundsystem marching on to the next-nearest. A run is over when none stands. A pretend player grows at a steady g units of F a minute and fights the biggest siege it can beat. Twelve seeds take seconds.
Ed's worry, which shaped what the simulator measured:
A worry is that all games last about the same time, and the difference between a good player and a bad player is small (because the enemies grow exponentially). You just go along and then suddenly get steamrolled.Ed, 2026-10-04, recorded in DESIGN.md
So the headline table became the skill spread: how many waves a player growing at 10, 30, 50, 70 or 100 F a minute lasts. Twice the skill should give roughly twice the survival, with no common cliff. The simulator could also try two pacing variants that never reached the game:
- Attrition: only a share of a won siege's survivors march on (
marchOn); the rest scatter home. - The director: reinforcements for the areas the next wave will wake, worth (base + perWave × wavepower) × max(0, 1 + α(player F ÷ expected − 1)) of F. With α at 0 it ignores the player; above 0 it pushes back harder on a strong one.
The first evening's reports on #59 are a good picture of how the builder worked: a table, a finding, a suggestion for Ed, the caveats. With populations set by distance from the dancefloor, an idle player at one-minute waves couldn't lose early at all: "A run ends only when every soundsystem is down at once, and a fresh 4000 hp one arrives every 60 s." Concentration was the cliff: "With no merging (every survivor scatters), an idle player is never beaten in 100 waves." The director's budget had to grow faster than the wave count (wave1.5) for survival to come out proportional to skill.
Later that night Ed ruled on populations instead: every area starts the same and every still-wild area grows one creature a wave. The builder built it into the game in the same pull request (grown creatures kept as counts until a witch comes near, then made out of her sight) and re-ran the simulator: "Growth alone gives the pressure the old tables couldn't. An idle player loses at about wave 19 with 60 s waves, and a late siege topples a soundsystem in about a minute." Attrition, the report found, spread skill best of anything: 28, 40, 60 and 100+ waves for players growing at 30, 50, 70 and 100 F a minute. Ed never turned it on; the variants stayed in the simulator as tools, and the director was never built.
Happy legends as a dial
Just after midnight the simulator learned to make each woken area's legend happy with chance p. It raised everyone's floor (an idle run went from about 19 waves at p = 0 to about 50 at 70%) without spreading skill much, because "a single legend can't hold back a concentrated force". The suggestion, which came true in a different form, was to make happiness something a skilled player earns rather than a coin toss.
Where an area starts
How many creatures an area starts with changed four times in a day and a half, each time on numbers from a bot and a sentence from Ed.
| When | Start | Why |
|---|---|---|
| 4 Oct, #59 | a baby and an adult | Ed's ruling that every area starts the same and grows |
| 5 Oct, #71 | a baby and two adults | "Two adults it is": the second adult turned growth-only's cliff into a steady climb in the simulator |
| 5 Oct, #115 | a young and an adult | the fight bot found first fights too hard; Ed: "could start with 1 youth instead of 1 baby and 1 adult; that should make it easier" |
| 5 Oct, #129 | a baby and a young | the opening bot, below |
The last change came in two steps. After playing, Ed wrote "the game is hard! we should start each area with just one baby and have a boot up period of 5 minutes", then "actually, the game should start with one baby and one youth, otherwise you can't avoid enraging lots of legends" (both recorded in DESIGN.md). The reason is a rule from the legend redesign: a sleeping legend grows restless as soon as its area holds none of its kind, and angry a minute later. With one creature an area, every invite empties an area.
The opening bot (tools/balance/opening.mjs, #129) checked it on the real rules, from her first step to wave 1, recruiting three ways: everything, carefully (leaving one of each area's kind), or everything and then putting the last sigil back. Each cell is legends angered / posse on her stack / knockouts, over three seeds:
| Start | Invite all | Careful | Park |
|---|---|---|---|
| 1 baby | 4.7 / 0.7 / 4.0 | 0 / 0 / 0 | 1.3 / 0 / 0.3 |
| 1 baby + 1 young | 5.0 / 0.3 / 3.0 | 0 / 5.7 / 17 | 0.3 / 3.7 / 19 |
| 1 young + 1 adult (before) | 2.3 / 0.3 / 15 | 0 / 1.3 / 25 | 1.3 / 0.3 / 17 |
With one baby an area, she either angers about five legends or has no posse at all; with a baby and a young, careful play builds a posse of about six with no legend angered. The same pull request made the home speakers boot for five minutes before the first wave's countdown, so wave 1 lands ten minutes after she gets up. Ed's rule for that boot-up: "animals still attack - you just have more time to build an army and do quests before things get difficult".
Strength and berries
Ed wanted some species to come as swarms and others as loners, and their levels to cost different numbers of berries, shown as segments on the ring under a party animal: "We should definitely segment the ring, and tie the cost to strength." #75 built strength classes (swarm ⅓, normal 1, loner 2), each multiplying a species' health and damage while its areas hold 1 ÷ strength as many creatures, so an area's F stays about the same. The berry rule became one sentence: a level costs the strength it gains, at a fixed strength per berry, rounded, at least one.
Ed then simplified it:
I don't think we should have distinct classes, just a number that goes up and down. If we need to increase berry spawn rate to make up the numbers, that's ok - we can make the berry clusters bigger.Ed, 2026-10-05, recorded in DESIGN.md
#80 made strength a number per species and measured berries by fighting value, so a berry buys the same F for every species: two berries then two, where it had been one then three. The simulator found strength made no difference to sieges ("the model can't tell a cloud of 0.4s from a pair of 2s"); where it would matter is in per-body effects such as area attacks, which is arena work, not simulator work. No species has been given a strength yet. On 6 October Ed asked to "Double the amount of berries animals need to evolve", and #241 did: four and four.
The creature-state model
On 5 October issue #87 redesigned the game around inviting. The witch does no damage at all; she sends 💌s. Wild creatures become happy and defend their area; leashing is a further step; a woken area's young and adults turn enraged and besiege. Creatures never attack their own kind. Legends sleep, grow restless when their area is emptied of their kind, turn angry, and can be made happy with a relic. Before any of it was built, the balance builder modelled it in #80 as src/rules/states.ts, run by tools/balance/states.mjs, with six ways of splitting invites: defend, third, leash, babies, relay and mass.
The reports on #80 that morning reshaped the design almost hour by hour:
- Inviting is the whole game. "It is winnable with no witch damage at the default 300 s gap." An area's own fight turned on about half: invite less than half of it before its wave and its defenders lose; more and they won three times in four.
- The own-kind rule changed that: with it, inviting three quarters of an area made its defenders win only one time in three. "Happy defenders defend almost nothing against their own kin." Leashing pays only when her army is a different kind from the area it guards.
- Relics were too strong. Modelled as turrets nothing could hurt, three or four happy legends decided the run. Ed's ruling made happy legends wearable-down, and a sweep of the legend's shot settled on 10 damage every 15 seconds at up to three targets within 420 m: "the most sensitive knob in the game".
- Logistics and fight danger, below, and the first 💌 throughput model (
src/rules/throughput.ts,tools/balance/buffs.mjs): with no buffs an adult always took about six seconds to fill; the worst stack of buffs filled ten animals in that time.
How losing should feel
Ed ruled there would be no win screen, and gave the endgame its brief: "once things are unrecoverable it's not fun to wait to lose" (on #87). tools/balance/endgame.mjs traced runs every ten seconds and tested signals for a point of no return. The best was simple: soundsystems standing fall to a quarter of their peak. It fired in every lost run with one false alarm in 64, and from there losing took about ten minutes at five-minute waves.
Ed had also asked: "there's not much penalty for losing a soundsystem. Maybe it penalises you wave time?" #101 built his pick: each soundsystem lost brings the next wave 60 seconds sooner (party.lossPenalty), with a "−60 s" popping out by the wave bar. tools/balance/waves.mjs showed runs now end on their own (35 of 36 within five hours, against 27 without) while skill still matters, but that the tail after the point of no return was unchanged at about eight minutes. The builder proposed a last stand, sieges hurrying threefold once a quarter of the soundsystems stand, which took the tail to about two and a half minutes. Ed's answer, recorded on #87 and in #106:
let's do more playtesting before we make extra rulesEd, 2026-10-05
The 60 seconds stayed fixed rather than a fifth of the gap, the last stand and mixed late sieges were parked, and on 6 October the end screen itself gave way to the afterparty.
Logistics: area size against treetop speed
Treetop mode is for travel and ground mode for fighting, and the animals' own walk is never a knob. tools/balance/logistics.mjs (#80) swept area size against treetop speed to find what limits a defence. The answer surprised: not the witch, but her animals. At 5.6 m/s along border routes, the posse is what arrives late; treetop speed made no difference within noise.
| Area size | Posse there before the soundsystem falls (48 m/s) | Areas she visits a wave |
|---|---|---|
| 112 m | 92% | 2.16 |
| 168 m | 77% | 1.80 |
| 224 m | 54% | 1.54 |
| 280 m | 49% | 1.35 |
Around Ed's reference of 168 m areas and 48 m/s, the posse saves about three soundsystems in four and usually arrives after the first blows: "you can't be everywhere, but you can get to most fights in time if you choose." The recommendation was to keep it, and to treat area size as the difficulty dial for defending.
Fights, measured
The simulators treat every creature of a level alike, so the builder kept warning that F "understates specific counters". For fights the tools had to play the real rules. tools/balance/fight.mjs (#80) ran newGame and stepGame headless with the witch among N wild creatures: standing among eight adult boars she took 143 hits a minute; circling at 30% of her ground speed, almost none from chasers. Only swarms and ranged attackers land on a moving witch.

Once #89 brought the 💌s, tools/balance/fightbot.mjs (#106) played 288 real fights: she lands in a wild area holding its wave-W population and invites the lot. The dashboard's headline was blunt: "Landing in a wild area to invite it is very dangerous from wave 1 on", and standing to shoot was a knockout in four to six seconds at every wave and build. #115 added a skilled bot that picks off stragglers from the edge, blinks out of telegraphs and rises to the treetops to heal. It was knocked out in 0 to 17% of fights where the crude bots were in 17 to 100%, and invited two to six times as much, so the first dashboard had "overstated the danger for good players". Ed kept the rule that wild animals attack her on sight.

Later combat changes were checked the same way. #339 (Ed's playtest: walking away from a charge should be no escape) came with tools/balance/charges.mjs, which tried every escape against every charger; the full-run bots, which circle and kite, barely felt it, and an idle player still lost nothing until 27 minutes.

The full-run bot
The overnight balance pass of 6 October began with #197: tools/balance/runbot.mjs, which plays the real rules headless for a whole run as one of several bots: skilled (recruits without emptying any area, parks guards at the next soundsystem, kites, heals), crude (invites everything in the nearest area, then moves on), idle (stands at home) and, from #202, hover (idles over the treetops, out of every fight).
Its first finding was that a pack of young could take all three of her hits in the same instant. The fix, a DECISION FOR ED it shipped with, is witchHealth.grace: for half a second after a hit no other blow lands. The skilled bot went from about two knockouts in 30 minutes to none. In the changelog's words: "After a hit you get half a second's grace: a pack biting at once now costs you one hit, not all three." The same runs checked others' work: #138's new 💌 numbers made inviting about 18% slower in play, and #154's freezing of far creatures didn't change outcomes.
Then the uncomfortable finding, on #211: "how long a run lasts hardly depended on how she played." An idle player's run ended at about 42 minutes, a hovering one's at 40, and the skilled bot's, feeding her young and parking her whole posse at each siege, at 46. The per-wave log showed why:
Her party peaked at wave 2 and was ground down holding sieges; by wave 3 the enemy was four times her strength whatever she did. Four knobs were tried. Doubly tough soundsystems lengthened every run alike; cheaper evolving changed nothing; triple berries tripled evolutions, but the evolved animals were knocked down holding sieges and her posse ended as babies again. Only one widened the gap between good play and none: wild areas growing half as fast, a creature every other wave. Idle runs went to about 57 minutes and skilled ones to about 72, a gap of 15 minutes where there had been 4. It shipped as population.growth.perWave 0.5, flagged for Ed, and the simulator agreed: an idle player at one-minute waves now lasted 25.3 waves rather than 13.3.
Through the afternoon the runbot became the standard check on any change to the rules. #259 ran 36 one-hour runs to test Ed's three layers for legends (quick players get quest buffs, lucky relic finds give allies, losing players see the rage). Quest bots got their buffs but lost, because "time spent fetching dream creatures is time not inviting"; relic legends guarded their own remote areas, not the sieges; and, once #254's kin fix landed, a fallen area's legend woke angry about a minute later, every time, but hardly shortened the run, because angry legends never hit soundsystems. That led to #290's bombardment (an angry legend lobs at the nearest soundsystem when the witch is out of reach), tested at 300 damage and shipped at 150 because 300 crushed the over-inviting crude player rather than the losing one.
The bot game
It would be good to be able to start the game and watch the skilled bot play. Maybe there can be a button on the start screen for this (bot game).Ed, 2026-10-06, on #316
#316 moved the bots out of the balance tool into one pure rules module, src/rules/bot.ts: newBot(kind).decide(game) returns the same controls a player's input gives, with no clock and no randomness, and bot.doing says what she's up to. The runbot gave identical results on the moved code. The game's 🤖 Bot game plays it in real time with a seeded witch, a corner tag ("flying to the stags", "posting guards at wave 3", "dodging a wolf at wave 2"), and 1×, 2× and 4× speeds.

Watching it showed things the headless runs never could. She had been hopping up and down at the 45 m landing line about once a second; now she rises only past 60 m and walks the last stretch. And Ed noticed: "It's notable that it doesn't seem to get any legend buffs or feed creatures any berries." #343 found the bot game had started the bot with all its options off. Turned on, fetching all six relics took her first ten minutes and halved her army, so the bot game's skilled witch now does up to three quests, brings at most two relics to the legends by the coming waves, gives up a trip that sends her up to heal, and leads her young to berries. Her defence held: five soundsystems standing and no knockouts at 30 minutes on both seeds.
Who the balance is for
On 6 October Ed set the frame every balance change is now judged by first:
What we can probably surmise is that most human players are much worse than the skilled bot (or even the crude bot) and the very best human players are much much better. We want poor players to still have a good game and not get crushed immediately, and we want the best players not to be able to steamroll the entire map. I think the current game achieves this, mostly just by having a slow start but a map with many waves that keep getting harder.Ed, 2026-10-06, recorded in DESIGN.md
So the bots became a bracket rather than "a player". #346 added a novice, who plays the crude bot's way but fires only every other second, aims a metre off and never blinks, and a top end: the skilled bot with six legends happy from the start. It also added five strategies to the skilled bot (invite, feed, quest, sigil, mixed). The strategy table, 4 seeds and 60 minutes each, said the bracket was narrow: doing nothing lasted about 50 minutes and the skilled bot only about nine more; novice and crude ended sooner than idle, "they hand the sieges their creatures by being knocked out"; only the top player was clearly ahead. Parking guards at the next soundsystem (the sigil strategy) did best.
The poor end had a sharp edge. A player who walks up to the first creature beside home and stands inviting it is knocked out in about five seconds: a lone young lands a hit about every 2.4 s at point-blank range, faster than nine 💌s fill its meter. The builder put four options to Ed (young strike more slowly, the areas by home start with a baby only, four hits instead of three, or leave it as a lesson in dodging), defaulting to the second, and left the choice open in its FINAL.
Quests as a gamble
#346 first narrowed each legend's dream to a nearby kind, after one bot run spent its whole time fetching a moth for the glow-worm legend. Ed's view of quests reversed it within the hour:
They're a high-roll mechanic; you don't know how hard the quest will be before you go off to try and find the creature. But buffs are permanent, and stack, and having several buffs that synergise should make you feel very powerful, so it's satisfying to try and do quests and succeed. On the other hand, they're optional, and you can play a decent game without them, it's just less fun.Ed, 2026-10-06, recorded in DESIGN.md
So a legend dreams of any kind again, near or far; only the farthest tenth is cut (legends.questCap, five areas, 840 m), and the far ones pay more (legends.questFar: the farthest quest's buff half again as strong). Across 1,173 legends on 12 maps the median dream stayed about 2.5 areas away. The same evening #350, still open as this was written, took up Ed's playtest note that "the invitation dropoff rate (the speed at which animals become less invited) is too fast": it slows the meter's drain from 0.08 to 0.03 of a full meter a second, after four seconds. On two seeds the crude bot's invites rose by about half and the skilled bot's didn't move.
Curves over versions
The builders posted their tables in pull request comments, each on the rules of that moment. Read across, a few numbers trace the game's arc. Every point below is copied from a posted table; the last point of the first chart is a fresh run of node tools/balance/sim.mjs --quick on 7 October.
The simulator's skill spread, 60-second waves, growth only
Waves survived by an idle player and by one growing at 50 F a minute (12 seeds). The map changed underneath: 20 × 20 areas of 112 m to #75, 14 × 14 of 168 m from #80, a circle about 7.9 areas in radius today. From #59 to #80 each rule change nudged survival down and the 50-a-minute player stayed only a few waves ahead of doing nothing: Ed's "suddenly get steamrolled". #211's "before" reflects the state model's correction that grown creatures appear spread over their area, not at its middle; its "after" is half growth, where the 50-a-minute player reaches the cap of 60 waves on every seed. Today's run gives the idle player 26.7 waves (about 33½ minutes), and at five-minute waves loses at wave 7.7, at about 47 minutes.
The full-run bot: when a run ends
Minutes until the party's over, real rules, 4 seeds a bot. Idle is the mean for #197/#211 and the median after; skilled is the mean for #211, and the median of three finished runs and one still going for #346. Hollow points mean every seed was still going at the 60-minute cap (#211's "after" ran longer). #290's point is its run without bombardment; at 300 damage a lob the idle run ended at 52.6. Half growth bought the 15-minute gap; by #346, with the rune pickup, sigil weight, circular map and much else landed, the idle run had come back to about 50 minutes and the skilled bot ended just inside the hour.
Wave 8: her party against the sieges
Fighting value at wave 8 from the runbot's per-wave log (means of 4 seeds). Before half growth the skilled bot's party was worth 24 against 540; afterwards her party stayed in the fight. Only the top bot, with six happy legends from the start, outweighs the sieges.
The opening and the grace, in brief
| Measure | Before | After | Source |
|---|---|---|---|
| Skilled fight bot knocked out at wave 1, no buffs | 17% (a baby and two adults) | 0% (a young and an adult) | #115 |
| Careful opening: posse at wave 1, legends angered | 1.3, 0 (a young and an adult) | 5.7, 0 (a baby and a young) | #129 |
| Skilled runbot: knockouts in 30 minutes | 2.0 (no grace) | 0 (0.5 s grace) | #197 |
| Skilled runbot: soundsystems ruined in 30 minutes | 1.8 (one creature a wave) | 0.5 (half growth) | #211 |
| Distance from a legend to its dream's nearest area, farthest | 1,591 m | 840 m (questCap 5) | #346 |
The balance pass, paused
The overnight programme's phase 4 was meant to be the balance pass: the level gap between young and adult, the strategy bots' priorities, the slower invite drain, and Ed's bracket checked at both ends, the poor player's first waves and the top player's ceiling. It was set aside. Ed put balance on hold "until other gameplay things are completed": tuning numbers against systems that are still changing shape would only have to be done again. The bots, the simulators and the curves above are all still here, and the bot-driven balance work will be picked up again once the game's systems settle.