Solution for
Programming Exercise 7.4


THIS PAGE DISCUSSES ONE POSSIBLE SOLUTION to the following exercise from this on-line Java textbook.

Exercise 7.4: In the Blackjack game BlackjackGUI.java from Exercise 6.8, the user can click on the "Hit", "Stand", and "NewGame" buttons even when it doesn't make sense to do so. It would be better if the buttons were disabled at the appropriate times. The "New Game" button should be disabled when there is a game in progress. The "Hit" and "Stand" buttons should be disabled when there is not a game in progress. The instance variable gameInProgress tells whether or not a game is in progress, so you just have to make sure that the buttons are properly enabled and disabled whenever this variable changes value. Make this change in the Blackjack program. This applet uses a nested class, BlackjackCanvas, to represent the board. You'll have to do most of your work in that class. In order to manipulate the buttons, you will have to use instance variables to refer to the buttons.

I strongly advise writing a subroutine that can be called whenever it is necessary to set the value of the gameInProgress variable. Then the subroutine can take responsibility for enabling and disabling the buttons. Recall that if bttn is a variable of type JButton, then bttn.setEnabled(false) disables the button and bttn.setEnabled(true) enables the button.


Discussion

Here is my applet:

In the original applet, the button variables are declared in the init() method of the applet, where the buttons are created. There are no instance variables that refer to the buttons, so it is not possible to do anything with the buttons outside the init() method. For this exercise, references to the buttons must be stored in instance variables. These variables are required in order to call the buttons' setEnabled() methods.

There are several ways that this could work. We could put the variables in the BlackjackCanvas class; then the init() method of the applet could refer to them through a variable of that class. Since the variable, board, refers to the canvas, the init() method would refer to a button named hit in the canvas as board.hit. It would set up the button with commands such as:

          board.hit = new JButton("Hit!");
          board.hit.addActionListener(board);
          buttonPanel.add(board.hit);

However, since the BlackjackCanvas class is nested inside the applet class, we might as well just declare the button variables in the main class. Since BlackJackCanvs is a nested, non-static class, the board will automatically have access to the instance variables in the main class.

The applet uses instance variables hit, stand, and newGame to refer to the buttons. The buttons must be enabled and disabled whenever the value of the variable gameInProgress changes. As recommended in the exercise, I wrote a method for changing the value of this variable. This method also enables and disables the buttons to reflect the state of the program:

          public void setGameInProgress(boolean inProgress) {
                // This method should be called whenever the value of
                // the gameInProgress variable needs to be changed.  It 
                // changes the value of the variable and also enables/disables
                // the buttons to reflect the state of the game.
             gameInProgress = inProgress;
             if (gameInProgress) {
                hit.setEnabled(true);
                stand.setEnabled(true);
                newGame.setEnabled(false);
             }
             else {
                hit.setEnabled(false);
                stand.setEnabled(false);
                newGame.setEnabled(true);
             }
          }

Once this routine is available, then any line in the old program that said "gameInProgress = false;" should be changed to "setGameInProgress(false);". And any line that said "gameInProgress = true;" should be changed to "setGameInProgress(true);". In this way, we can be sure that the buttons are always properly enabled and disabled. (You should also check that they are in the correct states when the applet first appears. They are, because the doNewGame() routine is called as part of the setup, and this routine always calls either "setGameInProgress(false);" or "setGameInProgress(true);".)

You should understand why I used a subroutine to set the value of gameInProgress. Every time gameInProgress changes, each of the buttons has to be enabled or disabled. That's three extra lines of code each time the program says gameInProgress = true or gameInProgress = false. We can avoid some extra typing by calling the subroutine. Furthermore, if we always call the subroutine to set the value of gameInProgress, we can be sure that the states of the buttons will always be set correctly to match the value of gameInProgress.

That's essentially all there is to this exercise, but my first attempt had a bug. The applet wouldn't start properly because of a NullPointerException. The problem was that the constructor in the BlackjackCanvas class calls doNewGame() which calls setGameInProgress. In order for this to work, the buttons must already exist. Otherwise, the value of hit is null in the setGameInProgress method, and the statement "hit.setEnabled(true);" generates the error because hit doesn't refer to any object. In order to fix this problem, I just had to create the buttons before creating the canvas in the applet's init() method.


The Solution

