From 8849bf52200c8003782ee70f40b5cd684af4cfbf Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Mon, 23 Mar 2026 23:50:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20dungeon=20generation=20=E2=80=94=20BSP?= =?UTF-8?q?=20rooms,=20room=20types,=20fog=20of=20war?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- dungeon/fov.go | 27 +++++++++++++++++++++ dungeon/generator.go | 49 +++++++++++++++++++++++++++++++++++++++ dungeon/generator_test.go | 42 +++++++++++++++++++++++++++++++++ dungeon/room.go | 43 ++++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 dungeon/fov.go create mode 100644 dungeon/generator.go create mode 100644 dungeon/generator_test.go create mode 100644 dungeon/room.go diff --git a/dungeon/fov.go b/dungeon/fov.go new file mode 100644 index 0000000..dad6080 --- /dev/null +++ b/dungeon/fov.go @@ -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 +} diff --git a/dungeon/generator.go b/dungeon/generator.go new file mode 100644 index 0000000..591a6fb --- /dev/null +++ b/dungeon/generator.go @@ -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 +} diff --git a/dungeon/generator_test.go b/dungeon/generator_test.go new file mode 100644 index 0000000..f1ba9e6 --- /dev/null +++ b/dungeon/generator_test.go @@ -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) + } +} diff --git a/dungeon/room.go b/dungeon/room.go new file mode 100644 index 0000000..aea616b --- /dev/null +++ b/dungeon/room.go @@ -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 + } +}