Add docstrings and doctests to table.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, stream, log):
92         """Show the brain any new log entries."""
93         new_index = len(log)
94         for line in LOG[self._next_log_index:new_index]:
95             stream.write(line + '\n')
96             stream.flush()
97         self._next_log_index = new_index
98
99     def clear_log(self):
100         self._next_log_index = 0  # unsent log index
101
102     def _record_info(self, active_players, pot, min_raise, flop, turn, river,
103                      log):
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)
116
117     def _human_play(self, active_players, pot, min_raise, flop, turn, river,
118                     log):
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:
126             if player != self:
127                 print 'Opp. %s STACK: %d' % (player, player.cash)
128         return raw_input('What do you want to bet, %s:' % self)
129
130     def _bet(self, **kwargs):
131         if not self.brain:
132             return self._human_play(**kwargs)
133         else:
134             self._record_info(**kwargs)
135             self._proc.stdin.write('MOVE\n')
136             self._proc.stdin.flush()
137             return self._proc.stdout.readline().strip()
138
139     def decide(self, **kwargs):
140         """Decide on a course of action (raise, fold, ...).
141
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.
157         """
158         bet = self._bet(**kwargs)
159         if bet.isalpha():
160             bet = bet.upper()
161             if   bet == 'A': bet = self.cash
162             elif bet == 'C': bet = self.owe
163             else           : self.status = 'fold'
164         else:
165             try   : bet = int(bet)
166             except: self.status = 'fold'
167
168         if bet >= self.cash:
169             self.status = 'all in'
170             return self.cash
171         elif bet < self.owe:
172             self.status = 'fold'
173             return 0
174         elif self.owe <= bet and bet < min_raise:  # round down to a check
175             return self.owe
176         elif self.owe >= min_raise:  # let's see some action!
177             return bet
178         raise IllegalBet(self, str(bet))
179
180
181 class Blinds (object):
182     """Blind schedule and per-hand blind calculation.
183
184     >>> b = Blinds(blinds=[1, 2, 4, 8], hand_clock=10)
185     >>> b(0)
186     (1, 2, 2)
187     >>> b(9)
188     (1, 2, 2)
189     >>> b(10)
190     (2, 4, 4)
191     >>> b(100)
192     (8, 16, 16)
193     """
194     def __init__(self, blinds, hand_clock):
195         self._blinds = blinds
196         self._hand_clock = hand_clock
197
198     def __call__(self, hand):
199         """Small blind, big blind, and min raise for the `hand`th hand.
200         """
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)
204
205
206 class Table (object):
207     """A poker table with a deck, players, and blind schedule.
208
209     Setup buffered players so we can control `Table` testing.
210
211     >>> class BufferedPlayer(Player):
212     ...     def __init__(self, **kwargs):
213     ...         super(BufferedPlayer, self).__init__(**kwargs)
214     ...         self.buffer = []
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))
220
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()]
225
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
239     INFO GAMEOVER 0
240
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:
244     ...     p.buffer = ['a']
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
260     INFO GAMEOVER 1
261     INFO Player P1 died after hand 2
262     INFO Player P2 died after hand 2
263
264     >>> t.dead_players
265     [(<BufferedPlayer P1>, <BufferedPlayer P2>)]
266     >>> t.players
267     [<BufferedPlayer P3>]
268     """
269     def __init__(self, deck=None, players=None, blinds=None):
270         self.deck = deck
271         self.players = players # list of surviving players
272         self.blinds = blinds
273         self.dealer = 0  # index of player acting as dealer (dealer button)
274         self.hand_count = 0
275         self.dead_players = [] # list of players in the order they died
276         # e.g. [(lastplayer,), (AtiedForThird, BtiedForThird), (secondplayer,)]
277
278     def players_from(self, index):
279         """Rotate the list of players to start from `self.players[index]`.
280
281         >>> t = Table(players=['a', 'b', 'c', 'd'])
282         >>> t.players_from(0)
283         ['a', 'b', 'c', 'd']
284         >>> t.players_from(1)
285         ['b', 'c', 'd', 'a']
286         >>> t.players_from(2)
287         ['c', 'd', 'a', 'b']
288         """
289         index = index % len(self.players)
290         return self.players[index:]+self.players[:index]
291
292     def play_round(self, shuffle=random.shuffle):
293         self.deal(shuffle=shuffle)
294         self.ante_up()
295         # PRE-FLOP ACTION
296         self.betting_round(first_round=True)
297         if self.hand_complete(): return self.judge()
298         # FLOP ACTION
299         self.flop = self.board[:3]
300         self.log.append('ACTION FLOP %s' % pp_hand(self.flop))
301         self.betting_round()
302         if self.hand_complete(): return self.judge()
303         # TURN ACTION
304         self.turn = self.board[3:4]
305         self.log.append('ACTION TURN %s' % pp_hand(self.turn))
306         self.betting_round()
307         if self.hand_complete(): return self.judge()
308         # RIVER ACTION
309         self.river = self.board[4:5]
310         self.log.append('ACTION RIVER %s' % pp_hand(self.river))
311         self.betting_round()
312         if self.hand_complete(): return self.judge()
313         # SHOWDOWN!
314         self.judge()
315
316     def deal(self, shuffle=random.shuffle):
317         """Shuffle the deck and deal two cards to each player."""
318         self.log = []
319         if shuffle:
320             shuffle(self.deck)
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):
324             player.new_hand()
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
330
331     def ante_up(self):
332         self.small_blind,self.big_blind,self.min_raise = self.blinds(
333             self.hand_count)
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]
345         else:
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')
351
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)
360         return contribution
361
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']
365
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)
377             else:
378                 order = self.players_from(self.dealer+1)
379
380         active = [p for p in order if p.active()]
381         tail = active[-1]
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):
389                 break
390             try:
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,
394                     river=self.river)
395             except IllegalBet, e:
396                 self.log.append((
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)
405             else:  # settle up
406                 if action >= min_raise:
407                     min_raise = 2*action
408                     raiser = player
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'
414                 else:
415                     assert action == 0, action
416                     verb = 'checks with'
417                     if player == tail and action == 0:
418                         active = []
419                     active.append(player)
420                 player.owe += action
421                 self.square_player(player, verb)
422
423     def hand_complete(self):
424         """Return `True` if there aren't enough active players for betting.
425         """
426         return len([p for p in self.players if p.active()]) <= 1
427
428     def judge(self):
429         """Judge the winner and allocate the winnings."""
430         runners = [p for p in self.players if p.status != 'fold']
431
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))
450             for p in runcp:
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)
454
455         # prepare for the next round
456         dead = [p for p in self.players if p.cash == 0]
457         if dead:
458             self.dead_players.append(tuple(dead))
459         self.hand_count += 1
460
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]
467         for p in dead:
468             self.log.append('INFO Player %s died after hand %d'
469                             % (p, self.hand_count))
470             self.players.remove(p)
471             p.kill()
472         self.dealer = self.players.index(next_dealing_player)
473
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
478             sum = 0
479             for player in winners+losers:
480                 contribution = min(player.pot, pot)
481                 player.pot -= contribution
482                 sum += 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:
492                     player.cash += 1
493                     winnings[player.name] += 1
494                     residual -= 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():
499             if take > 0:
500                 self.log.append('ACTION Player %s wins: %s' % (name, take))