[ Exercises | Chapter Index | Main Index ]

Solution for Programming Exercise 6.10


This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.


Exercise 6.10:

Write a GUI Blackjack program that lets the user play a game of Blackjack, with the computer as the dealer. The program should draw the user's cards and the dealer's cards, just as was done for the graphical HighLow card game in Subsection 6.6.6. You can use the source code for that game, HighLowGUI.java, for some ideas about how to write your Blackjack game. The structures of the HighLow panel and the Blackjack panel are very similar. You will certainly want to use the drawCard() method from the HighLow program.

You can find a description of the game of Blackjack in Exercise 5.5. Add the following rule to that description: If a player takes five cards without going over 21, that player wins immediately. This rule is used in some casinos. For your program, it means that you only have to allow room for five cards. You should assume that the panel is just wide enough to show five cards, and that it is tall enough show the user's hand and the dealer's hand.

Note that the design of a GUI Blackjack game is very different from the design of the text-oriented program that you wrote for Exercise 5.5. The user should play the game by clicking on "Hit" and "Stand" buttons. There should be a "New Game" button that can be used to start another game after one game ends. You have to decide what happens when each of these buttons is pressed. You don't have much chance of getting this right unless you think in terms of the states that the game can be in and how the state can change.

Your program will need the classes defined in Card.java, Hand.java, Deck.java, and BlackjackHand.java.

The next exercise has a picture of a blackjack game that you can use a guide, except that the version for this exercise does not allow betting.


Discussion

The constructor for this exercise can be almost identical to that in the HighLow game. The text of the buttons just has to be changed from "Higher" and "Lower" to "Hit" and "Stand". However, the nested class, CardPanel has to be rewritten to implement a game of Blackjack instead of a game of HighLow. The basic structure of the revised class remains similar to the original. All the programming for the game is in this class.

In the HighLow game, there is one "hand," which holds all the cards that have been dealt. Blackjack is a two-player game, so there are two hands, one for the player and one for the dealer. These hands are of type BlackjackHand. So, we need instance variables

BlackjackHand dealerHand;   // The dealer's cards.
BlackjackHand playerHand;   // The user's cards.

We also need a deck and a boolean-valued instance variable, gameInProgress, to keep track of the two basic states of the game: Is a game in progress, or are we between games. Finally, there is a message variable, which holds the string that is shown at the bottom of the game board.

The paintComponent() method uses the information in the dealerHand, playerHand, message, and gameInProgress variables. The reason it needs to look at the gameInProgress variable is that when a game is in progress, one of the dealer's cards is drawn face down, so the user can't see it. Once the game is over, the card is drawn face up so the user can see what the dealer was holding. Note that there is no point in the program where I say, "turn the dealer's first card face up"! It happens automatically because the state of the game changes, and the paintComponent() method checks the state when it draws the panel. If the game is over, the card is face up. If the game is in progress, the card is face down. This is nice example of state-machine thinking.

Note that writing the paintComponent() method required some calculation. The cards are 80 pixels wide and 100 pixels tall. Horizontally, there is a gap of 10 pixels between cards, and there are gaps of 10 pixels between the cards and the left and right edges. The total width needed for the card panel, 460, allows for five 80-pixel cards and six 10-pixel gaps: 5*80 + 6*10 = 460. The panel is another 6 pixels wide because of a 3-pixel wide border on each side. The N-th card, counting from 0, has its left edge at 10+90*N. It might be easier to see this as 10+80*N+10*N, 10 pixels on the left plus N 80-pixel cards, plus N 10-pixel gaps between cards. Vertically, I allow 30 pixels for each string, "Dealer's Cards" and "Your Cards". This puts the top of the first row of cards at y=30. Allowing 100 pixels for that row of cards and 30 pixels for the string "Your Cards", the top of the second row of cards is at 160. Given all this, you should be able to understand the paintComponent() method. Allowing 100 pixels for the second row of cards and 30 pixels for the message at the bottom of the board, we need a height of at least 290 pixels for the drawing area. I set the preferred height of the CardPanel to 310 to for some extra space between the cards and the message at the bottom of the panel.

