Example #1
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))
Example #2
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