Export (0) Print
Expand All

Pocket Jack: Writing a Card-Playing Application

.NET Compact Framework 1.0
 

Rob Miles
Department of Computer Science, University of Hull

June 2005

Applies to:
   Windows Mobile-based Smartphones
   Microsoft .NET Compact Framework
   Microsoft Visual Studio .NET 2003

Summary: This article describes how to create and use a card game for Smartphones. The description is illustrated by the development of a blackjack game for Smartphone.


Download Pocket Jack for Smartphone.msi from the Microsoft Download Center.

Contents

Introduction
Drawing the Cards on the Screen
Card Class
Game of Blackjack
Hand Class
CardShoe Class
Playing the Game
Displaying the Score Values
Paint Method
Complete Blackjack Game
Context-Sensitive User Interface
Betting Pot
Making a Finished Game
Conclusion

Introduction

Many games are based on a pack of cards. It therefore makes sense to implement a set of classes that provide a set of card management methods. You can then use those methods to implement a wide range of games. This article describes a mechanism for dealing cards randomly, displaying them on the screen, holding them in "hands" for each player, and managing card-based game play. It provides the implementation in the context of a fully functional blackjack game, called Pocket Jack, for the Smartphone.

To get started, you will need a copy of Visual Studio .NET 2003. You will also need the Smartphone 2003 SDK.

Drawing the Cards on the Screen

The first problem to solve is the display of the cards that a player is holding. The cards that are used in the game must be displayed on the Smartphone screen. To do this, you need 53 images: one for each card, plus an image that shows the back of a card. A set of appropriate images has been created in individual files, as shown in Figure 1.

Click here for larger image

Figure 1.The card images. Click the thumbnail for larger image.

Each of these files is a Graphics Interchange Format (GIF) image of the card in question. Files numbered 1 to 52 contain each of the cards in the four suits in sequence. File number 0 is the back of the card. These images can be added to the application project and used to draw the cards. Each card image is around 1 KB in size because it contains only four colors, so the images will not use much memory.

Creating the Project and Adding Card Images

To get started, you first need to create a new Visual Studio .NET 2003 smart device application that targets Smartphones.

To create a new smart device application

  1. In Visual Studio .NET 2003, click File, point to New, and then click Project.This will display the New Project dialog box.
  2. On the New Project dialog box, select the project type Visual C# Projects.
  3. Choose the Smart Device Application template.
  4. Name the project PocketJack as shown in Figure 2.

Click here for larger image

Figure 2. The New Project dialog box. Click the thumbnail for larger image.
  1. Click OK.

    To complete the creation of the Smart Device Application project, you need to decide what kind of smart device you want to target.

  2. On the Smart Device Application Wizard, select the Smartphone platform and Windows Application project type as shown in Figure 3.

    Figure 3. The Smart Device Application Wizard

  3. Click OK to create the project.
Note   To create Smartphone 2003 Smart Device Application projects with Visual Studio .NET 2003, you must first install the Smartphone 2003 SDK. If you do not see Smartphone listed as a platform on the Smart Device Application Wizard, close Visual Studio .NET 2003, and then download and install the Smartphone 2003 SDK before proceeding.

With the project created, you are now ready to include the card images. The best place to store the card images is in the application itself — this makes deployment much easier. It is best if you create a resource directory inside the project rather than store the images in the main project folder because this stops the large number of individual files from cluttering the project. To create the resource folder right-click the project, point to Add, and then click New Folder as the item you want to add, as shown in Figure 4.

Figure 4. Adding a new folder to a project

Name the folder cardImages. If you then look in the directory for the project, you will find that Visual Studio has made a folder called cardImages and linked it into the project, as shown in Figure 5.

Figure 5. CardImages folder

Now that you have your folder in the project, you need to put the images in it. The best way to do this is to select all of the images in the source folder, and then drag them into the folder in the Visual Studio Solution view. The files appear in the folder inside the solution, and they are also copied into the directory in the project. However, at the moment, they are not embedded into the project when it is deployed. To make this happen, you have to mark the image files as an embedded resource by selecting all of the images in the directory (click the top one, and then press SHIFT while clicking the bottom one to select them all). Then, modify the Build Action property to mark the files as embedded resources, as shown in Figure 6.

