Exemple #1
0
 def __computeDragonWindMelds(tileString):
     """returns lists with melds containing all (even single)
     dragons respective winds"""
     dragonMelds = []
     windMelds = []
     for split in tileString.split():
         if split[0] == 'R':
             pairs = Pairs(split[1:])
             for lst, tiles in ((windMelds, elements.wINDS), (dragonMelds, elements.dRAGONS)):
                 for tile in tiles:
                     count = pairs.count(tile)
                     if count:
                         lst.append(Meld([tile] * count))
         elif split[0] in 'dD':
             dragonMelds.append(Meld(split))
         elif split[0] in 'wW':
             windMelds.append(Meld(split))
     return dragonMelds, windMelds
Exemple #2
0
 def __separateBonusMelds(tileString):
     """keep them separate. One meld per bonus tile. Others depend on that."""
     result = []
     if 'f' in tileString or 'y' in tileString:
         for pair in Pairs(tileString.replace(' ', '').replace('R', '')):
             if pair[0] in 'fy':
                 result.append(Meld(pair))
                 tileString = tileString.replace(pair, '', 1)
     return result, tileString
Exemple #3
0
 def __computeDragonWindMelds(tileString):
     """returns lists with melds containing all (even single)
     dragons respective winds"""
     dragonMelds = []
     windMelds = []
     for split in tileString.split():
         if split[0] == 'R':
             pairs = Pairs(split[1:])
             for lst, tiles in ((windMelds, elements.wINDS),
                                (dragonMelds, elements.dRAGONS)):
                 for tile in tiles:
                     count = pairs.count(tile)
                     if count:
                         lst.append(Meld([tile] * count))
         elif split[0] in 'dD':
             dragonMelds.append(Meld(split))
         elif split[0] in 'wW':
             windMelds.append(Meld(split))
     return dragonMelds, windMelds
Exemple #4
0
    def __init__(self, ruleset, string, computedRules=None, robbedTile=None):
        """evaluate string using ruleset. rules are to be applied in any case.
        ruleset can be Hand, Game or Ruleset."""
        # silence pylint. This method is time critical, so do not split it into smaller methods
        # pylint: disable=R0902,R0914,R0912,R0915
        if isinstance(ruleset, Hand):
            self.ruleset = ruleset.ruleset
            self.player = ruleset.player
            self.computedRules = ruleset.computedRules
        elif isinstance(ruleset, Ruleset):
            self.ruleset = ruleset
            self.player = None
        else:
            self.player = ruleset
            self.ruleset = self.player.game.ruleset
        self.string = string
        self.robbedTile = robbedTile
        if computedRules is not None and not isinstance(computedRules, list):
            computedRules = list([computedRules])
        self.computedRules = computedRules or []
        self.__won = False
        self.mjStr = ''
        self.mjRule = None
        self.ownWind = None
        self.roundWind = None
        tileStrings = []
        mjStrings = []
        haveM = False
        splits = self.string.split()
        for part in splits:
            partId = part[0]
            if partId in 'Mmx':
                haveM = True
                self.ownWind = part[1]
                self.roundWind = part[2]
                mjStrings.append(part)
                self.__won = partId == 'M'
            elif partId == 'L':
                if len(part[1:]) > 8:
                    raise Exception('last tile cannot complete a kang:' + self.string)
                mjStrings.append(part)
            else:
                tileStrings.append(part)

        if not haveM:
            raise Exception('Hand got string without mMx: %s', self.string)
        self.mjStr = ' '.join(mjStrings)
        self.__lastTile = self.__lastSource = self.__announcements = ''
        self.__lastMeld = 0
        self.__lastMelds = []
        self.hiddenMelds = []
        self.declaredMelds = []
        self.melds = []
        tileString = ' '.join(tileStrings)
        self.bonusMelds, tileString = self.__separateBonusMelds(tileString)
        self.tileNames = Pairs(tileString.replace(' ','').replace('R', ''))
        self.tileNames.sort()
        self.values = ''.join(x[1] for x in self.tileNames)
        self.suits = set(x[0].lower() for x in self.tileNames)
        self.lenOffset = self.__computeLenOffset(tileString)
        self.dragonMelds, self.windMelds = self.__computeDragonWindMelds(tileString)
        self.__separateMelds(tileString)
        self.hiddenMelds = sorted(self.hiddenMelds, key=meldKey)
        self.tileNamesInHand = sum((x.pairs for x in self.hiddenMelds), [])
        self.sortedMeldsContent = meldsContent(self.melds)
        if self.bonusMelds:
            self.sortedMeldsContent += ' ' + meldsContent(self.bonusMelds)

        self.usedRules = []
        self.score = None
        oldWon = self.won
        self.__applyRules()
        if len(self.lastMelds) > 1:
            self.__applyBestLastMeld()
        if self.won != oldWon:
            # if not won after all, this might be a long hand.
            # So we might even have to unapply meld rules and
            # bonus points. Instead just recompute all again.
            # This should only happen with scoring manual games
            # and with scoringtest - normally kajongg would not
            # let you declare an invalid mah jongg
            self.__applyRules()
