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.1. 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 program and the Blackjack program 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 make the canvas just wide enough to show five cards, and tall enough to show both 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. It will also need the images file cards.png, which contains pictures of the cards.
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. (Some aesthetic changes to the GUI were made in that Blackjack program, compared to the HighLow program.)
The start() method for this exercise can be very similar to that in the HighLow game. Aside from some tweaks to appearance, the canvas has to be bigger and the text of the buttons just has to be changed from "Higher" and "Lower" to "Hit" and "Stand". (The tweaks are discussed in the solution to the next exercise; they are not really needed in this version of Blackjack.)
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 of cards 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.
There is a drawBoard() method that completely redraws the canvas, based on the current state of the game. It 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 drawBoard() method checks the state when it draws the canvas. 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 drawBoard() method required some calculation. The cards are 79 pixels wide and 123 pixels tall. Horizontally, there is a gap of 20 pixels between cards, and there are gaps of 20 pixels between the cards and the left and right edges. The total width needed for the canvas, 515, allows for five 79-pixel cards and six 20-pixel gaps: 5*79 + 6*20 = 515. The N-th card, counting from 0, has its left edge at 20+99*N. It might be easier to see this as 20+79*N+20*N, 20 pixels on the left plus N 79-pixel cards, plus a 20-pixel gap after each of the N cards. The vertical placement of the cards and strings was more experimental. I placed the dealer's hand at y-coordinate 40 and the player's hand at y-coordinate 206, leaving space to draw the strings "Dealer's Cards" and "Your Cards" above the hands. The message to the user is placed with its baseline 20 pixels above the bottom of the canvas, and I adjusted the height of the canvas to get the spacing right.
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 methods doHit(), doStand(), and doNewGame(). Each of these methods has responsibility for one part of the game of Blackjack. Note that each method 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 drawBoard() 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. It has to happen immediately after the first two cards are dealt. If one of the players has Blackjack, the game is over as soon as it starts, so gameInProgress 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 start() method. This sets up the first game, 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 canvas is redrawn to show the final state of the game.
import javafx.application.Application; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.image.Image; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.paint.Color; import javafx.scene.text.Font; /** * 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 program depends on the following classes: Card, Hand, * BlackjackHand, Deck. It also requires the image resource * file cards.png. */ public class BlackjackGUI extends Application { public static void main(String[] args) { launch(args); } //--------------------------------------------------------------------- private Deck deck; // A deck of cards to be used in the game. private BlackjackHand dealerHand; // Hand containing the dealer's cards. private BlackjackHand playerHand; // Hand containing the user's cards. private String message; // A message drawn on the canvas, which changes // to reflect the state of the game. private boolean gameInProgress; // Set to true when a game begins and to false // when the game ends. private Canvas board; // The canvas were cards and messages are displayed. private Image cardImages; // The image that contains all the cards in a deck. /** * The start() method() sets up the GUI and event handling. */ public void start(Stage stage) { cardImages = new Image("cards.png"); board = new Canvas(515, 390); // space for 5 cards across and 2 cards down, // with 20-pixel spaces between cards, // plus space for messages Button hitButton = new Button( "Hit!" ); hitButton.setOnAction( e -> doHit() ); Button standButton = new Button( "Stand!" ); standButton.setOnAction( e -> doStand() ); Button newGameButton = new Button( "New Game" ); newGameButton.setOnAction( e -> doNewGame() ); HBox buttonBar = new HBox(6,hitButton,standButton,newGameButton); buttonBar.setStyle("-fx-border-color: darkred; -fx-border-width: 3px 0 0 0;" + "-fx-padding: 8px; -fx-background-color:beige"); buttonBar.setAlignment(Pos.CENTER); BorderPane root = new BorderPane(); root.setStyle("-fx-border-color: darkred; -fx-border-width: 3px"); root.setCenter(board); root.setBottom(buttonBar); doNewGame(); // Start the first game. Scene scene = new Scene(root); stage.setScene(scene); stage.setTitle("Blackjack"); stage.setResizable(false); stage.show(); } // end start() /** * 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."; drawBoard(); 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?"; } drawBoard(); } /** * 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."; drawBoard(); 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() + "!"; drawBoard(); } /** * Called by the constructor, and called by doNewGame(). 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!"; drawBoard(); 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; } drawBoard(); } // end newGame(); /** * The drawBoard() 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 drawBoard() { GraphicsContext g = board.getGraphicsContext2D(); g.setFill( Color.DARKGREEN); g.fillRect(0,0,board.getWidth(),board.getHeight()); g.setFont( Font.font(16) ); g.setFill( Color.rgb(220,255,220) ); // Draw the message at the bottom of the canvas. g.fillText(message, 20, board.getHeight() - 20); // Draw labels for the two sets of cards. g.fillText("Dealer's Cards:", 20, 27); g.fillText("Your Cards:", 20, 190); // Draw dealer's cards. Draw first card face down if // the game is still in progress, It will be revealed // when the game ends. if (gameInProgress) drawCard(g, null, 20, 40); else drawCard(g, dealerHand.getCard(0), 20, 40); for (int i = 1; i < dealerHand.getCardCount(); i++) drawCard(g, dealerHand.getCard(i), 20 + i * 99, 40); // Draw the user's cards. for (int i = 0; i < playerHand.getCardCount(); i++) drawCard(g, playerHand.getCard(i), 20 + i * 99, 206); } // end drawBoard(); /** * Draws a card with top-left corner at (x,y). If card is null, * then a face-down card is drawn. The cards images are from * the file cards.png; this program will fail without it. */ private void drawCard(GraphicsContext g, Card card, int x, int y) { int cardRow, cardCol; if (card == null) { cardRow = 4; // row and column of a face down card cardCol = 2; } else { cardRow = 3 - card.getSuit(); cardCol = card.getValue() - 1; } double sx,sy; // top left corner of source rect for card in cardImages sx = 79 * cardCol; sy = 123 * cardRow; g.drawImage( cardImages, sx,sy,79,123, x,y,79,123 ); } // end drawCard() } // end class BlackjackGUI