Figure 6. Embedded image resources

The application now has 52 card images and a background image embedded in it. If you build the project, you will find that the executable file is around 83 KB in size. Most of this size is the image content.

Loading Card Images

The best way to manage the card images is in terms of an array of card images. The images can then be drawn on the playfield as required. The code to load the card images is as follows.

Image [] cardImages = new Bitmap [53];
System.Reflection.Assembly execAssem = 
    System.Reflection.Assembly.GetExecutingAssembly();
for ( int i=0 ; i < 53 ; i++ ) 
{
    cardImages [i] = new Bitmap (execAssem.GetManifestResourceStream ( 
        @"PocketJack.cardImages."+i+@".gif"));
}

This code creates an array to hold references to the bitmaps and then loads each of the bitmaps from their respective resources within the assembly. Note that the code creates the resource name by using the number of the image to be loaded. When creating embedded resources, Visual Studio .NET 2003 automatically adds the project namespace to the beginning of all resource names. In addition to including the project's default namespace, the resource names also include the subfolder name because the files used to create the resources are contained in a subfolder of a project. As a result, the code to load the resources must include the project's default namespace and subfolder name as part of the resource name, so the resource is located correctly.

Drawing Card Images

The next step to consider is the process of drawing the cards. The card images themselves have rounded corners. When they are drawn on the game background, these corners must be drawn transparently, so that the cards look realistic. This is a small point, but significant if you want users to really like your games. If you look closely at the card images that have been supplied, you will see that the corners have been drawn in dark green, as shown in Figure 7.

Figure 7. Card corner of the 8 of spades in detail

When the cards are drawn, you must nominate this color as transparent, so that the background shows around the edge of each card. You need to use the following code to create an ImageAttributes instance, which tells the draw process which color is transparent.

System.Drawing.Imaging.ImageAttributes transparentAttributes ;
transparentAttributes = new System.Drawing.Imaging.ImageAttributes();
transparentAttributes.SetColorKey(Color.Green, Color.Green);

The SetColorKey method gives the start and end range of the colors to be regarded as transparent. For the .NET Compact Framework, these two colors must be the same value; that is, only one color can be made transparent when drawn on the Smartphone. When the card image is being drawn, the DrawImage method is used as follows.

Rectangle drawRect;
Random rand = new Random();
foreach ( Image card in cardImages ) 
{
    drawRect.X = rand.Next(this.Width);
    drawRect.Y = rand.Next(this.Height);
    drawRect.Width = card.Width;
    drawRect.Height = card.Height;
    graphics.DrawImage(
        card,                    // Image
        drawRect,                // destination rectange
        0,                       // srcX
        0,                       // srcY
        card.Width,              // srcWidth
        card.Height,             // srcHeight
        GraphicsUnit.Pixel,      // srcUnit
        transparentAttributes);  // ImageAttributes
}

This example code draws all of the card images on the screen at random positions. It uses the DrawImage method member of the Graphics instance that the variable graphics refer to.

The Sample Card Drawing Program draws all of the cards on the screen each time the Paint method is called, as shown in Figure 8. You can open this program by downloading the Pocket Jack for Smartphone.msi.

Click here for larger image

Figure 8. Random card display. Click the thumbnail for larger image.

Card Class

An instance of the Card class represents each of the cards in the game. This class holds the actual value of the card and draws it on the screen. It also provides properties that enable users of the class to find the point value of the card and get the name of the card and other useful information. The Card class can be used in many different games, but there are some features that are specific to blackjack.

Speeding Up the Loading Process

The first version of the Card class performed all of the image loading when the application began running. Each of the 52 card pictures and the back of the card pattern were loaded at the start. This made the application slightly slower to start up. A way of speeding up the loading process is to only load the images on demand by using the following code.

if ( cardImages [dispNo]==null ) 
{
    cardImages [dispNo] = new Bitmap (execAssem.GetManifestResourceStream ( 
        @"PocketJack.images."+dispNo+@".gif"));
}

