ASP.NET Website #2: Mastermind

Mastermind….it sounds like a character from a Bond movie or the title of my autobiography but instead it is the theme for my second ASP.NET website :-)

It’s been a while coming but I’ll explain that in a bit more detail as I discuss some of the decisions I made. Before I begin though, it is worth recapping on the rules. It’s here, too, that you will learn that the original creator of the game is a man called Mordecai Meirowitz which I think is the coolest name ever. Topically, there is also a film by the name of Mordecai out at the moment, too. #endoftrivia. Let’s begin with the two main things that I wanted to use for this project.

Technologies Used

Compared with my previous website, Rock, Paper, Scissors, this one needed to maintain a state (or record, if you like) of what happened between page loads. I also wanted to practise using a unit testing framework, and that’s where NUNIT came in. As you will see soon, both were pretty integral to me completing the project.

Look No Hands!

To begin, let’s take a look at the main screen:

You can see that there is a heading, two buttons (the first is to make guesses and the second restarts the game), a block of four groups of radio buttons, some instructions and an area to show results underneath.

Representing The Colours and Results

I went to OpenClipart again for some pictorial inspiration and found just what I needed in the work by jean_victor_balin on this page and this one. I did the usual clipping and resizing in GIMP to produce these:

You can clearly see the colours that I used for the choices of codes in the game, but what about the last two? The first one represents when a user chooses a colour that is the right colour AND right place. The blue one is for when you have picked a colour that has been used by the computer but it’s NOT in the right place in your guess.

Let’s illustrate it with an example: The computer has chosen this secret sequence :

and the user responds with this:

Straight away, you can see that that they are almost exactly the same except the last two: they have been switched. That means we have two correct colours in the right place (green and orange) and two correct colours that are in the wrong place (red and purple). This gives us:

If all guesses had been wrong, nothing would be shown. If you get all of them right, you are told via a “Winner!” graphic. Got that?

Where Does ViewState Fit In?

I use the ViewState for two things:

  1. To keep track of all previous guesses made and,
  2. So that the webpage knows what the currently chosen four-colour code is for the computer.

*GuessesMade * is where I store an ArrayList of a class: GuessesMade. That is simply a guess and a result from scoring it, which you can see below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// Class used to hold guesses and the results for the game. Needs to be serializable because this
/// class will be added to an array and used in the ViewState
/// </summary>
[Serializable]
public class MastermindGuess
{
public MastermindGuess (string aGuess, string aResult)
{
this.Guess = aGuess;
this.Result = aResult;
}

public string Guess { get; set; }

public string Result { get; set; }
}

I create one of these for each guess the user makes and store that in the ViewState. Note that I flagged the class as being Serializable. It needs to be so that we can store it inside the page’s state. As for the computer’s choice, that was really easy. This is the Page_Load method that get’s called when the user clicks the button making their guess.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// Store the computer's colour sequence
if (ViewState["Code"] == null)
{
ms = new MastermindSequence(4);
ViewState["Code"] = ms.ToString();
}
sequenceLabel.Text = ViewState["Code"].ToString();

// Keep track of all the guesses made by the user
if (ViewState["GuessesMade"] == null)
{
ViewState["GuessesMade"] = new ArrayList();
}
}
else
{
// We have come back to the page after a guess has been made - restore key variables
ms = new MastermindSequence(ViewState["Code"].ToString());
guessesMade = (ArrayList)ViewState["GuessesMade"];
}
}

Simply put, this says that if this is the first time IIS has presented this page (line 3), then see if we have the key “Code” within the ViewState, and if not, add it by creating a computer code (via MastermindSequence) and assigning to it the output of ToString() – lines 6-11. The else statement near the bottom handles reloading of the classes when we have made a guess. If we didn’t, they data would be in ViewState but not accessible to all the other functions.

Representing the Colour Choices

How do I know which guess was made? Each of the radio buttons represents one of the colours you can choose to try to guess what the computer has hidden. When you select one, it has a value like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
<section class="buttons">
<fieldset>
<legend>1st Colour</legend>
<asp:RadioButtonList ID="Colour1" runat="server" RepeatDirection="Horizontal">
<asp:ListItem Selected="True" Value="R">Red</asp:ListItem>
<asp:ListItem Value="O">Orange</asp:ListItem>
<asp:ListItem Value="Y">Yellow</asp:ListItem>
<asp:ListItem Value="G">Green</asp:ListItem>
<asp:ListItem Value="B">Blue</asp:ListItem>
<asp:ListItem Value="P">Purple</asp:ListItem>
</asp:RadioButtonList>
</fieldset>
</section>

