Move *.py into new pbot package.
[poker.git] / pbot / 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             try:
45                 self._proc.stdin.write('END\n')
46                 self._proc.stdin.flush()
47             except IOError, e:
48                 pass
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)
53             self._proc = None
54
55     def __str__(self):
56         return self.name
57
58     def __repr__(self):
59         return '<%s %s>' % (self.__class__.__name__, self.name)
60
61     def __cmp__(self, other):
62         if other == None:
63             return 1
64         return cmp(self.name, other.name)
65
66     def cmp_hands(self, other):
67         return cmp(self.hand, other.hand)
68
69     def new_hand(self):
70         self.clear_log()
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
76
77     def active(self):
78         """Does this player still need to think?"""
79         return self.status not in ['all in', 'fold']
80
81     def pay(self, wager):
82         """Settle what you owe the table.
83
84         Called during play as well as at the end of a hand.
85         """
86         if wager >= self.cash:
87             self.status = 'all in'  # may be forced all-in (e.g. by a blind)
88             wager = self.cash
89         wager = max(wager, 0)
90         self.cash -= wager
91         self.owe  -= wager
92         self.owe   = min(self.owe, 0)
93         return wager
94
95     def log_flush(self, log):
96         """Show the brain any new log entries."""
97         if self._proc:
98             stream = self._proc.stdin
99         else:
100             stream = sys.stdout
101         new_index = len(log)
102         for line in log[self._next_log_index:new_index]:
103             stream.write(line + '\n')
104             stream.flush()
105         self._next_log_index = new_index
106
107     def clear_log(self):
108         self._next_log_index = 0  # unsent log index
109
110     def _record_info(self, active_players, pot, min_raise, flop, turn, river,
111                      log):
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)]:
120             if h:
121                 self._proc.stdin.write('INFO %s %s\n' % (phase, pp_hand(h)))
122         self._proc.stdin.flush()
123         self.log_flush(log)
124
125     def _human_play(self, active_players, pot, min_raise, flop, turn, river,
126                     log):
127         """Ask the human at `sys.stdin`/`sys.stdout` for guidance."""
128         self.log_flush(log)
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:
134             if player != self:
135                 print 'Opp. %s STACK: %d' % (player, player.cash)
136         return raw_input('What do you want to bet, %s:' % self)
137
138     def _bet(self, **kwargs):
139         if not self.brain:
140             return self._human_play(**kwargs)
141         else:
142             self._record_info(**kwargs)
143             self._proc.stdin.write('MOVE\n')
144             self._proc.stdin.flush()
145             return self._proc.stdout.readline().strip()
146
147     def decide(self, **kwargs):
148         """Decide on a course of action (raise, fold, ...).
149
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.
165         """
166         bet = self._bet(**kwargs)
167         if bet.isalpha():
168             bet = bet.upper()
169             if   bet == 'A': bet = self.cash
170             elif bet == 'C': bet = self.owe
171             else           : bet = 0
172         else:
173             try   : bet = int(bet)
174             except: self.status = 'fold'
175
176         if bet >= self.cash:
177             self.status = 'all in'
178             return self.cash
179         elif bet < self.owe:
180             self.status = 'fold'
181             return 0
182         elif self.owe <= bet and bet < min_raise:  # round down to a check
183             return self.owe
184         elif self.owe >= min_raise:  # let's see some action!
185             return bet
186         raise IllegalBet(self, str(bet))
187
188
189 class Blinds (object):
190     """Blind schedule and per-hand blind calculation.
191
192     >>> b = Blinds(blinds=[1, 2, 4, 8], hand_clock=10)
193     >>> b(0)
194     (1, 2, 2)
195     >>> b(9)
196     (1, 2, 2)
197     >>> b(10)
198     (2, 4, 4)
199     >>> b(100)
200     (8, 16, 16)
201     """
202     def __init__(self, blinds, hand_clock):
203         self._blinds = blinds
204         self._hand_clock = hand_clock
205
206     def __call__(self, hand):
207         """Small blind, big blind, and min raise for the `hand`th hand.
208         """
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)
212
213
214 class Table (object):
215     """A poker table with a deck, players, and blind schedule.
216
217     Setup buffered players so we can control `Table` testing.
218
219     >>> class BufferedPlayer(Player):
220     ...     def __init__(self, **kwargs):
221     ...         super(BufferedPlayer, self).__init__(**kwargs)
222     ...         self.buffer = []
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))
228
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()]
233
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
247     INFO GAMEOVER 0
248
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:
252     ...     p.buffer = ['a']
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
268     INFO GAMEOVER 1
269     INFO Player P1 died after hand 2
270     INFO Player P2 died after hand 2
271
272     >>> t.dead_players
273     [(<BufferedPlayer P1>, <BufferedPlayer P2>)]
274     >>> t.players
275     [<BufferedPlayer P3>]
276     """
277     def __init__(self, deck=None, players=None, blinds=None, verbose=False):
278         self.deck = deck
279         self.players = players # list of surviving players
280         self.blinds = blinds
281         self.verbose = verbose
282         self.dealer = 0  # index of player acting as dealer (dealer button)
283         self.hand_count = 0
284         self.dead_players = [] # list of players in the order they died
285         # e.g. [(lastplayer,), (AtiedForThird, BtiedForThird), (secondplayer,)]
286
287     def players_from(self, index):
288         """Rotate the list of players to start from `self.players[index]`.
289
290         >>> t = Table(players=['a', 'b', 'c', 'd'])
291         >>> t.players_from(0)
292         ['a', 'b', 'c', 'd']
293         >>> t.players_from(1)
294         ['b', 'c', 'd', 'a']
295         >>> t.players_from(2)
296         ['c', 'd', 'a', 'b']
297         """
298         index = index % len(self.players)
299         return self.players[index:]+self.players[:index]
300
301     def _log(self, msg):
302         """Add a message to the table log."""
303         self.log.append(msg)
304         if self.verbose:
305             print msg
306
307     def play_round(self, shuffle=random.shuffle):
308         self.deal(shuffle=shuffle)
309         self.ante_up()
310         # PRE-FLOP ACTION
311         self.betting_round(first_round=True)
312         if self.hand_complete(): return self.judge()
313         # FLOP ACTION
314         self.flop = self.board[:3]
315         self._log('ACTION FLOP %s' % pp_hand(self.flop))
316         self.betting_round()
317         if self.hand_complete(): return self.judge()
318         # TURN ACTION
319         self.turn = self.board[3:4]
320         self._log('ACTION TURN %s' % pp_hand(self.turn))
321         self.betting_round()
322         if self.hand_complete(): return self.judge()
323         # RIVER ACTION
324         self.river = self.board[4:5]
325         self._log('ACTION RIVER %s' % pp_hand(self.river))
326         self.betting_round()
327         if self.hand_complete(): return self.judge()
328         # SHOWDOWN!
329         self.judge()
330
331     def deal(self, shuffle=random.shuffle):
332         """Shuffle the deck and deal two cards to each player."""
333         self.log = []
334         if shuffle:
335             shuffle(self.deck)
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):
339             player.new_hand()
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
345
346     def ante_up(self):
347         self.small_blind,self.big_blind,self.min_raise = self.blinds(
348             self.hand_count)
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]
360         else:
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')
366
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
374         self._log(string)
375         return contribution
376
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']
380
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)
392             else:
393                 order = self.players_from(self.dealer+1)
394
395         active = [p for p in order if p.active()]
396         tail = active[-1]
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):
404                 break
405             try:
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:
411                 self._log((
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)
420             else:  # settle up
421                 if action >= min_raise:
422                     min_raise = 2*action
423                     raiser = player
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'
429                 else:
430                     assert action == 0, action
431                     verb = 'checks with'
432                     if player == tail and action == 0:
433                         active = []
434                     active.append(player)
435                 player.owe += action
436                 self.square_player(player, verb)
437
438     def hand_complete(self):
439         """Return `True` if there aren't enough active players for betting.
440         """
441         return len([p for p in self.players if p.active()]) <= 1
442
443     def judge(self):
444         """Judge the winner and allocate the winnings."""
445         runners = [p for p in self.players if p.status != 'fold']
446
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))
465             for p in runcp:
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)
469
470         # prepare for the next round
471         dead = [p for p in self.players if p.cash == 0]
472         if dead:
473             self.dead_players.append(tuple(dead))
474         self.hand_count += 1
475
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]
482         for p in dead:
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)
487
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
492             sum = 0
493             for player in winners+losers:
494                 contribution = min(player.pot, pot)
495                 player.pot -= contribution
496                 sum += 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:
506                     player.cash += 1
507                     winnings[player.name] += 1
508                     residual -= 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():
513             if take > 0:
514                 self._log('ACTION Player %s wins: %s' % (name, take))