The variable cardImages is an array of bitmaps. Initially, all of the card images are empty. In the preceding code, the variable dispNo holds the number of the bitmap that is required. If the given element is null, the image is loaded, and then it can be drawn. The next time the image is required, it will be found immediately. As a result, the application starts running a lot more quickly than if all of the cards were loaded at the start; the time taken to load just the few cards needed for the first hands that are displayed is not great. If your applications need a large number of images like this, it is worth spreading the load time over the application rather than performing it all at once at the start.

Game of Blackjack

The game of blackjack (also known as twenty-one or pontoon) has been popular for a very long time. It is the most widely played banking card game in the world. By drawing cards and adding them to his or her hand, a player attempts to achieve a point score as close to 21 as possible without going above that value, which is known as going "bust." If the player holds at a score of 21 or less, he or she must then have more than the score of the banker (who plays last) in order to win. This article describes each specific rule of the game later because these rules affect the design of the application.

Hand Class

You are going to need a container class to hold the cards. The finished game will require two instances of this container: one for the computer-controlled dealer and the other for the player. The Hand class you are going to use holds a number of cards. It is based on the ArrayList collection, which makes it easy for users of the Hand class to add and enumerate the cards in a hand. It also contains a method that will draw the cards in a hand, as shown in the following code.

public void DrawHand ( 
Graphics g, int startx, int starty, int gapx, int gapy )

The method draws the cards starting from a particular position on the screen. It places each successive card the given interval away from the next.

The Hand class also contains a method which will calculate the blackjack score of the cards it contains. It can be used in the following way.

public int BlackJackScoreHand () 
{
    int score=0;
    int aces = 0;
    foreach ( Card card in this ) 
    {
        score+=card.BlackJackScore;
        if ( card.BlackJackScore == 11 ) 
        {
            aces++;
        }
    }
    while ( ( score > 21 ) && ( aces > 0 ) ) 
    {
        score -=10;
        aces--;
    }
    return score;
}

The method works through each of the cards in the hand. It keeps track of the number of aces that it has found and then reduces the score of ace cards where appropriate to ensure that the score is as close to 21 as possible without going over.

CardShoe Class

The final card class that is required to manage the cards in the game is the CardShoe class. You use this class to provide a supply of random cards for the game. Casinos have a special device, called a shoe, that holds the cards. At the beginning of a game, a number of packs of cards are shuffled together and placed in the shoe. The shuffle process that PocketJack uses works by randomly swapping elements at different positions in an array of cards a large number of times. The CardShoe class contains this array and performs this form of shuffle on it at the start of the game. It then supplies each card in turn from the array until it reaches the end, at which point the array is shuffled again, and the process repeats.

Designing for Test

When you design a system, you must also think about how you are going to test it. It would be difficult for you to test the game if you had to play it 50 times just to make sure that it works correctly when a player gets a Blackjack score of 21. The CardShoe class has therefore been supplied with an additional feature. In addition to providing a constructor that allows the developer who is using the class to select the number of decks in the shoe, it provides a constructor that accepts a byte array of numeric values that represent a "stacked" deck. Such a deck is not shuffled, instead it will provide cards in a particular, predetermined sequence. The stacked deck enables developers to test the behavior of their card playing applications by providing the application with a particular sequence of card values.

To ensure that the stacked deck cannot be used for nefarious purposes, a flag informs the user of an instance of the CardShoe class whether or not a stacked deck of this form is being used. A developer who is using the instance of this class can test this flag and ensure that the game application is not working from a stacked deck.

When testing the application, you can create comparatively rare occurrences such as blackjacks simply by providing a stacked deck, as shown in the following code.

shoe = new CardShoe( new byte[] {1,14,11,25} );

The preceding stacked deck would give both players blackjacks when dealt. Card sequence 1 represents the first card of the first suit in the deck, which is an ace. Because each suit contains 13 cards, 14 represents the first card of the second suit, which is also an ace. In a complete game, dealing alternates between the player and banker; therefore, having the deck start with card sequences 1 and 14 results in both the player and the banker initially being dealt an ace. Card sequences 11 and 25 represent the jack from the first and second suits respectively, so each player receives a jack as their second card. Dealing both the player and the banker an ace, the jack combination results in both players having blackjack.

Playing the Game

When the game starts, the application clears the player's hand and deals two cards into it as follows.

