# DEEPFALL — Simulation Findings

What the balance-by-bot rig actually said, after 9 tuning iterations over ~thousands
of seeded games per sweep. This is the honest post-mortem: what the design got right,
what the sim broke, what I fixed, and what's still open.

The deliverable was never "a perfectly balanced game." It was: **design a mashup,
then let a simulator tell me the truth about it.** It did.

---

## 1. The headline

Final config (`configs/default.json`), 2000 seeds × 24 seat-permutations per strategy
sweep, 2000 games per combo:

| Metric | Result | Verdict |
|---|---|---|
| **Game length** | 10.9 rounds (10–11) | ✅ in the 8–14 target band; the Maw ends it cleanly, every time |
| **Victory margin** | 13.7% of winner's score | ✅ tight — **no runaway leader** |
| **Feel-bad rate** (Taken w/o Last Stand) | **0.00 / game** | ✅ the one true feel-bad is fully designed out |
| **Deep content reachability** | only **6.5%** of deep-floor claims by round-1 parties | ✅ the deep game is *not* decided at game start |
| **Combo spread** (best − worst of 36) | **32.5 pts** (8.1%–40.6%) | 🟡 every combo viable; still a soft Lineage/Calling tilt |
| **Strategy band** | WorthyEnd 46.7 / PressOn 25.8 / Balanced 24.8 / CashOut 2.8 | 🟡 aggression is the strongest line; pure passivity is weak |

Three of the design's central promises held up under simulation — **no runaway, no
elimination feel-bad, and a living deep game**. Two things stayed stubborn — the
draft has a soft power tilt, and the four play-styles aren't a flat band. Both are
understood (below), not mysterious.

---

## 2. The three biggest things the sim caught (and the fixes)

### A. Value and time were correlated → the arc fizzled and the back half ran away
Room glory was `base × depth` and the Maw eats deepest-first, so the richest rooms
scored *earliest*; the game crescendo'd in the middle and ended on its cheapest floors,
and the Worthy-End catch-up valve closed once the deep floors were gone. Both critics
flagged it before a line of code ran.

**Fix (the single best rule in the game):** on collapse, a fraction of each room's
*unclaimed* value **rises to the floor above** as "refugee plunder." The Maw's frontier
is now always the swelling hot zone, all the way to Floor 1. The data confirms it: the
claim distribution is **non-monotonic** — Floor 4 (162k claims) draws *more* than Floor 3
(96k), because value pools wherever the frontier sits, and Floor 1 draws the most of all
as everyone converges for the finale. The endgame crescendos instead of fizzling, and
margins stayed tight (13.7%).

### B. The deep game was nearly a game-start-only contest
A fresh party from the Cash-Out loop entered at Floor 1 and physically could not reach
the deep live floor before it collapsed (the balance critic walked the move-math). So
the Decline loop would have fed you parties that could only farm shallow rooms.

**Fix:** re-entering parties insert at `max(1, frontier − 1)`. Result: only **6.5%** of
deep-floor claims come from round-1 parties — i.e. **93.5% of the deep game is played by
later-drafted crews.** The Decline loop stays relevant the whole game.

### C. The multi-collapse feel-bad
`while accum ≥ 1.0` could eat two floors in one round late-game and Take a "flee next
turn" party with no Last Stand. **Fix:** cap one collapse per round (carry the
remainder). The feel-bad rate is a flat **0.00** across every config tested since.

---

## 3. The balance-by-bot trajectory

Each row is a tuning pass; the simulator drove every decision. Regressions are left in
on purpose — they're the method working (a change that *looked* good and the data
vetoed).

| Iter | Lever changed | Combo spread | Weakest combo | WorthyEnd / CashOut strat |
|---|---|---:|---:|---|
| 1 | initial design | 77.8 | 0.5% | 41.6 / 1.8 |
| 2 | sub-linear depth; neuter `ignore_threat`; mixed combo field | 59.8 | — | 44.8 / 0.0 |
| 3 | Champion-recruit Wardancer; Cautious plays the frontier | 46.5 | — | 44.8 / 3.1 |
| 4 | half-strength Fallen markers | **56.2** ⚠ | — | 44.0 / 3.5 |
| 5 | revert Fallen; cap marker overflow; raise dividend | 47.2 | — | 37.0 / 5.0 |
| 6 | bots compare worthy-vs-bank payouts | **55.5** ⚠ | — | 38.5 / 4.2 |
| 7 | Wardancer +1 marker strength; revert dividend | 43.4 | 4.9% | 46.1 / 2.8 |
| 8 | Dvergar −1 Cunning; Wisp gets a Warrior | 35.9 | 5.0% | 45.4 / 2.8 |
| 9 | Dvergar −1 Faith (gentler); trim Reaver | **32.5** | **8.1%** | 46.7 / 2.8 |

