-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlife.py
118 lines (98 loc) · 3.89 KB
/
life.py
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
# Standard Library
import collections
from typing import Iterator
# Third party
import numpy as np
from numpy.lib.stride_tricks import sliding_window_view as windows
# Local
import exceptions
import animator
import seeds
# Type aliases
LifeState = np.ndarray
LifeIterator = Iterator[LifeState]
# Constants
MAX_GENERATIONS = 1000
class Life:
def __init__(self, size=100, bounds="fixed", seed_type="tiles"):
exceptions.validate_args(size, bounds, seed_type)
self.size = size
self.bounds = bounds
self.seed_type = seed_type
self.pad_mode = "constant" if self.bounds == "fixed" else "wrap"
self.seed = self.__seed_generator()
self.state = self.__state_generator(next(self.seed))
self.history = collections.deque(maxlen=3)
self.generations = 0
def animate(self) -> None:
"""Iterates `Life` until an exit condition is reached, then produces
an animated gif with a filename auto-generated via `__str__()`.
An `exit_code` of 2 means that either a steady state or an oscillating
state of period two was reached in the course of iteration. In this
case, the last two states are duplicated 15 extra times so that the
animated gif doesn't `#gifsthatendtoosoon`."""
frames = []
try:
while True:
frames.append(next(self))
except StopIteration as exit_code:
if exit_code.value == 2:
frames.extend(frames[-2:]*15)
animator.make_animation(frames, f"./examples/{self}")
self.reset()
def reset(self) -> None:
"""Resets this object's seed and state generators"""
self.seed = self.__seed_generator()
self.state = self.__state_generator()
self.history = collections.deque(maxlen=3)
self.generations = 0
def __str__(self):
height, width = (self.size,)*2
return f"{width}x{height}_{self.bounds}_{self.seed_type}"
def __repr__(self):
size, bounds, seed_type = self.size, self.bounds, self.seed_type
return f"Life({size=}, {bounds=}, {seed_type=})"
def __iter__(self):
return self
def __next__(self):
return next(self.state)
def __seed_generator(self) -> seeds.LifeSeedGenerator:
return seeds.new_seed_generator(self.size, self.seed_type)
def __state_generator(self, state) -> LifeIterator:
while True:
yield state
self.generations += 1
self.history.append(state)
if (exit_code := self.__check_exit()) != 1:
return exit_code
state = self.__update(state)
def __update(self, state: LifeState) -> LifeState:
"""Calculates the next generation of `Life` given the current `state`
and number of living neighbor `nbrs`."""
adj = windows(np.pad(state, 1, self.pad_mode), (3, 3)).sum(axis=(2, 3))
nbrs = adj - state
return (nbrs < 4) * (1 - state * (nbrs % 2 - 1) + nbrs) // 4
def __check_exit(self) -> int:
"""
Checks for three exit conditions and returns a corresponding code.
Exit codes:
`0`: The maximum number of generations has been reached
`1`: OK to continue
`2`: A steady state has been reached
`2`: An oscillating state of period two has been reached
"""
exit_code = 1
if len(self.history) > 2:
if np.array_equal(self.history[-2], self.history[-1]):
print("steady state reached")
exit_code = 2
elif np.array_equal(self.history[-3], self.history[-1]):
print("oscillating state period two reached")
exit_code = 2
if self.generations == MAX_GENERATIONS:
print("reached maximum allowed generations")
exit_code = 0
return exit_code
if __name__ == "__main__":
life = Life(100)
[*life]