Game of Life in one tweet

When I’m bored, I resort to one of the usual ways to relax and have fun. For example, I can treat myself to a glass of good beer. Sometimes, in the process of tasting, various ideas come to my mind, and it becomes difficult for me to restrain myself: inevitably, I start to fence another useless, but fun project.

One fine Sunday, while sipping a beer and thinking about life, I suddenly thought: is it possible to fit a JavaScript implementation of the Game of Life into one tweet? And he could not resist the desire to try his hand.

This is not a board game

Let’s say you’ve never heard of the Game of Life and suddenly decide to go to Google and find out what it even is. Most likely, the first thing that catches your eye will be such a desktop.

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

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

With a high degree of probability, it will seem quite complicated to you, and you will think – why on earth am I even trying to cram all the logic of this game into 280 characters of code? So. This is not the Game of Life you are looking for.

The game “Life” by John Conway (Game Of Life) – that’s just about it will be discussed in this article. All the action takes place on a two-dimensional field with cells. Each cell can be either dead or alive. The state of a cell can change after each round, depending on the state of its neighbors (cells that are adjacent horizontally, vertically, or diagonally):

  • a living cell will remain alive in the next round if it has two or three living neighbors, otherwise it dies;

  • a dead cell becomes alive in the next round if it has exactly three living neighbors, otherwise it remains dead.

Here's what it looks like

Here’s what it looks like

That, in fact, is all. For those who want to learn more about the game, there is an article in Wikipedia.

A starting point

What do I actually mean when I talk about the JavaScript implementation of the Game of Life? Of course, I could just write a basic function that takes the current state of the game, does some magic, and returns the state for the next round. It will easily fit in one tweet. But I wanted to get something more complex and independent. In my mind, the code was to generate an initial (random) state of the game, run the game in an infinite loop, and give a visual representation of each round.

I sat down at my laptop and started writing code. In just a couple of minutes, I had a workable JavaScript implementation that did exactly what 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 you write better code? I think, yes. But I did not have a goal from the first attempt to achieve the ideal. I did not set myself the task of writing an ideal implementation of the game from the point of view of the code. All I needed was a starting point, a primary code that I would reduce and compact as much as possible.

Code runs in Node.js and does what it's told

Code runs in Node.js and does what it’s told

So let me briefly explain what’s going on here. I created a function gameOfLifewhich takes two arguments: sizeX And sizeY. They are used to create a two dimensional array statewhich is filled with random boolean values ​​(this is done in nested loops for. True means that the cell is alive, false – that is dead).

Then using setInterval An anonymous function is executed every second. It clears the current console output and generates new output based on the current state. In this output, the symbol X live cells are denoted, and dead cells are denoted by a space character.

Next, another set of nested for loops creates a new state (newState). For each cell (represented as x, y coordinates), the function checks all possible neighbors (from x-1 to x+1 and from y-1 to y+1) and counts the number of living (aliveNeighbours). For insurance, the current cell is excluded from the loop, as well as non-existent neighbors (for example, x=-1, y=-1). Based on the information about the number of living neighbors, a new state of the cell is set. Final state overwrites newState.

Finally the function is called gameOfLife with parameters of 20 rows by 20 columns. That’s all.

Target

Just in case, I’ll explain. By tweet, I mean a post on Twitter (social network with a bird), which is limited to 280 characters.

This is where I need to put my code. Of course, indentation and long variable names don’t make things any easier, so I’ll leave them in the source code for better readability, and then use uglify-js to remove extra spaces/lines and shorten variable names (it will be easier to solve the task with names with a length of one character).

After running through uglifier, I got the source code 549 characters long. To cram it into one tweet, I would have to cut it almost in half.

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

So, the requirements are formulated, there is no time to lose – let’s start shortening the code!

Declarations

First of all, it is not necessary to first declare a named function and then call it. I can convert it to a self-calling function like ((sizeX, sizeY) => {...})(20, 20) – this is quite enough, and it will take up less space.

The next point is variable declarations. I currently define variables when I need them, but this results in multiple occurrences in the code let And const (words as long as 5 characters!). Let’s just use the good old ‘var‘ 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 uglify-js do its job and… we’ll end up with 499 characters! This is still far from Twitter’s limit, but it’s good enough for Mastodon (another social media platform that competes with Twitter).

