8

I am developing a poker game as college project and our current assignment is to write an algorithm to score a hand of 5 cards, so that the scores of two hands can be compared to each other to determine which is the better hand. The score of a hand has nothing to do with the probability of what hands could be made upon the draw being dealt with random cards, etc. - The score of a hand is based solely on the 5 cards in the hand, and no other cards in the deck.

The example solution we were given was to give a default score for each type of Poker hand, with the score reflecting how good the hand is - like this for instance:

//HAND TYPES:
ROYAL_FLUSH = 900000
STRAIGHT_FLUSH = 800000
...
TWO_PAIR = 200000
ONE_PAR = 100000

Then if two hands of the same type are compared, the values of the cards in the hands should be factored into the hand's score.

So for example, the following formula could be used to score a hand:

HAND_TYPE + (each card value in the hand)^(the number of occurences of that value)

So, for a Full House of three Queens and two 7s, the score would be:

600000 + 12^3 + 7^2

This formula works for the most part, but I have determined that in some instances, two similar hands can return the exact same score, when one should actually beat the other. An example of this is:

hand1 = 4C, 6C, 6H, JS, KC
hand2 = 3H, 4H, 7C, 7D, 8H

These two hands both have one pair, so their respective scores are:

100000 + 4^1 + 6^2 + 11^1 + 13^1 = 100064
100000 + 3^1 + 4^1 + 7^2 + 8^1 = 100064

This results in a draw, when clearly a pair of 7s trumps a pair of 6s.

How can I improve this formula, or even, what is a better formula I can use?

By the way, in my code, hands are stored in an array of each card's value in ascending order, for example:

[2H, 6D, 10C, KS, AS]

EDIT:

