Tuesday, April 16, 2013

Blackjack Scoring: Aces 1 or 11

AP Computer Science Finishing Blackjack Scoring

AP Computer Science Finishing Blackjack Scoring

Introduction

In the last version of Blackjack ACEs were scored as 11 all the time. This provided a testing platform for the sorting of hands and scoring of cards in general. However in real Blackjack ACEs can be 11 or 1 to the advantage of obtaining a winning hand. For the scoring algorithm so far all that is needed is some code to handle ACEs.

Aside from the ACE scoring problem there are a couple of small items to fix. First if you look at the prompts and scanner code. The prompts have lower case text for hit and stand but expect all capitals. They also require that you spell the whole word. It would be nice to except any of the following input:

  • h,H,hit,HIT,s,S,stand,STAND

I will cheat and look at only the first letter of the entry if it's an 'h' or 'H' the input will be a HIT, if it's 's' or 'S' it will be accepted as STAND.

Finally when the player reaches 21 the computer shouldn't ask if you want to HIT or STAND it should automatically go to the dealer code.

The new scoring algorithm

The new scoring algorithm uses the cardvalue function. When an 11 is detected that is an ACE and an if statement is executed to handle ACE scoring. I did only a minor amount of analysis but I believe that checking the score of the sorted hand when an ACE is encountered (being greater than or equal to 11) is sufficient for the proper handling of ACEs. So score of hand >= 11 ACEs are 1 otherwise the ACE is 11. The new scorehand method follows:

public int scoreHand(int[] hnd, int size) {
    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(tmp_hnd);
    int score = 0;
    for (int i : tmp_hnd) {
        // 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 (card_value(i) == 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 += card_value(i);
        }
    }
    return score;
}

The minor fixes

To cheat on the input just check the first character and test it for 'h', 'H','s','S'. It turns out that a little annoyance we left in the user input code will cause a problem when we go to run the code. But I found it after I made the changes to HIT and STAND so read on to find out the problem.

The following is the code of the player hand input loop in the main method:

while (true) {
    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")) {
        card.dealPlayer();
        card.displayHands();
        scr = card.scoreHand(card.playerHand, card.pcards);
        if (scr > 21) {
            // Player has gone bust
            bust = true;
            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);
    }
}

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 
KS 8H 6H 5S 7H KC 7C 3H 2H 7S 4H TC TD QS 3S TS 4S 8S JD TH 5D 2C 6D AC QC QH JC AS JS 9D KD 8C 5C KH 7D 3C 9S 3D 4C AD 2S 9C 4D JH AH 6S 2D 6C 9H QD 5H 8D 
Player Hand  
KS 6H 
Dealer Hand:  
8H 5S 

Player score = 16
Dealer score = 13
Bankroll = 1000
Enter the amount you want to bet: 100
Player Hand  
JS QS 
Dealer Hand:  
4S 3D 

Player Hand hit or stand? Stand
Dealer Hand 16 or less: Must HIT
Player Hand  
JS QS 
Dealer Hand:  
4S 3D JD 

Player score: 20   Dealer: 17
You Win!!!
Bankroll = 1100
Enter the amount you want to bet: 100
Player Hand  
7S TS 
Dealer Hand:  
2D AD 

Player Hand hit or stand? Hit
Player Hand  
7S TS 9S 
Dealer Hand:  
2D AD 

BUST!! LOSER!!
Bankroll = 1000
Enter the amount you want to bet: 0
BUILD SUCCESSFUL (total time: 1 minute 28 seconds)

To get the program to work I had to fix a minor problem that has been plagueing the program since we added the user input. Notice that in the output of the game the line 'I don't understand:' doesn't appear after the bet is input. It turns out that when we get the integer for the bet it doesn't eat up the whole line (this is not knowledge you need for the AP but interesting never the less). So I added a uinp.nextLine(); method call.

            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();    // **New line added to eat the rest of the input line

Indeed if you look at the commented out lines I tried to handle the input originally by grabbing the whole line and then try to parse the integer out of it. It never worked well and I didn't feel like investigating because the change to the scanner methods made the problem go away.

Stop asking silly questions!

When the player gets to 21 the computer can be smart enough to check and continue to the dealer processing without asking for HIT or STAND. An extra if statement is all that's needed, right after we check for a 'bust'.