Exemple #5
0
class Hand(object):
    """represent the hand to be evaluated"""

    # pylint: disable=R0902
    # pylint we need more than 10 instance attributes

    cache = dict()
    misses = 0
    hits = 0

    @staticmethod
    def clearCache(game):
        """clears the cache with Hands"""
        if Debug.handCache and Hand.cache:
            game.debug('cache hits:%d misses:%d' % (Hand.hits, Hand.misses))
        Hand.cache.clear()
        Hand.hits = 0
        Hand.misses = 0

    @staticmethod
    def cached(ruleset, string, computedRules=None, robbedTile=None):
        """since a Hand instance is never changed, we can use a cache"""
        if computedRules is not None and not isinstance(computedRules, list):
            computedRules = list([computedRules])
        cRuleHash = '&&'.join([rule.name for rule in computedRules]) if computedRules else 'None'
        if isinstance(ruleset, Hand):
            cacheId = id(ruleset.player or ruleset.ruleset)
        else:
            cacheId = id(ruleset)
        cacheKey = hash((cacheId, string, robbedTile, cRuleHash))
        cache = Hand.cache
        if cacheKey in cache:
            if cache[cacheKey] is None:
                raise Exception('recursion: Hand calls itself for same content')
            Hand.hits += 1
            return cache[cacheKey]
        Hand.misses += 1
        cache[cacheKey] = None
        result = Hand(ruleset, string,
            computedRules=computedRules, robbedTile=robbedTile)
        cache[cacheKey] = result
        return result

    def __init__(self, ruleset, string, computedRules=None, robbedTile=None):
        """evaluate string using ruleset. rules are to be applied in any case.
        ruleset can be Hand, Game or Ruleset."""
        # silence pylint. This method is time critical, so do not split it into smaller methods
        # pylint: disable=R0902,R0914,R0912,R0915
        if isinstance(ruleset, Hand):
            self.ruleset = ruleset.ruleset
            self.player = ruleset.player
            self.computedRules = ruleset.computedRules
        elif isinstance(ruleset, Ruleset):
            self.ruleset = ruleset
            self.player = None
        else:
            self.player = ruleset
            self.ruleset = self.player.game.ruleset
        self.string = string
        self.robbedTile = robbedTile
        if computedRules is not None and not isinstance(computedRules, list):
            computedRules = list([computedRules])
        self.computedRules = computedRules or []
        self.__won = False
        self.mjStr = ''
        self.mjRule = None
        self.ownWind = None
        self.roundWind = None
        tileStrings = []
        mjStrings = []
        haveM = False
        splits = self.string.split()
        for part in splits:
            partId = part[0]
            if partId in 'Mmx':
                haveM = True
                self.ownWind = part[1]
                self.roundWind = part[2]
                mjStrings.append(part)
                self.__won = partId == 'M'
            elif partId == 'L':
                if len(part[1:]) > 8:
                    raise Exception('last tile cannot complete a kang:' + self.string)
                mjStrings.append(part)
            else:
                tileStrings.append(part)

        if not haveM:
            raise Exception('Hand got string without mMx: %s', self.string)
        self.mjStr = ' '.join(mjStrings)
        self.__lastTile = self.__lastSource = self.__announcements = ''
        self.__lastMeld = 0
        self.__lastMelds = []
        self.hiddenMelds = []
        self.declaredMelds = []
        self.melds = []
        tileString = ' '.join(tileStrings)
        self.bonusMelds, tileString = self.__separateBonusMelds(tileString)
        self.tileNames = Pairs(tileString.replace(' ','').replace('R', ''))
        self.tileNames.sort()
        self.values = ''.join(x[1] for x in self.tileNames)
        self.suits = set(x[0].lower() for x in self.tileNames)
        self.lenOffset = self.__computeLenOffset(tileString)
        self.dragonMelds, self.windMelds = self.__computeDragonWindMelds(tileString)
        self.__separateMelds(tileString)
        self.hiddenMelds = sorted(self.hiddenMelds, key=meldKey)
        self.tileNamesInHand = sum((x.pairs for x in self.hiddenMelds), [])
        self.sortedMeldsContent = meldsContent(self.melds)
        if self.bonusMelds:
            self.sortedMeldsContent += ' ' + meldsContent(self.bonusMelds)

        self.usedRules = []
        self.score = None
        oldWon = self.won
        self.__applyRules()
        if len(self.lastMelds) > 1:
            self.__applyBestLastMeld()
        if self.won != oldWon:
            # if not won after all, this might be a long hand.
            # So we might even have to unapply meld rules and
            # bonus points. Instead just recompute all again.
            # This should only happen with scoring manual games
            # and with scoringtest - normally kajongg would not
            # let you declare an invalid mah jongg
            self.__applyRules()

    @property
    def lastTile(self):
        """compute and cache, readonly"""
        if self.__lastTile == '':
            self.__setLastTile()
        return self.__lastTile

    @property
    def lastSource(self):
        """compute and cache, readonly"""
        if self.__lastTile == '':
            self.__setLastTile()
        return self.__lastSource

    @property
    def announcements(self):
        """compute and cache, readonly"""
        if self.__lastTile == '':
            self.__setLastTile()
        return self.__announcements

    @property
    def lastMeld(self):
        """compute and cache, readonly"""
        if self.__lastMeld == 0:
            self.__setLastMeld()
        return self.__lastMeld

    @property
    def lastMelds(self):
        """compute and cache, readonly"""
        if self.__lastMeld == 0:
            self.__setLastMeld()
        return self.__lastMelds

    @property
    def won(self):
        """have we been modified since load or last save?
        The "won" value is set to True when instantiating the hand,
        according to the mMx in the init string. Later on, it may
        only be cleared."""
        return self.__won

    @won.setter
    def won(self, value):
        """must never change to True"""
        value = bool(value)
        assert not value
        self.__won = value
        self.string = self.string.replace(' M', ' m')
        self.mjStr = self.mjStr.replace(' M', ' m')

    def debug(self, msg, btIndent=None):
        """try to use Game.debug so we get a nice prefix"""
        if self.player:
            self.player.game.debug(msg, btIndent=btIndent)
        else:
            logDebug(msg, btIndent=btIndent)

    def __applyRules(self):
        """find out which rules apply, collect in self.usedRules.
        This may change self.won"""
        self.usedRules = list([UsedRule(rule) for rule in self.computedRules])
        if self.__hasExclusiveRules():
            return
        self.__applyMeldRules()
        self.__applyHandRules()
        if self.__hasExclusiveRules():
            return
        self.score = self.__totalScore()

        # do the rest only if we know all tiles of the hand
        if 'Xy' in self.string:
            self.won = False    # we do not know better
            return
        if self.won:
            matchingMJRules = self.__maybeMahjongg()
            if not matchingMJRules:
                self.won = False
                self.score = self.__totalScore()
                return
            self.mjRule = matchingMJRules[0]
            self.usedRules.append(UsedRule(self.mjRule))
            if self.__hasExclusiveRules():
                return
            self.usedRules.extend(self.matchingWinnerRules())
            self.score = self.__totalScore()
        else: # not self.won
            assert self.mjRule is None
            loserRules = self.__matchingRules(self.ruleset.loserRules)
            if loserRules:
                self.usedRules.extend(list(UsedRule(x) for x in loserRules))
                self.score = self.__totalScore()

    def matchingWinnerRules(self):
        """returns a list of matching winner rules"""
        matching = self.__matchingRules(self.ruleset.winnerRules)
        for rule in matching:
            if (self.ruleset.limit and rule.score.limits >= 1) or 'absolute' in rule.options:
                return [UsedRule(rule)]
        return list(UsedRule(x) for x in matching)

    def __hasExclusiveRules(self):
        """if we have one, remove all others"""
        exclusive = list(x for x in self.usedRules if 'absolute' in x.rule.options)
        if exclusive:
            self.usedRules = exclusive
            self.score = self.__totalScore()
            self.won = self.__maybeMahjongg()
        return bool(exclusive)

    def __setLastTile(self):
        """sets lastTile, lastSource, announcements"""
        self.__announcements = ''
        self.__lastTile = None
        self.__lastSource = None
        parts = self.mjStr.split()
        for part in parts:
            if part[0] == 'L':
                part = part[1:]
                if len(part) > 2:
                    self.__lastMeld = Meld(part[2:])
                self.__lastTile = part[:2]
            elif part[0] == 'M':
                if len(part) > 3:
                    self.__lastSource = part[3]
                    if len(part) > 4:
                        self.__announcements = part[4:]
        if self.__lastTile:
            assert self.__lastTile in self.tileNames, 'lastTile %s is not in tiles %s, mjStr=%s' % (
                self.__lastTile, ' '.join(self.tileNames), self.mjStr)
            if self.__lastSource == 'k':
                assert self.tileNames.count(self.__lastTile.lower()) + \
                    self.tileNames.count(self.__lastTile.capitalize()) == 1, \
                    'Robbing kong: I cannot have lastTile %s more than once in %s' % (
                    self.__lastTile, ' '.join(self.tileNames))


    def __setLastMeld(self):
        """sets the shortest possible last meld. This is
        not yet the final choice, see __applyBestLastMeld"""
        self.__lastMeld = None
        if self.lastTile and self.won:
            if hasattr(self.mjRule.function, 'computeLastMelds'):
                self.__lastMelds = self.mjRule.function.computeLastMelds(self)
                if self.__lastMelds:
                    # syncHandBoard may return nothing
                    if len(self.__lastMelds) == 1:
                        self.__lastMeld = self.__lastMelds[0]
                    else:
                        totals = sorted((len(x), idx) for idx, x in enumerate(self.__lastMelds))
                        self.__lastMeld = self.__lastMelds[totals[0][1]]
            if not self.__lastMeld:
                self.__lastMeld = Meld([self.lastTile])
                self.__lastMelds = [self.__lastMeld]

    def __applyBestLastMeld(self):
        """select the last meld giving the highest score (only winning variants)"""
        assert len(self.lastMelds) > 1
        totals = []
        prev = self.lastMeld
        for lastMeld in self.lastMelds:
            self.__lastMeld = lastMeld
            self.__applyRules()
            totals.append((self.won, self.__totalScore().total(), lastMeld))
        if any(x[0] for x in totals): # if any won
            totals = list(x[1:] for x in totals if x[0]) # remove lost variants
            totals = sorted(totals) # sort by totalScore
            maxScore = totals[-1][0]
            totals = list(x[1] for x in totals if x[0] == maxScore)
            # now we have a list of only lastMelds reaching maximum score
            if prev not in totals or self.__lastMeld not in totals:
                if Debug.explain and prev not in totals:
                    if not self.player or not self.player.game.belongsToRobotPlayer():
                        self.debug('replaced last meld %s with %s' % (prev, totals[0]))
                self.__lastMeld = totals[0]
                self.__applyRules()

    def __sub__(self, tiles):
        """returns a copy of self minus tiles. Case of tiles (hidden
        or exposed) is ignored. If the tile is not hidden
        but found in an exposed meld, this meld will be hidden with
        the tile removed from it. Exposed melds of length<3 will also
        be hidden."""
        # pylint: disable=R0912
        # pylint says too many branches
        if not isinstance(tiles, list):
            tiles = list([tiles])
        hidden = 'R' + ''.join(self.tileNamesInHand)
        # exposed is a deep copy of declaredMelds. If lastMeld is given, it
        # must be first in the list.
        exposed = (Meld(x) for x in self.declaredMelds)
        if self.lastMeld:
            exposed = sorted(exposed, key=lambda x: (x.pairs != self.lastMeld.pairs, meldKey(x)))
        else:
            exposed = sorted(exposed, key=meldKey)
        bonus = sorted(Meld(x) for x in self.bonusMelds)
        for tile in tiles:
            assert isinstance(tile, str) and len(tile) == 2, 'Hand.__sub__:%s' % tiles
            if tile.capitalize() in hidden:
                hidden = hidden.replace(tile.capitalize(), '', 1)
            elif tile[0] in 'fy': # bonus tile
                for idx, meld in enumerate(bonus):
                    if tile == meld.pairs[0]:
                        del bonus[idx]
                        break
            else:
                for idx, meld in enumerate(exposed):
                    if tile.lower() in meld.pairs:
                        del meld.pairs[meld.pairs.index(tile.lower())]
                        del exposed[idx]
                        meld.conceal()
                        hidden += meld.joined
                        break
        for idx, meld in enumerate(exposed):
            if len(meld.pairs) < 3:
                del exposed[idx]
                meld.conceal()
                hidden += meld.joined
        mjStr = self.mjStr
        if self.lastTile in tiles:
            parts = mjStr.split()
            newParts = []
            for idx, part in enumerate(parts):
                if part[0] == 'M':
                    part = 'm' + part[1:]
                    if len(part) > 3 and part[3] == 'k':
                        part = part[:3]
                elif part[0] == 'L':
                    continue
                newParts.append(part)
            mjStr = ' '.join(newParts)
        newString = ' '.join([hidden, meldsContent(exposed), meldsContent(bonus), mjStr])
        return Hand.cached(self, newString, self.computedRules)

    def manualRuleMayApply(self, rule):
        """returns True if rule has selectable() and applies to this hand"""
        if self.won and rule in self.ruleset.loserRules:
            return False
        if not self.won and rule in self.ruleset.winnerRules:
            return False
        return rule.selectable(self) or rule.appliesToHand(self) # needed for activated rules

    def callingHands(self, wanted=1, excludeTile=None, mustBeAvailable=False):
        """the hand is calling if it only needs one tile for mah jongg.
        Returns up to 'wanted' hands which would only need one tile.
        If mustBeAvailable is True, make sure the missing tile might still
        be available.
        """
        result = []
        string = self.string
        if ' x' in string or self.lenOffset:
            return result
        for rule in self.ruleset.mjRules:
            # sort only for reproducibility
            if not hasattr(rule, 'winningTileCandidates'):
                raise Exception('rule %s, code=%s has no winningTileCandidates' % (
                    rule.name, rule.function))
            candidates = sorted(x.capitalize() for x in rule.winningTileCandidates(self))
            for tileName in candidates:
                if excludeTile and tileName == excludeTile.capitalize():
                    continue
                if mustBeAvailable and not self.player.tileAvailable(tileName, self):
                    continue
                hand = self.picking(tileName)
                if hand.won:
                    result.append(hand)
                    if len(result) == wanted:
                        break
            if len(result) == wanted:
                break
        return result

    def __maybeMahjongg(self):
        """check if this is a mah jongg hand.
        Return a sorted list of matching MJ rules, highest
        total first"""
        if not self.won:
            return []
        if self.lenOffset != 1:
            return []
        matchingMJRules = [x for x in self.ruleset.mjRules if x.appliesToHand(self)]
        if self.robbedTile and self.robbedTile.istitle():
            # Millington 58: robbing hidden kong is only allowed for 13 orphans
            matchingMJRules = [x for x in matchingMJRules if 'mayrobhiddenkong' in x.options]
        return sorted(matchingMJRules, key=lambda x: -x.score.total())

    def splitRegex(self, rest):
        """split rest into melds as good as possible"""
        rest = ''.join(rest)
        melds = []
        for rule in self.ruleset.splitRules:
            splits = rule.apply(rest)
            while len(splits) >1:
                for split in splits[:-1]:
                    melds.append(Meld(split))
                rest = splits[-1]
                splits = rule.apply(rest)
            if len(splits) == 0:
                break
        if len(splits) == 1 :
            assert Meld(splits[0]).isValid()   # or the splitRules are wrong
        return melds

    def __recurse(self, cVariants, foundMelds, rest, maxPairs, color):
        """build the variants recursively"""
        melds = []
        for value in set(rest):
            intValue = int(value)
            if rest.count(value) == 3:
                melds.append([value] * 3)
            elif rest.count(value) == 2:
                melds.append([value] * 2)
            if rest.count(str(intValue + 1)) and rest.count(str(intValue + 2)):
                melds.append([value, str(intValue+1), str(intValue+2)])
        pairsFound = sum(len(x) == 2 for x in foundMelds)
        for meld in (m for m in melds if len(m) !=2 or pairsFound < maxPairs):
            restCopy = rest[:]
            for value in meld:
                restCopy.remove(value)
            newMelds = foundMelds[:]
            newMelds.append(meld)
            if restCopy:
                self.__recurse(cVariants, newMelds, restCopy, maxPairs, color)
            else:
                for idx, newMeld in enumerate(newMelds):
                    newMelds[idx] = ''.join(color+x for x in newMeld)
                cVariants.append(' '.join(sorted(newMelds )))

    def genVariants(self, original0, maxPairs=1):
        """generates all possible meld variants out of original
        where original is a list of tile values like ['1','1','2']"""
        color = original0[0][0]
        original = [x[1] for x in original0]
        cVariants = []
        self.__recurse(cVariants, [], original, maxPairs, color)
        gVariants = []
        for cVariant in set(cVariants):
            melds = [Meld(x) for x in cVariant.split()]
            gVariants.append(melds)
        if not gVariants:
            gVariants.append(self.splitRegex(original0)) # fallback: nothing useful found
        return gVariants

