M2 capstone — the difficulty curve reproduces: Keep on the Shadowfell

the project's first designed experiment · zones: 3 × 50 seeds (06:43Z) · areas: all 19 × 30 (15:41Z, full power) · level-1 five-PC party, both sides Aggressive

The question behind this benchmark was the project's biggest: does the honest engine reproduce the difficulty the books designed? Keep on the Shadowfell is the classic published adventure — a village tutorial outside, a keep that punishes under-leveled parties inside. We assembled each zone's full encounter and ran it 50 times. The answer on first contact: yes, and the ordering is monotone in XP. At 450 XP the level-1 party wins comfortably; at 2,650–3,850 XP — eight times the threat — it wipes, and the wipe rate orders with XP exactly as the book intends: the village is the tutorial, the keep demands leveling.

Confirmed, not just reproduced. The declared re-run trigger fired the same day — a 3-of-5 hero re-stat plus an action-delivery engine change. All 150 re-run encounters match the first pass to the outcome vector: 37/0/13 · 0/24/26 · 0/32/18 — 74% / 48% / 64%, identical. The monotone-in-XP curve is doubly pinned.
74%
player wins in the village (37/50) — the tutorial is winnable at level 1
0% → 64%
wipe rate, monotone in XP: village → deep → upper
8.6×
XP threat, village 450 → upper 3,850 — one fixed level-1 party against all of it

Outcomes by zone — the curve

player winswipes (enemy wins)turn-capped
0%25% 50%75%100% 37 · 74% 13 · 26% 24 · 48% 26 · 52% 32 · 64% 18 · 36% village450 XP · 5 monsters keep-deep2,650 XP · 11 keep-upper3,850 XP · 16
Outcome shares per zone (50 seeds each, both sides Aggressive), ordered by XP budget. Zero-width segments are absences, not rounding: the village records no wipes at all; neither keep zone records a single level-1 player win.
zoneXPmonstersplayer winswipescappedrounds (mean)
village450*537 (74.0%)0 (0%)13 (26%)10.5
shadowfell-keep-deep2,650110 (0%)24 (48.0%)26 (52%)8.1
shadowfell-keep-upper3,850160 (0%)32 (64.0%)18 (36%)9.3

* as-summed at run time: the manifest's 450 covered the four statted kobolds; the Orcus Underpriest (level 5) also fielded in the village carried null XP in the manifest — its unit data says 200. The pre-build audit caught the gap; the manifest was corrected the same day (2b06191: underpriest 200, village budget 650). The encounter that fought never changed — only the accounting did. The spread with corrected budgets: 650 → 2,650 → 3,850, ordering untouched.

The full 19-area curve — the fifth pin

The zone benchmark's caveat was "zones aggregate their areas." The OCR and roster passes landed, and the complete pre-registered test ran: all 19 areas, n=30 each, full power (66/66 monster refs resolve). The shape is the book — early delve areas near-100% for the level-1 party, a mid-band grind, and the deep keep at 0–10%, death for the unleveled, exactly as published.

ρ = +0.873
Spearman XP-vs-difficulty across the 15 XP-carrying areas — H1 confirmed strongly (registered threshold: > 0.4)
H2 ✓
every XP ≤ 400 area ≥ 36.7% win; every XP ≥ 800 area ≤ 30% — band checks pass, no span anomalies
10/105
pairwise inversions — all between identical-XP areas (325 vs 325, 175 vs 175): tie noise, not curve violations
100% 50% 0% 0 XP300 600900 1,181 book-easy band book-deadly band 4 zero-XP areas † 9 13·10·19 12 17 1·2 3·14 approach 30% 8·11 18 · keep-ruins — 0%
Player win rate vs XP budget, all 19 areas (30 seeds each; the four zero-XP skeleton-family areas run but sit outside the stats). Win rates re-derived from the per-area results.json before this chart — all 19 match the README exactly; XP values are the assembly's own budgets.
Two honest flips, both printed. The first analysis pass ranked win-rate and printed ρ = −0.873 — the registration defines difficulty as 1 − win; the sign was self-caught and corrected before publication. And the preliminary run's H3 failure (a span anomaly) did not survive the drift fix plus full data — the flip is reported in both directions. Only one span remains noteworthy: area 12 wins 36.7% where its identical-XP neighbor 17 wins 60% — flagged for the content lane, not hidden from the curve.

Damage — how each zone feels

party damage / encmonster damage / enc
0 60 120 180 240 166.5 ± 23.4 37.1 ± 14.0 109.0 ± 55.0 104.9 ± 36.2 138.1 ± 49.0 114.7 ± 30.0 village keep-deep keep-upper party (upper dots) · monsters (lower dots) — single seeds reached 259
Damage dealt per encounter by faction (mean ± sd over 50 seeds). The feel of each zone in one glance: the village is a rout (166 v 37), keep-deep is mutual attrition (109 v 105), keep-upper leans on the party (138 v 115) — with single-seed party damage peaking at 259, enough to chew through a full level-1 party's hit points.

Two method finds the benchmark surfaced

1. The 50-turn cap was hiding the entire result. The keep zones field 16–21 units; a fixed 50-turn limit resolved almost nothing — the first pass came back 100% capped, a wall of "no verdict". Scaling the cap to unit count (10u→120, 16u→192, 21u→252) let the fights finish, and the curve appeared. A benchmark that measures nothing but its own ceiling isn't a benchmark.
2. Encounter mode must read the assembled map, not the manifest rows. Faction joins in `--encounter` initially fired against the raw manifest lists; once they resolve from the assembled map, both sides field who actually stands on the board. Small harness bug, total difference to every result.

What this does not claim — and what unlocks it

Zones aggregate their areas. The book intends per-area encounters (9–7 areas per delve); the manifests carry area lists but not per-area rosters yet. Zone-level is the coarsest honest cut — the per-area pass (OCR/render of the roster pages) will move every number and is the real curve test. The party is fixed at level 1 across all zones: this is each zone's un-leveled difficulty. The book-true comparison — a party leveled per delve — needs the level-2/3 five-PC sets, which is content-lane work the PC builder (#46) now feeds. Terrain stays off at this phase (standard-fan placement, variant overrides informational).