Monday, 22 July 2013

Python - Game of Life

While looking through a few programming forums I kept noticing people with a slightly odd looking avatar.



I thought that for the same emblem to keep cropping up it must have some meaning, but I had no idea what that was.

It took a few attempts at googling to find the symbols meaning. Trying to describe a symbol to google is not easy! I found that the symbol was derived from Conway's Game of Life. The symbol represents a Glider which is sometimes created during the Game of Life. The reason people use it as an avatar is because the symbol has been adopted by the hacking community in a similar way that Linux has Tux as its emblem.



Reading about the Game of Life on Wikipedia I became quite interested in the concept.

The Game of Life is in short a simulation of a group of cells. At every step in time, often referred to in the game as a tick, cells live or die depending on their surroundings.

There are four rules which are applied simultaneously to all the cells in the game.

1. Any live cell with fewer than two live neighbours dies, as if caused by under-population.
2. Any live cell with two or three live neighbours lives on to the next generation.
3. Any live cell with more than three live neighbours dies, as if by overcrowding.
4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

However I made my mind up that it must be too difficult to program, so I filed it away as one of those things to try when I am a better programmer.

For various reasons I was drawn towards giving pygame a whirl. I had played around with it in the past and felt it might potentially offer a nicer way to display certain things than Tkinter, which is my default for any GUI work in Python. I felt there was some potential there other than for writing games.

I learnt to program in Python using the free book 'Invent Your Own Computer Games with Python' by Al Sweigart. I cannot rate this book highly enough. If you want to learn Python, use this book. It is that good.

I knew he had also written a book aimed at teaching the basics of pygame. After my great experience with his first book, I thought I could not go wrong with his book called 'Making Games with Python and Pygame'. (These two books plus his latest book 'Hacking Secret Ciphers with Python' can be found on the website http://inventwithpython.com/)

I skimmed through the first few chapters of this book and knowing I learn better by doing, I thought I would crack on and write my own game. But what? Having seen that the first of the games in the pygame book revolved around a grid system, I thought I could always try and write a version of the Game of Life. I still had a feeling it would be too difficult, but I was up for the challenge.

After starting and finishing the program much quicker than anticipated, and once again astounding myself with how easy things are to do in Python, I thought it would make a great blog post.

Pygame comes as standard on the Raspberry Pi however if you are using a different platform for programming then you can download it from here:

http://www.pygame.org/news.html

This tutorial will lead you through my thought process for developing and creating this game. There will be a link to the code at the end for those who want to download it. :-)

I am going to assume you know how to start programming in Python. If not I have a previous blog which should get you up to speed.

http://trevorappleton.blogspot.co.uk/2013/04/basic-python-programming-word-checker.html

I am not going to go into all the details of how pygame works. Al Sweigart does a much better job of this that I ever could and having only started with pygame recently, I feel I would only be copying his good work. His book is definitely worth reading.

Throughout this blog I will show you what to type, and then show you a snippet of code showing the line you need to type and some preceding lines. This should avoid any confusion about where you should be typing the code, and will show you the required level of indentation for each line.

I am going to run through the process of producing the Game of Life in 4 stages.

Stage 1 - Creating a blank pygame screen.
Stage 2 - Creating a blank grid on the pygame screen.
Stage 3 - Creating random coloured cells on the screen.
Stage 4 - Working Game of Life.

A link to the source code is available at the end of each stage.

Stage 1 - Creating a blank pygame screen. 


The first thing I needed to achieve was to get a blank screen up and running. Luckily Al Sweigart provided the code to do this, so I did not have to go re-inventing the wheel.

Type in the following and then press F5 to save and run the code.

import pygame, sys
from pygame.locals import *

pygame.init()
DISPLAYSURF = pygame.display.set_mode((400,300)) 
pygame.display.set_caption('Hello World') 

while True: #main game loop
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
    pygame.display.update()
There are three main points to know about this code.

