Early Fable Adventures – Building A Memory Tiles Game

Getting aquianted with fable

Just about a month ago, I had the great pleasure of attending F# eXchange 2017 in London. It was 2 days of wonderful F# indulgence with fantastic talks and workshops. It was also a great experience meeting some of the people who have pretty much been virtual mentors to me on my F# journey like Kit Eason and Scott Wlaschin. I did Kit’s pluralsight F# courses and Scott’s fsharp for fun and profit site is a constant source of learning for me so to get chatting with these guys in person was really cool.

Among all of these great talks and workshops was a talk by Alfonso García-Caro, about his Fable compiler. This is an F# to javascript compiler that hooks in really well with the javascript ecosystem. I’m lucky that in my job I am coding in F# across the full stack currently using websharper for our F# to javascript needs. So Alfonso’s talk about Fable really caught my attention. I couldn’t wait to get a bit of free time to dive in and try it out.

Getting up and running with Fable was very quick and easy thanks to this extremely helpful blog post by Krzysztof Cieslak. I started by simply trying out simple dom event handling. Out of the box, Fable has an API that appears to be a light F# wrapper around the javascript dom API.
This is accessed simply with

open Fable.Import

From here, the dom is at your finger tips through the eyes of F# through the module Fable.Import.Browser.

A little game to get my teeth into Fable

After playing around with some dom operations through Fable, I wanted something a bit more substantial to get my teeth into. Recently I was playing this common memory game with my 3 year old daughter. Its a game where you have a grid of cards with pictures on them where every picture will appear on exactly 2 cards. You lay them out randomly upside down. Then you take turns turning 2 cards over at a time. If you get matching pictures, you keep the cards but if there is no match, you turn the cards upside down again. At the end, the player with the most mathed cards wins.

I thought this would be a great game to implement with Fable so I could get started properly with this wonderful F# to javascript compiler. I simplified the game for my first version and built just a one player game. You get a grid of blacked out tiles which have hidden colors where every color will exist twice on the grid. Then you click (or tap if on a mobile device) on 2 tiles to uncover their colors. If they match, the tiles stay open permently until you start a fresh game. If they are not a match, the tiles are blacked out again the next time you click a new tile. This first version was enough to get me building something with Fable. In future versions, I plan to make it a bit more interesting by adding time challenges and levels with bigger grids among other ideas.

Dom operations through Fable to get up and running

There are a few basic things that I wanted to do with the dom for my game interaction. The following were enough for the first version of the game.

Creating a dom element with attributes

let tileDiv = Browser.document.createElement("div")
             tileDiv.className <- "col-3"
             let bc = match tile.Status with
                      | UnMatched -> tile.CoverColor
                      | AttemptingMatch -> tile.HiddenColor
                      | Matched -> tile.HiddenColor
             tileDiv.style.backgroundColor <- bc
             tileDiv.style.border <- "white solid 4px"

In the above, the Browser module is coming from the earlier Fable.Import. From this module, there is a wealth of dom related functions closely matching the standard javascript document API. Here I’m creating a div, setting its class name and a couple other properties including its border and also its background color which is a result of a pattern match to check the status of the corresponding tile in the backing model.

Adding a click event

Adding events is straight forward too. I found if I got stuck because of not knowing the function or type to use from Fable.Import.Browser, I could just look up the corresponding javascript dom function/element on the Mozilla Dom API reference and the corresponding Fable module/type/function was named similarly.
For the memory tiles game, I needed a way to add a click event to a, “Start Fresh Game” button to refresh the grid with new randomized tiles. This is shown below:

let startNewGameButton = Browser.document.createElement("button")
      startNewGameButton.addEventListener_click(fun _ -> startNewGameCallback() :> obj)
      startNewGameButton.innerText <- "Start Fresh Game"
      gameContainer.appendChild startNewGameButton |> ignore

Here, I create the button to start a new game of randomized tiles. I then add a click event which can take a regular F# function as a parameter. Here I’m passing a lambda which, when executed, will call a function called startNewGameCallback. The block of code shown here exists inside a render function in my View module and this render function takes the startNewGameCallback function as a parameter. The full source code for this is on my github MemoryTiles.fsx

I use the same Fable addEventListener_click function for handling the click event on each tile as shown below.

let eventHandler r c d = Func<Browser.MouseEvent, obj>(fun _ -> tileClickCallback r c gameBoard :> obj)
        tileDiv.addEventListener_click(eventHandler rowIndex cIndex tileDiv)

This takes a lambda wrapping another callback which has been passed to the render function in my View module. This eventually calls back to the main driver of the game – the tileClick function in my Controller module, but first let me just show the data modelling from the Model module to put this tileClick function in context.

F# typing goodness with Fable

Fable supports the standard F# data modelling constructs bringing us the F# type safety that we love so much!
For the games’ backing model, I used standard F# records and disciminated unions to represent the immutable game board which serves as a point in time state to render a point in time view and also to enable the generation of a another game board depending on user input and the value of its tiles. A tile itself is modelled as a record and the status of a tile along with the the current selection of tiles are modelled as DUs. So even though this is a game running as javascript, I have still been able to use F# types and F# immutability to express the domain and behaviour of the game. This is shown below. I added a GameBoard helper module for the state transistions of updating tiles and the selection. There is some localized mutability within the updateTile function but the function is still pure with an immutable game board going in and another one coming out. The updateSelected is also returning a new immutable game board.

type GameBoard = {
    Selection : Selection
    Board : Tile list list
}
and Tile = {
    Row : int
    Col : int
    HiddenColor : string
    CoverColor : string
    Status : TileStatus
}
and TileStatus = UnMatched | AttemptingMatch | Matched
and Selection = NoneSelected | OneSelected of Tile | TwoSelected of Tile * Tile
 