playerHand.Clear();
playerHand.Add(shoe.DealCard());
playerHand.Add(shoe.DealCard());

Note   For simplicity, this code sample is taken from the Single Player game that is included in the sample download, which does not take into account alternating the act of dealing between the player and banker. The Single Player game sample demonstrates only the player portion of the code. The complete game and the implementation of alternating the act of dealing between the player and banker are discussed in the Complete Blackjack Game section.

A Menu soft key calls the method to "hit" the player with another card, as shown in the following code. It gives the player an additional card as long as the hand score is less than 21.

if ( playerHand.BlackJackScoreHand() < 21 ) 
{
    playerHand.Add(shoe.DealCard());
    this.Invalidate();
}

Note that the BlackJackScoreHand method returns the score of the hand each time. Note also the call of the Invalidate method. This call causes the form to be repainted. The application then draws the cards and updates the score display.

Displaying the Score Values

The Windows text drawing methods are fine for simple messages, but for a game, a user expects something slightly more artistic. For example, you can draw the text on a shaped background to make it stand out. You can implement this shaped background by repeatedly drawing the text in a number of positions around its desired location before putting the real text on top. A way to do this was written as part of a set of utilities, as shown in the following code.

static private SolidBrush messageBrush = new SolidBrush(Color.Black);

public static void BigText ( string message, int x, int y, 
    Color back, Color fore,
    Font messageFont, Graphics g ) 
{
    int i;

    messageBrush.Color = back;
    for ( i=1 ; i < 3 ; i++ ) 
    {
        g.DrawString(message,messageFont,messageBrush,x-i,y-i);
        g.DrawString(message,messageFont,messageBrush,x-i,y+i);
        g.DrawString(message,messageFont,messageBrush,x+i,y-i);
        g.DrawString(message,messageFont,messageBrush,x+i,y+i);
    }

    messageBrush.Color = fore;
    g.DrawString(message,messageFont,messageBrush,x,y);
}

This method is supplied with a reference to the graphics object to use for the drawing, the font to be used, and the position of the text. It is also given the color for the foreground and background versions of the text. It then draws a number of background versions of the text before putting the foreground version on the top. The method is static so that it can be called without needing an instance of the utility class. It is used as follows.

Utilities.BigText("Dealer Bust!",
20,80,Color.Black,Color.Yellow,messageFont,g);

The messageFont is created at the start of the application and used for all the message drawing.

Paint Method

The Paint method is called each time the screen needs to be redrawn. From a design point of view, it is considered bad practice to perform application functions inside the event hander itself. The application therefore calls a method to do the drawing.

private void PocketJackForm_Paint(object sender, 
    System.Windows.Forms.PaintEventArgs e)
{
    drawHand(e.Graphics);
}

The drawHand method is the one you supply to actually perform the drawing. It asks the hand to redraw itself on the screen and then displays the score in a large font, as specified in the following code.

private void drawHand ( Graphics g ) 
{
    playerHand.DrawHand(g,0,10,20,0);
    Utilities.BigText(
        "Score : " + playerHand.BlackJackScoreHand().ToString(),
        10,100,Color.Black,Color.Yellow,scoreFont,g);
}

The cards are drawn 20 pixels apart, giving a nice layering effect. Then, the BigText method draws the score, as shown in Figure 9.

Figure 9. Displaying messages

The screen shot in Figure 9 shows the display that the DrawHand method produces.

The sample project Single Player Program implements a single hand of the blackjack game. (You can open this program by downloading the Pocket Jack for Smartphone.msi.) The player can request extra cards until he or she busts. The player can use the Menu soft key to start a new game or exit the application.

Complete Blackjack Game

You now have an application that implements the display and card management required to start the blackjack game. In fact, you can use the aforementioned classes to implement most kinds of card games. Now you need to consider how a complete game can be implemented, enabling the player to make moves and then the dealer to respond.

Managing Game State

A blackjack game can be in any one of the following states at a particular point of play:

  • The player is making his or her moves.
  • The player busted.
  • The player has won.
  • The dealer is making his or her moves.
  • The dealer busted.
  • The dealer has won.
  • The score is tied (known as a push).