# TODO: get rid of __split, the mjRules should do that if they need it at all
# only __split at end of Hand.__init__, now we do it twice for winning hands
    def __split(self, rest):
        """work hard to always return the variant with the highest Mah Jongg value.
        Adds melds to self.melds.
        only one special mjRule may try to rearrange melds.
        A rest will be rearranged by standard rules."""
        if 'Xy' in rest:
            # hidden tiles of other players:
            self.melds.extend(self.splitRegex(rest))
            return
        arrangements = []
        for mjRule in self.ruleset.mjRules:
            func = mjRule.function
            if func.__class__.__name__ == 'StandardMahJongg':
                stdMJ = func
        if self.mjRule:
            rules = [self.mjRule]
        else:
            rules = self.ruleset.mjRules
        for mjRule in rules:
            func = mjRule.function
            if func != stdMJ and hasattr(func, 'rearrange'):
                if ((self.lenOffset == 1 and func.appliesToHand(self))
                        or (self.lenOffset < 1 and func.shouldTry(self))):
                    melds, pairs = func.rearrange(self, rest[:])
                    if melds:
                        arrangements.append((mjRule, melds, pairs))
        if arrangements:
# TODO: we should know for each arrangement how many tiles for MJ are still needed.
# If len(pairs) == 4, one or up to three might be needed. That would allow for better AI.
# TODO: if hand just completed and we did not win, only try stdmj
            arrangement = sorted(arrangements, key=lambda x: len(x[2]))[0]
            self.melds.extend(arrangement[1])
            self.melds.extend([Meld(x) for x in arrangement[2]])
            assert len(''.join(x.joined for x in self.melds)) == len(self.tileNames) * 2, '%s != %s' % (
                meldsContent(self.melds), self.tileNames)
        else:
            # stdMJ is special because it might build more than one pair
            # the other special hands would put that into the rest
            # if the above TODO is done, stdMJ does not have to be special anymore
            melds, _ = stdMJ.rearrange(self, rest[:])
            self.melds.extend(melds)
            assert len(''.join(x.joined for x in self.melds)) == len(self.tileNames) * 2, '%s != %s' % (
                meldsContent(self.melds), self.tileNames)

    def countMelds(self, key):
        """count melds having key"""
        result = 0
        if isinstance(key, str):
            for meld in self.melds:
                if meld.tileType() in key:
                    result += 1
        else:
            for meld in self.melds:
                if key(meld):
                    result += 1
        return result

    def __matchingRules(self, rules):
        """return all matching rules for this hand"""
        return list(rule for rule in rules if rule.appliesToHand(self))

    def __applyMeldRules(self):
        """apply all rules for single melds"""
        for rule in self.ruleset.meldRules:
            for meld in self.melds + self.bonusMelds:
                if rule.appliesToMeld(self, meld):
                    self.usedRules.append(UsedRule(rule, meld))

    def __applyHandRules(self):
        """apply all hand rules for both winners and losers"""
        for rule in self.ruleset.handRules:
            if rule.appliesToHand(self):
                self.usedRules.append(UsedRule(rule))

    def __totalScore(self):
        """use all used rules to compute the score"""
        pointsTotal = Score(ruleset=self.ruleset)
        maxLimit = 0.0
        maxRule = None
        for usedRule in self.usedRules:
            score = usedRule.rule.score
            if score.limits:
                # we assume that a hand never gets different limits combined
                maxLimit = max(maxLimit, score.limits)
                maxRule = usedRule
            else:
                pointsTotal += score
        if maxLimit:
            if maxLimit >= 1.0 or maxLimit * self.ruleset.limit > pointsTotal.total():
                self.usedRules =  [maxRule]
                return Score(ruleset=self.ruleset, limits=maxLimit)
        return pointsTotal

    def total(self):
        """total points of hand"""
        return self.score.total()

    def __computeLenOffset(self, tileString):
        """lenOffset is <0 for short hand, 0 for correct calling hand, >0 for long hand.
        Of course ignoring bonus tiles.
        if there are no kongs, 13 tiles will return 0"""
        result = len(self.tileNames) - 13
        for split in tileString.split():
            if split[0] != 'R':
                if Meld(split).isKong():
                    result -= 1
        return result

    @staticmethod
    def __computeDragonWindMelds(tileString):
        """returns lists with melds containing all (even single)
        dragons respective winds"""
        dragonMelds = []
        windMelds = []
        for split in tileString.split():
            if split[0] == 'R':
                pairs = Pairs(split[1:])
                for lst, tiles in ((windMelds, elements.wINDS), (dragonMelds, elements.dRAGONS)):
                    for tile in tiles:
                        count = pairs.count(tile)
                        if count:
                            lst.append(Meld([tile] * count))
            elif split[0] in 'dD':
                dragonMelds.append(Meld(split))
            elif split[0] in 'wW':
                windMelds.append(Meld(split))
        return dragonMelds, windMelds

    @staticmethod
    def __separateBonusMelds(tileString):
        """keep them separate. One meld per bonus tile. Others depend on that."""
        result = []
        if 'f' in tileString or 'y' in tileString:
            for pair in Pairs(tileString.replace(' ','').replace('R', '')):
                if pair[0] in 'fy':
                    result.append(Meld(pair))
                    tileString = tileString.replace(pair, '', 1)
        return result, tileString

    def __separateMelds(self, tileString):
        """build a meld list from the hand string"""
        # no matter how the tiles are grouped make a single
        # meld for every bonus tile
        # we need to remove spaces from the hand string first
        # for building only pairs with length 2
        splits = tileString.split()
        rest = ''
        for split in splits:
            if split[0] == 'R':
                rest = split[1:]
            else:
                meld = Meld(split)
                self.melds.append(meld)
                self.declaredMelds.append(meld)
        if rest:
            rest = sorted([rest[x:x+2] for x in range(0, len(rest), 2)])
            self.__split(rest)
        self.melds = sorted(self.melds, key=meldKey)
        for meld in self.melds:
            if not meld.isValid():
                raise Exception('%s has an invalid meld: %s' % (self.string, meld.joined))
        self.__categorizeMelds()

    def picking(self, tileName):
        """returns a new Hand built from this one plus tileName"""
        assert tileName.istitle(), 'tileName %s should be title:' % tileName
        parts = self.string.split()
        mPart = ''
        rPart = 'R%s' % tileName
        unchanged = []
        for part in parts:
            if part[0] in 'SBCDW':
                rPart += part
            elif part[0] == 'R':
                rPart += part[1:]
            elif part[0].lower() == 'm':
                mPart = part
            elif part[0] == 'L':
                pass
            else:
                unchanged.append(part)
        # combine all parts about hidden tiles plus the new one to one part
        # because something like DrDrS8S9 plus S7 will have to be reordered
        # anyway
        # set the "won" flag M
        parts = unchanged
        parts.extend([rPart, mPart.capitalize(), 'L%s' % tileName])
        return Hand.cached(self, ' '.join(parts))

    def __categorizeMelds(self):
        """categorize: hidden, declared"""
        self.hiddenMelds = []
        self.declaredMelds = []
        for meld in self.melds:
            if meld.state == CONCEALED and not meld.isKong():
                self.hiddenMelds.append(meld)
            else:
                self.declaredMelds.append(meld)

    def explain(self):
        """explain what rules were used for this hand"""
        result = [x.rule.explain() for x in self.usedRules
            if x.rule.score.points]
        result.extend([x.rule.explain() for x in self.usedRules
            if x.rule.score.doubles])
        result.extend([x.rule.explain() for x in self.usedRules
            if not x.rule.score.points and not x.rule.score.doubles])
        if any(x.rule.debug for x in self.usedRules):
            result.append(str(self))
        return result

    def doublesEstimate(self):
        """this is only an estimate because it only uses meldRules and handRules,
        but not things like mjRules, winnerRules, loserRules"""
        result = 0
        for meld in self.dragonMelds + self.windMelds:
            for rule in self.ruleset.doublingMeldRules:
                if rule.appliesToMeld(self, meld):
                    result += rule.score.doubles
        for rule in self.ruleset.doublingHandRules:
            if rule.appliesToHand(self):
                result += rule.score.doubles
        return result

    def __str__(self):
        """hand as a string"""
        return u' '.join([self.sortedMeldsContent, self.mjStr])

    def __repr__(self):
        """the default representation"""
        return 'Hand(%s)' % str(self)
