1

I am newbie to python and learning it from books, forums and developers. Recently, I tried to implement various hand-ranking categories in the poker program. The goal is to calculate probabilities and see if it agrees with theoretical poker hand probabilities?

Source: https://en.wikipedia.org/wiki/Poker_probability?oldformat=true

Please find below the code and logic I've used so far to build it. The code contains Card and Deck classes which together implement a deck of standard playing cards used, as well as a sample PyTest test function test_xxx().

So far, I've written hasOnePair, hasTwoPairs, hasThreeOfAKind, hasFullHouse and hasFourOfaKind() functions and it is working fine but I am struggling with Straight, flush, StraightFlush.

Could someone please suggest or give guideline about how to approach straight, flush, royalflush, straightflush cases? Also, any further suggestions to update this code will be great.


import random

SUITS = ["Clubs", "Diamonds", "Hearts", "Spades"]
RANKS = ["", "Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King"]

# here's two Python classes whose objects represent playing cards and decks thereof

class Card():
    """
    Represents a single playing card,
        whose rank internally is int _rank: 1..13 => "Ace".."King"
        and whose suit internally is int _suit 0..3 => "Clubs".."Spades"
    """

    def __init__(self, rank=1, suit=3): # this is the constructor!
        '''
        Initialize card with given int suit and int rank
        :param rank:
        :param suit:
        :return:
        '''
        self._rank = rank
        self._suit = suit

    def __str__(self): # this is the "stringifier"
        """
        Return the string name of this card:
        "Ace of Spades": translates int fields to strings
        :return:
        """

        # "Ace of Spades" is string for self._rank==1, self._suit==3

        toreturn = RANKS[self._rank] + " of " + SUITS[self._suit]

        return toreturn


class Deck():
    """
    Represents a deck of 52 standard playing cards,
        as a list of Card refs
    """

    def __init__(self): # constructor
        """
        Initialize deck: field _cards is list containing
            52 Card refs, initially
        :return: nothing
        """

        self._cards = []
        for rank in range(1, 14):
            for suit in range(4):
                c = Card(rank, suit) # create next Card with given value
                self._cards.append(c) # add it to this Deck

    def __str__(self):
        """
        "Stringified" deck: string of Card named,
        with \n for easier reading
        :return:
        """
        toreturn = ''

        # for index in range(len(self._cards)):
        #     self._cards[index]

        for c in self._cards:
            temp = str(c)  # temp is the stringified card
            toreturn = toreturn + temp + "\n"  # note \n at end

        return toreturn

    def shuffle(self):
        random.shuffle(self._cards)  # note random function to do this

    def dealCard(self):
        toreturn = self._cards.pop(0)  # get and remove top card from deck
        return toreturn

def buildDict(hand):
    dict = {}

    for card in hand:
        dict[card._rank] = dict.get(card._rank, 0) + 1


    return dict


def hasOnePair(dict):

    twocount = 0
    threecount = 0

    for v in dict.values():
        if v == 2:
            twocount += 1
        elif v == 3:
            threecount += 1

    if twocount==1 and threecount != 1:
        return True
    else:
        return False



def hasTwoPairs(dict):

    twocount1 = 0
    threecount1 = 0

    for v in dict.values():
        if v ==2:
            twocount1 += 1
        elif v == 3:
            threecount1 +=1

    if twocount1 == 2 and threecount1 != 1:
        return True
    else:
        return False



def hasThreeOfAKind(dict):


    twocount = 0
    threecount = 0

    for v in dict.values():
        if v == 2:
            twocount += 1
        elif v == 3:
            threecount += 1

    if twocount != 1 and threecount == 1:
        return True
    else:
        return False



def hasFullHouse(dict):

    twocount = 0
    threecount = 0

    for v in dict.values():
        if v == 2:
            twocount += 1
        elif v == 3:
            threecount += 1

    if twocount == 1 and threecount == 1:
        return True
    else:
        return False





def hasFourOfAKind(dict):

    fourcount = 0
    onecount = 0

    for v in dict.values():
        if v ==4:
            fourcount += 1
        elif v == 1:
            onecount +=1

    if fourcount == 1 and onecount == 1:
        return True
    else:
        return False


def hasStraight(hand):


    return False

def hasFlush(dict):


    return False

def hasStraightFlush(dict):

    return False

def hasRoyalFlush(dict):

    return False

