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:
Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. See Figure 1.
At each step in time (also called "tick"), the following transitions occur to all cells simultaneously:
Any live cell with fewer than two live neighbours dies, as if caused by underpopulation.
Any live cell with two or three live neighbours lives on to the next generation.
Any live cell with more than three live neighbours dies, as if by overpopulation.
Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
# 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()
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
still lifes and oscillators: [3 marks]¶For the following shapes from the data folder:
Find the number of live cells and the 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.
is defined as:
# 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')
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')
For the following shapes from the data folder:
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).
# 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
Conway's Life is classified as following the 3/23 rule.
Specifically a cell:
Another member in the family of "Life" cellular automata is HighLife that follow the 36/23 rule.
A cell:
Following the HighLife rules and employing the Replicator pattern present in the data folder ("data/Replicator.txt").
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