Imagine that there are four of these in total (try really hard because there really are!). Originally, I had really wanted to use images rather than plain-old textual names, but that upset the XHTML checker, sadly. There was also a whole host of fun trying to get everything lined up, as ever :-) On that note, I spent absolutely ages trying to get the radio buttons to go in a line. No matter what CSS skullduggery I used, I just couldn’t manage it. That is, until I saw this property:

1
RepeatDirection="Vertical"

Don’t you just love it when the solution is right in your face? It seems that this property takes precedence over any CSS you might try and rain down on the ASP control. One HUGE lesson learnt. You can see what I set it to in the code segment above.

Scoring and Checking Guesses

Within the code-behind, these radio buttons are all read within the button click event, passed to a class which holds the computer’s choice of code (MastermindSequence) and scored. The results of that are then used to dynamically update the results table at the bottom of the page, via the GuessesMade ArrayList, mentioned above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// Make a guess at what the colour sequence could be and update the display with the results
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void guessButton_Click(object sender, EventArgs e)
{
string lastGuess = Colour1.SelectedValue +
Colour2.SelectedValue +
Colour3.SelectedValue +
Colour4.SelectedValue;
int numberOfGuesses = guessesMade.Count + 1;

// Get results and add to table
String result = ms.ScoreGuess(lastGuess);
guessesMade.Add(new MastermindGuess(lastGuess, result));
ViewState["GuessesMade"] = guessesMade;

BuildGuessListTable(guessesMade);
}

You can see from the above code that the guess made by the user is passed in as a string made up of the four colours (such as “ROGY” for red, orange, green and a yellow). Let’s next look at how I work out the score (or result) for a guess.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Used to ignore colour codes when checking
private const char BLANK_CODE = 'U';

// Only valid result codes:
//
// W - You've done it! Well done.
// X - Right Colour Right Place
// - - Right Colour Wrong Place
public const string VALID_RESULT_CODES = "WX-";
public const string RIGHT_COLOUR_RIGHT_PLACE = "X";
public const string RIGHT_COLOUR_WRONG_PLACE = "-";
public const string WIN = "W";

/// <summary>
/// Given a guess and an existing sequence of colours, score that guess by
/// returning an string of X (Right Colour, Right Place), - (Right Colour, Wrong Place) or blank (Miss).
/// </summary>
/// <param name="guess"></param>
/// <returns></returns>
public string ScoreGuess(string guess)
{
string result = "";

// Null reference
if (guess == null)
{
throw new ArgumentException("ScoreGuess: null parameter passed");
}

// Guess must be the same length as the code or there is something awry
if (guess.Length != this.Sequence.Length)
{
throw new ArgumentException("ScoreGuess: Length of parameter doesn't match sequence stored");
}

// Turn these strings into arrays - we will need to remove elements
char[] sequenceCopy = this.Sequence.ToCharArray();
char[] guessCopy = guess.ToCharArray();

// Match against all the RightColourRightPlace ones first
result = findAllRightPlaceRightColour(sequenceCopy, guessCopy);

// what should be left are right colour, wrong place, and just plain old wrong!
result += findAllRightColourWrongPlace(sequenceCopy, guessCopy);

return result;
}

private string findAllRightPlaceRightColour(char[] sequenceCopy, char[] guessCopy)
{
string result = string.Empty;

for (int i = 0; i < guessCopy.Length; i++)
{
if (guessCopy[i] == sequenceCopy[i])
{
// We need to blank out the ones we have matched so we don't double count
sequenceCopy[i] = BLANK_CODE;
guessCopy[i] = BLANK_CODE;

// Label this as a Right Colour Right Place in the results
result += RIGHT_COLOUR_RIGHT_PLACE;
}
}

return result;
}

private string findAllRightColourWrongPlace(char[] sequenceCopy, char[] guessCopy)
{
string result = string.Empty;

for (int i = 0; i < guessCopy.Length; i++)
{
if (guessCopy[i] != BLANK_CODE)
{
for (int j = 0; j < sequenceCopy.Length; j++)
{
if (guessCopy[i] == sequenceCopy[j])
{
guessCopy[i] = BLANK_CODE;
sequenceCopy[j] = BLANK_CODE;

result += RIGHT_COLOUR_WRONG_PLACE;

// get out of inner for loop
break;
}
}
}
}
return result;
}

