Project 6 (part a):

The Game of Life

The Game of 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.

The universe of the Game of Life is an infinite two-dimensional array of square cells, each of which is in one of two possible states:

  • alive: cell has value = 1
  • dead: cell has value = 0

Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. See Figure 1.

Rules of the game:

At each step in time (also called "tick"), the following transitions occur to all cells simultaneously:

  1. Any live cell with fewer than two live neighbours dies, as if caused by underpopulation.

  2. Any live cell with two or three live neighbours lives on to the next generation.

  3. Any live cell with more than three live neighbours dies, as if by overpopulation.

  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

In [1]:
# Weekly functions (until I build a module to import) & imports
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML
plt.rcParams['figure.figsize'] = 6, 4
plt.rcParams["animation.html"] = "html5"

def GeneralListPlot(XList, YList, xlabel='X', ylabel='Y'):
    """A general function to plot 2 lists of floats, and label their axes"""
    plt.plot(XList, YList)
    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    plt.show()
    return()
In [2]:
class Game:
    
    def __init__ (self, file, S = [2,3], B = [3]): # Where S & B are survival and birth criteria
        self.B = B
        self.S = S
        self.world = np.loadtxt(file, dtype=np.int8)
        self.frame = 0 #Frame number/number of frames since start
        #Save the initial world, helps find the period
        self.initworld = self.world.copy()
        
    def __next__(self):
        """Generates the next frame of the game"""
        x_dim, y_dim = self.world.shape
        arr_next = np.zeros([x_dim, y_dim], dtype=np.int8)
        for i in range (x_dim):
            for j in range (y_dim):
                SurrCh = self.surrounds(i,j)
                if SurrCh in self.S and self.world[i,j] == 1:
                    arr_next[i,j] = self.world[i,j]
                elif SurrCh in self.B and self.world[i,j] == 0:
                    arr_next[i,j] = 1
                else:
                    arr_next[i,j] = 0 # <= to_unsigned(0,0);
        self.world = arr_next.reshape((x_dim, y_dim))
        self.frame += 1
        
    def living(self):
        """Returns the number of currently living cells in the world"""
        N = sum(self.world.flatten())
        return(N)
    
    def surrounds(self, x, y):
        """Checks the surroundings of a cell and returns the number of lives cells around it"""
        arr = []
        for i in range (-1, 2):
            for j in range (-1, 2):
                try:
                    arr.append(self.world[x+i,y+j])
                except IndexError:
                    arr.append(0)
        sum_ = sum(arr) - self.world[x,y]
        return(sum_)
        
    def livlocs(self):
        """Returns the locations of all the living cells in the world"""
        x, y = np.where(self.world == 1)
        return(x, y)
    
    def BCentre(self, DEBUG = 0):
        """Finds the Barycentre of the living cells of the board"""
        x, y = self.livlocs()
        xcent = sum(x)/self.living()
        ycent = sum(y)/self.living()
        if DEBUG == 1:
            print(xcent, ycent)
        return(xcent, ycent)
    
    def r_max(self, DEBUG = 0):
        """Returns the r_max of the current world"""
        centi,centj = self.BCentre()
        xs, ys = self.livlocs()
        r_max_ = 0
        for i in range (len(xs)):
            r_ij = np.sqrt(((xs[i] - centi)**2) + ((ys[i] - centj)**2))
            if DEBUG == 1:
                print('r_ij: ',r_ij)
            if r_ij > r_max_:
                r_max_ = r_ij
        return(r_max_)
        
    def reset(self):
        """Resets the world to it's initial state"""
        self.world = self.initworld
        self.frame = 0

1: Study of still lifes and oscillators: [3 marks]

For the following shapes from the data folder:

  • Loaf ("data/Loaf.txt")
  • Pulsar ("data/Pulsar.txt")
  • Pentadecathlon ("data/Pentadecathlon.txt")

Find the number of live cells and the rmax as a function of time (number of steps).

Can you determine the period of the above 3 shapes from these plots?
Hint: One of the shapes does not change at all, the other 2 return back to their original configuration after certain steps.

rmax is defined as:

  • Given N live cells at positions (i,j)
  • Calculate the population "barycenter" (centeri,centerj), where centeri=iNiN and centerj=jNjN
  • Define the distance : ri,j=(icenteri)2+(jcenterj)2
  • rmax is the maximum of all the ri,j i.e the distance of the furthest cell from the "barycenter" of the population.
