예제 #1
0
    def analyse(self):
        """
        Perform all analysis on game that has not been done.

        If you need to reanalyse the game, delete the existing analysis first.
        """
        self.ranges = {self.game.rgps[i].userid:
                       self.game.situation.players[i].range_raw
                       for i in range(len(self.game.situation.players))}
        self.stacks = {self.game.rgps[i].userid:
                       self.game.situation.players[i].stack
                       for i in range(len(self.game.situation.players))}
        self.contrib = {self.game.rgps[i].userid:
                        self.game.situation.players[i].contributed
                        for i in range(len(self.game.situation.players))}
        self.remaining_userids = [rgp.userid for rgp in self.game.rgps]
        self.left_to_act = [self.game.rgps[i].userid
                            for i in range(len(self.game.situation.players))
                            if self.game.situation.players[i].left_to_act]

        gameid = self.game.gameid

        logging.debug("gameid %d, AnalysisReplayer, analyse", gameid)
        items = [self.session.query(table)
                 .filter(table.gameid == gameid).all()
                 for table in [GameHistoryBoard,
                               GameHistoryUserRange,
                               GameHistoryActionResult,
                               GameHistoryRangeAction]]
        child_items = sorted(concatenate(items),
                             key=lambda c: c.order)
        for item in child_items:
            self.process_child_item(item)

        self.finalise_results()
예제 #2
0
def _impossible_deal(fixed):
    """
    fixed is a list of hands
    return true if it contains a duplicate
    """
    cards_ = concatenate(fixed)  # a hand is two cards, so concatenate hands
    return len(cards_) != len(set(cards_))
예제 #3
0
def generate_excluded_cards(game, hero=None):
    """
    calculate excluded cards, as if players had been dealt cards
    """
    map_to_range = {rgp: rgp.range for rgp in game.rgps
                    if rgp is not hero}
    # We consider the future board. By doing this here, we make action
    # probabilities consistent with calculated range sizes.
    # Implications:
    #  - we're using an actual board for everything, except pre-river all in
    #  - i.e. call ratio will be determined by future board cards, even though
    #    future board cards are ignored for equity calculations
    #  - it's extremely counter-intuitive, but I think it's the right result
    # More broadly:
    #  - (good) we'll never end up in an impossible situation
    #    (e.g. Ax vs. Ax on board AAxAx)
    #  - (bad) (already possible?) sometimes one range just won't be played
    #    out (actually, good and correct?, for both competition and
    #    optimization mode?)
    #  - (bad) when you get all in pre-river, future cards will be excluded
    #    from the perspective of the probability of the action, but included
    #    from the perspective of the showdown ranges and equities (but this
    #    is still the most preferable option)
    board = game.total_board or game.board
    player_to_dealt = deal_from_ranges(map_to_range, board)
    # note: this catches cards dealt to folded RGPs - as it should!
    excluded_cards = concatenate(player_to_dealt.values())
    excluded_cards.extend(board)
    return excluded_cards
예제 #4
0
 def analyse(self):
     """
     Perform all analysis on game that has not been done.
     
     If you need to reanalyse the game, delete the existing analysis first.
     """
     gameid = self.game.gameid
     logging.debug("gameid %d, AnalysisReplayer, analyse", gameid)
     items = [
         self.session.query(table).filter(table.gameid == gameid).all()
         for table in [
             GameHistoryBoard, GameHistoryUserRange,
             GameHistoryActionResult, GameHistoryRangeAction
         ]
     ]
     child_items = sorted(concatenate(items), key=lambda c: c.order)
     self.ranges = {
         self.game.rgps[i].userid: self.game.situation.players[i].range_raw
         for i in range(len(self.game.situation.players))
     }
     self.stacks = {
         self.game.rgps[i].userid: self.game.situation.players[i].stack
         for i in range(len(self.game.situation.players))
     }
     self.contrib = {
         self.game.rgps[i].userid:
         self.game.situation.players[i].contributed
         for i in range(len(self.game.situation.players))
     }
     self.remaining_userids = [rgp.userid for rgp in self.game.rgps]
     for item in child_items:
         self.process_child_item(item)
