import random from collections import Counter from dataclasses import dataclass card_values = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] ranks = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A'] rank_value = dict(zip(ranks, card_values)) value_rank = dict(zip(card_values, ranks)) suits = ['c', 'd', 'h', 's'] hand_values = {'hc': 1, 'pair': 2, '2pair': 3, '3ok': 4, 'straight': 5, 'flush': 6, 'boat': 7, '4ok': 8, 'straight_flush': 9 } HAND_REGISTRY = [] ##### CLASSES ##### @dataclass class Card: def __init__(self, card_str): self.rank = str(card_str[0]) self.suit = card_str[1] self.name = self.rank + self.suit self.value = rank_value[self.rank] def __str__(self): return self.name @property def pretty_name(self): rank = '10' if self.rank == 'T' else self.rank suit_map = { "c": "♣", "d": "♦", "h": "♥", "s": "♠", } return f'{rank}{suit_map[self.suit]}' def __getitem__(self, item): if item == 'rank': return self.rank elif item == 'suit': return self.suit elif item == 'name': return self.name elif item == 'value': return self.value @dataclass() class Hand: def __init__(self, type, high_value, low_value = 0, kicker=0): """Type = name of hand (e.g. Pair) value = value of the hand (i.e. Straight Flush is the most valuable) high_value = value. either the high card in straight or flush, the set in full house, the top pair in 2pair, etc low_value = the lower pair in 2 pair or the pair in a full house kicker = value of the kicker in the hand. Can be null """ kicker_rank = value_rank[kicker] if kicker in card_values else 0 low_rank = value_rank[low_value] if low_value in card_values else 0 self.type = type self.hand_value = hand_values[type] self.kicker = kicker self.kicker_rank = kicker_rank self.high_value = high_value self.high_rank = value_rank[self.high_value] self.low_value = low_value self.low_rank = low_rank def __str__(self): return f'{self.type}-{self.high_rank}' def __getitem__(self, item): if item in ['hand_value', 'high_value']: return self.high_value elif item == 'high_rank': return self.high_rank elif item == 'kicker': return self.kicker elif item == 'kicker_rank': return self.kicker_rank elif item == 'low_rank': return self.low_rank elif item == 'low_value': return self.low_value elif item == 'type': return self.type class Deck(list): def __init__(self, deck): self.deck = deck def __getitem__(self, item): return self.deck[item] def __iter__(self): yield from self.deck def __len__(self): return len(self.deck) def deal_card(self): """Select a random card from the deck. Return the card and the deck with the card removed""" i = random.randint(0, len(self)-1) card = self[i] self.deck.pop(i) return card, self def update_deck(self, card): """Remove card from deck""" deck_names = [card.name for card in self.deck] card_name = card.name if isinstance(card, Card) else card deck_idx = deck_names.index(card_name) self.deck.pop(deck_idx) ##### USEFUL FUNCTIONS ##### def register(func): """Add a function to the hand register""" HAND_REGISTRY.append(func) return func def make_card(input_list): """Input_list is either a list of Card objects or string Objects. If Cards, return the cards. If string, convert to Card and return""" if len(input_list) == 0: return input_list elif isinstance(input_list[0], Card): return input_list else: card_list = [Card(card) for card in input_list] return card_list def generate_deck(): deck = [] for rank in ranks: for suit in suits: card_str = rank + suit _card = Card(card_str) deck.append(_card) deck = Deck(deck) return deck ##### POKER ##### def find_multiple(hand, board, n=2): """Is there a pair, three of a kind, four of a kind/?""" hand = make_card(hand) board = make_card(board) multiple = False multiple_hand = None total_hand = hand + board values = [card.value for card in total_hand] c = Counter(values) for value in set(values): if c[value] == 2 and n == 2: multiple = True hand_type = 'pair' high_value = value low_value = max([value for value in values if value != high_value]) kicker = max([value for value in values if value not in [high_value, low_value]]) multiple_hand = Hand(hand_type, high_value, low_value=low_value, kicker=kicker) return multiple_hand elif c[value] == 3 and n == 3: multiple = True hand_type = '3ok' high_value = value low_value = max([foo for foo in values if foo != high_value]) kicker = max([bar for bar in values if bar not in [high_value, low_value]]) multiple_hand = Hand(hand_type, high_value, low_value=low_value, kicker=kicker) return multiple_hand elif c[value] == 4 and n == 4: multiple = True hand_type = '4ok' high_value = value low_value = max([value for value in values if value != high_value]) multiple_hand = Hand(hand_type, high_value, low_value=low_value) return multiple_hand return multiple def evaluate_straight(values): """Evaluates a list of card values to determine whether there are 5 consecutive values""" straight = False count = 0 straight_hand_values = [] sranks = [bit for bit in reversed(range(2, 15))] sranks.append(14) for rank in sranks: if rank in values: count += 1 straight_hand_values.append(rank) if count == 5: straight = True return straight, straight_hand_values else: count = 0 straight_hand_values = [] return straight, straight_hand_values @register def find_straight_flush(hand, board): """Find a straight flush in a given hand/board combination""" hand = make_card(hand) board = make_card(board) straight_flush = False flush = find_flush(hand, board) if flush: total_hand = hand + board total_hand = [card for card in total_hand] hand_suits = [card.suit for card in total_hand] c = Counter(hand_suits) flush_suit = c.most_common(1)[0][0] flush_hand = [card.value for card in total_hand if card.suit == flush_suit] straight_flush, straight_hand = evaluate_straight(flush_hand) if straight_flush: high_value = max(straight_hand) hand_type = 'straight_flush' straight_flush_hand = Hand(hand_type,high_value) return straight_flush_hand else: return straight_flush else: return straight_flush @register def find_quads(hand, board): quads = find_multiple(hand, board, n=4) return quads @register def find_full_house(hand, board): """Is there a full house?""" hand = make_card(hand) board = make_card(board) boat = False boat_hand = None total_hand = hand + board values = [card.value for card in total_hand] c = Counter(values) for value in set(values): if c[value] == 3: high_value = value c.pop(value) for value in set(values): if c[value] > 1: low_value = value kicker = max([value for value in values if value != high_value and value != low_value]) boat_hand = Hand('boat', high_value, low_value=low_value, kicker=kicker) boat = True return boat_hand return boat @register def find_flush(hand, board): """Does any combination of 5 cards in hand or on board amount to 5 of the same suit""" hand = make_card(hand) board = make_card(board) total_hand = hand + board total_hand_suits = [card.suit for card in total_hand] flush = False c = Counter(total_hand_suits) for suit in total_hand_suits: if c[suit] >= 5: flush = True if flush: flush_cards = [card for card in total_hand if card.suit == c.most_common(1)[0][0]] high_value = max([card.value for card in flush_cards]) flush_hand = Hand('flush', high_value) return flush_hand else: return flush @register def find_straight(hand, board): """Find a straight in a given hand/board combination""" hand = make_card(hand) board = make_card(board) straight = False straight_hand = None high_value = 2 reqd_hand_size = 5 # required hand size gives us some flexibility at the cost of more lines. could be more efficient if we say 'if len(values)<5' total_hand = hand + board values = [*set(card.value for card in total_hand)] slices = len(values) - reqd_hand_size if slices < 0: return straight else: straight, straight_hand_values = evaluate_straight(values) if straight: hand_type = 'straight' if 14 in straight_hand_values: # all([5,14]) does not work here so using nested ifs. if 5 in straight_hand_values: high_value = 5 else: high_value = max(straight_hand_values) straight_hand = Hand(hand_type, high_value) return straight_hand else: return straight @register def find_trips(hand, board): trips = find_multiple(hand, board, n=3) return trips @register def find_two_pair(hand, board): """Is there two-pair?""" hand = make_card(hand) board = make_card(board) two_pair = False # two_pair_hand = None total_hand = hand + board values = [card.value for card in total_hand] c = Counter(values) for value in values: if c[value] > 1: pair1 = Hand('pair', value) c.pop(value) for value in values: if c[value] > 1: pair2 = Hand('pair', value) kicker = max([value for value in values if value != pair1.high_value and value != pair2.high_value]) two_pair_hand = Hand('2pair', max(pair1.high_value, pair2.high_value), low_value=min(pair1.high_value, pair2.high_value), kicker=kicker) two_pair = True return two_pair_hand return two_pair @register def find_pair(hand, board): pair = find_multiple(hand, board, n=2) return pair @register def find_high_card(hand, board): hand = make_card(hand) board = make_card(board) total_hand = hand + board total_hand_values = [card.value for card in total_hand] total_hand_values.sort() high_value = total_hand_values[-1] low_value = total_hand_values[-2] kicker = total_hand_values[-3] high_card_hand = Hand('hc', high_value,low_value=low_value, kicker=kicker) return high_card_hand