Conway’s Game of Life in JavaScript in < 1000 bytes

Justin Golden
9 min readApr 28, 2021

A story in three parts

Overview

The doTick function will take in a 2D grid of cells (2D array of 0s and 1s), perform one timestep of Conway’s Game of Life, and return the mutated grid.

For learning about Conway’s Game of Life, check out Wikipedia.

Step 1: Writing the Code

In this step, I just wrote the functioning code, and cleaned everything up. This is human-readable code. (1126 bytes)

function doTick(grid) {
// deep clone 2d array
let nextGrid = grid.map(arr=>arr.slice());
for(let x=0; x<grid.length; x++) {
for(let y=0; y<grid[x].length; y++) {
doRules(grid, x, y, nextGrid);
}
}
grid = nextGrid.map(arr=>arr.slice());
return grid;
}
function doRules(grid, x, y, nextGrid) {
let count = getAdjacentCount(grid, x, y);
// 1. a dead cell with 3 neighbors becomes alive
if(count == 3 && grid[x][y] == 0) nextGrid[x][y] = 1;
// 2. a living cell with 2 or 3 neighbors stays alive
else if(count > 1 && count < 4 && grid[x][y] == 1) nextGrid[x][y] = 1;
// 3. in any other case the cell dies
else nextGrid[x][y] = 0;
}
function getAdjacentCount(grid, x, y) {
let count = 0;
if(x > 0 && y > 0) count += grid[x-1][y-1];
if(x > 0) count += grid[x-1][y];
if(x > 0 && y < grid[x].length-1) count += grid[x-1][y+1];
if(y > 0) count += grid[x][y-1];
if(y < grid[x].length-1) count += grid[x][y+1];
if(x < grid.length-1 && y > 0) count += grid[x+1][y-1];
if(x < grid.length-1) count += grid[x+1][y];
if(x < grid.length-1 && y < grid[x].length+1) count += grid[x+1][y+1];
return count;
}

Step 2: Simplifying

In this step, I simplified each of the functions my moving them inside the doTick function. (934 bytes)

const doTick = grid => {
let nextGrid = grid.map(arr=>arr.slice());
for(let x=0; x<grid.length; x++) {
for(let y=0; y<grid[x].length; y++) {
let count = 0;
if(x > 0 && y > 0) count += grid[x-1][y-1];
if(x > 0) count += grid[x-1][y];
if(x > 0 && y < grid[x].length-1) count += grid[x-1][y+1];
if(y > 0) count += grid[x][y-1];
if(y < grid[x].length-1) count += grid[x][y+1];
if(x < grid.length-1 && y > 0) count += grid[x+1][y-1];
if(x < grid.length-1) count += grid[x+1][y];
if(x < grid.length-1 && y < grid[x].length+1) count += grid[x+1][y+1];
// 1. a dead cell with 3 neighbors becomes alive
if(count == 3 && grid[x][y] == 0) nextGrid[x][y] = 1;
// 2. a living cell with 2 or 3 neighbors stays alive
else if(count > 1 && count < 4 && grid[x][y] == 1) nextGrid[x][y] = 1;
// 3. in any other case the cell dies
else nextGrid[x][y] = 0;
}
}
return nextGrid.map(arr=>arr.slice());
}

Step 3: Compression

