mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Branching zones G8d: Manor Blackspire stacked 3-way forks (hub style)
T3 zone, fourth authored graph. Shape: two stacked 3-way forks (great_hall, upper_hall). The player picks twice in a row from a wide menu — distinct from the binary forks in G8a/b and the parallel Y-trees in G8c. Completes lock-kind coverage by G8d: this is the first authored use of LockLevelMin (tower_observatory gated at L7). With Perception (DC 14 study, DC 15 hidden_oratory), StatCheck INT (library), and LockNone, all four non-trivial lock kinds now appear in shipping zones. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
82
internal/plugin/zone_graph_manor_blackspire.go
Normal file
82
internal/plugin/zone_graph_manor_blackspire.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package plugin
|
||||
|
||||
// Phase G8d — Haunted Manor of Blackspire branching graph.
|
||||
//
|
||||
// T3 zone. Shape: stacked 3-way "hub" forks. The plan calls T2+ for
|
||||
// "two forks", and a Manor of corridors and locked doors fits a wide-
|
||||
// fork hub-and-spoke pattern more naturally than a binary fork. Two
|
||||
// hub nodes (great_hall, upper_hall), each with three options:
|
||||
//
|
||||
// entry → foyer → great_hall (3-way)
|
||||
// ├─[unlocked]── portrait_gallery ─┐
|
||||
// ├─[Perception DC 14]── study ────┼── upper_hall (3-way)
|
||||
// └─[INT DC 14]── library ─────────┘ ├─[unlocked]── master_bedroom (elite) → boss
|
||||
// ├─[Perception DC 15]── hidden_oratory (secret) → boss
|
||||
// └─[LevelMin 7]── tower_observatory → boss
|
||||
//
|
||||
// Two consecutive 3-way fork prompts make this the first zone where
|
||||
// the player picks twice in a row from a wide menu — distinct from the
|
||||
// binary forks of G8a/b and the parallel Y-trees of G8c. Exercises
|
||||
// LockLevelMin (first authored use) so all four non-trivial lock kinds
|
||||
// (Perception, StatCheck, LevelMin, plus implicit LockNone) appear by
|
||||
// G8d.
|
||||
|
||||
func zoneManorBlackspireGraph() ZoneGraph {
|
||||
nodes := []ZoneNode{
|
||||
{NodeID: "manor_blackspire.entry", Kind: NodeKindEntry, IsEntry: true,
|
||||
Label: "Manor Gate", PosX: 0, PosY: 2},
|
||||
{NodeID: "manor_blackspire.foyer", Kind: NodeKindExploration,
|
||||
Label: "Foyer", PosX: 1, PosY: 2},
|
||||
{NodeID: "manor_blackspire.great_hall", Kind: NodeKindFork,
|
||||
Label: "Great Hall", PosX: 2, PosY: 2},
|
||||
{NodeID: "manor_blackspire.portrait_gallery", Kind: NodeKindExploration,
|
||||
Label: "Portrait Gallery", PosX: 3, PosY: 1},
|
||||
{NodeID: "manor_blackspire.study", Kind: NodeKindExploration,
|
||||
Label: "Locked Study", PosX: 3, PosY: 2},
|
||||
{NodeID: "manor_blackspire.library", Kind: NodeKindExploration,
|
||||
Label: "Forbidden Library", PosX: 3, PosY: 3},
|
||||
{NodeID: "manor_blackspire.upper_hall", Kind: NodeKindFork,
|
||||
Label: "Upper Hall", PosX: 4, PosY: 2},
|
||||
{NodeID: "manor_blackspire.master_bedroom", Kind: NodeKindElite,
|
||||
Label: "Master Bedroom", PosX: 5, PosY: 1},
|
||||
{NodeID: "manor_blackspire.tower_observatory", Kind: NodeKindExploration,
|
||||
Label: "Tower Observatory", PosX: 5, PosY: 2},
|
||||
{NodeID: "manor_blackspire.hidden_oratory", Kind: NodeKindSecret,
|
||||
Label: "Hidden Oratory", PosX: 5, PosY: 3,
|
||||
Content: ZoneNodeContent{LootBias: 2.0}},
|
||||
{NodeID: "manor_blackspire.boss", Kind: NodeKindBoss, IsBoss: true,
|
||||
Label: "Aldric's Sanctum", PosX: 6, PosY: 2},
|
||||
}
|
||||
edges := []ZoneEdge{
|
||||
{From: "manor_blackspire.entry", To: "manor_blackspire.foyer", Lock: LockNone},
|
||||
{From: "manor_blackspire.foyer", To: "manor_blackspire.great_hall", Lock: LockNone},
|
||||
// Great Hall — 3-way: portrait (open), study (perception), library (intelligence)
|
||||
{From: "manor_blackspire.great_hall", To: "manor_blackspire.portrait_gallery", Lock: LockNone, Weight: 1},
|
||||
{From: "manor_blackspire.great_hall", To: "manor_blackspire.study",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 14},
|
||||
Hint: "an out-of-place draft from a doorframe", Weight: 2},
|
||||
{From: "manor_blackspire.great_hall", To: "manor_blackspire.library",
|
||||
Lock: LockStatCheck, LockData: map[string]any{"stat": "INT", "dc": 14},
|
||||
Hint: "a runed door — you'd need to read it", Weight: 2},
|
||||
// All three converge at upper_hall.
|
||||
{From: "manor_blackspire.portrait_gallery", To: "manor_blackspire.upper_hall", Lock: LockNone},
|
||||
{From: "manor_blackspire.study", To: "manor_blackspire.upper_hall", Lock: LockNone},
|
||||
{From: "manor_blackspire.library", To: "manor_blackspire.upper_hall", Lock: LockNone},
|
||||
// Upper Hall — 3-way: master_bedroom (open elite), hidden_oratory (high perception secret), tower (level-gated)
|
||||
{From: "manor_blackspire.upper_hall", To: "manor_blackspire.master_bedroom", Lock: LockNone, Weight: 1},
|
||||
{From: "manor_blackspire.upper_hall", To: "manor_blackspire.hidden_oratory",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 15},
|
||||
Hint: "a candle behind a panel that shouldn't have a back", Weight: 2},
|
||||
{From: "manor_blackspire.upper_hall", To: "manor_blackspire.tower_observatory",
|
||||
Lock: LockLevelMin, LockData: map[string]any{"min_level": 7},
|
||||
Hint: "a spiral stair that creaks ominously — climb only if you trust your footing", Weight: 3},
|
||||
{From: "manor_blackspire.master_bedroom", To: "manor_blackspire.boss", Lock: LockNone},
|
||||
{From: "manor_blackspire.hidden_oratory", To: "manor_blackspire.boss", Lock: LockNone},
|
||||
{From: "manor_blackspire.tower_observatory", To: "manor_blackspire.boss", Lock: LockNone},
|
||||
}
|
||||
return BuildGraph(ZoneManorBlackspire, nodes, edges)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerZoneGraph(zoneManorBlackspireGraph())
|
||||
}
|
||||
71
internal/plugin/zone_graph_manor_blackspire_test.go
Normal file
71
internal/plugin/zone_graph_manor_blackspire_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package plugin
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestManorBlackspireGraph_Registered(t *testing.T) {
|
||||
g, ok := zoneGraphRegistry[ZoneManorBlackspire]
|
||||
if !ok {
|
||||
t.Fatal("zoneManorBlackspireGraph not registered")
|
||||
}
|
||||
if g.Entry != "manor_blackspire.entry" {
|
||||
t.Errorf("entry node = %q", g.Entry)
|
||||
}
|
||||
if len(g.Nodes) != 11 {
|
||||
t.Errorf("nodes = %d, want 11", len(g.Nodes))
|
||||
}
|
||||
}
|
||||
|
||||
// TestManorBlackspireGraph_TwoStackedThreeWayForks captures the design
|
||||
// shape: both great_hall and upper_hall expose three options.
|
||||
func TestManorBlackspireGraph_TwoStackedThreeWayForks(t *testing.T) {
|
||||
g := zoneManorBlackspireGraph()
|
||||
for _, hub := range []string{
|
||||
"manor_blackspire.great_hall",
|
||||
"manor_blackspire.upper_hall",
|
||||
} {
|
||||
outs := g.outgoingEdges(hub)
|
||||
if len(outs) != 3 {
|
||||
t.Errorf("%s outgoing = %d, want 3", hub, len(outs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestManorBlackspireGraph_AllSpokesReachBoss(t *testing.T) {
|
||||
g := zoneManorBlackspireGraph()
|
||||
for _, leaf := range []string{
|
||||
"manor_blackspire.master_bedroom",
|
||||
"manor_blackspire.tower_observatory",
|
||||
"manor_blackspire.hidden_oratory",
|
||||
"manor_blackspire.portrait_gallery",
|
||||
"manor_blackspire.study",
|
||||
"manor_blackspire.library",
|
||||
} {
|
||||
if !reachable(g, leaf, "manor_blackspire.boss") {
|
||||
t.Errorf("%s unreachable to boss", leaf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestManorBlackspireGraph_LockLevelMinFirstUse verifies this zone is
|
||||
// the first to author LockLevelMin, completing lock-kind coverage
|
||||
// (Perception, StatCheck, LevelMin, LockNone) by G8d.
|
||||
func TestManorBlackspireGraph_LockLevelMinFirstUse(t *testing.T) {
|
||||
g := zoneManorBlackspireGraph()
|
||||
var levelMinEdge *ZoneEdge
|
||||
for _, outs := range g.Edges {
|
||||
for i := range outs {
|
||||
if outs[i].Lock == LockLevelMin {
|
||||
levelMinEdge = &outs[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
if levelMinEdge == nil {
|
||||
t.Fatal("expected at least one LockLevelMin edge")
|
||||
}
|
||||
if min := lockDataInt(levelMinEdge.LockData, "min_level", 0); min < 7 {
|
||||
t.Errorf("min_level = %d, want >= 7 (T3 zone)", min)
|
||||
}
|
||||
if levelMinEdge.Hint == "" {
|
||||
t.Error("level-gated edge missing hint")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user