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