Avatar of cmccandless

cmccandless's solution

to Hangman in the C# Track

Published at Jul 13 2018 · 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.

Hints

This exercise requires you to work with Reactive extension. For more information, see this page .

In reactive programming it's easier to communicate intentions in marble diagrams. Tests are augmented with marble diagram information. Text format is parsable by this tool .

Running the tests

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

Initially, only the first test will be enabled. This is to encourage you to solve the exercise one step at a time. Once you get the first test passing, remove the Skip property from the next test and work on getting that test passing. Once none of the tests are skipped and they are all passing, you can submit your solution using exercism submit Hangman.cs

Further information

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

HangmanTest.cs

using System;
using Xunit;
using Microsoft.Reactive.Testing;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reactive.Concurrency;

public class HangmanTests: ReactiveTest
{
    [Fact]
    public void Initial_state_masks_the_word()
    {
        var hangman = new Hangman("foo");
        var actual = "";

        // +a->
        hangman.StateObservable.Subscribe(
            x => actual = x.MaskedWord,
            ex => throw new Exception("Should not finish with too many tries"),
            () => throw new Exception("Should not win yet"));
        Assert.Equal("___", actual);
    }

    [Fact(Skip = "Remove to run test")]
    public void Initial_state_has_9_remaining_guesses()
    {
        var hangman = new Hangman("foo");
        var actual = 9;

        // +a->
        hangman.StateObservable.Subscribe(x => actual = x.RemainingGuesses);

        Assert.Equal(9, actual);
    }

    [Fact(Skip = "Remove to run test")]
    public void Initial_state_has_no_guessed_chars()
    {
        var hangman = new Hangman("foo");
        var actual = new HashSet<char> {'x'}.ToImmutableHashSet();

        // +a->
        hangman.StateObservable.Subscribe(x => actual = x.GuessedChars);

        Assert.Equal(new HashSet<char>().ToImmutableHashSet(), actual);
    }

    [Fact(Skip = "Remove to run test")]
    public void Guess_changes_state()
    {
        var hangman = new Hangman("foo");
        HangmanState actual = null;
        hangman.StateObservable.Subscribe(x => actual = x);
        var initial = actual;

        // +--x->
        // +a-b->
        hangman.GuessObserver.OnNext('x');

        Assert.NotEqual(initial, actual);
    }

    [Fact(Skip = "Remove to run test")]
    public void Wrong_guess_decrements_remaining_guesses()
    {
        var hangman = new Hangman("foo");
        HangmanState actual = null;
        hangman.StateObservable.Subscribe(x => actual = x);
        var initial = actual;

        // +--x->
        // +a-b->
        hangman.GuessObserver.OnNext('x');

        Assert.Equal(initial.RemainingGuesses - 1, actual.RemainingGuesses);
    }

    [Fact(Skip = "Remove to run test")]
    public void After_10_incorrect_guesses_the_game_is_over()
    {
        var scheduler = new TestScheduler();
        IObservable<HangmanState> Create()
        {
            var hangman = new Hangman("foo");
            for (var i = 1; i <= 10; i++)
            {
                scheduler.Schedule(TimeSpan.FromTicks(i * 100), () => hangman.GuessObserver.OnNext('x'));
            }

            return hangman.StateObservable;
        }

        var expected = new[]
        {
            OnNext<HangmanState>(100, hangmanState => hangmanState.RemainingGuesses == 9),
            OnNext<HangmanState>(200, hangmanState => hangmanState.RemainingGuesses == 8),
            OnNext<HangmanState>(300, hangmanState => hangmanState.RemainingGuesses == 7),
            OnNext<HangmanState>(400, hangmanState => hangmanState.RemainingGuesses == 6),
            OnNext<HangmanState>(500, hangmanState => hangmanState.RemainingGuesses == 5),
            OnNext<HangmanState>(600, hangmanState => hangmanState.RemainingGuesses == 4),
            OnNext<HangmanState>(700, hangmanState => hangmanState.RemainingGuesses == 3),
            OnNext<HangmanState>(800, hangmanState => hangmanState.RemainingGuesses == 2),
            OnNext<HangmanState>(900, hangmanState => hangmanState.RemainingGuesses == 1),
            OnNext<HangmanState>(1000, hangmanState => hangmanState.RemainingGuesses == 0),
            OnError<HangmanState>(1100, ex => ex is TooManyGuessesException)
        };

        // +--x-x-x-x-x-x-x-x-x-x->
        // +a-b-c-d-e-f-g-h-i-j-#
        ITestableObserver<HangmanState> testableObserver = scheduler.Start(Create, 100, 100, 3000);

        ReactiveAssert.AreElementsEqual(expected, testableObserver.Messages);
    }