The basic algorithm is this:

  1. For each colour code (character) of the guess, go through the code stored for the computer and if they match and are in the right place, add an “X” (right colour, right place) to the results. Then blank off both characters so they don’t get re-used/double counted.
  2. For each colour code in the guess that remains, see if it’s anywhere in the computer’s code. If it is, add a “-“ representing a right colour, wrong place, to the results, and blank off both characters, as above.
  3. Do this until there are no more guess colours left.

A point worth mentioning is that I needed a copy of the guess and computer’s code because I change it. You can see that happen on lines 37 and 38.

Unit Testing With NUNIT

This part really, REALLY, helped me catch bugs. What I needed to do was to find all (most? but I tried to find all) of the kind of scenarios that could occur. I used Pragmatic Unit Testing in C# with NUNIT by Andrew Hunt, David Thomas and Matt Hargett to inspire me for this; it’s had a few rough reviews, but has helped me so far. Anyway, they advocate something called Right-BICEP, which sounds pretty powerful, for sure. Here’s what it means:

  • Right — Are the results right?
  • B — Are all the boundary conditions CORRECT?
  • I — Can you check inverse relationships?
  • C — Can you cross-check results using other means?
  • E — Can you force error conditions to happen?
  • P — Are performance characteristics within bounds?

The areas of that that I focussed on were Right, boundary conditions (B) and forcing error conditions (E). I basically wrote my tests, watched them fail, created a working test and then refactored. Here’s a breakdown showing the kinds of things that I tested for:

  • That ToString produced a proper representation of the code (i.e. ROGY for Red, Orange, Green and Yellow).
  • Passing in guesses that were too small, too big or null (causing an exception), compared to the size of the code chosen by the computer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Test]
[ExpectedException(typeof(ArgumentException))]
public void InvalidArgumentTest()
{
// null reference
MastermindSequence ms = new MastermindSequence("RRRR");
ms.ScoreGuess(null);

// argument array length of 1 when should be 4
ms.ScoreGuess("B");

// argument array length of 3 when should be 4
ms.ScoreGuess("BBB");

// argument array length of 5 when should be 4
ms.ScoreGuess("ROYGB");
}
  • That it was able to detect when the guessed colour was in the right place.
  • That it could find all examples where a right colour had been chosen, but placed in the wrong place.
  • A mix of the above two.
  • An example where the guess was a complete wash out (totally wrong).
1
2
3
4
5
6
7
8
9
/// <summary>
/// Won't match anything
/// </summary>
[Test]
public void ScoreSequenceAllIncorrectTest()
{
MastermindSequence ms = new MastermindSequence("RRRR");
Assert.AreEqual("", ms.ScoreGuess("BPBP"));
}

* That it created codes of a proper length (i.e. ask for four colours, get four colours).

How Could I Improve It?

  • I looked at this on my Windows Phone (Lumia 925) and it looks like a shocker! The sizes of the elements are all over the place and nothing looks as neatly lined up as it does on the big screen. There is clearly no responsive aspect to this website having been developed, tested and only used, on the desktop. I really want to get better at this.
  • It does tell the player that they have won but there isn’t exactly a fanfare. Perhaps it could make more of a song-and-dance about it with some video played? I don’t know.
  • I swapped things so that strings and characters were passed around to represent the colours chosen and results obtained for guesses when originally, I created a series of enumerated types. This made it so much easier to test and link to the main page, but I am left feeling that I really ought to have been more prescriptive and stuck with the structured types.
  • This could be immensely improved if I separated the view from the model. I will definitely make a more structured design in my later ones.

Lessons Learned

  • It seemed like I could use [TestFixtureSetup] as my attribute on a method even though [TestFixtureTearDown] worked as typed. I’m still not quite sure why that is, but to get around it, I used: [NUnit.Framework.TestFixtureSetUp].
  • I needed to use a serializable class for my ViewState. Prior to this project, I had been pushing around strings in my test project but of course, that feature is built into the String class.
  • NUNIT is the greatest! In fact, TDD is the greatest! I picked up so many errors and misconceptions through having to create the tests first that its value was immediately apparent. I will be using it for all projects going forward but will try to incorporate a variety of test types for practice purposes.
  • The RepeatDirection property controls how radio buttons are aligned. Repeat. It WILL WIN IN A FIGHT!

You can find the source in the GitHub repository.


Hi! Did you find this useful or interesting? I have an email list coming soon, but in the meantime, if you ready anything you fancy chatting about, I would love to hear from you. You can contact me here or at stephen ‘at’ logicalmoon.com