In this step I used an online code compressor (https://jscompress.com/) to do the rest of the work (whitespace, variable names, etc.) (447 bytes)

const doTick=n=>{let g=n.map(e=>e.slice());for(let t=0;t<n.length;t++)for(let l=0;l<n[t].length;l++){let e=0;0<t&&0<l&&(e+=n[t-1][l-1]),0<t&&(e+=n[t-1][l]),0<t&&l<n[t].length-1&&(e+=n[t-1][l+1]),0<l&&(e+=n[t][l-1]),l<n[t].length-1&&(e+=n[t][l+1]),t<n.length-1&&0<l&&(e+=n[t+1][l-1]),t<n.length-1&&(e+=n[t+1][l]),t<n.length-1&&l<n[t].length+1&&(e+=n[t+1][l+1]),3==e&&0==n[t][l]||1<e&&e<4&&1==n[t][l]?g[t][l]=1:g[t][l]=0}return g.map(e=>e.slice())};

Bonus: Making the Entire Webpage

Ok, I admittedly got a bit carried away here…

Adding our doTick to the rest of the page, we’ve got (1909 bytes):

<html><body>
<script>
let canvas, ctx;
window.onload = ( ()=> {
canvas = document.getElementById('canvas');
ctx = canvas.getContext('2d');
let grid = emptyGrid(200, 200);
setInterval(()=>{
grid = doTick(grid);
drawGrid(grid);
},100);
});
function emptyGrid(width, height) {
let grid = [];
for(let x=0; x<width; x++) {
grid[x] = [];
for(let y=0; y<height; y++) {
grid[x][y] = Math.random() >= 0.5;
}
}
return grid;
}
function drawGrid(grid) {
const width = grid.length, height = grid[0].length;
canvas.width = width;
canvas.height = height;
for(let x=0; x<width; x++) {
for(let y=0; y<height; y++) {
ctx.fillStyle = grid[x][y]==1?'#fff':'#000';
ctx.fillRect(x, y, 1, 1);
}
}
}
// --------function doTick(grid) {
// deep clone 2d array
let nextGrid = grid.map(arr=>arr.slice());
for(let x=0; x<grid.length; x++) {
for(let y=0; y<grid[x].length; y++) {
doRules(grid, x, y, nextGrid);
}
}
grid = nextGrid.map(arr=>arr.slice());
return grid;
}
function doRules(grid, x, y, nextGrid) {
let count = getAdjacentCount(grid, x, y);
// 1. a dead cell with 3 neighbors becomes alive
if(count == 3 && grid[x][y] == 0) nextGrid[x][y] = 1;
// 2. a living cell with 2 or 3 neighbors stays alive
else if(count > 1 && count < 4 && grid[x][y] == 1) nextGrid[x][y] = 1;
// 3. in any other case the cell dies
else nextGrid[x][y] = 0;
}
function getAdjacentCount(grid, x, y) {
let count = 0;
if(x > 0 && y > 0) count += grid[x-1][y-1];
if(x > 0) count += grid[x-1][y];
if(x > 0 && y < grid[x].length-1) count += grid[x-1][y+1];
if(y > 0) count += grid[x][y-1];
if(y < grid[x].length-1) count += grid[x][y+1];
if(x < grid.length-1 && y > 0) count += grid[x+1][y-1];
if(x < grid.length-1) count += grid[x+1][y];
if(x < grid.length-1 && y < grid[x].length+1) count += grid[x+1][y+1];
return count;
}
</script>
<canvas id="canvas"></canvas></body></html>

Of course this is meant to be human readable.

We can make this a little smaller by adding in our condensed doTick and putting the JS inside the body's onload and removing some of that stuff on the top (1648 bytes):

<html><body onload="
let grid = emptyGrid(200, 200);
setInterval(()=>{
grid = doTick(grid);
drawGrid(grid);
},100);
function emptyGrid(width, height) {
let grid = [];
for(let x=0; x<width; x++) {
grid[x] = [];
for(let y=0; y<height; y++) {
grid[x][y] = Math.random() >= 0.5;
}
}
return grid;
}
function drawGrid(grid) {
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
const width = grid.length, height = grid[0].length;
canvas.width = width;
canvas.height = height;
for(let x=0; x<width; x++) {
for(let y=0; y<height; y++) {
ctx.fillStyle = grid[x][y]==1?'#fff':'#000';
ctx.fillRect(x, y, 1, 1);
}
}
}
const doTick = grid => {
let nextGrid = grid.map(arr=>arr.slice());
for(let x=0; x<grid.length; x++) {
for(let y=0; y<grid[x].length; y++) {
let count = 0;
if(x > 0 && y > 0) count += grid[x-1][y-1];
if(x > 0) count += grid[x-1][y];
if(x > 0 && y < grid[x].length-1) count += grid[x-1][y+1];
if(y > 0) count += grid[x][y-1];
if(y < grid[x].length-1) count += grid[x][y+1];
if(x < grid.length-1 && y > 0) count += grid[x+1][y-1];
if(x < grid.length-1) count += grid[x+1][y];
if(x < grid.length-1 && y < grid[x].length+1) count += grid[x+1][y+1];
// 1. a dead cell with 3 neighbors becomes alive
if(count == 3 && grid[x][y] == 0) nextGrid[x][y] = 1;
// 2. a living cell with 2 or 3 neighbors stays alive
else if(count > 1 && count < 4 && grid[x][y] == 1) nextGrid[x][y] = 1;
// 3. in any other case the cell dies
else nextGrid[x][y] = 0;
}
}
return nextGrid.map(arr=>arr.slice());
}"><canvas id="canvas"></canvas></body></html>

