I’d been feeling demotivated all of yesterday. The week before, I’d been sick with an upset stomach, fever, and body ache. This week, the back pain had become unbearable. In isolation for the last four weeks due to COVID-19, I was unable to step out to get physiotherapy.

While going through all this, I came across xkcd’s RIP John Conway tribute comic.

Yesterday evening, I sat down with my younger son to explore the Game of Life online. I was blown away by the simplicity and yet the beauty of the game. Some things are indeed timeless. I was also surprised that I had not come across it earlier. I blame that on a lack of formal Computer Science education 🙂

I thought that the best way to appreciate this game would be to quickly implement it in code. And I chose to do it not in my favourite languages, bash or C, but in Python. I’ve been using Python for the last fifteen years, since RedHat started using it in their system tools such as yum and system-config. However, I’ve never really programmed algorithms in Python. I’ve always written scripts or Flask web applications. I thought this would be a good way to teach myself the Pythonic way of implementing simple algorithms.

With a little help from Stack Overflow, it was simple enough to implement the game. Here is the help screen from the final implementation of the game:

saurabh@saurabhj gol % python gol.py -h
usage: gol.py [-h] [--edge EDGE] [--debug] size interval pattern_file

The Game of Life

positional arguments:
  size          The size of the matrix
  interval      The interval between each iteration
  pattern_file  The pattern file to be used

optional arguments:
  -h, --help    show this help message and exit
  --edge EDGE   The optional edge size. A negative edge is considered alive.
  --debug       Debug the neighbourArray
Here are some sample runs of the game:
As I ran the game with various initial patterns, I realised that at the edge, my implementation was not behaving the same as BitStorm implementation. It was obvious that I hadn’t thought through the boundary conditions. I first superimposed the visible matrix onto a larger matrix. This was adequate for small starter patterns, but larger patterns would eventually bounce off the edge of the underlying matrix and reappear on the visible matrix. This bouncy edge had me really annoyed.

I then read online and realised that there are different boundary conditions possible in Life. I implemented two of these. One is a negative edge, and the other is an active edge. The cells in an active edge behave, when computing neighbours, like active cells. Such an edge is not as bouncy as a negative edge.

This was an evening well spent. My son lost interest when I started implementing the boundary conditions. He didn’t see what the big deal was. Perhaps, when I have time again, I will implement a toroidal boundary. Here is the source code as it currently stands.

#!/usr/bin/python
import time
import subprocess
import argparse

def printArray(array, edge):
  subprocess.call('clear',shell=True)
  N = len(array)
  edgeByte = u'\u2591'
  if edge < 0:
    edge = -edge
    edgeByte = u'\u2593'

  for i in range(N):
    for j in range(N):
      if i < edge or j < edge or i >= N - edge or j >= N - edge:
        print edgeByte,
      else:
        if array[i][j] == 0:
          print '.',
        else:
          print u'\u2588',
    print
  print

def neighbourCount(array, x, y, edge):
  count = 0
  N = len(array)

  for i in [x - 1, x, x + 1]:
    for j in [y - 1, y, y + 1]:
      if i >= 0 and i < N and j >= 0 and j < N and not (i == x and y == j):
        if edge < 0 and (i < abs(edge) or j < abs(edge) or i >= (N + edge) or j >= (N + edge)) and (array[x][y] == 1):
          count += 1
        else:
          count += array[i][j]
  return count

def makeNeighbourArray(array, neighbourArray, edge, debug):
  N = len(array)

  for i in range(N):
    for j in range(N):
      neighbourArray[i][j] = neighbourCount(array, i, j, edge)
      if edge > 0 and (i < edge or j < edge or i >= N - abs(edge) or j >= N - abs(edge)):
        neighbourArray[i][j] = 0
      if debug:
        print neighbourArray[i][j],
    if debug:
      print

# If the edge value is passed to this method, it assumes the edge is dead
def makeArrayFromNA(neighbourArray, array, edge):
  stopIt = True
  N = len(array)

  for i in range(N):
    for j in range(N):
      if (neighbourArray[i][j] == 2 and array[i][j] == 1) or neighbourArray[i][j] == 3:
        newval = 1

        if edge != 0:
          if i < edge or j < edge or i >= N - abs(edge) or j >= N - abs(edge):
            if edge > 0:
              newval = 0
            else:
              newval = 1
      else:
        newval = 0
      if newval != array[i][j]:
        stopIt = False
      array[i][j] = newval
  return stopIt

def insert_pattern(array, filename):
  N = len(array)
  file = open(filename, "r")
  lines = file.readlines()
  size = int(lines[0])
  offset = int((N - size) / 2)

  for i in range(1, size + 1):
    for j in range(size):
      array[offset + i][offset + j] = int(lines[i][j])

def main():
  parser = argparse.ArgumentParser(description='The Game of Life')
  parser.add_argument('size', help='The size of the matrix')
  parser.add_argument('interval', help='The interval between each iteration')
  parser.add_argument('pattern_file', help='The pattern file to be used')
  parser.add_argument('--edge', help='The optional edge size. A negative edge is considered alive.', type=int, required=False, default=0)
  parser.add_argument('--debug', help='Debug the neighbourArray', action='store_true')
  args = parser.parse_args()
  edge = int(args.edge)
  if edge > 1:
    edge = 1
  if edge < 0:
    edge = -1
  N = int(args.size) + (abs(edge) * 2)
  interval = float(args.interval)
  iteration = 0

  array = [[0 for i in range(N)] for j in range(N)]
  neighbourArray = [[0 for i in range(N)] for j in range(N)]
  insert_pattern(array, args.pattern_file)

  stopIt = False
  while not stopIt:
    printArray(array, edge)
    makeNeighbourArray(array, neighbourArray, edge, args.debug)
    stopIt = makeArrayFromNA(neighbourArray, array, edge)
    iteration += 1
    print ("Iteration: " + str(iteration))
    if interval:
      time.sleep(interval)
    else:
      ignore = raw_input("Press Enter to continue:")

if __name__ == '__main__':
    main()