Repository: tmcclintock/donjuan

Issue:
Create a Room class
A `Room` class should function similarly to a `Grid`, except it shouldn't have to be square (it would be cool to make funky-shaped rooms). The `Dungeon` should also have a dictionary of rooms.

This issue should be expanded on once thought is put into what attributes it should have.

Comments:

Patch:
diff --git a/donjuan/__init__.py b/donjuan/__init__.py
index 12928b9..c3f509d 100644
--- a/donjuan/__init__.py
+++ b/donjuan/__init__.py
@@ -8,3 +8,4 @@ from .face import BareFace, DoorFace, Face, Faces, HexFaces, SquareFaces
 from .grid import Grid, HexGrid, SquareGrid
 from .randomizer import RandomFilled, Randomizer
 from .renderer import BaseRenderer, Renderer
+from .room import Room
diff --git a/donjuan/cell.py b/donjuan/cell.py
index f008631..2f0bcb3 100644
--- a/donjuan/cell.py
+++ b/donjuan/cell.py
@@ -1,5 +1,5 @@
 from abc import ABC
-from typing import Any, List, Optional
+from typing import Any, List, Optional, Tuple
 
 from donjuan.door_space import DoorSpace
 from donjuan.face import Faces, HexFaces, SquareFaces
@@ -18,11 +18,28 @@ class Cell(ABC):
         filled: bool = False,
         door_space: Optional[DoorSpace] = None,
         contents: Optional[List[Any]] = None,
+        coordinates: Optional[Tuple[int, int]] = None,
     ):
         self.faces = faces
         self.filled = filled
         self.door_space = door_space
         self.contents = contents or []
+        self._coordinates = coordinates
+
+    def set_coordinates(self, x: int, y: int) -> None:
+        self._coordinates = (int(x), int(y))
+
+    @property
+    def coordinates(self) -> Tuple[int, int]:
+        return self._coordinates
+
+    @property
+    def x(self) -> int:
+        return self._coordinates[0]
+
+    @property
+    def y(self) -> int:
+        return self._coordinates[1]
 
     @property
     def n_sides(self) -> int:
@@ -47,10 +64,15 @@ class SquareCell(Cell):
         filled: bool = False,
         door_space: Optional[DoorSpace] = None,
         contents: Optional[List[Any]] = None,
+        coordinates: Optional[Tuple[int, int]] = None,
     ):
         faces = faces or SquareFaces()
         super().__init__(
-            faces=faces, filled=filled, door_space=door_space, contents=contents
+            faces=faces,
+            filled=filled,
+            door_space=door_space,
+            contents=contents,
+            coordinates=coordinates,
         )
 
 
@@ -72,8 +94,13 @@ class HexCell(Cell):
         filled: bool = False,
         door_space: Optional[DoorSpace] = None,
         contents: Optional[List[Any]] = None,
+        coordinates: Optional[Tuple[int, int]] = None,
     ):
         faces = faces or HexFaces()
         super().__init__(
-            faces=faces, filled=filled, door_space=door_space, contents=contents
+            faces=faces,
+            filled=filled,
+            door_space=door_space,
+            contents=contents,
+            coordinates=coordinates,
         )
diff --git a/donjuan/dungeon.py b/donjuan/dungeon.py
index c2b9fce..2e3ffb7 100644
--- a/donjuan/dungeon.py
+++ b/donjuan/dungeon.py
@@ -1,6 +1,7 @@
-from typing import Optional
+from typing import Dict, Optional
 
 from donjuan.grid import Grid, SquareGrid
+from donjuan.room import Room
 
 
 class Dungeon:
@@ -9,5 +10,15 @@ class Dungeon:
         n_rows: Optional[int] = 5,
         n_cols: Optional[int] = 5,
         grid: Optional[Grid] = None,
+        rooms: Dict[str, Room] = dict(),
     ):
