Add cellular atomata to the map generator finally!

Use the new map generator mechanism to generate rooms via cellular
atomata. Create a new CellularAtomatonRoomMethod class that uses
the Cellular Atomton class to create a room. Add a FreefromRoom class
that draws a room based on an ndarray of tiles.

Along the way I discovered I have misunderstood how numpy arrays
organize rows and columns. The numpy array creation routines take an
'order' argument that specifies whether arrays should be in C order (row
major) or Fortran order (column major). Fortran order lets you index
arrays with a more natural [x, y] coordinate order, and that's what the
tutorials I've read have shown. So I've been using that. When I was
developing the Cellular Atomaton, I wrote some code that assumed row-
major order. I think I want to move everything to row-major / C-style,
but that will take a bit more time.
This commit is contained in:
Eryn Wells 2023-03-05 18:40:02 -08:00
parent e6327deeef
commit 635aea5e3b
4 changed files with 111 additions and 27 deletions

2
ca.py
View file

@ -26,7 +26,7 @@ def main(argv):
log.init() log.init()
bounds = Rect(Point(), Size(20, 20)) bounds = Rect(Point(), Size(60, 20))
config = CellularAtomataMapGenerator.Configuration() config = CellularAtomataMapGenerator.Configuration()
config.number_of_rounds = args.rounds config.number_of_rounds = args.rounds

View file

@ -2,14 +2,16 @@
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional, TYPE_CHECKING
import numpy as np import numpy as np
from ... import log from ... import log
from ...geometry import Point, Rect from ...geometry import Point, Rect, Vector
from ..grid import make_grid from ..tile import Empty, Floor, Wall, tile_datatype
from ..tile import Floor, Wall
if TYPE_CHECKING:
from .. import Map
class CellularAtomataMapGenerator: class CellularAtomataMapGenerator:
@ -38,6 +40,7 @@ class CellularAtomataMapGenerator:
Initializer Initializer
### Parameters ### Parameters
`bounds` : `Rect` `bounds` : `Rect`
A rectangle representing the bounds of the cellular atomaton A rectangle representing the bounds of the cellular atomaton
`config` : `Optional[Configuration]` `config` : `Optional[Configuration]`
@ -46,7 +49,7 @@ class CellularAtomataMapGenerator:
''' '''
self.bounds = bounds self.bounds = bounds
self.configuration = config if config else CellularAtomataMapGenerator.Configuration() 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): def generate(self):
''' '''
@ -59,14 +62,23 @@ class CellularAtomataMapGenerator:
self._fill() self._fill()
self._run_atomaton() 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): def _fill(self):
fill_percentage = self.configuration.fill_percentage fill_percentage = self.configuration.fill_percentage
for y, x in np.ndindex(self.tiles.shape): 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): 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 number_of_rounds = self.configuration.number_of_rounds
if number_of_rounds < 1: if number_of_rounds < 1:
@ -100,21 +112,22 @@ class CellularAtomataMapGenerator:
# Start with 1 because the point is its own neighbor # Start with 1 because the point is its own neighbor
number_of_neighbors = 1 number_of_neighbors = 1
for neighbor in pt.neighbors: for neighbor in pt.neighbors:
if neighbor not in self.bounds: try:
continue if from_tiles[neighbor.y, neighbor.x] == Floor:
if from_tiles[neighbor.x, neighbor.y] == Floor:
number_of_neighbors += 1 number_of_neighbors += 1
except IndexError:
pass
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: if tile_is_alive and number_of_neighbors >= 5:
# Survival # Survival
to_tiles[pt.x, pt.y] = Floor to_tiles[idx] = Floor
elif not tile_is_alive and number_of_neighbors >= 5: elif not tile_is_alive and number_of_neighbors >= 5:
# Birth # Birth
to_tiles[pt.x, pt.y] = Floor to_tiles[idx] = Floor
else: else:
to_tiles[pt.x, pt.y] = Wall to_tiles[idx] = Empty
def __str__(self): def __str__(self):
return '\n'.join(''.join(chr(i['light']['ch']) for i in row) for row in self.tiles) return '\n'.join(''.join(chr(i['light']['ch']) for i in row) for row in self.tiles)

View file

@ -5,12 +5,14 @@ import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterable, Iterator, List, Optional, Tuple, TYPE_CHECKING from typing import Iterable, Iterator, List, Optional, Tuple, TYPE_CHECKING
import numpy as np
import tcod import tcod
from ... import log from ... import log
from ...geometry import Point, Rect, Size from ...geometry import Point, Rect, Size
from ..room import RectangularRoom, Room from ..room import FreeformRoom, RectangularRoom, Room
from ..tile import Empty, Floor, StairsDown, StairsUp, Wall from ..tile import Empty, Floor, StairsDown, StairsUp, Wall, tile_datatype
from .cellular_atomata import CellularAtomataMapGenerator
if TYPE_CHECKING: if TYPE_CHECKING:
from .. import Map from .. import Map
@ -196,6 +198,45 @@ class RectangularRoomMethod(RoomMethod):
return RectangularRoom(rect) 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): class OrRoomMethod(RoomMethod):
''' '''
A room generator method that picks between several RoomMethods at random A room generator method that picks between several RoomMethods at random

View file

@ -4,9 +4,12 @@
Implements an abstract Room class, and subclasses that implement it. Rooms are basic components of maps. 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: class Room:
@ -21,17 +24,17 @@ class Room:
return self.bounds.midpoint return self.bounds.midpoint
@property @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.''' '''An iterator over all the points that make up the walls of this room.'''
raise NotImplementedError() raise NotImplementedError()
@property @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''' '''An iterator over all the points that make of the floor of this room'''
raise NotImplementedError() raise NotImplementedError()
@property @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.''' '''An iterator over all the points that are walkable in this room.'''
raise NotImplementedError() raise NotImplementedError()
@ -47,14 +50,14 @@ class RectangularRoom(Room):
''' '''
@property @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) 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 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): for x in range(floor_rect.min_x, floor_rect.max_x + 1):
yield Point(x, y) yield Point(x, y)
@property @property
def wall_points(self) -> Iterator[Point]: def wall_points(self) -> Iterable[Point]:
bounds = self.bounds bounds = self.bounds
min_y = bounds.min_y min_y = bounds.min_y
@ -71,7 +74,7 @@ class RectangularRoom(Room):
yield Point(max_x, y) yield Point(max_x, y)
@property @property
def floor_points(self) -> Iterator[Point]: def floor_points(self) -> Iterable[Point]:
inset_bounds = self.bounds.inset_rect(1, 1, 1, 1) inset_bounds = self.bounds.inset_rect(1, 1, 1, 1)
min_y = inset_bounds.min_y min_y = inset_bounds.min_y
@ -85,3 +88,30 @@ class RectangularRoom(Room):
def __repr__(self) -> str: def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.bounds})' 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