Martin Tausch
Martin Tausch

Reputation: 744

Optimizing hand-evaluation algorithm for Poker-Monte-Carlo-Simulation

I've written an equilator for Hold'em Poker as a hobbyproject. It works correctly, but there is still one thing I am not happy with: In the whole process of simulating hands, the process of evaluating the hands takes about 35% of the time. That seems to be pretty much to me compared with what else has to be done like iterating through and cloning large arrays and stuff.

Any idea of how to get this more performant would be great.

This is the code:

    private static int getHandvalue(List<Card> sCards)
    {

        // --- Auf Straight / Straight Flush prüfen ---
        if ((sCards[0].Value - 1 == sCards[1].Value) && (sCards[1].Value - 1 == sCards[2].Value) && (sCards[2].Value - 1 == sCards[3].Value) && (sCards[3].Value - 1 == sCards[4].Value))
        {

            if ((sCards[0].Color == sCards[1].Color) && (sCards[1].Color == sCards[2].Color) && (sCards[2].Color == sCards[3].Color) && (sCards[3].Color == sCards[4].Color))
                return (8 << 20) + (byte)sCards[0].Value; // Höchste Karte Kicker

            else
                return (4 << 20) + (byte)sCards[0].Value; // Höchste Karte Kicker (Straight)

        }

        // --- Auf Wheel / Wheel Flush prüfen ---
        if ((sCards[4].Value == Card.CardValue.Two) && (sCards[3].Value == Card.CardValue.Three) && (sCards[2].Value == Card.CardValue.Four) && (sCards[1].Value == Card.CardValue.Five) && (sCards[0].Value == Card.CardValue.Ace))
        {

            if ((sCards[0].Color == sCards[1].Color) && (sCards[1].Color == sCards[2].Color) && (sCards[2].Color == sCards[3].Color) && (sCards[3].Color == sCards[4].Color))
                return(8 << 20) + (byte)sCards[1].Value; // Zweithöchste Karte Kicker

            else
                return(4 << 20) + (byte)sCards[1].Value; // Zweithöchste Karte Kicker (Straight)

        }


        // --- Auf Flush prüfen ---
        if ((sCards[0].Color == sCards[1].Color) && (sCards[1].Color == sCards[2].Color) && (sCards[2].Color == sCards[3].Color) && (sCards[3].Color == sCards[4].Color))
            return (5 << 20) + ((byte)sCards[0].Value << 16) + ((byte)sCards[1].Value << 12) + ((byte)sCards[2].Value << 8) + ((byte)sCards[3].Value << 4) + (byte)sCards[4].Value;


        // --- Auf Vierling prüfen ---
        if (((sCards[0].Value == sCards[1].Value) && (sCards[1].Value == sCards[2].Value) && (sCards[2].Value == sCards[3].Value)) || ((sCards[1].Value == sCards[2].Value) && (sCards[2].Value == sCards[3].Value) && (sCards[3].Value == sCards[4].Value)))
            return (7 << 20) + (byte)sCards[1].Value; // Wert des Vierlings (keine Kicker, da nicht mehrere Spieler den selben Vierling haben können)


        // --- Auf Full-House / Drilling prüfen ---
        // Drilling vorne
        if ((sCards[0].Value == sCards[1].Value) && (sCards[1].Value == sCards[2].Value))
        {

            // Full House
            if (sCards[3].Value == sCards[4].Value)
                return (6 << 20) + ((byte)sCards[0].Value << 4) + (byte)sCards[3].Value; // Drilling (höher bewerten)

            // Drilling
            return (3 << 20) + ((byte)sCards[0].Value << 8) + ((byte)sCards[3].Value << 4) + (byte)sCards[4].Value; // Drilling + Kicker 1 + Kicker 2

        }

        // Drilling hinten
        if ((sCards[2].Value == sCards[3].Value) && (sCards[3].Value == sCards[4].Value))
        {

            // Full House
            if (sCards[0].Value == sCards[1].Value)
                return (6 << 20) + ((byte)sCards[2].Value << 4) + (byte)sCards[0].Value; // Drilling (höher bewerten)

            // Drilling
            return (3 << 20) + ((byte)sCards[2].Value << 8) + ((byte)sCards[0].Value << 4) + (byte)sCards[1].Value; // Drilling + Kicker 1 + Kicker 2

        }

        // Drilling mitte
        if ((sCards[1].Value == sCards[2].Value) && (sCards[2].Value == sCards[3].Value))
            return (3 << 20) + ((byte)sCards[1].Value << 8) + ((byte)sCards[0].Value << 4) + (byte)sCards[4].Value; // Drilling + Kicker 1 + Kicker 2                            


        // --- Auf Zwei Paare prüfen ---
        // Erstes Paar vorne, zweites Paar mitte
        if ((sCards[0].Value == sCards[1].Value) && (sCards[2].Value == sCards[3].Value))
            return (2 << 20) + ((byte)sCards[0].Value << 8) + ((byte)sCards[2].Value << 4) + (byte)sCards[4].Value; // Erstes Paar + Zweites Paar + Kicker

        // Erstes Paar vorne, zweites Paar hinten
        if ((sCards[0].Value == sCards[1].Value) && (sCards[3].Value == sCards[4].Value))
            return (2 << 20) + ((byte)sCards[0].Value << 8) + ((byte)sCards[3].Value << 4) + (byte)sCards[2].Value; // Erstes Paar + Zweites Paar + Kicker

        // Erstes Paar mitte, zweites Paar hinten
        if ((sCards[1].Value == sCards[2].Value) && (sCards[3].Value == sCards[4].Value))
            return (2 << 20) + ((byte)sCards[1].Value << 8) + ((byte)sCards[3].Value << 4) + (byte)sCards[0].Value; // Erstes Paar + Zweites Paar + Kicker


        // --- Auf Paar prüfen ---
        // Paar vorne
        if (sCards[0].Value == sCards[1].Value)
            return (1 << 20) + ((byte)sCards[0].Value << 12) + ((byte)sCards[2].Value << 8) + ((byte)sCards[3].Value << 4) + (byte)sCards[4].Value; // Paar + Kicker 1 + Kicker 2 + Kicker 3

        // Paar mitte-vorne
        if (sCards[1].Value == sCards[2].Value)
            return (1 << 20) + ((byte)sCards[1].Value << 12) + ((byte)sCards[0].Value << 8) + ((byte)sCards[3].Value << 4) + (byte)sCards[4].Value; // Paar + Kicker 1 + Kicker 2 + Kicker 3

        // Paar mitte-hinten
        if (sCards[2].Value == sCards[3].Value)
            return (1 << 20) + ((byte)sCards[2].Value << 12) + ((byte)sCards[0].Value << 8) + ((byte)sCards[1].Value << 4) + (byte)sCards[4].Value; // Paar + Kicker 1 + Kicker 2 + Kicker 3

        // Paar hinten
        if (sCards[3].Value == sCards[4].Value)
            return (1 << 20) + ((byte)sCards[3].Value << 12) + ((byte)sCards[0].Value << 8) + ((byte)sCards[1].Value << 4) + (byte)sCards[2].Value; // Paar + Kicker 1 + Kicker 2 + Kicker 3


        // --- High Card bleibt übrig ---
        return ((byte)sCards[0].Value << 16) + ((byte)sCards[1].Value << 12) + ((byte)sCards[2].Value << 8) + ((byte)sCards[3].Value << 4) + (byte)sCards[4].Value; // High Card + Kicker 1 + Kicker 2 + Kicker 3 + Kicker 4

    }