Now, before compressing we can make grid, canvas, and ctx global, get the canvas by tag name, and condense our functions into one. This is about all the simplification I can think to do before minification (other than remove let from the variables which I’ll do right before minifying) (1530 bytes):

<html><body onload="
let grid = [], width = 200, height = 200;
// empty grid
for(let x=0; x<width; x++) {
grid[x] = [];
for(let y=0; y<height; y++) {
grid[x][y] = Math.random() >= 0.5;
}
}
setInterval(()=>{
// next tick
let nextGrid = grid.map(arr=>arr.slice());
for(let x=0; x<grid.length; x++) {
for(let y=0; y<grid[x].length; y++) {
let count = 0;
if(x > 0 && y > 0) count += grid[x-1][y-1];
if(x > 0) count += grid[x-1][y];
if(x > 0 && y < grid[x].length-1) count += grid[x-1][y+1];
if(y > 0) count += grid[x][y-1];
if(y < grid[x].length-1) count += grid[x][y+1];
if(x < grid.length-1 && y > 0) count += grid[x+1][y-1];
if(x < grid.length-1) count += grid[x+1][y];
if(x < grid.length-1 && y < grid[x].length+1) count += grid[x+1][y+1];
// 1. a dead cell with 3 neighbors becomes alive
if(count == 3 && grid[x][y] == 0) nextGrid[x][y] = 1;
// 2. a living cell with 2 or 3 neighbors stays alive
else if(count > 1 && count < 4 && grid[x][y] == 1) nextGrid[x][y] = 1;
// 3. in any other case the cell dies
else nextGrid[x][y] = 0;
}
}
grid = nextGrid.map(arr=>arr.slice());
// draw grid
let canvas = document.getElementsByTagName('canvas')[0];
let ctx = canvas.getContext('2d');
const width = grid.length, height = grid[0].length;
canvas.width = width;
canvas.height = height;
for(let x=0; x<width; x++) {
for(let y=0; y<height; y++) {
ctx.fillStyle = grid[x][y]==1?'#fff':'#000';
ctx.fillRect(x, y, 1, 1);
}
}
},100);"><canvas></canvas></body></html>

Removing let and const gets me to 1480 bytes before minifying. This could also be minified further by removing the option to put width and height in a variable, but I like being able to change it 😊

After minifying, I got this 992 byte mess:

<html><body onload='for(grid=[],width=200,height=200,x=0;x<width;x++)for(grid[x]=[],y=0;y<height;y++)grid[x][y]=.5<=Math.random();setInterval(()=>{for(nextGrid=grid.map(t=>t.slice()),x=0;x<grid.length;x++)for(y=0;y<grid[x].length;y++)(count=0)<x&&0<y&&(count+=grid[x-1][y-1]),0<x&&(count+=grid[x-1][y]),0<x&&y<grid[x].length-1&&(count+=grid[x-1][y+1]),0<y&&(count+=grid[x][y-1]),y<grid[x].length-1&&(count+=grid[x][y+1]),x<grid.length-1&&0<y&&(count+=grid[x+1][y-1]),x<grid.length-1&&(count+=grid[x+1][y]),x<grid.length-1&&y<grid[x].length+1&&(count+=grid[x+1][y+1]),3==count&&0==grid[x][y]||1<count&&count<4&&1==grid[x][y]?nextGrid[x][y]=1:nextGrid[x][y]=0;for(grid=nextGrid.map(t=>t.slice()),canvas=document.getElementsByTagName("canvas")[0],ctx=canvas.getContext("2d"),width=grid.length,height=grid[0].length,canvas.width=width,canvas.height=height,x=0;x<width;x++)for(y=0;y<height;y++)ctx.fillStyle=1==grid[x][y]?"#fff":"#000",ctx.fillRect(x,y,1,1)},100);'><canvas></canvas></body></html>

