<commit_before># Copyright (c) 2013 The SAYCBridge Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from core.call import Call
from core.callexplorer import CallExplorer
from itertools import chain
from third_party import enum
from third_party.memoized import memoized
from z3b.constraints import *
from z3b.model import *
from z3b.orderings import PartialOrdering
from z3b.preconditions import *


categories = enum.Enum(
    "Relay",
    "Gadget",
    "NoTrumpSystem",
    "Default",
    "Natural",
    "LawOfTotalTricks",
    "DefaultPass",
)

# This is a public interface from RuleGenerators to the rest of the system.
# This class knows nothing about the DSL.
class CompiledRule(object):
    def __init__(self, rule, preconditions, known_calls, shared_constraints, annotations, constraints):
        self.dsl_rule = rule
        self.preconditions = preconditions
        self.known_calls = known_calls
        self.shared_constraints = shared_constraints
        self._annotations = annotations
        self.constraints = constraints

    def requires_planning(self, history):
        return self.dsl_rule.requires_planning

    def annotations(self, history):
        return self._annotations

    @property
    def name(self):
        return self.dsl_rule.name

    def __str__(self):
        return self.name

    def __repr__(self):
        return "CompiledRule(%s)" % repr(self.dsl_rule)

    # FIXME: This exists for compatiblity with KBB's Rule interface and is used by bidder_handler.py
    def explanation_for_bid(self, call):
        return None

    # FIXME: This exists for compatiblity with KBB's Rule interface and is used by bidder_handler.py
    def sayc_page_for_bid(self, call):
        return None

    def _fits_preconditions(self, history, call, expected_call=None):
        try:
            for precondition in self.preconditions:
                if not precondition.fits(history, call):
                    if call == expected_call and expected_call in self.known_calls:
                        print " %s failed: %s" % (self, precondition)
                    return False
        except Exception, e:
            print "Exception evaluating preconditions for %s" % self.name
            raise
        return True

    def calls_over(self, history, expected_call=None):
        for call in history.legal_calls.intersection(self.known_calls):
            if self._fits_preconditions(history, call, expected_call):
                yield self.dsl_rule.category, call

    def _constraint_exprs_for_call(self, history, call):
        exprs = []
        per_call_constraints, _ = self.per_call_constraints_and_priority(call)
        if per_call_constraints:
            exprs.extend(RuleCompiler.exprs_from_constraints(per_call_constraints, history, call))
        exprs.extend(RuleCompiler.exprs_from_constraints(self.shared_constraints, history, call))
        return exprs

    def meaning_of(self, history, call):
        exprs = self._constraint_exprs_for_call(history, call)
        for condition, priority in self.dsl_rule.conditional_priorities:
            condition_exprs = RuleCompiler.exprs_from_constraints(condition, history, call)
            yield priority, z3.And(exprs + condition_exprs)

        _, priority = self.per_call_constraints_and_priority(call)
        assert priority
        yield priority, z3.And(exprs)

    # constraints accepts various forms including:
    # constraints = { '1H': hearts > 5 }
    # constraints = { '1H': (hearts > 5, priority) }
    # constraints = { ('1H', '2H'): hearts > 5 }

    # FIXME: Should we split this into two methods? on for priority and one for constraints?
    def per_call_constraints_and_priority(self, call):
        constraints_tuple = self.constraints.get(call.name)
        if constraints_tuple:
            try:
                if isinstance(list(constraints_tuple)[-1], enum.EnumValue):
                    assert len(constraints_tuple) == 2
                    return constraints_tuple
            except TypeError:
                pass
        assert self.dsl_rule.priority, "" + self.name + " is missing priority"
        return constraints_tuple, self.dsl_rule.priority


class RuleCompiler(object):
    @classmethod
    def exprs_from_constraints(cls, constraints, history, call):
        if not constraints:
            return [NO_CONSTRAINTS]

        if isinstance(constraints, Constraint):
            return [constraints.expr(history, call)]

        if isinstance(constraints, z3.ExprRef):
            return [constraints]

        return chain.from_iterable([cls.exprs_from_constraints(constraint, history, call) for constraint in constraints])

    @classmethod
    def _collect_from_ancestors(cls, dls_class, property_name):
        getter = lambda ancestor: getattr(ancestor, property_name, [])
        # The DSL expects that parent preconditions, etc. apply before child ones.
        return map(getter, reversed(dls_class.__mro__))

    @classmethod
    def _ensure_list(cls, value_or_list):
        if value_or_list and not hasattr(value_or_list, '__iter__'):
            return [value_or_list]
        return value_or_list

    @classmethod
    def _joined_list_from_ancestors(cls, dsl_class, property_name):
        values_from_ancestors = cls._collect_from_ancestors(dsl_class, property_name)
        mapped_values = map(cls._ensure_list, values_from_ancestors)
        return list(chain.from_iterable(mapped_values))

    @classmethod
    def _compile_known_calls(cls, dsl_class, constraints):
        if dsl_class.call_names:
            call_names = cls._ensure_list(dsl_class.call_names)
        else:
            call_names = constraints.keys()
        assert call_names, "%s: call_names or constraints map is required." % dsl_class.__name__
        return map(Call.from_string, call_names)

    @classmethod
    def _flatten_constraints(cls, constraints):
        flattened_constraints  = {}
        for key, value in constraints.iteritems():
            if hasattr(key, '__iter__'):
                for call_name in key:
                    flattened_constraints[call_name] = value
            else:
                flattened_constraints[key] = value
        return flattened_constraints

    @classmethod
    def _compile_annotations(cls, dsl_class):
        compiled_set = set(cls._joined_list_from_ancestors(dsl_class, 'annotations'))
        # FIXME: We should probably assert that no more than one of the "implies_artificial"
        # annotations are in this set at once.  Those all have distinct meanings.
        if implies_artificial.intersection(compiled_set):
            compiled_set.add(annotations.Artificial)
        return compiled_set

    @classmethod
    def compile(cls, dsl_rule_class):
        dsl_rule = dsl_rule_class()
        constraints = cls._flatten_constraints(dsl_rule_class.constraints)
        # Unclear if compiled results should be memoized on the rule?
        return CompiledRule(dsl_rule,
            known_calls=cls._compile_known_calls(dsl_rule_class, constraints),
            annotations=cls._compile_annotations(dsl_rule_class),
            preconditions=cls._joined_list_from_ancestors(dsl_rule_class, 'preconditions'),
            shared_constraints=cls._joined_list_from_ancestors(dsl_rule_class, 'shared_constraints'),
            constraints=constraints,
        )


