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)
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)
def test_callback(self): self.called_back = False machine = GameStateMachine() machine.add_state("postblinds", self.callback) machine.advance() self.assertEquals(True, self.called_back)
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)
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)
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)