In each of these states, the display required will be different, as will the response of the game to events. For example, the option to hit the dealer for another card is enabled only when the player is making his or her moves. You can represent these states in the application by means of an enumerated type, as follows.

public enum GameMode 
{
    PlayerActive,
    PlayerWon,
    PlayerBust,
    DealerActive,
    DealerWon,
    DealerBust,
    Push
}

A variable of type GameMode retains the state of the game. This variable controls what happens when the screen is redrawn. When the state of the game changes, a set of actions must occur. The best way to obtain this behavior is to implement the state management by means of a property, as follows.

private GameMode modeValue ;
private GameMode mode 
{
    get 
    {
        return modeValue;
    }
    set 
    {
        switch ( value ) 
        {
            case GameMode.PlayerActive:
                hitMenuItem.Enabled=true;
                stayMenuItem.Enabled=true;
                break;
            case GameMode.PlayerWon:
            case GameMode.PlayerBust:
            case GameMode.DealerActive:
            case GameMode.DealerWon:
            case GameMode.DealerBust:
            case GameMode.Push:
                hitMenuItem.Enabled=false;
                stayMenuItem.Enabled=false;
                break;
        }
        modeValue = value;
        this.Invalidate();
    }
}

When the property is assigned a value, the set part of the property is executed. When the set code runs, it performs a switch, which sets up the application in the correct way. For example, when the state property is changed into the PlayerActive state, the Hit and Stay menu items are activated. Wherever the state change occurs in the main application, the user interface is always in the correct state. It also means that you have to change the configuration of the game in only one place in the code. Note that when the state of the game changes, the Invalidate method is called to ensure that the screen is kept up to date.

The game state also controls what is drawn when the paint event is called, as shown in the following code.

private void drawHands ( Graphics g ) 
{
    switch ( mode) 
    {
        case GameMode.PlayerActive:
            dealerHand.DrawHand(g,10,5,80,0);
            playerHand.DrawHand(g,10,110,20,0);
            Utilities.BigText(playerHand.BlackJackScoreHand().ToString(),
                140,150,Color.Black,Color.Yellow,messageFont,g);
            break;
        case GameMode.PlayerWon:
            dealerHand.DrawHand(g,10,5,20,0);
            playerHand.DrawHand(g,10,110,20,0);
            Utilities.BigText(dealerHand.BlackJackScoreHand().ToString(),
            140,45,Color.Black,Color.Yellow,messageFont,g);
            Utilities.BigText(playerHand.BlackJackScoreHand().ToString(),
                140,150,Color.Black,Color.Yellow,messageFont,g);
            Utilities.BigText("You Win!",
                20,80,Color.Black,Color.Yellow,messageFont,g);
            break;
    }
}

The preceding sample code from the Paint method shows how the application manages the display of the messages and hands. The dealer hand score does not appear when the player is making moves.

Setting Up the Game

The dealer must receive two cards, one of which appears face down. You achieve this by using the following code.

// Clear the hands
playerHand.Clear();
dealerHand.Clear();

// Deal the face-down hole card
dealerHoleCard = shoe.DealCard();
dealerHoleCard.FaceUp=false;
dealerHand.Add(dealerHoleCard);

// Deal the first player card
playerHand.Add(shoe.DealCard());
// Deal the second dealer card (face up)
dealerHand.Add(shoe.DealCard());
// Deal the second player card
playerHand.Add(shoe.DealCard());

mode = GameMode.PlayerActive;

The application retains a reference to the "hole" card of the dealer, which is initially face down when a hand starts. This is achieved by setting the FaceUp property to false. When drawn on the screen the back of the hole card will be drawn. When the dealer starts making moves, the hole card's FaceUp property is changed to true so that the next time the dealer's hand is drawn, the picture on the card face will be visible. Note that the mode change at the bottom of the sample code moves the game into the active state, ready for the player to take part.

The application deals the cards in the same order as they would be dealt in a real game, with the dealer and the player taking turns to receive cards. There is no programmatic reason that requires that the act of dealing alternate between the dealer and player, but in a real game it is easier to "stack" the deck if dealing does not alternate.

Player Moves

