-
Notifications
You must be signed in to change notification settings - Fork 0
/
texasholdem.py
383 lines (310 loc) · 14.5 KB
/
texasholdem.py
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
#!/usr/bin/env python
# coding: utf-8
import random, re
from itertools import combinations, chain
from collections import Counter
SUITS = ['♣', '♦', '♥', '♠']
SUITS_ASCII = ['C', 'D', 'H', 'S']
SUITS_LONG = ['clubs', 'diamonds', 'hearts', 'spades']
CARD_RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
CARD_RANKS_LONG = ['two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'jack', 'queen', 'king', 'ace']
STATES = ['compulsory bets', 'pre-flop', 'flop', 'turn', 'river', 'showdown']
HAND_RANKS = ['high card',
'pair',
'two pair',
'three of a kind',
'straight',
'flush',
'full house',
'four of a kind',
'straight flush',
'royal flush']
WIN_PROBABILITY_SAMPLE_SIZE = 10000
class Card:
def __init__(self, rank, suit):
if rank in range(13) and suit in range(4):
self.__card_rank = rank
self.__suit = suit
elif rank not in range(13):
raise ValueError(f'{rank} is not a valid card rank (an int in range(13))')
else:
raise ValueError(f'{suit} is not a valid card suit (an int in range(4))')
def rank(self):
return self.__card_rank
def suit(self):
return self.__suit
@classmethod
def from_string(cls, str):
return cls(CARD_RANKS.index(str[:-1].upper()), SUITS_ASCII.index(str[-1].upper()))
def __eq__(self, other):
return self.rank() == other.rank()
def __ne__(self, other):
return self.rank() != other.rank()
def __lt__(self, other):
return self.rank() < other.rank()
def __le__(self, other):
return self.rank() <= other.rank()
def __gt__(self, other):
return self.rank() > other.rank()
def __ge__(self, other):
return self.rank() >= other.rank()
def __repr__(self):
return f"<Card {CARD_RANKS[self.rank()]}{SUITS[self.suit()]}>"
def __str__(self):
return f"{CARD_RANKS[self.rank()]}{SUITS[self.suit()]}"
class Player:
def __init__(self, id, pocket=[]):
if len(pocket) < 3 and all(isinstance(x, Card) for x in pocket):
self.__id = id
self.__pocket = pocket
else:
raise ValueError(f'{pocket} is not a list of at most two Cards')
def id(self):
return self.__id
def pocket(self):
return self.__pocket
def reset_pocket(self):
self.__pocket = []
def add_pocket_card(self, card):
if len(self.__pocket) == 2:
raise ValueError(f'{self} pocket is full')
elif isinstance(card, Card):
self.__pocket.append(card)
else:
raise ValueError(f'{cards} is not a Card')
def __repr__(self):
return f"<Player id {self.id()}, pocket {self.pocket()}>"
def __str__(self):
return f"{self.id()}: {','.join([f'{card}' for card in self.pocket()])}"
class Hand:
def __init__(self, cards):
if len(cards) == 5 and all(isinstance(x, Card) for x in cards):
self.__cards = sorted(cards, reverse=True)
ranks = [card.rank() for card in self.cards()]
suits = [card.suit() for card in self.cards()]
rank_counts = Counter(ranks).most_common()
suit_counts = Counter(suits).most_common()
low_card = min(ranks)
ranks_set = frozenset(ranks)
if suit_counts[0][1] == 5 and ranks_set == frozenset([8, 9, 10, 11, 12]):
self.__hand_rank = 9 # royal flush
elif suit_counts[0][1] == 5 and ranks_set == frozenset(range(low_card, low_card + 5)):
self.__hand_rank = 8 # straight flush
elif rank_counts[0][1] == 4:
self.__hand_rank = 7 # four of a kind, with one kicker
elif rank_counts[0][1] == 3 and rank_counts[1][1] == 2:
self.__hand_rank = 6 # full house
elif suit_counts[0][1] == 5:
self.__hand_rank = 5 # flush
elif ranks_set == frozenset(range(low_card, low_card + 5)) or ranks_set == frozenset([0, 1, 2, 3, 12]):
self.__hand_rank = 4 # straight
elif rank_counts[0][1] == 3:
self.__hand_rank = 3 # three of a kind, with two kickers
elif rank_counts[0][1] == 2 and rank_counts[1][1] == 2:
self.__hand_rank = 2 # two pair, with one kicker
elif rank_counts[0][1] == 2:
self.__hand_rank = 1 # pair, with three kickers
else:
self.__hand_rank = 0 # high card
else:
raise ValueError(f'{cards} is not a list containing 5 Cards')
def cards(self):
return self.__cards
def rank(self):
return self.__hand_rank
def description(self):
card_ranks = [ count[0] for count in Counter([card.rank() for card in self.cards()]).most_common() ]
if self.rank() == 9: # royal flush
return f"royal flush"
elif self.rank() == 8: # straight flush
return f"{CARD_RANKS_LONG[card_ranks[0]]} high straight flush"
elif self.rank() == 7: # four of a kind
return f"{CARD_RANKS_LONG[card_ranks[0]]}s four of a kind, {CARD_RANKS_LONG[card_ranks[1]]} kicker"
elif self.rank() == 6: # full house
return f"{CARD_RANKS_LONG[card_ranks[0]]} over {CARD_RANKS_LONG[card_ranks[1]]}"
elif self.rank() == 5: # flush
return f"{CARD_RANKS_LONG[card_ranks[0]]} high flush"
elif self.rank() == 4 and self.cards()[0].rank() == 12 and self.cards()[1].rank() == 3: # ace low straight
return f"5 high straight"
elif self.rank() == 4: # straight
return f"{CARD_RANKS_LONG[card_ranks[0]]} high straight"
elif self.rank() == 3: # three of a kind
return f"{CARD_RANKS_LONG[card_ranks[0]]} trip, {', '.join([CARD_RANKS_LONG[r] for r in card_ranks[1:]])} kickers"
elif self.rank() == 2: # two pair
return f"{CARD_RANKS_LONG[card_ranks[0]]} up, {CARD_RANKS_LONG[card_ranks[-1]]} kicker"
elif self.rank() == 1: # pair
return f"pair {CARD_RANKS_LONG[card_ranks[0]]}, {', '.join([CARD_RANKS_LONG[r] for r in card_ranks[1:]])} kickers"
else: # high card
return f"{CARD_RANKS_LONG[card_ranks[0]]} high"
def value(self):
card_ranks = [ count[0] for count in Counter([card.rank() for card in self.cards()]).most_common() ]
if self.rank() == 4 and card_ranks[0] == 12 and card_ranks[1] == 3: # address ace low straight case
card_ranks = card_ranks[1:] + [-1]
return [ self.rank() ] + card_ranks
@classmethod
def from_string(cls, str):
return cls([Card.from_string(card_str) for card_str in re.findall(r'\d{0,1}\w{1,2}', str) ])
def __eq__(self, other):
return self.value() == other.value()
def __ne__(self, other):
return self.value() != other.value()
def __lt__(self, other):
return self.value() < other.value()
def __le__(self, other):
return self.value() <= other.value()
def __gt__(self, other):
return self.value() > other.value()
def __ge__(self, other):
return self.value() >= other.value()
def __repr__(self):
return f"<Hand {self.cards()}>"
def __str__(self):
ranks = [card.rank() for card in self.cards()]
rank_counts = Counter(ranks).most_common()
if self.rank() == 9: # royal flush
cards = sorted(self.cards(), key=lambda x: x.rank())
elif self.rank() == 8: # straight flush
cards = sorted(self.cards(), key=lambda x: x.rank())
elif self.rank() == 7: # four of a kind
cards = [card for card in self.cards() if card.rank() == rank_counts[0][0]]
cards += [card for card in self.cards() if card.rank() == rank_counts[1][0]]
elif self.rank() == 6: # full house
cards = [card for card in self.cards() if card.rank() == rank_counts[0][0]]
cards += [card for card in self.cards() if card.rank() == rank_counts[1][0]]
elif self.rank() == 5: # flush
cards = self.cards()
elif self.rank() == 4 and self.cards()[0].rank() == 12 and self.cards()[1].rank() == 3: # ace low straight
cards = self.cards()[1:] + self.cards()[:1]
elif self.rank() == 4: # straight
cards = self.cards()
elif self.rank() == 3: # three of a kind
cards = [card for card in self.cards() if card.rank() == rank_counts[0][0]]
cards += [card for card in self.cards() if card.rank() != rank_counts[0][0]]
elif self.rank() == 2: # two pair
cards = [card for card in self.cards() if card.rank() == rank_counts[0][0]]
cards += [card for card in self.cards() if card.rank() == rank_counts[1][0]]
cards += [card for card in self.cards() if not card in cards]
elif self.rank() == 1: # pair
cards = [card for card in self.cards() if card.rank() == rank_counts[0][0]]
cards += [card for card in self.cards() if card.rank() != rank_counts[0][0]]
else: # high card
cards = self.cards()
return ','.join([f"{card}" for card in cards])
class Deck:
def __init__(self):
self.__cards = []
for suit in range(len(SUITS)):
for rank in range(len(CARD_RANKS)):
self.__cards.append(Card(rank, suit))
def cards(self):
return self.__cards
def shuffle(self):
random.shuffle(self.__cards)
def deal(self):
card = self.__cards.pop()
return card
def __repr__(self):
return f"<Deck of {len(self.cards())} cards>"
class Game:
def __init__(self, players, board=[], deck=Deck()):
if not (1 < len(players) < 23 and all(isinstance(x, Player) for x in players)):
raise ValueError(f'{players} is not a list of between 1 and 23 Players')
elif not all(isinstance(x, Card) for x in board):
raise ValueError(f'{board} is not a list of Cards')
elif len(board) > 5:
raise ValueError(f'{board} has more than 5 Cards')
elif not isinstance(deck, Deck):
raise ValueError(f'{deck} is not a Deck')
else:
self.__players = players
self.__board = board
self.__deck = deck
self.__state = 0
self.__dealer = 0
def players(self):
return self.__players
def deck(self):
return self.__deck
def state(self):
return f"{STATES[self.__state]}"
def dealer(self):
return self.players()[self.__dealer]
def board(self):
return self.__board
def compulsory_bets(self):
self.__state = 0
self.__deck = Deck()
self.__deck.shuffle()
for player in self.players():
player.reset_pocket()
self.__dealer = (self.__dealer + 1) % len(self.players())
self.__board = []
def pre_flop(self):
self.__state += 1
for i in range(2):
for player in self.players():
player.add_pocket_card(self.__deck.deal())
def flop(self):
self.__state += 1
self.__deck.deal()
for i in range(3):
self.__board.append(self.__deck.deal())
def turn(self):
self.__state += 1
self.__deck.deal()
self.__board.append(self.__deck.deal())
def river(self):
self.__state += 1
self.__deck.deal()
self.__board.append(self.__deck.deal())
def showdown(self):
self.__state += 1
def play_hand(self):
self.compulsory_bets()
self.pre_flop()
self.flop()
self.turn()
self.river()
self.showdown()
def possible_boards(self):
remaining_draws = 5 - len(self.board())
pocket_cards = [ card for card in chain.from_iterable([player.pocket() for player in self.players()]) ]
seen_cards = [ f'{card}' for card in self.board() + pocket_cards ] # ugly hack as we made cards equal if their rank is equal
fresh_deck = Deck()
unseen_cards = [card for card in fresh_deck.cards() if not f'{card}' in seen_cards]
return [self.board() + list(draw) for draw in combinations(unseen_cards, remaining_draws)]
def possible_hands(self, player, board):
cards = player.pocket() + board
return [Hand(list(combination)) for combination in combinations(cards, 5)]
def best_possible_hand(self, player, board):
return sorted(self.possible_hands(player, board), reverse=True)[0]
def player_odds(self):
boards = self.possible_boards()
n_boards = len(boards)
if n_boards > WIN_PROBABILITY_SAMPLE_SIZE:
boards = random.sample(boards, WIN_PROBABILITY_SAMPLE_SIZE)
n_boards = WIN_PROBABILITY_SAMPLE_SIZE
results = [ sorted([ (i, self.best_possible_hand(player, board)) for i, player in enumerate(self.players()) ], key=lambda x: x[1], reverse=True) for board in boards ]
winners = [ [ result[i][0] for i in range(len(result)) if result[i][1] == result[0][1] ] for result in results ]
wins = Counter([ winner[0] for winner in winners if len(winner) == 1 ])
ties = Counter(list(chain.from_iterable([ winner for winner in winners if len(winner) > 1 ])))
return [ (player, float(wins[i])/float(n_boards), float(ties[i])/float(n_boards)) for i, player in enumerate(self.players()) ]
def is_a_tie(self):
current_odds = sorted(self.player_odds(), key=lambda x: x[1:], reverse=True)
return len(self.board()) == 5 and current_odds[0][2] == 1.0
def hand_summary(self):
current_odds = sorted(self.player_odds(), key=lambda x: x[1:], reverse=True)
for player_odds in current_odds:
player = player_odds[0]
win_probability = player_odds[1]
tie_probability = player_odds[2]
if len(self.board()) == 5:
best_hand = self.best_possible_hand(player, self.board())
best_hand_description = f" \u2192 {best_hand} ({best_hand.description()})"
print(f'{str(player):<20} {win_probability:>7.2%} win {tie_probability:>7.2%} tie {str(best_hand):>18} ({best_hand.description()})')
else:
print(f'{str(player):<20} {win_probability:>7.2%} win {tie_probability:>7.2%} tie')
def __repr__(self):
return f"<Game {self.state()} {','.join([f'{card}' for card in self.board()])}>"
def __str__(self):
return f"{self.state()} {','.join([f'{card}' for card in self.board()])}"