    [Fact(Skip = "Remove to run test")]
    public void Correctly_guessing_a_letter_unmasks_it()
    {
        var scheduler = new TestScheduler();
        IObservable<HangmanState> Create()
        {
            var hangman = new Hangman("foobar");
            scheduler.Schedule(TimeSpan.FromTicks(100), () => hangman.GuessObserver.OnNext('b'));
            scheduler.Schedule(TimeSpan.FromTicks(200), () => hangman.GuessObserver.OnNext('o'));
            return hangman.StateObservable;
        }

        var expected = new[]
        {
            OnNext<HangmanState>(100, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "______"),
            OnNext<HangmanState>(200, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "___b__"),
            OnNext<HangmanState>(300, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "_oob__")
        };

        // +--b-o->
        // +a-b-c->
        ITestableObserver<HangmanState> testableObserver = scheduler.Start(Create, 100, 100, 3000);

        ReactiveAssert.AreElementsEqual(expected, testableObserver.Messages);
    }

    [Fact(Skip = "Remove to run test")]
    public void Guessing_a_correct_letter_twice_counts_as_a_failure()
    {
        var scheduler = new TestScheduler();
        IObservable<HangmanState> Create()
        {
            var hangman = new Hangman("foobar");
            scheduler.Schedule(TimeSpan.FromTicks(100), () => hangman.GuessObserver.OnNext('b'));
            scheduler.Schedule(TimeSpan.FromTicks(200), () => hangman.GuessObserver.OnNext('b'));
            return hangman.StateObservable;
        }

        var expected = new[]
        {
            OnNext<HangmanState>(100, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "______"),
            OnNext<HangmanState>(200, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "___b__"),
            OnNext<HangmanState>(300, hangmanState => hangmanState.RemainingGuesses == 8 && hangmanState.MaskedWord == "___b__")
        };

        // +--b-b->
        // +a-b-c->
        ITestableObserver<HangmanState> testableObserver = scheduler.Start(Create, 100, 100, 3000);

        ReactiveAssert.AreElementsEqual(expected, testableObserver.Messages);
    }

    [Fact(Skip = "Remove to run test")]
    public void Getting_all_the_letters_right_makes_for_a_win()
    {
        var scheduler = new TestScheduler();
        IObservable<HangmanState> Create()
        {
            var hangman = new Hangman("hello");
            scheduler.Schedule(TimeSpan.FromTicks(100), () => hangman.GuessObserver.OnNext('b'));
            scheduler.Schedule(TimeSpan.FromTicks(200), () => hangman.GuessObserver.OnNext('e'));
            scheduler.Schedule(TimeSpan.FromTicks(300), () => hangman.GuessObserver.OnNext('l'));
            scheduler.Schedule(TimeSpan.FromTicks(400), () => hangman.GuessObserver.OnNext('o'));
            scheduler.Schedule(TimeSpan.FromTicks(500), () => hangman.GuessObserver.OnNext('h'));
            return hangman.StateObservable;
        }

        var expected = new[]
        {
            OnNext<HangmanState>(100, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "_____"),
            OnNext<HangmanState>(200, hangmanState => hangmanState.RemainingGuesses == 8 && hangmanState.MaskedWord == "_____"),
            OnNext<HangmanState>(300, hangmanState => hangmanState.RemainingGuesses == 8 && hangmanState.MaskedWord == "_e___"),
            OnNext<HangmanState>(400, hangmanState => hangmanState.RemainingGuesses == 8 && hangmanState.MaskedWord == "_ell_"),
            OnNext<HangmanState>(500, hangmanState => hangmanState.RemainingGuesses == 8 && hangmanState.MaskedWord == "_ello"),
            OnCompleted<HangmanState>(600)
        };

        // +--b-e-l-o-h->
        // +a-b-c-d-e-|
        ITestableObserver<HangmanState> testableObserver = scheduler.Start(Create, 100, 100, 3000);

        ReactiveAssert.AreElementsEqual(expected, testableObserver.Messages);
    }

    // Advanced mode on>
    [Fact(Skip = "Remove to run test")]
    public void Second_player_sees_the_same_game_already_started()
    {
        var scheduler = new TestScheduler();
        var player2 = scheduler.CreateObserver<HangmanState>();
        var hangman = new Hangman("hello");

        var player1 = hangman.StateObservable;
        Ready(player1);

        scheduler.Schedule(TimeSpan.FromTicks(100), () => hangman.GuessObserver.OnNext('e'));
        scheduler.Schedule(TimeSpan.FromTicks(200), () => hangman.GuessObserver.OnNext('l'));
        scheduler.Schedule(TimeSpan.FromTicks(150), () => hangman.StateObservable.Subscribe(player2));

        var expected = new[]
        {
            OnNext<HangmanState>(150, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "_e___"),
            OnNext<HangmanState>(200, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "_ell_")
        };

        // +--e--l->
        // +a-b--c->
        // ...+b-c->
        scheduler.Start();

        ReactiveAssert.AreElementsEqual(expected, player2.Messages);
    }

