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/tunnelPlanat commitd5abc67.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
widthchanges.
3. Recommended order¶
- Issue 1 (connector-to-connector rungs) — one change, improves Issues 1, 3 and 6 at once.
- Issue 4 (rival placement retry) — small bug fix, restores a whole parked system.
- Issue 2 (derived clock) — small, and removes a 3× fairness gap between floors.
- Issue 5 (depth-scaled dead-end rewards) — small, fixes the reward gradient.
- 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¶
deadEndChest0.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.- 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 |