The Game of Life in one tweet

Michal Koczkodon
11 min readJul 14, 2023

--

When I have nothing to do and I’m bored, I have a couple of favourite things that help me relax. One of those things is sitting down and enjoying a good beer. Sometimes, this stimulates my brain in a way that gives me ideas and leads me to one of my other favourite activities: doing useless things.

On a lovely Sunday, while sipping my beer and contemplating life, a wild thought suddenly appeared: Is it possible to fit a JavaScript implementation of the Game of Life in a single tweet? I couldn’t resist the urge to give it a try.

Scene from The Lion King Movie (1994) — Rafiki holds young Simba (replaced with Twitter logo on this image).
The Game of Life is like Circle of Life (source: The Lion King, 1994)

It’s not a board game

Well, if you’ve never heard of the Game of Life and you try to search for it on Google Images, you might get confused because you’ll come across numerous images of a board game called The Game of Life.

The Game of Life board game, 1991 edition (source: amazon.com)

It may seem quite complicated, so you might wonder — why on earth you would attempt to squeeze the entire logic of this game into 280 characters of code? Well, I wouldn’t. That’s not the Game of Life I have in mind. The one that appeared in the Google Images results is probably more widely known, but I had never heard of it before. It’s a pity because it looks way cooler than the game I’m referring to.

John Conway’s Game Of Life — that’s our subject today. It takes place on two-dimensional grid of cells. Each cell can be either dead or alive. The state of each cell can change after every round, depending on the state of its neighbours (cells that are horizontally, vertically, or diagonally adjacent):

  • an alive cell remains alive in the next round if it has two or three living neighbours; otherwise, it dies;
  • a dead cell becomes alive in the next round if it has exactly three living neighbours; otherwise, it remains dead.
Example of John Conway’s Game Of Life (animation)
John Conway’s Game Of Life

That’s pretty much all about it. If you wanna know more, here’s the article on Wikipedia.

Starting point

What do I actually mean when I say JavaScript implementation of the Game of Life? Well, I could simply create a basic function that takes the current state of the game, does some magic, and returns the state of the next round. That would be easy to fit into one tweet. However, I wanted to have something more comprehensive. The code should generate an initial (random) state of the game, run the game (infinitely), and provide a visual representation for each round.

I sat down in front of my notebook and started typing. After a couple of minutes, I had a functioning JavaScript implementation that worked exactly as I wanted.

function gameOfLife(sizeX, sizeY) {
let state = [];

for (let y = 0; y < sizeY; y++) {
state.push([])

for (let x = 0; x < sizeX; x++) {
const alive = !!(Math.random() < 0.5);
state[y].push(alive)
}
}

setInterval(() => {
console.clear()

const consoleOutput = state.map(row => {
return row.map(cell => cell ? 'X' : ' ').join('')
}).join('\n')

console.log(consoleOutput)

const newState = []

for (let y = 0; y < sizeY; y++) {
newState.push([])

for (let x = 0; x < sizeX; x++) {
let aliveNeighbours = 0

for (let ny = y - 1; ny <= y + 1; ny++) {
if (state[ny]) {
for (let nx = x - 1; nx <= x + 1; nx++) {
if (!(nx === x && ny === y) && state[ny][nx]) {
aliveNeighbours++
}
}
}
}

if (state[y][x] && (aliveNeighbours < 2 || aliveNeighbours > 3)) {
newState[y].push(false)
} else if (!state[y][x] && aliveNeighbours === 3) {
newState[y].push(true)
} else {
newState[y].push(state[y][x])
}
}
}

state = newState
}, 1000)
}

gameOfLife(20, 20)

Could it be done better? Yes, probably. But making it perfect at first run wasn’t the goal here. Making it perfect in terms of good code is not a goal at all. I just wanted to have some starting point that I would then reduce to make it as compact as it’s possible.

Short recording of running game of life code in Node.js
Code runs in Node.js and does what it is told to.

Let’s just briefly talk about what’s going on here. I defined a function called gameOfLife, that takes two arguments: sizeX and sizeY.These arguments are used to create a two-dimensional array called state, which is populated with random Boolean values. (done in nested for loops; true means cell is alive, false — dead).

Then setInterval is used to execute anonymous function every 1 second. Within this function, the current console output is cleared, and a new output is generated based on the current state. In this output, the X character represents living cells, while a space character represents dead ones.

