示例#1
0
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)
示例#2
0
    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))
示例#3
0
    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))
示例#4
0
 def setUp(self):
     self.interpreter = BidInterpreter()
示例#5
0
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")
示例#6
0
 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
示例#7
0
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
示例#8
0
 def __init__(self):
     if use_z3:
         self.interpreter = Interpreter()
     else:
         self.interpreter = BidInterpreter()