Game of Life in Python

PasiduPerera
Level Up Coding
Published in
8 min readDec 26, 2020

--

If you don’t know, the game of life is a simulation for population.

The wikipedia page explains the very basic rules of the Game of Life and how you decide the states of a tile.

This video also gives a visual explanation if you’re a visual learner.

Rules:

A living state is black and a dead state is white. We

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  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 overpopulation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

If you look at the code, you might realise that it using very similar code to the chess grid, and this is because a lot of this program is a copy of the chess with new logic. So I copied most of the grid set-up (I might have accidentally left a new comments from the chess code too).

Full Code

Firtst I’ll share the full code and then explain it below.

import pygame
import sys
WIDTH = 800
ROWS = 20
WIN = pygame.display.set_mode((WIDTH, WIDTH))
pygame.display.set_caption("Game of Life")WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
class Node:
def __init__(self, row, col, width):
self.row = row
self.col = colself.x = int(row * width)self.y = int(col * width)self.colour = WHITEself.occupied = Nonedef draw(self, WIN):
pygame.draw.rect(WIN, self.colour, (self.x, self.y, WIDTH / 8, WIDTH / 8))
def make_grid(rows, width):
grid = []
gap = WIDTH // rowsprint(gap)for i in range(rows):grid.append([])for j in range(rows):
node = Node(j, i, gap)
grid[i].append(node)return griddef draw_grid(win, rows, width):
gap = width // ROWS
for i in range(rows):pygame.draw.line(win, BLACK, (0, i * gap), (width, i * gap))for j in range(rows):
pygame.draw.line(win, BLACK, (j * gap, 0), (j * gap, width))
"""The nodes are all white so this we need to draw the grey lines that separate all the chess tilesfrom each other and that is what this function does"""def update_display(win, grid, rows, width):
for row in grid:
for spot in row:
spot.draw(win)
draw_grid(win, rows, width)pygame.display.update()def Find_Node(pos, WIDTH):
interval = WIDTH / ROWS
y, x = posrows = y // intervalcolumns = x // intervalreturn int(rows), int(columns)def neighbour(tile):
col, row = tile.row, tile.col
# print(row, col)neighbours = [[row - 1, col - 1], [row - 1, col], [row - 1, col + 1],
[row, col - 1], [row, col + 1],
[row + 1, col - 1], [row + 1, col], [row + 1, col + 1], ]
actual = []for i in neighbours:
row, col = i
if 0 <= row <= (ROWS - 1) and 0 <= col <= (ROWS - 1):
actual.append(i)
# print(row, col, actual)
return actual
def update_grid(grid):
newgrid = []
for row in grid:
for tile in row:
neighbours = neighbour(tile)
count = 0
for i in neighbours:
row, col = i
if grid[row][col].colour == BLACK:
count += 1
if tile.colour == BLACK:
if count == 2 or count == 3:
newgrid.append(BLACK)
else:
newgrid.append(WHITE)
else:
if count == 3:
newgrid.append(BLACK)
else:
newgrid.append(WHITE)
return newgriddef main(WIN, WIDTH):
run = None
grid = make_grid(ROWS, WIDTH)
while True:
pygame.time.delay(50) ##stops cpu dying
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
run = True
if event.type == pygame.MOUSEBUTTONDOWN:
pos = pygame.mouse.get_pos()
row, col = Find_Node(pos, WIDTH)
if grid[col][row].colour == WHITE:
grid[col][row].colour = BLACK
elif grid[col][row].colour == BLACK:
grid[col][row].colour = WHITE
while run:
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONDOWN:
run = False
#pygame.time.delay(50)
newcolours = update_grid(grid)
count=0
for i in range(0,len(grid[0])):
for j in range(0, len(grid[0])):
grid[i][j].colour=newcolours[count]
count+=1
update_display(WIN, grid, ROWS, WIDTH)
#run= False
update_display(WIN, grid, ROWS, WIDTH)main(WIN, WIDTH)

Code Explained:

import pygame
import sys

WIDTH = 800
ROWS = 20
WIN = pygame.display.set_mode((WIDTH, WIDTH))

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

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)


class Node:
def __init__(self, row, col, width):
self.row = row

self.col = col

self.x = int(row * width)

self.y = int(col * width)

self.colour = WHITE

self.occupied = None

def draw(self, WIN):
pygame.draw.rect(WIN, self.colour, (self.x, self.y, WIDTH / 8, WIDTH / 8))


def make_grid(rows, width):
grid = []

gap = WIDTH // rows

print(gap)

for i in range(rows):

grid.append([])

for j in range(rows):
node = Node(j, i, gap)

grid[i].append(node)

return grid


def draw_grid(win, rows, width):
gap = width // ROWS

for i in range(rows):

pygame.draw.line(win, BLACK, (0, i * gap), (width, i * gap))

for j in range(rows):
pygame.draw.line(win, BLACK, (j * gap, 0), (j * gap, width))

"""

The nodes are all white so this we need to draw the grey lines that separate all the chess tiles

from each other and that is what this function does"""


def update_display(win, grid, rows, width):
for row in grid:

for spot in row:
spot.draw(win)

draw_grid(win, rows, width)

pygame.display.update()


def Find_Node(pos, WIDTH):
interval = WIDTH / ROWS

y, x = pos

