|
1 | 1 | """ |
2 | 2 | This file contains the test cases for the generator class. |
3 | 3 | """ |
| 4 | +import json |
| 5 | +import os |
| 6 | +import tempfile |
4 | 7 | import unittest |
| 8 | +from collections import deque |
5 | 9 |
|
| 10 | +from floors.floor import Floor |
6 | 11 | from generators.generator import Generator |
| 12 | +from rooms.teleport_room import TeleportRoom |
| 13 | +from utils.globals import FLOOR_HEIGHT, FLOOR_WIDTH |
| 14 | +from utils.room_type import RoomType |
| 15 | + |
| 16 | +_SAMPLE_SEEDS = ("1", "42", "abc", "test-seed", "zzz", "0") |
| 17 | + |
| 18 | + |
| 19 | +def _reachable_coordinates(floor): |
| 20 | + """ |
| 21 | + Returns which room coordinates are reachable from the start room by |
| 22 | + walking grid-adjacent rooms and following teleport-room connections, |
| 23 | + together with the set of all room coordinates on the floor. |
| 24 | + """ |
| 25 | + rooms = floor.get_rooms() |
| 26 | + rooms_by_id = {room.get_id(): room for room in rooms} |
| 27 | + coordinates = {(room[0], room[1]) for room in rooms} |
| 28 | + start_room = next(room for room in rooms if room.get_type() == RoomType.START_ROOM) |
| 29 | + |
| 30 | + seen = {(start_room[0], start_room[1])} |
| 31 | + queue = deque([start_room]) |
| 32 | + while queue: |
| 33 | + current = queue.popleft() |
| 34 | + neighbour_coordinates = [ |
| 35 | + (current[0] + dx, current[1] + dy) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)) |
| 36 | + ] |
| 37 | + if isinstance(current, TeleportRoom): |
| 38 | + connected_room = rooms_by_id[current.get_connected_room_id()] |
| 39 | + neighbour_coordinates.append((connected_room[0], connected_room[1])) |
| 40 | + for room in rooms: |
| 41 | + room_coordinates = (room[0], room[1]) |
| 42 | + if room_coordinates in neighbour_coordinates and room_coordinates not in seen: |
| 43 | + seen.add(room_coordinates) |
| 44 | + queue.append(room) |
| 45 | + return seen, coordinates |
7 | 46 |
|
8 | 47 |
|
9 | 48 | class GeneratorTest(unittest.TestCase): |
10 | 49 | def setUp(self) -> None: |
11 | 50 | self._generator = Generator("1", "test", "", 1) |
12 | 51 |
|
| 52 | + def test_generate_creates_exactly_one_start_room(self): |
| 53 | + self._generator.generate() |
| 54 | + start_rooms = [ |
| 55 | + room |
| 56 | + for room in self._generator._floor.get_rooms() |
| 57 | + if room.get_type() == RoomType.START_ROOM |
| 58 | + ] |
| 59 | + self.assertEqual(1, len(start_rooms)) |
| 60 | + |
| 61 | + def test_generate_creates_a_boss_room(self): |
| 62 | + self._generator.generate() |
| 63 | + self.assertIsNotNone(self._generator._floor.get_boss_room()) |
| 64 | + |
| 65 | + def test_generate_rooms_are_within_floor_bounds(self): |
| 66 | + for seed in _SAMPLE_SEEDS: |
| 67 | + with self.subTest(seed=seed): |
| 68 | + generator = Generator(seed, "test", "", 1) |
| 69 | + generator.generate() |
| 70 | + for room in generator._floor.get_rooms(): |
| 71 | + self.assertTrue(0 <= room[0] < FLOOR_WIDTH) |
| 72 | + self.assertTrue(0 <= room[1] < FLOOR_HEIGHT) |
| 73 | + |
| 74 | + def test_generate_does_not_place_two_rooms_on_the_same_tile(self): |
| 75 | + for seed in _SAMPLE_SEEDS: |
| 76 | + with self.subTest(seed=seed): |
| 77 | + generator = Generator(seed, "test", "", 1) |
| 78 | + generator.generate() |
| 79 | + coordinates = [ |
| 80 | + (room[0], room[1]) for room in generator._floor.get_rooms() |
| 81 | + ] |
| 82 | + self.assertEqual(len(coordinates), len(set(coordinates))) |
| 83 | + |
| 84 | + def test_generate_produces_a_fully_reachable_dungeon(self): |
| 85 | + # Reachability also has to follow teleport rooms: the boss room can be |
| 86 | + # placed in a corner with no direct neighbours and only be reachable |
| 87 | + # through its connected teleport room. |
| 88 | + for seed in _SAMPLE_SEEDS: |
| 89 | + with self.subTest(seed=seed): |
| 90 | + generator = Generator(seed, "test", "", 1) |
| 91 | + generator.generate() |
| 92 | + seen, coordinates = _reachable_coordinates(generator._floor) |
| 93 | + self.assertEqual(coordinates, seen) |
| 94 | + |
| 95 | + def test_generate_is_deterministic_for_the_same_seed(self): |
| 96 | + first = Generator("det-seed", "test", "", 1) |
| 97 | + first.generate() |
| 98 | + second = Generator("det-seed", "test", "", 1) |
| 99 | + second.generate() |
| 100 | + self.assertEqual(first._floor.get_rooms(), second._floor.get_rooms()) |
| 101 | + |
| 102 | + def test_mark_dead_ends_finds_all_rooms_with_a_single_neighbour(self): |
| 103 | + floor = self._generator._floor |
| 104 | + floor.add_room(2, 2, RoomType.START_ROOM) |
| 105 | + floor.add_room(1, 2) # only neighbour is (2, 2) -> dead end |
| 106 | + floor.add_room(3, 2) # neighbours are (2, 2) and (4, 2) -> not a dead end |
| 107 | + floor.add_room(4, 2) # only neighbour is (3, 2) -> dead end |
| 108 | + |
| 109 | + dead_end_indices = self._generator.mark_dead_ends() |
| 110 | + |
| 111 | + self.assertEqual([1, 3], dead_end_indices) |
| 112 | + for index in dead_end_indices: |
| 113 | + self.assertEqual(RoomType.DEAD_END, floor.get_rooms()[index].get_type()) |
| 114 | + |
| 115 | + def test_add_special_rooms_assigns_item_and_shop_room(self): |
| 116 | + floor = self._generator._floor |
| 117 | + floor.add_room(2, 2, RoomType.START_ROOM) |
| 118 | + floor.add_room(1, 2, RoomType.DEAD_END) |
| 119 | + floor.add_room(3, 2, RoomType.DEAD_END) |
| 120 | + |
| 121 | + self._generator.add_special_rooms([1, 2]) |
| 122 | + |
| 123 | + self.assertEqual(RoomType.ITEM_ROOM, floor.get_rooms()[1].get_type()) |
| 124 | + self.assertEqual(RoomType.SHOP_ROOM, floor.get_rooms()[2].get_type()) |
| 125 | + |
| 126 | + def test_add_special_rooms_stops_when_there_are_fewer_dead_ends_than_special_rooms(self): |
| 127 | + floor = self._generator._floor |
| 128 | + floor.add_room(2, 2, RoomType.START_ROOM) |
| 129 | + floor.add_room(1, 2, RoomType.DEAD_END) |
| 130 | + |
| 131 | + self._generator.add_special_rooms([1]) |
| 132 | + |
| 133 | + self.assertEqual(RoomType.ITEM_ROOM, floor.get_rooms()[1].get_type()) |
| 134 | + |
| 135 | + def test_to_json_matches_the_generated_floor(self): |
| 136 | + self._generator.generate() |
| 137 | + |
| 138 | + result = json.loads(self._generator.to_json(1)) |
| 139 | + |
| 140 | + self.assertEqual(FLOOR_WIDTH, result["_width"]) |
| 141 | + self.assertEqual(FLOOR_HEIGHT, result["_height"]) |
| 142 | + self.assertEqual("python", result["_generated_by"]) |
| 143 | + self.assertEqual("1", result["_floor"]["_seed"]) |
| 144 | + self.assertEqual( |
| 145 | + len(self._generator._floor.get_rooms()), len(result["_floor"]["_rooms"]) |
| 146 | + ) |
| 147 | + |
| 148 | + def test_to_json_round_trips_through_floor_from_json(self): |
| 149 | + self._generator.generate() |
| 150 | + |
| 151 | + restored = Floor.from_json(self._generator.to_json(1)) |
| 152 | + |
| 153 | + self.assertEqual(self._generator._floor.get_rooms(), restored.get_rooms()) |
| 154 | + |
| 155 | + def test_save_writes_the_generated_floor_to_the_given_path(self): |
| 156 | + self._generator.generate() |
| 157 | + with tempfile.TemporaryDirectory() as tmp_dir: |
| 158 | + output_path = os.path.join(tmp_dir, "floor.json") |
| 159 | + |
| 160 | + result_path = self._generator.save(output_path) |
| 161 | + |
| 162 | + self.assertEqual(output_path, result_path) |
| 163 | + with open(output_path, encoding="utf-8") as f: |
| 164 | + content = f.read() |
| 165 | + self.assertEqual(self._generator.to_json(1), content) |
| 166 | + |
13 | 167 |
|
14 | 168 | if __name__ == "__main__": |
15 | 169 | unittest.main() |
0 commit comments