Long expeditions D1-b: crypt_valdris + T2 zones to new room bands

Three more zones follow the goblin_warrens pattern from D1-a: add the
missing Trap anchor and deepen the graph so the longest entry→boss walk
lands in its tier's target band.

  crypt_valdris  (T1):  8 → 17 nodes, longest 13 (band 12–14)
  forest_shadows (T2):  9 → 19 nodes, longest 16 (band 16–20)
  sunken_temple  (T2): 10 → 26 nodes, longest 16 (band 16–20)

Topology preserved per zone:
- Crypt diamond + Perception-DC-15 secret off side_chapel.
- Forest asymmetric main fork (long branch carries the elite) +
  WIS-DC-14 secret pre-boss.
- Sunken Temple sequential forks with no mid-path merge (four leaves
  all terminate at the single boss room).

MinRooms/MaxRooms re-pitched to the §2 bands so the dice fallback also
lands in band for any future graphless zone.
This commit is contained in:
prosolis
2026-05-27 17:32:29 -07:00
parent 4999368031
commit 89edc76829
7 changed files with 275 additions and 102 deletions

View File

@@ -258,8 +258,8 @@ func zoneCryptValdris() ZoneDefinition {
Faction: "Undead",
Atmosphere: "Stone corridors, dripping water, candles that shouldn't still be burning.",
Hook: "The iron gate hangs open — someone left in a hurry. Carved into the stone above: \"HERE LIES VALDRIS. DO NOT.\" The rest has been chiseled away. I decline to speculate.",
MinRooms: 6,
MaxRooms: 7,
MinRooms: 12,
MaxRooms: 14,
Enemies: []ZoneEnemy{
// Phase 4-B (outlier fix): the live roster was the only
// dual-killer-elite zone (wight + flameskull, both CR3+ on
@@ -314,8 +314,8 @@ func zoneForestShadows() ZoneDefinition {
Faction: "Beasts, Fey-corrupted creatures, Bandits",
Atmosphere: "Ancient forest, twisted paths, eerie silence, bioluminescent fungi, things in the canopy.",
Hook: "The forest was beautiful once. Travelers still say so, usually right before they stop saying anything at all. The trees lean in when you're not looking. I have noted this is not a metaphor.",
MinRooms: 6,
MaxRooms: 8,
MinRooms: 16,
MaxRooms: 20,
Enemies: []ZoneEnemy{
// Phase 4-B (outlier fix): standard pool was carrying two
// real killers — Displacer Beast (38% win as a standard
@@ -377,8 +377,8 @@ func zoneSunkenTemple() ZoneDefinition {
Faction: "Kuo-toa, Water Elementals, Aboleth-touched",
Atmosphere: "Flooded stone chambers, barnacled pillars, salt smell, alien glyphs, things that swim in the dark water.",
Hook: "The tide went out thirty years ago and never fully came back. The temple stayed wet anyway. Something down there keeps it that way. I suggest waterproofing your spellbook.",
MinRooms: 6,
MaxRooms: 8,
MinRooms: 16,
MaxRooms: 20,
Enemies: []ZoneEnemy{
{BestiaryID: "kuo_toa", SpawnWeight: 7},
{BestiaryID: "kuo_toa_whip", SpawnWeight: 4},

View File

@@ -1,54 +1,101 @@
package plugin
// Phase G7 — Crypt of Valdris POC graph.
// Crypt of Valdris branching graph.
//
// First hand-authored branching graph. Topology per
// gogobee_branching_zones_plan.md §3:
// Long-expedition plan D1-b: extended from the original 8-node POC sketch
// to the new T1 length band (1214 rooms traversed). Topology preserves
// the original diamond — main_hall (elite) vs side_chapel (perception
// fork), with a Perception-DC-15 secret dangling off side_chapel — and
// adds the missing Trap anchor + extra exploration on both arms so the
// longest entry→boss walk lands at 13.
//
// entry → corridor → fork
// ├──[unlocked]── main_hall (elite) ──┐
// │ ├── antechamber → boss
// └──[Perception DC 12]── side_chapel ─┘
//
// └──[Perception DC 15]── secret_chamber → antechamber
// entry → processional_stair → bone_corridor →
// grave_dust_trap (TRAP) → ossuary_walk → fork
// ├──[unlocked]── main_hall (ELITE)
// │ → reliquary_hall → ash_passage ──┐
// │ ├── antechamber
// └──[Perception DC 12]── side_chapel │ → choir_landing
// │ → catacomb_passage │ → throne_steps
// │ → vault_landing ──────────┘ → boss
// └──[Perception DC 15]── secret_chamber (SECRET) → antechamber
//
// Two paths to the boss (main_hall vs side_chapel); secret_chamber
// dangles off side_chapel for an extra Perception gate with loot.
// Both main and side arms walk 3 mid-nodes after the fork; secret stays a
// 1-node side dangle off side_chapel that merges back at antechamber, so
// the Perception-DC-15 path is a loot dip rather than a length detour.
func zoneCryptValdrisGraph() ZoneGraph {
nodes := []ZoneNode{
{NodeID: "crypt_valdris.entry", Kind: NodeKindEntry, IsEntry: true,
Label: "Crypt Threshold", PosX: 0, PosY: 1},
{NodeID: "crypt_valdris.corridor", Kind: NodeKindExploration,
Label: "Bone Corridor", PosX: 1, PosY: 1},
{NodeID: "crypt_valdris.processional_stair", Kind: NodeKindExploration,
Label: "Processional Stair", PosX: 1, PosY: 1},
{NodeID: "crypt_valdris.bone_corridor", Kind: NodeKindExploration,
Label: "Bone Corridor", PosX: 2, PosY: 1},
{NodeID: "crypt_valdris.grave_dust_trap", Kind: NodeKindTrap,
Label: "Grave-Dust Cascade", PosX: 3, PosY: 1},
{NodeID: "crypt_valdris.ossuary_walk", Kind: NodeKindExploration,
Label: "Ossuary Walk", PosX: 4, PosY: 1},
{NodeID: "crypt_valdris.fork", Kind: NodeKindFork,
Label: "Branching Passage", PosX: 2, PosY: 1},
Label: "Branching Passage", PosX: 5, PosY: 1},
// Left arm — default, contains the elite.
{NodeID: "crypt_valdris.main_hall", Kind: NodeKindElite,
Label: "Main Hall", PosX: 3, PosY: 0},
Label: "Main Hall", PosX: 6, PosY: 0},
{NodeID: "crypt_valdris.reliquary_hall", Kind: NodeKindExploration,
Label: "Reliquary Hall", PosX: 7, PosY: 0},
{NodeID: "crypt_valdris.ash_passage", Kind: NodeKindExploration,
Label: "Ash Passage", PosX: 8, PosY: 0},
// Right arm — perception-gated, elite-free.
{NodeID: "crypt_valdris.side_chapel", Kind: NodeKindExploration,
Label: "Side Chapel", PosX: 3, PosY: 2},
Label: "Side Chapel", PosX: 6, PosY: 2},
{NodeID: "crypt_valdris.catacomb_passage", Kind: NodeKindExploration,
Label: "Catacomb Passage", PosX: 7, PosY: 2},
{NodeID: "crypt_valdris.vault_landing", Kind: NodeKindExploration,
Label: "Vault Landing", PosX: 8, PosY: 2},
// Secret dangle off side_chapel — high-DC Perception, loot-biased.
{NodeID: "crypt_valdris.secret_chamber", Kind: NodeKindSecret,
Label: "Hidden Reliquary", PosX: 4, PosY: 3,
Label: "Hidden Reliquary", PosX: 7, PosY: 3,
Content: ZoneNodeContent{LootBias: 2.0}},
// Merge + approach.
{NodeID: "crypt_valdris.antechamber", Kind: NodeKindMerge,
Label: "Antechamber", PosX: 5, PosY: 1},
Label: "Antechamber", PosX: 9, PosY: 1},
{NodeID: "crypt_valdris.choir_landing", Kind: NodeKindExploration,
Label: "Choir Landing", PosX: 10, PosY: 1},
{NodeID: "crypt_valdris.throne_steps", Kind: NodeKindExploration,
Label: "Throne Steps", PosX: 11, PosY: 1},
{NodeID: "crypt_valdris.boss", Kind: NodeKindBoss, IsBoss: true,
Label: "Valdris's Sanctum", PosX: 6, PosY: 1},
Label: "Valdris's Sanctum", PosX: 12, PosY: 1},
}
edges := []ZoneEdge{
{From: "crypt_valdris.entry", To: "crypt_valdris.corridor", Lock: LockNone},
{From: "crypt_valdris.corridor", To: "crypt_valdris.fork", Lock: LockNone},
{From: "crypt_valdris.entry", To: "crypt_valdris.processional_stair", Lock: LockNone},
{From: "crypt_valdris.processional_stair", To: "crypt_valdris.bone_corridor", Lock: LockNone},
{From: "crypt_valdris.bone_corridor", To: "crypt_valdris.grave_dust_trap", Lock: LockNone},
{From: "crypt_valdris.grave_dust_trap", To: "crypt_valdris.ossuary_walk", Lock: LockNone},
{From: "crypt_valdris.ossuary_walk", To: "crypt_valdris.fork", Lock: LockNone},
{From: "crypt_valdris.fork", To: "crypt_valdris.main_hall", Lock: LockNone, Weight: 1},
{From: "crypt_valdris.fork", To: "crypt_valdris.side_chapel",
Lock: LockPerception, LockData: map[string]any{"dc": 12},
Hint: "a faint draft from a crack in the wall", Weight: 2},
{From: "crypt_valdris.main_hall", To: "crypt_valdris.antechamber", Lock: LockNone},
{From: "crypt_valdris.side_chapel", To: "crypt_valdris.antechamber", Lock: LockNone, Weight: 1},
{From: "crypt_valdris.main_hall", To: "crypt_valdris.reliquary_hall", Lock: LockNone},
{From: "crypt_valdris.reliquary_hall", To: "crypt_valdris.ash_passage", Lock: LockNone},
{From: "crypt_valdris.ash_passage", To: "crypt_valdris.antechamber", Lock: LockNone},
{From: "crypt_valdris.side_chapel", To: "crypt_valdris.catacomb_passage", Lock: LockNone, Weight: 1},
{From: "crypt_valdris.side_chapel", To: "crypt_valdris.secret_chamber",
Lock: LockPerception, LockData: map[string]any{"dc": 15},
Hint: "a faint scratching behind the altar", Weight: 2},
{From: "crypt_valdris.catacomb_passage", To: "crypt_valdris.vault_landing", Lock: LockNone},
{From: "crypt_valdris.vault_landing", To: "crypt_valdris.antechamber", Lock: LockNone},
{From: "crypt_valdris.secret_chamber", To: "crypt_valdris.antechamber", Lock: LockNone},
{From: "crypt_valdris.antechamber", To: "crypt_valdris.boss", Lock: LockNone},
{From: "crypt_valdris.antechamber", To: "crypt_valdris.choir_landing", Lock: LockNone},
{From: "crypt_valdris.choir_landing", To: "crypt_valdris.throne_steps", Lock: LockNone},
{From: "crypt_valdris.throne_steps", To: "crypt_valdris.boss", Lock: LockNone},
}
return BuildGraph(ZoneCryptValdris, nodes, edges)
}

View File

@@ -18,8 +18,10 @@ func TestCryptValdrisGraph_Registered(t *testing.T) {
if g.Boss != "crypt_valdris.boss" {
t.Errorf("boss node = %q, want crypt_valdris.boss", g.Boss)
}
if len(g.Nodes) != 8 {
t.Errorf("nodes = %d, want 8", len(g.Nodes))
// Long-expedition D1-b widened this zone from 8 → 17 nodes so the
// longest entry→boss walk lands in the T1 [12,14] traversal band.
if len(g.Nodes) != 17 {
t.Errorf("nodes = %d, want 17", len(g.Nodes))
}
}

View File

@@ -1,64 +1,111 @@
package plugin
// Phase G8b — Forest of Shadows branching graph.
// Forest of Shadows branching graph.
//
// T2 zone. Per plan §G8 ("two forks for T2+") and the G8 design
// session: deliberately a different shape from the Crypt diamond.
// Long-expedition plan D1-b: extended from the original 9-node sketch to
// the new T2 length band (1620 rooms traversed). Topology preserves the
// G8 design intent — asymmetric main fork (long branch carries the
// elite) + WIS-gated secret hanging pre-boss — and adds the missing Trap
// anchor + extra exploration on both arms and the boss approach.
//
// Shape: asymmetric main fork + WIS-gated secret pre-boss.
// entry → mossy_threshold → twisted_path → root_snare (TRAP) →
// creeping_brush → dappled_hollow → witchlight_clearing → fork1
// ├─[unlocked, w=1]── grove_descent →
// │ dryad_circle (ELITE) → torn_meadow ──┐
// │ ├── fork2
// └─[Perception DC 13, w=2]── thorn_tunnel │
// → briar_warren ────────────────────────┘
// │
// ┌─[unlocked, w=1]── ritual_glade → │
// │ antler_path → hollow_steps → boss ─┘
// │
// └─[WIS DC 14, w=2]── sapling_shrine (SECRET) → boss
//
// entry → twisted_path → fork1
// ├─[unlocked, w=1]── grove_descent → dryad_circle (elite) ─┐
// │ ├── fork2
// └─[Perception DC 13, w=2]── thorn_tunnel ──────────────────┘
// │
// ┌─[unlocked, w=1]── boss ────────────────┘
// │
// └─[WIS DC 14, w=2]── sapling_shrine (secret) → boss
//
// Asymmetry: long branch is two mid-nodes (grove_descent → dryad_circle
// (elite)), short branch is one (thorn_tunnel). Map renders the long
// branch as a "+1 column" arm so the topology reads at a glance.
// Secret uses LockStatCheck WIS (not Perception) to exercise the
// stat-check lock kind — Crypt's secret is Perception, so this is the
// first authored zone using stat_check.
// Asymmetry preserved: long branch is 3 mid-nodes (grove_descent →
// dryad_circle (elite) → torn_meadow), short branch is 2 (thorn_tunnel →
// briar_warren). Longest entry→boss walk = 16 (long branch path).
func zoneForestShadowsGraph() ZoneGraph {
nodes := []ZoneNode{
{NodeID: "forest_shadows.entry", Kind: NodeKindEntry, IsEntry: true,
Label: "Forest Edge", PosX: 0, PosY: 1},
{NodeID: "forest_shadows.mossy_threshold", Kind: NodeKindExploration,
Label: "Mossy Threshold", PosX: 1, PosY: 1},
{NodeID: "forest_shadows.twisted_path", Kind: NodeKindExploration,
Label: "Twisted Path", PosX: 1, PosY: 1},
Label: "Twisted Path", PosX: 2, PosY: 1},
{NodeID: "forest_shadows.root_snare", Kind: NodeKindTrap,
Label: "Root Snare", PosX: 3, PosY: 1},
{NodeID: "forest_shadows.creeping_brush", Kind: NodeKindExploration,
Label: "Creeping Brush", PosX: 4, PosY: 1},
{NodeID: "forest_shadows.dappled_hollow", Kind: NodeKindExploration,
Label: "Dappled Hollow", PosX: 5, PosY: 1},
{NodeID: "forest_shadows.witchlight_clearing", Kind: NodeKindExploration,
Label: "Witchlight Clearing", PosX: 6, PosY: 1},
{NodeID: "forest_shadows.fork1", Kind: NodeKindFork,
Label: "Splintered Trail", PosX: 2, PosY: 1},
Label: "Splintered Trail", PosX: 7, PosY: 1},
// Long branch (3 mid-nodes, carries the elite).
{NodeID: "forest_shadows.grove_descent", Kind: NodeKindExploration,
Label: "Grove Descent", PosX: 3, PosY: 0},
Label: "Grove Descent", PosX: 8, PosY: 0},
{NodeID: "forest_shadows.dryad_circle", Kind: NodeKindElite,
Label: "Dryad's Circle", PosX: 4, PosY: 0},
Label: "Dryad's Circle", PosX: 9, PosY: 0},
{NodeID: "forest_shadows.torn_meadow", Kind: NodeKindExploration,
Label: "Torn Meadow", PosX: 10, PosY: 0},
// Short branch (2 mid-nodes, perception-gated).
{NodeID: "forest_shadows.thorn_tunnel", Kind: NodeKindExploration,
Label: "Thorn Tunnel", PosX: 3, PosY: 2},
Label: "Thorn Tunnel", PosX: 8, PosY: 2},
{NodeID: "forest_shadows.briar_warren", Kind: NodeKindExploration,
Label: "Briar Warren", PosX: 9, PosY: 2},
{NodeID: "forest_shadows.fork2", Kind: NodeKindFork,
Label: "Hollow King's Approach", PosX: 5, PosY: 1},
Label: "Hollow King's Approach", PosX: 11, PosY: 1},
// Post-fork2 main approach.
{NodeID: "forest_shadows.ritual_glade", Kind: NodeKindExploration,
Label: "Ritual Glade", PosX: 12, PosY: 1},
{NodeID: "forest_shadows.antler_path", Kind: NodeKindExploration,
Label: "Antler Path", PosX: 13, PosY: 1},
{NodeID: "forest_shadows.hollow_steps", Kind: NodeKindExploration,
Label: "Hollow Steps", PosX: 14, PosY: 1},
// Secret dangle off fork2 — WIS gate, loot-biased, shortcut to boss.
{NodeID: "forest_shadows.sapling_shrine", Kind: NodeKindSecret,
Label: "Sapling Shrine", PosX: 6, PosY: 2,
Label: "Sapling Shrine", PosX: 13, PosY: 2,
Content: ZoneNodeContent{LootBias: 2.0}},
{NodeID: "forest_shadows.boss", Kind: NodeKindBoss, IsBoss: true,
Label: "Throne of Antlers", PosX: 7, PosY: 1},
Label: "Throne of Antlers", PosX: 15, PosY: 1},
}
edges := []ZoneEdge{
{From: "forest_shadows.entry", To: "forest_shadows.twisted_path", Lock: LockNone},
{From: "forest_shadows.twisted_path", To: "forest_shadows.fork1", Lock: LockNone},
{From: "forest_shadows.entry", To: "forest_shadows.mossy_threshold", Lock: LockNone},
{From: "forest_shadows.mossy_threshold", To: "forest_shadows.twisted_path", Lock: LockNone},
{From: "forest_shadows.twisted_path", To: "forest_shadows.root_snare", Lock: LockNone},
{From: "forest_shadows.root_snare", To: "forest_shadows.creeping_brush", Lock: LockNone},
{From: "forest_shadows.creeping_brush", To: "forest_shadows.dappled_hollow", Lock: LockNone},
{From: "forest_shadows.dappled_hollow", To: "forest_shadows.witchlight_clearing", Lock: LockNone},
{From: "forest_shadows.witchlight_clearing", To: "forest_shadows.fork1", Lock: LockNone},
{From: "forest_shadows.fork1", To: "forest_shadows.grove_descent", Lock: LockNone, Weight: 1},
{From: "forest_shadows.fork1", To: "forest_shadows.thorn_tunnel",
Lock: LockPerception, LockData: map[string]any{"dc": 13},
Hint: "a deer-track winding into thicker brush", Weight: 2},
{From: "forest_shadows.grove_descent", To: "forest_shadows.dryad_circle", Lock: LockNone},
{From: "forest_shadows.dryad_circle", To: "forest_shadows.fork2", Lock: LockNone},
{From: "forest_shadows.thorn_tunnel", To: "forest_shadows.fork2", Lock: LockNone},
{From: "forest_shadows.fork2", To: "forest_shadows.boss", Lock: LockNone, Weight: 1},
{From: "forest_shadows.dryad_circle", To: "forest_shadows.torn_meadow", Lock: LockNone},
{From: "forest_shadows.torn_meadow", To: "forest_shadows.fork2", Lock: LockNone},
{From: "forest_shadows.thorn_tunnel", To: "forest_shadows.briar_warren", Lock: LockNone},
{From: "forest_shadows.briar_warren", To: "forest_shadows.fork2", Lock: LockNone},
{From: "forest_shadows.fork2", To: "forest_shadows.ritual_glade", Lock: LockNone, Weight: 1},
{From: "forest_shadows.fork2", To: "forest_shadows.sapling_shrine",
Lock: LockStatCheck, LockData: map[string]any{"stat": "WIS", "dc": 14},
Hint: "a humming you can almost place — antlers, somewhere", Weight: 2},
{From: "forest_shadows.ritual_glade", To: "forest_shadows.antler_path", Lock: LockNone},
{From: "forest_shadows.antler_path", To: "forest_shadows.hollow_steps", Lock: LockNone},
{From: "forest_shadows.hollow_steps", To: "forest_shadows.boss", Lock: LockNone},
{From: "forest_shadows.sapling_shrine", To: "forest_shadows.boss", Lock: LockNone},
}
return BuildGraph(ZoneForestShadows, nodes, edges)