Here is my final solution thanks to the answers below:

    /**
     * Sorts cards by putting the "most important" cards first, and the rest in decreasing order.
     * e.g. High Hand:  KS, 9S, 8C, 4D, 2H
     *      One Pair:   3S, 3D, AH, 7S, 2C
     *      Full House: 6D, 6C, 6S, JC, JH
     *      Flush:      KH, 9H, 7H, 6H, 3H
     */
    private void sort() {
        Arrays.sort(hand, Collections.reverseOrder());      // Initially sorts cards in descending order of game value
        if (isFourOfAKind()) {                              // Then adjusts for hands where the "most important" cards
            sortFourOfAKind();                              // must come first
        } else if (isFullHouse()) {
            sortFullHouse();
        } else if (isThreeOfAKind()) {
            sortThreeOfAKind();
        } else if (isTwoPair()) {
            sortTwoPair();
        } else if (isOnePair()){
            sortOnePair();
        }
    }

    private void sortFourOfAKind() {
        if (hand[0].getGameValue() != hand[HAND_SIZE - 4].getGameValue()) {     // If the four of a kind are the last four cards
            swapCardsByIndex(0, HAND_SIZE - 1);                                 // swap the first and last cards
        }                                                                       // e.g. AS, 9D, 9H, 9S, 9C => 9C, 9D, 9H, 9S, AS
    }

    private void sortFullHouse() {
        if (hand[0].getGameValue() != hand[HAND_SIZE - 3].getGameValue()) {     // If the 3 of a kind cards are the last three
            swapCardsByIndex(0, HAND_SIZE - 2);                                 // swap cards 1 and 4, 2 and 5
            swapCardsByIndex(HAND_SIZE - 4, HAND_SIZE - 1);                     // e.g. 10D, 10C, 6H, 6S, 6D => 6S, 6D, 6H, 10D, 10C
        }
    }

    private void sortThreeOfAKind() {                                                                                                                               // If the 3 of a kind cards are the middle 3 cards
        if (hand[0].getGameValue() != hand[HAND_SIZE - 3].getGameValue() && hand[HAND_SIZE - 1].getGameValue() != hand[HAND_SIZE - 3].getGameValue()) {             // swap cards 1 and 4
            swapCardsByIndex(0, HAND_SIZE - 2);                                                                                                                     // e.g. AH, 8D, 8S, 8C, 7D => 8C, 8D, 8S, AH, 7D
        } else if (hand[0].getGameValue() != hand[HAND_SIZE - 3].getGameValue() && hand[HAND_SIZE - 4].getGameValue() != hand[HAND_SIZE - 3].getGameValue()) {
            Arrays.sort(hand);                                                                                                                                      // If the 3 of a kind cards are the last 3,
            swapCardsByIndex(HAND_SIZE - 1, HAND_SIZE - 2);                                                                                                         // reverse the order (smallest game value to largest)
        }                                                                                                                                                           // then swap the last two cards (maintain the large to small ordering)
    }                                                                                                                                                               // e.g. KS, 9D, 3C, 3S, 3H => 3H, 3S, 3C, 9D, KS => 3H, 3S, 3C, KS, 9D

    private void sortTwoPair() {                                                                                                                                    
        if (hand[0].getGameValue() != hand[HAND_SIZE - 4].getGameValue()) {                                                                                         // If the two pairs are the last 4 cards
            for (int i = 0; i < HAND_SIZE - 1; i++) {                                                                                                               // "bubble" the first card to the end
                swapCardsByIndex(i, i + 1);                                                                                                                         // e.g. AH, 7D, 7S, 6H, 6C => 7D, 7S, 6H, 6C, AH
            }
        } else if (hand[0].getGameValue() == hand[HAND_SIZE - 4].getGameValue() && hand[HAND_SIZE - 2].getGameValue() == hand[HAND_SIZE - 1].getGameValue()) {      // If the two pairs are the first and last two cards
            swapCardsByIndex(HAND_SIZE - 3, HAND_SIZE - 1);                                                                                                         // swap the middle and last card
        }                                                                                                                                                           // e.g. JS, JC, 8D, 4H, 4S => JS, JC, 4S, 4H, 8D
    }

    private void sortOnePair() {                                                                    // If the pair are cards 2 and 3, swap cards 1 and 3
        if (hand[HAND_SIZE - 4].getGameValue() == hand[HAND_SIZE - 3].getGameValue()) {             // e.g QD, 8H, 8C, 6S, 4J => 8C, 8H, QD, 6S, 4J
            swapCardsByIndex(0, HAND_SIZE - 3);
        } else if (hand[HAND_SIZE - 3].getGameValue() == hand[HAND_SIZE - 2].getGameValue()) {      // If the pair are cards 3 and 4, swap 1 and 3, 2 and 4 
            swapCardsByIndex(0, HAND_SIZE - 3);                                                     // e.g. 10S, 8D, 4C, 4H, 2H => 4C, 4H, 10S, 8D, 2H
            swapCardsByIndex(HAND_SIZE - 4, HAND_SIZE - 2);
        } else if (hand[HAND_SIZE - 2].getGameValue() == hand[HAND_SIZE - 1].getGameValue()) {      // If the pair are the last 2 cards, reverse the order
            Arrays.sort(hand);                                                                      // and then swap cards 3 and 5
            swapCardsByIndex(HAND_SIZE - 3, HAND_SIZE - 1);                                         // e.g. 9H, 7D, 6C, 3D, 3S => 3S, 3D, 6C, 7D, 9H => 3S, 3D, 9H, 7D, 6C 
        }
    }

    /**
     * Swaps the two cards of the hand at the indexes taken as parameters
     * @param index1
     * @param index2
     */
    private void swapCardsByIndex(int index1, int index2) {
        PlayingCard temp = hand[index1];
        hand[index1] = hand[index2];
        hand[index2] = temp;
    }

    /**
     * Gives a unique value of any hand, based firstly on the type of hand, and then on the cards it contains
     * @return The Game Value of this hand
     * 
     * Firstly, a 24 bit binary string is created where the most significant 4 bits represent the value of the type of hand
     * (defined as constants private to this class), the last 20 bits represent the values of the 5 cards in the hand, where
     * the "most important" cards are at greater significant places. Finally, the binary string is converter to an integer.
     */
    public int getGameValue() {
        String handValue = addPaddingToBinaryString(Integer.toBinaryString(getHandValue()));

        for (int i = 0; i < HAND_SIZE; i++) {
            handValue += addPaddingToBinaryString(Integer.toBinaryString(getCardValue(hand[i])));
        }

        return Integer.parseInt(handValue, 2);
    }

    /**
     * @param binary
     * @return the same binary string padded to 4 bits long
     */
    private String addPaddingToBinaryString(String binary) {
        switch (binary.length()) {
        case 1: return "000" + binary;
        case 2: return "00" + binary;
        case 3: return "0" + binary;
        default: return binary;
        }
    }

    /**
     * @return Default value for the type of hand
     */
    private int getHandValue() {
        if (isRoyalFlush())     { return ROYAL_FLUSH_VALUE; }       
        if (isStraightFlush())  { return STRAIGHT_FLUSH_VALUE; }
        if (isFourOfAKind())    { return FOUR_OF_A_KIND_VALUE; }
        if (isFullHouse())      { return FULL_HOUSE_VALUE; }
        if (isFlush())          { return FLUSH_VALUE; }     
        if (isStraight())       { return STRAIGHT_VALUE; }      
        if (isThreeOfAKind())   { return THREE_OF_A_KIND_VALUE; }
        if (isTwoPair())        { return TWO_PAIR_VALUE; }
        if (isOnePair())        { return ONE_PAIR_VALUE; }
        return 0;
    }

    /**
     * @param card
     * @return the value for a given card type, used to calculate the Hand's Game Value
     * 2H = 0, 3D = 1, 4S = 2, ... , KC = 11, AH = 12
     */
    private int getCardValue(PlayingCard card) {
        return card.getGameValue() - 2;
    }
