Add pbotlib.odds.
[poker.git] / pbot / deck.py
1 """Define a deck of cards, single-player scoring rules, and pretty-printing.
2 """
3
4 from .combinations import xunique_combinations
5
6
7 SUITS = ['spades', 'hearts', 'diamonds', 'clubs']
8 """Ordered list of suits.
9 """
10
11 FACE_CARDS = ['ten', 'jack', 'queen', 'king', 'ace']
12 """Ordered list of face cards.
13 """
14
15 def new_deck():
16     """Generate a new deck.
17
18     Cards in the deck are stored as `(n,m)`, where `n` is the rank
19     number `n=[0,12]`, and `m` is the suit `m=[0,3]`.
20
21     Examples:
22
23     * `(0,1)`  is a two of hearts, 
24     * `(11,0)` is a king of spades,
25     * `(12,3)` is an ace of clubs
26     
27     >>> deck = new_deck()
28     >>> deck[:6]
29     [(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1)]
30     >>> [pp_card(c) for c in deck[:6]]
31     ['2s', '2h', '2d', '2c', '3s', '3h']
32     """
33     return [(c/4, c%4) for c in range(52)]
34
35 def pp_card(card):
36     """Pretty-print a card.
37
38     >>> pp_card((0, 1))
39     '2h'
40     >>> pp_card((8, 3))
41     'Tc'
42     >>> pp_card((11, 0))
43     'Ks'
44     """
45     s = []
46     rank_num = card[0] + 2
47     if rank_num >= 10:
48         rank = FACE_CARDS[rank_num-10].capitalize()[0]
49     else:
50         rank = str(rank_num)
51     
52     suit = SUITS[card[1]]
53     return '%s%s' % (rank, suit[0])
54
55 def unpp_card(card):
56     """Un-pretty-print a card.
57
58     >>> unpp_card('2h')
59     (0, 1)
60     >>> unpp_card('Tc')
61     (8, 3)
62     >>> unpp_card('Ks')
63     (11, 0)
64     """
65     rank,suit = card
66     try:
67         rank = int(rank)
68     except ValueError:
69         rank = rank.lower()
70         rank = [i for i,r in enumerate(FACE_CARDS) if r.startswith(rank)][0]
71         rank += 10
72     rank -= 2
73     suit = [i for i,s in enumerate(SUITS) if s[0] == suit][0]
74     return (rank, suit)
75
76 def pp_hand(hand):
77     """Return a hand in a human readable format (pretty-print)
78
79     >>> pp_hand([(0, 1), (11, 0), (12, 3)])
80     '2h Ks Ac'
81     """
82     return ' '.join([pp_card(c) for c in hand])
83
84
85 class FiveCardHand (object):
86     """Poker hand with five cards.
87
88     To determine which five card hand is stronger according to
89     standard poker rules, each hand is given a score and a pair
90     rank. The score cooresponds to the poker hand (Straight Flush=8,
91     Four of a Kind=7, etc...). The pair score is used to break ties.
92     The pair score creates a set of pairs then rank sorts within each
93     set.
94
95     >>> h = FiveCardHand([unpp_card(c) for c in ['7h','5h','7s','5s','Ks']])
96     >>> h.score()
97     (2, [(2, [3, 5]), (1, [11])])
98     >>> h = FiveCardHand([unpp_card(c) for c in ['8h','8d','5s','As','5c']])
99     >>> h.score()
100     (2, [(2, [3, 6]), (1, [12])])
101     >>> h = FiveCardHand([unpp_card(c) for c in ['Ah','Kd','Qs','Ks','2c']])
102     >>> h.score()
103     (1, [(2, [11]), (1, [0, 10, 12])])
104     >>> h = FiveCardHand([unpp_card(c) for c in ['Ah','5d','4s','3s','2c']])
105     >>> h.score()
106     (4, [(1, [0, 1, 2, 3, 12])])
107     >>> h.pp_score()
108     'straight - Ah 5d 4s 3s 2c'
109     """
110     types = ['x high', 'pair 2', 'double pair 2', 'pair 3', 'straight',
111              'flush', 'full house', 'pair 4', 'straight flush']
112
113     def __init__(self, hand=None):
114         self.hand = hand
115
116     def __str__(self):
117         return pp_hand(self.hand)
118
119     def __cmp__(self, other):
120         return cmp(self.score(), other.score())
121
122     def _pair_score(self, ranks):
123         """Returns the pair scores from a list of card ranks in a hand.
124
125         For the sake of clarity, name the pairscore tuple elements
126         `(paircount,ranks)`.
127
128         >>> h = FiveCardHand()
129         >>> h._pair_score([5, 3, 5, 3, 11])
130         [(2, [3, 5]), (1, [11])]
131         >>> h._pair_score([8, 8, 5, 8, 5])
132         [(3, [8]), (2, [5])]
133         """
134         rank_counts = [(ranks.count(r), r) for r in set(ranks)]
135         counts = sorted(set([rc[0] for rc in rank_counts]), reverse=True)
136         return [(c, [rc[1] for rc in rank_counts if rc[0] == c])
137                 for c in counts]
138
139     def _is_full_house(self, pair_score, **kwargs):
140         """
141         >>> FiveCardHand()._is_full_house([(3, [8]), (2, [5])])
142         True
143         """
144         return [p[0] for p in pair_score] == [3, 2]
145
146     def _is_pair_4(self, pair_score, **kwargs):
147         return [p[0] for p in pair_score] == [4, 1]
148
149     def _is_pair_3(self, pair_score, **kwargs):
150         return [p[0] for p in pair_score] == [3, 1]
151
152     def _is_double_pair_2(self, pair_score, **kwargs):
153         return ([p[0] for p in pair_score] == [2, 1]
154                 and len(pair_score[0][1]) == 2)
155
156     def _is_pair_2(self, pair_score, **kwargs):
157         return ([p[0] for p in pair_score] == [2, 1]
158                 and len(pair_score[0][1]) == 1)
159
160     def _is_flush(self, suits, **kwargs):
161         return len(set(suits)) == 1
162
163     def _is_straight(self, ranks, **kwargs):  # Handles the low Ace as well
164         ranks = sorted(ranks)
165         diff  = [(x2-x1) for x1,x2 in zip(ranks, ranks[1:])]
166         return set(diff) == set([1]) or ranks == [0,1,2,3,12]
167
168     def _is_straight_flush(self, **kwargs):
169         return self._is_straight(**kwargs) and self._is_flush(**kwargs)
170
171     def _type_index(self, ranks, suits, pair_score):
172         for i,type in enumerate(reversed(self.types)):
173             if type == 'x high': continue
174             is_type = getattr(self, '_is_%s' % type.replace(' ', '_'))
175             if is_type(ranks=ranks, suits=suits, pair_score=pair_score):
176                 return len(self.types) - i - 1
177         return 0
178
179     def score(self):
180         if hasattr(self, '_score'): return self._score # return cached
181         ranks,suits = zip(*self.hand)
182         pair_score = self._pair_score(ranks)
183         type_index = self._type_index(ranks, suits, pair_score)
184         self._score = (type_index, pair_score) # cache
185         return self._score
186
187     def pp_score(self):
188         score = self.score()
189         type = self.types[score[0]]
190         return '%s - %s' % (type, str(self))
191
192
193 class SevenChooseFiveHand (FiveCardHand):
194     """Poker hand with seven cards.
195
196     To determine who wins a poker hand each player determines the best
197     hand they can make within their (7 choose 5) = 21 possible five card
198     combinations.
199
200     >>> h = SevenChooseFiveHand([unpp_card(c) for c in
201     ...     ['7h','5h','7s','5s','Ks', '7d', 'Jh']])
202     >>> h.full_hand
203     [(5, 1), (3, 1), (5, 0), (3, 0), (11, 0), (5, 2), (9, 1)]
204     >>> h.hand
205     [(5, 1), (3, 1), (5, 0), (3, 0), (5, 2)]
206     >>> h.score()
207     (6, [(3, [5]), (2, [3])])
208     >>> h.pp_score()
209     'full house - 7h 5h 7s 5s 7d Ks Jh'
210     """
211     # Prevent a bajillion calls to xunqiue_combinations
212     _hand_indices = list(xunique_combinations(range(7), 5))
213     """List of unique indices selecting 5 cards from a hand of 7.
214     """
215
216     def __init__(self, hand):
217         self.full_hand = hand
218         self.hand,self.residual = self.high_hand()
219
220     def __str__(self):
221         return pp_hand(self.hand + self.residual)
222
223     def high_hand(self):
224         score = hand = residual = None
225         for indices in self._hand_indices:
226             h = FiveCardHand([self.full_hand[i] for i in indices])
227             if h.score() > score:
228                 residual = [self.full_hand[i]
229                             for i in range(len(self.full_hand))
230                             if i not in indices]
231                 score = h.score()
232                 hand = h.hand
233         return (hand, residual)