In 1970, British mathematician John Horton Conway published the rules to a quite intriguing game called “The Game of Life”. The game consists of a grid of black and white cells. At the start of the game, a human player determines which of the cells are alive (= black) and which are dead (= white). In each subsequent round of the game, based on two very simple rules, new cells are born and some of the old cells die. This creates some of the most fascinating patterns, without the human player having to make any further moves or decisions. The game now has a life of its own! In this tutorial, you will learn how to program this game in Python using pygame.

The rules of the game

The game really only has two rules:

Our program

In our program we would like the user to be able to do the following:

Let’s get started

We’ll import three modules: datasclasses to create our cell-class, copy to make deep copies and pygame to create the game interface.

# load modules
from dataclasses import dataclass
import copy
import pygame as pg
## pygame 1.9.6
## Hello from the pygame community. https://www.pygame.org/contribute.html

Next, we need to set a couple of parameters and import two images. Later, when we launch the game a new window will open in which the game will be played in. Therefore, we must determine how large that window is going to be (in terms of the number of pixels) and how many cells we would like to fit into our grid. We must also load two image files. We do that using the commands pg.image.load() One is a black square the other is a white square. These .png files will be used to display the cells in our game (you can find both images on my GitHub page). Note that we must also scale these images to the proper resolution using the command pg.transform.scale().

# set parameter values
res  = 800 # pixels
size =  40 # no. of squares per row/col

# load and scale .png-files
black = pg.image.load("img_black.png")
white = pg.image.load("img_white.png")
scale = res // size
black = pg.transform.scale(black,(scale-1,scale-1))
white = pg.transform.scale(white,(scale-1,scale-1))

We now define a class called Cell. In Conway’s Game of Life, each cell is positioned in a grid. Thus, each cell has a row position and column position. A cell is either alive or dead and each cell has a certain number of neighbors (depending on whether the cell is located at the edge board of the board or somewhere in the center). At most, each cell has eight direct neighbors (bottom left, bottom, bottom right, left, right, top left, top, and top right). Our Cell class will obviously need a method to count the number of neighbors that are alive. Otherwise, we would know whether the cell comes to life, stays alive or dies in the next round. We will call this method countNeighbors() and base it on a list of all possible neighbors indicated by the number of row and column steps necessary to get to them. We call this list “neighbors”. All cells will be stored in a list, which will simply refer to as “matrix”. The Cell class itself than has three integer variables “row”, “col” and “n_neighbors”, and a Boolean called “is_alive”.

The Cell class’s countNeighbors() method first assumes that a cell’s neighbors are all dead (n = 0). It then loops through each of the cell’s neighbors and adds +1 to “n” in case the neighbor is alive. An upstream if-statement makes sure that we only consider neighbors that actually exist. This is only relevant in case a cell is located at the edge of the board. Here, some steps in the “neighbors” list would throw you of the board.

We complete the “cell” class with a second method called show(). This method will be used to display the cell in the game window. If the cell is alive it will display the black .png image, if the cell is dead, it will display the white .png image. To display an image in the game window (or “screen”) we use the method blit().

# Cell class
neighbors = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
@dataclass
class Cell():
    
    row : int
    col : int
    is_alive : bool = False
    n_neighbors : int = 0
    
    def countNeighbors(self):
        n = 0
        for pos in neighbors:
            row = self.row + pos[0]
            col = self.col + pos[1]
            if 0 <= row < size and 0 <= col < size:
                cell = matrix[row*size+col]
                if cell.is_alive:
                    n += 1
        self.n_neighbors = n

    def show(self):
        pos = (self.row*scale, self.col*scale)
        if self.is_alive:
            screen.blit(black,pos)
        else:
            screen.blit(white,pos)

Next, we create the aforementioned “matrix” list in which we store all cells. We create a counter called “game_round” to track the current round of the game. Lastly, we crate an empty list called “previous_rounds” in which we still the matrices of previous rounds. This is necessary for the user to be able to go back and forth between different rounds of the game.

# create matrix/grid
matrix = [Cell(i,j) for i in range(size) for j in range(size)]

# empty list in which to store matrix of each round
game_round = 0
previous_rounds = []

Step by step towards the game program

Our code to run the game will eventually be a while loop. But before writing out the code in full, let me explain the individual parts of that while loop. Above you will have noticed the “screen” object. This is window that will pop up in which the game will then be played in. We set up this game screen using:

# set up game window
pg.init()
screen = pg.display.set_mode([res,res])

Next we need to add all the cells onto this empty screen. We do so using our custom-built python show() method and the display.flip() method that comes with python pygame.

# show screen
for cell in matrix:
    cell.show()
pg.display.flip()

