Photo by Kasturi Roy on Unsplash

The Game of Life Using JavaScript

Exploring Cellular Automata and Core JavaScript

gravity well
Feb 11 · 17 min read

The Game of Life was originally designed John Conway to study cellular automata.

I have enjoyed writing this program in every language I have learned and always used it as a teaching tool when teaching programming as it makes use of variables, arrays, loops, conditionals, functions, etc.

Our goal is to create a fun application that uses lots of core JavaScript.

Premise of The Game

One interacts with the game by creating an initial configuration of “inhabitants” and observing how they evolve by self replication through “generations”. From the application rules of some “live” and “die”.

The initial configuration above is in Bold because that’s the only “playing” the user gets to do. Then they sit back and watch evolution unfold.

Final Product

The final application can be seen and played with here if you’d like to preview what we will be building.

I know your time is valuable, and this article can be long. While I encourage you to code along, if necessary, copy and paste the code as well as study it.

The complete JavaScript code is also at the end of article.

The Rules

The rules for the game of life are simple. We start off by populating a “world” with as many “live” cells as we choose, the initial configuration. Then based on the rules below, we see our population grow, change and die off as generations go by.

  • Any live cell with fewer than two live neighbors dies, as if caused by under population.
  • Any live cell with two or three live neighbors lives on to the next generation.
  • Any live cell with more than three live neighbors dies, as if by overcrowding.
  • Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

We will use two-dimensional arrays to keep track of the current generation and the next generation, which is created after applying the rules.


We will use a table to build a grid for the display of the current generation. This is the visual for the player.

Visual Table. User has set some cells alive

Let’s Begin!

I will be using Visual Studio Code but any editor in which you can create HTML, CSS and JavaScript will do.

Setup

  1. Create an HTML page called index.html.

2. Create a JavaScript file called gol.js that will contain all the JavaScript code we need.

3. Create a Style Sheet file called gol.css that will contain all the styling we need.

HTML

Create a file called index.html

  • Add the script reference to our gol.js in the body.
  • Add the style sheet reference in the head.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=
, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Game of Life</title>
<!-- Our Style Sheet -->
<link rel="stylesheet" href='gol.css'>
</head>
<body>
<!-- Our JavaScript -->
<script src='gol.js'></script>
</body>
</html>

The Steps

Step One — Build the World

First we want to build our “world”, world grid, where our population will thrive (or not.) This will be done using an HTML table built with JavaScript. Again, this is the visual for the player.

First we will add a <div> element to contain the world grid in index.html and then build the HTML table and append it to the div.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=
, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Game of Life</title>
<!-- Our Style Sheet -->
<link rel="stylesheet" href='gol.css'>
</head>
<body>
<div id='world'>
</div><!-- Our JavaScript -->
<script src='gol.js'></script>
</body>
</html>

Next we will write the JavaScript for our world grid.

The world will be an HTML table. Each cell can contain an inhabitant. Whether a cell is inhabited (alive) or not (dead) is part of the initial configuration as well as the evolutionary process determined by the rules.

Note: In the original Game of Life, the world should be infinite. Sorry. Not ours.

We need some variables to store how many rows and columns we want in our table.

Then we will create a function, createWorld(), which will build the table, row by row, column by column and cell by cell.

Then we will append this to our div. We will call this function from the window.onload.

Add the following to gol.js.

const rows = 40;
const cols = 40;
function createWorld() {
let world = document.querySelector('#world');

let tbl = document.createElement('table');
tbl.setAttribute('id','worldgrid');
for (let i = 0; i < rows; i++) {
let tr = document.createElement('tr');
for (let j = 0; j < cols; j++) {
let cell = document.createElement('td');

tr.appendChild(cell);
}
tbl.appendChild(tr);
}
world.appendChild(tbl);
}
window.onload=()=>{
createWorld();
}

You will not see much until we add some styling to our table, so

add the following to gol.css

#world {
padding-bottom: 10px;
}
table {
background-color: whitesmoke;
}
td {
border: 1px solid darkgray;
width: 10px;
height: 10px;
}

