Developing DeducTile Reasoning
DeducTile Reasoning is a game where four tiles appear top to bottom, and the player must guess the correct sequence of the tiles based on the clues given. Like Brain Drain Concentration, the user’s opponent is the clock—he must solve the puzzle in a set amount of time. Figure 4-3 shows the game.
Figure 4-3: DeducTile Reasoning, guessing the order of tiles by textual clues
In this game, the puzzle is constrained by the fact that each shape and each color is represented only once each. This limits the number of possible puzzle permutations to 576. Where did that number come from? Well, you’re dealing with four shapes, which you can represent using the integers 0, 1, 2, and 3. There are 24 ways to order these four integers:
0,1,2,3 | 2,0,1,3 | 3,2,0,1 |
0,1,3,2 | 2,0,3,1 | 3,1,0,2 |
0,2,1,3 | 3,0,1,2 | 1,2,3,0 |
0,2,3,1 | 3,0,2,1 | 1,3,2,0 |
0,3,2,1 | 1,2,0,3 | 2,3,1,0 |
0,3,1,2 | 1,3,0,2 | 2,1,3,0 |
1,0,2,3 | 2,1,0,3 | 3,2,1,0 |
1,0,3,2 | 2,3,0,1 | 3,1,2,0 |
This gives you all the possible arrangements of the four shapes. The same 24 permutations represent the possible arrangements of colors. Multiplying 24 by 24 yields the total possible puzzle permutations of four shapes and four colors, giving a total of 576 puzzle possibilities.
Generating Clues
The difficult part of coding this game is generating the clues. The game must give enough information to solve the puzzle or it’ll be impossible to win. Take, for example, a case where you give a single clue:
The Red Tile is first.
This obviously isn’t enough information to solve the puzzle. If this is the only clue you give, the game would devolve into a random guessing game, which isn’t fun. Adding a second clue yields a bit more information:
The Circle is not Red.
This clue tells the player that the circle is green, blue, or yellow. When coupled with the first clue, the player knows that the circle must be in position 2, 3, or 4. This is more information than the player originally had but still not enough information to solve the puzzle.The game must be sure it generates enough clues so that the player can solve the puzzle. To accomplish this, the program generates all 576 possible solutions and then picks one at random as the puzzle that will be presented to the user. Then, the program generates a clue that pertains to the puzzle, and all the non-solutions that this clue doesn’t pertain to are “crossed off the list.” Clue generation continues until only one of the 576 puzzles remains—this remaining puzzle is the same as the puzzle presented to the user.
Creating the FourTuple Class
You could spend a ton of time hard-coding the 576 possible puzzles, but you’re better off having the computer do the work for you. The first step of generating these puzzles is to store the 24 permutations of the numbers 0–3. Listing 4-9 shows the class to store four integers.Listing 4.9: The FourTuple Class, Meant to Store Four Integers Only
Private Class FourTuple
Public a As Integer
Public b As Integer
Public c As Integer
Public d As Integer
Public Sub New(ByVal ia As Integer, ByVal ib As Integer, _
ByVal ic As Integer, ByVal id As Integer)
a= ia
b = ib
c = ic
d = id
End Sub
End Class
Note | This listing breaks one of my own rules by not even bothering to make “real” properties on this class to store the four integers—instead it uses simple public variables, so you can be pretty sure I’m not going to use this quickie class for anything important. |
Creating the TileCombo Class
The next class, called TileCombo, holds four individual ColoredShape classes. Listing 4-10 shows the public interface for this class.
Listing 4.10: The TileCombo Class, Used During Clue Generation
Public Class TileCombo
Public Sub New(ByVal a As ColoredShape, ByVal b As ColoredShape, _
ByVal c As ColoredShape, ByVal d As ColoredShape)
Public Overloads Function Equals(ByVal t As TileCombo) As Boolean
ReadOnly Property ColoredShape(ByVal i As Integer) As ColoredShape
Property Eliminated() As Boolean
Public Overrides Function ToString() As String
End Class
As you can see, the four ColoredShapes are passed right into the constructor, and no method exists to change them. The program uses a Boolean property named Eliminated when generating clues to determine if this clue has been “crossed off the list” as a possible puzzle solution based on the clues generated to this point.
Creating the TileComboPossiblesList Class
The group of 576 TileCombo classes would also benefit from being encapsulated into their own class because it seems natural to think of this “group of tiles” as a separate object with its own responsibilities. I named this class TileComboPossiblesList; Listing 4-11 shows its public interface. TileComboPossiblesList stores the 576 possible solutions and the one puzzle that will be shown to the user.Listing 4.11: TileComboPossiblesList
Public Class TileComboPossiblesList
Public Sub New()
Public Function Item(ByVal i As Integer) As TileCombo
Public Function Solution() As TileCombo
Public Function SolutionsLeft() As Integer
Public Function AllNonSolutionsEliminated() As Boolean
Public Function NumberClueWouldEliminate_
(ByVal c As TileComboClue) As Integer
Public Sub EliminateBasedOnClue(ByVal c As TileComboClue)
Public Sub EnumerateRemaining()
End Class
It’s the job of TileComboPossiblesList to generate the 576 permutations and then choose one at random as the puzzle to be shown to the player. In addition, clue instances will be passed into this class to eliminate “bad” tile combos from the list until only one remains.As promised, the design for this seemingly simple game is getting complicated in a hurry. Perhaps seeing some code will help make some sense of how the classes fit together. Listing 4-12 shows a method on TileComboPossiblesList named GeneratePossibles, which creates the permutations of the solutions. This class is declared as private, which explains why it wasn’t listed in the public interface shown in Listing 4-11.Listing 4.12: The GeneratePossibles Method
Private Sub GeneratePossibles()
Dim FTuples As ArrayList
Dim i, j As Integer
Dim oTi, oTj As FourTuple
'these are the 24 ordered possibilities for integers 0,1,2 ,3
FTuples = New ArrayList
With FTuples
.Add(New FourTuple(0, 1, 2, 3))
.Add(New FourTuple(0, 1, 3, 2))
.Add(New FourTuple(0, 2, 1, 3))
(repeat 24 times for every numeric combination)
End With
'we need every permuation of every permutation.
'this gives us 576 combinations
FPossibles = New ArrayList
For Each oTi In FTuples
For Each oTj In FTuples
FPossibles.Add(New TileCombo( _
ColoredShape.CreateByIndex(oTi.a, oTj.a), _
ColoredShape.CreateByIndex(oTi.b, oTj.b), _
ColoredShape.CreateByIndex(oTi.c, oTj.c), _
ColoredShape.CreateByIndex(oTi.d, oTj.d)))
Next
Next
End Sub
Listing 4-12 doesn’t show the repetitive code inside the block that starts With FTuples. As you can see, each numeric combination of integers 0–3 is stored in a FourTuple instance, and each instance is in turn stored in an ArrayList named FTuples. From there, a double For Each loop begins and inside a new TileCombo is created based on the integers stored in the FTuples ArrayList.
Creating the TileComboClue Class and Its Subclasses
What’s now needed is a class to encapsulate a single textual clue. Listing 4-13 shows the code that serves this purpose, the TileComboClue class.Listing 4.13: The TileComboClue Class
Public MustInherit Class TileComboClue
Public Sub New(ByVal t As TileCombo)
MustOverride Function ClueText() As String
MustOverride Function CluePertainsTo(ByVal t As TileCombo) As Boolean
Function HalfTheTime() As Boolean
Public Function PositionalText(ByVal iPos As Integer) As String
End Class
This class is declared MustInherit, which means you can’t create an instance of this class directly. Instead, you must create instances of the subclasses of this class. The subclasses of TileComboClue represent the several different styles of textual clues. For example, one clue type links the color and shape of a tile by stating something such as The Square is Red or The Blue tile is a Circle. Another type of clue indicates position, such as The first tile is Green or The second tile is the Triangle. There are eight separate clue subclasses, and you can add more by simply adding subclasses to the program. You’ll see a neat trick that allows the program to randomly select which clue subclasses to use a bit later in the section “Creating the PuzzleGenerator Class.”Let’s look at one of the clue subclasses in detail to learn how it functions. Listing 4-14 shows the clue class that links the color of one of the tiles to the shape. The ComboClueShapeIsColor class tells the player The Square is Yellow or The Green Tile is a Triangle.Listing 4.14: The ComboClueShapeIsColor Class
Public Class ComboClueTheShapeIsColor
Inherits TileComboClue
Private FTile As ColoredShape
Public Sub New(ByVal t As TileCombo)
MyBase.New(t)
Dim oRand As New Random
FTile = t.ColoredShape(oRand.Next(0, 4))
End Sub
Overrides Function ClueText() As String
If HalfTheTime() Then
Return "The " & FTile.ColorWord & _
" tile is a " & FTile.ShapeWord
Else
Return "The " & FTile.ShapeWord & _
" is " & FTile.ColorWord
End If
End Function
'return true if the
Overrides Function CluePertainsTo(ByVal t As TileCombo) As Boolean
Dim ocs As ColoredShape
Dim i As Integer
For i = 0 To 3
ocs = t.ColoredShape(i)
If ocs.Color.Equals(FTile.Color) Then
Return ocs.ShapeWord.Equals(FTile.ShapeWord)
End If
Next
End Function
End Class
The constructor of all the TileComboClue classes receives a TileCombo instance as its parameter. In all cases, this TileCombo instance is the solution to the puzzle that will be displayed to the player. It’s the job of the class to take this puzzle and construct a clue based on the puzzle. This class generates clue by simply choosing one of the four tiles randomly and storing it in the local variable FTile for later use.The method ClueText is the function that returns the text of the clue. As with many of the clues, the clue class can return the same clue in different forms to further vary the appearance of the clues. In this method, a sentence of the form The <shape> is <color> displays 50 percent of the time, and a sentence of the form The <color> tile is <shape> displays the remainder of the time. Both forms describe the same information, obviously; they just do so slightly differently.
The clue classes use the CluePertainsTo method to determine if a passed-in TileCombo is correctly described by the clue. This is used when crossing possible tile combinations off the list when whittling the list of 576 possible puzzles down to the final solution. In this subclass, each of the four tiles is scanned until the one matching the clue is found. Once that tile is found, the method returns True if that tile is the same shape as the tile in the solution. For example, if one instance of this class generated the clue The Triangle is Red, then the CluePertainsTo method would return True for any TileCombo passed into it in which the triangle tile is also red. The method would return False for all TileCombos passed in where the triangle is some other color.
Creating the PuzzleGenerator Class
The final class to discuss in the clue generation process is the class that links all the other classes together. You can name this class, appropriately enough, PuzzleGenerator. One instance of the PuzzleGenerator class is created each time the game is played. Listing 4-15 shows the complete PuzzleGenerator class so you can study how all these other classes fit together.Listing 4.15: The PuzzleGenerator Class
Public Class PuzzleGenerator
Private oPL As TileComboPossiblesList
Private FClueClassNames As ArrayList
Private FClues As ArrayList
Private FAsm As Reflection.Assembly
Public Sub New()
MyBase.New()
oPL = New TileComboPossiblesList
GetClueClasses()
GenerateClues()
End Sub
'use reflection to find all the clue subclasses and load them up
Private Sub GetClueClasses()
FAsm = System.Reflection.Assembly.GetExecutingAssembly()
Dim t As Type
FClueClassNames = New ArrayList
'GetType used when not instantiating
'(can't instantiate b/c of MustInherit)
Dim tParent As Type = GetType(TileComboClue)
For Each t In FAsm.GetTypes
If t.IsSubclassOf(tParent) Then
FClueClassNames.Add(t)
End If
Next
End Sub
Private Sub GenerateClues()
Dim c As TileComboClue
Dim oTyp As Type
Dim oRand As New Random
Dim oArgs() As Object = {oPL.Solution}
Debug.WriteLine("-------------------------------------------")
Debug.WriteLine(oPL.Solution.ToString)
Debug.WriteLine("-------------------------------------------")
FClues = New ArrayList
Do
oTyp = FClueClassNames.Item(_
oRand.Next(0, FClueClassNames.Count))
c = Activator.CreateInstance(oTyp, oArgs)
If oPL.NumberClueWouldEliminate(c) > 0 Then
oPL.EliminateBasedOnClue(c)
FClues.Add(c)
End If
Loop Until oPL.AllNonSolutionsEliminated
End Sub
Public Sub PopulateListBox(ByVal lb As ListBox)
Dim c As TileComboClue
lb.Items.Clear()
For Each c In FClues
lb.Items.Add(c.ClueText)
Next
End Sub
Public Function IsSolution(ByVal a As ColoredShape, _
ByVal b As ColoredShape, ByVal c As ColoredShape, _
ByVal d As ColoredShape) As Boolean
Dim t As New TileCombo(a, b, c, d)
Return t.Equals(oPL.Solution)
End Function
End Class
As shown in Listing 4-15, the PuzzleGenerator class uses a private instance of the TileComboPossiblesList class to store the 576 puzzle possibilities and remove them in the GenerateClues method. It accomplishes this by generating a random clue that pertains to the solution and then attempting to remove remaining puzzle possibilities whose tile arrangements don’t match this clue. If new possibilities are indeed removed, then this clue is added to the list of clues to be shown to the user. If the newly generated clue doesn’t remove any new tile combinations, then this clue adds no new information to the puzzle and it’s discarded. An example of this might be a new clue being generated that reads The Triangle is Red when clues already exist that read The Triangle is neither Blue nor Yellow and The Triangle is not Green. Because of these two clues, the clue engine has already crossed off all possibilities except those in which the triangle is red, so this latest clue adds no new information.One other interesting piece of code in the PuzzleGenerator class is how it selects one of the clue classes randomly as the generation process occurs. This happens using a feature of the .NET Framework known as reflection. Reflection is the ability to write programs that describe the class structure of other programs. The clich example is the ability to write a program that can enumerate all of the classes in the .NET Framework for display in a treeview control, similar to the Visual Studio Object Browser (shown in Figure 4-4). You can imagine that in order to write such a program, you’d need the ability to open the .NET Framework assemblies (DLLs) and loop through the classes found within them.