Now comes a never-ending while loop that runs the game. This while loop only ends if the user closes the game window. Thus, we will have to check wether the user does anything. We do so using a loop over any possible “event” in pygame’s event.get() method. The first thing we check is whether the user wants to exit the game by closing the game window. In that case the “event type” will be equal to “QUIT”. In this case, we execute pygame’s quit() function.

# user closes game
if event.type == pg.QUIT:
    pg.quit()
    break

The next kind of event we will look out for is a click on one of the mouse buttons. In that case the “event type” will be equal to “MOUSEBUTTONDOWN”. We must first determine where the cursor is on the screen and then code what happens when the user presses the left or the right mouse button. To this end we use the two methods mouse.get_pos() and mouse.get_pressed().

# user clicks mouse
if event.type == pg.MOUSEBUTTONDOWN:
    mouseX, mouseY = pg.mouse.get_pos()
    row = mouseX // scale
    col = mouseY // scale
    i = row*size+col
    cell = matrix[i]
    
    # left click to spawn cell
    if pg.mouse.get_pressed()[0]:
        cell.is_alive = True
        
    # right click to kill cell
    if pg.mouse.get_pressed()[2]:
        cell.is_alive = False

You might now be wondering what mouse.get_pressed()[1] would do. This piece of code would refer to the center button or wheel of the mouse, which we don’t need for our game.

Besides clicking the mouse buttons, the user can also press keys on the keyboard. In that case the “event type” will be equal to “KEYDOWN”. As we said earlier, we would like our user to be able to use the left and right arrow keys to advance to the next round or go back one round. The “events” of pressing the left or right arrow key are called “K_LEFT” and “K_RIGHT”. When the user presses the right arrow key, we store the current game matrix in the matrices list, advance the game round counter by one, and, based on recounting the neighbors, determine which cells die and which cells are born. When the user presses the left arrow key, we reduce the round counter by one and remove go back to the last matrix in the matrices list. Lastly, if the user presses the “r” key, we would like to restart the game. We do so by killing all cells.

# user hits keys
if event.type == pg.KEYDOWN:
    
    # "right arrow" key for next round
    if event.key == pg.K_RIGHT:
        
        # store current matrix in matrices list
        game_round += 1 
        previous_rounds.append(copy.deepcopy(matrix))
        
        # recount neighbors
        for cell in matrix:
            cell.countNeighbors()
                
        # spawn/kill new cells
        for cell in matrix:
            if cell.is_alive == True and cell.n_neighbors != 2:
                cell.is_alive = False
            if cell.is_alive == False and cell.n_neighbors == 3:
                cell.is_alive = True

    # "left arrow" key for previous round
    if event.key == pg.K_LEFT:
        if game_round > 0:
            matrix = previous_rounds[-1]
            del previous_rounds[-1]
            game_round -= 1

    # "r" for restart
    if event.key == pg.K_r:
        for cell in matrix:
            cell.is_alive = False

The full while loop

Alright, we are now ready to write out the game loop in full. The following code chunk runs the game:

# set up game window
pg.init()
screen = pg.display.set_mode([res,res])

# run game
while True:
 
    # show screen
    for cell in matrix:
        cell.show()
    pg.display.flip()
       
    # check user action
    for event in pg.event.get():
                
        # user closes game
        if event.type == pg.QUIT:
            pg.quit()
            break
        
        # user clicks mouse
        if event.type == pg.MOUSEBUTTONDOWN:
            mouseX, mouseY = pg.mouse.get_pos()
            row = mouseX // scale
            col = mouseY // scale
            i = row*size+col
            cell = matrix[i]
            
            # left click to spawn cell
            if pg.mouse.get_pressed()[0]:
                cell.is_alive = True
                
            # right click to kill cell
            if pg.mouse.get_pressed()[2]:
                cell.is_alive = False
                
        # user hits keys
        if event.type == pg.KEYDOWN:
            
            # "right arrow" key for next round
            if event.key == pg.K_RIGHT:
                
                # store current matrix in matrices list
                game_round += 1 
                previous_rounds.append(copy.deepcopy(matrix))
                
                # recount neighbors
                for cell in matrix:
                    cell.countNeighbors()
                        
                # spawn/kill new cells
                for cell in matrix:
                    if cell.is_alive == True and cell.n_neighbors != 2:
                        cell.is_alive = False
                    if cell.is_alive == False and cell.n_neighbors == 3:
                        cell.is_alive = True

            # "left arrow" key for previous round
            if event.key == pg.K_LEFT:
                if game_round > 0:
                    matrix = previous_rounds[-1]
                    del previous_rounds[-1]
                    game_round -= 1

            # "r" for restart
            if event.key == pg.K_r:
                for cell in matrix:
                    cell.is_alive = False