KOB
  • 4,084
  • 9
  • 44
  • 88
  • 2
    Change addition to multiplication, and change the ranks to the first 13 primes, and you will have invented the "Cactus Kev" technique. One of the first reasonably fast poker hand evaluators. – Lee Daniel Crocker Feb 22 '17 at 00:37
  • @LeeDanielCrocker I have been playing around with this, but I still can't get it to work consistently - for example, a pair of 2s can beat a pair of 3s by a substantial amount, provided that the other cards in the pair of 2s hand are greater than those in the pair of 3s. 22QKA = 2^2 * 31 * 37 * 41 = 188108 and 23345 = 2 * 3^2 * 5 * 7 = 630 – KOB Feb 22 '17 at 12:59
  • I gave you the "Cactus Kev" reference. If you can't Google that yourself and read it, I can't help you. – Lee Daniel Crocker Feb 22 '17 at 17:20
  • here is my implementation of the same task https://github.com/brotherla/PokerHandAnalyzer/blob/master/PokerHandAnalyzer/src/com/lwg/poker/HandCategoryAnalyzer.java – Iłya Bursov Feb 15 '18 at 05:40
  • Here is an algorithm that uses perfect hash to score a poker hand. The same algorithm can also score 7-card poker and Omaha poker hands. https://github.com/HenryRLee/PokerHandEvaluator/blob/master/Documentation/Algorithm.md – HenryLee Jul 02 '20 at 01:44

3 Answers3

5

There are 10 recognized poker hands:

9 - Royal flush
8 - Straight flush (special case of royal flush, really)
7 - Four of a kind
6 - Full house
5 - Flush
4 - Straight
3 - Three of a kind
2 - Two pair
1 - Pair
0 - High card

If you don't count suit, there are only 13 possible card values. The card values are:

2 - 0
3 - 1
4 - 2
5 - 3
6 - 4
7 - 5
8 - 6
9 - 7
10 - 8
J - 9
Q - 10
K - 11
A - 12

It takes 4 bits to code the hand, and 4 bits each to code the cards. You can code an entire hand in 24 bits.

A royal flush would be 1001 1100 1011 1010 1001 1000 (0x9CBA98)

A 7-high straight would be 0100 0101 0100 0011 0010 0001 (0x454321)

Two pair, 10s and 5s (and an ace) would be 0010 1000 1000 0011 0011 1100 (0x28833C)

I assume you have logic that will figure out what hand you have. In that, you've probably written code to arrange the cards in left-to-right order. So a royal flush would be arranged as [A,K,Q,J,10]. You can then construct the number that represents the hand using the following logic:

