Beispiel #1
0
    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
Beispiel #2
0
 def test_callback(self):
     self.called_back = False
     machine = GameStateMachine()
     machine.add_state("postblinds", self.callback)
     machine.advance()
     self.assertEquals(True, self.called_back)
Beispiel #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)
Beispiel #4
0
 def test_add_state_after_starting(self):
     machine = GameStateMachine()
     machine.add_state("postblinds", self.callback)
     machine.advance()
     self.assertRaises(RounderException, machine.add_state, "any",
         self.callback)
Beispiel #5
0
 def test_advance_too_far(self):
     machine = GameStateMachine()
     machine.add_state("postblinds", self.callback)
     self.assertEquals(None, machine.current)
     machine.advance()
     self.assertRaises(RounderException, machine.advance)
Beispiel #6
0
 def test_advance(self):
     machine = GameStateMachine()
     machine.add_state("postblinds", self.callback)
     machine.add_state("preflop", self.callback)
     machine.add_state("holecards", self.callback)
     machine.add_state("flop", self.callback)
     machine.add_state("turn", self.callback)
     machine.add_state("river", self.callback)
     machine.add_state("done", self.callback)
     self.assertEquals(None, machine.current)
     for i in range(0, 7):
         machine.advance()
         self.assertEquals(i, machine.current)
Beispiel #7
0
 def test_first_advance(self):
     machine = GameStateMachine()
     machine.add_state("postblinds", self.callback)
     self.assertEquals(None, machine.current)
     machine.advance()
     self.assertEquals(0, machine.current)
Beispiel #8
0
 def test_add_state(self):
     machine = GameStateMachine()
     machine.add_state("postblinds", self.callback)
     self.assertEquals(1, len(machine.states))
     self.assertEquals("postblinds", machine.states[0])
     self.assertEquals(self.callback, machine.actions["postblinds"])