Tutorial 1: Build Conway's Game of Life (Without Visualization)

In this tutorial we will build a basic version of Conway's Game of Life, a simple two-dimensional cellular automata. The simulation will run from the command line and will not be visualizable (that's Tutorial 2) so you'll have to take it on faith that it runs properly.

This tutorial teaches:

Create a SimState

In the sim/app/tutorial1and2 directory, create a file called Tutorial1.java In this file, add:


package sim.app.tutorial1and2;

import sim.engine.*;
import sim.field.grid.*;
import ec.util.*;

public class Tutorial1 extends SimState
    {
    public Tutorial1(long seed)
        {
        super(new MersenneTwisterFast(seed), new Schedule(1));
        }

A sim.engine.SimState is the basic class for modelling a simulation. You can think of it as a singleton object which holds all of your simulation information. You create a model by subclassing SimState and adding things to it as you like.

Why the ec package rather than the sim package?

Because MersenneTwisterFast was originally part of GMU's ECJ Evolutionary Computation system

The basic SimState already holds two important objects for you: an ec.util.MersenneTwisterFast random number generator, and a sim.engine.Schedule. The Schedule is a discrete event scheduler which represents time in the simulation and lets you schedule events to occur in specific times in the future. The MersenneTwisterFast is a fast, unsynchronized implementation of the Mersenne Twister random number generator, a very high quality and popular generator. MersenneTwisterFast has the same functions as java.util.Random, so you should feel at home with it.

The SimState constructor needs a MersenneTwisterFast and a Schedule. We built of each and passed them in. Here we constructed the Schedule with 1 order. An order is a subdivision of a time tick. When a Schedule is ready to fire off the events at a given time tick, it must determine which events to fire first. To do this, the events are first sorted by order. Within an order, events are shuffled randomly. In our simulation, we will only have a single agent to register events for on the schedule; so there's no reason to have more than a single order.

Make a Grid

In this simulation we will implement Conway's Game of Life, a simple two-dimensional, two-state, eight-neighbor cellular automaton. The Game of Life is played out on a 2D grid of "live" (state=1) and "dead" (state=0) cells. It begins with some initial configuration of cells. Each timestep, all of the cells simultaneously (synchronously) update themselves. Each cell uses the same rule to update itself, based on its eight neighboring cells. That rule is:

A good webpage on the Game of Life can be found here.

We will use a two-dimensional toroidal grid that is 100 x 100 in size, consisting entirely of 1's and 0's. To do this, we'll borrow special Field called sim.field.grid.IntGrid2D. Fields are our simulation's representation of space: they relate objects or data using some neighborhood function. In this case, our field is simply a wrapper two-dimensional array of integers. Feel free to examine the IntGrid2D code: it's very simple and straightforward. The 2D integer array it contains is public, and you are strongly encouraged to directly access the data for speed.

Add the grid as follows:


    public IntGrid2D grid;
    
    // our own parameters for setting the grid size later on
    public int gridWidth = 100;
    public int gridHeight = 100;

Seed the Grid

What's a b-heptomino?

A heptomino is seven live cells. Martin Gardner popularized the Game of Life by showing the dynamics of various heptominos, which he named the a-heptomino, b-heptomino, c-heptomino, etc.

Next we need to define the function which seeds the grid with some initial configuration. Let's use the b-heptomino seed popular in the Game of Life. We'll seed the grid by placing the b-heptomino right in the middle of the grid. Add the seeding code:


    // A b-heptomino looks like this:
    //  X
    // XXX
    // X XX
    public static final int[][] b_heptomino = new int[][]
        {{0, 1, 1},
         {1, 1, 0},
         {0, 1, 1},
         {0, 0, 1}};
    
    void seedGrid()
        {
        // we stick a b_heptomino in the center of the grid
        for(int x=0;x<b_heptomino.length;x++)
            for(int y=0;y<b_heptomino[x].length;y++)
                grid.field[x + grid.field.length/2 - b_heptomino.length/2]
                          [y + grid.field[x].length/2 - b_heptomino[x].length/2] =
                    b_heptomino[x][y];
        }

Notice that we're directly accessing the grid's 2D integer field array. IntGrid2D's require that their 2D array be rectangular, so we could have written grid.width and grid.height instead of grid.field.length and grid.field[x].length respectively.

Add the Start and Finish Methods

The general structure of a simulation is as follows:

We don't need a finish() method, but we do need a start() method. In this method, we need to call super.start() to let the SimState set up its Schedule. Then we create the grid and seed it. Last, we schedule our (in this case) single agent (called CA -- we'll come to that) to repeatedly manipulate the simulation:


    public void start()
        {
        super.start();  // very important!  This resets and cleans out the Schedule.
        grid = new IntGrid2D(gridWidth, gridHeight);
        seedGrid();
        schedule.scheduleRepeating(new CA());
        }

How do you stop an infinitely repeating schedule?