예제 #5
0
def showdown(board, players_cards, memo=None):
    """
    board is a list of five board cards

    players_cards is a dict of {player: cards}
    order of this list is the order in which players will show down

    returns a list of tuples: (player, hand shown down), and a list of winners

    will return a hand for every player, with None representing a fold (because
    the player did not show down a hand)
    """
    shown_down = []  # list of: (player, hand or None)
    best = None  # best hand shown down so far
    total_cards = board + concatenate(
        [c for _p, c in players_cards.iteritems()])
    if len(set(total_cards)) != len(total_cards):
        assert False
    for player, cards_ in players_cards.iteritems():
        all_cards = frozenset(board + list(cards_))
        if memo is None:
            hand = BestHandEval7(all_cards)
        elif memo.has_key(all_cards):
            hand = memo[all_cards]
        else:
            hand = BestHandEval7(all_cards)
            memo[all_cards] = hand
        # They fold if it's worse than best, otherwise they show
        if best is None or hand >= best:
            best = hand
            shown_down.append((player, hand))
        else:
            shown_down.append((player, None))
    winners = [player for player, hand in shown_down if hand == best]
    return shown_down, winners
예제 #6
0
def range_sum_equal(fold_range, passive_range, aggressive_range,
                    original_range):
    """
    Returns validity (boolean), and reason (string, or None if valid)
    """
    # pylint:disable=R0914
    all_ranges = [fold_range, passive_range, aggressive_range]
    original_hand_options = original_range.generate_options()
    all_ranges_hand_options = concatenate([r.generate_options()
                                           for r in all_ranges])
    original_hand_options.sort(cmp=_cmp_weighted_options)
    all_ranges_hand_options.sort(cmp=_cmp_weighted_options)
    prev = (None, -1)
    # pylint:disable=W0141
    for ori, new in map(None, original_hand_options, all_ranges_hand_options):
        # we compare two hands, each of which is a set of two Card
        if ori != new:
            # actually three scenarios:
            # hand is in multiple action ranges
            # (new is the same as the previous new) 
            # hand is in action ranges but not original range
            # (new < ori means new is not in original range)
            # hand is in original range but not action ranges
            # (new > ori means ori is not in action ranges)
            if new is None:
                message =  \
                    "hand in original range but not in action ranges: %s" %  \
                    _option_to_text(ori[0])
            elif ori is None:
                message =  \
                    "hand in action ranges but not in original range: %s" %  \
                    _option_to_text(new[0])
            else:
                newh, neww = new
                orih, oriw = ori
                if newh == prev[0]:
                    message = "hand in multiple ranges: %s" %  \
                        _option_to_text(newh)
                elif _cmp_options(newh, orih) > 0:
                    message = "hand in original range but not in " +  \
                        "action ranges: %s" % _option_to_text(orih)
                elif _cmp_options(newh, orih) < 0:
                    message = "hand in action ranges but not in " +  \
                        "original range: %s" % _option_to_text(newh)
                elif neww != oriw:
                    message = "weight changed from %d to %d for hand %s" %  \
                        (oriw, neww, _option_to_text(orih))
                else:
                    raise RuntimeError("hands not equal, but can't " +  \
                                       "figure out why: %s, %s" % \
                                       (_option_to_text(orih),
                                        _option_to_text(newh)))
            return False, message
        prev = new
    return True, None