int handValue = HandType; (i.e. 0 for high card, 7 for Four of a kind, etc.)
for each card
    handValue = (handValue << 4) + cardValue  (i.e. 0 for 2, 9 for Jack, etc.)

The result will be a unique value for each hand, and you're sure that a Flush will always beat a Straight and a king-high Full House will beat a 7-high Full House, etc.

Normalizing the hand

The above algorithm depends on the poker hand being normalized, with the most important cards first. So, for example, the hand [K,A,10,J,Q] (all of the same suit) is a royal flush. It's normalized to [A,K,Q,J,10]. If you're given the hand [10,Q,K,A,J], it also would be normalized to [A,K,Q,J,10]. The hand [7,4,3,2,4] is a pair of 4's. It will be normalized to [4,4,7,3,2].

Without normalization, it's very difficult to create a unique integer value for every hand and guarantee that a pair of 4's will always beat a pair of 3's.

Fortunately, sorting the hand is part of figuring out what the hand is. You could do that without sorting, but sorting five items takes a trivial amount of time and it makes lots of things much easier. Not only does it make determining straights easier, it groups common cards together, which makes finding pairs, triples, and quadruples easier.

For straights, flushes, and high card hands, all you need to do is sort. For the others, you have to do a second ordering pass that orders by grouping. For example a full house would be xxxyy, a pair would be xxabc, (with a, b, and c in order), etc. That work is mostly done for you anyway, by the sort. All you have to do is move the stragglers to the end.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • I am a big fan of this solution - I have already implemented it and it works falwlessly. The only problem is that throughtout my entire progam, hands are compared to each other by `int` values e.g. `if (hand1.getValue() > hand2.getValue())`. Is there any way I can add to this solution so that unique binary string for a hand is expressed as a unique integer? I tried simply converting the entire 24 bit binary string to an integer, but this does not work. – KOB Feb 22 '17 at 16:46
  • EDIT: I think the reason that convertign the binary string to an integer wasn't working was because the binary string wasn't padded to 24 bits – KOB Feb 22 '17 at 16:58
  • I believe this method still has the same problem that for example a pair of 2s could return a higher score than a pair of 3s - AKQ22 vs 54332 – KOB Feb 22 '17 at 17:17
  • In this solution, a hand that contains `22456` would be coded as `0001 0000 0000 0010 0011 0100`, or `0x100234`. The hand `33456` would be `0001 0001 0001 0010 0011 0100`, or `0x111234`. I think you forgot the part where you order the cards so that the pair is always in front. This shouldn't be difficult, and depending on how you compute what the hand is, might already be done. – Jim Mischel Feb 22 '17 at 17:59
  • I believe that this answer and my answer are basically the same in terms of implementation. The *ordering* requirement is the difference. If two hands with "one pair" are distinguished primarily by the cards making up the pair, then you need to sort them to the high digits of your composed integer. – aghast Feb 22 '17 at 20:40
  • @AustinHastings: I think you're right that our approaches are essentially the same. The key is that the hand 7,3,4,2,4 will be "normalized" to 4,4,7,3,2: basically putting the most important information at the front so that when it gets encoded it can compare correctly with other hands of the same type. You alluded to that in your answer. – Jim Mischel Feb 22 '17 at 20:50
  • @KOB: See the comments, and the additional info in my answer. – Jim Mischel Feb 22 '17 at 21:07
  • @JimMischel After normalising the cards, it all works excellently. As an added bonus, I much prefer having the cards displyed with the "most important" cards first, rather than just in ascending order. Thank you very much for your help. – KOB Feb 23 '17 at 02:01
  • I have added my final solution to my original post. – KOB Feb 23 '17 at 02:12
  • @JimMischel Let me know if this following comment warrants a whole new post. The next task we have is to create a function `getDiscardProbability(int position)` that will return the probability of the hand being improved if `hand[position]` is replaced with a (random) card from the deck. The examples given to us were along the lines of, for example, if you had 96532 then what is the probability of replacing the 9 with a 4 to complete the straight. Since my hands have a binary representation, would my best approach be to calculate the probability of increasing this binary value? – KOB Feb 27 '17 at 18:01
  • @KOB: That would be a completely different question. – Jim Mischel Feb 27 '17 at 20:39
