Introduction
The "game" is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input. One interacts with the Game of Life by creating an initial configuration and observing how it evolves. First we create the main function in JavaScript, then call it in the body and then apply CSS.
To make things more interesting and efficient, I used two Canvas elements, one on top of the other rather than a single Canvas. The Canvas at the bottom of the Z-order (display order) is the first and I use it to draw the grid and background colour, the second Canvas is used only to render the life forms.
I implemented the main code in JavaScript class called Life, like this:
function Life(size) {
this.size = size;
var count = size * size;
this.torus = new Array(count);
this.clear = function () {
for (var i = 0; i < count; i++)
this.torus[i] = 0;
};
this.getNeighbours = function (x, y) {
var count = [0, 0, 0, 0, 0];
// prev row
count[this.get(x - 1, y - 1)]++;
count[this.get(x, y - 1)]++;
count[this.get(x + 1, y - 1)]++;
// this row
count[this.get(x - 1, y)]++;
count[this.get(x + 1, y)]++;
// next row
count[this.get(x - 1, y + 1)]++;
count[this.get(x, y + 1)]++;
count[this.get(x + 1, y + 1)]++;
return count;
};
this.get = function (x, y) {
return this.torus[this.getIndex(x, y)];
};
this.set = function (x, y, value) {
this.torus[this.getIndex(x, y)] = value;
};
this.getIndex = function (x, y) {
if (x < -1 || y < -1 || x > size || y > size)
throw "Index is out of bounds";
if (x == -1)
x = size - 1;
else if (x == size)
x = 0;
if (y == -1)
y = size - 1;
else if (y == size)
y = 0;
return x + y * this.size;
};
this.clear();
}
The class implements an NxN array but stored internally as a one-dimensional array. This is basically what the getIndex()
function does, but with a slight twist to implement the torus. So the row at -1 is mapped to the row at N-1 and row N is mapped to row 0; similarly for the columns. The getIndex
function is in turn used by a simple set value and get value function and these are in turn used by the main function called getNeighbours()
which returns an array of length 5 where the first element is not used and the other four elements are the counts of each type of life form.
The reason the first element is not used is to simplify the code is because the life forms are stored as integers in the grid, e.g. a cell value of 0
corresponds to empty, a value of 1
corresponds to life form type 1
. The only other function is a clear()
which sets all values to 0
(empty).
The HTML used to accomplish this is shown below
<div style="position:relative">
<canvas id='canvas2' width='641' height='641' on></canvas>
<canvas id='canvas1' width='641' height='641' on>Canvas is not supported by this browser.</canvas>
</div>
I positioned the two Canvas elements using CSS. The key point is that they need to be placed in a <div> that has position: relative and the embedded style sheet for Canvas is set to position: absolute and top and bottom set to 0.
The whole program look like this:
<head>
<title>Mouse Game</title>
<style type="text/css">
body
{
font-size: 11pt;
font-family: verdana, arial, sans-serif;
}
select
{
font-size: 11pt;
}
div#params
{
margin: 11px;
}
canvas
{
border-color: Gray;
border-width: thin;
position: absolute;
top: 0px;
left: 0px;
}
#canvas2
{
background-color: #f5f5f5;
}
button
{
width: 80px;
color: #393939;
}
</style>
<script type="text/javascript" >
function Life(size) {
this.size = size;
var count = size * size;
this.torus = new Array(count);
this.clear = function () {
for (var i = 0; i < count; i++)
this.torus[i] = 0;
};
this.getNeighbours = function (x, y) {
var count = [0, 0, 0, 0, 0];
// previous row
count[this.get(x - 1, y - 1)]++;
count[this.get(x, y - 1)]++;
count[this.get(x + 1, y - 1)]++;
// this row
count[this.get(x - 1, y)]++;
count[this.get(x + 1, y)]++;
// next row
count[this.get(x - 1, y + 1)]++;
count[this.get(x, y + 1)]++;
count[this.get(x + 1, y + 1)]++;
return count;
};
this.get = function (x, y) {
return this.torus[this.getIndex(x, y)];
};
this.set = function (x, y, value) {
this.torus[this.getIndex(x, y)] = value;
};
this.getIndex = function (x, y) {
if (x < -1 || y < -1 || x > size || y > size)
throw "Index out of bounds";
if (x == -1)
x = size - 1;
else if (x == size)
x = 0;
if (y == -1)
y = size - 1;
else if (y == size)
y = 0;
return x + y * this.size;
};
this.clear();
}
function relMouseCoords(event) {
var totalOffsetX = 0;
var totalOffsetY = 0;
var canvasX = 0;
var canvasY = 0;
var currentElement = this;
do {
totalOffsetX += currentElement.offsetLeft;
totalOffsetY += currentElement.offsetTop;
}
while (currentElement = currentElement.offsetParent)
canvasX = event.pageX - totalOffsetX;
canvasY = event.pageY - totalOffsetY;
return { x: canvasX, y: canvasY }
}
HTMLCanvasElement.prototype.relMouseCoords = relMouseCoords;
</script>
</head>
<body>
<div id='params'>
<button onclick="clearGame()">Clear</button>
<button onclick="advance()" >Next</button>
<button id="btnAnimate" onclick="animate()">Animate</button>
<select id="color_menu0" name="color_menu0" style="width: 60px">
<option style="background-color:#00ced1" value="#00ced1" selected="selected"/>
<option style="background-color:#ff8c70" value="#ff8c70"/>
<option style="background-color:#008b8b" value="#008b8b"/>
<option style="background-color:#ff1493" value="#ff1493"/>
</select>
<span id="generation" style="width: 130">Generation: 0</span>
<span id="population" style="width: 130">Population: 0</span>
</div>
<div style="position:relative">
<canvas id='canvas2' width='641' height='641' on></canvas>
<canvas id='canvas1' width='641' height='641' on>Canvas is not supported by this browser.</canvas>
</div>
<script type="text/javascript" >
// Keep a torus for the current and next generation
var _size = 64;
var _cellSize = 10;
var _torus1 = new Life(_size);
var _torus2 = new Life(_size);
var _animate = false;
var _generation = 0;
var isMouseDown = false;
function clearGame() {
_torus1.clear();
_generation = 0;
generation.textContent = "Generation: 0";
render();
updatePopulation();
}
function animate() {
_animate = !_animate;
if (_animate) {
advance();
btnAnimate.textContent = "Stop";
} else {
btnAnimate.textContent = "Animate";
}
}
function advance() {
var _population = 0;
for (var x = 0; x < _size; x++)
for (var y = 0; y < _size; y++) {
var neighbours = _torus1.getNeighbours(x, y); // dim 5 array
var alive = 0;
var kind = _torus1.get(x, y);
if (kind > 0) {
// its alive and stay alive if it has 2 or 3 neighbours
var count = neighbours[kind];
alive = (count == 2 || count == 3) ? kind : 0;
}
else {
// Its dead but will be born if any "kind" has exactly 3 neighbours
// This isn't "fair" but we use the first kind that has three neightbours
for (kind = 1; kind <= 4 && alive == 0; kind++) {
if (neighbours[kind] == 3)
alive = kind;
}
}
_torus2.set(x, y, alive);
if (alive)
_population++;
}
var temp = _torus1; // arrays are only references!
_torus1 = _torus2;
_torus2 = temp;
render();
generation.textContent = "Generation: " + String(++_generation);
population.textContent = "Population: " + String(_population);
if (_animate)
setTimeout("advance()", 50);
}
function renderCanvas(canvas, size, torus) {
// read from Life and write to canvas
var context = canvas.getContext('2d');
context.fillStyle = '#ff7f50';
context.clearRect(0, 0, size * _cellSize, size * _cellSize);
for (var x = 0; x < size; x++)
for (var y = 0; y < size; y++) {
var kind = _torus1.get(x, y) - 1;
if (kind >= 0) {
context.fillStyle = color_menu0.options[kind].value;
context.fillRect(x * _cellSize, y * _cellSize, _cellSize, _cellSize);
}
}
}
function render() {
renderCanvas(canvas1, _size, _torus1);
}
function drawGrid() {
// Only ever called once!
var context = canvas2.getContext('2d'); // canvas2 is the background canvas
context.strokeStyle = '#808080';
context.beginPath();
for (var i = 0; i <= _size; i++) {
// Draw vertical lines
context.moveTo(i * _cellSize + 0.5, 0.5);
context.lineTo(i * _cellSize + 0.5, _size * _cellSize);
// Draw horizontal lines
context.moveTo(0.5, i * _cellSize + 0.5);
context.lineTo(_size * _cellSize, i * _cellSize + 0.5);
}
context.stroke();
}
drawGrid();
canvas1.onmousedown = function canvasMouseDown(ev) {
isMouseDown = true;
var x = ev.pageX - this.offsetLeft;
var y = ev.pageY - this.offsetTop;
var coords = this.relMouseCoords(ev);
setPoint(coords.x, coords.y);
}
canvas1.onmouseup = function canvasMouseDown(ev) {
isMouseDown = false;
}
canvas1.onmousemove = function canvasMouseDown(ev) {
if (isMouseDown) {
var coords = this.relMouseCoords(ev);
setPoint(coords.x, coords.y);
}
}
function setPoint(x, y) {
// convert to torus coords
var i = Math.floor(x / _cellSize);
var j = Math.floor(y / _cellSize);
// Which kind
var kind = 1 + color_menu0.selectedIndex;
_torus1.set(i, j, kind);
render();
updatePopulation();
}
function updatePopulation() {
var _population = 0;
for (var x = 0; x < _size; x++)
for (var y = 0; y < _size; y++) {
if (_torus1.get(x, y))
_population++;
}
population.textContent = "Population: " + String(_population);
}
</script>
</body>
Our output looks like this: