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()
bounds = Rect(Point(), Size(20, 20))
bounds = Rect(Point(), Size(60, 20))
config = CellularAtomataMapGenerator.Configuration()
config.number_of_rounds = args.rounds

View file

@ -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)

View file

@ -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

View file

@ -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