aboutsummaryrefslogtreecommitdiff
path: root/gol.py
blob: a4d5558a54ce34b122cdb5ca788d43ecacd9dbbf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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)])