File indexing completed on 2024-04-21 04:01:53

0001 #pylint: disable=too-many-lines
0002 # -*- coding: utf-8 -*-
0003 
0004 """Copyright (C) 2009-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0005 
0006 SPDX-License-Identifier: GPL-2.0
0007 
0008 """
0009 
0010 from tile import Tile, elements
0011 from tilesource import TileSource
0012 from meld import Meld, MeldList
0013 from common import IntDict
0014 from wind import East
0015 from message import Message
0016 from query import Query
0017 from permutations import Permutations
0018 
0019 
0020 class RuleCode:
0021 
0022     """Parent for all RuleCode classes. A RuleCode class can be used to
0023     define the behaviour of a Rule. Classes Rule and RuleCode
0024     are separate because
0025     - different rulesets may have a Rule with the same name
0026       but with different behaviour
0027     - different rulesets may use different names for the same rule
0028     - the RuleCode class should be as short and as concise
0029       as possible because this is the important part about
0030       implementing a new ruleset, and it is the most error prone.
0031 
0032     All methods in RuleCode classes will automatically be converted
0033     into staticmethods or classmethods if the 1st arg is named 'cls'.
0034 
0035     winningTileCandidates(cls, hand):
0036         All rules for going MahJongg must have such a method.
0037         This is used to find all winning hands which only need
0038         one tile: The calling hands (after calling)
0039 
0040     """
0041 
0042     cache = ()
0043 
0044 
0045 # pylint: disable=missing-docstring
0046 # the class and method names are mostly self explaining, we do not
0047 # need docstringss
0048 
0049 # pylint: disable=no-self-argument, no-self-use, no-value-for-parameter, no-member
0050 # pylint: disable=too-many-function-args, unused-argument, arguments-differ
0051 
0052 class MJRule(RuleCode):
0053 
0054     def computeLastMelds(hand):
0055         """return all possible last melds"""
0056 
0057 
0058 class DragonPungKong(RuleCode):
0059 
0060     def appliesToMeld(hand, meld):
0061         return meld.isPungKong and meld.isDragonMeld
0062 
0063 
0064 class ExposedMinorPung(RuleCode):
0065 
0066     def appliesToMeld(hand, meld):
0067         return meld.isPung and meld[0].isMinor and meld.isExposed
0068 
0069 
0070 class ExposedTerminalsPung(RuleCode):
0071 
0072     def appliesToMeld(hand, meld):
0073         return meld.isExposed and meld[0].isTerminal and meld.isPung
0074 
0075 
0076 class ExposedHonorsPung(RuleCode):
0077 
0078     def appliesToMeld(hand, meld):
0079         return meld.isExposed and meld.isHonorMeld and meld.isPung
0080 
0081 
0082 class ExposedMinorKong(RuleCode):
0083 
0084     def appliesToMeld(hand, meld):
0085         return meld.isExposed and meld[0].isMinor and meld.isKong
0086 
0087 
0088 class ExposedTerminalsKong(RuleCode):
0089 
0090     def appliesToMeld(hand, meld):
0091         return meld.isExposed and meld[0].isTerminal and meld.isKong
0092 
0093 
0094 class ExposedHonorsKong(RuleCode):
0095 
0096     def appliesToMeld(hand, meld):
0097         return meld.isExposed and meld.isHonorMeld and meld.isKong
0098 
0099 
0100 class ConcealedMinorPung(RuleCode):
0101 
0102     def appliesToMeld(hand, meld):
0103         return meld.isConcealed and meld[0].isMinor and meld.isPung
0104 
0105 
0106 class ConcealedTerminalsPung(RuleCode):
0107 
0108     def appliesToMeld(hand, meld):
0109         return meld.isConcealed and meld[0].isTerminal and meld.isPung
0110 
0111 
0112 class ConcealedHonorsPung(RuleCode):
0113 
0114     def appliesToMeld(hand, meld):
0115         return meld.isConcealed and meld.isHonorMeld and meld.isPung
0116 
0117 
0118 class ConcealedMinorKong(RuleCode):
0119 
0120     def appliesToMeld(hand, meld):
0121         return meld.isConcealed and meld[0].isMinor and meld.isKong
0122 
0123 
0124 class ConcealedTerminalsKong(RuleCode):
0125 
0126     def appliesToMeld(hand, meld):
0127         return meld.isConcealed and meld[0].isTerminal and meld.isKong
0128 
0129 
0130 class ConcealedHonorsKong(RuleCode):
0131 
0132     def appliesToMeld(hand, meld):
0133         return meld.isConcealed and meld.isHonorMeld and meld.isKong
0134 
0135 
0136 class OwnWindPungKong(RuleCode):
0137 
0138     def appliesToMeld(hand, meld):
0139         return meld[0].value is hand.ownWind
0140 
0141     def mayApplyToMeld(meld):
0142         """for meld rules which depend on context like hand.ownWind, we want
0143         to know if there could be a context where this rule applies. See
0144         Meld.rules.
0145         NOTE: If a rulecode class has mayApplyToMeld, its appliesToMeld can
0146         assume that mayApplyToMeld has already been checked."""
0147         return meld.isPungKong and meld.isWindMeld
0148 
0149 
0150 class OwnWindPair(RuleCode):
0151 
0152     def appliesToMeld(hand, meld):
0153         return meld[0].value is hand.ownWind
0154 
0155     def mayApplyToMeld(meld):
0156         return meld.isPair and meld.isWindMeld
0157 
0158 
0159 class RoundWindPungKong(RuleCode):
0160 
0161     def appliesToMeld(hand, meld):
0162         return meld[0].value is hand.roundWind
0163 
0164     def mayApplyToMeld(meld):
0165         return meld.isPungKong and meld.isWindMeld
0166 
0167 
0168 class RoundWindPair(RuleCode):
0169 
0170     def appliesToMeld(hand, meld):
0171         return meld[0].value is hand.roundWind
0172 
0173     def mayApplyToMeld(meld):
0174         return meld.isPair and meld.isWindMeld
0175 
0176 
0177 class DragonPair(RuleCode):
0178 
0179     def appliesToMeld(hand, meld):
0180         return meld.isDragonMeld and meld.isPair
0181 
0182 
0183 class LastTileCompletesPairMinor(RuleCode):
0184 
0185     def appliesToHand(hand):
0186         return hand.lastMeld and hand.lastMeld.isPair and hand.lastTile.isMinor
0187 
0188 
0189 class Flower(RuleCode):
0190 
0191     def appliesToMeld(hand, meld):
0192         return meld.isSingle and meld.group == Tile.flower
0193 
0194 
0195 class Season(RuleCode):
0196 
0197     def appliesToMeld(hand, meld):
0198         return meld.isSingle and meld.group == Tile.season
0199 
0200 
0201 class LastTileCompletesPairMajor(RuleCode):
0202 
0203     def appliesToHand(hand):
0204         return hand.lastMeld and hand.lastMeld.isPair and hand.lastTile.isMajor
0205 
0206 
0207 class LastFromWall(RuleCode):
0208 
0209     def appliesToHand(hand):
0210         return hand.lastTile and hand.lastTile.isConcealed
0211 
0212 
0213 class ZeroPointHand(RuleCode):
0214 
0215     def appliesToHand(hand):
0216         return not any(x.meld for x in hand.usedRules if x.meld and len(x.meld) > 1)
0217 
0218 
0219 class NoChow(RuleCode):
0220 
0221     def appliesToHand(hand):
0222         return not any(x.isChow for x in hand.melds)
0223 
0224 
0225 class OnlyConcealedMelds(RuleCode):
0226 
0227     def appliesToHand(hand):
0228         return not any((x.isExposed and not x.isClaimedKong) for x in hand.melds)
0229 
0230 
0231 class FalseColorGame(RuleCode):
0232 
0233     def appliesToHand(hand):
0234         dwSet = set(Tile.honors)
0235         return dwSet & hand.suits and len(hand.suits - dwSet) == 1
0236 
0237 
0238 class TrueColorGame(RuleCode):
0239 
0240     def appliesToHand(hand):
0241         return len(hand.suits) == 1 and hand.suits < set(Tile.colors)
0242 
0243 
0244 class Purity(RuleCode):
0245 
0246     def appliesToHand(hand):
0247         return (len(hand.suits) == 1 and hand.suits < set(Tile.colors)
0248                 and not any(x.isChow for x in hand.melds))
0249 
0250 
0251 class ConcealedTrueColorGame(RuleCode):
0252 
0253     def appliesToHand(hand):
0254         if len(hand.suits) != 1 or hand.suits >= set(Tile.colors):
0255             return False
0256         return not any((x.isExposed and not x.isClaimedKong) for x in hand.melds)
0257 
0258 
0259 class OnlyMajors(RuleCode):
0260 
0261     def appliesToHand(hand):
0262         return all(x.isMajor for x in hand.tiles)
0263 
0264 
0265 class OnlyHonors(RuleCode):
0266 
0267     def appliesToHand(hand):
0268         return all(x.isHonor for x in hand.tiles)
0269 
0270 
0271 class HiddenTreasure(RuleCode):
0272 
0273     def appliesToHand(hand):
0274         return (not any(((x.isExposed and not x.isClaimedKong) or x.isChow) for x in hand.melds)
0275                 and hand.lastTile and hand.lastTile.isConcealed
0276                 and len(hand.melds) == 5)
0277 
0278 
0279 class BuriedTreasure(RuleCode):
0280 
0281     def appliesToHand(hand):
0282         return (len(hand.suits - set(Tile.honors)) == 1
0283                 and sum(x.isPung for x in hand.melds) == 4
0284                 and all((x.isPung and x.isConcealed) or x.isPair for x in hand.melds))
0285 
0286 
0287 class AllTerminals(RuleCode):
0288 
0289     def appliesToHand(hand):
0290         return all(x.isTerminal for x in hand.tiles)
0291 
0292 
0293 class StandardMahJongg(MJRule):
0294 
0295     cache = ('appliesToHand',)
0296 
0297     def computeLastMelds(hand):
0298         """return all possible last melds"""
0299         return MeldList(x for x in hand.melds if hand.lastTile in x and len(x) < 4)
0300 
0301     def appliesToHand(hand):
0302         """winner rules are not yet applied to hand"""
0303         # pylint: disable=too-many-return-statements
0304         # too many return statements
0305         if len(hand.melds) != 5:
0306             return False
0307         if any(len(x) not in (2, 3, 4) for x in hand.melds):
0308             return False
0309         if any(x.isRest or x.isKnitted for x in hand.melds):
0310             return False
0311         if sum(x.isChow for x in hand.melds) > hand.ruleset.maxChows:
0312             return False
0313         if hand.arranged is None:
0314             # this is only Hand.__arrange
0315             return True
0316         if hand.score.total() < hand.ruleset.minMJPoints:
0317             return False
0318         if hand.score.doubles >= hand.ruleset.minMJDoubles:
0319             # shortcut
0320             return True
0321         # but maybe we have enough doubles by winning:
0322         doublingWinnerRules = sum(
0323             x.rule.score.doubles for x in hand.matchingWinnerRules())
0324         return hand.score.doubles + doublingWinnerRules >= hand.ruleset.minMJDoubles
0325 
0326     def fillChow(group, values):
0327         val0, val1 = values
0328         if val0 + 1 == val1:
0329             if val0 == 1:
0330                 return {Tile(group, val0 + 2)}
0331             if val0 == 8:
0332                 return {Tile(group, val0 - 1)}
0333             return {Tile(group, val0 - 1), Tile(group, val0 + 2)}
0334         assert val0 + 2 == val1, 'group:%s values:%s' % (group, values)
0335         return {Tile(group, val0 + 1)}
0336 
0337     def winningTileCandidates(cls, hand):
0338         # pylint: disable=too-many-locals,too-many-return-statements,too-many-branches,too-many-statements
0339         if len(hand.melds) > 7:
0340             # hope 7 is sufficient, 6 was not
0341             return set()
0342         if not hand.tilesInHand:
0343             return set()
0344         inHand = [x.exposed for x in hand.tilesInHand]
0345         result = inHand[:]
0346         pairs = 0
0347         isolated = 0
0348         maxChows = hand.ruleset.maxChows - \
0349             sum(x.isChow for x in hand.declaredMelds)
0350         # TODO: does not differentiate between maxChows == 1 and maxChows > 1
0351         # test with kajonggtest and a ruleset where maxChows == 2
0352         if maxChows < 0:
0353             return set()
0354         if maxChows == 0:
0355             checkTiles = set(inHand)
0356         else:
0357             checkTiles = set(inHand) & elements.honors
0358         for tileName in checkTiles:
0359             count = inHand.count(tileName)
0360             if count == 1:
0361                 isolated += 1
0362             elif count == 2:
0363                 pairs += 1
0364             else:
0365                 for _ in range(count):
0366                     result.remove(tileName)
0367         if maxChows:
0368             if pairs > 2 or isolated > 2 or (pairs > 1 and isolated > 1):
0369                 # this is not a calling hand
0370                 return set()
0371         else:
0372             if pairs + isolated > 2:
0373                 return set()
0374         if maxChows == 0:
0375             return set(result)
0376         melds = []
0377         for group in sorted(hand.suits & set(Tile.colors)):
0378             values = sorted(x.value for x in result if x.group == group)
0379             changed = True
0380             while (changed and len(values) > 2
0381                    and values.count(values[0]) == 1
0382                    and values.count(values[1]) == 1
0383                    and values.count(values[2]) == 1):
0384                 changed = False
0385                 if values[0] + 2 == values[2] and (len(values) == 3 or values[3] > values[0] + 3):
0386                     # logDebug('removing first 3 from %s' % values)
0387                     meld = Tile(group, values[0]).chow
0388                     # pylint: disable=not-an-iterable
0389                     # must be a pylint bug. meld is TileList is list
0390                     for pair in meld:
0391                         result.remove(pair)
0392                     melds.append(meld)
0393                     values = values[3:]
0394                     changed = True
0395                 elif values[0] + 1 == values[1] and values[2] > values[0] + 2:
0396                     # logDebug('found incomplete chow at start of %s' %
0397                     # values)
0398                     return cls.fillChow(group, values[:2])
0399             changed = True
0400             while (changed and len(values) > 2
0401                    and values.count(values[-1]) == 1
0402                    and values.count(values[-2]) == 1
0403                    and values.count(values[-3]) == 1):
0404                 changed = False
0405                 if values[-1] - 2 == values[-3] and (len(values) == 3 or values[-4] < values[-1] - 3):
0406                     meld = Tile(group, values[-3]).chow
0407                     # pylint: disable=not-an-iterable
0408                     # must be a pylint bug. meld is TileList is list
0409                     for pair in meld:
0410                         result.remove(pair)
0411                     melds.append(meld)
0412                     values = values[:-3]
0413                     changed = True
0414                 elif values[-1] - 1 == values[-2] and values[-3] < values[-1] - 2:
0415                     # logDebug('found incomplete chow at end of %s' % values)
0416                     return cls.fillChow(group, values[-2:])
0417 
0418             if len(values) % 3 == 0:
0419                 # adding a 4th, 7th or 10th tile with this color can not let us win,
0420                 # so we can exclude this color from the candidates
0421                 result = [x for x in result if x.group != group]
0422                 continue
0423             valueSet = set(values)
0424             if len(values) == 4 and len(values) == len(valueSet):
0425                 if values[0] + 3 == values[-1]:
0426                     # logDebug('seq4 in %s' % hand.tilesInHand)
0427                     return {Tile(group, values[0]), Tile(group, values[-1])}
0428             if len(values) == 7 and len(values) == len(valueSet):
0429                 if values[0] + 6 == values[6]:
0430                     # logDebug('seq7 in %s' % hand.tilesInHand)
0431                     return {Tile(group, values[x]) for x in (0, 3, 6)}
0432             if len(values) == 1:
0433                 # only a pair of this value is possible
0434                 # logDebug('need pair')
0435                 return {Tile(group, values[0]).concealed}
0436             if len(valueSet) == 1:
0437                 # no chow reachable, only pair/pung
0438                 continue
0439             singles = {x for x in valueSet
0440                        if values.count(x) == 1
0441                        and not {x - 1, x - 2, x + 1, x + 2} & valueSet}
0442             isolated += len(singles)
0443             if isolated > 1:
0444                 # this is not a calling hand
0445                 return set()
0446             if len(values) == 2 and len(valueSet) == 2:
0447                 # exactly two adjacent values: must be completed to Chow
0448                 if maxChows == 0:
0449                     # not a calling hand
0450                     return set()
0451                 # logDebug('return fillChow for %s' % values)
0452                 return cls.fillChow(group, values)
0453             if (len(values) == 4 and len(valueSet) == 2
0454                     and values[0] == values[1] and values[2] == values[3]):
0455                 return {Tile(group, values[0]), Tile(group, values[2])}
0456             if maxChows:
0457                 for value in valueSet:
0458                     if value > 1:
0459                         result.append(Tile(group, value - 1))
0460                     if value < 9:
0461                         result.append(Tile(group, value + 1))
0462         return set(result)
0463 
0464     def shouldTry(hand, maxMissing=10):
0465         return True
0466 
0467     def rearrange(hand, rest):
0468         """rest is a list of those tiles that can still
0469         be rearranged: No declared melds and no bonus tiles.
0470         done is already arranged, do not change this.
0471         TODO: also return how many tiles are missing for winning"""
0472         permutations = Permutations(rest)
0473         for variantMelds in permutations.variants:
0474             yield tuple(variantMelds), tuple()
0475 
0476 
0477 class SquirmingSnake(StandardMahJongg):
0478     cache = ()
0479 
0480     def appliesToHand(hand):
0481         cacheKey = (hand.ruleset.standardMJRule.__class__, 'appliesToHand')
0482         std = hand.ruleCache.get(cacheKey, None)
0483         if std is False:
0484             return False
0485         if len(hand.suits) != 1 or hand.suits >= set(Tile.colors):
0486             return False
0487         values = hand.values
0488         if values.count(1) < 3 or values.count(9) < 3:
0489             return False
0490         pairs = [x for x in (2, 5, 8) if values.count(x) == 2]
0491         if len(pairs) != 1:
0492             return False
0493         return len(set(values)) == len(values) - 5
0494 
0495     def winningTileCandidates(hand):
0496         """they have already been found by the StandardMahJongg rule"""
0497         return set()
0498 
0499 
0500 class WrigglingSnake(MJRule):
0501 
0502     def shouldTry(hand, maxMissing=3):
0503         if hand.declaredMelds:
0504             return False
0505         return (len({x.exposed for x in hand.tiles}) + maxMissing > 12
0506                 and all(not x.isChow for x in hand.declaredMelds))
0507 
0508     def computeLastMelds(hand):
0509         return [hand.lastTile.pair] if hand.lastTile.value == 1 else [hand.lastTile.single]
0510 
0511     def winningTileCandidates(hand):
0512         suits = hand.suits.copy()
0513         if Tile.wind not in suits or Tile.dragon in suits or len(suits) > 2:
0514             return set()
0515         suits -= {Tile.wind}
0516         if not suits:
0517             return set()
0518         group = suits.pop()
0519         values = set(hand.values)
0520         if len(values) < 12:
0521             return set()
0522         if len(values) == 12:
0523             # one of 2..9 or a wind is missing
0524             if hand.values.count(1) < 2:
0525                 # and the pair of 1 is incomplete too
0526                 return set()
0527             return (elements.winds | {Tile(group, x) for x in range(2, 10)}) \
0528                 - {x.exposed for x in hand.tiles}
0529         # pair of 1 is not complete
0530         return {Tile(group, '1')}
0531 
0532     def rearrange(hand, rest):
0533         melds = []
0534         for tileName in rest[:]:
0535             if rest.count(tileName) >= 2:
0536                 melds.append(tileName.pair)
0537                 rest.remove(tileName)
0538                 rest.remove(tileName)
0539             elif rest.count(tileName) == 1:
0540                 melds.append(tileName.single)
0541                 rest.remove(tileName)
0542         yield tuple(melds), tuple(rest)
0543 
0544     def appliesToHand(hand):
0545         if hand.declaredMelds:
0546             return False
0547         suits = hand.suits.copy()
0548         if Tile.wind not in suits:
0549             return False
0550         suits -= {Tile.wind}
0551         if len(suits) != 1 or suits >= set(Tile.colors):
0552             return False
0553         if hand.values.count(1) != 2:
0554             return False
0555         return len(set(hand.values)) == 13
0556 
0557 
0558 class CallingHand(RuleCode):
0559 
0560     def appliesToHand(cls, hand):
0561         for callHand in hand.callingHands:
0562             used = (x.rule.__class__ for x in callHand.usedRules)
0563             if cls.limitHand in used:
0564                 return True
0565         return False
0566 
0567 
0568 class TripleKnitting(MJRule):
0569 
0570     def computeLastMelds(cls, hand):
0571         """return all possible last melds"""
0572         if not hand.lastTile:
0573             return None
0574         triples, rest = cls.findTriples(hand)
0575         assert len(rest) == 2
0576         triples = [triples]
0577         triples.append(rest)
0578         return [Meld(x) for x in triples if hand.lastTile in x]
0579 
0580     def claimness(cls, hand, discard):
0581         result = IntDict()
0582         if cls.shouldTry(hand):
0583             result[Message.Pung] = -999
0584             result[Message.Kong] = -999
0585             result[Message.Chow] = -999
0586         return result
0587 
0588     def weigh(cls, aiInstance, candidates):
0589         if cls.shouldTry(candidates.hand):
0590             _, rest = cls.findTriples(candidates.hand)
0591             for candidate in candidates:
0592                 if candidate.group in Tile.honors:
0593                     candidate.keep -= 50
0594                 if rest.count(candidate.tile) > 1:
0595                     candidate.keep -= 10
0596         return candidates
0597 
0598     def rearrange(cls, hand, rest):
0599         melds = []
0600         for triple in cls.findTriples(hand)[0]:
0601             melds.append(triple)
0602             rest.remove(triple[0])
0603             rest.remove(triple[1])
0604             rest.remove(triple[2])
0605         while len(rest) >= 2:
0606             for tile in sorted(set(rest)):
0607                 value = tile.value
0608                 suits = {x.group for x in rest if x.value == value}
0609                 if len(suits) < 2:
0610                     yield tuple(melds), tuple(rest)
0611                     return
0612                 pair = (Tile(suits.pop(), value), Tile(suits.pop(), value))
0613                 melds.append(Meld(sorted(pair)))
0614                 rest.remove(pair[0])
0615                 rest.remove(pair[1])
0616         yield tuple(melds), tuple(rest)
0617 
0618     def appliesToHand(cls, hand):
0619         if any(x.isHonor for x in hand.tiles):
0620             return False
0621         if len(hand.declaredMelds) > 1:
0622             return False
0623         if hand.lastTile and hand.lastTile.isConcealed and hand.declaredMelds:
0624             return False
0625         triples, rest = cls.findTriples(hand)
0626         return (len(triples) == 4 and len(rest) == 2
0627                 and rest[0].group != rest[1].group and rest[0].value == rest[1].value)
0628 
0629     def winningTileCandidates(cls, hand):
0630         if hand.declaredMelds:
0631             return set()
0632         if any(x.isHonor for x in hand.tiles):
0633             return set()
0634         _, rest = cls.findTriples(hand)
0635         if len(rest) not in (1, 4):
0636             return set()
0637         result = list(
0638             Tile(x, y.value).concealed for x in Tile.colors for y in rest)
0639         for restTile in rest:
0640             result.remove(restTile)
0641         return set(result)
0642 
0643     def shouldTry(cls, hand, maxMissing=3):
0644         if hand.declaredMelds:
0645             return False
0646         tripleWanted = 4 - maxMissing // 3  # count triples
0647         tripleCount = len(cls.findTriples(hand)[0])
0648         return tripleCount >= tripleWanted
0649 
0650     def findTriples(cls, hand):
0651         """return a list of triple knitted melds, including the mj triple.
0652         Also returns the remaining untripled tiles"""
0653         if hand.declaredMelds:
0654             if len(hand.declaredMelds) > 1:
0655                 return (Meld(), None)
0656         result = []
0657         tilesS = [x.concealed for x in hand.tiles if x.lowerGroup == Tile.stone]
0658         tilesB = [x.concealed for x in hand.tiles if x.lowerGroup == Tile.bamboo]
0659         tilesC = [x.concealed for x in hand.tiles if x.lowerGroup == Tile.character]
0660         for tileS in tilesS[:]:
0661             tileB = Tile(Tile.bamboo, tileS.value).concealed
0662             tileC = Tile(Tile.character, tileS.value).concealed
0663             if tileB in tilesB and tileC in tilesC:
0664                 tilesS.remove(tileS)
0665                 tilesB.remove(tileB)
0666                 tilesC.remove(tileC)
0667                 result.append(tileS.knitted3)
0668         return tuple(result), tuple(tilesS + tilesB + tilesC)
0669 
0670 
0671 class Knitting(MJRule):
0672 
0673     def computeLastMelds(cls, hand):
0674         """return all possible last melds"""
0675         if not hand.lastTile:
0676             return []
0677         couples, rest = cls.findCouples(hand)
0678         assert not rest, '%s: couples=%s rest=%s' % (
0679             hand.string, couples, rest)
0680         return [Meld(x) for x in couples if hand.lastTile in x]
0681 
0682     def claimness(cls, hand, discard):
0683         result = IntDict()
0684         if cls.shouldTry(hand):
0685             result[Message.Pung] = -999
0686             result[Message.Kong] = -999
0687             result[Message.Chow] = -999
0688         return result
0689 
0690     def weigh(cls, aiInstance, candidates):
0691         if cls.shouldTry(candidates.hand):
0692             for candidate in candidates:
0693                 if candidate.group in Tile.honors:
0694                     candidate.keep -= 50
0695         return candidates
0696 
0697     def shouldTry(cls, hand, maxMissing=4):
0698         if hand.declaredMelds:
0699             return False
0700         pairWanted = 7 - maxMissing // 2  # count pairs
0701         pairCount = len(cls.findCouples(hand)[0])
0702         return pairCount >= pairWanted
0703 
0704     def appliesToHand(cls, hand):
0705         if any(x.isHonor for x in hand.tiles):
0706             return False
0707         if len(hand.declaredMelds) > 1:
0708             return False
0709         if hand.lastTile and hand.lastTile.isConcealed and hand.declaredMelds:
0710             return False
0711         return len(cls.findCouples(hand)[0]) == 7
0712 
0713     def winningTileCandidates(cls, hand):
0714         if hand.declaredMelds:
0715             return set()
0716         if any(x.isHonor for x in hand.tiles):
0717             return set()
0718         couples, singleTile = cls.findCouples(hand)
0719         if len(couples) != 6:
0720             return set()
0721         if not singleTile:
0722             # single tile has wrong suit
0723             return set()
0724         assert len(singleTile) == 1
0725         singleTile = singleTile[0]
0726         otherSuit = (hand.suits - {singleTile.lowerGroup}).pop()
0727         otherTile = Tile(otherSuit, singleTile.value).concealed
0728         return {otherTile}
0729 
0730     def rearrange(cls, hand, rest):
0731         melds = []
0732         for couple in cls.findCouples(hand, rest)[0]:
0733             if couple[0].islower():
0734                 # this is the mj pair, lower after claiming
0735                 continue
0736             melds.append(Meld(couple))
0737             rest.remove(couple[0])
0738             rest.remove(couple[1])
0739         yield tuple(melds), tuple(rest)
0740 
0741     def findCouples(cls, hand, pairs=None):
0742         """return a list of tuples, including the mj couple.
0743         Also returns the remaining uncoupled tiles IF they
0744         are of the wanted suits"""
0745         if hand.declaredMelds:
0746             if len(hand.declaredMelds) > 1 or len(hand.declaredMelds[0]) > 2:
0747                 return [], []
0748         result = []
0749         if pairs is None:
0750             pairs = hand.tiles
0751         suits = cls.pairSuits(hand)
0752         if not suits:
0753             return [], []
0754         tiles0 = [x for x in pairs if x.lowerGroup == suits[0]]
0755         tiles1 = [x for x in pairs if x.lowerGroup == suits[1]]
0756         for tile0 in tiles0[:]:
0757             if tile0.isExposed:
0758                 tile1 = Tile(suits[1], tile0.value)
0759             else:
0760                 tile1 = Tile(suits[1], tile0.value).concealed
0761             if tile1 in tiles1:
0762                 tiles0.remove(tile0)
0763                 tiles1.remove(tile1)
0764                 result.append((tile0, tile1))
0765         return result, tiles0 + tiles1
0766 
0767     def pairSuits(hand):
0768         """return a lowercase string with two suit characters. If no prevalence, returns None"""
0769         suitCounts = [len([x for x in hand.tiles if x.lowerGroup == y]) for y in Tile.colors]
0770         minSuit = min(suitCounts)
0771         result = ''.join(x for idx, x in enumerate(Tile.colors) if suitCounts[idx] > minSuit)
0772         if len(result) == 2:
0773             return result
0774         return None
0775 
0776 
0777 class AllPairHonors(MJRule):
0778 
0779     def computeLastMelds(hand):
0780         return [hand.lastTile.pair]
0781 
0782     def claimness(hand, discard):
0783         result = IntDict()
0784         if AllPairHonors.shouldTry(hand):
0785             result[Message.Pung] = -999
0786             result[Message.Kong] = -999
0787             result[Message.Chow] = -999
0788         return result
0789 
0790     def maybeCallingOrWon(hand):
0791         if any(x.value in Tile.minors for x in hand.tiles):
0792             return False
0793         return len(hand.declaredMelds) < 2
0794 
0795     def appliesToHand(cls, hand):
0796         if not cls.maybeCallingOrWon(hand):
0797             return False
0798         if len(set(hand.tiles)) != 7:
0799             return False
0800         return {len([x for x in hand.tiles if x == y]) for y in hand.tiles} == {2}
0801 
0802     def winningTileCandidates(cls, hand):
0803         if not cls.maybeCallingOrWon(hand):
0804             return set()
0805         single = [x for x in hand.tiles if hand.tiles.count(x) == 1]
0806         if len(single) != 1:
0807             return set()
0808         return set(single)
0809 
0810     def shouldTry(hand, maxMissing=4):
0811         if hand.declaredMelds:
0812             return False
0813         tiles = [x.exposed for x in hand.tiles]
0814         pairCount = kongCount = 0
0815         for tile in elements.majors:
0816             count = tiles.count(tile)
0817             if count == 2:
0818                 pairCount += 1
0819             elif count == 4:
0820                 kongCount += 1
0821         pairWanted = 7 - maxMissing // 2  # count pairs
0822         result = pairCount >= pairWanted or (
0823             pairCount + kongCount * 2) > pairWanted
0824         return result
0825 
0826     def rearrange(hand, rest):
0827         melds = []
0828         for pair in sorted(set(rest) & elements.mAJORS):
0829             while rest.count(pair) >= 2:
0830                 melds.append(pair.pair)
0831                 rest.remove(pair)
0832                 rest.remove(pair)
0833         yield tuple(melds), tuple(rest)
0834 
0835     def weigh(aiInstance, candidates):
0836         hand = candidates.hand
0837         if not AllPairHonors.shouldTry(hand):
0838             return candidates
0839         keep = 10
0840         for candidate in candidates:
0841             if candidate.value in Tile.minors:
0842                 candidate.keep -= keep
0843             else:
0844                 if candidate.occurrence == 3:
0845                     candidate.keep -= keep / 2
0846                 else:
0847                     candidate.keep += keep
0848         return candidates
0849 
0850 
0851 class FourfoldPlenty(RuleCode):
0852 
0853     def appliesToHand(hand):
0854         return len(hand.tiles) == 18
0855 
0856 
0857 class ThreeGreatScholars(RuleCode):
0858 
0859     def appliesToHand(cls, hand):
0860         return (BigThreeDragons.appliesToHand(hand)
0861                 and ('nochow' not in cls.options or not any(x.isChow for x in hand.melds)))
0862 
0863 
0864 class BigThreeDragons(RuleCode):
0865 
0866     def appliesToHand(hand):
0867         return len([x for x in hand.melds if x.isDragonMeld and x.isPungKong]) == 3
0868 
0869 
0870 class BigFourJoys(RuleCode):
0871 
0872     def appliesToHand(hand):
0873         return len([x for x in hand.melds if x.isWindMeld and x.isPungKong]) == 4
0874 
0875 
0876 class LittleFourJoys(RuleCode):
0877 
0878     def appliesToHand(hand):
0879         lengths = sorted(min(len(x), 3) for x in hand.melds if x.isWindMeld)
0880         return lengths == [2, 3, 3, 3]
0881 
0882 
0883 class LittleThreeDragons(RuleCode):
0884 
0885     def appliesToHand(hand):
0886         return sorted(min(len(x), 3) for x in hand.melds if x.isDragonMeld) == [2, 3, 3]
0887 
0888 
0889 class FourBlessingsHoveringOverTheDoor(RuleCode):
0890 
0891     def appliesToHand(hand):
0892         return len([x for x in hand.melds if x.isPungKong and x.isWindMeld]) == 4
0893 
0894 
0895 class AllGreen(RuleCode):
0896 
0897     def appliesToHand(hand):
0898         return {x.exposed for x in hand.tiles} < elements.greenHandTiles
0899 
0900 
0901 class LastTileFromWall(RuleCode):
0902 
0903     def appliesToHand(hand):
0904         return hand.lastSource is TileSource.LivingWall
0905 
0906 
0907 class LastTileFromDeadWall(RuleCode):
0908 
0909     def appliesToHand(hand):
0910         return hand.lastSource is TileSource.DeadWall
0911 
0912     def selectable(hand):
0913         """for scoring game"""
0914         return hand.lastSource is TileSource.LivingWall
0915 
0916 
0917 class IsLastTileFromWall(RuleCode):
0918 
0919     def appliesToHand(hand):
0920         return hand.lastSource is TileSource.LivingWallEnd
0921 
0922     def selectable(hand):
0923         """for scoring game"""
0924         return hand.lastSource is TileSource.LivingWall
0925 
0926 
0927 class IsLastTileFromWallDiscarded(RuleCode):
0928 
0929     def appliesToHand(hand):
0930         return hand.lastSource is TileSource.LivingWallEndDiscard
0931 
0932     def selectable(hand):
0933         """for scoring game"""
0934         return hand.lastSource is TileSource.LivingWallDiscard
0935 
0936 
0937 class RobbingKong(RuleCode):
0938 
0939     def appliesToHand(hand):
0940         return hand.lastSource is TileSource.RobbedKong
0941 
0942     def selectable(hand):
0943         """for scoring game"""
0944         return (hand.lastSource in (TileSource.RobbedKong, TileSource.LivingWall, TileSource.LivingWallDiscard)
0945                 and hand.lastTile and hand.lastTile.group.islower()
0946                 and [x.exposed for x in hand.tiles].count(hand.lastTile.exposed) < 2)
0947 
0948 
0949 class GatheringPlumBlossomFromRoof(RuleCode):
0950 
0951     def appliesToHand(hand):
0952         return LastTileFromDeadWall.appliesToHand(hand) and hand.lastTile is Tile(Tile.stone, '5').concealed
0953 
0954 
0955 class PluckingMoon(RuleCode):
0956 
0957     def appliesToHand(hand):
0958         return IsLastTileFromWall.appliesToHand(hand) and hand.lastTile is Tile(Tile.stone, '1').concealed
0959 
0960 
0961 class ScratchingPole(RuleCode):
0962 
0963     def appliesToHand(hand):
0964         return RobbingKong.appliesToHand(hand) and hand.lastTile is Tile(Tile.bamboo, '2')
0965 
0966 
0967 class StandardRotation(RuleCode):
0968 
0969     def rotate(game):
0970         return game.winner and game.winner.wind is not East
0971 
0972 
0973 class EastWonNineTimesInARow(RuleCode):
0974     nineTimes = 9
0975 
0976     def appliesToHand(cls, hand):
0977         return cls.appliesToGame(hand.player.game)
0978 
0979     def appliesToGame(cls, game, needWins=None):
0980         if needWins is None:
0981             needWins = EastWonNineTimesInARow.nineTimes
0982             if game.isScoringGame():
0983                 # we are only proposing for the last needed Win
0984                 needWins -= 1
0985         if game.winner and game.winner.wind is East and game.notRotated >= needWins:
0986             eastMJCount = int(Query("select count(1) from score "
0987                                     "where game=%d and won=1 and wind='E' and player=%d "
0988                                     "and prevailing='%s'" %
0989                                     (game.gameid, game.players[East].nameid, game.roundWind.char)).records[0][0])
0990             return eastMJCount == needWins
0991         return False
0992 
0993     def rotate(cls, game):
0994         return cls.appliesToGame(game, needWins=EastWonNineTimesInARow.nineTimes)
0995 
0996 
0997 class GatesOfHeaven(StandardMahJongg):
0998     """as used for Classical Chinese BMJA.
0999 
1000     I believe that when they say a run of 2..8, they must
1001     all be concealed
1002     """
1003 
1004     cache = ()
1005 
1006 # TODO: in BMJA, 111 and 999 must be concealed, we do not check this
1007     def computeLastMelds(hand):
1008         return [x for x in hand.melds if hand.lastTile in x]
1009 
1010     def shouldTry(hand, maxMissing=None):
1011         if hand.declaredMelds:
1012             return False
1013         for suit in Tile.colors:
1014             count19 = sum(x.isTerminal for x in hand.tiles)
1015             suitCount = len([x for x in hand.tiles if x.lowerGroup == suit])
1016             if suitCount > 10 and count19 > 4:
1017                 return True
1018         return False
1019 
1020     def appliesToHand(cls, hand):
1021         if len(hand.suits) != 1 or hand.suits >= set(Tile.colors):
1022             return False
1023         if any(len(x) > 2 for x in hand.declaredMelds):
1024             return False
1025         values = hand.values
1026         if len(set(values)) < 9 or values.count(1) != 3 or values.count(9) != 3:
1027             return False
1028         values = list(values[3:-3])
1029         for value in Tile.minors:
1030             if value in values:
1031                 values.remove(value)
1032         if len(values) != 1:
1033             return False
1034         surplus = values[0]
1035         return 1 < surplus < 9
1036 
1037     def winningTileCandidates(cls, hand):
1038         if hand.declaredMelds:
1039             return set()
1040         if len(hand.suits) != 1 or hand.suits >= set(Tile.colors):
1041             return set()
1042         values = hand.values
1043         if len(set(values)) < 9:
1044             return set()
1045         # we have something of all values
1046         if values.count(1) != 3 or values.count(9) != 3:
1047 # TODO: we may get them from the wall but not by claim. Differentiate!
1048             return set()
1049         for suit in hand.suits:
1050             return {Tile(suit, x) for x in Tile.minors}
1051 
1052     def rearrange(hand, rest):
1053         melds = []
1054         for suit in hand.suits & set(Tile.colors):
1055             for value in Tile.numbers:
1056                 tile = Tile(suit, value).concealed
1057                 if rest.count(tile) == 3 and tile.isTerminal:
1058                     melds.append(tile.pung)
1059                     rest.remove(tile)
1060                     rest.remove(tile)
1061                     rest.remove(tile)
1062                 elif rest.count(tile) == 2:
1063                     melds.append(tile.pair)
1064                     rest.remove(tile)
1065                     rest.remove(tile)
1066                 elif rest.count(tile) == 1:
1067                     melds.append(tile.single)
1068                     rest.remove(tile)
1069             break
1070         yield tuple(melds), tuple(rest)
1071 
1072 
1073 class NineGates(GatesOfHeaven):
1074     """as used for Classical Chinese DMJL"""
1075 
1076     def appliesToHand(cls, hand):
1077         """last tile may also be 1 or 9"""
1078         if hand.declaredMelds:
1079             return False
1080         if len(hand.suits) != 1 or hand.suits >= set(Tile.colors):
1081             return False
1082         values = hand.values
1083         if len(set(values)) < 9:
1084             return False
1085         if values.count(1) != 3 or values.count(9) != 3:
1086             return False
1087         values = list(values[3:-3])
1088         for value in Tile.minors:
1089             if value in values:
1090                 values.remove(value)
1091         if len(values) != 1:
1092             return False
1093         surplus = values[0]
1094         return hand.lastTile and surplus == hand.lastTile.value
1095 
1096     def winningTileCandidates(cls, hand):
1097         if hand.declaredMelds:
1098             return set()
1099         if len(hand.suits) != 1 or hand.suits >= set(Tile.colors):
1100             return set()
1101         values = hand.values
1102         if len(set(values)) < 9:
1103             return set()
1104         # we have something of all values
1105         if values.count(1) != 3 or values.count(9) != 3:
1106             return set()
1107         for suit in hand.suits:
1108             return {Tile(suit, x) for x in Tile.numbers}
1109 
1110 
1111 class ThirteenOrphans(MJRule):
1112 
1113     def computeLastMelds(hand):
1114         meldSize = hand.tilesInHand.count(hand.lastTile)
1115         if meldSize == 0:
1116             # the last tile is called and not yet in the hand
1117             return [Meld()]
1118         return [hand.lastTile.meld(meldSize)]
1119 
1120     def rearrange(hand, rest):
1121         melds = []
1122         for tileName in rest:
1123             if rest.count(tileName) >= 2:
1124                 melds.append(tileName.pair)
1125                 rest.remove(tileName)
1126                 rest.remove(tileName)
1127             elif rest.count(tileName) == 1:
1128                 melds.append(tileName.single)
1129                 rest.remove(tileName)
1130         yield tuple(melds), tuple(rest)
1131 
1132     def claimness(cls, hand, discard):
1133         result = IntDict()
1134         if cls.shouldTry(hand):
1135             doublesCount = hand.doublesEstimate(discard)
1136 # TODO: compute scoring for resulting hand. If it is high anyway,
1137 # prefer pung over trying 13 orphans
1138             if doublesCount < 2 or cls.shouldTry(hand, 1):
1139                 result[Message.Pung] = -999
1140                 result[Message.Kong] = -999
1141                 result[Message.Chow] = -999
1142         return result
1143 
1144     def appliesToHand(hand):
1145         return {x.exposed for x in hand.tiles} == elements.majors
1146 
1147     def winningTileCandidates(cls, hand):
1148         if any(x in hand.values for x in Tile.minors):
1149             # no minors allowed
1150             return set()
1151         if not cls.shouldTry(hand, 1):
1152             return set()
1153         handTiles = {x.exposed for x in hand.tiles}
1154         missing = elements.majors - handTiles
1155         if not missing:
1156             # if all 13 tiles are there, we need any one of them:
1157             return elements.majors
1158         assert len(missing) == 1
1159         return missing
1160 
1161     def shouldTry(hand, maxMissing=4):
1162         # TODO: look at how many tiles there still are on the wall
1163         if hand.declaredMelds:
1164             return False
1165         handTiles = {x.exposed for x in hand.tiles}
1166         missing = elements.majors - handTiles
1167         if len(missing) > maxMissing:
1168             return False
1169         for missingTile in missing:
1170             if not hand.player.tileAvailable(missingTile, hand):
1171                 return False
1172         return True
1173 
1174     def weigh(cls, aiInstance, candidates):
1175         hand = candidates.hand
1176         if not cls.shouldTry(hand):
1177             return candidates
1178         handTiles = {x.exposed for x in hand.tiles}
1179         missing = elements.majors - handTiles
1180         havePair = False
1181         keep = (6 - len(missing)) * 5
1182         for candidate in candidates:
1183             if candidate.value in Tile.minors:
1184                 candidate.keep -= keep
1185             else:
1186                 if havePair and candidate.occurrence >= 2:
1187                     candidate.keep -= keep
1188                 else:
1189                     candidate.keep += keep
1190                 havePair = candidate.occurrence == 2
1191         return candidates
1192 
1193 
1194 class OwnFlower(RuleCode):
1195 
1196     def appliesToMeld(hand, meld):
1197         return meld[0].value is hand.ownWind
1198 
1199     def mayApplyToMeld(meld):
1200         # pylint: disable=unsubscriptable-object
1201         # must be a pylint bug. meld is TileList is list
1202         return meld.isBonus and meld[0].group == Tile.flower
1203 
1204 
1205 class OwnSeason(RuleCode):
1206 
1207     def appliesToMeld(hand, meld):
1208         return meld[0].value is hand.ownWind
1209 
1210     def mayApplyToMeld(meld):
1211         # pylint: disable=unsubscriptable-object
1212         # must be a pylint bug. meld is TileList is list
1213         return meld.isBonus and meld[0].group == Tile.season
1214 
1215 
1216 class OwnFlowerOwnSeason(RuleCode):
1217 
1218     def appliesToHand(hand):
1219         return sum(x.isBonus and x[0].value is hand.ownWind for x in hand.bonusMelds) == 2
1220 
1221 
1222 class AllFlowers(RuleCode):
1223 
1224     def appliesToHand(hand):
1225         return len([x for x in hand.bonusMelds if x.group == Tile.flower]) == 4
1226 
1227 
1228 class AllSeasons(RuleCode):
1229 
1230     def appliesToHand(hand):
1231         return len([x for x in hand.bonusMelds if x.group == Tile.season]) == 4
1232 
1233 
1234 class ThreeConcealedPongs(RuleCode):
1235 
1236     def appliesToHand(hand):
1237         return len([x for x in hand.melds if (
1238             x.isConcealed or x.isClaimedKong) and x.isPungKong]) >= 3
1239 
1240 
1241 class MahJonggWithOriginalCall(RuleCode):
1242 
1243     def appliesToHand(hand):
1244         return ('a' in hand.announcements
1245                 and sum(x.isExposed for x in hand.melds) < 3)
1246 
1247     def selectable(hand):
1248         """for scoring game"""
1249         # one tile may be claimed before declaring OC and one for going MJ
1250         # the previous regex was too strict
1251         return sum(x.isExposed for x in hand.melds) < 3
1252 
1253     def claimness(hand, discard):
1254         result = IntDict()
1255         player = hand.player
1256         if player.originalCall and player.mayWin:
1257             if player.originalCallingHand.chancesToWin():
1258                 # winning with OriginalCall is still possible
1259                 result[Message.Pung] = -999
1260                 result[Message.Kong] = -999
1261                 result[Message.Chow] = -999
1262             else:
1263                 player.mayWin = False  # bad luck
1264         return result
1265 
1266 
1267 class TwofoldFortune(RuleCode):
1268 
1269     def appliesToHand(hand):
1270         return 't' in hand.announcements
1271 
1272     def selectable(hand):
1273         """for scoring game"""
1274         kungs = [x for x in hand.melds if len(x) == 4]
1275         return len(kungs) >= 2
1276 
1277 
1278 class BlessingOfHeaven(RuleCode):
1279 
1280     def appliesToHand(hand):
1281         if hand.lastSource is not TileSource.East14th:
1282             return False
1283         if hand.ownWind is not East:
1284             return False
1285         assert not any(x.isExposed for x in hand.melds)
1286         assert hand.lastTile is None, '{}: Blessing of Heaven: There can be no last tile'.format(hand)
1287         return True
1288 
1289     def selectable(hand):
1290         """for scoring game"""
1291         return (hand.ownWind is East
1292                 and hand.lastSource in (TileSource.LivingWall, TileSource.LivingWallDiscard)
1293                 and not hand.announcements - {'a'})
1294 
1295 
1296 class BlessingOfEarth(RuleCode):
1297 
1298     def appliesToHand(hand):
1299         if hand.lastSource is not TileSource.East14th:
1300             return False
1301         if hand.ownWind is East:
1302             return False
1303         return True
1304 
1305     def selectable(hand):
1306         """for scoring game"""
1307         return (hand.ownWind is not East
1308                 and hand.lastSource in (TileSource.LivingWall, TileSource.LivingWallDiscard)
1309                 and not hand.announcements - {'a'})
1310 
1311 
1312 class LongHand(RuleCode):
1313 
1314     def appliesToHand(hand):
1315         return hand.lenOffset > 0 if not hand.won else hand.lenOffset > 1
1316 
1317 
1318 class FalseDiscardForMJ(RuleCode):
1319 
1320     def appliesToHand(hand):
1321         return not hand.won
1322 
1323     def selectable(hand):
1324         """for scoring game"""
1325         return not hand.won
1326 
1327 
1328 class DangerousGame(RuleCode):
1329 
1330     def appliesToHand(hand):
1331         return not hand.won
1332 
1333     def selectable(hand):
1334         """for scoring game"""
1335         return not hand.won
1336 
1337 
1338 class LastOnlyPossible(RuleCode):
1339 
1340     """check if the last tile was the only one possible for winning"""
1341     def appliesToHand(cls, hand):
1342         if not hand.lastTile:
1343             return False
1344         if any(hand.lastTile in x for x in hand.melds if len(x) == 4):
1345             # the last tile completed a Kong
1346             return False
1347         shortHand = hand - hand.lastTile
1348         return len(shortHand.callingHands) == 1