MasterMind Game
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 4Permutations
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 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 event handler, which enables or disables btnCheck. Setting the ToolStripComboBox.SelectedIndex sets up the UI and the call to newGame() initializes the variables for gameplay:
The winning line 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
VB.Net - Totris