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, stream, log):
92 """Show the brain any new log entries."""
94 for line in LOG[self._next_log_index:new_index]:
95 stream.write(line + '\n')
97 self._next_log_index = new_index
100 self._next_log_index = 0 # unsent log index
102 def _record_info(self, active_players, pot, min_raise, flop, turn, river,
104 """Show the brain the current state of events."""
105 self._proc.stdin.write('INFO NAME %s\n' % str(self.name))
106 self._proc.stdin.write('INFO STACK %s\n' % str(self.cash))
107 self._proc.stdin.write('INFO POT %s\n' % str(pot))
108 self._proc.stdin.write('INFO MINRAISE %s\n' % str(min_raise))
109 self._proc.stdin.write('INFO OWE %s\n' % str(self.owe))
110 self._proc.stdin.write('INFO HOLE %s\n' % pp_hand(self.hole))
111 self._proc.stdin.write('INFO FLOP %s\n' % pp_hand(flop))
112 self._proc.stdin.write('INFO TURN %s\n' % pp_hand(turn))
113 self._proc.stdin.write('INFO RIVER %s\n' % pp_hand(river))
114 self._proc.stdin.flush()
115 self._log_flush(self._proc.stdin, log)
117 def _human_play(self, active_players, pot, min_raise, flop, turn, river,
119 """Ask the human at `sys.stdin`/`sys.stdout` for guidance."""
120 self._log_flush(sys.stdout, log)
121 print '[', pp_hand(self.hole), '] Board: ',
122 print '[', pp_hand(flop), pp_hand(turn), pp_hand(river), ']'
123 print 'POT: ', pot, ' OWE: ', self.owe, ' MIN_RAISE: ', min_raise,
124 print ' | ', 'STACK: ', self.cash
125 for player in active_players:
127 print 'Opp. %s STACK: %d' % (player, player.cash)
128 return raw_input('What do you want to bet, %s:' % self)
130 def _bet(self, **kwargs):
132 return self._human_play(**kwargs)
134 self._record_info(**kwargs)
135 self._proc.stdin.write('MOVE\n')
136 self._proc.stdin.flush()
137 return self._proc.stdout.readline().strip()
139 def decide(self, **kwargs):
140 """Decide on a course of action (raise, fold, ...).
142 The program accepts an (A) as an all-in bet, (C) as a call or
143 check and any valid bet. Valid bets are either the amount
144 owed to the pot or at least the min raise. All bets made
145 greater then the players stack will be considered an all-in.
146 All bets that are less than the amount owed will be considered
147 a fold. A bet in the range of the amount owed to the min
148 raise will be considered a CALL and the player will put in the
149 amount owed to the table. Any other input (negative, decimal,
150 or alpha) will be considered to be a FOLD. To keep a
151 presistant state (and for speed reasons) your program will
152 have the information fed to it. This is done by piping
153 `stdin` and `stdout` to your program. The dealer will wait
154 once the command `MOVE` is sent. Along with the `MOVE` tag,
155 your code will be given information on the current game state
156 and history. See `Player.record_info()` for details.
158 bet = self._bet(**kwargs)
161 if bet == 'A': bet = self.cash
162 elif bet == 'C': bet = self.owe
163 else : self.status = 'fold'
166 except: self.status = 'fold'
169 self.status = 'all in'
174 elif self.owe <= bet and bet < min_raise: # round down to a check
176 elif self.owe >= min_raise: # let's see some action!
178 raise IllegalBet(self, str(bet))
181 class Blinds (object):
182 """Blind schedule and per-hand blind calculation.
184 >>> b = Blinds(blinds=[1, 2, 4, 8], hand_clock=10)
194 def __init__(self, blinds, hand_clock):
195 self._blinds = blinds
196 self._hand_clock = hand_clock
198 def __call__(self, hand):
199 """Small blind, big blind, and min raise for the `hand`th hand.
201 i = min(hand / self._hand_clock, len(self._blinds)-1)
202 small_blind = self._blinds[i]
203 return (small_blind, small_blind*2, small_blind*2)
206 class Table (object):
207 """A poker table with a deck, players, and blind schedule.
209 Setup buffered players so we can control `Table` testing.
211 >>> class BufferedPlayer(Player):
212 ... def __init__(self, **kwargs):
213 ... super(BufferedPlayer, self).__init__(**kwargs)
215 ... def _bet(self, **kwargs):
216 ... return self.buffer.pop(0)
217 >>> players = [BufferedPlayer(cash=10, name='P%d' % (i+1))
218 ... for i in range(3)]
219 >>> t = Table(players=players, blinds=Blinds((1, 2, 4), 3))
221 Stack the deck so we know what's coming.
222 >>> from deck import unpp_card
223 >>> t.deck = [unpp_card(c)
224 ... for c in 'As Ks Qs Js Ts 9s 8s 7s 6s 5s 4s'.split()]
226 Everyone folds the first round (P3, going last, will win).
227 >>> for p in t.players:
228 ... p.buffer = ['-1']
229 >>> t.play_round(shuffle=False)
230 >>> print '\\n'.join(t.log) # doctest: +REPORT_UDIFF
231 INFO CHIPCOUNT Player P1 10
232 INFO CHIPCOUNT Player P2 10
233 INFO CHIPCOUNT Player P3 10
234 ACTION Player P2 (pot 1) blinds 1
235 ACTION Player P3 (pot 2) blinds 2
236 ACTION Player P1 (pot 0) folds
237 ACTION Player P2 (pot 1) folds
238 ACTION Player P3 wins: 3
241 Play another round on the same deck, everyone goes all in. P3
242 will win with the higher straight flush.
243 >>> for p in t.players:
245 >>> t.play_round(shuffle=False)
246 >>> print '\\n'.join(t.log) # doctest: +REPORT_UDIFF
247 INFO CHIPCOUNT Player P1 10
248 INFO CHIPCOUNT Player P2 9
249 INFO CHIPCOUNT Player P3 11
250 ACTION Player P3 (pot 1) blinds 1
251 ACTION Player P1 (pot 2) blinds 2
252 ACTION Player P2 (pot 9) meets and raises for a total of 9
253 ACTION Player P3 (pot 11) goes all in with 10
254 ACTION Player P1 (pot 10) goes all in with 8
255 ACTION Player P3 wins: 30
256 INFO board 8s 7s 6s 5s 4s
257 INFO Player P3 hole: Ts 9s best: straight flush - Ts 9s 8s 7s 6s 5s 4s cash: 30
258 INFO Player P1 hole: As Ks best: straight flush - 8s 7s 6s 5s 4s As Ks cash: 0
259 INFO Player P2 hole: Qs Js best: straight flush - 8s 7s 6s 5s 4s Qs Js cash: 0
261 INFO Player P1 died after hand 2
262 INFO Player P2 died after hand 2
265 [(<BufferedPlayer P1>, <BufferedPlayer P2>)]
267 [<BufferedPlayer P3>]
269 def __init__(self, deck=None, players=None, blinds=None):
271 self.players = players # list of surviving players
273 self.dealer = 0 # index of player acting as dealer (dealer button)
275 self.dead_players = [] # list of players in the order they died
276 # e.g. [(lastplayer,), (AtiedForThird, BtiedForThird), (secondplayer,)]
278 def players_from(self, index):
279 """Rotate the list of players to start from `self.players[index]`.
281 >>> t = Table(players=['a', 'b', 'c', 'd'])
282 >>> t.players_from(0)
284 >>> t.players_from(1)
286 >>> t.players_from(2)
289 index = index % len(self.players)
290 return self.players[index:]+self.players[:index]
292 def play_round(self, shuffle=random.shuffle):
293 self.deal(shuffle=shuffle)
296 self.betting_round(first_round=True)
297 if self.hand_complete(): return self.judge()
299 self.flop = self.board[:3]
300 self.log.append('ACTION FLOP %s' % pp_hand(self.flop))
302 if self.hand_complete(): return self.judge()
304 self.turn = self.board[3:4]
305 self.log.append('ACTION TURN %s' % pp_hand(self.turn))
307 if self.hand_complete(): return self.judge()
309 self.river = self.board[4:5]
310 self.log.append('ACTION RIVER %s' % pp_hand(self.river))
312 if self.hand_complete(): return self.judge()
316 def deal(self, shuffle=random.shuffle):
317 """Shuffle the deck and deal two cards to each player."""
321 N = len(self.players)
322 self.board = [self.deck[n] for n in xrange(2*N,2*N+5)] # common cards
323 for i,player in enumerate(self.players):
325 self.log.append('INFO CHIPCOUNT Player %s %s'
326 % (player, player.cash))
327 player.hole = [self.deck[n] for n in xrange(2*i, 2*(i+1))]
328 player.hand = SevenChooseFiveHand(player.hole+self.board)
329 self.flop = self.turn = self.river = None
332 self.small_blind,self.big_blind,self.min_raise = self.blinds(
334 self.pot = 0 # total chips up for grabs
335 self.per_player_pot = 0 # needed to stay active
336 if len(self.players) > 2:
337 sb,bb = self.players_from(self.dealer+1)[0:2]
338 elif len(self.players) == 2:
339 # When only two players remain, special 'head-to-head' or
340 # 'heads up' rules are enforced and the blinds are posted
341 # differently. In this case, the person with the dealer
342 # button posts the small blind, while his/her opponent
343 # places the big blind.
344 sb,bb = self.players_from(self.dealer)[0:2]
346 raise Exception, "can't play with %d players" % len(self.players)
347 sb.owe = self.small_blind
348 self.square_player(sb, 'blinds')
349 bb.owe = self.big_blind
350 self.square_player(bb, 'blinds')
352 def square_player(self, player, verb, append_cost=True):
353 contribution = player.pay(player.owe)
354 self.pot += contribution
355 player.pot += contribution
356 self.per_player_pot = max(self.per_player_pot, player.pot)
357 string = 'ACTION Player %s (pot %s) %s' % (player, player.pot, verb)
358 if append_cost: string += ' %d' % contribution
359 self.log.append(string)
362 def betting_round(self, first_round=False):
363 all_in = [p for p in self.players if p.status == 'all in']
364 folded = [p for p in self.players if p.status == 'fold']
366 if len(self.players) > 2:
367 if first_round == True: # start after the big blind
368 order = self.players_from(self.dealer+3)
369 else: # start with the small blind
370 order = self.players_from(self.dealer+1)
371 elif len(self.players) == 2: # 'head-to-head' rules
372 # The dealer acts first before the flop. After the flop,
373 # the dealer acts last and continues to do so for the
374 # remainder of the hand.
375 if first_round == True:
376 order = self.players_from(self.dealer)
378 order = self.players_from(self.dealer+1)
380 active = [p for p in order if p.active()]
382 min_raise = self.min_raise
383 raiser = None # no-one has raised yet this round
384 while (len(active) > 0): # i.e. continue until everyone folds/calls
385 player = active.pop(0)
386 player.owe = self.per_player_pot - player.pot
387 if player == raiser or (raiser == None
388 and player == tail and len(active) == 0):
391 action = player.decide(
392 active_players=[player]+active+all_in, pot=self.pot,
393 min_raise=min_raise, flop=self.flop, turn=self.turn,
395 except IllegalBet, e:
397 'ACTION Player %s bets %s illegally and FOLDS. '
398 'Valid bets are %d or anything >= %d')
399 % (e.player, e.bet, e.player.owe, min_raise))
400 player.status = 'fold'
401 if player.status == 'fold':
402 player.owe = 0 # don't charge the folding player
403 folded.append(player)
404 self.square_player(player, 'folds', append_cost=False)
406 if action >= min_raise:
409 active.append(player)
410 verb = 'meets and raises for a total of'
411 elif player.status == 'all in':
412 all_in.append(player)
413 verb = 'goes all in with'
415 assert action == 0, action
417 if player == tail and action == 0:
419 active.append(player)
421 self.square_player(player, verb)
423 def hand_complete(self):
424 """Return `True` if there aren't enough active players for betting.
426 return len([p for p in self.players if p.active()]) <= 1
429 """Judge the winner and allocate the winnings."""
430 runners = [p for p in self.players if p.status != 'fold']
432 if len(runners) == 1: # everyone else folded, no need to show cards
433 folders = [p for p in self.players if p.status == 'fold']
434 self.pay_players(runners, folders)
435 else: # we need to look show hands
436 runners.sort(Player.cmp_hands, reverse=True) # sort high -> low
437 runcp = [p for p in runners]
438 losers = [p for p in self.players] # copy into a new list
439 # start from the top, breaking ties...
440 while len(runners) > 0:
441 winners = [runners.pop(0)]
442 losers.remove(winners[-1]) # remove from losers pool
443 while len(runners) > 0 and runners[0] == winners[0]:
444 winners.append(runners.pop(0)) # pop any winning tie
445 losers.remove(winners[-1]) # remove from losers pool
446 self.pay_players(winners, losers)
447 # allow the players to view the end result
448 flop,turn,river = self.board[:3], self.board[3:4], self.board[4:5]
449 self.log.append('INFO board %s' % pp_hand(self.board))
451 self.log.append('INFO Player %s hole: %s best: %s cash: %d'
452 % (p, pp_hand(p.hole), p.hand.pp_score(), p.cash))
453 self.log.append('INFO GAMEOVER %d' % self.hand_count)
455 # prepare for the next round
456 dead = [p for p in self.players if p.cash == 0]
458 self.dead_players.append(tuple(dead))
461 self.dealer = (self.dealer + 1) % len(self.players)
462 next_dealing_player = self.players[self.dealer]
463 while next_dealing_player.cash == 0: # next dealer is dead
464 # pass the dealer card to successor
465 self.dealer = (self.dealer + 1) % len(self.players)
466 next_dealing_player = self.players[self.dealer]
468 self.log.append('INFO Player %s died after hand %d'
469 % (p, self.hand_count))
470 self.players.remove(p)
472 self.dealer = self.players.index(next_dealing_player)
474 def pay_players(self, winners, losers):
475 winning_pots = sorted(set([p.pot for p in winners]))
476 winnings = dict().fromkeys([p.name for p in winners], 0)
477 for pot in winning_pots: # smallest to largest winning pots
479 for player in winners+losers:
480 contribution = min(player.pot, pot)
481 player.pot -= contribution
483 per_winner = int(floor(sum / len(winners)))
484 residual = sum % len(winners)
485 for player in winners:
486 player.cash += per_winner
487 winnings[player.name] += per_winner
488 # any extra chips go to the first players after the dealer
489 for player in self.players_from(self.dealer+1):
490 if residual == 0: break
491 if player in winners:
493 winnings[player.name] += 1
495 # drop the winners who are no longer invested in the pot
496 winners = [p for p in winners if p.pot > 0]
497 self.per_player_pot -= pot
498 for name,take in winnings.items():
500 self.log.append('ACTION Player %s wins: %s' % (name, take))