Next — newState is created using another set of nested for loops. For each cell (represented by an x, y point), the function checks every possible neighbour (from x-1 to x+1 and from y-1 to y+1) and counts the number of aliveNeighbours. To ensure correctness, the current cell is excluded from this loop, as well as non-existent neighbours (e.g., x=-1, y=-1). Based on information of number of living neighbours new state of cell is set. In the end state is overwritten by newState.

Finally, the gameOfLife function is called, with the size set to 20 rows by 20 columns. And that’s all.

The Goal

Well I mentioned it somewhere between the lines at the beginning, and of course it’s in the title of this article, but just to make it clear —when I say tweet, I’m not referring to the sound made by birds. I’m referring to a message on Twitter (a social media platform), that can be posted by a regular user (not a blue one) and that’s limited to 280 characters.

That’s all I have for my code. Well, of course indentations and long variable names won’t make it easier to fit in one tweet, but I’m going to leave them in source code for better readability and then use uglify-js — to remove unnecessary white spaces / new lines, as well as to mangle variable names (to make one character long names instead of ones I use in the source).

Starting point code has 549 characters after running it through uglifier. I’ll need to reduce it by almost half to fit within a tweet.

function gameOfLife(t,f){let s=[];for(let o=0;o<f;o++){s.push([]);for(let e=0;e<t;e++){const l=!!(Math.random()<.5);s[o].push(l)}}setInterval(()=>{console.clear();const e=s.map(e=>{return e.map(e=>e?"X":" ").join("")}).join("\n");console.log(e);const o=[];for(let l=0;l<f;l++){o.push([]);for(let f=0;f<t;f++){let t=0;for(let o=l-1;o<=l+1;o++){if(s[o]){for(let e=f-1;e<=f+1;e++){if(!(e===f&&o===l)&&s[o][e]){t++}}}}if(s[l][f]&&(t<2||t>3)){o[l].push(false)}else if(!s[l][f]&&t===3){o[l].push(true)}else{o[l].push(s[l][f])}}}s=o},1e3)}gameOfLife(20,20);

Refactoring

All requirements set, there’s no time to waste, let’s start cutting down the code.

Mastodon level — declarations

First of all, I don’t really need to declare a named function first and then call it. I can convert it into a self-invoking function, like ((sizeX, sizeY) => {…})(20, 20) — that’s enough and takes up less space.

Next thing — variable declarations. Currently, I define variables when I need them, but that results in multiple appearances of let and const (5 chars long!) in the final code. Let’s instead use the good old ‘var’ keyword once and declare all variables at the beginning of the function.

((sizeX, sizeY) => {
var state = [],
y, x, consoleOutput, ny, nx, aliveNeighbours, newState;
...
})(20, 20)

Now, let’s allow uglify-js to do its job, and… 499 characters! It’s still far beyond the Twitter limit, but it’s suitable for Mastodon (another social media platform, kind of Twitter competitor).

Screenshot of mastodon post containing minimized Game of Life code
Mastodon post, you’ll find it here
((o,e)=>{var r=[],s,f,n,a,l,p,u;for(s=0;s<e;s++){r.push([]);for(f=0;f<o;f++){const h=!!(Math.random()<.5);r[s].push(h)}}setInterval(()=>{console.clear();n=r.map(o=>{return o.map(o=>o?"X":" ").join("")}).join("\n");console.log(n);u=[];for(s=0;s<e;s++){u.push([]);for(f=0;f<o;f++){p=0;for(a=s-1;a<=s+1;a++){if(r[a]){for(l=f-1;l<=f+1;l++){if(!(l===f&&a===s)&&r[a][l]){p++}}}}if(r[s][f]&&(p<2||p>3)){u[s].push(false)}else if(!r[s][f]&&p===3){u[s].push(true)}else{u[s].push(r[s][f])}}}r=u},1e3)})(20,20);

Initial state made right

Well, using nested for loops to make an initial state works, but I can do better. I’m going to use Array.from method — and it will be quite important player for further changes.

var state = Array.from(Array(sizeY), () => Array.from(Array(sizeX), () => Math.random() < .5 ? 'X' : ' ' ))

