AP Computer Science Object Oriented Blackjack
Introduction
Refactoring is a fancy name used in objected oriented programming for restructuring and redesigning code to make it more readable or more elegant or just easier to understand. In the case of the Blackjack Program example I purposely designed it to use very little object orientedness (if that's even a word). Now it's time to refactor the Blackjack code to take advantage of object oriented programming. I find the new code much more readable (hopefully you will as well). More importantly the new code structure will allow us to go beyond AP Computer Science constraints and turn this into a graphical card game. That will be the subject of another article.
What objects to make?
Blackjack object
The BlackJack object will be the place where the main routine resides. I used the style of object oriented programming I proposed in the previous "hello world". The main routine will instantiate a BlackJack object and call the 'run' method. All the program logic will be in the 'run' method. This may seem like a bit of overkill but what if you wanted to run a BlackJack server? Each player that logs in needs their own BlackJack game. Each game needs to run in a separate thread of execution (threading is a way of portioning off processor access so that objects do not have to wait for other objects to execute). While that may still seem like overkill if you pursue Computer Science as a major you will ultimately come across this 'pattern' of program structure. From my point of view this structure is easy to implement has no effect on performance is pure object oriented programming so why not.
PlayingCard object
When you grab a real deck of cards you will notice there are 52 objects with a back cover design and a face design. This is a natural object that we can model as an object in our program.
CardDeck object
The CardDeck object is the object model for a deck of cards. In computer terms the deck of cards is a 'container' for 'PlayingCard's.
Hand object
The Hand object is the object model of a set of cards. In the original program these were arrays with code for tracking the dealer hand and code for tracking the player hand. The Hand object greatly simplifies the code. Separate tracking is done by instantiating 2 'Hand' objects. A player 'Hand' and a dealer 'Hand'.
Unified Modeling Language
Many Computer Scientists are very poor artists. But sometimes a picture really is worth 1000 words. To standardize the drawing of object relationships and provide computer tools to assist the artistically challenge Unified Modeling Language was developed. The Object diagram is a box with the name of the Object (ie. Class) in the title of the box (located at the top of the box). Following the title are fields and methods listed below the title. Arrows can be drawn between the class objects to signify relationships and usage. You have probably seen some of these diagrams already while studying this topic.
The following represents the class objects created for the Blackjack program:
The above diagram represents the all the classes created to make the new BlackJack program. In the code that follows I have commented out the old code I used and tried to highlight the new code. It makes the code a little longer but it will give you an idea of the refactoring process.
The only thing that didn't fit well into the model is the scoring of the hands. This needs a little work. Even though this has been about BlackJack you would like to reuse the classes for other card games. Hands would be scored differently for different games. There is an object oriented way to handle this but it is a little beyond the scope of this article and level of programming. For now it's hard coded into the PlayingCard object so it's only good for BlackJack and you would have to adapt that class for a different type of game.
The Code
BlackJack Class Code
/* * BlackJack.java - main class that performs the black jack game */ package BlackJack; import java.util.Scanner; /** * * @author Nasty Old Dog */ public class BlackJack { int card_score[] = {11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10}; CardDeck card; Hand player; Hand dealer; public BlackJack() { this.card = new CardDeck(); } public void run() { // CardDeck card = new CardDeck(); this.card.display(); this.card.shuffle(); this.card.display(); // This is the old code to test hands /* card.dealPlayer(); card.dealDealer(); card.dealPlayer(); card.dealDealer(); card.displayHands(); System.out.println("Player score = " + card.scoreHand(card.playerHand, card.pcards)); System.out.println("Dealer score = " + card.scoreHand(card.dealerHand, card.dcards)); // Reset hands card.resetHands(); */ // Now I just need to create the hand objects this.player = new Hand("Player Hand"); this.dealer = new Hand("Dealer Hand"); this.player.deal(card); this.dealer.deal(card); this.player.deal(card); this.dealer.deal(card); this.player.display(); this.dealer.display(); System.out.println("Player score = " + player.scoreHand()); System.out.println("Dealer score = " + dealer.scoreHand()); // Now to reset hands just create new ones this.player = new Hand("Player Hand"); this.dealer = new Hand("Dealer Hand"); String user_inp = " "; int bankroll = 1000; int bet = 0; // if the user bets 0 quit the game loop // Need a Scanner for user input Scanner uinp = new Scanner(System.in); // game loop just loop forever while (true) { // Tell the user the amount of the bank roll System.out.println("Bankroll = " + bankroll); // get bet System.out.print("Enter the amount you want to bet: "); // user_inp = uinp.nextLine(); // bet = Integer.getInteger(user_inp).intValue(); bet = uinp.nextInt(); uinp.nextLine(); // bet == 0 is the signal to quit loop and end program if (bet == 0) { break; } this.card.shuffle(); // Old way /* card.dealPlayer(); card.dealDealer(); card.dealPlayer(); card.dealDealer(); card.displayHands(); */ this.player.deal(card); this.dealer.deal(card); this.player.deal(card); this.dealer.deal(card); this.player.display(); this.dealer.display(); // Need a loop to handle HIT or STAND commands boolean bust = false; // Old:int scr = scr = card.scoreHand(card.playerHand, card.pcards); // New: int scr = this.player.scoreHand(); while (scr != 21) { System.out.print("Player Hand hit or stand? "); user_inp = uinp.nextLine(); String firstch = user_inp.substring(0, 1); if (firstch.equals("H") || firstch.equals("h")) { // Old /* card.dealPlayer(); card.displayHands(); scr = card.scoreHand(card.playerHand, card.pcards); * */ // New: this.player.deal(card); this.player.display(); scr = this.player.scoreHand(); if (scr > 21) { // Player has gone bust bust = true; break; } // Check if player at 21 and stop asking questions if (scr == 21) { break; } } else if (firstch.equals("S") || firstch.equals("s")) { break; } else { // If we end up here it's because of a typo or wiseguy // print error response and go back to looping System.out.println("I don't understand: " + user_inp); } } if (bust) { System.out.println("BUST!! LOSER!!"); // deduct bet from bankroll bankroll -= bet; // reset hands for next game // Old: card.resetHands(); // New: this.player = new Hand("Player Hand"); this.dealer = new Hand("Dealer Hand"); continue; // go back to top of main loop and start new game } // Manually control dealer hand the same way // We will swap this out for code that will run the dealer rules // automatically // Old: int dscr = card.scoreHand(card.dealerHand, card.dcards); // New: int dscr = dealer.scoreHand(); while (true) { if (dscr < 17) { System.out.println("Dealer Hand 16 or less: Must HIT"); // Old: /* card.dealDealer(); card.displayHands(); dscr = card.scoreHand(card.dealerHand, card.dcards); * */ this.dealer.deal(card); this.player.display(); this.dealer.display(); dscr = dealer.scoreHand(); } else { break; } } // Code to score the game and settle the bet will go here // For now assume we win every time System.out.println("Player score: " + scr + " Dealer: " + dscr); if (scr > dscr) { System.out.println("You Win!!!"); bankroll = bankroll + bet; } else if (dscr > 21) { System.out.println("You Win!! Dealer has BUSTED"); bankroll += bet; } else if (scr == dscr) { System.out.println("PUSH"); } else { System.out.println("LOSER!!!"); bankroll -= bet; } // reset hands to play a new game this.player = new Hand("Player Hand"); this.dealer = new Hand("Dealer Hand"); //Old: card.resetHands(); } } public static void main(String[] args) { BlackJack game = new BlackJack(); game.run(); } }
CardDeck Class Code
/* * CardDeck.java - Greatly simplified version of a deck of cards. Cards are * represented internally as a number from 0 - 51 for a total of 52 cards * The class just deals with standard operations you would do with a deck: * - deal * - shuffle * - display * - a constructor to initialize the deck */ package BlackJack; /** * * @author Nasty Old Dog */ public class CardDeck { PlayingCard deck[] = new PlayingCard[52]; int deckidx = 0; // the location of the next card to be dealt public CardDeck() { for (int i = 0; i < 52; i++) { deck[i] = new PlayingCard(i); } } void shuffle() { int rindex; PlayingCard swap; for (int i = 0; i < 52; i++) { // each time through the loop there are less numbers to randomize // 52 - i to be exact. But then everything from 0 to i-1 has already // been selected at random so add i to the random number to get the // appropriate index rindex = (int) ((Math.random() * ((double) (52 - i))) + i); swap = deck[i]; deck[i] = deck[rindex]; deck[rindex] = swap; } } void display() { int card_val; int card_suit; for (int i = 0; i < 52; i++) { //card_val = deck[i]%13; //card_suit = deck[i]%4; System.out.print(this.deck[i].getCardText() + " "); } System.out.println(); } //Old /* public PlayingCard deal() { if (this.deckidx < 52) { return this.deck[this.deckidx++]; // return the top card and increment to the next card } else { return this.deck[this.deckidx - 1]; } } */ //New: when we get to the end of the deck lets just reshuffle and keep // going public PlayingCard deal() { if (this.deckidx >= 52) { deckidx = 0; this.shuffle(); } return this.deck[this.deckidx++]; } }
Hand Class Code
/* * Hand.java - a class that models a hand of cards as one would expect in * any card game. For the purposes of the computer handling things the * following methods are implemented * - display * - sortHand (private) * - scoreHand * */ package BlackJack; import java.util.ArrayList; /** * * @author Nasty Old Dog */ public class Hand { ArrayList<PlayingCard> cards = new ArrayList<PlayingCard>(); ArrayList<PlayingCard> sorted = new ArrayList<PlayingCard>(); String displayName = "change in constructor"; // Used to need the following in CardDeck to track this /* int playerHand[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; int dealerHand[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; int dcards = 0; // the number of cards in the dealer hand so far int pcards = 0; // the number of cards in the player hand so far */ public Hand() { this.displayName = "Hand"; } public Hand(String displayName) { this.displayName = displayName; } public void deal(CardDeck deck) { this.cards.add(deck.deal()); } // All the commented code was needed because we did not have a // good object model for how to handle Hands /* public void dealPlayer() { this.playerHand[pcards] = deal(); this.pcards++; } public void dealDealer() { this.dealerHand[dcards] = deal(); this.dcards++; } public void resetHands() { for (int i = 0; i < playerHand.length; i++) { playerHand[i] = 0; dealerHand[i] = 0; } dcards = 0; pcards = 0; } */ private void sortHand() { // sort hand make Aces 11 for sorting purposes // Use the insertion sort code and modify it by using the card_value // method as the way to sort this.sorted = (ArrayList<PlayingCard>) this.cards.clone(); // The clone statement does a deep copy of the array list // Just to prove it I print out the sorted field individually // If you had to copy individually (like for the AP) the code would // follow the structure below System.out.print("Cloned: "); for (PlayingCard i : this.sorted) System.out.print(i.getCardText() + " "); System.out.print("Size: " + this.sorted.size()); System.out.println(); PlayingCard tmp; for (int i = 1; i < this.sorted.size(); i++) { for (int j = i; j > 0 && this.sorted.get(j).card_value() < this.sorted.get(j-1).card_value(); j--) { // swap values tmp = this.sorted.get(j); this.sorted.set(j, this.sorted.get(j - 1)); this.sorted.set(j - 1, tmp); } } } public int scoreHand() { // No longer need this it is handled by keeping a sorted field /* int tmp_hnd[] = new int[size]; // Copy hand into a new array to play with without disturbing the // original hand for (int i = 0; i < size; i++) { tmp_hnd[i] = hnd[i]; } * */ this.sortHand(); int score = 0; for (PlayingCard i : this.sorted) { // Need to test each card to see if it is an ACE // If an ACE and score is < 11 use 11 as ACE value otherwise use 1 // I haven't done a proof but I believe that checking the score // less than 11 is sufficient to handle ACEs and you won't bust a // hand incorrectly if (i.card_value() == 11) { // Processing ACE if (score >= 11) { // ACEs must all be 1 score += 1; } else { score += 11; } } else { // Just add card_value it's not an ACE score += i.card_value(); } } return score; } public void display() { System.out.println(this.displayName); for (PlayingCard i : cards) { System.out.print(i.getCardText() + " "); // The commented code was necesary because hands were not objects // Now a hand knows how to display itself /* System.out.println("Player Hand "); for (int i = 0; i < this.pcards; i++) { System.out.print(this.getCardText(this.playerHand[i]) + " "); } System.out.println(); System.out.println("Dealer Hand: "); for (int j = 0; j < this.dcards; j++) { System.out.print(this.getCardText(this.dealerHand[j]) + " "); } */ } System.out.println(); } }
PlayingCard Class Code
/* * PlayingCard.java - Playing card class for BlackJack */ package BlackJack; /** * * @author Nasty Old Dog */ public class PlayingCard { private int cardno; private int card_score[] = {11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10}; //private int card_score[] = {1,2,3,4,5,6,7,8,9,10,10,10,10}; private String faceVal = "A23456789TJQK"; private String suit = "HDSC"; public PlayingCard(int cardno) { this.cardno = cardno; } public PlayingCard(int cardno, int[] card_score) { this.cardno = cardno; this.card_score = card_score; } public int card_value() { return card_score[cardno % 13]; } public String getCardText() { int card_val = this.cardno % 13; int card_suit = this.cardno % 4; return this.faceVal.substring(card_val, card_val + 1) + this.suit.substring(card_suit, card_suit + 1); } }
Output 1
run: AH 2D 3S 4C 5H 6D 7S 8C 9H TD JS QC KH AD 2S 3C 4H 5D 6S 7C 8H 9D TS JC QH KD AS 2C 3H 4D 5S 6C 7H 8D 9S TC JH QD KS AC 2H 3D 4S 5C 6H 7D 8S 9C TH JD QS KC 8H AC AH 3D KC 2H 5C KH 7S 5H 7H 9C 2D 8C KD JD 5D 7C 6C 3S QD AS 4S 8S 3H 4C AD 6D 5S TS 9H 6H JS 6S JC 9D 8D TD 2C JH 4D 4H TH 7D QS QC QH TC 2S 9S KS 3C Player Hand 8H AH Dealer Hand AC 3D Cloned: 8H AH Size: 2 Player score = 19 Cloned: AC 3D Size: 2 Dealer score = 14 Bankroll = 1000 Enter the amount you want to bet: 100 Player Hand TS 8H Dealer Hand 9C KH Cloned: TS 8H Size: 2 Player Hand hit or stand? h Player Hand TS 8H 5D Cloned: TS 8H 5D Size: 3 BUST!! LOSER!! Bankroll = 900 Enter the amount you want to bet: 100 Player Hand 5D TC Dealer Hand 7D AS Cloned: 5D TC Size: 2 Player Hand hit or stand? h Player Hand 5D TC QS Cloned: 5D TC QS Size: 3 BUST!! LOSER!! Bankroll = 800 Enter the amount you want to bet: 0 BUILD SUCCESSFUL (total time: 33 minutes 27 seconds)
Conclusion
Four classes:
- Main class BlackJack
- 2 that the main Class BlackJack uses
- CardDeck
- Hand
- 1 class PlayingCards that is used by Hand and CardDeck
That's all it takes to make a card game. The full code is reproduced below sans the comments from the old code. This should give you a better feel for just what the Object Oriented style is doing for us.
References
UML Diagram made with PlantUML under emacs editor
- Plant UML website http://plantuml.sourceforge.net
Full Code sans comments
/* * BlackJack.java - main class that performs the black jack game */ package BlackJack; import java.util.Scanner; /** * * @author Nasty Old Dog */ public class BlackJack { int card_score[] = {11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10}; CardDeck card; Hand player; Hand dealer; public BlackJack() { this.card = new CardDeck(); } public void run() { this.card.display(); this.card.shuffle(); this.card.display(); // Now I just need to create the hand objects this.player = new Hand("Player Hand"); this.dealer = new Hand("Dealer Hand"); this.player.deal(card); this.dealer.deal(card); this.player.deal(card); this.dealer.deal(card); this.player.display(); this.dealer.display(); System.out.println("Player score = " + player.scoreHand()); System.out.println("Dealer score = " + dealer.scoreHand()); // Now to reset hands just create new ones this.player = new Hand("Player Hand"); this.dealer = new Hand("Dealer Hand"); String user_inp = " "; int bankroll = 1000; int bet = 0; // if the user bets 0 quit the game loop // Need a Scanner for user input Scanner uinp = new Scanner(System.in); // game loop just loop forever while (true) { // Tell the user the amount of the bank roll System.out.println("Bankroll = " + bankroll); // get bet System.out.print("Enter the amount you want to bet: "); bet = uinp.nextInt(); uinp.nextLine(); // bet == 0 is the signal to quit loop and end program if (bet == 0) { break; } this.card.shuffle(); this.player.deal(card); this.dealer.deal(card); this.player.deal(card); this.dealer.deal(card); this.player.display(); this.dealer.display(); // Need a loop to handle HIT or STAND commands boolean bust = false; int scr = this.player.scoreHand(); while (scr != 21) { System.out.print("Player Hand hit or stand? "); user_inp = uinp.nextLine(); String firstch = user_inp.substring(0, 1); if (firstch.equals("H") || firstch.equals("h")) { this.player.deal(card); this.player.display(); scr = this.player.scoreHand(); if (scr > 21) { // Player has gone bust bust = true; break; } // Check if player at 21 and stop asking questions if (scr == 21) { break; } } else if (firstch.equals("S") || firstch.equals("s")) { break; } else { // If we end up here it's because of a typo or wiseguy // print error response and go back to looping System.out.println("I don't understand: " + user_inp); } } if (bust) { System.out.println("BUST!! LOSER!!"); // deduct bet from bankroll bankroll -= bet; this.player = new Hand("Player Hand"); this.dealer = new Hand("Dealer Hand"); continue; // go back to top of main loop and start new game } int dscr = dealer.scoreHand(); while (true) { if (dscr < 17) { System.out.println("Dealer Hand 16 or less: Must HIT"); this.dealer.deal(card); this.player.display(); this.dealer.display(); dscr = dealer.scoreHand(); } else { break; } } // Code to score the game and settle the bet will go here // For now assume we win every time System.out.println("Player score: " + scr + " Dealer: " + dscr); if (scr > dscr) { System.out.println("You Win!!!"); bankroll = bankroll + bet; } else if (dscr > 21) { System.out.println("You Win!! Dealer has BUSTED"); bankroll += bet; } else if (scr == dscr) { System.out.println("PUSH"); } else { System.out.println("LOSER!!!"); bankroll -= bet; } // reset hands to play a new game this.player = new Hand("Player Hand"); this.dealer = new Hand("Dealer Hand"); } } public static void main(String[] args) { BlackJack game = new BlackJack(); game.run(); } } // ----------------------------------------------------- /* * CardDeck.java - Greatly simplified version of a deck of cards. Cards are * represented internally as a number from 0 - 51 for a total of 52 cards * The class just deals with standard operations you would do with a deck: * - deal * - shuffle * - display * - a constructor to initialize the deck */ package BlackJack; /** * * @author Nasty Old Dog */ public class CardDeck { PlayingCard deck[] = new PlayingCard[52]; int deckidx = 0; // the location of the next card to be dealt public CardDeck() { for (int i = 0; i < 52; i++) { deck[i] = new PlayingCard(i); } } void shuffle() { int rindex; PlayingCard swap; for (int i = 0; i < 52; i++) { // each time through the loop there are less numbers to randomize // 52 - i to be exact. But then everything from 0 to i-1 has already // been selected at random so add i to the random number to get the // appropriate index rindex = (int) ((Math.random() * ((double) (52 - i))) + i); swap = deck[i]; deck[i] = deck[rindex]; deck[rindex] = swap; } } void display() { int card_val; int card_suit; for (int i = 0; i < 52; i++) { System.out.print(this.deck[i].getCardText() + " "); } System.out.println(); } public PlayingCard deal() { if (this.deckidx >= 52) { deckidx = 0; this.shuffle(); } return this.deck[this.deckidx++]; } } //------------------------------------------------- /* * Hand.java - a class that models a hand of cards as one would expect in * any card game. For the purposes of the computer handling things the * following methods are implemented * - display * - sortHand (private) * - scoreHand * */ package BlackJack; import java.util.ArrayList; /** * * @author Nasty Old Dog */ public class Hand { ArrayList<PlayingCard> cards = new ArrayList<PlayingCard>(); ArrayList<PlayingCard> sorted = new ArrayList<PlayingCard>(); String displayName = "change in constructor"; public Hand() { this.displayName = "Hand"; } public Hand(String displayName) { this.displayName = displayName; } public void deal(CardDeck deck) { this.cards.add(deck.deal()); } private void sortHand() { // sort hand make Aces 11 for sorting purposes // Use the insertion sort code and modify it by using the card_value // method as the way to sort this.sorted = (ArrayList<PlayingCard>) this.cards.clone(); // The clone statement does a deep copy of the array list // Just to prove it I print out the sorted field individually // If you had to copy individually (like for the AP) the code would // follow the structure below System.out.print("Cloned: "); for (PlayingCard i : this.sorted) System.out.print(i.getCardText() + " "); System.out.print("Size: " + this.sorted.size()); System.out.println(); PlayingCard tmp; for (int i = 1; i < this.sorted.size(); i++) { for (int j = i; j > 0 && this.sorted.get(j).card_value() < this.sorted.get(j-1).card_value(); j--) { // swap values tmp = this.sorted.get(j); this.sorted.set(j, this.sorted.get(j - 1)); this.sorted.set(j - 1, tmp); } } } public int scoreHand() { this.sortHand(); int score = 0; for (PlayingCard i : this.sorted) { // Need to test each card to see if it is an ACE // If an ACE and score is < 11 use 11 as ACE value otherwise use 1 // I haven't done a proof but I believe that checking the score // less than 11 is sufficient to handle ACEs and you won't bust a // hand incorrectly if (i.card_value() == 11) { // Processing ACE if (score >= 11) { // ACEs must all be 1 score += 1; } else { score += 11; } } else { // Just add card_value it's not an ACE score += i.card_value(); } } return score; } public void display() { System.out.println(this.displayName); for (PlayingCard i : cards) { System.out.print(i.getCardText() + " "); } System.out.println(); } } //--------------------------------------------------------------- /* * PlayingCard.java - Playing card class for BlackJack */ package BlackJack; /** * * @author Nasty Old Dog */ public class PlayingCard { private int cardno; private int card_score[] = {11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10}; private String faceVal = "A23456789TJQK"; private String suit = "HDSC"; public PlayingCard(int cardno) { this.cardno = cardno; } public PlayingCard(int cardno, int[] card_score) { this.cardno = cardno; this.card_score = card_score; } public int card_value() { return card_score[cardno % 13]; } public String getCardText() { int card_val = this.cardno % 13; int card_suit = this.cardno % 4; return this.faceVal.substring(card_val, card_val + 1) + this.suit.substring(card_suit, card_suit + 1); } }
No comments:
Post a Comment