Add CellularAtomataMapGenerator
First pass at a cellular atomata map generator. Add map.grid and a make_grid function to make it easier to make numpy arrays for Map purposes. Add ca.py to test the generator.
This commit is contained in:
parent
be7198b16d
commit
a542bb956a
3 changed files with 174 additions and 0 deletions
44
ca.py
Normal file
44
ca.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Eryn Wells <eryn@erynwells.me>
|
||||||
|
|
||||||
|
'''
|
||||||
|
Run the cellular atomaton from ErynRL and print the results.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from erynrl import log
|
||||||
|
from erynrl.geometry import Point, Rect, Size
|
||||||
|
from erynrl.map.generator.cellular_atomata import CellularAtomataMapGenerator
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv, *a, **kw):
|
||||||
|
'''Parse command line arguments'''
|
||||||
|
parser = argparse.ArgumentParser(*a, **kw)
|
||||||
|
parser.add_argument('--rounds', type=int, default=5)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
'''The script'''
|
||||||
|
|
||||||
|
args = parse_args(argv[1:], prog=argv[0])
|
||||||
|
|
||||||
|
log.init()
|
||||||
|
|
||||||
|
bounds = Rect(Point(), Size(20, 20))
|
||||||
|
|
||||||
|
config = CellularAtomataMapGenerator.Configuration()
|
||||||
|
config.number_of_rounds = args.rounds
|
||||||
|
|
||||||
|
gen = CellularAtomataMapGenerator(bounds, config)
|
||||||
|
|
||||||
|
gen.generate()
|
||||||
|
|
||||||
|
print(gen)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
result = main(sys.argv)
|
||||||
|
sys.exit(0 if not result else result)
|
120
erynrl/map/generator/cellular_atomata.py
Normal file
120
erynrl/map/generator/cellular_atomata.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
# Eryn Wells <eryn@erynwells.me>
|
||||||
|
|
||||||
|
import random
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ... import log
|
||||||
|
from ...geometry import Point, Rect
|
||||||
|
from ..grid import make_grid
|
||||||
|
from ..tile import Floor, Wall
|
||||||
|
|
||||||
|
|
||||||
|
class CellularAtomataMapGenerator:
|
||||||
|
'''
|
||||||
|
A map generator that utilizes a cellular atomaton to place floors and walls.
|
||||||
|
'''
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Configuration:
|
||||||
|
'''
|
||||||
|
Configuration of a cellular atomaton map generator.
|
||||||
|
|
||||||
|
### Attributes
|
||||||
|
`fill_percentage` : `float`
|
||||||
|
The percentage of tiles to fill with Floor when the map is seeded.
|
||||||
|
`number_of_rounds` : `int`
|
||||||
|
The number of rounds to run the atomaton. 5 is a good default. More
|
||||||
|
rounds results in smoother output; fewer rounds results in more
|
||||||
|
jagged, random output.
|
||||||
|
'''
|
||||||
|
fill_percentage: float = 0.5
|
||||||
|
number_of_rounds: int = 5
|
||||||
|
|
||||||
|
def __init__(self, bounds: Rect, config: Optional[Configuration] = None):
|
||||||
|
'''
|
||||||
|
Initializer
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
`bounds` : `Rect`
|
||||||
|
A rectangle representing the bounds of the cellular atomaton
|
||||||
|
`config` : `Optional[Configuration]`
|
||||||
|
A configuration object specifying parameters for the atomaton. If
|
||||||
|
None, the instance will use a default configuration.
|
||||||
|
'''
|
||||||
|
self.bounds = bounds
|
||||||
|
self.configuration = config if config else CellularAtomataMapGenerator.Configuration()
|
||||||
|
self.tiles = make_grid(bounds.size)
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
'''
|
||||||
|
Run the cellular atomaton on a grid of `self.bounds.size` shape.
|
||||||
|
|
||||||
|
First fill the grid with random Floor and Wall tiles according to
|
||||||
|
`self.configuration.fill_percentage`, then run the simulation for
|
||||||
|
`self.configuration.number_of_rounds` rounds.
|
||||||
|
'''
|
||||||
|
self._fill()
|
||||||
|
self._run_atomaton()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def _run_atomaton(self):
|
||||||
|
alternate_tiles = make_grid(self.bounds.size)
|
||||||
|
|
||||||
|
number_of_rounds = self.configuration.number_of_rounds
|
||||||
|
if number_of_rounds < 1:
|
||||||
|
raise ValueError('Refusing to run cellular atomaton for less than 1 round')
|
||||||
|
|
||||||
|
log.MAP_CELL_ATOM.info(
|
||||||
|
'Running cellular atomaton over %s for %d round%s',
|
||||||
|
self.bounds,
|
||||||
|
number_of_rounds,
|
||||||
|
'' if number_of_rounds == 1 else 's')
|
||||||
|
|
||||||
|
for i in range(number_of_rounds):
|
||||||
|
if i % 2 == 0:
|
||||||
|
from_tiles = self.tiles
|
||||||
|
to_tiles = alternate_tiles
|
||||||
|
else:
|
||||||
|
from_tiles = alternate_tiles
|
||||||
|
to_tiles = self.tiles
|
||||||
|
|
||||||
|
self._do_round(from_tiles, to_tiles)
|
||||||
|
|
||||||
|
# If we ended on a round where alternate_tiles was the "to" tile grid
|
||||||
|
# above, save it back to self.tiles.
|
||||||
|
if number_of_rounds % 2 == 0:
|
||||||
|
self.tiles = alternate_tiles
|
||||||
|
|
||||||
|
def _do_round(self, from_tiles: np.ndarray, to_tiles: np.ndarray):
|
||||||
|
for y, x in np.ndindex(from_tiles.shape):
|
||||||
|
pt = Point(x, y)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
if from_tiles[neighbor.x, neighbor.y] == Floor:
|
||||||
|
number_of_neighbors += 1
|
||||||
|
|
||||||
|
tile_is_alive = from_tiles[pt.x, pt.y] == Floor
|
||||||
|
if tile_is_alive and number_of_neighbors >= 5:
|
||||||
|
# Survival
|
||||||
|
to_tiles[pt.x, pt.y] = Floor
|
||||||
|
elif not tile_is_alive and number_of_neighbors >= 5:
|
||||||
|
# Birth
|
||||||
|
to_tiles[pt.x, pt.y] = Floor
|
||||||
|
else:
|
||||||
|
to_tiles[pt.x, pt.y] = Wall
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '\n'.join(''.join(chr(i['light']['ch']) for i in row) for row in self.tiles)
|
10
erynrl/map/grid.py
Normal file
10
erynrl/map/grid.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Eryn Wells <eryn@erynwells.me>
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .tile import Empty
|
||||||
|
from ..geometry import Size
|
||||||
|
|
||||||
|
|
||||||
|
def make_grid(size: Size):
|
||||||
|
return np.full(size.numpy_shape, fill_value=Empty, order='F')
|
Loading…
Add table
Add a link
Reference in a new issue