Connect4 – designing a Silverlight ViewModel

Last time we discussed the implementation details on the Connect4 game in F#. Today I will show you how you can implement a MVVM ViewModel for Silverlight in F#.

Ok I use no full-fledged MVVM framework (indeed in only implement INotifyPropertyChanged for the models, but there are some nice tricks hidden here I just had to talk about (you can use some of these even in C# or VB.net).

View model for a game board cell

This is really straight forward. In order to easily do data-binding in XAML later on I just wrap every property I will need and fill them in the constructor. There is no mutable states or anything (the cells will be recreated on every move – no big deal for our small game). But there is one extra: the Clicked-method. Sometimes you might want to implement this with some command pattern but we will see in the next post, that I choose to wrap the board into a custom control and this control will handle a MouseButtonDown-event directly to this method. The method itself just calls an external function mapped by the constructor and this is a technique I really like (even in C#/VB.Net with Action<t> or Func<t,…>) Instead of passing a small interface around just use those “method-pointers”

type GameBoardCell internal (col : int, row : int, content : Player option, isWinning : bool, isLastSet : bool, onClicked : unit -&gt; unit) =

    member bc.IsWinningPiece = isWinning
    member bc.OccupiedByWhite = content |&gt; Option.exists (fun c -&gt; c = Player.White)
    member bc.OccupiedByBlack = content |&gt; Option.exists (fun c -&gt; c = Player.Black)
    member bc.IsLastSetPiece = isLastSet

    member bc.Column = col
    member bc.Row = row

    member bc.Clicked() = onClicked()

The View model for the game

This one will implement INotifyPropertyChanged so we have to work with events. This is no big deal but notice the implementation of the interface. Also note that I track the sync. context to post the events back on the UI thread (so of course the UI thread should instantiate the view model):

    let context = System.Threading.SynchronizationContext.Current
    let propertyChanged = new Event&lt;_,_&gt;()
    let onPropertyChanged(propName) =
        context.Post1)fun _ -&gt; propertyChanged.Trigger(this, new System.ComponentModel.PropertyChangedEventArgs(propName), null)

    interface System.ComponentModel.INotifyPropertyChanged with
        member i.add_PropertyChanged(handler) = propertyChanged.Publish.AddHandler(handler)
        member i.remove_PropertyChanged(handler) = propertyChanged.Publish.RemoveHandler(handler)

The state of the board itself will of course be mutable and I handle this with an internal ref and here I use a nice trick to hide the implementation and the ref-field itself (something you cannot easily do in C#): I create a tuple of functions with a let and hide it inside the body:

    let getState, setState, lastPos =
        let stateLock = new obj()
        let gameState = ref (GameSearchSpace.Root (Board.initialize cols rows))
        let lastPos : Coordinate option ref = ref None
        let get() = lock stateLock (fun () -&gt; !gameState)
        let set(state) =
            lock stateLock (fun () -&gt; lastPos := findLastSetCoord (!gameState).Board state.Board
                                      gameState := state)
        get, set, (fun () -&gt; !lastPos)

In addition I use a lock to handle concurrency issues (although I don’t think there would be any) as an additional demonstration why this is really nice. So you end having 3 methods (getState, setState, lastPos) using the internal ref but hiding this in a (hopefully) thread-safe way.

findLastSetCoord is just a helper function – comparing two board-states and finding the first differing cell (if any) – you can look at it below where I post the full code of the class.

Next I need two flag indicating if the computer is thinking on its move and if the board is currently changing (both may happen on a different thread if the computer is processing it’s next move) – so I need a thread-safe way.

I opted to use ManualResetEvent – objects for this but hide the setting and resetting of this by wrapping it inside a higher-order function that will produce a “side-effect” inside (doing some work like finding the next move or changing the board):

    let movingFlag = new System.Threading.ManualResetEvent(false)
    let isMoving() = movingFlag.WaitOne(0)
    let moving(f) =
        if isMoving() then false
        else try
                movingFlag.Set() |&gt; ignore
                match f() with
                | Some state -&gt; setState state
                                true
                | None       -&gt; false
             finally
                movingFlag.Reset() |&gt; ignore
                onPropertyChanged(&quot;BoardCells&quot;)
                onPropertyChanged(&quot;WhiteWon&quot;)
                onPropertyChanged(&quot;BlackWon&quot;)
                onPropertyChanged(&quot;Remis&quot;)
                onPropertyChanged(&quot;GameOver&quot;)

    let thinkingFlag = new System.Threading.ManualResetEvent(false)
    let isThinking() = thinkingFlag.WaitOne(0)
    let thinking(f) =
        try
            thinkingFlag.Set() |&gt; ignore
            onPropertyChanged(&quot;IsThinking&quot;)
            f()
        finally
            thinkingFlag.Reset() |&gt; ignore
            onPropertyChanged(&quot;IsThinking&quot;)

 

Note that this also calls our notify-event for the right properties.

A example usage is this (where the computer does his move):

    member g.ComputerMove() =
        let state = getState()
        async {
            let compute() =
                let computeMove = AlphaBetaAlgorithm.Search (computerStrenght, Board.searchSpace cols rows) &gt;&gt; Option.map fst
                thinking (fun () -&gt; computeMove (state, GameSearchSpace.getLayerRateType state))
            moving compute |&gt; ignore
        } |&gt; Async.Start

Here I also use the F#’s flagship – the mighty Async-workflow (you just have to do it Zwinkerndes Smiley)

The last interesting piece is the construction of the cells – here I just compute the state of the game (did someone win?) and create a GameBoardCell for each cell (note: I flip the row-numbers so I don’t end up filling the columns from top to bottom later on):

    member g.BoardCells =
        let state =             getState()
        let winning =           match Board.getWinCoordinates state.Board with
                                | None             -&gt; Set.empty
                                | Some (_, coords) -&gt; Set.ofArray coords
        let createCell (c,r) =  new GameBoardCell (
                                      c,
                                      rows - 1 - r, // flip or pieces hang on the ceiling
                                      Board.getPieceAt state.Board (c,r),
                                      winning.Contains (c,r),
                                      lastPos() |&gt; Option.exists (fun pos -&gt; pos = (c,r)),
                                      fun () -&gt; g.InsertPieceAt(c) |&gt; ignore )
        [ for c in 0..cols-1 do
          for r in 0..rows-1 do
          yield (c, r)
        ]
        |&gt; Seq.map createCell

Ok I think that are the main points – please don’t hesitate to ask I you have problems on any concept or code.

Here is the complete code on the view model:

type GameViewModel(cols : int, rows : int, computerStrenght : int) as this =

    let context = System.Threading.SynchronizationContext.Current
    let propertyChanged = new Event&lt;_,_&gt;()
    let onPropertyChanged(propName) =
        context.Post2)fun _ -&gt; propertyChanged.Trigger(this, new System.ComponentModel.PropertyChangedEventArgs(propName), null)

    let findLastSetCoord oldBoard newBoard =
        seq { for c in 0..cols-1 do
              for r in 0..rows-1 do
              yield (c, r)
            }
        |&gt; Seq.map (fun coord -&gt; coord, Board.getPieceAt oldBoard coord = Board.getPieceAt newBoard coord)
        |&gt; Seq.tryPick (fun (coord, same) -&gt; if not same then Some coord else None)

    let getState, setState, lastPos =
        let stateLock = new obj()
        let gameState = ref (GameSearchSpace.Root (Board.initialize cols rows))
        let lastPos : Coordinate option ref = ref None
        let get() = lock stateLock (fun () -&gt; !gameState)
        let set(state) =
            lock stateLock (fun () -&gt; lastPos := findLastSetCoord (!gameState).Board state.Board
                                      gameState := state)
        get, set, (fun () -&gt; !lastPos)

    let movingFlag = new System.Threading.ManualResetEvent(false)
    let isMoving() = movingFlag.WaitOne(0)
    let moving(f) =
        if isMoving() then false
        else try
                movingFlag.Set() |&gt; ignore
                match f() with
                | Some state -&gt; setState state
                                true
                | None       -&gt; false
             finally
                movingFlag.Reset() |&gt; ignore
                onPropertyChanged(&quot;BoardCells&quot;)
                onPropertyChanged(&quot;WhiteWon&quot;)
                onPropertyChanged(&quot;BlackWon&quot;)
                onPropertyChanged(&quot;Remis&quot;)
                onPropertyChanged(&quot;GameOver&quot;)

    let thinkingFlag = new System.Threading.ManualResetEvent(false)
    let isThinking() = thinkingFlag.WaitOne(0)
    let thinking(f) =
        try
            thinkingFlag.Set() |&gt; ignore
            onPropertyChanged(&quot;IsThinking&quot;)
            f()
        finally
            thinkingFlag.Reset() |&gt; ignore
            onPropertyChanged(&quot;IsThinking&quot;)

    let isGameOver() = Board.hasGameEnded (getState()).Board

    new() = new GameViewModel(8, 6, 4)

    member g.BoardCells =
        let state =             getState()
        let winning =           match Board.getWinCoordinates state.Board with
                                | None             -&gt; Set.empty
                                | Some (_, coords) -&gt; Set.ofArray coords
        let createCell (c,r) =  new GameBoardCell (
                                      c,
                                      rows - 1 - r, // flip or pieces hang on the ceiling
                                      Board.getPieceAt state.Board (c,r),
                                      winning.Contains (c,r),
                                      lastPos() |&gt; Option.exists (fun pos -&gt; pos = (c,r)),
                                      fun () -&gt; g.InsertPieceAt(c) |&gt; ignore )
        [ for c in 0..cols-1 do
          for r in 0..rows-1 do
          yield (c, r)
        ]
        |&gt; Seq.map createCell

    member g.GameOver with get() = isGameOver() &lt;&gt; GameState.StillRunning
    member g.WhiteWon with get() = isGameOver() = GameState.WhiteWon
    member g.BlackWon with get() = isGameOver() = GameState.BlackWon
    member g.Remis with get() = isGameOver() = GameState.Remis

    member g.IsThinking with get() = isThinking()

    member g.Reset() =
        let reset() = GameSearchSpace.Root (Board.initialize cols rows) |&gt; Some
        moving reset

    member g.InsertPieceAt(col : Column) : bool =
        let insert() =
            let state = getState()
            if Board.isMovePossible state.Board col
            then Some &lt;| Board.makeMove state col
            else None
        let ok = moving insert
        if ok then
            g.ComputerMove()
            true
        else
            false

    member g.ComputerMove() =
        let state = getState()
        async {
            let compute() =
                let computeMove = AlphaBetaAlgorithm.Search (computerStrenght, Board.searchSpace cols rows) &gt;&gt; Option.map fst
                thinking (fun () -&gt; computeMove (state, GameSearchSpace.getLayerRateType state))
            moving compute |&gt; ignore
        } |&gt; Async.Start

    interface System.ComponentModel.INotifyPropertyChanged with
        member i.add_PropertyChanged(handler) = propertyChanged.Publish.AddHandler(handler)
        member i.remove_PropertyChanged(handler) = propertyChanged.Publish.RemoveHandler(handler)

 

Let’s wrap this up for today. Next time: XAML Smiley mit geöffnetem Mund

References   [ + ]

1, 2. fun _ -&gt; propertyChanged.Trigger(this, new System.ComponentModel.PropertyChangedEventArgs(propName