Screenshot of post on Mastodon with game code

Screenshot of post on Mastodon with game code

You can see the post itself. by this link.

((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);

Combing the generation of the initial state

Using nested for loops to set the initial state works quite well, but it can be done even better. For example, use the method Array.from.

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

Array.from takes two arguments. The first one is required and is an iterable object that will be converted to an array. The second, optional, is a callback. The value returned by callback is placed in the output array. Array(n) returns an array of length n filled with empty values, but the callback can override them.

Because and Array.fromAnd Array are used twice, I can save space with variables.

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

It may not be visible now, but after the variable names are mangled by uglifier, the code will become a few characters shorter.

You may have noticed that I don’t use booleans anymore. Since I need the X and space characters for console output, it’s easier to use them in the state as well. Thanks to this, the code for managing the console can be shortened:

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

As you can see, I got rid of the variable consoleOutput. In addition, to save space due to mangling, I can push the console into a variable, since it is used twice in the code:

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

A few more minor adjustments (due to the use of X and space instead of booleans) and the minified code is… 448 characters long. There are less than 200 left.

((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);

Transition to a new state

From the beginning, I didn’t really like my implementation newState. I am a supporter of using array methods when working with them, so I decided to apply reduce and reduce the number of cycles for. Additionally, I assigned status indicators (X characters / space) to the new variables. In addition, assigning a new cell state is now handled more efficiently. The latest improvement in this iteration is replacing the triple equal sign (===) with a double equal sign (==) for comparisons.

...
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)

After all these manipulations, I got 367 characters (naturally, in minified form). Not a bad result, but still not capacious enough for Twitter.

Appreciate what you already have

Like I said, I’m a big fan of array methods (especially reduce). However, here I actively use and mapAnd reduce, and the names of these methods take up quite a lot of space. Above I have already applied Array.from and put it in a variable, and after a couple of extra minutes of studying the code, I realized that I could use it instead map And reduce in the following way:

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
    )
}))

In addition, I still didn’t like the code that determines the new state of each cell (although it worked correctly), so after a while I came to this solution:

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

After minification, I got a code with a length of 321 characters.

((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);

Diving even deeper

Well, now I have reached the point where 40 characters began to seem like a whole book to me. What else can be reduced and simplified? Following the practice of reusing existing tools (Array.from), I can rewrite this snippet:

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

in the following way:

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

Of course, in its unminified form, this code is longer than the original. However, after minification, it was reduced to 319 characters, and I saved as much as 2 characters – which, to put it mildly, is not that much. There are still 38 left.

In fact, it is not necessary to pass size as two separate arguments – it can be a single argument used for both x and y. And in general, instead of 20, I can use 9, which will reduce the value of the argument by one character. So how much have we won? 311 characters minified code.

What’s next? Let’s say I can use numbers – 0 for a dead cell and 1 for a live one. We get just one character instead of a cumbersome three-character representation (0 instead of ‘ ‘ and 1 instead of ‘X’). And since it’s just one character, I don’t need to store it in a separate variable. 299 characters. Victory is close.

Now, using numbers as status indicators, I can slightly tweak the logic responsible for counting the number 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 to see if a potential neighbor has the same coordinates as the cell I’m counting live neighbors for. Instead, I subtract the value of this cell from the total. Additionally, I replaced nx <= x + 1 on nx < x + 2 (same for y) – the result is the same, but one character shorter. 286 characters. Only 6 left!

I looked at the code that uglify-js generates and realized that it saves curly braces for loops for - for (...){for(...){...}}. But you can remove them and write everything in one line:

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

Let’s run this through uglify-js, and…

Finally

Exactly 280 characters. Well, technically 281 characters, but uglify-js adds a semicolon at the end, which I don’t really need.

Here is the final 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 script is only 280 characters long and it works!

The script is only 280 characters long and it works!

tweet

And here is the tweet

Anticipating comments – I’m sure you’ll find a way to “cut off” a couple more characters. You might even do better than me!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *