I’ve been learning more about JavaScript and the DOM in particular, and I wanted to try out something to cement what I’d learned. To that end, I’ve created an implementation of Conway’s Game of Life using nothing but vanilla JavaScript and CSS.
(This is obviously not any kind of monumental achievement - in fact, this is probably, like, implementation #4,920,110 of the Game of Life in Javascript - but it was a fun project, and I’d recommend it to anyone interested in some quick practice in basic DOM traversal.
Conway’s Game of Life takes place on a grid of “cells” which can have two possible states - alive or dead. The grid is periodically updated, and during these updates, cells will come alive or die based on the following rules:
And that’s all. From just those rules, you can get a dizzying variety of results and patterns - including a Turing machine!
The cells themselves are represented as td
elements
in an HTML table.
Each cell is of either the class alive
or the class
dead
, and those classes are used both to keep track of the
cell’s state in the script and to manage CSS formatting.
Each cell also has an “age” class that can be either age0
,
age1
, age2
, or age3
. This is
outside the strict rules of the Game of Life and is purely a cosmetic
thing: it’s used to add CSS rules showing how long which cells recently
came alive or died.
The elements themselves are automatically added (both to avoid repetitive HTML entry, and to make the size of the grid easily adjustable):
The most unusual part of using an HTML table for this is accessing
each cell’s neighbors to determine whether the cell must live or die.
In most implementations of this game, the cell data would probably be
stored in a matrix; then, checking the cell at
(x, y)
would be accomplished by looking up the cell at,
say, (x+1, y+1)
, for a diagonal neighbor. You could do this
here, too! You could implement the grid’s data as a matrix, and then pass
that data to your HTML table to “draw” it.
But if you’re storing the data in a table anyway, then that opens up
another method for checking the neighbors: DOM traversal! In particular,
the methods
previousElementSibling
,
nextElementSibling
, and
parentElement
can work well here.
The horizontal neighbors are the easiest to check, since they are located
in the adjacent td
elements, so they can be accessed by
previousElementSibling
and nextElementSibling
.
(If the cell being checked is at the left or right edge, then
there is no sibling, and the relevant one of these methods will just return
null
- which I interpret here as equivalent to “dead”:
The other six neighbors are a little more complicated, since they’re
in different rows. There isn’t really (as far as I know) an equivalent
one-liner for “traverse to the td
directly above this one.”
Instead, order to get to them, we have to first traverse to
the parent row (the tr
element) that contains our cell.
Then, we use
previousElementSibling
and nextElementSibling
to
get to the adjacent rows (where before we were traversing between cells.)
Assuming the adjacent row isn’t null
, we then
access the td
elements inside it using its
children
property. But which of the children do we
look at? Even though we aren’t using a matrix, we still need to know our
X coordinate… but we can get it using previousElementSibling
.
How? We just call it repeatedly until it null
s out:
With that, we can access the correct elements of the adjacent rows, which will be our vertically adjacent elements. We can also pick up the diagonally adjacent elements while we’re at it.
Getting the number of living neighbors was the hard part. Once that is known, actually doing that whole Game of Life thing is easy!
One implementation detail I didn’t quite figure out: if you
fire it up and try to draw on the grid,
you can only do it one cell at a time.
You can’t click and drag multiple cells at
once, which makes it a bit harder to draw in a pattern you want.
Right now, clicking on the grid is implemented using
the mousedown
event:
But there might be a better event, or a better way to use it. If you know how I might improve this part of the experience, feel free to drop me a line!
You can check out the full script here along with the full implementation here.