DEV Community

Cover image for It's alive! Simulate organisms with Conway's game of life on a canvas πŸ§«πŸ”‹
Pascal Thormeier
Pascal Thormeier

Posted on

It's alive! Simulate organisms with Conway's game of life on a canvas πŸ§«πŸ”‹

Today, we create living organisms! We'll kind of, at least. It's the next best thing to becoming a 21st century digital "Web Dev Doctor Frankenstein": Conway's game of life.

What?

Excellent question. The best, actually! Let me explain...

John Horton Conway was a British mathematician. He contributed to a lot of different fields in mathematics, such as number theory, algebra, geometry, combinatorial game theory, algorithmics, group theory, and analysis.

He developed a ton of remarkable algorithms, such as the Doomsday algorithm, that lets you find out the weekday of any given date with a only a few steps. I've explained the Doomsday rule in this post some time ago:

Conway developed the "Game of Life" in 1970 as an applied example of abstract computers. It's a 2-dimensional field with X and Y coordinates, where each integer coordinate represents a cell that can be either alive or dead, depending on some rules.

But, since it's a game, how is it played?

The rules of the game

You can think of the Game of Life as a sandbox. Originally, no cell is alive. Alive cells can be either set by the user or sprinkled in randomly. In each game tick, the game determines which cells are alive and which ones are dead in the next generation. This step is then repeated until the user interrupts.

To determine the next generation, the game looks at each cells neighbors and applies a set of rules:

  • If a cell was alive in the current generation:
    • If it has less than 2 (loneliness) or more than 3 (overpopulation) alive neighbors, it dies in the next generation, otherwise it stays alive
  • If a cell was dead in the current generation:
    • If it has exactly 3 alive neighbors, it will become alive in the next generation, otherwise it stays dead