The application contains a method that is called when the player wants to hit. It gets an additional card only if the score is less than 21, as shown in the following code.

private void playerHits () 
{
    if ( playerHand.BlackJackScoreHand() < 21 ) 
    {
        playerHand.Add(shoe.DealCard());
        if ( playerHand.BlackJackScoreHand() > 21 ) 
        {
            mode = GameMode.PlayerBust;
        }
    }
}

If the player score exceeds 21, the player busts, and the state of the game changes to reflect this. Otherwise, the display is invalidated, which causes a redraw and the new card to be added to the screen.

When the player has reached a score that he or she is happy with, the player can "stay" by selecting the appropriate item, as shown in the following code.

private void playerStays () 
{
    dealerHoleCard.FaceUp=true;
    mode = GameMode.DealerActive;
    this.Refresh();
    System.Threading.Thread.Sleep(750);
    while ( dealerHand.BlackJackScoreHand() < 17 ) 
    {
        dealerHand.Add(shoe.DealCard());
        this.Refresh();
        System.Threading.Thread.Sleep(750);
    }
    if ( dealerHand.BlackJackScoreHand() > 21) 
    {
        mode = GameMode.DealerBust;
        return;
    }
    if  ( playerHand.BlackJackScoreHand()>dealerHand.BlackJackScoreHand() ) 
    {
        mode = GameMode.PlayerWon;
        return;
    }
    
    if  ( playerHand.BlackJackScoreHand()<dealerHand.BlackJackScoreHand() ) 
    {
        mode = GameMode.DealerWon;
        return;
    }

    if  ( playerHand.BlackJackScoreHand()==dealerHand.BlackJackScoreHand() ) 
    {
        mode = GameMode.Push;
        return;
    }
}

The method has quite a bit of work to do. It must change the game state to DealerActive and then play out the dealer hand. It also turns the dealer hole card face up so that it will be drawn for the player to see. The dealer turn is played out by a loop that repeatedly gives new cards for the dealer while the dealer score is less than 17. The dealer is required to play the game in this mechanistic way. The application contains a delay of 750 milliseconds between each dealer card to add to the excitement. Calls of the Refresh method ensure that the player is kept informed of each successive dealer card.

If the dealer receives a score greater than 21, the state is changed to DealerBust, and the method finishes. Otherwise, the method decides who has won and sets the state accordingly. The player can then select a new game, which sets up the hands again and sets the state appropriately.

Context-Sensitive User Interface

Smartphones are restricted by the available user interface controls. Although it has a lot of keys, it does not have a touch screen or mouse, which means that you cannot implement on-screen buttons for the user to select. Furthermore, the Smartphone is designed to be used with only one hand, which means that the controls must be easy to use. At any given point, there is a limited number of actions that the player can perform. To make the playing easier, it is sensible to map these actions onto the controls that the player most easily uses (as shown in the following code). In the case of the Smartphone, this means the Menu soft key and the joystick.

private void doEnter () 
{
    switch ( mode ) 
    {
        case GameMode.PlayerActive:
            playerHits();
            break;
        case GameMode.PlayerWon:
        case GameMode.PlayerBust:
        case GameMode.DealerActive:
        case GameMode.DealerWon:
        case GameMode.DealerBust:
        case GameMode.Push:
            startGame();
            break;
    }
}

When the player presses the joystick on a Smartphone, the preceding code decides the most sensible thing for the application to do. In PlayerActive mode, pressing the joystick hits the player with another card. In any other mode, it deals the cards for a new game. By also using the left Menu soft key for the Stay command, the user can play the entire game just by using the joystick and the Menu soft key.

You should consider this issue when designing your games. Try to use the game with the phone held in one hand while the other holds a cup of coffee. This should be possible! Note that Menu commands can also select all these context-sensitive actions; that is, the Menu also holds a Hit command for the user. A player is never required to guess how to perform a particular action.

The Dealer Game sample project enables the player to play a single game against the dealer. You can open this program by downloading the Pocket Jack for Smartphone.msi.

Betting Pot

