diff --git a/scripts/main_ui.py b/scripts/main_ui.py index 68d6332..d43ffbf 100644 --- a/scripts/main_ui.py +++ b/scripts/main_ui.py @@ -2,64 +2,96 @@ import pygame import random +import time from cellular_automaton.cellular_automaton import CellularAutomaton -from cellular_automaton.ca_cell import CACell -from cellular_automaton.ca_grid import CAGrid -from cellular_automaton.ca_rule import CARule +from cellular_automaton.ca_rule import Rule +from cellular_automaton.ca_neighborhood import MooreNeighborhood, EdgeRule class WorldGeneratorWindow: - def __init__(self, windows_size): + def __init__(self, windows_size: list, cellular_automaton: CellularAutomaton): self.window_size = windows_size + self.grid_size = self.window_size.copy() + self.grid_size[1] -= 20 pygame.init() pygame.display.set_caption("World Generator") - pygame.display.set_mode(self.window_size) + self.screen = pygame.display.set_mode(self.grid_size) - def display_cellular_automaton(self, cellular_automaton_instance): - pass + self._cellular_automaton = cellular_automaton + self.font = pygame.font.SysFont("monospace", 15) + + def set_cellular_automaton(self, cellular_automaton): + self._cellular_automaton = cellular_automaton + + def _display_cellular_automaton(self): + grid_dimension = self._cellular_automaton.grid.get_dimension() + + cell_size = [x / y for x, y in zip(self.grid_size, grid_dimension)] + + surfaces_to_update = [] + for cell in self._cellular_automaton.grid.get_active_cells().values(): + if not cell.dirty: + continue + cell_coordinate = cell.coordinate + status = cell.get_status_for_iteration(self._cellular_automaton.get_iteration_index()) + if status is None: + status = [0] + red = 0 + if status[0] >= 10: + red = 255 + cell_color = [red, 0, 0] + surface_pos = [x * y for x, y in zip(cell_size, cell_coordinate)] + surface_pos[1] += 20 + surfaces_to_update.append(self.screen.fill(cell_color, (surface_pos, cell_size))) + pygame.display.update(surfaces_to_update) + + def main_loop(self): + running = True + + while running: + time_ca_start = time.time() + self._cellular_automaton.evolve() + time_ca_end = time.time() + self._display_cellular_automaton() + time_ds_end = time.time() + self._print_process_duration(time_ca_end, time_ca_start, time_ds_end) + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + def _print_process_duration(self, time_ca_end, time_ca_start, time_ds_end): + self.screen.fill([0, 0, 0], ((0, 0), (self.window_size[0], 30))) + self._write_text((10, 5), "CA: " + "{0:.4f}".format(time_ca_end - time_ca_start) + "s") + self._write_text((310, 5), "Display: " + "{0:.4f}".format(time_ds_end - time_ca_end) + "s") + + def _write_text(self, pos, text, color=(0, 255, 0)): + label = self.font.render(text, 1, color) + update_rect = self.screen.blit(label, pos) + pygame.display.update(update_rect) -def main(): - - running = True - pygame.init() - pygame.display.set_caption("minimal program") - - screen = pygame.display.set_mode((1000, 730)) - image = pygame.image.load("../images/map.png") - screen.blit(image, (0, 0)) - screen.set_at((50, 60), [50, 0, 0]) - screen.set_at((50, 61), [50, 0, 0]) - screen.set_at((51, 60), [50, 0, 0]) - screen.set_at((51, 61), [50, 0, 0]) - pygame.display.flip() - - while running: - for x in range(0, 1000): - for y in range(0, 700): - screen.set_at((x, y), [random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255)]) - pygame.display.flip() - - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - -class TestRule(CARule): - def evolve_cell(self, cell, neighbors): - if neighbors[1][0] != 0: - return [1] - else: - return [0] +class TestRule(Rule): + def evolve_cell(self, cell, neighbors, iteration_index): + last_iteration = iteration_index - 1 + if cell.get_status_for_iteration(last_iteration) is None: + rand = random.randrange(0, 101, 1) + if rand <= 99: + rand = 0 + cell.set_status_for_iteration([rand], iteration_index) + cell.set_status_for_iteration([rand], iteration_index + 1) + if rand != 0: + cell.dirty = True + elif len(neighbors) == 8: + left_neighbour_status = neighbors[3].get_status_for_iteration(last_iteration) + cell.set_status_for_iteration(left_neighbour_status, iteration_index) + return cell.dirty if __name__ == "__main__": - main() - dim = [200, 500] - ca = CellularAutomaton(2) - - new_grid = CAGrid(dim) - new_grid.set_cell_by_coordinate([1, 1], CACell([1])) rule = TestRule() + ca = CellularAutomaton([500, 500], MooreNeighborhood(EdgeRule.IGNORE_EDGE_CELLS), rule, thread_count=1) + ca_window = WorldGeneratorWindow([1000, 730], ca) + ca_window.main_loop() diff --git a/src/cellular_automaton/ca_cell.py b/src/cellular_automaton/ca_cell.py index 854a95d..63c2620 100644 --- a/src/cellular_automaton/ca_cell.py +++ b/src/cellular_automaton/ca_cell.py @@ -1,7 +1,26 @@ class Cell: - def __init__(self, name: str): + def __init__(self, name: str, coordinate: list): self.name = name + self.coordinate = coordinate self.neighbours = [] + self._status = [None, None] + self.dirty = False def set_neighbours(self, neighbours: list): self.neighbours = neighbours + + def set_status_for_iteration(self, new_status, iteration): + """ Will set the new status for Iteration. + :param new_status: The new status to set. + :param iteration: Uses the iteration index, to differ between current and next state. + """ + self._status[iteration % 2] = new_status + + self.dirty = self._status[0] != self._status[1] + + def get_status_for_iteration(self, iteration): + """ Will return the status for the iteration. + :param iteration: Uses the iteration index, to differ between current and next state. + :return The status for this iteration. + """ + return self._status[iteration % 2] diff --git a/src/cellular_automaton/ca_grid.py b/src/cellular_automaton/ca_grid.py index cc0bd77..33f54d4 100644 --- a/src/cellular_automaton/ca_grid.py +++ b/src/cellular_automaton/ca_grid.py @@ -1,9 +1,11 @@ +import sys + from cellular_automaton.ca_cell import Cell -from cellular_automaton.ca_neighborhood import CellularAutomatonNeighborhood +from cellular_automaton.ca_neighborhood import Neighborhood class Grid: - def __init__(self, dimension: list, neighborhood: CellularAutomatonNeighborhood): + def __init__(self, dimension: list, neighborhood: Neighborhood): self._dimension = dimension self._cells = {} self._neighborhood = neighborhood @@ -11,15 +13,22 @@ class Grid: self._create_cells() self._set_cell_neighbours() - self._active_cells = {} - self.set_all_cells_active() + self._active_cells = self._cells.copy() + self._set_all_cells_active() - def set_all_cells_active(self): - for cell_key in self._cells: - self._active_cells[cell_key] = 1 + def get_names_of_active_cells(self): + return list(self._active_cells.keys()) def get_active_cells(self): - return self._active_cells.keys() + return self._active_cells + + def clear_active_cells(self): + self._active_cells.clear() + + def set_cell_and_neighbours_active(self, cell_info: list): + self._active_cells[cell_info[0].name] = cell_info[0] + for neighbour in cell_info[1]: + self._active_cells[neighbour.name] = neighbour def get_cell_and_neighbors(self, cell_name): cell = self._cells[cell_name] @@ -30,6 +39,15 @@ class Grid: return [cell, neighbour_objects] + def get_cell_by_coordinate(self, coordinate): + return self._cells[_join_coordinate(coordinate)] + + def get_dimension(self): + return self._dimension + + def _set_all_cells_active(self): + for cell_key, cell in self._cells.items(): + self._active_cells[cell_key] = cell def _create_cells(self, dimension_index=0, coordinate=None): """ Recursively steps down the dimensions to create cells in n dimensions and adds them to a dict. @@ -37,13 +55,13 @@ class Grid: :param coordinate: The coordinate generated so far. (each recursion adds one dimension to the coordinate. """ - coordinate = self.instantiate_coordinate_if_necessary(coordinate) + coordinate = _instantiate_coordinate_if_necessary(coordinate) try: self._recursive_step_down_dimensions(coordinate, dimension_index, self._create_cells) except IndexError: - coordinate_string = '-'.join(coordinate) - self._cells[coordinate_string] = Cell(coordinate_string) + coordinate_string = _join_coordinate(coordinate) + self._cells[coordinate_string] = Cell(coordinate_string, coordinate) def _recursive_step_down_dimensions(self, coordinate, dimension_index, recursion_method): """ For the range of the current dimension, recalls the recursion method. @@ -52,14 +70,8 @@ class Grid: :param recursion_method: The method to call for recursion. """ for cell_index in range(self._dimension[dimension_index]): - coordinate.append(cell_index) - recursion_method(dimension_index + 1, coordinate.copy()) - - @staticmethod - def instantiate_coordinate_if_necessary(coordinate): - if coordinate is None: - coordinate = [] - return coordinate + new_cod = coordinate + [cell_index] + recursion_method(dimension_index + 1, new_cod) def _set_cell_neighbours(self, dimension_index=0, coordinate=None): """ Recursively steps down the dimensions to get the string instances for each cells neighbours. @@ -67,12 +79,30 @@ class Grid: :param coordinate: The coordinate generated so far. (each recursion adds one dimension to the coordinate. """ - coordinate = self.instantiate_coordinate_if_necessary(coordinate) + coordinate = _instantiate_coordinate_if_necessary(coordinate) try: - self._recursive_step_down_dimensions(coordinate, dimension_index, self._set_cell_neighbours) + self._recursive_step_down_dimensions(coordinate.copy(), dimension_index, self._set_cell_neighbours) except IndexError: neighbours_coordinates = self._neighborhood.get_neighbor_coordinates(coordinate, self._dimension) - neighbour_names = [self._cells['-'.join(nc)].name for nc in neighbours_coordinates] - self._cells['-'.join(coordinate)].set_neighbours(neighbour_names) + neighbour_names = [self._cells[_join_coordinate(nc)].name for nc in neighbours_coordinates] + self._cells[_join_coordinate(coordinate)].set_neighbours(neighbour_names) + +# def __sizeof__(self): +# size = 0 +# for cell in self._cells.values(): +# size += sys.getsizeof(cell) +# size += sys.getsizeof(self._dimension) + sys.getsizeof(self._cells) + sys.getsizeof(self._active_cells) + +# return size + + +def _instantiate_coordinate_if_necessary(coordinate): + if coordinate is None: + coordinate = [] + return coordinate + + +def _join_coordinate(coordinate): + return '-'.join(str(x) for x in coordinate) diff --git a/src/cellular_automaton/ca_neighborhood.py b/src/cellular_automaton/ca_neighborhood.py index c156b2e..809c393 100644 --- a/src/cellular_automaton/ca_neighborhood.py +++ b/src/cellular_automaton/ca_neighborhood.py @@ -7,7 +7,7 @@ class EdgeRule(Enum): FIRST_AND_LAST_CELL_OF_DIMENSION_ARE_NEIGHBORS = 2 -class CellularAutomatonNeighborhood: +class Neighborhood: def __init__(self, neighbors: list, edge_rule: EdgeRule): self._neighbors = neighbors self.edge_rule = edge_rule @@ -18,7 +18,9 @@ class CellularAutomatonNeighborhood: def get_neighbor_coordinates(self, cell_coordinate, dimensions): self.dimensions = dimensions - if not self._does_ignore_edge_cell_rule_apply(cell_coordinate): + if self._does_ignore_edge_cell_rule_apply(cell_coordinate): + return [] + else: return self._apply_edge_rule_to_neighbours_of(cell_coordinate) def _does_ignore_edge_cell_rule_apply(self, coordinate): @@ -37,6 +39,7 @@ class CellularAutomatonNeighborhood: for neighbour in self._neighbors: if not self._does_ignore_edge_cell_neighbours_rule_apply(neighbour, cell_coordinate): remaining_neighbours.append(self._calculate_neighbour_coordinate(neighbour, cell_coordinate)) + return remaining_neighbours def _does_ignore_edge_cell_neighbours_rule_apply(self, neighbour, cell_coordinate): if self.edge_rule == EdgeRule.IGNORE_MISSING_NEIGHBORS_OF_EDGE_CELLS: @@ -47,18 +50,20 @@ class CellularAutomatonNeighborhood: return False def _calculate_neighbour_coordinate(self, neighbour, cell_coordinate): + new_coordinate = [] for rel_nd, cd, d in zip(neighbour, cell_coordinate, self.dimensions): nd = cd + rel_nd if nd < 0: nd = d - 1 elif nd >= d: nd = 0 - return nd + new_coordinate.append(nd) + return new_coordinate -class MooreNeighborhood(CellularAutomatonNeighborhood): +class MooreNeighborhood(Neighborhood): def __init__(self, edge_rule: EdgeRule): super().__init__([[-1, -1], [0, -1], [1, -1], - [-1, 0], [0, 0], [1, 0], + [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]], edge_rule) diff --git a/src/cellular_automaton/ca_rule.py b/src/cellular_automaton/ca_rule.py index 6853d76..21c9801 100644 --- a/src/cellular_automaton/ca_rule.py +++ b/src/cellular_automaton/ca_rule.py @@ -1,9 +1,18 @@ from cellular_automaton.ca_cell import Cell +from abc import abstractmethod class Rule: def __init__(self): pass - def evolve_cell(self, cell: Cell, neighbours: list): - pass + @abstractmethod + def evolve_cell(self, cell: Cell, neighbours: list, iteration_index: int): + """ Calculates and sets new state of 'cell'. + :param cell: The cell to calculate new state for. + :param neighbours: The neighbour cells of this cell. + :param iteration_index: The current iteration index, to choose the correct state. + :return: True if state changed, False if not. + A cells evolution will only be called if it or at least one of its neighbours has changed last iteration cycle. + """ + return False diff --git a/src/cellular_automaton/cellular_automaton.py b/src/cellular_automaton/cellular_automaton.py index 5689d8f..8367152 100644 --- a/src/cellular_automaton/cellular_automaton.py +++ b/src/cellular_automaton/cellular_automaton.py @@ -3,13 +3,15 @@ import time from cellular_automaton.ca_grid import Grid from cellular_automaton.ca_rule import Rule +from cellular_automaton.ca_neighborhood import Neighborhood class CellularAutomaton: - def __init__(self, dimension: list, rule_: Rule=None, thread_count: int=4): - self.grid = Grid(dimension) + def __init__(self, dimension: list, neighborhood: Neighborhood, rule_: Rule=None, thread_count: int=4): + self.grid = Grid(dimension, neighborhood) self._rule = rule_ - self._thread_count=thread_count + self._thread_count = thread_count + self._iteration = 0 def set_rule(self, rule: Rule): self._rule = rule @@ -17,13 +19,28 @@ class CellularAutomaton: def set_thread_count(self, thread_count: int): self._thread_count = thread_count + def get_iteration_index(self): + return self._iteration + def evolve(self): + if self._all_cells_are_inactive(): + return True + else: + self._iteration += 1 + self._delegate_evolve_to_threads() + return False + + def _delegate_evolve_to_threads(self): cell_lists_for_threats = self.create_cell_lists_for_threads() + self.grid.clear_active_cells() threads = self._start_treads_to_evolve_grid(cell_lists_for_threats) self._wait_for_all_threads_to_finish(threads) + def _all_cells_are_inactive(self): + return len(self.grid.get_names_of_active_cells()) == 0 + def create_cell_lists_for_threads(self): - active_cells = self.grid.get_active_cells() + active_cells = self.grid.get_names_of_active_cells() cell_count_per_thread = int(len(active_cells) / self._thread_count) return self.divide_active_cells(cell_count_per_thread, active_cells) @@ -35,7 +52,7 @@ class CellularAutomaton: def _start_treads_to_evolve_grid(self, cell_lists_for_threats): threads = [] for t in range(self._thread_count): - new_thread = _EvolutionThread(self.grid, self._rule, cell_lists_for_threats[t]) + new_thread = _EvolutionThread(self.grid, self._rule, cell_lists_for_threats[t], self._iteration) threads.append(new_thread) new_thread.start() return threads @@ -50,17 +67,22 @@ class CellularAutomaton: class _EvolutionThread(threading.Thread): - def __init__(self, grid: Grid, rule: Rule, cell_list: list): + def __init__(self, grid: Grid, rule: Rule, cell_list: list, iteration: int): super(_EvolutionThread, self).__init__() self._grid = grid self._rule = rule self._cell_list = cell_list self._next_state = [] self._finished = False + self._iteration = iteration def run(self): for cell in self._cell_list: - self._rule.evolve_cell(*self._grid.get_cell_and_neighbors(cell)) + cell_info = self._grid.get_cell_and_neighbors(cell) + active = self._rule.evolve_cell(cell_info[0], cell_info[1], self._iteration) + + if active: + self._grid.set_cell_and_neighbours_active(cell_info) self._finished = True def get_new_cell_states(self):