class InterpreterProxy(object): def __init__(self): if use_z3: self.interpreter = Interpreter() else: self.interpreter = BidInterpreter() def _pretty_string_for_position_view(self, position_view): # kbb HandConstraints just so we can use it's pretty_print function. position_knowledge = _position_knowledge_from_position_view(position_view) kbb_oneline = position_knowledge.pretty_one_line(include_last_call_name=False) return kbb_oneline + " " + ", ".join(map(str, position_view.annotations_for_last_call)) def knowledge_string_and_rule_for_last_call(self, call_history): knowledge_string = None rule = None if use_z3: with self.interpreter.create_history(call_history) as history: return self._pretty_string_for_position_view(history.rho), history.rho.rule_for_last_call else: existing_knowledge, knowledge_builder = self.interpreter.knowledge_from_history(call_history) matched_rules = knowledge_builder.matched_rules() knowledge_string = existing_knowledge.rho.pretty_one_line(include_last_call_name=False) if existing_knowledge else None, return knowledge_string, matched_rules[-1] def knowledge_from_history(self, call_history, loose_constraints=None): if use_z3: return (None, None) return self.interpreter.knowledge_from_history(call_history, loose_constraints)
def get(self, calls_string=None): calls_string = calls_string or "" calls_string = urllib.unquote(calls_string) calls_string = calls_string.upper() history = self._history_from_calls_string(calls_string) # This automatically cleans up our urls for us, which is nice when copy/pasting from unit tests: if calls_string and history.comma_separated_calls() != calls_string: self._redirect_to_history(history) return interpreter = BidInterpreter() possible_calls = [] history_for_next_to_bid = history.copy_with_history_until_after_last_call_from_position(history.position_to_call()) existing_knowledge, knowledge_builder = interpreter.knowledge_from_history(history) for future in CallExplorer().possible_futures(history): knowledge, consuming_rule = interpreter.knowledge_including_new_bid(knowledge_builder, future.last_call(), loose_constraints=True) possible_calls.append([future.last_call(), consuming_rule, knowledge]) template_values = { 'call_history': history, 'possible_calls': possible_calls, 'knowledge_positions': existing_knowledge.absolute_positions(history.position_to_call()), } self.response.out.write(jinja_environment.get_template('bid_explorer.html').render(template_values))
def get(self): interpreter = BidInterpreter() calls_string = self.request.get('calls_string') or '' dealer_char = self.request.get('dealer') or '' vulnerability_string = self.request.get('vulnerability') or '' history = CallHistory.from_string(calls_string, dealer_char, vulnerability_string) existing_knowledge, knowledge_builder = interpreter.knowledge_from_history(history) matched_rules = knowledge_builder.matched_rules() explore_dict = { 'knowledge_string': existing_knowledge.rho.pretty_one_line(include_last_call_name=False) if existing_knowledge else None, } explore_dict.update(self._json_from_rule(matched_rules[-1], history.calls[-1])) self.response.headers["Content-Type"] = "application/json" self.response.headers["Cache-Control"] = "public" expires_date = datetime.datetime.utcnow() + datetime.timedelta(days=1) expires_str = expires_date.strftime("%d %b %Y %H:%M:%S GMT") self.response.headers.add_header("Expires", expires_str) self.response.out.write(json.dumps(explore_dict))
def setUp(self): self.interpreter = BidInterpreter()
class BidInterpreterTest(unittest2.TestCase): def setUp(self): self.interpreter = BidInterpreter() def _rule_for_last_call(self, call_history_string): history = CallHistory.from_string(call_history_string) knowledge, knowledge_builder = self.interpreter.knowledge_from_history(history) matched_rules = knowledge_builder.matched_rules() return matched_rules[-1] def _hand_knowledge_from_last_call(self, call_history_string): history = CallHistory.from_string(call_history_string) knowledge, _ = self.interpreter.knowledge_from_history(history) return knowledge.rho def test_not_crash(self): # We used to hit an assert when considering SecondNegative for 3C (it shouldn't match, but was asserting). self._rule_for_last_call("2C,P,2D,P,2H,P,2S,P,2N,P,3C") # FIXME: It's possible these various asserts should be combined # so that we can do multiple tests only running the interpreter once over a history. def _assert_point_range(self, call_history_string, expected_point_range): history = CallHistory.from_string(call_history_string) knowledge, matched_rules = self.interpreter.knowledge_from_history(history) # We use rho() instead of me() because knowledge_from_history auto-rotates the Knowledge. self.assertEqual(knowledge.rho.hcp_range_tuple(), expected_point_range) def _assert_rule_name(self, call_history_string, expected_rule_name): last_rule = self._rule_for_last_call(call_history_string) self.assertEqual(last_rule.name, expected_rule_name) def test_one_level_opening(self): self._assert_point_range("1C", (12, 21)) self._assert_rule_name("1C", "MinorOpen") def test_lead_directing_double(self): self._assert_rule_name("P,1N,P,2C,X", "LeadDirectingDouble") self._assert_rule_name("P,1N,P,2C,P,2D,X", "LeadDirectingDouble") self._assert_rule_name("2C X", "LeadDirectingDouble") def test_negative_double(self): # From p133: hand_knowledge = self._hand_knowledge_from_last_call("1C 1D X") self.assertEquals(hand_knowledge.min_length(HEARTS), 4) self.assertEquals(hand_knowledge.min_length(SPADES), 4) hand_knowledge = self._hand_knowledge_from_last_call("1D 1H X") self.assertEquals(hand_knowledge.min_length(SPADES), 4) self.assertEquals(hand_knowledge.max_length(SPADES), 4) hand_knowledge = self._hand_knowledge_from_last_call("1D 1S X") self.assertEquals(hand_knowledge.min_length(HEARTS), 4) def test_michaels_minor_request(self): self._assert_rule_name("1H 2H P 2N", "MichaelsMinorRequest") self._assert_rule_name("1S 2S P 2N", "MichaelsMinorRequest") self._assert_rule_name("2H 3H P 4C", "MichaelsMinorRequest") self._assert_rule_name("2S 3S P 4C", "MichaelsMinorRequest") # FIXME: We don't currently support 4-level Michaels # self._assert_rule_name("3H 4H P 4N", "MichaelsMinorRequest") # self._assert_rule_name("3S 4S P 4N", "MichaelsMinorRequest") self._assert_rule_name("2H 3H 4H 4N", "UnforcedMichaelsMinorRequest") self._assert_rule_name("2H 3H 4H 4N", "UnforcedMichaelsMinorRequest") def _assert_is_stayman(self, history_string, should_be_stayman): knowledge = self._hand_knowledge_from_last_call(history_string) self.assertEqual(knowledge.last_call.stayman, should_be_stayman) def test_stayman(self): self._assert_is_stayman("1N P 2C", True) self._assert_is_stayman("1N P 3C", False) self._assert_is_stayman("1N 2C X", True) self._assert_is_stayman("2N P 3C", True) self._assert_is_stayman("3N P 4C", True) self._assert_is_stayman("4N P 5C", False) self._assert_is_stayman("1D P 1H P 2N P 3C", False) self._assert_is_stayman("2C P 2D P 2N P 3C", True) self._assert_is_stayman("2C P 2D P 3N P 4C", True) self._assert_is_stayman("2C P 2D P 4N P 5C", False) # FIXME: These 2C -> 5N sequences should be changed to use 3N # once we introduce 3N as meaning "minimum", since currently # the bidder asserts trying to interpret 3N since the partnership # clearly has 30+ points and can make 5N. No sense in wasting # all that bidding space to show a minimum 22hcp however. self._assert_is_stayman("2C P 2H P 5N P 6C", False) self._assert_is_stayman("2C P 2S P 5N P 6C", False) self._assert_is_stayman("2C P 2N P 5N P 6C", False) # FIXME: It seems this should be stayman showing 4 hearts and 4 points? # self._assert_is_stayman("2C 2S P P 2N P 3C", True) # FIXME: It seems this should be stayman showing 4 hearts and ?? points? # self._assert_is_stayman("2C 2S P P 3N P 4C", True) def _assert_is_jacoby_transfer(self, history_string, should_be_transfer): knowledge = self._hand_knowledge_from_last_call(history_string) self.assertEqual(knowledge.last_call.jacoby_transfer, should_be_transfer) def test_jacoby_transfers(self): self._assert_is_jacoby_transfer("1N P 2D", True) self._assert_is_jacoby_transfer("1N P 2H", True) self._assert_is_jacoby_transfer("1N P 2S", False) # Special transfer, not jacoby self._assert_is_jacoby_transfer("1N X 2D", True) self._assert_is_jacoby_transfer("1N 2C 2D", True) self._assert_is_jacoby_transfer("1N X 2H", True) self._assert_is_jacoby_transfer("1N 2C 2H", True) self._assert_is_jacoby_transfer("2N X 3D", True) self._assert_is_jacoby_transfer("2N 3C 3D", True) self._assert_is_jacoby_transfer("2N X 3H", True) self._assert_is_jacoby_transfer("2N 3C 3H", True) # Although we might like to play these as a transfer, that's not currently SAYC: self._assert_is_jacoby_transfer("1N 2D X", False) self._assert_is_jacoby_transfer("1N 2D 2H", False) def _assert_is_gerber(self, history_string, should_be_gerber): last_rule = self._rule_for_last_call(history_string) if should_be_gerber: self.assertEqual(last_rule.name, "Gerber") else: self.assertNotEqual(last_rule.name, "Gerber") def test_gerber(self): self._assert_is_gerber("1N P 4C", True) self._assert_is_gerber("1D P 1S P 1N P 4C", True) # FIXME: Should this really be gerber? Currently we treat it as such. self._assert_is_gerber("1N 3S 4C", True) self._assert_is_gerber("2C P 2N P 4C", True) def test_4N_over_jacoby_2N(self): # 4N is not a valid bid over Jacoby2N. self.assertEqual(self._rule_for_last_call("1H P 2N P 4N"), None) def test_is_fourth_suit_forcing(self): # Raising FSF never makes any sense, and is not a FSF bid. self.assertEqual(self._rule_for_last_call("1D,P,1S,P,2C,P,2H,P,3C,P,3H"), None) def _assert_is_takeout(self, history_string, should_be_takeout): knowledge = self._hand_knowledge_from_last_call(history_string) self.assertEqual(knowledge.last_call.takeout_double, should_be_takeout) def test_is_takeout(self): self._assert_is_takeout("1H X", True) self._assert_is_takeout("1H P 2H X", True) self._assert_is_takeout("1H P 1S X", True) self._assert_is_takeout("1H 1S X", False) def test_help_suit_game_try(self): # We believe 2S here is called a HelpSuitGameTry even though it's very similar to a reverse. self._assert_rule_name("1H P 2H P 2S", "HelpSuitGameTry") @unittest2.expectedFailure def test_4H_does_not_assert(self): self._assert_rule_name("1C 2N P 3H P 4H", "MajorGame")
def __init__(self, history, hand): self.interpreter = BidInterpreter() self.existing_knowledge, self.knowledge_builder = self.interpreter.knowledge_from_history(history) self.hand = hand assert self.existing_knowledge, "Oracle can't understant existing history: %s" % history
class ConsistencyOracle(object): def __init__(self, history, hand): self.interpreter = BidInterpreter() self.existing_knowledge, self.knowledge_builder = self.interpreter.knowledge_from_history(history) self.hand = hand assert self.existing_knowledge, "Oracle can't understant existing history: %s" % history def _is_rule_of_twenty(self, hand): two_longest_suits = sorted(hand.suit_lengths())[-2:] return sum(two_longest_suits) + hand.high_card_points() >= 20 def _is_rule_of_fifteen(self, hand): return hand.length_of_suit(SPADES) + hand.high_card_points() >= 15 def _is_garbage_stayman(self, hand): if hand.high_card_points() > 7: return False # We can have at fewest 4 diamonds or 3 of either major. # That gaurentees at least a (rare!) 6 card diamond fit, or 7 card major fit. for suit in MAJORS: # If we have a 5 card major we should escape transfer to it instead. if hand.length_of_suit(suit) not in (3, 4): return False if hand.length_of_suit(DIAMONDS) < 4: return False return True def _is_minor_game_force_stayman(self, hand): if hand.high_card_points() < 13: # 15 + 13 = 28 return False if hand.length_of_suit(CLUBS) >= 6 or hand.length_of_suit(DIAMONDS) >= 6: return True # FIXME: Should promise 3o5 or 2o3 in the Minor? return False def _should_bid_stayman(self, knowledge, hand): # FIXME: GarbageStayman and MinorGameForceStayman are only valid over 1N openings. if knowledge.partner.last_call.level() == 1: if self._is_garbage_stayman(hand): return True if self._is_minor_game_force_stayman(hand): return True # Otherwise Stayman promises a 4-card major and 8+ points (over 1N). major_lengths = tuple(hand.suit_lengths()[-2:]) if 4 not in major_lengths: return False return True # Promised points will be enforced normally in the point check. def _can_big_hand_double(self, hand): # p118 says not to plan a big-hand double with a two suited hand. long_suit_count = 0 for suit in SUITS: if hand.length_of_suit(suit) >= 5: long_suit_count += 1 if long_suit_count >= 2: return False return hand.high_card_points() >= 17 def _have_shape_for_two_spades_puppet(self, hand): return hand.length_of_suit(CLUBS) >= 6 or hand.length_of_suit(DIAMONDS) >= 6 def _have_unspecified_minor_for_michaels_cuebid_of_a_major(self, hand): for suit in MINORS: if hand.length_of_suit(suit) >= 5: return True return False def _trump_suit(self, knowledge): suit_fits = filter(lambda suit: (knowledge.me.min_length(suit) + knowledge.partner.min_length(suit)) >= 8, SUITS) if len(suit_fits) == 1: return suit_fits[0] # If the bid we're considering making happens to be one of the suits we have a fit with partner in # then we'll assume that's the suit we should use to count support points. if knowledge.me.last_call.strain in suit_fits: return knowledge.me.last_call.strain if len(suit_fits) > 1: # Would be nice to log here, but we hit this way too often for crazy bids over takeout doubles or NT openings. #_log.warn("Multiple suit fits %s, unsure what point system to use, using LengthPoints." % map(suit_name, suit_fits)) return None _log.warn("Unable to find suit fit, yet fit-requring point system used! Rule error?") return None def _points_for_hand(self, hand, point_system, knowledge): if point_system == point_systems.HighCardPoints: return hand.high_card_points() if point_system == point_systems.NotrumpSupport: # When bidding for a NT auction, we deduct one point for a flat hand. return hand.high_card_points() - 1 if hand.is_flat() else hand.high_card_points() if point_system == point_systems.SupportPointsIfFit: trump_suit = self._trump_suit(knowledge) if trump_suit is not None: our_trump_count = hand.length_of_suit(trump_suit) partners_trump_count = knowledge.partner.min_length(trump_suit) if our_trump_count < partners_trump_count: return hand.support_points(trump_suit) # It's somewhat unclear to me when we should count support points # when the trumps are split evenly. This appears to be required # for some of the examples in the book. This is a somewhat conservative # first approximation. if (trump_suit in MAJORS and our_trump_count == partners_trump_count and our_trump_count == 4 and knowledge.partner.median_hcp() > hand.high_card_points() and not knowledge.partner.notrump_protocol): return hand.support_points(trump_suit) # SupportPointsIfFit falls back to LengthPoints if there is no fit. return hand.length_points() # Make sure we never add a point system enum without adding actual support for it. assert point_system == point_systems.LengthPoints return hand.length_points() def _points_fit_knowledge(self, hand, point_system, knowledge): # Ignore point checks for garbage stayman. # FIXME: There must be a better way to do this! if knowledge.partner.last_call and knowledge.partner.last_call.name == '1N' and knowledge.me.last_call.name == '2C': if self._is_garbage_stayman(hand): return True points = self._points_for_hand(hand, point_system, knowledge) hand_knowledge = knowledge.me # The rule of twenty overrides normal point checks for an opening bid. if hand_knowledge.rule_of_twenty is not None: if hand_knowledge.rule_of_twenty != self._is_rule_of_twenty(hand): return False # We can't just return True here, or we'd be short-circuting all # point range checks ever made against this hand. To make sure # that the opening point range check passes, we'll just pretend # that any hand passing Ro20 has 12 playing points # (it does as length_points, but using length_points here would # cause 21 hcp hands to fail to open!). if hand_knowledge.rule_of_twenty: points = max(12, points) # Rule of Fifteen uses similar magic to Rule of Twenty, above. if hand_knowledge.rule_of_fifteen is not None: if hand_knowledge.rule_of_fifteen != self._is_rule_of_fifteen(hand): return False if hand_knowledge.rule_of_fifteen: points = max(10, points) if points not in hand_knowledge.hcp_range(): return False return True def _honors_match_suit(self, honors, hand, suit): if honors == HonorConstraint.TWO_OF_TOP_THREE and not hand.has_at_least(2, 'AKQ', suit): return False # FIXME: This could be expressed more concisely! Expecting 3o5 is always satisfied by having 2o3. if honors == HonorConstraint.THREE_OF_TOP_FIVE and not (hand.has_at_least(3, 'AKQJT', suit) or hand.has_at_least(2, 'AKQ', suit)): return False # We don't need to check "or 2o3 or 3o5" here since has_fourth_round_stopper recognizes Qxx as stopped. if honors == HonorConstraint.FOURTH_ROUND_STOPPER and not hand.has_fourth_round_stopper(suit): return False if honors == HonorConstraint.THIRD_ROUND_STOPPER and not hand.has_third_round_stopper(suit): return False return True def _matches_strong_four_card_suit_exception(self, hand, hand_knowledge, suit): length_in_suit = hand.length_of_suit(suit) # If our last bid was an overcalls, we're allowed to lie about # having five cards and bid a "good" four-card suit. The logic # rule CorrectOneLevelOvercallSuitLength will unwind this lie # after our partner has had a chance to respond. if not hand_knowledge.could_be_strong_four_card(suit): return False if length_in_suit < 4: return False assert length_in_suit == 4 if not hand.has_at_least(2, 'AKQ', suit): return False return True def _shape_fits_hand_knowledge(self, hand, hand_knowledge): for suit in SUITS: length_in_suit = hand.length_of_suit(suit) if length_in_suit not in hand_knowledge.length_range(suit): if not self._matches_strong_four_card_suit_exception(hand, hand_knowledge, suit): return False if not self._honors_match_suit(hand_knowledge.min_honors(suit), hand, suit): return False if hand_knowledge.longest_suit() == suit and not hand.is_longest_suit(suit, except_suits=hand_knowledge.longest_suit_exceptions()): return False if hand_knowledge.is_balanced(): # We only have to check that the hand doesn't have more than one doubleton, # as the lengths check above catches suits outside of the 2-5 card range. if hand.suit_lengths().count(2) > 1: return False if hand_knowledge.last_call.takeout_double: # We only have to check that the hand doesn't have more than one tripleton, # as the lengths check above catches suits outside of expected ranges. # p98, h1 (as well as a bunch of misc_hands_from_play) seem to want this check. # I think this is to avoid having partner choose the suit when you clearly must # have a 5card suit of your own to mention. If your suit is a major, you definitely # want to mention it to find the better fit. If it's a minor, then your 3-card majors # are great support only if he has a 5-card major to mention himself, right? # FIXME: This doesn't seem right above the one level. It's often that you can't overcall # at the 3 level. if hand.suit_lengths().count(3) > 1: return False return True def _fits_honor_count_constraints(self, hand, hand_knowledge): if hand_knowledge.ace_constraint() is not None: ace_constraint = hand_knowledge.ace_constraint() if ace_constraint == HandConstraints.ZERO_OR_FOUR: if hand.ace_count() != 0 and hand.ace_count() != 4: return False elif hand.ace_count() != ace_constraint: return False if hand_knowledge.king_constraint() is not None: king_constraint = hand_knowledge.king_constraint() if king_constraint == HandConstraints.ZERO_OR_FOUR: if hand.king_count() != 0 and hand.king_count() != 4: return False elif hand.king_count() != king_constraint: return False return True # FIXME: The knowledge object should include a list of hand expectation objects # and those expectations should be able to match themselves against a hand. # This current hard-coded solution will not scale long-term. def _knowledge_is_consistent_with_hand(self, knowledge, point_system, hand): hand_knowledge = knowledge.me if hand_knowledge.last_call.stayman and not self._should_bid_stayman(knowledge, hand): return False if hand_knowledge.last_call.two_spades_puppet and not self._have_shape_for_two_spades_puppet(hand): return False if hand_knowledge.last_call.michaels_cuebid and hand_knowledge.last_call.strain in MAJORS and not self._have_unspecified_minor_for_michaels_cuebid_of_a_major(hand): return False if hand_knowledge.last_call.takeout_double: # FIXME: The min_hcp() < 17 check is to prevent us from planning a big-hand double # (ignoring the rest of the hand shape), after a previous big-hand double. if hand_knowledge.min_hcp() < 17 and self._can_big_hand_double(hand): return True # Notice that we short-circuit all the remaining constraints when planning a big-hand double. if not self._points_fit_knowledge(hand, point_system, knowledge): return False if not self._shape_fits_hand_knowledge(hand, hand_knowledge): return False if not self._fits_honor_count_constraints(hand, hand_knowledge): return False if hand_knowledge.quick_tricks() is not None: partner_min_lengths = [knowledge.partner.min_length(suit) for suit in SUITS] if hand.tricks(partner_min_lengths) < hand_knowledge.quick_tricks(): return False # FIXME: We should check the hand against longer_major, longer_minor, longest_suit # being careful to ignore longest_suit_except. return True def priority_bid_rule_hand_knowledge_tuples(self, new_bid): # Note: This method is only ever called for evaluating bids with our own hand, otherwise we'd need to explicitly choose the correct rules. new_knowledge, consuming_rule = self.interpreter.knowledge_including_new_bid(self.knowledge_builder, new_bid, loose_constraints=True) if not new_knowledge: return priorities.InvalidBid, new_bid, consuming_rule, None point_system = consuming_rule.point_system_for_bid(new_bid) if not self._knowledge_is_consistent_with_hand(new_knowledge, point_system, self.hand): return priorities.InvalidBid, new_bid, consuming_rule, new_knowledge.me # Once we know that a bid is consistent with the hand, we then # lookup the bidding priority for the bid. This is different # from the "interpretation" priority which is implicit # in the rule ordering in BidInterpreter.partner_rules. return consuming_rule.priority_for_bid(self.hand, new_bid), new_bid, consuming_rule, new_knowledge.me
def __init__(self): if use_z3: self.interpreter = Interpreter() else: self.interpreter = BidInterpreter()