Avatar of martinfreedman

martinfreedman's solution

to Hangman in the C# Track

Published at Jul 13 2018 · 1 comment
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;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;

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

public enum HangmanStatus
{
    Busy,
    Win,
    Lose
}

public class HangmanGame
{
    private string _word;
    private HangmanState _state;
    private const int _maxGuesses = 9;

    public HangmanGame(string word)
    {
        _word = word;

        _state = new HangmanState
        {
            RemainingGuesses = _maxGuesses,
            Guesses = new HashSet<char>(),
            Status = HangmanStatus.Busy,
            MaskedWord = string.Concat(Enumerable.Repeat('_', _word.Length))
        };
    }

    private void OnChanged() => StateChanged?.Invoke(this, _state);

    public void Start() => OnChanged();

    public void Guess(char c)
    {
        if (!_word.Contains(c) || _state.Guesses.Contains(c))
            _state.RemainingGuesses--;
        else
            _state.MaskedWord = string.Concat(_state.MaskedWord.Zip(_word, (m, w) => w == c ? w : m));

        _state.Guesses.Add(c);

        if (!_state.MaskedWord.Contains('_'))
            _state.Status = HangmanStatus.Win;
        else if (_state.RemainingGuesses == 0)
            _state.Status = HangmanStatus.Lose;

        OnChanged();
    }

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

    public event StateHandler StateChanged;
}

Community comments

Find this solution interesting? Ask the author a question to learn more.
Avatar of martinfreedman
martinfreedman
Solution Author
commented almost 2 years ago

Easily done but not with FRP/ System.Reactive. Will revisit

Don't get it. The tests are forcing the use of conventional events, not trying to subscribe which is what is needed for Reactive Linq.

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?