    private IDisposable Ready(IObservable<HangmanState> player)
    {
        return player.Subscribe(x => { });
    }

    // Expert mode on>
    [Fact(Skip = "Remove to run test")]
    public void Multiple_players_see_the_same_game_already_started()
    {
        var scheduler = new TestScheduler();
        var player2 = scheduler.CreateObserver<HangmanState>();
        var player3 = scheduler.CreateObserver<HangmanState>();
        var hangman = new Hangman("hello");

        var player1 = hangman.StateObservable;
        Ready(player1);

        scheduler.Schedule(TimeSpan.FromTicks(100), () => hangman.GuessObserver.OnNext('e'));
        scheduler.Schedule(TimeSpan.FromTicks(200), () => hangman.GuessObserver.OnNext('l'));
        scheduler.Schedule(TimeSpan.FromTicks(150), () =>
            {
                hangman.StateObservable.Subscribe(player2);
                hangman.StateObservable.Subscribe(player3);
            });

        var expected = new[]
        {
            OnNext<HangmanState>(150, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "_e___"),
            OnNext<HangmanState>(200, hangmanState => hangmanState.RemainingGuesses == 9 && hangmanState.MaskedWord == "_ell_"),
        };

        // +--e--l->
        // +a-b--c->
        // ...+b-c->
        // ...+b-c->
        scheduler.Start();

        ReactiveAssert.AreElementsEqual(expected, player2.Messages);
        ReactiveAssert.AreElementsEqual(expected, player3.Messages);
    }

    [Fact(Skip = "Remove to run test")]
    public void Player_joins_after_other_players_quit()
    {
        var scheduler = new TestScheduler();
        var player2 = scheduler.CreateObserver<HangmanState>();
        var hangman = new Hangman("a");

        var player1 = hangman.StateObservable;
        var subscription = Ready(player1);

        scheduler.Schedule(TimeSpan.FromTicks(100), () => hangman.GuessObserver.OnNext('a'));
        scheduler.Schedule(TimeSpan.FromTicks(300), () =>
            {
                hangman.StateObservable.Subscribe(player2);
            });
        scheduler.Schedule(TimeSpan.FromTicks(200), () => subscription.Dispose());

        var expected = new[]
        {
            OnCompleted<HangmanState>(300)
        };

        // +--a-|
        // +a-|
        // .....+|
        scheduler.Start();

        ReactiveAssert.AreElementsEqual(expected, player2.Messages);
    }
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive;
using System.Reactive.Subjects;

public class HangmanState
{
    public string MaskedWord { get; }
    public ImmutableHashSet<char> GuessedChars { get; }
    public int RemainingGuesses { get; }

    public HangmanState(string maskedWord, ImmutableHashSet<char> guessedChars, int remainingGuesses)
    {
        MaskedWord = maskedWord;
        GuessedChars = guessedChars;
        RemainingGuesses = remainingGuesses;
    }
}

public class TooManyGuessesException : Exception
{
}

public class Hangman
{
	private const int MaxGuesses = 9;

    public IObservable<HangmanState> StateObservable { get; }
    public IObserver<char> GuessObserver { get; }

    public Hangman(string word)
    {
		var emptySet = new HashSet<char>();
		var subject = new BehaviorSubject<HangmanState>(new HangmanState(
			Mask(word, emptySet),
			emptySet.ToImmutableHashSet(),
			MaxGuesses
		));

		this.StateObservable = subject;

		this.GuessObserver = Observer.Create<char>(ch =>
			{
				var guessedChars = new HashSet<char>(subject.Value.GuessedChars);
				var remainingGuesses = subject.Value.RemainingGuesses;
				if (word.Contains(ch) && !guessedChars.Contains(ch)) guessedChars.Add(ch);
				else remainingGuesses--;
				var masked = Mask(word, guessedChars);
				if (masked == word)
				{
					subject.OnCompleted();
				}
				else if (remainingGuesses < 0) // Game over
				{
					subject.OnError(new TooManyGuessesException());
				}
				else
				{
					subject.OnNext(new HangmanState(masked, guessedChars.ToImmutableHashSet(), remainingGuesses));
				}
			}
		);
    }

	private static string Mask(string word, HashSet<char> guessedChars) => new string(
		word.ToCharArray().Select(ch => guessedChars.Contains(ch) ? ch : '_').ToArray()
	);
}

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?