Test Run

Open index.html and you should see your brave new world just waiting to be populated!

Our brave new world!

Step Two — The Cells

  • Initially, all cells will be dead.
  • When a cell is clicked by the player during initial configuration, before starting the generational process, we want the cells to toggle between being alive (blue) or dead (transparent.)
  • After that, we want to to be able to process generations, i.e., apply the rules.

So we need to attached a couple of attributes to the table cells.

  • An id attribute needs to be added to each cell so we can keep track of them. We will use i and j in the form i_j. So the upper left corner would be 0_0, etc.
  • A class of dead needs to be added to each cell. This class can be changed to alive during initial configuration or during evolution.

Add the following attributes to the cells (in bold),

const rows = 40;
const cols = 40;
function createWorld() {
let world = document.querySelector('#world');

let tbl = document.createElement('table');
tbl.setAttribute('id','worldgrid');
for (let i = 0; i < rows; i++) {
let tr = document.createElement('tr');
for (let j = 0; j < cols; j++) {
let cell = document.createElement('td');
cell.setAttribute('id', i + '_' + j);
cell.setAttribute('class', 'dead');


tr.appendChild(cell);
}
tbl.appendChild(tr);
}
world.appendChild(tbl);
}
window.onload=()=>{
createWorld();
}

Making Cells Come Alive

To toggle the cells between alive and dead, we will attach a click Event Listener to each cell. This is for the player to set up the initial configuration.

Later we will need to keep track of the current generation as well as the next generation, created after the rules are applied. For this we will need two two-dimensional arrays. One for the current and one for the next generation.

Note: JavaScript does not directly support two-dimensional arrays. We have to construct them as an array of arrays. More information on two-dimensional arrays can be found here.

Add the following listener and cellClick() function.

const rows = 40;
const cols = 40;
function createWorld() {
let world = document.querySelector('#world');

let tbl = document.createElement('table');
tbl.setAttribute('id','worldgrid');
for (let i = 0; i < rows; i++) {
let tr = document.createElement('tr');
for (let j = 0; j < cols; j++) {
let cell = document.createElement('td');
cell.setAttribute('id', i + '_' + j);
cell.setAttribute('class', 'dead');
cell.addEventListener('click',cellClick);
tr.appendChild(cell);
}
tbl.appendChild(tr);
}
world.appendChild(tbl);
}
function cellClick() {
let loc = this.id.split("_");
let row = Number(loc[0]);//Get i
let col = Number(loc[1]);//Get j
// Toggle cell alive or dead
if (this.className==='alive'){
this.setAttribute('class', 'dead');

}else{
this.setAttribute('class', 'alive');

}
}window.onload=()=>{
createWorld();
}

Add the following to the gol.css

td.dead {
background-color: transparent;
}
td.alive {
background-color:blue;
}

Test Run

Open index.html and you should be able to “toggle” a cell between alive (blue) and dead (transparent) by clicking on the cell. This is how they player will create the initial configuration of inhabitants.

Click to toggle alive or dead

Since the amount of code is getting long, feel free to place the functions we create wherever you’d like in the .js file.

Setting Up The Current and Next Generation Arrays

When we do the initial clicking, we are setting up the current (beginning) generation.

After we start processing generations, we will need to apply the rules to create the next generation. The next generation then becomes the current generation and so on as the world evolves.

We need to set up two-dimensional arrays to store the current and next generation (calculated after applying rules for “life” and “death”.)

The array declarations will establish one-dimensional arrays based on the number of rows. We will then create an array at each array location to create our required two-dimensional arrays.

The arrays need to match the the state of each of the table cells, alive or dead. We will use array value of 1 for alive and 0 for dead.

We will initially set each array location to 0 (dead), to match the table.

Add the following array declarations and the functions createGenArrays(), which creates the arrays and initGenArrays(), which sets the array values to 0 (dead).

I placed my code after the rows and cols declaration and above the createWorld() function.