# The rules of SAYC are all described in terms of Rule.
# These classes exist to support the DSL and make it easy to concisely express
# the conventions of SAYC.
class Rule(object):
    # FIXME: Consider splitting call_preconditions out from preconditions
    # for preconditions which only operate on the call?
    preconditions = [] # Auto-collects from parent classes
    category = categories.Default # Intra-bid priority
    requires_planning = False

    # All possible call names must be listed.
    # Having an up-front list makes the bidder's rule-evaluation faster.
    call_names = None # Required.

    constraints = {}
    shared_constraints = [] # Auto-collects from parent classes
    annotations = []
    conditional_priorities = []
    priority = None

    def __init__(self):
        assert self.priority or self.constraints, "" + self.name + " is missing priority"
        # Rules have to apply some constraints to the hand.
        assert self.constraints or self.shared_constraints, "" + self.name + " is missing constraints"
        # conditional_priorities doesn't work with self.constraints
        assert not self.conditional_priorities or not self.constraints
        assert not self.conditional_priorities or self.call_names

    @property
    def name(self):
        return self.__class__.__name__

    def __repr__(self):
        return "%s()" % self.name


relay_priorities = enum.Enum(
    "SuperAccept",
    "Accept",
)


natural_priorities = enum.Enum(
    "SevenLevelNaturalNT",
    "SevenLevelNaturalMajor",
    "SevenLevelNaturalMinor",

    "SixLevelNaturalNT",
    "SixLevelNaturalMajor",
    "SixLevelNaturalMinor",

    "FourLevelNaturalMajor",
    "ThreeLevelNaturalNT", # FIXME: Where does 3N go?
    "FiveLevelNaturalMinor",

    "FourLevelNaturalNT", # Should 4N be higher priority than 5N?
    "FiveLevelNaturalNT",

    "FiveLevelNaturalMajor",

    "FourLevelNaturalMinor",

    "ThreeLevelNaturalMajor",
    "ThreeLevelNaturalMinor",

    "TwoLevelNaturalMajor",
    "TwoLevelNaturalMinor",
    "TwoLevelNaturalNT",

    "OneLevelNaturalNT",
)


class Natural(Rule):
    # FIXME: This should have a SomeoneOpened() precondition.
    category = categories.Natural


class SuitedToPlay(Natural):
    preconditions = [
        MinimumCombinedPointsPrecondition(12),
        PartnerHasAtLeastLengthInSuit(1)
    ]
    constraints = {
        ('2C', '2D'): (MinimumCombinedPoints(19), natural_priorities.TwoLevelNaturalMinor),
        ('2H', '2S'): (MinimumCombinedPoints(19), natural_priorities.TwoLevelNaturalMajor),

        ('3C', '3D'): (MinimumCombinedPoints(22), natural_priorities.ThreeLevelNaturalMinor),
        ('3H', '3S'): (MinimumCombinedPoints(22), natural_priorities.ThreeLevelNaturalMajor),

        ('4C', '4D'): (MinimumCombinedPoints(25), natural_priorities.FourLevelNaturalMinor),
        ('4H', '4S'): (MinimumCombinedPoints(25), natural_priorities.FourLevelNaturalMajor),

        ('5C', '5D'): (MinimumCombinedPoints(28), natural_priorities.FiveLevelNaturalMinor),
        ('5H', '5S'): (MinimumCombinedPoints(28), natural_priorities.FiveLevelNaturalMajor),

        ('6C', '6D'): (MinimumCombinedPoints(33), natural_priorities.SixLevelNaturalMinor),
        ('6H', '6S'): (MinimumCombinedPoints(33), natural_priorities.SixLevelNaturalMajor),

        ('7C', '7D'): (MinimumCombinedPoints(37), natural_priorities.SevenLevelNaturalMinor),
        ('7H', '7S'): (MinimumCombinedPoints(37), natural_priorities.SevenLevelNaturalMajor),
    }
    shared_constraints = [MinimumCombinedLength(8)]


class NotrumpToPlay(Natural):
    constraints = {
        '1N': [MinimumCombinedPoints(19), natural_priorities.OneLevelNaturalNT],
        '2N': [MinimumCombinedPoints(22), natural_priorities.TwoLevelNaturalNT],
        '3N': [MinimumCombinedPoints(25), natural_priorities.ThreeLevelNaturalNT],
        '4N': [MinimumCombinedPoints(28), natural_priorities.FourLevelNaturalNT],
        '5N': [MinimumCombinedPoints(31), natural_priorities.FiveLevelNaturalNT],
        '6N': [MinimumCombinedPoints(34), natural_priorities.SixLevelNaturalNT],
        '7N': [MinimumCombinedPoints(37), natural_priorities.SevenLevelNaturalNT],
    }


opening_priorities = enum.Enum(
    "StrongTwoClubs",
    "NoTrumpOpening",
    "LongestMajor",
    "HigherMajor",
    "LowerMajor",
    "LongestMinor",
    "HigherMinor",
    "LowerMinor",
)


class Opening(Rule):
    annotations = annotations.Opening
    preconditions = NoOpening()


class OneLevelSuitOpening(Opening):
    shared_constraints = OpeningRuleConstraint()


class OneClubOpening(OneLevelSuitOpening):
    call_names = '1C'
    shared_constraints = clubs >= 3
    conditional_priorities = [
        (z3.Or(clubs > diamonds, z3.And(clubs == 3, diamonds == 3)), opening_priorities.LongestMinor),
    ]
    priority = opening_priorities.LowerMinor


class OneDiamondOpening(OneLevelSuitOpening):
    call_names = '1D'
    shared_constraints = diamonds >= 3
    conditional_priorities = [
        (diamonds > clubs, opening_priorities.LongestMinor),
    ]
    priority = opening_priorities.HigherMinor


class OneHeartOpening(OneLevelSuitOpening):
    call_names = '1H'
    shared_constraints = hearts >= 5
    conditional_priorities = [
        (hearts > spades, opening_priorities.LongestMajor),
    ]
    priority = opening_priorities.LowerMajor


