Пример #1
0
    def __begin_hand(self):
        """
        GameStateMachine callback to actually begin a game.
        """
        logger.info("Table %s: New game starting" % self.id)
        active_players = self.seats.active_players

        dealer_index = self.__find_players_index(active_players, self.seats.dealer)
        sb_index = self.__find_players_index(active_players, self.small_blind)
        bb_index = self.__find_players_index(active_players, self.big_blind)

        self.game = TexasHoldemGame(
            limit=self.limit,
            players=self.seats.active_players,
            dealer_index=dealer_index,
            sb_index=sb_index,
            bb_index=bb_index,
            callback=self.game_over,
            table=self,
        )
        self.game.advance()
Пример #2
0
    def __create_game(self, chip_counts, dealer_index, sb_index, bb_index,
        deck=None):

        limit = FixedLimit(small_bet=Currency(2), big_bet=Currency(4))

        temp_tuple = create_table(chip_counts, dealer_index)
        table = temp_tuple[1]
        self.players = temp_tuple[2]

        # Copy the players list, the game can modify it's own list and we
        # need to maintain the references to the original players:
        players_copy = []
        players_copy.extend(self.players)

        self.game = TexasHoldemGame(limit=limit, players=players_copy,
            dealer_index=dealer_index, sb_index=sb_index, bb_index=bb_index,
            callback=self.game_over_callback, table=table, deck=deck)
        self.game.advance()

        # Referenced in game over callback for determining that a game ended
        # as expected.
        self.game_over = False
