mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
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
128 lines
4.4 KiB
Go
128 lines
4.4 KiB
Go
package plugin
|
|
|
|
import "testing"
|
|
|
|
func TestFeywildCrossingGraph_Registered(t *testing.T) {
|
|
g, ok := zoneGraphRegistry[ZoneFeywildCrossing]
|
|
if !ok {
|
|
t.Fatal("zoneFeywildCrossingGraph not registered")
|
|
}
|
|
// Long-expedition D1-d widened this zone from 9 → 53 nodes so the
|
|
// longest entry→boss walk lands in the T4 [28,34] traversal band.
|
|
if len(g.Nodes) != 53 {
|
|
t.Errorf("nodes = %d, want 53", len(g.Nodes))
|
|
}
|
|
}
|
|
|
|
func TestFeywildCrossingGraph_LongestPathInBand(t *testing.T) {
|
|
g := zoneFeywildCrossingGraph()
|
|
got := graphLongestPath(g)
|
|
if got < 28 || got > 34 {
|
|
t.Errorf("longest path = %d, want in T4 band [28,34]", got)
|
|
}
|
|
}
|
|
|
|
// TestFeywildCrossingGraph_HagCircleSharedReach verifies hag_circle
|
|
// has TWO incoming edges — one from each first-stage path. This is the
|
|
// woven-fork shape's signature; if a future edit splits hag_circle so
|
|
// each path has its own copy, this fails (and the shape comment needs
|
|
// to be re-authored).
|
|
func TestFeywildCrossingGraph_HagCircleSharedReach(t *testing.T) {
|
|
g := zoneFeywildCrossingGraph()
|
|
incoming := map[string][]string{}
|
|
for from, outs := range g.Edges {
|
|
for _, e := range outs {
|
|
incoming[e.To] = append(incoming[e.To], from)
|
|
}
|
|
}
|
|
hagSources := incoming["feywild_crossing.hag_circle"]
|
|
if len(hagSources) != 2 {
|
|
t.Errorf("hag_circle incoming = %d, want 2 (woven fork)", len(hagSources))
|
|
}
|
|
}
|
|
|
|
// TestFeywildCrossingGraph_PartialOverlap verifies time_eddy is
|
|
// reachable only via the grove and illusion_garden only via the marsh.
|
|
// This is what makes first-stage choice meaningful even though
|
|
// hag_circle is shared.
|
|
func TestFeywildCrossingGraph_PartialOverlap(t *testing.T) {
|
|
g := zoneFeywildCrossingGraph()
|
|
if reachable(g, "feywild_crossing.wisp_marsh", "feywild_crossing.time_eddy") {
|
|
t.Error("time_eddy should NOT be reachable from wisp_marsh — grove-exclusive")
|
|
}
|
|
if reachable(g, "feywild_crossing.glamoured_grove", "feywild_crossing.illusion_garden") {
|
|
t.Error("illusion_garden should NOT be reachable from glamoured_grove — marsh-exclusive")
|
|
}
|
|
}
|
|
|
|
// 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 || 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
|
|
// LockStatCheck CHA — completing CHA in lock-kind coverage by G8f.
|
|
func TestFeywildCrossingGraph_FirstCHALock(t *testing.T) {
|
|
g := zoneFeywildCrossingGraph()
|
|
found := false
|
|
for _, outs := range g.Edges {
|
|
for _, e := range outs {
|
|
if e.Lock == LockStatCheck && lockDataString(e.LockData, "stat") == "CHA" {
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected at least one LockStatCheck CHA edge — Feywild theme")
|
|
}
|
|
}
|
|
|
|
// TestFeywildCrossingGraph_TrapAnchor verifies D1-d added the missing
|
|
// Trap node. Original G8f graph had elite/secret but no trap.
|
|
func TestFeywildCrossingGraph_TrapAnchor(t *testing.T) {
|
|
g := zoneFeywildCrossingGraph()
|
|
var trapCount int
|
|
for _, n := range g.Nodes {
|
|
if n.Kind == NodeKindTrap {
|
|
trapCount++
|
|
}
|
|
}
|
|
if trapCount != 1 {
|
|
t.Errorf("trap nodes = %d, want 1", trapCount)
|
|
}
|
|
}
|
|
|
|
// TestFeywildCrossingGraph_FaeCourtMerge verifies all three second-
|
|
// stage endings (hag_circle, time_eddy, illusion_garden) converge at
|
|
// the fae_court merge before the final boss approach. D1-d added this
|
|
// merge to avoid triplicating the long pre-boss walk.
|
|
func TestFeywildCrossingGraph_FaeCourtMerge(t *testing.T) {
|
|
g := zoneFeywildCrossingGraph()
|
|
for _, ending := range []string{
|
|
"feywild_crossing.hag_circle",
|
|
"feywild_crossing.time_eddy",
|
|
"feywild_crossing.illusion_garden",
|
|
} {
|
|
if !reachable(g, ending, "feywild_crossing.fae_court") {
|
|
t.Errorf("%s does not reach fae_court merge", ending)
|
|
}
|
|
}
|
|
if !reachable(g, "feywild_crossing.fae_court", "feywild_crossing.boss") {
|
|
t.Error("fae_court → boss path missing")
|
|
}
|
|
}
|