module GameBoard = 
    let updateTile r c t gb = 
        let updatedRow = gb.Board.[r] |> List.toArray |> (fun arr -> arr.[c] <- t; arr) |> List.ofArray
        {
            gb with
                Board = gb.Board |> List.toArray |> (fun arr -> arr.[r] <- updatedRow; arr) |> List.ofArray
        }
 
    let updateSelected s gb = { gb with Selection = s}

Using standard .Net events for to trigger view rendering

.Net event handling is one of my favourite features of the .Net platform and, in particular, the way it is implemented in F#. Fable allows us to use standard .Net event triggering and handling. I designed the game using a version of the model, view, controller pattern. I wanted to keep my view decoupled from the model but I still needed a way for it to be rendered whenever the game board changed. I also wanted it to remain as decoupled as possible from the controller module. The controller module passes the main engine of the game, its tileClick function, as a function parameter to the view but the view doesn’t know anything about its implementation. This is similar for the callback to start a fresh game. In order for the view to be rendered and re-rendered, I set up a an event in the in the Model module as follows.

let modelChangeEvent = (new Event<GameBoard>())

In the Controller module, there is a an initialise function which starts everything off. It publishs an observation to the above event and adds a listener, which takes a game board as a parameter and which will call render on the View whenever the a modelChangeEvent is triggered.
A startGame() function, called by initialise, simply triggers a modelChangeEvent to trigger the initial rendering of the game.

let startGame() = modelChangeEvent.Trigger (Model.generateRandomModel 4)
 
let initialise() = 
    Model.modelChangeEvent.Publish.Add(fun gameBoard -> View.render tileClick startGame gameBoard)
    startGame()

Main game logic with F# pattern matching

The main game logic is in the tileClick function of the Controller module. It receives a game board and a row and column for the clicked tile and, based on these, it determines a new game board to be created and triggers the modelChangeEvent so that the View.render function is called to re-render with the new game state.For some cases, no event is triggered and the view is not re-rendered e.g. if the same tile is clicked multiple times in succession. Standard F# pattern matching can be used here and the full function is below.

let tileClick tileRow tileCol (gameBoard: GameBoard) = 
    let board = gameBoard.Board
    let tile = board.[tileRow].[tileCol] 
    let lastSelection = gameBoard.Selection
    if tile.Status = Matched then ()
    else
        match lastSelection with
        | TwoSelected(t1, t2) when t1.Status = Matched && t2.Status = Matched && tile <> t1 && tile <> t2->
            gameBoard
            |> GameBoard.updateTile tile.Row tile.Col { tile with Status = AttemptingMatch }
            |> GameBoard.updateSelected (OneSelected { tile with Status = AttemptingMatch })
            |> modelChangeEvent.Trigger
        | TwoSelected(t1, t2) when t1.Status = Matched && tile <> t1 && tile <> t2 -> 
            gameBoard
            |> GameBoard.updateTile tile.Row tile.Col { tile with Status = AttemptingMatch }
            |> GameBoard.updateTile t2.Row t2.Col { t2 with Status = UnMatched }
            |> GameBoard.updateSelected (OneSelected { tile with Status = AttemptingMatch })
            |> modelChangeEvent.Trigger
        | TwoSelected(t1, t2) when t2.Status = Matched && tile <> t1 && tile <> t2 -> 
            gameBoard
            |> GameBoard.updateTile tile.Row tile.Col { tile with Status = AttemptingMatch }
            |> GameBoard.updateTile t1.Row t1.Col { t1 with Status = UnMatched }
            |> GameBoard.updateSelected (OneSelected ({ tile with Status = AttemptingMatch }))
            |> modelChangeEvent.Trigger
        | TwoSelected(t1, t2) when tile <> t1 && tile <> t2 -> 
            gameBoard 
            |> GameBoard.updateTile t1.Row t1.Col { t1 with Status = UnMatched }
            |> GameBoard.updateTile t2.Row t2.Col { t2 with Status = UnMatched }
            |> GameBoard.updateTile tile.Row tile.Col { tile with Status = AttemptingMatch }
            |> GameBoard.updateSelected (OneSelected ({ tile with Status = AttemptingMatch }))
            |> modelChangeEvent.Trigger           
        | OneSelected t1 when t1.HiddenColor = tile.HiddenColor && tile <> t1 -> 
            gameBoard
            |> GameBoard.updateTile t1.Row t1.Col { t1 with Status = Matched }
            |> GameBoard.updateTile tile.Row tile.Col { tile with Status = Matched }              
            |> GameBoard.updateSelected (TwoSelected ({ t1 with Status = Matched },{ tile with Status = Matched }))
            |> modelChangeEvent.Trigger
        | OneSelected t1 when tile <> t1 -> 
            gameBoard
            |> GameBoard.updateTile tile.Row tile.Col { tile with Status = AttemptingMatch }              
            |> GameBoard.updateSelected (TwoSelected (t1, { tile with Status = AttemptingMatch }))
            |> modelChangeEvent.Trigger 
        | NoneSelected when tile.Status <> Matched -> 
            gameBoard
            |> GameBoard.updateTile tile.Row tile.Col { tile with Status = AttemptingMatch }              
            |> GameBoard.updateSelected (OneSelected ({ tile with Status = AttemptingMatch }))
            |> modelChangeEvent.Trigger
        | _ -> ()

Conclusion

I had loads of fun making this game and definitely want to add more to it. My daughter has been playing away with it and having great fun!
I’ll definitely be continuing on my Fable journey.
The full source code for this game is on my github:
MemoryTiles.fsx
Also the current version of the game can be played here

One thought on “Early Fable Adventures – Building A Memory Tiles Game

Leave a Reply

Your email address will not be published. Required fields are marked *