Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

A VB.NET Version of the Spider Solitaire Game

4.93/5 (9 votes)
14 Jan 2016CPOL8 min read 24.7K   1.7K  
In this article I will describe the approach I took in creating a VB.NET implementation of the Spider Solitaire Game.

Image 1

Image 2

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.

VB
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
'
' How to invoke
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

VB
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:

VB
Left
Top
SmallHorizontalSpace
LargeHorizontalSpace
SmallVerticalSpace
LargeVerticalSpace
VerticalSpacing
HorizontalSpacing
StackKey                ' Enum denoting which stack is represented. Values are STOCK, PILE1 thru PILE10, and HOME1 thru HOME10
TOS                     ' Top of Stack index
Open                    ' Stack can be played to

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:

VB
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:

VB
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.

VB
_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.

VB
Public Declare Function LockWindowUpdate Lib "user32" (ByVal hwndLock As Integer) As Integer
'
' API calls to stop Windows refreshing during complex operations. Not so necessary when form.DoubleBuffered is set to true.
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.

VB
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.

VB
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:

VB
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.

VB
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
    '
    ' Work out how many steps there are from source pile to target pile. Throttle increases the number of steps.
    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
    '
    ' Locate the left and top co-ordinates in the target pile
    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
    '
    ' Get the card to move and its co-ordibates
    Dim card As Card = oldPile.ThisCard(startIndex)
    Dim leftSource As Integer = card.Image.Left
    Dim topSource As Integer = card.Image.Top
    '
    ' Calculate how far to move on each iteraction
    Dim leftInc As Integer = (leftTarget - leftSource) / steps
    Dim topInc As Integer = (topTarget - topSource) / steps
    With card.Image
        left = .Left
        top = .Top
        '
        ' Critical step. If this isn't done, the animation flickers badly
        Me.Enabled = False
        '
        ' Iterate through the steps
        For i As Integer = 1 To steps
            '
            ' On last step, force card to target location
            If i = steps Then
                left = leftTarget
                top = topTarget
            Else
                left += leftInc
                top += topInc
            End If
            '
            ' Slow things down
            Thread.Sleep(1)
            '
            ' Move the card
            .SetBounds(left, top, .Width, .Height, Windows.Forms.BoundsSpecified.X Or Windows.Forms.BoundsSpecified.Y)
            '
            ' Ensure it is visible
            .BringToFront()
            .Refresh()
            '
            ' Let Windows do its thing
            Application.DoEvents()
        Next
        '
        ' Reset the card position
        card.Top = topTarget
        card.LeftPos = leftTarget
        '
        ' Re-enable the tableau.
        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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)