The following is an edited repost of an item I originally wrote last year.
Sometimes, my brain needs to take a break from work, from arguing on the Internet and all the rest. On these occasions, I like to take a classic problem and see what I can do with it in a short time: Given an hour and a computer, what can I accomplish? Can I use a new feature of some tool which I haven't employed before? These sorts of largely inconsequential, fun little problems can be a more effective mental vacation than getting blitzed and watching WALL-E; not to disparage the latter in general, but exercising the intellect has its own way of clearing out the mind-cruft. Doing anything quickly can be a great relief from the slog of research.
Also, when you can get a program like that running in half an hour, the spectacle of creationists failing to implement a WEASEL program after days and days becomes truly enjoyable. You know, sometimes it's the little joys which make life so beautiful.
First, the punchline:

This is a fifty-frame animation of Conway's Game of Life, a classic example of cellular automata, some of whose history I covered in this book review. This particular implementation begins with a 32×32 grid of random values and successively advances by applying the transition rules of that famous automaton. The purpose of the exercise is twofold: first, to demonstrate some basic programming techniques and show how much you can do in Python with half an hour of free time; and second, to see how persistent and cyclic features arise from random configurations.
I chose to establish periodic boundary conditions, so that patterns leaving one side wrap around to the other (readers of my generation will recall the game Asteroids). This means we're playing Life on a torus — is there anything donuts can't do? We'll be using the Python language with a few extra modules (SciPy) for stuff like visual output.
Now, the code!
#!/usr/bin/python
# conway.py
# Blake Stacey
# (bstacey at alum.removethis.mit.andthis.edu)
import math, scipy, random, pylab
def iTest(x, p):
if(x <= p):
return 0
else:
return 1
fP = 0.5 # probability of dead square
iGridEdge = 32 # size of grid
iTrials = 50 # number of times to loop
random.seed()
imThis = scipy.array(scipy.rand(iGridEdge, iGridEdge))
imThat = scipy.array(scipy.zeros((iGridEdge, iGridEdge)))
for i in range(iGridEdge):
for j in range(iGridEdge):
imThis[i][j] = iTest(imThis[i][j], fP)
pylab.ion()
pylab.hold(False)
for t in range(iTrials):
for i in range(iGridEdge):
for j in range(iGridEdge):
# bounds checking
iPlus = (i + 1) % iGridEdge
iMinus = (i - 1) % iGridEdge
jPlus = (j + 1) % iGridEdge
jMinus = (j - 1) % iGridEdge
# count number of neighbors
iNext = (imThis[i][jPlus]
+ imThis[iPlus][j]
+ imThis[iMinus][j]
+ imThis[i][jMinus]
+ imThis[iPlus][jPlus]
+ imThis[iMinus][jMinus]
+ imThis[iPlus][jMinus]
+ imThis[iMinus][jPlus])
iCurrent = imThis[i][j]
# update rule
if(iCurrent == 0):
if(iNext == 3):
imThat[i][j] = 1
else:
imThat[i][j] = 0
else:
if(iNext <= 1):
imThat[i][j] = 0
elif(iNext > 3):
imThat[i][j] = 0
elif((iNext == 2) or (iNext == 3)):
imThat[i][j] = 1
# end loop over elements
# swap buffers
imOther = imThis
imThis = imThat
imThat = imOther
# print summary statistic and display image
print sum(sum(imThis)) / float((iGridEdge * iGridEdge))
pylab.imshow(imThis, cmap = pylab.cm.prism)
pylab.draw()
By sticking a pylab.savefig command after the pylab.imshow call, the images can be written to files:
pylab.imshow(imThis, cmap = pylab.cm.prism)
pylab.savefig("frame_" + str(t) + ".png")
pylab.draw()
There's an awful lot of whitespace in the resulting images, and the quickest hack for, well, hacking it off is to use ImageMagick:
convert -crop 682x682+272+83 frame_0.png trimmed_0.png
I actually did all fifty frames with a bash loop:
for i in `seq 0 49`; do convert -crop 682x682+272+83
frame_$i.png $i.png; done
Noting that the images had more resolution than was really necessary, I sized them down:
for i in `seq 0 49`; do convert -resize 100x100 $i.png
resize_$i.png; done
Then I made the frames into an animated GIF, realized that they were out of order because the single-digit names were not prefixed with 0, fixed that problem, and made a new animation:
convert -delay 20 -loop 0 resize*.png animation.gif
The result was the image seen above. Here, I used a random initial configuration, because I had allotted myself half an hour to do the whole thing and I didn’t feel like specifying anything more detailed. However, you can put in a feature like a glider with code like this:
imThis = scipy.array(scipy.zeros((iGridEdge, iGridEdge)))
imThat = scipy.array(scipy.zeros((iGridEdge, iGridEdge)))
imThis[5][5] = 1
imThis[5][6] = 1
imThis[5][7] = 1
imThis[6][5] = 1
imThis[7][6] = 1
An old comrade of mine ported this code to JavaScript so that one can play with it on the Web.