But of course, the variable names are still there. I tried adding back let and const but it was of no use. Since there are only a handful of variables, I replaced their names manually (nextGrid is n, grid is g, width is w, height is h, count is i, canvas is c, ctx is t, I had to make sure not to replace the html name “canvas” or name ctx “x”, and not replace “grid” in “nextGrid”).

I then found I could remove the html tag (at least in Chrome, I’m not testing a piece of code golf in all major browsers…)

<body onload='for(g=[],w=200,h=200,x=0;x<w;x++)for(g[x]=[],y=0;y<h;y++)g[x][y]=.5<=Math.random();setInterval(()=>{for(n=g.map(t=>t.slice()),x=0;x<g.length;x++)for(y=0;y<g[x].length;y++)(i=0)<x&&0<y&&(i+=g[x-1][y-1]),0<x&&(i+=g[x-1][y]),0<x&&y<g[x].length-1&&(i+=g[x-1][y+1]),0<y&&(i+=g[x][y-1]),y<g[x].length-1&&(i+=g[x][y+1]),x<g.length-1&&0<y&&(i+=g[x+1][y-1]),x<g.length-1&&(i+=g[x+1][y]),x<g.length-1&&y<g[x].length+1&&(i+=g[x+1][y+1]),3==i&&0==g[x][y]||1<i&&i<4&&1==g[x][y]?n[x][y]=1:n[x][y]=0;for(g=n.map(t=>t.slice()),c=document.getElementsByTagName("canvas")[0],t=c.getContext("2d"),w=g.length,h=g[0].length,c.w=w,c.h=h,x=0;x<w;x++)for(y=0;y<h;y++)t.fillStyle=1==g[x][y]?"#fff":"#000",t.fillRect(x,y,1,1)},100);'><canvas></canvas></body>

There is is: my 745 byte masterpiece. You can visit https://justingolden.me/smallgameoflife/ to check it out, and to check the source code, just hit ctrl+U (to prove I’m not lying). To run it yourself, copy the code above and save it as “index.html”, then open with your browser.

Food for thought: I suspect that dynamically adding the canvas to the DOM then storing the reference in a variable may save a few bytes, and there must be a way shorter than 6 bytes to express black and white. Maybe there’s a way to fill a pixel in slightly less bytes than fillRect, and it now occurs to me that some of the logic checks may be avoidable if I were to just have a one pixel white or black border, and the logic checks don’t need the short circuit&& and can save a byte with & at the cost of performance. But these small changes are more than I care to put in after an already fairly large amount of time into this project, and most of these have a small chance of saving an insignificant amount of bytes, and I’m quite happy with my current total. Of course, changing the width and height to double-digit numbers would save two bytes as well… I could also change the canvas pixel render mode (image-rendering: pixelated;) with CSS to make it so zooming in doesn’t blur the pixels, but that would take a few more precious bytes 😊(you can always do this in browser by clicking ctrl+shift+C, then clicking the canvas, and pasting the code under element.style)

The repository is at https://github.com/justingolden21/smallgameoflife and the raw code can also be seen at https://raw.githubusercontent.com/justingolden21/smallgameoflife/main/index.html

Game of Life in Action

Conclusion: Assessing the Results

This of course pales in comparison to other peoples’ code, but I had to check anyway. You too can view the age old question on Code Golf if you wish.

If you enjoyed this article, feel free to give it a clap or two 😊

--

--

Justin Golden

Web developer living in San Diego. Love board and card games, basketball and football, and all things design.