In this GUI version of Blackjack, things happen when the user clicks the "Hit", "Stand", and "New Game" buttons. The program handles these events by calling the routines doHit(), doStand(), and doNewGame(). Each of these routines has responsibility for one part of the game of Blackjack. Note that each routine starts by checking the state of the game to make sure that it is legal to call the routine at this time. If gameInProgress is true, the user can legally click "Hit" or "Stand". If gameInProgress is false, the user can legally click "New Game". If the user made an illegal move, an error message is stored in the message variable, and repaint() is called so the user will see the new message. This is similar to the way the three buttons in HighLowGUI are handled.

The doNewGame() routine has to set up a new game. This means creating the deck and hands, shuffling the deck and dealing two cards into each hand. At this point, the first time I wrote the game, I just set gameInProgress to true, to record the fact that the state of the game has changed. Later, I realized that the doNewGame() routine also has to check whether one of the players has Blackjack, since there is really no other place where this can be done. If one of the players has Blackjack, the game is over as soon as it starts, so gameIsProgress has to be false, and the only action that the user can take at that point is to click the "New Game" button again. (Note that the doNewGame() routine is also called by the constructor of the CardPanel class. This sets up the first game, when the panel is first created, so the user doesn't have to click on the "New Game" button to start the first game.)

When the user clicks "Hit", if the game is in progress, we deal a card into the user's hand. At this point, the state of the game might have changed. If the user has over 21, the user loses and the game is over. If the user has taken 5 cards without going over 21, the user wins and the game is over. In either of these cases, the value of the state variable gameInProgress becomes false. Otherwise, gameInProgress retains the value true, and the game will continue. Since gameInProgress is true, the user still has the choice of clicking "Hit" or "Stand". (Note that there is no loop in the program that says "while the user continues to hit." The progress of the game is driven by events.)

Finally, when the user clicks "Stand", the game is definitely over, so gameInProgress is set to false. However, before the game can end, the dealer gets to draw cards and a winner is determined. This all has to be done in the doStand() routine. Then, the panel is repainted to show the final state of the game.


The Solution

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;


/**
 * In this program, the user plays a game of Blackjack.  The
 * computer acts as the dealer.  The user plays by clicking
 * "Hit!" and "Stand!" buttons.
 * 
 * This class defines a panel, but it also contains a main()
 * routine that makes it possible to run the program as a
 * stand-alone application.  
 *
 * This program depends on the following classes:  Card, Hand,
 * BlackjackHand, Deck.
 */
public class BlackjackGUI extends JPanel {
   
   /**
    * The main routine simply opens a window that shows a BlackjackGUI.
    */
   public static void main(String[] args) {
      JFrame window = new JFrame("Blackjack");
      BlackjackGUI content = new BlackjackGUI();
      window.setContentPane(content);
      window.pack();  // Set size of window to preferred size of its contents.
      window.setResizable(false);  // User can't change the window's size.
      Dimension screensize = Toolkit.getDefaultToolkit().getScreenSize();
      window.setLocation( (screensize.width - window.getWidth())/2, 
            (screensize.height - window.getHeight())/2 );
      window.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      window.setVisible(true);
   }
      
   
   /**
    * The constructor lays out the panel.  A CardPanel occupies the CENTER 
    * position of the panel (where CardPanel is a subclass of JPanel that is 
    * defined below).  On the bottom is a panel that holds three buttons.  
    * The CardPanel listens for ActionEvents from the buttons and does all 
    * the real work of the program.
    */
   public BlackjackGUI() {
      
      setBackground( new Color(130,50,40) );
      
      setLayout( new BorderLayout(3,3) );
      
      CardPanel board = new CardPanel();
      add(board, BorderLayout.CENTER);
      
      JPanel buttonPanel = new JPanel();
      buttonPanel.setBackground( new Color(220,200,180) );
      add(buttonPanel, BorderLayout.SOUTH);
      
      JButton hitButton = new JButton( "Hit!" );
      hitButton.addActionListener( evt -> board.doHit() );
      buttonPanel.add(hitButton);
      
      JButton standButton = new JButton( "Stand!" );
      standButton.addActionListener( evt -> board.doStand() );
      buttonPanel.add(standButton);
      
      JButton newGame = new JButton( "New Game" );
      newGame.addActionListener( evt -> board.doNewGame() );
      buttonPanel.add(newGame);
      
      setBorder(BorderFactory.createLineBorder( new Color(130,50,40), 3) );
      
   }  // end constructor
   
   
   
   /**
    * A nested class that displays the game and does all the work
    * of keeping track of the state and responding to user events.
    */
   private class CardPanel extends JPanel {
      
      Deck deck;         // A deck of cards to be used in the game.
      
      BlackjackHand dealerHand;   // Hand containing the dealer's cards.
      BlackjackHand playerHand;   // Hand containing the user's cards.
      
      String message;  // A message drawn on the canvas, which changes
                       //    to reflect the state of the game.
      
      boolean gameInProgress;  // Set to true when a game begins and to false
                               //   when the game ends.
      
      Font bigFont;      // Font that will be used to display the message.
      Font smallFont;    // Font that will be used to draw the cards.
      
      
      /**
       * The constructor creates the fonts and starts the first game.
       * It also sets a preferred size of 460-by-310 for the panel.
       * The paintComponent() method assumes that this is in fact the
       * size of the panel (although it can be a little taller with
       * no bad effect).
       */
      CardPanel() {
         setPreferredSize( new Dimension(460,310) );
         setBackground( new Color(0,120,0) );
         smallFont = new Font("SansSerif", Font.PLAIN, 12);
         bigFont = new Font("Serif", Font.BOLD, 14);
         doNewGame();
      }
      
      
      /**
       * This method is called when the user clicks the "Hit!" button.  First 
       * check that a game is actually in progress.  If not, give  an error 
       * message and exit.  Otherwise, give the user a card.  The game can end 
       * at this point if the user goes over 21 or if the user has taken 5 cards 
       * without going over 21.
       */
      void doHit() {
         if (gameInProgress == false) {
            message = "Click \"New Game\" to start a new game.";
            repaint();
            return;
         }
         playerHand.addCard( deck.dealCard() );
         if ( playerHand.getBlackjackValue() > 21 ) {
            message = "You've busted!  Sorry, you lose.";
            gameInProgress = false;
         }
         else if (playerHand.getCardCount() == 5) {
            message = "You win by taking 5 cards without going over 21.";
            gameInProgress = false;
         }
         else {
            message = "You have " + playerHand.getBlackjackValue() + ".  Hit or Stand?";
         }
         repaint();
      }
      
      
      /**
       * This method is called when the user clicks the "Stand!" button.
       * Check whether a game is actually in progress.  If it is, the game 
       * ends.  The dealer takes cards until either the dealer has 5 cards 
       * or more than 16 points.  Then the  winner of the game is determined. 
       */
      void doStand() {
         if (gameInProgress == false) {
            message = "Click \"New Game\" to start a new game.";
            repaint();
            return;
         }
         gameInProgress = false;
         while (dealerHand.getBlackjackValue() <= 16 && dealerHand.getCardCount() < 5)
            dealerHand.addCard( deck.dealCard() );
         if (dealerHand.getBlackjackValue() > 21)
            message = "You win!  Dealer has busted with " + dealerHand.getBlackjackValue() + ".";
         else if (dealerHand.getCardCount() == 5)
            message = "Sorry, you lose.  Dealer took 5 cards without going over 21.";
         else if (dealerHand.getBlackjackValue() > playerHand.getBlackjackValue())
            message = "Sorry, you lose, " + dealerHand.getBlackjackValue()
                        + " to " + playerHand.getBlackjackValue() + ".";
         else if (dealerHand.getBlackjackValue() == playerHand.getBlackjackValue())
            message = "Sorry, you lose.  Dealer wins on a tie.";
         else
            message = "You win, " + playerHand.getBlackjackValue()
                        + " to " + dealerHand.getBlackjackValue() + "!";
         repaint();
      }
      
      
      /**
       * Called by the constructor, and called by actionPerformed() if  the 
       * user clicks the "New Game" button.  Start a new game.  Deal two cards 
       * to each player.  The game might end right then  if one of the players 
       * had blackjack.  Otherwise, gameInProgress is set to true and the game 
       * begins.
       */
      void doNewGame() {
         if (gameInProgress) {
               // If the current game is not over, it is an error to try
               // to start a new game.
            message = "You still have to finish this game!";
            repaint();
            return;
         }
         deck = new Deck();   // Create the deck and hands to use for this game.
         dealerHand = new BlackjackHand();
         playerHand = new BlackjackHand();
         deck.shuffle();
         dealerHand.addCard( deck.dealCard() );  // Deal two cards to each player.
         dealerHand.addCard( deck.dealCard() );
         playerHand.addCard( deck.dealCard() );
         playerHand.addCard( deck.dealCard() );
         if (dealerHand.getBlackjackValue() == 21) {
            message = "Sorry, you lose.  Dealer has Blackjack.";
            gameInProgress = false;
         }
         else if (playerHand.getBlackjackValue() == 21) {
            message = "You win!  You have Blackjack.";
            gameInProgress = false;
         }
         else {
            message = "You have " + playerHand.getBlackjackValue() + ".  Hit or stand?";
            gameInProgress = true;
         }
         repaint();
      }  // end newGame();
      
      
      /**
       * The paint method shows the message at the bottom of the
       * canvas, and it draws all of the dealt cards spread out
       * across the canvas.
       */
      public void paintComponent(Graphics g) {
         
         super.paintComponent(g); // fill with background color.
         
         g.setFont(bigFont);
         g.setColor(Color.GREEN);
         g.drawString(message, 10, getHeight() - 10);
         
         // Draw labels for the two sets of cards.
         
         g.drawString("Dealer's Cards:", 10, 23);
         g.drawString("Your Cards:", 10, 153);
         
         // Draw dealer's cards.  Draw first card face down if
         // the game is still in progress,  It will be revealed
         // when the game ends.
         
         g.setFont(smallFont);
         if (gameInProgress)
            drawCard(g, null, 10, 30);
         else
            drawCard(g, dealerHand.getCard(0), 10, 30);
         for (int i = 1; i < dealerHand.getCardCount(); i++)
            drawCard(g, dealerHand.getCard(i), 10 + i * 90, 30);
         
         // Draw the user's cards.
         
         for (int i = 0; i < playerHand.getCardCount(); i++)
            drawCard(g, playerHand.getCard(i), 10 + i * 90, 160);
         
      }  // end paint();
      
      
      /**
       * Draws a card as a 80 by 100 rectangle with upper left corner at (x,y).
       * The card is drawn in the graphics context g.  If card is null, then
       * a face-down card is drawn.  (The cards are rather primitive!)
       */
      void drawCard(Graphics g, Card card, int x, int y) {
         if (card == null) {  
               // Draw a face-down card
            g.setColor(Color.blue);
            g.fillRect(x,y,80,100);
            g.setColor(Color.white);
            g.drawRect(x+3,y+3,73,93);
            g.drawRect(x+4,y+4,71,91);
         }
         else {
            g.setColor(Color.white);
            g.fillRect(x,y,80,100);
            g.setColor(Color.gray);
            g.drawRect(x,y,79,99);
            g.drawRect(x+1,y+1,77,97);
            if (card.getSuit() == Card.DIAMONDS || card.getSuit() == Card.HEARTS)
               g.setColor(Color.red);
            else
               g.setColor(Color.black);
            g.drawString(card.getValueAsString(), x + 10, y + 30);
            g.drawString("of", x+ 10, y + 50);
            g.drawString(card.getSuitAsString(), x + 10, y + 70);
         }
      }  // end drawCard()
      
      
   } // end nested class CardPanel
   
   
} // end class BlackjackGUI

[ Exercises | Chapter Index | Main Index ]