Introduction
My daughter likes to play the Master Mind game. I don’t always have time to play the game with her, so I decided to write a program so that when I am not available she can play the game by herself using my HTC Touch.
In this article, I will briefly describe the design of this program.
Background
Master Mind is a code-breaking board game. The game is played using a decoding board, some code pegs of six (or more) different colors and some key pegs with black and white colors.
Two players play the game: code maker and code breaker. The code maker chooses a pattern of four code pegs and places them in the four holes covered by the shield, not visible to the code breaker.
The code breaker tries to guess the pattern, in both order and color. Each guess is made by placing a row of code pegs on the decoding board. Once placed, the code maker provides feedback by placing key pegs in the holes of the row with the guess. A black key peg is placed for each code peg from the guess which is correct in both color and position. A white peg indicates the existence of a correct color peg placed in the wrong position. Please refer to Wikipedia for details.
The Design
The design of the program basically follows the MVC design pattern. The model manages the state of a game, the guesses that have been made, and the answer-the pattern chosen by code maker. The view displays the state of the game and provides feedbacks. The controller handles the mouse events, updates the model and the view.
The Model
The Game
class represents a session of the Master Mind game. The Pattern
class can represents both the answer chosen and the guesses made by the code breaker. A Game
object keeps a Pattern
object as the Answer and a number of Pattern
objects as the Guesses
. When a new guess is made, the Guess()
method is called on the Game
object.
The Guess()
method returns a GuessResult
object indicating the result of the guess. The Succeeded
property returns true if the guess is exactly the same as the answer. The Failed
property returns true if the guess is different from the answer and the number of guesses has exceeded the limit.
The CodePeg
and KeyPeg
classes inherit from the Peg abstract class and override the Draw()
method.
The View and the Controller
The GameBox
class is responsible for displaying the state of a game, providing visual feedbacks to the user, and responding to the mouse events. In a sense, GameBox
acts as the Controller of the MVC pattern. GameBox
has a set of properties that define the layout of the View,
including a Board
and a PegBox
. The Board
is where the user places the code pegs and where the guess results are shown. The PegBox
is where a code peg of a particular color is chosen to be placed in the Board
.
In order to reduce the dependency of the Board
and PegBox
classes on the GameBox
class, the IGameBox
interface was introduced. The Board
and PegBox
classes depend on the IGameBox
interface, which is implemented by the GameBox
class.
To avoid flickering, we don’t draw directly onto the client area of the main form. Instead, we draw the GameBox
on the image of a PictureBox
that covers the entire client area of the main form, represented by the MainForm
class, inheriting from the Form
class.
As shown in the following sequence diagram, when the MainForm
is being loaded, the InitializeGameBox()
method is called, which creates an instance of the GameBox
class and invokes the Initialize()
method on it. The GameBox.Initialize()
method first creates one instance each of Board
and PegBox
, and then creates an bitmap of the size of client area of the main form, gets the Graphics
object from the Bitmap
, and calls the Draw()
method on the Board
and PegBox
objects with the Graphics
object as a parameter. So effectively, the Board.Draw()
and PegBox.Draw()
methods draw graphics on the bitmap, which is subsequently set to the PictureBox
object in the MainForm
.
The Board.Draw()
method draws the board, the rows, and the holes for placing code pegs and key pegs. The PegBox.Draw()
method draws the 6 code pegs with different colors for the user to choose.
public void Initialize()
{
Random ran = new Random();
CurrentColor = (PegColor)ran.Next(0, NumOfCodeColors);
{
int boardWidth =
BoxBorderWidth * 2 +
RowHeight * 5 +
CellBorderWidth;
int boardHeight =
BoxBorderWidth * 2 +
RowHeight * MaxGuesses +
CellBorderWidth * (MaxGuesses - 1);
int yMargin = (TotalHeight - boardHeight) / 2;
int xMargin = LeftMargin + yMargin;
Rectangle rcBoard = new Rectangle(xMargin, yMargin,
boardWidth, boardHeight);
m_board = new Board(this, rcBoard);
}
{
int width = RowHeight * 2;
int height = RowHeight * NumOfCodeColors / 2;
Rectangle rect = new Rectangle
(
TotalWidth - width - RightMargin,
TotalHeight - height - RightMargin,
width,
height
);
m_pegBox = new PegBox(this, rect);
}
DrawBitmap();
}
private void DrawBitmap()
{
Bitmap = new Bitmap(TotalWidth, TotalHeight);
using (Graphics g = Graphics.FromImage(Bitmap))
{
Draw(g);
}
}
public void Draw(Graphics g)
{
var rc = new Rectangle(0, 0, TotalWidth, TotalHeight);
using (SolidBrush bgBrush = new SolidBrush(BackgroundColor))
{
g.FillRectangle(bgBrush, rc);
}
m_board.Draw(g);
m_pegBox.Draw(g);
}
When the user taps on the PictureBox
, the MouseUp
event is triggered. The pbGameBox_MouseUp()
method gets the Graphics
object from the image of the PictureBox
, and passes it together with position of the mouse to the OnMouseUp()
method on the GameBox
object. Notice that the Graphics
object gotten from the bitmap of the PictureBox
control is passed to the GameBox.OnMouseUp()
method, to allow the GameBox
object to update the screen.
private void pbGameBox_MouseUp(object sender, MouseEventArgs e)
{
Status status = Status.Continue;
using (Graphics g = Graphics.FromImage(pbGameBox.Image))
{
status = m_gameBox.OnMouseUp(g, e.X, e.Y);
}
pbGameBox.Refresh();
switch (status)
{
case Status.Succeeded:
MessageBox.Show("Correct!", "MasterMind");
break;
case Status.Failed:
{
Pattern answer = m_gameBox.Game.Answer;
MessageBox.Show(string.Format(
"The correct answer is: {0} {1} {2} {3}",
answer.GetAt(0), answer.GetAt(1), answer.GetAt(2),
answer.GetAt(3)),
"MasterMind");
}
break;
}
}
The GameBox.OnMouseUp()
method calls the OnMouseUp()
method on the Board
object or the PegBox
object depending on the position of the mouse.
public Status OnMouseUp(Graphics g, int x, int y)
{
if (m_board.Rectangle.Contains(x, y))
{
return m_board.OnMouseUp(g, x, y);
}
else if (m_pegBox.Rectangle.Contains(x, y))
{
m_pegBox.OnMouseUp(g, x, y);
}
return Status.Continue;
}
If the mouse is in the row of the current guess, the Board.OnMouseUp()
method check puts the current selected code peg into the closest hole. If the current guess is completed, it displays the guess result and returns the status of the game.
public Status OnMouseUp(Graphics g, int x, int y)
{
var rowRect = GetCurrentRowRect();
if (rowRect.Contains(x, y))
{
PutCodePeg(g, rowRect, x);
if (m_gameBox.CurrentGuess.IsComplete)
{
GuessResult r = m_gameBox.Game.Guess(m_gameBox.CurrentGuess);
DrawGuessResult(g, r, y);
DrawRowIndicator(g);
m_gameBox.CurrentGuess.Clear();
if (r.Failed)
{
return Status.Failed;
}
else if (r.Succeeded)
{
return Status.Succeeded;
}
}
}
return Status.Continue;
}
The PegBox.OnMouseUp()
methods gets and draws the color selected.
public void OnMouseUp(Graphics g, int x, int y)
{
x -= Rectangle.Left;
y -= Rectangle.Top;
int i = y / m_gameBox.RowHeight; int j = x / m_gameBox.RowHeight;
m_gameBox.CurrentColor = m_gameBox.Game.PegColors[i * 2 + j];
DrawCurrentColor(g);
}
Some Other Code
Two methods of the Pattern
class are worth mentioning: Generate()
and Compare()
. The Generate()
method generates a pattern randomly. In the current implementation, the 4 colors in the generated pattern must be unique. The m_random
member variable was initialized in the constructor.
public void Generate(int colors)
{
var usedColors = new Dictionary<int, int>();
for (int i = 0; i < 4; i++)
{
int n = -1;
do
{
n = m_random.Next(0, colors - 1);
} while (usedColors.ContainsKey(n));
usedColors[n] = n;
m_pegs[i] = m_pegColors[n];
}
}
The Compare()
method compares the given pattern with the answer. It first checks how many pegs are correct in both color and order, and then checks how many pegs are correct in color but in wrong position. The black
variable indicates the code pegs that should be marked with black key pegs. The blackWhite
variable indicates the code pegs that should be marked with either black or white key pegs.
public GuessResult Compare(Pattern pegs)
{
int blackCount = 0;
int whiteCount = 0;
bool[] black = new bool[4] { false, false, false, false };
for (int i = 0; i < 4; i++)
{
if (m_pegs[i] == pegs.GetAt(i))
{
blackCount++;
black[i] = true;
}
}
if (blackCount < 4)
{
bool[] blackWhite = new bool[4];
black.CopyTo(blackWhite, 0);
for (int j = 0; j < 4; j++)
{
if (!black[j])
{
for (int k = 0; k < 4; k++)
{
if (!blackWhite[k] && m_pegs[j] == pegs.GetAt(k))
{
whiteCount++;
blackWhite[k] = true;
break;
}
}
}
}
}
return new GuessResult(blackCount, whiteCount);
}
Unit Tests
A Unit Test project, named TestMasterMind, is created to unit test the classes that are unit-testable. Right now, only the Pattern
and Game
classes are unit-tested.
As an example, the GameTester.TestSucceeded()
test method makes two guesses and checks if the result of the second guess is Succeded
.
[TestMethod]
public void TestSucceeded()
{
m_game.NewGame();
m_game.Answer.SetAt(0, PegColor.Green);
m_game.Answer.SetAt(1, PegColor.Magenta);
m_game.Answer.SetAt(2, PegColor.Orange);
m_game.Answer.SetAt(3, PegColor.Red);
Pattern p = new Pattern();
p.SetAt(0, PegColor.Orange);
p.SetAt(1, PegColor.Blue);
p.SetAt(2, PegColor.Green);
p.SetAt(3, PegColor.Yellow);
GuessResult r = m_game.Guess(p);
Assert.AreEqual(false, r.Succeeded);
Assert.AreEqual(false, r.Failed);
p.SetAt(0, PegColor.Green);
p.SetAt(1, PegColor.Magenta);
p.SetAt(2, PegColor.Orange);
p.SetAt(3, PegColor.Red);
r = m_game.Guess(p);
Assert.AreEqual(true, r.Succeeded);
Assert.AreEqual(false, r.Failed);
}
Note that since the TestMasterMind project depends on the MasterMind project, a Smart Device project, when you run the unit tests, you will be asked to deploy the MasterMind program to an emulator, which is a bit annoying.
Points of Interest
Drawing graphics in the bitmap of a PictureBox
control helps avoid flickering.
Although this program is very small, defining the unit-testable domain classes helped make the design loose-coupled and shorten the development process.