I implemented a program for computing Conway's Game of Life iterations in Python starting from an initial grid configuration given as input. It is intended to handle arbitrarily large grids. I tried to be as concise and efficient as possible and it seems to be working appropriately. Are there any improvements I could make? I'm specially interested in advice on more appropriate data structures (other than set and defaultdict). Thanks.
from collections import defaultdict
from typing import Tuple, Set
class ConwaysAutomaton:
def __init__(self, pattern: Set[Tuple[int, int]]):
# Store all alive cells in current iteration
self.alive = pattern
def update(self):
# The dict's keys are cells, represented by a tuple
# with their coordinates in the grid(row, column),
# and the values are the number of neighbours they
# currently have.
# It stores the set of all cells with potential to stay alive
# or to come to life (because they have neighbours).
hot_cells = defaultdict(int)
for alive in self.alive:
hot_cells[(alive[0]-1, alive[1]-1)] += 1
hot_cells[(alive[0]-1, alive[1])] += 1
hot_cells[(alive[0]-1, alive[1]+1)] += 1
hot_cells[(alive[0], alive[1]-1)] += 1
hot_cells[(alive[0], alive[1]+1)] += 1
hot_cells[(alive[0]+1, alive[1]-1)] += 1
hot_cells[(alive[0]+1, alive[1])] += 1
hot_cells[(alive[0]+1, alive[1]+1)] += 1
new_alive = set()
for candidate in hot_cells.keys():
# From Wikipedia:
# Any live cell with two or three live neighbours survives.
# Any dead cell with three live neighbours becomes a live cell.
# All other live cells die in the next generation.
# Similarly, all other dead cells stay dead.
if (candidate in self.alive and hot_cells[candidate] in [2, 3]) \
or (candidate not in self.alive and hot_cells[candidate] == 3):
new_alive.add(candidate)
self.alive = new_alive
if __name__ == '__main__':
import os, time
clear = lambda: os.system('cls')
os.system('clear')
#Pi-heptomino
# ooo
# o o
# o o
pattern = {(25, 25), (25, 26), (25, 27), (26, 25), (26, 27), (27, 25), (27, 27)}
iteration = 0
automaton = ConwaysAutomaton(pattern)
while True:
clear()
print("Iteration #", iteration)
for row in range(50):
for col in range(50):
print('#' if (row, col) in automaton.alive else ' ', end = '')
print()
input()
automaton.update()
iteration += 1
-
\$\begingroup\$ Seems perfect to me \$\endgroup\$QuasiStellar– QuasiStellar2023年09月06日 10:10:31 +00:00Commented Sep 6, 2023 at 10:10
1 Answer 1
I have just a few recommendations and a question:
Use Docstrings
You currently have no Docstrings. Some of the comments in method ConwaysAutomation.update
could serve to document what the method's purpose is. For example:
class ConwaysAutomaton:
"""Class to implement Conway's Game of Life"""
def __init__(self, pattern: Set[Tuple[int, int]]):
"""
Arguments
---------
pattern : Set
A set of tuples representing the coordinates of live cells.
"""
self.alive = pattern
def update(self):
"""Compute the next generation of live cells.
It stores the set of all cells with potential to stay alive
or to come to life (because they have neighbours).
"""
# The dict's keys are cells, represented by a tuple
# with their coordinates in the grid(row, column),
# and the values are the number of neighbours they
# currently have.
hot_cells = defaultdict(int)
...
Substitute collections.Counter
for collections.defaultdict
You currently have:
hot_cells = defaultdict(int)
Consider instead:
from collections import Counter
...
hot_cells = Counter()
The rest of the code remains unchanged but now it is immediately clear that the integer values of this dictionary hold counts.
Make Attribute alive
Private
You currently have client code that is accessing self.alive
assuming its implementation, which could change in the future. Suggest that this attribute is "private" by renaming it to _alive
and then create a method is_alive
that the client calls instead:
def is_alive(self, coordinate: Tuple[int, int]) -> bool:
"""Is the cell at the passed coordinate alive?"""
return coordinate in self._alive
Question
You have:
if __name__ == '__main__':
import os, time
clear = lambda: os.system('cls')
os.system('clear')
...
Invoking clear()
appears to clear the screen on Windows, where the cls console command exists for that purpose. But then you call os.system('clear')
, which won't work on Windows. So I am not clear why you have that. You could test the platform and define clear
to issue either a cls
or clear
command accordingly.
-
\$\begingroup\$ Thanks for you reply! About your question, I probably messed up that snippet after copying it from somewhere else. After some research, I found out that os.system("cls||clear") does the job. Thanks for noticing though. Also liked the suggestion of using Counter instead of defaultdict. In this case they're indeed interchangeable, and Counter really improves semantics! \$\endgroup\$frix– frix2023年09月07日 03:49:31 +00:00Commented Sep 7, 2023 at 3:49
Explore related questions
See similar questions with these tags.