mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
J3 D10: T4/T5 anchor-room variety (2nd elite + trap per zone)
In-place node kind swaps on fork branches — no node added/removed, longest path unchanged — adding one Elite + one Trap of anchor variety to each T4/T5 zone, placed to keep fork-choice risk/loot asymmetric: underdark +Elite drow_gate (R2->R4 region-guardian) +Trap silenced_chamber feywild +Elite singing_orchard (grove branch) +Trap mire_steps (marsh) dragons_lair+Elite coin_strewn_hall (treasure spur) +Trap hidden_passage (hoard spur) abyss_portal+Elite wardens_hall (R3 guardian) +Trap hush_corridor, seam_threshold abyss_portal shipped with zero trap nodes; D10 gives it two. A/B sim corpus (n=480/side): boss-clear flat (+1.5pp aggregate, per-cell within n=15 noise), median day-counts stable, combats/run rose where anchors hit common paths. New Test*Graph_*Anchors tests lock the per-zone trap/elite counts.
This commit is contained in:
@@ -56,6 +56,18 @@ package plugin
|
||||
// Longest entry→boss walk: 10 (R1) + 10 (R2) + 10 (R3) + 10 (R4) = 40
|
||||
// nodes, inside [36, 44]. All four meaningful walks (fork1 × fork2 ×
|
||||
// capstone) hit the same node count by construction.
|
||||
//
|
||||
// D10 anchor variety (in-place kind swaps, no length change): the Abyss
|
||||
// shipped with NO trap node and only the fork2 vrock ELITE, so D10 adds
|
||||
// two traps and a region-guardian elite —
|
||||
// - Hush Corridor (fork1 silent_chambers Perception-loot spur) and
|
||||
// Seam Threshold (fork3 reality_seam SECRET spur) become TRAPs, so
|
||||
// the two loot/secret branches each carry a hazard.
|
||||
// - Warden's Hall (R3 wardens_post buildup, main path) becomes an
|
||||
// ELITE — the region literally named for its wardens finally has
|
||||
// one, giving every walk a mid-zone region-guardian.
|
||||
// The burning_wastes / mind_corridor / void_charge / usurper_throne
|
||||
// routes stay clean, preserving the safe-vs-loot fork trade-off.
|
||||
|
||||
func zoneAbyssPortalGraph() ZoneGraph {
|
||||
r1 := "abyss_outer_rift"
|
||||
@@ -96,7 +108,7 @@ func zoneAbyssPortalGraph() ZoneGraph {
|
||||
|
||||
{NodeID: "abyss_portal.silent_chambers", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Silent Chambers", PosX: 10, PosY: 3},
|
||||
{NodeID: "abyss_portal.hush_corridor", Kind: NodeKindExploration, RegionID: r2,
|
||||
{NodeID: "abyss_portal.hush_corridor", Kind: NodeKindTrap, RegionID: r2,
|
||||
Label: "Hush Corridor", PosX: 11, PosY: 3},
|
||||
{NodeID: "abyss_portal.listening_room", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Listening Room", PosX: 12, PosY: 3},
|
||||
@@ -135,7 +147,7 @@ func zoneAbyssPortalGraph() ZoneGraph {
|
||||
// R3 buildup to fork3.
|
||||
{NodeID: "abyss_portal.wardens_outer_post", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Outer Warden Post", PosX: 23, PosY: 2},
|
||||
{NodeID: "abyss_portal.wardens_hall", Kind: NodeKindExploration, RegionID: r3,
|
||||
{NodeID: "abyss_portal.wardens_hall", Kind: NodeKindElite, RegionID: r3,
|
||||
Label: "Warden's Hall", PosX: 24, PosY: 2},
|
||||
{NodeID: "abyss_portal.wardens_chapel", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Warden's Chapel", PosX: 25, PosY: 2},
|
||||
@@ -163,7 +175,7 @@ func zoneAbyssPortalGraph() ZoneGraph {
|
||||
{NodeID: "abyss_portal.claimed_path", Kind: NodeKindExploration, RegionID: r4,
|
||||
Label: "Claimed Path", PosX: 32, PosY: 2},
|
||||
|
||||
{NodeID: "abyss_portal.seam_threshold", Kind: NodeKindExploration, RegionID: r4,
|
||||
{NodeID: "abyss_portal.seam_threshold", Kind: NodeKindTrap, RegionID: r4,
|
||||
Label: "Seam Threshold", PosX: 30, PosY: 3},
|
||||
{NodeID: "abyss_portal.reality_seam", Kind: NodeKindSecret, RegionID: r4,
|
||||
Label: "Reality Seam", PosX: 31, PosY: 3,
|
||||
|
||||
@@ -119,3 +119,34 @@ func TestAbyssPortalGraph_RealitySeamHighestBias(t *testing.T) {
|
||||
t.Errorf("reality_seam LootBias = %v, want >= 3.0 (Abyss capstone)", seam.Content.LootBias)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAbyssPortalGraph_D10Anchors verifies the D10 anchor-variety pass.
|
||||
// The Abyss shipped with no Trap node and a single fork2 Elite, so D10
|
||||
// adds two branch Traps (Hush Corridor, Seam Threshold) and a main-path
|
||||
// region-guardian Elite (Warden's Hall). Counts: 2 traps, 2 elites.
|
||||
func TestAbyssPortalGraph_D10Anchors(t *testing.T) {
|
||||
g := zoneAbyssPortalGraph()
|
||||
var trapCount, eliteCount int
|
||||
for _, n := range g.Nodes {
|
||||
switch n.Kind {
|
||||
case NodeKindTrap:
|
||||
trapCount++
|
||||
case NodeKindElite:
|
||||
eliteCount++
|
||||
}
|
||||
}
|
||||
if trapCount != 2 {
|
||||
t.Errorf("trap nodes = %d, want 2 (D10 hush_corridor + seam_threshold)", trapCount)
|
||||
}
|
||||
if eliteCount != 2 {
|
||||
t.Errorf("elite nodes = %d, want 2 (vrock_aerie + D10 wardens_hall)", eliteCount)
|
||||
}
|
||||
if g.Nodes["abyss_portal.wardens_hall"].Kind != NodeKindElite {
|
||||
t.Error("D10: wardens_hall (R3 region-guardian) should be an Elite")
|
||||
}
|
||||
for _, id := range []string{"abyss_portal.hush_corridor", "abyss_portal.seam_threshold"} {
|
||||
if g.Nodes[id].Kind != NodeKindTrap {
|
||||
t.Errorf("D10: %s should be a Trap", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,17 @@ package plugin
|
||||
// Longest entry→boss walk: 10 (R1) + 10 (R2) + 9 (R3) + 10 (R4) = 39
|
||||
// nodes, inside [36, 44]. The two R2 spurs and three R4 capstones each
|
||||
// reach the boss in the same node count by construction.
|
||||
//
|
||||
// D10 anchor variety (in-place kind swaps, no length change): the two
|
||||
// loot-leaning branches each gain a guard, so "the richer route costs
|
||||
// more" —
|
||||
// - Coin-Strewn Hall (treasure_vault spur) becomes an ELITE; the
|
||||
// Perception loot route is now guarded, mirroring the ash_bridge
|
||||
// spur's TRAP so both fork1 branches carry an anchor.
|
||||
// - Hidden Passage (hoard_pillar SECRET capstone spur) becomes a
|
||||
// TRAP guarding the densest loot in the zone.
|
||||
// The direct-confrontation and dragon-bargain routes stay clean, so the
|
||||
// fork choice trades safety for loot.
|
||||
|
||||
func zoneDragonsLairGraph() ZoneGraph {
|
||||
r1 := "dragons_lair_kobold_warrens"
|
||||
@@ -110,7 +121,7 @@ func zoneDragonsLairGraph() ZoneGraph {
|
||||
{NodeID: "dragons_lair.treasure_vault", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Treasure Vault", PosX: 17, PosY: 2,
|
||||
Content: ZoneNodeContent{LootBias: 1.5}},
|
||||
{NodeID: "dragons_lair.coin_strewn_hall", Kind: NodeKindExploration, RegionID: r2,
|
||||
{NodeID: "dragons_lair.coin_strewn_hall", Kind: NodeKindElite, RegionID: r2,
|
||||
Label: "Coin-Strewn Hall", PosX: 18, PosY: 2},
|
||||
{NodeID: "dragons_lair.vault_passage", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Vault Passage", PosX: 19, PosY: 2},
|
||||
@@ -152,7 +163,7 @@ func zoneDragonsLairGraph() ZoneGraph {
|
||||
Label: "Audience Hall", PosX: 31, PosY: 1},
|
||||
|
||||
// R4 hoard_pillar spur (Perception 17, SECRET).
|
||||
{NodeID: "dragons_lair.hidden_passage", Kind: NodeKindExploration, RegionID: r4,
|
||||
{NodeID: "dragons_lair.hidden_passage", Kind: NodeKindTrap, RegionID: r4,
|
||||
Label: "Hidden Passage", PosX: 29, PosY: 2},
|
||||
{NodeID: "dragons_lair.hoard_pillar", Kind: NodeKindSecret, RegionID: r4,
|
||||
Label: "Hidden Hoard Pillar", PosX: 30, PosY: 2,
|
||||
|
||||
@@ -111,3 +111,32 @@ func TestDragonsLairGraph_LootBiasEscalation(t *testing.T) {
|
||||
t.Errorf("hoard_pillar LootBias = %v, want >= 2.5 (T5 secret)", hoard.Content.LootBias)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDragonsLairGraph_D10Anchors verifies the D10 anchor-variety pass:
|
||||
// the treasure_vault spur gains a guarding Elite (Coin-Strewn Hall) to
|
||||
// mirror the ash_bridge spur's Trap, and the hoard_pillar SECRET capstone
|
||||
// spur gains a Trap (Hidden Passage). Counts: 2 traps, 2 elites.
|
||||
func TestDragonsLairGraph_D10Anchors(t *testing.T) {
|
||||
g := zoneDragonsLairGraph()
|
||||
var trapCount, eliteCount int
|
||||
for _, n := range g.Nodes {
|
||||
switch n.Kind {
|
||||
case NodeKindTrap:
|
||||
trapCount++
|
||||
case NodeKindElite:
|
||||
eliteCount++
|
||||
}
|
||||
}
|
||||
if trapCount != 2 {
|
||||
t.Errorf("trap nodes = %d, want 2 (ash_bridge + D10 hidden_passage)", trapCount)
|
||||
}
|
||||
if eliteCount != 2 {
|
||||
t.Errorf("elite nodes = %d, want 2 (wyrmlings_nest + D10 coin_strewn_hall)", eliteCount)
|
||||
}
|
||||
if g.Nodes["dragons_lair.coin_strewn_hall"].Kind != NodeKindElite {
|
||||
t.Error("D10: coin_strewn_hall (treasure_vault spur) should be an Elite")
|
||||
}
|
||||
if g.Nodes["dragons_lair.hidden_passage"].Kind != NodeKindTrap {
|
||||
t.Error("D10: hidden_passage (hoard_pillar spur) should be a Trap")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,16 @@ package plugin
|
||||
// Also new: cursed_thicket TRAP anchor in the preamble — every walk
|
||||
// hits it. The original G8f graph had no Trap node.
|
||||
//
|
||||
// D10 anchor variety (in-place kind swaps, no length change): the two
|
||||
// first-stage approaches each gain one distinct anchor so the fork
|
||||
// choice carries a flavor, not just a skill gate —
|
||||
// - Grove approach: Singing Orchard becomes an ELITE (the CHA-bonus
|
||||
// route is guarded; you pay for the bargain in blood).
|
||||
// - Marsh approach: Mire Steps becomes a TRAP (the free route is
|
||||
// hazardous footing rather than a fight).
|
||||
// The time_eddy / illusion_garden exclusive endings stay anchor-light so
|
||||
// they keep their "skip the hag elite for loot" trade-off.
|
||||
//
|
||||
// Preamble (10 nodes, ending in fork1):
|
||||
// entry → twilight_path → veiled_glade → faerie_lights →
|
||||
// cursed_thicket (TRAP) → revel_road → moonshadow_bridge →
|
||||
@@ -87,7 +97,7 @@ func zoneFeywildCrossingGraph() ZoneGraph {
|
||||
Label: "Grove Threshold", PosX: 10, PosY: 0},
|
||||
{NodeID: "feywild_crossing.starlight_path", Kind: NodeKindExploration,
|
||||
Label: "Starlight Path", PosX: 11, PosY: 0},
|
||||
{NodeID: "feywild_crossing.singing_orchard", Kind: NodeKindExploration,
|
||||
{NodeID: "feywild_crossing.singing_orchard", Kind: NodeKindElite,
|
||||
Label: "Singing Orchard", PosX: 12, PosY: 0},
|
||||
{NodeID: "feywild_crossing.mirror_pond", Kind: NodeKindExploration,
|
||||
Label: "Mirror Pond", PosX: 13, PosY: 0},
|
||||
@@ -109,7 +119,7 @@ func zoneFeywildCrossingGraph() ZoneGraph {
|
||||
Label: "Fog Basin", PosX: 12, PosY: 4},
|
||||
{NodeID: "feywild_crossing.moaning_reeds", Kind: NodeKindExploration,
|
||||
Label: "Moaning Reeds", PosX: 13, PosY: 4},
|
||||
{NodeID: "feywild_crossing.mire_steps", Kind: NodeKindExploration,
|
||||
{NodeID: "feywild_crossing.mire_steps", Kind: NodeKindTrap,
|
||||
Label: "Mire Steps", PosX: 14, PosY: 4},
|
||||
{NodeID: "feywild_crossing.submerged_stones", Kind: NodeKindExploration,
|
||||
Label: "Submerged Stones", PosX: 15, PosY: 4},
|
||||
|
||||
@@ -93,6 +93,8 @@ func TestFeywildCrossingGraph_FirstCHALock(t *testing.T) {
|
||||
|
||||
// TestFeywildCrossingGraph_TrapAnchor verifies D1-d added the missing
|
||||
// Trap node. Original G8f graph had elite/secret but no trap.
|
||||
// TestFeywildCrossingGraph_TrapAnchor verifies the preamble trap plus
|
||||
// the D10 marsh-branch trap (Mire Steps).
|
||||
func TestFeywildCrossingGraph_TrapAnchor(t *testing.T) {
|
||||
g := zoneFeywildCrossingGraph()
|
||||
var trapCount int
|
||||
@@ -101,8 +103,30 @@ func TestFeywildCrossingGraph_TrapAnchor(t *testing.T) {
|
||||
trapCount++
|
||||
}
|
||||
}
|
||||
if trapCount != 1 {
|
||||
t.Errorf("trap nodes = %d, want 1", trapCount)
|
||||
if trapCount != 2 {
|
||||
t.Errorf("trap nodes = %d, want 2 (cursed_thicket + D10 mire_steps)", trapCount)
|
||||
}
|
||||
if g.Nodes["feywild_crossing.mire_steps"].Kind != NodeKindTrap {
|
||||
t.Error("D10: mire_steps (marsh branch) should be a Trap")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFeywildCrossingGraph_EliteAnchors verifies the shared hag_circle
|
||||
// elite plus the D10 grove-branch elite (Singing Orchard), so the first
|
||||
// fork carries a per-branch anchor (grove=elite, marsh=trap).
|
||||
func TestFeywildCrossingGraph_EliteAnchors(t *testing.T) {
|
||||
g := zoneFeywildCrossingGraph()
|
||||
var eliteCount int
|
||||
for _, n := range g.Nodes {
|
||||
if n.Kind == NodeKindElite {
|
||||
eliteCount++
|
||||
}
|
||||
}
|
||||
if eliteCount != 2 {
|
||||
t.Errorf("elite nodes = %d, want 2 (hag_circle + D10 singing_orchard)", eliteCount)
|
||||
}
|
||||
if g.Nodes["feywild_crossing.singing_orchard"].Kind != NodeKindElite {
|
||||
t.Error("D10: singing_orchard (grove branch) should be an Elite")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,16 @@ package plugin
|
||||
//
|
||||
// Trap anchor (Collapsed Arch) is new — the original G8i graph had no
|
||||
// Trap node. Placed in the R1 preamble so every walk hits it.
|
||||
//
|
||||
// D10 anchor variety (in-place kind swaps, no length change):
|
||||
// - Silenced Chamber (R3 illithid arm) becomes a TRAP — the second
|
||||
// trap, on a fork branch so the illithid route reads riskier than
|
||||
// the drow route.
|
||||
// - Drow Gate Garrison (R2 arm tail, at the R2→R4 boundary) becomes a
|
||||
// region-guardian ELITE, giving the drow arm two elites (Captain +
|
||||
// Garrison) and the multi-region transition a teeth-y interrupt.
|
||||
// The deep_chasm spur stays anchor-light (harvest, no second trap/elite)
|
||||
// so the CON-gated climb keeps its "denser loot, less fighting" identity.
|
||||
|
||||
func zoneUnderdarkGraph() ZoneGraph {
|
||||
r1 := "underdark_surface_tunnels"
|
||||
@@ -111,15 +121,15 @@ func zoneUnderdarkGraph() ZoneGraph {
|
||||
Label: "Drow Descent", PosX: 17, PosY: 0},
|
||||
{NodeID: "underdark.drow_passage", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Lower Passage", PosX: 18, PosY: 0},
|
||||
{NodeID: "underdark.drow_gate", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Drow Gate", PosX: 19, PosY: 0},
|
||||
{NodeID: "underdark.drow_gate", Kind: NodeKindElite, RegionID: r2,
|
||||
Label: "Drow Gate Garrison", PosX: 19, PosY: 0},
|
||||
|
||||
// R3 illithid_warren arm.
|
||||
{NodeID: "underdark.psionic_corridor", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Psionic Corridor", PosX: 8, PosY: 2},
|
||||
{NodeID: "underdark.whispering_hall", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Whispering Hall", PosX: 9, PosY: 2},
|
||||
{NodeID: "underdark.silenced_chamber", Kind: NodeKindExploration, RegionID: r3,
|
||||
{NodeID: "underdark.silenced_chamber", Kind: NodeKindTrap, RegionID: r3,
|
||||
Label: "Silenced Chamber", PosX: 10, PosY: 2},
|
||||
{NodeID: "underdark.mind_tank_room", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Brine Tanks", PosX: 11, PosY: 2},
|
||||
|
||||
@@ -106,8 +106,8 @@ func TestUnderdarkGraph_AllArmsReachBoss(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnderdarkGraph_TrapAnchor verifies D1-d added the missing Trap
|
||||
// node. Original G8i graph had elite/boss/harvest but no trap.
|
||||
// TestUnderdarkGraph_TrapAnchor verifies D1-d added the missing R1 Trap
|
||||
// (collapsed_arch) and D10 added the illithid-arm Trap (silenced_chamber).
|
||||
func TestUnderdarkGraph_TrapAnchor(t *testing.T) {
|
||||
g := zoneUnderdarkGraph()
|
||||
var trapCount int
|
||||
@@ -116,7 +116,30 @@ func TestUnderdarkGraph_TrapAnchor(t *testing.T) {
|
||||
trapCount++
|
||||
}
|
||||
}
|
||||
if trapCount != 1 {
|
||||
t.Errorf("trap nodes = %d, want 1", trapCount)
|
||||
if trapCount != 2 {
|
||||
t.Errorf("trap nodes = %d, want 2 (collapsed_arch + D10 silenced_chamber)", trapCount)
|
||||
}
|
||||
if g.Nodes["underdark.silenced_chamber"].Kind != NodeKindTrap {
|
||||
t.Error("D10: silenced_chamber (illithid arm) should be a Trap")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnderdarkGraph_EliteAnchors verifies the per-arm elites (drow
|
||||
// Captain, illithid Mind Flayer) plus the D10 region-guardian elite at
|
||||
// the drow→throne boundary (drow_gate), so the drow arm carries two
|
||||
// elites while the chasm spur stays anchor-light.
|
||||
func TestUnderdarkGraph_EliteAnchors(t *testing.T) {
|
||||
g := zoneUnderdarkGraph()
|
||||
var eliteCount int
|
||||
for _, n := range g.Nodes {
|
||||
if n.Kind == NodeKindElite {
|
||||
eliteCount++
|
||||
}
|
||||
}
|
||||
if eliteCount != 3 {
|
||||
t.Errorf("elite nodes = %d, want 3 (drow_captain + mind_flayer + D10 drow_gate)", eliteCount)
|
||||
}
|
||||
if g.Nodes["underdark.drow_gate"].Kind != NodeKindElite {
|
||||
t.Error("D10: drow_gate (R2→R4 boundary) should be a region-guardian Elite")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user