Exemple #6
0
    def __init__(self, ruleset, string, computedRules=None, robbedTile=None):
        """evaluate string using ruleset. rules are to be applied in any case.
        ruleset can be Hand, Game or Ruleset."""
        # silence pylint. This method is time critical, so do not split it into smaller methods
        # pylint: disable=R0902,R0914,R0912,R0915
        if isinstance(ruleset, Hand):
            self.ruleset = ruleset.ruleset
            self.player = ruleset.player
            self.computedRules = ruleset.computedRules
        elif isinstance(ruleset, Ruleset):
            self.ruleset = ruleset
            self.player = None
        else:
            self.player = ruleset
            self.ruleset = self.player.game.ruleset
        self.string = string
        self.robbedTile = robbedTile
        if computedRules is not None and not isinstance(computedRules, list):
            computedRules = list([computedRules])
        self.computedRules = computedRules or []
        self.__won = False
        self.mjStr = ''
        self.mjRule = None
        self.ownWind = None
        self.roundWind = None
        tileStrings = []
        mjStrings = []
        haveM = False
        splits = self.string.split()
        for part in splits:
            partId = part[0]
            if partId in 'Mmx':
                haveM = True
                self.ownWind = part[1]
                self.roundWind = part[2]
                mjStrings.append(part)
                self.__won = partId == 'M'
            elif partId == 'L':
                if len(part[1:]) > 8:
                    raise Exception('last tile cannot complete a kang:' +
                                    self.string)
                mjStrings.append(part)
            else:
                tileStrings.append(part)

        if not haveM:
            raise Exception('Hand got string without mMx: %s', self.string)
        self.mjStr = ' '.join(mjStrings)
        self.__lastTile = self.__lastSource = self.__announcements = ''
        self.__lastMeld = 0
        self.__lastMelds = []
        self.hiddenMelds = []
        self.declaredMelds = []
        self.melds = []
        tileString = ' '.join(tileStrings)
        self.bonusMelds, tileString = self.__separateBonusMelds(tileString)
        self.tileNames = Pairs(tileString.replace(' ', '').replace('R', ''))
        self.tileNames.sort()
        self.values = ''.join(x[1] for x in self.tileNames)
        self.suits = set(x[0].lower() for x in self.tileNames)
        self.lenOffset = self.__computeLenOffset(tileString)
        self.dragonMelds, self.windMelds = self.__computeDragonWindMelds(
            tileString)
        self.__separateMelds(tileString)
        self.hiddenMelds = sorted(self.hiddenMelds, key=meldKey)
        self.tileNamesInHand = sum((x.pairs for x in self.hiddenMelds), [])
        self.sortedMeldsContent = meldsContent(self.melds)
        if self.bonusMelds:
            self.sortedMeldsContent += ' ' + meldsContent(self.bonusMelds)

        self.usedRules = []
        self.score = None
        oldWon = self.won
        self.__applyRules()
        if len(self.lastMelds) > 1:
            self.__applyBestLastMeld()
        if self.won != oldWon:
            # if not won after all, this might be a long hand.
            # So we might even have to unapply meld rules and
            # bonus points. Instead just recompute all again.
            # This should only happen with scoring manual games
            # and with scoringtest - normally kajongg would not
            # let you declare an invalid mah jongg
            self.__applyRules()