2

As you have found, if you add together the values of the cards in the way you have proposed then you can get ambiguities.

100000 + 4^1 + 6^2 + 11^1 + 13^1 = 100064
100000 + 3^1 + 4^1 + 7^2 + 8^1 = 100064

However, addition is not quite the right tool here. You are already using ^ which means you're partway there. Use multiplication instead and you can avoid ambiguities. Consider:

100000 + (4^1 * 6^2 * 11^1 * 13^1)
100000 + (3^1 * 4^1 * 7^2 * 8^1)

This is nearly correct, but there are still ambiguities (for example 2^4 = 4^2). So, reassign new (prime!) values to each card:

Ace => 2
3 => 3
4 => 5
5 => 7
6 => 11
...

Then, you can multiply the special prime values of each card together to produce a unique value for every possible hand. Add in your value for type of hand (pair, full house, flush, etc) and use that. You may need to increase the magnitude of your hand type values so they stay out of the way of the card value composite.

Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
  • Excellent solution. What is the exact purpose of the primes? Is it to achieve an exponential increase in a hand's value if it were to contain higher value cards? – KOB Feb 22 '17 at 00:09
  • 2
    @KOB: In short, it's to make sure that the calculated values for one rank of card don't conflict with calculated values of a nearby rank. – Greg Hewgill Feb 22 '17 at 00:11
  • 1
    I am a bit lost with this method as there still seems to be inconsistencies - for example a pair of 2s can beat a pair of 3s by a substantial amount, provided that the other cards in the pair of 2s hand are greater than those in the pair of 3s. 22QKA = 2^2 * 31 * 37 * 41 = 188108 and 23345 = 2 * 3^2 * 5 * 7 = 630 – KOB Feb 22 '17 at 12:47
  • @KOB: That's true. You may need to further develop your solution using the observation from some of the other answers here that the *participating* cards in the type of hand matter more than the non-participating cards. I think you're well on your way though. – Greg Hewgill Feb 22 '17 at 16:54
  • @KOB Additionally, you can map the product to a specific hand rank, although you also have to take into account whether or not the hand is a flush. – Herb Feb 23 '17 at 05:02
2

The highest value for a card will be 14, assuming you let non-face cards keep their value (2..10), then J=11, QK, A=14.

The purpose of the scoring would be to differentiate between hands in a tie-breaking scenario. That is, "pair" vs. "pair." If you detect a different hand configuration ("two pair"), that puts the scores into separate groups.

You should carefully consult your requirements. I suspect that at least for some hands, the participating cards are more important than non-participating cards. For example, does a pair of 4's with a 7-high beat a pair of 3's with a queen-high? (Is 4,4,7,3,2 > 3,3,Q,6,5?) The answer to this should determine an ordering for the cards in the hand.

Given you have 5 cards, and the values are < 16, convert each card to a hexadecimal digit: 2..10,JQKA => 2..ABCDE. Put the cards in order, as determined above. For example, 4,4,7,3,2 will probably become 4,4,7,3,2. Map those values to hex, and then to an integer value: "0x44732" -> 0x44732.

Let your combo scores be multiples of 0x100000, to ensure that no card configuration can promote a hand into a higher class, then add them up.

aghast
  • 14,785
  • 3
  • 24
  • 56
  • This is a very practical solution, however, reorering the cards in relation to the type of hand they are would be another very substantial task – KOB Feb 22 '17 at 00:35
  • The original post mentions having this information: `ROYAL_FLUSH = 900000`, etc. I assume that is either part of the fixture, or part of the assignment. – aghast Feb 22 '17 at 20:37
  • 1
    Giving a Royal Flush a default score of 900,000 was my own idea, it was not necessary. In the end, I did what @JimMischel suggested and had the default hand scores from 0-9. I understand that your solution is almost identical to Jim's, but I have accepted his as the answer as he went into great detail. Also, I regret not taking your advice on reordering th cards - I thought it would be far more difficult than it was. I have implemented the reordering of the cards in relation to the type of hand they are in, and everything works flawlessly. – KOB Feb 23 '17 at 02:05