def test_compare(self): """ Test BestHandEval7 compare """ hand1 = BestHandEval7(Card.many_from_text("AsTdTs5sAc5cQd")) hand2 = BestHandEval7(Card.many_from_text("AsTdTs5sAc5cJd")) self.assertTrue(hand1 > hand2) # Q kicker beats J kicker
def do_test_re_deal(self, action, dealt, key, board, result, re_dealt, iterations=1, delta=0.0): """ Worker function for testing re_deal """ # pylint:disable=R0913,R0914 action2 = ActionDetails(HandRange(action[0]), HandRange(action[1]), HandRange(action[2]), 2) board2 = Card.many_from_text(board) re_dealt2 = [(HandRange(text).generate_options(board2), ratio) for text, ratio in re_dealt] counts = [0 for _ in re_dealt2] for _ndx in xrange(iterations): dealt2 = {k: Card.many_from_text(val) for k, val in dealt.items()} _te, fpct, _pp, _ap = re_deal(action2, dealt2, key, board2, False, True) continue_ratio = 1.0 - fpct self.assertEqual(continue_ratio, result, msg="Expected continue ratio %0.3f but got %0.3f" % (result, continue_ratio)) for ndx, (options, ratio) in enumerate(re_dealt2): if (frozenset(dealt2[key]), 1) in options: counts[ndx] += 1 break else: self.fail("dealt[key]=%r not in expected re_dealt2 ranges: %r" % (dealt2[key], re_dealt2)) for ndx, val in enumerate(counts): expected = re_dealt[ndx][1] * iterations self.assertAlmostEqual(val, expected, delta=delta, msg=("Expected count %0.0f but got count %0.0f" + \ " for re_dealt item %s") % (expected, val, re_dealt[ndx][0]))
def test_cmp_options(self): """ Test _cmp_options """ cqh = Card.from_text("Qh") ckh = Card.from_text("Kh") cah = Card.from_text("Ah") cks = Card.from_text("Ks") ckc = Card.from_text("Kc") kh_qh = set([ckh, cqh]) ah_kh = set([ckh, cah]) ks_kh = set([ckh, cks]) kh_kc = set([ckh, ckc]) self.assertEqual(-1, _cmp_options(kh_qh, ah_kh)) self.assertEqual(-1, _cmp_options(kh_qh, ks_kh)) self.assertEqual(-1, _cmp_options(kh_qh, kh_kc)) self.assertEqual(1, _cmp_options(ah_kh, kh_qh)) self.assertEqual(1, _cmp_options(ah_kh, ks_kh)) self.assertEqual(1, _cmp_options(ah_kh, kh_kc)) self.assertEqual(1, _cmp_options(ks_kh, kh_qh)) self.assertEqual(-1, _cmp_options(ks_kh, ah_kh)) self.assertEqual(1, _cmp_options(ks_kh, kh_kc)) self.assertEqual(1, _cmp_options(kh_kc, kh_qh)) self.assertEqual(-1, _cmp_options(kh_kc, ah_kh)) self.assertEqual(-1, _cmp_options(kh_kc, ks_kh))
def from_afei(cls, afei): """ Create from AnalysisFoldEquityItem """ cards = [Card.from_text(afei.higher_card), Card.from_text(afei.lower_card)] return cls(cards, afei.is_aggressive, afei.is_passive, afei.is_fold, afei.fold_ratio, afei.immediate_result, afei.semibluff_ev, afei.semibluff_equity)
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 generate_options(self, board=None): """ option is a list of (hand, weight) """ # TODO: 3: remove the concept of weighted options from HandRange # It might still be worth having a WeightedHandRange, specifically for # situations, but then convert to (unweighted) HandRange when the game # starts. excluded_cards = board or [] excluded_mnemonics = [card.to_mnemonic() for card in excluded_cards] # it's really nice for this to be a list, for self.polarise_weights options = [] for part, weight in self.subranges: option_mnemonics = hands_in_subrange(part) # list of e.g. "AhKh" not_excluded = lambda hand: (hand[ 0:2] not in excluded_mnemonics and hand[2:4] not in excluded_mnemonics) option_mnemonics = [o for o in option_mnemonics if not_excluded(o)] hands = [ frozenset(Card.many_from_text(txt)) for txt in option_mnemonics ] options.extend([(hand, weight) for hand in hands]) if self.is_strict: return options else: return list(set(options))
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 test_description(self): """ Test BestHandEval7 to string """ cards = Card.many_from_text("AsTdTs5sAc5cQd") hand = BestHandEval7(cards) self.assertEqual(str(hand), "two pair, Aces and Tens")
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 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_hand_vs_range_monte_carlo(self): hand = Card.many_from_text("AsAd") villain = HandRange("AA,A3o,32s") board = [] equity = py_hand_vs_range_monte_carlo( hand, villain, board, 10000000) self.assertAlmostEqual(equity, 0.85337, delta=0.002)
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 generate_options_unweighted(self, board=None): """ just hand, no weight error if weights are not all the same """ excluded_cards = board or [] excluded_mnemonics = [card.to_mnemonic() for card in excluded_cards] # it's really nice for this to be a list, for self.polarise_weights options = [] first_weight = None for part, weight in self.subranges: if first_weight == None: first_weight = weight elif weight != first_weight: raise ValueError("range is not evenly weighted") option_mnemonics = hands_in_subrange(part) # list of e.g. "AhKh" not_excluded = lambda hand: (hand[0:2] not in excluded_mnemonics and hand[2:4] not in excluded_mnemonics) option_mnemonics = [o for o in option_mnemonics if not_excluded(o)] hands = [frozenset(Card.many_from_text(txt)) for txt in option_mnemonics] options.extend(hands) if self.is_strict: return options else: return list(set(options))
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 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 inject_range_sizes(players, board_raw): """ Inject a range size into each situation player """ for player in players: player.range_size = len(HandRange(player.range_raw).generate_options( Card.many_from_text(board_raw)))
def generate_options_unweighted(self, board=None): """ just hand, no weight error if weights are not all the same """ excluded_cards = board or [] excluded_mnemonics = [card.to_mnemonic() for card in excluded_cards] # it's really nice for this to be a list, for self.polarise_weights options = [] first_weight = None for part, weight in self.subranges: if first_weight == None: first_weight = weight elif weight != first_weight: raise ValueError("range is not evenly weighted") option_mnemonics = hands_in_subrange(part) # list of e.g. "AhKh" not_excluded = lambda hand: (hand[ 0:2] not in excluded_mnemonics and hand[2:4] not in excluded_mnemonics) option_mnemonics = [o for o in option_mnemonics if not_excluded(o)] hands = [ frozenset(Card.many_from_text(txt)) for txt in option_mnemonics ] options.extend(hands) if self.is_strict: return options else: return list(set(options))
def test_all_hands_vs_range(self): hero = HandRange("AsAd,3h2c") villain = HandRange("AA,A3o,32s") board = [] equity_map = py_all_hands_vs_range(hero, villain, board, 10000000) self.assertEqual(len(equity_map), 2) hand1 = frozenset(Card.many_from_text("AsAd")) hand2 = frozenset(Card.many_from_text("3h2c")) self.assertAlmostEqual(equity_map[hand1], 0.85337, delta=0.002) self.assertAlmostEqual(equity_map[hand2], 0.22865, delta=0.002) # Hero has an impossible hand in his range. hero = HandRange("JsJc,QsJs") villain = HandRange("JJ") board = Card.many_from_text("KhJd8c") equity_map = py_all_hands_vs_range(hero, villain, board, 10000000) hand = frozenset(Card.many_from_text("QsJs")) self.assertAlmostEqual(equity_map[hand], 0.03687, delta=0.0002) self.assertEqual(len(equity_map), 1)
def safe_board_form(field_name): """ Pull a board (list of Card) from request form field <field_name>. If there is a problem, return an empty list. """ value = request.form.get(field_name, '', type=str) try: return Card.many_from_text(value) except ValueError: return []
def safe_board(arg_name): """ Pull a board (list of Card) from request arg <arg_name>. If there is a problem, return an empty list. """ value = request.args.get(arg_name, '', type=str) try: return Card.many_from_text(value) except ValueError: return []
def _test_calculate_equity(self, ranges_txt, board_txt, expect, expect_iters): from rvr.poker.handrange import HandRange from rvr.poker.cards import Card ranges = {"Player %d" % i: HandRange(ranges_txt[i]) for i in range(len(ranges_txt))} board = Card.many_from_text(board_txt) expected_results = {"Player %d" % i: expect[i] for i in range(len(expect))} results, iterations = showdown_equity(ranges, board, 20000) self.assertEqual(iterations, expect_iters) self.assert_equity_almost_equal(results, expected_results)
def process_board(self, item): """ Process a GameHistoryBoard """ self.contrib = {u:0 for u in self.remaining_userids} self.left_to_act = self.remaining_userids[:] assert self.fea is None self.street = item.street board_before = self.board self.board = Card.many_from_text(item.cards) board_after = self.board self.board_payment(item, board_before, board_after)
def __init__(self, gameid, order, street, board, bettor, range_action, raise_total, pot_before_bet, bet_cost, pot_if_called, potential_folders): logging.debug("gameid %d, FEA %d, initialising", gameid, order) self.gameid = gameid self.order = order self.street = street self.board = Card.many_from_text(board) self.bettor = bettor self.range_action = range_action self.raise_total = raise_total self.pot_before_bet = pot_before_bet self.bet_cost = bet_cost self.pot_if_called = pot_if_called self.potential_folders = potential_folders # userids self.folds = [] # list of (userid, fold_ratio)
def _add_card_combos(self): """ Populate table of RangeItem """ deck1 = [ Card(rank, suit) for rank in RANKS_HIGH_TO_LOW for suit in SUITS_HIGH_TO_LOW ] deck2 = deck1[:] err = None for card1 in deck1: for card2 in deck2: if card1 > card2: err = err or self._add_card_combo(higher_card=card1, lower_card=card2) return err
def all_combos_ev(self, userid, local=False): """ Return a mapping of combo to EV at this point in the game tree, for each combo in the user's range at this point. Recall that a combo is a frozenset of two cards. """ user_range = self.ranges_by_userid[userid] combos = user_range.generate_options(Card.many_from_text(self.board)) results = {} for combo in combos: try: results[combo] = self.combo_ev(combo, userid, local) except InvalidComboForTree as ex: pass # TODO: 0.0: happening way too often! return results
def _test_estimate_showdown_equity(self, ranges_txt, board_txt, expect, delta, iterations): from rvr.poker.handrange import HandRange from rvr.poker.cards import Card ranges = {"Player %d" % i: HandRange(ranges_txt[i]) for i in range(len(ranges_txt))} board = Card.many_from_text(board_txt) expected_results = {"Player %d" % i: expect[i] for i in range(len(expect))} delta_by_player = {"Player %d" % i: delta[i] for i in range(len(expect))} options_by_player = {p: r.generate_options(board) for p, r in ranges.iteritems()} wins_by_player = _estimate_showdown_equity(options_by_player, board, iterations) print "\nwins_by_player: %r\n" % wins_by_player self.assert_wins_almost_equal(wins_by_player, expected_results, delta_by_player)
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 generate_options(self, board=None): """ option is a list of hand """ excluded_cards = board or [] excluded_mnemonics = [card.to_mnemonic() for card in excluded_cards] options = [] for part in self.subranges: option_mnemonics = hands_in_subrange(part) # list of e.g. "AhKh" not_excluded = lambda hand: (hand[0:2] not in excluded_mnemonics and hand[2:4] not in excluded_mnemonics) option_mnemonics = [o for o in option_mnemonics if not_excluded(o)] hands = [frozenset(Card.many_from_text(txt)) for txt in option_mnemonics] options.extend(hands) if self.is_strict: return options else: return list(set(options))
def _test_estimate_showdown_equity2(self, ranges_txt, board_txt, expect, sds, iterations): from rvr.poker.handrange import HandRange from rvr.poker.cards import Card ranges = {"Player %d" % i: HandRange(ranges_txt[i]) for i in range(len(ranges_txt))} board = Card.many_from_text(board_txt) sdev = [stddev_one(prob) for prob in expect] # e.g. p=0.05, sdev=0.2179 expected_results = {"Player %d" % i: expect[i] * iterations for i in range(len(expect))} delta_by_player = {"Player %d" % i: sds * sdev[i] * (iterations ** 0.5) for i in range(len(expect))} options_by_player = {p: r.generate_options(board) for p, r in ranges.iteritems()} wins_by_player = _estimate_showdown_equity(options_by_player, board, iterations) print "\nwins_by_player: %r\n" % wins_by_player self.assert_wins_almost_equal(wins_by_player, expected_results, delta_by_player)
def generate_options(self, board=None): """ option is a list of (hand, weight) """ excluded_cards = board or [] excluded_mnemonics = [card.to_mnemonic() for card in excluded_cards] # it's really nice for this to be a list, for self.polarise_weights options = [] for part, weight in self.subranges: option_mnemonics = hands_in_subrange(part) # list of e.g. "AhKh" not_excluded = lambda hand: (hand[0:2] not in excluded_mnemonics and hand[2:4] not in excluded_mnemonics) option_mnemonics = [o for o in option_mnemonics if not_excluded(o)] hands = [frozenset(Card.many_from_text(txt)) for txt in option_mnemonics] options.extend([(hand, weight) for hand in hands]) if self.is_strict: return options else: return list(set(options))
def test_hand_vs_range_exact(self): hand = Card.many_from_text("AcAh") villain = HandRange("AA") board = Card.many_from_text("KhJd8c5d2s") equity = py_hand_vs_range_exact(hand, villain, board) self.assertEqual(equity, 0.5) hand = Card.many_from_text("AcAh") villain = HandRange("AsAd") board = Card.many_from_text("KhJd8c5d2s") equity = py_hand_vs_range_exact(hand, villain, board) self.assertEqual(equity, 0.5) hand = Card.many_from_text("AsAd") villain = HandRange("AA,A3o,32s") board = Card.many_from_text("KhJd8c5d2s") equity = py_hand_vs_range_exact(hand, villain, board) self.assertAlmostEqual(equity, 0.95, places=7)
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 generate_options(self, board=None): """ option is a list of (hand, weight) """ # TODO: 3: remove the concept of weighted options from HandRange # It might still be worth having a WeightedHandRange, specifically for # situations, but then convert to (unweighted) HandRange when the game # starts. excluded_cards = board or [] excluded_mnemonics = [card.to_mnemonic() for card in excluded_cards] # it's really nice for this to be a list, for self.polarise_weights options = [] for part, weight in self.subranges: option_mnemonics = hands_in_subrange(part) # list of e.g. "AhKh" not_excluded = lambda hand: (hand[0:2] not in excluded_mnemonics and hand[2:4] not in excluded_mnemonics) option_mnemonics = [o for o in option_mnemonics if not_excluded(o)] hands = [frozenset(Card.many_from_text(txt)) for txt in option_mnemonics] options.extend([(hand, weight) for hand in hands]) if self.is_strict: return options else: return list(set(options))
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 get_cards_dealt(self): """ Get cards dealt, as list of two Card """ return Card.many_from_text(self.cards_dealt_raw)
def get_board(self): """ Get board, as list of Card """ return Card.many_from_text(self.board_raw)
def test_range_action_to_action(self): """ Test range_action_to_action """ # pylint:disable=R0914,R0915 # will test: # - act fold, # - act check, # - act call, # - act raise, # - act case where hand is in no range (although invalid) # won't test: # - act case where hand is in all ranges (because the result is # undefined) options_with_check = ActionOptions(0, False, 2, 196) options_with_call = ActionOptions(10, True, 20, 196) options_without_raise = ActionOptions(196) range_aa = HandRange("AA") range_72o = HandRange("72o") range_22_72o = HandRange("22,72o") range_empty = HandRange("nothing") range_22_weighted = HandRange("22(3)") hand_72o = Card.many_from_text("7h2c") hand_22 = Card.many_from_text("2h2c") hand_aa = Card.many_from_text("AhAc") hand_72s = Card.many_from_text("7h2h") action_with_raise = ActionDetails(range_72o, range_22_weighted, range_aa, 40) rng, act = range_action_to_action(action_with_raise, hand_72o, options_with_check) self.assert_(act.is_fold) self.assertEqual("72o", rng.description) rng, act = range_action_to_action(action_with_raise, hand_22, options_with_check) self.assert_(act.is_passive) self.assertEqual(act.call_cost, 0) rng, act = range_action_to_action(action_with_raise, hand_aa, options_with_check) self.assert_(act.is_aggressive) self.assertEqual(act.raise_total, 40) try: act = range_action_to_action(action_with_raise, hand_72s, options_with_check) self.assertTrue(False) except ValueError: pass rng, act = range_action_to_action(action_with_raise, hand_72o, options_with_call) self.assert_(act.is_fold) rng, act = range_action_to_action(action_with_raise, hand_22, options_with_call) self.assert_(act.is_passive) self.assertEqual(act.call_cost, 10) rng, act = range_action_to_action(action_with_raise, hand_aa, options_with_call) self.assert_(act.is_aggressive) self.assertEqual(act.raise_total, 40) try: act = range_action_to_action(action_with_raise, hand_72s, options_with_call) self.assertTrue(False) except ValueError: pass action_without_raise = ActionDetails(range_22_72o, range_aa, range_empty, 0) rng, act = range_action_to_action(action_without_raise, hand_72o, options_with_check) self.assert_(act.is_fold) rng, act = range_action_to_action(action_without_raise, hand_22, options_with_check) self.assert_(act.is_fold) rng, act = range_action_to_action(action_without_raise, hand_aa, options_with_check) self.assert_(act.is_passive) self.assertEqual(act.call_cost, 0) try: act = range_action_to_action(action_without_raise, hand_72s, options_with_check) self.assertTrue(False) except ValueError: pass rng, act = range_action_to_action(action_without_raise, hand_72o, options_with_call) self.assert_(act.is_fold) rng, act = range_action_to_action(action_without_raise, hand_22, options_with_call) self.assert_(act.is_fold) rng, act = range_action_to_action(action_without_raise, hand_aa, options_with_call) self.assert_(act.is_passive) self.assertEqual(act.call_cost, 10) try: act = range_action_to_action(action_without_raise, hand_72s, options_with_call) self.assertTrue(False) except ValueError: pass rng, act = range_action_to_action(action_without_raise, hand_72o, options_without_raise) self.assert_(act.is_fold) rng, act = range_action_to_action(action_without_raise, hand_22, options_without_raise) self.assert_(act.is_fold) rng, act = range_action_to_action(action_without_raise, hand_aa, options_without_raise) self.assert_(act.is_passive) self.assertEqual(act.call_cost, 196) try: rng, act = range_action_to_action(action_without_raise, hand_72s, options_without_raise) self.assertTrue(False) except ValueError: pass
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]
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