From dd97a0033273fc5b228a8f2636caa02a00e047c9 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sat, 9 May 2026 16:56:07 -0700 Subject: [PATCH] Branching zones G8e: Underforge pre-boss gauntlet (late triple fork) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T3 zone, fifth authored graph. Shape: five-node linear preamble (sealed_gate → forge_descent → cooling_river → magma_chamber elite) followed by a single 3-way antechamber fork before the boss. Bends the plan §G8 "two forks for T2+" guideline to a single late fork-with-three-options because the gauntlet shape is the design intent — Kharak Dûn is a one-way descent. Triple-option antechamber (direct / DEX-checked catwalks / Perception-found forge_vault secret) preserves agency at the commitment moment. Map renders as a long horizontal stem with a 3-leaf fan at the right edge — visually distinct from any earlier zone. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/zone_graph_underforge.go | 71 +++++++++++++++++++ internal/plugin/zone_graph_underforge_test.go | 50 +++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 internal/plugin/zone_graph_underforge.go create mode 100644 internal/plugin/zone_graph_underforge_test.go diff --git a/internal/plugin/zone_graph_underforge.go b/internal/plugin/zone_graph_underforge.go new file mode 100644 index 0000000..92708e7 --- /dev/null +++ b/internal/plugin/zone_graph_underforge.go @@ -0,0 +1,71 @@ +package plugin + +// Phase G8e — The Underforge branching graph. +// +// T3 zone. Shape: pre-boss gauntlet — long linear preamble through the +// forge-city, then a single 3-way antechamber fork right before the +// boss. The plan §G8 calls for "two forks for T2+"; this zone bends +// that to a single late fork with three options because the gauntlet +// shape is the design intent (Kharak Dûn is a one-way descent — there +// is no scenic route, only the approach to what was sealed in). The +// triple-option antechamber preserves player agency at the +// commitment moment. +// +// entry → sealed_gate → forge_descent → cooling_river → magma_chamber (elite) → antechamber (3-way) +// ├─[unlocked]── direct_assault → boss +// ├─[DEX DC 14]── catwalks → boss +// └─[Perception DC 15]── forge_vault (secret) → boss +// +// Distinct from prior zones in two ways: +// 1. Five-node linear preamble (no zone yet has > 2 linear preamble nodes). +// 2. Fork is delayed to the final node before boss; the !zone map +// therefore renders as a long horizontal stem with a 3-leaf fan at +// the right edge. + +func zoneUnderforgeGraph() ZoneGraph { + nodes := []ZoneNode{ + {NodeID: "underforge.entry", Kind: NodeKindEntry, IsEntry: true, + Label: "Sealed Threshold", PosX: 0, PosY: 1}, + {NodeID: "underforge.sealed_gate", Kind: NodeKindExploration, + Label: "Sealed Gate", PosX: 1, PosY: 1}, + {NodeID: "underforge.forge_descent", Kind: NodeKindExploration, + Label: "Forge Descent", PosX: 2, PosY: 1}, + {NodeID: "underforge.cooling_river", Kind: NodeKindTrap, + Label: "Cooling River", PosX: 3, PosY: 1}, + {NodeID: "underforge.magma_chamber", Kind: NodeKindElite, + Label: "Magma Chamber", PosX: 4, PosY: 1}, + {NodeID: "underforge.antechamber", Kind: NodeKindFork, + Label: "Antechamber of Kharak Dûn", PosX: 5, PosY: 1}, + {NodeID: "underforge.direct_assault", Kind: NodeKindExploration, + Label: "Direct Assault", PosX: 6, PosY: 0}, + {NodeID: "underforge.catwalks", Kind: NodeKindExploration, + Label: "Forge Catwalks", PosX: 6, PosY: 1}, + {NodeID: "underforge.forge_vault", Kind: NodeKindSecret, + Label: "Forge Vault", PosX: 6, PosY: 2, + Content: ZoneNodeContent{LootBias: 2.0}}, + {NodeID: "underforge.boss", Kind: NodeKindBoss, IsBoss: true, + Label: "The Sealed Hall", PosX: 7, PosY: 1}, + } + edges := []ZoneEdge{ + {From: "underforge.entry", To: "underforge.sealed_gate", Lock: LockNone}, + {From: "underforge.sealed_gate", To: "underforge.forge_descent", Lock: LockNone}, + {From: "underforge.forge_descent", To: "underforge.cooling_river", Lock: LockNone}, + {From: "underforge.cooling_river", To: "underforge.magma_chamber", Lock: LockNone}, + {From: "underforge.magma_chamber", To: "underforge.antechamber", Lock: LockNone}, + {From: "underforge.antechamber", To: "underforge.direct_assault", Lock: LockNone, Weight: 1}, + {From: "underforge.antechamber", To: "underforge.catwalks", + Lock: LockStatCheck, LockData: map[string]any{"stat": "DEX", "dc": 14}, + Hint: "rusted catwalks above the magma — quick footing required", Weight: 2}, + {From: "underforge.antechamber", To: "underforge.forge_vault", + Lock: LockPerception, LockData: map[string]any{"dc": 15}, + Hint: "a draft from a stone seam, where no draft should be", Weight: 2}, + {From: "underforge.direct_assault", To: "underforge.boss", Lock: LockNone}, + {From: "underforge.catwalks", To: "underforge.boss", Lock: LockNone}, + {From: "underforge.forge_vault", To: "underforge.boss", Lock: LockNone}, + } + return BuildGraph(ZoneUnderforge, nodes, edges) +} + +func init() { + registerZoneGraph(zoneUnderforgeGraph()) +} diff --git a/internal/plugin/zone_graph_underforge_test.go b/internal/plugin/zone_graph_underforge_test.go new file mode 100644 index 0000000..b7992f4 --- /dev/null +++ b/internal/plugin/zone_graph_underforge_test.go @@ -0,0 +1,50 @@ +package plugin + +import "testing" + +func TestUnderforgeGraph_Registered(t *testing.T) { + g, ok := zoneGraphRegistry[ZoneUnderforge] + if !ok { + t.Fatal("zoneUnderforgeGraph not registered") + } + if len(g.Nodes) != 10 { + t.Errorf("nodes = %d, want 10", len(g.Nodes)) + } +} + +// TestUnderforgeGraph_LinearPreamble locks in the gauntlet shape: +// the first five nodes after entry must each have exactly one outgoing +// edge (linear chain). If a future edit splits the preamble, this test +// catches it — that change should re-author the shape comment too. +func TestUnderforgeGraph_LinearPreamble(t *testing.T) { + g := zoneUnderforgeGraph() + for _, id := range []string{ + "underforge.entry", + "underforge.sealed_gate", + "underforge.forge_descent", + "underforge.cooling_river", + "underforge.magma_chamber", + } { + outs := g.outgoingEdges(id) + if len(outs) != 1 { + t.Errorf("preamble node %s outgoing = %d, want 1 (gauntlet)", id, len(outs)) + } + } +} + +func TestUnderforgeGraph_AntechamberThreeWay(t *testing.T) { + g := zoneUnderforgeGraph() + outs := g.outgoingEdges("underforge.antechamber") + if len(outs) != 3 { + t.Fatalf("antechamber outgoing = %d, want 3", len(outs)) + } + for _, leaf := range []string{ + "underforge.direct_assault", + "underforge.catwalks", + "underforge.forge_vault", + } { + if !reachable(g, leaf, "underforge.boss") { + t.Errorf("%s unreachable to boss", leaf) + } + } +}