def consider_all(self): """ Plays every action in range_action, except the zero-weighted ones. If the game can continue, this will return an appropriate action_result. If not, it will return a termination. The game will continue if any action results in at least two players remaining in the hand, and at least one player left to act. (Whether or not that includes this player.) Inputs: see __init__ Outputs: an ActionResult: fold, passive, aggressive, or terminate Side effects: - reduce current factor based on non-playing ranges - redeal rgp's cards based on new range - (later) equity payments and such """ # note that we only consider the possible # mostly copied from the old re_deal cards_dealt = {rgp: rgp.cards_dealt for rgp in self.game.rgps} dead_cards = [card for card in self.game.board if card is not None] dead_cards.extend(concatenate([v for k, v in cards_dealt.iteritems() if k is not self.rgp])) fold_options = self.range_action.fold_range \ .generate_options_unweighted(dead_cards) passive_options = self.range_action.passive_range \ .generate_options_unweighted(dead_cards) aggressive_options = self.range_action.aggressive_range \ .generate_options_unweighted(dead_cards) # Consider fold fold_action = ActionResult.fold() if len(fold_options) > 0: self.bough.append(Branch(self.fold_continue(), fold_options, fold_action, self.range_action.fold_range)) # Consider call passive_action = ActionResult.call(self.current_options.call_cost) if len(passive_options) > 0: self.bough.append(Branch(self.passive_continue(), passive_options, passive_action, self.range_action.passive_range)) # Consider raise aggressive_action = ActionResult.raise_to( self.range_action.raise_total, self.current_options.is_raise) if len(aggressive_options) > 0: self.bough.append(Branch(self.aggressive_continue(), aggressive_options, aggressive_action, self.range_action.aggressive_range))
def consider_all(self): """ Plays every action in range_action, except the zero-weighted ones. If the game can continue, this will return an appropriate action_result. If not, it will return a termination. The game will continue if any action results in at least two players remaining in the hand, and at least one player left to act. (Whether or not that includes this player.) Inputs: see __init__ Outputs: an ActionResult: fold, passive, aggressive, or terminate Side effects: - reduce current factor based on non-playing ranges - redeal rgp's cards based on new range - (later) equity payments and such """ # note that we only consider the possible # mostly copied from the old re_deal cards_dealt = {rgp: rgp.cards_dealt for rgp in self.game.rgps} dead_cards = [card for card in self.game.board if card is not None] dead_cards.extend( concatenate( [v for k, v in cards_dealt.iteritems() if k is not self.rgp])) fold_options = self.range_action.fold_range \ .generate_options_unweighted(dead_cards) passive_options = self.range_action.passive_range \ .generate_options_unweighted(dead_cards) aggressive_options = self.range_action.aggressive_range \ .generate_options_unweighted(dead_cards) # Consider fold fold_action = ActionResult.fold() if len(fold_options) > 0: self.bough.append( Branch(self.fold_continue(), fold_options, fold_action, self.range_action.fold_range)) # Consider call passive_action = ActionResult.call(self.current_options.call_cost) if len(passive_options) > 0: self.bough.append( Branch(self.passive_continue(), passive_options, passive_action, self.range_action.passive_range)) # Consider raise aggressive_action = ActionResult.raise_to( self.range_action.raise_total, self.current_options.is_raise) if len(aggressive_options) > 0: self.bough.append( Branch(self.aggressive_continue(), aggressive_options, aggressive_action, self.range_action.aggressive_range))
def range_action_to_action(range_action, hand, current_options): """ range_action is a ActionDetails, ranges have text descriptions hand is a list of two Card returns a new range, and an action """ # Note that this gives incorrect results if an option is in two ranges if range_contains_hand(range_action.fold_range, hand): return range_action.fold_range, ActionResult.fold() elif range_contains_hand(range_action.passive_range, hand): return range_action.passive_range, \ ActionResult.call(current_options.call_cost) elif range_contains_hand(range_action.aggressive_range, hand): return range_action.aggressive_range, \ ActionResult.raise_to(range_action.raise_total, current_options.is_raise) else: raise ValueError("hand is %s, range_action is invalid: %r" % (hand, range_action))
def calculate_what_will_be(self): """ Choose one of the non-terminal actions, or return termination if they're all terminal. Also update game's current_factor. """ # reduce current factor by the ratio of non-terminal-to-terminal options non_terminal = [] terminal = [] for branch in self.bough: if not branch.options: continue if branch.is_continue: logging.debug("gameid %d, potential action %r would continue", self.game.gameid, branch.action) non_terminal.extend(branch.options) else: logging.debug("gameid %d, potential action %r would terminate", self.game.gameid, branch.action) terminal.extend(branch.options) total = len(non_terminal) + len(terminal) reduction = float(len(non_terminal)) / total logging.debug( "gameid %d, with %d non-terminal and %d terminal, " + "multiplying current factor by %0.2f from %0.2f to %0.2f", self.game.gameid, len(non_terminal), len(terminal), reduction, self.game.current_factor, self.game.current_factor * reduction) # the more non-terminal, the less effect on current factor self.game.current_factor *= reduction if non_terminal: chosen_option = random.choice(non_terminal) # but which action was chosen? for branch in self.bough: if chosen_option in branch.options: logging.debug("gameid %d, chosen %r for action %r", self.game.gameid, chosen_option, branch.action) self.action_result = branch.action self.re_range(branch) else: logging.debug("gameid %d, what will be is to terminate", self.game.gameid) self.action_result = ActionResult.terminate() return self.action_result
def calculate_what_will_be(self): """ Choose one of the non-terminal actions, or return termination if they're all terminal. Also update game's current_factor. """ # reduce current factor by the ratio of non-terminal-to-terminal options non_terminal = [] terminal = [] for branch in self.bough: if not branch.options: continue if branch.is_continue: logging.debug("gameid %d, potential action %r would continue", self.game.gameid, branch.action) non_terminal.extend(branch.options) else: logging.debug("gameid %d, potential action %r would terminate", self.game.gameid, branch.action) terminal.extend(branch.options) total = len(non_terminal) + len(terminal) reduction = float(len(non_terminal)) / total logging.debug("gameid %d, with %d non-terminal and %d terminal, " + "multiplying current factor by %0.2f from %0.2f to %0.2f", self.game.gameid, len(non_terminal), len(terminal), reduction, self.game.current_factor, self.game.current_factor * reduction) # the more non-terminal, the less effect on current factor self.game.current_factor *= reduction if non_terminal: chosen_option = random.choice(non_terminal) # but which action was chosen? for branch in self.bough: if chosen_option in branch.options: logging.debug("gameid %d, chosen %r for action %r", self.game.gameid, chosen_option, branch.action) self.action_result = branch.action self.re_range(branch) else: logging.debug("gameid %d, what will be is to terminate", self.game.gameid) self.action_result = ActionResult.terminate() return self.action_result
def consider_all(self): """ Plays every action in range_action, except the zero-weighted ones. If the game can continue, this will return an appropriate action_result. If not, it will return a termination. The game will continue if any action results in at least two players remaining in the hand, and at least one player left to act. (Whether or not that includes this player.) Inputs: see __init__ Outputs: range ratios Side effects: - analyse effects of a fold, a passive play, or an aggressive play """ # note that we only consider the possible # mostly copied from the old re_deal (originally!) # TODO: REVISIT: sometimes this considers something not to be an... # option when it truly is, e.g: # - Villain happens to draw AA for dealt/excluded cards # - board has two aces also # - Hero has only Ax in his calling range # - result is that villain has 0% calls for this game # - blech! dead_cards = generate_excluded_cards(self.game, hero=self.rgp) fold_options = self.range_action.fold_range \ .generate_options(dead_cards) passive_options = self.range_action.passive_range \ .generate_options(dead_cards) aggressive_options = self.range_action.aggressive_range \ .generate_options(dead_cards) # Consider fold fold_action = ActionResult.fold() if len(fold_options) > 0: self.bough.append(Branch(self.fold_continue(), fold_options, fold_action, self.range_action.fold_range)) # Consider call passive_action = ActionResult.call(self.current_options.call_cost) if len(passive_options) > 0: self.bough.append(Branch(self.passive_continue(), passive_options, passive_action, self.range_action.passive_range)) # Consider raise aggressive_action = ActionResult.raise_to( self.range_action.raise_total, self.current_options.is_raise) if len(aggressive_options) > 0: self.bough.append(Branch(self.aggressive_continue(), aggressive_options, aggressive_action, self.range_action.aggressive_range)) len_fol = len(fold_options) len_pas = len(passive_options) len_agg = len(aggressive_options) total = len_fol + len_pas + len_agg return { 'fold': 1.0 * len_fol / total, 'passive': 1.0 * len_pas / total, 'aggressive': 1.0 * len_agg / total}
def from_game(cls, game): """ Create a partial game tree from a single game. Note that this still creates branches (multi-child nodes) due to non-terminal folds and showdowns. """ # TODO: REVISIT: Blackbox testing? Unit testing? Something! # Any error in any of this logic will create some obscure bug in some # game tree. # TODO: 4: A general purpose replayer. How many times do we have to # write this code before we refactor it into something reusable... and # demonstrably correct! actual_ranges = {} for rgp, player in zip(game.rgps, game.situation.players): new_range = remove_board_from_range(player.range, game.situation.board) actual_ranges[rgp.userid] = new_range.description board_raw = game.situation.board_raw session = object_session(game) # We need to look for: # - GameHistoryActionResult, to find actions that happened # - GameHistoryRangeAction, to find folds # - GameHistoryShowdown, to find showdown calls # - GameHistoryBoard, to know its the river, because then three-handed # folds can be terminal history = [] for table in [tables.GameHistoryActionResult, tables.GameHistoryRangeAction, tables.GameHistoryShowdown, tables.GameHistoryBoard]: history.extend(session.query(table) \ .filter(table.gameid == game.gameid).all()) history.sort(key=lambda row: row.order) current_round = game.situation.current_round stacks = {rgp.userid: player.stack for rgp, player in zip(game.rgps, game.situation.players)} contrib = {rgp.userid: player.contributed for rgp, player in zip(game.rgps, game.situation.players)} total_contrib = dict(contrib) to_act = {rgp.userid for rgp, player in zip(game.rgps, game.situation.players) if player.left_to_act} pot = game.situation.pot_pre + \ sum(p.contributed for p in game.situation.players) raise_total = max(p.contributed for p in game.situation.players) remain = {rgp.userid for rgp in game.rgps} root = cls(game.situation.current_round, board_raw, None, None, None, actual_ranges, total_contrib) node = root # where we're adding actions prev_range_action = None for item in history: # reset game state for new round if isinstance(item, tables.GameHistoryBoard): current_round = item.street board_raw = item.cards to_act = set(remain) contrib = {rgp.userid: 0 for rgp in game.rgps} raise_total = 0 for userid, old_range in actual_ranges.iteritems(): new_range = remove_board_from_range(HandRange(old_range), Card.many_from_text(item.cards)) actual_ranges[userid] = new_range.description if isinstance(item, tables.GameHistoryShowdown): # add call call_cost = raise_total - contrib[prev_range_action.userid] action = ActionResult.call(call_cost) ranges = dict(actual_ranges) ranges[prev_range_action.userid] = \ prev_range_action.passive_range showdown_contrib = dict(total_contrib) showdown_contrib[prev_range_action.userid] += call_cost showdown_pot = pot + call_cost child = cls(current_round, board_raw, prev_range_action.userid, action, node, ranges, total_contrib=showdown_contrib, winners=remain, final_pot=showdown_pot) node.children.append(child) # Only if the fold is terminal is it part of the tree. # Folds are terminal when: # - two-handed; or, # - three-handed when: # - all other players have acted on the river; or, # - are all in before the river; or, # - they fold 100% if isinstance(item, tables.GameHistoryRangeAction): prev_range_action = item if item.fold_ratio is None: has_fold = item.fold_range != NOTHING else: has_fold = item.fold_ratio != 0.0 is_final_round = current_round == RIVER or \ not all(stacks.values()) heads_up = len(remain) == 2 final_action = len(to_act) == 1 and is_final_round # There's no (implicit) fold when multi-way and play continues. if has_fold and (final_action or heads_up): # Play would not continue with a fold here, so there will be # no actual fold action. # TODO: 3: use game_continues? # Add a non-played fold. We keep the folded player's range # in here, because it is relevant to consider the EV of each # folded combo. winners = set(remain) winners.remove(item.userid) # winners may be one (HU) or multiple (multiway) ranges = actual_ranges ranges[item.userid] = item.fold_range child = cls(current_round, board_raw, item.userid, ActionResult.fold(), node, ranges, total_contrib=total_contrib, winners=winners, final_pot=pot) node.children.append(child) if isinstance(item, tables.GameHistoryActionResult): # maintain game state if item.is_passive: actual_ranges[item.userid] = prev_range_action.passive_range stacks[item.userid] -= item.call_cost contrib[item.userid] += item.call_cost total_contrib[item.userid] += item.call_cost pot += item.call_cost action = ActionResult.call(item.call_cost) if item.is_aggressive: actual_ranges[item.userid] = \ prev_range_action.aggressive_range chips = item.raise_total - contrib[item.userid] stacks[item.userid] -= chips contrib[item.userid] += chips total_contrib[item.userid] += chips pot += chips raise_total = item.raise_total to_act = set(remain) action = ActionResult.raise_to(raise_total, item.is_raise) if item.is_fold: remain.remove(item.userid) action = ActionResult.fold() # and yes, we still traverse in (multi-way) to_act.remove(item.userid) # add fold, check, call, raise or bet, and traverse in child = cls(current_round, board_raw, item.userid, action, node, actual_ranges, total_contrib) node.children.append(child) # traverse down node = child return root