1. DISPLAYSURF = pygame.display.set_mode((400,300))
This sets the size of the window. Run the code with different numbers and see what happens. Don't forget these numbers are within two sets of brackets!

2. pygame.display.set_caption('Hello World')
This sets the title of the Window. Change 'Hello World' to 'Game of Life' and see what happens.

3. while True: #main game loop
As the comment suggests this is the main game loop. Everything that needs repeating within our game will need to be contained within this loop.

Now press F5 to save and run your program. Is this what you see?



That was fairly simple wasn't it? Ok it doesn't do much but it gives us great foundations for building upon.

Blank Screen Source Code

Stage 2 - Creating a blank grid on the pygame screen


My next step was to see how to create and display a grid on the screen.

The first thing I did was to create global variables for the width and height of the window. I thought I may like to increase or decrease the size of the window during testing and running the Game of Life.

WINDOWWIDTH = 640
WINDOWHEIGHT = 480

import pygame, sys
from pygame.locals import *

WINDOWWIDTH = 640
WINDOWHEIGHT = 480


We will now have to change the window size so it uses the values from WINDOWWIDTH and WINDOWHEIGHT rather than the values we had previously typed in there.

Therefore change the line:

DISPLAYSURF = pygame.display.set_mode((400,300))

to

DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH,WINDOWHEIGHT))


Having these as global variables means you can change the values at one location in your code, and not all the way through.

I also wanted to ensure I could change the size of the cells. Again this allowed me to have larger or smaller populations within the window so I created another global variable.

CELLSIZE = 10

WINDOWWIDTH = 640
WINDOWHEIGHT = 480
CELLSIZE = 10

The next two lines make use of the assert command.

assert WINDOWWIDTH % CELLSIZE == 0, "Window width must be a multiple of cell size"
assert WINDOWHEIGHT % CELLSIZE == 0, "Window height must be a multiple of cell size"


CELLSIZE = 10
assert WINDOWWIDTH % CELLSIZE == 0, "Window width must be a multiple of cell size"
assert WINDOWHEIGHT % CELLSIZE == 0, "Window height must be a multiple of cell size"


The idea behind using the assert command is to catch any bugs you may have as early as possible. The assert command carries out a calculation in a controlled location, and if it fails will let you know. It is easier to catch a bug if you go looking for it rather than it surprising you later on. We are using the assert command to check if there are an exact number of cell sizes within the width and height of the window.

How do we check if there is an exact number of cells able to fit into the window we have specified? We need to be able to check if the the size of the window divided by the size of the cells gives us an exact number. To do this we can use the mod (%) function. This divides two numbers and reports back the remainders.

An example should explain it.

We know how dividing works.

15 / 4 = 3.75

The mod function on the other hand reports back any remainders.

15 % 4 = 3

Shall we just look into that last equation again? 4 goes into 15 three whole times  (3 x 4 = 12) with a remainder of 3.

20 % 3 = 2

Three goes into 20 six times (3 x 6 = 18) therefore the remainder is 2.

If one number goes into another number an exact number of times, then the mod would be 0.

Hopefully that all makes sense!

The next two lines determine how many cells there are in the x direction (CELLWIDTH) and in the y direction (CELLHEIGHT). This will be useful to us later on.

CELLWIDTH = WINDOWWIDTH / CELLSIZE # number of cells wide
CELLHEIGHT = WINDOWHEIGHT / CELLSIZE # Number of cells high


assert WINDOWWIDTH % CELLSIZE == 0, "Window width must be a multiple of cell size"
assert WINDOWHEIGHT % CELLSIZE == 0, "Window height must be a multiple of cell size"
CELLWIDTH = WINDOWWIDTH / CELLSIZE # number of cells wide
CELLHEIGHT = WINDOWHEIGHT / CELLSIZE # Number of cells high


