Recently, I’ve been reading a very interesting book (The Recursive Universe) about a game called Game of Life.
The game consists of three elements:
- An (unlimited) grid where each cell can be defined alive or dead;
- A number of alive cells placed somewhere in the grid;
- Three simple rules which set the conditions for cells to duplicate or die
Once the grid and the set are defined, the next generation of cells is obtained by applying three rules recursively:
- Any live cell with fewer than 2 neighbours and more than 3 dies by isolation or overpopulation, respectively;
- Any live cell with exactly 2 or 3 live cells stays alive;
- Any dead cell with exacly 3 live neighbours becomes alive.
I thought about implementing the Game of Life in a simple Python script. The World in the Game of Life universe becomes a Python class. The whole source code is available at the end of the post (and on Bitbucket at bitbucket.org/co2h5/gameoflife ). Here, a quick description of the class is given.
class World: def __init__(self, sizex, sizey): # This method initialises the class by defining the maximum size of the World. ... def set(self, x, y, value): # Set the state of the cell at location (x,y). The state can be on (alive) or off (dead). If the location is outside the World, no assignment is performed. ... def get(self, x,y): # Get the state of the cell at location (x,y). If the location is outside the World, a value corresponding to a dead cell is returned. ... def empty_grid(self): # Initialise an empty World by allocating memory and setting all cells to off. ... def count_neighbours(self, x, y): # Count the number of neighbouring cells which are alive. ... def next_step(self): # Take the next step of the evolution by applying the three rules recursively. ... def generate_picture(self, filename, blksize=2, xrange=None, yrange=None, colorscheme=1): # Generate a picture of the World and save it to file. This method uses the Pillow library to generate a PNG file. ... def save_to_file(self, filename): # Save the state of the simulation to file so that it is possible to restart from this point later now. This method uses Scipy IO library. ... def load_from_file(self, filename): # Load the state of the simulation from file so that it is possible to restart from that point. This method uses Scipy IO library. ...
And now… some interesting animations from the Game of Life!
R-pentomino
Evolution in the Game of Life is something very interesting. Once the initial set of alive cells is given, the user does not intervene anymore. A simple initial set of cells can lead to an interesting final pattern. For example, the pattern described below is called r-pentomino and it takes around 1100 generations to get to its final step. This is the animation which describes the whole evolution:
Random set
The fun is also to try playing the Game of Life with a random set of cells. For example, what will happen to these few cells?
More information about the Game of Life can be found on this Wiki.
Source code
import subprocess import random import numpy as np import scipy.io as sio class World: def __init__(self, sizex, sizey): self.grid = (sizex, sizey) self.data = self.empty_grid() self.age = 0 def set(self, x, y, value): if x < 0 or x > self.grid[0]: return if y < 0 or y > self.grid[1]: return self.data[x,y] = value def get(self, x,y): if x < 0 or x >= self.grid[0]: return False if y < 0 or y >= self.grid[1]: return False return self.data[x,y] def empty_grid(self): return np.zeros(self.grid, dtype=bool) def count_neighbours(self, x, y): count = 0 for c in range(x-1, x+2): for r in range(y-1, y+2): if x == c and r == y: continue if self.get(c,r): count += 1 return count def place_cells(self, cells, offset=(0,0)): for x, y in cells: self.set(x+offset[0], y+offset[1], True) def place_cells_random(self, num, xrange=None, yrange=None): if xrange is not None: xmin, xmax = xrange else: xmin, xmax = 0, self.grid[0] if yrange is not None: ymin, ymax = yrange else: ymin, ymax = 0, self.grid[1] for i in range(num): x = random.randint(xmin, xmax-1) y = random.randint(ymin, ymax-1) self.set(x, y, True) def next_step(self): new_data = self.empty_grid() for x in range(self.grid[0]): for y in range(self.grid[1]): n = self.count_neighbours(x,y) if n == 2: new_data[x,y] = self.get(x,y) elif n == 3: new_data[x,y] = True self.data = new_data self.age += 1 def generate_picture(self, filename, blksize=2, xrange=None, yrange=None, colorscheme=1): from PIL import Image, ImageDraw if xrange is not None: xmin, xmax = xrange else: xmin, xmax = 0, self.grid[0] if yrange is not None: ymin, ymax = yrange else: ymin, ymax = 0, self.grid[1] if colorscheme == 2: bkc = 'white' fgc = 'black' else: bkc = 'black' fgc = 'white' img = Image.new('RGB', ((xmax - xmin)*blksize, (ymax - ymin)*blksize)) draw = ImageDraw.Draw(img) draw.rectangle((0, 0, (xmax - xmin)*blksize, (ymax - ymin)*blksize), fill=bkc) for x in range(xmin, xmax): for y in range(ymin, ymax): if self.get(x,y): draw.rectangle(((x-xmin)*blksize, (y-ymin)*blksize, (x-xmin+1)*blksize, (y-ymin+1)*blksize), fill=fgc) img.save(filename) def save_to_file(self, filename): sio.savemat(filename, {'data': self.data, 'age': self.age}, appendmat=True) pass def load_from_file(self, filename): data_dic = sio.loadmat(filename, appendmat=True) self.grid = (self.data.shape[0], self.data.shape[1]) self.data = np.array(data_dic['data'], dtype=bool) self.age = data_dic['age'][0,0] if __name__ == "__main__": cells = [ ( 0, 1), ( 0, 0), ( 0, -1), (-1, 0), ( 1, 1), ] wld = World(256,256) try: wld.load_from_file('last_step') except FileNotFoundError: wld.place_cells(cells, offset=(128,128)) # #wld.place_cells_random(128, xrange=(448,576), yrange=(448,576)) for i in range(wld.age, wld.age + 1200): print("Step {}".format(i)) wld.generate_picture('step{:05d}.png'.format(i), xrange=(43,213), yrange=(64,192), blksize=8, colorscheme=2) wld.save_to_file('last_step') wld.next_step() output_gif = 'r_pentomino.gif' subprocess.call(['ffmpeg', '-framerate', str(20), '-i', 'step%05d.png', '-pix_fmt', 'gray', output_gif])
Leave a Reply