1
\$\begingroup\$

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
\$\endgroup\$
2
  • \$\begingroup\$ Seems perfect to me \$\endgroup\$ Sep 6, 2023 at 10:10
  • 1
    \$\begingroup\$ @QuasiStellar thank you! appreciate it! \$\endgroup\$
    – frix
    Sep 7, 2023 at 3:36

1 Answer 1

2
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\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
    Sep 7, 2023 at 3:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.