Versão PT-BR aqui.
Introduction:
Hi there, are you looking for cool projects to improve your Javascript, CSS and HTML skills?
In this tutorial I will teach how you can do your own version of the Game of Life, a game idea developed by the british mathematic John Conway.
This game is a part of an tag called “cellular automata”,which according to wikipedia mean: "simpler temporal evolution models capable of exhibiting complicated behavior"
But don't worry about this complex explanation, we're basically going to make a game without players, almost like it's alive.
This is the final result, a field populated by blocks that change their state based on predefined rules.
This is my Github repository below, to help you:
https://github.com/akadot/game-of-life
Okay, let's do it.
Construction:
To build this project, we will use a powerfull HTML resource called Canvas API, which allows shapes 2D or 3D forms using only Vanilla Javascript. But don't worry about this, everything is very simple, the Canvas API is a native HTML tag.
The first step is create the three files that we will use, starting by the HTML file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
<title>Jogin da Vida</title>
</head>
<body>
<canvas id="board"></canvas>
<script src="game.js"></script>
</body>
</html>
We will just use a <canvas>
tag, with an idreference to use in our JS file.
Now we'll create a simple CSS file:
* {
padding: 0;
margin: 0;
outline: 0;
box-sizing: border-box;
}
body {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background-color: #000000;
}
#board {
border: 5px solid #5c3ec9;
border-radius: 5px;
background-color: #f8f8f2;
box-shadow: 0px 0px 10px #5c3ec9;
}
Done, now we can open on our browser and...
Okay, I know, it's just an empty board, but I promise it will be very cool.
Now we need to set the Canvas properties, there are a lot of ways to do this, but I prefer to do everything inside the Javascript file.
Logic:
Let's really get your hands on the code. First we need to reference the <canvas>
tag id in our JS file, to define what context we will work (2D or 3D):
const canvas = document.querySelector("#board");
const ctx = canvas.getContext("2d");
//ctx define o contexto do nosso canvas, no caso será 2D
Then, we set some constants that will help us along the code:
const GRID_WIDTH = 500;
const GRID_HEIGHT = 500;
const RES = 5;
const COL = GRID_WIDTH / RES;
const ROW = GRID_HEIGHT / RES;
Now, we can use this constants to define the canvas height and width:
canvas.width = GRID_WIDTH;
canvas.height = GRID_HEIGHT;
Done, now I promisse we will can see something. But to get sure, let's put an addEventListener()
around the code, to wait our HTML content load before code runs:
document.addEventListener("DOMContentLoaded", () => {
const canvas = document.querySelector("#board");
const ctx = canvas.getContext("2d");
const GRID_WIDTH = 500;
const GRID_HEIGHT = 500;
const RES = 5;
const COL = GRID_WIDTH / RES;
const ROW = GRID_HEIGHT / RES;
canvas.width = GRID_WIDTH;
canvas.height = GRID_HEIGHT;
}
The next step is draw our blocks. They will be simples squares inside an 2D array, like a cell inside a Excel grid or an Google Sheets file.
We will create a function called createGrid(), that will receive the numbers of rows and columns. Then, the function will return a new empty array with the length equal the number of the columns and, for each position insite this array, it will be created a new array with the same number of the rows but filled filled with 0 or 1 randomly:
function createGrid(cols, rows) {
return new Array(cols)
.fill(null)
.map(() => new Array(rows)
.fill(null)
.map(() => Math.round(Math.random())));
}
let grid = createGrid(COL, ROW); //we will keep the array inside a variable "grid"
We can start to draw our blocks now, based on the cell values, where the numbers 1 will be filled and the numbers 0 will be blanked:
To do it, we need a new function called drawGrid(), that will receive our grid, our rows and columns and our block resolution/size:
function drawGrid(grid, cols, rows, reslution) {
ctx.clearRect(0, 0, cols, rows);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const cell = grid[i][j];
ctx.fillStyle = cell ? "#5c3ec9" : "#f8f8f2";
ctx.fillRect(i * reslution, j * reslution, reslution, reslution);
}
}
}
});
As you can see, to paint the cells fisrt we need to run the Canvas API native function clearRect(), that will clear our board before everything. It's receive on two first parameters the initial coordinates to start the cleaning, and on the last two parameters, we need to set the full size of our board, where the function will stop the cleaning.
Once that's done, let's do two repeat loops, to go through our entire array. For each loop, we will keep the current cell inside a constant called cell and, with a ternary if, we will check if the cell has a 0 or an 1.
If the cell's value is 1, we will apply a color #5c3ec9, using another Canvas API native property called fillStyle, else we just apply the same color of our background (remeber, in JS the value 1 means true/existing, and the value 0 means false/nonexistent).
On the next line, another native tag, but this time we will usa the function fillRect(), that will draw our square following 4 parameters:
- First: the X coordinate where the square inits (in this case, we will put our resolution * the current array position);
- Second: the Y coordinate where the square inits (in this case, we will put our resolution * the current array position again);
- Third: the square width (our resolution);
- Fourth: the square heigth (our resolution again).
Now we can draw our squares inside the canvas:
drawGrid(grid, COL, ROW, RES);
Explaining the Game Rules
Before we proceed, we need to understand the rules proposed by John Conway, so that the game is actually "self-playing".
There are four simple rules to do this, that defines if a cell is alive (our purple/1 cells), or dead (our black/0 cells). The rules are:
- 1: Any live cell with fewer than two live neighbours dies, as if by underpopulation;
- 2: Any live cell with two or three live neighbours lives on to the next generation;
- 3: Any live cell with more than three live neighbours dies, as if by overpopulation;
- 4: Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
Following this rules, let's create our function to do this. It's will go through our entire array, apply the rules and generate a new array to be drawn by the drawGrid() function.
At each repetition of this cycle, we will consider that the new array is a new generation of cells that inherit the last generation conditions.
This function will be called nexGen() and, as a first step we will keep the last geeration in a constant.
function nextGen(grid) {
const nextGen = grid.map((arr) => [...arr]);
In case you don't already know, in the [...arr]
excerpt we use the SPREAD operator, which was added to Javascript from version 6 and is intended to store a greater number of information at once, widely used with arrays and objects. You can also use the .push()
or .slice()
functions instead of the spread operator, there's no problem with that.
The next step is to start the loops, that will go through the array to apply the game rules. As we did above, we need to run through all the lines, using grid.length
and then all the columns, using grid[col].length
(the col parameter is just the name I gave the for control variable, but you can use the letters i and j as usual).
We'll take the opportunity to capture the initial cell in a constant and create a variable to count the number of living neighbor cells.
for (let col = 0; col < grid.length; col++) {
for (let row = 0; row < grid[col].length; row++) {
const currentCell = grid[col][row];
let sumNeighbors = 0;
The next step is, for each cell, to go through all its 8 neighbors and check whether they are alive or not.It might seem a little difficult to understand the code at first glance, but here's an explanation with screenshot:
Yes, I used Google Sheets for that, but the important thing is that our next loop will iterate through the values between -1 and 1, finding the number of live neighbors.
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
if (i === 0 && j === 0) {
continue;
}
We put the condition if (i === 0 && j === 0)
, because this is the position of the current cell, which we don't want to add to the number of neighbors.
The next section will deal with the "corners" of our field. Think about it like this, if a cell is pasted on the left side of our canvas, we won't be able to access the neighbors that are in a column before it, that is, the leftmost one, as they don't exist. So, we're going to add values to the sumNeighbors
variable only if its coordinates are within the bounds of the canvas.
const x = col + i
const y = row + j;
if (x >= 0 && y >= 0 && x < COL && y < ROW) {
const currentNeighbor = grid[col + i][row + j];
sumNeighbors += currentNeighbor;
Once the conditions are satisfied, the sumNeighbors
variable will receive its previous value, plus the value of the live cells, remembering that the dead cells here receive the value zero, which does not impact the sum.
Once that's done, we can apply the rules described by John Conway with a simple if/else
:
if (currentCell === 0 && sumNeighbors === 3) {
nextGen[col][row] = 1;
} else if (currentCell === 1 && (sumNeighbors < 2 || sumNeighbors > 3)){
nextGen[col][row] = 0;
}
Explaining, the first condition tests if the current cell is empty and if it has 3 neighbors, if it is true the next generation will receive in that same position the value 1 or alive.
The second condition gathers the other rules into one, testing whether the current cell is live and;if there are less than two neighbors next generation will receive zero, if there are more than 3 neighbors the next generation will also receive zero.
Finally, just return the next generation return nextGen;
, and the function will look like this:
function nextGen(grid) {
const nextGen = grid.map((arr) => [...arr]); //make a copy of grid with spread operator
for (let col = 0; col < grid.length; col++) {
for (let row = 0; row < grid[col].length; row++) {
const currentCell = grid[col][row];
let sumNeighbors = 0; //to verify the total of neighbors
//Verifying the 8 neigbours of current cell
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
if (i === 0 && j === 0) {
continue; // because this is the current cell's position
}
const x = col + i;
const y = row + j;
if (x >= 0 && y >= 0 && x < COL && y < ROW) {
const currentNeighbor = grid[col + i][row + j];
sumNeighbors += currentNeighbor;
}
}
}
//Aplying rules
if (currentCell === 0 && sumNeighbors === 3) {
nextGen[col][row] = 1;
} else if (
currentCell === 1 &&
(sumNeighbors < 2 || sumNeighbors > 3)
) {
nextGen[col][row] = 0;
}
}
}
return nextGen;
}
By doing this, we are almost close to finishing our project, the next step is very simple, we will create a function called update() to execute all the created functions in order, and we will use the requestAnimationFrame() function, native Javascript, to repeat the looping process in the browser.
requestAnimationFrame(update);
function update() {
grid = nextGen(grid);
drawGrid(grid, COL, ROW, RES);
requestAnimationFrame(update); //running again to repeat the loop
}
Okay, now everything is ready and your file should have looked like this:
document.addEventListener("DOMContentLoaded", () => {
const canvas = document.querySelector("#board");
const ctx = canvas.getContext("2d");
const GRID_WIDTH = 500;
const GRID_HEIGHT = 500;
const RES = 5;
const COL = GRID_WIDTH / RES;
const ROW = GRID_HEIGHT / RES;
canvas.width = GRID_WIDTH;
canvas.height = GRID_HEIGHT;
//Making a grid and filling with 0 or 1
function createGrid(cols, rows) {
return new Array(cols)
.fill(null)
.map(() =>
new Array(rows).fill(null).map(() => Math.round(Math.random()))
);
}
let grid = createGrid(COL, ROW);
requestAnimationFrame(update);
function update() {
grid = nextGen(grid);
drawGrid(grid, COL, ROW, RES);
requestAnimationFrame(update);
}
//Generate nex generation
function nextGen(grid) {
const nextGen = grid.map((arr) => [...arr]); //make a copy of grid with spread operator
for (let col = 0; col < grid.length; col++) {
for (let row = 0; row < grid[col].length; row++) {
const currentCell = grid[col][row];
let sumNeighbors = 0; //to verify the total of neighbors
//Verifying the 8 neigbours of current cell
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
if (i === 0 && j === 0) {
continue; // because this is the current cell's position
}
const x = col + i;
const y = row + j;
if (x >= 0 && y >= 0 && x < COL && y < ROW) {
const currentNeighbor = grid[col + i][row + j];
sumNeighbors += currentNeighbor;
}
}
}
//Aplying rules
if (currentCell === 0 && sumNeighbors === 3) {
nextGen[col][row] = 1;
} else if (
currentCell === 1 &&
(sumNeighbors < 2 || sumNeighbors > 3)
) {
nextGen[col][row] = 0;
}
}
}
return nextGen;
}
//Draw cells on canvas
function drawGrid(grid, cols, rows, reslution) {
ctx.clearRect(0, 0, cols, rows);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const cell = grid[i][j];
ctx.fillStyle = cell ? "#5c3ec9" : "#f8f8f2";
ctx.fillRect(i * reslution, j * reslution, reslution, reslution);
}
}
}
});
Now just run the HTML file to see this (or something better in your case, as I had some problems recording my screen):
Final Considerations
Although it doesn't seem like a big deal, this project is very interesting to train the basic knowledge of HTML, CSS and JS, mainly in the manipulation of arrays.In case you're interested, I'll leave some links to larger projects that used the same concepts as this game.
Creating the Game of Life in Excel - https://github.com/asgunzi/JogodaVidaExcel
The Video that inspired me, from the youtube channel O Programador (PT/BR) - https://youtu.be/qTwqL69PK_Y
I hope you enjoyed it and that you were able to learn something cool, always remember what Bob Ross said: "as long as you are learning, you are not failing".
Just keep going, however slowly.
See you. ✌️
Top comments (0)