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:
Algorithm explained: The Doomsday rule
Pascal Thormeier γ» Nov 9 '20
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.
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:
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:
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
}
}
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],
]
}
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
}
// ...
}
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
}
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
)
}
}))
}
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)
})
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)
})
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>
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":
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!
Top comments (18)
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 π¦!
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! :)
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!)
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...
Have you seen the FOL computer that outputs the Fibonacci sequence? RAM, CPU etc, just makes me realise how little I know π€£π
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
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!
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
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
Ahaha, hopefully I do get around to implement it someday
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.
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.
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.
Beautiful animations!
If you like what you see, have a look at ashware.nl/buglife/# :)
Oh wow! I love how the animation also allows to see previous generations. What were the challenges you faced during this implementation?
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.
The best explanation with code, thanks!π©π»βπ»