Implementing the Game of Life

Ahmed Addous
15 min readJul 23, 2021

--

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:

class Cell:
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:

Of course, state is 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.col and self.row with a width of self.width_.

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:

class Grid:
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.

So the __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 columns.

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 (self.grid):

for column in self.grid:

and then over each cell in the column:

for column in self.grid:
for cell in column:

and then show() the cell:

for column in self.grid:
for cell in column:
cell.show()

Remember the show() method we created for the Cell class.

Here is the show() method for the Grid class:

For some reason while writing the code, I decided to use the name `col` instead of `column`.

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: self, 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 column_index and row_index. Indexing 2-D lists in Python could be done in this way: lst[a][b], 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]

The whole get_neighbors() method then looks like this:

Default Configurations

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.

The Glider:

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 try-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 raise an 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 current_column and current_row values.

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 Grid and Cell classes and their methods, let’s put them to use.

Here was my plan:

  1. Create a grid, called reading_grid.
  2. Create another empty grid, called writing_grid. (By empty, I mean a grid where all cells are initialized to being dead.)
  3. Based on reading_grid, fill in writing_grid.
  4. Make writing_grid the reading_grid of the next generation.
  5. Show reading_grid on the screen.
  6. Erase the elements of writing_grid.

Here’s a quick crash course on how Processing works: You have 2 functions called setup() and draw(). 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 reading_grid and writing_grid (Step 1):

reading_grid = Grid(cell_width, *DIM)
writing_grid = Grid(cell_width, *DIM)
reading_grid.create()

cell_width and 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 (600, 400).

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 setup() function.

def setup():
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 reading_grid and 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 False.

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 column_index and 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 read_cell.

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 write_cell.

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”, writing_grid when 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 running to 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:

Here, j, and k are referring to the column and row, respectively. mouseX and 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 mouseClicked().

def keyPressed():
global running
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 kill_cells() method:

def keyPressed():
global running
if key == ENTER:
running = not running
if not running:
if key == "c":
reading_grid.kill_cells()

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.

Improvements

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)!

--

--

Ahmed Addous
0 Followers

Interested in programming. I write some articles from time to time.