Introduction
The program implements the standard two-deck version of Spider. This version is called Redback. The classes that represent cards, stacks of cards, and move history could be reused for other card based solitaire games since the specific game logic is on the tableau form.
Background
RedBack is modelled on the Windows 3.1 shareware game Arachnid that my wife loved to play. This game didn't work under Windows 95 - it would load but the cards wouldn't show - so I wrote my version as a learning exercise in Visual Basic object based programming.
In 2004, I converted the game from VB6 to VB.NET, using the 1.1 .NET framework. I added more spider graphics plus an option to hide the pictures for the arachnophobic.
In keeping with the original name, I called my version RedBack after an infamous Australian spider. It is a close relative of the Black Widow.
I recently redid the graphics and tidied up the code for this article.
Redback has some irregular features that I added at my wife's request:
- You can undo moves and deals right back to the beginning of the game.
- You can make moves simply by clicking the card you want to move.
Creating the graphics
The first version used the card graphics that came with early Windows 3.1 solitaire games. By today's standards, they are too small, and scaling them up reduced the image quality. I created the card faces by running an old deck of cards through a sheet-feed scanner. I then processed the images using Paint.Net. I added a two pixel wide black border with rounded corners. Initially, the corner areas outside the border were white and they looked bad on the screen. I didn't feel like going back and manually resetting those few pixels to transparent. That would have required 52 x 4 separate edits working at the pixel level. Instead, I added code to make the offending pixels transparent.
Public Sub CleanUpCorners(ByRef bmp As System.Drawing.Bitmap, color As Drawing.Color)
Dim limits() As Integer = {7, 5, 4, 3, 2, 1, 1}
For y As Integer = 0 To 6
For x As Integer = 0 To limits(y) - 1
bmp.SetPixel(x, y, color)
bmp.SetPixel(bmp.Width - 1 - x, y, color)
bmp.SetPixel(x, bmp.Height - 1 - y, color)
bmp.SetPixel(bmp.Width - 1 - x, bmp.Height - 1 - y, color)
Next
Next
End Sub
bmp = New Bitmap(clsResources.GetImage("c" + .GetImageIndex(.Suit, .Rank).ToString()))
CleanUpCorners(bmp, System.Drawing.Color.Transparent)
It is not perfect, because the pixels are changed before the image gets scaled.
I wanted a spider themed background so I obtained some Australian spider photographs from an Australian expert on spiders, with his permission, of course. These are used to create a tiled background image. There is a menu option labelled "I hate spiders" to turn off this beautiful background feature. I didn't want to implement this menu option, but my wife was adamant.
Building Blocks
Tableau
The tableau is a standard VB.NET form. It has a panel control for the splash screen, eight panel controls for completed suits, and ten panels for the cards that are dealt. It also has 104 picture boxes to hold card images. They could more easily be created at run-time but this was a legacy from the original VB6 version.
Playing Card object
This is a card game, so it needs a class that represents a single card. The class Card
represents an instance of a card. It declares public Enums to define the possible decks, suits and ranks. It also has these properties
Rank As RankSet
Size as CardSize
FaceUp As Boolean
Playable As Boolean
Clickable As Boolean
Dragable As Boolean
Left As Single
Top As Single
Stack As Stack
StackPosition As Short
Image As System.Windows.Forms.PictureBox
The class clsCard
also needs to respond to events on the Image, a standard PictureBox
. The requisite events are Mouse_Down, Mouse_Up and Mouse_Move.
In Spider, you can move stacks of cards of the same suit as a group. Initially, moving multiple cards with a drag and drop operation resulted in horrible flickering. The solution was very simple. Just set the form property DoubleBuffered
to True
.
Stack object
Solitaire card games involve moving cards from one stack of cards to another stack of cards. The class clsStack
represents a stack of cards. It inherits from CollectionBase
, and I haven't updated it to use a generic list object.
Internally, it now uses a Dictionary
object to store cards. It exposes the following properties:
Left
Top
SmallHorizontalSpace
LargeHorizontalSpace
SmallVerticalSpace
LargeVerticalSpace
VerticalSpacing
HorizontalSpacing
StackKey
TOS
Open
The class implements methods for each EnumMoveType
and for going back one move and back one deal. It would not need changing to cater to a different game.
Stacks object
This is a collection object that holds sets of stack objects. It provides a way to iterate through a collection of stacks.
Dealer object
This object deals cards in pseudo random order. It implements a DealCard
method and a CardsLeft
property. Its most important method is DealCard
. It looks like this:
Public Function DealCard(ByVal faceUp As Boolean) As Card
Dim deck As Card.DeckSet
Dim suit As Card.SuitSet
Dim rank As Card.RankSet
Dim card As Card = Nothing
If Me.CardsLeft = 0 Then
Return Nothing
Exit Function
End If
Select Case _DealMode
Case EnumDealMode.RANDOM_DEAL
Do
deck = GetRandom(Card.DeckSet.DECK_ONE, _Decks - 1)
rank = GetRandom(Card.RankSet.LOWEST_RANK + 1, Card.RankSet.HIGHEST_RANK - 1)
suit = GetRandom(Card.SuitSet.LOWEST_SUIT + 1, Card.SuitSet.HIGHEST_SUIT - 1)
Loop Until _CardRecords(deck, rank, suit).Dealt = False
_Seq = _Seq + 1
_CardRecords(deck, rank, suit).Dealt = True
_CardRecords(deck, rank, suit).Seq = _Seq
card = _Cards.Item(CStr(deck) & "." & CStr(rank) & "." & CStr(suit))
card.FaceUp = faceUp
End Select
DealCard = card
End Function
The function selects a random card based on deck, suit and rank. If that card has already been dealt, it tries again. It is game independent.
Move Logging object
Since the RedBack version of Spider implements unlimited undos and deal undos, it needs to track every move and provide methods to undo them. This class handles that complexity.
The class also exposes these two Enums
:
Public Enum MoveTypeSet
START_DEAL
END_DEAL
START_MOVE
END_MOVE
MOVE_CARD_FROM_PILE_TO_LIST
MOVE_CARD_FROM_LIST_TO_PILE
TURN_CARD_FACE_UP
End Enum
Public Enum ReplayTypeSet
UNDO_ONE_MOVE
DEAL_1
DEAL_2
DEAL_3
DEAL_4
DEAL_5
End Enum
Each move is logged so it can be reversed by an undo. Here is the logic that logs a valid card click. It logs the removal of a card from a stack, its placement on a new stack, and whether the newly exposed card needs to be turned face up.
_Log.StartMove()
Stop_Redraw()
For cardIndex = oldPile.Count To startIndex Step -1
moved = moved + 1
cardsToMove(moved) = oldPile.RemovedCard
_Log.MoveCardFromPileToList((oldPile.StackKey))
Next cardIndex
For cardIndex = moved To 1 Step -1
pile.AddCard(cardsToMove(cardIndex))
_Log.MoveCardFromListToPile((pile.StackKey))
Next cardIndex
If Not oldPile.TopCard Is Nothing Then
If oldPile.TopCard.FaceUp = False Then
oldPile.TurnUpTopCard()
_Log.TurnCardFaceUp((oldPile.StackKey))
End If
End If
pile.Refresh()
oldPile.Refresh()
_Log.EndMove()
Start_Redraw()
Game Logic
The actual game logic is implemented on the form. This may not be the best place to put it; a configurable game engine would be better. However, that was where it was in the original VB6 version so that is where it lives now. The first step is creating the initial tableau or starting position. RedBack calls a method called StartGame
to get things underway. It shows a splash screen, which is just a Panel control. It needs to be dismissed before anything can happen. StartGame
then instantiates and populates a stack for the STOCK, ten stacks for the playable columns and eight stacks to receive completed suits. To reduce screen flicker, it encloses this operation between calls to Stop_Redraw
and Start_Redraw
.
Public Declare Function LockWindowUpdate Lib "user32" (ByVal hwndLock As Integer) As Integer
Public Sub Stop_Redraw()
LockWindowUpdate(Me.Handle)
End Sub
Public Sub Start_Redraw()
LockWindowUpdate(0)
End Sub
Once the game is initialized, it waits for the player to click or drag cards. These events are handled in the Card
class. The MouseMove
event has to deal with the fact that there may be cards of the same suit on top of the card being dragged.
It invokes a method in the Card
class called MoveCardsOnTop
. This methods looks for the cards on top and moves them along with the selected card. It uses SetBounds
to move elements, rather than setting left
and top
in two separate statements.
Here is the method.
Private Sub MoveCardsOnTop()
Dim pile As Stack
Dim cardIndex As Short
Dim verticalSpace As Single
pile = Me.Stack
verticalSpace = pile.VerticalSpacing
For cardIndex = Me.StackPosition + 1 To pile.Count
With pile.Card(cardIndex).Image
.SetBounds(Me.Image.Left + pile.HorizontalSpacing, Me.Image.Top + verticalSpace, 0, 0, Windows.Forms.BoundsSpecified.x Or Windows.Forms.BoundsSpecified.y)
End With
verticalSpace += pile.VerticalSpacing
Next cardIndex
End Sub
The Card
MouseUp event has to decide whether it is responding to a drag and drop event, or a click event. It does it by timimg the duration betweem the original MouseDown event and the MouseUp event. If the duration is less than 0.2 seconds, it assumes it is responding to a click event. In either case, it invokes methods on the form to process the event. Here is the event code.
Private Sub _imgCard_MouseUp(ByVal eventSender As System.Object, ByVal eventArgs As System.Windows.Forms.MouseEventArgs) Handles _CardImage.MouseUp
Dim newTime As Double
If Not _Playable Then
Exit Sub
End If
newTime = Microsoft.VisualBasic.DateAndTime.Timer
If (newTime - _MouseDownTime) < 0.2 Then
_Moving = False
End If
If _Moving Then
_Moving = False
frmTableau.ProcessCardDrop(Me)
Else
frmTableau.ProcessCardClick(Me)
End If
_MouseDown = False
End Sub
The frmTableau.ProcessCardClick
method processes a card click. It goes through the various actions it can take to respond to a card click. If it is the Stock pile, then it deals another row onto the column stacks. If a complete suit has been selected, then it moves the suit to a Home pile. Otherwise, it looks for a matching suit and rank. If it doesn't find that, it looks for a matching suit. If it doesn't find that, it looks for an empty column. If the search it unsuccessful, it returns. Otherwise, it makes the move and finishes up. The move logic is done using methods of the Stack
object. Here is the logic:
moved = 0
For cardIndex = oldPile.Count To startIndex Step -1
moved = moved + 1
cardsToMove(moved) = oldPile.RemovedCard
_Log.MoveCardFromPileToList((oldPile.StackKey))
Next cardIndex
For cardIndex = moved To 1 Step -1
pile.AddCard(cardsToMove(cardIndex))
_Log.MoveCardFromListToPile((pile.StackKey))
Next cardIndex
If Not oldPile.TopCard Is Nothing Then
If oldPile.TopCard.FaceUp = False Then
oldPile.TurnUpTopCard()
_Log.TurnCardFaceUp((oldPile.StackKey))
End If
End If
Resizing
Some versions of Spider have a problem when too many cards are added to a column stack and they disappear off the bottom of the board and become unreachable. To overcome that, Redback lets the player choose between large, medium and small card sizes.
Animation
The beta tester, a.k.a my wife, wanted to see the cards being dealt when the stock was clicked. I added a method for moving a card wth animation. Getting smooth animation without flickering proved to be a bit of a challenge. In the case of this code, I suspect the underlying problem is that there are so many controls on the form. Every screen paint probably iterates though all these controls to see if they need to be repainted. The solution was to disable the form during the animation.
I also added animation to the card click, so you can see the card move to its selected destination. This is the animation method.
Private Sub AnimateMove(oldPile As Stack, startIndex As Integer, pile As Stack, cumVertOffSet As Integer, throttle As Integer)
Dim left As Integer
Dim top As Integer
Dim steps As Integer = Math.Max(Math.Abs(oldPile.LeftPos - pile.LeftPos), Math.Abs(oldPile.Top - pile.Top)) / throttle
Select Case _Speed
Case SpeedSet.FAST
steps = Math.Min(_FastTHrottle, steps)
Case SpeedSet.MEDIUM
steps = Math.Min(_MediumThrottle, steps)
Case SpeedSet.SLOW
steps = Math.Min(_SlowThrottle, steps)
End Select
Dim leftTarget As Integer = If(pile.Count = 0, pile.LeftPos, pile.TopCard.Image.Left)
Dim topTarget As Integer = If(pile.Count = 0, pile.Top, pile.TopCard.Image.Top) + cumVertOffSet
Dim card As Card = oldPile.ThisCard(startIndex)
Dim leftSource As Integer = card.Image.Left
Dim topSource As Integer = card.Image.Top
Dim leftInc As Integer = (leftTarget - leftSource) / steps
Dim topInc As Integer = (topTarget - topSource) / steps
With card.Image
left = .Left
top = .Top
Me.Enabled = False
For i As Integer = 1 To steps
If i = steps Then
left = leftTarget
top = topTarget
Else
left += leftInc
top += topInc
End If
Thread.Sleep(1)
.SetBounds(left, top, .Width, .Height, Windows.Forms.BoundsSpecified.X Or Windows.Forms.BoundsSpecified.Y)
.BringToFront()
.Refresh()
Application.DoEvents()
Next
card.Top = topTarget
card.LeftPos = leftTarget
Me.Enabled = True
End With
End Sub
Help
I created a help file for the VB6 version using Robohelp. When I tried to update it, I found I didn't have RoboHelp installed anymore. So, I created a simple HTML help file. It looks better than the old .CHM file.
Using the code
This code uses intermediate VB.NET coding techniques. As such, it should be relatively easy for an intermediate developer to adapt the code for other or additional solitaire games. The Card
, Stack
, Dealer
, and LogMove
classes are not specific to Spider solitaire and could be used without change in another game.
Some mail servers reject zip files containing files with with the extension .vb. After you extract the files from the zip file, change the extension of .vbsrc files to .vb.
Points of Interest
The ability to click a card and have it move to the most obvious location speeds up the game. Unlimited undos remove the frustration of playing one of the more challenging Solitaire games. The spider backgrounds are a novelty that most players will turn off.
History
First Version for Code-Project