rows = y // interval

columns = x // interval

return int(rows), int(columns)

All of this code is a copy from the chess program I made, and I basically just creates the tiles of the grid using a class which I have called a Node. If you want an explanation of the grid, I would recommend going and looking at my code for chess.

  • Draw - A method that I use to draw the nodes onto the window, note- these won’t have the black borders around them so we need to add these separately.
  • make_grid is called at the beginning of the program and it’s going to create the initial 2d array full of the node elements which is going to represent the grids. The nested loops are going to automatically set the positions of the nodes on the screen in regular intervals
  • draw_grid- as I said earlier, when we draw squares onto the screen, they don’t immediately have a border when they are white eg. if we drew a white square onto the screen, its border is also going to be white and since the background colour of our window is automatically white, this means it would blend into the background and also into other nodes of the same colour. This function will therefore draw the black lines that separate all of the nodes from each other.
  • update_display- for each tick, we need to update the screen with any changes that have been made so we use this function at the end of the while True loop to update the screen. We do this by calling the draw methods again and if any changes have been made(we change the node colours when a change has happened) this will appear on the screen.
  • Find_Node is used because if we click on the screen, the co-ordinate of the house is going to be in a 800 x 800 range(since it’s the coordinate on the window) and we want a index to the array grid which is in a smaller range. This is where we use mod(%) to convert into a grid index which corresponds to the node the mouse was in when we clicked the screen.
def neighbour(tile):
col, row = tile.row, tile.col

# print(row, col)

neighbours = [[row - 1, col - 1], [row - 1, col], [row - 1, col + 1],
[row, col - 1], [row, col + 1],
[row + 1, col - 1], [row + 1, col], [row + 1, col + 1], ]

actual = []

for i in neighbours:
row, col = i

if 0 <= row <= (ROWS - 1) and 0 <= col <= (ROWS - 1):
actual.append(i)
# print(row, col, actual)
return actual


def update_grid(grid):
newgrid = []
for row in grid:
for tile in row:
neighbours = neighbour(tile)
count = 0
for i in neighbours:
row, col = i
if grid[row][col].colour == BLACK:
count += 1

if tile.colour == BLACK:
if count == 2 or count == 3:
newgrid.append(BLACK)
else:
newgrid.append(WHITE)

else:
if count == 3:
newgrid.append(BLACK)
else:
newgrid.append(WHITE)

return newgrid


def main(WIN, WIDTH):
run = None
grid = make_grid(ROWS, WIDTH)

while True:
pygame.time.delay(50) ##stops cpu dying
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
run = True

if event.type == pygame.MOUSEBUTTONDOWN:
pos = pygame.mouse.get_pos()
row, col = Find_Node(pos, WIDTH)
if grid[col][row].colour == WHITE:
grid[col][row].colour = BLACK

elif grid[col][row].colour == BLACK:
grid[col][row].colour = WHITE

while run:
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONDOWN:
run = False

#pygame.time.delay(50)
newcolours = update_grid(grid)
count=0
for i in range(0,len(grid[0])):
for j in range(0, len(grid[0])):
grid[i][j].colour=newcolours[count]
count+=1
update_display(WIN, grid, ROWS, WIDTH)
#run= False

update_display(WIN, grid, ROWS, WIDTH)


main(WIN, WIDTH)

The following code is the new code that I had to write for this to work. Also I do want to credit Edwin again here, because I made some pretty stupid logic errors in the code where I was indexing the 2D arrays using [column][row] and I couldn’t debug where the logic error was and Edwin was the one who found the logic error for me and fixed the code.

Neighbour:

This function is very similar to the logic we put in the chess pieces in the chess program, but now we are just taking the position of the node we are in and then checking each of its neighbours and if they are in the range of the grid then they are placed in a 2D array which we return

Note: This is a very inefficient way of figuring out the nodes that we want to switch to alive/dead, a more efficiency way fo doing this is going to add the alive nodes on the screen and incrementing a counter on all the nodes around it and then working out whether the node should be alive or dead. This way still works but your CPU won’t be enjoying very much.

Update_grid:

This function is going to figure out which tiles are going to become alive and dead for the next window update. You may be wondering why I didn’t immediately implement the colour changes directly onto the tiles and instead put it into an array, I did this because if you immediately update the values, the this new value will affect the results of the next checks on the tiles next to it so we need to calculate all the nodes before implementing the change. If you know anything about Linear Regression and more particularly Gradient Descent inside of it, this is why we update all the parameters after we’ve done all the calculations involving the gradient descent equation. The if statements are also where we implement the rules that we displayed earlier, this is where we decide whether the tile is going to be dead/alive in the next iteration of the program.

Main:

The main loop is what runs when the program starts. We start the game by letting the user click the space button and when they do, it switches the state of run to True which starts an infinite loop where the calculation functions we made are run. Before that, we let the user be able to click on any tile and then we would colour it black(to be alive) and if they click a live tile it would turn it back to dead. This allows the user to customise whatever starting arrangement they would want and also helped me and Edwin validate that the logic in the program was all actually working correctly.

That is the Game of Life complete! I remember looking at this program probably only a month ago and thinking it was a very hard program completely out of my reach but all of the code was completed within 2 hours which astounds me(although this does involve the logic error which I never found(edwin did)).

--

--