Repository: tmcclintock/donjuan

Issue:
Room randomizer
Create a subclass of `Randomizer` that creates rooms using the same algorithm used in the OG donjon script. Roughly this is:

1. roll for the number of rooms (`N`)
2. until `N` rooms have been created, loop over these substeps:
  a. roll a room location and random rectangular size
  b. loop over all existing rooms and check for overlap
  c. if no overlap, append the room to the list of existing rooms
3. *donjuan specific* - "paste" the rooms into the `Dungeon.grid` attribute, since the rooms can exist independent of the grid when they are created

Discussion:

Patch:
diff --git a/.isort.cfg b/.isort.cfg
new file mode 100644
index 0000000..8584edd
--- /dev/null
+++ b/.isort.cfg
@@ -0,0 +1,7 @@
+[settings]
+multi_line_output = 3
+include_trailing_comma = True
+force_grid_wrap = 0
+use_parentheses = True
+ensure_newline_before_comments = True
+line_length = 88
\ No newline at end of file
diff --git a/donjuan/__init__.py b/donjuan/__init__.py
index c3f509d..abad7f1 100644
--- a/donjuan/__init__.py
+++ b/donjuan/__init__.py
@@ -1,11 +1,20 @@
-__version__ = "0.0.2"
+__version__ = "0.0.3"
 __docs__ = "Package for generating dungeons."
 
+
 from .cell import Cell, HexCell, SquareCell
 from .door_space import Archway, Door, DoorSpace, Portcullis
 from .dungeon import Dungeon
 from .face import BareFace, DoorFace, Face, Faces, HexFaces, SquareFaces
 from .grid import Grid, HexGrid, SquareGrid
+from .hallway import Hallway
 from .randomizer import RandomFilled, Randomizer
 from .renderer import BaseRenderer, Renderer
 from .room import Room
+from .room_randomizer import (
+    AlphaNumRoomName,
+    DungeonRandomizer,
+    RoomPositionRandomizer,
+    RoomSizeRandomizer,
+)
+from .space import Space
diff --git a/donjuan/dungeon.py b/donjuan/dungeon.py
index 2e3ffb7..2145bc1 100644
--- a/donjuan/dungeon.py
+++ b/donjuan/dungeon.py
@@ -1,4 +1,4 @@
-from typing import Dict, Optional
+from typing import Dict, List, Optional
 
 from donjuan.grid import Grid, SquareGrid
 from donjuan.room import Room
@@ -10,15 +10,64 @@ class Dungeon:
         n_rows: Optional[int] = 5,
         n_cols: Optional[int] = 5,
         grid: Optional[Grid] = None,
-        rooms: Dict[str, Room] = dict(),
+        rooms: Optional[Dict[str, Room]] = None,
+        randomizers: Optional[List["Randomizer"]] = None,
     ):
         self._grid = grid or SquareGrid(n_rows, n_cols)
-        self._rooms = rooms
+        self._rooms = rooms or {}
+        self._randomizers = randomizers or []
+
+    def add_room(self, room: Room) -> None:
+        self._rooms[room.name] = room
 
     @property
     def grid(self) -> Grid:
         return self._grid
 
+    @property
+    def n_cols(self) -> int:
+        return self.grid.n_cols
+
+    @property
+    def n_rows(self) -> int:
+        return self.grid.n_rows
+
+    @property
+    def randomizers(self) -> List["Randomizer"]:
+        return self._randomizers
+
     @property
     def rooms(self) -> Dict[str, Room]:
         return self._rooms
