from __future__ import annotations from collections import Counter from dataclasses import dataclass from itertools import combinations from texas_holdem.cards import Card CATEGORY_NAMES = { 8: "straight_flush", 7: "four_of_a_kind", 6: "full_house", 5: "flush", 4: "straight", 3: "three_of_a_kind", 2: "two_pair", 1: "pair", 0: "high_card", } @dataclass(frozen=True, order=True, slots=True) class HandValue: category: int ranks: tuple[int, ...] @property def name(self) -> str: return CATEGORY_NAMES[self.category] def to_dict(self) -> dict[str, object]: return {"category": self.category, "name": self.name, "ranks": list(self.ranks)} def evaluate(cards: list[Card]) -> HandValue: if len(cards) < 5: raise ValueError("at least five cards are required") return max(_evaluate_five(list(combo)) for combo in combinations(cards, 5)) def _evaluate_five(cards: list[Card]) -> HandValue: ranks = sorted((card.rank for card in cards), reverse=True) counts = Counter(ranks) groups = sorted(counts.items(), key=lambda item: (item[1], item[0]), reverse=True) is_flush = len({card.suit for card in cards}) == 1 straight_high = _straight_high(ranks) if is_flush and straight_high is not None: return HandValue(8, (straight_high,)) if groups[0][1] == 4: quad_rank = groups[0][0] kicker = max(rank for rank in ranks if rank != quad_rank) return HandValue(7, (quad_rank, kicker)) if groups[0][1] == 3 and groups[1][1] == 2: return HandValue(6, (groups[0][0], groups[1][0])) if is_flush: return HandValue(5, tuple(ranks)) if straight_high is not None: return HandValue(4, (straight_high,)) if groups[0][1] == 3: trip_rank = groups[0][0] kickers = sorted((rank for rank in ranks if rank != trip_rank), reverse=True) return HandValue(3, (trip_rank, *kickers)) if groups[0][1] == 2 and groups[1][1] == 2: pair_ranks = sorted((rank for rank, count in counts.items() if count == 2), reverse=True) kicker = max(rank for rank in ranks if rank not in pair_ranks) return HandValue(2, (*pair_ranks, kicker)) if groups[0][1] == 2: pair_rank = groups[0][0] kickers = sorted((rank for rank in ranks if rank != pair_rank), reverse=True) return HandValue(1, (pair_rank, *kickers)) return HandValue(0, tuple(ranks)) def _straight_high(ranks: list[int]) -> int | None: unique = sorted(set(ranks), reverse=True) if {14, 5, 4, 3, 2}.issubset(unique): unique.append(1) for index in range(0, len(unique) - 4): window = unique[index : index + 5] if window[0] - window[4] == 4 and len(set(window)) == 5: return 5 if window == [5, 4, 3, 2, 1] else window[0] return None