Exemple #7
0
class Hand(object):
    """represent the hand to be evaluated"""

    # pylint: disable=R0902
    # pylint we need more than 10 instance attributes

    cache = dict()
    misses = 0
    hits = 0

    @staticmethod
    def clearCache(game):
        """clears the cache with Hands"""
        if Debug.handCache and Hand.cache:
            game.debug('cache hits:%d misses:%d' % (Hand.hits, Hand.misses))
        Hand.cache.clear()
        Hand.hits = 0
        Hand.misses = 0

    @staticmethod
    def cached(ruleset, string, computedRules=None, robbedTile=None):
        """since a Hand instance is never changed, we can use a cache"""
        if computedRules is not None and not isinstance(computedRules, list):
            computedRules = list([computedRules])
        cRuleHash = '&&'.join([rule.name for rule in computedRules
                               ]) if computedRules else 'None'
        if isinstance(ruleset, Hand):
            cacheId = id(ruleset.player or ruleset.ruleset)
        else:
            cacheId = id(ruleset)
        cacheKey = hash((cacheId, string, robbedTile, cRuleHash))
        cache = Hand.cache
        if cacheKey in cache:
            if cache[cacheKey] is None:
                raise Exception(
                    'recursion: Hand calls itself for same content')
            Hand.hits += 1
            return cache[cacheKey]
        Hand.misses += 1
        cache[cacheKey] = None
        result = Hand(ruleset,
                      string,
                      computedRules=computedRules,
                      robbedTile=robbedTile)
        cache[cacheKey] = result
        return result

    def __init__(self, ruleset, string, computedRules=None, robbedTile=None):
        """evaluate string using ruleset. rules are to be applied in any case.
        ruleset can be Hand, Game or Ruleset."""
        # silence pylint. This method is time critical, so do not split it into smaller methods
        # pylint: disable=R0902,R0914,R0912,R0915
        if isinstance(ruleset, Hand):
            self.ruleset = ruleset.ruleset
            self.player = ruleset.player
            self.computedRules = ruleset.computedRules
        elif isinstance(ruleset, Ruleset):
            self.ruleset = ruleset
            self.player = None
        else:
            self.player = ruleset
            self.ruleset = self.player.game.ruleset
        self.string = string
        self.robbedTile = robbedTile
        if computedRules is not None and not isinstance(computedRules, list):
            computedRules = list([computedRules])
        self.computedRules = computedRules or []
        self.__won = False
        self.mjStr = ''
        self.mjRule = None
        self.ownWind = None
        self.roundWind = None
        tileStrings = []
        mjStrings = []
        haveM = False
        splits = self.string.split()
        for part in splits:
            partId = part[0]
            if partId in 'Mmx':
                haveM = True
                self.ownWind = part[1]
                self.roundWind = part[2]
                mjStrings.append(part)
                self.__won = partId == 'M'
            elif partId == 'L':
                if len(part[1:]) > 8:
                    raise Exception('last tile cannot complete a kang:' +
                                    self.string)
                mjStrings.append(part)
            else:
                tileStrings.append(part)

        if not haveM:
            raise Exception('Hand got string without mMx: %s', self.string)
        self.mjStr = ' '.join(mjStrings)
        self.__lastTile = self.__lastSource = self.__announcements = ''
        self.__lastMeld = 0
        self.__lastMelds = []
        self.hiddenMelds = []
        self.declaredMelds = []
        self.melds = []
        tileString = ' '.join(tileStrings)
        self.bonusMelds, tileString = self.__separateBonusMelds(tileString)
        self.tileNames = Pairs(tileString.replace(' ', '').replace('R', ''))
        self.tileNames.sort()
        self.values = ''.join(x[1] for x in self.tileNames)
        self.suits = set(x[0].lower() for x in self.tileNames)
        self.lenOffset = self.__computeLenOffset(tileString)
        self.dragonMelds, self.windMelds = self.__computeDragonWindMelds(
            tileString)
        self.__separateMelds(tileString)
        self.hiddenMelds = sorted(self.hiddenMelds, key=meldKey)
        self.tileNamesInHand = sum((x.pairs for x in self.hiddenMelds), [])
        self.sortedMeldsContent = meldsContent(self.melds)
        if self.bonusMelds:
            self.sortedMeldsContent += ' ' + meldsContent(self.bonusMelds)

        self.usedRules = []
        self.score = None
        oldWon = self.won
        self.__applyRules()
        if len(self.lastMelds) > 1:
            self.__applyBestLastMeld()
        if self.won != oldWon:
            # if not won after all, this might be a long hand.
            # So we might even have to unapply meld rules and
            # bonus points. Instead just recompute all again.
            # This should only happen with scoring manual games
            # and with scoringtest - normally kajongg would not
            # let you declare an invalid mah jongg
            self.__applyRules()

    @property
    def lastTile(self):
        """compute and cache, readonly"""
        if self.__lastTile == '':
            self.__setLastTile()
        return self.__lastTile

    @property
    def lastSource(self):
        """compute and cache, readonly"""
        if self.__lastTile == '':
            self.__setLastTile()
        return self.__lastSource

    @property
    def announcements(self):
        """compute and cache, readonly"""
        if self.__lastTile == '':
            self.__setLastTile()
        return self.__announcements

    @property
    def lastMeld(self):
        """compute and cache, readonly"""
        if self.__lastMeld == 0:
            self.__setLastMeld()
        return self.__lastMeld

    @property
    def lastMelds(self):
        """compute and cache, readonly"""
        if self.__lastMeld == 0:
            self.__setLastMeld()
        return self.__lastMelds

    @property
    def won(self):
        """have we been modified since load or last save?
        The "won" value is set to True when instantiating the hand,
        according to the mMx in the init string. Later on, it may
        only be cleared."""
        return self.__won

    @won.setter
    def won(self, value):
        """must never change to True"""
        value = bool(value)
        assert not value
        self.__won = value
        self.string = self.string.replace(' M', ' m')
        self.mjStr = self.mjStr.replace(' M', ' m')

    def debug(self, msg, btIndent=None):
        """try to use Game.debug so we get a nice prefix"""
        if self.player:
            self.player.game.debug(msg, btIndent=btIndent)
        else:
            logDebug(msg, btIndent=btIndent)

    def __applyRules(self):
        """find out which rules apply, collect in self.usedRules.
        This may change self.won"""
        self.usedRules = list([UsedRule(rule) for rule in self.computedRules])
        if self.__hasExclusiveRules():
            return
        self.__applyMeldRules()
        self.__applyHandRules()
        if self.__hasExclusiveRules():
            return
        self.score = self.__totalScore()

        # do the rest only if we know all tiles of the hand
        if 'Xy' in self.string:
            self.won = False  # we do not know better
            return
        if self.won:
            matchingMJRules = self.__maybeMahjongg()
            if not matchingMJRules:
                self.won = False
                self.score = self.__totalScore()
                return
            self.mjRule = matchingMJRules[0]
            self.usedRules.append(UsedRule(self.mjRule))
            if self.__hasExclusiveRules():
                return
            self.usedRules.extend(self.matchingWinnerRules())
            self.score = self.__totalScore()
        else:  # not self.won
            assert self.mjRule is None
            loserRules = self.__matchingRules(self.ruleset.loserRules)
            if loserRules:
                self.usedRules.extend(list(UsedRule(x) for x in loserRules))
                self.score = self.__totalScore()

    def matchingWinnerRules(self):
        """returns a list of matching winner rules"""
        matching = self.__matchingRules(self.ruleset.winnerRules)
        for rule in matching:
            if (self.ruleset.limit
                    and rule.score.limits >= 1) or 'absolute' in rule.options:
                return [UsedRule(rule)]
        return list(UsedRule(x) for x in matching)

    def __hasExclusiveRules(self):
        """if we have one, remove all others"""
        exclusive = list(x for x in self.usedRules
                         if 'absolute' in x.rule.options)
        if exclusive:
            self.usedRules = exclusive
            self.score = self.__totalScore()
            self.won = self.__maybeMahjongg()
        return bool(exclusive)

    def __setLastTile(self):
        """sets lastTile, lastSource, announcements"""
        self.__announcements = ''
        self.__lastTile = None
        self.__lastSource = None
        parts = self.mjStr.split()
        for part in parts:
            if part[0] == 'L':
                part = part[1:]
                if len(part) > 2:
                    self.__lastMeld = Meld(part[2:])
                self.__lastTile = part[:2]
            elif part[0] == 'M':
                if len(part) > 3:
                    self.__lastSource = part[3]
                    if len(part) > 4:
                        self.__announcements = part[4:]
        if self.__lastTile:
            assert self.__lastTile in self.tileNames, 'lastTile %s is not in tiles %s, mjStr=%s' % (
                self.__lastTile, ' '.join(self.tileNames), self.mjStr)
            if self.__lastSource == 'k':
                assert self.tileNames.count(self.__lastTile.lower()) + \
                    self.tileNames.count(self.__lastTile.capitalize()) == 1, \
                    'Robbing kong: I cannot have lastTile %s more than once in %s' % (
                    self.__lastTile, ' '.join(self.tileNames))

    def __setLastMeld(self):
        """sets the shortest possible last meld. This is
        not yet the final choice, see __applyBestLastMeld"""
        self.__lastMeld = None
        if self.lastTile and self.won:
            if hasattr(self.mjRule.function, 'computeLastMelds'):
                self.__lastMelds = self.mjRule.function.computeLastMelds(self)
                if self.__lastMelds:
                    # syncHandBoard may return nothing
                    if len(self.__lastMelds) == 1:
                        self.__lastMeld = self.__lastMelds[0]
                    else:
                        totals = sorted(
                            (len(x), idx)
                            for idx, x in enumerate(self.__lastMelds))
                        self.__lastMeld = self.__lastMelds[totals[0][1]]
            if not self.__lastMeld:
                self.__lastMeld = Meld([self.lastTile])
                self.__lastMelds = [self.__lastMeld]

    def __applyBestLastMeld(self):
        """select the last meld giving the highest score (only winning variants)"""
        assert len(self.lastMelds) > 1
        totals = []
        prev = self.lastMeld
        for lastMeld in self.lastMelds:
            self.__lastMeld = lastMeld
            self.__applyRules()
            totals.append((self.won, self.__totalScore().total(), lastMeld))
        if any(x[0] for x in totals):  # if any won
            totals = list(x[1:] for x in totals
                          if x[0])  # remove lost variants
            totals = sorted(totals)  # sort by totalScore
            maxScore = totals[-1][0]
            totals = list(x[1] for x in totals if x[0] == maxScore)
            # now we have a list of only lastMelds reaching maximum score
            if prev not in totals or self.__lastMeld not in totals:
                if Debug.explain and prev not in totals:
                    if not self.player or not self.player.game.belongsToRobotPlayer(
                    ):
                        self.debug('replaced last meld %s with %s' %
                                   (prev, totals[0]))
                self.__lastMeld = totals[0]
                self.__applyRules()

    def __sub__(self, tiles):
        """returns a copy of self minus tiles. Case of tiles (hidden
        or exposed) is ignored. If the tile is not hidden
        but found in an exposed meld, this meld will be hidden with
        the tile removed from it. Exposed melds of length<3 will also
        be hidden."""
        # pylint: disable=R0912
        # pylint says too many branches
        if not isinstance(tiles, list):
            tiles = list([tiles])
        hidden = 'R' + ''.join(self.tileNamesInHand)
        # exposed is a deep copy of declaredMelds. If lastMeld is given, it
        # must be first in the list.
        exposed = (Meld(x) for x in self.declaredMelds)
        if self.lastMeld:
            exposed = sorted(exposed,
                             key=lambda x:
                             (x.pairs != self.lastMeld.pairs, meldKey(x)))
        else:
            exposed = sorted(exposed, key=meldKey)
        bonus = sorted(Meld(x) for x in self.bonusMelds)
        for tile in tiles:
            assert isinstance(
                tile, str) and len(tile) == 2, 'Hand.__sub__:%s' % tiles
            if tile.capitalize() in hidden:
                hidden = hidden.replace(tile.capitalize(), '', 1)
            elif tile[0] in 'fy':  # bonus tile
                for idx, meld in enumerate(bonus):
                    if tile == meld.pairs[0]:
                        del bonus[idx]
                        break
            else:
                for idx, meld in enumerate(exposed):
                    if tile.lower() in meld.pairs:
                        del meld.pairs[meld.pairs.index(tile.lower())]
                        del exposed[idx]
                        meld.conceal()
                        hidden += meld.joined
                        break
        for idx, meld in enumerate(exposed):
            if len(meld.pairs) < 3:
                del exposed[idx]
                meld.conceal()
                hidden += meld.joined
        mjStr = self.mjStr
        if self.lastTile in tiles:
            parts = mjStr.split()
            newParts = []
            for idx, part in enumerate(parts):
                if part[0] == 'M':
                    part = 'm' + part[1:]
                    if len(part) > 3 and part[3] == 'k':
                        part = part[:3]
                elif part[0] == 'L':
                    continue
                newParts.append(part)
            mjStr = ' '.join(newParts)
        newString = ' '.join(
            [hidden, meldsContent(exposed),
             meldsContent(bonus), mjStr])
        return Hand.cached(self, newString, self.computedRules)

    def manualRuleMayApply(self, rule):
        """returns True if rule has selectable() and applies to this hand"""
        if self.won and rule in self.ruleset.loserRules:
            return False
        if not self.won and rule in self.ruleset.winnerRules:
            return False
        return rule.selectable(self) or rule.appliesToHand(
            self)  # needed for activated rules

    def callingHands(self, wanted=1, excludeTile=None, mustBeAvailable=False):
        """the hand is calling if it only needs one tile for mah jongg.
        Returns up to 'wanted' hands which would only need one tile.
        If mustBeAvailable is True, make sure the missing tile might still
        be available.
        """
        result = []
        string = self.string
        if ' x' in string or self.lenOffset:
            return result
        for rule in self.ruleset.mjRules:
            # sort only for reproducibility
            if not hasattr(rule, 'winningTileCandidates'):
                raise Exception(
                    'rule %s, code=%s has no winningTileCandidates' %
                    (rule.name, rule.function))
            candidates = sorted(x.capitalize()
                                for x in rule.winningTileCandidates(self))
            for tileName in candidates:
                if excludeTile and tileName == excludeTile.capitalize():
                    continue
                if mustBeAvailable and not self.player.tileAvailable(
                        tileName, self):
                    continue
                hand = self.picking(tileName)
                if hand.won:
                    result.append(hand)
                    if len(result) == wanted:
                        break
            if len(result) == wanted:
                break
        return result

    def __maybeMahjongg(self):
        """check if this is a mah jongg hand.
        Return a sorted list of matching MJ rules, highest
        total first"""
        if not self.won:
            return []
        if self.lenOffset != 1:
            return []
        matchingMJRules = [
            x for x in self.ruleset.mjRules if x.appliesToHand(self)
        ]
        if self.robbedTile and self.robbedTile.istitle():
            # Millington 58: robbing hidden kong is only allowed for 13 orphans
            matchingMJRules = [
                x for x in matchingMJRules if 'mayrobhiddenkong' in x.options
            ]
        return sorted(matchingMJRules, key=lambda x: -x.score.total())

    def splitRegex(self, rest):
        """split rest into melds as good as possible"""
        rest = ''.join(rest)
        melds = []
        for rule in self.ruleset.splitRules:
            splits = rule.apply(rest)
            while len(splits) > 1:
                for split in splits[:-1]:
                    melds.append(Meld(split))
                rest = splits[-1]
                splits = rule.apply(rest)
            if len(splits) == 0:
                break
        if len(splits) == 1:
            assert Meld(splits[0]).isValid()  # or the splitRules are wrong
        return melds

    def __recurse(self, cVariants, foundMelds, rest, maxPairs, color):
        """build the variants recursively"""
        melds = []
        for value in set(rest):
            intValue = int(value)
            if rest.count(value) == 3:
                melds.append([value] * 3)
            elif rest.count(value) == 2:
                melds.append([value] * 2)
            if rest.count(str(intValue + 1)) and rest.count(str(intValue + 2)):
                melds.append([value, str(intValue + 1), str(intValue + 2)])
        pairsFound = sum(len(x) == 2 for x in foundMelds)
        for meld in (m for m in melds if len(m) != 2 or pairsFound < maxPairs):
            restCopy = rest[:]
            for value in meld:
                restCopy.remove(value)
            newMelds = foundMelds[:]
            newMelds.append(meld)
            if restCopy:
                self.__recurse(cVariants, newMelds, restCopy, maxPairs, color)
            else:
                for idx, newMeld in enumerate(newMelds):
                    newMelds[idx] = ''.join(color + x for x in newMeld)
                cVariants.append(' '.join(sorted(newMelds)))

    def genVariants(self, original0, maxPairs=1):
        """generates all possible meld variants out of original
        where original is a list of tile values like ['1','1','2']"""
        color = original0[0][0]
        original = [x[1] for x in original0]
        cVariants = []
        self.__recurse(cVariants, [], original, maxPairs, color)
        gVariants = []
        for cVariant in set(cVariants):
            melds = [Meld(x) for x in cVariant.split()]
            gVariants.append(melds)
        if not gVariants:
            gVariants.append(
                self.splitRegex(original0))  # fallback: nothing useful found
        return gVariants

