Ready for enhancements

We are now to a point where the code makes enough sense that we can begin to work on our change requests. We have broken the random password generation portion of the code into its own method, so now we can work on it independently.

One of the first things we need to do is to stop using Random. Random is, by nature, unpredictable and outside of our control. We need a way to feed the number generation to verify that we can get the expected outputs when Random provides specific inputs.

We will extract an interface and mock class similar to what we did for Console. Here is the first round of tests, the mock class, and the interface that were created.

In the file RandomNumberTests.cs:

public class RandomNumberTests
{
private readonly MockRandomGenerator _rand;

public RandomNumberTests()
{
_rand = new MockRandomGenerator();
}

[Fact]
public void ItExists()
{
_rand.Number();
}

[Fact]
public void ItReturnsDefaultValue()
{
// Act
var result = _rand.Number();

// Assert
Assert.Equal(0, result);
}

[Fact]
public void ItCanReturnPredeterminedNumbers()
{
// Arrange
_rand.SetNumbers(1, 2, 3, 4, 5);

// Act
var a = _rand.Number();
var b = _rand.Number();
var c = _rand.Number();
var d = _rand.Number();
var e = _rand.Number();

// Arrange
Assert.Equal(1, a);
Assert.Equal(2, b);
Assert.Equal(3, c);
Assert.Equal(4, d);
Assert.Equal(5, e);
}

[Fact]
public void ItCanHaveAMaxRange()
{
// Arrange
const int maxRange = 3;
_rand.SetNumbers(1, 2, 3, 4, 5);

// Act
var a = _rand.Number(maxRange);
var b = _rand.Number(maxRange);
var c = _rand.Number(maxRange);
var d = _rand.Number(maxRange);
var e = _rand.Number(maxRange);

// Arrange
Assert.Equal(1, a);
Assert.Equal(2, b);
Assert.Equal(3, c);
Assert.Equal(3, d);
Assert.Equal(3, e);
}

[Fact]
public void ItCanHaveAMinMaxRange()
{
// Arrange
const int minRange = 2;
const int maxRange = 3;
_rand.SetNumbers(1, 2, 3, 4, 5);

// Act
var a = _rand.Number(minRange, maxRange);
var b = _rand.Number(minRange, maxRange);
var c = _rand.Number(minRange, maxRange);
var d = _rand.Number(minRange, maxRange);
var e = _rand.Number(minRange, maxRange);

// Arrange
Assert.Equal(2, a);
Assert.Equal(2, b);
Assert.Equal(3, c);
Assert.Equal(3, d);
Assert.Equal(3, e);
}
}

In the file IRandomGenerator.cs:

public interface IRandomGenerator
{
int Number(int max = 100);
int Number(int min, int max);
}

In the file MockRandomGenerator.cs:

public class MockRandomGenerator : IRandomGenerator
{
private readonly List<int> _numbers;
private List<int>.Enumerator _numbersEnumerator;

public MockRandomGenerator(List<int> numbers = null)
{
_numbers = numbers ?? new List<int>();
_numbersEnumerator = _numbers.GetEnumerator();
}

public int Number(int min, int max)
{
var result = Number(max);

return result < min ? min : result;
}

public int Number(int max = 100)
{
_numbersEnumerator.MoveNext();
var result = _numbersEnumerator.Current;

return result > max ? max : result;
}

public void SetNumbers(params int[] args)
{
_numbers.AddRange(args);
_numbersEnumerator = _numbers.GetEnumerator();
}
}

Now, to create the production RandomGenerator class and inject it into our application.

In the file RandomGenerator.cs:

public class RandomGenerator : IRandomGenerator
{
private readonly Random _rand;

public RandomGenerator()
{
_rand = new Random();
}

public int Number(int max = 100)
{
return _rand.Next(0, max);
}

public int Number(int min, int max)
{
return _rand.Next(min, max);
}
}

In the file Program.cs:

class Program
{
static void Main(string[] args)
{
var rand = new RandomGenerator();
var inout = new ConsoleInputOutput();
var game = new Mastermind(inout, rand);

var password = args.Length > 0 ? args[0] : null;
game.Play(password);

inout.WriteLine("Press any key to quit.");
inout.Read();
}
}

In the file Mastermind.cs:

public class Mastermind
{
private readonly IInputOutput _inout;
private readonly IRandomGenerator _random;

private int _tries;

public Mastermind(IInputOutput inout, IRandomGenerator random)
{
_inout = inout;
_random = random;
}

public void Play(string password = null)
{
password = password ?? CreateRandomPassword();
var correctPositions = 0;

while (correctPositions != 4)
{
correctPositions = GuessPasswordAndCheck(password);
}

_inout.WriteLine("Congratulations you guessed the password in " +
_tries + " tries.");
}

private int GuessPasswordAndCheck(string password)
{
var guess = Guess();
return Check(guess, password);
}

private int Check(string guess, string password)
{
var checkResult = "";

for (var x = 0; x < 4; x++)
{
if (guess[x] == password[x])
{
checkResult += "+";
}
else if (password.Contains(guess[x]))
{
checkResult += "-";
}
}

_inout.WriteLine(checkResult);
return checkResult.Count(c => c == '+');
}

private string Guess()
{
_tries = _tries + 1;

_inout.Write("Take a guess: ");
var guess = _inout.ReadLine();

if (guess.Length == 4)
{
return guess.ToUpper();
}

// Password guess was wrong size - Error Message
_inout.WriteLine("Password length is 4.");
return Guess();
}

private string CreateRandomPassword()
{
var password = new[] { 'A', 'A', 'A', 'A' };

var j = 0;

password_loop:
password[j] = (char)(_random.Number(6) + 65);
j = j + 1;

if (j < 4) goto password_loop;

return new string(password);
}
}

