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

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