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 canvas 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'll need instance variables to refer to the buttons. One problem you have to deal with is that the buttons are used in both the applet class and the canvas class.

I strongly advise using a subroutine 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 Button, then bttn.setEnabled(false) disables the button and bttn.setEnabled(true) enables the button.


Discussion

Here is my applet:

In the original applet, the buttons are created in the init() method of the applet. 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. The instance variables could be in either the applet class or the canvas class, but they will be used in both classes.

There are several ways that this could work. If the variables are in the canvas 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 Button("Hit!");
          board.hit.addActionListener(board);
          board.hit.setBackground(Color.lightGray);
          buttonPanel.add(board.hit);

Alternatively, if the instance variables are in the applet class, then the canvas could store a reference to the applet object in an instance variable. If that variable is called owner, then the canvas class could refer to a button, hit, in the applet class as owner.hit. It would disable this button by saying owner.hit.setEnabled(false). The value for owner would typically be passed as a parameter to the constructor of the canvas object. This might sound complicated, but it can be a reasonable way for one object to communicate with another in some cases.

However, the easiest solution in this case is to make the canvas class a nested class inside the applet class. The instance variables can be defined in the applet class. But since the canvas class is nested inside the applet class, it has access to all the members of the applet class. In particular, both the applet class and the canvas class can refer to the button variables directly. Simple sharing of variables like this is a good reason to use nested classes.

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 is 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);".)

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 canvas 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 java.applet.*;
    
    public class BlackjackGUI2 extends Applet {
    
       /* 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 BJCanvas. */
          
       Button hit, stand, newGame;
                              
       public void init() {
       
             // The init() method lays out the applet using a BorderLayout.
             // A BJCanvas occupies the CENTER position of the layout.
             // On the bottom is a panel that holds three buttons.  The
             // BJCanvas object listens for ActionEvents from the buttons
             // and does all the real work of the program.
             
          hit = new Button("Hit!");          // Create buttons first to avoid
          stand = new Button("Stand!");      //    a NullPointerException when
          newGame = new Button("New Game");  //    the BJCanvas constructor is called.
    
          setBackground( new Color(130,50,40) );
          setLayout( new BorderLayout(3,3) );
          
          BJCanvas board = new BJCanvas();  // (uses nested class BJCanvas)
          add(board, BorderLayout.CENTER);
          
          Panel buttonPanel = new Panel();
          buttonPanel.setBackground( new Color(220,200,180) );
          add(buttonPanel, BorderLayout.SOUTH);
          
          hit.addActionListener(board);
          hit.setBackground(Color.lightGray);
          buttonPanel.add(hit);
          
          stand.addActionListener(board);
          stand.setBackground(Color.lightGray);
          buttonPanel.add(stand);
          
          newGame.addActionListener(board);
          newGame.setBackground(Color.lightGray);
          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);
       }
    
       class BJCanvas extends Canvas implements ActionListener {
             // This class is NESTED in the BlackjackGUI2 class.
    
             // A 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.
          
    
          BJCanvas() {
                // 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 setGameInProgress(boolean inProgress) {
                // This method should be called whenever the value of
                // the gameInProgress variable is 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);
             }
          }
          
    
          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();
          }
          
    
          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 21 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) {
                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 paint(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.
    
             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 ]