A single game of blackjack is all very well, but most players want an opportunity to break the bank. To do this, the game must implement a series of hands, with the player betting a stake on each one. The betting part of the game is performed prior to each hand being played. The player is shown the size of the pot available and can increase or decrease his or her bet before playing the hand. If the player's funds run low, the application should give the player the option of resetting the pot back to the original start values. The pot has been implemented as a separate class, so that it can be used in other games if required. Before each hand, the player can decide how much to bet, up to the amount in the pot. If the player tries to bet more than the size of the pot, the application offers to reset the pot to the original value.

The player easily manages the size of the bet by moving the joystick up or down. However, the commands are also available on the Menu soft key as well.

After selecting the bet size, the player can start the game by pressing the Menu soft key or pressing the joystick. The betting mode is actually managed by means of an additional game state, which also causes a betting backdrop image to appear, as shown in Figure 10.

Click here for larger image

Figure 10. Managing the bet. Click the thumbnail for larger image.

Making a Finished Game

The application now has all of the features required to play blackjack. However, a few additions will make it into a proper game. These additions include a Help screen, sound, and a loading screen.

Adding a Help Screen

The Help screen is actually another form that contains a single TextBox that holds the help information, as shown in Figure 11. When the user selects the Help option from the menu, the help form appears. When the form is closed, the display reverts back to the game form. The help form Load and Closing events cause the parent form to be hidden and displayed, respectively, to manage the transition between the two forms on the Smartphone. The form has a single menu command, which just closes it.

Figure 11. The Help screen

Adding Sound

The sound for the game adds greatly to the atmosphere. However, because not everybody will want the sound all the time, you must provide an option to turn off the sound.

The Sound utility class supplies all of the sound behaviors. This class allows sounds to be played continuously as loops or as a single sound. For each sound you create, you create an instance of the Sound class. There are multiple constructors for the class so that you can create an instance by using a path to an internal resource, or by using a stream, as follows.

private void readStream ( Stream soundStream ) 
{
    // Create a byte array to hold the sample
    soundBytes = new byte [soundStream.Length];
    // Read the sample from the stream
    soundStream.Read(soundBytes, 0,(int)soundStream.Length);
}

public Sound( Stream soundStream)    
{
    readStream(soundStream);
}

public Sound ( string filename )
{
    System.Reflection.Assembly execAssem = 
        System.Reflection.Assembly.GetExecutingAssembly();
    Stream soundStream = execAssem.GetManifestResourceStream(filename);
    readStream ( soundStream );
}

The readStream method performs the actual sound loading. A native operating system method controls the sound generation. The Sound class contains a platform invoke (P/Invoke) binding to the method so that it can be called to control the sound, as follows.

private enum Flags 
{
    SND_SYNC = 0x0000,          /* play synchronously (default) */
    SND_ASYNC = 0x0001,      /* play asynchronously */
    SND_NODEFAULT = 0x0002,  /* silence (!default) if sound not found */
    SND_MEMORY = 0x0004,  /* pszSound points to a memory file */
    SND_LOOP = 0x0008,  /* loop the sound until next sndPlaySound */
    SND_NOSTOP = 0x0010,  /* don't stop any currently playing sound */
    SND_NOWAIT = 0x00002000, /* don't wait if the driver is busy */
    SND_ALIAS = 0x00010000, /* name is a registry alias */
    SND_ALIAS_ID = 0x00110000, /* alias is a predefined ID */
    SND_FILENAME = 0x00020000, /* name is file name */
    SND_RESOURCE = 0x00040004  /* name is resource name or atom */
}
[DllImport("CoreDll.DLL", EntryPoint="PlaySound", SetLastError=true)]
private extern static int WCE_PlaySoundBytes (byte[] szSound, IntPtr hMod,
int flags);

A set of flags controls the sound generation itself. The Sound class contains a static member that holds a reference to the currently looping sound (if any). If the sound is turned off and then on again, the looped sound can resume playing. The sound instance contains Play and PlayLoop methods to select which particular kind of play is required, as follows.

public void Play () 
{
    loopSound = null;
    if ( Sound.Enabled ) 
    {
        WCE_PlaySoundBytes (
            soundBytes, // sample array
            IntPtr.Zero, // null - not using a resource
             (int) (Flags.SND_ASYNC | Flags.SND_MEMORY)
            );
    }
}
public void PlayLoop () 
{
    loopSound = soundBytes;
    if ( Sound.Enabled ) 
    {
        WCE_PlaySoundBytes (
            soundBytes, // sample array
            IntPtr.Zero, // null - not using a resource
             (int) (Flags.SND_ASYNC | Flags.SND_MEMORY | Flags.SND_LOOP) 
            );
    }
}