class OneSpadeOpening(OneLevelSuitOpening):
    call_names = '1S'
    shared_constraints = spades >= 5
    conditional_priorities = [
        (spades > hearts, opening_priorities.LongestMajor),
    ]
    priority = opening_priorities.HigherMajor


class NoTrumpOpening(Opening):
    annotations = annotations.NoTrumpSystemsOn
    constraints = {
        '1N': z3.And(points >= 15, points <= 17, balanced),
        '2N': z3.And(points >= 20, points <= 21, balanced)
    }
    priority = opening_priorities.NoTrumpOpening


class StrongTwoClubs(Opening):
    call_names = '2C'
    shared_constraints = points >= 22  # FIXME: Should support "or 9+ winners"
    priority = opening_priorities.StrongTwoClubs


response_priorities = enum.Enum(
    "NegativeDouble",
    "Jacoby2N",
    "JumpShiftResponseToOpen",
    "MajorJumpToGame",
    "MajorLimitRaise",
    "MajorMinimumRaise",
    "LongestNewMajor",
    "OneSpadeWithFiveResponse",
    "OneHeartWithFiveResponse",
    "OneDiamondResponse",
    "OneHeartWithFourResponse",
    "OneSpadeWithFourResponse",
    "TwoHeartNewSuitResponse",
    "TwoSpadeNewSuitResponse",
    "TwoClubNewSuitResponse",
    "TwoDiamondNewSuitResponse",
    "MinorLimitRaise",
    "TwoNotrumpLimitResponse",
    "MinorMinimumRaise",
    "OneNotrumpResponse",
)


class Response(Rule):
    preconditions = [LastBidHasAnnotation(positions.Partner, annotations.Opening)]


class ResponseToOneLevelSuitedOpen(Response):
    preconditions = [
        LastBidHasLevel(positions.Partner, 1),
        InvertedPrecondition(LastBidHasStrain(positions.Partner, suit.NOTRUMP))
    ]

class OneLevelResponse(ResponseToOneLevelSuitedOpen):
    shared_constraints = points >= 6


class OneDiamondResponse(OneLevelResponse):
    call_names = '1D'
    shared_constraints = diamonds >= 4
    priority = response_priorities.OneDiamondResponse


class OneHeartResponse(OneLevelResponse):
    call_names = '1H'
    shared_constraints = hearts >= 4
    conditional_priorities = [
        (z3.And(hearts >= 5, hearts > spades), response_priorities.LongestNewMajor),
        (hearts >= 5, response_priorities.OneHeartWithFiveResponse),
    ]
    priority = response_priorities.OneHeartWithFourResponse


class OneSpadeResponse(OneLevelResponse):
    call_names = '1S'
    shared_constraints = spades >= 4
    conditional_priorities = [
        (spades >= 5, response_priorities.OneSpadeWithFiveResponse)
    ]
    priority = response_priorities.OneSpadeWithFourResponse


class OneNotrumpResponse(OneLevelResponse):
    call_names = '1N'
    priority = response_priorities.OneNotrumpResponse


class RaiseResponse(ResponseToOneLevelSuitedOpen):
    preconditions = [
        RaiseOfPartnersLastSuit(),
        LastBidHasAnnotation(positions.Partner, annotations.Opening)
    ]


class MajorMinimumRaise(RaiseResponse):
    call_names = ['2H', '2S']
    shared_constraints = [MinimumCombinedLength(8), MinimumCombinedPoints(18)]
    priority = response_priorities.MajorMinimumRaise


class MajorLimitRaise(RaiseResponse):
    call_names = ['3H', '3S']
    shared_constraints = [MinimumCombinedLength(8), MinimumCombinedPoints(22)]
    priority = response_priorities.MajorLimitRaise


class MajorJumpToGame(RaiseResponse):
    call_names = ['4H', '4S']
    shared_constraints = [MinimumCombinedLength(10), points < 10]
    priority = response_priorities.MajorJumpToGame


class MinorMinimumRaise(RaiseResponse):
    call_names = ['2C', '2D']
    shared_constraints = [MinimumCombinedLength(8), MinimumCombinedPoints(18)]
    priority = response_priorities.MinorMinimumRaise


class MinorLimitRaise(RaiseResponse):
    call_names = ['3C', '3D']
    shared_constraints = [MinimumCombinedLength(8), MinimumCombinedPoints(22)]
    priority = response_priorities.MinorLimitRaise


class TwoNotrumpLimitResponse(ResponseToOneLevelSuitedOpen):
    preconditions = LastBidHasStrain(positions.Partner, [suit.CLUBS, suit.DIAMONDS])
    call_names = '2N'
    shared_constraints = [balanced, MinimumCombinedPoints(22)]
    priority = response_priorities.TwoNotrumpLimitResponse

# We should bid longer suits when possible, up the line for 4 cards.
# we don't currently bid 2D over 2C when we have longer diamonds.

class NewSuitAtTheTwoLevel(ResponseToOneLevelSuitedOpen):
    preconditions = [UnbidSuit(), NotJumpFromLastContract()]
    constraints = {
        '2C' : (clubs >= 4, response_priorities.TwoClubNewSuitResponse),
        '2D' : (diamonds >= 4, response_priorities.TwoDiamondNewSuitResponse),
        '2H' : (hearts >= 5, response_priorities.TwoHeartNewSuitResponse),
        '2S' : (spades >= 5, response_priorities.TwoSpadeNewSuitResponse),
    }
    shared_constraints = MinimumCombinedPoints(22)


class ResponseToMajorOpen(ResponseToOneLevelSuitedOpen):
    preconditions = [
        LastBidHasStrain(positions.Partner, suit.MAJORS),
        InvertedPrecondition(LastBidHasAnnotation(positions.Partner, annotations.Artificial))
    ]


class Jacoby2N(ResponseToMajorOpen):
    preconditions = LastBidWas(positions.RHO, 'P')
    call_names = '2N'
    shared_constraints = [points >= 14, SupportForPartnerLastBid(4)]
    priority = response_priorities.Jacoby2N
    annotations = annotations.Jacoby2N


jacoby_2n_response_priorities = enum.Enum(
    # Currently favoring features over slam interest.  Unclear if that's correct?
    "SolidSuit",
    "Singleton",
    "Slam",
    "Notrump",
    "MinimumGame",
)


class ResponseToJacoby2N(Rule):
    # Bids above 4NT are either natural or covered by other conventions.
    preconditions = LastBidHasAnnotation(positions.Partner, annotations.Jacoby2N)
    category = categories.Gadget


