Learning F#: The Game of Life (Vol. 6)

Intro

I have been crafting the logic required for the Game of Life. In the last article, I documented how I struggled with life cycles. However, I made some progress recently.

Tests

As I discussed in past articles, I wanted to learn how to implement this system via TDD.

The following tests have been implemented:

[<Test>]
let ``Any live cell with fewer than two live neighbors dies, as if caused by under-population``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid
                        |> setCell { X=2; Y=2; State=Alive }
                        |> setReaction (2,2)
    // Verify
    grid |> getStatus (2,2) |> should equal Dead

[<Test>]
let ``Any live cell with two or three live neighbours lives on to the next generation``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid
                        |> setCell { X=1; Y=2; State=Alive }
                        |> setCell { X=2; Y=2; State=Alive }
                        |> setCell { X=3; Y=2; State=Alive }
                        |> setReaction (2,2)
    // Verify
    grid |> getStatus (2,2) |> should equal Alive

[<Test>]
let ``Any live cell with more than three live neighbours dies, as if by over-population``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid
                        |> setCell { X=1; Y=2; State=Alive }
                        |> setCell { X=2; Y=2; State=Alive }
                        |> setCell { X=3; Y=2; State=Alive }
                        |> setCell { X=3; Y=3; State=Alive }
                        |> setCell { X=2; Y=1; State=Alive }
                        |> setReaction (2,2)
    // Verify
    grid |> getStatus (2,2) |> should equal Dead

[<Test>]
let ``Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid
                        |> setCell { X=1; Y=2; State=Alive }
                        |> setCell { X=2; Y=2; State=Dead }
                        |> setCell { X=3; Y=2; State=Alive }
                        |> setCell { X=3; Y=3; State=Alive }
                        |> setReaction (2,2)
    // Verify
    grid |> getStatus (2,2) |> should equal Alive

Business Logic

I wrote some additional business logic to capture a cell’s neighbors. In addition, I also wrote code to capture a cell’s response to its neighbors.

Neighbors

Each grid block is comprised of nine cells. Thus, I decided to name a cell based on its relative relationship to the original cell that’s being targeted for a response.

Thus, I implemented the following function to get neighboring coordinates:

let getNeighbors (coordinate:int*int) =

    let x,y = coordinate
    let west = x-1, y
    let northWest = x-1, y+1
    let north = x, y+1
    let northEast = x+1, y+1
    let east = x+1, y
    let southEast = x+1, y-1
    let south = x, y-1
    let southWest = x-1, y-1

    [west; northWest; north; northEast; east; southEast; south; southWest]

Response

The Game of Life requires that a cell responds to its neighbors based on the state of its neighbors.

The following code was implemented to reflect a cell’s responde to its neighbors:

let setReaction coordinate grid:Map<(int * int), Cell> = 

    let x,y = coordinate
    let count = coordinate |> getNeighbors
                           |> List.filter (fun coordinate -> grid |> getStatus coordinate = Alive)
                           |> List.length
    match count with
    | count when count < 2               ||  count > 3 -> grid |> setCell { X=x; Y=y; State=Dead }
    | 3                    -> match grid.TryFind coordinate with
                              | Some cell -> if cell.State = Dead then
                                                  grid |> setCell { cell with State=Alive }
                                             else grid
                              | None      -> failwith "Cell doesn't exists"
    | _ -> grid

The full logic is as follows:

type State = Alive | Dead
type Cell = { X:int; Y:int; State:State }

type Response = | Die
                | Survive
                | Resurect

let isNeighbor cell1 cell2 =

    let isAbsNeighbor v1 v2 =
        match abs (v1 - v2) with
        | 0 | 1 -> true
        | _     -> false

    let isValueNeighbor v1 v2 =
        match v1 >= 0
          &&  v2 >= 0 with
        | true  -> isAbsNeighbor v1 v2
        | _     -> isAbsNeighbor v2 v1

    match cell1.X <> cell2.X
      ||  cell1.Y <> cell2.Y with
    | true ->   isValueNeighbor cell1.X cell2.X
             && isValueNeighbor cell1.Y cell2.Y
    | _    -> false

