The Game of Life in one tweet
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.
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.
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.
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.
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).
((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)
The Tweet
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.