class SingletonResponseToJacoby2N(ResponseToJacoby2N):
    preconditions = InvertedPrecondition(RebidSameSuit())
    call_names = ['3C', '3D', '3H', '3S']
    shared_constraints = MaxLength(1)
    annotations = annotations.Artificial
    priority = jacoby_2n_response_priorities.Singleton


class SolidSuitResponseToJacoby2N(ResponseToJacoby2N):
    preconditions = InvertedPrecondition(RebidSameSuit())
    call_names = ['4C', '4D', '4H', '4S']
    shared_constraints = [MinLength(5), ThreeOfTheTopFiveOrBetter()]
    priority = jacoby_2n_response_priorities.SolidSuit


class SlamResponseToJacoby2N(ResponseToJacoby2N):
    preconditions = RebidSameSuit()
    call_names = ['3C', '3D', '3H', '3S']
    shared_constraints = points >= 18
    priority = jacoby_2n_response_priorities.Slam


class MinimumResponseToJacoby2N(ResponseToJacoby2N):
    preconditions = RebidSameSuit()
    call_names = ['4C', '4D', '4H', '4S']
    shared_constraints = NO_CONSTRAINTS
    priority = jacoby_2n_response_priorities.MinimumGame


class NotrumpResponseToJacoby2N(ResponseToJacoby2N):
    call_names = ['3N']
    shared_constraints = points > 15 # It's really 15-17
    priority = jacoby_2n_response_priorities.Notrump


class JumpShift(object):
    preconditions = [UnbidSuit(), JumpFromLastContract(exact_size=1)]


class JumpShiftResponseToOpen(ResponseToOneLevelSuitedOpen):
    preconditions = JumpShift.preconditions
    # Jumpshifts must be below game and are off in competition so
    # 1S P 3H is the highest available response jumpshift.
    call_names = ['2D', '2H', '2S', '3C', '3D', '3H']
    # FIXME: Shouldn't this be MinHighCardPoints?
    shared_constraints = [points >= 19, MinLength(5)]
    priority = response_priorities.JumpShiftResponseToOpen


class NegativeDouble(ResponseToOneLevelSuitedOpen):
    call_names = 'X'
    preconditions = [
        LastBidHasSuit(positions.RHO),
        MaxLevel(2),
    ]
    priority = response_priorities.NegativeDouble
    annotations = annotations.NegativeDouble


class NegativeDoubleOfOneDiamondOverOneClub(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '1D'),
        LastBidWas(positions.Partner, '1C'),
    ]
    shared_constraints = [points >= 6, hearts >= 4, spades >= 4]


class NegativeDoubleOfOneHeartOverOneClub(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '1H'),
        LastBidWas(positions.Partner, '1C'),
    ]
    shared_constraints = [points >= 6, spades == 4]


class NegativeDoubleOfOneSpadeOverOneClub(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '1S'),
        LastBidWas(positions.Partner, '1C'),
    ]
    shared_constraints = [points >= 6, diamonds >= 3, hearts >= 4]


class NegativeDoubleOfTwoDiamondsOverOneClub(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2D'),
        LastBidWas(positions.Partner, '1C'),
    ]
    shared_constraints = [points >= 8, hearts >= 4, spades >= 4]


class NegativeDoubleOfTwoHeartsOverOneClub(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2H'),
        LastBidWas(positions.Partner, '1C'),
    ]
    shared_constraints = [points >= 8, diamonds >= 3, spades >= 4]


class NegativeDoubleOfTwoSpadesOverOneClub(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2S'),
        LastBidWas(positions.Partner, '1C'),
    ]
    shared_constraints = [points >= 8, diamonds >= 3, hearts >= 4]


class NegativeDoubleOfOneHeartOverOneDiamond(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '1H'),
        LastBidWas(positions.Partner, '1D'),
    ]
    shared_constraints = [points >= 6, spades == 4]


class NegativeDoubleOfOneSpadeOverOneDiamond(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '1S'),
        LastBidWas(positions.Partner, '1D'),
    ]
    shared_constraints = [points >= 6, clubs >= 3, hearts >= 4]


class NegativeDoubleOfTwoClubsOverOneDiamond(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2C'),
        LastBidWas(positions.Partner, '1D'),
    ]
    shared_constraints = [points >= 8, hearts >= 4, spades >= 4]


class NegativeDoubleOfTwoHeartsOverOneDiamond(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2H'),
        LastBidWas(positions.Partner, '1D'),
    ]
    shared_constraints = [points >= 8, clubs >= 3, spades >= 4]


class NegativeDoubleOfTwoSpadesOverOneDiamond(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2S'),
        LastBidWas(positions.Partner, '1D'),
    ]
    shared_constraints = [points >= 8, clubs >= 3, hearts >= 4]


class NegativeDoubleOfOneSpadeOverOneHeart(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '1S'),
        LastBidWas(positions.Partner, '1H'),
    ]
    shared_constraints = [points >= 6, clubs >= 3, diamonds >= 3]


class NegativeDoubleOfTwoClubsOverOneHeart(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2C'),
        LastBidWas(positions.Partner, '1H'),
    ]
    shared_constraints = [points >= 8, diamonds >= 3, spades >= 4]


class NegativeDoubleOfTwoDiamondsOverOneHeart(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2D'),
        LastBidWas(positions.Partner, '1H'),
    ]
    shared_constraints = [points >= 8, clubs >= 3, spades >= 4]


class NegativeDoubleOfTwoSpadesOverOneHeart(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2S'),
        LastBidWas(positions.Partner, '1H'),
    ]
    shared_constraints = [points >= 8, clubs >= 3, diamonds >= 3]


class NegativeDoubleOfTwoClubsOverOneSpade(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2C'),
        LastBidWas(positions.Partner, '1S'),
    ]
    shared_constraints = [points >= 8, diamonds >= 3, hearts >= 4]


class NegativeDoubleOfTwoDiamondsOverOneSpades(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2D'),
        LastBidWas(positions.Partner, '1S'),
    ]
    shared_constraints = [points >= 8, clubs >= 3, hearts >= 4]


class NegativeDoubleOfTwoHeartsOverOneSpade(NegativeDouble):
    preconditions = [
        LastBidWas(positions.RHO, '2H'),
        LastBidWas(positions.Partner, '1S'),
    ]
    shared_constraints = [points >= 8, clubs >= 3, diamonds >= 3]