const rows = 40;
const cols = 40;
// Need 2D arrays. These are 1D
let currGen =[rows];
let nextGen =[rows];
// Creates two-dimensional arrays
function createGenArrays() {
for (let i = 0; i < rows; i++) {
currGen[i] = new Array(cols);
nextGen[i] = new Array(cols);
}
}
function initGenArrays() {
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
currGen[i][j] = 0;
nextGen[i][j] = 0;
}
}
}

We will add the createGenArrays() and initGenArrays() function calls to our onload and add two lines to out cellClick() function.

Your code should look as follows.

  • Notice the currGen addition to cellClick().
  • Notice the additions to onlaod.
const rows = 40;
const cols = 40;
// Need 2D arrays. These are 1D
let currGen =[rows];
let nextGen =[rows];
// Creates two-dimensional arrays
function createGenArrays() {
for (let i = 0; i < rows; i++) {
currGen[i] = new Array(cols);
nextGen[i] = new Array(cols);
}
}
function initGenArrays() {
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
currGen[i][j] = 0;
nextGen[i][j] = 0;
}
}
}
function createWorld() {
let world = document.querySelector('#world');

let tbl = document.createElement('table');
tbl.setAttribute('id','worldgrid');
for (let i = 0; i < rows; i++) {
let tr = document.createElement('tr');
for (let j = 0; j < cols; j++) {
let cell = document.createElement('td');
cell.setAttribute('id', i + '_' + j);
cell.setAttribute('class', 'dead');
cell.addEventListener('click',cellClick);
tr.appendChild(cell);
}
tbl.appendChild(tr);
}
world.appendChild(tbl);
}
function cellClick() {
let loc = this.id.split("_");
let row = Number(loc[0]);
let col = Number(loc[1]);
// Toggle cell alive or dead
if (this.className==='alive'){
this.setAttribute('class', 'dead');
currGen[row][col] = 0;
}else{
this.setAttribute('class', 'alive');
currGen[row][col] = 1;
}
}
window.onload=()=>{
createWorld();// The visual table
createGenArrays();// current and next generations
initGenArrays();//Set all array locations to 0=dead
}

Evolution-Count Neighbors and Apply the Rules

We are at the point now where we need to write code to perform the evolution by applying the rules and creating a next generation.

We need to,

  • Count the adjacent cells/neighbors of each table cell that are alive.
  • Apply the rules based on the neighbor count to create the next generation.
  • Make the new generation the current generation.
  • Create a way to display the current (new) generation (update the world table.)
  • Create a temporary button to Start the process and perform generation changes. This button will be for testing only.

Eventually we want buttons for starting an automatic generational process.


Initially, for testing, we will only go from the current generation to the next manually. But then we will use a loop to process as many generations as possible.


Counting Neighbors

The rules for whether a cell becomes alive or dies all depends on the number of neighbors a given cell has.

Let’s add a function, getNeighborCount(), to count the number of neighbors for a given cell.

After that, in another function, a we will loop through the cells in the current generation, count the neighbors using getNeighborCount() and apply the rules, creating the next generation, which is stored in the stored in the nextGen array.

We have to look the eight adjacent cells/neighbors,

  • Above
  • Upper Left
  • Upper Right
  • Left
  • Right
  • Bottom Left
  • Bottom Right
  • Bottom

When we count neighbors, we have to make sure we are not looking “out of bounds”, outside the world grid. To do this we will always make sure when looking at adjacent cells, that the current row and/or column is not less than 0.

You can add the following function anywhere. I put mine at the bottom of the JavaScript.

