Philipp Burckhardt

On Statistics, Programming and the Social Sciences

Game Of Life

This semester, I am taking a class taught by Prof. Freeman called Statistical Computing. Over time, I will create a series of posts about the material I am learning there; the class aims to give a broad exposure to the topic of numerical and statistical computing, and introduce us to working in different programming languages. In a way, one is pushed to leave one's comfort zone and look at problems in a new way.

As a preliminary exercise, we were asked to implement the "Game of Life" in our favorite programming language. This is a very famous example of a cellular automaton, and one of the staple exercises people solve when first engaging with the field of artificial intelligence. The title is actually a bit of a misnomer, since this cellular automaton, which was developed by mathematician Horton Conway, is actual not a game, but rather a simulation based on a few rules, simulating the evolutionary development of a 2d grid of cells which are set either alive or dead based on a few deterministic rules. In case you are not familiar with it, the Wikipedia articles gives a very good explanation about the rules and also the history and importance of the "Game of Life".

Myself, I chose to implement it in JavaScript, as I wanted to have a visual representation of the grid and it is currently my favorite language to work in. The final result can be accessed under

http://life.philipp-burckhardt.com

A few comments about the implementation:

As a first function, I have written a constructor function for the 2d grid which handles all of the game logic, separated from the visual representation which is handled by the View class. The Grid function has two parameters, the width and height of the grid to be created. Other important variables are initialized to null to indicate that they will be used, but are not yet initialized.

function Grid( gridWidth, gridHeight ){
    var self = this;
    this.width = gridWidth || 50;
    this.height = gridHeight || 50;
    this.rows = [];
    this.nsteps = null;
    this.current_step = null;
    this.timer = null;
    this.view = null;

    this.init = function(){
            var row;
            self.rows = [];
            for (var y = 0; y < self.height; y++) {
                row = [];
                for (var x = 0; x < self.width; x++) {
                    var ncell = new Cell(x, y, self);
                    row.push(ncell);
                }
                self.rows.push(row);
            }
    };
// ...
}

The init function creates and populates a 2d matrix in the form of a nested array with objects of class Cell. Since we are not creating more than one grid, it is no problem to attach the init function directly to objects created via the constructor. For the cells, we instead attach all functions to the prototype object in order to avoid attaching an individual version of the functions to each created instance:

function Cell( x, y, grid ) {
  this.x = x;
  this.y = y;
  this.grid = grid;
  this.alive = false;
  this.liveNeighbours = null;
}

Cell.prototype.update = function() {
  if (this.alive === true){
    this.alive = ( 
      this.liveNeighbours === 2 || 
      this.liveNeighbours === 3 
    ) ? true : false;
  } 
  else if ( this.liveNeighbours === 3 ) {
    this.alive = true;
  }
};

Cell.prototype.countLivingNeighbours = function(){
  var count = 0;
  var i_low =  Math.max(this.y - 1, 0);
  var i_high = Math.min(this.y + 1, this.grid.height - 1);
  var j_low = Math.max(this.x - 1, 0);
  var j_high = Math.min(this.x + 1, this.grid.width - 1);
  for ( var i = i_low; i <= i_high; i++ ) {
    for ( var j = j_low; j <= j_high; j++ ) {
      if (
        this.grid.rows[i][j].alive === true && 
        !(i === this.y && j === this.x)
      ) {
        count++;
      }
    }
  }
  this.liveNeighbours = count;
};

Cells are created via new Cell(x,y,grid), where x and y denote the coordinates of the cell in the grid and grid is the grid to which the cell belongs. The update method of the cells updates the alive status according to the rules of the "Game of Life" and the current number of living neighbors, which is stored in the arribute liveNeighbours.
The other methods for cells, countLivingNeighbours, calculates the number of living neighbors for each cell by iterating through the cells and looking at all neighboring cells (at most there are eight, less on the edges of the grid).
A Grid instance contains a few other very important functions:


   this.traverse = function( callback ) {
      for (var y = 0; y < self.height; y++) {
        for (var x = 0; x < self.width; x++) {
          callback(self.rows[y][x], x, y);
        }
      }
    };

    this.step = function() {
      if ( self.current_step < self.nsteps ) {
        console.log( "Calculating round " + self.current_step );
        self.traverse( function( cell ) {
          cell.countLivingNeighbours();
        });
        self.traverse( function( cell ) {
          cell.update();
        });
        self.view.update();
        self.current_step++;
      } else {
        clearInterval(self.timer);
      }
    };

Here, the traverse function uses some of the functional programming capabilities of JS: It takes as its input a function and applies it to all cells in the grid. This is used in the step method, which goes through all the individual actions of one step of the "Game of Life".

The full source code of the project is also hosted on GitHub.