예제 #7
0
def range_sum_equal(fold_range, passive_range, aggressive_range,
                    original_range):
    """
    Returns validity (boolean), and reason (string, or None if valid)
    """
    # pylint:disable=R0914
    all_ranges = [fold_range, passive_range, aggressive_range]
    original_hand_options = original_range.generate_options()
    all_ranges_hand_options = concatenate(
        [r.generate_options() for r in all_ranges])
    original_hand_options.sort(cmp=_cmp_weighted_options)
    all_ranges_hand_options.sort(cmp=_cmp_weighted_options)
    prev = (None, -1)
    # pylint:disable=W0141
    for ori, new in map(None, original_hand_options, all_ranges_hand_options):
        # we compare two hands, each of which is a set of two Card
        if ori != new:
            # actually three scenarios:
            # hand is in multiple action ranges
            # (new is the same as the previous new)
            # hand is in action ranges but not original range
            # (new < ori means new is not in original range)
            # hand is in original range but not action ranges
            # (new > ori means ori is not in action ranges)
            if new is None:
                message =  \
                    "hand in original range but not in action ranges: %s" %  \
                    _option_to_text(ori[0])
            elif ori is None:
                message =  \
                    "hand in action ranges but not in original range: %s" %  \
                    _option_to_text(new[0])
            else:
                newh, neww = new
                orih, oriw = ori
                if newh == prev[0]:
                    message = "hand in multiple ranges: %s" %  \
                        _option_to_text(newh)
                elif _cmp_options(newh, orih) > 0:
                    message = "hand in original range but not in " +  \
                        "action ranges: %s" % _option_to_text(orih)
                elif _cmp_options(newh, orih) < 0:
                    message = "hand in action ranges but not in " +  \
                        "original range: %s" % _option_to_text(newh)
                elif neww != oriw:
                    message = "weight changed from %d to %d for hand %s" %  \
                        (oriw, neww, _option_to_text(orih))
                else:
                    raise RuntimeError("hands not equal, but can't " +  \
                                       "figure out why: %s, %s" % \
                                       (_option_to_text(orih),
                                        _option_to_text(newh)))
            return False, message
        prev = new
    return True, None
예제 #8
0
 def _deal_to_board(self, game):
     """
     Deal as many cards as are needed to bring the board up to the current round
     """
     total = TOTAL_COMMUNITY_CARDS[game.current_round]
     current = len(game.board)
     excluded_cards = concatenate(rgp.cards_dealt for rgp in game.rgps)
     excluded_cards.extend(game.board)
     new_board = game.board + deal_cards(excluded_cards, total - current)
     game.board = new_board
     if total > current:
         self._record_board(game)
예제 #9
0
 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))
예제 #10
0
 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))
예제 #11
0
 def _deal_to_board(self, game):
     """
     Deal as many cards as are needed to bring the board up to the current
     round. Also remove these cards from players' ranges.
     """
     total = TOTAL_COMMUNITY_CARDS[game.current_round]
     current = len(game.board)
     excluded_cards = concatenate(rgp.cards_dealt for rgp in game.rgps)
     excluded_cards.extend(game.board)
     new_board = game.board + deal_cards(excluded_cards, total - current)
     game.board = new_board
     if total > current:
         self._record_board(game)
     for rgp in game.rgps:
         rgp.range = remove_board_from_range(rgp.range, game.board)
예제 #12
0
 def _deal_to_board(self, game):
     """
     Deal as many cards as are needed to bring the board up to the current
     round. Also remove these cards from players' ranges.
     """
     total = TOTAL_COMMUNITY_CARDS[game.current_round]
     current = len(game.board)
     excluded_cards = concatenate(rgp.cards_dealt for rgp in game.rgps)
     excluded_cards.extend(game.board)
     new_board = game.board + deal_cards(excluded_cards, total - current)
     game.board = new_board
     if total > current:
         self._record_board(game)
     for rgp in game.rgps:
         rgp.range = remove_board_from_range(rgp.range, game.board)
예제 #13
0
 def _get_history_items(self, game, userid=None):
     """
     Returns a list of game history items (tables.GameHistoryBase with
     additional details from child tables), with private data only for
     <userid>, if specified.
     """
     # pylint:disable=W0142
     child_items = [self.session.query(table)
                    .filter(table.gameid == game.gameid).all()
                    for table in MAP_TABLE_DTO.keys()]
     all_child_items = sorted(concatenate(child_items),
                              key=lambda c: c.order)
     child_dtos = [dtos.GameItem.from_game_history_child(child)
                   for child in all_child_items]
     return [dto for dto in child_dtos
             if game.is_finished or dto.should_include_for(userid)]
