Your code works and is readable, which is nice. It also mostly follows PEP8 style conventions, which is nice, although this can be improved, as it was already mentioned in the other answer.
Of course, there is always room for improvement.
What your code lacks is structure: everything happens at the top level, with no use of functions or classes.
This is bad, as it makes the code harder to read and follow along, less maintainable and less reusable.
Breaking the code into functions allows you to focus on each aspect of the code independently. Consider the following pseudocode:
In this case, the main loop is very simple and easy to follow. Now, if you want to work on how to display the cells, you can easily go to the relevant function's definition and work on just that. If you want to try another way to display, for example with a graphic display instead of characters on console, you can define another function and replace just one line in the main loop to try it out.
It also shows that it needs a way to keep track of the current state of the cell grid.
In your code, you use a global variable called
nextCells. Global variables are considered bad practice for a number of good reasons, mainly because it makes it hard to keep track where in the code they are acted upon, and thus what it's value is supposed to be at a given point in the code, especially once you break the code into multiple functions.
One way to fix that is to pass the variable as arguments to functions:
cells = initialize()
cells = update_state(cells)
You could argue that
cells is still technically a global variable, as it is defined at the root level of the code. One way to deal with that is to encapsulate the game state into a class. The class should hold the game state, provide ways to act upon it and to access information about it.
Implementing a class, the code now looks something like this:
game = GameOfLife()
Now there are just a few more things left to address.
First is documentation: while the code is simple and mostly self-documenting, it is good practice to document the code using docstrings and comments, if necessary.
Docstrings should describe the purpose of classes and functions, and how to use them. Should the class/function be used elsewhere, they can be accessed with the
help() builtin function of Python, and allow users to read on how to use your work without reading actual code.
Comments should provide why things are done a certain way, if it is unclear. If the code is made using descriptive names and logical units, there should be little need for comments.
Finally is reusability. Now that the game of life is encapsulated into a convenient object, it can be useful to
import it from another script and use it. However, as is, the last 2 lines would execute and an instance of the game would run, which is not desirable in this case.
imports and running the script, you can put the code inside a "main guard".
My take on the problem is:
from time import sleep
An implementation of Conway's game of life on a finite and wrapping grid
LIVE_CELL = '#'
DEAD_CELL = ' '
DELAY = 1
def __init__(self, width=60, height=20):
self.width = width
self.height = height
self.cells = [[random.choice([True, False]) for _ in range(self.width)] for _ in range(self.height)]
Updates the cell grid according to Conway's rules
next_cells = [[False for _ in range(self.width)] for _ in range(self.height)]
for j in range(self.width):
for i in range(self.height):
neighbor_count = self._count_live_neighbors(i, j)
if ((self.cells[i][j] and neighbor_count in (2, 3))
or (not self.cells[i][j] and neighbor_count == 3)):
next_cells[i][j] = True
self.cells = next_cells
def _count_live_neighbors(self, i, j):
Counts the number of live neighbor of a cell, given the cell indices on the grid
count = 0
for di, dj in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]:
if self.cells[(i + di) % self.height][(j + dj) % self.width]:
count += 1
Print the current state of the cell grid on the console
for row in self.cells:
chars = [self.LIVE_CELL if c else self.DEAD_CELL for c in row]
Run the game of life until a keyboard interrupt is raise (by pressing ctrl+c)
if __name__ == '__main__':
life = Life()
As you can see, I also changed some of the logic, so I'll explain my reasoning.
Using an array of booleans for the cell grid
True represent live cells,
False dead ones. This is to improve reusability, leaving the display logic decide how to display dead or alive cells. It also simplifies some of the logic.
Using a loop to iterate over neighbors
Less copy-and-pasted code makes it less error-prone and more maintainable, while still being easy to understand in this case.
Using list comprehensions
Python one-liners can sometimes get hard to read, but list comprehensions to initialize lists is very useful and muck more efficient than iteratively appending values.
In simple cases like that, it is still quite readable.
If you're interested in going further as an exercise, I can think of a few improvements that can be done to the code:
- Implement other ways to initialize the game, for example by passing a cell grid to the constructor (easy), or by parsing a file (a bit harder). This would allow to try out some patterns.
- The canonical game of life as described by John Conway is carried out on an infinite grid. While this is impossible in practice, it can be approximated by keeping track of the game state for some distance outside of what is actually displayed (medium)
- Displaying on console is not very good, as the displayed cells do not look very good, are not square, and are quite limited in number, while the console flickers. A graphical display would be more suited, but also a lot harder to implement.
- Updating the game state can be heavily optimized. It's fine for a small number of cells (and as such for console display), but if you eventually move to a large grid, this solution is probably not good enough. Possible improvements I can think of:
- As of now, each time the grid state is updated, a new list is allocated, then the old one is disposed of by the garbage collector. Both of these actions are slow. A solution would be to keep 2 lists in memory, and point to either one for the current generation and the next generation.
- Calculating the state of the next generation is easily parallelisable, as each cell is independent from every other one.