Skip to content

Commit

Permalink
Merge pull request #47 from tmcclintock/entrance_randomizer
Browse files Browse the repository at this point in the history
Entrance randomizer
  • Loading branch information
tmcclintock committed Jan 12, 2021
2 parents 791c6c3 + 065cfdb commit ab6d567
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 86 deletions.
3 changes: 2 additions & 1 deletion donjuan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .cell import Cell, HexCell, SquareCell
from .door_space import Archway, Door, DoorSpace, Portcullis
from .dungeon import Dungeon
from .dungeon_randomizer import DungeonRandomizer
from .edge import Edge
from .grid import Grid, HexGrid, SquareGrid
from .hallway import Hallway
Expand All @@ -13,7 +14,7 @@
from .room import Room
from .room_randomizer import (
AlphaNumRoomName,
DungeonRandomizer,
RoomEntrancesRandomizer,
RoomPositionRandomizer,
RoomSizeRandomizer,
)
Expand Down
5 changes: 4 additions & 1 deletion donjuan/dungeon.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Union

from donjuan.edge import Edge
from donjuan.grid import Grid, SquareGrid
from donjuan.hallway import Hallway
from donjuan.room import Room
Expand All @@ -20,6 +21,7 @@ def __init__(
self._rooms = rooms or {}
self._hallways = hallways or {}
self._randomizers = randomizers or []
self.room_entrances: Dict[Union[int, str], List[Edge]] = {}

def add_room(self, room: Room) -> None:
self._rooms[room.name] = room
Expand Down Expand Up @@ -81,4 +83,5 @@ def emplace_space(self, space: Space) -> None:
for cell in space.cells:
self.grid.cells[cell.y][cell.x] = cell
self.grid.link_edges_to_cells()
assert cell.edges is not None, "problem linking edges to cell"
return
124 changes: 124 additions & 0 deletions donjuan/dungeon_randomizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from typing import Optional

from donjuan.dungeon import Dungeon
from donjuan.randomizer import Randomizer
from donjuan.room import Room
from donjuan.room_randomizer import (
AlphaNumRoomName,
RoomEntrancesRandomizer,
RoomPositionRandomizer,
RoomSizeRandomizer,
)


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_entrance_randomizer (Optional[Randomizer]): randomizer for the
entrances of a room. If ``None`` then default to a
``RoomEntrancesRandomizer``.
room_size_randomizer (Optional[Randomizer]): randomizer for the
room size. It must have a 'max_size' attribute. If ``None`` then
default to a ``RoomSizeRandomizer``.
room_name_randomizer (Optional[Randomizer]): randomizer for the room
name. If ``None`` default to a ``AlphaNumRoomName``.
room_position_randomizer (Optional[Randomizer]): 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_entrance_randomizer: Optional[Randomizer] = None,
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_entrance_randomizer = (
room_entrance_randomizer or RoomEntrancesRandomizer()
)
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

# Emplace the rooms
dungeon.emplace_rooms()

# Open entrances the rooms
for room_name, room in dungeon.rooms.items():
self.room_entrance_randomizer.randomize_room_entrances(room, dungeon)
dungeon.room_entrances[room.name] = []
for entrance in room.entrances:
dungeon.room_entrances[room.name].append(entrance)

return
11 changes: 9 additions & 2 deletions donjuan/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ class Edge:
Args:
cell1 (Optional[Cell]): cell on one side of the edge
cell2 (Optional[Cell]): cell on the other side of the edge
has_door (bool, optional): default ``False``, indicates whether this
object has a door
"""

def __init__(
self, cell1: Optional[Cell] = None, cell2: Optional[Cell] = None,
self,
cell1: Optional[Cell] = None,
cell2: Optional[Cell] = None,
has_door: bool = False,
):
self._cell1 = cell1
self._cell2 = cell2
self.has_door = has_door

@property
def cell1(self) -> Optional[Cell]:
Expand All @@ -35,7 +41,8 @@ def set_cell2(self, cell: Cell) -> None:
@property
def is_wall(self) -> bool:
return (
(self.cell1 is None)
(not self.has_door)
or (self.cell1 is None)
or (self.cell2 is None)
or (self.cell1.filled != self.cell2.filled)
or (self.cell1.space != self.cell2.space)
Expand Down
4 changes: 4 additions & 0 deletions donjuan/randomizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def randomize_hallway(self, hallway: Hallway) -> None:
"""Randomize properties of the `Hallway`"""
pass # pragma: no cover

def randomize_room_entrances(self, room: Room, *args) -> None:
"""Randomize the entrances of the `Room`"""
pass # pragma: no cover

def randomize_room_size(self, room: Room, *args) -> None:
"""Randomize the size of the `Room`"""
pass # pragma: no cover
Expand Down
118 changes: 38 additions & 80 deletions donjuan/room_randomizer.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import random
from math import sqrt
from string import ascii_uppercase
from typing import Optional, Type
from typing import Set, Type

from donjuan import Randomizer
from donjuan.cell import Cell, SquareCell
from donjuan.dungeon import Dungeon
from donjuan.randomizer import Randomizer
from donjuan.room import Room


Expand Down Expand Up @@ -92,96 +93,53 @@ def randomize_room_position(self, room: Room, dungeon: Dungeon) -> None:
return


class DungeonRandomizer(Randomizer):
class RoomEntrancesRandomizer(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.
Randomizes the number of entrances on a room. The number is picked to be
the square root of the number of cells in the room divided by 2 plus 1
(``N``) plus a uniform random integer from 0 to ``N``.
"""

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.
def __init__(self, max_attempts: int = 100):
self.max_attempts = max_attempts

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 gen_num_entrances(self, cells: Set[Cell]) -> int:
N = int(sqrt(len(cells))) // 2 + 1
return N + random.randint(0, N)

def randomize_dungeon(self, dungeon: Dungeon) -> None:
def randomize_room_entrances(self, room: Room, *args) -> None:
"""
Randomly put rooms in the dungeon.
Randomly open edges of cells in a `Room`. The cells in the room must
already be linked to edges in a `Grid`. See
:meth:`~donjuan.dungeon.emplace_rooms`.
.. note::
This algorithm does not allow for a cell in a room to have two
entrances.
Args:
dungeon (Dungeon): dungeon to randomize the rooms of
room (Room): room to try to create entrances for
"""
# Compute the number
n_rooms = self.get_number_of_rooms(dungeon.n_rows, dungeon.n_cols)

# Create rooms, randomize, and check for overlap
n_entrances = self.gen_num_entrances(room.cells)
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
# Shuffle cells
cell_list = random.sample(room.cells, k=len(room.cells))
for cell in cell_list:
assert cell.edges is not None, "cell edges not linked"

# If an edge has a wall, set it to having a door
# and record it
# TODO: objectify this method so that cells can have many entrances
edges = random.sample(cell.edges, k=len(cell.edges))
for edge in edges:
if edge.is_wall:
edge.has_door = True
room.entrances.append(edge)
break

if not overlaps:
dungeon.add_room(room)

# Check for max attempts
i += 1
if i == self.max_room_attempts:
i += 1 # increment attempts
if i >= self.max_attempts or len(room.entrances) >= n_entrances:
break
return
7 changes: 6 additions & 1 deletion donjuan/space.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
from abc import ABC
from typing import Optional, Sequence, Set, Tuple, Union
from typing import List, Optional, Sequence, Set, Tuple, Union

from donjuan.cell import Cell
from donjuan.edge import Edge


class Space(ABC):
"""
A space is a section of a dungeon composed of `Cell`s. This object
contains these cells in a ``set`` under the property :attr:`cells`.
It also has a :attr:`name` and knows about any entrances to the room
(a list of `Edge` objects) via the :attr:`entrances` property.
Args:
cells (Optional[Set[Cell]]): cells that make up this space
name (Union[int, str], optional): defaults to '', the room name
"""

def __init__(self, cells: Optional[Set[Cell]] = None, name: Union[int, str] = ""):
assert isinstance(name, (int, str))
self._name = name
self._cells = cells or set()
self.entrances: List[Edge] = []
self.assign_space_to_cells()
self.reset_cell_coordinates()

Expand Down

0 comments on commit ab6d567

Please sign in to comment.