Array.from takes two arguments. The first one is required, which is an iterable object that will be converted to an array. The second one is optional and is a callback that is invoked for every element, where the value returned by the callback is inserted into the output array. Array(n) returns an array of length n filled with empty values, but the callback can override these values.

Since both Array.from and Array are used twice, I can save some space by assigning them to variables.

var array = Array,
arrayFrom = array.from,
state = arrayFrom(array(sizeY), () => arrayFrom(array(sizeX), () => Math.random() < .5 ? 'X' : ' ' )),

Well, it may not be clear at this point, but after the names are mangled, the code will be a few characters shorter.

You may have noticed that I’m no longer using booleans. Since I need X and space characters to create the console output, it’s easier to use them in state as well. Thanks to that the code for console management can be reduced:

console.clear()
console.log(state.map(row => row.join('')).join('\n'))

As you can see, I got rid of consoleOutput variable. Additionally, to save space through mangling, I can assign the console to a variable since it is used twice in the code:

...
conzole = console,
...
conzole.clear()
conzole.log(...)

Few more small adjustments (because of using X and space instead of booleans) and minified code has… 448 characters. Less than 200 to go.

((r,o)=>{var e=Array,f=e.from,s=console,a=f(e(o),()=>f(e(r),()=>Math.random()<.5?"X":" ")),i,l,n,h,p,m,t;setInterval(()=>{s.clear();s.log(a.map(r=>r.join("")).join("\n"));t=[];for(i=0;i<o;i++){t.push([]);for(l=0;l<r;l++){m=0;for(n=i-1;n<=i+1;n++){if(a[n]){for(h=l-1;h<=l+1;h++){if(!(h===l&&n===i)&&a[n][h]==="X"){m++}}}}p=a[i][l].trim();if(p&&(m<2||m>3)){t[i].push(" ")}else if(!p&&m===3){t[i].push("X")}else{t[i].push(a[i][l])}}}a=t},1e3)})(20,20);

Reduce to new state

From the beginning I didn’t really liked my implementation of newState. I’m a big fan of using array methods when working with arrays, so I decided to use reduce method to reduce usage of for loops. Additionally I assigned state indicators (X / space characters) to new variables. Furthermore, the assignment of the new state for cells is now handled better. The last improvement in this iteration is the substitution of the triple equal sign (===) with a double equal sign (==) for comparisons, which is sufficient and takes up less space.

...
alive = 'X',
dead = ' '
...
setInterval(() => {
...
state = state.map((row, y) => row.reduce((newRow, cell, x) => {
aliveNeighbours = 0

for (ny = y - 1; ny <= y + 1; ny++) {
for (nx = x - 1; nx <= x + 1; nx++) {
if (!(nx == x && ny == y) && state[ny]?.[nx] == alive) aliveNeighbours++
}
}

newRow.push(cell.trim()
? [2,3].includes(aliveNeighbours) ? alive : dead
: aliveNeighbours == 3 ? alive : dead
)

return newRow
}, []))
}, 1000)

Ok, so how much is it? 367 characters (minified). Nice, but still can’t post it on Twitter.

Appreciate what I already have

As I mentioned I’m a big fan of array methods (especially reduce). However, in this case, I’m using both map and reduce and the names of these methods consume space. I’ve already used Array.from and assigned it to a variable, after couple of minutes of staring at the code I realized that I can use it instead of map and reduce directly like this:

state = arrayFrom(state, (row, y) => arrayFrom(row, (cell, x) => {
aliveNeighbours = 0

for (ny = y - 1; ny <= y + 1; ny++) {
for (nx = x - 1; nx <= x + 1; nx++) {
if (!(nx == x && ny == y) && state[ny]?.[nx] == alive) aliveNeighbours++
}
}

return cell.trim()
? [2,3].includes(aliveNeighbours) ? alive : dead
: aliveNeighbours == 3 ? alive : dead
)
}))

Furthermore, the code that determines the new state of each cell still felt wrong (even though it worked correctly), and after a while, I finally came up with the best solution:

return aliveNeighbours == 3
? alive
: aliveNeighbours == 2 ? cell : dead

And after minification I got code that was 321 characters long.