let createGrid rowCount = 

    [for x in 1..rowCount do
        for y in 1..rowCount do
            yield { X=x; Y=y; State=Dead }
    ]|> List.map (fun c -> (c.X, c.Y), { X=c.X; Y=c.Y; State=Dead })
     |> Map.ofList

let setCell cell (grid:Map<(int * int), Cell>) =

    grid |> Map.map (fun k v -> match k with
                                | c when c = (cell.X, cell.Y) -> { v with State=cell.State }
                                | _ -> v)

let getStatus coordinate (grid:Map<(int * int), Cell>) =

    match grid.TryFind coordinate with
    | Some cell -> cell.State
    | None      -> Dead

let getNeighbors (coordinate:int*int) =

    let x,y = coordinate
    let west = x-1, y
    let northWest = x-1, y+1
    let north = x, y+1
    let northEast = x+1, y+1
    let east = x+1, y
    let southEast = x+1, y-1
    let south = x, y-1
    let southWest = x-1, y-1

    [west; northWest; north; northEast; east; southEast; south; southWest]

let setReaction coordinate grid:Map<(int * int), Cell> = 

    let x,y = coordinate
    let count = coordinate |> getNeighbors
                           |> List.filter (fun coordinate -> grid |> getStatus coordinate = Alive)
                           |> List.length
    match count with
    | count when count < 2               ||  count > 3 -> grid |> setCell { X=x; Y=y; State=Dead }
    | 3                    -> match grid.TryFind coordinate with
                              | Some cell -> if cell.State = Dead then
                                                  grid |> setCell { cell with State=Alive }
                                             else grid
                              | None      -> failwith "Cell doesn't exists"
    | _ -> grid

The full tests is as follows:

[<Test>]
let ``cells sharing x-coordinate are neighbors``() =
   // Setup
   let cell1 = { X=0; Y=0; State=Dead }
   let cell2 = { X=0; Y=1 ; State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal true

[<Test>]
let ``cells sharing y-coordinate are neighbors``() =
   // Setup
   let cell1 = { X=0; Y=1; State=Dead }
   let cell2 = { X=1; Y=1; State=Dead }

   cell1 |> isNeighbor cell2
         |> should equal true

[<Test>]
let ``cell that's right 1 and down 1 is neighbor``() =
   // Setup
   let cell1 = { X=0; Y=0; State=Dead }
   let cell2 = { X=1; Y=(-1); State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal true

[<Test>]
let ``cell that's right 1 and down-0 is neighbor``() =
   // Setup
   let cell1 = { X=0; Y=0; State=Dead }
   let cell2 = { X=1; Y=0; State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal true

[<Test>]
let ``cell that's right 1 and up 1 is neighbor``() =
   // Setup
   let cell1 = { X=0; Y=0; State=Dead }
   let cell2 = { X=1; Y=1 ; State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal true

[<Test>]
let ``cell that's up 1 is neighbor``() =
   // Setup
   let cell1 = { X=0; Y=0; State=Dead }
   let cell2 = { X=0; Y=1 ; State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal true

[<Test>]
let ``cell that's down 1 is neighbor``() =
   // Setup
   let cell1 = { X=0; Y=0;    State=Dead }
   let cell2 = { X=0; Y=(-1); State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal true

[<Test>]
let ``cell that's left 1 and up 1 is neighbor``() =
   // Setup
   let cell1 = { X=0; Y=0; State=Dead }
   let cell2 = { X=(-1); Y=1 ; State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal true

[<Test>]
let ``cell that's left 1 and down 1 is neighbor``() =
   // Setup
   let cell1 = { X=0;    Y=0;    State=Dead }
   let cell2 = { X=(-1); Y=(-1); State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal true

[<Test>]
let ``far away x-coordinates are not neighbors``() =
   // Setup
   let cell1 = { X=(-1); Y=0; State=Dead }
   let cell2 = { X=(+1); Y=0; State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal false

[<Test>]
let ``far away y-coordinates are not neighbors``() =
   // Setup
   let cell1 = { X=0; Y=(+1); State=Dead }
   let cell2 = { X=0; Y=(-1); State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal false

[<Test>]
let ``far away x,y-coordinates are not neighbors``() =
   // Setup
   let cell1 = { X=(+1); Y=(+1); State=Dead }
   let cell2 = { X=(+1); Y=(-1); State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal false

[<Test>]
let ``cells with same coordinates cannot be neighbors``() =
   // Setup
   let cell1 = { X=0; Y=0; State=Dead }
   let cell2 = { X=0; Y=0; State=Dead }

   // Verify
   cell1 |> isNeighbor cell2
         |> should equal false

[<Test>]
let ``create grid``() =
    // Test
    let rowCount = 3
    let grid = rowCount |> createGrid

    // Verify
    grid.Count |> should equal 9

[<Test>]
let ``find center``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid

    let getCoordinate coordinate =
        match grid.TryFind coordinate with
        | Some coordinate -> true
        | None            -> false

    // Test
    let found = getCoordinate (2,2)

    // Verify
    found |> should equal true

[<Test>]
let ``get status``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid

    // Test
    let center = grid |> getStatus (2,2)

    // Verify
    center |> should equal Dead

[<Test>]
let ``set cell to alive``() =
    // Setup
    let rowCount = 3
    let target = { X=2; Y=2; State=Alive }

    let grid = rowCount |> createGrid
                        |> setCell target
    // Test
    let result = grid |> getStatus (2,2)

    // Verify
    result |> should equal Alive

[<Test>]
let ``get neighbors``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid

    let center = 2,2
    let count = center |> getNeighbors
                       |> List.length
    // Verify
    count |> should equal 8

[<Test>]
let ``Any live cell with fewer than two live neighbors dies, as if caused by under-population``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid
                        |> setCell { X=2; Y=2; State=Alive }
                        |> setReaction (2,2)
    // Verify
    grid |> getStatus (2,2) |> should equal Dead

[<Test>]
let ``Any live cell with two or three live neighbours lives on to the next generation``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid
                        |> setCell { X=1; Y=2; State=Alive }
                        |> setCell { X=2; Y=2; State=Alive }
                        |> setCell { X=3; Y=2; State=Alive }
                        |> setReaction (2,2)
    // Verify
    grid |> getStatus (2,2) |> should equal Alive

[<Test>]
let ``Any live cell with more than three live neighbours dies, as if by over-population``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid
                        |> setCell { X=1; Y=2; State=Alive }
                        |> setCell { X=2; Y=2; State=Alive }
                        |> setCell { X=3; Y=2; State=Alive }
                        |> setCell { X=3; Y=3; State=Alive }
                        |> setCell { X=2; Y=1; State=Alive }
                        |> setReaction (2,2)
    // Verify
    grid |> getStatus (2,2) |> should equal Dead

[<Test>]
let ``Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction``() =
    // Setup
    let rowCount = 3
    let grid = rowCount |> createGrid
                        |> setCell { X=1; Y=2; State=Alive }
                        |> setCell { X=2; Y=2; State=Dead }
                        |> setCell { X=3; Y=2; State=Alive }
                        |> setCell { X=3; Y=3; State=Alive }
                        |> setReaction (2,2)
    // Verify
    grid |> getStatus (2,2) |> should equal Alive

Conclusion

In conclusion, I have been crafting the logic required for the Game of Life. In the last article, I documented how I struggled with life cycles. However, I made some progress recently. I showed how I retrieved neighbors of a cell as well as update a cell’s state in response to it’s neighbors.

Leave a comment