function getNeighborCount(row, col) {
let count = 0;
let nrow=Number(row);
let ncol=Number(col);

// Make sure we are not at the first row
if (nrow - 1 >= 0) {
// Check top neighbor
if (currGen[nrow - 1][ncol] == 1)
count++;
}
// Make sure we are not in the first cell
// Upper left corner
if (nrow - 1 >= 0 && ncol - 1 >= 0) {
//Check upper left neighbor
if (currGen[nrow - 1][ncol - 1] == 1)
count++;
}
// Make sure we are not on the first row last column
// Upper right corner
if (nrow - 1 >= 0 && ncol + 1 < cols) {
//Check upper right neighbor
if (currGen[nrow - 1][ncol + 1] == 1)
count++;
}
// Make sure we are not on the first column
if (ncol - 1 >= 0) {
//Check left neighbor
if (currGen[nrow][ncol - 1] == 1)
count++;
}
// Make sure we are not on the last column
if (ncol + 1 < cols) {
//Check right neighbor
if (currGen[nrow][ncol + 1] == 1)
count++;
}
// Make sure we are not on the bottom left corner
if (nrow + 1 < rows && ncol - 1 >= 0) {
//Check bottom left neighbor
if (currGen[nrow + 1][ncol - 1] == 1)
count++;
}
// Make sure we are not on the bottom right
if (nrow + 1 < rows && ncol + 1 < cols) {
//Check bottom right neighbor
if (currGen[nrow + 1][ncol + 1] == 1)
count++;
}


// Make sure we are not on the last row
if (nrow + 1 < rows) {
//Check bottom neighbor
if (currGen[nrow + 1][ncol] == 1)
count++;
}


return count;
}

Creating The Next Generation

Now we will create a function, createNextGen(), which will loop through each cell, get the neighbor count using getNeighborCount(), and use this with the rules to determine if the current cell is to stay alive, come alive, stay dead or die.

Create the following function placing it anywhere in your JavaScript. I put mine above getNeighborCount().

function createNextGen() {
for (row in currGen) {
for (col in currGen[row]) {

let neighbors = getNeighborCount(row, col);

// Check the rules
// If Alive
if (currGen[row][col] == 1) {

if (neighbors < 2) {
nextGen[row][col] = 0;
} else if (neighbors == 2 || neighbors == 3) {
nextGen[row][col] = 1;
} else if (neighbors > 3) {
nextGen[row][col] = 0;
}
} else if (currGen[row][col] == 0) {
// If Dead or Empty

if (neighbors == 3) {
// Propogate the species
nextGen[row][col] = 1;//Birth?
}
}
}
}

}

Whew. Hang in there!

Updating the World

Now that we have computed the next generation, stored in the nextGen array, we need to update the currGen array with the values in nextGen and update our world table accordingly.

We will also create a temporary button to run through generations for testing.

We will add a function, updateCurrGen(), which will take nextGen array values and put them in currGen array.

We will also add a function, updateWorld(), which will update the visual display of our world.

Add the following functions.

function updateCurrGen() {

for (row in currGen) {
for (col in currGen[row]) {
// Update the current generation with
// the results of createNextGen function
currGen[row][col] = nextGen[row][col];
// Set nextGen back to empty
nextGen[row][col] = 0;
}
}

}
function updateWorld() {
let cell='';
for (row in currGen) {
for (col in currGen[row]) {
cell = document.getElementById(row + '_' + col);
if (currGen[row][col] == 0) {
cell.setAttribute('class', 'dead');
} else {
cell.setAttribute('class', 'alive');
}
}
}
}

Manual Testing

Now we will create a simple function, evolve(), which will be called from our temporary button (to be added.)

Add the following function.

function evolve(){

createNextGen();//Apply the rules
updateCurrGen();//Set Current values from new generation
updateWorld();//Update the world view
}

Update index.html’s body as below with the temporary button.

<body>
<div id='world'>
</div>
<div>
<input type='button' value='Evolve' onclick='evolve()'.>
</div>

<!-- Our JavaScript -->
<script src='gol.js'></script>
</body>

Test Run

Open index.html and click some cells to start your civilization. Then click the Evolve button as often as desired to see it evolve.

Here are my initial, first, second and tenth generations.

Left to right: initial, first, second and tenth.

Adding Some Player Controls

Now that we have verified it works, we can add some player controls. We will add the ability for the user to,

  • Start the reproduction process — generations and let it keep running to continually evolve.
  • Stop the reproduction process. Stop evolving.
  • Reset the World.