In [3]:
# Pulling all the data files at once
loaf = Game('data/Loaf.txt')
pulsar = Game('data/Pulsar.txt')
pentadeca = Game('data/Pentadecathlon.txt')
glider = Game('data/Glider.txt')
glider_gun = Game('data/GliderGun.txt')
blinker = Game('data/Blinker.txt')
repl = Game('data/Replicator.txt')
rpant = Game('data/R-Pantomino.txt')
pond = Game('data/Pond.txt')
In [4]:
def part1run(world,nframes = 100):
    """Returns lists of r_max, number of living cells, and frame count of the world as the game progresses"""
    world.reset()
    rlist = []
    livlist = []
    framelist = []
    rlist.append(world.r_max())
    livlist.append(world.living())
    framelist.append(world.frame)
    for i in range(nframes):
        next(world)
        rlist.append(world.r_max())
        livlist.append(world.living())
        framelist.append(world.frame)
    return(rlist, livlist, framelist)
    
rlistLOAF, livlistLOAF, framelistLOAF = part1run(loaf, 10)
rlistPULS, livlistPULS, framelistPULS = part1run(pulsar, 9)
rlistPENTD, livlistPENTD, framelistPENTD = part1run(pentadeca, 30)

#Plotting the required graphs
print('Loaf: ')
GeneralListPlot(framelistLOAF, rlistLOAF, 'Time', 'r_max')
GeneralListPlot(framelistLOAF, livlistLOAF, 'Time', 'Number of Living Cells')
print('Pulsar: ')
GeneralListPlot(framelistPULS, rlistPULS, 'Time', 'r_max')
GeneralListPlot(framelistPULS, livlistPULS, 'Time', 'Number of Living Cells')
print('Pentadecathlon: ')
GeneralListPlot(framelistPENTD, rlistPENTD, 'Time', 'r_max')
GeneralListPlot(framelistPENTD, livlistPENTD, 'Time', 'Number of Living Cells')


print('Loaf is unchanging/No period')
print('Pulsar period = 3')
print('Pentadecathlon period = 15')
Loaf: 
Out[4]:
Out[4]:
Pulsar: 
Out[4]:
Out[4]:
Pentadecathlon: 
Out[4]:
Out[4]:
Loaf is unchanging/No period
Pulsar period = 3
Pentadecathlon period = 15

2: Simple animations : [3 marks]

For the following shapes from the data folder:

  • Blinker ("data/Blinker.txt")
  • Pulsar ("data/Pulsar.txt")
  • Pentadecathlon ("data/Pentadecathlon.txt")

Produce animations for each one.

Hint : Use the results of the previous exercise in order to determine the minimum number of frames where applicable (typically you will not need more the 20 frames or so).

In [5]:
# Modified versions of the example code to produes an animation

def generate(game): # Using 'game' because world.world just looks wrong and could cause issues
    next(game)
    newworld = game.world
    return(newworld)

def animatefunction(world, inputframes = 100, inputinterval = 100):
    world.reset()
    fig = plt.figure()
    img = plt.imshow(generate(world), animated = True)
    
    def animate(frame):         # Why do we put this in here and not outside?
        img.set_data(generate(world))
        return (img,)
       
    plt.close()
    anim=animation.FuncAnimation(fig, func=animate, frames=inputframes, interval=inputinterval,blit=True)
    display(anim)
    pass

# Animations for the 3 asked for
animatefunction(blinker, inputframes = 2) # Number of frames just from observing the animation
animatefunction(pulsar, inputframes = 4) # Also from observing the animation
animatefunction(pentadeca, inputframes = 15) # From previous exercise
Out[5]:
Out[5]:
WARNING: Some output was deleted.

3: Replicator : [4 marks]

Conway's Life is classified as following the B3/S23 rule.

Specifically a cell:

  • is Born if it has exactly 3 neighbours
  • Survives if it has 2 or 3 living neighbours
  • it dies otherwise.

Another member in the family of "Life" cellular automata is HighLife that follow the B36/S23 rule.

A cell:

  • is Born if it has 3 or 6 neighbours
  • Survives if it has 2 or 3 living neighbours
  • it dies otherwise.

Following the HighLife rules and employing the Replicator pattern present in the data folder ("data/Replicator.txt").

  1. Evolve it for 36 generations.
  2. Produce an animation of the above 36 frames showing the system evolution
  3. Plot the Number of cells versus time
  4. Plot the rmax as a function of time
  5. Can you see why it is called a "Replicator"?
In [6]:
HighLifeRepl = Game('data/Replicator.txt', S = [2,3], B = [3,6]) # The magic of our class allows us to freely change the game rules

rlistHLR, livlistHLR, framelistHLR = part1run(HighLifeRepl, nframes = 36)

animatefunction(HighLifeRepl, inputframes = 36)

print('HighLife: ')
GeneralListPlot(framelistHLR, rlistHLR, 'Time', 'r_max')
GeneralListPlot(framelistHLR, livlistHLR, 'Time', 'Number of Living Cells')

# Duplicates (or replicates) the starting shape repeatedly over time
WARNING: Some output was deleted.
In [0]: