This post is a continuation of the previous post at the London clojure dojo.
When we left the story, we had generated a HTML table with id
attributes
related to the (x,y)
co-ordinates and CSS classes to determine the
colour.
We have some methods on the client side relevant to updating the table
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
The first method fill-random
creates a sequence of values, one
for each entry in the grid. The possible values are either :alive
or nil
The update-view
method, which is a callback when the grid data has changed,
maps across the grid sequence and calls set-cell-state
for each
entry. Note that we use map-indexed
instead of map to get both the
index and the state and then infer the (x,y)
co-ordinates from the
index. The other point to note here is that, since map-indexed
produces a lazy sequence we need to force the evaluation with
doall
or we never see the side effects (i.e. the calls to set-cell-state
).
So now we have a data representation and a call back that updates the grid with new values. The final piece of the puzzle is the code that creates the next generation based on the current one. i.e. applies the rules of game of life. Let’s remind ourselves what the rules are:
- Any live cell with fewer than two live neighbours dies, as if caused by under-population.
- Any live cell with two or three live neighbours lives on to the next generation.
- Any live cell with more than three live neighbours dies, as if by overcrowding.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
We first we need a method to find adjacent cells then we can count the
values in those adjacent cells and apply the rules above. There seem
to be many ways to skin this cat. A simple implementation might be to
calculate multiple offsets from the current index. The juxt
function lets you do
something like this, but it doesn’t quite work as expected…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
a different approach is needed.
Consider these two examples. In each case we’d like to find the indices adjacent to the number highlighted in red. i.e. the squares highlighted in blue.
-c1 -r1 | c0 | c1 | c2 | c3 | c4 | c5 |
r0 | 00 | 01 | 02 | 03 | 04 | |
r1 | 05 | 06 | 07 | 08 | 09 | |
r2 | 10 | 11 | 12 | 13 | 14 | |
r3 | 15 | 16 | 17 | 18 | 19 | |
r4 | 20 | 21 | 22 | 23 | 24 | |
r5 |
-c1 -r1 | c0 | c1 | c2 | c3 | c4 | c5 |
r0 | 00 | 01 | 02 | 03 | 04 | |
r1 | 05 | 06 | 07 | 08 | 09 | |
r2 | 10 | 11 | 12 | 13 | 14 | |
r3 | 15 | 16 | 17 | 18 | 19 | |
r4 | 20 | 21 | 22 | 23 | 24 | |
r5 |
Note that in the second example, some of the adjacent squares are off
the board - we shouldn’t return indices related to those squares. If
we alter our representation from a single sequence to a sequence of
sequences we can then operate on the elements in each row. By
discarding rows (highlighted in green) and columns (highlighted in
pink) we should be left with the required values. Discarding from the front
of a sequence is easily achieved with the drop
function and
discarding from the end of a sequence via drop-last
. There’s also
a useful feature of those functions that if you pass a negative index
nothing happens. This means we don’t need to worry about validating
the count passed to drop
or drop-last
1 2 3 4 5 6 7 8 |
|
So by combining that all together we can drop rows before and after those we’re interested in and similarly with columns. This is what I came up with
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Great! we solved the adjacency problem……or have we? Lets check the performance characteristics of the two approaches.
1 2 3 4 5 6 7 |
|
Quite a difference and since we’re applying this function once for
each cell, this difference can easily accumulate. But since the
adjacent indices are static values for a particular grid
we can easily memoize
the adjacent-indices function to avoid
recalculating known values…
1 2 3 4 5 6 7 8 |
|
Having solved the adjacency problem, if should be fairly simple to calculate the states of each cell in the next generation by applying the rules.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
The final piece is to add a watch to the *grid*
atom so that the
update-view
callback is fired everytime the grid updates and also
to update the model regularly using a timer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
See the final version in action here
All code on github details in the README