feat: dungeon generation — BSP rooms, room types, fog of war

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 23:50:43 +09:00
parent 4fdd7a1ad0
commit 8849bf5220
4 changed files with 161 additions and 0 deletions

27
dungeon/fov.go Normal file
View File

@@ -0,0 +1,27 @@
package dungeon
type Visibility int
const (
Hidden Visibility = iota
Visited
Visible
)
func UpdateVisibility(floor *Floor) {
for i, room := range floor.Rooms {
if i == floor.CurrentRoom {
room.Visited = true
}
}
}
func GetRoomVisibility(floor *Floor, roomIdx int) Visibility {
if roomIdx == floor.CurrentRoom {
return Visible
}
if floor.Rooms[roomIdx].Visited {
return Visited
}
return Hidden
}

49
dungeon/generator.go Normal file
View File

@@ -0,0 +1,49 @@
package dungeon
import "math/rand"
type Floor struct {
Number int
Rooms []*Room
CurrentRoom int
}
func GenerateFloor(floorNum int) *Floor {
numRooms := 5 + rand.Intn(4)
rooms := make([]*Room, numRooms)
for i := 0; i < numRooms; i++ {
rt := RandomRoomType()
rooms[i] = &Room{
Type: rt,
X: (i % 3) * 20,
Y: (i / 3) * 10,
Width: 12 + rand.Intn(6),
Height: 6 + rand.Intn(4),
Neighbors: []int{},
}
}
rooms[numRooms-1].Type = RoomBoss
for i := 0; i < numRooms-1; i++ {
rooms[i].Neighbors = append(rooms[i].Neighbors, i+1)
rooms[i+1].Neighbors = append(rooms[i+1].Neighbors, i)
}
extras := 1 + rand.Intn(2)
for e := 0; e < extras; e++ {
a := rand.Intn(numRooms)
b := rand.Intn(numRooms)
if a != b && !hasNeighbor(rooms[a], b) {
rooms[a].Neighbors = append(rooms[a].Neighbors, b)
rooms[b].Neighbors = append(rooms[b].Neighbors, a)
}
}
return &Floor{Number: floorNum, Rooms: rooms, CurrentRoom: 0}
}
func hasNeighbor(r *Room, idx int) bool {
for _, n := range r.Neighbors {
if n == idx {
return true
}
}
return false
}

42
dungeon/generator_test.go Normal file
View File

@@ -0,0 +1,42 @@
package dungeon
import "testing"
func TestGenerateFloor(t *testing.T) {
floor := GenerateFloor(1)
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
for i := 0; i < n; i++ {
counts[RandomRoomType()]++
}
combatPct := float64(counts[RoomCombat]) / float64(n) * 100
if combatPct < 40 || combatPct > 50 {
t.Errorf("Combat room probability: got %.1f%%, want ~45%%", combatPct)
}
}

43
dungeon/room.go Normal file
View File

@@ -0,0 +1,43 @@
package dungeon
import "math/rand"
type RoomType int
const (
RoomCombat RoomType = iota
RoomTreasure
RoomShop
RoomEvent
RoomEmpty
RoomBoss
)
func (r RoomType) String() string {
return [...]string{"Combat", "Treasure", "Shop", "Event", "Empty", "Boss"}[r]
}
type Room struct {
Type RoomType
X, Y int
Width, Height int
Visited bool
Cleared bool
Neighbors []int
}
func RandomRoomType() RoomType {
r := rand.Float64() * 100
switch {
case r < 45:
return RoomCombat
case r < 60:
return RoomTreasure
case r < 70:
return RoomShop
case r < 85:
return RoomEvent
default:
return RoomEmpty
}
}