Conway’s Game of Life is a zero-player game introduced by the mathematician John Horton Conway in 1970. Well, It has it’s own wiki and all.

The rules are simple. You start with a grid of cells with initial state of either living or dead. Cells interact with neighbors to define the next generation of cells.

  • Any live cell with fewer than two live neighbors dies, as if by under-population.
  • 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

Implementation Link to heading

It’s using the following packages

  • numpy and scipy for matrix operations
  • matplotlib for plotting and animation

Example image

Animation Link to heading

matplotlib is new to me, so it took me some time to understand the basic building blocks. but what i want to highlight below is FuncAnimation

    def generations(self):
        for g in range (self.G):
            yield self.step()


    def run(self):
        fig, ax = plt.subplots()
        mat = ax.matshow(self.grid)   
        ani = animation.FuncAnimation(fig, mat.set_data, self.generations, interval=500, repeat=False)
        plt.show()

docs says

fig: Figure The figure object that is used to get draw, resize, and any other needed events.

func: callable The function to call at each frame. The first argument will be the next value in frames. Any additional positional arguments can be > supplied via the fargs parameter.

frames: iterable, int, generator function, or None, optional If an iterable, then simply use the values provided. If the iterable has a length, it will override the save_count kwarg.

So, the second parameter is callable, in this case i am passing mat.set_data so, it will be called by matplolib to update frames. but the most important part is that third parameter is iterable or int or generator function. and this parameter will be passed to the callable in parameter 2.

def func(frame, *fargs) 

which effectively is doing the following considering the generator will run G generations before stopping.

def mat.set_data(this.grid)

gotcha Link to heading

well, the game is straightforward to write but there was something that bugged me little. Mainly, calculating the number of neighbors living cells.

I initially implemented it by manually counting ucelsls sing (i,j) index and it worked but it was ugly because of the boundary checks. Then I found convolve2d from scipy. It’s neat because using the right kernel, i can count the neighbors for all cells with one line. more details about convolution wiki

        W = np.array([[1, 1, 1],
              [1, 0, 1],
              [1, 1, 1]])
        self.neighbour = signal.convolve2d(self.grid, W, 'same')

putting it all together Link to heading

import numpy as np
from scipy import signal
import matplotlib.pyplot as plt 
import matplotlib.animation as animation

import argparse

class Conway():
    def __init__(self, N=10, G=1, shape='random'):
        self.N = N
        self.G = G
        self.grid = None
        self.neighbour = None

        if shape == 'random':
            self.grid = np.random.choice(a=[False, True], size=(N, N))
        else:
            self.grid = np.zeros((N, N), dtype='bool')

    def calc(self):
        W = np.array([[1, 1, 1],
              [1, 0, 1],
              [1, 1, 1]])
        self.neighbour = signal.convolve2d(self.grid, W, 'same')
            
    def step(self):
        self.calc()
        for i in range(self.N):
            for j in range(self.N):
                # Rule1: if dead cell have exactly 3 surrouding living cell, it become a live
                if (self.grid[i,j] ==  0 and self.neighbour[i,j] == 3):
                    self.grid[i,j] = 1
                # Rule2 and Rule3: if living cell has > 3 or < 2 surrounding, it dies
                if (self.grid[i,j] == 1 and(self.neighbour[i,j] < 2 or self.neighbour[i,j] > 3) ):
                    self.grid[i,j] = 0
                # livining cell has 2 or 3, it staying alive
        return self.grid

    def generations(self):
        for g in range (self.G):
            yield self.step()


    def run(self):
        fig, ax = plt.subplots()
        mat = ax.matshow(self.grid)   
        ani = animation.FuncAnimation(fig, mat.set_data, self.generations, interval=500, repeat=False)
        plt.show()

def main():
    parser = argparse.ArgumentParser(description='Conway game of life')

    parser.add_argument('N', type=int, help='Grid Size')
    parser.add_argument('G', type=int, help='Generations Count')

    args = parser.parse_args()

    g = Conway(args.N,args.G)
    g.run()
    

if __name__ == "__main__":
    main()