Пример #3
0
class Table(object):

    """
    Representation of a table at which a poker game is taking place.
    """

    def __init__(self, name, limit, seats=10, server=None):
        global table_id_counter
        table_id_counter += 1
        self.id = table_id_counter

        self.name = name
        self.limit = limit
        self.seats = Seats(num_seats=seats)

        self.gsm = GameStateMachine()
        self.gsm.add_state(STATE_SMALL_BLIND, self.prompt_small_blind)
        self.gsm.add_state(STATE_BIG_BLIND, self.prompt_big_blind)
        self.gsm.add_state(HAND_UNDERWAY, self.__begin_hand)
        logger.info("Created table: %s" % self)

        self.observers = []
        self.game_over_event_queue = []
        self.game = None

        # Optional server object represents a parent object that creates
        # tables. If provided, it will be used for any communication with
        # players, as well as notified whenever a hand has ended.
        self.server = server

    def __repr__(self):
        return "%s (#%s)" % (self.name, self.id)

    def begin(self):
        """
        Select a new dealer and prompt for players to agree to post
        their blinds. Once we receive the appropriate responses the hand
        will be started.

        Intended to be called by a parent object, usually a server.
        """
        if len(self.seats.active_players) < MIN_PLAYERS_FOR_HAND:
            raise RounderException("Table %s: %s players required to begin hand." % (self.id, MIN_PLAYERS_FOR_HAND))
        if self.gsm.current == None:
            self.seats.new_dealer()
            self.gsm.advance()
        else:
            raise RounderException("Table %s: hand already underway.")

    @staticmethod
    def __find_players_index(player_list, player):
        """ This needs to go... """
        i = 0
        for p in player_list:
            if p.username == player.username:
                return i
            i += 1
        return None

    def __begin_hand(self):
        """
        GameStateMachine callback to actually begin a game.
        """
        logger.info("Table %s: New game starting" % self.id)
        active_players = self.seats.active_players

        dealer_index = self.__find_players_index(active_players, self.seats.dealer)
        sb_index = self.__find_players_index(active_players, self.small_blind)
        bb_index = self.__find_players_index(active_players, self.big_blind)

        self.game = TexasHoldemGame(
            limit=self.limit,
            players=self.seats.active_players,
            dealer_index=dealer_index,
            sb_index=sb_index,
            bb_index=bb_index,
            callback=self.game_over,
            table=self,
        )
        self.game.advance()

    def game_over(self):
        """ Called by a game when it has finished. """
        logger.info("Table %s: Game over" % self.id)

        self.small_blind = None
        self.big_blind = None
        self.game = None
        self.gsm.reset()

        for event in self.game_over_event_queue:
            self.notify_all(event)
        self.game_over_event_queue = []

        # Pass control up to the server if we were provided one.
        # if self.server != None:
        #    self.game_over_callback()

    def __restart(self):
        """
        Restarts the action at this table. Mostly just useful in the event
        we're three handed and the big blind sits out, requiring that we
        find a new small blind.
        """
        logger.debug("Table %s: Restarting hand" % self.id)
        # TODO: exception if game is already underway
        self.small_blind = None
        self.big_blind = None
        self.gsm.reset()
        self.gsm.advance()

    def wait(self):
        """
        Put the table on hold while we wait for more players.

        Parent will normally restart the action. Should never be called when
        a hand is already underway.
        """
        logger.info("Table %s: Waiting for more players." % (self.id))
        if self.gsm.current != None:
            event = HandCancelled(self)
            self.notify_all(event)

        self.gsm.reset()
        self.small_blind = None
        self.big_blind = None
        self.game = None

    def seat_player(self, player, seat_num):
        self.seats.seat_player(player, seat_num)
        player.table = self
        logger.debug("Table %s: %s took seat %s" % (self.id, player.username, seat_num))
        event = PlayerJoinedTable(self, player.username, seat_num)
        self.notify_all(event)

    def prompt_small_blind(self):
        """
        Prompt the small blind to agree to post. No chips actually change
        hands here, but the table is responsible for finding the two players
        who agree to post the blinds to pass into the next game played. The
        game itself is responsible for collecting those blinds.
        """
        sb = self.seats.small_blind_to_prompt()
        logger.debug("Table %s: Requesting small blind from: %s" % (self.id, sb.username))
        post_sb = PostBlind(self.limit.small_blind)
        self.prompt_player(sb, [post_sb])

    def prompt_player(self, player, actions_list):
        # self.pending_actions[player] = actions_list

        # TODO: is this even needed?
        # Doesn't actually prompt the player.
        player.prompt(actions_list)

        event = PlayerPrompted(self, player.username)
        self.notify_all(event)

        if self.server != None:
            self.server.prompt_player(self, player.username, actions_list)

    def prompt_big_blind(self):
        """
        Prompt the big blind to agree to post. No chips actually change
        hands here, but the table is responsible for finding the two players
        who agree to post the blinds to pass into the next game played. The
        game itself is responsible for collecting those blinds.
        """

        # If heads-up, non-dealer becomes the big blind:
        bb = self.seats.big_blind_to_prompt()
        logger.debug("Table %s: Requesting big blind from: %s" % (self.id, bb.username))
        post_bb = PostBlind(self.limit.big_blind)
        self.prompt_player(bb, [post_bb])

    def sit_out(self, player, left_table=False):
        """
        Called by a player who wishes to sit out.

        Because the edge case code for when a player sits out is so similar
        to when they leave the table, handling both in this one method.
        """
        logger.info("Table %s: Sitting player out: %s" % (self.id, player))
        pending_actions_copy = []
        pending_actions_copy.extend(player.pending_actions)
        player.sit_out()

        event = PlayerSatOut(self, player.username)
        if left_table:
            seat_num = self.seats.get_seat_number(player.username)
            self.seats.remove_player(player.username)
            event = PlayerLeftTable(self, player.username, seat_num)

        if self.hand_underway():
            self.game_over_event_queue.append(event)
            self.game.sit_out(player)
        else:
            self.notify_all(event)
            # Check if this players departure interferes with our gathering
            # blinds for a new hand:

            if len(self.seats.active_players) < MIN_PLAYERS_FOR_HAND:
                logger.debug("Table %s: Not enough players for a new hand." % (self.id))
                self.wait()

            if (
                find_action_in_list(PostBlind, pending_actions_copy) != None
                and self.gsm.get_current_state() == STATE_SMALL_BLIND
            ):
                player.sit_out()
                self.prompt_small_blind()

            if (
                find_action_in_list(PostBlind, pending_actions_copy) != None
                and self.gsm.get_current_state() == STATE_BIG_BLIND
            ):
                player.sit_out()
                if len(self.seats.active_players) == 2:
                    # if down to heads up, we need a different small blind:
                    self.__restart()
                self.prompt_big_blind()

    def process_action(self, username, action_index, params):
        """
        Process an incoming action from a player.

        Actions are supplied to the player as a list, but to ensure a player
        never performs an action they weren't allowed to in the first place,
        clients return an action index into the original list.

        Actions can accept parameters, which are returned from the client
        as a list and passed to the actual action for validation and use.

        This method *must* do nothing but locate the correct action and apply
        it's parameters. The game will handle the action.
        """
        if not self.seats.has_username(username):
            raise RounderException("Unable to find player %s at table %s" % (username, self.id))
        p = self.seats.players_by_username[username]

        # Verify the action index is valid:
        if action_index < 0 or action_index > len(p.pending_actions) - 1:
            raise RounderException("Invalid action index: %s" % action_index)
        action = p.pending_actions[action_index]

        action.validate(params)

        pending_actions_copy = []
        pending_actions_copy.extend(p.pending_actions)
        p.clear_pending_actions()

        if isinstance(action, PostBlind):
            if self.gsm.get_current_state() == STATE_SMALL_BLIND:
                self.small_blind = p
                # Game actually collects the blinds, but it makes more sense
                # for the client to see the event as soon as they agree to
                # post, as they already saw that they were prompted.
                blind_event = PlayerPostedBlind(self, p.username, self.limit.small_blind)
                self.notify_all(blind_event)
                self.gsm.advance()
            elif self.gsm.get_current_state() == STATE_BIG_BLIND:
                self.big_blind = p
                blind_event = PlayerPostedBlind(self, p.username, self.limit.big_blind)
                self.notify_all(blind_event)
                self.gsm.advance()
        else:
            self.game.process_action(p, action)

    # Setup two properties for the small and big blinds, which are actually
    # stored on the tables seat object.

    def __get_small_blind(self):
        return self.seats.small_blind

    def __set_small_blind(self, small_blind):
        self.seats.small_blind = small_blind

    small_blind = property(__get_small_blind, __set_small_blind)

    def __get_big_blind(self):
        return self.seats.big_blind

    def __set_big_blind(self, big_blind):
        self.seats.big_blind = big_blind

    big_blind = property(__get_big_blind, __set_big_blind)

    def __get_dealer(self):
        return self.seats.dealer

    dealer = property(__get_dealer, None)

    def add_observer(self, username):
        """ Add a username to the list of observers. """
        # Sanity check: make sure this user isn't already observing:
        logger.info("Table %s: %s observing table" % (self.id, username))
        if username in self.observers:
            raise RounderException("%s already observing table %s" % (username, self.id))
        self.observers.append(username)

    def remove_observer(self, username):
        """
        Remove a username from the list of observers.

        Called both when a user disconnects and leaves the table.
        """
        logger.info("Table %s: %s left table." % (self.id, username))
        self.observers.remove(username)
        # TODO: Split into two calls, one for leaving seat, another for
        # leaving the actual table?
        # TODO: internal state to worry about here?
        if self.seats.has_username(username):
            seat_num = self.seats.get_seat_number(username)
            player = self.seats.get_player(seat_num)
            self.sit_out(player, left_table=True)

    def notify_all(self, event):
        """ Notify observers of this table that a player was seated. """
        for o in self.observers:
            logger.debug("Table %s: Notifying %s: %s" % (self.id, o, event))
            if self.server != None:
                self.server.notify(self.id, o, event)

    def notify(self, player, event):
        """
        Notify a specific player of an event intended for their eyes only.
        """
        logger.debug("Table %s: Notifying %s: %s" % (self.id, player, event))
        if self.server != None:
            self.server.notify(self.id, player, event)

    def hand_underway(self):
        """ Return True if a hand is currently underway. """
        return self.gsm.get_current_state() == HAND_UNDERWAY

    def chat_message(self, player, message):
        logger.debug("Table %s: Chat Message: <%s> %s" % (self.id, player, message))
        chat_event = PlayerSentChatMessage(self, player, message)
        self.notify_all(chat_event)
