joshbright.com

Game of Life: Part 2

Published on: Tuesday, February 20, 2024 at 4:00 PM PST

Written by Josh Bright

In Part 1, we started building the Game of Life in Javascript. We ended that part with drawing a checkboard type of pattern. In this post we will get the actual game of life working.

General Ideas

What we will be doing to get this working is to generate a 2d array of 0s and 1s, which will represent the cells in our grid, with 0 being a dead cell and 1 being an alive cell. Once we have that 2d array, we need to be able to render it to the canvas.

The next step after that is to click a step button which will run all the cells through the game of life rules and update our grid. We then render that grid.

Generating our grid

We currently have some integers defining how many cells our grid uses:

<script>
// ...

// Size information
let cellSize = 20;
let cellsWide = 10;
let cellsHigh = 10;

// ...

We can use that information to build our grid:

<script>
const generateGrid = () => {
	let result = [];
	for (let y = 0; y < cellsHigh; y++) {
		let row = [];
		for (let x = 0; x < cellsWide; x++) {
			row.push(0);
		}
		result.push(row);
	}
	return result;
}
</script>

Here we are going through two loops to build our grid. The first loop goes over each row, and the second loop goes over each column in that row. For now we just set our cells to all be dead.

Rendering the grid

If we were to render our grid right now, it is going to just be empty, so lets set some alive cells first

<script>
let grid = generateGrid();

grid[5][5] = 1;
grid[5][6] = 1;
grid[5][7] = 1;
grid[6][6] = 1;
</script>

Next, lets write a function to take in our grid, and render it to the canvas

<script>
const renderGrid = (grid) => {
	let color, cell;

	for (y = 0; y < cellsHigh; y++) {
		for (x = 0; x < cellsWide; x++) {
			cell = grid[y][x];

			if (cell == 0) {
				color = "black";
			} else {
				color = "gray";
			}
			ctx.fillStyle = color;
			ctx.fillRect(
				x * cellSize,
				y * cellSize,
				cellSize,
				cellSize
			);
		}
	}
}
</script>

Here we are doing a similar two loop process to render the grid. For each cell, we check if the cell is alive or dead, and we set a color based on that. After finding what color our cell is, we draw a rectangle for that cell.

(Show example)

Preparing for Stepping

Now that we can render our grid, lets get to the meat of the game of life, updating our cells. One tool we will need to use is the ability to know how many alive neighbors a cell has. To do this, we need to check the surrounding 8 cells and count how many of those are alive. We also need to deal with situations where we are at an edge of the grid and we don’t have all 8 cells to check.

<script>
const countNeighbors = (x, y, grid) => {
	let result = 0;

	// top left
	if ((y >= 1) && (x >= 1) && (grid[y - 1][x - 1] == 1)) {
		result += 1;
	}

	// top
	if ((y >= 1) && (grid[y - 1][x] == 1)) {
		result += 1;
	}

	// top right
	if ((y >= 1) && (x <= grid[0].length) && (grid[y - 1][x + 1] == 1)) {
		result += 1;
	}

	// left
	if ((x >= 1) && (grid[y][x - 1] == 1)) {
		result += 1;
	}

	// right
	if ((x < grid[y].length - 1) && grid[y][x + 1] == 1) {
		result += 1;
	}

	// bottom left
	if ((y < grid.length - 1) && (x >= 1) && (grid[y + 1][x - 1] == 1)) {
		result += 1;
	}

	// bottom
	if ((y < grid.length - 1) && (grid[y + 1][x] == 1)) {
		result += 1;
	}

	// bottom right
	if ((y < grid.length - 1) && (x < grid[0].length) && (grid[y + 1][x + 1] == 1)) {
		result += 1;
	}

	return result;
}
</script>

Here you can see how we check each of the 8 slots for an alive neighbor. In each if check, we first see if we have a valid cell to check, and then if that cell is a 1.

Another helper function we are going to use is a way to tell what we need to do based upon how many alive neighbors a cell has. This represents the rules of the game of life.

<script>
const stepRule = (val, cell) => {
	let result;

	if (cell === 0) {
		if (val == 3) {
			result = 1; // (Rule 4)
		} else {
			result = 0;
		}
	} else {
		if (val < 2) {
			result = 0; // (Rule 1)
		} else if (val <= 3) {
			result = 1; // (Rule 2)
		} else {
			result = 0; // (Rule 3)
		}
	}

	return result;
}
</script>

As a reminder, the rules of the game of life are:

  1. If a cell is alive, and has less than two neighbors, it will die.
  2. If a cell is alive, and has two or three neighbors, it stays alive.
  3. If a cell is alive, and has more than three neighbors, it dies.
  4. If a cell is dead, and has three live neighbors, it will become alive.

Now we need a function to take our existing grid, and generate a new one based on the rules.

<script>
const step = (grid) => {
	let neighborCount, cell;
	let result = [];
	for (let y = 0; y < grid.length; y++) {
		result.push([]);
		for (let x = 0; x < grid[0].length; x++) {
			let cell = grid[y][x];
			neighborCount = countNeighbors(x, y, grid);
			let stepVal = stepRule(neighborCount, cell);

			result[y].push(stepVal);
		}
	}

	return result;
};
</script>

Here we are taking in a grid, and doing the same two loop thing to go over each cell. We find out how many neighbors that cell has, and then we pass that neighbor count into our step rule function. This returns the new state of the cell we are checking.

When doing this step, it is important that we are not changing the existing grid. The new step needs to be a new grid, as we don’t want our neighbor checking to be looking at the new state of the cells while we are processing them.

After we run our step and get a new grid, we then just need to render that new grid.

Putting it all together

The final step is that we need a way to actually run the step. We will create a button, add an event listener to it, and when we click on it, we will run the step function.

<button id="step-button">Step</button>

<script>
document.getElementById("step-button").addEventListener("click", (event) => {
	// Generate a new grid
	cells = step(cells);

	// Render the grid
	renderCanvas(cells);
})
</script>

One Last Thing!

To make this a bit more interesting, lets also create a button to randomize the grid. This makes stepping through the simulation much more interesting:

<script>
const generateGrid = (random=false) => {
	let result = [];

	for (let y = 0; y < cellsHigh; y++) {
		result.push([]);
		for (let x = 0; x < cellsWide; x++) {
			let cell = 0;

			if (random) {
				let random = Math.random();

				if (random <= 0.3) {
					cell = 1;
				}
			}

			result[y].push(cell);
		}
	}

	return result;
};
</script>

Here we are just updating our generateGrid function from earlier, but now we have a random argument that we pass into the function. This lets us know if we need to create a blank grid, or a random one. We now need to have two new buttons, a randomize, and a reset button.

<button id="randomize-button">Randomize</button>
<button id="reset-button">Reset</button>

<script>
document.getElementById("random-button").addEventListener("click", (event) => {
	cells = generateGrid(true);
	renderCanvas(cells);
});

document.getElementById("reset-button").addEventListener("click", (event) => {
	cells = generateGrid();
	renderCanvas(cells);
});
</script>

We should now be able to randomize our grid and step through it.

In part 3, we will add: