It's ALIVE!! Eat your heart out Conway!
See the Pen Game of Life by Joe Peterson (@skullzarmy) on CodePen.
Initial Prompt
write a clean, well architected implementation of Conway's Game of Life in JavaScript. When the file is included in an HTML file it should be able to be invoked and passed a container id within which it should draw the game using HTML5 canvas
class GameOfLife {
constructor(canvas, width, height, cellSize) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
this.width = width;
this.height = height;
this.cellSize = cellSize;
this.columns = Math.floor(this.width / this.cellSize);
this.rows = Math.floor(this.height / this.cellSize);
this.grid = this.createGrid();
this.populateGrid();
}
createGrid() {
let grid = new Array(this.columns);
for (let i = 0; i < grid.length; i++) {
grid[i] = new Array(this.rows).fill(0);
}
return grid;
}
populateGrid() {
for (let i = 0; i < this.columns; i++) {
for (let j = 0; j < this.rows; j++) {
this.grid[i][j] = Math.floor(Math.random() * 2);
}
}
}
drawGrid() {
this.ctx.clearRect(0, 0, this.width, this.height);
for (let i = 0; i < this.columns; i++) {
for (let j = 0; j < this.rows; j++) {
if (this.grid[i][j] === 1) {
this.ctx.fillStyle = "#000";
this.ctx.fillRect(
i * this.cellSize,
j * this.cellSize,
this.cellSize,
this.cellSize,
);
}
}
}
}
getNextGridState() {
let newGrid = this.createGrid();
for (let i = 0; i < this.columns; i++) {
for (let j = 0; j < this.rows; j++) {
let neighbors = this.countNeighbors(i, j);
if (this.grid[i][j] === 1 && (neighbors === 2 || neighbors === 3)) {
newGrid[i][j] = 1;
} else if (this.grid[i][j] === 0 && neighbors === 3) {
newGrid[i][j] = 1;
}
}
}
return newGrid;
}
countNeighbors(x, y) {
let count = 0;
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) continue;
let newX = (x + i + this.columns) % this.columns;
let newY = (y + j + this.rows) % this.rows;
count += this.grid[newX][newY];
}
}
return count;
}
update() {
this.grid = this.getNextGridState();
this.drawGrid();
}
}
function initGameOfLife(containerId, width = 800, height = 800, cellSize = 10) {
const container = document.getElementById(containerId);
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
container.appendChild(canvas);
const gameOfLife = new GameOfLife(canvas, width, height, cellSize);
function gameLoop() {
gameOfLife.update();
requestAnimationFrame(gameLoop);
}
gameLoop();
}
// Usage example:
// Include this JavaScript file in your HTML and then use the following line to initialize the Game of Life:
initGameOfLife("game-of-life", 800, 800, 10);
... that was easy...
Follow Up Prompt
would you keep count of the current generation, add a print out of it on the bottom right corner "Current Generation: XXX" that counts up each time you loop the game. Also, when (if) it runs out of updates to make and the game reaches a stale state, please update the print to Game Over! Total Generations: xxxx and then stop the loop
I've updated the implementation to include the generation counter, display it on the bottom right corner, and stop the loop when the game reaches a stale state:
class GameOfLife {
// ... (Previous class implementation)
drawGenerationCounter() {
this.ctx.fillStyle = "#000";
this.ctx.font = "16px Arial";
this.ctx.fillText(
`Current Generation: ${this.generations}`,
this.width - 200,
this.height - 10,
);
}
isGridEqual(grid1, grid2) {
for (let i = 0; i < this.columns; i++) {
for (let j = 0; j < this.rows; j++) {
if (grid1[i][j] !== grid2[i][j]) {
return false;
}
}
}
return true;
}
update() {
const newGrid = this.getNextGridState();
if (this.isGridEqual(this.grid, newGrid)) {
this.drawGameOver();
return;
}
this.grid = newGrid;
this.drawGrid();
this.drawGenerationCounter();
this.generations++;
}
drawGameOver() {
this.ctx.fillStyle = "#000";
this.ctx.font = "16px Arial";
this.ctx.fillText(
`Game Over! Total Generations: ${this.generations}`,
this.width - 300,
this.height - 10,
);
}
}
function initGameOfLife(containerId, width = 800, height = 800, cellSize = 10) {
// ... (Previous initGameOfLife implementation)
gameOfLife.generations = 0;
function gameLoop() {
gameOfLife.update();
if (!gameOfLife.isGameOver) {
requestAnimationFrame(gameLoop);
}
}
gameLoop();
}
// Usage example:
// Include this JavaScript file in your HTML and then use the following line to initialize the Game of Life:
// initGameOfLife("containerId", 800, 800, 10);
Now, the generation counter will display at the bottom right corner and update each time the game loops. When the game reaches a stale state, the text will change to "Game Over! Total Generations: XXXX", and the loop will stop.
I realize now that "stale state" wasn't really a fair request without defining further, since invariably you will end up with infintely recursive shapes that always have a change, yet never really do. It was at this point I realized it was nearly 12:30 in the morning and that was good enough...