The last global variables we will specify for now define some colours. Lets define a black, a white and a dark gray for now. These colours are defined using RGB, the amount of Red, Green and Blue in the mix to make up the colour.

# set up the colours
BLACK =    (0,  0,  0)
WHITE =    (255,255,255)
DARKGRAY = (40, 40, 40)


CELLWIDTH = WINDOWWIDTH / CELLSIZE # number of cells wide
CELLHEIGHT = WINDOWHEIGHT / CELLSIZE # Number of cells high

# set up the colours
BLACK =    (0,  0,  0)
WHITE =    (255,255,255)
DARKGRAY = (40, 40, 40)

Lets go and write the main part of the software now. We will modify the code we wrote during stage one to create the blank screen. After typing def main(): indent the remainder of the blank screen code as shown to place the code within the main function.

def main():
    pygame.init()
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH,WINDOWHEIGHT))
    pygame.display.set_caption('Game of Life')
    
    while True: #main game loop
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
        pygame.display.update()

We will have to add global DISPLAYSURF into the main function, so add this below the pygame.init() line.

global DISPLAYSURF

    pygame.init()
    global DISPLAYSURF
    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH,WINDOWHEIGHT))


Lets now make the background of the screen white in colour.

DISPLAYSURF.fill(WHITE)


    pygame.display.set_caption('Game of Life')

    DISPLAYSURF.fill(WHITE) #fills the screen white


Before we get into the main loop, lets draw our grid. We will write the function later to draw this, so lets just call that function for now.

drawGrid()


    DISPLAYSURF.fill(WHITE) #fills the screen white

    drawGrid()


Then we shall update the display to show the new grid.

pygame.display.update()


    drawGrid()
    pygame.display.update()


I have drawn the grid and updated the screen before the main game loop, so the screen is set up before we get into the main part of the game. However we will want to do the same thing for each generation, or tick, as it is known in the Game of Life. So lets also add drawGrid() into the while loop.

drawGrid()


                sys.exit()
        drawGrid()
        pygame.display.update()


Now after the main function we have just written, lets write some code to call main function, otherwise the Game of Life will never start.

if __name__=='__main__':
    main()


        pygame.display.update()
        
if __name__=='__main__':
    main()

All that is left is for us to write that function we called earlier to draw the grid! We will write this function directly after where we set the global variables for colours.

As always, the first thing we will do is to define the function name.

def drawGrid():


DARKGRAY = (40, 40, 40)
    
#Draws the grid lines
def drawGrid():


Now we need to draw lines to map out each of the different cells. We have put some work in upfront which tells us the size of the window, the cell size and the number of cells. We know from our assert functions there are the correct number of cells for the size of the screen, so we can concentrate on drawing the grid.

We will want to draw vertical lines across the width of the screen separated by the width of the size of each cell.

How will we do this?

Well if you type the following into the IDLE command line:

for x in range (100):
    print x

We see the numbers from 1 - 100

Now try:

for x in range (20,30):
  print x

We see the numbers 20 to 29 inclusive.

Now try:

for x in range (20,30,2):
  print x

We see the numbers 20,22,24,26,28

So the 20 defines the starting number, the 30 the finishing number, and the 2 the size of the step we should take.

We know the start of our window will be 0, we know the end will be WINDOWWIDTH and we know the spacing we should use is CELLSIZE. So we can use the range command to determine where we should draw our grid lines.

for x in range(0, WINDOWWIDTH, CELLSIZE): # draw vertical lines

we can then use a pygame function to draw out grid lines at each value of x

pygame.draw.line(DISPLAYSURF, DARKGRAY, (x,0),(x,WINDOWHEIGHT))


def drawGrid():
    for x in range(0, WINDOWWIDTH, CELLSIZE): # draw vertical lines
        pygame.draw.line(DISPLAYSURF, DARKGRAY, (x,0),(x,WINDOWHEIGHT))


Lets just look at the code within this line.

