##### Reading Time: 5mins, First Published: Sat, Oct 20, 2018

In this post we will develop a Python implementation Conway’s Game of Life, set in a donut shaped universe!

The post will utilise numpy, matplotlib’s animation features, and Scipy’s 2D convolution tool kit. It also provides a nice demonstration of what can be achieved with just a few lines of Python!

There are a series of 5 longer more colourful video examples at the end of the post

### About the game of life

The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970.

The game is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input. One interacts with the Game of Life by creating an initial configuration and observing how it evolves, or, for advanced players, by creating patterns with particular properties.

### Import Libraries

from IPython.display import HTML
from matplotlib.animation import FuncAnimation
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib import animation
import numpy as np
import pandas as pd
from scipy.signal import convolve2d

%matplotlib inline


## Conway’s Game of Life

### Basic Rules

From Wikipedia:

1. Any live cell with fewer than two live neighbors dies, as if by underpopulation.
2. Any live cell with two or three live neighbors lives on to the next generation.
3. Any live cell with more than three live neighbors dies, as if by overpopulation.
4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

A single cell has a total of 8 neighbouring cells i.e. left, right, top, bottom + diagonals.

### Edges of the universe

#### Torus

When calculating the number of neighbours we have a few options when we reach the edge of the universe, the most simplistic implementation is that the game “universe” has distinct edges. A most sophisticated solution is to wrap the universe edges in a circular fashion. Represented in 3D space this shape would be toroid specifically a torus.

We can pass the “wrap” option to scipy’s 2D convolution tool to facilitate this. In 3 dimensions this generates a Torus.

### My Basic Methodology/ Thinking:

• Represent the “board” in our game using a numpy array, which can be constructed using numpy’s meshgrid function.
• Represent the status “live” or “dead” of each “square” on the board as True and False.
• Instantiate the board at random using numpy’s random() function and specifying an initial probability of life (1 - all cells alive, 0 - no live cells).
• Plot the board using matplotlibs scatter plot
• Update the board using matplotlibs FuncAnimation tool, by passing an update function
• The update function will:
1. apply conways rules based on the current system state:
• Calculate the number of neighbours using convolution - very fast
• Apply Conway’s rules to generate the new state
2. Update the status
3. Update the color and transparency of the scatter markers:
• Translucent - alive
• Coloured squares/cells depending on the the number of neighbours

## Game Code

def play_game(size=(50, 50), initial_prob_life=0.5, animation_kwargs=None, marker_size=300):
"""Implementation of Conway's Game of Life

Parameters
----------

size: tuple
Defines the size of the board (n by m)

initial_prob_life: float
Float in the range (0, 1] defines the initial probability of a square containing life at initialisation

animation_kwargs: dict
Dictionary of keyword arguments to be passed to FuncAnimation

marker_size:
Size of scatter plot markers

Returns
-------
Instance of FuncAnimation

"""
board = get_board(size)
status = get_init_status(size, initial_prob_life)

fig, axes = plt.subplots(figsize=(15, 15))
scatter = axes.scatter(*board, animated=True, s=marker_size, edgecolor=None)
axes.text(0.5, 0.965, "CONWAY'S ", transform=axes.transAxes, ha="right", va="bottom", color="w", fontsize=25,
family="sans-serif", fontweight="light")
axes.text(0.5, 0.965, "GAME OF LIFE", transform=axes.transAxes, ha="left", va="bottom", color="dodgerblue",
fontsize=25, family="sans-serif", fontweight="bold")
axes.set_facecolor("black")
axes.get_xaxis().set_visible(False)
axes.get_yaxis().set_visible(False)

def update(frame):
nonlocal status
status, live_neigbors = apply_conways_game_of_life_rules(status)
colors = get_updated_colors(status, live_neigbors)
scatter.set_facecolor(colors)
return scatter

animation_kwargs = {} if animation_kwargs is None else animation_kwargs
return FuncAnimation(fig, update, **animation_kwargs)

def get_board(size):
xs = np.arange(0, size[0])
ys = np.arange(0, size[1])
board = np.meshgrid(xs, ys)
return board

def get_init_status(size, initial_prob_life):
status = np.random.uniform(0, 1, size=size) <= initial_prob_life
return status

def apply_conways_game_of_life_rules(status):
"""Applies Conway's Game of Life rules given the current status of the game

Rules:
1. Any live cell with fewer than two live neighbors dies, as if by underpopulation.
2. Any live cell with two or three live neighbors lives on to the next generation.
3. Any live cell with more than three live neighbors dies, as if by overpopulation.
4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

Returns
-------
new_status, new_neighbors: tuple
A tuple containing an array representing the new status of each square on the board, and a second array
representing the number of neighbors post update

"""
live_neighbors = count_live_neighbors(status)
survive_underpopulation = live_neighbors >= 2
survive_overpopulation = live_neighbors <= 3
survive = status * survive_underpopulation * survive_overpopulation
new_status = np.where(live_neighbors==3, True, survive)  # Reproduce
new_neighbors = count_live_neighbors(new_status)
return new_status, new_neighbors

def count_live_neighbors(status):
"""Counts the number of neighboring live cells"""
kernel = np.array(
[[1, 1, 1],
[1, 0, 1],
[1, 1, 1]]
)
c = convolve2d(status, kernel, mode='same', boundary="wrap")
return c

def get_updated_colors(status, c):
cmap = mpl.cm.Blues_r
rescale = c / 8  # Maximum of 8 neighbors
colors = [cmap(neighbors) for neighbors in rescale.flatten()]
is_live = status.flatten()
colors = [(r, g, b, 0.9) if live else (r, g, b, 0) for live, (r, g, b, a) in zip(is_live, colors)]
return colors


## Run Game

for i in range(5):
ani = play_game(
size=(100, 100),
marker_size=100,
initial_prob_life=0.5,
animation_kwargs={
"frames": 300,
}
)

ani.save(f'game_of_life_{i}.mp4',
writer=animation.FFMpegFileWriter())


#### Example 5

You can view the video in a Jupyter notebook using the following code.

# HTML(ani.to_html5_video))


I ran into a few problems when trying to save larger files using certain writers like imagemagick and fmmpeg. I found a post on Stack Overflow that mentioned using animation.FFMpegFileWriter() which does seem to work.

ani.save('longer.gif', writer='pillow')


### Conclusion

I hope you enjoyed the post. In a few lines of code we built a complex visualisation, and simulation.

This project highlights some of the powerful features of Python’s core scientific programming libraries: matrix calculations, convolution, and animated visualisations are handled with ease.

There’s still scope for expanding this project, for instance we could try seeding some interesting patterns into game, but that will have to wait for now.