92 lines
2.8 KiB
Python
92 lines
2.8 KiB
Python
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
|