Пример #4
0
class TexasHoldemTests(unittest.TestCase):

    def game_over_callback(self):
        self.game_over = True

    def __create_game(self, chip_counts, dealer_index, sb_index, bb_index,
        deck=None):

        limit = FixedLimit(small_bet=Currency(2), big_bet=Currency(4))

        temp_tuple = create_table(chip_counts, dealer_index)
        table = temp_tuple[1]
        self.players = temp_tuple[2]

        # Copy the players list, the game can modify it's own list and we
        # need to maintain the references to the original players:
        players_copy = []
        players_copy.extend(self.players)

        self.game = TexasHoldemGame(limit=limit, players=players_copy,
            dealer_index=dealer_index, sb_index=sb_index, bb_index=bb_index,
            callback=self.game_over_callback, table=table, deck=deck)
        self.game.advance()

        # Referenced in game over callback for determining that a game ended
        # as expected.
        self.game_over = False

    def test_collect_blinds(self):
        self.__create_game([1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000,
            1000, 1000], 0, 1, 2)
        self.assertEquals(10, len(self.game.players))
        # self.assertEquals(3, self.game.pot_mgr.total_value())
        self.assertEquals(CHIPS - 1, self.players[1].chips)
        self.assertEquals(CHIPS - 2, self.players[2].chips)

        # At this point, players should be dealt their hole cards:
        for player in self.players:
            self.assertEquals(2, len(player.cards))

    def test_preflop_everybody_in(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)

        self.assertEquals(STATE_PREFLOP, self.game.gsm.get_current_state())
        self.__call(self.players[3], 2, CHIPS - 2)

        self.assertEquals(STATE_PREFLOP, self.game.gsm.get_current_state())
        self.__call(self.players[0], 2, CHIPS - 2)

        self.assertEquals(STATE_PREFLOP, self.game.gsm.get_current_state())
        self.__call(self.players[1], 1, CHIPS - 2)

        self.assertEquals(STATE_PREFLOP, self.game.gsm.get_current_state())
        self.__call(self.players[2], 0, CHIPS - 2)

        # Onward to the flop!
        self.assertEquals(STATE_FLOP, self.game.gsm.get_current_state())

    def test_preflop_big_blind_checks(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)
        self.__call(self.players[3], 2, CHIPS - 2)
        self.__call(self.players[0], 2, CHIPS - 2)
        self.__call(self.players[1], 1, CHIPS - 2)
        self.__call(self.players[2], 0, CHIPS - 2)
        self.assertEquals(STATE_FLOP, self.game.gsm.get_current_state())

    def __find_player_with_action(self):
        for player in self.players:
            if player.pending_actions:
                return player

    def __play_round(self, plays):
        """ Make the simple plays 'c' == call, 'f' == fold.  """
        for play in plays:
            player = self.__find_player_with_action()
            if play == 'c':
                action = find_action_in_list(Call, player.pending_actions)
            elif play == 'f':
                action = find_action_in_list(Fold, player.pending_actions)
            else:
                self.assertTrue(False, "Only 'c'all and 'f'old supported")
            self.game.process_action(player, action)

    def __call(self, player, expected_amount, expected_chips=None):
        self.assertEquals(3, len(player.pending_actions))
        call = find_action_in_list(Call, player.pending_actions)
        self.assertEquals(expected_amount, call.amount)
        self.game.process_action(player, call)
        if expected_chips:
            self.assertEquals(expected_chips, player.chips)

    def __raise(self, player, amount, expected_chips):
        self.assertEquals(3, len(player.pending_actions))
        raise_action = find_action_in_list(Raise, player.pending_actions)
        self.assertEquals(None, raise_action.amount)
        raise_action.validate([amount])
        self.game.process_action(player, raise_action)
        self.assertEquals(amount, raise_action.amount)
        self.assertEquals(expected_chips, player.chips)

    def __fold(self, player, expected_chips):
        self.assertEquals(3, len(player.pending_actions))
        fold = find_action_in_list(Fold, player.pending_actions)
        self.game.process_action(player, fold)
        self.assertEquals(expected_chips, player.chips)

    def test_preflop_fold_to_big_blind(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)
        self.__fold(self.players[3], CHIPS)
        self.__fold(self.players[0], CHIPS)
        self.__fold(self.players[1], CHIPS - 1)
        self.assertTrue(self.game.finished)
        self.assertTrue(self.game_over)
        self.assertEquals(3, self.game.pot_mgr.total_value())
        self.assertEquals(CHIPS + 1, self.players[2].chips)

    def test_flop_checked_around(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)
        self.__call(self.players[3], 2, CHIPS - 2)
        self.__call(self.players[0], 2, CHIPS - 2)
        self.__call(self.players[1], 1, CHIPS - 2)
        self.__call(self.players[2], 0, CHIPS - 2)
        self.assertEquals(STATE_FLOP, self.game.gsm.get_current_state())
        self.assertEquals(3, len(self.game.community_cards))

        self.__call(self.players[1], 0, CHIPS - 2)
        self.__call(self.players[2], 0, CHIPS - 2)
        self.__call(self.players[3], 0, CHIPS - 2)
        self.__call(self.players[0], 0, CHIPS - 2)
        self.assertEquals(STATE_TURN, self.game.gsm.get_current_state())

    def test_flop_betting(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)
        self.__call(self.players[3], 2, CHIPS - 2)
        self.__call(self.players[0], 2, CHIPS - 2)
        self.__call(self.players[1], 1, CHIPS - 2)
        self.__call(self.players[2], 0, CHIPS - 2)
        self.assertEquals(STATE_FLOP, self.game.gsm.get_current_state())
        self.assertEquals(3, len(self.game.community_cards))

        self.__raise(self.players[1], 2, CHIPS - 4)
        self.__call(self.players[2], 2, CHIPS - 4)
        self.__raise(self.players[3], 2, CHIPS - 6)
        self.__call(self.players[0], 4, CHIPS - 6)
        self.__raise(self.players[1], 2, CHIPS - 8)
        self.__call(self.players[2], 4, CHIPS - 8)
        self.__raise(self.players[3], 2, CHIPS - 10)
        self.__call(self.players[0], 4, CHIPS - 10)
        self.__call(self.players[1], 2, CHIPS - 10)
        self.__call(self.players[2], 2, CHIPS - 10)

        self.assertEquals(40, self.game.pot_mgr.total_value())
        self.assertEquals(STATE_TURN, self.game.gsm.get_current_state())

    def test_flop_betting_with_raises_and_folds(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)
        self.__call(self.players[3], 2, CHIPS - 2)
        self.__call(self.players[0], 2, CHIPS - 2)
        self.__call(self.players[1], 1, CHIPS - 2)
        self.__call(self.players[2], 0, CHIPS - 2)
        self.assertEquals(STATE_FLOP, self.game.gsm.get_current_state())
        self.assertEquals(3, len(self.game.community_cards))

        self.__raise(self.players[1], 2, CHIPS - 4)
        self.__call(self.players[2], 2, CHIPS - 4)
        self.__fold(self.players[3], CHIPS - 2)
        self.__call(self.players[0], 2, CHIPS - 4)

        self.assertEquals(14, self.game.pot_mgr.total_value())
        self.assertEquals(STATE_TURN, self.game.gsm.get_current_state())

    def test_hand_ends_on_flop(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)
        self.__call(self.players[3], 2, CHIPS - 2)
        self.__call(self.players[0], 2, CHIPS - 2)
        self.__call(self.players[1], 1, CHIPS - 2)
        self.__call(self.players[2], 0, CHIPS - 2)

        # Flop:
        self.__raise(self.players[1], 2, CHIPS - 4)
        self.__fold(self.players[2], CHIPS - 2)
        self.__fold(self.players[3], CHIPS - 2)
        self.__fold(self.players[0], CHIPS - 2)

        self.assertTrue(self.game.finished)
        self.assertTrue(self.game_over)
        self.assertEquals(10, self.game.pot_mgr.total_value())
        self.assertEquals(CHIPS + 6, self.players[1].chips)

    def test_full_hand_betting(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)
        self.__call(self.players[3], 2, CHIPS - 2)
        self.__call(self.players[0], 2, CHIPS - 2)
        self.__call(self.players[1], 1, CHIPS - 2)
        self.__call(self.players[2], 0, CHIPS - 2)
        self.assertEquals(STATE_FLOP, self.game.gsm.get_current_state())
        self.assertEquals(3, len(self.game.community_cards))

        self.__raise(self.players[1], 2, CHIPS - 4)
        self.__call(self.players[2], 2, CHIPS - 4)
        self.__call(self.players[3], 2, CHIPS - 4)
        self.__call(self.players[0], 2, CHIPS - 4)

        self.assertEquals(16, self.game.pot_mgr.total_value())
        self.assertEquals(STATE_TURN, self.game.gsm.get_current_state())

        self.__call(self.players[1], 0, CHIPS - 4)
        self.__call(self.players[2], 0, CHIPS - 4)
        self.__call(self.players[3], 0, CHIPS - 4)
        self.__call(self.players[0], 0, CHIPS - 4)
        self.assertEquals(STATE_RIVER, self.game.gsm.get_current_state())

        self.__call(self.players[1], 0)
        self.__call(self.players[2], 0)
        self.__call(self.players[3], 0)
        self.__call(self.players[0], 0)
        self.assertEquals(STATE_GAMEOVER, self.game.gsm.get_current_state())
        self.assertTrue(self.game.finished)
        self.assertEquals(16, self.game.pot_mgr.total_value())

        # TODO check that there was a winner and they received the pot?

    def test_player_to_act_sits_out(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)
        self.__call(self.players[3], 2, CHIPS - 2)
        self.__call(self.players[0], 2, CHIPS - 2)

        self.assertTrue(self.players[1] in self.game.pending_actions.keys())
        self.players[1].sit_out() # table does this normally
        self.game.sit_out(self.players[1])
        self.assertFalse(self.players[1] in self.game.pending_actions.keys())
        self.assertTrue(self.players[1].folded)

        # Action should have moved on to the next player:
        self.assertTrue(self.players[2] in self.game.pending_actions.keys())

    def test_non_pending_player_sits_out(self):
        self.__create_game([1000, 1000, 1000, 1000], 0, 1, 2)
        self.__raise(self.players[3], 2, CHIPS - 4)
        self.__call(self.players[0], 4, CHIPS - 4)

        # Player 2 sits out when we're waiting for player 1 to act:
        self.players[2].sit_out() # table does this normally
        self.game.sit_out(self.players[2])
        self.assertFalse(self.players[2].folded)

        # Now player 1 calls, player 2 should immediately fold:
        self.__call(self.players[1], 3, CHIPS - 4)
        self.assertTrue(self.players[2].folded)

    def test_best_hand_on_flop(self):
        """ Player 0 with best hand loses because of fold """
        cards = ['Ah', '2d', '3d', '4d', # First Card
                 'Kh', '5d', '6d', '7d', # Second Card
                 'Qh', 'Jh', 'Th',      # Flop
                 '8s',                   # Turn
                 '9s']                   # River

        deck = Deck()
        reorder_deck(deck, create_cards_from_list(cards))
        self.__create_game([1000, 1000, 1000, 1000], 3, 1, 2, deck)

        self.__play_round(['c', 'c', 'c', 'c']) # Preflop (order different)
        self.__play_round(['f', 'c', 'c', 'f']) # Flop
        self.__play_round(['c', 'c'])           # Turn
        self.__play_round(['c', 'c'])           # River

        # Straight on board, should split pot between [1, 2]
        self.assertEquals(STATE_GAMEOVER, self.game.gsm.get_current_state())
        self.assertTrue(self.game.finished)
        self.assertEquals(CHIPS + 2, self.players[2].chips)
        self.assertEquals(CHIPS + 2, self.players[1].chips)