And lastly, let's modify the gold standard test to use random password generation.

In the file GoldStandardTests.cs:

public class GoldStandardTests
{
[Fact]
public void StandardTestRun()
{
// Arrange
var inout = new MockInputOutput();
var rand = new MockRandomGenerator();
var game = new Mastermind(inout, rand);

// Arrange - Inputs
rand.SetNumbers(0, 1, 2, 5);
inout.InFeed.Enqueue("AAA");
inout.InFeed.Enqueue("AAAA");
inout.InFeed.Enqueue("ABBB");
inout.InFeed.Enqueue("ABCC");
inout.InFeed.Enqueue("ABCD");
inout.InFeed.Enqueue("ABCF");
inout.InFeed.Enqueue(" ");

// Arrange - Outputs
var expectedOutputs = new Queue<string>();
expectedOutputs.Enqueue("Take a guess: ");
expectedOutputs.Enqueue("Password length is 4." +
Environment.NewLine);
expectedOutputs.Enqueue("Take a guess: ");
expectedOutputs.Enqueue("+---" + Environment.NewLine);
expectedOutputs.Enqueue("Take a guess: ");
expectedOutputs.Enqueue("++--" + Environment.NewLine);
expectedOutputs.Enqueue("Take a guess: ");
expectedOutputs.Enqueue("+++-" + Environment.NewLine);
expectedOutputs.Enqueue("Take a guess: ");
expectedOutputs.Enqueue("+++" + Environment.NewLine);
expectedOutputs.Enqueue("Take a guess: ");
expectedOutputs.Enqueue("++++" + Environment.NewLine);
expectedOutputs.Enqueue("Congratulations you guessed the password
in 6 tries." + Environment.NewLine);

// Act
game.Play();

// Assert
inout.OutFeed.ForEach(text =>
{
Assert.Equal(expectedOutputs.Dequeue(), text);
});
}
}

Now we are ready to refactor the password generation method and extend it to provide us with the requested change. First, there is a looping structure that is not core to the language. Let's focus in on the CreateRandomPassword method and fix the looping structure:

private string CreateRandomPassword()
{
var password = new[] { 'A', 'A', 'A', 'A' };

for(var j = 0; j < 4; j++)
{
password[j] = (char)(_random.Number(6) + 65);
}

return new string(password);
}

Next, for fun, let's see if we can generalize and compress this loop, since we have a very similar loop in the Check method. While not necessary, this is fun example of reducing duplication of code. Here is what that refactoring looks like:

private int Check(string guess, string password)
{
var checkResult = "";

Times(4, x => {
if (guess[x] == password[x])
{
checkResult += "+";
}
else if (password.Contains(guess[x]))
{
checkResult += "-";
}
});

_inout.WriteLine(checkResult);
return checkResult.Count(c => c == '+');
}

private string CreateRandomPassword()
{
var password = new[] { 'A', 'A', 'A', 'A' };

Times(4, x => password[x] = (char)(_random.Number(6) + 65));

return new string(password);
}

private static void Times(int count, Action<int> act)
{
for (var index = 0; index < count; index++)
{
act(index);
}
}

Now let's do one more refactoring before we extend the application. Looking at how the characters are generated, it is not very obvious what is going on. Instead, we would like the code to be as straightforward as possible. There is no reason that the random generator class can't just directly return letters, so let's add that functionality.

In the file RandomLetterTests.cs:

public class RandomLetterTests
{
private readonly MockRandomGenerator _rand;

public RandomLetterTests()
{
_rand = new MockRandomGenerator();
}

[Fact]
public void ItExists()
{
_rand.Letter();
}

[Fact]
public void ItReturnsDefaultValue()
{
// Act
var result = _rand.Letter();

// Assert
Assert.Equal('A', result);
}

[Fact]
public void ItCanReturnPredeterminedLetters()
{
// Arrange
_rand.SetLetters('A', 'B', 'C', 'D', 'E');

// Act
var a = _rand.Letter();
var b = _rand.Letter();
var c = _rand.Letter();
var d = _rand.Letter();
var e = _rand.Letter();

// Assert
Assert.Equal('A', a);
Assert.Equal('B', b);
Assert.Equal('C', c);
Assert.Equal('D', d);
Assert.Equal('E', e);
}

[Fact]
public void ItCanHaveAMaxRange()
{
// Arrange
const char maxRange = 'C';
_rand.SetLetters('A', 'B', 'C', 'D', 'E');

// Act
var a = _rand.Letter(maxRange);
var b = _rand.Letter(maxRange);
var c = _rand.Letter(maxRange);
var d = _rand.Letter(maxRange);
var e = _rand.Letter(maxRange);

// Arrange
Assert.Equal('A', a);
Assert.Equal('B', b);
Assert.Equal('C', c);
Assert.Equal('C', d);
Assert.Equal('C', e);
}

[Fact]
public void ItCanHaveAMinMaxRange()
{
// Arrange
const char minRange = 'B';
const char maxRange = 'C';
_rand.SetLetters('A', 'B', 'C', 'D', 'E');

// Act
var a = _rand.Letter(minRange, maxRange);
var b = _rand.Letter(minRange, maxRange);
var c = _rand.Letter(minRange, maxRange);
var d = _rand.Letter(minRange, maxRange);
var e = _rand.Letter(minRange, maxRange);

// Arrange
Assert.Equal('B', a);
Assert.Equal('B', b);
Assert.Equal('C', c);
Assert.Equal('C', d);
Assert.Equal('C', e);
}
}

In the file MockRandomGenerator.cs:

public class MockRandomGenerator : IRandomGenerator
{
private readonly List<int> _numbers;
private List<int>.Enumerator _numbersEnumerator;

private readonly List<char> _letters;
private List<char>.Enumerator _lettersEnumerator;

private const char NullChar = '';

public MockRandomGenerator(List<int> numbers = null, List<char>
letters = null)
{
_numbers = numbers ?? new List<int>();
_numbersEnumerator = _numbers.GetEnumerator();

_letters = letters ?? new List<char>();
_lettersEnumerator = _letters.GetEnumerator();
}

public int Number(int min, int max)
{
var result = Number(max);

return result < min ? min : result;
}

public int Number(int max = 100)
{
_numbersEnumerator.MoveNext();
var result = _numbersEnumerator.Current;

return result > max ? max : result;
}

public void SetNumbers(params int[] args)
{
_numbers.AddRange(args);
_numbersEnumerator = _numbers.GetEnumerator();
}

public int Letter(char min, char max)
{
var result = Letter(max);

return result < min ? min : result;
}

public char Letter(char max = 'Z')
{
_lettersEnumerator.MoveNext();
var result = _lettersEnumerator.Current;
result = result == NullChar ? 'A' : result;

return result > max ? max : result;
}

public void SetLetters(params char[] args)
{
_letters.AddRange(args);
_lettersEnumerator = _letters.GetEnumerator();
}
}

In the file IRandomGenerator.cs:

public interface IRandomGenerator
{
int Number(int max = 100);
int Number(int min, int max);
char Letter(char max = 'Z');
char Letter(char min, char max);
}

In the file RandomGenerator.cs:

public class RandomGenerator : IRandomGenerator
{
private readonly Random _rand;

public RandomGenerator()
{
_rand = new Random();
}

public int Number(int max = 100)
{
return Number(0, max);
}

public int Number(int min, int max)
{
return _rand.Next(min, max);
}

public char Letter(char max = 'Z')
{
return Letter('A', max);
}

public char Letter(char min, char max)
{
return (char) _rand.Next(min, max);
}
}

In the file Mastermind.cs:

private string CreateRandomPassword()
{
var password = new[] { 'A', 'A', 'A', 'A' };

Times(4, x => password[x] = _random.Letter('F'));

return new string(password);
}

In the file GoldStandardTests.cs:

// Arrange - Inputs
rand.SetLetters('A', 'B', 'C', 'F');

That is the final refactoring for this exercise. We only have one thing to do, and that is to extend the application to generate passwords using the full range of the English alphabet. Because of the effort we put into testing and refactoring, this is now a trivial matter, and, in fact, only requires the removal of three characters in the Mastermind class.

In the file Mastermind.cs:

private string CreateRandomPassword()
{
var password = new[] { 'A', 'A', 'A', 'A' };

Times(4, x => password[x] = _random.Letter());

return new string(password);
}

Now a more complicated password, consisting of the full range of the alphabet, is created. This causes a much more difficult password, and a game with output similar to the following:

Take a guess: AAAA
Take a guess: BBBB
Take a guess: CCCC
---+
Take a guess: DDDC
+
Take a guess: EEEC
+
Take a guess: FFFC
+
Take a guess: GGGC
+
Take a guess: HHHC
+
Take a guess: IIIC
+
Take a guess: JJJC
+
Take a guess: KKKC
+
Take a guess: LLLC
+
Take a guess: mmmc
+
Take a guess: nnnc
+
Take a guess: oooc
+--+
Take a guess: oppc
++-+
Take a guess: opqc
+++
Take a guess: oprc
+++
Take a guess: opsc
+++
Take a guess: optc
+++
Take a guess: opuc
+++
Take a guess: opvc
+++
Take a guess: opwc
++++
Congratulations you guessed the password in 23 tries.

Press any key to quit.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.125.169