diff options
Diffstat (limited to 'gol.py')
| -rw-r--r-- | gol.py | 143 |
1 files changed, 143 insertions, 0 deletions
@@ -0,0 +1,143 @@ +"""Module that contain the GameOfLife class + +The Game append on an infinite 2D grid, +each node of it can be either alive or dead. + +A dead node become alive if he as 3 alive neighbors, +an alive node become dead if he as less than 2 or more than 3 neighbors, +else they stay in their current state. +""" + +import re +import os +import sys +from time import sleep +from math import inf +from random import random +from itertools import product, count +from functools import reduce +from collections import namedtuple + +from graphic import Graphic + + +class GameOfLife: + """Class of Conway's Game of life""" + + NEIGHBORS_MOD = list(product([-1, 0, 1], repeat=2)) + SIZE = namedtuple('Size', 'w h') + NODE = namedtuple('Node', 'x y') + + def __init__(self, kwargs): + """Initialize the set of alive nodes with the selected options.""" + + self.generation_counter = 0 + self.pattern_name = '' + for key, value in kwargs.items(): + setattr(self, key, value) + + if kwargs['search'] != '': + self._search_pattern(kwargs['search']) + + if self.random_rate != 0.0: + self.size = self.SIZE(100, 100) + self.alive_nodes = { + self.NODE(x, y) for x in range(self.size.h) + for y in range(self.size.w) + if random() < self.random_rate} + else: + self.alive_nodes = { + self.NODE(x, y) for x, row in enumerate( + self._generate_pattern_grid_from_file()) + for y, node in enumerate(row) if node} + + def _search_pattern(self, search): + file_names = os.listdir('./patterns') + match = sorted( + filter(lambda n: re.match(r'.*' + re.escape(search) + r'.*', n), + map(lambda n: n[:-4], file_names)), + key=lambda k: len(k) + ) + for m in match: + print(f'- {m}') + sys.exit() + + def _generate_pattern_grid_from_file(self): + """ Try to read the file with the pattern_name, + and extract a grid of 1's and 0's from it. + """ + + with open(f'./patterns/{self.pattern_file_name}.rle', 'r') as pattern_file: + for line in pattern_file: + if line[0] == '#': + if line[1] == 'N': + self.pattern_name = line[3:-1] + elif line[0] == 'x': + self.size = self.SIZE(*map( + int, + re.search(r'^x = (\d+), y = (\d+)', line).groups())) + break + return [ + [ + 1 if n[-1] == 'o' else 0 + for n in re.findall(r'(\d*[a-z])', line) + for _ in range(1 if len(n) == 1 else int(n[:-1]))] + for line in pattern_file.read().replace('\n', '').split('$')] + + def start(self): + """Iterate over generations and print them.""" + + if self.console_display: + while self.generation_counter <= self.max_gen: + self._console_display() + self.next_generation() + + else: + graphic = Graphic(self) + graphic.main_loop() + + def _console_display(self): + print(f'Generation: {self.generation_counter}\n{self._to_string()}\n') + input('Press Enter') if self.inspect else sleep(self.time_step) + + def next_generation(self): + """Go to the next generation.""" + + alive_nodes_cpy = self.alive_nodes.copy() + for n_pos in self._nodes_to_check(): + # Get the number of alive neighbors + # around a cell to apply the rules on it. + alive_neighbors = reduce( + lambda acc, p: acc + 1 if p in self.alive_nodes else acc, + [ + (x + n_pos.x, y + n_pos.y) + for x, y in self.NEIGHBORS_MOD if (x, y) != (0, 0) + ], 0) + + if n_pos not in self.alive_nodes and alive_neighbors == 3: + alive_nodes_cpy.add(n_pos) + elif n_pos in self.alive_nodes and not 2 <= alive_neighbors <= 3: + alive_nodes_cpy.remove(n_pos) + + self.alive_nodes = alive_nodes_cpy + self.generation_counter += 1 + + def _nodes_to_check(self): + """Return all the node to check at some generation""" + + return {self.NODE(x + n_pos.x, y + n_pos.y) + for x, y in self.NEIGHBORS_MOD + for n_pos in self.alive_nodes} + + def _to_string(self): + """Convert a grid of positions in a string. + Use the range of self width and height + and the size of the pattern + """ + + return '\n'.join( + [''.join([ + (self.alive_node_repr + if (x, y) in self.alive_nodes else self.dead_node_repr) + for y in range(-28, 32)]) + for x in range(-13, 17)]) |
