Overview:




MasterMind is a code-breaking game for two players, in this case, you versus your PC.

The commercial board game version of the game was invented in 1970 and is thought to have been derived from a much earlier pencil and paper game called Bulls and Cows (source: Mastermind (board game), Wikipedia), though the name doesn't sound descriptive.

In each new game, depending on the level of difficulty, the computer chooses four, five, or six random colors, (Red, Green, Blue, or Yellow) which can be duplicated, so for example, it could be Red, Red, Red, Red.

Depending on the level of difficulty, you have eight, ten, or twelve tries to guess the correct sequence of colors, which is done by right clicking the ellipses in the current row (Figure 1), and selecting a color for each of them from the context menus, or you can cycle through the colors by left clicking the ellipses.

I said guess but in reality it's more a case of eliminating possibilities, assessing the feedback you get for each submitted line, and planning your next attempt based on that...

   

Figure 1 Current line

When you have chosen a color for each of the peg positions, the Check button becomes enabled, and you click it to check for wins and to reveal feedback on your guesses (Figure 2).

 

Figure 2 Feedback

Feedback consists of four, five, or six smaller ellipses, which can either be:

  • Black

                Indicates a correct color, placed in a correct position.

  • Red

                Indicates a correct color, placed in an incorrect position.

  • Empty (SystemColors.Control)

                Indicates you have an incorrectly guessed color.

Feedback of one Black ellipse (Figure 2) indicates that you have correctly chosen one  color, and that the color is in the correct position.

The feedback ellipses don’t directly refer to a position in the line, but to any correct or incorrect color in any position.

As you progress through the game, the current line moves up one row after you've checked a line, until, either, you correctly guess the sequence of colors (Figure 3), or you reach the top row without winning.

 

 

Figure 3 Winning

 

The possible color positions count increases steeply as the level of difficulty increases (Figure 4), but these numbers of combinations need to be adjusted because of duplicated colors which drastically reduces the possible permutations.





Figure 4
Permutations

The actual number of possible permutations, can be calculated like this:

R,G,B,Y = 24 permutations (4*3*2*1=24)

R,G,B,Y,R = 60 permutations (5*4*3*2*1/2(duplicated colors)=60)

R,G,B,Y,R,G = 180 permutations (6*5*4*3*2*1/4(duplicated colors)=180)

See Figure 5.


Figure 5 Distinct Permutations


This is the third incarnation of this article, mainly because I wanted to extend the game to make it more interesting, but also because I was concerned about overheads in the previous version (see:
Performance Optimization in Visual Basic .NET).
I also made some further changes so the program was a better example of OOP (see:
Object-Oriented Programming in Visual Basic .NET).



Code (The pegPlace custom control):

 



The rows of ellipses are actually controls derived from the Control Class with a pegColor Property (which is initially set to Empty), and a Boolean property: CMSEnabled:

 

Public Property pegColor As Color = SystemColors.Control
Public Property CMSEnabled As Boolean



...an overridden OnPaint event. (I eventually decided regular Green and Blue were too dark, so I chose LimeGreen and RoyalBlue instead):



 

Protected Overrides Sub OnPaint(pe As PaintEventArgs)
    Dim fillColor As Color = Color.FromName(Me.pegColor.Name.Replace("Green", "LimeGreen").Replace("Blue", "RoyalBlue"))      
    pe.Graphics.FillEllipse(
New SolidBrush(fillColor), New Rectangle(0, 0, 15, 15))
    pe.Graphics.DrawEllipse(Pens.Black, New Rectangle(0, 0, 15, 15))
    MyBase.OnPaint(pe)
End Sub

 


...a reset method:


Public Sub reset()
    pegColor = SystemColors.Control
    Me.Invalidate()
End Sub



...a ContextMenuStrip and some handlers and a public event:


 Public Event colorSelected(sender As Object)


Private
WithEvents cms As New ContextMenuStrip
 