# TODO: get rid of __split, the mjRules should do that if they need it at all
# only __split at end of Hand.__init__, now we do it twice for winning hands

    def __split(self, rest):
        """work hard to always return the variant with the highest Mah Jongg value.
        Adds melds to self.melds.
        only one special mjRule may try to rearrange melds.
        A rest will be rearranged by standard rules."""
        if 'Xy' in rest:
            # hidden tiles of other players:
            self.melds.extend(self.splitRegex(rest))
            return
        arrangements = []
        for mjRule in self.ruleset.mjRules:
            func = mjRule.function
            if func.__class__.__name__ == 'StandardMahJongg':
                stdMJ = func
        if self.mjRule:
            rules = [self.mjRule]
        else:
            rules = self.ruleset.mjRules
        for mjRule in rules:
            func = mjRule.function
            if func != stdMJ and hasattr(func, 'rearrange'):
                if ((self.lenOffset == 1 and func.appliesToHand(self))
                        or (self.lenOffset < 1 and func.shouldTry(self))):
                    melds, pairs = func.rearrange(self, rest[:])
                    if melds:
                        arrangements.append((mjRule, melds, pairs))
        if arrangements:
            # TODO: we should know for each arrangement how many tiles for MJ are still needed.
            # If len(pairs) == 4, one or up to three might be needed. That would allow for better AI.
            # TODO: if hand just completed and we did not win, only try stdmj
            arrangement = sorted(arrangements, key=lambda x: len(x[2]))[0]
            self.melds.extend(arrangement[1])
            self.melds.extend([Meld(x) for x in arrangement[2]])
            assert len(''.join(x.joined for x in self.melds)) == len(
                self.tileNames) * 2, '%s != %s' % (meldsContent(
                    self.melds), self.tileNames)
        else:
            # stdMJ is special because it might build more than one pair
            # the other special hands would put that into the rest
            # if the above TODO is done, stdMJ does not have to be special anymore
            melds, _ = stdMJ.rearrange(self, rest[:])
            self.melds.extend(melds)
            assert len(''.join(x.joined for x in self.melds)) == len(
                self.tileNames) * 2, '%s != %s' % (meldsContent(
                    self.melds), self.tileNames)

    def countMelds(self, key):
        """count melds having key"""
        result = 0
        if isinstance(key, str):
            for meld in self.melds:
                if meld.tileType() in key:
                    result += 1
        else:
            for meld in self.melds:
                if key(meld):
                    result += 1
        return result

    def __matchingRules(self, rules):
        """return all matching rules for this hand"""
        return list(rule for rule in rules if rule.appliesToHand(self))

    def __applyMeldRules(self):
        """apply all rules for single melds"""
        for rule in self.ruleset.meldRules:
            for meld in self.melds + self.bonusMelds:
                if rule.appliesToMeld(self, meld):
                    self.usedRules.append(UsedRule(rule, meld))

    def __applyHandRules(self):
        """apply all hand rules for both winners and losers"""
        for rule in self.ruleset.handRules:
            if rule.appliesToHand(self):
                self.usedRules.append(UsedRule(rule))

    def __totalScore(self):
        """use all used rules to compute the score"""
        pointsTotal = Score(ruleset=self.ruleset)
        maxLimit = 0.0
        maxRule = None
        for usedRule in self.usedRules:
            score = usedRule.rule.score
            if score.limits:
                # we assume that a hand never gets different limits combined
                maxLimit = max(maxLimit, score.limits)
                maxRule = usedRule
            else:
                pointsTotal += score
        if maxLimit:
            if maxLimit >= 1.0 or maxLimit * self.ruleset.limit > pointsTotal.total(
            ):
                self.usedRules = [maxRule]
                return Score(ruleset=self.ruleset, limits=maxLimit)
        return pointsTotal

    def total(self):
        """total points of hand"""
        return self.score.total()

    def __computeLenOffset(self, tileString):
        """lenOffset is <0 for short hand, 0 for correct calling hand, >0 for long hand.
        Of course ignoring bonus tiles.
        if there are no kongs, 13 tiles will return 0"""
        result = len(self.tileNames) - 13
        for split in tileString.split():
            if split[0] != 'R':
                if Meld(split).isKong():
                    result -= 1
        return result

    @staticmethod
    def __computeDragonWindMelds(tileString):
        """returns lists with melds containing all (even single)
        dragons respective winds"""
        dragonMelds = []
        windMelds = []
        for split in tileString.split():
            if split[0] == 'R':
                pairs = Pairs(split[1:])
                for lst, tiles in ((windMelds, elements.wINDS),
                                   (dragonMelds, elements.dRAGONS)):
                    for tile in tiles:
                        count = pairs.count(tile)
                        if count:
                            lst.append(Meld([tile] * count))
            elif split[0] in 'dD':
                dragonMelds.append(Meld(split))
            elif split[0] in 'wW':
                windMelds.append(Meld(split))
        return dragonMelds, windMelds

    @staticmethod
    def __separateBonusMelds(tileString):
        """keep them separate. One meld per bonus tile. Others depend on that."""
        result = []
        if 'f' in tileString or 'y' in tileString:
            for pair in Pairs(tileString.replace(' ', '').replace('R', '')):
                if pair[0] in 'fy':
                    result.append(Meld(pair))
                    tileString = tileString.replace(pair, '', 1)
        return result, tileString

    def __separateMelds(self, tileString):
        """build a meld list from the hand string"""
        # no matter how the tiles are grouped make a single
        # meld for every bonus tile
        # we need to remove spaces from the hand string first
        # for building only pairs with length 2
        splits = tileString.split()
        rest = ''
        for split in splits:
            if split[0] == 'R':
                rest = split[1:]
            else:
                meld = Meld(split)
                self.melds.append(meld)
                self.declaredMelds.append(meld)
        if rest:
            rest = sorted([rest[x:x + 2] for x in range(0, len(rest), 2)])
            self.__split(rest)
        self.melds = sorted(self.melds, key=meldKey)
        for meld in self.melds:
            if not meld.isValid():
                raise Exception('%s has an invalid meld: %s' %
                                (self.string, meld.joined))
        self.__categorizeMelds()

    def picking(self, tileName):
        """returns a new Hand built from this one plus tileName"""
        assert tileName.istitle(), 'tileName %s should be title:' % tileName
        parts = self.string.split()
        mPart = ''
        rPart = 'R%s' % tileName
        unchanged = []
        for part in parts:
            if part[0] in 'SBCDW':
                rPart += part
            elif part[0] == 'R':
                rPart += part[1:]
            elif part[0].lower() == 'm':
                mPart = part
            elif part[0] == 'L':
                pass
            else:
                unchanged.append(part)
        # combine all parts about hidden tiles plus the new one to one part
        # because something like DrDrS8S9 plus S7 will have to be reordered
        # anyway
        # set the "won" flag M
        parts = unchanged
        parts.extend([rPart, mPart.capitalize(), 'L%s' % tileName])
        return Hand.cached(self, ' '.join(parts))

    def __categorizeMelds(self):
        """categorize: hidden, declared"""
        self.hiddenMelds = []
        self.declaredMelds = []
        for meld in self.melds:
            if meld.state == CONCEALED and not meld.isKong():
                self.hiddenMelds.append(meld)
            else:
                self.declaredMelds.append(meld)

    def explain(self):
        """explain what rules were used for this hand"""
        result = [
            x.rule.explain() for x in self.usedRules if x.rule.score.points
        ]
        result.extend(
            [x.rule.explain() for x in self.usedRules if x.rule.score.doubles])
        result.extend([
            x.rule.explain() for x in self.usedRules
            if not x.rule.score.points and not x.rule.score.doubles
        ])
        if any(x.rule.debug for x in self.usedRules):
            result.append(str(self))
        return result

    def doublesEstimate(self):
        """this is only an estimate because it only uses meldRules and handRules,
        but not things like mjRules, winnerRules, loserRules"""
        result = 0
        for meld in self.dragonMelds + self.windMelds:
            for rule in self.ruleset.doublingMeldRules:
                if rule.appliesToMeld(self, meld):
                    result += rule.score.doubles
        for rule in self.ruleset.doublingHandRules:
            if rule.appliesToHand(self):
                result += rule.score.doubles
        return result

    def __str__(self):
        """hand as a string"""
        return u' '.join([self.sortedMeldsContent, self.mjStr])

    def __repr__(self):
        """the default representation"""
        return 'Hand(%s)' % str(self)