Reading Time: 5mins, First Published: Sat, Oct 20, 2018
Game-of-Life
Matplotlib
Data Science
Python


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.

torus

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

simpsons

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:
      • Transparent - dead
      • Translucent - alive
        • Coloured squares/cells depending on the the number of neighbours

Further Reading:


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.