The Sound class should stop playing sounds when the user moves away from the game screen. When the player returns to the form, the sounds should be resumed. The events that control this behavior are the Deactivate and Activate events. These events are not available from the events editor for the form, so you need to set them up in the constructor method as follows.

this.Activated += new EventHandler(this.OnActivate);
this.Deactivate += new EventHandler(this.OnDeActivate);

The OnActivate and OnDeActivate methods are called when the form is activated or deactivated, respectively. They call static methods in the Sound class to control the sound playing, as follows.

void OnActivate(object sender, EventArgs e) 
{
    Sound.ResumeSound();
}

void OnDeActivate(object sender, EventArgs e) 
{
    Sound.StopSound();
}

The StopSound method in the Sound class stops the presently playing sound by requesting a null sound to be played immediately, as follows.

public static void StopSound() 
{
    // Only stop the sound if you are looping a sound
    if ( loopSound != null) 
    {
        WCE_PlaySoundBytes (
            null, // Null sound array
            IntPtr.Zero, // Null - not using a resource
            0 // Playback mode
            );
    }
}

The ResumeSound method only resumes a sound if a looped sound was previously playing, as shown in the following code.

public static void ResumeSound () 
{
    if ( !Sound.Enabled ) 
    {
        return;
    }
    if ( loopSound != null) 
    {
        WCE_PlaySoundBytes (
            loopSound, // Sample array
            IntPtr.Zero, // Null - not using a resource
             (int) (Flags.SND_ASYNC | Flags.SND_MEMORY | Flags.SND_LOOP)
            );
    }
}

The enabled member of the class is the flag that indicates that sound is enabled. If false, the sound is not resumed or played. The use of a selected version of a menu option manages this flag, as shown in Figure 12.

Figure 12. The Sound menu option

Adding a Loading Screen

The final component to be added is a loading screen. This screen gives the player something to look at while all of the game components are being loaded and forms are being built. The loading screen has been implemented as yet another game mode. When loading is being performed, the Paint action draws the loading screen. The only issue to be addressed is that for the Paint action to draw the loading screen, the game form itself must be visible. To make the game form visible, you set the form's Visible property appropriately, as shown in the following code.

System.Reflection.Assembly asm = 
    System.Reflection.Assembly.GetExecutingAssembly();
loadingImage = new Bitmap(asm.GetManifestResourceStream(
    "PocketJack.images.loading.gif"));
this.Refresh();
mode = GameMode.LoadingDisplay;
this.Visible=true;

After the game starts, the mode moves into a game state and the banner no longer appears. The loading image appears over the form during the loading process on top of the betting background, as the following code specifies.

private void paintForm ( Graphics g ) 
{
    switch ( mode) 
    {
        case GameMode.LoadingDisplay:
            g.DrawImage(bankImage,0,0);
            g.DrawImage(loadingImage,0,60);
            break;
        case GameMode.PlacingBets:
            g.DrawImage(bankImage,0,0);
            Utilities.BigText("Pot : " + pot.PotValue.ToString(),
                10,40,Color.Black,Color.Yellow,messageFont,g);
            Utilities.BigText("Bet : " + pot.BetValue.ToString(),
                10,80,Color.Black,Color.Yellow,messageFont,g);
            break;
        // Other cases go here
    }
}

The loading screen consists of the betting background with a message displayed across it, as shown in Figure 13.

Click here for larger image

Figure 13. The loading screen. Click the thumbnail for larger image.

The project Pots and Sounds contains a fully functional version of the Pocket Jack game that has all the enhancements described previously. You can open this program by downloading the Pocket Jack for Smartphone.msi.

Conclusion

This article describes the implementation of a complete BlackJack game that you can use to while away the hours. However, it should be a useful repository of methods and techniques that you can deploy when you write a Smartphone card game of your own.

Show:
© 2014 Microsoft