65.9K
CodeProject is changing. Read more.
Home

A VB.NET Version of the Spider Solitaire Game

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (9 votes)

Jan 14, 2016

CPOL

8 min read

viewsIcon

24723

downloadIcon

1688

In this article I will describe the approach I took in creating a VB.NET implementation of the Spider Solitaire Game.

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
    '
    ' 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

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

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

    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
        '
        ' 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