def main():


    TRIALS = 1000  # int(input ("Input number of hands to test: "))

    hand = []  # list of Card in hand

    # accumulators for different counts

    onepairCount = 0
    twopairCount = 0
    threeCount = 0
    fourCount = 0
    fullHouseCount = 0
    StraightCount = 0



    for num in range(TRIALS):

        # create new Deck and shuffle
        d = Deck()
        d.shuffle()

        # initialize hand as empty list
        hand = []

        # deal top 5 cards of deck, adding to hand

        for count in range(5):
            hand.append(d.dealCard())


        # build the dictionary of card ranks in hand

        dict = buildDict(hand)



        # use dictionary to make hand checking easier

        if hasOnePair(dict):
            onepairCount += 1
        elif hasTwoPairs(dict):
            twopairCount += 1
        elif hasThreeOfAKind(dict):
            threeCount += 1
        elif hasFourOfAKind(dict):
            fourCount += 1
        elif hasFullHouse(dict):
            fullHouseCount += 1
        elif hasStraight(dict):
            StraightCount +=1

    # add more if needed...

    # print out results...

    print("Number of one pair hands is: ", onepairCount)
    print("% of hands: ", 100.0 * onepairCount / TRIALS)

    print("Number of two pair hands is: ", twopairCount)
    print("% of hands: ", 100.0 * twopairCount / TRIALS)


    print("Number of trips hand is: ", threeCount)
    print("% of hands: ", 100.0 * threeCount / TRIALS)

    print("Number of quads hand is: ", fourCount)
    print("% of hands: ", 100.0 * fourCount / TRIALS)

    print("Number of trips hand is: ", fullHouseCount)
    print("% of hands: ", 100.0 * fullHouseCount / TRIALS)

    print("Number of trips hand is: ", StraightCount)
    print("% of hands: ", 100.0 * StraightCount / TRIALS)



def card_example():

    card1 = Card()  # Card(1,3) => Ace of Clubs
    card2 = Card(12, 2) # Card (12,2) => Queen of Hearts

    card1._newfield = 47 # we can add new fields to any Python object!

    # three ways of printing a Card
    #

    print(card1.__str__())  # calling the methods against card
    print(str(card2)) # type-casting
    print(card2) # short-cut: passing obj ref to print does str() automagically

    print(card1._newfield) # see the new field value?

    print(card1._rank) # see the rank (1..13)
    print(card1._suit) # see the suit (0..3)

def deck_example():
    """
    Test Deck: create, print then shuffle, print again
    Then deal first two cards and print, along with bottom card
    """

    deck = Deck()
    print(str(deck)) # see entire deck before shuffling

    print("Now we shuffle:\n")

    deck.shuffle()
    print(str(deck)) # see entire deck after shuffling

    card1 = deck.dealCard()
    card2 = deck.dealCard()

    print("The first card dealt is", str(card1), "and the second is", str(card2))
    print("Bottom of deck is", deck._cards[-1])  # can't hide the implementation!

if __name__ == "__main__": # only run this if this .py is NOT imported
    # pass

    # card_example() # uncomment to test creating & calling Card methods

    # deck_example()  # uncomment to test Deck: create, print, shuffle, print

    main()  # uncomment to run general poker odds calculations

#
# -------------------------------------------------------------------------
#

#pytest follows...

def test_one_pair():
    testhand = [Card(2, 3), Card(1, 2),
                Card(3, 1), Card(13, 2),
                Card(2, 0)]

    dict = buildDict(testhand)

    assert hasOnePair(dict)  #hasTwoPairs

def test_two_pair():
    testhand = [Card(2, 3), Card(1, 2),
                Card(3, 1), Card(3, 2),
                Card(2, 0)]

    dict = buildDict(testhand)

    assert hasTwoPairs(dict)

def test_three_pair():
    testhand = [Card(1, 3), Card(1, 2),
                Card(1, 1), Card(13, 2),
                Card(2, 0)]

    dict = buildDict(testhand)

    assert hasThreeOfAKind(dict)

def has_Four_Of_A_Kind():
    testhand = [Card(1, 3), Card(1, 2),
                Card(1, 1), Card(1, 0),
                Card(2, 0)]

    dict = buildDict(testhand)

    assert hasFourOfAKind(dict)

def test_full_house():
    testhand = [Card(1, 3), Card(1, 2),
                Card(1, 1), Card(13, 2),
                Card(13, 2)]

    dict = buildDict(testhand)

    assert hasFullHouse(dict)