Changes from BlackjackGUI are shown in red.


    /*
       In this applet, the user plays a game of Blackjack.  The
       computer acts as the dealer.  The user plays by clicking
       "Hit!" and "Stand!" buttons.
       
       The programming of this applet assumes that the applet is
       set up to be about 466 pixels wide and about 346 pixels high.
       That width is just big enough to show 2 rows of 5 cards.
       The height is probably a little bigger than necessary,
       to allow for variations in the size of buttons from one platform
       to another.
       
    */
    
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    
    public class BlackjackGUI2 extends JApplet {
    
       /* Declare the buttons that the user clicks to control the
          game.  These buttons are set up in the init() method and
          are used in the nested class BlackjackCanvas. */
          
       JButton hit, stand, newGame;

       public void init() {
       
             // The init() method creates components and lays out the applet.
             // A BlackjackCanvas occupies the CENTER position of the layout.
             // On the bottom is a panel that holds three buttons.  The
             // BlackjackCanvas object listens for events from the buttons
             // and does all the real work of the program.
             
          setBackground( new Color(130,50,40) );
          
          hit = new JButton( "Hit!" );
          stand = new JButton( "Stand!" );
          newGame = new JButton( "New Game" );
             // I had to move these three lines to the start of the init() 
             // method because the constructor of the BlackjackCanvas
             // class refers to the buttons (indirectly, by
             // calling doNewGame()).  This led to a NullPointerException
             // when the constructor was called before the buttons
             // were created.

          BlackjackCanvas board = new BlackjackCanvas();
          getContentPane().add(board, BorderLayout.CENTER);
          
          JPanel buttonPanel = new JPanel();
          buttonPanel.setBackground( new Color(220,200,180) );
          getContentPane().add(buttonPanel, BorderLayout.SOUTH);
          
          hit.addActionListener(board);
          buttonPanel.add(hit);
          
          stand.addActionListener(board);
          buttonPanel.add(stand);
          
          newGame.addActionListener(board);
          buttonPanel.add(newGame);
          
       }  // end init()
       
       public Insets getInsets() {
             // Specify how much space to leave between the edges of
             // the applet and the components it contains.  The background
             // color shows through in this border.
          return new Insets(3,3,3,3);
       }
       

       // --- The remainder of this class consists of a nested class ---

    
       class BlackjackCanvas extends JPanel implements ActionListener {

             // A nested class that displays the card game and does all the work
             // of keeping track of the state and responding to user events.

          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.


          BlackjackCanvas() {
                // Constructor.  Creates fonts and starts the first game.
             setBackground( new Color(0,120,0) );
             smallFont = new Font("SansSerif", Font.PLAIN, 12);
             bigFont = new Font("Serif", Font.BOLD, 14);
             doNewGame();
          }


          public void actionPerformed(ActionEvent evt) {
                 // Respond when the user clicks on a button by calling
                 // the appropriate procedure.  Note that the canvas is
                 // registered as a listener in the BlackjackGUI class.
             String command = evt.getActionCommand();
             if (command.equals("Hit!"))
                doHit();
             else if (command.equals("Stand!"))
                doStand();
             else if (command.equals("New Game"))
                doNewGame();
          }


          public void setGameInProgress(boolean inProgress) {
                // This method should be called whenever the value of
                // the gameInProgress variable needs to be changed.  It 
                // changes the value of the variable and also enables/disables
                // the buttons to reflect the state of the game.
             gameInProgress = inProgress;
             if (gameInProgress) {
                hit.setEnabled(true);
                stand.setEnabled(true);
                newGame.setEnabled(false);
             }
             else {
                hit.setEnabled(false);
                stand.setEnabled(false);
                newGame.setEnabled(true);
             }
          }

          void doHit() {
                 // 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.
             if (gameInProgress == false) {
                    // This should be impossible, since the Hit button is disabled
                    // when the game is not is progress, but it doesn't hurt to check.
                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.";
                setGameInProgress(false);
             }
             else if (playerHand.getCardCount() == 5) {
                message = "You win by taking 5 cards without going over 21.";
                setGameInProgress(false);
             }
             else {
                message = "You have " + playerHand.getBlackjackValue() + ".  Hit or Stand?";
             }
             repaint();
          }


          void doStand() {
                  // 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.
             if (gameInProgress == false) {
                message = "Click \"New Game\" to start a new game.";
                repaint();
                return;
             }
             setGameInProgress(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();
          }


          void doNewGame() {
                 // Called by the constructor, and called by actionPerformed() if
                 // the use 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.
             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.";
                 setGameInProgress(false);
             }
             else if (playerHand.getBlackjackValue() == 21) {
                 message = "You win!  You have Blackjack.";
                 setGameInProgress(false);
             }
             else {
                 message = "You have " + playerHand.getBlackjackValue() + ".  Hit or stand?";
                 setGameInProgress(true);
             }
             repaint();
          }  // end newGame();


          public void paintComponent(Graphics g) {
                // 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.

             super.paintComponent(g); // fill with background color.

             g.setFont(bigFont);
             g.setColor(Color.green);
             g.drawString(message, 10, getSize().height - 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();


          void drawCard(Graphics g, Card card, int x, int y) {
                  // 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.)
             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 BlackjackCanvas


    } // end class BlackJackGUI2
    
    



[ Exercises | Chapter Index | Main Index ]