aboutsummaryrefslogtreecommitdiff
path: root/poker/rank/hands.py
blob: 6a81af5addb3d997161b73094747c6267bbd1f1e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
from typing import Optional, Union

from poker.constants import Rank, Value
from poker.models import Card, Hand, RankedHand
from poker.rank.descriptions import DESCRIPTIONS


def _is_flush(cards: list[Card]) -> bool:
    """Calculate whether hand is a flush."""
    return len(set(card.suit for card in cards)) == 1


def _is_straight(cards: list[Card]) -> Optional[Value]:
    """Calculate whether hand is a straight.

    Arguments:
        cards: List of cards in hand.

    Returns:
        High card if hand is a straight, None otherwise.
    """
    card_values = sorted([card.value for card in cards])

    # If card values are not unique it is not a straight
    if len(set(card_values)) != len(card_values):
        return None

    # Check for a ace low straight
    if card_values == [Value.TWO, Value.THREE, Value.FOUR, Value.FIVE, Value.ACE]:
        return Value.FIVE

    # If all card differences are 1 it is a straight
    diffs = [second - first for first, second in zip(card_values[:-1], card_values[1:])]
    if set(diffs) == {1}:
        return max(cards).value

    return None


def royal_flush(hand: Hand) -> dict[str, Union[str, int]]:
    """Calculate whether hand a royal flush."""
    if not _is_flush(hand.cards):
        return {}

    values = [card.value for card in hand.cards]
    _is = all(
        value in values
        for value in [Value.ACE, Value.KING, Value.QUEEN, Value.JACK, Value.TEN]
    )
    if _is:
        return {"suit": hand.cards[0].suit}

    return {}


def straight_flush(hand: Hand) -> dict[str, Union[str, int]]:
    """Calculate whether hand a straight flush."""
    high = _is_straight(hand.cards)
    if high is not None and _is_flush(hand.cards):
        return {
            "high": high,
            "suit": hand.cards[0].suit,
        }

    return {}


def four_of_a_kind(hand: Hand) -> dict[str, Union[str, int]]:
    """Calculate whether hand has four of a kind."""
    if hand.value_counts[0][1] == 4:
        return {"value": hand.value_counts[0][0]}

    return {}


def full_house(hand: Hand) -> dict[str, Union[str, int]]:
    """Calculate whether hand is a full house."""
    if hand.value_counts[0][1] == 3 and hand.value_counts[1][1] == 2:
        return {
            "trips": hand.value_counts[0][0],
            "pair": hand.value_counts[1][0],
        }

    return {}


def flush(hand: Hand) -> dict[str, Union[str, int]]:
    """Calculate whether hand is a flush."""
    high = _is_straight(hand.cards)
    if _is_flush(hand.cards) and high is None:
        return {"suit": hand.cards[0].suit}

    return {}


def straight(hand: Hand) -> dict[str, Union[str, int]]:
    """Calculate whether hand is a flush."""
    high = _is_straight(hand.cards)
    if high is not None and not _is_flush(hand.cards):
        return {"high": high}

    return {}


def three_of_a_kind(hand: Hand) -> dict[str, Union[str, int]]:
    """Calculate whether hand is a full house."""
    if (
        hand.value_counts[0][1] == 3
        and hand.value_counts[1][1] == 1
        and hand.value_counts[2][1] == 1
    ):
        return {"value": hand.value_counts[0][0]}

    return {}


def two_pair(hand: Hand) -> dict[str, Union[str, int]]:
    """Calculate whether hand is a two pair."""
    if hand.value_counts[0][1] == 2 and hand.value_counts[1][1] == 2:
        values = [hand.value_counts[0][0], hand.value_counts[1][0]]
        return {
            "high": max(values),
            "low": min(values),
        }

    return {}


def pair(hand: Hand) -> dict[str, Union[str, int]]:
    """Calculate whether hand is a pair."""
    if hand.value_counts[0][1] == 2 and hand.value_counts[1][1] == 1:
        return {"value": hand.value_counts[0][0]}

    return {}


def high_card(hand: Hand) -> dict[str, Union[str, int]]:
    """Get high card."""
    return {"value": max(hand.cards).value}


_FUNCTIONS = {
    Rank.ROYAL_FLUSH: royal_flush,
    Rank.STRAIGHT_FLUSH: straight_flush,
    Rank.FOUR_OF_A_KIND: four_of_a_kind,
    Rank.FULL_HOUSE: full_house,
    Rank.FLUSH: flush,
    Rank.STRAIGHT: straight,
    Rank.THREE_OF_A_KIND: three_of_a_kind,
    Rank.TWO_PAIR: two_pair,
    Rank.PAIR: pair,
    Rank.HIGH_CARD: high_card,
}


def rank_hand(hand: Hand) -> RankedHand:
    """Get hand rank.

    TODO: Use a factory pattern to avoid this huge flow control.
    """
    out = None
    for rank, func in _FUNCTIONS.items():
        result = func(hand)
        if result:
            out = RankedHand(
                cards=hand.cards,
                rank=rank,
                description=DESCRIPTIONS[rank].format(**result),
            )

    if out is None:
        raise ValueError("No rank found.")

    return out