Solution for
Programming Exercise 6.8
THIS PAGE DISCUSSES ONE POSSIBLE SOLUTION to the following exercise from this on-line Java textbook.
Exercise 6.8:Write a Blackjack applet that lets the user play a game of Blackjack, with the computer as the dealer. The applet should draw the user's cards and the dealer's cards, just as was done for the graphical HighLow card game in Section 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 applet and the Blackjack applet are very similar. You will certainly want to use the drawCard() method from that applet.
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 applet, it means that you only have to allow room for five cards. You should assume that your applet is just wide enough to show five cards, and that it is tall enough to 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 are 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, BlackjackHand.java, and Deck.java. Here is a working version of the applet:
Discussion
The main applet class for this exercise is identical to the applet class for the HighLowGUI game, except that it uses a BlackjackCavas instead of a HighLowCanvas. So, the real work of this program is writing the BlackjackCanvas 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 paint() 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 paint() method checks the state when it draws the applet. If the game is over, the card is face up. This is nice example of state-machine thinking.
Note that writing the paint() 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 applet, 466, allows for five 80-pixel cards, six 10-pixel gaps, and two 3-pixel borders along the edges: 5*80 + 6*10 + 2*3 = 466.) 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 paint() 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 290 pixels for the canvas. I set the overall height of the applet to 346 to allow 6 pixels for a border and 50 pixels for the panel that contains the buttons. (This might be too much, but the sizes of buttons can vary from one platform to another, and I want to be safe.)
In this GUI version of Blackjack, things happen when the user clicks the "Hit", "Stand", and "New Game" buttons. The applet 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.
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, 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 BlackjackCanvas class. This sets up the first game, when the applet 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 again 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 gameInProgrss 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 applet is repainted to show the final state of the game.
The Solution
/* 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. This file defines two classes, the applet class BlackjackGUI and a class BlackjackCanvas which is used in the applet. (This is slightly bad style, but is OK since the BlackjackCanvas class is not public and is not used outside this file.) */ import java.awt.*; import java.awt.event.*; import java.applet.*; public class BlackjackGUI extends Applet { public void init() { // The init() method lays out the applet using a BorderLayout. // A BlackjackCanvas occupies the CENTER position of the layout. // On the bottom is a panel that holds three buttons. The // HighLowCanvas object listens for ActionEvents from the buttons // and does all the real work of the program. setBackground( new Color(130,50,40) ); setLayout( new BorderLayout(3,3) ); BlackjackCanvas board = new BlackjackCanvas(); add(board, BorderLayout.CENTER); Panel buttonPanel = new Panel(); buttonPanel.setBackground( new Color(220,200,180) ); add(buttonPanel, BorderLayout.SOUTH); Button hit = new Button( "Hit!" ); hit.addActionListener(board); hit.setBackground(Color.lightGray); buttonPanel.add(hit); Button stand = new Button( "Stand!" ); stand.addActionListener(board); stand.setBackground(Color.lightGray); buttonPanel.add(lower); Button newGame = new Button( "New Game" ); 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); } } // end class HighLowGUI class BlackjackCanvas extends Canvas implements ActionListener { // 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. 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(); } 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) { 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(); } 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; } 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(); } 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."; 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(); 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 class BlackjackCanvas
[ Exercises | Chapter Index | Main Index ]