This method returns an exact value for every sorted 5-Card-Combination in poker. It gets called by another method:

    private static int getHandvalueList(List<Card> sCards)
    {

        int count = sCards.Count;
        if (count == 5) return getHandvalue(sCards);

        int HighestValue = 0;
        Card missingOne;
        int tempValue;

        for (int i = 0; i < count - 1; i++)
        {

            missingOne = sCards[i];
            sCards.RemoveAt(i);

            tempValue = getHandvalueList(sCards);
            if (tempValue > HighestValue) HighestValue = tempValue;

            sCards.Insert(i, missingOne);

        }

        missingOne = sCards[count - 1];
        sCards.RemoveAt(count - 1);

        tempValue = getHandvalueList(sCards);
        if (tempValue > HighestValue) HighestValue = tempValue;

        sCards.Add(missingOne);
        return HighestValue;

    }

This recursive method returns the highest value of all possible 5-card-combinations. And this one gets called by the final public method:

    public static int GetHandvalue(List<Card> sCards)
    {

        if (sCards.Count < 5) return 0;
        sCards.Sort(new ICardComparer());
        return getHandvalueList(sCards);

    }

It gets delivered a maximum of 7 cards.

Update

So far: Each time, the public function gets called with 7 cards (which is the case most of the time), it has to call the hand-evaluation method 21 times (one time for every possible 5-card-combo).

I thought about caching the value for each possible set of 5 to 7 cards in a hashtable und just look it up. But if I am not wrong, it would have to store more than 133.784.560 32-bit-integer values, which is about 500MB.

What could be good hashfunction to assign every possible combination to exactly one arrayindex?

Created a new question on that: Hashfunction to map combinations of 5 to 7 cards

Update: For further improvement regarding the accepted answer, have alook at: Efficient way to randomly select set bit

Upvotes: 3

Views: 2533

Answers (1)

tmyklebu
tmyklebu

Reputation: 14205

I've written a rather fast poker hand evaluator in the past. I used a rather different representation from yours, and I think that was the key to performance.

I represented a hand of at most 7 cards by a 64-bit integer; bit 4 was for the two of hearts, bit 5 for the two of diamonds, 6 for the two of spades, 7 for the two of clubs, 8 for the three of hearts, and so forth.

You first check for straight flushes; form hh = h & (h>>4) & (h>>8) & (h>>12) & (h>>16). If hh is nonzero, then you have a straight flush beginning at its high bit.

Then you check for four-of-a-kinds; form hh = h & (h>>1) & (h>>2) & (h>>3) & 0x1111...1. If hh is nonzero, you've got yourself a four-of-a-kind.

At this point, you want to find out which ranks have three-of-a-kind and which ranks have pairs. Similar to the four-of-a-kind case, form bitmasks telling you which ranks have at least three cards, which ranks have at least two cards, and which ranks have at least one card. (Think about sorting each nibble of the hand.) If popcount(threes) + popcount(twos) >= 2, you have a full house to find.

Flush and straight conditions are straightforward to check. And, at this point, so are three-of-a-kind, two pair, and pair conditions.

A nice feature of this approach is that it can straightforwardly return an integer representing the rank of the hand, reducing hand comparison to a bunch of bit-fiddling to preprocess the hands and then a single integer comparison. (Exactly as you're doing, now that I look at your post again.) It also directly operates on 7-card hands if written properly, eliminating the need to iterate over all subsets of 5 cards in the hand.

Upvotes: 9

Related Questions