Sounds Fancy, because it is!
The inspiration
I was really bored at home, not motivated to make a website or a discord bot, so I was browsing youtube, when I rediscovered the Conway's game of life.
What is Conway's game of life though?
Read about it here on Wikipedia
Basically,
The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970.[1] It 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. It is Turing complete and can simulate a universal constructor or any other Turing machine.
This is sooo cool!!! I was thinking about it when it struck - Let's try to make a python implementation myself!
(I later realised that this so very very common - More than 40k repositories on github)
You can visit the repository here, Leave a ✨ if you liked it!
Getting started
First, I had to install a python package called PyLab (it's kinda like a subset of matplotlib, which is all I wanted)
That's what will be used to plot these beautiful looking graphs, very easily
The Rules for the Game of Life are:
- Any live cell with fewer than two live neighbours dies, as if caused by under-population/starvation.
- 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 overcrowding.
New cells are born if a dead cell has exactly three live neighbours.
Let's say, living cells are 1
and dead cells are 0
Let's make a class GameOfLife
class GameOfLife:
def __init__(
self, N: Union[int, None] = 100, T: int = 200
):
"""Set up Conway's Game of Life.
:param N: The size of the grid.
:param T: The number of generations to evolve."""
# Here we create two grids to hold the old and new configurations.
# This assumes an N*N grid of points.
# Each point is either alive or dead, represented by integer values of 1 and 0, respectively.
self.N = N
# highlight
self.old_grid = numpy.zeros(N * N, dtype="i").reshape(N, N)
self.new_grid = numpy.zeros(N * N, dtype="i").reshape(N, N)
# /highlight
I created two grids, one for the old configuration, and one for the new configuration (that will get generated)
It's just N * N shaped arrays with zeros
I need to generate a random pattern to be able to get started, so I used this method using the random
module (It probably isn't the best way, but works)
for i in range(0, self.N):
for j in range(0, self.N):
if random.randint(0, 100) < 15:
self.old_grid[i][j] = 1
else:
self.old_grid[i][j] = 0
So 15% of the points are alive, and the rest are dead.
The key element of the game is to find out the number of live neighbours for each point, so I just made a function to make it easier for myself
def live_neighbours(self, i, j):
"""Count the number of live neighbours around point (i, j)."""
s = 0 # The total number of live neighbours.
# Loop over all the neighbours.
for x in [i - 1, i, i + 1]:
for y in [j - 1, j, j + 1]:
if x == i and y == j:
continue # Skip the current point itself - we only want to count the neighbours!
if x != self.N and y != self.N:
s += self.old_grid[x][y]
# The remaining branches handle the case where the neighbour is off the end of the grid.
# In this case, we loop back round such that the grid becomes a "toroidal array".
elif x == self.N and y != self.N:
s += self.old_grid[0][y]
elif x != self.N and y == self.N:
s += self.old_grid[x][0]
else:
s += self.old_grid[0][0]
return s
Now all that's left, is to make the game evolve.
For this, I created a pylab colormesh and added a colorbar to it
And saved the original (randomly generated) image
We don't want the image in the final render as it would be too small / big
pylab.pcolormesh(self.old_grid)
pylab.colorbar()
pylab.savefig("generation0.png")
It would be very inefficient to save the image every generation, so there's a write_frequency
to save every 5th image only
Now, all that's left is to loop over each cell and apply Conway's rules
- Get number of live neighbours
- If the cell is alive, and has 2 or 3 live neighbours, it stays alive
- If more or less, it dies
- if the cell is dead and there are exactly 3 live neighbours, it becomes alive
All changes are done to the new grid
for i in range(self.N):
for j in range(self.N):
live = self.live_neighbours(i, j)
# highlight-start
if self.old_grid[i][j] == 1 and live < 2:
self.new_grid[i][j] = 0 # Dead from starvation.
elif self.old_grid[i][j] == 1 and (live == 2 or live == 3):
self.new_grid[i][j] = 1 # Continue living.
elif self.old_grid[i][j] == 1 and live > 3:
self.new_grid[i][j] = 0 # Dead from overcrowding.
elif self.old_grid[i][j] == 0 and live == 3:
self.new_grid[i][j] = 1 # Alive from reproduction.
# highlight-end
Now that we have our new grid, we need to save it as a nice image
# Output the new configuration.
if t % write_frequency == 0:
pylab.pcolormesh(grid)
pylab.savefig(f"generations/generation{t}.png", dpi=300)
And that's it!! The program will run and evolve for T generations and save images every 5 generations
if __name__ == "__main__":
game = GameOfLife(N=20, T=200)
game.play()
Now, let's try to make a GIF animation of the evolution
For this, I use glob
and imageio
, and it's a fairly straightforward process
import glob
import imageio
I defined a staticmethod animate_folder
to convert all the images in the folder into a nice single GIF
@staticmethod
def animate_folder():
"""Makes an animated gif from a folder of images."""
images = []
for image in glob.glob("generations/*.png"):
images.append(imageio.imread(image))
# Using mimsave to convert into GIF
imageio.mimsave("animation.gif", images, "GIF", duration=0.5)
This is a bit of a hack I found on stackoverflow
The beauty of Conway's game of life is that it's so sophesticated, and it's so simple to implement, yet so powerful. I dug up some numpy arrays and added a Patterns
class that has really really cool patterns like
glider, glider shooter, mothership, beacon, block, an unbounded one which keeps growing forever
class Patterns:
"""A class to store all the patterns"""
def __init__(self):
self.patterns = {}
self.glider = numpy.array([[0, 0, 1], [1, 0, 1], [0, 1, 1], [0, 0, 1]])
...
and Finally added a patterns parameter to the GameOfLife class so I can use them easily
I've also added terminal styling to make it look really good. You can try it out yourself very easily by cloning the repository and running python3 game_of_life.py
git clone https://github.com/dhravya/game-of-life.git
Ofcourse, any contributions are welcome too!!
Thanks for reading my blog. I hope you enjoyed it, and learned something new! Leave a comment down below if you have any suggestions, ideas or comments.