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!