View File

@@ -13,8 +13,10 @@ func TestForestShadowsGraph_Registered(t *testing.T) {
if g.Boss != "forest_shadows.boss" {
t.Errorf("boss node = %q, want forest_shadows.boss", g.Boss)
}
if len(g.Nodes) != 9 {
t.Errorf("nodes = %d, want 9", len(g.Nodes))
// Long-expedition D1-b widened this zone from 9 → 19 nodes so the
// longest entry→boss walk lands in the T2 [16,20] traversal band.
if len(g.Nodes) != 19 {
t.Errorf("nodes = %d, want 19", len(g.Nodes))
}
}
@@ -42,11 +44,13 @@ func TestForestShadowsGraph_AsymmetricMainFork(t *testing.T) {
g := zoneForestShadowsGraph()
longLen := bfsHops(g, "forest_shadows.fork1", "forest_shadows.fork2", "forest_shadows.grove_descent")
shortLen := bfsHops(g, "forest_shadows.fork1", "forest_shadows.fork2", "forest_shadows.thorn_tunnel")
if longLen != 3 {
t.Errorf("long branch hops = %d, want 3 (grove_descent → dryad_circle → fork2)", longLen)
// D1-b: long branch is now 3 mid-nodes (grove_descent → dryad_circle
// → torn_meadow), short is 2 (thorn_tunnel → briar_warren).
if longLen != 4 {
t.Errorf("long branch hops = %d, want 4 (grove_descent → dryad_circle → torn_meadow → fork2)", longLen)
}
if shortLen != 2 {
t.Errorf("short branch hops = %d, want 2 (thorn_tunnel → fork2)", shortLen)
if shortLen != 3 {
t.Errorf("short branch hops = %d, want 3 (thorn_tunnel → briar_warren → fork2)", shortLen)
}
if longLen <= shortLen {
t.Errorf("expected asymmetric branches (long > short); got long=%d short=%d", longLen, shortLen)

View File

@@ -1,23 +1,30 @@
package plugin
// Phase G8c — Sunken Temple of Dar'eth branching graph.
// Sunken Temple of Dar'eth branching graph.
//
// T2 zone. Shape: sequential forks (no mid-path merge). Four distinct
// paths to the boss; the player commits at fork1 (dry vs. wet) and
// commits again at fork2a/fork2b. Path is unique up to the boss room.
// Long-expedition plan D1-b: extended from the original 10-node sketch to
// the new T2 length band (1620 rooms traversed). Topology preserves the
// G8c design intent — sequential forks with no mid-path merge — and adds
// the missing Trap anchor + deeper pre-fork and per-leaf chains. The
// four leaf chains still terminate at the single boss room (validator
// requires exactly one boss).
//
// entry → flooded_atrium → fork1
// ├─[unlocked, w=1]── fork2a (dry)
// │ ├─[unlocked]── kuo_toa_pen (elite) → boss
// │ └─[STR DC 13]── glyph_chamber → boss
// └─[Perception DC 13, w=2]── fork2b (wet)
// ├─[unlocked]── aboleth_thralls → boss
// └─[Perception DC 15]── coral_reliquary (secret) → boss
// entry → flooded_atrium → tide_passage → kelp_trap (TRAP) →
// barnacled_steps → drowned_atrium → sunken_nave → fork1
// ├─[unlocked, w=1]── dry_corridor → silent_columns →
// │ echoing_vault → fork2a
// ├─[unlocked]── kuo_toa_pen (ELITE) →
// trophy_pool → idol_court → boss
// └─[STR DC 13]── glyph_chamber → silt_arch → boss
// └─[Perception DC 13, w=2]── submerged_passage →
// coral_lattice → drowned_nave → fork2b
// ├─[unlocked]── aboleth_thralls →
// │ thrall_basin → sodden_hall → boss
// └─[Perception DC 15]── coral_reliquary (SECRET) → boss
//
// Distinct from the Crypt diamond and the Forest asymmetric-diamond:
// no two paths share a mid-node, so the !zone map paints two parallel
// "Y" subtrees instead of a converging branch. The four leaves all
// terminate at the boss (validator requires exactly one boss node).
// Distinct from the Crypt diamond and the Forest asymmetric-diamond: no
// two paths share a mid-node, so the !zone map paints two parallel "Y"
// subtrees instead of a converging branch. Longest entry→boss = 16.
func zoneSunkenTempleGraph() ZoneGraph {
nodes := []ZoneNode{
@@ -25,42 +32,104 @@ func zoneSunkenTempleGraph() ZoneGraph {
Label: "Tide-Stained Threshold", PosX: 0, PosY: 2},
{NodeID: "sunken_temple.flooded_atrium", Kind: NodeKindExploration,
Label: "Flooded Atrium", PosX: 1, PosY: 2},
{NodeID: "sunken_temple.tide_passage", Kind: NodeKindExploration,
Label: "Tide Passage", PosX: 2, PosY: 2},
{NodeID: "sunken_temple.kelp_trap", Kind: NodeKindTrap,
Label: "Kelp-Snare", PosX: 3, PosY: 2},
{NodeID: "sunken_temple.barnacled_steps", Kind: NodeKindExploration,
Label: "Barnacled Steps", PosX: 4, PosY: 2},
{NodeID: "sunken_temple.drowned_atrium", Kind: NodeKindExploration,
Label: "Drowned Atrium", PosX: 5, PosY: 2},
{NodeID: "sunken_temple.sunken_nave", Kind: NodeKindExploration,
Label: "Sunken Nave", PosX: 6, PosY: 2},
{NodeID: "sunken_temple.fork1", Kind: NodeKindFork,
Label: "Split Stair", PosX: 2, PosY: 2},
Label: "Split Stair", PosX: 7, PosY: 2},
// Dry subtree.
{NodeID: "sunken_temple.dry_corridor", Kind: NodeKindExploration,
Label: "Dry Corridor", PosX: 8, PosY: 0},
{NodeID: "sunken_temple.silent_columns", Kind: NodeKindExploration,
Label: "Silent Columns", PosX: 9, PosY: 0},
{NodeID: "sunken_temple.echoing_vault", Kind: NodeKindExploration,
Label: "Echoing Vault", PosX: 10, PosY: 0},
{NodeID: "sunken_temple.fork2a", Kind: NodeKindFork,
Label: "Dry Crossing", PosX: 3, PosY: 0},
{NodeID: "sunken_temple.fork2b", Kind: NodeKindFork,
Label: "Submerged Crossing", PosX: 3, PosY: 4},
Label: "Dry Crossing", PosX: 11, PosY: 0},
{NodeID: "sunken_temple.kuo_toa_pen", Kind: NodeKindElite,
Label: "Kuo-toa Pen", PosX: 4, PosY: 0},
Label: "Kuo-toa Pen", PosX: 12, PosY: -1},
{NodeID: "sunken_temple.trophy_pool", Kind: NodeKindExploration,
Label: "Trophy Pool", PosX: 13, PosY: -1},
{NodeID: "sunken_temple.idol_court", Kind: NodeKindExploration,
Label: "Idol Court", PosX: 14, PosY: -1},
{NodeID: "sunken_temple.glyph_chamber", Kind: NodeKindExploration,
Label: "Glyph Chamber", PosX: 4, PosY: 1},
Label: "Glyph Chamber", PosX: 12, PosY: 1},
{NodeID: "sunken_temple.silt_arch", Kind: NodeKindExploration,
Label: "Silt Arch", PosX: 13, PosY: 1},
// Wet subtree.
{NodeID: "sunken_temple.submerged_passage", Kind: NodeKindExploration,
Label: "Submerged Passage", PosX: 8, PosY: 4},
{NodeID: "sunken_temple.coral_lattice", Kind: NodeKindExploration,
Label: "Coral Lattice", PosX: 9, PosY: 4},
{NodeID: "sunken_temple.drowned_nave", Kind: NodeKindExploration,
Label: "Drowned Nave", PosX: 10, PosY: 4},
{NodeID: "sunken_temple.fork2b", Kind: NodeKindFork,
Label: "Submerged Crossing", PosX: 11, PosY: 4},
{NodeID: "sunken_temple.aboleth_thralls", Kind: NodeKindExploration,
Label: "Thrall Pool", PosX: 4, PosY: 3},
Label: "Thrall Pool", PosX: 12, PosY: 3},
{NodeID: "sunken_temple.thrall_basin", Kind: NodeKindExploration,
Label: "Thrall Basin", PosX: 13, PosY: 3},
{NodeID: "sunken_temple.sodden_hall", Kind: NodeKindExploration,
Label: "Sodden Hall", PosX: 14, PosY: 3},
{NodeID: "sunken_temple.coral_reliquary", Kind: NodeKindSecret,
Label: "Coral Reliquary", PosX: 4, PosY: 4,
Label: "Coral Reliquary", PosX: 12, PosY: 5,
Content: ZoneNodeContent{LootBias: 1.8}},
{NodeID: "sunken_temple.boss", Kind: NodeKindBoss, IsBoss: true,
Label: "Aboleth's Pool", PosX: 5, PosY: 2},
Label: "Aboleth's Pool", PosX: 15, PosY: 2},
}
edges := []ZoneEdge{
{From: "sunken_temple.entry", To: "sunken_temple.flooded_atrium", Lock: LockNone},
{From: "sunken_temple.flooded_atrium", To: "sunken_temple.fork1", Lock: LockNone},
{From: "sunken_temple.fork1", To: "sunken_temple.fork2a", Lock: LockNone, Weight: 1},
{From: "sunken_temple.fork1", To: "sunken_temple.fork2b",
{From: "sunken_temple.flooded_atrium", To: "sunken_temple.tide_passage", Lock: LockNone},
{From: "sunken_temple.tide_passage", To: "sunken_temple.kelp_trap", Lock: LockNone},
{From: "sunken_temple.kelp_trap", To: "sunken_temple.barnacled_steps", Lock: LockNone},
{From: "sunken_temple.barnacled_steps", To: "sunken_temple.drowned_atrium", Lock: LockNone},
{From: "sunken_temple.drowned_atrium", To: "sunken_temple.sunken_nave", Lock: LockNone},
{From: "sunken_temple.sunken_nave", To: "sunken_temple.fork1", Lock: LockNone},
{From: "sunken_temple.fork1", To: "sunken_temple.dry_corridor", Lock: LockNone, Weight: 1},
{From: "sunken_temple.fork1", To: "sunken_temple.submerged_passage",
Lock: LockPerception, LockData: map[string]any{"dc": 13},
Hint: "wet stone glistens down a side passage", Weight: 2},
{From: "sunken_temple.dry_corridor", To: "sunken_temple.silent_columns", Lock: LockNone},
{From: "sunken_temple.silent_columns", To: "sunken_temple.echoing_vault", Lock: LockNone},
{From: "sunken_temple.echoing_vault", To: "sunken_temple.fork2a", Lock: LockNone},
{From: "sunken_temple.fork2a", To: "sunken_temple.kuo_toa_pen", Lock: LockNone, Weight: 1},
{From: "sunken_temple.fork2a", To: "sunken_temple.glyph_chamber",
Lock: LockStatCheck, LockData: map[string]any{"stat": "STR", "dc": 13},
Hint: "a stone door wedged half-shut by silt", Weight: 2},
{From: "sunken_temple.kuo_toa_pen", To: "sunken_temple.trophy_pool", Lock: LockNone},
{From: "sunken_temple.trophy_pool", To: "sunken_temple.idol_court", Lock: LockNone},
{From: "sunken_temple.idol_court", To: "sunken_temple.boss", Lock: LockNone},
{From: "sunken_temple.glyph_chamber", To: "sunken_temple.silt_arch", Lock: LockNone},
{From: "sunken_temple.silt_arch", To: "sunken_temple.boss", Lock: LockNone},
{From: "sunken_temple.submerged_passage", To: "sunken_temple.coral_lattice", Lock: LockNone},
{From: "sunken_temple.coral_lattice", To: "sunken_temple.drowned_nave", Lock: LockNone},
{From: "sunken_temple.drowned_nave", To: "sunken_temple.fork2b", Lock: LockNone},
{From: "sunken_temple.fork2b", To: "sunken_temple.aboleth_thralls", Lock: LockNone, Weight: 1},
{From: "sunken_temple.fork2b", To: "sunken_temple.coral_reliquary",
Lock: LockPerception, LockData: map[string]any{"dc": 15},
Hint: "a coral arch glittering under the surface", Weight: 2},
{From: "sunken_temple.kuo_toa_pen", To: "sunken_temple.boss", Lock: LockNone},
{From: "sunken_temple.glyph_chamber", To: "sunken_temple.boss", Lock: LockNone},
{From: "sunken_temple.aboleth_thralls", To: "sunken_temple.boss", Lock: LockNone},
{From: "sunken_temple.aboleth_thralls", To: "sunken_temple.thrall_basin", Lock: LockNone},
{From: "sunken_temple.thrall_basin", To: "sunken_temple.sodden_hall", Lock: LockNone},
{From: "sunken_temple.sodden_hall", To: "sunken_temple.boss", Lock: LockNone},
{From: "sunken_temple.coral_reliquary", To: "sunken_temple.boss", Lock: LockNone},
}
return BuildGraph(ZoneSunkenTemple, nodes, edges)

View File

@@ -13,8 +13,12 @@ func TestSunkenTempleGraph_Registered(t *testing.T) {
if g.Boss != "sunken_temple.boss" {
t.Errorf("boss node = %q", g.Boss)
}
if len(g.Nodes) != 10 {
t.Errorf("nodes = %d, want 10", len(g.Nodes))
// Long-expedition D1-b widened this zone from 10 → 26 nodes so the
// longest entry→boss walk lands in the T2 [16,20] traversal band.
// (Sunken Temple is wide by design — no mid-merge means each leaf
// owns its own chain to the boss.)
if len(g.Nodes) != 26 {
t.Errorf("nodes = %d, want 26", len(g.Nodes))
}
}