+
+    def randomize(self) -> None:
+        """
+        For each item in :attr:`randomizers`, run the
+        :meth:`Randomizer.randomize_dungeon` method on this dungeon.
+        """
+        for rng in self.randomizers:
+            rng.randomize_dungeon(self)
+        return
+
+    def emplace_rooms(self) -> None:
+        """
+        Replace the cells in the :attr:`grid` with the cells of the
+        :attr:`rooms`.
+        """
+
+        for room_name, room in self.rooms.items():
+            self.emplace_room(room)
+        return
+
+    def emplace_room(self, room: Room) -> None:
+        """
+        Replace the cells in the :attr:`grid` with the cells of the `Room`.
+
+        Args:
+            room (Room): room to emplace in the :attr:`grid`
+        """
+        for i in range(room.n_rows):
+            for j in range(room.n_cols):
+                cell = room.cells[i][j]
+                self.grid.cells[cell.y][cell.x] = cell
+        return
diff --git a/donjuan/grid.py b/donjuan/grid.py
index 5a139ac..7951dfd 100644
--- a/donjuan/grid.py
+++ b/donjuan/grid.py
@@ -16,7 +16,7 @@ class Grid(ABC):
         assert n_rows > 1
         assert n_cols > 1
         cells = cells or [
-            [self.cell_type(coordinates=(i, j)) for j in range(n_cols)]
+            [self.cell_type(filled=True, coordinates=(i, j)) for j in range(n_cols)]
             for i in range(n_rows)
         ]
         assert len(cells) == n_rows, f"{len(cells)} vs {n_rows}"
diff --git a/donjuan/hallway.py b/donjuan/hallway.py
new file mode 100644
index 0000000..5ef0325
--- /dev/null
+++ b/donjuan/hallway.py
@@ -0,0 +1,9 @@
+from donjuan.space import Space
+
+
+class Hallway(Space):
+    """
+    A hallway in a dungeon. It has a start and end cell.
+
+    .. todo:: Finish this class. It is incomplete.
+    """
diff --git a/donjuan/randomizer.py b/donjuan/randomizer.py
index 5b50d59..7023ea9 100644
--- a/donjuan/randomizer.py
+++ b/donjuan/randomizer.py
@@ -1,7 +1,10 @@
 import random
 from typing import Optional
 
-from donjuan import Cell, Grid
+from donjuan.cell import Cell
+from donjuan.dungeon import Dungeon
+from donjuan.grid import Grid
+from donjuan.room import Room
 
 
 class Randomizer:
@@ -13,10 +16,26 @@ class Randomizer:
         """Randomize properties of the `Cell`"""
         pass  # pragma: no cover
 
+    def randomize_dungeon(self, dungeon: Dungeon) -> None:
+        """Randomize properties of the `Dungeon`"""
+        pass  # pragma: no cover
+
     def randomize_grid(self, grid: Grid) -> None:
         """Randomize properties of the `Grid`"""
         pass  # pragma: no cover
 
+    def randomize_room_size(self, room: Room, *args) -> None:
+        """Randomize the size of the `Room`"""
+        pass  # pragma: no cover
+
+    def randomize_room_name(self, room: Room, *args) -> None:
+        """Randomize the name of a `Room`"""
+        pass  # pragma: no cover
+
+    def randomize_room_position(self, room: Room, *args) -> None:
+        """Randomize the position of a `Room`"""
+        pass  # pragma: no cover
+
     @classmethod
     def seed(cls, seed: Optional[int] = None) -> None:
         """
diff --git a/donjuan/room.py b/donjuan/room.py
index 7c2dfd6..175c5ab 100644
--- a/donjuan/room.py
+++ b/donjuan/room.py
@@ -1,56 +1,36 @@
-from itertools import chain
-from typing import List, Optional
+from typing import List, Optional, Union
 
-from donjuan import Cell
+from donjuan.cell import Cell
+from donjuan.space import Space
 
 
-class Room:
-    def __init__(self, cells: Optional[List[List[Cell]]] = None):
-        self._cells = cells or [[]]
+class Room(Space):
+    """
+    A room in a dungeon. Rooms can have names. In future versions, rooms
+    will have additional features.
+    """
+
+    def __init__(
+        self, cells: Optional[List[List[Cell]]] = None, name: Union[int, str] = ""
+    ):
+        super().__init__(cells=cells or [[]])
+        assert isinstance(name, (int, str))
+        self._name = name
 
     @property
-    def cells(self) -> List[List[Cell]]:
-        return self._cells
-
-    def overlaps(self, other: "Room") -> bool:
-        """
-        Compare the cells of this room to the other room to determine
-        whether they overlap or not. Note, this algorithm is ``O(N*M)``
-        where ``N`` is the number of cells in this room and ``M`` is
-        the number of cells in the other room.
-
-        Args:
-            other (Room): other room to check against
-
-        Returns:
-            ``True`` if they overlap, ``False`` if not
-        """
-        # Loop over all of this room's cells
-        for c1 in chain.from_iterable(self.cells):
-            for c2 in chain.from_iterable(other.cells):
-                if c1.coordinates == c2.coordinates:
-                    return True
-        # No overlap
-        return False
-
-    def shift_vertical(self, n: int) -> None:
-        """
-        Change the ``y`` coordinates of all :attr:`cells` by ``n``.
-
-        Args:
-            n (int): number to increment vertical position of cells
-        """
-        for c in chain.from_iterable(self.cells):
-            c.set_y(c.y + int(n))
+    def name(self) -> str:
+        return str(self._name)
+
+    def set_name(self, name: Union[int, str]) -> None:
+        self._name = name
         return
 
-    def shift_horizontal(self, n: int) -> None:
-        """
-        Change the ``x`` coordinates of all :attr:`cells` by ``n``.
+    @property
+    def n_rows(self) -> int:
+        if self.cells == [[]]:
+            return 0  # special case
+        return len(self.cells)
 
