Files
Catacombs/dungeon/generator_test.go
tolelom 65c062a1f7 feat: seed-based dungeon generation for deterministic floors
Thread *rand.Rand through GenerateFloor, splitBSP, and RandomRoomType
so floors can be reproduced from a seed. This enables daily challenges
in Phase 3. All callers now create a local rng instance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:39:21 +09:00

134 lines
3.6 KiB
Go

package dungeon
import (
"math/rand"
"testing"
)
func newTestRng() *rand.Rand {
return rand.New(rand.NewSource(rand.Int63()))
}
func TestGenerateFloor(t *testing.T) {
floor := GenerateFloor(1, newTestRng())
if len(floor.Rooms) < 5 || len(floor.Rooms) > 8 {
t.Errorf("Room count: got %d, want 5~8", len(floor.Rooms))
}
bossCount := 0
for _, r := range floor.Rooms {
if r.Type == RoomBoss {
bossCount++
}
}
if bossCount != 1 {
t.Errorf("Boss rooms: got %d, want 1", bossCount)
}
visited := make(map[int]bool)
var dfs func(int)
dfs = func(idx int) {
if visited[idx] { return }
visited[idx] = true
for _, n := range floor.Rooms[idx].Neighbors { dfs(n) }
}
dfs(0)
if len(visited) != len(floor.Rooms) {
t.Errorf("Not all rooms connected: reachable %d / %d", len(visited), len(floor.Rooms))
}
}
func TestRoomTypeProbability(t *testing.T) {
counts := make(map[RoomType]int)
n := 10000
rng := rand.New(rand.NewSource(12345))
for i := 0; i < n; i++ {
counts[RandomRoomType(rng)]++
}
combatPct := float64(counts[RoomCombat]) / float64(n) * 100
if combatPct < 40 || combatPct > 50 {
t.Errorf("Combat room probability: got %.1f%%, want ~45%% (range 5-50)", combatPct)
}
}
func TestSecretRoomInRandomType(t *testing.T) {
counts := make(map[RoomType]int)
n := 10000
rng := rand.New(rand.NewSource(12345))
for i := 0; i < n; i++ {
counts[RandomRoomType(rng)]++
}
secretPct := float64(counts[RoomSecret]) / float64(n) * 100
if secretPct < 2 || secretPct > 8 {
t.Errorf("Secret room probability: got %.1f%%, want ~5%%", secretPct)
}
// Verify RoomMiniBoss is never returned by RandomRoomType
if counts[RoomMiniBoss] > 0 {
t.Errorf("MiniBoss rooms should not be generated randomly, got %d", counts[RoomMiniBoss])
}
}
func TestMiniBossRoomPlacement(t *testing.T) {
for _, floorNum := range []int{4, 9, 14, 19} {
floor := GenerateFloor(floorNum, newTestRng())
found := false
for _, r := range floor.Rooms {
if r.Type == RoomMiniBoss {
found = true
break
}
}
if !found {
t.Errorf("Floor %d should have a mini-boss room", floorNum)
}
}
// Non-miniboss floors should not have mini-boss rooms
for _, floorNum := range []int{1, 3, 5, 10} {
floor := GenerateFloor(floorNum, newTestRng())
for _, r := range floor.Rooms {
if r.Type == RoomMiniBoss {
t.Errorf("Floor %d should not have a mini-boss room", floorNum)
break
}
}
}
}
func TestFloorHasTileMap(t *testing.T) {
floor := GenerateFloor(1, newTestRng())
if floor.Tiles == nil {
t.Fatal("Floor should have tile map")
}
if floor.Width != 60 || floor.Height != 20 {
t.Errorf("Map size: got %dx%d, want 60x20", floor.Width, floor.Height)
}
// Current room should have floor tiles
room := floor.Rooms[0]
centerTile := floor.Tiles[room.Y+room.H/2][room.X+room.W/2]
if centerTile != TileFloor {
t.Errorf("Room center should be floor tile, got %d", centerTile)
}
}
func TestDeterministicGeneration(t *testing.T) {
rng1 := rand.New(rand.NewSource(42))
rng2 := rand.New(rand.NewSource(42))
f1 := GenerateFloor(5, rng1)
f2 := GenerateFloor(5, rng2)
if len(f1.Rooms) != len(f2.Rooms) {
t.Fatalf("room counts differ: %d vs %d", len(f1.Rooms), len(f2.Rooms))
}
for i, r := range f1.Rooms {
if r.Type != f2.Rooms[i].Type || r.X != f2.Rooms[i].X || r.Y != f2.Rooms[i].Y {
t.Errorf("room %d differs: type=%v/%v x=%d/%d y=%d/%d",
i, r.Type, f2.Rooms[i].Type, r.X, f2.Rooms[i].X, r.Y, f2.Rooms[i].Y)
}
}
// Also verify tile maps match
for y := 0; y < f1.Height; y++ {
for x := 0; x < f1.Width; x++ {
if f1.Tiles[y][x] != f2.Tiles[y][x] {
t.Errorf("tile at (%d,%d) differs: %d vs %d", x, y, f1.Tiles[y][x], f2.Tiles[y][x])
}
}
}
}