J3 D8-f #2: T4 difficulty lift (leaders-define-band) + feywild fork1 soft-lock fix

T4 monster tuning so martial leaders land in the 60-75% band (underdark
59/80, feywild 65/84 at L10/L12, n=50); casters trail (class-side gap that
monster tuning provably can't compress -- Pass 1 showed casters pinned at 0%
while martials moved). T5 deferred (walls everyone at its L12 floor; needs an
L15-16 corpus).

- dnd_bestiary.go: underdark elite/boss HP+AC up + caster-lethal proc cuts
  (mind_flayer/drow_mage/roper); feywild HP+AC up.
- bestiary_srd.go: feywild multiattack profiles (fomorian/night_hag/green_hag)
  + Thornmother 2->3 lashes -- HP/AC alone didn't move feywild's low-damage
  roster; multiattack is what pulls facerolling martials into band.
- zone_graph_feywild_crossing.go: free fork1's marsh edge. fork1 was the only
  fork in the game with every exit skill-locked (CHA+Perception, no LockNone)
  and deterministic no-retry rolls -- a prod SOFT-LOCK stranding ~60% of
  players (low CHA+WIS). Kept grove's CHA bargain as a bonus route.
- expedition_sim.go: firstUnlockedForkChoice -- sim fork policy picks the
  first unlocked option instead of blind 'go 1' (which looped forever on
  locked forks); halts fork_all_locked if none.
- tests: graph-wide TestZoneGraphs_NoSoftLockedFork + fork1 free-path guard.

Writeup: sim_results/d8f_findings.md
This commit is contained in:
prosolis
2026-05-28 19:02:58 -07:00
parent a46b773750
commit 4934383a9a
6 changed files with 132 additions and 33 deletions

View File

@@ -55,16 +55,23 @@ func TestFeywildCrossingGraph_PartialOverlap(t *testing.T) {
}
}
// TestFeywildCrossingGraph_NoFreeChoiceAtFork1 captures the design
// intent: both fork1 outgoing edges are locked. The player must succeed
// at CHA or Perception to enter; no LockNone fallback.
func TestFeywildCrossingGraph_NoFreeChoiceAtFork1(t *testing.T) {
// TestFeywildCrossingGraph_Fork1HasFreePath guards the no-soft-lock
// invariant: fork1 must offer at least one LockNone exit. The original
// design locked BOTH edges (CHA + Perception) with no fallback — fork
// rolls are deterministic with no retry, so a character failing both was
// permanently stranded (D8-f part 2 found this stranded ~60% of runs).
// Every other zone fork has a free path; fork1 must too.
func TestFeywildCrossingGraph_Fork1HasFreePath(t *testing.T) {
g := zoneFeywildCrossingGraph()
free := 0
for _, e := range g.outgoingEdges("feywild_crossing.fork1") {
if e.Lock == LockNone {
t.Errorf("fork1 has unlocked edge to %s — expected all locked", e.To)
if e.Lock == LockNone || e.Lock == "" {
free++
}
}
if free == 0 {
t.Error("fork1 has no free (LockNone) exit — soft-lock: a player failing every skill check is permanently stranded")
}
}
// TestFeywildCrossingGraph_FirstCHALock confirms this zone uses