def test_create_afei_two_folders_raise(self): """ Test _create_afei for a raise against two players""" # raise from 10 to 30 on an original pot of 10 # unprofitable bluff fea = FoldEquityAccumulator(gameid=0, order=0, street=RIVER, board=[], bettor=0, range_action=None, raise_total=30, pot_before_bet=20, bet_cost=30, pot_if_called=70, potential_folders=[]) fold_range = HandRange("KK") nonfold_range = HandRange("AA") fea.folds.append((1, fold_range, nonfold_range)) # folds 2/3 fold_range = HandRange("QQ") nonfold_range = HandRange("KK+") fea.folds.append((1, fold_range, nonfold_range)) # folds 1/4 afei = fea._create_afei(combo=Card.many_from_text("AsQh"), is_agg=True) self.assertAlmostEqual(afei.fold_ratio, 1.0 / 6.0) self.assertAlmostEqual(afei.immediate_result, 1.0 / 6.0 * 20.0 + (5.0 / 6.0) * (-30)) # -21.66... self.assertAlmostEqual(afei.semibluff_ev, None) self.assertAlmostEqual(afei.semibluff_equity, None)
def test_create_afei_two_fodlers_reraise(self): """ Test _create_afei for a reraise against two players""" # raise from 30 to 50 on an original pot of 10 # profitable bluff fea = FoldEquityAccumulator( gameid=0, order=0, street=RIVER, board=[], bettor=0, range_action=None, raise_total=50, pot_before_bet=50, bet_cost=50, pot_if_called=120, # assumes called by the raiser, not the bettor potential_folders=[]) fold_range = HandRange("KK") nonfold_range = HandRange("AA") fea.folds.append((1, fold_range, nonfold_range)) # folds 2/3 fold_range = HandRange("KK-JJ") nonfold_range = HandRange("AA") fea.folds.append((1, fold_range, nonfold_range)) # folds 5/6 afei = fea._create_afei(combo=Card.many_from_text("AsQh"), is_agg=True) self.assertAlmostEqual(afei.fold_ratio, 10.0 / 18.0) self.assertAlmostEqual(afei.immediate_result, 10.0 / 18.0 * 50.0 + 8.0 / 18.0 * (-50.0)) # 5.55... self.assertAlmostEqual(afei.semibluff_ev, None) self.assertAlmostEqual(afei.semibluff_equity, None)
def __init__(self, fold_range=None, passive_range=None, aggressive_range=None, raise_total=None, fold_raw=None, passive_raw=None, aggressive_raw=None): """ fold_range is the part of their range they fold here passive_range is the part of their range they check or call here aggressive_range is the part of their range the bet or raise here """ if (fold_range is not None and fold_raw is not None) or \ (passive_range is not None and passive_raw is not None) or \ (aggressive_range is not None and aggressive_raw is not None): raise ValueError("Specified range and raw") if (fold_range is None and fold_raw is None) or \ (passive_range is None and passive_raw is None) or \ (aggressive_range is None and aggressive_raw is None): raise ValueError("Specified neither range or raw") if raise_total is None: raise ValueError("No raise total") self.fold_range = fold_range \ if isinstance(fold_range, HandRange) \ else HandRange(fold_raw) self.passive_range = passive_range \ if isinstance(passive_range, HandRange) \ else HandRange(passive_raw) self.aggressive_range = aggressive_range \ if isinstance(aggressive_range, HandRange) \ else HandRange(aggressive_raw) self.raise_total = raise_total
def test_create_afei_two_folders_bet(self): """ Test _create_afei for a bet against two players""" # bet 10 on a pot of 10 # profitable bluff fea = FoldEquityAccumulator(gameid=0, order=0, street=RIVER, board=[], bettor=0, range_action=None, raise_total=10, pot_before_bet=10, bet_cost=10, pot_if_called=30, potential_folders=[]) fold_range = HandRange("KK") nonfold_range = HandRange("AA") fea.folds.append((1, fold_range, nonfold_range)) fold_range = HandRange("KK") nonfold_range = HandRange("AA") fea.folds.append((1, fold_range, nonfold_range)) afei = fea._create_afei(combo=Card.many_from_text("AsQh"), is_agg=True) self.assertAlmostEqual(afei.fold_ratio, 4.0 / 9.0) self.assertAlmostEqual(afei.immediate_result, 4.0 / 9.0 * 10.0 + (5.0 / 9.0) * (-10)) self.assertEqual(afei.semibluff_ev, None) self.assertEqual(afei.semibluff_equity, None)
def re_range(self, branch): """ Assign new range to rgp, and redeal their hand """ self.rgp.range_raw = weighted_options_to_description(branch.options) rgp_range = HandRange(self.rgp.range_raw) self.rgp.cards_dealt = rgp_range.generate_hand(self.game.board) logging.debug("new range for userid %d, gameid %d, new range %r, " + "new cards_dealt %r", self.rgp.userid, self.rgp.gameid, self.rgp.range_raw, self.rgp.cards_dealt)
def safe_hand_range_form(field_name, fallback): """ Pull a HandRange object from request form field <field_name>. If there is a problem, return HandRange(fallback). """ value = request.form.get(field_name, fallback, type=str) hand_range = HandRange(value, is_strict=False) if not hand_range.is_valid(): hand_range = HandRange(fallback) return hand_range
def safe_hand_range(arg_name, fallback): """ Pull a HandRange object from request arg <arg_name>. If there is a problem, return HandRange(fallback). """ value = request.args.get(arg_name, fallback, type=str) hand_range = HandRange(value, is_strict=False) if not hand_range.is_valid(): hand_range = HandRange(fallback) return hand_range
def folder(self, ghra): """ ghra is a GameHistoryRangeAction, who we consider to be a folder. Returns True if this fea is complete. """ logging.debug("gameid %d, FEA %d, adding folder: userid %d", self.gameid, self.order, ghra.userid) self.potential_folders.remove(ghra.userid) fold_range = HandRange(ghra.fold_range) pas = HandRange(ghra.passive_range) agg = HandRange(ghra.aggressive_range) nonfold_range = pas.add(agg, self.board) self.folds.append((ghra.userid, fold_range, nonfold_range)) return len(self.potential_folders) == 0
def _action_summary_to_vars(range_action, action_result, action_result_index, user_range, index): """ Summarise an action result and user range in the context of the most recent range action. """ new_total = len(HandRange(user_range.range_raw). \ generate_options_unweighted()) fol = pas = agg = NOTHING if action_result.action_result.is_fold: original = fol = range_action.range_action.fold_range.description elif action_result.action_result.is_passive: original = pas = range_action.range_action.passive_range.description else: original = agg = range_action.range_action.aggressive_range.description # NOTE: some of this is necessarily common with RANGE_ACTION return {"screenname": user_range.user.screenname, "action_result": action_result.action_result, "action_result_index": action_result_index, "percent": 100.0 * new_total / len(SET_ANYTHING_OPTIONS), "combos": new_total, "is_check": range_action.is_check, "is_raise": range_action.is_raise, "original": original, "fold": fol, "passive": pas, "aggressive": agg, "index": index}
def rank_class(row, col, color_maker, board): """ Give the appropriate class for this rank combo """ txt = rank_text(row, col) options = HandRange(txt).generate_options_unweighted(board) return color_maker.get_color(options)
def finalise(self, session): """ Assuming complete, return an AnalysisFoldEquity """ logging.debug("gameid %d, FEA %d, calculating...", self.gameid, self.order) assert len(self.potential_folders) == 0 afe = self._create_afe() session.add(afe) for combo in HandRange(self.range_action.aggressive_range) \ .generate_options_unweighted(self.board): afei = self._create_afei(combo, is_agg=True) session.add(afei) for combo in HandRange(self.range_action.passive_range) \ .generate_options_unweighted(self.board): afei = self._create_afei(combo, is_pas=True) session.add(afei) for combo in HandRange(self.range_action.fold_range) \ .generate_options_unweighted(self.board): afei = self._create_afei(combo, is_fol=True) session.add(afei) logging.debug("gameid %d, FEA %d, finalised", self.gameid, self.order) return afe
def test_create_afei_one_folder_raise(self): """ Test _create_afei for a raise against one player""" # raise from 10 to 30 on an original pot of 10 # profitable bluff fea = FoldEquityAccumulator(gameid=0, order=0, street=PREFLOP, board=[], bettor=0, range_action=None, raise_total=30, pot_before_bet=20, bet_cost=30, pot_if_called=70, potential_folders=[]) fold_range = HandRange("KK-JJ") nonfold_range = HandRange("AA") fea.folds.append((1, fold_range, nonfold_range)) afei = fea._create_afei(combo=Card.many_from_text("KsQh"), is_agg=True) self.assertAlmostEqual(afei.fold_ratio, 2.0 / 3.0) self.assertAlmostEqual(afei.immediate_result, 2.0 / 3.0 * 20.0 + (1.0 / 3.0) * (-30.0)) # 3.33... self.assertAlmostEqual(afei.semibluff_ev, -10.0) self.assertAlmostEqual(afei.semibluff_equity, -10.0 / 70.0)
def test_create_afei_one_folder_reraise(self): """ Test _create_afei for a reraise against one player""" # raise from 30 to 50 on an original pot of 10 # unprofitable bluff fea = FoldEquityAccumulator(gameid=0, order=0, street=PREFLOP, board=[], bettor=0, range_action=None, raise_total=50, pot_before_bet=50, bet_cost=40, pot_if_called=110, potential_folders=[]) fold_range = HandRange("QQ") nonfold_range = HandRange("AA-KK") fea.folds.append((1, fold_range, nonfold_range)) afei = fea._create_afei(combo=Card.many_from_text("KsQh"), is_agg=True) self.assertAlmostEqual(afei.fold_ratio, 1.0 / 4.0) self.assertAlmostEqual(afei.immediate_result, 1.0 / 4.0 * 50.0 + (3.0 / 4.0) * (-40.0)) # -17.5 self.assertAlmostEqual(afei.semibluff_ev, 4.0 / 3.0 * 17.5) self.assertAlmostEqual(afei.semibluff_equity, 4.0 / 3.0 * 17.5 / 110.0)
def rank_hover(row, col, color_maker, board, is_raised, is_can_check): """ Hover text for this rank combo. Something like "calling As8s, Ah8h; folding Ad8d". """ txt = rank_text(row, col) options = HandRange(txt).generate_options_unweighted(board) inputs = [("unassigned", color_maker.opt_una), ("folding", color_maker.opt_fol), ("checking" if is_can_check else "calling", color_maker.opt_pas), ("raising" if is_raised else "betting", color_maker.opt_agg)] return " -- ".join([ rank_hover_part(item[0], item[1].intersection(options)) for item in inputs if item[1].intersection(options) ])
def get_selected_options(original, board): """ Get a list of options selected in the current range editor submission """ options = set() for row in range(13): for col in range(13): desc = rank_text(row, col) field = "sel_" + desc is_sel = request.form.get(field, "false") == "true" if is_sel: new = set(HandRange(desc).generate_options_unweighted(board)) new.intersection(original) options.update( [option for option in new if is_suit_selected(option)]) return options
def test_range_contains_hand(self): """ Test range_contains_hand """ from rvr.poker import cards range_ = HandRange("AA(5),KK") hands_in = [[ Card(cards.ACE, cards.SPADES), Card(cards.ACE, cards.HEARTS) ], [Card(cards.KING, cards.CLUBS), Card(cards.KING, cards.DIAMONDS)]] hands_out = [[ Card(cards.ACE, cards.SPADES), Card(cards.KING, cards.HEARTS) ], [Card(cards.DEUCE, cards.CLUBS), Card(cards.DEUCE, cards.DIAMONDS)]] for hand_in in hands_in: self.assertTrue(range_contains_hand(range_, hand_in)) for hand_out in hands_out: self.assertFalse(range_contains_hand(range_, hand_out))
def get_template_args(args): updated_args = {} updated_args['images'] = card_names(board_raw) board = safe_board_form('board') opt_ori = rng_original.generate_options_unweighted(board) opt_fol = rng_fold.generate_options_unweighted(board) opt_pas = rng_passive.generate_options_unweighted(board) opt_agg = rng_aggressive.generate_options_unweighted(board) opt_una = list(set(opt_ori) - set(opt_fol) - set(opt_pas) - set(opt_agg)) updated_args['rng_unassigned'] = HandRange( unweighted_options_to_description(opt_una)) color_maker = ColorMaker(opt_ori=opt_ori, opt_una=opt_una, opt_fol=opt_fol, opt_pas=opt_pas, opt_agg=opt_agg) if len(opt_ori) != 0: pct_unassigned = 100.0 * len(opt_una) / len(opt_ori) pct_fold = 100.0 * len(opt_fol) / len(opt_ori) pct_passive = 100.0 * len(opt_pas) / len(opt_ori) pct_aggressive = 100.0 * len(opt_agg) / len(opt_ori) else: pct_unassigned = pct_fold = pct_passive = pct_aggressive = 0.0 updated_args['rank_table'] = make_rank_table(color_maker, board, can_check=can_check, is_raised=args['raised']) updated_args['suited_table'] = make_suited_table() updated_args['pair_table'] = make_pair_table() updated_args['offsuit_table'] = make_offsuit_table() updated_args['hidden_fields'] = [ ("raised", args['raised']), ("can_check", args['can_check']), ("can_raise", args['can_raise']), ("min_raise", args['min_raise']), ("max_raise", args['max_raise']), ("board", args['board_raw']), ("rng_original", args['rng_original'].description), ("rng_unassigned", args['rng_unassigned'].description), ("rng_fold", args['rng_fold'].description), ("rng_passive", args['rng_passive'].description), ("rng_aggressive", args['rng_aggressive'].description) ]
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)
def _range_desc_to_size(range_description): """ Given a range description, determine the number of combos it represents. """ return len(HandRange(range_description).generate_options())
def range_editor_get(): """ An HTML range editor! """ embedded = request.args.get('embedded', 'false') raised = request.args.get('raised', '') can_check = request.args.get('can_check', '') can_raise = request.args.get('can_raise', 'true') min_raise = request.args.get('min_raise', '0') max_raise = request.args.get('max_raise', '200') rng_original = safe_hand_range('rng_original', ANYTHING) rng_fold = safe_hand_range('rng_fold', NOTHING) rng_passive = safe_hand_range('rng_passive', NOTHING) rng_aggressive = safe_hand_range('rng_aggressive', NOTHING) l_una = request.args.get('l_una', '') == 'checked' l_fol = request.args.get('l_fol', 'checked') == 'checked' l_pas = request.args.get('l_pas', 'checked') == 'checked' l_agg = request.args.get('l_agg', 'checked') == 'checked' board_raw = request.args.get('board', '') images = card_names(board_raw) board = safe_board_form('board') opt_ori = rng_original.generate_options_unweighted(board) opt_fol = rng_fold.generate_options_unweighted(board) opt_pas = rng_passive.generate_options_unweighted(board) opt_agg = rng_aggressive.generate_options_unweighted(board) opt_una = list(set(opt_ori) - set(opt_fol) - set(opt_pas) - set(opt_agg)) rng_unassigned = HandRange(unweighted_options_to_description(opt_una)) color_maker = ColorMaker(opt_ori=opt_ori, opt_una=opt_una, opt_fol=opt_fol, opt_pas=opt_pas, opt_agg=opt_agg) if len(opt_ori) != 0: pct_unassigned = 100.0 * len(opt_una) / len(opt_ori) pct_fold = 100.0 * len(opt_fol) / len(opt_ori) pct_passive = 100.0 * len(opt_pas) / len(opt_ori) pct_aggressive = 100.0 * len(opt_agg) / len(opt_ori) else: pct_unassigned = pct_fold = pct_passive = pct_aggressive = 0.0 rank_table = make_rank_table(color_maker, board, can_check=can_check, is_raised=raised) suited_table = make_suited_table() pair_table = make_pair_table() offsuit_table = make_offsuit_table() hidden_fields = [("raised", raised), ("can_check", can_check), ("can_raise", can_raise), ("min_raise", min_raise), ("max_raise", max_raise), ("board", board_raw), ("rng_original", rng_original.description), ("rng_unassigned", rng_unassigned.description), ("rng_fold", rng_fold.description), ("rng_passive", rng_passive.description), ("rng_aggressive", rng_aggressive.description)] if embedded == 'true': template = 'web/range_viewer.html' else: template = 'web/range_editor.html' return render_template(template, title="Range Editor", next_map=NEXT_MAP, hidden_fields=hidden_fields, rank_table=rank_table, suited_table=suited_table, pair_table=pair_table, offsuit_table=offsuit_table, card_names=images, rng_unassigned=rng_unassigned.description, rng_fold=rng_fold.description, rng_passive=rng_passive.description, rng_aggressive=rng_aggressive.description, l_una=l_una, l_fol=l_fol, l_pas=l_pas, l_agg=l_agg, pct_unassigned=pct_unassigned, pct_fold=pct_fold, pct_passive=pct_passive, pct_aggressive=pct_aggressive, raised=raised, can_check=can_check, can_raise=can_raise, min_raise=min_raise, max_raise=max_raise)
def test_range_action_fits(self): """ Test range_action_fits """ # pylint:disable=R0915 # will test that: # - hand in original but not action should fail # - hand not in original but in action should fail # - hand in two or more ranges should fail # - raise size within band should succeed # - raise size outside band should fail # - should work the same with weights as without range_original = HandRange("AA(5),22,72o") range_aa = HandRange("AA(5)") range_kk = HandRange("KK") range_22 = HandRange("22") range_72o = HandRange("72o") range_22_72o = HandRange("22,72o") range_aa_22 = HandRange("AA(5),22") range_empty = HandRange("nothing") range_22_weighted = HandRange("22(3)") #options = [FoldOption(), CheckOption(), RaiseOption(2, 194)] options = ActionOptions(0, False, 2, 194) # invalid, raise size too small range_action = ActionDetails(range_72o, range_22, range_aa, 1) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual(rsn, "raise total must be between 2 and 194") # valid, minraise range_action = ActionDetails(range_72o, range_22, range_aa, 2) val, rsn = range_action_fits(range_action, options, range_original) self.assertTrue(val) # valid, never folding when we can check range_action = ActionDetails(range_empty, range_22_72o, range_aa, 2) val, rsn = range_action_fits(range_action, options, range_original) self.assertTrue(val) # valid, max raise range_action = ActionDetails(range_72o, range_22, range_aa, 194) val, rsn = range_action_fits(range_action, options, range_original) self.assertTrue(val) # invalid, raise size too big range_action = ActionDetails(range_72o, range_22, range_aa, 195) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual(rsn, "raise total must be between 2 and 194") # invalid, AA in original but not action range_action = ActionDetails(range_72o, range_22, range_empty, 2) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual( rsn, "hand in original range but not in action ranges: AdAc") # invalid, KK in action but not original range_action = ActionDetails(range_72o, range_aa_22, range_kk, 2) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual( rsn, "hand in action ranges but not in original range: KdKc") # invalid, AA in multiple ranges range_action = ActionDetails(range_72o, range_aa_22, range_aa, 2) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual(rsn, "hand in multiple ranges: AdAc") #options = [FoldOption(), CallOption(10), RaiseOption(20, 194)] options = ActionOptions(10, True, 20, 194) # invalid, re-weighted range_action = ActionDetails(range_72o, range_22_weighted, range_aa, 20) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual(rsn, "weight changed from 1 to 3 for hand 2d2c") # valid, empty raise range (still has a raise size, which is okay) range_action = ActionDetails(range_aa, range_22_72o, range_empty, 20) val, rsn = range_action_fits(range_action, options, range_original) self.assertTrue(val) # invalid, raise too big range_action = ActionDetails(range_72o, range_22, range_aa, 195) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual(rsn, "raise total must be between 20 and 194") #options = [FoldOption(), CallOption(194)] options = ActionOptions(194) # valid, 0 raise size is okay if empty raise range range_action = ActionDetails(range_22_72o, range_aa, range_empty, 0) val, rsn = range_action_fits(range_action, options, range_original) self.assertTrue(val) # valid, 200 raise size is okay if empty raise range range_action = ActionDetails(range_22_72o, range_aa, range_empty, 200) val, rsn = range_action_fits(range_action, options, range_original) self.assertTrue(val) # valid, has raise size but raise range is empty range_action = ActionDetails(range_original, range_empty, range_empty, 20) val, rsn = range_action_fits(range_action, options, range_original) self.assertTrue(val) # invalid, has raise range range_action = ActionDetails(range_72o, range_22, range_aa, 20) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual( rsn, "there was a raising range, but raising was not an option") range_action = ActionDetails(range_72o, range_22, range_aa, 0) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual( rsn, "there was a raising range, but raising was not an option") # invalid, doesn't equal original range_action = ActionDetails(range_empty, range_aa, range_empty, 0) val, rsn = range_action_fits(range_action, options, range_original) self.assertFalse(val) self.assertEqual( rsn, "hand in original range but not in action ranges: 2d2c")
def get_range(self): """ Get range, as HandRange instance """ return HandRange(self.range_raw)