two_clubs_response_priorities = enum.Enum(
    "SuitResponse",
    "NoBiddableSuit",
    "WaitingResponse",
)


class ResponseToStrongTwoClubs(Response):
    preconditions = LastBidWas(positions.Partner, '2C')


class WaitingResponseToStrongTwoClubs(ResponseToStrongTwoClubs):
    call_names = '2D'
    shared_constraints = NO_CONSTRAINTS
    annotations = annotations.Artificial
    priority = two_clubs_response_priorities.WaitingResponse


class SuitResponseToStrongTwoClubs(ResponseToStrongTwoClubs):
    call_names = ['2H', '2S', '3C', '3D']
    shared_constraints = [MinLength(5), TwoOfTheTopThree(), points >= 8]<commit_msg>Rewrite NegativeDouble to use much less code.<commit_after># Copyright (c) 2013 The SAYCBridge Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from core.call import Call
from core.callexplorer import CallExplorer
from itertools import chain
from third_party import enum
from third_party.memoized import memoized
from z3b.constraints import *
from z3b.model import *
from z3b.orderings import PartialOrdering
from z3b.preconditions import *


categories = enum.Enum(
    "Relay",
    "Gadget",
    "NoTrumpSystem",
    "Default",
    "Natural",
    "LawOfTotalTricks",
    "DefaultPass",
)

# This is a public interface from RuleGenerators to the rest of the system.
# This class knows nothing about the DSL.
class CompiledRule(object):
    def __init__(self, rule, preconditions, known_calls, shared_constraints, annotations, constraints):
        self.dsl_rule = rule
        self.preconditions = preconditions
        self.known_calls = known_calls
        self.shared_constraints = shared_constraints
        self._annotations = annotations
        self.constraints = constraints

    def requires_planning(self, history):
        return self.dsl_rule.requires_planning

    def annotations(self, history):
        return self._annotations

    @property
    def name(self):
        return self.dsl_rule.name

    def __str__(self):
        return self.name

    def __repr__(self):
        return "CompiledRule(%s)" % repr(self.dsl_rule)

    # FIXME: This exists for compatiblity with KBB's Rule interface and is used by bidder_handler.py
    def explanation_for_bid(self, call):
        return None

    # FIXME: This exists for compatiblity with KBB's Rule interface and is used by bidder_handler.py
    def sayc_page_for_bid(self, call):
        return None

    def _fits_preconditions(self, history, call, expected_call=None):
        try:
            for precondition in self.preconditions:
                if not precondition.fits(history, call):
                    if call == expected_call and expected_call in self.known_calls:
                        print " %s failed: %s" % (self, precondition)
                    return False
        except Exception, e:
            print "Exception evaluating preconditions for %s" % self.name
            raise
        return True

    def calls_over(self, history, expected_call=None):
        for call in history.legal_calls.intersection(self.known_calls):
            if self._fits_preconditions(history, call, expected_call):
                yield self.dsl_rule.category, call

    def _constraint_exprs_for_call(self, history, call):
        exprs = []
        per_call_constraints, _ = self.per_call_constraints_and_priority(call)
        if per_call_constraints:
            exprs.extend(RuleCompiler.exprs_from_constraints(per_call_constraints, history, call))
        exprs.extend(RuleCompiler.exprs_from_constraints(self.shared_constraints, history, call))
        return exprs

    def meaning_of(self, history, call):
        exprs = self._constraint_exprs_for_call(history, call)
        for condition, priority in self.dsl_rule.conditional_priorities:
            condition_exprs = RuleCompiler.exprs_from_constraints(condition, history, call)
            yield priority, z3.And(exprs + condition_exprs)

        _, priority = self.per_call_constraints_and_priority(call)
        assert priority
        yield priority, z3.And(exprs)

    # constraints accepts various forms including:
    # constraints = { '1H': hearts > 5 }
    # constraints = { '1H': (hearts > 5, priority) }
    # constraints = { ('1H', '2H'): hearts > 5 }

    # FIXME: Should we split this into two methods? on for priority and one for constraints?
    def per_call_constraints_and_priority(self, call):
        constraints_tuple = self.constraints.get(call.name)
        if constraints_tuple:
            try:
                if isinstance(list(constraints_tuple)[-1], enum.EnumValue):
                    assert len(constraints_tuple) == 2
                    return constraints_tuple
            except TypeError:
                pass
        assert self.dsl_rule.priority, "" + self.name + " is missing priority"
        return constraints_tuple, self.dsl_rule.priority


class RuleCompiler(object):
    @classmethod
    def exprs_from_constraints(cls, constraints, history, call):
        if not constraints:
            return [NO_CONSTRAINTS]

        if isinstance(constraints, Constraint):
            return [constraints.expr(history, call)]

        if isinstance(constraints, z3.ExprRef):
            return [constraints]

        return chain.from_iterable([cls.exprs_from_constraints(constraint, history, call) for constraint in constraints])

    @classmethod
    def _collect_from_ancestors(cls, dls_class, property_name):
        getter = lambda ancestor: getattr(ancestor, property_name, [])
        # The DSL expects that parent preconditions, etc. apply before child ones.
        return map(getter, reversed(dls_class.__mro__))

    @classmethod
    def _ensure_list(cls, value_or_list):
        if value_or_list and not hasattr(value_or_list, '__iter__'):
            return [value_or_list]
        return value_or_list

    @classmethod
    def _joined_list_from_ancestors(cls, dsl_class, property_name):
        values_from_ancestors = cls._collect_from_ancestors(dsl_class, property_name)
        mapped_values = map(cls._ensure_list, values_from_ancestors)
        return list(chain.from_iterable(mapped_values))

    @classmethod
    def _compile_known_calls(cls, dsl_class, constraints):
        if dsl_class.call_names:
            call_names = cls._ensure_list(dsl_class.call_names)
        else:
            call_names = constraints.keys()
        assert call_names, "%s: call_names or constraints map is required." % dsl_class.__name__
        return map(Call.from_string, call_names)

    @classmethod
    def _flatten_constraints(cls, constraints):
        flattened_constraints  = {}
        for key, value in constraints.iteritems():
            if hasattr(key, '__iter__'):
                for call_name in key:
                    flattened_constraints[call_name] = value
            else:
                flattened_constraints[key] = value
        return flattened_constraints

    @classmethod
    def _compile_annotations(cls, dsl_class):
        compiled_set = set(cls._joined_list_from_ancestors(dsl_class, 'annotations'))
        # FIXME: We should probably assert that no more than one of the "implies_artificial"
        # annotations are in this set at once.  Those all have distinct meanings.
        if implies_artificial.intersection(compiled_set):
            compiled_set.add(annotations.Artificial)
        return compiled_set

    @classmethod
    def compile(cls, dsl_rule_class):
        dsl_rule = dsl_rule_class()
        constraints = cls._flatten_constraints(dsl_rule_class.constraints)
        # Unclear if compiled results should be memoized on the rule?
        return CompiledRule(dsl_rule,
            known_calls=cls._compile_known_calls(dsl_rule_class, constraints),
            annotations=cls._compile_annotations(dsl_rule_class),
            preconditions=cls._joined_list_from_ancestors(dsl_rule_class, 'preconditions'),
            shared_constraints=cls._joined_list_from_ancestors(dsl_rule_class, 'shared_constraints'),
            constraints=constraints,
        )