schedule.scheduleRepeating(agent) returns a sim.engine.Stoppable object. Call stop() on that object.

The schedule has a number of methods for scheduling agents to have events fired once or repeating. We have chosen the simplest repeating schedule: schedule the agent to be fired once every time tick forever.

Last we'll write a very simple main(String[] args) method which creates a Tutorial1 with a random seed, starts it, calls step(tutorial1) until the schedule ticks to 5000, then finishes up:


    public static void main(String[] args)
        {
        Tutorial1 tutorial1 = new Tutorial1(System.currentTimeMillis());
        tutorial1.start();
        long time;
        while((time = tutorial1.schedule.time()) < 5000)
            {
            if (time % 500 == 0) System.out.println(time);
            if (!tutorial1.schedule.step(tutorial1))
                break;
            }
        tutorial1.finish();
        }
    }

time is a long just to be consistent with the fact that Schedules perceive timesteps as longs.

Create the Cellular Automaton

All that's left is to write the actual agent which updates the cells in the grid. Since all the cells must be updated synchronously, at each time step this agent will dump the grid into a secondary grid; then it will update the original grid cells based on the secondary grid cell values. Create a new file called CA.java. In this file, put:


package sim.app.tutorial1and2;

import sim.engine.*;
import sim.field.grid.*;

public class CA implements Steppable
    {
    // the width and height will change later
    public IntGrid2D tempGrid = new IntGrid2D(0,0);

What's an Agent?

We define an Agent in a very specific manner. An Agent is an object which can be scheduled on a Schedule to be activated at some time step, obstensibly in order for it to change the simulation environment in some way. Agents don't have to be physically in the environment (that is, in any Field). They can of course, and often are: we call such agents embodied agents.

So far we've spoken of Agents receiving events from the Schedule. In fact agents can receive only a single kind of event from the Schedule: the Agent can have its step(SimState) method called. All agents must implement this method, which in turn means that they must implement the single-method interface sim.engine.Steppable.

Here's the step(SimState) method:


    public void step(SimState state)
        {
        Tutorial1 tut = (Tutorial1)state;
        tempGrid.setTo(tut.grid);   // first copy the grid into tempGrid
        
        // for each cell...
        int width = tempGrid.getWidth();
        int height = tempGrid.getHeight();
        for(int x=0;x<width;x++)
            for(int y=0;y<height;y++)
                {
                int count = 0;
                // count the number of live neighbors around the cell,
                // and to simplify the for-loop, just include the cell itself
                for(int dx = -1; dx < 2; dx++)
                    for(int dy = -1; dy < 2; dy++)
                        count += tempGrid.field[tempGrid.stx(x+dx)][tempGrid.sty(y+dy)];
                
                // since we're including the cell itself, the rule is slightly different:
                // if the count is 2 or less, or 5 or higher, the cell dies
                // else if the count is 3 exactly, a dead cell becomes live again
                // else the cell stays as it is
                        
                if (count <= 2 || count >= 5)  // death
                    tut.grid.field[x][y] = 0;
                else if (count == 3)           // birth
                    tut.grid.field[x][y] = 1;
                }
        }
    }

Can this be faster?

In HotSpot, you can get a 15% improvement in speed by copying instance variables to final local variables, like this:


final int width = tempGrid.width;
final int height = tempGrid.height;
final int[][] field = tempGrid.field;
final int[][] field2 = tut.grid.field;
for(int x=0;x<width;x++)
    for(int y=0;y<height;y++)
        {
        count = 0;
        for(int dx = -1; dx < 2; dx++)
            for(int dy = -1; dy < 2; dy++)
                count += field[tempGrid.stx(x+dx)][tempGrid.sty(y+dy)];
        if (count <= 2 || count >= 5)
            field2[x][y] = 0;
        else if (count == 3)
            field2[x][y] = 1;
        }
Unfortunately, HotSpot doesn't inline the stx(...) and sty(...) methods -- they're slightly too big (real smart, Sun!). So to get still faster (a 50% improvement total), you'd also need to implement your own toroidal math instead:

        for(int dx = -1; dx < 2; dx++)
            for(int dy = -1; dy < 2; dy++)
                {
                int xx = x+dx;  
                int yy = y+dy;
                
                if (xx < 0) xx += width; 
                else if (xx >= width) xx -= width;
                
                if (yy < 0) yy += height; 
                else if (yy >= height) yy -= height;
                
                count += field[xx][yy];
                }

Some other tricks can make it a little faster yet. Anyway, the first one is easily readable. The second one is messier (eight lines replacing one line), and even though it's significantly faster, I personally don't think it's worth the trouble for more complex simulations where the toroidal math is less prevalent.

Some notes:

Run the simulation

As mentioned before, when we run the simulation from the command line, it won't do anything exciting. You can see the simulation by completing Tutorial 2.

Compile the simulation's two Java files. Then issue java sim.app.tutorial1and2.Tutorial1 from the command line. Java should run for a second, then silently finish.