Private Sub itemClicked(sender As Object, e As EventArgs)
    Dim c As String = DirectCast(sender, ToolStripMenuItem).Text
    Me.pegColor = Color.FromName(c)
    Me.Invalidate()
    RaiseEvent colorSelected(Me)
End Sub

Private Sub cms_Opening(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles cms.Opening
    e.Cancel = Not Me.CMSEnabled
End Sub 



Notice how the ContextMenuStrip only opens for controls in the current line?

In the itemClicked event, I raised an event to inform the parent control (Form1) that a color had been set for the pegPlace. On receiving that event notification, the code in the form checks whether to enable btnCheck or not.

This code is duplicated in the control’s MouseDown event, where you can click to cycle through the colors:

 

Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
    If Not Me.CMSEnabled OrElse e.Button <> Windows.Forms.MouseButtons.Left Then Return
    Dim colors() As String = {"Red", "Green", "Blue", "Yellow"}
    Dim index As Integer = Array.IndexOf(colors, Me.pegColor.Name) + 1
    Me.pegColor = Color.FromName(colors(index Mod 4))
    Me.Invalidate()
    RaiseEvent colorSelected(Me)
End Sub 

 

In the control’s constructor, I set the size, and setup the ContextMenuStrip:

A ContextMenuStrip.Items property contains objects of type: ToolStripMenuItem.
In the code I added an array of ToolStripMenuItem, created from an array of Strings, using the 
Array.ConvertAll Generic Method with a Lambda Function to convert each string to a ToolStripMenuItem, using the ToolStripMenuItem OverLoad that takes a String (the text), an Image (unused in my implementation), and an EventHandler (the common itemClicked Sub-Procedure):


Public Sub New()
    Me.Size = New Size(16, 16)
    Me.ContextMenuStrip = cms
    cms.Items.AddRange(Array.ConvertAll(Of String, ToolStripMenuItem)(New String() {"Red", "Green", "Blue", "Yellow"}, Function(s) New ToolStripMenuItem(s, Nothing, AddressOf itemClicked)))
End Sub




Code (The Form):

 



In the Load event, I load some arrays (which are reference types)and add the pegPlace colorSelected eventhandler, which enables or disables btnCheck. Setting the ToolStripComboBox.SelectedIndex sets up the UI and the call to newGame() initializes the variables for gameplay:

The winningLine array is a string array, which contains the current random winning sequence of colors. Colors are assigned with a Form level Random object, ensuring maximum randomness (see: Random Class and: Random Constructor (Int32)):



Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    ToolStripComboBox1.Items.AddRange(New String() {"Beginner", "Intermediate", "Advanced"})

    scoreLine = New pegPlace() {PegPlace1, PegPlace2, PegPlace3, PegPlace4, PegPlace37, PegPlace38}
    lines = New pegPlace(,) {{PegPlace5, PegPlace6, PegPlace7, PegPlace8, PegPlace39, PegPlace40}, _
                               {PegPlace9, PegPlace10, PegPlace11, PegPlace12, PegPlace41, PegPlace42}, _
                               {PegPlace13, PegPlace14, PegPlace15, PegPlace16, PegPlace43, PegPlace44}, _
                               {PegPlace17, PegPlace18, PegPlace19, PegPlace20, PegPlace45, PegPlace46}, _
                               {PegPlace21, PegPlace22, PegPlace23, PegPlace24, PegPlace47, PegPlace48}, _
                               {PegPlace25, PegPlace26, PegPlace27, PegPlace28, PegPlace49, PegPlace50}, _
                               {PegPlace29, PegPlace30, PegPlace31, PegPlace32, PegPlace51, PegPlace52}, _
                               {PegPlace33, PegPlace34, PegPlace35, PegPlace36, PegPlace53, PegPlace54}, _
                               {PegPlace55, PegPlace56, PegPlace57, PegPlace58, PegPlace59, PegPlace60}, _
                               {PegPlace61, PegPlace62, PegPlace63, PegPlace64, PegPlace65, PegPlace66}, _
                               {PegPlace67, PegPlace68, PegPlace69, PegPlace70, PegPlace71, PegPlace72}, _
                               {PegPlace73, PegPlace74, PegPlace75, PegPlace76, PegPlace77, PegPlace78}}

 

    For y As Integer = 0 To 11
        For x As Integer = 0 To 5
            AddHandler lines(y, x).colorSelected, AddressOf pp_colorSelected
      Next 
    Next

     ToolStripComboBox1.SelectedIndex = 0

End Sub


Private
Sub pp_colorSelected(sender As Object)
    For y As Integer = 0 To 11
        For x As Integer = 0 To 5
            If sender Is lines(y, x) Then
                Dim line() As String = Array.ConvertAll(Enumerable.Range(0, level).Select(Function(i) lines(y, i)).ToArray, Function(pp) pp.pegColor.Name)
                btnCheck.Enabled = line.All(Function(s) colors.Contains(s))
            End If
        Next
    Next
End Sub

 

Private Sub ToolStripComboBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ToolStripComboBox1.SelectedIndexChanged
    level = ToolStripComboBox1.SelectedIndex + 4
    Dim levelSizes() As Size = {New Size(154, 329), New Size(176, 373), New Size(192, 416)}
    Me.SetClientSizeCore(levelSizes(level - 4).Width, levelSizes(level - 4).Height)

   
For x As Integer = 0 To 5
        scoreLine(x).Visible = (x < level)
    Next

    For y As Integer = 0 To 11
        For x As Integer = 0 To 5
            lines(y, x).Visible = (y < level * 2) AndAlso (x < level)
        Next 
    Next

    newGame()

End Sub  

 

Private Sub newGame()
    winningLine = Enumerable.Range(0, level).Select(Function(x) colors(r.Next(0, 4))).ToArray
    lineIndex = (level * 2) - 1

    broadcastCMSEnabled(lineIndex)

    Erase clues
    ReDim clues(11)

    For x As Integer = 0 To 5
        scoreLine(x).reset()
    Next

    For y As Integer = 0 To 11
        For x As Integer = 0 To 5
            lines(y, x).reset()
        Next 
    Next

    btnNew.Enabled = False 
    Me.Invalidate()

End Sub 



...The feedback scores are stored in an array of structure (again a Reference Type array, although each element is a Value Type), with one element for each row of pegPlaces. This array is used in the Form’s Paint event, where the feedback ellipses are drawn directly on the form:


Private Structure feedback 
    Dim black As Integer
    Dim red As Integer
End Structure

Dim clues(11) As feedback

Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
    e.Graphics.DrawLine(New Pen(Brushes.Black, 3), 12, 65, Me.ClientSize.Width - 12, 65)
    Dim positions() As Point = If(level = 6, {New Point(8, 2), New Point(13, 2), New Point(18, 2), New Point(8, 7), New Point(13, 7), New Point(18, 7)}, _
                                              If(level = 5, {New Point(8, 2), New Point(13, 2), New Point(18, 5), New Point(8, 7), New Point(13, 7)}, _
                                            {New Point(8, 2), New Point(13, 2), New Point(8, 7), New Point(13, 7)}))

    For y As Integer = 11 To 0 Step -1
        If Not lines(y, 0).Visible Then Continue For
        For x As Integer = 1 To level
            Dim fillColor As Color = Nothing 
            If x <= clues(y).black Then 
                fillColor = Color.Black
            Else 
                If x <= clues(y).black + clues(y).red Then 
                    fillColor = Color.Red
                End If 
            End If

            If Not fillColor = Nothing Then 
                e.Graphics.FillEllipse(New SolidBrush(fillColor), New Rectangle(lines(y, level - 1).Right + positions(x - 1).X, lines(y, level - 1).Top + positions(x - 1).Y + 2, 3, 3))
            End If 
            e.Graphics.DrawEllipse(Pens.Black, New Rectangle(lines(y, level - 1).Right + positions(x - 1).X, lines(y, level - 1).Top + positions(x - 1).Y + 2, 3, 3))
        Next 
    Next

End Sub


...The btnCheck Click event checks the guessed sequence of colors against the winning sequence of colors. During this checking, the feedback for the row being checked is calculated, then the Paint event is invoked (Control.Invalidate Method) to draw the feedback on the form.

Also in this event, the app. checks if the game is over, either as a result of a correct guess, or because the eight, ten, or twelve guesses have been unsuccessful. The two Buttons, btnCheck and btnNew are enabled or disabled in this event, depending on the state of play:



 

Private Sub btnCheck_Click(sender As Object, e As EventArgs) Handles btnCheck.Click
    Dim line() As pegPlace = Enumerable.Range(0, level).Select(Function(x) lines(lineIndex, x)).ToArray
    Dim lineColors() As String = Array.ConvertAll(line, Function(pp) pp.pegColor.Name)
    clues(lineIndex) = New feedback 
    clues(lineIndex).black = Enumerable.Range(0, level).Count(Function(x) winningLine(x) = lineColors(x))

    For x As Integer = 0 To level - 1
        If Not winningLine(x) = lineColors(x) Then 
            For y As Integer = 0 To level - 1
                If Not winningLine(y) = lineColors(y) AndAlso winningLine(x) = lineColors(y) Then 
                    Dim temp As String = lineColors(x)
                    lineColors(x) = lineColors(y)
                    clues(lineIndex).red += 1
                    lineColors(y) = temp
                    If winningLine(y) = lineColors(y) Then 
                        clues(lineIndex).red += 1
                    End If 
                End If 
            Next 
        End If 
    Next

    Me.Invalidate()

    If clues(lineIndex).black = level Then 
        MsgBox("You've won!")
        lineIndex = -1
        broadcastCMSEnabled(lineIndex)
        btnNew.Enabled = True 
        btnCheck.Enabled = False 
    Else 
        btnCheck.Enabled = False 
        lineIndex -= 1
        broadcastCMSEnabled(lineIndex)
        If lineIndex = -1 Then 
            MsgBox("You've lost!")
            btnNew.Enabled = True 
        Else 
            Return 
        End If 
    End If

    For x As Integer = 0 To level - 1
        scoreLine(x).pegColor = Color.FromName(winningLine(x))
        scoreLine(x).Invalidate()
    Next

End Sub

 

...This method is used after each line is checked to change all of the pegPlaces’ CMSEnabled property:


 

Private Sub broadcastCMSEnabled(index As Integer)
    For y As Integer = 0 To 11
        For x As Integer = 0 To 5
            lines(y, x).CMSEnabled = (y = index)
        Next 
    Next
End Sub


...The btnNew Click event calls newGame() which puts a new random sequence of colors in the winningLine array, then resets all of the Class level variables back to their original state, for a fresh start:



 

Private Sub btnNew_Click(sender As Object, e As EventArgs) Handles btnNew.Click
    newGame()
End Sub
 



VB2013 Source code:


You can download the project here.

Online version:


The online version is available at: http://www.scproject.biz/masterMind.php

The online version differs from the desktop version, in that it doesn't use a  ContextMenu to select colors, but instead just cycles through the colors Red, Green, Blue, Yellow and repeat (and so on), when the user clicks on an ellipse in the current line.




Articles related to game programming



VB.Net - WordSearch
VB.Net - Vertex
VB.Net - Perspective
VB.Net - OOP BlackJack
VB.Net - Numbers Game
VB.Net - HangMan
Console BlackJack - VB.Net | C#
TicTacToe - VB.Net | C#
OOP Conway's Game of Life - VB.Net | C#
OOP Sudoku - VB.Net | C#
OctoWords VB.Net | C#
OOP Buttons Guessing Game VB.Net | C#
OOP Tangram Shapes Game VB.Net | C#
VB.Net - Three-card Monte
VB.Net - Split Decisions
VB.Net - Pascal's Pyramid
VB.Net - Random Maze Games
(Office) Wordsearch Creator
VB.Net - Event Driven Programming - LockWords Game
C# - Crack the Lock