Game of Life in Elm (Pt 4)

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!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s