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:
44 self._proc.stdin.write('END\n')
45 self._proc.stdin.flush()
46 exit_code = self._proc.wait()
47 assert exit_code == 0, 'Error exiting from %s: %d' % (
55 return '<%s %s>' % (self.__class__.__name__, self.name)
57 def __cmp__(self, other):
60 return cmp(self.name, other.name)
62 def cmp_hands(self, other):
63 return cmp(self.hand, other.hand)
67 self.owe = 0 # amount required to call
68 self.pot = 0 # amount committed to the pot this round
69 self.status = 'in' # one of ['in', 'all in', 'fold']
70 self.hole = [] # pair of down cards
71 self.hand = None # holds the player's hand in the event of a showdown
74 """Does this player still need to think?"""
75 return self.status not in ['all in', 'fold']
78 """Settle what you owe the table.
80 Called during play as well as at the end of a hand.
82 if wager >= self.cash:
83 self.status = 'all in' # may be forced all-in (e.g. by a blind)
88 self.owe = min(self.owe, 0)
91 def log_flush(self, log):
92 """Show the brain any new log entries."""
94 stream = self._proc.stdin
98 for line in log[self._next_log_index:new_index]:
99 stream.write(line + '\n')
101 self._next_log_index = new_index
104 self._next_log_index = 0 # unsent log index
106 def _record_info(self, active_players, pot, min_raise, flop, turn, river,
108 """Show the brain the current state of events."""
109 self._proc.stdin.write('INFO NAME %s\n' % str(self.name))
110 self._proc.stdin.write('INFO STACK %s\n' % str(self.cash))
111 self._proc.stdin.write('INFO POT %s\n' % str(pot))
112 self._proc.stdin.write('INFO MINRAISE %s\n' % str(min_raise))
113 self._proc.stdin.write('INFO OWE %s\n' % str(self.owe))
114 for phase,h in [('HOLE', self.hole), ('FLOP', flop),
115 ('TURN', turn), ('RIVER', river)]:
117 self._proc.stdin.write('INFO %s %s\n' % (phase, pp_hand(h)))
118 self._proc.stdin.flush()
121 def _human_play(self, active_players, pot, min_raise, flop, turn, river,
123 """Ask the human at `sys.stdin`/`sys.stdout` for guidance."""
125 print '[', pp_hand(self.hole), '] Board: ',
126 print '[', pp_hand(flop), pp_hand(turn), pp_hand(river), ']'
127 print 'POT: ', pot, ' OWE: ', self.owe, ' MIN_RAISE: ', min_raise,
128 print ' | ', 'STACK: ', self.cash
129 for player in active_players:
131 print 'Opp. %s STACK: %d' % (player, player.cash)
132 return raw_input('What do you want to bet, %s:' % self)
134 def _bet(self, **kwargs):
136 return self._human_play(**kwargs)
138 self._record_info(**kwargs)
139 self._proc.stdin.write('MOVE\n')
140 self._proc.stdin.flush()
141 return self._proc.stdout.readline().strip()
143 def decide(self, **kwargs):
144 """Decide on a course of action (raise, fold, ...).
146 The program accepts an (A) as an all-in bet, (C) as a call or
147 check and any valid bet. Valid bets are either the amount
148 owed to the pot or at least the min raise. All bets made
149 greater then the players stack will be considered an all-in.
150 All bets that are less than the amount owed will be considered
151 a fold. A bet in the range of the amount owed to the min
152 raise will be considered a CALL and the player will put in the
153 amount owed to the table. Any other input (negative, decimal,
154 or alpha) will be considered to be a FOLD. To keep a
155 presistant state (and for speed reasons) your program will
156 have the information fed to it. This is done by piping
157 `stdin` and `stdout` to your program. The dealer will wait
158 once the command `MOVE` is sent. Along with the `MOVE` tag,
159 your code will be given information on the current game state
160 and history. See `Player.record_info()` for details.
162 bet = self._bet(**kwargs)
165 if bet == 'A': bet = self.cash
166 elif bet == 'C': bet = self.owe
167 else : self.status = 'fold'
170 except: self.status = 'fold'
173 self.status = 'all in'
178 elif self.owe <= bet and bet < min_raise: # round down to a check
180 elif self.owe >= min_raise: # let's see some action!
182 raise IllegalBet(self, str(bet))
185 class Blinds (object):
186 """Blind schedule and per-hand blind calculation.
188 >>> b = Blinds(blinds=[1, 2, 4, 8], hand_clock=10)
198 def __init__(self, blinds, hand_clock):
199 self._blinds = blinds
200 self._hand_clock = hand_clock
202 def __call__(self, hand):
203 """Small blind, big blind, and min raise for the `hand`th hand.
205 i = min(hand / self._hand_clock, len(self._blinds)-1)
206 small_blind = self._blinds[i]
207 return (small_blind, small_blind*2, small_blind*2)
210 class Table (object):
211 """A poker table with a deck, players, and blind schedule.
213 Setup buffered players so we can control `Table` testing.
215 >>> class BufferedPlayer(Player):
216 ... def __init__(self, **kwargs):
217 ... super(BufferedPlayer, self).__init__(**kwargs)
219 ... def _bet(self, **kwargs):
220 ... return self.buffer.pop(0)
221 >>> players = [BufferedPlayer(cash=10, name='P%d' % (i+1))
222 ... for i in range(3)]
223 >>> t = Table(players=players, blinds=Blinds((1, 2, 4), 3))
225 Stack the deck so we know what's coming.
226 >>> from deck import unpp_card
227 >>> t.deck = [unpp_card(c)
228 ... for c in 'As Ks Qs Js Ts 9s 8s 7s 6s 5s 4s'.split()]
230 Everyone folds the first round (P3, going last, will win).
231 >>> for p in t.players:
232 ... p.buffer = ['-1']
233 >>> t.play_round(shuffle=False)
234 >>> print '\\n'.join(t.log) # doctest: +REPORT_UDIFF
235 INFO CHIPCOUNT Player P1 10
236 INFO CHIPCOUNT Player P2 10
237 INFO CHIPCOUNT Player P3 10
238 ACTION Player P2 (pot 1) blinds 1
239 ACTION Player P3 (pot 2) blinds 2
240 ACTION Player P1 (pot 0) folds
241 ACTION Player P2 (pot 1) folds
242 ACTION Player P3 wins: 3
245 Play another round on the same deck, everyone goes all in. P3
246 will win with the higher straight flush.
247 >>> for p in t.players:
249 >>> t.play_round(shuffle=False)
250 >>> print '\\n'.join(t.log) # doctest: +REPORT_UDIFF
251 INFO CHIPCOUNT Player P1 10
252 INFO CHIPCOUNT Player P2 9
253 INFO CHIPCOUNT Player P3 11
254 ACTION Player P3 (pot 1) blinds 1
255 ACTION Player P1 (pot 2) blinds 2
256 ACTION Player P2 (pot 9) meets and raises for a total of 9
257 ACTION Player P3 (pot 11) goes all in with 10
258 ACTION Player P1 (pot 10) goes all in with 8
259 ACTION Player P3 wins: 30
260 INFO board 8s 7s 6s 5s 4s
261 INFO Player P3 hole: Ts 9s best: straight flush - Ts 9s 8s 7s 6s 5s 4s cash: 30
262 INFO Player P1 hole: As Ks best: straight flush - 8s 7s 6s 5s 4s As Ks cash: 0
263 INFO Player P2 hole: Qs Js best: straight flush - 8s 7s 6s 5s 4s Qs Js cash: 0
265 INFO Player P1 died after hand 2
266 INFO Player P2 died after hand 2
269 [(<BufferedPlayer P1>, <BufferedPlayer P2>)]
271 [<BufferedPlayer P3>]
273 def __init__(self, deck=None, players=None, blinds=None, verbose=False):
275 self.players = players # list of surviving players
277 self.verbose = verbose
278 self.dealer = 0 # index of player acting as dealer (dealer button)
280 self.dead_players = [] # list of players in the order they died
281 # e.g. [(lastplayer,), (AtiedForThird, BtiedForThird), (secondplayer,)]
283 def players_from(self, index):
284 """Rotate the list of players to start from `self.players[index]`.
286 >>> t = Table(players=['a', 'b', 'c', 'd'])
287 >>> t.players_from(0)
289 >>> t.players_from(1)
291 >>> t.players_from(2)
294 index = index % len(self.players)
295 return self.players[index:]+self.players[:index]
298 """Add a message to the table log."""
303 def play_round(self, shuffle=random.shuffle):
304 self.deal(shuffle=shuffle)
307 self.betting_round(first_round=True)
308 if self.hand_complete(): return self.judge()
310 self.flop = self.board[:3]
311 self._log('ACTION FLOP %s' % pp_hand(self.flop))
313 if self.hand_complete(): return self.judge()
315 self.turn = self.board[3:4]
316 self._log('ACTION TURN %s' % pp_hand(self.turn))
318 if self.hand_complete(): return self.judge()
320 self.river = self.board[4:5]
321 self._log('ACTION RIVER %s' % pp_hand(self.river))
323 if self.hand_complete(): return self.judge()
327 def deal(self, shuffle=random.shuffle):
328 """Shuffle the deck and deal two cards to each player."""
332 N = len(self.players)
333 self.board = [self.deck[n] for n in xrange(2*N,2*N+5)] # common cards
334 for i,player in enumerate(self.players):
336 self._log('INFO CHIPCOUNT Player %s %s'
337 % (player, player.cash))
338 player.hole = [self.deck[n] for n in xrange(2*i, 2*(i+1))]
339 player.hand = SevenChooseFiveHand(player.hole+self.board)
340 self.flop = self.turn = self.river = None
343 self.small_blind,self.big_blind,self.min_raise = self.blinds(
345 self.pot = 0 # total chips up for grabs
346 self.per_player_pot = 0 # needed to stay active
347 if len(self.players) > 2:
348 sb,bb = self.players_from(self.dealer+1)[0:2]
349 elif len(self.players) == 2:
350 # When only two players remain, special 'head-to-head' or
351 # 'heads up' rules are enforced and the blinds are posted
352 # differently. In this case, the person with the dealer
353 # button posts the small blind, while his/her opponent
354 # places the big blind.
355 sb,bb = self.players_from(self.dealer)[0:2]
357 raise Exception, "can't play with %d players" % len(self.players)
358 sb.owe = self.small_blind
359 self.square_player(sb, 'blinds')
360 bb.owe = self.big_blind
361 self.square_player(bb, 'blinds')
363 def square_player(self, player, verb, append_cost=True):
364 contribution = player.pay(player.owe)
365 self.pot += contribution
366 player.pot += contribution
367 self.per_player_pot = max(self.per_player_pot, player.pot)
368 string = 'ACTION Player %s (pot %s) %s' % (player, player.pot, verb)
369 if append_cost: string += ' %d' % contribution
373 def betting_round(self, first_round=False):
374 all_in = [p for p in self.players if p.status == 'all in']
375 folded = [p for p in self.players if p.status == 'fold']
377 if len(self.players) > 2:
378 if first_round == True: # start after the big blind
379 order = self.players_from(self.dealer+3)
380 else: # start with the small blind
381 order = self.players_from(self.dealer+1)
382 elif len(self.players) == 2: # 'head-to-head' rules
383 # The dealer acts first before the flop. After the flop,
384 # the dealer acts last and continues to do so for the
385 # remainder of the hand.
386 if first_round == True:
387 order = self.players_from(self.dealer)
389 order = self.players_from(self.dealer+1)
391 active = [p for p in order if p.active()]
393 min_raise = self.min_raise
394 raiser = None # no-one has raised yet this round
395 while (len(active) > 0): # i.e. continue until everyone folds/calls
396 player = active.pop(0)
397 player.owe = self.per_player_pot - player.pot
398 if player == raiser or (raiser == None
399 and player == tail and len(active) == 0):
402 action = player.decide(
403 active_players=[player]+active+all_in, pot=self.pot,
404 min_raise=min_raise, flop=self.flop, turn=self.turn,
405 river=self.river, log=self.log)
406 except IllegalBet, e:
408 'ACTION Player %s bets %s illegally and FOLDS. '
409 'Valid bets are %d or anything >= %d')
410 % (e.player, e.bet, e.player.owe, min_raise))
411 player.status = 'fold'
412 if player.status == 'fold':
413 player.owe = 0 # don't charge the folding player
414 folded.append(player)
415 self.square_player(player, 'folds', append_cost=False)
417 if action >= min_raise:
420 active.append(player)
421 verb = 'meets and raises for a total of'
422 elif player.status == 'all in':
423 all_in.append(player)
424 verb = 'goes all in with'
426 assert action == 0, action
428 if player == tail and action == 0:
430 active.append(player)
432 self.square_player(player, verb)
434 def hand_complete(self):
435 """Return `True` if there aren't enough active players for betting.
437 return len([p for p in self.players if p.active()]) <= 1
440 """Judge the winner and allocate the winnings."""
441 runners = [p for p in self.players if p.status != 'fold']
443 if len(runners) == 1: # everyone else folded, no need to show cards
444 folders = [p for p in self.players if p.status == 'fold']
445 self.pay_players(runners, folders)
446 else: # we need to look show hands
447 runners.sort(Player.cmp_hands, reverse=True) # sort high -> low
448 runcp = [p for p in runners]
449 losers = [p for p in self.players] # copy into a new list
450 # start from the top, breaking ties...
451 while len(runners) > 0:
452 winners = [runners.pop(0)]
453 losers.remove(winners[-1]) # remove from losers pool
454 while len(runners) > 0 and runners[0] == winners[0]:
455 winners.append(runners.pop(0)) # pop any winning tie
456 losers.remove(winners[-1]) # remove from losers pool
457 self.pay_players(winners, losers)
458 # allow the players to view the end result
459 flop,turn,river = self.board[:3], self.board[3:4], self.board[4:5]
460 self._log('INFO board %s' % pp_hand(self.board))
462 self._log('INFO Player %s hole: %s best: %s cash: %d'
463 % (p, pp_hand(p.hole), p.hand.pp_score(), p.cash))
464 self._log('INFO GAMEOVER %d' % self.hand_count)
466 # prepare for the next round
467 dead = [p for p in self.players if p.cash == 0]
469 self.dead_players.append(tuple(dead))
472 self.dealer = (self.dealer + 1) % len(self.players)
473 next_dealing_player = self.players[self.dealer]
474 while next_dealing_player.cash == 0: # next dealer is dead
475 # pass the dealer card to successor
476 self.dealer = (self.dealer + 1) % len(self.players)
477 next_dealing_player = self.players[self.dealer]
479 self._log('INFO Player %s died after hand %d'
480 % (p, self.hand_count))
481 self.players.remove(p)
482 self.dealer = self.players.index(next_dealing_player)
484 def pay_players(self, winners, losers):
485 winning_pots = sorted(set([p.pot for p in winners]))
486 winnings = dict().fromkeys([p.name for p in winners], 0)
487 for pot in winning_pots: # smallest to largest winning pots
489 for player in winners+losers:
490 contribution = min(player.pot, pot)
491 player.pot -= contribution
493 per_winner = int(floor(sum / len(winners)))
494 residual = sum % len(winners)
495 for player in winners:
496 player.cash += per_winner
497 winnings[player.name] += per_winner
498 # any extra chips go to the first players after the dealer
499 for player in self.players_from(self.dealer+1):
500 if residual == 0: break
501 if player in winners:
503 winnings[player.name] += 1
505 # drop the winners who are no longer invested in the pot
506 winners = [p for p in winners if p.pot > 0]
507 self.per_player_pot -= pot
508 for name,take in winnings.items():
510 self._log('ACTION Player %s wins: %s' % (name, take))