Figure 4-4: You could write a program such as the Object Browser using reflection
In DeducTile Reasoning, you use reflection to loop through the game executable and find all the subclasses of the TileComboClue class. These subclass types are stored in the ever-useful ArrayList, and then this ArrayList is plundered at random when a new clue is required. Listing 4-16 shows the GetClueClasses method.Listing 4.16: The GetClueClasses Method
'use reflection to find all the clue subclasses and load them up
Private Sub GetClueClasses()
FAsm = System.Reflection.Assembly.GetExecutingAssembly()
Dim t As Type
FClueClassNames = New ArrayList
Dim tParent As Type = GetType(TileComboClue)
For Each t In FAsm.GetTypes
If t.IsSubclassOf(tParent) Then
FClueClassNames.Add(t)
End If
Next
End Sub
The Assembly.GetExecutingAssembly method (a shared class method) is the one that returns the currently executing assembly (a.k.a. this program) as a variable that can be opened and the parts therein studied. In this case, you’re interested in looking for all of the classes in your program that inherit from the TileComboClue class. You do this using a class named Type, a class that represents a type declaration in the assembly.
Note | A type declaration can be a class, interface, array, value, or enumeration declaration in an assembly. |
As each TileComboClue subclass is discovered, a Type variable representing this subclass is stored in the ArrayList named FClueClassNames. Later, in the GenerateClues method, the code that actually selects one of the clues at random is as follows:
Dim c As TileComboClue
Dim oTyp As Type
Dim oRand As New Random
Dim oArgs() As Object = {oPL.Solution}
oTyp = FClueClassNames.Item(_
oRand.Next(0, FClueClassNames.Count))
c = Activator.CreateInstance(oTyp, oArgs)
What this code does is select an item out of the ArrayList, place that item in a Type variable, and then use the Activator.CreateInstance method to create an instance variable of this type. The trickiness is that if any parameters are required in the constructor of this variable, then these parameters must be sent to the Activator.CreateInstance method enclosed within an array. The array in the previous code is named oArgs. Because all the TileComboClue classes need a TileCombo (the puzzle solution) sent to them in their constructor, you can enclose the TileCombo (the oPL.Solution property) in the array. When the previous code runs, the variable c contains a random clue. The beauty of using reflection in this way is that you can add new subclasses of the TileComboClue classes to the program that represent different styles of clues, and you don’t have to change any of the clue generation code to “activate” these new clue styles.The remainder of the PuzzleGenerator class is pretty simple. A PopulateListBox method fills a listbox with the text of all the generated clues, and an IsSolution method determines if four ColoredShape tile classes match the generated puzzle.
You now have a complete PuzzleGenerator class, and all that’s needed to complete this game is the user interface.
Creating the DeducTile Reasoning Interface
The hard part of this seemingly endless game is done. Completing the game requires creating four tiles on the screen, displaying the clue text, allowing the user to move the tiles around, and determining if the player wins or loses.You create and display the tiles as you did in the Brain Drain Concentration game. You add four ColoredShape instances both to the game’s form and to a private ArrayList so you can get at them exclusively when needed. Listing 4-17 shows the procedure StartGame, in which the tiles are created and added to these collections.Listing 4.17: The Method StartGame on the DeducTile Reasoning Form
Private Sub StartGame()
Dim oShape As ColoredShape
Dim oRand As New Random
Dim i As Integer
Me.Cursor = Cursors.WaitCursor
oGT.StopTimer()
If Not FTiles Is Nothing Then
For Each oShape In FTiles
Me.Controls.Remove(oShape)
Next
End If
FTiles = New ArrayList
For i = 0 To 3
oShape = New ColoredShape(i, i)
With oShape
.Backwards = False
.Width = 64
.Location = New Point(16, 32 + (i * 64))
.AllowDrop = True
AddHandler .MouseDown, AddressOf ShapeMouseDown
AddHandler .MouseMove, AddressOf ShapeMouseMove
AddHandler .MouseUp, AddressOf ShapeMouseUp
AddHandler .DragOver, AddressOf ShapeDragOver
AddHandler .DragDrop, AddressOf ShapeDragDrop
End With
'adding a reference to two places, the form and an arraylist
FTiles.Add(oShape)
Me.Controls.Add(oShape)
Next
FPuzGen = New DeductileReasoning.PuzzleGenerator
FPuzGen.PopulateListBox(lbClues)
oGT.StartAt = New TimeSpan(0, 3, 0)
oGT.StartTimer()
Me.Cursor = Cursors.Default
End Sub
In addition to the four user interface tiles being created and attached to awhole slew of event handlers, you can also see the PuzzleGenerator variable (FPuzGen) being instantiated and the PopulateListBox method being called. The puzzle solution and all the clues are generated as part of this class’s constructor, so these two lines of code are all that are required by an outside user to create a batch of clues and to fill a listbox with the text of those clues. Finally, you can see another GameTimer class (oGT)created and set up to act as the foil of the player.
Dragging and Dropping
This game has a unique interface in that it allows dragging with both the left and right mouse buttons. Dragging one tile onto the other using the left mouse button switches the positions of the two tiles. Dragging one tile onto another with the right mouse button switches only the color of the two tiles, but the shapes remain in their current places. The user interface code has to handle both types of drag operations.Listing 4-17 showed five event handlers attached to the four ColoredShape tiles that are displayed on the form, and all five handlers relate to dragging and dropping. Table 4-2 describes these events.
HANDLER NAME | EVENT | PURPOSE |
---|---|---|
ShapeMouseDown | MouseDown | Defines a rectangle outside of which the dragging should begin |
ShapeMouseMove | MouseMove | Begins a drag operation if the mouse moves outside of this rectangle (with the button still down) |
ShapeMouseUp | MouseUp | Clears the rectangle |
ShapeDragOver | DragOver | Displays the drag icon |
ShapeDragDrop | DragDrop | Performs the tile switching |
All of the event handlers except for the last one are standard drag/drop code that you can find in the online help, so they won’t be defined here. Listing 4-18 shows the DragDrop code.Listing 4.18: Dropping One Tile onto Another
Private Sub ShapeDragDrop(ByVal sender As Object, ByVal e As DragEventArgs)
Dim oDest As ColoredShape = CType(sender, ColoredShape)
If oDest.Equals(FDragShape) Then Exit Sub
If Not FDidRight Then
'left drag, simply swap positions
Dim p As Point
p = FDragShape.Location
FDragShape.Location = oDest.Location
oDest.Location = p
Else
'right drag, swap colors
Dim c As Color
c = FDragShape.Color
FDragShape.Color = oDest.Color
oDest.Color = c
oDest.Invalidate()
FDragShape.Invalidate()
End If
End Sub
This routine first makes sure that a tile isn’t dragged and dropped onto itself. If this happens, then no action is required. If a tile is dragged onto another tile, however, then the code determines whether the right or mouse button was the one doing the dragging (via the Boolean variable FDidRight). If it was a left drag, then the Location variables of the two ColoredShape classes are swapped. If it was aright drag, then the colors are swapped and the two tiles are redrawn.
Ending the Game
The user now has the ability to read the clues and arrange the tiles until he thinks he might have the puzzle solved, at which point he clicks the Guess button at the bottom of the form to see if he’s right. Listing 4-19 shows the code that handles the Guess button click.Listing 4.19: Taking a Guess
Private Sub cbGuess_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles cbGuess.Click
FTiles.Sort() 'calls compareTo
If FPuzGen.IsSolution(FTiles.Item(0), FTiles.Item(1), _
FTiles.Item(2), FTiles.Item(3)) Then
oGT.StopTimer()
If MsgBox("You win, play again?", _
MsgBoxStyle.YesNo Or MsgBoxStyle.Question, _
"Try Again") = MsgBoxResult.Yes Then
StartGame()
Else
Me.Close()
End If
Else
oGT.AddTime(New TimeSpan(0, 0, -5))
End If
End Sub
The first step in the guess routine is to call the Sort method on the ArrayList that holds the tiles. The reason this is required is because the tiles originally went into the array in a certain order, but the user has most likely swapped the physical order of the tiles on the form by dragging and dropping. The IsSolution method on the PuzzleGenerator assumes that the topmost tile is going to be the first parameter passed in, the second tile passed in second, and so on. Before sorting, you really don’t know if the first tile in the FTiles ArrayList is really the topmost tile on the form anymore.The remarkable part of this code is how the Sort method shown in Listing 4-19 “knows” that it needs to sort the tiles according to their positions on the form. Why doesn’t it sort them in, say, alphabetical order by color or in descending alphabetical order by the last letter of the name of their shape? These sort criteria seem as arbitrary as the tiles’ top-to-bottom arrangement on the form.The answer to this puzzle is that you told the system how you planned to sort the tiles earlier. Do you remember when? If you’ll recall, the ColoredShape tile class implemented the IComparable interface, meaning that it contained a CompareTo method. The following code is the implementation of that method:
Public Function CompareTo(ByVal obj As Object) _
As Integer Implements System.IComparable.CompareTo
Dim o As ColoredShape = CType(obj, ColoredShape)
Return Me.Top.CompareTo(o.Top)
End Function
The result of this CompareTo method is the result of comparing the Top property (an integer) of the two tiles in question. If you were to consult the online help for how CompareTo works on integers, you would find that it returns –1 if the method caller (Me.Top previously) is less than the parameter (o.Top), 0 if they’re equal, and 1 if the method caller is greater. This gives you the result you’re after—sorting the tiles in their top-to-bottom order on the form.You might imagine cases where different sorting requirements exist for the same class depending on the circumstance. For example, you might create classes to implement a deck of cards for a card game. In some cases, a pile of cards may have to be sorted according to their position on a form. In other cases, they might have to be sorted by their face value or their suit. In still other cases, acrazy sort value might be required (such as in the game Euchre, where the highest card is one of the Jacks, and the second highest card is the other Jack of the same color).The .NET Framework can handle classes with multiple sorting requirements by passing an instance of a class that implements the IComparer interface (which is different from IComparable). This isn’t covered in detail here except in passing— it’s important at this time only to point out that implementing multiple sorting mechanisms within a class is possible.Once you’ve sorted the tiles in the Arraylist according to their physical positions on the form, you can pass them to the IsSolution method of the PuzzleGenerator variable. If the solution is indeed correct, the program displays a message and asks if the player wants to play again. If the solution is incorrect, the player is “punished” by having five seconds removed from the available time.Believe it or not, this finally completes the discussion of the DeducTile Reasoning game! I personally enjoy playing this game as much as any other in the book; it presents a nice little brainteaser for the player.