if (firstch.equals("H") || firstch.equals("h")) {
    card.dealPlayer();
    card.displayHands();
    scr = card.scoreHand(card.playerHand, card.pcards);
    if (scr > 21) {
        // Player has gone bust
        bust = true;
        break;
    }
    // Check if player at 21 and stop asking questions
    if (scr == 21) {
        break;
    }

Output 2

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 
5S TH 3H 4H QC AH 5H 4S QH 9C 6D 5C 2S 8S KC TD TC KD 6C 4C TS JC AC AD 7D 8D 4D 9S 3S 7H 6H 9H 5D 3D 8H JS 7S 3C 2C 2H 7C JH QD 2D JD KH KS 8C AS 6S QS 9D 
Player Hand  
5S 3H 
Dealer Hand:  
TH 4H 

Player score = 8
Dealer score = 14
Bankroll = 1000
Enter the amount you want to bet: 100
Player Hand  
2H 8C 
Dealer Hand:  
QD 6D 

Player Hand hit or stand? h
Player Hand  
2H 8C 4H 
Dealer Hand:  
QD 6D 

Player Hand hit or stand? h
Player Hand  
2H 8C 4H 7H 
Dealer Hand:  
QD 6D 

Dealer Hand 16 or less: Must HIT
Player Hand  
2H 8C 4H 7H 
Dealer Hand:  
QD 6D 4C 

Player score: 21   Dealer: 20
You Win!!!
Bankroll = 1100
Enter the amount you want to bet: 0
BUILD SUCCESSFUL (total time: 37 seconds)

There you have it the computer moves right along once the player hits to 21. The only thing not checked for is if the player has blackjack from the beginning. So I changed the player input while loop to loop while scr != 21. Which makes the if statement a little redundant but if you were going to make this program more like Vegas blackjack the payout is 1.5 times for 21 dealt on first 2 cards. I'll leave it in. It may come in handy later on if you want to make this game more realistic. Look at the full code at the end for the above change.

Conclusion

The point of this exercise was to show the AP student that with only the programming skills they have learned in AP Computer Science they can put together the start of a real card game. I believe the only thing we used that is not part of the AP test is the Scanner class. Many teachers introduce this class anyway as it's an easy way to accept user input. In the AP test they will generally tell you that user input is accepted and what form it comes in, they won't expect you to know the specific code. They do expect you to understand that computers accept user input.

I will revisit this program in order to show you how I would refactor (ie. redesign) this code to make it object oriented. Hopefully you have your own ideas at this point for what I did wrong from an Object Oriented perspective. It's very close to the 2013 AP exam date so I will try to hurry this next article.

You may be wondering if I would have coded this program the same way if I were trying to make a Java based blackjack program. The answer is yes and no. I would have designed some objects in the beginning that seemed obvious. But then some of the insight into how to solve a problem comes from getting the code down and seeing how it works. If an idea comes to mind I usually try to capture it in code. Then I will refactor it into the Object Model. Understand though that an experienced Object Oriented Programmer may naturally start with a better initial object model than a less experienced programmer. In the end, however, any code that works properly and gives a seamless user experience is sufficient by definition object oriented or not.

Don't let the constraints of programming mess with your creativity. In the end the people who are really successful at this craft are the creative programmers the elegance of their coding doesn't really matter. Think about it do you care if "Angry Birds" has the most elegant object oriented design? No you play it because it's fun not because the code is elegant!

Final Code

package apcompsci;

/**
 *
 * @author Nasty Old Dog
 */
import java.util.Scanner;

public class CardDeck {

    int deck[] = new int[52];
    String faceVal = "A23456789TJQK";
    String suit = "HDSC";

    public CardDeck() {
        for (int i = 0; i < 52; i++) {
            deck[i] = i;
        }
    }

    void shuffle() {
        int rindex;
        int 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;
        }
    }

    public String getCardText(int crd) {
        int card_val = crd % 13;
        int card_suit = crd % 4;
        return this.faceVal.substring(card_val, card_val + 1)
                + this.suit.substring(card_suit, card_suit + 1);
        //return this.faceVal.substring(card_val, card_val+1).concat(
        //this.suit.substring(card_suit, card_suit+1));
    }

    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.getCardText(deck[i]) + " ");
        }
        System.out.println();
    }
    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
    int deckidx = 0; // the location of the next card to be dealt

    public int 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];
        }
    }

    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;
    }

    public int card_value(int c) {
        // I need an array of sort values 11,2,3,4,5,6,7,8,9,10,10,10,10
        int card_score[] = {11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10};
        // I will use modulo 13 again to take a card and convert to face value
        // then use that to index into the card_score array for the sort value
        return card_score[c % 13];
    }

    public void sortHand(int[] hnd) {
        // 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

        int tmp;
        for (int i = 1; i < hnd.length; i++) {
            for (int j = i;
                    j > 0
                    && this.card_value(hnd[j]) < this.card_value(hnd[j - 1]);
                    j--) {
                // swap values
                tmp = hnd[j];
                hnd[j] = hnd[j - 1];
                hnd[j - 1] = tmp;
            }
        }
    }

    public int scoreHand(int[] hnd, int size) {
        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(tmp_hnd);
        int score = 0;
        for (int i : tmp_hnd) {
            // 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 (card_value(i) == 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 += card_value(i);
            }
        }
        return score;
    }

    public void displayHands() {
        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();
        System.out.println();
    }

    public static void main(String[] args) {
        CardDeck card = new CardDeck();

        card.display();
        card.shuffle();
        card.display();

        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();

        // CardDeck card = new CardDeck();

        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;
            }

            card.shuffle();

            card.dealPlayer();
            card.dealDealer();
            card.dealPlayer();
            card.dealDealer();

            card.displayHands();

            // Need a loop to handle HIT or STAND commands
            boolean bust = false;
            int scr = scr = card.scoreHand(card.playerHand, card.pcards);
            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")) {
                    card.dealPlayer();
                    card.displayHands();
                    scr = card.scoreHand(card.playerHand, card.pcards);
                    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
                card.resetHands();
                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
            int dscr = dscr = card.scoreHand(card.dealerHand, card.dcards);
            while (true) {
                if (dscr < 17) {
                    System.out.println("Dealer Hand 16 or less: Must HIT");
                    card.dealDealer();
                    card.displayHands();
                    dscr = card.scoreHand(card.dealerHand, card.dcards);
                } 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
            card.resetHands();
        }
    }
}

Author: Nasty Old Dog

No comments:

Post a Comment