From 3a8f3c26d6303cf00aba571d318d428ea09468a4 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sat, 9 May 2026 16:59:32 -0700 Subject: [PATCH] Branching zones G8g: Dragon's Lair converging mid-fork + capstone 3-way MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T5 zone, seventh authored graph. Shape: long approach (kobold_warrens → drake_pens) → binary mid-fork that converges at the elite (wyrmlings_nest) → 3-way capstone fork into the boss (direct / CHA bargain / Perception-found hoard_pillar secret). First zone combining two distinct fork stages where the first converges diamond-style and the second spreads triple-wide. Secret carries LootBias 2.5 — highest of any shipping zone, reserved for T5 — locked in by TestDragonsLairGraph_LootBiasEscalation. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/zone_graph_dragons_lair.go | 85 +++++++++++++++++++ .../plugin/zone_graph_dragons_lair_test.go | 62 ++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 internal/plugin/zone_graph_dragons_lair.go create mode 100644 internal/plugin/zone_graph_dragons_lair_test.go diff --git a/internal/plugin/zone_graph_dragons_lair.go b/internal/plugin/zone_graph_dragons_lair.go new file mode 100644 index 0000000..d5de585 --- /dev/null +++ b/internal/plugin/zone_graph_dragons_lair.go @@ -0,0 +1,85 @@ +package plugin + +// Phase G8g — Dragon's Lair (Infernus Peak) branching graph. +// +// T5 zone. Shape: long approach + mid-zone fork (converging at the +// elite) + capstone 3-way fork at the boss antechamber. Combines two +// distinct fork styles in one zone: a binary-converging fork and a +// triple-spread capstone — escalating the player's decision-weight as +// they descend. +// +// entry → kobold_warrens → drake_pens → fork1 (binary, converges) +// ├─[unlocked]── ash_bridge ────┐ +// └─[Perception DC 15]── treasure_vault ─┤ +// ↓ +// wyrmlings_nest (elite young red dragon) +// ↓ +// fork2 (capstone, 3-way) +// ├─[unlocked]── direct_confrontation → boss +// ├─[CHA DC 16]── dragon_bargain → boss +// └─[Perception DC 17]── hoard_pillar (secret) → boss +// +// First authored zone with two distinct fork stages where the first +// converges (diamond-style) and the second spreads (3-way capstone). +// Earlier zones used either-or: stacked equal-width hubs (Manor) or +// late-only fork (Underforge). The escalation pattern ("commit, then +// commit harder") is unique to T5. + +func zoneDragonsLairGraph() ZoneGraph { + nodes := []ZoneNode{ + {NodeID: "dragons_lair.entry", Kind: NodeKindEntry, IsEntry: true, + Label: "Mountain Pass", PosX: 0, PosY: 1}, + {NodeID: "dragons_lair.kobold_warrens", Kind: NodeKindExploration, + Label: "Kobold Warrens", PosX: 1, PosY: 1}, + {NodeID: "dragons_lair.drake_pens", Kind: NodeKindExploration, + Label: "Drake Pens", PosX: 2, PosY: 1}, + {NodeID: "dragons_lair.fork1", Kind: NodeKindFork, + Label: "The Cinder Crossing", PosX: 3, PosY: 1}, + {NodeID: "dragons_lair.ash_bridge", Kind: NodeKindTrap, + Label: "Ash Bridge", PosX: 4, PosY: 0}, + {NodeID: "dragons_lair.treasure_vault", Kind: NodeKindExploration, + Label: "Treasure Vault", PosX: 4, PosY: 2}, + {NodeID: "dragons_lair.wyrmlings_nest", Kind: NodeKindElite, + Label: "Wyrmling's Nest", PosX: 5, PosY: 1}, + {NodeID: "dragons_lair.fork2", Kind: NodeKindFork, + Label: "Hoard Approach", PosX: 6, PosY: 1}, + {NodeID: "dragons_lair.direct_confrontation", Kind: NodeKindExploration, + Label: "Direct Approach", PosX: 7, PosY: 0}, + {NodeID: "dragons_lair.dragon_bargain", Kind: NodeKindExploration, + Label: "Words With Infernax", PosX: 7, PosY: 1}, + {NodeID: "dragons_lair.hoard_pillar", Kind: NodeKindSecret, + Label: "Hidden Hoard Pillar", PosX: 7, PosY: 2, + Content: ZoneNodeContent{LootBias: 2.5}}, + {NodeID: "dragons_lair.boss", Kind: NodeKindBoss, IsBoss: true, + Label: "Infernax's Crown", PosX: 8, PosY: 1}, + } + edges := []ZoneEdge{ + {From: "dragons_lair.entry", To: "dragons_lair.kobold_warrens", Lock: LockNone}, + {From: "dragons_lair.kobold_warrens", To: "dragons_lair.drake_pens", Lock: LockNone}, + {From: "dragons_lair.drake_pens", To: "dragons_lair.fork1", Lock: LockNone}, + // Fork1 — converges at wyrmlings_nest. + {From: "dragons_lair.fork1", To: "dragons_lair.ash_bridge", Lock: LockNone, Weight: 1}, + {From: "dragons_lair.fork1", To: "dragons_lair.treasure_vault", + Lock: LockPerception, LockData: map[string]any{"dc": 15}, + Hint: "a draft from a side passage — and the dull glint of gold beyond it", Weight: 2}, + {From: "dragons_lair.ash_bridge", To: "dragons_lair.wyrmlings_nest", Lock: LockNone}, + {From: "dragons_lair.treasure_vault", To: "dragons_lair.wyrmlings_nest", Lock: LockNone}, + {From: "dragons_lair.wyrmlings_nest", To: "dragons_lair.fork2", Lock: LockNone}, + // Fork2 — capstone 3-way. + {From: "dragons_lair.fork2", To: "dragons_lair.direct_confrontation", Lock: LockNone, Weight: 1}, + {From: "dragons_lair.fork2", To: "dragons_lair.dragon_bargain", + Lock: LockStatCheck, LockData: map[string]any{"stat": "CHA", "dc": 16}, + Hint: "Infernax speaks. The voice fills the chamber. You could speak back.", Weight: 2}, + {From: "dragons_lair.fork2", To: "dragons_lair.hoard_pillar", + Lock: LockPerception, LockData: map[string]any{"dc": 17}, + Hint: "a single coin standing on edge — a draft, not a tremor", Weight: 3}, + {From: "dragons_lair.direct_confrontation", To: "dragons_lair.boss", Lock: LockNone}, + {From: "dragons_lair.dragon_bargain", To: "dragons_lair.boss", Lock: LockNone}, + {From: "dragons_lair.hoard_pillar", To: "dragons_lair.boss", Lock: LockNone}, + } + return BuildGraph(ZoneDragonsLair, nodes, edges) +} + +func init() { + registerZoneGraph(zoneDragonsLairGraph()) +} diff --git a/internal/plugin/zone_graph_dragons_lair_test.go b/internal/plugin/zone_graph_dragons_lair_test.go new file mode 100644 index 0000000..cd47013 --- /dev/null +++ b/internal/plugin/zone_graph_dragons_lair_test.go @@ -0,0 +1,62 @@ +package plugin + +import "testing" + +func TestDragonsLairGraph_Registered(t *testing.T) { + g, ok := zoneGraphRegistry[ZoneDragonsLair] + if !ok { + t.Fatal("zoneDragonsLairGraph not registered") + } + if len(g.Nodes) != 12 { + t.Errorf("nodes = %d, want 12", len(g.Nodes)) + } +} + +// TestDragonsLairGraph_Fork1Converges verifies the binary mid-fork +// converges at wyrmlings_nest. +func TestDragonsLairGraph_Fork1Converges(t *testing.T) { + g := zoneDragonsLairGraph() + for _, mid := range []string{"dragons_lair.ash_bridge", "dragons_lair.treasure_vault"} { + outs := g.outgoingEdges(mid) + if len(outs) != 1 || outs[0].To != "dragons_lair.wyrmlings_nest" { + t.Errorf("%s outs = %+v, want single edge to wyrmlings_nest", mid, outs) + } + } +} + +// TestDragonsLairGraph_Fork2Capstone verifies the late fork has 3 +// distinct paths to the boss, each through its own capstone node. +func TestDragonsLairGraph_Fork2Capstone(t *testing.T) { + g := zoneDragonsLairGraph() + outs := g.outgoingEdges("dragons_lair.fork2") + if len(outs) != 3 { + t.Fatalf("fork2 outgoing = %d, want 3", len(outs)) + } + for _, leaf := range []string{ + "dragons_lair.direct_confrontation", + "dragons_lair.dragon_bargain", + "dragons_lair.hoard_pillar", + } { + if !reachable(g, leaf, "dragons_lair.boss") { + t.Errorf("%s unreachable to boss", leaf) + } + } + // Locks escalate: open / CHA 16 / Perception 17 (T5 secret bias). + for _, e := range outs { + if e.To == "dragons_lair.hoard_pillar" { + if dc := lockDataInt(e.LockData, "dc", 0); dc < 17 { + t.Errorf("hoard_pillar DC = %d, want >= 17 (T5)", dc) + } + } + } +} + +// TestDragonsLairGraph_LootBiasEscalation verifies the T5 secret has +// the highest loot bias of any authored zone (≥ 2.5). +func TestDragonsLairGraph_LootBiasEscalation(t *testing.T) { + g := zoneDragonsLairGraph() + hoard := g.Nodes["dragons_lair.hoard_pillar"] + if hoard.Content.LootBias < 2.5 { + t.Errorf("hoard_pillar LootBias = %v, want >= 2.5 (T5 secret)", hoard.Content.LootBias) + } +}