def board_payment(self, board_item, board_before, board_after): """ Payments in compensation of change of equity due to board cards. """ range_map = {key: HandRange(txt) for key, txt in self.ranges.iteritems() if key in self.remaining_userids} equity_map_before, _ = showdown_equity(range_map, board_before) equity_map_after, _ = showdown_equity(range_map, board_after) for userid in self.remaining_userids: # payment equal to what was lost amount = self.pot * (equity_map_before[userid] - equity_map_after[userid]) * board_item.factor logging.debug('gameid %d, order %d, userid %d, board payment: ' 'pot %d * (before %0.4f - after %0.4f) * factor %0.4f' ' = amount %0.8f', board_item.gameid, board_item.order, userid, self.pot, equity_map_before[userid], equity_map_after[userid], board_item.factor, amount) payment = PaymentToPlayer() payment.reason = PaymentToPlayer.REASON_BOARD payment.gameid = board_item.gameid payment.order = board_item.order payment.userid = userid payment.amount = amount self.session.add(payment)
def analyse_showdown(self, ranges, order, is_passive, userids): """ Create a showdown with given userids. Pre-river if pre-river. """ showdowns = self.session.query(GameHistoryShowdown) \ .filter(GameHistoryShowdown.gameid == self.game.gameid) \ .filter(GameHistoryShowdown.order == order) \ .filter(GameHistoryShowdown.is_passive == is_passive).all() assert len(showdowns) == 1 logging.debug("gameid %d, order %d, confirmed existing showdown", self.game.gameid, order) showdown = showdowns[0] # TODO: REVISIT: this ignores ranges of folded players # It might make a difference in situations where a player has (for # example) limited their range to Ax and later folded, hence surely # removing an ace from the deck for the other players (significantly # changing their equities) # Actually, where it makes a difference, it would be really neat to see # it. Imagine someone saying "hey, this stupid site says I made a bad # call here with 23% equity when really I had 32% equity and it was a # great call!" Well no actually, the card removal effects of the folded # players change your equity, and you suck at poker. range_map = {k: v for k, v in ranges.iteritems() if k in userids} equity_map, iterations = showdown_equity(range_map, self.game.board) logging.debug('gameid %d, order %d, is_passive %r, factor %0.8f, ' 'showdown with userids: %r, equity: %r ' '(iterations %d)', self.game.gameid, order, is_passive, showdown.factor, userids, equity_map, iterations) existing_equities = {p.showdown_order: p for p in showdown.participants} #pylint:disable=no-member for showdown_order, userid in enumerate(userids): # create if not exist, otherwise update if showdown_order in existing_equities: participant = existing_equities[showdown_order] else: # TODO: REVISIT: this is ordered by situation player order, # not showdown order participant = GameHistoryShowdownEquity() self.session.add(participant) existing_equities[showdown_order] = participant participant.gameid = self.game.gameid participant.order = order participant.is_passive = is_passive participant.showdown_order = showdown_order participant.userid = userid participant.equity = equity_map[userid] self.showdown_payments(showdown=showdown, equities=existing_equities.values())
def equity_payments(self, item): """ Payments in compensation due to one player's range changing. Note that every remaining player gets a payment. This happens whenever a player has multiple "play continues" ranges. When heads up, this means any time before the river when someone has a passive range and an aggressive range. More generally, when three-handed, it can include folds. In fact, it could probably be generalised to include fold equity! (But note that fold equity is much less contentious!) (The issue with fold equity is that it requires us to scale down the rest of play, because we don't allow the fold line to happen, ever.) item is a GameHistoryRangeAction """ # Establish "old ranges", meaning after anything that can't happen # doesn't happen, but before it has been decided which of the things # that can happen will happen. userid = item.userid old_ranges = {k: HandRange(v) for k, v in self.ranges.iteritems() if k in self.remaining_userids} allowed = {'f', 'p', 'a'} if not self.fold_continues: allowed.discard('f') old_ranges[userid] = old_ranges[userid] \ .subtract(HandRange(self.prev_range_action.fold_range)) if not self.passive_continues: allowed.discard('p') old_ranges[userid] = old_ranges[userid] \ .subtract(HandRange(self.prev_range_action.passive_range)) if self.prev_range_action.fold_ratio == 0.0: allowed.discard('f') if self.prev_range_action.passive_ratio == 0.0: allowed.discard('p') if self.prev_range_action.aggressive_ratio == 0.0: allowed.discard('a') if len(allowed) <= 1: # one path, or zero paths (end of the game) logging.debug("gameid %d, order %d, no equity payments, " "allowed: %s", item.gameid, item.order, ','.join(allowed)) return # Establish "new ranges", what will actually be. new_ranges = dict(old_ranges) if item.is_fold: new_ranges[userid] = HandRange(self.prev_range_action.fold_range) if item.is_passive: new_ranges[userid] = HandRange(self.prev_range_action.passive_range) if item.is_aggressive: new_ranges[userid] = \ HandRange(self.prev_range_action.aggressive_range) # Calculate equities old_equities, _ = showdown_equity(old_ranges, self.board) new_equities, _ = showdown_equity(new_ranges, self.board) for userid in self.remaining_userids: # payment equal to what was lost # note the use of item.factor (new), not prev_range_action.factor amount = self.pot * (old_equities[userid] - new_equities[userid]) * item.factor logging.debug('gameid %d, order %d, userid %d, equity payment: ' 'pot %d * (before %0.4f - after %0.4f) * factor %0.4f' ' = amount %0.8f', item.gameid, item.order, userid, self.pot, old_equities[userid], new_equities[userid], item.factor, amount) payment = PaymentToPlayer() payment.reason = PaymentToPlayer.REASON_BRANCH payment.gameid = item.gameid payment.order = item.order payment.userid = userid payment.amount = amount self.session.add(payment)
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]