Implementing the Game of Life
In a previous article I have written, I explained what the Game of Life (which from now is going to be abbreviated to GOL) is. I discussed the rules that govern it, and the important concepts that had to be known in order for one to get a basic idea of what it is. In this article, I will share my implementation of GOL.
During this article, I assume a basic understanding of some programming concepts such as classes and functions, and some, albeit a minor, familiarity with Processing.py, a framework I describe later.
What Was Used
To implement GOL I used Processing.py, a Python framework based on Processing, a notorious framework amongst the generative art community. I chose this because it was the easiest way I could implement GOL.
Implementation — Part 1
First, it would make sense to define class which outlines what a cell would look like. A cell has a state (alive or dead), and a set number of neighbors (something we’ll tackle later on). Also, each cell exists in a column and a row, since we they are going to be in a grid. This is what our cell class would look like:
def __init__(self, col, row):
self.col = col
self.row = row
# all cells are initially dead, we input the alive ones.
self.state = False
Alright, we’re getting somewhere. Each cell also has a specific size, right? We’ll call that
width_. (Note that we can’t use
width, because it is a special variable in Processing.py.) We also want to specify a command, called a method, to show the cell on the screen. With these extra ideas in mind, we can update our cell class to finally be:
False when the cell is dead, and
True when the cell is alive. What’s happening in
show() is that the cell appears white if
self.state evaluates to
True, which means it is alive, and black if
self.state evaluates to
False, meaning that it is dead. The cell is represented as a
square() in a location
self.row with a width of
Our code later on will be easier if we defined a class that outlines how grids should look like and behave, so let’s implement that.
A grid has a width, a height, a number of rows, and a number of columns. We could make something basic like this:
def __init__(self, cell_width, width_, height_):
self.width_ = width_
self.height_ = height_
self.cell_width = cell_width
self.cols = self.width_ // self.cell_width
self.rows = self.height_ // self.cell_width
Now, why are we using
cell_width as an argument over here? Well, instead of directly stating how many rows and how many columns a grid has, I found out later that it is easier to just declare what each individual cell’s size is going to be. Here’s my rationale: Based on what should we calculate the size of each cell? Should it be based on the number of columns, or rows? In most cases, you might get the desired number of a single entity, but never quite both. Allowing the number of rows and columns to be calculated based on a single, inputted width is the better solution in my opinion.
__init__() method of the
Grid class is going to look like this:
Moving on, we need to remember that the the grid instance itself is basically going to be a list. Precisely, it’s going to have to be a 2-D list of cells, something we will discuss now.
“Creating” the Grid.
There are a couple of methods we need to implement in order for the code to be simpler later. The first method is the
create() method, which actually fills in the empty list
self.grid with cells. I’m going to skip directly to showing the code, and I’m going to elaborate on what it is doing later on.
Over here, we are filling in a 2-D list, which makes sense in this case. You want to have a list of columns (you could also have a list of rows; it’s up to you). Something like this:
grid = [column1, column2, column3] # as an example
and each column is in itself a list of cells that exist in the column; for example:
column1 = [cell1, cell2, cell3]
column2 = [cell4, cell5, cell6]
column3 = [cell7, cell8, cell9]
So now, substituting the values of the columns variables, we get:
grid = [
[cell1, cell2, cell3], # used to be column1
[cell4, cell5, cell6], # used to be column2
[cell7, cell8, cell9] # used to be column3
If this is the first time you’re dealing with 2-D lists, it might be a little confusing, but the more you encounter them, the more intuitive they are going to be.
To understand what the
create() method does, look at the animation first.
The code I wrote to create this animation is almost exactly identical as the code written in the
create() method. What you want, as we discussed, is a list of columns. So you loop over the x-axis of the screen (
self.width_) by steps of
self.cell_width. Why with steps of
self.cell_width? Think about it. If we want the cells in a grid to have no space between them, we would want the one side of one square (which would take a space of
self.cell_width) to be the beginning side of the next square.
Anyways, as we were saying, we want to loop over the x-axis by steps of
self.cell_width. We then want to create a new list called
column, because later we are going to loop through every y value in the certain x value ( something like (0, 100), (0, 200), (0, 300), etc.), which is what the inner loop that goes from lines 12 to 14 does. It also creates the cell at that certain x and y values, much like how that dot in the animation would be at a new location for the cell to be created at. In line 15, all we are doing is adding that column, that list of cells, to
self.grid, or the list of
Showing the Grid
We need to have a method that reveals, or shows, the cells in
self.grid. This is reasonably easy to do. All you have to do is loop over each
column in the grid (
for column in self.grid:
and then over each cell in the column:
for column in self.grid:
for cell in column:
show() the cell:
for column in self.grid:
for cell in column:
show() method we created for the
Here is the
show() method for the
Erasing the Grid
We will want to be able to erase the components of the array
self.grid for reasons that will become clear later. Usually how clearing a list in Python works is by using the
clear() method (e.g.,
lst.clear()). However, in Processing.py, the framework I’m using, this didn’t work (probably because
clear() is a special function that is defined globally).
The work-around to this obstacle is very simple:
Why do I not just use
del self.grid? Because then the attribute itself,
self.grid, is going to be deleted from the memory and we won’t be able to use it later. What
del self.grid[:] does is deleting every element in
self.grid, but not deleting the variable itself, much like resetting a variable’s value.
Getting the Neighbors of a Cell
We will want to be able to get the neighbors of a cell later. To make the code look less messy, we might as well define a method that will do that.
Now when I first attempted to do that, I wrote a method
get_neighbors() that accepted the following arguments:
cell. I then fell into the rabbit hole of finding the index of that cell to find its neighbors. All this was unnecessary.
What we need is a method
get_neighbors() that will accept the arguments
row_index. Indexing 2-D lists in Python could be done in this way:
a, in our case, is going to be the column index, and
b is going to be the row index. We want the eight adjacent neighbors, and we can keep them in a list:
A question that might be lingering in your mind is: What about the edge cases? If there is a cell on the left edge of the grid, it won’t have 3 neighbors to the left. That’s right. Remember, John Conway, basically the creator of the GOL, assumed an infinite grid, a luxury we don’t posses in the physical world. What most people do, and what I chose to do eventually, is wrap the grid around itself. That is, for example, a cell in the left edge has it’s 3 left neighbors on the right edge of the screen. This seems hard to do, but it’s really not. We’ll just use the modulo operator:
This is just like when you add times on a 12-hours clock. For example, 6 + 7, according to a clock, is 1. What’s happening here is actually (6 + 7) (mod 12) = 1. The way the modulo operator works is by dividing the 2 numbers and returning the remainder. So, if there is a 3-by-3 grid, and a cell is at the (2, 2) — the last row and last column — the neighbors that were supposed to be downwards are going to be at row (2 + 1) (mod 3) = 0 ((mod 3) since 3 is
self.rows in this specific example).
With that aside, what we actually want is to return the states of these neighboring cells, since that is the important part. This is done like this:
return [cell.state for cell in neighbors]
get_neighbors() method then looks like this:
It might be easier to test this implementation if we already have some initial configurations to choose from instead of manually inputting the initial configuration into the grid. That’s why I created some methods that allowed the user to, with a press of a button, initialize the grid to either a Gosper glider gun, a glider, a pulsar, or a line configuration. You can Google what these are to get an idea of what I’m saying.
I’ll be bringing up one example, and then just make available the code for the rest of the configurations since they are based on the same principle.
I’ve implemented a method called
kill_cells() which just sets all the cells in the grid to be dead. I’m sure you could implement it yourself.
Anyways we want to make sure we are not mixing our configuration with a previously filled grid, so, just to make sure, we kill all the cells.
We then find out the center of the screen, so that the final shape appears in the center.
We then, in a
try clause, begin filling out the grid manually. The
except clause is just a way to handle errors. We need to make sure the grid is big enough to contain the shape we want, and so if it is not (meaning if we receive an
IndexError) we just want to
IndexError that says
Glider needs to have a bigger grid.
Anyways, inside the
try clause is the gist of how we accomplish this. This is how a glider looks like:
In this particular case,
current_column is going to be 5, and
current_row is going to be 5 too. So column and row 5 is where the center of the shape is going to be at.
So, we make the cell on the left of it alive (
True) in line 6. We make the cell, which lies on the same row, but on the next column also
True on line 7. We make the cell lying in the same column, but under it, alive too (remember that adding to the y value is actually going down. One of the initially unintuitive things of frameworks and libraries like Processing). And so on, and so forth.
The idea is simple: manually make cells alive by accessing them on the grid. Where you place them is based on the
Here is the rest of the code:
This might seem complicated, but the basic principle is the same (I just utilized
for loops to condense the code).
Implementation — Part 2
Alright, we’ve created the
Cell classes and their methods, let’s put them to use.
Here was my plan:
- Create a grid, called
- Create another empty grid, called
writing_grid. (By empty, I mean a grid where all cells are initialized to being dead.)
- Based on
reading_grid, fill in
reading_gridof the next generation.
reading_gridon the screen.
- Erase the elements of
Here’s a quick crash course on how Processing works: You have 2 functions called
setup() is only called once in the program, and so it’s the place where you put the stuff that only needs to be done once at the beginning of the program (like declaring the screen’s dimensions, etc.).
draw(), on the other hand, is called about 60 times per second by default, I believe. It’s the
mainloop() of the program. So that’s where the majority of our code will go to.
First, we initialize the
writing_grid (Step 1):
reading_grid = Grid(cell_width, *DIM)
writing_grid = Grid(cell_width, *DIM)
DIM are variables declared at the first lines of the finished code.
cell_width is self-explanatory, and
DIM is the dimensions of our screen. It is a tuple, and so the asterisk
* is there to unpack the values of a tuple (basically,
x = (a, b); print(*x) is going to print out
a, b). Don’t bother with it a lot. Anyways, these values could be anything you want; I chose for
cell_width to be 20, and
DIM to be
We are not “creating”, or filling in,
writing_grid because we have no need to do that now. As it will become clear later, by default, the grid that is going to appear on the screen is
reading_grid, so it must be created first.
We now want to declare our screen’s size, something that could be done only in the
size(*DIM) # unpack the values of DIM
We now enter the main part of the program, the
draw() function. Let me show the code and then explain what it is doing.
In line 2, we make
writing_grid global variables so we can modify them throughout the function (This is how scopes work in Python).
running is also one of those variables declared in the very first lines of the program. It is by default
If the program is running (line 10), then we create
writing_grid in the sense that all the cells are initialized to be dead (line 11, Step 2). We then loop over each cell in the 2 grids simultaneously (line 12 to 13), one from
reading_grid, which we’ll call
read_cell, and one from
writing_grid, which we’ll call
write_cell. Now remember, based on
read_cell and its neighbors, we are going to change the value of
write_cell (Step 3).
Remember that we created a method
get_neighbors(self, column_index, row_index). From line 14 to 15, we are getting the values of
row_index. Remember that
reading_grid.grid is a list of columns, so finding the index is basically finding what column we are in (hence,
column_index = reading_grid.grid.index(read_column)). Finding the value of
row_index is basically the index of the cell in the column itself (hence,
row_index = read_column.index(read_cell)). Now, we pass those values to the method, and assign the value returned in a variable called
neighbors_state. Remember, we want the neighbors of
The block of code from line 17 to 24 is just applying the rules over here. Remember, if a cell’s state is
True, that means it’s alive. Counting the numbers of
Trues from the
neighbors_state list will give us the number of alive and dead neighbors of
read_cell, and will therefore allow us to modify the corresponding state of
After this evaluation is done, I was very inclined to just show
writing_grid in the end, but that’s a huge mistake. What happens when
draw() gets called again? Nothing’s going to change, because we haven’t updated
reading_grid and we are going to see the same shape on the screen again. This was a problem that I faced in the beginning. What we should do is implement Step 4 in the algorithm I wrote before. This is done in line 26. Erasing
reading_grid (line 25) is actually unnecessary (a conclusion I reached while writing this article), and so it could be ignored. We are then showing the new, updated
reading_grid (line 27) and then we are erasing
writing_grid. We are doing so in order to create a new, “blank”,
draw() is called again in order to implement the algorithm I wrote up.
The last 2 lines in the
draw() function are giving instructions to just show
reading_grid when the program is not running. This part will get clearer later on.
We are almost done. We just need to handle some basic key and mouse events. Remember how the program is initially not running and is showing
read_grid. We could utilize that and change the cell’s state as we wish. We can click on a cell, and its state should change. Then, after we change how many cells’ states, we can change the variable
True by clicking on a button. Let’s implement that first.
Processing comes with very neat ways for handling events. One of these ways is by defining the function
mouseClicked(), which is called every time the mouse is clicked. This could be used to change the cells’ state:
k are referring to the column and row, respectively.
mouseY are global variables that contain the x and y coordinates of the mouse. Dividing them by the cells’ width will give us the column and row index of the cell the mouse clicked. We then change the cell’s state.
While this seems fine, there is nothing preventing us from changing the cell’s state while we are running the program, so we need to add some protection:
This ensures that nothing’s happening while we are running the program.
How do we even change
running, though? We change it through a key event. If we press a certain key, then
running's value should change. I chose to give the Enter key that property. We can use
keyPressed(), a function similar to
if key == ENTER:
running = not running
We need to establish that
running is a global variable, hence the second line. Alright, what about if we want to clear the screen and initialize all of the cells to
False? We can add another
if-statement and use the
if key == ENTER:
running = not running
if not running:
if key == "c":
Of course, we want to clear the screen when we are not running the game.
One last thing I added was the ability to save frames to have an image sequence in order to create a video output later. If you want to know more on how this works, watch this video. The final
keyPressed() function is going to look something like this:
What about the ability of the user to choose from different shapes instead of manually inputting it? Well, that’s quite easy to do:
Different numbers relate to different shapes.
And we’re done! Congratulations if you made it to here! Here is the final code, just to make it easier to visualize where each code snippet goes.
Some of you who might already tried this code out noticed that the program started to become slower once there were too many cells in the grid. That’s because of the way this thing works. We are constantly looping over every cell. Say we have a grid of 20 cells-by-20 cells. We are looping over 400 cells. And that’s just to show the grid, and not to actually do the evaluation and updating processes.
Maybe if we were able to create a sort of boundary box, from where the computation happens, and expand the box so that it always encompasses the area where the grid is getting updated, the program might be a little bit quicker. Now, if that seems like a lot of work, it’s because it is a lot of work.
Another suggestion I have is not having the grid to wrap around. Wrapping the grid around itself might cause some annoying behavior. For example, when we have the Gosper glider gun as our initial configuration, the expected behavior is that gliders are going to be kept reproducing. But because of the nature of the grid, the glider that passes below the grid will return upwards, and thus will directly hit the generator, and no more gliders are produced.
I have an unguaranteed solution to this: Every cell on the edge of the screen dies in the next generation. Giving the effect of something disappearing downwards. I’m not quite sure of this, but it’s something that’s worth considering.
The (not really) last improvement is stepping. That is, the stepping between different generations. There needs to be a “clock” or something that keeps the game running at a consistent, steady, and hardware-independent speed. What I mean by hardware-independent is the program not having to be quicker or slower based on machine performance, but rather on how much we specify. 1 generation per second or 10 generation per minute for example are some examples of steady stepping.
Still, there are many things to improve, many things to fix, and many things to do. The program’s between your hands, and your only limit is your imagination (and your determination)!