Add docstrings and doctests to table.py + Python cleanups.
authorW. Trevor King <wking@drexel.edu>
Tue, 7 Dec 2010 15:57:52 +0000 (10:57 -0500)
committerW. Trevor King <wking@drexel.edu>
Tue, 7 Dec 2010 16:02:46 +0000 (11:02 -0500)
GAMEPLAY.py [deleted file]
table.py [new file with mode: 0644]

diff --git a/GAMEPLAY.py b/GAMEPLAY.py
deleted file mode 100644 (file)
index 101f249..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-from DECK import *
-GLOBAL_TXT = []
-
-# ***************************************
-from os import system, kill
-from popen2 import popen2,popen3
-class player:
-    def __init__(self, name='', brain=''):
-        self.cash  = 0
-        self.owe   = 0
-        self.option= True
-        self.hole  = []
-        self.IN, self.OUT = 0,0
-        self.brain = brain.strip()
-        self.name  = name+' '
-        self.FOLD  = False
-        self.score = 0
-        self.GP_BUFFER = []
-        
-        if self.brain:
-            self.OUT, self.IN = popen2("./"+self.brain)
-
-    def die(self): 
-        self.IN.write("END\n"); self.IN.flush()
-        system("pkill -9 "+self.name)
-
-    def bet(self, wager, other_player):
-        wager = int(wager)
-        if wager > self.cash: wager  = self.cash
-        if wager < 0:         wager  = 0
-        if wager-self.owe > other_player.cash: 
-            wager = other_player.cash
-        self.cash -= wager
-        self.owe  -= wager
-        self.owe   = self.owe if self.owe < 0 else 0
-        GP("ACTION Player ", self.name, " bets: ", wager)
-        return wager
-    
-    def record_info(self, pot, min_raise, flop, turn, river, other_player):
-        global GLOBAL_TXT
-        self.IN.write("INFO NAME " + str(self.name) + '\n'); self.IN.flush()
-        self.IN.write("INFO ONAME " + str(other_player.name) + '\n'); self.IN.flush()
-        self.IN.write("INFO STACK " + str(self.cash) + '\n'); self.IN.flush()
-        self.IN.write("INFO OSTACK " + str(other_player.cash) + '\n'); self.IN.flush()
-        self.IN.write("INFO POT " + str(pot) + '\n'); self.IN.flush()
-        self.IN.write("INFO MINRAISE " + str(min_raise)+'\n'); self.IN.flush()
-        self.IN.write("INFO OWE " + str(self.owe)  + '\n'); self.IN.flush()
-        self.IN.write("INFO HOLE " + pp_hand(self.hole) + '\n'); self.IN.flush()
-        self.IN.write("INFO FLOP " + pp_hand(flop) + '\n'); self.IN.flush()
-        self.IN.write("INFO TURN " + pp_hand(turn) + '\n'); self.IN.flush()
-        self.IN.write("INFO RIVER " + pp_hand(river) + '\n'); self.IN.flush()
-        self.GP_FLUSH(GLOBAL_TXT)
-
-    def GP_FLUSH(self, TXT):
-        for g in TXT:
-            if g not in self.GP_BUFFER:
-                self.GP_BUFFER.append(g)
-                self.IN.write(g + "\n")
-                self.IN.flush()
-
-    def human_play(self, pot, min_raise, flop, turn, river, other_player):
-        if GLOBAL_TXT: print GLOBAL_TXT[-1]
-        print "[", pp_hand(self.hole), "] Board: ",
-        print "[", pp_hand(flop), pp_hand(turn), pp_hand(river), "]"
-        print "POT: ", pot, " OWE: ", self.owe, " MIN_RAISE: ", min_raise,
-        print "   |   ", "STACK: ", self.cash, "Opp. STACK: ", other_player.cash
-        return raw_input("What do you want to bet, " + self.name + ": ")
-
-    def decide(self, pot, min_raise, flop, turn, river, other_player,endgame=False):
-
-        in_bet = ''
-
-        if not self.brain:
-            in_bet = self.human_play(pot,min_raise, flop, turn, river, other_player)
-
-        else:
-            self.record_info(pot,min_raise,flop,turn,river,other_player)
-            self.IN.write("MOVE \n"); self.IN.flush()
-            in_bet = self.OUT.readline().strip()
-            
-        if endgame: return False
-
-        if in_bet.isalpha(): 
-            in_bet = in_bet.upper()
-            if   in_bet == 'A': in_bet = self.cash
-            elif in_bet == 'C': in_bet = self.owe
-            else              : self.FOLD = True
-        else:
-            try   : in_bet = int(in_bet)
-            except: self.FOLD = True
-
-        if self.FOLD: return False
-
-        if in_bet >= self.cash or in_bet == self.owe or in_bet >= min_raise:
-            out_bet  = self.bet(in_bet, other_player)
-            other_player.owe += out_bet
-            return out_bet
-
-        # Bets between whats owed and min_raise are considered a call
-        if in_bet >= self.owe and in_bet < min_raise:
-            out_bet  = self.bet(self.owe, other_player)
-            other_player.owe += out_bet
-            return out_bet
-
-        # Illegal bets will be counted as folds!
-        GP("ACTION Player ", self.name, "bets ", str(in_bet), " illegally and FOLDS. Valid bets are ", str(self.owe), " or anything >= ", min_raise)
-        self.FOLD = True
-        return False
-
-
-def GP(*S): GLOBAL_TXT.append(''.join(map(str,S)))
-def returnGP(): return GLOBAL_TXT
-     
-def hand_judgement(A,B, board, pot):
-    if    A.FOLD: 
-        GP("ACTION Player ", A.name, " folds" )
-        GP("ACTION Player ", B.name, " wins: ", pot)
-        B.cash += pot
-        return GLOBAL_TXT
-
-    elif  B.FOLD:
-        GP("ACTION Player ", B.name, "folds")
-        GP("ACTION Player ", A.name, "wins: ", pot)
-        A.cash += pot
-        return GLOBAL_TXT
-    S = HOLDEM_score(A, B, board)
-
-    if   S == 1: 
-        GP("ACTION Player ", A.name, "wins: ", pot)
-        A.cash += pot
-        return GLOBAL_TXT
-
-    elif S ==-1: 
-        GP( "ACTION Player ", B.name, "wins: ", pot)
-        B.cash += pot
-        return GLOBAL_TXT
-
-    elif S == 0:
-        GP("ACTION Split pot ")
-        split_pot, carry = pot/2, pot%2
-        A.cash += split_pot + carry
-        B.cash += split_pot
-        return GLOBAL_TXT
-
-def checkFOLD(A,B):
-    if   A.FOLD or B.FOLD: return True
-    return False
-
-def betting_round(P,pot,min_raise,flop,turn,river):
-    for player in P: 
-        player.option = True
-
-    while((P[0].owe>0 or P[1].owe>0) or (P[0].option or P[1].option) and (P[0].cash)):
-        action  = P[0].decide(pot, min_raise, flop,turn,river, P[-1])
-        P[0].option = False
-
-        if action >= min_raise: min_raise = 2*action
-        if checkFOLD(P[0],P[1]): return pot
-
-        pot += action
-        P = [P[-1]] + P[:-1] # Cycle the player order
-    return pot
-
-
-def gameplay(DECK, A, B, smallB, ):
-    global GLOBAL_TXT
-
-    # Shuffle the deck and deal two cards to each player
-    shuffle(DECK)
-
-    A.hole = [DECK[n] for n in xrange(2)  ]
-    B.hole = [DECK[n] for n in xrange(2,4)]
-    board  = [DECK[n] for n in xrange(4,9)]
-
-    flop, turn, river = '', '', ''
-    GLOBAL_TXT        = []
-
-    pot   = 0
-    A.owe = 2*smallB
-    B.owe = smallB
-    A.FOLD, B.FOLD = False, False
-
-    # Handle the case if a player can't post the blinds completely, ALL_IN
-    if    B.owe > B.cash:
-        pot  += B.bet(B.owe, A)
-        pot  += A.bet(pot,   B)
-        return hand_judgement(A,B,board,pot)
-    elif  A.owe > A.cash:
-        pot += A.bet(A.owe,  B)
-        pot += B.bet(pot,    A)
-        return hand_judgement(A,B,board,pot)
-
-    # Both players can post the blinds
-    pot += A.bet(A.owe, B)
-    pot += B.bet(B.owe, A)
-
-    # PRE-FLOP ACTION - Player B is small blind, first to act
-    B.owe        += smallB
-    A.owe        -= smallB
-    min_raise     = smallB*4
-    play_order    = [B,A]
-    pot = betting_round(play_order,pot,min_raise,flop,turn,river)
-    if checkFOLD(A,B): return hand_judgement(A,B,board,pot)
-    if not A.cash or not B.cash: return hand_judgement(A,B,board,pot)
-
-    # FLOP ACTION - Player A is now first to act
-    flop          = board[:3]
-    GP( "ACTION FLOP ", pp_hand(flop) )
-    A.owe, B.owe  = 0, 0
-    min_raise     = smallB*2
-    play_order    = [A,B]
-    pot = betting_round(play_order,pot,min_raise,flop,turn,river)
-    if checkFOLD(A,B): return hand_judgement(A,B,board,pot)
-    if not A.cash or not B.cash: return hand_judgement(A,B,board,pot)
-
-    # TURN ACTION
-    turn          = board[3:4]
-    GP( "ACTION TURN ", pp_hand(turn) )
-    A.owe, B.owe  = 0, 0
-    play_order    = [A,B]
-    pot = betting_round(play_order,pot,min_raise,flop,turn,river)
-    if checkFOLD(A,B): return hand_judgement(A,B,board,pot)
-    if not A.cash or not B.cash: return hand_judgement(A,B,board,pot)
-
-    # RIVER ACTION
-    river         = board[4:5]
-    GP( "ACTION RIVER ", pp_hand(river))
-    A.owe, B.owe  = 0, 0
-    play_order    = [A,B]
-    pot = betting_round(play_order,pot,min_raise,flop,turn,river)
-    if checkFOLD(A,B): return hand_judgement(A,B,board,pot)
-    
-    # SHOWDOWN!
-    return hand_judgement(A,B,board,pot)
diff --git a/table.py b/table.py
new file mode 100644 (file)
index 0000000..98f6b44
--- /dev/null
+++ b/table.py
@@ -0,0 +1,500 @@
+"""Define a poker table with players.
+"""
+
+from math import floor
+import random
+import sys
+import subprocess
+
+from deck import pp_hand, SevenChooseFiveHand
+
+
+class IllegalBet (ValueError):
+    """Raised on invalid bet attempts.
+    """
+    def __init__(self, player, bet):
+        super(IllegalBet, self).__init__(bet)
+        self.player = player
+        self.bet = bet
+
+    def __str__(self):
+        return 'Illegal bet %s by %s' % (self.player, self.name)
+
+
+class Player (object):
+    """Poker player.
+    """
+    def __init__(self, cash=0, name='', brain=None):
+        self.cash  = cash  # uncommitted chips
+        self.name  = name
+        self.brain = brain
+        self._proc = None
+        self.clear_log()
+
+        if self.brain:
+            self._proc = subprocess.Popen(
+                './'+self.brain, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE, close_fds=True)
+
+    def __str__(self):
+        return self.name
+
+    def kill(self):
+        if self._proc != None:
+            self._proc.stdin.write('END\n')
+            self._proc.stdin.flush()
+            exit_code = self._proc.wait()
+            assert exit_code == 0, 'Error exiting from %s: %d' % (
+                self, exit_code)
+            self._proc = None
+
+    def __str__(self):
+        return self.name
+
+    def __repr__(self):
+        return '<%s %s>' % (self.__class__.__name__, self.name)
+
+    def __cmp__(self, other):
+        if other == None:
+            return 1
+        return cmp(self.name, other.name)
+
+    def cmp_hands(self, other):
+        return cmp(self.hand, other.hand)
+
+    def new_hand(self):
+        self.clear_log()
+        self.owe    = 0    # amount required to call
+        self.pot    = 0    # amount committed to the pot this round
+        self.status = 'in' # one of ['in', 'all in', 'fold']
+        self.hole  = []    # pair of down cards
+        self.hand  = None  # holds the player's hand in the event of a showdown
+
+    def active(self):
+        """Does this player still need to think?"""
+        return self.status not in ['all in', 'fold']
+
+    def pay(self, wager):
+        """Settle what you owe the table.
+
+        Called during play as well as at the end of a hand.
+        """
+        if wager >= self.cash:
+            self.status = 'all in'  # may be forced all-in (e.g. by a blind)
+            wager = self.cash
+        wager = max(wager, 0)
+        self.cash -= wager
+        self.owe  -= wager
+        self.owe   = min(self.owe, 0)
+        return wager
+
+    def _log_flush(self, stream, log):
+        """Show the brain any new log entries."""
+        new_index = len(log)
+        for line in LOG[self._next_log_index:new_index]:
+            stream.write(line + '\n')
+            stream.flush()
+        self._next_log_index = new_index
+
+    def clear_log(self):
+        self._next_log_index = 0  # unsent log index
+
+    def _record_info(self, active_players, pot, min_raise, flop, turn, river,
+                     log):
+        """Show the brain the current state of events."""
+        self._proc.stdin.write('INFO NAME %s\n' % str(self.name))
+        self._proc.stdin.write('INFO STACK %s\n' % str(self.cash))
+        self._proc.stdin.write('INFO POT %s\n' % str(pot))
+        self._proc.stdin.write('INFO MINRAISE %s\n' % str(min_raise))
+        self._proc.stdin.write('INFO OWE %s\n' % str(self.owe))
+        self._proc.stdin.write('INFO HOLE %s\n' % pp_hand(self.hole))
+        self._proc.stdin.write('INFO FLOP %s\n' % pp_hand(flop))
+        self._proc.stdin.write('INFO TURN %s\n' % pp_hand(turn))
+        self._proc.stdin.write('INFO RIVER %s\n' % pp_hand(river))
+        self._proc.stdin.flush()
+        self._log_flush(self._proc.stdin, log)
+
+    def _human_play(self, active_players, pot, min_raise, flop, turn, river,
+                    log):
+        """Ask the human at `sys.stdin`/`sys.stdout` for guidance."""
+        self._log_flush(sys.stdout, log)
+        print '[', pp_hand(self.hole), '] Board: ',
+        print '[', pp_hand(flop), pp_hand(turn), pp_hand(river), ']'
+        print 'POT: ', pot, ' OWE: ', self.owe, ' MIN_RAISE: ', min_raise,
+        print '   |   ', 'STACK: ', self.cash
+        for player in active_players:
+            if player != self:
+                print 'Opp. %s STACK: %d' % (player, player.cash)
+        return raw_input('What do you want to bet, %s:' % self)
+
+    def _bet(self, **kwargs):
+        if not self.brain:
+            return self._human_play(**kwargs)
+        else:
+            self._record_info(**kwargs)
+            self._proc.stdin.write('MOVE\n')
+            self._proc.stdin.flush()
+            return self._proc.stdout.readline().strip()
+
+    def decide(self, **kwargs):
+        """Decide on a course of action (raise, fold, ...).
+
+        The program accepts an (A) as an all-in bet, (C) as a call or
+        check and any valid bet.  Valid bets are either the amount
+        owed to the pot or at least the min raise.  All bets made
+        greater then the players stack will be considered an all-in.
+        All bets that are less than the amount owed will be considered
+        a fold.  A bet in the range of the amount owed to the min
+        raise will be considered a CALL and the player will put in the
+        amount owed to the table.  Any other input (negative, decimal,
+        or alpha) will be considered to be a FOLD.  To keep a
+        presistant state (and for speed reasons) your program will
+        have the information fed to it.  This is done by piping
+        `stdin` and `stdout` to your program.  The dealer will wait
+        once the command `MOVE` is sent.  Along with the `MOVE` tag,
+        your code will be given information on the current game state
+        and history.  See `Player.record_info()` for details.
+        """
+        bet = self._bet(**kwargs)
+        if bet.isalpha():
+            bet = bet.upper()
+            if   bet == 'A': bet = self.cash
+            elif bet == 'C': bet = self.owe
+            else           : self.status = 'fold'
+        else:
+            try   : bet = int(bet)
+            except: self.status = 'fold'
+
+        if bet >= self.cash:
+            self.status = 'all in'
+            return self.cash
+        elif bet < self.owe:
+            self.status = 'fold'
+            return 0
+        elif self.owe <= bet and bet < min_raise:  # round down to a check
+            return self.owe
+        elif self.owe >= min_raise:  # let's see some action!
+            return bet
+        raise IllegalBet(self, str(bet))
+
+
+class Blinds (object):
+    """Blind schedule and per-hand blind calculation.
+
+    >>> b = Blinds(blinds=[1, 2, 4, 8], hand_clock=10)
+    >>> b(0)
+    (1, 2, 2)
+    >>> b(9)
+    (1, 2, 2)
+    >>> b(10)
+    (2, 4, 4)
+    >>> b(100)
+    (8, 16, 16)
+    """
+    def __init__(self, blinds, hand_clock):
+        self._blinds = blinds
+        self._hand_clock = hand_clock
+
+    def __call__(self, hand):
+        """Small blind, big blind, and min raise for the `hand`th hand.
+        """
+        i = min(hand / self._hand_clock, len(self._blinds)-1)
+        small_blind = self._blinds[i]
+        return (small_blind, small_blind*2, small_blind*2)
+
+
+class Table (object):
+    """A poker table with a deck, players, and blind schedule.
+
+    Setup buffered players so we can control `Table` testing.
+
+    >>> class BufferedPlayer(Player):
+    ...     def __init__(self, **kwargs):
+    ...         super(BufferedPlayer, self).__init__(**kwargs)
+    ...         self.buffer = []
+    ...     def _bet(self, **kwargs):
+    ...         return self.buffer.pop(0)
+    >>> players = [BufferedPlayer(cash=10, name='P%d' % (i+1))
+    ...            for i in range(3)]
+    >>> t = Table(players=players, blinds=Blinds((1, 2, 4), 3))
+
+    Stack the deck so we know what's coming.
+    >>> from deck import unpp_card
+    >>> t.deck = [unpp_card(c)
+    ...           for c in 'As Ks Qs Js Ts 9s 8s 7s 6s 5s 4s'.split()]
+
+    Everyone folds the first round (P3, going last, will win).
+    >>> for p in t.players:
+    ...     p.buffer = ['-1']
+    >>> t.play_round(shuffle=False)
+    >>> print '\\n'.join(t.log)  # doctest: +REPORT_UDIFF
+    INFO CHIPCOUNT Player P1 10
+    INFO CHIPCOUNT Player P2 10
+    INFO CHIPCOUNT Player P3 10
+    ACTION Player P2 (pot 1) blinds 1
+    ACTION Player P3 (pot 2) blinds 2
+    ACTION Player P1 (pot 0) folds
+    ACTION Player P2 (pot 1) folds
+    ACTION Player P3 wins: 3
+    INFO GAMEOVER 0
+
+    Play another round on the same deck, everyone goes all in.  P3
+    will win with the higher straight flush.
+    >>> for p in t.players:
+    ...     p.buffer = ['a']
+    >>> t.play_round(shuffle=False)
+    >>> print '\\n'.join(t.log)  # doctest: +REPORT_UDIFF
+    INFO CHIPCOUNT Player P1 10
+    INFO CHIPCOUNT Player P2 9
+    INFO CHIPCOUNT Player P3 11
+    ACTION Player P3 (pot 1) blinds 1
+    ACTION Player P1 (pot 2) blinds 2
+    ACTION Player P2 (pot 9) meets and raises for a total of 9
+    ACTION Player P3 (pot 11) goes all in with 10
+    ACTION Player P1 (pot 10) goes all in with 8
+    ACTION Player P3 wins: 30
+    INFO board 8s 7s 6s 5s 4s
+    INFO Player P3 hole: Ts 9s   best: straight flush - Ts 9s 8s 7s 6s 5s 4s   cash: 30
+    INFO Player P1 hole: As Ks   best: straight flush - 8s 7s 6s 5s 4s As Ks   cash: 0
+    INFO Player P2 hole: Qs Js   best: straight flush - 8s 7s 6s 5s 4s Qs Js   cash: 0
+    INFO GAMEOVER 1
+    INFO Player P1 died after hand 2
+    INFO Player P2 died after hand 2
+
+    >>> t.dead_players
+    [(<BufferedPlayer P1>, <BufferedPlayer P2>)]
+    >>> t.players
+    [<BufferedPlayer P3>]
+    """
+    def __init__(self, deck=None, players=None, blinds=None):
+        self.deck = deck
+        self.players = players # list of surviving players
+        self.blinds = blinds
+        self.dealer = 0  # index of player acting as dealer (dealer button)
+        self.hand_count = 0
+        self.dead_players = [] # list of players in the order they died
+        # e.g. [(lastplayer,), (AtiedForThird, BtiedForThird), (secondplayer,)]
+
+    def players_from(self, index):
+        """Rotate the list of players to start from `self.players[index]`.
+
+        >>> t = Table(players=['a', 'b', 'c', 'd'])
+        >>> t.players_from(0)
+        ['a', 'b', 'c', 'd']
+        >>> t.players_from(1)
+        ['b', 'c', 'd', 'a']
+        >>> t.players_from(2)
+        ['c', 'd', 'a', 'b']
+        """
+        index = index % len(self.players)
+        return self.players[index:]+self.players[:index]
+
+    def play_round(self, shuffle=random.shuffle):
+        self.deal(shuffle=shuffle)
+        self.ante_up()
+        # PRE-FLOP ACTION
+        self.betting_round(first_round=True)
+        if self.hand_complete(): return self.judge()
+        # FLOP ACTION
+        self.flop = self.board[:3]
+        self.log.append('ACTION FLOP %s' % pp_hand(self.flop))
+        self.betting_round()
+        if self.hand_complete(): return self.judge()
+        # TURN ACTION
+        self.turn = self.board[3:4]
+        self.log.append('ACTION TURN %s' % pp_hand(self.turn))
+        self.betting_round()
+        if self.hand_complete(): return self.judge()
+        # RIVER ACTION
+        self.river = self.board[4:5]
+        self.log.append('ACTION RIVER %s' % pp_hand(self.river))
+        self.betting_round()
+        if self.hand_complete(): return self.judge()
+        # SHOWDOWN!
+        self.judge()
+
+    def deal(self, shuffle=random.shuffle):
+        """Shuffle the deck and deal two cards to each player."""
+        self.log = []
+        if shuffle:
+            shuffle(self.deck)
+        N = len(self.players)
+        self.board = [self.deck[n] for n in xrange(2*N,2*N+5)]  # common cards
+        for i,player in enumerate(self.players):
+            player.new_hand()
+            self.log.append('INFO CHIPCOUNT Player %s %s'
+                            % (player, player.cash))
+            player.hole = [self.deck[n] for n in xrange(2*i, 2*(i+1))]
+            player.hand = SevenChooseFiveHand(player.hole+self.board)
+        self.flop = self.turn = self.river = None
+
+    def ante_up(self):
+        self.small_blind,self.big_blind,self.min_raise = self.blinds(
+            self.hand_count)
+        self.pot = 0             # total chips up for grabs
+        self.per_player_pot = 0  # needed to stay active
+        if len(self.players) > 2:
+            sb,bb = self.players_from(self.dealer+1)[0:2]
+        elif len(self.players) == 2:
+            # When only two players remain, special 'head-to-head' or
+            # 'heads up' rules are enforced and the blinds are posted
+            # differently. In this case, the person with the dealer
+            # button posts the small blind, while his/her opponent
+            # places the big blind.
+            sb,bb = self.players_from(self.dealer)[0:2]
+        else:
+            raise Exception, "can't play with %d players" % len(self.players)
+        sb.owe = self.small_blind
+        self.square_player(sb, 'blinds')
+        bb.owe = self.big_blind
+        self.square_player(bb, 'blinds')
+
+    def square_player(self, player, verb, append_cost=True):
+        contribution = player.pay(player.owe)
+        self.pot += contribution
+        player.pot += contribution
+        self.per_player_pot = max(self.per_player_pot, player.pot)
+        string = 'ACTION Player %s (pot %s) %s' % (player, player.pot, verb)
+        if append_cost: string += ' %d' % contribution
+        self.log.append(string)
+        return contribution
+
+    def betting_round(self, first_round=False):
+        all_in = [p for p in self.players if p.status == 'all in']
+        folded = [p for p in self.players if p.status == 'fold']
+
+        if len(self.players) > 2:
+            if first_round == True:  # start after the big blind
+                order = self.players_from(self.dealer+3)
+            else:  # start with the small blind
+                order = self.players_from(self.dealer+1)
+        elif len(self.players) == 2:  # 'head-to-head' rules
+            # The dealer acts first before the flop. After the flop,
+            # the dealer acts last and continues to do so for the
+            # remainder of the hand.
+            if first_round == True:
+                order = self.players_from(self.dealer)
+            else:
+                order = self.players_from(self.dealer+1)
+
+        active = [p for p in order if p.active()]
+        tail = active[-1]
+        min_raise = self.min_raise
+        raiser = None  # no-one has raised yet this round
+        while (len(active) > 0):  # i.e. continue until everyone folds/calls
+            player = active.pop(0)
+            player.owe = self.per_player_pot - player.pot
+            if player == raiser or (raiser == None
+                                    and player == tail and len(active) == 0):
+                break
+            try:
+                action = player.decide(
+                    active_players=[player]+active+all_in, pot=self.pot,
+                    min_raise=min_raise, flop=self.flop, turn=self.turn,
+                    river=self.river)
+            except IllegalBet, e:
+                self.log.append((
+                        'ACTION Player %s bets %s illegally and FOLDS. '
+                        'Valid bets are %d or anything >= %d')
+                                % (e.player, e.bet, e.player.owe, min_raise))
+                player.status = 'fold'
+            if player.status == 'fold':
+                player.owe = 0  # don't charge the folding player
+                folded.append(player)
+                self.square_player(player, 'folds', append_cost=False)
+            else:  # settle up
+                if action >= min_raise:
+                    min_raise = 2*action
+                    raiser = player
+                    active.append(player)
+                    verb = 'meets and raises for a total of'
+                elif player.status == 'all in':
+                    all_in.append(player)
+                    verb = 'goes all in with'
+                else:
+                    assert action == 0, action
+                    verb = 'checks with'
+                    if player == tail and action == 0:
+                        active = []
+                    active.append(player)
+                player.owe += action
+                self.square_player(player, verb)
+
+    def hand_complete(self):
+        """Return `True` if there aren't enough active players for betting.
+        """
+        return len([p for p in self.players if p.active()]) <= 1
+
+    def judge(self):
+        """Judge the winner and allocate the winnings."""
+        runners = [p for p in self.players if p.status != 'fold']
+
+        if len(runners) == 1:  # everyone else folded, no need to show cards
+            folders = [p for p in self.players if p.status == 'fold']
+            self.pay_players(runners, folders)
+        else:  # we need to look show hands
+            runners.sort(Player.cmp_hands, reverse=True)  # sort high -> low
+            runcp = [p for p in runners]
+            losers = [p for p in self.players]  # copy into a new list
+            # start from the top, breaking ties...
+            while len(runners) > 0:
+                winners = [runners.pop(0)]
+                losers.remove(winners[-1])  # remove from losers pool
+                while len(runners) > 0 and runners[0] == winners[0]:
+                    winners.append(runners.pop(0))  # pop any winning tie
+                    losers.remove(winners[-1])  # remove from losers pool
+                self.pay_players(winners, losers)
+            # allow the players to view the end result
+            flop,turn,river = self.board[:3], self.board[3:4], self.board[4:5]
+            self.log.append('INFO board %s' % pp_hand(self.board))
+            for p in runcp:
+                self.log.append('INFO Player %s hole: %s   best: %s   cash: %d'
+                   % (p, pp_hand(p.hole), p.hand.pp_score(), p.cash))
+        self.log.append('INFO GAMEOVER %d' % self.hand_count)
+
+        # prepare for the next round
+        dead = [p for p in self.players if p.cash == 0]
+        if dead:
+            self.dead_players.append(tuple(dead))
+        self.hand_count += 1
+
+        self.dealer = (self.dealer + 1) % len(self.players)
+        next_dealing_player = self.players[self.dealer]
+        while next_dealing_player.cash == 0:  # next dealer is dead
+            # pass the dealer card to successor
+            self.dealer = (self.dealer + 1) % len(self.players)
+            next_dealing_player = self.players[self.dealer]
+        for p in dead:
+            self.log.append('INFO Player %s died after hand %d'
+                            % (p, self.hand_count))
+            self.players.remove(p)
+            p.kill()
+        self.dealer = self.players.index(next_dealing_player)
+
+    def pay_players(self, winners, losers):
+        winning_pots = sorted(set([p.pot for p in winners]))
+        winnings = dict().fromkeys([p.name for p in winners], 0)
+        for pot in winning_pots:  # smallest to largest winning pots
+            sum = 0
+            for player in winners+losers:
+                contribution = min(player.pot, pot)
+                player.pot -= contribution
+                sum += contribution
+            per_winner = int(floor(sum / len(winners)))
+            residual = sum % len(winners)
+            for player in winners:
+                player.cash += per_winner
+                winnings[player.name] += per_winner
+            # any extra chips go to the first players after the dealer
+            for player in self.players_from(self.dealer+1):
+                if residual == 0: break
+                if player in winners:
+                    player.cash += 1
+                    winnings[player.name] += 1
+                    residual -= 1
+            # drop the winners who are no longer invested in the pot
+            winners = [p for p in winners if p.pot > 0]
+            self.per_player_pot -= pot
+        for name,take in winnings.items():
+            if take > 0:
+                self.log.append('ACTION Player %s wins: %s' % (name, take))