Skip to content

Commit 1b9d39f

Browse files
authored
Merge pull request ardalis#11 from ardalis/ardalis/memento
Add memento
2 parents f292180 + 0085489 commit 1b9d39f

11 files changed

+341
-1
lines changed

DesignPatternsInCSharp.sln

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1212
EndProject
1313
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GreeterService", "GreeterService\GreeterService.csproj", "{1F527722-FD31-4DC4-9FC9-8F259724107D}"
1414
EndProject
15-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DesignPatternsInCSharp.Benchmarks", "DesignPatternsInCSharp.Benchmarks\DesignPatternsInCSharp.Benchmarks.csproj", "{D6F7EB79-9204-4062-A3EB-EC001F60ECAB}"
15+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesignPatternsInCSharp.Benchmarks", "DesignPatternsInCSharp.Benchmarks\DesignPatternsInCSharp.Benchmarks.csproj", "{D6F7EB79-9204-4062-A3EB-EC001F60ECAB}"
16+
EndProject
17+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HangmanGameApp", "HangmanGame\HangmanGameApp.csproj", "{A4CB28DD-1647-4FA3-A8CF-9ECE34A525F6}"
1618
EndProject
1719
Global
1820
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -32,6 +34,10 @@ Global
3234
{D6F7EB79-9204-4062-A3EB-EC001F60ECAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
3335
{D6F7EB79-9204-4062-A3EB-EC001F60ECAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
3436
{D6F7EB79-9204-4062-A3EB-EC001F60ECAB}.Release|Any CPU.Build.0 = Release|Any CPU
37+
{A4CB28DD-1647-4FA3-A8CF-9ECE34A525F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38+
{A4CB28DD-1647-4FA3-A8CF-9ECE34A525F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
39+
{A4CB28DD-1647-4FA3-A8CF-9ECE34A525F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
40+
{A4CB28DD-1647-4FA3-A8CF-9ECE34A525F6}.Release|Any CPU.Build.0 = Release|Any CPU
3541
EndGlobalSection
3642
GlobalSection(SolutionProperties) = preSolution
3743
HideSolutionNode = FALSE
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System;
2+
3+
namespace DesignPatternsInCSharp.Memento
4+
{
5+
public class DuplicateGuessException : Exception
6+
{
7+
}
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace DesignPatternsInCSharp.Memento
2+
{
3+
// TODO: Consider refactoring to use https://github.com/ardalis/SmartEnum
4+
public enum GameResult
5+
{
6+
InProgress,
7+
Lost,
8+
Won
9+
}
10+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text.RegularExpressions;
5+
6+
namespace DesignPatternsInCSharp.Memento
7+
{
8+
public class HangmanGame
9+
{
10+
private readonly string _secretWord;
11+
private const char _maskChar = '_';
12+
protected const int INITIAL_GUESSES = 5;
13+
14+
public HangmanGame(string secretWord = "secret")
15+
{
16+
_secretWord = secretWord.ToUpperInvariant();
17+
}
18+
19+
public bool IsOver => this.Result > GameResult.InProgress;
20+
public string CurrentMaskedWord => new string(_secretWord.Select(c => PreviousGuesses.Contains(c) ? c : _maskChar).ToArray());
21+
public List<char> PreviousGuesses { get; } = new List<char>();
22+
public int GuessesRemaining => INITIAL_GUESSES - PreviousGuesses.Count(c => !CurrentMaskedWord.Contains(c));
23+
public GameResult Result { get; private set; }
24+
25+
public void Guess(char guessChar)
26+
{
27+
// TODO: Consider using Ardalis.GuardClauses
28+
// TODO: Consider returning Ardalis.Result
29+
if (char.IsWhiteSpace(guessChar)) throw new InvalidGuessException("Guess cannot be blank.");
30+
if (!Regex.IsMatch(guessChar.ToString(), "^[A-Z]$")) throw new InvalidGuessException("Guess must be a capital letter A through Z");
31+
if (IsOver) throw new InvalidGuessException("Can't make guesses after game is over.");
32+
33+
if (PreviousGuesses.Any(g => g == guessChar)) throw new DuplicateGuessException();
34+
35+
PreviousGuesses.Add(guessChar);
36+
37+
if (_secretWord.IndexOf(guessChar) >= 0)
38+
{
39+
if (!CurrentMaskedWord.Contains(_maskChar))
40+
{
41+
Result = GameResult.Won;
42+
}
43+
return;
44+
}
45+
46+
if(GuessesRemaining <= 0)
47+
{
48+
Result = GameResult.Lost;
49+
}
50+
}
51+
}
52+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace DesignPatternsInCSharp.Memento
2+
{
3+
public class HangmanGameWithUndo : HangmanGame
4+
{
5+
public HangmanMemento CreateSetPoint()
6+
{
7+
var guesses = PreviousGuesses.ToArray();
8+
return new HangmanMemento() { Guesses = guesses };
9+
}
10+
11+
public void ResumeFrom(HangmanMemento memento)
12+
{
13+
var guesses = memento.Guesses;
14+
PreviousGuesses.Clear();
15+
PreviousGuesses.AddRange(guesses);
16+
}
17+
}
18+
19+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace DesignPatternsInCSharp.Memento
2+
{
3+
public sealed class HangmanMemento
4+
{
5+
internal char[] Guesses { get; set; }
6+
}
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace DesignPatternsInCSharp.Memento
4+
{
5+
public class InvalidGuessException : Exception
6+
{
7+
public InvalidGuessException(string message) : base(message)
8+
{
9+
}
10+
}
11+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Xunit;
2+
3+
namespace DesignPatternsInCSharp.Memento.Tests
4+
{
5+
public class HangmaneGamConstructor
6+
{
7+
private const string TEST_SECRET_WORD = "TEST";
8+
9+
private HangmanGame _game = new HangmanGame(TEST_SECRET_WORD);
10+
11+
[Fact]
12+
public void HasMaskedWordOfAllUnderscores()
13+
{
14+
Assert.Equal("____", _game.CurrentMaskedWord);
15+
}
16+
}
17+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using Xunit;
3+
4+
namespace DesignPatternsInCSharp.Memento.Tests
5+
{
6+
public class HangmanGameGuess
7+
{
8+
private const string TEST_SECRET_WORD = "TEST";
9+
10+
private HangmanGame _game = new HangmanGame(TEST_SECRET_WORD);
11+
12+
[Theory]
13+
[InlineData(' ')]
14+
[InlineData('-')]
15+
[InlineData('1')]
16+
public void ThrowsGivenInvalidGuess(char invalidGuess)
17+
{
18+
Assert.Throws<InvalidGuessException>(() => _game.Guess(invalidGuess));
19+
}
20+
21+
[Fact]
22+
public void ThrowsGivenDuplicateGuess()
23+
{
24+
char wrongGuess = 'Z';
25+
_game.Guess(wrongGuess);
26+
27+
Assert.Throws<DuplicateGuessException>(() => _game.Guess(wrongGuess));
28+
}
29+
30+
[Fact]
31+
public void ThrowsGivenValidGuessWhenGameIsOver()
32+
{
33+
_game.Guess('T');
34+
_game.Guess('E');
35+
_game.Guess('S'); // game over - WON
36+
37+
Assert.Throws<InvalidGuessException>(() => _game.Guess('A'));
38+
}
39+
40+
[Fact]
41+
public void DecrementsGuessesRemainingGivenInvalidGuess()
42+
{
43+
int initialGuesses = _game.GuessesRemaining;
44+
char wrongGuess = 'Z';
45+
46+
_game.Guess(wrongGuess);
47+
48+
Assert.Equal(initialGuesses - 1, _game.GuessesRemaining);
49+
}
50+
51+
[Theory]
52+
[InlineData('E')]
53+
public void DoesNotDecrementGuessesRemainingGivenValidGuess(char correctGuess)
54+
{
55+
int initialGuesses = _game.GuessesRemaining;
56+
57+
_game.Guess(correctGuess);
58+
59+
Assert.Equal(initialGuesses, _game.GuessesRemaining);
60+
}
61+
62+
[Fact]
63+
public void MaskedWordIncludesGuessLetterIfCorrect()
64+
{
65+
char correctGuess = 'E';
66+
67+
_game.Guess(correctGuess);
68+
69+
Assert.True(_game.CurrentMaskedWord.IndexOf(Convert.ToChar(correctGuess)) >= 0);
70+
}
71+
72+
[Fact]
73+
public void SetsGameIsOverTrueAndResultLostWhenGuessesLeftReaches0()
74+
{
75+
_game.Guess('A');
76+
_game.Guess('B');
77+
_game.Guess('C');
78+
_game.Guess('D');
79+
_game.Guess('E'); // correct
80+
_game.Guess('F');
81+
82+
Assert.True(_game.IsOver);
83+
Assert.Equal(GameResult.Lost, _game.Result);
84+
}
85+
86+
[Fact]
87+
public void SetsGameIsOverTrueAndResultWonWhenEntireWordIsGuessed()
88+
{
89+
_game.Guess('T');
90+
_game.Guess('E');
91+
_game.Guess('S');
92+
93+
Assert.True(_game.IsOver);
94+
Assert.Equal(GameResult.Won, _game.Result);
95+
}
96+
97+
}
98+
}

HangmanGame/HangmanGameApp.csproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp3.1</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\DesignPatternsInCSharp\DesignPatternsInCSharp.csproj" />
10+
</ItemGroup>
11+
12+
</Project>

HangmanGame/Program.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#define SupportUndo
2+
3+
using DesignPatternsInCSharp.Memento;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading;
7+
8+
namespace HangmanGameApp
9+
{
10+
/// <summary>
11+
/// This program uses the Memento pattern
12+
/// </summary>
13+
class Program
14+
{
15+
static void Main(string[] args)
16+
{
17+
18+
#if SupportUndo
19+
// MEMENTO NOTES:
20+
// HangmanGameWithUndo == ORIGINATOR
21+
// This Main Program == CARETAKER
22+
// HangmanMemento == MEMENTO
23+
var game = new HangmanGameWithUndo();
24+
var gameHistory = new Stack<HangmanMemento>();
25+
gameHistory.Push(game.CreateSetPoint());
26+
#else
27+
var game = new HangmanGame();
28+
#endif
29+
30+
while (!game.IsOver)
31+
{
32+
Console.Clear();
33+
Console.SetCursorPosition(0, 0);
34+
Console.WriteLine("Welcome to Hangman");
35+
36+
Console.WriteLine(game.CurrentMaskedWord);
37+
Console.WriteLine($"Previous Guesses: {String.Join(',', game.PreviousGuesses.ToArray())}");
38+
Console.WriteLine($"Guesses Left: {game.GuessesRemaining}");
39+
40+
#if SupportUndo
41+
Console.WriteLine("Guess (a-z or '-' to undo last guess): ");
42+
#else
43+
Console.WriteLine("Guess (a-z): ");
44+
#endif
45+
46+
var entry = char.ToUpperInvariant(Console.ReadKey().KeyChar);
47+
48+
#if SupportUndo
49+
if(entry == '-')
50+
{
51+
if(gameHistory.Count > 1)
52+
{
53+
gameHistory.Pop();
54+
game.ResumeFrom(gameHistory.Peek());
55+
Console.WriteLine();
56+
continue;
57+
}
58+
}
59+
#endif
60+
try
61+
{
62+
game.Guess(entry);
63+
#if SupportUndo
64+
gameHistory.Push(game.CreateSetPoint());
65+
#endif
66+
Console.WriteLine();
67+
}
68+
catch (DuplicateGuessException)
69+
{
70+
OutputError("You already guessed that.");
71+
continue;
72+
}
73+
catch (InvalidGuessException)
74+
{
75+
OutputError("Sorry, invalid guess.");
76+
continue;
77+
}
78+
}
79+
80+
if (game.Result == GameResult.Won)
81+
{
82+
Console.WriteLine("CONGRATS! YOU WON!");
83+
}
84+
85+
if (game.Result == GameResult.Lost)
86+
{
87+
Console.WriteLine("SORRY, You lost this time. Try again!");
88+
}
89+
}
90+
91+
private static void OutputError(string message)
92+
{
93+
Console.WriteLine();
94+
Console.ForegroundColor = ConsoleColor.Red;
95+
Console.WriteLine(message);
96+
Console.ResetColor();
97+
Thread.Sleep(3000);
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)