6
\$\begingroup\$

First attempt at doing anything F#-y from a very heavy C# background, think I've grasped the fundamentals of FP, but always good to get other eyes on it:

namespace FSharp.GameOfLife

open System
open System.Drawing

module Array2D =
    let toArray (arr: 'T [,]) = arr |> Seq.cast<'T> |> Seq.toArray

module Array =
    let count (predicate: 'T -> bool) (arr: 'T[]) = arr |> Array.filter(predicate) |> Array.length

module Game =

    let (<&>) f g = (fun x -> f x && g x)

    type Cell (position) = 
        member this.Position = position
        member val IsAlive = false with get, set

    and Board (width : int, height : int) = 
        let grid = Array2D.init width height (fun x y -> Cell(System.Drawing.Point(x, y)))

        let withinBounds (x : int, y : int) =
            let insideWidth = width > x && x >= 0
            let insideHeight = height > y && y >= 0
            insideWidth && insideHeight

        let isSamePosition (cell : Cell, x : int, y : int) = 
            cell.Position = Point(x, y)

        let getNeighbours (grid : Cell[,]) (cell : Cell) =
            let isDifferentPosition (x, y) = not (isSamePosition (cell, x, y))
            let positionModifiers = Array2D.initBased -1 -1 3 3 (fun x y -> (cell.Position.X + x, cell.Position.Y + y))
            positionModifiers |> Array2D.toArray |> Array.filter (withinBounds <&> isDifferentPosition) |> Array.map (fun (x, y) -> grid.[x, y].IsAlive)

        member this.Grid with get() = grid

        member this.Tick() = 
            let cellWithNeighbourStates = this.Grid |> Array2D.map (fun cell -> (cell, (getNeighbours this.Grid cell))) |> Array2D.toArray
            cellWithNeighbourStates |> Array.iter (fun (cell, neighbours) -> 
                let liveNeighbours = neighbours |> Array.count (fun c -> c = true) 
                if cell.IsAlive then
                    cell.IsAlive <- 2 = liveNeighbours || 3 = liveNeighbours
                else
                    cell.IsAlive <- 3 = liveNeighbours
                )

        member this.SetAlive(cells : list<int * int>) =
            cells |> List.iter (fun (x, y) -> this.Grid.[y, x].IsAlive <- true)

        override this.ToString() = 
            let structs = this.Grid |> Array2D.map (fun cell -> if cell.IsAlive then "[]" else "()")
            let asRows = [|0..(height - 1)|] |> Array.map (fun row -> structs.[0.., row] |> Array.fold (fun acc elem -> sprintf "%s%s" acc elem) Environment.NewLine)
            asRows |> Array.fold (fun acc elem -> sprintf "%s%s" acc elem) Environment.NewLine

Can be tested as such:

open FSharp.GameOfLife.Game

[<EntryPoint>]
let main argv = 
    let board = Board (10, 10)

    //standard ocilator
    let x = board.SetAlive([
        (1, 1)
        (1, 2)
        (2, 1)
        (2, 2)
        (3, 3)
        (3, 4)
        (4, 3)
        (4, 4)
    ])

    printf "%s" (board.ToString())

    [|0..2|] |> Array.iter (fun _ -> 
        board.Tick()
        printf "%s" (board.ToString())
    )

    System.Console.ReadKey() |> ignore
    0 // return an integer exit code
\$\endgroup\$
3
\$\begingroup\$

You don't need the functions you created to work with arrays. There's no point in putting them in modules like that unless you're going to give the modules the [<RequireQualifiedAccess>] attribute.

You also didn't need the Cell type or that operator You shouldn't use and to define your types unless they're mutually recursive.

open System
open System.Text

type Board (width : int, height : int) =
    let grid = Array2D.create width height false

    let withinBounds (x:int) (y:int) =
        (0 <= x  && x < width) && (0 <= y && y < height)

    let getNbs x y =
        ([| x-1,y+1; x,y+1; x+1,y+1;
            x-1,y  ;        x+1,y  ;
            x-1,y-1; x,y-1; x+1,y-1 |]
         |> Array.filter(fun (a,b) -> (withinBounds a b))
         |> Array.filter(fun (a,b) ->  grid.[a,b] = true)).Length

    member __.Item
        with get (x,y)   = grid.[x,y]
        and  set (x,y) v = grid.[x,y] <- v

    member __.Tick() =
        grid |> Array2D.iteri( fun x y alive ->
            let liveNeighbors = getNbs x y
            match alive with
            | true  -> grid.[x,y] <- liveNeighbors = 2 || liveNeighbors = 3
            | false -> grid.[x,y] <- liveNeighbors = 3
        )

    override __.ToString() =
        let frame = Array2D.zeroCreate height width
        grid |> Array2D.iteri(fun x y alive ->
                match alive with
                |true  -> frame.[height-1-y,x] <-"[@]"
                |false -> frame.[height-1-y,x] <-"[ ]"
        )
        let sb = StringBuilder()
        frame |> Array2D.iteri(fun _ y str ->
            match y = height-1 with
            | true  -> sb.Append(str).Append("\n") |> ignore
            | false -> sb.Append(str) |> ignore
        )
        sb.ToString()

The __.Item method gives you indexed acces into the board like this

let tb = Board(5,5) 
tb.[1,1] <- true
tb.[1,0] <- true
tb.[0,1] <- true
tb.[0,3] <- true
tb.[2,4] <- true
tb.[4,4] <- true

printfn "%A" tb 
tb.Tick()
printfn "%A" tb
tb.Tick()
printfn "%A" tb

[ ][ ][@][ ][@] 
[@][ ][ ][ ][ ] 
[ ][ ][ ][ ][ ] 
[@][@][ ][ ][ ] 
[ ][@][ ][ ][ ] 

[ ][ ][ ][ ][ ] 
[ ][ ][ ][ ][ ] 
[@][ ][ ][ ][ ] 
[@][ ][ ][ ][ ] 
[@][@][ ][ ][ ] 

[ ][ ][ ][ ][ ] 
[ ][ ][ ][ ][ ] 
[ ][ ][ ][ ][ ] 
[@][@][ ][ ][ ] 
[@][@][ ][ ][ ] 

There's no need to build and use the console to test this code, write your code in an .fsx and run it in FSI

\$\endgroup\$
  • \$\begingroup\$ Thanks! That Cell type was left over from the first attempt where I had it recursive, realised all I cared about was the bool, and forgot to remove it \$\endgroup\$ – Psytronic Aug 14 '15 at 11:41
  • \$\begingroup\$ Nice layout for the nearest neighbours array, a lot clearer as to what I'm trying to achieve there. The Item method was a useful tip. \$\endgroup\$ – Psytronic Aug 14 '15 at 11:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.