Note

This notebook can be downloaded here: GameOfLife.ipynb

Code author: Ludovic Charleux <ludovic.charleux@univ-smb.fr>

Game of Life and other cellular automatons

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib nbagg
from matplotlib import animation, rc, cm
import IPython, io, urllib
rc('animation', html='html5')

Introduction

This notebook was inspired by the great video proposed by David Louapre available on his Youtube channel “Science Etonnante”:

IPython.display.YouTubeVideo('S-W0NX97DB0')

The Game of Life (GoL) is a good way to learn about the use of object oriented programming, numpy and matplotlib. It is also a very interresting scientific and mathematical problem. The GoL belongs to the wider group of problems called Cellular Automatons. A lot of alternative sets of interresting rules have been created/discovered over time since the invention of the GoL. In this tutorial, we introduce a simple class that can solve all those problems called Life Like Cellular Automatons (LLCA).

Further readings:

A generic class to simulate LLCAs

class LLCA:
    """
    A Life Like Cellular Automaton (LLCA)

    Inputs:
    * C: a binary matrix representing the cells where 1 stands for alive and 0 for dead.
    * rule: the rule of the in the format 'BXSY' where X and Y are the birth and survival conditions.
            Example: GOL rule is "B3S23".
    """
    def __init__(self, C = np.random.rand(50, 50), rule = "B3S23"):
        self.C = np.array(C).astype(np.bool)
        self.rule = rule

    def parse_rule(self):
        """
        Parses the rule string
        """
        r = self.rule.upper().split("S")
        B = np.array([int(i) for i in r[0][1:] ]).astype(np.int64)
        S = np.array([int(i) for i in r[1] ]).astype(np.int64)
        return B, S

    def neighbors(self):
        """
        Returns the number of living neigbors of each cell.
        """
        C = self.C
        N = np.zeros(C.shape, dtype = np.int8) # Neighbors matrix
        N[ :-1,  :  ]  += C[1:  , :  ] # Living cells south
        N[ :  ,  :-1]  += C[ :  ,1:  ] # Living cells east
        N[1:  ,  :  ]  += C[ :-1, :  ] # Living cells north
        N[ :  , 1:  ]  += C[ :  , :-1] # Living cells west
        N[ :-1,  :-1]  += C[1:  ,1:  ] # Living cells south east
        N[1:  ,  :-1]  += C[ :-1,1:  ] # Living cells north east
        N[1:  , 1:  ]  += C[ :-1, :-1] # Living cells north west
        N[ :-1, 1:  ]  += C[1:  , :-1] # Living cells south west
        return N

    def iterate(self):
        """
        Iterates one time.
        """
        B, S = self.parse_rule()
        N = self.neighbors()
        C = self.C
        C1 = np.zeros(C.shape, dtype = np.int8)
        for b in B: C1 += ((C == False) & (N == b))
        for s in S: C1 += (C & (N == s))
        self.C[:] = C1 > 0

The orginal Game of Life (rule B3S23)

# INITIAL CONFIGURATION
N = 100
t = np.linspace(0., 1., N+1)
X, Y = np.meshgrid(t, t)
f = 4
C0 = np.sin(2. * np.pi * f * X ) * np.sin(2. * np.pi * 2 * f * Y )  > -.1
g = LLCA(C0, rule = "B3S23")

# ANIMATION
def updatefig(*args):
    g.iterate()
    im.set_array(g.C)
    return im,

fig, ax = plt.subplots()
ax.axis('off')
im = plt.imshow(g.C, interpolation = "nearest", cmap = cm.binary, animated=True)
anim = animation.FuncAnimation(fig, updatefig, frames=200, interval=50, blit=True)

plt.close()
anim
#plt.show()
<IPython.core.display.Javascript object>

Alternative rule: Day and Night (B3678S34678)

N = 100
t = np.linspace(0., 1., N+1)
X, Y = np.meshgrid(t, t)
f = 10
C0 = np.sin(2. * np.pi * f * X ) * np.sin(2. * np.pi * 2 * f * Y )  > 0.

g = LLCA(C0, rule = "B3678S34678")

def updatefig(*args):
    g.iterate()
    im.set_array(g.C)
    return im,


fig, ax = plt.subplots()
ax.axis('off')
im = plt.imshow(g.C, interpolation = "nearest", cmap = cm.binary, animated=True)
anim = animation.FuncAnimation(fig, updatefig, frames=200, interval=50, blit=True)
plt.close()
anim
#plt.show()
<IPython.core.display.Javascript object>

Alternative rule: fractal-like B1S123

N = 200
C0 = np.zeros((N, N))
C0[1,1] = 1

g = LLCA(C0, rule = "B1S123")

def updatefig(*args):
    g.iterate()
    im.set_array(g.C)
    return im,


fig, ax = plt.subplots()
ax.axis('off')
im = plt.imshow(g.C, interpolation = "nearest", cmap = cm.binary, animated=True)
anim = animation.FuncAnimation(fig, updatefig, frames=200, interval=40, blit=True)
plt.close()
anim
#plt.show()
<IPython.core.display.Javascript object>

Existing structures in GoL

Let’s simulate an existing puffer:

http://www.conwaylife.com/patterns/hivenudger2_106.lif

def life_parser(path, bottom_margin = 10, top_margin = 10, left_margin = 10, right_margin = 10):
    """
    A life 1.06 file parser

    http://www.conwaylife.com/wiki/Life_1.06
    """
    data = "".join([l for l in open(path).readlines() if not l.startswith("#")])
    data = pd.read_csv(io.StringIO(data), header = None, sep = " ").values
    xmin, xmax = data[:,0].min(), data[:,0].max()
    ymin, ymax = data[:,1].min(), data[:,1].max()
    C0 = np.zeros( (xmax - xmin + top_margin + bottom_margin,
                    ymax - ymin + left_margin + right_margin) )
    data[:,0] += -xmin + top_margin
    data[:,1] += -ymin + left_margin
    C0[data[:,0] , data[:,1]] = 1
    return C0

path = "_data/hivenudger2_106.lif"
C0 = life_parser(path, top_margin = 100).T[:, ::-1]
fig, ax = plt.subplots()
plt.imshow(C0, cmap = cm.binary)
ax.axis('off')
plt.show()
<IPython.core.display.Javascript object>
g = LLCA(C0, rule = "B3S23") # B2S23 means Birth if 2 living neighbours and survival if 2 or 3 living neighbours

def updatefig(*args):
    g.iterate()
    im.set_array(g.C)
    return im,

fig, ax = plt.subplots()
im = plt.imshow(g.C, interpolation = "nearest", cmap = cm.binary, animated=True)
anim = animation.FuncAnimation(fig, updatefig, frames=200, interval=50, blit=True)
#ax.axis('off')
plt.close()
anim
#plt.show()
<IPython.core.display.Javascript object>