# The rules of SAYC are all described in terms of Rule.
# These classes exist to support the DSL and make it easy to concisely express
# the conventions of SAYC.
class Rule(object):
    # FIXME: Consider splitting call_preconditions out from preconditions
    # for preconditions which only operate on the call?
    preconditions = [] # Auto-collects from parent classes
    category = categories.Default # Intra-bid priority
    requires_planning = False

    # All possible call names must be listed.
    # Having an up-front list makes the bidder's rule-evaluation faster.
    call_names = None # Required.

    constraints = {}
    shared_constraints = [] # Auto-collects from parent classes
    annotations = []
    conditional_priorities = []
    priority = None

    def __init__(self):
        assert self.priority or self.constraints, "" + self.name + " is missing priority"
        # Rules have to apply some constraints to the hand.
        assert self.constraints or self.shared_constraints, "" + self.name + " is missing constraints"
        # conditional_priorities doesn't work with self.constraints
        assert not self.conditional_priorities or not self.constraints
        assert not self.conditional_priorities or self.call_names

    @property
    def name(self):
        return self.__class__.__name__

    def __repr__(self):
        return "%s()" % self.name


relay_priorities = enum.Enum(
    "SuperAccept",
    "Accept",
)


natural_priorities = enum.Enum(
    "SevenLevelNaturalNT",
    "SevenLevelNaturalMajor",
    "SevenLevelNaturalMinor",

    "SixLevelNaturalNT",
    "SixLevelNaturalMajor",
    "SixLevelNaturalMinor",

    "FourLevelNaturalMajor",
    "ThreeLevelNaturalNT", # FIXME: Where does 3N go?
    "FiveLevelNaturalMinor",

    "FourLevelNaturalNT", # Should 4N be higher priority than 5N?
    "FiveLevelNaturalNT",

    "FiveLevelNaturalMajor",

    "FourLevelNaturalMinor",

    "ThreeLevelNaturalMajor",
    "ThreeLevelNaturalMinor",

    "TwoLevelNaturalMajor",
    "TwoLevelNaturalMinor",
    "TwoLevelNaturalNT",

    "OneLevelNaturalNT",
)


class Natural(Rule):
    # FIXME: This should have a SomeoneOpened() precondition.
    category = categories.Natural


class SuitedToPlay(Natural):
    preconditions = [
        MinimumCombinedPointsPrecondition(12),
        PartnerHasAtLeastLengthInSuit(1)
    ]
    constraints = {
        ('2C', '2D'): (MinimumCombinedPoints(19), natural_priorities.TwoLevelNaturalMinor),
        ('2H', '2S'): (MinimumCombinedPoints(19), natural_priorities.TwoLevelNaturalMajor),

        ('3C', '3D'): (MinimumCombinedPoints(22), natural_priorities.ThreeLevelNaturalMinor),
        ('3H', '3S'): (MinimumCombinedPoints(22), natural_priorities.ThreeLevelNaturalMajor),

        ('4C', '4D'): (MinimumCombinedPoints(25), natural_priorities.FourLevelNaturalMinor),
        ('4H', '4S'): (MinimumCombinedPoints(25), natural_priorities.FourLevelNaturalMajor),

        ('5C', '5D'): (MinimumCombinedPoints(28), natural_priorities.FiveLevelNaturalMinor),
        ('5H', '5S'): (MinimumCombinedPoints(28), natural_priorities.FiveLevelNaturalMajor),

        ('6C', '6D'): (MinimumCombinedPoints(33), natural_priorities.SixLevelNaturalMinor),
        ('6H', '6S'): (MinimumCombinedPoints(33), natural_priorities.SixLevelNaturalMajor),

        ('7C', '7D'): (MinimumCombinedPoints(37), natural_priorities.SevenLevelNaturalMinor),
        ('7H', '7S'): (MinimumCombinedPoints(37), natural_priorities.SevenLevelNaturalMajor),
    }
    shared_constraints = [MinimumCombinedLength(8)]


class NotrumpToPlay(Natural):
    constraints = {
        '1N': [MinimumCombinedPoints(19), natural_priorities.OneLevelNaturalNT],
        '2N': [MinimumCombinedPoints(22), natural_priorities.TwoLevelNaturalNT],
        '3N': [MinimumCombinedPoints(25), natural_priorities.ThreeLevelNaturalNT],
        '4N': [MinimumCombinedPoints(28), natural_priorities.FourLevelNaturalNT],
        '5N': [MinimumCombinedPoints(31), natural_priorities.FiveLevelNaturalNT],
        '6N': [MinimumCombinedPoints(34), natural_priorities.SixLevelNaturalNT],
        '7N': [MinimumCombinedPoints(37), natural_priorities.SevenLevelNaturalNT],
    }


opening_priorities = enum.Enum(
    "StrongTwoClubs",
    "NoTrumpOpening",
    "LongestMajor",
    "HigherMajor",
    "LowerMajor",
    "LongestMinor",
    "HigherMinor",
    "LowerMinor",
)


class Opening(Rule):
    annotations = annotations.Opening
    preconditions = NoOpening()


class OneLevelSuitOpening(Opening):
    shared_constraints = OpeningRuleConstraint()