Net: combo spread **−58%** (77.8 → 32.5), weakest combo **×16** (0.5% → 8.1%), and the
dead cells (Sylphid ~1%, Wardancer ~0.3%) all revived into the playable band.

The two ⚠ regressions taught real lessons: half-strength Fallen markers (iter 4) handed
Warden's ×2 markers an immortal hold and spiked it to 56%; and making bots *compare*
exit payouts (iter 6) — correct in itself — exposed that Wardancer's whole value was a
rare conditional, which is what forced the iter-7 fix.

---

## 4. Final combo heatmap (focal combo as Balanced vs. a mixed random field)

```
           Reaver     Lorekeeper Wardancer  Hierophant Warden     Prospector
  Dvergar  40.6%      33.7%      18.4%      32.5%      30.9%      31.6%
  Wisp     20.6%      15.9%      11.9%      14.5%      14.4%      14.2%
  Revenant 17.3%      13.9%       8.9%      12.2%      12.7%      11.7%
  Gnoll    31.7%      28.7%       9.6%      23.9%      24.4%      24.8%
  Sylphid  23.3%      18.8%      12.4%      15.0%      15.3%      14.6%
  GolemBorn24.9%      20.4%       8.1%      19.6%      19.1%      32.0%

  Lineage avg : Dvergar 31.2  Gnoll 23.9  GolemBorn 20.7  Sylphid 16.6  Wisp 15.3  Revenant 12.8
  Calling avg : Reaver 26.4  Lorekeeper 21.9  Prospector 21.5  Hierophant 19.6  Warden 19.5  Wardancer 11.6
```

(Mean focal win-rate sits below 25% because the focal always plays the generalist
Balanced bot while the field includes the more aggressive WorthyEnd/PressOn strategies —
so read the *vs-mean* column in `combo_winrates.csv`, not the raw 25% line.)

---

## 5. What's still open (and why)

### The Cash-Out vs. Worthy-End substitution (the deepest finding)
The strategy band stays lopsided (WorthyEnd 46.7%, CashOut 2.8%) for a structural
reason the sim made undeniable: **for a glory-maximizing agent, Cash-Out and Worthy-End
are substitutes** — both convert a doomed party into points. So *every* lever that helps
the safe banker (e.g. the depth dividend) directly nerfs the martyr, and vice-versa.
They sat on a trade-off frontier across iterations 5–10: I could balance the *strategies*
or the *Wardancer calling*, but not both with the same `worthy_factor`.

The intended differentiator — Worthy-End's **denial** (the Maw salts your held rooms so
no rival inherits them) — is real in the rules but **invisible to the bots**, because a
win-rate maximizer doesn't model the opponent's loss, and you mostly salt rooms you were
soloing anyway. *This is the headline design lesson:* the two exits only become a true
triangle (not a binary) when denial is made to **matter to your own score** — e.g. score
denial explicitly (gain a cut of the value you deny), or make Worthy-End pay on
*cumulative* depth dived across the whole game (rewarding a play-pattern banking can't
copy). That's the first thing I'd prototype next.

### The soft draft tilt
Dvergar (threat-reduction is just broadly useful) and Reaver (loot-on-claim is a
flywheel) sit on top; Revenant and Wardancer sit low. No combo is dead (worst is 8.1%),
so the draft is *spicy, not solved* — acceptable for a Small-World-style game where some
combos are stronger — but a shipping version would want Dvergar's hook costed harder and
Revenant's Dread-to-Mettle made to actually pay.

### Pacing variance is too tight
Game length is 10–11 rounds with almost no spread. Real push-your-luck wants more
variance in *when* the Maw breaks through. The `maw_dread_push` and `press_dread_push`
knobs are the levers; I'd widen them and accept a 9–14 spread.

---

## 6. Reproduce

```bash
cd deepfall
cargo test                                       # 11 tests: determinism, pacing, no-double-count
cargo run --release -p deepfall-sim -- --out out --seeds 2000
python3 analysis/analyze.py out                  # the heatmap above, no dependencies
```

Every number here is a pure function of `(configs/default.json, seed)` — same input,
same output, on any machine.