((r,o)=>{var a=Array,n=a.from,e=console,f="X",l=" ",t=n(a(o),()=>n(a(r),()=>Math.random()<.5?f:l)),i,m,c;setInterval(()=>{e.clear();e.log(t.map(r=>r.join("")).join("\n"));t=n(t,(r,a)=>n(r,(r,o)=>{c=0;for(i=a-1;i<=a+1;i++){for(m=o-1;m<=o+1;m++){if(!(m==o&&i==a)&&t[i]?.[m]==f)c++}}return c===3?f:c===2?r:l}))},1e3)})(20,20);

We need to go deeper

Well, I got to the point where 40 characters felt like the size of an entire book. What else can I simplify? Following the rule of using what I already have (Array.from) then I can change this part:

conzole.log(state.map(row => row.join('')).join('\n'))

into this:

conzole.log(arrayFrom(state, (row) => row.join('')).join('\n'))

Of course, in its non-minified form, it is longer than the original. However, after minification, it was reduced to 319 characters, so I saved 2 characters — that’s… not much. Still 38 to go.

I don’t actually need to pass size as two separate arguments — it could be a single argument used for both x and y. Additionally, I can change it to 9 instead of 20, reducing it by one character. So, how much it is now? 311 characters of minified code.

What’s next? Well, I could use numbers — 0 to represent the dead state and 1 for the alive state. In both cases, it’s one character compared to the previous 3-character representations (0 instead of ' ' and 1 instead of 'X'). And since it’s only one character, I don’t really need to keep them as separate variables. So, what does this change? 299 chars. Feels so close.

Now that I have numbers as state indicators, I can make a small adjustment to the logic responsible for counting the number of aliveNeightbours:

...
for (ny = y - 1; ny < y + 2; ny++) {
for (nx = x - 1; nx < x + 2; nx++) {
if (state[ny]?.[nx] == 1) aliveNeighbours++
}
}

return aliveNeighbours - cell == 3
? 1
: aliveNeighbours - cell == 2 ? cell : 0

I no longer check if the potential neighbor has the same coordinates as the cell for which I’m counting the alive neighbors. Instead, I subtract the value of the cell from the final sum. Additionally, I changed nx <= x + 1 to nx < x + 2 (same for y) — in this case result is the same, but it takes one character less. So how is it now? 286 characters. Only 6 more to go!

I looked on the code that’s generated by uglify-js and realised, that it keeps curly brackets for the for loops — for (…){for(…){…}}. If I remove the curly brackets in the source code and keep it on one line, then it should keep it that way.

for (ny = y - 1; ny < y + 2; ny++) for (nx = x - 1; nx < x + 2; nx++) state[ny]?.[nx] == 1 && aliveNeighbours++

Now let’s run it through uglify-js, and…

Finally

I have exactly 280 characters. Well, technically it was 281 characters, but uglify-js adds a semicolon at the end of the code, which I don’t really need.

Here’s the final source code:

((size) => {
var array = Array,
arrayFrom = array.from,
conzole = console,
state = arrayFrom(array(size), () => arrayFrom(array(size), () => Math.random() < .5 ? 1 : 0 )),
ny, nx, aliveNeighbours;

setInterval(() => {
conzole.clear()

conzole.log(arrayFrom(state, (row) => row.join('')).join('\n'))

state = arrayFrom(state, (row, y) => arrayFrom(row, (cell, x) => {
aliveNeighbours = 0

for (ny = y - 1; ny < y + 2; ny++) for (nx = x - 1; nx < x + 2; nx++) state[ny]?.[nx] == 1 && aliveNeighbours++

return aliveNeighbours - cell == 3
? 1
: aliveNeighbours - cell == 2 ? cell : 0
}))
}, 1000)
})(9)
Working 280-characters long script of the game of life.
It has only 280 characters, and it works!

The Tweet

You can find the code here

Enough

Yes, it’s possible to cut off few characters more. You can try it if you like. Maybe you can do it even better than I did.

Thanks for your time! I hope you enjoyed the article. Don’t hesitate to give a clap or leave a comment!

You can follow me on medium, twitter or mastodon.

More from me:
How to create a personal website, but it’s 1999 — going back in time to the late ’90s to create a simple website.
Do not waste your money on developer certifications — why you should think twice before you spend your money to get another dev certificate.

--

--

Michal Koczkodon

Web dev. Lives and codes somewhere in Poland. JavaScript enjoyer and craft beer lover.