-        Args:
-            n (int): number to increment horizontal position of cells
-        """
-        for c in chain.from_iterable(self.cells):
-            c.set_x(c.x + int(n))
-        return
+    @property
+    def n_cols(self) -> int:
+        return len(self.cells[0])
diff --git a/donjuan/room_randomizer.py b/donjuan/room_randomizer.py
new file mode 100644
index 0000000..4e240dd
--- /dev/null
+++ b/donjuan/room_randomizer.py
@@ -0,0 +1,178 @@
+import random
+from string import ascii_uppercase
+from typing import Optional, Type
+
+from donjuan import Randomizer
+from donjuan.cell import Cell, SquareCell
+from donjuan.dungeon import Dungeon
+from donjuan.room import Room
+
+
+class RoomSizeRandomizer(Randomizer):
+    """
+    Randomize the size of a `Room`.
+    """
+
+    def __init__(
+        self, min_size: int = 2, max_size: int = 4, cell_type: Type[Cell] = SquareCell
+    ):
+        super().__init__()
+        assert issubclass(cell_type, Cell)
+        assert min_size >= 1, f"{min_size}"
+        assert max_size >= min_size, f"{max_size} < {min_size}"
+        assert max_size <= 100, f"{max_size}"  # arbitrary
+        self.min_size = min_size
+        self.max_size = max_size
+        self.cell_type = cell_type
+
+    def randomize_room_size(self, room: Room) -> None:
+        """
+        Randomly determine the size of the room, and set the cells of
+        the room to a 2D array of unfilled cells of that size.
+        """
+        # Draw the dimensions
+        height = random.randint(self.min_size, self.max_size)
+        width = random.randint(self.min_size, self.max_size)
+
+        # Create empty cells and set them in the room
+        cells = [
+            [self.cell_type(filled=False, coordinates=(i, j)) for j in range(height)]
+            for i in range(width)
+        ]
+        room.set_cells(cells)
+        return
+
+
+class AlphaNumRoomName(Randomizer):
+    """
+    Simple room name randomizer that names rooms as alphabetical letters.
+    followed by a number.
+    Rooms are sequentially named 'A0', 'B0', ... 'Z0', 'A1', 'B1', ...
+    """
+
+    def __init__(self):
+        super().__init__()
+        self.uppercase_letters_iterator = iter(ascii_uppercase)
+        self.n = 0
+
+    def next_name(self) -> str:
+        try:
+            letter = next(self.uppercase_letters_iterator)
+        except StopIteration:
+            # reset
+            self.uppercase_letters_iterator = iter(ascii_uppercase)
+            self.n += 1
+            letter = next(self.uppercase_letters_iterator)
+        return f"{letter}{self.n}"
+
+    def randomize_room_name(self, room: Room, *args) -> None:
+        room.set_name(self.next_name())
+        return
+
+
+class RoomPositionRandomizer(Randomizer):
+    """
+    Randomly shift a room, assuming it has :attr:`n_cols`
+    and :attr:`n_rows` attributes.
+    """
+
+    def randomize_room_position(self, room: Room, dungeon: Dungeon) -> None:
+        # Draw random positions and shift
+        room.shift_horizontal(random.randint(0, dungeon.n_cols - room.n_cols))
+        room.shift_vertical(random.randint(0, dungeon.n_rows - room.n_rows))
+        return
+
+
+class DungeonRandomizer(Randomizer):
+    """
+    Randomize a dungeon by first creating rooms and then applying
+    room size, name, and position randomizers to sequentially generated
+    rooms.
+
+    Args:
+        room_size_randomizer (Optional[RoomRandomizer]): randomizer for the
+            room size. It must have a 'max_size' attribute. If ``None`` then
+            default to a ``RoomSizeRandomizer``.
+        room_name_randomizer (RoomRandomizer): randomizer for the room name.
+            If ``None`` default to a ``AlphaNumRoomName``.
+        room_position_randomizer (RoomRandomizer): randomizer for the room
+            position. If ``None`` default to a ``RoomPositionRandomizer``.
+        max_num_rooms (Optional[int]): maximum number of rooms to draw,
+            if ``None` then default to the :attr:`max_room_attempts`. See
+            :meth:`DungeonRoomRandomizer.get_number_of_rooms` for details.
+        max_room_attempts (int, optional): default is 100. Maximum number of
+            attempts to generate rooms.
+    """
+
+    def __init__(
+        self,
+        room_size_randomizer: Optional[Randomizer] = None,
+        room_name_randomizer: Optional[Randomizer] = None,
+        room_position_randomizer: Optional[Randomizer] = None,
+        max_num_rooms: Optional[int] = None,
+        max_room_attempts: int = 100,
+    ):
+        super().__init__()
+        self.room_size_randomizer = room_size_randomizer or RoomSizeRandomizer()
+        assert hasattr(self.room_size_randomizer, "max_size")
+        self.room_name_randomizer = room_name_randomizer or AlphaNumRoomName()
+        self.room_position_randomizer = (
+            room_position_randomizer or RoomPositionRandomizer()
+        )
+        self.max_num_rooms = max_num_rooms or max_room_attempts
+        self.max_room_attempts = max_room_attempts
+
+    def get_number_of_rooms(self, dungeon_n_rows: int, dungeon_n_cols: int) -> int:
+        """
+        Randomly determine the number of rooms based on the size
+        of the incoming grid or the :attr:`max_num_rooms` attribute,
+        whichever is less.
+
+        Args:
+            dungeon_n_rows (int): number of rows
+            dungeon_n_cols (int): number of columns
+        """
+        dungeon_area = dungeon_n_rows * dungeon_n_cols
+        max_room_area = self.room_size_randomizer.max_size ** 2
+        return min(self.max_num_rooms, dungeon_area // max_room_area)
+
+    def randomize_dungeon(self, dungeon: Dungeon) -> None:
+        """
+        Randomly put rooms in the dungeon.
+
+        Args:
+            dungeon (Dungeon): dungeon to randomize the rooms of
+        """
+        # Compute the number
+        n_rooms = self.get_number_of_rooms(dungeon.n_rows, dungeon.n_cols)
+
+        # Create rooms, randomize, and check for overlap
+        i = 0
+        while len(dungeon.rooms) < n_rooms:
+            # Create the room
+            room = Room()
+
+            # Randomize the name
+            self.room_name_randomizer.randomize_room_name(room)
+
+            # Randomize the size
+            self.room_size_randomizer.randomize_room_size(room)
+
+            # Randomize positions
+            self.room_position_randomizer.randomize_room_position(room, dungeon)
+
+            # Check for overlap
+            overlaps = False
+            for existing_room_id, existing_room in dungeon.rooms.items():
+                if room.overlaps(existing_room):
+                    overlaps = True
+                    break
+
+            if not overlaps:
+                dungeon.add_room(room)
+
+            # Check for max attempts
+            i += 1
+            if i == self.max_room_attempts:
+                break
+        return
diff --git a/donjuan/space.py b/donjuan/space.py
new file mode 100644
index 0000000..b283d9d
--- /dev/null
+++ b/donjuan/space.py
@@ -0,0 +1,69 @@
+from abc import ABC
+from itertools import chain
+from typing import List
+
+from donjuan.cell import Cell
+
+
+class Space(ABC):
+    """
+    A space is a section of a dungeon composed of cells.
+    """
+
+    def __init__(self, cells: List[List[Cell]]):
+        self._cells = cells
+
+    @property
+    def cells(self) -> List[List[Cell]]:
+        return self._cells
+
+    def set_cells(self, cells: List[List[Cell]]) -> None:
+        assert isinstance(cells, list)
+        if len(cells) > 0:
+            assert isinstance(cells[0], list)
+            if len(cells[0]) > 0:
+                assert isinstance(cells[0][0], Cell)
+        self._cells = cells
+
+    def overlaps(self, other: "Space") -> bool:
+        """
+        Compare the cells of this room to the other room to determine
+        whether they overlap or not. Note, this algorithm is ``O(N*M)``
+        where ``N`` is the number of cells in this room and ``M`` is
+        the number of cells in the other room.
+
+        Args:
+            other (Room): other room to check against
+
+        Returns:
+            ``True`` if they overlap, ``False`` if not
+        """
+        # Loop over all of this space's cells
+        for c1 in chain.from_iterable(self._cells):
+            for c2 in chain.from_iterable(other._cells):
+                if c1.coordinates == c2.coordinates:
+                    return True
+        # No overlap
+        return False
+
+    def shift_vertical(self, n: int) -> None:
+        """
+        Change the ``y`` coordinates of all :attr:`cells` by ``n``.
+
+        Args:
+            n (int): number to increment vertical position of cells
+        """
+        for c in chain.from_iterable(self.cells):
+            c.set_y(c.y + int(n))
+        return
+
+    def shift_horizontal(self, n: int) -> None:
+        """
+        Change the ``x`` coordinates of all :attr:`cells` by ``n``.
+
+        Args:
+            n (int): number to increment horizontal position of cells
+        """
+        for c in chain.from_iterable(self.cells):
+            c.set_x(c.x + int(n))
+        return
diff --git a/examples/render_a_randomized_dungeon.py b/examples/render_a_randomized_dungeon.py
new file mode 100644
index 0000000..aa2c5f3
--- /dev/null
+++ b/examples/render_a_randomized_dungeon.py
@@ -0,0 +1,26 @@
+import os
+
+import matplotlib as mpl
+
+from donjuan import Dungeon, DungeonRoomRandomizer, Renderer, RoomRandomizer
+
+# Create and randomize the dungeon
+room_randomizer = RoomRandomizer(max_size=4)
+randomizer = DungeonRoomRandomizer(max_num_rooms=10, room_randomizers=[room_randomizer])
+randomizer.seed(54321)
+dungeon = Dungeon(30, 30, randomizers=[randomizer])
+
+dungeon.randomize()
+dungeon.emplace_rooms()
+
+# Render the dungeon
+renderer = Renderer()
+file_path = "test.png"
+renderer.render(dungeon, file_path=file_path)
+
+# Look at it
+mpl.image.imread(file_path)
+mpl.pyplot.show()
+
+# Delete the image from on disk
+os.remove(file_path)


Test patch:
diff --git a/tests/dungeon_test.py b/tests/dungeon_test.py
index 733ba44..ff67da8 100644
--- a/tests/dungeon_test.py
+++ b/tests/dungeon_test.py
@@ -1,31 +1,79 @@
 from unittest import TestCase
 
-from donjuan import Dungeon, HexGrid, SquareGrid
+from donjuan import Dungeon, HexGrid, Room, SquareCell, SquareGrid
 
 
 class DungeonTest(TestCase):
     def test_smoke(self):
         d = Dungeon()
         assert d is not None
+        assert d.grid is not None
+        assert d.randomizers == []
+
+    def test_randomize(self):
+        class FakeRandomizer:
+            def randomize_dungeon(self, dungeon: Dungeon):
+                # set a dummy attribute
+                setattr(dungeon, "dummy", True)
+                return
+
+        rng = FakeRandomizer()
+        d = Dungeon(randomizers=[rng])
+        assert not hasattr(d, "dummy")
+        d.randomize()
+        assert hasattr(d, "dummy")
+        assert d.dummy is True
 
     def test_initial_attributes(self):
         d = Dungeon()
         assert d.rooms == {}
 
+    def test_add_room(self):
+        r = Room(name="blah")
+        d = Dungeon()
+        d.add_room(r)
+        assert d.rooms["blah"] == r
+
     def test_hex_grid(self):
         hg = HexGrid(4, 5)
         d = Dungeon(grid=hg)
         assert isinstance(d.grid, HexGrid)
-        assert d.grid.n_rows == 4
-        assert d.grid.n_cols == 5
+        assert d.n_rows == 4
+        assert d.n_cols == 5
 
     def test_pass_dimensions(self):
         d = Dungeon(n_rows=4, n_cols=5)
-        assert d.grid.n_rows == 4
-        assert d.grid.n_cols == 5
+        assert d.n_rows == 4
+        assert d.n_cols == 5
 
     def test_pass_grid(self):
         grid = SquareGrid(3, 4)
         d = Dungeon(grid=grid)
-        assert d.grid.n_rows == 3
-        assert d.grid.n_cols == 4
+        assert d.n_rows == 3
+        assert d.n_cols == 4
+
+    def test_emplace_room(self):
+        grid = SquareGrid(3, 4)
+        d = Dungeon(grid=grid)
+        cs = [[SquareCell(filled=False, coordinates=(1, 2))]]
+        room = Room(cells=cs)
+        d.emplace_room(room)
+        for i in range(d.n_rows):
+            for j in range(d.n_cols):
+                if i == 1 and j == 2:
+                    assert not d.grid.cells[i][j].filled
+                else:
+                    assert d.grid.cells[i][j].filled, f"({i}, {j})"
+
+    def test_emplace_rooms(self):
+        grid = SquareGrid(3, 4)
+        cs = [[SquareCell(filled=False, coordinates=(1, 2))]]
+        room = Room(cells=cs)
+        d = Dungeon(grid=grid, rooms={"0": room})
+        d.emplace_rooms()
+        for i in range(d.n_rows):
+            for j in range(d.n_cols):
+                if i == 1 and j == 2:
+                    assert not d.grid.cells[i][j].filled
+                else:
+                    assert d.grid.cells[i][j].filled, f"({i}, {j})"
diff --git a/tests/grid_test.py b/tests/grid_test.py
index 3b35fd9..e2b9745 100644
--- a/tests/grid_test.py
+++ b/tests/grid_test.py
@@ -34,15 +34,15 @@ class SquareGridTest(TestCase):
     def test_get_filled_grid_some_unfilled(self):
         sg = SquareGrid(5, 4)
         for i in range(5):
-            sg.cells[i][3].filled = True
+            sg.cells[i][3].filled = False
         fg = sg.get_filled_grid()
         for i in range(5):
             for j in range(4):
                 assert fg[i][j] == sg.cells[i][j].filled, (i, j)
                 if j != 3:
-                    assert not fg[i][j], (i, j)
-                else:
                     assert fg[i][j], (i, j)
+                else:
+                    assert not fg[i][j], (i, j)
 
     def test_reset_cell_coordinates(self):
         cells = [[SquareCell() for i in range(4)] for j in range(5)]
diff --git a/tests/hallway_test.py b/tests/hallway_test.py
new file mode 100644
index 0000000..65c27bd
--- /dev/null
+++ b/tests/hallway_test.py
@@ -0,0 +1,9 @@
+from unittest import TestCase
+
+
+class HallwayTest(TestCase):
+    """
+    .. todo:: Make tests once the `Hallway` class has content.
+    """
+
+    pass
diff --git a/tests/randomizer_test.py b/tests/randomizer_test.py
index 6751161..6dda4bc 100644
--- a/tests/randomizer_test.py
+++ b/tests/randomizer_test.py
@@ -1,12 +1,13 @@
 from unittest import TestCase
 
-from donjuan import HexGrid, RandomFilled, Randomizer, SquareGrid
+from donjuan import Dungeon, HexGrid, RandomFilled, Randomizer, SquareGrid
 
 
 class RandomizerTestCase(TestCase):
     def setUp(self):
         self.grid = SquareGrid(n_rows=4, n_cols=5)
         self.hexgrid = HexGrid(n_rows=4, n_cols=5)
+        self.dungeon = Dungeon(grid=self.grid)
 
 
 class RandomizerTest(RandomizerTestCase):
@@ -29,7 +30,7 @@ class RandomFilledTest(RandomizerTestCase):
         grid = self.grid
         for i in range(grid.n_rows):
             for j in range(grid.n_cols):
-                assert not grid.cells[i][j].filled
+                assert grid.cells[i][j].filled
         rng.randomize_grid(grid)
         # Test that at least one cell became filled
         for i in range(grid.n_rows):
@@ -44,7 +45,7 @@ class RandomFilledTest(RandomizerTestCase):
         grid = self.hexgrid
         for i in range(grid.n_rows):
             for j in range(grid.n_cols):
-                assert not grid.cells[i][j].filled
+                assert grid.cells[i][j].filled
         rng.randomize_grid(grid)
         # Test that at least one cell became filled
         for i in range(grid.n_rows):
diff --git a/tests/renderer_test.py b/tests/renderer_test.py
index dfb1b0e..bc583d8 100644
--- a/tests/renderer_test.py
+++ b/tests/renderer_test.py
@@ -5,7 +5,7 @@ from unittest import TestCase
 
 import pytest
 
-from donjuan import Dungeon, Renderer
+from donjuan import Dungeon, DungeonRandomizer, Renderer
 
 
 class RendererTest(TestCase):
@@ -22,7 +22,7 @@ class RendererTest(TestCase):
         assert r.scale == 3
 
     @pytest.mark.slow
-    def test_render(self):
+    def test_render_dummy_dungeon(self):
         inpath = os.path.abspath(os.path.dirname(__file__))
         inpath = os.path.join(inpath, "fixtures/dummy_dungeon.json")
         with open(inpath, "r") as f:
@@ -39,3 +39,16 @@ class RendererTest(TestCase):
         r = Renderer()
         r.render(dungeon, file_path=fp)
         assert os.path.exists(fp)
+
+    @pytest.mark.slow
+    def test_render_dungeon_with_rooms(self):
+        randomizer = DungeonRandomizer()
+        dungeon = Dungeon(10, 10, randomizers=[randomizer])
+        dungeon.randomize()
+        dungeon.emplace_rooms()
+        renderer = Renderer()
+
+        # Render and check for the file
+        fp = os.path.join(self.TEMP_DIR, "rendered_dungeon.png")
+        renderer.render(dungeon, file_path=fp)
+        assert os.path.exists(fp)
diff --git a/tests/room_randomizer_test.py b/tests/room_randomizer_test.py
new file mode 100644
index 0000000..58aa1ee
--- /dev/null
+++ b/tests/room_randomizer_test.py
@@ -0,0 +1,110 @@
+from unittest import TestCase
+
+from donjuan import (
+    AlphaNumRoomName,
+    Cell,
+    Dungeon,
+    DungeonRandomizer,
+    HexGrid,
+    Room,
+    RoomPositionRandomizer,
+    RoomSizeRandomizer,
+    SquareCell,
+    SquareGrid,
+)
+
+
+class RandomizerTestCase(TestCase):
+    def setUp(self):
+        self.grid = SquareGrid(n_rows=4, n_cols=5)
+        self.hexgrid = HexGrid(n_rows=4, n_cols=5)
+        self.dungeon = Dungeon(grid=self.grid)
+
+
+class AlphaNumRoomNameTest(RandomizerTestCase):
+    def test_names(self):
+        rr = AlphaNumRoomName()
+        room = Room()
+        rr.randomize_room_name(room)
+        assert room.name == "A0"
+        rr.randomize_room_name(room)
+        assert room.name == "B0"
+        rr.randomize_room_name(room)
+        assert room.name == "C0"
+        for i in range(23):
+            rr.randomize_room_name(room)
+        assert room.name == "Z0"
+        rr.randomize_room_name(room)
+        assert room.name == "A1"
+        rr.randomize_room_name(room)
+        assert room.name == "B1"
+        rr.randomize_room_name(room)
+        assert room.name == "C1"
+
+
+class RoomPositionRandomizerTest(RandomizerTestCase):
+    def test_randomize_room_position(self):
+        # Make a 1x1 room. try moving it around.
+        rr = RoomPositionRandomizer()
+        rr.seed(123)
+        cells = [[SquareCell()]]
+        room = Room(cells=cells)
+        assert room.cells[0][0].coordinates == (0, 0)
+        # Try moving it. 5% chance this fails if the seed is not set
+        # Works with seed 123
+        rr.randomize_room_position(room, self.dungeon)
+        assert room.cells[0][0].coordinates != (0, 0)
+
+
+class RoomSizeRandomizerTest(RandomizerTestCase):
+    def test_smoke(self):
+        rng = RoomSizeRandomizer()
+        assert rng is not None
+        assert rng.min_size == 2
+        assert rng.max_size == 4
+        assert issubclass(rng.cell_type, Cell)
+
+    def test_randomize_room(self):
+        room = Room()
+        assert room.cells == [[]]
+        rng = RoomSizeRandomizer()
+        rng.randomize_room_size(room)
+        assert len(room.cells) >= rng.min_size
+        assert len(room.cells) <= rng.max_size
+        assert len(room.cells[0]) >= rng.min_size
+        assert len(room.cells[0]) <= rng.max_size
+        assert isinstance(room.cells[0][0], Cell)
+        assert not room.cells[0][0].filled
+
+
+class DungeonRandomizerTest(RandomizerTestCase):
+    def test_smoke(self):
+        rng = DungeonRandomizer()
+        assert rng.max_room_attempts == 100
+
+    def test_get_number_of_rooms(self):
+        rr = RoomSizeRandomizer(max_size=2)
+        rng = DungeonRandomizer(room_size_randomizer=rr)
+        assert rng.get_number_of_rooms(4, 4) == 4
+        assert rng.get_number_of_rooms(5, 4) == 5
+        assert rng.get_number_of_rooms(5, 5) == 6
+
+    def test_get_number_of_rooms_preset(self):
+        rr = RoomSizeRandomizer(max_size=2)
+        rng = DungeonRandomizer(max_num_rooms=3, room_size_randomizer=rr)
+        assert rng.get_number_of_rooms(4, 4) == 3
+        assert rng.get_number_of_rooms(5, 4) == 3
+        assert rng.get_number_of_rooms(5, 5) == 3
+
+    def test_randomize_dungeon_one_room_max(self):
+        rng = DungeonRandomizer()
+        rng.randomize_dungeon(self.dungeon)
+        assert len(self.dungeon.rooms) == 1
+        assert "A0" in self.dungeon.rooms
+
+    def test_randomize_dungeon_up_to_five_rooms(self):
+        rr = RoomSizeRandomizer(max_size=2)
+        rng = DungeonRandomizer(room_size_randomizer=rr)
+        rng.randomize_dungeon(self.dungeon)
+        assert len(self.dungeon.rooms) <= 5
+        assert len(self.dungeon.rooms) > 0
diff --git a/tests/room_test.py b/tests/room_test.py
index 9a240f6..2d3ef2e 100644
--- a/tests/room_test.py
+++ b/tests/room_test.py
@@ -9,6 +9,30 @@ class RoomTest(TestCase):
         r = Room()
         assert r is not None
         assert r.cells == [[]]
+        assert r.name == ""
+        assert r.n_rows == 0
+        assert r.n_cols == 0
+
+    def test_name(self):
+        r = Room(name="testroom")
+        assert r.name == "testroom"
+        r = Room(name=12)
+        assert r.name == "12"
+
+    def test_set_name(self):
+        r = Room()
+        r.set_name("catdog")
+        assert r.name == "catdog"
+
+    def test_set_cells(self):
+        cs = [[SquareCell()]]
+        r = Room(cells=cs)
+        assert r.n_rows == 1
+        assert r.n_cols == 1
+        cs = [[SquareCell(coordinates=(i, j)) for j in range(3)] for i in range(2)]
+        r.set_cells(cs)
+        assert r.n_rows == 2
+        assert r.n_cols == 3
 
     def test_shift_vertical(self):
         cs = [[SquareCell(coordinates=(i, j)) for j in range(5)] for i in range(4)]