class OneClubOpening(OneLevelSuitOpening):
    call_names = '1C'
    shared_constraints = clubs >= 3
    conditional_priorities = [
        (z3.Or(clubs > diamonds, z3.And(clubs == 3, diamonds == 3)), opening_priorities.LongestMinor),
    ]
    priority = opening_priorities.LowerMinor


class OneDiamondOpening(OneLevelSuitOpening):
    call_names = '1D'
    shared_constraints = diamonds >= 3
    conditional_priorities = [
        (diamonds > clubs, opening_priorities.LongestMinor),
    ]
    priority = opening_priorities.HigherMinor


class OneHeartOpening(OneLevelSuitOpening):
    call_names = '1H'
    shared_constraints = hearts >= 5
    conditional_priorities = [
        (hearts > spades, opening_priorities.LongestMajor),
    ]
    priority = opening_priorities.LowerMajor


class OneSpadeOpening(OneLevelSuitOpening):
    call_names = '1S'
    shared_constraints = spades >= 5
    conditional_priorities = [
        (spades > hearts, opening_priorities.LongestMajor),
    ]
    priority = opening_priorities.HigherMajor


class NoTrumpOpening(Opening):
    annotations = annotations.NoTrumpSystemsOn
    constraints = {
        '1N': z3.And(points >= 15, points <= 17, balanced),
        '2N': z3.And(points >= 20, points <= 21, balanced)
    }
    priority = opening_priorities.NoTrumpOpening


class StrongTwoClubs(Opening):
    call_names = '2C'
    shared_constraints = points >= 22  # FIXME: Should support "or 9+ winners"
    priority = opening_priorities.StrongTwoClubs


response_priorities = enum.Enum(
    "NegativeDouble",
    "Jacoby2N",
    "JumpShiftResponseToOpen",
    "MajorJumpToGame",
    "MajorLimitRaise",
    "MajorMinimumRaise",
    "LongestNewMajor",
    "OneSpadeWithFiveResponse",
    "OneHeartWithFiveResponse",
    "OneDiamondResponse",
    "OneHeartWithFourResponse",
    "OneSpadeWithFourResponse",
    "TwoHeartNewSuitResponse",
    "TwoSpadeNewSuitResponse",
    "TwoClubNewSuitResponse",
    "TwoDiamondNewSuitResponse",
    "MinorLimitRaise",
    "TwoNotrumpLimitResponse",
    "MinorMinimumRaise",
    "OneNotrumpResponse",
)


class Response(Rule):
    preconditions = [LastBidHasAnnotation(positions.Partner, annotations.Opening)]


class ResponseToOneLevelSuitedOpen(Response):
    preconditions = [
        LastBidHasLevel(positions.Partner, 1),
        InvertedPrecondition(LastBidHasStrain(positions.Partner, suit.NOTRUMP))
    ]

class OneLevelResponse(ResponseToOneLevelSuitedOpen):
    shared_constraints = points >= 6


class OneDiamondResponse(OneLevelResponse):
    call_names = '1D'
    shared_constraints = diamonds >= 4
    priority = response_priorities.OneDiamondResponse


class OneHeartResponse(OneLevelResponse):
    call_names = '1H'
    shared_constraints = hearts >= 4
    conditional_priorities = [
        (z3.And(hearts >= 5, hearts > spades), response_priorities.LongestNewMajor),
        (hearts >= 5, response_priorities.OneHeartWithFiveResponse),
    ]
    priority = response_priorities.OneHeartWithFourResponse


class OneSpadeResponse(OneLevelResponse):
    call_names = '1S'
    shared_constraints = spades >= 4
    conditional_priorities = [
        (spades >= 5, response_priorities.OneSpadeWithFiveResponse)
    ]
    priority = response_priorities.OneSpadeWithFourResponse


class OneNotrumpResponse(OneLevelResponse):
    call_names = '1N'
    priority = response_priorities.OneNotrumpResponse


class RaiseResponse(ResponseToOneLevelSuitedOpen):
    preconditions = [
        RaiseOfPartnersLastSuit(),
        LastBidHasAnnotation(positions.Partner, annotations.Opening)
    ]


class MajorMinimumRaise(RaiseResponse):
    call_names = ['2H', '2S']
    shared_constraints = [MinimumCombinedLength(8), MinimumCombinedPoints(18)]
    priority = response_priorities.MajorMinimumRaise


class MajorLimitRaise(RaiseResponse):
    call_names = ['3H', '3S']
    shared_constraints = [MinimumCombinedLength(8), MinimumCombinedPoints(22)]
    priority = response_priorities.MajorLimitRaise


class MajorJumpToGame(RaiseResponse):
    call_names = ['4H', '4S']
    shared_constraints = [MinimumCombinedLength(10), points < 10]
    priority = response_priorities.MajorJumpToGame


class MinorMinimumRaise(RaiseResponse):
    call_names = ['2C', '2D']
    shared_constraints = [MinimumCombinedLength(8), MinimumCombinedPoints(18)]
    priority = response_priorities.MinorMinimumRaise


class MinorLimitRaise(RaiseResponse):
    call_names = ['3C', '3D']
    shared_constraints = [MinimumCombinedLength(8), MinimumCombinedPoints(22)]
    priority = response_priorities.MinorLimitRaise


class TwoNotrumpLimitResponse(ResponseToOneLevelSuitedOpen):
    preconditions = LastBidHasStrain(positions.Partner, [suit.CLUBS, suit.DIAMONDS])
    call_names = '2N'
    shared_constraints = [balanced, MinimumCombinedPoints(22)]
    priority = response_priorities.TwoNotrumpLimitResponse

# We should bid longer suits when possible, up the line for 4 cards.
# we don't currently bid 2D over 2C when we have longer diamonds.

class NewSuitAtTheTwoLevel(ResponseToOneLevelSuitedOpen):
    preconditions = [UnbidSuit(), NotJumpFromLastContract()]
    constraints = {
        '2C' : (clubs >= 4, response_priorities.TwoClubNewSuitResponse),
        '2D' : (diamonds >= 4, response_priorities.TwoDiamondNewSuitResponse),
        '2H' : (hearts >= 5, response_priorities.TwoHeartNewSuitResponse),
        '2S' : (spades >= 5, response_priorities.TwoSpadeNewSuitResponse),
    }
    shared_constraints = MinimumCombinedPoints(22)


