mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Branching zones G8c: Sunken Temple sequential forks (no mid-path merge)
T2 zone, third authored graph. Shape: two-tier sequential forks where each path is unique up to the boss room. Four committed leaves (kuo_toa_pen / glyph_chamber / aboleth_thralls / coral_reliquary) all terminate at the boss; no mid-path node has >1 incoming edge. Differentiates from Crypt's diamond and Forest's asymmetric-diamond by producing two parallel "Y" subtrees on the !zone map instead of a converging branch. Exercises Perception + StatCheck (STR) + LockNone in a single zone (covered by TestSunkenTempleGraph_LockKindCoverage). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
71
internal/plugin/zone_graph_sunken_temple.go
Normal file
71
internal/plugin/zone_graph_sunken_temple.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package plugin
|
||||
|
||||
// Phase G8c — 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.
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// 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).
|
||||
|
||||
func zoneSunkenTempleGraph() ZoneGraph {
|
||||
nodes := []ZoneNode{
|
||||
{NodeID: "sunken_temple.entry", Kind: NodeKindEntry, IsEntry: true,
|
||||
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.fork1", Kind: NodeKindFork,
|
||||
Label: "Split Stair", PosX: 2, PosY: 2},
|
||||
{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},
|
||||
{NodeID: "sunken_temple.kuo_toa_pen", Kind: NodeKindElite,
|
||||
Label: "Kuo-toa Pen", PosX: 4, PosY: 0},
|
||||
{NodeID: "sunken_temple.glyph_chamber", Kind: NodeKindExploration,
|
||||
Label: "Glyph Chamber", PosX: 4, PosY: 1},
|
||||
{NodeID: "sunken_temple.aboleth_thralls", Kind: NodeKindExploration,
|
||||
Label: "Thrall Pool", PosX: 4, PosY: 3},
|
||||
{NodeID: "sunken_temple.coral_reliquary", Kind: NodeKindSecret,
|
||||
Label: "Coral Reliquary", PosX: 4, PosY: 4,
|
||||
Content: ZoneNodeContent{LootBias: 1.8}},
|
||||
{NodeID: "sunken_temple.boss", Kind: NodeKindBoss, IsBoss: true,
|
||||
Label: "Aboleth's Pool", PosX: 5, 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",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 13},
|
||||
Hint: "wet stone glistens down a side passage", Weight: 2},
|
||||
{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.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.coral_reliquary", To: "sunken_temple.boss", Lock: LockNone},
|
||||
}
|
||||
return BuildGraph(ZoneSunkenTemple, nodes, edges)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerZoneGraph(zoneSunkenTempleGraph())
|
||||
}
|
||||
80
internal/plugin/zone_graph_sunken_temple_test.go
Normal file
80
internal/plugin/zone_graph_sunken_temple_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package plugin
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSunkenTempleGraph_Registered(t *testing.T) {
|
||||
g, ok := zoneGraphRegistry[ZoneSunkenTemple]
|
||||
if !ok {
|
||||
t.Fatal("zoneSunkenTempleGraph not registered")
|
||||
}
|
||||
if g.Entry != "sunken_temple.entry" {
|
||||
t.Errorf("entry node = %q", g.Entry)
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSunkenTempleGraph_FourDistinctPaths verifies the four leaves
|
||||
// (kuo_toa_pen, glyph_chamber, aboleth_thralls, coral_reliquary) all
|
||||
// reach the boss. Together with the no-merge invariant below, this
|
||||
// confirms the design intent: four committed paths, not a diamond.
|
||||
func TestSunkenTempleGraph_FourDistinctPaths(t *testing.T) {
|
||||
g := zoneSunkenTempleGraph()
|
||||
for _, leaf := range []string{
|
||||
"sunken_temple.kuo_toa_pen",
|
||||
"sunken_temple.glyph_chamber",
|
||||
"sunken_temple.aboleth_thralls",
|
||||
"sunken_temple.coral_reliquary",
|
||||
} {
|
||||
if !reachable(g, leaf, "sunken_temple.boss") {
|
||||
t.Errorf("%s unreachable to boss", leaf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSunkenTempleGraph_NoMidPathMerge ensures no node other than the
|
||||
// boss has more than one incoming edge. If a future edit collapses the
|
||||
// two subtrees into a converging diamond, this test fails — that's the
|
||||
// signal to either rename the zone's shape comment or re-author.
|
||||
func TestSunkenTempleGraph_NoMidPathMerge(t *testing.T) {
|
||||
g := zoneSunkenTempleGraph()
|
||||
incoming := map[string]int{}
|
||||
for _, outs := range g.Edges {
|
||||
for _, e := range outs {
|
||||
incoming[e.To]++
|
||||
}
|
||||
}
|
||||
for id, n := range g.Nodes {
|
||||
if n.IsBoss {
|
||||
continue
|
||||
}
|
||||
if incoming[id] > 1 {
|
||||
t.Errorf("node %q has %d incoming edges, want ≤1 (no mid-path merge)", id, incoming[id])
|
||||
}
|
||||
}
|
||||
// Boss intentionally has 4 incoming edges (one per leaf).
|
||||
if incoming["sunken_temple.boss"] != 4 {
|
||||
t.Errorf("boss incoming = %d, want 4", incoming["sunken_temple.boss"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSunkenTempleGraph_LockKindCoverage(t *testing.T) {
|
||||
g := zoneSunkenTempleGraph()
|
||||
kinds := map[ZoneEdgeLockKind]bool{}
|
||||
for _, outs := range g.Edges {
|
||||
for _, e := range outs {
|
||||
kinds[e.Lock] = true
|
||||
}
|
||||
}
|
||||
// Sunken Temple deliberately exercises Perception + StatCheck +
|
||||
// LockNone in one zone.
|
||||
for _, k := range []ZoneEdgeLockKind{LockNone, LockPerception, LockStatCheck} {
|
||||
if !kinds[k] {
|
||||
t.Errorf("missing lock kind %s — variety check", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user