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:
- Any live cell with fewer than two live neighbors dies, as if by underpopulation.
- Any live cell with two or three live neighbors lives on to the next generation.
- Any live cell with more than three live neighbors dies, as if by overpopulation.
- 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:
- 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
- Update the status
- Update the color and transparency of the scatter markers:
- Transparent - dead
- Translucent - alive
- Coloured squares/cells depending on the the number of neighbours
- apply conways rules based on the current system state:
Further Reading:
- Conway’s Game of Life: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
- Matplotlib animate: https://matplotlib.org/api/_as_gen/matplotlib.animation.FuncAnimation.html#matplotlib.animation.FuncAnimation
- Convolution: https://machinelearninguru.com/computer_vision/basics/convolution/image_convolution_1.html
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 1
Example 2
Example 3
Example 4
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.