![[sex and science]](http://www.sunclipse.org/downloads/sexandscience3.png)







Comments
Ah, Life. I wasted many hours in my twenties writing and rewriting my 16-bit Windows Life program, trying all sorts of tricks to speed it up. (The last version I wrote was actually written in 32-bit assembly, which required jumping through all sorts of hoops to make work on 16-bit Windows. Plus you could change the colors while it was running!)
Good times.
Posted by: Brian
| April 15, 2009 12:23 PM
My own attempt at a Python Life implementation. I use the PIL instead, which means I had to scale up end result to get a resolution where the results were easily visible.
Hopefully, Sciblogs won't mangle the code too much.
from PIL import Image
import random, itertools
max_x = 32
max_y = 32
def neighbours(x,y,state):
px = (x + 1) % max_x
py = (y + 1) % max_y
# Negative indices wrap around, so no need to correct.
mx = x - 1
my = y - 1
return sum([state[i][j] for i in (mx, x, px) for j in (my, y, py)])
def next_state(state):
# States under which a cell is alive: If its dead but has 3 living neighbours,
# or is alive and has 2 or 3 neighbours (not including itself)
return [[ (state[x][y], neighbours(x,y,state)) in ((0,3), (1,3), (1,4)) for y in xrange(max_y)] for x in xrange(max_x)]
green = (0,255,0)
red = (255,0,0)
colors = (red, green)
def to_image(index, state):
im = Image.new('RGB', (max_x, max_y))
# Technically gets x and y mixed around, but it doesn't really matter. If we cared, we'd toss in a zip(*)
colormap = [colors[c] for c in itertools.chain(*state)]
im.putdata(colormap)
im.resize((max_x * 2, max_y * 2)).save('%02i.png' % index)
# Set up our initial state
state = [[random.randint(0,1) for y in xrange(max_y)] for x in xrange(max_x)]
for i in xrange(50):
to_image(i, state)
state = next_state(state)
Posted by: mds | April 15, 2009 1:06 PM
Heh, first thing I did when I got my first PC (A 16MHz 386SX -- what awesome compute power! Almost as much as my current cellphone!!) was to install MASM and write a Game of Life that ran in 320x200(?) 16-colur mode. Very pretty. Acceptably fast and memory efficient, too.
For an encore, I taught myself C by writing a Mandelbrot generator (then I discovered the InterToobz and downloaded Fractint. It was about then I realized that everything either fun or useful had already been written by someone else).
Posted by: Eamon Knight | April 15, 2009 1:12 PM
...Needs more explosions.
Posted by: Bronze Dog
| April 17, 2009 11:16 AM
I bet nobody's done life on a klein bottle.
Posted by: zpmorgan | April 18, 2009 11:23 PM
I should have been less loath to take comp sci.
What little I learnt I've forgotten. I did once want to do something, but I'd lost the stuff I'd written for homework, and I couldn't arsed to find out how to jump through hoops to import data from a file again. If he cared, Edmund (pdf, Danish) would slap me silly now.
(Yes, I'm working through my backlog of blogs. Remarkable how many hours there are in the day once one's alienated all one's msn contacts.)
Posted by: Sili
| May 17, 2009 3:25 PM