DISPLAYSURF is required to draw the line.
DARKGRAY uses our global variable to set the colour of the line.
The (x,0) are the co-ordinates mapping out the start of the line. We want it to start at the top of the screen which is y = 0. Remember x will increase by a CELLWIDTH amount each time.
(x,WINDOWHEIGHT) sets the end co-ordinate. We want it to finish at the bottom of the screen, which is the height of the window, which we defined as a global variable WINDOWHEIGHT earlier in our program. Again x will increase as we go through the range of values.

Lets run our program and see how it looks.

Do you get a white screen with vertical gray lines on it? Perfect.



Lets add some code to the drawGrid function for the horizontal lines.

for y in range (0, WINDOWHEIGHT, CELLSIZE): # draw horizontal lines
 
The range is the almost the same but we want to use WINDOWHEIGHT to determine the max value in the range.

Similarly for the draw.line function

pygame.draw.line(DISPLAYSURF, DARKGRAY, (0,y), (WINDOWWIDTH, y))


#Draws the grid lines
def drawGrid():
    for x in range(0, WINDOWWIDTH, CELLSIZE): # draw vertical lines
        pygame.draw.line(DISPLAYSURF, DARKGRAY, (x,0),(x,WINDOWHEIGHT))
    for y in range (0, WINDOWHEIGHT, CELLSIZE): # draw horizontal lines
        pygame.draw.line(DISPLAYSURF, DARKGRAY, (0,y), (WINDOWWIDTH, y)


Now we say that the y value should change each iteration through the range, and the x values of 0 and WINDOWWIDTH should stay constant.

Once again run your program. Does your screen now look like this? Perfect!




We have finished creating the basis of the Game of Life board.

Blank Grid Source Code

Stage 3 - Creating random coloured cells on the screen


Lets now try to fill some of the cells with life, and colour those that are alive a different colour. To begin with we will randomly assign life to some of the cells.

First we need a strategy for checking which cells are alive and which cells are dead. We can use a dictionary to keep track of the cells and if they are dead, we can label them with a 0, or if they are alive we can use a 1.

Let us create a function to create a dictionary of all the cells and to start with lets assign them all a 0. We can easily change the 0 to a 1 later on.

Below the drawGrid() function let us create a new function called blankGrid.

def blankGrid():


        pygame.draw.line(DISPLAYSURF, DARKGRAY, (0,y), (WINDOWWIDTH, y))

def blankGrid():


We have said we will store the information about the cell location, if it is dead or alive, in a dictionary.

Earlier in our program we stored global variables to keep track of the number of cells in the x direction (CELLWIDTH) and those in the y direction (CELLHEIGHT)

We can use the position of the cell in x and y as co-ordinates to give each one a unique number. That way we can easily know which cell we are talking about.

To do this we need two for loops. One dealing with x and the other with y.

Lets type the code and then analyse it

    gridDict = {}
    for y in range (CELLHEIGHT):
        for x in range (CELLWIDTH):
            gridDict[x,y] = 0


def blankGrid():
    gridDict = {}
    for y in range (CELLHEIGHT):
        for x in range (CELLWIDTH):
            gridDict[x,y] = 0


First we are creating a new dictionary, to allow us to store the data we are about to create. This is called gridDict.

Now we see two for loops, the second one indented in the first one.

The y loop says iterate through all the integers (whole numbers) in CELLHEIGHT. This will start at 0, then 1, then 2 all the way up to the number that is stored in CELLHEIGHT. However before moving from one number to the second number, we run through the second for loop. This deals with all the numbers in CELLWIDTH.

These co-ordinates are stored in our new dictionary and assigned the number 0.

So if CELLHEIGHT is 3 and CELLWIDTH is 2 we would see

y = 0 and x = 0 placed into the dictionary
y = 0 and x = 1 placed into the dictionary
y = 0 and x = 2 placed into the dictionary

As we have moved through all the values in x, the value of y increases

y = 1 and x = 0 placed into the dictionary
y = 1 and x = 1 placed into the dictionary
y = 1 and x = 2 placed into the dictionary

again y would increase

y = 2 and x = 0 placed into the dictionary
y = 2 and x = 1 placed into the dictionary
y = 2 and x = 2 placed into the dictionary

and so on until all the numbers have been used.

This technique is used a lot in programming so it is worth you spending a few minutes thinking about what is happening here if you are unsure.

Once we have been through all our cells and stored them with a 0 in the dictionary, we can return the dictionary.

return gridDict


            gridDict[x,y] = 0
    return gridDict


Now we call this function in the main part of our program.  Underneath DISPLAYSURF.fill(WHITE) place the result from the function blankGrid into lifeDict

lifeDict = blankGrid()


    DISPLAYSURF.fill(WHITE)

    lifeDict = blankGrid() # creates library and populates to match blank grid


Ok when we started this section we said we would assign random life to each cell. So far we have assigned a 0. We could have instead assigned a random number in the blankGrid function, but that would have limited our options for playing around with the Game of Life code later. So lets now call a second function to assign a 0 or 1 to our cells.

Lets call the function startingGridRandom

We want to modify our dictionary so we know which items are alive or dead. Therefore lets pass the dictionary into the funtion so we can modify it.

lifeDict = startingGridRandom(lifeDict)


    lifeDict = blankGrid()
    lifeDict = startingGridRandom(lifeDict) # Assign random life


Now somewhere above the main function lets create a new function to carry out this task.

def startingGridRandom(lifeDict):


    return gridDict

def startingGridRandom(lifeDict):


For each item in the dictionary we want to assign it a 0 or a 1. This is really easy. Again we are using a for loop to iterate through our dictionary. Once we have finished we return the dictionary.

    for item in lifeDict:
        lifeDict[item] = random.randint(0,1)
    return lifeDict


def startingGridRandom(lifeDict):
    for item in lifeDict:
        lifeDict[item] = random.randint(0,1)
    return lifeDict


lifeDict[item] = random.randint(0,1) assigns randomly either a 0 or a 1 to the current item in lifeDict. As we are in a for loop looping through all items we should assign a 0 or a 1 to all items in the dictionary.

random.randint uses the random libraries, which are not a standard part of python. We need to make sure we import these. So at the very top where we imported the pygame and sys libraries type.

import random


import pygame, sys
from pygame.locals import *
import random


Now we have a grid drawn, we have created a dictionary which maps out if all the cells are alive or dead and we have randomly assigned life to each of the squares. Next we have to visually represent life, and we can do that by colouring in the squares which are alive a different colour, and ensuring those which should have a dead cell are displayed as white.

Lets make our living cells green.

The first thing we need to do is to assign a GREEN global variable. We know the three numbers make up Red, Green and Blue, so green will need the numbers, (0, 255, 0)

GREEN = (0, 255, 0)

# set up the colors
BLACK =    (0,  0,  0)
WHITE =    (255,255,255)
DARKGRAY = (40, 40, 40)
GREEN =    (0,  255,0)


Now back into the main function.  Straight after we have assigned the random grid, lets call a function to set each cell the right colour.

    for item in lifeDict:
        colourGrid(item, lifeDict)



lifeDict = startingGridRandom(lifeDict) # Assign random life

    for item in lifeDict:
        colourGrid(item, lifeDict)


This calls the function colourGrid for each item. It passes the item through to the function, so we know which item we are dealing with, it also passes the dictionary to the function, which has all the information about the items being alive or dead.

We will now write the colourGrid function with all the other functions.

def colourGrid(item, lifeDict):


    return lifeDict

def colourGrid(item, lifeDict):


The first line names the function and tells it what information it expects to be passed into the function.

An item is made up of an x and a y component, lets seperate these out to make it easier to understand what we are referring to.

    x = item[0]
    y = item[1]


def colourGrid(item, lifeDict):
    x = item[0]
    y = item[1]


At the moment we are still refering to our cells as the number they are positioned relative to the x and y axis. But we know we have created a CELLSIZE variable to allow us to make the cells which are displayed graphically, larger or smaller. So when we go to colour in the cells we need to take into account the fact that the cells are a certain size. We do this by multiplying the cell number by CELLSIZE to determine where to start drawing.

    y = y * CELLSIZE # translates array into grid size
    x = x * CELLSIZE # translates array into grid size


    y = item[1]
    y = y * CELLSIZE # translates array into grid size
    x = x * CELLSIZE # translates array into grid size


Now we need to work out if the item should be green or white, depending if it is dead or alive. Starting with the dead cells.

if lifeDict[item] ==0:


    x = x * CELLSIZE # translates array into grid size
    if lifeDict[item] == 0:


Now we draw a rectangle in white using the co-ordinates and the size of the cell to determine the position of it.


pygame.draw.rect(DISPLAYSURF, WHITE, (x, y, CELLSIZE, CELLSIZE))

    if lifeDict[item] == 0:
        pygame.draw.rect(DISPLAYSURF, WHITE, (x, y, CELLSIZE, CELLSIZE))


Repeating the process for the cells which are alive is similar, but we want to colour these green.


    if lifeDict[item] == 1:
        pygame.draw.rect(DISPLAYSURF, GREEN, (x, y, CELLSIZE, CELLSIZE))

        pygame.draw.rect(DISPLAYSURF, WHITE, (x, y, CELLSIZE, CELLSIZE))
    if lifeDict[item] == 1:
        pygame.draw.rect(DISPLAYSURF, GREEN, (x, y, CELLSIZE, CELLSIZE))


Finally we return None.

return None


        pygame.draw.rect(DISPLAYSURF, GREEN, (x, y, CELLSIZE, CELLSIZE))
    return None

Pressing F5 to save and run the program you should see a screen which looks something like this.



Random Colours Source Code

Stage 4 - Working Game of Life


This takes us onto the final stage which is now letting the cells we have generated get on with the Game of Life.

We know there are 4 rules to the game of life. As a reminder these are:

1. Any live cell with fewer than two live neighbours dies, as if caused by under-population.
2. Any live cell with two or three live neighbours lives on to the next generation.
3. Any live cell with more than three live neighbours dies, as if by overcrowding.
4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

We can see that what happens to a cell is very much dependant upon how many neighbours it has. Therefore a function to count all the neighbours of a cell would be useful. Lets write that now.

We will create a function called getNeighbours. The function will need to know for which cell we want to get the neighbours, and the state of all the other cells. Therefore we will pass into the function the current cell and the dictionary of all cells.

Place this function with all the other functions.

def getNeighbours (item, lifeDict):

        pygame.draw.rect(DISPLAYSURF, GREEN, (x, y, CELLSIZE, CELLSIZE))
    return None

def getNeighbours(item,lifeDict):

The purpose of this function is to keep count of the number of neighbours which contain live cells, so lets create a variable to do that.

neighbours = 0


def getNeighbours(item,lifeDict):
    neighbours = 0


We know that for each cell there are 8 potential neighbours.


x - 1 | x = 0 | x + 1
y - 1 | y - 1 | y - 1
------+-------+------
x -  1 |   x   | x + 1
y = 0 |   y   | y = 0
------+-------+------
x -  1 | x = 0 | x + 1
y + 1 | y + 1 | y + 1

By using our technique of a for loop within a for loop we should be able to iterate through all these with very little code.

for x in range (-1, 2):
    for y in range (-1, 2):



    neighbours = 0
    for x in range (-1,2):
        for y in range (-1,2):


This will give us 9 results, as it will include x = 0 and y = 0 which is our origonal cell. We don't want to class that as a neighbour, so we will deal with that later.

We now want to use either the -1, 0 or + 1 for both x and y and if we add these onto the co-ordinates for the cell we are checking we should see all surrounding cells.

Lets make a variable to highlight which cell we are actually checking.

checkCell = (item[0]+x,item[1]+y)


        for y in range (-1,2):
            checkCell = (item[0]+x,item[1]+y)


Wait a minute! Is there a potential problem? What if we are checking a cell which is at the left side of our grid? We would not be able to check any values for x - 1 as these would be off the side of our window we have created. This could cause our program to crash.

Let us ignore any cells which are outside of the playing area, so we wont try to count any of these as neighbours.

We will use an if statement to check both the width and the height.

We know for the width our cells start at 0 on the left and finish at the value stored in CELLWIDTH on the right. Let us check to ensure our cells fall within that boundary.

if checkCell[0] < CELLWIDTH  and checkCell[0] >=0:


            checkCell = (item[0]+x,item[1]+y)
            if checkCell[0] < CELLWIDTH  and checkCell[0] >=0:


This asks if the cell is less than the CELLWIDTH, AND greater than 0

If the cell we are checking passes this criteria, lets now test it fits in the height of the grid.

if checkCell [1] < CELLHEIGHT and checkCell[1]>= 0:


            if checkCell[0] < CELLWIDTH  and checkCell[0] >=0:
                if checkCell [1] < CELLHEIGHT and checkCell[1]>= 0:


Any cell which has made it past both these if statements should be on our actual grid.

Now we can interogate the cell we are checking and see if it has life in it. The whole purpose of this was to count any life in the surrounding cells.

if lifeDict[checkCell] == 1:


                if checkCell [1] < CELLHEIGHT and checkCell[1]>= 0:
                    if lifeDict[checkCell] == 1:


So if there is a 1 in this field, we can say there is life, so can class that as a neighbour. Well not quite yet... As I mentioned earlier our two for loops are checking the eight neighbours and the central cell for neighbours. If we are looking at the central cell, then this is not a neighbouring cell it is our actual cell! So we should ignore this.

if x == 0 and y == 0: # negate the central cell
    neighbours += 0
else:
    neighbours += 1


                    if lifeDict[checkCell] == 1:
                        if x == 0 and y == 0:
                            neighbours += 0
                        else:
                            neighbours += 1


This will only count neighbours in the surrounding 8 cells and not in the actual cell itself.

Finally we return the number of neighbours.

return neighbours.


                            neighbours += 1
    return neighbours


We know on our grid where the alive cells are. We also have a method of determining the number of neighbours each cell has. All we need to do now is act on that information and determine what happens in the next generation, which is sometimes referred to as a tick in the Game of Life.

Lets write a function to determine what happens every tick. This function is the main crux of the program, but is very easy to write!

def tick (lifeDict):


    return neighbours

def tick(lifeDict):


Lets create a function called tick, and pass in the dictionary of all our life cells.

Our four rules need to apply to all the cells at once. To avoid us writing over current data, which would alter the results, lets create a temporary dictionary to store the information about the next generation or tick.

newTick = {}


def tick(lifeDict):
    newTick = {}


Now we should look at each cell within our population.

for item in lifeDict:


    newTick = {}
    for item in lifeDict:


We can determine the number of neighbours by calling the function we wrote earlier which returns the number of neighbours.

numberNeighbours = getNeighbours(item, lifeDict)


    for item in lifeDict:
        numberNeighbours = getNeighbours(item, lifeDict)


Three of the rules refer to the cells which are alive, which we have denoted as a 1. Lets deal with those first.

if lifeDict[item] == 1:


        numberNeighbours = getNeighbours(item, lifeDict)
        if lifeDict[item] == 1:


We know if the population is less than 2, then the cell will die due to under population. We do a simple test and store the result of a dead cell (a 0) into the temporary newTick dictionary.

if numberNeighbours < 2:
    newTick[item] = 0


        if lifeDict[item] == 1: # For those cells already alive
            if numberNeighbours < 2: # kill under-population
                newTick[item] = 0


If the number of neighbours is larger than 3 there is over crowding, again resulting in a dead cell.

elif numberNeighbours > 3:
     newTick[item] = 0



            if numberNeighbours < 2: # kill under-population
                newTick[item] = 0
            elif numberNeighbours > 3: #kill over-population
                newTick[item] = 0


We have dealt with less than 2 and greater than 3 neighbours. However not specified what happens to those cells with 2 or 3 neighbours.

Well these survive.

else:
    newTick[item] = 1 # keep status quo (life)



            elif numberNeighbours > 3: #kill over-population
                newTick[item] = 0
            else:
                newTick[item] = 1 # keep status quo (life)


The else part of the if statement covers all other options which will be either 2 or 3.

Now lets look at the possible options if the cell is currently dead.

elif lifeDict[item] == 0:


            else:
                newTick[item] = 1 # keep status quo (life)
        elif lifeDict[item] == 0:
     

It will become alive if there are exactly three neighbours which are alive.

if numberNeighbours == 3: # cell reproduces
newTick[item] = 1



        elif lifeDict[item] == 0:
            if numberNeighbours == 3: # cell reproduces
                newTick[item] = 1


Otherwise it will remain devoid of life....

else:
newTick[item] = 0 # keep status quo (death)



            if numberNeighbours == 3: # cell reproduces
                newTick[item] = 1
            else:
                newTick[item] = 0 # keep status quo (death)


Thats all the options dealt with. It should have been determined what the new state for every position on the newGrid should be, so lets now return the dictionary with this information.

return newTick


                newTick[item] = 0 # keep status quo (death)
    return newTick


So far in our main function we have been preparing the grid and getting it ready to play the game. But we have not placed anything within the continuous loop which allows the game to keep running. It is the creating of this new generation or tick which should be placed in that while loop.

lifeDict = tick(lifeDict)


    while True: #main game loop
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()

        #runs a tick
        lifeDict = tick(lifeDict)


We are replacing the information of our current generation with the information from the new generation we have determined.

Now our dictionary contains the information for the new generation, lets run through all the cells and ensure they are all coloured in correctly.

        for item in lifeDict:
            colourGrid(item, lifeDict)



        #runs a tick
        lifeDict = tick(lifeDict)

        #Colours the live cells, blanks the dead
        for item in lifeDict:
            colourGrid(item, lifeDict


We have done this before when colouring in our random starting grid, but this ensures it is done each generation.

Finally we need to specify how often we want the screen to update with our new information. We can set a value of FPS or Frames Per Second to do this.

So with your global variables type:

FPS = 10


import random

#Number of frames per second
FPS = 10


In our main function we need to specify the clock which will control the speed of the ticks.

FPSCLOCK = pygame.time.Clock()
 

    global DISPLAYSURF
    FPSCLOCK = pygame.time.Clock()


Now lets make sure our while loop runs a new tick referring to the FPS.

To do this at the bottom of your while loop add the following line

FPSCLOCK.tick(FPS)


        for item in lifeDict:
            colourGrid(item, lifeDict)

        drawGrid()
        pygame.display.update()    
        FPSCLOCK.tick(FPS)


If you want to slow or speed up the Game of Life you can do this by modifying the global FPS value. However it will only run as fast as your computer can cope with!

Run the game and you should see cells living and dying right in front of your eyes!

If you would like full source code for the game of life it can be found here.

Game of Life Source Code

I hope you have enjoyed this tutorial. When I said at the start of this that I felt the Game of Life was too difficult for me to write, I was wrong... If you break everything down into manageable chunks like I have shown here, there is nothing difficult about it.

For those of you interested I have create a second blog on this topic. This moves away from a random starting point shown here, and demonstrates some of the more interesting controlled starts. The link is below.

Game of Life - Alternative Starts