In the last part we started a timer, to update the game; now we need implement the “business logic”.
To check that everything is working, we can start with a simple function that inverts the state of the board on every tick:
nextGeneration : Board -> Board
nextGeneration board =
Array.indexedMap (\i r -> (nextGenRow i r board)) board
nextGenRow : Int -> Row -> Board -> Row
nextGenRow rowIndex row board =
Array.indexedMap (\i c -> (nextGenCell i rowIndex c board)) row
nextGenCell : Int -> Int -> Cell -> Board -> Cell
nextGenCell cellIndex rowIndex cell board =
case cell of
Alive -> Dead
Dead -> Alive
Satisfied that the board updates as expected, we can move onto implementing the real rules. Everything is conditional on the current state of the cell, and the number of live neighbours it has:
nextGenCell : Cell -> Int -> Cell
nextGenCell cell liveNeighbours =
case cell of
Alive ->
if liveNeighbours < 2 then
Dead
else if liveNeighbours > 3 then
Dead
else
Alive
Dead ->
if liveNeighbours == 3 then
Alive
else
Dead
We can count the neighbours for each cell:
liveNeighbours : Int -> Int -> Board -> Int
liveNeighbours rowIndex colIndex board =
liveNeighboursAbove rowIndex colIndex board +
liveNeighboursAdjacent rowIndex colIndex board +
liveNeighboursBelow rowIndex colIndex board
liveNeighboursAbove : Int -> Int -> Board -> Int
liveNeighboursAbove rowIndex colIndex board =
isAlive (rowIndex - 1) (colIndex - 1) board +
isAlive (rowIndex - 1) (colIndex) board +
isAlive (rowIndex - 1) (colIndex + 1) board
liveNeighboursAdjacent : Int -> Int -> Board -> Int
liveNeighboursAdjacent rowIndex colIndex board =
isAlive rowIndex (colIndex - 1) board +
isAlive rowIndex (colIndex + 1) board
liveNeighboursBelow : Int -> Int -> Board -> Int
liveNeighboursBelow rowIndex colIndex board =
isAlive (rowIndex + 1) (colIndex - 1) board +
isAlive (rowIndex + 1) (colIndex) board +
isAlive (rowIndex + 1) (colIndex + 1) board
isAlive : Int -> Int -> Board -> Int
isAlive rowIndex colIndex board =
case (getCell colIndex (getRow rowIndex board)) of
Alive -> 1
Dead -> 0
If the row or column index is 0, we need to wrap the grid around from top to bottom. We could do this by checking the index, but as the Array getters return a Maybe we can just handle the case that it doesn’t exist:
getRow : Int -> Board -> Row
getRow i board =
case Array.get i board of
Just row -> row
Nothing ->
case Array.get ((Array.length board) - 1) board of
Just row -> row
Nothing -> Debug.crash "oops"
getCell : Int -> Row -> Cell
getCell i row =
case Array.get i row of
Just cell -> cell
Nothing ->
case Array.get ((Array.length row) - 1) row of
Just cell -> cell
Nothing -> Debug.crash "oops"
The Elm philosophy leans towards covering all edge cases, which is generally a Good Thing, but can become annoying e.g. when parsing json.
Now we can put all the pieces together with a map:
nextGeneration : Board -> Board
nextGeneration board =
Array.indexedMap (\i r -> (nextGenRow i r board)) board
nextGenRow : Int -> Row -> Board -> Row
nextGenRow rowIndex row board =
Array.indexedMap (\i c -> (nextGenCell c (liveNeighbours rowIndex i board))) row
And we finally have a working Game of Life!