1 """Define a poker table with players.
9 from deck import pp_hand, SevenChooseFiveHand
12 class IllegalBet (ValueError):
13 """Raised on invalid bet attempts.
15 def __init__(self, player, bet):
16 super(IllegalBet, self).__init__(bet)
21 return 'Illegal bet %s by %s' % (self.player, self.name)
24 class Player (object):
27 def __init__(self, cash=0, name='', brain=None):
28 self.cash = cash # uncommitted chips
35 self._proc = subprocess.Popen(
36 './'+self.brain, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
37 stderr=subprocess.PIPE, close_fds=True)
43 if self._proc != None:
45 self._proc.stdin.write('END\n')
46 self._proc.stdin.flush()
49 stdout,stderr = self._proc.communicate()
50 exit_code = self._proc.wait()
51 assert exit_code == 0, 'Error exiting from %s: %d\n%s' % (
52 self, exit_code, stderr)
59 return '<%s %s>' % (self.__class__.__name__, self.name)
61 def __cmp__(self, other):
64 return cmp(self.name, other.name)
66 def cmp_hands(self, other):
67 return cmp(self.hand, other.hand)
71 self.owe = 0 # amount required to call
72 self.pot = 0 # amount committed to the pot this round
73 self.status = 'in' # one of ['in', 'all in', 'fold']
74 self.hole = [] # pair of down cards
75 self.hand = None # holds the player's hand in the event of a showdown
78 """Does this player still need to think?"""
79 return self.status not in ['all in', 'fold']
82 """Settle what you owe the table.
84 Called during play as well as at the end of a hand.
86 if wager >= self.cash:
87 self.status = 'all in' # may be forced all-in (e.g. by a blind)
92 self.owe = min(self.owe, 0)
95 def log_flush(self, log):
96 """Show the brain any new log entries."""
98 stream = self._proc.stdin
102 for line in log[self._next_log_index:new_index]:
103 stream.write(line + '\n')
105 self._next_log_index = new_index
108 self._next_log_index = 0 # unsent log index
110 def _record_info(self, active_players, pot, min_raise, flop, turn, river,
112 """Show the brain the current state of events."""
113 self._proc.stdin.write('INFO NAME %s\n' % str(self.name))
114 self._proc.stdin.write('INFO STACK %s\n' % str(self.cash))
115 self._proc.stdin.write('INFO POT %s\n' % str(pot))
116 self._proc.stdin.write('INFO MINRAISE %s\n' % str(min_raise))
117 self._proc.stdin.write('INFO OWE %s\n' % str(self.owe))
118 for phase,h in [('HOLE', self.hole), ('FLOP', flop),
119 ('TURN', turn), ('RIVER', river)]:
121 self._proc.stdin.write('INFO %s %s\n' % (phase, pp_hand(h)))
122 self._proc.stdin.flush()
125 def _human_play(self, active_players, pot, min_raise, flop, turn, river,
127 """Ask the human at `sys.stdin`/`sys.stdout` for guidance."""
129 print '[', pp_hand(self.hole), '] Board: ',
130 print '[', pp_hand(flop), pp_hand(turn), pp_hand(river), ']'
131 print 'POT: ', pot, ' OWE: ', self.owe, ' MIN_RAISE: ', min_raise,
132 print ' | ', 'STACK: ', self.cash
133 for player in active_players:
135 print 'Opp. %s STACK: %d' % (player, player.cash)
136 return raw_input('What do you want to bet, %s:' % self)
138 def _bet(self, **kwargs):
140 return self._human_play(**kwargs)
142 self._record_info(**kwargs)
143 self._proc.stdin.write('MOVE\n')
144 self._proc.stdin.flush()
145 return self._proc.stdout.readline().strip()
147 def decide(self, **kwargs):
148 """Decide on a course of action (raise, fold, ...).
150 The program accepts an (A) as an all-in bet, (C) as a call or
151 check and any valid bet. Valid bets are either the amount
152 owed to the pot or at least the min raise. All bets made
153 greater then the players stack will be considered an all-in.
154 All bets that are less than the amount owed will be considered
155 a fold. A bet in the range of the amount owed to the min
156 raise will be considered a CALL and the player will put in the
157 amount owed to the table. Any other input (negative, decimal,
158 or alpha) will be considered to be a FOLD. To keep a
159 presistant state (and for speed reasons) your program will
160 have the information fed to it. This is done by piping
161 `stdin` and `stdout` to your program. The dealer will wait
162 once the command `MOVE` is sent. Along with the `MOVE` tag,
163 your code will be given information on the current game state
164 and history. See `Player.record_info()` for details.
166 bet = self._bet(**kwargs)
169 if bet == 'A': bet = self.cash
170 elif bet == 'C': bet = self.owe
171 else : self.status = 'fold'
174 except: self.status = 'fold'
177 self.status = 'all in'
182 elif self.owe <= bet and bet < min_raise: # round down to a check
184 elif self.owe >= min_raise: # let's see some action!
186 raise IllegalBet(self, str(bet))
189 class Blinds (object):
190 """Blind schedule and per-hand blind calculation.
192 >>> b = Blinds(blinds=[1, 2, 4, 8], hand_clock=10)
202 def __init__(self, blinds, hand_clock):
203 self._blinds = blinds
204 self._hand_clock = hand_clock
206 def __call__(self, hand):
207 """Small blind, big blind, and min raise for the `hand`th hand.
209 i = min(hand / self._hand_clock, len(self._blinds)-1)
210 small_blind = self._blinds[i]
211 return (small_blind, small_blind*2, small_blind*2)
214 class Table (object):
215 """A poker table with a deck, players, and blind schedule.
217 Setup buffered players so we can control `Table` testing.
219 >>> class BufferedPlayer(Player):
220 ... def __init__(self, **kwargs):
221 ... super(BufferedPlayer, self).__init__(**kwargs)
223 ... def _bet(self, **kwargs):
224 ... return self.buffer.pop(0)
225 >>> players = [BufferedPlayer(cash=10, name='P%d' % (i+1))
226 ... for i in range(3)]
227 >>> t = Table(players=players, blinds=Blinds((1, 2, 4), 3))
229 Stack the deck so we know what's coming.
230 >>> from deck import unpp_card
231 >>> t.deck = [unpp_card(c)
232 ... for c in 'As Ks Qs Js Ts 9s 8s 7s 6s 5s 4s'.split()]
234 Everyone folds the first round (P3, going last, will win).
235 >>> for p in t.players:
236 ... p.buffer = ['-1']
237 >>> t.play_round(shuffle=False)
238 >>> print '\\n'.join(t.log) # doctest: +REPORT_UDIFF
239 INFO CHIPCOUNT Player P1 10
240 INFO CHIPCOUNT Player P2 10
241 INFO CHIPCOUNT Player P3 10
242 ACTION Player P2 (pot 1) blinds 1
243 ACTION Player P3 (pot 2) blinds 2
244 ACTION Player P1 (pot 0) folds
245 ACTION Player P2 (pot 1) folds
246 ACTION Player P3 wins: 3
249 Play another round on the same deck, everyone goes all in. P3
250 will win with the higher straight flush.
251 >>> for p in t.players:
253 >>> t.play_round(shuffle=False)
254 >>> print '\\n'.join(t.log) # doctest: +REPORT_UDIFF
255 INFO CHIPCOUNT Player P1 10
256 INFO CHIPCOUNT Player P2 9
257 INFO CHIPCOUNT Player P3 11
258 ACTION Player P3 (pot 1) blinds 1
259 ACTION Player P1 (pot 2) blinds 2
260 ACTION Player P2 (pot 9) meets and raises for a total of 9
261 ACTION Player P3 (pot 11) goes all in with 10
262 ACTION Player P1 (pot 10) goes all in with 8
263 ACTION Player P3 wins: 30
264 INFO board 8s 7s 6s 5s 4s
265 INFO Player P3 hole: Ts 9s best: straight flush - Ts 9s 8s 7s 6s 5s 4s cash: 30
266 INFO Player P1 hole: As Ks best: straight flush - 8s 7s 6s 5s 4s As Ks cash: 0
267 INFO Player P2 hole: Qs Js best: straight flush - 8s 7s 6s 5s 4s Qs Js cash: 0
269 INFO Player P1 died after hand 2
270 INFO Player P2 died after hand 2
273 [(<BufferedPlayer P1>, <BufferedPlayer P2>)]
275 [<BufferedPlayer P3>]
277 def __init__(self, deck=None, players=None, blinds=None, verbose=False):
279 self.players = players # list of surviving players
281 self.verbose = verbose
282 self.dealer = 0 # index of player acting as dealer (dealer button)
284 self.dead_players = [] # list of players in the order they died
285 # e.g. [(lastplayer,), (AtiedForThird, BtiedForThird), (secondplayer,)]
287 def players_from(self, index):
288 """Rotate the list of players to start from `self.players[index]`.
290 >>> t = Table(players=['a', 'b', 'c', 'd'])
291 >>> t.players_from(0)
293 >>> t.players_from(1)
295 >>> t.players_from(2)
298 index = index % len(self.players)
299 return self.players[index:]+self.players[:index]
302 """Add a message to the table log."""
307 def play_round(self, shuffle=random.shuffle):
308 self.deal(shuffle=shuffle)
311 self.betting_round(first_round=True)
312 if self.hand_complete(): return self.judge()
314 self.flop = self.board[:3]
315 self._log('ACTION FLOP %s' % pp_hand(self.flop))
317 if self.hand_complete(): return self.judge()
319 self.turn = self.board[3:4]
320 self._log('ACTION TURN %s' % pp_hand(self.turn))
322 if self.hand_complete(): return self.judge()
324 self.river = self.board[4:5]
325 self._log('ACTION RIVER %s' % pp_hand(self.river))
327 if self.hand_complete(): return self.judge()
331 def deal(self, shuffle=random.shuffle):
332 """Shuffle the deck and deal two cards to each player."""
336 N = len(self.players)
337 self.board = [self.deck[n] for n in xrange(2*N,2*N+5)] # common cards
338 for i,player in enumerate(self.players):
340 self._log('INFO CHIPCOUNT Player %s %s'
341 % (player, player.cash))
342 player.hole = [self.deck[n] for n in xrange(2*i, 2*(i+1))]
343 player.hand = SevenChooseFiveHand(player.hole+self.board)
344 self.flop = self.turn = self.river = None
347 self.small_blind,self.big_blind,self.min_raise = self.blinds(
349 self.pot = 0 # total chips up for grabs
350 self.per_player_pot = 0 # needed to stay active
351 if len(self.players) > 2:
352 sb,bb = self.players_from(self.dealer+1)[0:2]
353 elif len(self.players) == 2:
354 # When only two players remain, special 'head-to-head' or
355 # 'heads up' rules are enforced and the blinds are posted
356 # differently. In this case, the person with the dealer
357 # button posts the small blind, while his/her opponent
358 # places the big blind.
359 sb,bb = self.players_from(self.dealer)[0:2]
361 raise Exception, "can't play with %d players" % len(self.players)
362 sb.owe = self.small_blind
363 self.square_player(sb, 'blinds')
364 bb.owe = self.big_blind
365 self.square_player(bb, 'blinds')
367 def square_player(self, player, verb, append_cost=True):
368 contribution = player.pay(player.owe)
369 self.pot += contribution
370 player.pot += contribution
371 self.per_player_pot = max(self.per_player_pot, player.pot)
372 string = 'ACTION Player %s (pot %s) %s' % (player, player.pot, verb)
373 if append_cost: string += ' %d' % contribution
377 def betting_round(self, first_round=False):
378 all_in = [p for p in self.players if p.status == 'all in']
379 folded = [p for p in self.players if p.status == 'fold']
381 if len(self.players) > 2:
382 if first_round == True: # start after the big blind
383 order = self.players_from(self.dealer+3)
384 else: # start with the small blind
385 order = self.players_from(self.dealer+1)
386 elif len(self.players) == 2: # 'head-to-head' rules
387 # The dealer acts first before the flop. After the flop,
388 # the dealer acts last and continues to do so for the
389 # remainder of the hand.
390 if first_round == True:
391 order = self.players_from(self.dealer)
393 order = self.players_from(self.dealer+1)
395 active = [p for p in order if p.active()]
397 min_raise = self.min_raise
398 raiser = None # no-one has raised yet this round
399 while (len(active) > 0): # i.e. continue until everyone folds/calls
400 player = active.pop(0)
401 player.owe = self.per_player_pot - player.pot
402 if player == raiser or (raiser == None
403 and player == tail and len(active) == 0):
406 action = player.decide(
407 active_players=[player]+active+all_in, pot=self.pot,
408 min_raise=min_raise, flop=self.flop, turn=self.turn,
409 river=self.river, log=self.log)
410 except IllegalBet, e:
412 'ACTION Player %s bets %s illegally and FOLDS. '
413 'Valid bets are %d or anything >= %d')
414 % (e.player, e.bet, e.player.owe, min_raise))
415 player.status = 'fold'
416 if player.status == 'fold':
417 player.owe = 0 # don't charge the folding player
418 folded.append(player)
419 self.square_player(player, 'folds', append_cost=False)
421 if action >= min_raise:
424 active.append(player)
425 verb = 'meets and raises for a total of'
426 elif player.status == 'all in':
427 all_in.append(player)
428 verb = 'goes all in with'
430 assert action == 0, action
432 if player == tail and action == 0:
434 active.append(player)
436 self.square_player(player, verb)
438 def hand_complete(self):
439 """Return `True` if there aren't enough active players for betting.
441 return len([p for p in self.players if p.active()]) <= 1
444 """Judge the winner and allocate the winnings."""
445 runners = [p for p in self.players if p.status != 'fold']
447 if len(runners) == 1: # everyone else folded, no need to show cards
448 folders = [p for p in self.players if p.status == 'fold']
449 self.pay_players(runners, folders)
450 else: # we need to look show hands
451 runners.sort(Player.cmp_hands, reverse=True) # sort high -> low
452 runcp = [p for p in runners]
453 losers = [p for p in self.players] # copy into a new list
454 # start from the top, breaking ties...
455 while len(runners) > 0:
456 winners = [runners.pop(0)]
457 losers.remove(winners[-1]) # remove from losers pool
458 while len(runners) > 0 and runners[0] == winners[0]:
459 winners.append(runners.pop(0)) # pop any winning tie
460 losers.remove(winners[-1]) # remove from losers pool
461 self.pay_players(winners, losers)
462 # allow the players to view the end result
463 flop,turn,river = self.board[:3], self.board[3:4], self.board[4:5]
464 self._log('INFO board %s' % pp_hand(self.board))
466 self._log('INFO Player %s hole: %s best: %s cash: %d'
467 % (p, pp_hand(p.hole), p.hand.pp_score(), p.cash))
468 self._log('INFO GAMEOVER %d' % self.hand_count)
470 # prepare for the next round
471 dead = [p for p in self.players if p.cash == 0]
473 self.dead_players.append(tuple(dead))
476 self.dealer = (self.dealer + 1) % len(self.players)
477 next_dealing_player = self.players[self.dealer]
478 while next_dealing_player.cash == 0: # next dealer is dead
479 # pass the dealer card to successor
480 self.dealer = (self.dealer + 1) % len(self.players)
481 next_dealing_player = self.players[self.dealer]
483 self._log('INFO Player %s died after hand %d'
484 % (p, self.hand_count))
485 self.players.remove(p)
486 self.dealer = self.players.index(next_dealing_player)
488 def pay_players(self, winners, losers):
489 winning_pots = sorted(set([p.pot for p in winners]))
490 winnings = dict().fromkeys([p.name for p in winners], 0)
491 for pot in winning_pots: # smallest to largest winning pots
493 for player in winners+losers:
494 contribution = min(player.pot, pot)
495 player.pot -= contribution
497 per_winner = int(floor(sum / len(winners)))
498 residual = sum % len(winners)
499 for player in winners:
500 player.cash += per_winner
501 winnings[player.name] += per_winner
502 # any extra chips go to the first players after the dealer
503 for player in self.players_from(self.dealer+1):
504 if residual == 0: break
505 if player in winners:
507 winnings[player.name] += 1
509 # drop the winners who are no longer invested in the pot
510 winners = [p for p in winners if p.pot > 0]
511 self.per_player_pot -= pot
512 for name,take in winnings.items():
514 self._log('ACTION Player %s wins: %s' % (name, take))