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:
parent
e6327deeef
commit
635aea5e3b
4 changed files with 111 additions and 27 deletions
2
ca.py
2
ca.py
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
number_of_neighbors += 1
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
if from_tiles[neighbor.x, neighbor.y] == Floor:
|
idx = (pt.y, pt.x)
|
||||||
number_of_neighbors += 1
|
tile_is_alive = from_tiles[idx] == Floor
|
||||||
|
|
||||||
tile_is_alive = from_tiles[pt.x, pt.y] == 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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue