Long expeditions D1-e: T5 zones to new 36-44 room band

Closes D1. Both T5 zones follow the D1-a/b/c/d pattern: extend the
graph so the longest entry→boss walk lands in band, keep the zone's
authored topology intact. Also backfills the missing per-node
RegionID authoring against dnd_expedition_region.go — the D1-d
deferral note flagged this as a prerequisite for T5.

  dragons_lair (T5): 12 → 47 nodes, longest 38 (band 36-44)
  abyss_portal (T5): 13 → 51 nodes, longest 39 (band 36-44)

Topology preserved per zone:
- Dragon's Lair: 4-region authoring (every node carries a valid
  RegionID); binary-converging fork1 (ash_bridge TRAP / treasure_vault
  Perception 15) still converges at the R3 wyrmlings_nest elite;
  capstone fork2 still 3-way (LockNone / CHA 16 / Perception 17
  SECRET LootBias 2.5) with each spur preserving its own named
  capstone node. New R4 infernax_doors MERGE consolidates the long
  final hall instead of triplicating it.
- Abyss Portal: 4-region authoring; three sequential forks preserved
  (binary / binary / ternary) — fork1 Perception 16, fork2 CON 16
  (full STR/DEX/CON/INT/WIS/CHA roster still covered), fork3 capstone
  3-way with reality_seam SECRET LootBias 3.0. New R4 belaxath_doors
  MERGE consolidates the final tear-approach.

MinRooms/MaxRooms re-pitched 9-10 → 36-44 so the dice fallback also
lands in band for any future graphless variant.
This commit is contained in:
prosolis
2026-05-27 18:13:33 -07:00
parent d99f975074
commit 42fb805ee0
5 changed files with 607 additions and 136 deletions

View File

@@ -7,8 +7,59 @@ func TestAbyssPortalGraph_Registered(t *testing.T) {
if !ok {
t.Fatal("zoneAbyssPortalGraph not registered")
}
if len(g.Nodes) != 13 {
t.Errorf("nodes = %d, want 13", len(g.Nodes))
// Long-expedition D1-e widened this zone from 13 → 51 nodes so the
// longest entry→boss walk lands in the T5 [36,44] traversal band.
if len(g.Nodes) != 51 {
t.Errorf("nodes = %d, want 51", len(g.Nodes))
}
}
func TestAbyssPortalGraph_LongestPathInBand(t *testing.T) {
g := zoneAbyssPortalGraph()
got := graphLongestPath(g)
if got < 36 || got > 44 {
t.Errorf("longest path = %d, want in T5 band [36,44]", got)
}
}
// TestAbyssPortalGraph_AllNodesHaveRegion confirms D1-e backfilled the
// missing RegionID authoring per dnd_expedition_region.go: every node
// carries a non-empty RegionID matching the registry.
func TestAbyssPortalGraph_AllNodesHaveRegion(t *testing.T) {
g := zoneAbyssPortalGraph()
validRegions := map[string]bool{
"abyss_outer_rift": true,
"abyss_demon_assembly": true,
"abyss_wardens_post": true,
"abyss_the_tear": true,
}
for id, n := range g.Nodes {
if n.RegionID == "" {
t.Errorf("node %s has empty RegionID — D1-e requires region authoring on every node", id)
}
if !validRegions[n.RegionID] {
t.Errorf("node %s RegionID = %q, not in dnd_expedition_region.go registry", id, n.RegionID)
}
}
}
// TestAbyssPortalGraph_AllFourRegionsRepresented confirms each authored
// region has at least one node.
func TestAbyssPortalGraph_AllFourRegionsRepresented(t *testing.T) {
g := zoneAbyssPortalGraph()
regions := map[string]int{}
for _, n := range g.Nodes {
regions[n.RegionID]++
}
for _, r := range []string{
"abyss_outer_rift",
"abyss_demon_assembly",
"abyss_wardens_post",
"abyss_the_tear",
} {
if regions[r] == 0 {
t.Errorf("region %q has no nodes — multi-region invariant broken", r)
}
}
}