def test_Straight():
    testhand = [Card(11, 1), Card(10, 3),
                Card(9, 2), Card(8, 1),
                Card(7, 3)]

    dict = buildDict(testhand)

    assert hasStraight(dict)


E_net4
  • 27,810
  • 13
  • 101
  • 139
Sachin Sharma
  • 352
  • 5
  • 18
  • Why are you using `buildDict` rather than directly operating on the hand? – hwaring Jul 03 '19 at 16:30
  • @hwaring: I wanted to solve it using this concepts and it worked well/look more readable but I am not getting desired results for straight, flush and straight flush. – Sachin Sharma Jul 03 '19 at 16:36
  • https://codereview.stackexchange.com/questions/128702/poker-hands-in-python – jtlz2 Jul 03 '19 at 16:37
  • https://pypi.org/project/poker/ – jtlz2 Jul 03 '19 at 16:39
  • Because your Q is python, it is not a duplicate - but here someone did it in python in response to a similar Java Q https://stackoverflow.com/a/20715903/1021819 – jtlz2 Jul 03 '19 at 16:42
  • Also if you are doing a stats analysis are you going for speed? Or are you going for readability? Or both? Or something else? – jtlz2 Jul 03 '19 at 16:43
  • FWIW, I followed a link from a triple-double hand here and came up with this regex: `(?=^(\d))(?=\1*(?!\1)(\d))(?=^(?:\1|\2){5}$)(?=(?:\1\2*){2,3})(?=(?:\1*\2){2,3})^\d{5}$` [(Demo)](https://regex101.com/r/r4Bfpg/1/) – Nick Reed Oct 18 '19 at 21:19

3 Answers3

1

The condition for a straight is for your five cards to have adjacent ranks, such that no two cards have the same rank. You can use two different checks to confirm this:

  1. when sorted, the difference between the top card and the bottom card is equal to the total number of cards minus one(e.g. a 4-8 straight has a difference of 4)
  2. no two cards in the straight have the same rank (thus by pigeonhole principle, all ranks between the minimum and maximum must be present

Here's a sample implementation of that:

def hasStraight(hand):
    # account for both low-ace and high-ace
    ranks_low = sorted([card._rank for card in hand])
    ranks_high = sorted([(14 if card._rank == 1 else card._rank) for card in hand])

    return (
        (
            ranks_low[-1] - (len(hand) - 1) == ranks_low[0]
            or ranks_high[-1] - (len(hand) - 1) == ranks_high[0]
        )                                                        # condition 1
        and len(set(hand)) == len(hand)                          # condition 2
    )

You will also have to account for low-aces and high-aces. You can do this by, for example, doing two similar checks, once with the normal card ranks and once by doing something like `if card._rank == 1: card._rank == 14


The condition for a flush is simply that all cards are the same suit. This is easy to verify, by simply making a set of all unique suits in the hand, and returning true if that set has only one element (thus all the suits must be the same):

def hasFlush(hand):
    suits_set = set(*[card._suit for card in hand])
    return len(suits_set) == 1

Once you have those, straight flush and royal flush are easy:

def hasStraightFlush(hand):
    return hasStraight(hand) and hasFlush(hand)

def hasRoyalFlush(hand):
    ranks = sorted([14 if card._rank == 1 else card._rank for card in hand])
    return (
        hasStraightFlush(hand)
        and ranks[0] == 10 and ranks[-1] == 14
    ) # royal flush starts at 10, ends at high-ace
Green Cloak Guy
  • 23,793
  • 4
  • 33
  • 53
  • Thanks for detailed instruction and help. I am using buildDict to build dictionary and using dict as parameter instead of directly using hand. Could you please suggest, if possible in short, how it can be implemented using dict as input parameter and whether it is a good approach. – Sachin Sharma Jul 03 '19 at 17:44
  • In this case, it's usually easier to use a list as your input parameter, and if for some reason you really need to, convert it to a dict inside the function. As you can see, in this case we didn't really need to, but in your case when you're counting pairs/full houses/n-of-a-kinds, you do have to do that. The solutions you've already come up with will usually do fine - even if it's often more terse to use "casting to `set()`" to filter things for uniqueness. – Green Cloak Guy Jul 03 '19 at 19:05
  • Will the straight flush check work if the hand is `10D 9C 8S 7S 6D 2H 4H 6H 8H 10H`? This hand has a straight and a flush but not a straight flush. – alex Oct 10 '19 at 19:59
  • @alex with the code as written that wouldn't trigger as a Flush. In general as-written the code assumes there are five cards in the hand, in which case it's impossible to have a straight and a flush at the same time. You'd need to modify the code to account for larger hand sizes – Green Cloak Guy Oct 10 '19 at 23:18
0
import random
from collections import Counter
import numpy as np
import heapq

def table_hands(time):
    
    poker = 0
    full_house = 0
    three_of_a_kind = 0
    two_pair = 0
    pair = 0
    flush = 0
    straight = 0
    straight_royal_flush = 0
    high_card = 0
    straight_output = [[1,2,3,4,5],[2,3,4,5,6],[3,4,5,6,7],[4,5,6,7,8],[5,6,7,8,9],
                       [6,7,8,9,10],[7,8,9,10,11],[8,9,10,11,12],
                       [9,10,11,12,13],[10,11,12,13,1]]
    
    
    for i in range(time):
        
        desk_numbers = (np.arange(1,14,1).tolist())*4
        random.shuffle(desk_numbers)
        
        desk_colors = (np.arange(1,5,1).tolist())*13
        random.shuffle(desk_colors)
        
        player_hand_numbers = Counter((desk_numbers[0],desk_numbers[1], desk_numbers[5],desk_numbers[6],desk_numbers[7],desk_numbers[9],desk_numbers[11]))
        player_hand_colors = Counter((desk_colors[0],desk_colors[1],desk_colors[5],desk_colors[6],desk_colors[7],desk_colors[9],desk_colors[11]))
        
        straight_royal_flush_true = 0
        high_card_true = 1
        
        # dealer_hand = Counter((desk_numbers[2],desk_numbers[3],desk_numbers[5],desk_numbers[6],desk_numbers[7],desk_numbers[9],desk_numbers[11]))

        
        if max(player_hand_numbers.values()) == 4:
            poker += 1
            # print(player_hand_numbers)
            
        elif max(player_hand_numbers.values()) == 3:
            if heapq.nlargest(2, player_hand_numbers.values())[1] == 2:
                full_house += 1
                # print(player_hand_numbers)
            elif max(player_hand_colors.values()) >= 5:
                flush += 1
                # print(player_hand_colors,player_hand_numbers)
            else:
                three_of_a_kind += +1
                # print(player_hand_numbers)
                
        elif max(player_hand_numbers.values()) == 2:
            if max(player_hand_colors.values()) >= 5:
                flush+=1 
                # print(list(player_hand_numbers.keys()))
                # print(player_hand_colors)
            elif heapq.nlargest(2, player_hand_numbers.values())[1] == 2:
                two_pair += 1
                # print(player_hand_numbers)
            else:
                pair += 1
                
                # print(player_hand_numbers)

        
        elif max(player_hand_numbers.values()) >= 5:
            player_hand_numbers_list = list(player_hand_numbers.keys())
            for i in straight_output:
                if all(x in player_hand_numbers_list for x in i) == True:
                    straight_royal_flush_true = 1
                    break
            
            if straight_royal_flush_true == 1:
                straight_royal_flush += 1
                print(player_hand_numbers_list)
                print(player_hand_colors)
            else:
                flush += 1
            
        else:
            player_hand_numbers_list = list(player_hand_numbers.keys())
            for i in straight_output:
                if all(x in player_hand_numbers_list for x in i) == True:
                    high_card_true = 0
                    break
                
            if high_card_true == 0:
                straight += 1
                # print(player_hand_colors, player_hand_numbers_list)
            else:
                high_card += 1
                # print(player_hand_colors, player_hand_numbers_list)

        
    return print(straight_royal_flush,poker,full_house,flush, straight, three_of_a_kind,two_pair,pair, high_card)


table_hands(10000000)
Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
  • Thank you for contributing to the Stack Overflow community. This may be a correct answer, but it’d be really useful to provide additional explanation of your code so developers can understand your reasoning. This is especially useful for new developers who aren’t as familiar with the syntax or struggling to understand the concepts. **Would you kindly [edit] your answer to include additional details for the benefit of the community?** – Jeremy Caney Jul 27 '23 at 17:57
0

you can use the library, tilted to achieve what you want. It's got a good README for hand comparison & evaluation so this will give you what you want and more:

pip install tilted:

https://github.com/MaxAtkinson/tilted

psilocybin
  • 1,070
  • 7
  • 25