File indexing completed on 2024-04-14 03:59:09
0001 # -*- coding: utf-8 -*- 0002 0003 """Copyright (C) 2009-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de> 0004 0005 SPDX-License-Identifier: GPL-2.0 0006 0007 0008 0009 0010 Read the user manual for a description of the interface to this scoring engine 0011 """ 0012 0013 from itertools import chain 0014 import weakref 0015 from hashlib import md5 0016 0017 from log import dbgIndent, Fmt, fmt 0018 from tile import Tile, TileList 0019 from tilesource import TileSource 0020 from meld import Meld, MeldList 0021 from rule import Score, UsedRule 0022 from common import Debug, StrMixin 0023 from intelligence import AIDefaultAI 0024 from util import callers 0025 from message import Message 0026 0027 0028 class Hand(StrMixin): 0029 0030 """represent the hand to be evaluated. 0031 0032 lenOffset is 0033 <0 for a short hand 0034 0 for a correct calling hand 0035 1 for a correct winner hand or a long loser hand 0036 >1 for a long winner hand 0037 Of course ignoring bonus tiles and respecting kong replacement tiles. 0038 if there are no kongs, 13 tiles will return 0 0039 0040 We assume that long hands never happen. For manual scoring, this should 0041 be asserted by the caller after creating the Hand instance. If the Hand 0042 has lenOffset 1 but is no winning hand, the Hand instance will not be 0043 fully evaluated, it is given Score 0 and hand.won == False. 0044 0045 declaredMelds are those which cannot be changed anymore: Chows, Pungs, 0046 Kongs. 0047 0048 tilesInHand are those not in declaredMelds 0049 0050 Only tiles passed in the 'R' substring may be rearranged. 0051 0052 mjRule is the one out of mjRules with the highest resulting score. Every 0053 hand gets an mjRule even it is not a wining hand, it is the one which 0054 was used for rearranging the hiden tiles to melds. 0055 0056 suits include dragons and winds.""" 0057 0058 # pylint: disable=too-many-instance-attributes 0059 0060 indent = 0 0061 class __NotWon(UserWarning): # pylint: disable=invalid-name 0062 0063 """should be won but is not a winning hand""" 0064 0065 def __new__(cls, player, string, prevHand=None): 0066 # pylint: disable=unused-argument 0067 """since a Hand instance is never changed, we can use a cache""" 0068 cache = player.handCache 0069 cacheKey = string 0070 if cacheKey in cache: 0071 result = cache[cacheKey] 0072 player.cacheHits += 1 0073 return result 0074 player.cacheMisses += 1 0075 result = object.__new__(cls) 0076 cache[cacheKey] = result 0077 return result 0078 0079 def __init__(self, player, string, prevHand=None): 0080 """evaluate string for player. rules are to be applied in any case""" 0081 if hasattr(self, 'string'): 0082 # I am from cache 0083 return 0084 0085 # shortcuts for speed: 0086 self._player = weakref.ref(player) 0087 self.ruleset = player.game.ruleset 0088 self.intelligence = player.intelligence if player else AIDefaultAI() 0089 self.string = string 0090 self.__robbedTile = Tile.unknown 0091 self.prevHand = prevHand 0092 self.__won = None 0093 self.__score = None 0094 self.__callingHands = None 0095 self.__mjRule = None 0096 self.ruleCache = {} 0097 self.__lastTile = None 0098 self.__lastSource = TileSource.Unknown 0099 self.__announcements = set() 0100 self.__lastMeld = 0 0101 self.__lastMelds = MeldList() 0102 self.tiles = None 0103 self.melds = MeldList() 0104 self.bonusMelds = MeldList() 0105 self.usedRules = [] 0106 self.__rest = TileList() 0107 self.__arranged = None 0108 0109 self.__parseString(string) 0110 self.__won = self.lenOffset == 1 and player.mayWin 0111 0112 if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): 0113 self.debug(fmt('{callers}', 0114 callers=callers(exclude=['__init__']))) 0115 Hand.indent += 1 0116 self.debug('New Hand {} lenOffset={}'.format(string, self.lenOffset)) 0117 0118 try: 0119 self.__arrange() 0120 self.__calculate() 0121 self.__arranged = True 0122 except Hand.__NotWon as notwon: 0123 if Debug.mahJongg: 0124 self.debug(fmt(str(notwon))) 0125 self.__won = False 0126 self.__score = Score() 0127 finally: 0128 self._fixed = True 0129 if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): 0130 self.debug('Fixing {} {}{}'.format(self, 'won ' if self.won else '', self.score)) 0131 Hand.indent -= 1 0132 0133 def __parseString(self, inString): 0134 """parse the string passed to Hand()""" 0135 # pylint: disable=too-many-branches 0136 tileStrings = [] 0137 for part in inString.split(): 0138 partId = part[0] 0139 if partId == 'm': 0140 if len(part) > 1: 0141 try: 0142 self.__lastSource = TileSource.byChar[part[1]] 0143 except KeyError as _: 0144 raise Exception('{} has unknown lastTile {}'.format(inString, part[1])) from _ 0145 if len(part) > 2: 0146 self.__announcements = set(part[2]) 0147 elif partId == 'L': 0148 if len(part[1:]) > 8: 0149 raise Exception( 0150 'last tile cannot complete a kang:' + inString) 0151 if len(part) > 3: 0152 self.__lastMeld = Meld(part[3:]) 0153 self.__lastTile = Tile(part[1:3]) 0154 else: 0155 if part != 'R': 0156 tileStrings.append(part) 0157 self.bonusMelds, tileStrings = self.__separateBonusMelds(tileStrings) 0158 tileString = ' '.join(tileStrings) 0159 self.tiles = TileList(tileString.replace(' ', '').replace('R', '')) 0160 self.tiles.sort() 0161 for part in tileStrings[:]: 0162 if part[:1] != 'R': 0163 self.melds.append(Meld(part)) 0164 tileStrings.remove(part) 0165 0166 self.values = tuple(x.value for x in self.tiles) 0167 self.suits = {x.lowerGroup for x in self.tiles} 0168 self.declaredMelds = MeldList(x for x in self.melds if x.isDeclared) 0169 declaredTiles = list(sum((x for x in self.declaredMelds), [])) 0170 self.tilesInHand = TileList(x for x in self.tiles 0171 if x not in declaredTiles) 0172 self.lenOffset = (len(self.tiles) - 13 0173 - sum(x.isKong for x in self.melds)) 0174 0175 assert len(tileStrings) < 2, tileStrings 0176 self.__rest = TileList() 0177 if tileStrings: 0178 self.__rest.extend(TileList(tileStrings[0][1:])) 0179 0180 last = self.__lastTile 0181 if last and not last.isBonus: 0182 assert last in self.tiles, \ 0183 'lastTile %s is not in hand %s' % (last, str(self)) 0184 if self.__lastSource is TileSource.RobbedKong: 0185 assert self.tiles.count(last.exposed) + \ 0186 self.tiles.count(last.concealed) == 1, ( 0187 'Robbing kong: I cannot have ' 0188 'lastTile %s more than once in %s' % ( 0189 last, ' '.join(self.tiles))) 0190 0191 @property 0192 def arranged(self): 0193 """readonly""" 0194 return self.__arranged 0195 0196 @property 0197 def player(self): 0198 """weakref""" 0199 return self._player() 0200 0201 @property 0202 def ownWind(self): 0203 """for easier usage""" 0204 return self.player.wind 0205 0206 @property 0207 def roundWind(self): 0208 """for easier usage""" 0209 return self.player.game.roundWind 0210 0211 def __calculate(self): 0212 """apply rules, calculate score""" 0213 assert not self.__rest, ( 0214 'Hand.__calculate expects there to be no rest tiles: %s' % self) 0215 oldWon = self.__won 0216 self.__applyRules() 0217 if len(self.lastMelds) > 1: 0218 self.__applyBestLastMeld() 0219 if self.__won != oldWon: 0220 # if not won after all, this might be a long hand. 0221 # So we might even have to unapply meld rules and 0222 # bonus points. Instead just recompute all again. 0223 # This should only happen with scoring manual games 0224 # and with scoringtest - normally kajongg would not 0225 # let you declare an invalid mah jongg 0226 self.__applyRules() 0227 0228 def hasTiles(self): 0229 """tiles are assigned to this hand""" 0230 return self.tiles or self.bonusMelds 0231 0232 @property 0233 def mjRule(self): 0234 """getter""" 0235 return self.__mjRule 0236 0237 @mjRule.setter 0238 def mjRule(self, value): 0239 """changing mjRule must reset score""" 0240 if self.__mjRule != value: 0241 self.__mjRule = value 0242 self.__score = None 0243 0244 @property 0245 def lastTile(self): 0246 """compute and cache, readonly""" 0247 return self.__lastTile 0248 0249 @property 0250 def lastSource(self): 0251 """compute and cache, readonly""" 0252 return self.__lastSource 0253 0254 @property 0255 def announcements(self): 0256 """compute and cache, readonly""" 0257 return self.__announcements 0258 0259 @property 0260 def score(self): 0261 """calculate it first if not yet done""" 0262 if self.__score is None and self.__arranged is not None: 0263 self.__score = Score() 0264 self.__calculate() 0265 return self.__score 0266 0267 @property 0268 def lastMeld(self): 0269 """compute and cache, readonly""" 0270 if self.__lastMeld == 0: 0271 self.__setLastMeld() 0272 return self.__lastMeld 0273 0274 @property 0275 def lastMelds(self): 0276 """compute and cache, readonly""" 0277 if self.__lastMeld == 0: 0278 self.__setLastMeld() 0279 return self.__lastMelds 0280 0281 @property 0282 def won(self): 0283 """do we really have a winner hand?""" 0284 return self.__won 0285 0286 def debug(self, msg): 0287 """try to use Game.debug so we get a nice prefix""" 0288 idPrefix = Fmt.num_encode(hash(self)) 0289 if self.prevHand: 0290 idPrefix += '<{}'.format(Fmt.num_encode(hash(self.prevHand))) 0291 idPrefix = 'Hand({})'.format(idPrefix) 0292 self.player.game.debug(' '.join([dbgIndent(self, self.prevHand), idPrefix, msg])) 0293 0294 def __applyRules(self): 0295 """find out which rules apply, collect in self.usedRules""" 0296 self.usedRules = [] 0297 for meld in chain(self.melds, self.bonusMelds): 0298 self.usedRules.extend(UsedRule(x, meld) for x in meld.rules(self)) 0299 for rule in self.ruleset.handRules: 0300 if rule.appliesToHand(self): 0301 self.usedRules.append(UsedRule(rule)) 0302 0303 self.__score = self.__totalScore() 0304 0305 self.ruleCache.clear() 0306 # do the rest only if we know all tiles of the hand 0307 if Tile.unknown in self.string: 0308 return 0309 if self.__won: 0310 matchingMJRules = self.__maybeMahjongg() 0311 if not matchingMJRules: 0312 self.__score = Score() 0313 raise Hand.__NotWon('no matching MJ Rule') 0314 self.__mjRule = matchingMJRules[0] 0315 self.usedRules.append(UsedRule(self.__mjRule)) 0316 self.usedRules.extend(self.matchingWinnerRules()) 0317 self.__score = self.__totalScore() 0318 else: # not self.won 0319 loserRules = self.__matchingRules(self.ruleset.loserRules) 0320 if loserRules: 0321 self.usedRules.extend(UsedRule(x) for x in loserRules) 0322 self.__score = self.__totalScore() 0323 self.__checkHasExclusiveRules() 0324 0325 def matchingWinnerRules(self): 0326 """return a list of matching winner rules""" 0327 matching = [UsedRule(x) for x in self.__matchingRules(self.ruleset.winnerRules)] 0328 limitRule = self.maxLimitRule(matching) 0329 return [limitRule] if limitRule else matching 0330 0331 def __checkHasExclusiveRules(self): 0332 """if we have one, remove all others""" 0333 exclusive = [x for x in self.usedRules if 'absolute' in x.rule.options] 0334 if exclusive: 0335 self.usedRules = exclusive 0336 self.__score = self.__totalScore() 0337 if self.__won and not bool(self.__maybeMahjongg()): 0338 raise Hand.__NotWon(fmt('exclusive rule {exclusive} does not win')) 0339 0340 def __setLastMeld(self): 0341 """set the shortest possible last meld. This is 0342 not yet the final choice, see __applyBestLastMeld""" 0343 self.__lastMeld = None 0344 if self.lastTile and self.__won: 0345 if self.mjRule: 0346 self.__lastMelds = self.mjRule.computeLastMelds(self) 0347 if self.__lastMelds: 0348 # syncHandBoard may return nothing 0349 if len(self.__lastMelds) == 1: 0350 self.__lastMeld = self.__lastMelds[0] 0351 else: 0352 totals = sorted( 0353 (len(x), idx) 0354 for idx, x in enumerate(self.__lastMelds)) 0355 self.__lastMeld = self.__lastMelds[totals[0][1]] 0356 if not self.__lastMeld: 0357 self.__lastMeld = self.lastTile.single 0358 self.__lastMelds = MeldList(self.__lastMeld) 0359 0360 def __applyBestLastMeld(self): 0361 """select the last meld giving the highest score 0362 (only winning variants)""" 0363 assert len(self.lastMelds) > 1 0364 totals = [] 0365 prev = self.lastMeld 0366 for rule in self.usedRules: 0367 assert isinstance(rule, UsedRule) 0368 for lastMeld in self.lastMelds: 0369 self.__lastMeld = lastMeld 0370 try: 0371 self.__applyRules() 0372 totals.append((self.__totalScore().total(), lastMeld)) 0373 except Hand.__NotWon: 0374 pass 0375 if totals: 0376 totals = sorted(totals) # sort by totalScore 0377 maxScore = totals[-1][0] 0378 totals = [x[1] for x in totals if x[0] == maxScore] 0379 # now we have a list of only lastMelds reaching maximum score 0380 if prev not in totals or self.__lastMeld not in totals: 0381 if Debug.explain and prev not in totals: 0382 if not self.player.game.belongsToRobotPlayer(): 0383 self.debug(fmt( 0384 'replaced last meld {prev} with {totals[0]}')) 0385 self.__lastMeld = totals[0] 0386 self.__applyRules() 0387 0388 def chancesToWin(self): 0389 """count the physical tiles that make us win and still seem available""" 0390 assert self.lenOffset == 0 0391 result = [] 0392 for completedHand in self.callingHands: 0393 result.extend( 0394 [completedHand.lastTile] * 0395 (self.player.tileAvailable(completedHand.lastTile, self))) 0396 return result 0397 0398 def newString(self, melds=1, rest=1, lastSource=1, announcements=1, lastTile=1, lastMeld=1): 0399 """create string representing a hand. Default is current Hand, but every part 0400 can be overridden or excluded by passing None""" 0401 if melds == 1: 0402 melds = chain(self.melds, self.bonusMelds) 0403 if rest == 1: 0404 rest = self.__rest 0405 if lastSource == 1: 0406 lastSource = self.lastSource 0407 if announcements == 1: 0408 announcements = self.announcements 0409 if lastTile == 1: 0410 lastTile = self.lastTile 0411 if lastMeld == 1: 0412 lastMeld = self.__lastMeld 0413 parts = [str(x) for x in sorted(melds)] 0414 if rest: 0415 parts.append('R' + ''.join(str(x) for x in sorted(rest))) 0416 if lastSource or announcements: 0417 parts.append('m{}{}'.format( 0418 self.lastSource.char, 0419 ''.join(self.announcements))) 0420 if lastTile: 0421 parts.append('L{}{}'.format(lastTile, lastMeld if lastMeld else '')) 0422 return ' '.join(parts).strip() 0423 0424 def __add__(self, addTile): 0425 """return a new Hand built from this one plus addTile""" 0426 assert addTile.isConcealed, 'addTile %s should be concealed:' % addTile 0427 # combine all parts about hidden tiles plus the new one to one part 0428 # because something like DrDrS8S9 plus S7 will have to be reordered 0429 # anyway 0430 newString = self.newString( 0431 melds=chain(self.declaredMelds, self.bonusMelds), 0432 rest=self.tilesInHand + [addTile], 0433 lastSource=None, 0434 lastTile=addTile, 0435 lastMeld=None 0436 ) 0437 return Hand(self.player, newString, prevHand=self) 0438 0439 def __sub__(self, subtractTile): 0440 """return a copy of self minus subtractTiles. 0441 Case of subtractTile (hidden or exposed) is ignored. 0442 subtractTile must either be undeclared or part of 0443 lastMeld. Exposed melds of length<3 will be hidden.""" 0444 # pylint: disable=too-many-branches 0445 # If lastMeld is given, it must be first in the list. 0446 # Next try undeclared melds, then declared melds 0447 assert self.lenOffset == 1 0448 if self.lastTile: 0449 if self.lastTile is subtractTile and self.prevHand: 0450 return self.prevHand 0451 declaredMelds = self.declaredMelds 0452 tilesInHand = TileList(self.tilesInHand) 0453 boni = MeldList(self.bonusMelds) 0454 lastMeld = self.lastMeld 0455 if subtractTile.isBonus: 0456 for idx, meld in enumerate(boni): 0457 if subtractTile is meld[0]: 0458 del boni[idx] 0459 break 0460 else: 0461 if lastMeld and lastMeld.isDeclared and ( 0462 subtractTile.exposed in lastMeld.exposed): 0463 declaredMelds.remove(lastMeld) 0464 tilesInHand.extend(lastMeld.concealed) 0465 tilesInHand.remove(subtractTile.concealed) 0466 for meld in declaredMelds[:]: 0467 if len(meld) < 3: 0468 declaredMelds.remove(meld) 0469 tilesInHand.extend(meld.concealed) 0470 # if we robbed a kong, remove that announcement 0471 mjPart = '' 0472 announcements = self.announcements - set('k') 0473 if announcements: 0474 mjPart = 'm.' + ''.join(announcements) 0475 rest = 'R' + str(tilesInHand) 0476 newString = ' '.join(str(x) for x in ( 0477 declaredMelds, rest, boni, mjPart)) 0478 return Hand(self.player, newString, prevHand=self) 0479 0480 def manualRuleMayApply(self, rule): 0481 """return True if rule has selectable() and applies to this hand""" 0482 if self.__won and rule in self.ruleset.loserRules: 0483 return False 0484 if not self.__won and rule in self.ruleset.winnerRules: 0485 return False 0486 return rule.selectable(self) or rule.appliesToHand(self) 0487 # needed for activated rules 0488 0489 @property 0490 def callingHands(self): 0491 """the hand is calling if it only needs one tile for mah jongg. 0492 Returns all hands which would only need one tile. 0493 If mustBeAvailable is True, make sure the missing tile might still 0494 be available. 0495 """ 0496 if self.__callingHands is None: 0497 self.__callingHands = self.__findAllCallingHands() 0498 return self.__callingHands 0499 0500 def __findAllCallingHands(self): 0501 """always try to find all of them""" 0502 result = [] 0503 string = self.string 0504 if ' x' in string or self.lenOffset: 0505 return result 0506 candidates = [] 0507 for rule in self.ruleset.mjRules: 0508 cand = rule.winningTileCandidates(self) 0509 if Debug.hand and cand: 0510 # Py2 and Py3 show sets differently 0511 candis = ''.join(str(x) for x in sorted(cand)) # pylint: disable=unused-variable 0512 self.debug('callingHands found {} for {}'.format(candis, rule)) 0513 candidates.extend(x.concealed for x in cand) 0514 for tile in sorted(set(candidates)): 0515 if sum(x.exposed == tile.exposed for x in self.tiles) == 4: 0516 continue 0517 hand = self + tile 0518 if hand.won: 0519 result.append(hand) 0520 if Debug.hand: 0521 _hiderules = ', '.join({x.mjRule.name for x in result}) 0522 if _hiderules: 0523 self.debug(fmt('Is calling {_hiderules}')) 0524 return result 0525 0526 @property 0527 def robbedTile(self): 0528 """cache this here for use in rulecode""" 0529 if self.__robbedTile is Tile.unknown: 0530 self.__robbedTile = None 0531 if self.player.game.moves: 0532 # scoringtest does not (yet) simulate this 0533 lastMove = self.player.game.moves[-1] 0534 if (lastMove.message == Message.DeclaredKong 0535 and lastMove.player != self.player): 0536 self.__robbedTile = lastMove.meld[1] 0537 # we want it concealed only for a hidden Kong 0538 return self.__robbedTile 0539 0540 def __maybeMahjongg(self): 0541 """check if this is a mah jongg hand. 0542 Return a sorted list of matching MJ rules, highest 0543 total first. If no rule matches, return None""" 0544 if self.lenOffset == 1 and self.player.mayWin: 0545 matchingMJRules = [x for x in self.ruleset.mjRules 0546 if x.appliesToHand(self)] 0547 if matchingMJRules: 0548 if self.robbedTile and self.robbedTile.isConcealed: 0549 # Millington 58: robbing hidden kong is only 0550 # allowed for 13 orphans 0551 matchingMJRules = [ 0552 x for x in matchingMJRules 0553 if 'mayrobhiddenkong' in x.options] 0554 result = sorted(matchingMJRules, key=lambda x: -x.score.total()) 0555 if Debug.mahJongg: 0556 self.debug(fmt('{callers} Found {matchingMJRules}', 0557 callers=callers())) 0558 return result 0559 return None 0560 0561 def __arrangements(self): 0562 """find all legal arrangements. 0563 Returns a list of tuples with the mjRule and a list of concealed melds""" 0564 self.__rest.sort() 0565 result = [] 0566 stdMJ = self.ruleset.standardMJRule 0567 if self.mjRule: 0568 rules = [self.mjRule] 0569 else: 0570 rules = self.ruleset.mjRules 0571 for mjRule in rules: 0572 if ((self.lenOffset == 1 and mjRule.appliesToHand(self)) 0573 or (self.lenOffset < 1 and mjRule.shouldTry(self))): 0574 if self.__rest: 0575 for melds, rest2 in mjRule.rearrange(self, self.__rest[:]): 0576 if rest2: 0577 melds = list(melds) 0578 restMelds, _ = next( 0579 stdMJ.rearrange(self, rest2[:])) 0580 melds.extend(restMelds) 0581 result.append((mjRule, melds)) 0582 if not result: 0583 result.extend( 0584 (stdMJ, x[0]) 0585 for x in stdMJ.rearrange(self, self.__rest[:])) 0586 return result 0587 0588 def __arrange(self): 0589 """work hard to always return the variant with the highest Mah Jongg value.""" 0590 if any(not x.isKnown for x in self.__rest): 0591 melds, rest = divmod(len(self.__rest), 3) 0592 self.melds.extend([Tile.unknown.pung] * melds) 0593 if rest: 0594 self.melds.append(Meld(Tile.unknown * rest)) 0595 self.__rest = [] 0596 if not self.__rest: 0597 self.melds.sort() 0598 mjRules = self.__maybeMahjongg() 0599 if self.won: 0600 if not mjRules: 0601 # how could this ever happen? 0602 raise Hand.__NotWon('Long Hand with no rest') 0603 self.mjRule = mjRules[0] 0604 return 0605 wonHands = [] 0606 lostHands = [] 0607 for mjRule, melds in self.__arrangements(): 0608 allMelds = self.melds[:] + list(melds) 0609 lastTile = self.lastTile 0610 if self.lastSource and self.lastSource.isDiscarded: 0611 lastTile = lastTile.exposed 0612 lastMelds = sorted( 0613 (x for x in allMelds if not x.isDeclared and lastTile.concealed in x), 0614 key=lambda x: len(x)) # pylint: disable=unnecessary-lambda 0615 if lastMelds: 0616 allMelds.remove(lastMelds[0]) 0617 allMelds.append(lastMelds[0].exposed) 0618 _ = self.newString( 0619 chain(allMelds, self.bonusMelds), 0620 rest=None, lastTile=lastTile, lastMeld=None) 0621 tryHand = Hand(self.player, _, prevHand=self) 0622 if tryHand.won: 0623 tryHand.mjRule = mjRule 0624 wonHands.append((mjRule, melds, tryHand)) 0625 else: 0626 lostHands.append((mjRule, melds, tryHand)) 0627 # we prefer a won Hand even if a lost Hand might have a higher score 0628 tryHands = wonHands if wonHands else lostHands 0629 bestRule, bestVariant, _ = max(tryHands, key=lambda x: x[2]) 0630 if wonHands: 0631 self.mjRule = bestRule 0632 self.melds.extend(bestVariant) 0633 self.melds.sort() 0634 self.__rest = [] 0635 self.ruleCache.clear() 0636 assert sum(len(x) for x in self.melds) == len(self.tiles), ( 0637 '%s != %s' % (self.melds, self.tiles)) 0638 0639 def __gt__(self, other): 0640 """compares hand values""" 0641 assert self.player == other.player 0642 if not other.arranged: 0643 return True 0644 if self.won and not (other.arranged and other.won): 0645 return True 0646 if not (self.arranged and self.won) and other.won: 0647 return False 0648 return (self.intelligence.handValue(self) 0649 > self.intelligence.handValue(other)) 0650 0651 def __lt__(self, other): 0652 """compares hand values""" 0653 return other.__gt__(self) 0654 0655 def __eq__(self, other): 0656 """compares hand values""" 0657 assert self.player == other.player 0658 return self.string == other.string 0659 0660 def __ne__(self, other): 0661 """compares hand values""" 0662 assert self.player == other.player 0663 return self.string != other.string 0664 0665 def __matchingRules(self, rules): 0666 """return all matching rules for this hand""" 0667 return [rule for rule in rules if rule.appliesToHand(self)] 0668 0669 @staticmethod 0670 def maxLimitRule(usedRules): 0671 """return the rule with the highest limit score or None""" 0672 result = None 0673 maxLimit = 0 0674 usedRules = [x for x in usedRules if x.rule.score.limits] 0675 for usedRule in usedRules: 0676 score = usedRule.rule.score 0677 if score.limits > maxLimit: 0678 maxLimit = score.limits 0679 result = usedRule 0680 return result 0681 0682 def __totalScore(self): 0683 """use all used rules to compute the score""" 0684 maxRule = self.maxLimitRule(self.usedRules) 0685 maxLimit = 0.0 0686 pointsTotal = sum((x.rule.score for x in self.usedRules), 0687 Score(ruleset=self.ruleset)) 0688 if maxRule: 0689 maxLimit = maxRule.rule.score.limits 0690 if (maxLimit >= 1.0 0691 or maxLimit * self.ruleset.limit > pointsTotal.total()): 0692 self.usedRules = [maxRule] 0693 return Score(ruleset=self.ruleset, limits=maxLimit) 0694 return pointsTotal 0695 0696 def total(self): 0697 """total points of hand""" 0698 return self.score.total() 0699 0700 @staticmethod 0701 def __separateBonusMelds(tileStrings): 0702 """One meld per bonus tile. Others depend on that.""" 0703 bonusMelds = MeldList() 0704 for tileString in tileStrings[:]: 0705 if len(tileString) == 2: 0706 tile = Tile(tileString) 0707 if tile.isBonus: 0708 bonusMelds.append(tile.single) 0709 tileStrings.remove(tileString) 0710 return bonusMelds, tileStrings 0711 0712 def explain(self): 0713 """explain what rules were used for this hand""" 0714 usedRules = self.player.sortRulesByX(self.usedRules) 0715 result = [x.rule.explain(x.meld) for x in usedRules 0716 if x.rule.score.points] 0717 result.extend( 0718 [x.rule.explain(x.meld) for x in usedRules 0719 if x.rule.score.doubles]) 0720 result.extend( 0721 [x.rule.explain(x.meld) for x in usedRules 0722 if not x.rule.score.points and not x.rule.score.doubles]) 0723 if any(x.rule.debug for x in usedRules): 0724 result.append(str(self)) 0725 return result 0726 0727 def doublesEstimate(self, discard=None): 0728 """this is only an estimate because it only uses meldRules and handRules, 0729 but not things like mjRules, winnerRules, loserRules""" 0730 result = 0 0731 if discard and self.tiles.count(discard) == 2: 0732 melds = chain(self.melds, self.bonusMelds, [discard.exposed.pung]) 0733 else: 0734 melds = chain(self.melds, self.bonusMelds) 0735 for meld in melds: 0736 result += sum(x.score.doubles for x in meld.doublingRules(self)) 0737 for rule in self.ruleset.doublingHandRules: 0738 if rule.appliesToHand(self): 0739 result += rule.score.doubles 0740 return result 0741 0742 def __str__(self): 0743 """hand as a string""" 0744 return self.newString() 0745 0746 def __hash__(self): 0747 """used for debug logging to identify the hand""" 0748 if not hasattr(self, 'string'): 0749 return 0 0750 md5sum = md5() 0751 md5sum.update(self.player.name.encode('utf-8')) 0752 md5sum.update(self.string.encode()) 0753 digest = md5sum.digest() 0754 assert len(digest) == 16 0755 result = 0 0756 for part in range(4): 0757 result = (result << 8) + digest[part] 0758 return result