(These rules allow for some pretty complex structures, but we'll come to that later!)

Let's make an example or two

Let's consider a 3 by 3 grid. We're going to see how the rules work by applying them to the center cell. All other cells are the center cell's neighbors.

Here we can see what happens if less than 2 neighboring cells are alive.

Loneliness rule applied: In the first 3 by 3 grid, the cell is green, then there's an arrow pointing to a second, empty 3 by 3 grid. The arrow has the text

The filled cell in the middle is alive in this generation, but dies the next generation.

In the following picture, we can see how it could look like if a cell is being born:

The same two grids as above, but in the left grid, there's three cells (top right, top middle and bottom left) and the center cell is dead. In the second grid, the center cell is alive. There's an arrow pointing from the first to the second grid with the text

One thing is important, though: The next generation needs to be calculated all at once. Meaning: If the game sets cell 1 as "alive" that was dead before and starts applying the rules to its immediate neighbor cell 2, it should not consider the new state of cell 1 (alive) but the old one (dead) for the calculation of cell 2.

But this begs a question: What does it do at the border of the field?

There's two possibilities: Either we consider the border as always dead (they are neighbors, but the rules are never applied to them) or the world is actually formed like a donut.

Tasty torus

When the field is shaped like a donut, it behaves like this: A square field with arrows indicating that what leaves the top enters again in the bottom and vice versa and what leaves the left side enters again on the right and vice versa.

Whatever leaves either side will reenter on the opposite side. When you connect those sides, the shape will actually look like a donut. Or in mathematics speech: A torus.

So, that's all the info we need. Let's start implementing this!

Coding out the game of life

Let's start with the field. I will create the field as a nested array of 100 by 100 boolean variables:

const field = []
for (let y = 0; y < 100; y++) {
  field[y] = []
  for (let x = 0; x < 100; x++) {
    field[y][x] = false
  }
}
Enter fullscreen mode Exit fullscreen mode

By setting everything false, the code will consider all cells as dead. True, on the other hand, would mean that a cell is alive.

Next, I need a function to get any cell's neighbors. A cell is identified by its X and Y values, so I can add and subtract 1 to to those values to get all neighbors:

const getNeighbors = (x, y, field) => {
  let prevX = x - 1
  let nextX = x + 1
  let prevY = y - 1
  let nextY = y + 1

  return [
    field[prevY][prevX],
    field[prevY][x],
    field[prevY][nextX],
    field[y][prevX],
    // field[y][x], That's the cell itself - we don't need this.
    field[y][nextX],
    field[nextY][prevX],
    field[nextY][x],
    field[nextY][nextX],
  ]
}
Enter fullscreen mode Exit fullscreen mode

But wait - the field is a donut. So I need to catch the border cases as well:

const getNeighbors = (x, y, field) => {
  let prevX = x - 1
  if (prevX < 0) {
    prevX = field[0].length - 1
  }

  let nextX = x + 1
  if (nextX === field[0].length) {
    nextX = 0
  }

  let prevY = y - 1
  if (prevY < 0) {
    prevY = field.length - 1
  }

  let nextY = y + 1
  if (nextY === field.length) {
    nextY = 0
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

So this function now returns an array of boolean values. The game's rules don't care about which neighbors are alive or dead, only how many of them are.

The next step is to actually implement the rules. Ideally, I've got a function that takes X and Y values as well as the field and returns the state of the cell for the next generation:

const getDeadOrAlive = (x, y, field) => {
  const neighbors = getNeighbors(x, y, field)
  const numberOfAliveNeighbors = neighbors.filter(Boolean).length

  // Cell is alive
  if (field[y][x]) {
    if (numberOfAliveNeighbors < 2 || numberOfAliveNeighbors > 3) {
      // Cell dies
      return false
    }

    // Cell stays alive
    return true
  }

  // Cell is dead
  if (numberOfAliveNeighbors === 3) {
    // Cell becomes alive
    return true
  }

  // Cell stays dead
  return false
}
Enter fullscreen mode Exit fullscreen mode

And that's pretty much it for the game rules!

Now I create a function to draw the entire field on a square canvas:

const scaleFactor = 8

const drawField = field => {
  const canvas = document.querySelector('canvas')
  const context = canvas.getContext('2d')

  // Fill entire field
  context.fillStyle = '#fff'
  context.fillRect(0, 0, 100 * scaleFactor, 100 * scaleFactor);

  context.fillStyle = '#008000'

  // Fill alive cells as small rectangles
  field.forEach((row, y) => row.forEach((cell, x) => {
    if (cell) {
      context.fillRect(
        x * scaleFactor, 
        y * scaleFactor, 
        scaleFactor, 
        scaleFactor
      )
    }
  }))
}
Enter fullscreen mode Exit fullscreen mode

Now let's add some control buttons to let the game automatically calculate and draw new generations each 80ms:

let nextField = field

drawField(field)

const step = () => {
  nextField = nextField.map((row, y) => row.map((_, x) => {
    return getDeadOrAlive(x, y, nextField)
  }))

  drawField(nextField)
}

let interval = null

document.querySelector('#step').addEventListener('click', step)

document.querySelector('#start').addEventListener('click', () => {
  interval = setInterval(step, 80)
})

document.querySelector('#stop').addEventListener('click', () => {
  clearInterval(interval)
})
Enter fullscreen mode Exit fullscreen mode

And some more controls for defaults, random, reset, etc.:

document.querySelector('#reset').addEventListener('click', () => {
  for (let y = 0; y < 100; y++) {
    for (let x = 0; x < 100; x++) {
      field[y][x] = false
    }
  }

  nextField = field

  drawField(field)
})

document.querySelector('#glider').addEventListener('click', () => {
  for (let y = 0; y < 100; y++) {
    for (let x = 0; x < 100; x++) {
      field[y][x] = false
    }
  }

  field[20][20] = true
  field[20][21] = true
  field[20][22] = true
  field[19][22] = true
  field[18][21] = true

  nextField = field

  drawField(field)
})

document.querySelector('#random').addEventListener('click', () => {
  for (let y = 0; y < 100; y++) {
    for (let x = 0; x < 100; x++) {
      field[y][x] = Math.random() * 100 > 65
    }
  }

  nextField = field

  drawField(field)
})

document.querySelector('canvas').addEventListener('click', event => {
  const x = Math.floor(event.offsetX / scaleFactor)
  const y = Math.floor(event.offsetY / scaleFactor)

  field[y][x] = !field[y][x]

  nextField = field

  drawField(field)
})
Enter fullscreen mode Exit fullscreen mode

Of course this needs some HTML, too:

<!DOCTYPE html>
<html>
  <head>
    <style>
      canvas {
        box-sizing: border-box;
        border: 1px solid #000;
        width: 800px;
        height: 800px;
      }

      .container {
        box-sizing: border-box;
        width: 800px;
        border: 1px solid #000;
        margin-top: 10px;
        padding: 10px;
      }
    </style>
  </head>
  <body>
    <h1>Conway's game of life on a canvas</h1>
    <canvas id="canvas" width="800" height="800"></canvas>

    <div class="container">
      <button id="start">Start</button>
      <button id="stop">Stop</button>
      <button id="step">Step</button>
    </div>

    <div class="container">
      <button id="reset">Reset to empty</button>
      <button id="glider">Set single glider</button>
      <button id="random">Random (35% alive)</button>
    </div>

    <script src="./index.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

The final result

And here's a codepen where you can play around with it:

(Because of the size of the canvas and the non-responsive nature of the example, I recommend running it in 0.5 scale)

Have fun exploring!

Some remarkable structures

There's some cell structures that are worth mentioning. A rather simple one is called a "glider":

The 5 different states of a glider

As you can see, this thing actually moves in a straight line by one unit on the X and Y axis every 5 generations.

Since it's going back to its original state again, this structure is able to move indefinitely!

But there's more: Some structures are static (for example a 2 by 2 alive square), flip between two states (one example being a straight line along either the X or Y axis consisting of 3 alive cells), others are capable of moving and even producing gliders at intervals!

You see, this really is the closest thing to creating living organisms as you can get with around 200 lines of JS and a canvas!


I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❀️ or a πŸ¦„! I write tech articles in my free time and like to drink coffee every once in a while.

If you want to support my efforts, buy me a coffee β˜• or follow me on Twitter 🐦! You can also support me directly via Paypal!

Buy me a coffee button

Top comments (18)

Collapse
 
grahamthedev profile image
GrahamTheDev • Edited

Always great fun to play with conways game of life. However, as with everything, there is always someone who does it bigger and better...(skip to 1:08 for actual video)

One minor bug is if you press "reset" before pressing "stop" the canvas will still try and update and so drawing becomes difficult. Plus grid lines would be great when drawing, but that is a fun one for others to try and add!

Well worth a ❀ and a πŸ¦„!

Collapse
 
thormeier profile image
Pascal Thormeier

Holy moly, that's an amazing video! Thank you for sharing!

Reminds of this one, where someone actually managed to code the Game of Life into the Game of Life:

Yes, it does have it's bugs right now, and I will fix those in the Codepen at least in the next few days! :)

Collapse
 
grahamthedev profile image
GrahamTheDev • Edited

Yeah I was in two minds whether to link to that one instead, but the music makes the first one feel epic! πŸ˜‹πŸ€£

I think the second one would melt my CPU (especially as it is so warm in the UK at the moment!)

Thread Thread
 
thormeier profile image
Pascal Thormeier

There's So. Damn. Many. Interesting videos about GOL, I could seriously watch them all day long :D I think building the GOL-in-GOL version would alone take ages, let alone figuring it all out without any help...

Thread Thread
 
grahamthedev profile image
GrahamTheDev

Have you seen the FOL computer that outputs the Fibonacci sequence? RAM, CPU etc, just makes me realise how little I know 🀣😜

Collapse
 
squarebat profile image
Khadija Sidhpuri

I first learned about Game of Life in this video by veritasium. Following that I simulated it for a hackathon. It was so cool to see simulate cool rifle and complex simulators using it. Plam to simulate a 3d version when I have a cpu capable of doing that :D

Collapse
 
thormeier profile image
Pascal Thormeier

I love Veritasium, been following that channel for years now! I haven't heard about any 3D version of GOL yet, would adapt the rules to fit the amount of neighboring cells? You definitely have to make a post about it once it's done!

Collapse
 
squarebat profile image
Khadija Sidhpuri

Haven't figured out the rules of a 3D GoL just yet, but if I ever implement it, if I were to do it, I'd start with the same rules as GoL, but in 3D space. Will be interesting to see the results. Will definitely write a post if I implement it :D

Thread Thread
 
thormeier profile image
Pascal Thormeier

I wonder what a 3D glider would look like... I could imagine that it's a lot more complex to achieve the same behaviour as in 2D space. So many things to explore, can't wait for your article! :D

Thread Thread
 
squarebat profile image
Khadija Sidhpuri

Ahaha, hopefully I do get around to implement it someday

Collapse
 
eljayadobe profile image
Eljay-Adobe

When discussing Conway's Game of Life, I always like to provide the APL version of the code. For contrast with the language that is being presented, in this case in contrast with JavaScript (and HTML and CSS).

APL version of Conway's Game of Life.
life ← {βŠƒ1 ⍡ ∨.∧ 3 4 = +/ +⌿ Β―1 0 1 ∘.βŠ– Β―1 0 1 ⌽¨ βŠ‚β΅}

At the age of 82, Dr. John Horton Conway passed away on 2020-Apr-11, from COVID-19.

For an high-level introduction to APL, the article a Glimpse of Heaven by Bernard Legrand.

Collapse
 
thormeier profile image
Pascal Thormeier

I haven't used APL before and it does look interesting indeed. Thank you for sharing this implementation! An interesting video you might like is Dr. Conway talking about the Game of Life himself, over at Numberphile. He said he used to even hate it, because he didn't find it all too interesting and it was overshadowing much more important things.

Collapse
 
esger profile image
Esger Jellema • Edited

I built this many times before, trying to speed things up. This is my latest version ashware.nl/fast-life/
The code can be found via my homepage.

Collapse
 
aleksandrhovhannisyan profile image
Aleksandr Hovhannisyan

Beautiful animations!

Collapse
 
esger profile image
Esger Jellema • Edited

If you like what you see, have a look at ashware.nl/buglife/# :)

Collapse
 
thormeier profile image
Pascal Thormeier

Oh wow! I love how the animation also allows to see previous generations. What were the challenges you faced during this implementation?

Collapse
 
esger profile image
Esger Jellema

Thank you! Here my main challenge was trying to speed things up, again :)
So I built an AureliaJs web app and a web worker to do the heavy lifting.
The 'trails' were quite simple to accomplish: just draw the new generation with opacity < 1 in order to faint all 'old' cells a bit every generation.
PS. You can even change the rules of life while it's running and change various other settings as well.

Collapse
 
katyi profile image
Alexandra Egorova

The best explanation with code, thanks!πŸ‘©πŸ»β€πŸ’»