Source code for maverick.utils.scoring

from typing import Tuple, TYPE_CHECKING
from itertools import combinations

if TYPE_CHECKING:  # pragma: no cover
    from ..card import Card

from ..enums import HandType


__all__ = ["score_hand", "find_highest_scoring_hand"]


def _check_four_of_a_kind(numbers: list[int]) -> float:
    four_val = None
    kickers = []
    for i in numbers:
        if numbers.count(i) == 4:
            four_val = i
        else:
            kickers.append(i)
    kickers = sorted(set(kickers), reverse=True)
    score = 800 + four_val
    score += sum([kickers[i] / (100 ** (i + 1)) for i in range(len(kickers))])
    return score


def _check_full_house(numbers: list[int]) -> float:
    triple_val = None
    pair_val = None
    for i in numbers:
        if numbers.count(i) == 3:
            triple_val = i
        elif numbers.count(i) == 2:
            pair_val = i
    score = 700 + triple_val + pair_val / 100
    return score


def _check_three_of_a_kind(numbers: list[int]) -> float:
    triple_val = None
    kickers = []
    for i in numbers:
        if numbers.count(i) == 3:
            triple_val = i
        else:
            kickers.append(i)
    kickers = sorted(set(kickers), reverse=True)
    score = 400 + triple_val
    score += sum([kickers[i] / (100 ** (i + 1)) for i in range(len(kickers))])
    return score


def _check_two_pair(numbers: list[int]) -> float:
    pairs = []
    kickers = []
    for i in numbers:
        if numbers.count(i) == 2:
            pairs.append(i)
        elif numbers.count(i) == 1:
            kickers.append(i)
    pairs = sorted(set(pairs), reverse=True)
    kickers = sorted(set(kickers), reverse=True)
    score = 300 + pairs[0] + pairs[1] / 100
    score += sum([kickers[i] / (100 ** (i + 2)) for i in range(len(kickers))])
    return score


def _check_pair(numbers: list[int]) -> float:
    pair_val = None
    kickers = []
    for i in numbers:
        if numbers.count(i) == 2:
            pair_val = i
        else:
            kickers.append(i)
    kickers = sorted(set(kickers), reverse=True)
    score = 200 + pair_val
    score += sum([kickers[i] / (100 ** (i + 1)) for i in range(len(kickers))])
    return score


[docs] def score_hand(hand: list["Card"]) -> Tuple["HandType", float]: """ Classifies and scores a poker hand. Works with any number of cards (not just 5). Returns (HandType, float_score) where higher scores = stronger hands. Hand ranking (base scores): - High Card: 100+ - Pair: 200+ - Two Pair: 300+ - Three of a Kind: 400+ - Straight: 500+ - Flush: 600+ - Full House: 700+ - Four of a Kind: 800+ - Straight Flush: 900+ - Royal Flush: 1000 Parameters ---------- hand : list[Card] The list of cards in the hand. Returns ------- tuple[HandType, float] A tuple containing the hand type and its score. """ assert len(hand) > 0, "At least one card is required to score a hand." # Extract suit and rank values suit_values = [card.suit.value for card in hand] rank_values = [card.rank.value for card in hand] # Count repetitions rnum = [rank_values.count(i) for i in rank_values] rlet = [suit_values.count(i) for i in suit_values] # Check for flush (all same suit, and at least 5 cards) is_flush = max(rlet) >= 5 # Check for straight and find highest card in straight unique_ranks = sorted(set(rank_values)) is_straight = False straight_high_card = 0 if len(unique_ranks) >= 5: # Check if any 5 consecutive ranks exist for i in range(len(unique_ranks) - 4): if unique_ranks[i + 4] - unique_ranks[i] == 4: is_straight = True straight_high_card = unique_ranks[i + 4] # Special case: A-2-3-4-5 (wheel) - counts as 5-high, not ace-high if set([14, 2, 3, 4, 5]).issubset(set(unique_ranks)): is_straight = True if straight_high_card == 0: # Only wheel, no higher straight straight_high_card = 5 # Wheel is 5-high handtype = HandType.HIGH_CARD score = 0.0 # Royal Flush: A-K-Q-J-10 all same suit if ( is_flush and is_straight and set([14, 13, 12, 11, 10]).issubset(set(rank_values)) ): handtype = HandType.ROYAL_FLUSH score = 1000.0 # Straight Flush elif is_flush and is_straight: handtype = HandType.STRAIGHT_FLUSH score = 900 + straight_high_card / 100 # Four of a Kind elif 4 in rnum: handtype = HandType.FOUR_OF_A_KIND score = _check_four_of_a_kind(rank_values) # Full House elif 3 in rnum and 2 in rnum: handtype = HandType.FULL_HOUSE score = _check_full_house(rank_values) # Flush elif is_flush: handtype = HandType.FLUSH n = sorted(rank_values, reverse=True) score = 600 + sum([n[i] / (100 ** (i + 1)) for i in range(len(n))]) # Straight elif is_straight: handtype = HandType.STRAIGHT score = 500 + straight_high_card / 100 # Three of a Kind elif 3 in rnum: handtype = HandType.THREE_OF_A_KIND score = _check_three_of_a_kind(rank_values) # Two Pair elif rnum.count(2) >= 4: # At least 2 pairs handtype = HandType.TWO_PAIR score = _check_two_pair(rank_values) # Pair elif 2 in rnum: handtype = HandType.PAIR score = _check_pair(rank_values) # High Card else: handtype = HandType.HIGH_CARD n = sorted(rank_values, reverse=True) score = 100 + sum([n[i] / (100 ** (i + 1)) for i in range(len(n))]) return handtype, score
def find_highest_scoring_hand( private_cards: list["Card"], community_cards: list["Card"], n_private: int = 0, ) -> tuple[list["Card"], "HandType", float]: """ Find the highest scoring 5-card hand from the given private and community cards with at least n_private cards from the private cards. Parameters ---------- private_cards : list[Card] The player's private cards. community_cards : list[Card] The community cards on the table. n_private : int, optional The number of private cards that must be included in the hand (default is 0). A value of 0 means any number of private cards can be used. Returns ------- tuple[list[Card], HandType, float] A tuple containing the best 5-card hand, its hand type, and its score. """ all_cards = private_cards + community_cards # If we have 5 or fewer cards total, return all of them if len(all_cards) <= 5: return all_cards, *score_hand(all_cards) best_hand = None best_score = -1.0 n_card = min(5, len(all_cards)) # Try all 5-card combinations for hand in combinations(all_cards, n_card): # Count how many private cards are in this hand private_count = sum(1 for card in hand if card in private_cards) # Skip if doesn't meet minimum private card requirement if (n_private > 0) and (private_count != n_private): continue # Score this hand _, score = score_hand(list(hand)) # Update best hand if this one is better if score > best_score: best_score = score best_hand = hand if best_hand is None: return [], None, 0.0 # Return the best hand found return list(best_hand), *score_hand(list(best_hand))