class ResponseToMajorOpen(ResponseToOneLevelSuitedOpen):
    preconditions = [
        LastBidHasStrain(positions.Partner, suit.MAJORS),
        InvertedPrecondition(LastBidHasAnnotation(positions.Partner, annotations.Artificial))
    ]


class Jacoby2N(ResponseToMajorOpen):
    preconditions = LastBidWas(positions.RHO, 'P')
    call_names = '2N'
    shared_constraints = [points >= 14, SupportForPartnerLastBid(4)]
    priority = response_priorities.Jacoby2N
    annotations = annotations.Jacoby2N


jacoby_2n_response_priorities = enum.Enum(
    # Currently favoring features over slam interest.  Unclear if that's correct?
    "SolidSuit",
    "Singleton",
    "Slam",
    "Notrump",
    "MinimumGame",
)


class ResponseToJacoby2N(Rule):
    # Bids above 4NT are either natural or covered by other conventions.
    preconditions = LastBidHasAnnotation(positions.Partner, annotations.Jacoby2N)
    category = categories.Gadget


class SingletonResponseToJacoby2N(ResponseToJacoby2N):
    preconditions = InvertedPrecondition(RebidSameSuit())
    call_names = ['3C', '3D', '3H', '3S']
    shared_constraints = MaxLength(1)
    annotations = annotations.Artificial
    priority = jacoby_2n_response_priorities.Singleton


class SolidSuitResponseToJacoby2N(ResponseToJacoby2N):
    preconditions = InvertedPrecondition(RebidSameSuit())
    call_names = ['4C', '4D', '4H', '4S']
    shared_constraints = [MinLength(5), ThreeOfTheTopFiveOrBetter()]
    priority = jacoby_2n_response_priorities.SolidSuit


class SlamResponseToJacoby2N(ResponseToJacoby2N):
    preconditions = RebidSameSuit()
    call_names = ['3C', '3D', '3H', '3S']
    shared_constraints = points >= 18
    priority = jacoby_2n_response_priorities.Slam


class MinimumResponseToJacoby2N(ResponseToJacoby2N):
    preconditions = RebidSameSuit()
    call_names = ['4C', '4D', '4H', '4S']
    shared_constraints = NO_CONSTRAINTS
    priority = jacoby_2n_response_priorities.MinimumGame


class NotrumpResponseToJacoby2N(ResponseToJacoby2N):
    call_names = ['3N']
    shared_constraints = points > 15 # It's really 15-17
    priority = jacoby_2n_response_priorities.Notrump


class JumpShift(object):
    preconditions = [UnbidSuit(), JumpFromLastContract(exact_size=1)]


class JumpShiftResponseToOpen(ResponseToOneLevelSuitedOpen):
    preconditions = JumpShift.preconditions
    # Jumpshifts must be below game and are off in competition so
    # 1S P 3H is the highest available response jumpshift.
    call_names = ['2D', '2H', '2S', '3C', '3D', '3H']
    # FIXME: Shouldn't this be MinHighCardPoints?
    shared_constraints = [points >= 19, MinLength(5)]
    priority = response_priorities.JumpShiftResponseToOpen


class ShapeForNegativeDouble(Constraint):
    def expr(self, history, call):
        call_string = '%s %s' % (history.partner.last_call.name, history.rho.last_call.name)
        return {
            '1C 1D': z3.And(hearts >= 4, spades >= 4),
            '1C 1H': spades == 4,
            '1C 1S': z3.And(diamonds >= 3, hearts >= 4),
            '1C 2D': z3.And(hearts >= 4, spades >= 4),
            '1C 2H': z3.And(diamonds >= 3, spades >= 4),
            '1C 2S': z3.And(diamonds >= 3, hearts >= 4),
            '1D 1H': spades == 4,
            '1D 1S': z3.And(clubs >= 3, hearts >= 4),
            '1D 2C': z3.And(hearts >= 4, spades >= 4),
            '1D 2H': z3.And(clubs >= 3, spades >= 4),
            '1D 2S': z3.And(clubs >= 3, hearts >= 4),
            '1H 1S': z3.And(clubs >= 3, diamonds >= 3), # Probably promises 4+ in both minors?
            '1H 2C': z3.And(diamonds >= 3, spades >= 4),
            '1H 2D': z3.And(clubs >= 3, spades >= 4),
            '1H 2S': z3.And(clubs >= 3, diamonds >= 3),
            '1S 2C': z3.And(diamonds >= 3, hearts >= 4),
            '1S 2D': z3.And(clubs >= 3, hearts >= 4),
            '1S 2H': z3.And(clubs >= 3, diamonds >= 3),
        }[call_string]


class NegativeDouble(ResponseToOneLevelSuitedOpen):
    call_names = 'X'
    preconditions = [
        LastBidHasAnnotation(positions.Partner, annotations.Opening),
        LastBidHasSuit(positions.Partner),
        LastBidHasSuit(positions.RHO),
    ]
    shared_constraints = ShapeForNegativeDouble()
    priority = response_priorities.NegativeDouble
    annotations = annotations.NegativeDouble


class OneLevelNegativeDouble(NegativeDouble):
    preconditions = LastBidHasLevel(positions.RHO, 1)
    shared_constraints = points >= 6


class TwoLevelNegativeDouble(NegativeDouble):
    preconditions = LastBidHasLevel(positions.RHO, 2)
    shared_constraints = points >= 8


two_clubs_response_priorities = enum.Enum(
    "SuitResponse",
    "NoBiddableSuit",
    "WaitingResponse",
)


class ResponseToStrongTwoClubs(Response):
    preconditions = LastBidWas(positions.Partner, '2C')


class WaitingResponseToStrongTwoClubs(ResponseToStrongTwoClubs):
    call_names = '2D'
    shared_constraints = NO_CONSTRAINTS
    annotations = annotations.Artificial
    priority = two_clubs_response_priorities.WaitingResponse


class SuitResponseToStrongTwoClubs(ResponseToStrongTwoClubs):
    call_names = ['2H', '2S', '3C', '3D']
    shared_constraints = [MinLength(5), TwoOfTheTopThree(), points >= 8]
{'fraction_non_alphanumeric': 0.09644037154790848, 'fraction_numerical': 0.014255137875859776, 'mean_word_length': 3.703743524582152, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 149, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7414310', 'n_tokens_mistral': 15891, 'n_tokens_neox': 14483, 'n_words': 3058}