-        self.grid = grid if grid else SquareGrid(n_rows, n_cols)
+        self._grid = grid or SquareGrid(n_rows, n_cols)
+        self._rooms = rooms
+
+    @property
+    def grid(self) -> Grid:
+        return self._grid
+
+    @property
+    def rooms(self) -> Dict[str, Room]:
+        return self._rooms
diff --git a/donjuan/grid.py b/donjuan/grid.py
index 3f04e0b..5a139ac 100644
--- a/donjuan/grid.py
+++ b/donjuan/grid.py
@@ -16,7 +16,8 @@ class Grid(ABC):
         assert n_rows > 1
         assert n_cols > 1
         cells = cells or [
-            [self.cell_type() for i in range(n_cols)] for j in range(n_rows)
+            [self.cell_type(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}"
         assert len(cells[0]) == n_cols, f"{len(cells[0])} vs {n_cols}"
@@ -56,6 +57,17 @@ class Grid(ABC):
         assert isinstance(cells[0][0], cls.cell_type), msg
         return cls(len(cells), len(cells[0]), cells)
 
+    def reset_cell_coordinates(self) -> None:
+        """
+        Helper function that sets the coordinates of the cells in the grid
+        to their index values. Useful if a grid was created by
+        :meth:`from_cells`.
+        """
+        for i in range(self.n_rows):
+            for j in range(self.n_cols):
+                self.cells[i][j].set_coordinates(i, j)
+        return
+
 
 class SquareGrid(Grid):
     """
diff --git a/donjuan/room.py b/donjuan/room.py
new file mode 100644
index 0000000..8e36b52
--- /dev/null
+++ b/donjuan/room.py
@@ -0,0 +1,36 @@
+from itertools import chain
+from typing import List, Optional
+
+from donjuan import Cell
+
+
+class Room:
+    def __init__(self, cells: Optional[List[List[Cell]]] = None):
+        self._cells = cells or [[]]
+        for cell in chain.from_iterable(self._cells):
+            assert cell.coordinates is not None, "room cell must have coordinates"
+
+    @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


Test patch:
diff --git a/tests/cell_test.py b/tests/cell_test.py
index d5a1437..0dbee62 100644
--- a/tests/cell_test.py
+++ b/tests/cell_test.py
@@ -22,6 +22,14 @@ class SquareCellTest(TestCase):
         c = SquareCell()
         assert c.n_sides == 4
 
+    def test_coordinates(self):
+        c = SquareCell()
+        assert c.coordinates is None
+        c.set_coordinates(1, 2)
+        assert c.coordinates == (1, 2)
+        assert c.x == 1
+        assert c.y == 2
+
 
 class HexCellTest(TestCase):
     def test_smoke(self):
@@ -35,3 +43,11 @@ class HexCellTest(TestCase):
     def test_n_sides(self):
         c = HexCell()
         assert c.n_sides == 6
+
+    def test_coordinates(self):
+        c = HexCell()
+        assert c.coordinates is None
+        c.set_coordinates(1, 2)
+        assert c.coordinates == (1, 2)
+        assert c.x == 1
+        assert c.y == 2
diff --git a/tests/dungeon_test.py b/tests/dungeon_test.py
index f4649a9..733ba44 100644
--- a/tests/dungeon_test.py
+++ b/tests/dungeon_test.py
@@ -1,6 +1,6 @@
 from unittest import TestCase
 
-from donjuan import Dungeon, SquareGrid
+from donjuan import Dungeon, HexGrid, SquareGrid
 
 
 class DungeonTest(TestCase):
@@ -8,6 +8,17 @@ class DungeonTest(TestCase):
         d = Dungeon()
         assert d is not None
 
+    def test_initial_attributes(self):
+        d = Dungeon()
+        assert d.rooms == {}
+
+    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
+
     def test_pass_dimensions(self):
         d = Dungeon(n_rows=4, n_cols=5)
         assert d.grid.n_rows == 4
diff --git a/tests/grid_test.py b/tests/grid_test.py
index d1bd39e..b0f5d0f 100644
--- a/tests/grid_test.py
+++ b/tests/grid_test.py
@@ -20,24 +20,41 @@ class SquareGridTest(TestCase):
         assert sg.n_rows == 5
         assert isinstance(sg.cells[0][0], SquareCell)
 
+    def test_cell_coordinates(self):
+        sg = SquareGrid(5, 4)
+        for i in range(sg.n_rows):
+            for j in range(sg.n_cols):
+                assert sg.cells[i][j].coordinates == (i, j)
+
     def test_get_filled_grid(self):
-        sg = SquareGrid(5, 5)
+        sg = SquareGrid(5, 4)
         fg = sg.get_filled_grid()
         assert all(fg)
 
     def test_get_filled_grid_some_unfilled(self):
-        sg = SquareGrid(5, 5)
+        sg = SquareGrid(5, 4)
         for i in range(5):
             sg.cells[i][3].filled = True
         fg = sg.get_filled_grid()
         for i in range(5):
-            for j 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)
 
+    def test_reset_cell_coordinates(self):
+        cells = [[SquareCell() for i in range(4)] for j in range(5)]
+        sg = SquareGrid.from_cells(cells)
+        for i in range(sg.n_rows):
+            for j in range(sg.n_cols):
+                assert sg.cells[i][j].coordinates is None
+        sg.reset_cell_coordinates()
+        for i in range(sg.n_rows):
+            for j in range(sg.n_cols):
+                assert sg.cells[i][j].coordinates == (i, j)
+
 
 class HexGridTest(TestCase):
     def test_smoke(self):
diff --git a/tests/room_test.py b/tests/room_test.py
new file mode 100644
index 0000000..8d7e5fa
--- /dev/null
+++ b/tests/room_test.py
@@ -0,0 +1,34 @@
+from copy import deepcopy
+from unittest import TestCase
+
+import pytest
+
+from donjuan import Room, SquareCell
+
+
+class RoomTest(TestCase):
+    def test_smoke(self):
+        r = Room()
+        assert r is not None
+        assert r.cells == [[]]
+
+    def test_assert_cell_coords(self):
+        c = SquareCell()
+        with pytest.raises(AssertionError):
+            Room(cells=[[c]])
+
+    def test_overlaps(self):
+        cs = [[SquareCell(coordinates=(i, j)) for j in range(5)] for i in range(4)]
+        r1 = Room(cs)
+        r2 = Room(deepcopy(cs))
+        assert r1.overlaps(r2)
+
+    def test_no_overlap(self):
+        cs = [[SquareCell(coordinates=(i, j)) for j in range(5)] for i in range(4)]
+        r1 = Room(cs)
+        cs2 = deepcopy(cs)
+        for i in range(len(cs)):
+            for j in range(len(cs[0])):
+                cs2[i][j].set_coordinates(100 + i, j)
+        r2 = Room(cs2)
+        assert not r1.overlaps(r2)