Solution for Programming Exercise 6.11
This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.
Exercise 6.11:
In the Blackjack game from Exercise 6.10, 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. I strongly advise writing a method that can be called every time it is necessary to set the value of the gameInProgress variable. That method can take full responsibility for enabling and disabling the buttons (as long as it is used consistently). Recall that if bttn is a variable of type Button, then bttn.setDisable(true) disables the button and bttn.setDisable(false) enables the button.
As a second (and more difficult) improvement, make it possible for the user to place bets on the Blackjack game. When the program starts, give the user $100. Add a TextField to the strip of controls along the bottom of the panel. The user enters the bet in this TextField. When the game begins, check the amount of the bet. You should do this when the game begins, not when it ends, because several errors can occur: The contents of the TextField might not be a legal number, the bet that the user places might be more money than the user has, or the bet might be <= 0. You should detect these errors and show an error message instead of starting the game. The user's bet should be an integral number of dollars.
It would be a good idea to make the TextField uneditable while the game is in progress. If betInput is the TextField, you can make it editable and uneditable by the user with the commands betInput.setEditable(true) and betInput.setEditable(false).
In the drawBoard() method, you should include commands to display the amount of money that the user has left.
There is one other thing to think about: Ideally, the program should not start a new game when it is first created. The user should have a chance to set a bet amount before the game starts. So, in the start() method, you should not call doNewGame(). You might want to display a message such as "Welcome to Blackjack" before the first game starts.
Here is a picture of my program:
First, I want to briefly discuss the changes that I made to the appearance of the user interface, compared to the program, HighLowGUI.java, on which the Blackjack programs are based.
For Blackjack, I added a dark red border around the BorderPane that serves as the root node of the scene graph. I also wanted a red line between the canvas and the buttonBar that is in the bottom position. For that, I added a border to buttonBar, but only on its top edge; the border width on the other three sides of the buttonBar is set to zero.
In HighLowGUI, the three buttons are all the same size, and they fill the buttonBar. In the final version of Blackjack, the button bar contains five components, and it didn't look good to make them all the same width. Furthermore, I wanted to give a nicer background color (beige) to the button bar, and it needed some padding to add some space between the components and the edges of the button bar. Finally, to put the components that it contains in the center of the button bar rather than at the left end, the alignment property of the button bar is set to Pos.CENTER. So, in my Blackjack program, the root node and button bar are created and configured in the start() method as follows:
HBox buttonBar = new HBox(6, hitButton, standButton, newGameButton, new Label(" Your bet:"), betInput); 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);
All of these details might seem very complicated, but it does get easier with practice!
The buttons in the program must be enabled and disabled whenever the value of the variable gameInProgress changes. At the same time, the text field should be made editable or non-editable. As recommended in the exercise, I wrote a method for changing the value of gameInProgress. This method also sets the buttons and text field to reflect the state of the program, which allows the state of the buttons and text field to be controlled in one location:
/** * This method is called whenever the value of the gameInProgress * property has to be changed. In addition to setting the value * of the gameInProgress variable, it also enables and disables * the buttons and text input box to reflect the state of the game. * @param inProgress The new value of gameInProgress. */ private void setGameInProgress( boolean inProgress ) { gameInProgress = inProgress; if (gameInProgress) { hitButton.setDisable(false); standButton.setDisable(false); newGameButton.setDisable(true); betInput.setEditable(false); hitButton.requestFocus(); } else { hitButton.setDisable(true); standButton.setDisable(true); newGameButton.setDisable(false); betInput.setEditable(true); newGameButton.requestFocus(); } }
Note that this method requests that the "Hit" button get focus of there is a game in progress, and that the "New Game" button get focus if not. When a button has the input focus, the user can trigger the button by pressing the space bar (or the Enter key). The idea is to give the input focus to the button that represents the user's most likely next action. This makes it possible for the user to play the game largely by hitting the space bar, instead of using the mouse. (In fact, since the TAB key can be used to move the input focus, the user can play the game without ever touching the mouse.)
Once setGameInProgress() 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. Note that I added a call to setGameInProgress(false) to the start() method as well, to make sure the buttons and textfield are properly configured at the beginning of the program. Since the new program does not immediately start the first game, gameInProgress should be set to false in the start() method. (Also, the call to doNewGame() in the start() method was deleted).
You should understand why I use a subroutine to set the value of gameInProgress. Every time gameInProgress changes, each of the buttons has to be enabled or disabled and the text field has to be made editable or uneditable. That's four 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 and text field will always be set correctly to match the value of gameInProgress.
The changes that I've discussed so far are enough to complete the first part of the exercise, enabling and disabling the buttons. We still have to implement betting.
I added several new instance variables to the program to implement betting: a TextField, betInput, where the user inputs the bet amount; an int variable, usersMoney, to hold the number of dollars that the user currently has; and another int variable, betAmount, to store the amount of the user's bet while a game is in progress.
The TextField for the user's input is created with the command "betInput = new TextField("10");". The parameter in the constructor specifies the initial content of the text input box. This is meant as a reasonable value for the bet, but the user can change it if she wants to. The number of preferred columns for the textfield is reduced by calling betInput.setPrefColumnCount(5). The preferred size of the TextField is computed based on the preferred column count. Without this change, the textfield would be too big for this program.
The value of usersMoney is initialized to 100 when it is declared. Any time a game ends, if the user wins, then betAmount is added to the user's money, and if the user loses, then betAmount is subtracted from the user's money. We have to decide what happens if the user runs out of money. One possibility would be to shut the game down, but that seems drastic since it's only play money anyway. So, if the value of usersMoney drops to zero, I give the user another $100 by setting the value of usersMoney back to 100; this is done in doNewGame() at the start of the next game.
At the beginning of a game in doNewGame(), the program has to look at the number in the text field to determine how much money the user wants to bet on the game. Several things can go wrong at this time: The text in the box might not be a legal integer, the amount might be more money than the user has, and the value might be less than or equal to zero. Each of these possibilities has to be tested, and, if an error is detected, an error message has to be shown to the user and no new game is started. It's a little complicated:
try { // get the amount of the user's bet and check for errors betAmount = Integer.parseInt(betInput.getText()); } catch (NumberFormatException e) { message = "Bet amount must be an integer!"; betInput.requestFocus(); betInput.selectAll(); drawBoard(); return; } if (betAmount > usersMoney) { message = "The bet amount can't be more than you have!"; betInput.requestFocus(); betInput.selectAll(); drawBoard(); return; } if (betAmount <= 0) { message = "The bet has to be a positive number"; betInput.requestFocus(); betInput.selectAll(); drawBoard(); return; }
If the program gets past all of these tests, it starts the game as it did in the old program.
When the game ends for any reason, the user's money has to be adjusted. There are many points in the source code where the game ends. In each of those places, I inserted a line "usersMoney = usersMoney + betAmount" if the user won or "usersMoney = usersMoney - betAmount" if the user lost. It might have been a good idea to write another subroutine to handle this task.
I also added some code to the drawBoard() method to display the user's current amount of money. To accommodate this, this version of the panel is 25 pixels taller than the previous version.
If the first game has not started, drawBoard() draws a welcome message instead of showing the user's and dealer's hands. There is the question of how to test whether the first game has started. The obvious (and perhaps best) way to handle that would have been to add another boolean state variable such as firstGameHasStarted. However, I used the fact that dealerHand is null before the first game starts (and only then). So drawBoard() can test whether dealerHand is null to decide whether to draw the welcome message.
Here is the new version of the Blackjack source code, with changes from Exercise 6.10 shown in red:
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.control.Label; import javafx.scene.control.TextField; 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. The user can place bets. * At the beginning of the game, the user is given $100. * * This program depends on the following classes: Card, Hand, * BlackjackHand, Deck. It also requires the image resource * file cards.png. */ public class BlackjackGUI2 extends Application { public static void main(String[] args) { launch(); } //--------------------------------------------------------------------- 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 Button hitButton, standButton, newGameButton; private TextField betInput; // An input box for the user's bet amount. 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. private int usersMoney = 100; // How much money the user currently has. private int betAmount; // The amount the use bet on the current game, // when a game is in progress. /** * The start() method() sets up the GUI and event handling. */ public void start(Stage stage) { cardImages = new Image("cards.png"); board = new Canvas(515, 415); // space for 5 cards across and 2 cards down, // with 20-pixel spaces between cards, // plus space for messages hitButton = new Button( "Hit!" ); hitButton.setOnAction( e -> doHit() ); standButton = new Button( "Stand!" ); standButton.setOnAction( e -> doStand() ); newGameButton = new Button( "New Game" ); newGameButton.setOnAction( e -> doNewGame() ); betInput = new TextField("10"); betInput.setPrefColumnCount(5); HBox buttonBar = new HBox(6, hitButton, standButton, newGameButton, new Label(" Your bet:"), betInput); 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); setGameInProgress(false); drawBoard(); Scene scene = new Scene(root); stage.setScene(scene); stage.setTitle("Blackjack"); stage.setResizable(false); stage.show(); } // end start() /** * This method is called whenever the value of the gameInProgress * property has to be changed. In addition to setting the value * of the gameInProgress variable, it also enables and disables * the buttons and text input box to reflect the state of the game. * @param inProgress The new value of gameInProgress. */ private void setGameInProgress( boolean inProgress ) { gameInProgress = inProgress; if (gameInProgress) { hitButton.setDisable(false); standButton.setDisable(false); newGameButton.setDisable(true); betInput.setEditable(false); hitButton.requestFocus(); } else { hitButton.setDisable(true); standButton.setDisable(true); newGameButton.setDisable(false); betInput.setEditable(true); newGameButton.requestFocus(); } } /** * 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) { // Should be impossible, since the Hit button // is disabled when it is not legal to use it. message = "Click \"New Game\" to start a new game."; drawBoard(); return; } playerHand.addCard( deck.dealCard() ); if ( playerHand.getBlackjackValue() > 21 ) { usersMoney = usersMoney - betAmount; message = "You've busted! Sorry, you lose."; setGameInProgress(false); } else if (playerHand.getCardCount() == 5) { usersMoney = usersMoney + betAmount; message = "You win by taking 5 cards without going over 21."; setGameInProgress(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) { // Should be impossible, since the Stand button // is disabled when it is not legal to use it. message = "Click \"New Game\" to start a new game."; drawBoard(); return; } setGameInProgress(false); while (dealerHand.getBlackjackValue() <= 16 && dealerHand.getCardCount() < 5) dealerHand.addCard( deck.dealCard() ); if (dealerHand.getBlackjackValue() > 21) { usersMoney = usersMoney + betAmount; message = "You win! Dealer has busted with " + dealerHand.getBlackjackValue() + "."; } else if (dealerHand.getCardCount() == 5) { usersMoney = usersMoney - betAmount; message = "Sorry, you lose. Dealer took 5 cards without going over 21."; } else if (dealerHand.getBlackjackValue() > playerHand.getBlackjackValue()) { usersMoney = usersMoney - betAmount; message = "Sorry, you lose, " + dealerHand.getBlackjackValue() + " to " + playerHand.getBlackjackValue() + "."; } else if (dealerHand.getBlackjackValue() == playerHand.getBlackjackValue()) { usersMoney = usersMoney - betAmount; message = "Sorry, you lose. Dealer wins on a tie."; } else { usersMoney = usersMoney + betAmount; 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. Should be impossible, since // the New Game button is disabled when it is not legal to use it. message = "You still have to finish this game!"; drawBoard(); return; } if (usersMoney == 0) { // User is broke; give the user another $100. usersMoney = 100; } try { // get the amount of the user's bet and check for errors betAmount = Integer.parseInt(betInput.getText()); } catch (NumberFormatException e) { message = "Bet amount must be an integer!"; betInput.requestFocus(); betInput.selectAll(); drawBoard(); return; } if (betAmount > usersMoney) { message = "The bet amount can't be more than you have!"; betInput.requestFocus(); betInput.selectAll(); drawBoard(); return; } if (betAmount <= 0) { message = "The bet has to be a positive number"; betInput.requestFocus(); betInput.selectAll(); 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."; usersMoney = usersMoney - betAmount; setGameInProgress(false); } else if (playerHand.getBlackjackValue() == 21) { message = "You win! You have Blackjack."; usersMoney = usersMoney + betAmount; setGameInProgress(false); } else { message = "You have " + playerHand.getBlackjackValue() + ". Hit or stand?"; setGameInProgress(true); } drawBoard(); } // end newGame(); /** * The drawBoard() method shows the messages at the bottom of the * canvas, and it draws all of the dealt cards spread out * across the canvas. If the first game has not started, it shows * a welcome message instead of the cards. */ public void drawBoard() { GraphicsContext g = board.getGraphicsContext2D(); g.setFill( Color.DARKGREEN); g.fillRect(0,0,board.getWidth(),board.getHeight()); g.setFont( Font.font(16) ); // Draw a message telling how much money the user has. g.setFill(Color.YELLOW); if (usersMoney > 0) { g.fillText("You have $" + usersMoney, 20, board.getHeight() - 45); } else { g.fillText("YOU ARE BROKE! (I will give you another $100.)", 20, board.getHeight() - 45 ); usersMoney = 100; } g.setFill( Color.rgb(220,255,220) ); if (dealerHand == null) { // The first game has not yet started. // Draw a welcome message and return. g.setFont( Font.font(30) ); g.fillText("Welcome to Blackjack!\nPlace your bet and\nclick \"New Game\".", 40,80); return; } // 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 BlackjackGUI2