Avatar of feigaoxyz

feigaoxyz's solution

to Hangman in the F# Track

Published at Apr 20 2019 · 0 comments
Instructions
Test suite
Solution

Implement the logic of the hangman game using functional reactive programming.

Hangman is a simple word guessing game.

Functional Reactive Programming is a way to write interactive programs. It differs from the usual perspective in that instead of saying "when the button is pressed increment the counter", you write "the value of the counter is the sum of the number of times the button is pressed."

Implement the basic logic behind hangman using functional reactive programming. You'll need to install an FRP library for this, this will be described in the language/track specific files of the exercise.

Running the tests

To run the tests, run the command dotnet test from within the exercise directory.

Further information

For more detailed information about the F# track, including how to get help if you're having trouble, please visit the exercism.io F# language page.

HangmanTest.fs

// This file was created manually and its version is 1.0.0.

module HangmanTest

open Xunit
open FsUnit.Xunit

open Hangman

[<Fact>]
let ``Initially 9 failures are allowed`` () =
    let game = createGame "foo"
    let states = statesObservable game

    let mutable lastProgress = Busy 9
    states.Add(fun state -> lastProgress <- state.progress) |> ignore

    startGame game |> ignore

    lastProgress |> should equal <| Busy 9

[<Fact(Skip = "Remove to run test")>]
let ``Initially no letters are guessed`` () =
    let game = createGame "foo"
    let states = statesObservable game

    let mutable lastMaskedWord = ""
    states.Add(fun state -> lastMaskedWord <- state.maskedWord) |> ignore

    startGame game |> ignore

    lastMaskedWord |> should equal "___"

[<Fact(Skip = "Remove to run test")>]
let ``After 10 failures the game is over`` () =
    let game = createGame "foo"
    let states = statesObservable game

    let mutable lastProgress = Busy 9
    states.Add(fun state -> lastProgress <- state.progress) |> ignore

    startGame game |> ignore

    [for x in 1..10 do makeGuess 'x' game] |> ignore

    lastProgress |> should equal Lose
    
[<Fact(Skip = "Remove to run test")>]
let ``Feeding a correct letter removes underscores`` () =
    let game = createGame "foobar"
    let states = statesObservable game

    let mutable lastState = None
    states.Add(fun state -> lastState <- Some state) |> ignore

    startGame game |> ignore

    makeGuess 'b' game |> ignore

    lastState.Value.progress |> should equal <| Busy 9
    lastState.Value.maskedWord |> should equal "___b__"

    makeGuess 'o' game |> ignore

    lastState.Value.progress |> should equal <| Busy 9
    lastState.Value.maskedWord |> should equal "_oob__"
    
[<Fact(Skip = "Remove to run test")>]
let ``Feeding a correct letter twice counts as a failure`` () =
    let game = createGame "foobar"
    let states = statesObservable game

    let mutable lastState = None
    states.Add(fun state -> lastState <- Some state) |> ignore

    startGame game |> ignore

    makeGuess 'b' game |> ignore

    lastState.Value.progress |> should equal <| Busy 9
    lastState.Value.maskedWord |> should equal "___b__"

    makeGuess 'b' game |> ignore

    lastState.Value.progress |> should equal <| Busy 8
    lastState.Value.maskedWord |> should equal "___b__"
     
[<Fact(Skip = "Remove to run test")>]
let ``Getting all the letters right makes for a win`` () =
    let game = createGame "hello"
    let states = statesObservable game

    let mutable lastState = None
    states.Add(fun state -> lastState <- Some state) |> ignore

    startGame game |> ignore

    makeGuess 'b' game |> ignore

    lastState.Value.progress |> should equal <| Busy 8
    lastState.Value.maskedWord |> should equal "_____"

    makeGuess 'e' game |> ignore

    lastState.Value.progress |> should equal <| Busy 8
    lastState.Value.maskedWord |> should equal "_e___"

    makeGuess 'l' game |> ignore

    lastState.Value.progress |> should equal <| Busy 8
    lastState.Value.maskedWord |> should equal "_ell_"

    makeGuess 'o' game |> ignore

    lastState.Value.progress |> should equal <| Busy 8
    lastState.Value.maskedWord |> should equal "_ello"

    makeGuess 'h' game |> ignore

    lastState.Value.progress |> should equal Win
    lastState.Value.maskedWord |> should equal "hello"
module Hangman

open System

type Progress =
    | Busy of int
    | Win
    | Lose
    | Ready

type Model =
    { answer : char list
      guesses : char list
      progress : Progress
      maskedWord : string }

type Msg =
    | Guess of char
    | Start
    | Reduce

type Game(initState : Model, updateFunc : Msg -> Model -> Model) =
    let mutable state = initState
    let mutable handlers = []
    let update = updateFunc

    let loop msg =
        let newState = update msg state
        for handler in handlers do
            handler newState
        state <- newState

    member __.Guess ch = loop (Guess ch)
    member __.Start() = loop Start
    member this.Observar = this
    member __.Add h = handlers <- h :: handlers

let rec update (msg : Msg) (model : Model) : Model =
    match msg with
    | Start -> { model with progress = Busy 9 }
    | Guess g ->
        match model.progress with
        | Busy n ->
            if List.contains g model.answer
               && (List.contains g model.guesses |> not) then
                update Reduce { model with guesses = g :: model.guesses
                                           progress = Busy n }
            else
                update Reduce { model with guesses = g :: model.guesses
                                           progress = Busy(n - 1) }
        | _ -> model
    | Reduce ->
        match model.progress with
        | Busy 0 -> { model with progress = Lose }
        | Busy n ->
            { model with maskedWord =
                             model.answer
                             |> List.map (fun c ->
                                    if List.contains c model.guesses then c
                                    else '_')
                             |> String.Concat
                         progress =
                             if model.answer
                                |> List.forall
                                       (fun c -> List.contains c model.guesses) then
                                 Win
                             else Busy n }
        | _ -> model

let createGame (answer : string) =
    let initState =
        { answer = answer |> Seq.toList
          progress = Ready
          guesses = []
          maskedWord = answer |> String.map (fun _ -> '_') }
    Game(initState, update)

let statesObservable (game : Game) = game.Observar
let startGame (game : Game) = game.Start()
let makeGuess (guess : char) (game : Game) = game.Guess guess

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?