Developing Brain Drain Concentration
You’ve spent a ton of time creating a game piece—it’s finally time to create a game. The first game of the trio is the standard “memory game.” It’s named Brain Drain Concentration because having to memorize the location of colors and shapes gets pretty confusing.Fortunately, you’ve already done most of the work in creating the memory game because the Tile class (and its descendants) handles most of the work (following the rule that an object should be responsible for itself). The Tile class draws itself and can be flipped over, which is what’s required for a Concentration-style game like this. There are really only a few pieces of logic remaining to get this game working:Creating and arranging multiple tiles to make up the board
Removing tile matches from the board
Handling when the player wins or loses
The next sections handle each of these tasks individually.
Creating the Game Board
The game board consists of 32 tiles—four possible colors and four possible shapes make 16 total tiles. A pair of each type of tile is required so they can be matched, and this gives you 32. You create the 32 tiles using nested loops, as shown in Listing 4-4.Listing 4.4: Creating 32 Tiles
For i = 0 To 3 '4 shapes...
For j = 0 To 3 '4 colors...
For k = 0 To 1 '2 copies...
oShape = ColoredShape.CreateByIndex(i, j)
oShape.Backwards = True
oShape.Width = 48
AddHandler oShape.Click, AddressOf ShapeClick
'set the location on the form
oShape.Left = (iCtr Mod 8) * oShape.Width
oShape.Top = (iCtr \ 8) * oShape.Height
iCtr += 1
'adding a reference to two places, the form and an arraylist
FTiles.Add(oShape)
Me.Controls.Add(oShape)
Next
Next
Next
The key to creating the tiles in this way is the CreateByIndex method in the ColoredShape class. If you’ll recall, this is a shared method on the base class that returns a tile based on integers to represent both the color and shape that the tile should be.Once this code creates a tile, it has to do a few other things. First, the code sets the tile’s Backwards property to True, meaning that the tile is flipped over so the user can’t see the shape. Next, the code sets the size of each tile to 48 (remember that the ColoredShape control forces the tiles to be square), and it sets the location of each tile so that they appear in four rows of eight tiles each. After that, the code attaches a method named ShapeClick to each tile’s Click event, meaning that this code will run whenever a user clicks a tile (you’ll examine that code later in the section “Removing Tile Matches from the Board”). Finally, the code adds each tile to two different collections. The first collection is an ArrayList named FTiles that’s declared on the game’s form, and the second is the form’s Controls collection (you must add all controls explicitly to their parent’s Controls collection or they won’t render).You might be asking why each control is added to two collections. The reason is that future routines are going to have to loop through all the tiles and perform some action on them, and it’s easier to iterate through a collection containing only ColoredShape class instances than iterating through the form’s Controls collection, which may contain other controls besides the ColoredShape classes.You’ve now created the 32 tiles, but their arrangement is a problem. Because you created them in a structured loop, all the tiles live in a prearranged pattern on the board. It wouldn’t take a player very long to figure out this pattern and use this knowledge to solve the puzzle. Clearly, you need to mix up the arrangement of the tiles on the board. Listing 4-5 handles this.
Listing 4.5: Mixing Up the Tiles
Dim oRand As New Random
Dim i, j, k As Integer
Dim p As Point
'swap positions randomly
For k = 0 To 999
i = oRand.Next(0, FTiles.Count)
j = oRand.Next(0, FTiles.Count)
If i <> j Then
p = CType(FTiles.Item(i), ColoredShape).Location
CType(FTiles.Item(i), ColoredShape).Location = _
CType(FTiles.Item(j), ColoredShape).Location
CType(FTiles.Item(j), ColoredShape).Location = p
End If
Next
The randomizer routine works by looping 1,000 times. In each loop, the program chooses two tiles at random and swaps their positions. This loops gives one example of why the program required a second separate collection containing only ColoredTile instances instead of simply using the form’s Controls collection. Having a separate loop allows you to easily choose two tiles randomly in the collection without having to see if the control you chose is actually a Tile instance. In addition, you can typecast the element of the collection back to a ColoredShape (with the CType function) without fear that you’ve accidentally tried to typecast a MenuItem or a Button into the wrong class, which generates an exception (a runtime error).
Removing Tile Matches from the Board
As mentioned earlier, each tile runs an event handler named ShapeClick whenever it’s clicked. Listing 4-6 shows that event handler in its entirety, which runs when a user clicks a tile.Listing 4.6: The ShapeClick Method
Private Sub ShapeClick(ByVal sender As Object, _
ByVal e As System.EventArgs)
Dim s As ColoredShape = sender
s.Backwards = False
oWav.Play("die1", 100)
Application.DoEvents()
If FFirstOneFlipped Is Nothing Then
FFirstOneFlipped = s
Else
If s.Equals(FFirstOneFlipped) Then
System.Threading.Thread.Sleep(100)
Me.Controls.Remove(s)
Me.Controls.Remove(FFirstOneFlipped)
FTiles.Remove(s)
FTiles.Remove(FFirstOneFlipped)
oWav.Play("die1", 100)
If FTiles.Count = 0 Then
oGT.StopTimer()
oWav.Play("ovation", 100)
MsgBox("you win")
End If
Else
System.Threading.Thread.Sleep(500)
s.Backwards = True
FFirstOneFlipped.Backwards = True
oWav.Play("ouch", 100)
End If
FFirstOneFlipped = Nothing
End If
End Sub
The first lines of the ShapeClick routine flip the tile over by simply setting the Backwards property back to False. A sound effect also plays—the same WavLibrary class used in Chapter 3, “Understanding Object-Oriented Programming from the Start.” (Code reuse at its finest!)The next thing to determine is if the tile that was just flipped over is the first tile of a pair or the second. If it’s the first tile, then the ShapeClick can end because there’s nothing left to do. Only after the second tile is flipped over should you conduct any checks to determine if the user found a match.You answer the first tile/second tile question by using a form-level variable named FFirstOneFlipped. This variable either holds a ColoredShape instance or is set to Nothing. If the variable is empty, you know that the tile just flipped over is the first one of the pair, and all you need to do is to set the FFirstOneFlipped variable to point to the tile just flipped. If the variable already has a value, then you know that the tile just flipped over is the second of a pair, and you can check to see if the user found a match.The code after the Else in Listing 4-6 is the code that runs when a user flips over the second tile of a pair. The program first compares the two tiles to see if they’re the same color and shape using the Equals method you put on the ColoredShape class. If they’re equal, the program then removes the tiles from both collections. If the tiles aren’t equal, then both tiles flip back over after a short half-second pause. In either case, whether the tiles are a match or not, the FFirstOneFlipped variable is set back to Nothing, which tells the program after the next tile flip that a new tile pair has begun.
Handling When the Player Wins or Loses
Hidden in Listing 4-6 is the check for whether the player has won the game. You conduct this check by seeing if there are zero tiles left in the FTiles ArrayList (another example of why the game required a separate collection to hold the tile instances). If there are indeed no tiles left, then the player has won the game and is told so.Losing the game is a bit more complicated because as of yet there’s no way to lose the game (the player can simply flip tiles over forever until he clears the board). Because games aren’t much of a challenge if there’s no way to lose them, you have to introduce some new element so that the player can fail as well as succeed. Let’s add a countdown timer to the game so the player loses if the timer reaches zero. The countdown timer is a good candidate for a new class that can count down from a specified time and then fire an event when the clock reaches 0. Listing 4-7 shows the public interface for this new class.Listing 4.7: The GameTimer Class Public Interface
Public Class GameTimer
Inherits Control
Public Event SecondsChanged(ByVal sender As Object, ByVal t As TimeSpan)
Public Event TimesUp(ByVal sender As Object)
Property StartAt() As TimeSpan
Public Sub StartTimer()
Public Sub StopTimer()
Protected Overrides Sub OnPaint(ByVal e As _
System.Windows.Forms.PaintEventArgs)
Public Sub AddTime(ByVal t As TimeSpan)
Shadows ReadOnly Property Enabled() As Boolean
End Class
The GameTimer class doesn’t do anything new that needs to be covered on a line-by-line basis. Instead, Table 4-1 summarizes what each member does.
MEMBER NAME | MEMBER TYPE | DESCRIPTION |
|---|---|---|
StartAt | Property | Defines at which time the clock should begin (“Put three minutes on the clock, Bob”). This property is of type TimeSpan—a .NET Framework type that represents a time interval. This type is different from a DateTime type, which represents a single point in time. See the .NET Framework help documentation for more information on TimeSpan types. |
StartTimer | Method | Starts the clock. |
StopTime | Method | Stops the clock. |
AddTime | Method | Allows time to be added (or taken off) the clock during the game. You could implement a 10-second penalty using this method. |
OnPaint | Method | Draws the remaining time onto the control. |
SecondsChanged | Event | Fires whenever the seconds tick down by one. Used in this game to change the color of the time remaining to red when less than one minute remains. |
TimesUp | Event | Fires when no time remains on the clock. |
Listing 4-8 shows a GameTimer being created by the Brain Drain Concentration form during its Load event.
Listing 4.8: Creating a GameTimer Class and Its Event Handlers
Dim oGT As GameTimer
Private Sub fConcentration_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
oGT = New GameTimer
With oGT
.Dock = DockStyle.Bottom
.Height = 32
.Font = New Font("Tahoma", 16, _
FontStyle.Italic Or FontStyle.Bold)
.ForeColor = Color.LightGray
AddHandler .TimesUp, AddressOf TimerDone
AddHandler .SecondsChanged, AddressOf TimerSeconds
End With
Me.Controls.Add(oGT)
StartGame()
End Sub
Private Sub TimerDone(ByVal sender As Object)
MsgBox("you lose")
End Sub
Private Sub TimerSeconds(ByVal sender As Object, ByVal t As TimeSpan)
If t.TotalSeconds < 60 Then
CType(sender, GameTimer).ForeColor = Color.Red
Else
CType(sender, GameTimer).ForeColor = Color.LightGray
End If
End Sub
Now that you’ve implemented a countdown timer opponent, writing the code for the player losing the game is trivial—you simply notify him in the TimesUp event of the GameTimer class.
Wrapping Up Brain Drain Concentration
Brain Drain Concentration is the easiest of the three games in this chapter to implement because the game logic is pretty simple. All you had to do was create two of each tile types, mix them up, and handle the user clicking and removing matches.The next game, DeducTile Reasoning, is actually the most difficult of the three to implement because the game logic gets pretty difficult. It’s a great example of a seemingly simple problem spiraling into a complex logical problem.