Javascript programming - Part III (Game of Life)

Now that Javascript is set up, we could implement more boring programs that print stuff to the console. As discussed in my introduction, however, the benefit of javascript is how easy it is to visualise/debug stuff that is less boring. To that end, I'm implementing a famous Cellular Automaton (CA) next. It was first described by John Horton Conway in 1970, and it looks cool. It does more than look cool, however, as its interesting properties have kept scientists busy for decades. I should say up front that the implementation of Game of Life described below is directly based on a video of the Youtube channel The Coding Train.

The idea of Game of Life is simple. A simple grid is defined where each cell (i.e. a pixel) can be either alive (1) or dead (0). Every time step, grid points update their state according to the 8 pixels in their local neighbourhood. The update rules are as follows:

  1. A cell surrounded by two or three living cells, stays alive.

  2. A cell surrounded by any other number of cells, dies.

  3. A dead cell surrounded by exactly three cells, becomes alive.

So. Let's first implement a simple CA class in Javascript, which has a property named "grid" in which our pixels will live or die:

ca.js:

// Class definition
class CA
{
    // Constructor
    constructor(cols,rows)
    {
        // Make empty grid
        this.grid = MakeEmptyGrid(cols,rows);  // Seperate function defined below
        this.nc = cols
        this.nr = rows        
        
        // Fill it up with random 0's and 1's
        for(let i=0;i long --> grid[cols]
    for(let r = 0; r< cols; r++)
        grid[r] = new Array(cols);      // Insert a row of  long   --> grid[cols][rows]
    return grid;
}

As you can see, the code above does not yet update the grid, but let's test if everything works by making a new CA object and calling the "show"-method:

gol.js

let CA = require("./ca.js")

let nrow = 100
let ncol = 200
let scale = 5
let gol = new CA(ncol,nrow)
gol.show()

As described in the previous section, you can run this in the browser, or not. Either way, this is the output if it works:

Drawing the grid on an HTML canvas

Before we implement the so-called nextstate function, let's first make this grid visible as actual pixels. For this, there are some excellent javascript bundles available, such as p5js. However, I have found that these libraries are never as fast as directly drawing pixels on the HTML canvas. So that's what I do below.

Name

index.html:

<html><head><title>GoL</title></head><body><canvas id="canvas"></canvas><script src="./gol_browser.js"></script></body></html>

gol.js:

let CA = require("./ca.js");

let nrow = 100;
let ncol = 200;
let scale = 5;
let gol = new CA(ncol,nrow);

if (typeof window !== 'undefined')  // We are in the browser
{
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    canvas.width = ncol*scale;
    canvas.height = nrow*scale;
    function draw()
    {
        gol.update(); 
        ctx.fillStyle = "#000";
        ctx.fillRect(0, 0, ncol*scale, nrow*scale);

        for(let i=0;i<ncol;i++)
        {
            for(let j=0;j<nrow;j++)     // j are columns
            {

                let x = i*scale;
                let y = j*scale;
                ctx.strokeStyle = "#555"
                ctx.lineWidth = 1
                if(gol.grid[i][j] == 1)
                {
                    ctx.fillStyle = "#FFF";                    
                    ctx.fillRect(x, y, scale, scale);                    
                    ctx.strokeRect(x, y, scale, scale);
                }
                else
                {
                    ctx.fillStyle = "#000";                    
                    ctx.fillRect(x, y, scale, scale);                    
                }
            }
        }
        requestAnimationFrame(draw);
    }
    requestAnimationFrame(draw);
}
else // We are in the console, do something more generic like a simple loop
{
    for(let T=0; T<1000; T++)
    {
        gol.update();
        gol.show();
    }
}

To ensure the CA will function on the client-side (i.e. your computer), the file "gol.js" needs to be bundled with browserify or rollup:

> watchify gol.js -o gol_browser.js

Notice that in the same way I did in the previous section, I make sure the code knows whether I am in the browser, or using the command-line by verifying if the "window" variable is defined. When we inspect the HTML page above, we will see a grid drawn on an HTML canvas:

Notice how I use requestAnimationFrame recursively: at the end the function calls itself. In other words, despite not much going on yet, this grid is continuously being refreshed.

Alright. Let's implement the update function. Game of life, like any other CA, updates synchronously, meaning that each cell in the grid is updated at the same time. To do this, we want to make sure to use two versions of the grid: one for the previous state, and one for the next state. If we don't do this, the update will be asynchronous, meaning that certain cells (e.g. the top left) are updated earlier than others. Here's how we'll avoid that:

new_grid = MakeEmptyGrid(cols,rows)

for(let i=0;i<this.nc;i++)
{
  for(let j=0;j<this.nr;j++)
  {
    // Nextstate function goes here
  }
}

this.grid = new_grid;

Now, let's implement the nextstate-function:

// Count living neighbours
let neighbours = this.CountNeighbours(i,j)
let state = this.grid[i][j]

// Apply the three rules of GoL
if(state == 0 && neighbours == 3)
  new_grid[i][j] = 1
else if(state == 1 && (neighbours < 2 || neighbours > 3))
  new_grid[i][j] = 0
else
  new_grid[i][j] = state
  
CountNeighbours(col,row){
	let num_living = 0
	for(let v=-1;v<2;v++)   // Check +/-1 vertically{
		for(let h=-1;h<2;h++) // Check +/-1 horizontally
    	{
     	if(h==0 && v == 0) continue       // Do not count self
		let x = (col+h+this.nc) % this.nc // Wraps neighbours left-to-right
		let y = (row+v+this.nr) % this.nr // Wraps neighbours top-to-bottom
		num_living += this.grid[x][y]
    	}
	return num_living;
}

... where CountNeighbors is implemented as a method for the CA-class. Notice how the modulo operator (%) makes sure the left-right and top-bottom neighbourhoods of the grids connect, making the grid a torus. This can be helpful if you want to prevent edge artefacts.

Now, when we inspect the web page, we should see something like this:

There you go. Game of Life. A couple of optimalisation steps in the visualisation even allow for this example to run very fast indeed:

Here are the files for this implementation of Game of Life (including the faster version which uses get/putimagedata to speed up the drawing):

After having looked for a quick and responsive display for CAs for nearly a decade, it's quite surprising to me that I ended up with Javascript. Ever since my web-developing days, I always expected Javascript to die out eventually. However, it is in fact making a comeback, for example with amazing new data visualisation toolkits.

And the best thing is: nobody needs to install anything to see your program in action.

Previous
Previous

Splitting a multi-fasta file

Next
Next

Javascript programming - Part II (My setup)