The Start and Stop will be the same button. After the play clicks Start, it will change to Stop.

Note: It will be important that the player clicks Stop even if all the inhabitants die off. The process runs regardless.

Remove the temporary button and add the following buttons to the body of index.html.

<body>
<div id='world'>
</div>
<div>

<Input type='button' id='btnstartstop' value='Start Reproducing'/>
<Input type='button' id='btnreset' value='Reset World'/>


</div>
<!-- Our JavaScript -->
<script src='gol.js'></script>
</body>
Player Controls

The Start Button

For Start/Stop to work, we will need some variables.

  • We need to know if the Start/Stop button has been clicked (know if it’s in a Start or Stop state.)
  • We need to be able to repeat the evolution process.
  • We need to control the speed of evolution.

Add the following near the top of your JavaScript.

let started=false;// Set to true when use clicks start
let timer;//To control evolutions
let evolutionSpeed=1000;// One second between generations

Next we need a function to Start and Stop the evolutionary process.

If we are in a Start state, we need a loop to run, calling the evolve() function and use the timer to control speed of evolution.

We will create a function, startStopGol() and add some code to our evolve() function.

Add the following function.

function startStopGol(){
let startstop=document.querySelector('#btnstartstop');

if (!started) {
started = true;
startstop.value='Stop Reproducing';
evolve();

} else {
started = false;
startstop.value='Start Reproducing';
clearTimeout(timer);
}
}

Notice this calls the evolve() function. Once in the evolve() function we need to continue evolving until the user clicks Stop.

Modify the evolve() as follows,

function evolve(){

createNextGen();//Apply the rules
updateCurrGen();//Set Current values from new generation
updateWorld();//Update the world view
if (started) {
timer = setTimeout(evolve, evolutionSpeed);
}
}

The Reset Button

To reset we will simply reload the page. That will re-initialze everything.

Add the following function.

function resetWorld() {
location.reload();
}

The last step is to attach the startStopGol() and resetWorld() functions to the buttons.

For this we will go old school and use the onclick. We will also add some instructions.

Modify the body of index.html as follows.

<h2>Click on table cells to toggle the cells as alive or dead.</h2>
<h3>Click the Start Reproducing button to Start and Stop</h3>
<Input type='button' id='btnstartstop' value='Start Reproducing' onclick='startStopGol();'/>
<Input type='button' id='btnreset' value='Reset World' onclick='resetWorld();'/>

Done!

You should now have a complete Game of Life!

All the code for gol.js is at the end of the article.

Conclusion

The Game Of Life is all about cellular automata so I encourage you to study the concept and play with the rules. Look at some variations of the Game Of Life.

But it also makes for a fun game as well as allowing us to use some great JavaScript. I also encourage you to experiment with the application and improve upon the code.

Thank you for reading and happy coding!

You may find this of interest.

gol.js