예제 #14
0
 def _get_history_items(self, game, userid=None):
     """
     Returns a list of game history items (tables.GameHistoryBase with
     additional details from child tables), with private data only for
     <userid>, if specified.
     """
     # pylint:disable=W0142
     child_items = [
         self.session.query(table).filter(
             table.gameid == game.gameid).all()
         for table in MAP_TABLE_DTO.keys()
     ]
     all_child_items = sorted(concatenate(child_items),
                              key=lambda c: c.order)
     child_dtos = [
         dtos.GameItem.from_game_history_child(child)
         for child in all_child_items
     ]
     return [
         dto for dto in child_dtos
         if game.is_finished or dto.should_include_for(userid)
     ]
예제 #15
0
def _simulate_showdown(wins_by_player, options_by_player, board, memo):
    """
    Run one simulated showdown based on ranges.
    """
    selected = {}
    while True:
        for player, options in options_by_player.iteritems():
            selected[player] = random.choice(options)
        fixed = {player: hand for player, hand in selected.iteritems()}
        if not _impossible_deal(fixed.values() + [board]):
            break
    excluded = concatenate(fixed.values())
    fixed_board = []
    for i in range(5):
        if i < len(board):
            card = board[i]
            excluded.append(card)
        else:
            card = cards.deal_card(excluded)  # extends excluded
        fixed_board.append(card)
    _shown_down, winners = showdown(fixed_board, fixed, memo)
    for player in selected.keys():
        if player in winners:
            wins_by_player[player] += 1.0 / len(winners)
예제 #16
0
def re_deal(range_action, cards_dealt, dealt_key, board, can_fold, can_call):
    """
    This function is for when we're not letting them fold (sometimes not even
    call).
    
    We reassign their hand so that it's not a fold (or call). If possible.
    Changes cards_dealt[dealt_key] to a new hand from one of the other ranges
    (unless they're folding 100%, of course).
    
    Returns fold ratio, passive ratio, aggressive ratio.
    
    In the case of can_fold and can_call, we calculate these ratios but don't
    re_deal.
    """
    # pylint:disable=R0913,R0914
    # Choose between passive and aggressive probabilistically
    # But do this by re-dealing, to account for card removal effects (other
    # hands, and the board)
    # Note that in a range vs. range situation, it's VERY HARD to determine the
    # probability that a player will raise cf. calling. But very luckily, we
    # can work around that, by using the fact that all other players are
    # currently dealt cards with a probability appropriate to their ranges, so
    # when we use their current cards as dead cards, we achieve the
    # probabilities we need here, without knowing exactly what those
    # probabilities are. (But no, I haven't proven this mathematically.)
    dead_cards = [card for card in board if card is not None]
    dead_cards.extend(concatenate([v for k, v in cards_dealt.iteritems()
                                   if k is not dealt_key]))
    fold_options = range_action.fold_range.generate_options(dead_cards)
    passive_options = range_action.passive_range.generate_options(dead_cards)
    aggressive_options =  \
        range_action.aggressive_range.generate_options(dead_cards)
    terminate = False
    if not can_fold:
        if can_call:
            allowed_options = passive_options + aggressive_options
        else:
            # On the river, heads-up, if we called it would end the hand
            # (like a fold elsewhere)
            allowed_options = aggressive_options
            if not allowed_options:
                # The hand must end. Now.
                # They have no bet/raise range, so they cannot raise.
                # They cannot call, because the cost and benefit of calling is
                # already handled as a partial showdown payment
                # They cannot necessarily fold, because they might not have a
                # fold range.
                # Example: on the river, heads-up, they call their whole range.
                # In fact, that's more than an example, it happens all the time.
                # In this case, we don't have a showdown, or even complete an
                # action for this range_action. In fact, whether we record a
                # call action or not is perhaps irrelevant.
                terminate = True
        # Edge case: It's just possible that there will be no options here. This
        # can happen when the player's current hand is in their fold range, and
        # due to cards dealt to the board and to other players, they can't have
        # any of the hands in their call or raise ranges.
        # Example: The board has AhAd, and an opponent has AsAc, and all hand in
        # the player's passive and aggressive ranges contain an Ace, and the
        # only options that don't contain an Ace are in the fold range.
        # Resolution: In this case, we let them fold. They will find this
        # astonishing, but... I think we have to.
        if allowed_options:
            description = weighted_options_to_description(allowed_options)
            allowed_range = HandRange(description)
            cards_dealt[dealt_key] = allowed_range.generate_hand()
    # Note that we can't use RelativeSizes for this. Because that doesn't
    # account for card removal effects. I.e. for the opponent's range. But this
    # way indirectly does. Yes, sometimes this strangely means that Hero
    # surprisingly folds 100%. Yes, sometimes this strangely means that Hero
    # folds 0%. And that's okay, because those things really do happen some
    # times. (Although neither might player might know Hero is doing it!)
    fol = len(fold_options)
    pas = len(passive_options)
    agg = len(aggressive_options)
    total = fol + pas + agg
    return (terminate,
        float(fol) / total,
        float(pas) / total,
        float(agg) / total)
