diff --git a/README.md b/README.md index 71feeac..63eaf6b 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,12 @@ There ist still quite some work to do. And for all others, don't hesitate to open issues when you have problems! ## Changelog +#### 1.0.8 +- Fixes automaton using edge cells with radius > 1 not working +- Fixes automaton is not stopping after evolution ended + #### 1.0.7 -- Fixes automaton ont active on reactivation +- Fixes automaton not active on reactivation #### 1.0.6 - Fixes reactivation not redrawing all cells diff --git a/cellular_automaton/automaton.py b/cellular_automaton/automaton.py index b0f7b05..6875424 100644 --- a/cellular_automaton/automaton.py +++ b/cellular_automaton/automaton.py @@ -75,15 +75,19 @@ class CellularAutomatonCreator(abc.ABC): class CellularAutomaton(CellularAutomatonCreator, abc.ABC): - """ - This class represents a cellular automaton. + """ This class represents a cellular automaton. It can be created with n dimensions and can handle different neighborhood definitions. - :param dimension: Iterable of len = dimensions - (e.g. [4, 3, 3, 3] = 4 x 3 x 3 x 3 cells in a four dimensional cube). - :param neighborhood: Defines which cells are considered neighbors. + It is intended to be uses as base class. + Override `init_cell_state()` to define the state the cell(s) are initiated with. + Override `evolve()` to define the rule that is aplied on every evolution step of this automaton. """ def __init__(self, neighborhood: Neighborhood, *args, **kwargs): + """ Initiates a cellular automaton by the use of the `init_cell_state` method. + :param neighborhood: Defines which cells are considered neighbors. + :param dimension: Iterable of len = dimensions + (e.g. [4, 3, 3, 3] = 4 x 3 x 3 x 3 cells in a four dimensional cube). + """ super().__init__(neighborhood=neighborhood, *args, **kwargs) self._evolution_step = 0 self._active = True @@ -132,7 +136,7 @@ class CellularAutomaton(CellularAutomatonCreator, abc.ABC): evolution_rule = self.evolve_rule for old, new in zip(this_state.values(), next_state.values()): if old.is_active: - new_state = evolution_rule(old.state, [n.state for n in old.neighbors]) + new_state = evolution_rule(old.state.copy(), [n.state for n in old.neighbors]) old.is_active = False evolve_cell(old, new, new_state) diff --git a/cellular_automaton/display.py b/cellular_automaton/display.py index d45ca48..79c9741 100644 --- a/cellular_automaton/display.py +++ b/cellular_automaton/display.py @@ -117,7 +117,8 @@ class CAWindow: time.sleep(rest_time) def _not_at_the_end(self, last_evolution_step): - return self._cellular_automaton.evolution_step < last_evolution_step or last_evolution_step <= 0 + return (self._cellular_automaton.evolution_step < last_evolution_step or last_evolution_step <= 0) \ + and self._cellular_automaton.active def __calculate_cell_display_size(self, stretch_cells): # pragma: no cover grid_dimension = self._cellular_automaton.dimension diff --git a/cellular_automaton/neighborhood.py b/cellular_automaton/neighborhood.py index 05be418..ebe637c 100644 --- a/cellular_automaton/neighborhood.py +++ b/cellular_automaton/neighborhood.py @@ -83,7 +83,7 @@ class Neighborhood: yield tuple(map(operator.add, rel_n, cell_coordinate)) def __is_coordinate_on_an_edge(self, coordinate): - return any(ci in [0, di-1] for ci, di in zip(coordinate, self._grid_dimensions)) + return any(not(self._radius-1 < ci < di-self._radius) for ci, di in zip(coordinate, self._grid_dimensions)) class MooreNeighborhood(Neighborhood): @@ -154,7 +154,6 @@ class RadialNeighborhood(Neighborhood): cross_sum = 0 for coordinate_i in rel_neighbor: cross_sum += pow(coordinate_i, 2) - return math.sqrt(cross_sum) <= self._radius + self.delta diff --git a/examples/times.py b/examples/times.py index 09577e2..9946eb0 100644 --- a/examples/times.py +++ b/examples/times.py @@ -59,5 +59,7 @@ def profile(code): if __name__ == "__main__": with contextlib.suppress(KeyboardInterrupt): + print("=== CREATION ===") profile('ca = StarFallAutomaton()') + print("=== COMPUTATION ===") profile('ca.evolve(times=10)') diff --git a/setup.py b/setup.py index 3b3c553..9d8eecb 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open('README.md') as f: setup( name="cellular_automaton", - version="1.0.7", + version="1.0.8", author="Richard Feistenauer", author_email="r.feistenauer@web.de", packages=find_packages(exclude=('tests', 'docs', 'examples')), diff --git a/tests/test_display.py b/tests/test_display.py index 63fed3f..e28b90f 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -52,7 +52,10 @@ class TAutomaton(ca.CellularAutomaton): return [1] if cell_coordinate == (1, 1) else [0] def evolve_rule(self, last_cell_state, neighbors_last_states): - return [last_cell_state[0] + 1] if neighbors_last_states else last_cell_state + ns = last_cell_state[:] + if 0 < last_cell_state[0] < 40: + ns[0] += 1 + return ns @pytest.fixture @@ -70,3 +73,12 @@ def test_evolution_steps_per_draw(automaton, pygame_mock): def test_updated_rectangle_calls(automaton, pygame_mock): ca.CAWindow(cellular_automaton=automaton, window_size=(10, 10)).run(last_evolution_step=4) assert pygame_mock.display.update.call_count == 4 * (3 + 1) # steps * (texts + changed cells) + +@import_mock(module='pygame') +def test_ends_when_ca_is_done(automaton, pygame_mock): + automaton.evolve(39) + assert automaton.active == True + assert automaton.evolution_step == 39 + ca.CAWindow(cellular_automaton=automaton, window_size=(10, 10)).run(last_evolution_step=45) + assert automaton.active == False + assert automaton.evolution_step == 40 diff --git a/tests/test_neighborhood.py b/tests/test_neighborhood.py index 8f2b979..0ebf3e9 100644 --- a/tests/test_neighborhood.py +++ b/tests/test_neighborhood.py @@ -93,6 +93,26 @@ def test_radial(): (1, 4), (2, 4), (3, 4)) +def test_radial_neighbor_coords(): + neighborhood = ca.RadialNeighborhood(edge_rule=ca.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS, radius=2) + neighbor_coords = neighborhood.calculate_cell_neighbor_coordinates((0, 0), (10, 10)) + assert neighbor_coords == ((9, 8), (0, 8), (1, 8), + (8, 9), (9, 9), (0, 9), (1, 9), (2, 9), + (8, 0), (9, 0), (1, 0), (2, 0), + (8, 1), (9, 1), (0, 1), (1, 1), (2, 1), + (9, 2), (0, 2), (1, 2)) + + +def test_radial_neighbor_coords(): + neighborhood = ca.RadialNeighborhood(edge_rule=ca.EdgeRule.FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS, radius=2) + neighbor_coords = neighborhood.calculate_cell_neighbor_coordinates((1, 1), (10, 10)) + assert neighbor_coords == ((0, 9), (1, 9), (2, 9), + (9, 0), (0, 0), (1, 0), (2, 0), (3, 0), + (9, 1), (0, 1), (2, 1), (3, 1), + (9, 2), (0, 2), (1, 2), (2, 2), (3, 2), + (0, 3), (1, 3), (2, 3)) + + @pytest.mark.parametrize(('coordinate', 'expected_neighborhood'), (((2, 2), ((1, 0), (2, 0), (3, 0), (0, 1), (1, 1), (2, 1), (3, 1),