Python Forum
I need help understanding a program structure using classes
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
I need help understanding a program structure using classes
#21
I added a rules class that uses Hand to determine value. I should flatten the list before returning but I grow weary of this exercise.
import collections
import copy
import random

class Card():
    """A playing card that has a rank and suit"""
    # These are class variables.  They are associated with the class, not instances of the class
    suit_names = ["Clubs", "Diamonds", "Hearts", "Spades"]
    short_suit_names = ["C", "D", "H", "S"]
    rank_names = ["", "Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King", "Ace"]
    short_rank_names = ["", "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
    rank_range = range(2, 15)

    def __init__(self, rank, suit=None):
        # These are instance variables.  Each instance has their own rank an suit
        if suit is None:
            # Assume rank is a card repr.  Extract rank and suit
            suit = Card.suit_names[Card.short_suit_names.index(rank[-1])]
            rank = Card.short_rank_names[2:].index(rank[:-1])+2
        self.rank = rank
        self.suit = suit

    def __lt__(self, other):
        """For sorting and comparing by rank"""
        return self.rank < other.rank  # self.rank is my rank.  other.rank is the rank of another card

    def __gt__(self, other):
        """For sorting and comparing by rank"""
        return self.rank > other.rank

    def __eq__(self, other):
        """For sorting and comparing by rank"""
        return self.rank == other.rank

    def name(self):
        """Get long name for card"""
        return f"{self.rank_names[self.rank]} {self.suit}"

    def __repr__(self):
        """Get short name for card"""
        return f"{self.short_rank_names[self.rank]}{self.suit[0]}"

class Hand():
    """A list of cards"""
    def __init__(self, cards=None):
        self.cards = [] if cards is None else cards

    def ranks(self):
        """Group cards into ranks.  Sort ranks by number of cards in a rank"""
        def sort_key(item):
            """Sorting primarily on number of matching cards and secondarily on rank value"""
            return (len(item), item[0].rank)

        ranks = {}
        for card in self.cards:
            if card.rank in ranks:
                ranks[card.rank].append(card)
            else:
                ranks[card.rank] = [card]
        ranks = list(ranks.values())
        ranks.sort(key=sort_key, reverse=True)
        return ranks

    def suits(self):
        """Group cards into suits. Sort suits by number of cards in a suit"""
        suits = {}
        for card in self.cards:
            if card.suit in suits:
                suits[card.suit].append(card)
            else:
                suits[card.suit] = [card]
        suits = list(suits.values())
        for suit in suits:
            suit.sort(reverse=True)
        suits.sort(key=len, reverse=True)
        return suits

    def __add__(self, other):
        """Create a new hand by combining two existing hands"""
        return Hand(self.cards + other.cards)

    def __repr__(self):
        """Print cards in hand"""
        return ", ".join([str(card) for card in self.cards])


class Deck():
    """Deck of cards"""
    def __init__(self, shuffle=False):
        self.cards = [Card(rank, suit) for rank in Card.rank_range for suit in Card.suit_names]
        if shuffle:
            random.shuffle(self.cards)

    def __len__(self):
        """Return number of cards in deck"""
        return len(self.cards)

    def deal(self, count):
        """Deal count cards from top of deck"""
        cards = self.cards[:count]
        self.cards = self.cards[count:]
        return cards

class Rules():
    ROYAL_FLUSH = 9
    STRAIGHT_FLUSH = 8
    FOUR_OF_A_KIND = 7
    FULL_HOUSE = 6
    FLUSH = 5
    STRAIGHT = 4
    THREE_OF_A_KIND = 3
    TWO_PAIR = 2
    ONE_PAIR = 1
    HIGH_CARD = 0

    HAND_NAMES = [
        "High Card",
        "One Pair",
        "Two Pair",
        "Three of a Kind",
        "Straight",
        "Flush",
        "Full House",
        "Four of a Kind",
        "Straight Flush",
        "Royal Flush"
    ]

    @staticmethod
    def ofAKind(ranks, count):
        """Return cards if we have four card with the same rank"""
        for rank in ranks:
            if len(rank) == count:
                return rank
        return None

    @staticmethod
    def flush(suits):
        """Return cards if there are 5 of the same suit"""
        return suits[0] if len(suits[0]) > 4 else None

    @staticmethod
    def straight(ranks):
        """Return cards if there are 5 cards in a row"""
        def sort_key(item):
            """Sort ranks by rank value"""
            return item[0].rank

        straight = None
        # Sort the ranks by decreasing value.
        ranks = copy.deepcopy(ranks)
        ranks.sort(key=sort_key, reverse=True)
        if ranks[0][0].rank == 14:
            # If we have aces, treat them as both rank 14 and rank 1
            ranks.append([Card(1, card.suit) for card in ranks[0]])

        # look for 5 consecutive ranks
        for rank in ranks:
            if straight is None or straight[-1][0].rank - rank[0].rank > 1:
                straight = [rank]
            else:
                straight.append(rank)
                if len(straight) >= 5:
                    return straight
        return None

    @staticmethod
    def straightFlush(suits):
        """Return cards if there are 5 cards in a row with the same rank"""
        if (cards := Rules.flush(suits)) is not None:
            return Rules.straight(Hand(cards).ranks())
        return None

    @staticmethod
    def royalFlush(suits):
        """Return cards if there is an ace high straight flush"""
        if (cards := Rules.straightFlush(suits)) is not None and cards[0][0].rank == 14:
            return cards
        return None

    @staticmethod
    def fullHouse(ranks):
        """Return cards if there is a full house"""
        if len(ranks) > 1 and len(ranks[0]) >= 3 and len(ranks[1]) >= 2:
            return ranks[0] + ranks[1]
        return None

    @staticmethod
    def twoPair(ranks):
        """Return cards if there are two pairs"""
        if len(ranks) > 1 and len(ranks[0]) >= 2 and len(ranks[1]) >= 2:
            return ranks[0] + ranks[1]
        return None

    @staticmethod
    def highCard(ranks):
        """Return the high card"""
        high_rank = 0
        for rank in ranks:
            high = max(rank[0].rank, high_rank)
        return high_rank

    @classmethod
    def evaluate(cls, hand):
        ranks = hand.ranks()
        suits = hand.suits()
        high_card = cls.highCard(ranks)

        if (cards := cls.royalFlush(suits)):
            return cls.ROYAL_FLUSH, cards, high_card
        elif (cards := cls.straightFlush(suits)):
            return cls.STRAIGHT_FLUSH, cards, high_card
        elif (cards := cls.ofAKind(ranks, 4)):
            return cls.FOUR_OF_A_KIND, cards, high_card
        elif (cards := cls.fullHouse(ranks)):
            return cls.FULL_HOUSE, cards, high_card
        elif (cards := cls.flush(suits)):
            return cls.FLUSH, cards, high_card
        elif (cards := cls.straight(ranks)):
            return cls.STRAIGHT, cards, high_card
        elif (cards := cls.ofAKind(ranks, 3)):
            return cls.THREE_OF_A_KIND, cards, high_card
        elif (cards := cls.twoPair(ranks)):
            return cls.TWO_PAIR, cards, high_card
        elif (cards := cls.ofAKind(ranks, 2)):
            return cls.ONE_PAIR, cards, high_card
        else:
            return cls.HIGH_CARD, high_card, high_card

# Test different hands
flop = Hand([Card(card) for card in ["KS", "QS", "JS"]])

def test(*cards):
    hand = Hand([Card(card) for card in cards]) + flop
    result, cards, high_card = Rules.evaluate(hand)
    print(f"{cards}, {Rules.HAND_NAMES[result]}")

test("AC", "AD", "AH", "AS", "10S")
test("KC", "KD", "JH", "10S", "9S")
test("AC", "AD", "AH", "AS", "10H")
test("KC", "KD", "QH", "5S", "4H")
test("2S", "4S", "2C", "4C", "5S")
test("AD", "2D", "3H", "4C", "5C")
test("AC", "AD", "AH", "5H", "3D")
test("AC", "AD", "2S", "2D", "3D")
test("AS", "AD", "3H", "4C", "5C")
Output:
[[AS], [KS], [QS], [JS], [10S]], Royal Flush [[KS], [QS], [JS], [10S], [9S]], Straight Flush [AC, AD, AH, AS], Four of a Kind [KC, KD, KS, QH, QS], Full House [KS, QS, JS, 5S, 4S, 2S], Flush [[5C], [4C], [3H], [2D], [AD]], Straight [AC, AD, AH], Three of a Kind [AC, AD, 2S, 2D], Two Pair [AS, AD], One Pair
Reply
#22
A few improvements. Flattens cards list returned by Rules.evaluate. Generic Hand.groupby(attribute) instead of Hand.ranks() and Hand.suits(). Use intertools.product() to create deck instead of list comprehension. Added trump to cards, Got rid of a big, ugly if statement. 25% code reduction. Made Rules a normal class so you can have different types of rules and treat that generically.
import itertools
import copy
import random

class Card():
    """A playing card that has a rank and suit"""
    # These are class variables.  They are associated with the class, not instances of the class
    suit_names = ["Clubs", "Diamonds", "Hearts", "Spades"]
    short_suit_names = ["C", "D", "H", "S"]
    rank_names = ["", "Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King", "Ace"]
    short_rank_names = ["", "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
    rank_range = range(2, 15)

    def __init__(self, rank, suit=None, value=None):
        # These are instance variables.  Each instance has their own rank an suit
        if suit is None:
            # Assume rank is a card repr.  Extract rank and suit
            suit = Card.suit_names[Card.short_suit_names.index(rank[-1])]
            rank = Card.short_rank_names[2:].index(rank[:-1])+2
        self.rank = rank
        self.suit = suit
        self.trump = None

    @property
    def value(self):
        """Get value of card"""
        return self.rank + 14 if self.suit == self.trump else self.rank

    def __lt__(self, other):
        """For sorting and comparing by value"""
        return self.value < other.value

    def __gt__(self, other):
        """For sorting and comparing by value"""
        return self.value > other.value

    def __eq__(self, other):
        """For sorting and comparing by value"""
        return self.value == other.value

    def name(self):
        """Get long name for card"""
        return f"{self.rank_names[self.rank]} {self.suit}"

    def __repr__(self):
        """Get short name for card"""
        return f"{self.short_rank_names[self.rank]}{self.suit[0]}"


class Hand():
    """A list of cards"""
    def __init__(self, cards=None):
        self.cards = [] if cards is None else cards

    def groupby(self, attr='rank'):
        """Group cards by attribute.  Sort groups by len"""
        cards = sorted(self.cards, reverse=True)
        groups = {}
        for card in cards:
            value = getattr(card, attr)
            groups[value] = groups.get(value, []) + [card]
        return sorted(groups.values(), key=len, reverse=True)

    def __len__(self):
        """Return number of cards"""
        return len(self.cards)

    def __add__(self, cards):
        """Create a new hand by combining two existing hands"""
        if cards.__class__ == self.__class__:
            cards = cards.cards
        return self.__class__(self.cards + cards)

    def __repr__(self):
        """Print cards in hand"""
        return ", ".join([str(card) for card in self.cards])

    def set_trump(self, suit):
        """Set trump for all cards in hand"""
        for card in self.cards:
            card.trump = suit


class Deck(Hand):
    """Deck of cards"""
    def __init__(self, shuffle=False):
        super().__init__(itertools.product(Card.suit_names, Card.rank_range))
        if shuffle:
            random.shuffle(self.cards)

    def deal(self, count=1):
        """Deal count cards from deck"""
        cards = self.cards[:count]
        self.cards = self.cards[count:]
        return cards


class PokerRules():
    # Dictionary of poker hands; name: {value, rule}
    def __init__(self, hand=None):
        self.hands = {
            "Royal Flush":     {"value":9, "rule":self.royal_flush},
            "Straight Flush":  {"value":8, "rule":self.straight_flush},
            "Four of a Kind":  {"value":7, "rule":lambda: self.of_a_kind(4)},
            "Full House":      {"value":6, "rule":self.full_house},
            "Flush":           {"value":5, "rule":self.flush},
            "Straight":        {"value":4, "rule":self.straight},
            "Three of a Kind": {"value":3, "rule":lambda: self.of_a_kind(3)},
            "Two Pair":        {"value":2, "rule":self.two_pair},
            "One Pair":        {"value":1, "rule":self.one_pair},
            "High Card":       {"value":0, "rule":self.high_card}
        }
        self.hand = hand

    def of_a_kind(self, count):
        """Return cards if we have count+ with the same rank"""
        return self.ranks[0] if len(self.ranks[0]) >= count else None

    def flush(self, suits=None):
        """Return cards if there are 5 of the same suit"""
        suits = self.suits if suits is None else suits
        return suits[0] if len(suits[0]) > 4 else None

    def straight(self, ranks=None):
        """Return cards if there are 5 cards in a row"""
        # Aces can be both 14 and 1 for a straight
        ranks = self.ranks if ranks is None else ranks
        if ranks[0][0].rank == 14:
            ranks = copy.deepcopy(ranks) + [[Card(1, card.suit) for card in ranks[0]]]
        # look for 5 consecutive ranks
        start = 0
        for end in range(1, len(ranks)):
            if ranks[end-1][0].rank - ranks[end][0].rank > 1:
                start = end
            elif end - start >= 4:
                return list(itertools.chain(*ranks[start:end+1]))
        return None

    def straight_flush(self):
        """Return cards if there are 5 cards in a row with the same rank"""
        if (cards := self.flush()) is not None:
            return self.straight(Hand(cards).groupby("rank"))
        return None

    def royal_flush(self):
        """Return cards if there is an ace high straight flush"""
        if (cards := self.straight_flush()) is not None and cards[0].rank == 14:
            return cards
        return None

    def full_house(self):
        """Return cards if there is a full house"""
        if len(self.ranks) > 1 and len(self.ranks[0]) >= 3 and len(self.ranks[1]) >= 2:
            return self.ranks[0] + self.ranks[1]
        return None

    def two_pair(self):
        """Return cards if there are two pairs"""
        if len(self.ranks) > 1 and len(self.ranks[0]) >= 2 and len(self.ranks[1]) >= 2:
            return self.ranks[0] + self.ranks[1]
        return None

    def one_pair(self):
        """Return cards if there is a pair"""
        return self.ranks[0] if len(self.ranks[0]) >= 2 else None

    def high_card(self):
        """Return the high card"""
        return [sorted(self.hand.cards)[-1]]

    def evaluate(self, hand=None):
        """Return """
        if hand is not None:
            self.hand = hand
        self.ranks = self.hand.groupby('rank')
        self.suits = self.hand.groupby('suit')
        for key, value in self.hands.items():
            if (cards := value["rule"]()):
                return value["value"], key, cards

# Test different hands
flop = Hand([Card(card) for card in ["KS", "QS", "JS"]])

def test(*cards):
    hand = Hand([Card(card) for card in cards]) + flop
    value, result, cards = PokerRules().evaluate(hand)
    print(f"{cards}, {result}, {value}")

test("AC", "AD", "AH", "AS", "10S")
test("KC", "KD", "JH", "10S", "9S")
test("AC", "AD", "AH", "AS", "10H")
test("KC", "KD", "QH", "5S", "4H")
test("2S", "4S", "2C", "4C", "5S")
test("AD", "2D", "3H", "4C", "5C")
test("AC", "AD", "AH", "5H", "3D")
test("AC", "AD", "2S", "2D", "3D")
test("AS", "AD", "3H", "4C", "5C")
test("3H", "9C", "7D", "5S", "AH")
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Understanding Python classes PythonNewbee 3 1,195 Nov-10-2022, 11:07 PM
Last Post: deanhystad
  Understanding Python super() for classes OmegaRed94 1 1,840 Jun-09-2021, 09:02 AM
Last Post: buran
  Understanding program blocks newbieAuggie2019 2 1,978 Oct-02-2019, 06:22 PM
Last Post: newbieAuggie2019
  help with understanding a program prompt drasil 5 2,974 Feb-14-2019, 05:54 PM
Last Post: ichabod801
  Help, not understanding how classes work... Peter_EU 1 2,335 Jan-20-2018, 06:07 PM
Last Post: wavic
  Using classes? Can I just use classes to structure code? muteboy 5 5,077 Nov-01-2017, 04:20 PM
Last Post: metulburr
  I need help understanding how to use and run this program! Thanks in advance! tc1chosen 6 4,816 Sep-01-2017, 01:56 PM
Last Post: tc1chosen

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020