const rows = 40;
const cols = 40;
let started=false;// Set to true when use clicks start
let timer;//To control evolutions
let evolutionSpeed=1000;// One second between generations
// Need 2D arrays. These are 1D
let currGen =[rows];
let nextGen =[rows];
// Creates two-dimensional arrays
function createGenArrays() {
for (let i = 0; i < rows; i++) {
currGen[i] = new Array(cols);
nextGen[i] = new Array(cols);

}
}
function initGenArrays() {
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
currGen[i][j] = 0;
nextGen[i][j] = 0;
}
}
}
function createWorld() {
let world = document.querySelector('#world');

let tbl = document.createElement('table');
tbl.setAttribute('id','worldgrid');
for (let i = 0; i < rows; i++) {
let tr = document.createElement('tr');
for (let j = 0; j < cols; j++) {
let cell = document.createElement('td');
cell.setAttribute('id', i + '_' + j);
cell.setAttribute('class', 'dead');
cell.addEventListener('click',cellClick);
tr.appendChild(cell);
}
tbl.appendChild(tr);
}
world.appendChild(tbl);
}
function cellClick() {
let loc = this.id.split("_");
let row = Number(loc[0]);
let col = Number(loc[1]);
// Toggle cell alive or dead
if (this.className==='alive'){
this.setAttribute('class', 'dead');
currGen[row][col] = 0;
}else{
this.setAttribute('class', 'alive');
currGen[row][col] = 1;
}
}
function createNextGen() {
for (row in currGen) {
for (col in currGen[row]) {

let neighbors = getNeighborCount(row, col);

// Check the rules
// If Alive
if (currGen[row][col] == 1) {

if (neighbors < 2) {
nextGen[row][col] = 0;
} else if (neighbors == 2 || neighbors == 3) {
nextGen[row][col] = 1;
} else if (neighbors > 3) {
nextGen[row][col] = 0;
}
} else if (currGen[row][col] == 0) {
// If Dead or Empty

if (neighbors == 3) {
// Propogate the species
nextGen[row][col] = 1;// Birth?
}
}
}
}

}
function getNeighborCount(row, col) {
let count = 0;
let nrow=Number(row);
let ncol=Number(col);

// Make sure we are not at the first row
if (nrow - 1 >= 0) {
// Check top neighbor
if (currGen[nrow - 1][ncol] == 1)
count++;
}
// Make sure we are not in the first cell
// Upper left corner
if (nrow - 1 >= 0 && ncol - 1 >= 0) {
//Check upper left neighbor
if (currGen[nrow - 1][ncol - 1] == 1)
count++;
}
// Make sure we are not on the first row last column
// Upper right corner
if (nrow - 1 >= 0 && ncol + 1 < cols) {
//Check upper right neighbor
if (currGen[nrow - 1][ncol + 1] == 1)
count++;
}
// Make sure we are not on the first column
if (ncol - 1 >= 0) {
//Check left neighbor
if (currGen[nrow][ncol - 1] == 1)
count++;
}
// Make sure we are not on the last column
if (ncol + 1 < cols) {
//Check right neighbor
if (currGen[nrow][ncol + 1] == 1)
count++;
}
// Make sure we are not on the bottom left corner
if (nrow + 1 < rows && ncol - 1 >= 0) {
//Check bottom left neighbor
if (currGen[nrow + 1][ncol - 1] == 1)
count++;
}
// Make sure we are not on the bottom right
if (nrow + 1 < rows && ncol + 1 < cols) {
//Check bottom right neighbor
if (currGen[nrow + 1][ncol + 1] == 1)
count++;
}


// Make sure we are not on the last row
if (nrow + 1 < rows) {
//Check bottom neighbor
if (currGen[nrow + 1][ncol] == 1)
count++;
}


return count;
}

function updateCurrGen() {

for (row in currGen) {
for (col in currGen[row]) {
// Update the current generation with
// the results of createNextGen function
currGen[row][col] = nextGen[row][col];
// Set nextGen back to empty
nextGen[row][col] = 0;
}
}

}
function updateWorld() {
let cell='';
for (row in currGen) {
for (col in currGen[row]) {
cell = document.getElementById(row + '_' + col);
if (currGen[row][col] == 0) {
cell.setAttribute('class', 'dead');
} else {
cell.setAttribute('class', 'alive');
}
}
}
}
function evolve(){

createNextGen();
updateCurrGen();
updateWorld();
if (started) {
timer = setTimeout(evolve, evolutionSpeed);
}
}function startStopGol(){
let startstop=document.querySelector('#btnstartstop');

if (!started) {
started = true;
startstop.value='Stop Reproducing';
evolve();

} else {
started = false;
startstop.value='Start Reproducing';
clearTimeout(timer);
}
}


function resetWorld() {
location.reload();

}
window.onload=()=>{
createWorld();// The visual table
createGenArrays();// current and next generations
initGenArrays();//Set all array locations to 0=dead
}

JavaScript in Plain English

Learn the web's most important programming language.

gravity well

Written by

Self-Employed Software Developer, Trainer, Consultant. Keeping up to date. I’ve noticed in over 28 years of programming, one’s current skills have a shelf life.

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade