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 -> unit) = member bc.IsWinningPiece = isWinning member bc.OccupiedByWhite = content |> Option.exists (fun c -> c = Player.White) member bc.OccupiedByBlack = content |> Option.exists (fun c -> 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<_,_>()
let onPropertyChanged(propName) =
context.Post1)fun _ -> 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 () -> !gameState) let set(state) = lock stateLock (fun () -> lastPos := findLastSetCoord (!gameState).Board state.Board gameState := state) get, set, (fun () -> !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() |> ignore match f() with | Some state -> setState state true | None -> false finally movingFlag.Reset() |> ignore onPropertyChanged("BoardCells") onPropertyChanged("WhiteWon") onPropertyChanged("BlackWon") onPropertyChanged("Remis") onPropertyChanged("GameOver") let thinkingFlag = new System.Threading.ManualResetEvent(false) let isThinking() = thinkingFlag.WaitOne(0) let thinking(f) = try thinkingFlag.Set() |> ignore onPropertyChanged("IsThinking") f() finally thinkingFlag.Reset() |> ignore onPropertyChanged("IsThinking")
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) >> Option.map fst thinking (fun () -> computeMove (state, GameSearchSpace.getLayerRateType state)) moving compute |> ignore } |> Async.Start
Here I also use the F#’s flagship – the mighty Async-workflow (you just have to do it )
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 -> Set.empty | Some (_, coords) -> 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() |> Option.exists (fun pos -> pos = (c,r)), fun () -> g.InsertPieceAt(c) |> ignore ) [ for c in 0..cols-1 do for r in 0..rows-1 do yield (c, r) ] |> 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<_,_>()
let onPropertyChanged(propName) =
context.Post2)fun _ -> 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)
}
|> Seq.map (fun coord -> coord, Board.getPieceAt oldBoard coord = Board.getPieceAt newBoard coord)
|> Seq.tryPick (fun (coord, same) -> 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 () -> !gameState)
let set(state) =
lock stateLock (fun () -> lastPos := findLastSetCoord (!gameState).Board state.Board
gameState := state)
get, set, (fun () -> !lastPos)
let movingFlag = new System.Threading.ManualResetEvent(false)
let isMoving() = movingFlag.WaitOne(0)
let moving(f) =
if isMoving() then false
else try
movingFlag.Set() |> ignore
match f() with
| Some state -> setState state
true
| None -> false
finally
movingFlag.Reset() |> ignore
onPropertyChanged("BoardCells")
onPropertyChanged("WhiteWon")
onPropertyChanged("BlackWon")
onPropertyChanged("Remis")
onPropertyChanged("GameOver")
let thinkingFlag = new System.Threading.ManualResetEvent(false)
let isThinking() = thinkingFlag.WaitOne(0)
let thinking(f) =
try
thinkingFlag.Set() |> ignore
onPropertyChanged("IsThinking")
f()
finally
thinkingFlag.Reset() |> ignore
onPropertyChanged("IsThinking")
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 -> Set.empty
| Some (_, coords) -> 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() |> Option.exists (fun pos -> pos = (c,r)),
fun () -> g.InsertPieceAt(c) |> ignore )
[ for c in 0..cols-1 do
for r in 0..rows-1 do
yield (c, r)
]
|> Seq.map createCell
member g.GameOver with get() = isGameOver() <> 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) |> Some
moving reset
member g.InsertPieceAt(col : Column) : bool =
let insert() =
let state = getState()
if Board.isMovePossible state.Board col
then Some <| 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) >> Option.map fst
thinking (fun () -> computeMove (state, GameSearchSpace.getLayerRateType state))
moving compute |> ignore
} |> 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
References
1, 2. | ↑ | fun _ -> propertyChanged.Trigger(this, new System.ComponentModel.PropertyChangedEventArgs(propName |