Add docstrings and doctests to pbot.py + Python cleanups.
[poker.git] / table.py
1 """Define a poker table with players.
2 """
3
4 from math import floor
5 import random
6 import sys
7 import subprocess
8
9 from deck import pp_hand, SevenChooseFiveHand
10
11
12 class IllegalBet (ValueError):
13     """Raised on invalid bet attempts.
14     """
15     def __init__(self, player, bet):
16         super(IllegalBet, self).__init__(bet)
17         self.player = player
18         self.bet = bet
19
20     def __str__(self):
21         return 'Illegal bet %s by %s' % (self.player, self.name)
22
23
24 class Player (object):
25     """Poker player.
26     """
27     def __init__(self, cash=0, name='', brain=None):
28         self.cash  = cash  # uncommitted chips
29         self.name  = name
30         self.brain = brain
31         self._proc = None
32         self.clear_log()
33
34         if self.brain:
35             self._proc = subprocess.Popen(
36                 './'+self.brain, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
37                 stderr=subprocess.PIPE, close_fds=True)
38
39     def __str__(self):
40         return self.name
41
42     def kill(self):
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' % (
48                 self, exit_code)
49             self._proc = None
50
51     def __str__(self):
52         return self.name
53
54     def __repr__(self):
55         return '<%s %s>' % (self.__class__.__name__, self.name)
56
57     def __cmp__(self, other):
58         if other == None:
59             return 1
60         return cmp(self.name, other.name)
61
62     def cmp_hands(self, other):
63         return cmp(self.hand, other.hand)
64
65     def new_hand(self):
66         self.clear_log()
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
72
73     def active(self):
74         """Does this player still need to think?"""
75         return self.status not in ['all in', 'fold']
76
77     def pay(self, wager):
78         """Settle what you owe the table.
79
80         Called during play as well as at the end of a hand.
81         """
82         if wager >= self.cash:
83             self.status = 'all in'  # may be forced all-in (e.g. by a blind)
84             wager = self.cash
85         wager = max(wager, 0)
86         self.cash -= wager
87         self.owe  -= wager
88         self.owe   = min(self.owe, 0)
89         return wager
90
91     def log_flush(self, log):
92         """Show the brain any new log entries."""
93         if self._proc:
94             stream = self._proc.stdin
95         else:
96             stream = sys.stdout
97         new_index = len(log)
98         for line in log[self._next_log_index:new_index]:
99             stream.write(line + '\n')
100             stream.flush()
101         self._next_log_index = new_index
102
103     def clear_log(self):
104         self._next_log_index = 0  # unsent log index
105
106     def _record_info(self, active_players, pot, min_raise, flop, turn, river,
107                      log):
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)]:
116             if h:
117                 self._proc.stdin.write('INFO %s %s\n' % (phase, pp_hand(h)))
118         self._proc.stdin.flush()
119         self.log_flush(log)
120
121     def _human_play(self, active_players, pot, min_raise, flop, turn, river,
122                     log):
123         """Ask the human at `sys.stdin`/`sys.stdout` for guidance."""
124         self.log_flush(log)
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:
130             if player != self:
131                 print 'Opp. %s STACK: %d' % (player, player.cash)
132         return raw_input('What do you want to bet, %s:' % self)
133
134     def _bet(self, **kwargs):
135         if not self.brain:
136             return self._human_play(**kwargs)
137         else:
138             self._record_info(**kwargs)
139             self._proc.stdin.write('MOVE\n')
140             self._proc.stdin.flush()
141             return self._proc.stdout.readline().strip()
142
143     def decide(self, **kwargs):
144         """Decide on a course of action (raise, fold, ...).
145
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.
161         """
162         bet = self._bet(**kwargs)
163         if bet.isalpha():
164             bet = bet.upper()
165             if   bet == 'A': bet = self.cash
166             elif bet == 'C': bet = self.owe
167             else           : self.status = 'fold'
168         else:
169             try   : bet = int(bet)
170             except: self.status = 'fold'
171
172         if bet >= self.cash:
173             self.status = 'all in'
174             return self.cash
175         elif bet < self.owe:
176             self.status = 'fold'
177             return 0
178         elif self.owe <= bet and bet < min_raise:  # round down to a check
179             return self.owe
180         elif self.owe >= min_raise:  # let's see some action!
181             return bet
182         raise IllegalBet(self, str(bet))
183
184
185 class Blinds (object):
186     """Blind schedule and per-hand blind calculation.
187
188     >>> b = Blinds(blinds=[1, 2, 4, 8], hand_clock=10)
189     >>> b(0)
190     (1, 2, 2)
191     >>> b(9)
192     (1, 2, 2)
193     >>> b(10)
194     (2, 4, 4)
195     >>> b(100)
196     (8, 16, 16)
197     """
198     def __init__(self, blinds, hand_clock):
199         self._blinds = blinds
200         self._hand_clock = hand_clock
201
202     def __call__(self, hand):
203         """Small blind, big blind, and min raise for the `hand`th hand.
204         """
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)
208
209
210 class Table (object):
211     """A poker table with a deck, players, and blind schedule.
212
213     Setup buffered players so we can control `Table` testing.
214
215     >>> class BufferedPlayer(Player):
216     ...     def __init__(self, **kwargs):
217     ...         super(BufferedPlayer, self).__init__(**kwargs)
218     ...         self.buffer = []
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))
224
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()]
229
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
243     INFO GAMEOVER 0
244
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:
248     ...     p.buffer = ['a']
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
264     INFO GAMEOVER 1
265     INFO Player P1 died after hand 2
266     INFO Player P2 died after hand 2
267
268     >>> t.dead_players
269     [(<BufferedPlayer P1>, <BufferedPlayer P2>)]
270     >>> t.players
271     [<BufferedPlayer P3>]
272     """
273     def __init__(self, deck=None, players=None, blinds=None, verbose=False):
274         self.deck = deck
275         self.players = players # list of surviving players
276         self.blinds = blinds
277         self.verbose = verbose
278         self.dealer = 0  # index of player acting as dealer (dealer button)
279         self.hand_count = 0
280         self.dead_players = [] # list of players in the order they died
281         # e.g. [(lastplayer,), (AtiedForThird, BtiedForThird), (secondplayer,)]
282
283     def players_from(self, index):
284         """Rotate the list of players to start from `self.players[index]`.
285
286         >>> t = Table(players=['a', 'b', 'c', 'd'])
287         >>> t.players_from(0)
288         ['a', 'b', 'c', 'd']
289         >>> t.players_from(1)
290         ['b', 'c', 'd', 'a']
291         >>> t.players_from(2)
292         ['c', 'd', 'a', 'b']
293         """
294         index = index % len(self.players)
295         return self.players[index:]+self.players[:index]
296
297     def _log(self, msg):
298         """Add a message to the table log."""
299         self.log.append(msg)
300         if self.verbose:
301             print msg
302
303     def play_round(self, shuffle=random.shuffle):
304         self.deal(shuffle=shuffle)
305         self.ante_up()
306         # PRE-FLOP ACTION
307         self.betting_round(first_round=True)
308         if self.hand_complete(): return self.judge()
309         # FLOP ACTION
310         self.flop = self.board[:3]
311         self._log('ACTION FLOP %s' % pp_hand(self.flop))
312         self.betting_round()
313         if self.hand_complete(): return self.judge()
314         # TURN ACTION
315         self.turn = self.board[3:4]
316         self._log('ACTION TURN %s' % pp_hand(self.turn))
317         self.betting_round()
318         if self.hand_complete(): return self.judge()
319         # RIVER ACTION
320         self.river = self.board[4:5]
321         self._log('ACTION RIVER %s' % pp_hand(self.river))
322         self.betting_round()
323         if self.hand_complete(): return self.judge()
324         # SHOWDOWN!
325         self.judge()
326
327     def deal(self, shuffle=random.shuffle):
328         """Shuffle the deck and deal two cards to each player."""
329         self.log = []
330         if shuffle:
331             shuffle(self.deck)
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):
335             player.new_hand()
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
341
342     def ante_up(self):
343         self.small_blind,self.big_blind,self.min_raise = self.blinds(
344             self.hand_count)
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]
356         else:
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')
362
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
370         self._log(string)
371         return contribution
372
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']
376
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)
388             else:
389                 order = self.players_from(self.dealer+1)
390
391         active = [p for p in order if p.active()]
392         tail = active[-1]
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):
400                 break
401             try:
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:
407                 self._log((
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)
416             else:  # settle up
417                 if action >= min_raise:
418                     min_raise = 2*action
419                     raiser = player
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'
425                 else:
426                     assert action == 0, action
427                     verb = 'checks with'
428                     if player == tail and action == 0:
429                         active = []
430                     active.append(player)
431                 player.owe += action
432                 self.square_player(player, verb)
433
434     def hand_complete(self):
435         """Return `True` if there aren't enough active players for betting.
436         """
437         return len([p for p in self.players if p.active()]) <= 1
438
439     def judge(self):
440         """Judge the winner and allocate the winnings."""
441         runners = [p for p in self.players if p.status != 'fold']
442
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))
461             for p in runcp:
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)
465
466         # prepare for the next round
467         dead = [p for p in self.players if p.cash == 0]
468         if dead:
469             self.dead_players.append(tuple(dead))
470         self.hand_count += 1
471
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]
478         for p in dead:
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)
483
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
488             sum = 0
489             for player in winners+losers:
490                 contribution = min(player.pot, pot)
491                 player.pot -= contribution
492                 sum += 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:
502                     player.cash += 1
503                     winnings[player.name] += 1
504                     residual -= 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():
509             if take > 0:
510                 self._log('ACTION Player %s wins: %s' % (name, take))