mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Branching zones G8a: Goblin Warrens diamond graph
T1 single-fork shape with no secret — deliberately the simplest
authored topology, mirroring the Crypt of Valdris pattern minus the
secret tail. Serves as the baseline reference shape; G8b onward will
vary topology (stub branch, sequential forks, hub-and-spoke, gauntlet,
convergent triangle) so zones feel structurally distinct on the
!zone map.
Validator-checked at init via BuildGraph. Both branches reach boss;
the Perception (DC 12) side carries a player-facing hint per plan §G8
guideline ("locked paths should always have a hint").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
53
internal/plugin/zone_graph_goblin_warrens.go
Normal file
53
internal/plugin/zone_graph_goblin_warrens.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package plugin
|
||||
|
||||
// Phase G8a — Goblin Warrens branching graph.
|
||||
//
|
||||
// First non-POC zone. T1 design constraint per
|
||||
// gogobee_branching_zones_plan.md §G8: one fork per zone for T1, no
|
||||
// secret (the secret pattern is exercised by Crypt of Valdris; Goblin
|
||||
// Warrens deliberately validates the bare single-fork shape on a
|
||||
// non-POC zone so we can confirm legacy compileLegacyZoneGraph fully
|
||||
// hands off to the authored graph end-to-end).
|
||||
//
|
||||
// entry → guard_post → fork
|
||||
// ├──[unlocked]── warband_pit (elite) ──┐
|
||||
// │ ├── warchief_hall → boss
|
||||
// └──[Perception DC 12]── collapsed_shaft ┘
|
||||
//
|
||||
// Both branches reach the boss; Perception side is hinted (per plan:
|
||||
// "Locked paths should always have a hint — cruel design otherwise").
|
||||
|
||||
func zoneGoblinWarrensGraph() ZoneGraph {
|
||||
nodes := []ZoneNode{
|
||||
{NodeID: "goblin_warrens.entry", Kind: NodeKindEntry, IsEntry: true,
|
||||
Label: "Tunnel Mouth", PosX: 0, PosY: 1},
|
||||
{NodeID: "goblin_warrens.guard_post", Kind: NodeKindExploration,
|
||||
Label: "Goblin Guard Post", PosX: 1, PosY: 1},
|
||||
{NodeID: "goblin_warrens.fork", Kind: NodeKindFork,
|
||||
Label: "Cavern Junction", PosX: 2, PosY: 1},
|
||||
{NodeID: "goblin_warrens.warband_pit", Kind: NodeKindElite,
|
||||
Label: "Warband Pit", PosX: 3, PosY: 0},
|
||||
{NodeID: "goblin_warrens.collapsed_shaft", Kind: NodeKindExploration,
|
||||
Label: "Collapsed Shaft", PosX: 3, PosY: 2},
|
||||
{NodeID: "goblin_warrens.warchief_hall", Kind: NodeKindMerge,
|
||||
Label: "Warchief's Hall", PosX: 4, PosY: 1},
|
||||
{NodeID: "goblin_warrens.boss", Kind: NodeKindBoss, IsBoss: true,
|
||||
Label: "Grol's Throne", PosX: 5, PosY: 1},
|
||||
}
|
||||
edges := []ZoneEdge{
|
||||
{From: "goblin_warrens.entry", To: "goblin_warrens.guard_post", Lock: LockNone},
|
||||
{From: "goblin_warrens.guard_post", To: "goblin_warrens.fork", Lock: LockNone},
|
||||
{From: "goblin_warrens.fork", To: "goblin_warrens.warband_pit", Lock: LockNone, Weight: 1},
|
||||
{From: "goblin_warrens.fork", To: "goblin_warrens.collapsed_shaft",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 12},
|
||||
Hint: "a muffled scrape behind a heap of fallen timbers", Weight: 2},
|
||||
{From: "goblin_warrens.warband_pit", To: "goblin_warrens.warchief_hall", Lock: LockNone},
|
||||
{From: "goblin_warrens.collapsed_shaft", To: "goblin_warrens.warchief_hall", Lock: LockNone},
|
||||
{From: "goblin_warrens.warchief_hall", To: "goblin_warrens.boss", Lock: LockNone},
|
||||
}
|
||||
return BuildGraph(ZoneGoblinWarrens, nodes, edges)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerZoneGraph(zoneGoblinWarrensGraph())
|
||||
}
|
||||
63
internal/plugin/zone_graph_goblin_warrens_test.go
Normal file
63
internal/plugin/zone_graph_goblin_warrens_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package plugin
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGoblinWarrensGraph_Registered(t *testing.T) {
|
||||
g, ok := zoneGraphRegistry[ZoneGoblinWarrens]
|
||||
if !ok {
|
||||
t.Fatal("zoneGoblinWarrensGraph not registered")
|
||||
}
|
||||
if g.Entry != "goblin_warrens.entry" {
|
||||
t.Errorf("entry node = %q, want goblin_warrens.entry", g.Entry)
|
||||
}
|
||||
if g.Boss != "goblin_warrens.boss" {
|
||||
t.Errorf("boss node = %q, want goblin_warrens.boss", g.Boss)
|
||||
}
|
||||
if len(g.Nodes) != 7 {
|
||||
t.Errorf("nodes = %d, want 7", len(g.Nodes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoblinWarrensGraph_BothPathsReachBoss(t *testing.T) {
|
||||
g := zoneGoblinWarrensGraph()
|
||||
if !reachable(g, "goblin_warrens.warband_pit", "goblin_warrens.boss") {
|
||||
t.Error("warband_pit path unreachable to boss")
|
||||
}
|
||||
if !reachable(g, "goblin_warrens.collapsed_shaft", "goblin_warrens.boss") {
|
||||
t.Error("collapsed_shaft path unreachable to boss")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoblinWarrensGraph_ForkLayout(t *testing.T) {
|
||||
g := zoneGoblinWarrensGraph()
|
||||
outs := g.outgoingEdges("goblin_warrens.fork")
|
||||
if len(outs) != 2 {
|
||||
t.Fatalf("fork outgoing edges = %d, want 2", len(outs))
|
||||
}
|
||||
// outgoingEdges sorts by weight asc — warband_pit (weight 1) first.
|
||||
if outs[0].To != "goblin_warrens.warband_pit" || outs[0].Lock != LockNone {
|
||||
t.Errorf("first edge = %+v, want warband_pit/LockNone", outs[0])
|
||||
}
|
||||
if outs[1].To != "goblin_warrens.collapsed_shaft" || outs[1].Lock != LockPerception {
|
||||
t.Errorf("second edge = %+v, want collapsed_shaft/LockPerception", outs[1])
|
||||
}
|
||||
if outs[1].Hint == "" {
|
||||
t.Error("collapsed_shaft edge missing player-facing hint (plan §G8: cruel design)")
|
||||
}
|
||||
if dc := lockDataInt(outs[1].LockData, "dc", 0); dc != 12 {
|
||||
t.Errorf("perception DC = %d, want 12", dc)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGoblinWarrensGraph_NoSecretByDesign locks in the T1 "single fork,
|
||||
// no secret" decision recorded in project_phase_G_progress memo. If a
|
||||
// later session adds a secret to this zone, this test should be removed
|
||||
// or updated deliberately — not silently.
|
||||
func TestGoblinWarrensGraph_NoSecretByDesign(t *testing.T) {
|
||||
g := zoneGoblinWarrensGraph()
|
||||
for _, n := range g.Nodes {
|
||||
if n.Kind == NodeKindSecret {
|
||||
t.Errorf("unexpected secret node %q — Goblin Warrens is T1 single-fork by design", n.NodeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user