Skip to content

Tunnel Map Generation — 100-Floor Audit

Method: 100 floors generated across 3 biomes × 5 stages (biome = i % 3, stage = 1 + i % 5), each measured structurally — BFS distance fields, detour costs, reachability probes, and a linearity proof per rung. No visual inspection was used to reach any conclusion below. Code under audit: genTunnelMap / tunnelPlan at commit d5abc67.

UPDATE — Issues 1, 4 and 5 fixed (2026-07-30). Re-audited over another 100 floors; results in §4. Issues 2, 3, 6, 7 and 8 remain open by decision.

Numbering warning. These ISSUE numbers are the reference. A chat summary re-numbered a shortlist of four (arms / rivals / clock / gradient) and the mismatch caused the wrong item to be worked first. Cite issues by name, not number.


1. Correctness — clean

Every hard invariant holds on all 100 floors.

Property Result
Descend portal reachable from spawn 100 / 100
Every safe exit reachable 100 / 100
Every chest reachable 100 / 100
Every ground drop reachable 100 / 100
Walled-off floor regions (carved but unreachable) 0
Loops (wall a rung's rock band → portal must disconnect) 0
Rung count matches the rolled plan 100 / 100
Floors missing a guard 7 — all B1F1, which is guardsFromStep: 1 working as designed

Generation cost: median 0.8 ms, mean 1.8 ms, one 30.7 ms first-call outlier (JIT warm-up).

Diversity is real: start side 51/49, rung counts spanning 6–11, corridor width 44/56 across 3/4, rung-length spread median 11 tiles with only 1 floor below 5.


2. Issues, worst first

ISSUE 1 — Rung arms produce ~23 dead ends per floor (structural, high)

Measured: 23 dead ends median (8–38), deepest detour 36 tiles median, 60 max. A 60-tile detour is 2,400 px — roughly 24 seconds of walking to reach nothing.

Cause. Every rung is carved from its anchor wall to past the centre line. But the connector arriving from the rung below lands at that rung's far end, which sits mid-map. The stretch between the anchor wall and the incoming connector is therefore a long dead arm — one per rung, by construction.

Fix — rungs should span connector-to-connector, not wall-to-wall. Set x0 (or x1) to the incoming connector's position rather than the map margin. The spine becomes a clean zigzag with no arms, and the freed strip between wall and rung start becomes better room space than the current arrangement. This is the single highest-value change in this list: it also directly reduces Issues 2 and 3.

Knock-on: dead-end count should fall to roughly the deliberate stubs plus room approaches (~4–8), at which point deadEndPays can drop back to a small cap and each payout can be more generous.


ISSUE 2 — One clock for floors that differ 3× in length (balance, high)

Measured: shortest route 89 tiles (36 s of pure walking), longest 288 tiles (115 s), median 176 tiles / 70 s. Every one of them gets the same roundTime: 300.

Cause. roundTime is a flat config value, while floor length is derived from the rolled plan (rungs 6–11 plus rungsPerStage).

Fix — derive the clock the same way the map height is derived. tunnelPlan already knows the rung count, gaps and corridor width, so it can compute the spine length and set the floor's clock as base + spineTiles × perTile. An 11-rung floor then gets proportionally more time than a 6-rung floor instead of both getting 300 s.


ISSUE 3 — 48% of the map is open; it reads as halls, not tunnels (feel, medium)

Measured: open floor median 48.3% (38.7–56.4), with 14 floors above 52%.

Cause. The centre-crossing invariant forces rungLen ≥ W/2 + cw + 1, so corridors span well over half the width; combine that with corridorW 3–4 and a chamber up to 20 tall and half the rock is gone.

Fix. Mostly falls out of Issue 1 (connector-to-connector rungs are shorter). Beyond that, width is the lever, not rungLen — narrowing corridors requires widening the map, because the invariant is proportional to width. Worth exposing an explicit openPctTarget guard-rail if it stays high.


ISSUE 4 — Rival raiders never spawn (bug, medium)

Measured: 92 / 100 floors have zero rivals. Max on any floor: 1.

Cause. A real bug in the placement loop. It picks one candidate per rival and continues if the spot is within spawnSafe of the player or within botSpawnClear (380 px) of any creature. With ~34 mobs strung along the spine, nearly every single-shot candidate fails, so the loop exhausts without placing anyone. The entire rival/Tier-0 system is effectively dead on tunnel maps.

Fix. Retry per rival (the zone generator's freeTile does exactly this with a 500-attempt loop) instead of abandoning after one sample; fall back to the least-bad candidate rather than none.


ISSUE 5 — Reward gradient runs backwards (balance, medium)

Measured: 44 / 100 floors are chest-front-heavy (>1.5× more chests in the first half of the route than the second); only 52 are balanced. Mob median position is 0.58–0.60, so danger does rise with depth — the loot doesn't follow it.

Cause. Room rarity scales with depth (tier = rungIdx / rungs), but dead-end payouts — now the bulk of all chests — roll rarity independently of position, and dead ends are as common low as high.

Fix. Scale dead-end reward rarity (and chest-vs-drop odds) by the tip's route fraction, which the detour BFS already computes. Deep dead ends should pay meaningfully better than the first one you meet.


ISSUE 6 — Dead ends are never fully covered (polish, low)

Measured: 98 / 100 floors have at least one bare dead end; mean 3.1, worst 13.

Cause. Two interacting caps: deadEndPays tops out at 24 while some floors have 38 dead ends, and tips within 3 tiles of existing loot are skipped as already-served (a reasonable rule that my probe counts as bare when the neighbour is an extract rather than loot).

Fix. Largely dissolves with Issue 1 — far fewer dead ends means the cap stops binding. The residual "near an extract" case is cosmetic and arguably correct as-is.


ISSUE 7 — Every floor ends identically (variety, low)

The chamber is always an axis-aligned rectangle, always centred on the top rung, always entered from directly below. Across 100 floors only its dimensions changed.

Fix. Cheap variety wins: off-centre the chamber, vary the entry side, occasionally carve it as two overlapping rectangles, or gate it behind a short approach corridor so the guard is heard before seen.


ISSUE 8 — Minor observations (low)

  • 5 / 100 floors have zero spur rooms — every room landed on-path, so that floor has no optional detour rooms at all. Worth forcing at least one spur when rooms ≥ 2.
  • Mob counts reach 51 on the longest floors (median 34, density a steady 9 per 1000 tiles). Density is consistent, but 51 is a lot of individual actors for a mobile frame budget — worth a cap.
  • Bush tiles range 64–203. At the top end that's ~11% of open floor; fine now, but it scales with corridor area rather than with a target, so it will drift if width changes.

  1. Issue 1 (connector-to-connector rungs) — one change, improves Issues 1, 3 and 6 at once.
  2. Issue 4 (rival placement retry) — small bug fix, restores a whole parked system.
  3. Issue 2 (derived clock) — small, and removes a 3× fairness gap between floors.
  4. Issue 5 (depth-scaled dead-end rewards) — small, fixes the reward gradient.
  5. Issues 7 / 8 — variety and polish once the structure settles.

Nothing in this list threatens the design; the invariants that matter (solvable, strictly linear, everything reachable) are all holding at 100/100. These are quality problems on a sound skeleton.


4. Post-fix re-audit (Issues 1 and 4)

Same 100-floor method, after spanning rungs connector-to-connector (Issue 1) and making rival placement retry instead of giving up (Issue 4).

Metric Before After
Dead ends per floor (median) 23 14 −39%
Deepest detour (median / max, tiles) 36 / 60 28 / 54 the 24-second walk-to-nothing is gone
Bare dead ends (mean / worst) 3.1 / 13 0.9 / 1 worst case went from 13 to 1
Floors fully covered 2 10
Open floor (median) 48.3% 41.5% reads as tunnels, not halls
Rivals per floor (median) 0 3
Floors with zero rivals 92 0 Tier-0 is alive on tunnel maps
Chests / drops (median) 9 / 28 9 / 18 held after re-tuning, see below

Correctness held at 100/100 throughout: everything reachable, zero loops, zero orphaned regions. The 7 guardless floors are still exactly the B1F1 set.

Two follow-through adjustments the fix forced

  1. deadEndChest 0.22 → 0.38. The low rate existed only because the wall-to-wall spine produced ~20 dead ends a floor and a higher rate made a pinata. With ~13, the leaner map bottomed out at 1 chest on some floors, so each surviving dead end has to be worth more.
  2. Room rungs are now a preference, not a fixed set. Shorter rungs sometimes leave under 5 tiles of rock on both sides, and floors were generating with no rooms at all. The generator now falls through to the next-roomiest rung. Zero-room floors: 3 → 1.

Still open after the fix

  • Zero-room floors: 1/100 and zero-spur floors: 4/100 — the fallback narrowed but didn't close it.
  • Walk time 35–128 s (median 78, up slightly from 70 because rungs oscillate rather than reaching the walls). This is Issue 2, deliberately left open — one flat 300 s clock still covers a 3.7× spread.
  • deadEndPays [0, 24] no longer binds at ~13 dead ends a floor; it could be retired or lowered.

Issue 5 — reward gradient, fixed after the fact

Dead-end payouts now scale with how far along the climb the tip sits, using the route fraction the detour BFS already computes. deadEndDepthBias: 0.8 controls it; 0 reproduces the old flat behaviour exactly.

Front half Back half Ratio
Chest rate (formula) 0.304 0.456 1.50× (was 1.00× flat)
Chests (200k simulated, uniform tips) 31,707 45,867 1.45×
Reward value index 222,479 520,821 2.34×

Chest odds are monotonic across depth (0.228 → 0.532), and rarity steps too: below 0.42 of the route it's almost all common, above 0.72 it's rare-or-epic.

End-to-end confirmation (100 fresh floors). The debt above is now paid, and the real numbers are more interesting than the formula's:

Front half Back half Ratio
Chest count 349 530 1.52×
Ground-drop count 1009 894 0.89×
Total reward value 2571 6900 2.68×

Front-heavy floors fell 44 → 30, back-heavy rose to 29, balanced 58. Chests and value are now clearly back-weighted; ground drops remain slightly front-weighted because dead ends themselves are more numerous low on the spine — the per-dead-end bias corrects the value, not the count. That's the right outcome: the deep half of a floor pays 2.7× the shallow half.

Rivals disabled (2026-07-30): prog.botsFromBiome: 99. Any value above the biome count switches rivals off in both generators, so the Tier-0 layer (neutrality, tap-to-MARK, safe crossfire, Opportunists) is idle. Verified 0 rivals across 100 floors. Issue 4's fix stays in the code, dormant.


5. Remaining open items

Item Status Note
Issue 2 — one clock, 3.7× length spread open by decision routes now 35–128 s of walking, all on one 300 s clock
Issue 7 — every floor ends identically open chamber always the same rectangle, centred on the top rung, entered from below
Issue 8 — 4/100 floors have zero spur rooms open every room landed on-path; force one spur when rooms ≥ 2
Issue 8 — mob count reaches 44 open density is steady (~9/1000 tiles); the cap is a frame-budget question
Issue 8 — bush count scales with area, not a target open 58–192 tiles; will drift if width changes
New — 1/100 floors has zero rooms open the roomiest-rung fallback narrowed it but didn't close it
New — deadEndPays [0,24] no longer binds cosmetic ~13 dead ends a floor now; the cap could be retired
Issue 3 — open floor % addressed 48.3% → 41.5% as a side effect of the connector-to-connector fix
Issue 6 — bare dead ends addressed mean 3.1 → 0.9, worst 13 → 1
Verification debt cleared gradient confirmed end-to-end; live 300 s raid re-run clean after the spine change