Avatar of ErikSchierboom

ErikSchierboom's solution

to Hangman in the C# Track

Published at Jul 13 2018 · 2 comments
Instructions
Test suite
Solution

Note:

This solution was written on an old version of Exercism. The tests below might not correspond to the solution code, and the exercise may have changed since this code was written.

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.

Hints

This exercise requires you to work with events. For more information, see [this page] (https://docs.microsoft.com/en-us/dotnet/articles/csharp/programming-guide/events/) .

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

HangmanTest.cs

using Xunit;

public class HangmanTest
{
    [Fact]
    public void Initially_9_failures_are_allowed()
    {
        var game = new HangmanGame("foo");

        HangmanState lastState = null;
        game.StateChanged += (sender, state) => lastState = state;

        game.Start();

        Assert.Equal(HangmanStatus.Busy, lastState.Status);
        Assert.Equal(9, lastState.RemainingGuesses);
    }

    [Fact(Skip = "Remove to run test")]
    public void Initially_no_letters_are_guessed()
    {
        var game = new HangmanGame("foo");
        
        HangmanState lastState = null;
        game.StateChanged += (sender, state) => lastState = state;

        game.Start();

        Assert.Equal("___", lastState.MaskedWord);
    }

    [Fact(Skip = "Remove to run test")]
    public void After_10_failures_the_game_is_over()
    {
        var game = new HangmanGame("foo");
        
        HangmanState lastState = null;
        game.StateChanged += (sender, state) => lastState = state;

        game.Start();

        for (var i = 0; i < 10; i++)
        {
            game.Guess('x');
        }

        Assert.Equal(HangmanStatus.Lose, lastState.Status);
    }

    [Fact(Skip = "Remove to run test")]
    public void Feeding_a_correct_letter_removes_underscores()
    {
        var game = new HangmanGame("foobar");

        HangmanState lastState = null;
        game.StateChanged += (sender, state) => lastState = state;

        game.Start();

        game.Guess('b');

        Assert.Equal(HangmanStatus.Busy, lastState.Status);
        Assert.Equal(9, lastState.RemainingGuesses);
        Assert.Equal("___b__", lastState.MaskedWord);

        game.Guess('o');

        Assert.Equal(HangmanStatus.Busy, lastState.Status);
        Assert.Equal(9, lastState.RemainingGuesses);
        Assert.Equal("_oob__", lastState.MaskedWord);
    }

    [Fact(Skip = "Remove to run test")]
    public void Feeding_a_correct_letter_twice_counts_as_a_failure()
    {
        var game = new HangmanGame("foobar");
        
        HangmanState lastState = null;
        game.StateChanged += (sender, state) => lastState = state;

        game.Start();

        game.Guess('b');

        Assert.Equal(HangmanStatus.Busy, lastState.Status);
        Assert.Equal(9, lastState.RemainingGuesses);
        Assert.Equal("___b__", lastState.MaskedWord);

        game.Guess('b');

        Assert.Equal(HangmanStatus.Busy, lastState.Status);
        Assert.Equal(8, lastState.RemainingGuesses);
        Assert.Equal("___b__", lastState.MaskedWord);
    }

    [Fact(Skip = "Remove to run test")]
    public void Getting_all_the_letters_right_makes_for_a_win()
    {
        var game = new HangmanGame("hello");
        
        HangmanState lastState = null;
        game.StateChanged += (sender, state) => lastState = state;

        game.Start();

        game.Guess('b');

        Assert.Equal(HangmanStatus.Busy, lastState.Status);
        Assert.Equal(8, lastState.RemainingGuesses);
        Assert.Equal("_____", lastState.MaskedWord);

        game.Guess('e');

        Assert.Equal(HangmanStatus.Busy, lastState.Status);
        Assert.Equal(8, lastState.RemainingGuesses);
        Assert.Equal("_e___", lastState.MaskedWord);

        game.Guess('l');

        Assert.Equal(HangmanStatus.Busy, lastState.Status);
        Assert.Equal(8, lastState.RemainingGuesses);
        Assert.Equal("_ell_", lastState.MaskedWord);

        game.Guess('o');

        Assert.Equal(HangmanStatus.Busy, lastState.Status);
        Assert.Equal(8, lastState.RemainingGuesses);
        Assert.Equal("_ello", lastState.MaskedWord);

        game.Guess('h');

        Assert.Equal(HangmanStatus.Win, lastState.Status);
        Assert.Equal("hello", lastState.MaskedWord);
    }
}
using System.Collections.Generic;
using System.Linq;

public delegate void HangmanChangedEventHandler(object sender, HangmanState state);

public class HangmanState
{
    public HangmanGame.Status Status { get; set; }
    public int RemainingGuesses { get; set; }
    public string MaskedWord { get; set; }
    public HashSet<char> Guesses { get; set; }
}

public class HangmanGame
{
    private const int NumberOfAllowedGuesses = 9;
    private const char UnguessedCharacterPlaceHolder = '_';

    private readonly string word;
    private readonly HangmanState state;

    public enum Status
    {
        Busy,
        Win,
        Lose
    }

    public HangmanGame(string word)
    {
        this.word = word;

        state = new HangmanState
        {
            RemainingGuesses = NumberOfAllowedGuesses,
            Guesses = new HashSet<char>()
        };

        UpdateMaskedWord();
        UpdateStatus();
    }

    public event HangmanChangedEventHandler StateChanged;

    public void Start()
    {
        StateChanged?.Invoke(this, state);
    }

    public void Guess(char c)
    {
        UpdateRemainingGuesses(c);
        UpdateMaskedWord();
        UpdateStatus();

        StateChanged?.Invoke(this, state);
    }

    private void UpdateRemainingGuesses(char c)
    {
        if (UnknownCharacter(c) || CharacterAlreadyGuessed(c))
            state.RemainingGuesses--;

        state.Guesses.Add(c);
    }

    private bool UnknownCharacter(char c) => word.All(x => x != c);

    private bool CharacterAlreadyGuessed(char c) => state.Guesses.Contains(c);    

    private void UpdateMaskedWord()
    {
        state.MaskedWord = new string(word.Select(c => state.Guesses.Contains(c) ? c : UnguessedCharacterPlaceHolder).ToArray());
    }

    private void UpdateStatus()
    {
        if (state.MaskedWord == word)
            state.Status = Status.Win;
        else if (state.RemainingGuesses < 0)
            state.Status = Status.Lose;        
        else
            state.Status = Status.Busy;
    }
}

Community comments

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

Like me not done with FRP/ System.Reactive. Are we missing something?

Avatar of ErikSchierboom

@martinfreedman commented:

Like me not done with FRP/ System.Reactive. Are we missing something?

I think this is just how the exercise has been structured. We should consider restructuring it.

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?