Files
gogobee/internal/plugin/dnd_zone_test.go
prosolis ba2a2b5e90 Adv 2.0 D&D Phase 11 D1a: zone registry + Tier 1 zones
Zone Infrastructure SUB-A: ZoneDefinition / ZoneTier / ZoneEnemy /
ZoneBoss / ZoneLootEntry types and dndZoneRegistry. Tier 1 zones
populated per gogobee_dungeon_zones.md §5: Goblin Warrens (T1, L1-3,
boss Grol the Unbroken CR3) and Crypt of Valdris (T1, L1-3, boss
Valdris the Unburied CR5, phase 2 at 50% HP).

Helpers: getZone, zonesForLevel (2-tier-above ceiling), zonesByTier,
allZones.

Bestiary additions for Tier 1 enemy rosters: goblin_sneak,
goblin_archer, hobgoblin_grunt, worg, goblin_shaman,
hobgoblin_warchief, zombie, shadow, specter, wight, flameskull, plus
the two zone bosses. Zombie AC bumped 8->10 to satisfy engine
TestBestiaryAllWellFormed minimum.

7 new tests covering registry presence, bestiary-ref resolution, boss
CR floor, level-tier gating, loot-chance bounds, room-count sanity,
elite flag presence, and tier->level-range alignment. Full plugin
suite green.

Also marks SUB3 done in gogobee_subclass_system.md (all 15 subclasses
through L15 shipped across SUB3a-d).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00

123 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package plugin
import "testing"
// Phase 11 D1a — zone registry tests. Validates that:
// 1. Tier 1 zones are registered.
// 2. All enemy/boss BestiaryID references resolve.
// 3. Level gating (zonesForLevel) respects the 2-tier-above ceiling.
// 4. Loot tables either have explicit drop chances in [0,1] or
// UniqueAlways set.
func TestZoneRegistry_Tier1Present(t *testing.T) {
if _, ok := getZone(ZoneGoblinWarrens); !ok {
t.Fatal("Goblin Warrens not registered")
}
if _, ok := getZone(ZoneCryptValdris); !ok {
t.Fatal("Crypt of Valdris not registered")
}
t1 := zonesByTier(ZoneTierBeginner)
if len(t1) != 2 {
t.Fatalf("expected 2 Tier 1 zones, got %d", len(t1))
}
}
func TestZoneRegistry_BestiaryRefsResolve(t *testing.T) {
for _, z := range allZones() {
for _, e := range z.Enemies {
if _, ok := dndBestiary[e.BestiaryID]; !ok {
t.Errorf("zone %s enemy %s missing from bestiary", z.ID, e.BestiaryID)
}
}
if _, ok := dndBestiary[z.Boss.BestiaryID]; !ok {
t.Errorf("zone %s boss %s missing from bestiary", z.ID, z.Boss.BestiaryID)
}
}
}
func TestZoneRegistry_BossCRMatchesZoneTier(t *testing.T) {
// Sanity: boss CR should be at least at the high end of tier
// CR range. Tier 1 = CR 1/81 enemies, but boss is CR 35.
for _, z := range allZones() {
if z.Boss.CR < 1 {
t.Errorf("zone %s boss CR %.1f too low", z.ID, z.Boss.CR)
}
}
}
func TestZonesForLevel_TierGate(t *testing.T) {
// L1 player (tier 1): can reach tier 1+2=3. With only T1 zones
// registered in D1a, returns both.
got := zonesForLevel(1)
if len(got) != 2 {
t.Fatalf("L1 should see 2 T1 zones, got %d", len(got))
}
// L20 player should also see all zones (no upper cutoff).
got = zonesForLevel(20)
if len(got) < 2 {
t.Fatalf("L20 should see all zones, got %d", len(got))
}
}
func TestZoneRegistry_LootDropChances(t *testing.T) {
for _, z := range allZones() {
for _, l := range z.Loot {
if l.UniqueAlways {
continue
}
if l.DropChance <= 0 || l.DropChance > 1 {
t.Errorf("zone %s loot %s drop chance %.2f out of range",
z.ID, l.ItemID, l.DropChance)
}
}
}
}
func TestZoneRegistry_RoomCountSane(t *testing.T) {
for _, z := range allZones() {
if z.MinRooms < 5 || z.MaxRooms > 10 || z.MinRooms > z.MaxRooms {
t.Errorf("zone %s rooms %d-%d outside design (5-10, min<=max)",
z.ID, z.MinRooms, z.MaxRooms)
}
}
}
func TestZoneRegistry_ElitesFlagged(t *testing.T) {
// Each Tier 1 zone roster should contain at least one elite (per
// design doc each zone lists ELITE entries).
for _, z := range allZones() {
anyElite := false
for _, e := range z.Enemies {
if e.IsElite {
anyElite = true
break
}
}
if !anyElite {
t.Errorf("zone %s has no elite enemy", z.ID)
}
}
}
func TestZoneRegistry_LevelRangeMatchesTier(t *testing.T) {
// Tier→expected level range per design doc §2.
expected := map[ZoneTier][2]int{
ZoneTierBeginner: {1, 3},
ZoneTierApprentice: {3, 5},
ZoneTierJourneyman: {5, 8},
ZoneTierVeteran: {8, 12},
ZoneTierLegendary: {12, 20},
}
for _, z := range allZones() {
want, ok := expected[z.Tier]
if !ok {
t.Errorf("zone %s has unknown tier %d", z.ID, z.Tier)
continue
}
if z.LevelMin < want[0] || z.LevelMax > want[1] {
t.Errorf("zone %s level %d-%d outside tier %d range %d-%d",
z.ID, z.LevelMin, z.LevelMax, z.Tier, want[0], want[1])
}
}
}