예제 #17
0
    def calculate_combo_ev(self, combo, userid):
        """
        Calculate EV for a combo at this point in the game tree.
        """
        if self.children:
            # intermediate node, EV is combination of children's
            # oh, each child needs a weight / probability
            # oh, it's combo specific... "it depends" ;)

            # EV of combo is:
            # weighted sum of EV of children, but only for children where the
            # combo is present (where combo isn't present, probability is zero)

            # to assess probability:
            # - if this node's children are performed by this user:
            #   - EV is the EV of the action that contains this combo
            # - otherwise:
            #   - remove combo from the children's actor's current range
            #   - consider how many combo's of children's actor's current range
            #     proceed to each child
            #   - voila
            # This conveniently works even in a multi-way pot. It's a very good
            # approximation of the true probabilities. And note that truly
            # calculating the true probabilities is not possible.
            actor = self.children[0].actor
            if actor == userid:
                # EV is the EV of the action that contains this combo
                for node in self.children:
                    if range_contains_hand(node.ranges_by_userid[userid],
                                           combo):
                        return node.combo_ev(combo, userid)
                raise InvalidComboForTree('Combo not in child ranges for userid'
                    ' %d at betting line %s.' %
                    (userid, line_description(self.betting_line)))
            else:
                # probabilistic weighting of child EVs
                # size of bucket is probability of this child
                valid_children = [child for child in self.children
                    if range_contains_hand(child.ranges_by_userid[userid],
                                           combo)]
                buckets = {child: child.ranges_by_userid[actor]  \
                    .generate_options(Card.many_from_text(child.board) +
                                      list(combo))
                    for child in valid_children}
                total = len(concatenate(buckets.values()))
                if total == 0:
                    raise InvalidComboForTree('Combo not in child ranges for'
                        ' userid %d at betting line %s. ' % (userid,
                        line_description(self.betting_line)))
                probabilities = {child: 1.0 * len(buckets[child]) / total
                                 for child in valid_children}
                ev = sum(probabilities[child] * child.combo_ev(combo, userid)
                    for child in valid_children)
                return ev
                # Invalid combos are ignored / not calculated or aggregated.
        elif userid not in self.winners:
            # they folded
            return 0.0 - self.total_contrib[userid]
        elif len(self.winners) == 1:
            # uncontested pot
            return 0.0 + self.final_pot - self.total_contrib[userid]
        else:
            # showdown
            ranges = {userid: range_
                      for userid, range_ in self.ranges_by_userid.items()}
            combos = set([combo])
            description = unweighted_options_to_description(combos)
            ranges[userid] = HandRange(description)
            equities, _iteration =  \
                showdown_equity(ranges, Card.many_from_text(self.board),
                    hard_limit=10000)  # TODO: 1: this is not good enough
            equity = equities[userid]
            return equity * self.final_pot - self.total_contrib[userid]