Game of Life and other cellular automatons#

# UNCOMMENT FOR INTERACTIVE PLOTTING
# %matplotlib notebook
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import animation, rc, cm
import IPython, io, urllib
import requests
import warnings

warnings.filterwarnings("ignore")

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(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.0, 1.0, N + 1)
X, Y = np.meshgrid(t, t)
f = 4
C0 = np.sin(2.0 * np.pi * f * X) * np.sin(2.0 * np.pi * 2 * f * Y) > -0.1
g = LLCA(C0, rule="B3S23")


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


fig, ax = plt.subplots(figsize=(8, 6))
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()

Alternative rule: Day and Night (B3678S34678)#

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

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


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


fig, ax = plt.subplots(figsize=(8, 6))
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()

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(figsize=(8, 6))
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()

Existing structures in GoL#

Over time, many structures have been discovered in GoL. For example, some of them are translating leaving debris in their wake. They are called puffers. Here is an example:

https://conwaylife.com/wiki/Hivenudger_2

def get_gol_structure_by_url(url):
    """
    Gets a GoL structure using plain text format on an url.
    """
    content = requests.get(url, stream=True).content.decode()
    out = []
    for line in content.split("\n"):
        line = line.strip()
        # print(line)
        if not line.startswith("!"):
            line_data = []
            for c in line:
                if c == ".":
                    line_data.append(0)
                if c == "O":
                    line_data.append(1)
            out.append(line_data)
    max_length = 0
    for line in out:
        if len(line) > max_length:
            max_length = len(line)
    for line in out:
        if len(line) < max_length:
            for i in range(max_length - len(line)):
                line.append(0)
    out = np.array(out)
    return out


url = "https://conwaylife.com/patterns/hivenudger2.cells"
inner_cells = get_gol_structure_by_url(url)
inner_shape = inner_cells.shape
hmargin = 5
lmargin = 50
rmargin = 5
cells = np.zeros((inner_shape[0] + hmargin * 2, inner_shape[1] + lmargin + rmargin))
cells[hmargin:-hmargin, lmargin:-rmargin] = inner_cells
g = LLCA(
    cells, 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(figsize=(8, 6))
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.tight_layout()
plt.close()
anim