diff --git a/ca.py b/ca.py index 3d2ff6e..8035e53 100644 --- a/ca.py +++ b/ca.py @@ -26,7 +26,7 @@ def main(argv): log.init() - bounds = Rect(Point(), Size(20, 20)) + bounds = Rect(Point(), Size(60, 20)) config = CellularAtomataMapGenerator.Configuration() config.number_of_rounds = args.rounds diff --git a/erynrl/map/generator/cellular_atomata.py b/erynrl/map/generator/cellular_atomata.py index c1e2f0b..1fad789 100644 --- a/erynrl/map/generator/cellular_atomata.py +++ b/erynrl/map/generator/cellular_atomata.py @@ -2,14 +2,16 @@ import random from dataclasses import dataclass -from typing import Optional +from typing import Optional, TYPE_CHECKING import numpy as np from ... import log -from ...geometry import Point, Rect -from ..grid import make_grid -from ..tile import Floor, Wall +from ...geometry import Point, Rect, Vector +from ..tile import Empty, Floor, Wall, tile_datatype + +if TYPE_CHECKING: + from .. import Map class CellularAtomataMapGenerator: @@ -38,6 +40,7 @@ class CellularAtomataMapGenerator: Initializer ### Parameters + `bounds` : `Rect` A rectangle representing the bounds of the cellular atomaton `config` : `Optional[Configuration]` @@ -46,7 +49,7 @@ class CellularAtomataMapGenerator: ''' self.bounds = bounds self.configuration = config if config else CellularAtomataMapGenerator.Configuration() - self.tiles = make_grid(bounds.size) + self.tiles = np.full((bounds.size.height, bounds.size.width), fill_value=Empty, dtype=tile_datatype, order='C') def generate(self): ''' @@ -59,14 +62,23 @@ class CellularAtomataMapGenerator: self._fill() self._run_atomaton() + def apply(self, map: 'Map'): + origin = self.bounds.origin + for y, x in np.ndindex(self.tiles.shape): + map_pt = origin + Vector(x, y) + tile = self.tiles[y, x] + if tile == Floor: + map.tiles[map_pt.numpy_index] = tile + def _fill(self): fill_percentage = self.configuration.fill_percentage for y, x in np.ndindex(self.tiles.shape): - self.tiles[x, y] = Floor if random.random() < fill_percentage else Wall + self.tiles[y, x] = Floor if random.random() < fill_percentage else Empty def _run_atomaton(self): - alternate_tiles = make_grid(self.bounds.size) + alternate_tiles = np.full((self.bounds.size.height, self.bounds.size.width), + fill_value=Empty, dtype=tile_datatype, order='C') number_of_rounds = self.configuration.number_of_rounds if number_of_rounds < 1: @@ -100,21 +112,22 @@ class CellularAtomataMapGenerator: # Start with 1 because the point is its own neighbor number_of_neighbors = 1 for neighbor in pt.neighbors: - if neighbor not in self.bounds: - continue + try: + if from_tiles[neighbor.y, neighbor.x] == Floor: + number_of_neighbors += 1 + except IndexError: + pass - if from_tiles[neighbor.x, neighbor.y] == Floor: - number_of_neighbors += 1 - - tile_is_alive = from_tiles[pt.x, pt.y] == Floor + idx = (pt.y, pt.x) + tile_is_alive = from_tiles[idx] == Floor if tile_is_alive and number_of_neighbors >= 5: # Survival - to_tiles[pt.x, pt.y] = Floor + to_tiles[idx] = Floor elif not tile_is_alive and number_of_neighbors >= 5: # Birth - to_tiles[pt.x, pt.y] = Floor + to_tiles[idx] = Floor else: - to_tiles[pt.x, pt.y] = Wall + to_tiles[idx] = Empty def __str__(self): return '\n'.join(''.join(chr(i['light']['ch']) for i in row) for row in self.tiles) diff --git a/erynrl/map/generator/room.py b/erynrl/map/generator/room.py index 3cc534b..d0ee4e9 100644 --- a/erynrl/map/generator/room.py +++ b/erynrl/map/generator/room.py @@ -5,12 +5,14 @@ import random from dataclasses import dataclass from typing import Iterable, Iterator, List, Optional, Tuple, TYPE_CHECKING +import numpy as np import tcod from ... import log from ...geometry import Point, Rect, Size -from ..room import RectangularRoom, Room -from ..tile import Empty, Floor, StairsDown, StairsUp, Wall +from ..room import FreeformRoom, RectangularRoom, Room +from ..tile import Empty, Floor, StairsDown, StairsUp, Wall, tile_datatype +from .cellular_atomata import CellularAtomataMapGenerator if TYPE_CHECKING: from .. import Map @@ -196,6 +198,45 @@ class RectangularRoomMethod(RoomMethod): return RectangularRoom(rect) +class CellularAtomatonRoomMethod(RoomMethod): + + def __init__(self, cellular_atomaton_config: CellularAtomataMapGenerator.Configuration): + self.cellular_atomaton_configuration = cellular_atomaton_config + + def room_in_rect(self, rect: Rect) -> Optional[Room]: + # The cellular atomaton doesn't generate any walls, just floors and + # emptiness. Inset it by 1 all the way around so that we can draw walls + # around it. + + atomaton_rect = rect.inset_rect(1, 1, 1, 1) + room_generator = CellularAtomataMapGenerator(atomaton_rect, self.cellular_atomaton_configuration) + room_generator.generate() + + # Create a new tile array and copy the result of the atomaton into it, + # then draw walls everywhere that neighbors a floor tile. + + width = rect.width + height = rect.height + + room_tiles = np.full((height, width), fill_value=Empty, dtype=tile_datatype, order='C') + room_tiles[1:height - 1, 1:width - 1] = room_generator.tiles + + for y, x in np.ndindex(room_tiles.shape): + if room_tiles[y, x] == Floor: + continue + + for neighbor in Point(x, y).neighbors: + try: + if room_tiles[neighbor.y, neighbor.x] != Floor: + continue + room_tiles[y, x] = Wall + break + except IndexError: + pass + + return FreeformRoom(rect, room_tiles) + + class OrRoomMethod(RoomMethod): ''' A room generator method that picks between several RoomMethods at random diff --git a/erynrl/map/room.py b/erynrl/map/room.py index 0e8ddce..5228620 100644 --- a/erynrl/map/room.py +++ b/erynrl/map/room.py @@ -4,9 +4,12 @@ Implements an abstract Room class, and subclasses that implement it. Rooms are basic components of maps. ''' -from typing import Iterator +from typing import Iterable -from ..geometry import Point, Rect +import numpy as np + +from ..geometry import Point, Rect, Vector +from .tile import Floor, Wall class Room: @@ -21,17 +24,17 @@ class Room: return self.bounds.midpoint @property - def wall_points(self) -> Iterator[Point]: + def wall_points(self) -> Iterable[Point]: '''An iterator over all the points that make up the walls of this room.''' raise NotImplementedError() @property - def floor_points(self) -> Iterator[Point]: + def floor_points(self) -> Iterable[Point]: '''An iterator over all the points that make of the floor of this room''' raise NotImplementedError() @property - def walkable_tiles(self) -> Iterator[Point]: + def walkable_tiles(self) -> Iterable[Point]: '''An iterator over all the points that are walkable in this room.''' raise NotImplementedError() @@ -47,14 +50,14 @@ class RectangularRoom(Room): ''' @property - def walkable_tiles(self) -> Iterator[Point]: + def walkable_tiles(self) -> Iterable[Point]: floor_rect = self.bounds.inset_rect(top=1, right=1, bottom=1, left=1) for y in range(floor_rect.min_y, floor_rect.max_y + 1): for x in range(floor_rect.min_x, floor_rect.max_x + 1): yield Point(x, y) @property - def wall_points(self) -> Iterator[Point]: + def wall_points(self) -> Iterable[Point]: bounds = self.bounds min_y = bounds.min_y @@ -71,7 +74,7 @@ class RectangularRoom(Room): yield Point(max_x, y) @property - def floor_points(self) -> Iterator[Point]: + def floor_points(self) -> Iterable[Point]: inset_bounds = self.bounds.inset_rect(1, 1, 1, 1) min_y = inset_bounds.min_y @@ -85,3 +88,30 @@ class RectangularRoom(Room): def __repr__(self) -> str: return f'{self.__class__.__name__}({self.bounds})' + + +class FreeformRoom(Room): + def __init__(self, bounds: Rect, tiles: np.ndarray): + super().__init__(bounds) + self.tiles = tiles + + @property + def floor_points(self) -> Iterable[Point]: + room_origin_vector = Vector.from_point(self.bounds.origin) + for y, x in np.ndindex(self.tiles.shape): + if self.tiles[y, x] == Floor: + yield Point(x, y) + room_origin_vector + + @property + def wall_points(self) -> Iterable[Point]: + room_origin_vector = Vector.from_point(self.bounds.origin) + for y, x in np.ndindex(self.tiles.shape): + if self.tiles[y, x] == Wall: + yield Point(x, y) + room_origin_vector + + @property + def walkable_tiles(self) -> Iterable[Point]: + room_origin_vector = Vector.from_point(self.bounds.origin) + for y, x in np.ndindex(self.tiles.shape): + if self.tiles[y, x]['walkable']: + yield Point(x, y) + room_origin_vector