def _clientDiscarded4(self, dummyResults, msg, dangerousText, mustPlayDangerous): """client told us he discarded a tile. Continue, check for dangerous game""" block = DeferredBlock(self) player = msg.player if dangerousText: if mustPlayDangerous and not player.lastSource.isDiscarded: if Debug.dangerousGame: tile = Tile(msg.args[0]) logDebug( '%s claims no choice. Discarded %s, keeping %s. %s' % (player, tile, ''.join( player.concealedTiles), ' / '.join(dangerousText))) player.claimedNoChoice = True block.tellAll(player, Message.NoChoice, tiles=TileList(player.concealedTiles)) else: player.playedDangerous = True if Debug.dangerousGame: tile = Tile(msg.args[0]) logDebug( '%s played dangerous. Discarded %s, keeping %s. %s' % (player, tile, ''.join( player.concealedTiles), ' / '.join(dangerousText))) block.tellAll(player, Message.DangerousGame, tiles=TileList(player.concealedTiles)) if msg.answer == Message.OriginalCall: player.isCalling = True block.callback(self.clientMadeOriginalCall, msg) else: block.callback(self._askForClaims, msg)
def without(self, remove): """self without tile. The rest will be uppercased.""" tiles = TileList() for tile in self: if tile is remove: remove = None else: tiles.append(tile.concealed) return tiles
def __parseString(self, inString): """parse the string passed to Hand()""" # pylint: disable=too-many-branches tileStrings = [] for part in inString.split(): partId = part[0] if partId == 'm': if len(part) > 1: try: self.__lastSource = TileSource.byChar[part[1]] except KeyError: raise Exception('{} has unknown lastTile {}'.format(inString, part[1])) if len(part) > 2: self.__announcements = set(part[2]) elif partId == 'L': if len(part[1:]) > 8: raise Exception( 'last tile cannot complete a kang:' + inString) if len(part) > 3: self.__lastMeld = Meld(part[3:]) self.__lastTile = Tile(part[1:3]) else: if part != 'R': tileStrings.append(part) self.bonusMelds, tileStrings = self.__separateBonusMelds(tileStrings) tileString = ' '.join(tileStrings) self.tiles = TileList(tileString.replace(' ', '').replace('R', '')) self.tiles.sort() for part in tileStrings[:]: if part[:1] != 'R': self.melds.append(Meld(part)) tileStrings.remove(part) self.values = tuple(x.value for x in self.tiles) self.suits = set(x.lowerGroup for x in self.tiles) self.declaredMelds = MeldList(x for x in self.melds if x.isDeclared) declaredTiles = list(sum((x for x in self.declaredMelds), [])) self.tilesInHand = TileList(x for x in self.tiles if x not in declaredTiles) self.lenOffset = (len(self.tiles) - 13 - sum(x.isKong for x in self.melds)) assert len(tileStrings) < 2, tileStrings self.__rest = TileList() if len(tileStrings): self.__rest.extend(TileList(tileStrings[0][1:])) last = self.__lastTile if last and not last.isBonus: assert last in self.tiles, \ 'lastTile %s is not in hand %s' % (last, str(self)) if self.__lastSource is TileSource.RobbedKong: assert self.tiles.count(last.exposed) + \ self.tiles.count(last.concealed) == 1, ( 'Robbing kong: I cannot have ' 'lastTile %s more than once in %s' % ( last, ' '.join(self.tiles)))
def check(cls): """check cache consistency""" for key, value in cls.cache.items(): assert key == value.key or key == str(value), 'cache wrong: cachekey=%s realkey=%s value=%s' % ( key, value.key, value) assert value.key == 1 + value.hashTable.index(value) / 2 assert value.key == TileList.key(value), \ 'static key:%s current key:%s, static value:%s, current value:%s ' % ( value.key, TileList.key(value), value.original, value)
def check(cls): """check cache consistency""" for key, value in cls.cache.items(): assert key == value.key or key == str( value), 'cache wrong: cachekey=%s realkey=%s value=%s' % ( key, value.key, value) assert value.key == 1 + value.hashTable.index(value) / 2 assert value.key == TileList.key(value), \ 'static key:%s current key:%s, static value:%s, current value:%s ' % ( value.key, TileList.key(value), value.original, value)
def __new__(cls, newContent=None): """try to use cache""" if isinstance(newContent, str) and newContent in cls.cache: return cls.cache[newContent] if isinstance(newContent, Meld): return newContent tiles = TileList(newContent) cacheKey = tiles.key() if cacheKey in cls.cache: return cls.cache[cacheKey] return TileList.__new__(cls, tiles)
def __possibleChows(self): """returns a unique list of lists with possible claimable chow combinations""" if self.game.lastDiscard is None: return [] exposedChows = [x for x in self._exposedMelds if x.isChow] if len(exposedChows) >= self.game.ruleset.maxChows: return [] tile = self.game.lastDiscard within = TileList(self.concealedTiles[:]) within.append(tile) return within.hasChows(tile)
def __init__(self, player, string, prevHand=None): """evaluate string for player. rules are to be applied in any case""" if hasattr(self, 'string'): # I am from cache return # shortcuts for speed: self._player = weakref.ref(player) self.ruleset = player.game.ruleset self.intelligence = player.intelligence if player else AIDefault() self.string = string self.__robbedTile = Tile.unknown self.prevHand = prevHand self.__won = None self.__score = None self.__callingHands = None self.__mjRule = None self.ruleCache = {} self.__lastTile = None self.__lastSource = TileSource.Unknown self.__announcements = set() self.__lastMeld = 0 self.__lastMelds = MeldList() self.tiles = None self.melds = MeldList() self.bonusMelds = MeldList() self.usedRules = [] self.__rest = TileList() self.__arranged = None self.__parseString(string) self.__won = self.lenOffset == 1 and player.mayWin if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): self.debug(fmt('{callers}', callers=callers(exclude=['__init__']))) Hand.indent += 1 self.debug('New Hand {} {}'.format(string, self.lenOffset)) try: self.__arrange() self.__calculate() self.__arranged = True except Hand.__NotWon as notwon: if Debug.mahJongg: self.debug(fmt(str(notwon))) self.__won = False self.__score = Score() finally: self._fixed = True if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): self.debug('Fixing {} {} {}'.format(self, self.won, self.score)) Hand.indent -= 1
def divided(self, dummyResults=None): """the wall is now divided for all clients""" if not self.running: return block = DeferredBlock(self) for clientPlayer in self.game.players: for player in self.game.players: if player == clientPlayer or self.game.playOpen: tiles = player.concealedTiles else: tiles = TileList(Tile.unknown * 13) block.tell(player, clientPlayer, Message.SetConcealedTiles, tiles=TileList(chain(tiles, player.bonusTiles))) block.callback(self.dealt)
def __init__(self, player, command, kwargs): if isinstance(command, Message): self.message = command else: self.message = Message.defined[command] self.table = None self.notifying = False self._player = weakref.ref(player) if player else None self.token = kwargs['token'] self.kwargs = kwargs.copy() del self.kwargs['token'] self.score = None self.lastMeld = None for key, value in kwargs.items(): assert not isinstance(value, bytes), 'value is bytes:{}'.format( repr(value)) if value is None: self.__setattr__(key, None) else: if key.lower().endswith('tile'): self.__setattr__(key, Tile(value)) elif key.lower().endswith('tiles'): self.__setattr__(key, TileList(value)) elif key.lower().endswith('meld'): self.__setattr__(key, Meld(value)) elif key.lower().endswith('melds'): self.__setattr__(key, MeldList(value)) elif key == 'playerNames': if Internal.isServer: self.__setattr__(key, value) else: self.__setattr__(key, self.__convertWinds(value)) else: self.__setattr__(key, value)
def __init__(self, player, string, prevHand=None): """evaluate string for player. rules are to be applied in any case""" if hasattr(self, 'string'): # I am from cache return # shortcuts for speed: self._player = weakref.ref(player) self.ruleset = player.game.ruleset self.intelligence = player.intelligence if player else AIDefault() self.string = string self.__robbedTile = Tile.unknown self.prevHand = prevHand self.__won = None self.__score = None self.__callingHands = None self.__mjRule = None self.ruleCache = {} self.__lastTile = None self.__lastSource = TileSource.Unknown self.__announcements = set() self.__lastMeld = 0 self.__lastMelds = MeldList() self.tiles = None self.melds = MeldList() self.bonusMelds = MeldList() self.usedRules = [] self.__rest = TileList() self.__arranged = None self.__parseString(string) self.__won = self.lenOffset == 1 and player.mayWin if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): self.debug(fmt('{callers}', callers=callers(exclude=['__init__']))) Hand.indent += 1 _hideString = string self.debug(fmt('New Hand {_hideString} {self.lenOffset}')) try: self.__arrange() self.__calculate() self.__arranged = True except Hand.__NotWon as notwon: if Debug.mahJongg: self.debug(fmt(str(notwon))) self.__won = False self.__score = Score() finally: self._fixed = True if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): _hideSelf = str(self) _hideScore = str(self.score) self.debug(fmt( 'Fixing {_hideSelf} {self.won} {_hideScore}')) Hand.indent -= 1
def callingTest(self, string, expected): """test a calling hand""" for idx, ruleset in enumerate(RULESETS): game = GAMES[idx] game.players[0].clearCache() hand = Hand(game.players[0], string) testSays = TileList( set(x.lastTile.exposed for x in hand.callingHands)).sorted() if isinstance(expected, list): if idx >= len(expected): idx %= len(RULESETS) // 2 expIdx = idx if expIdx >= len(expected): expIdx %= len(RULESETS) // 2 exp = expected[expIdx] else: exp = expected completingTiles = TileList(exp) self.assertTrue(testSays == completingTiles, '%s: %s may be completed by %s but testresult is %s' % ( ruleset.name, string, completingTiles or 'None', testSays or 'None'))
class TestTileList(TestCase): tl = None def setUp(self): lv = LoadValues("test.txt", groups=True) self.tl = TileList() for tile in lv.raw_values: T = Tile(tile) self.tl.add_tile(T) def test_find_neigh_in_direction(self): t = self.tl.tiles[3079] s = self.tl.find_neigh_in_direction(3079, 2) print(s) t2 = self.tl.tiles[2473] t2.flip(True) t2.rotate(3) print(t2.get_side(2)) s2 = self.tl.find_neigh_in_direction(2473, 2) print(s2) def test_final_map(self): res = [ ".#.#..#.##...#.##..#####", "###....#.#....#..#......", "##.##.###.#.#..######...", "###.#####...#.#####.#..#", "##.#....#.##.####...#.##", "...########.#....#####.#", "....#..#...##..#.#.###..", ".####...#..#.....#......", "#..#.##..#..###.#.##....", "#.####..#.####.#.#.###..", "###.#.#...#.######.#..##", "#.####....##..########.#", "##..##.#...#...#.#.#.#..", "...#..#..#.#.##..###.###", ".#.#....#.##.#...###.##.", "###.#...#..#.##.######..", ".#.#.###.##.##.#..#.##..", ".####.###.#...###.#..#.#", "..#.#..#..#.#.#.####.###", "#..####...#.#.#.###.###.", "#####..#####...###....##", "#.##..#..#...#..####...#", ".#.###..##..##..####.##.", "...###...##...#...#..###" ]
def endHand(self, dummyResults=None): """hand is over, show all concealed tiles to all players""" if not self.running: return if self.game.playOpen: self.saveHand() else: block = DeferredBlock(self) for player in self.game.players: # there might be no winner, winner.others() would be wrong if player != self.game.winner: # the winner tiles are already shown in claimMahJongg block.tellOthers(player, Message.ShowConcealedTiles, show=True, tiles=TileList(player.concealedTiles)) block.callback(self.saveHand)
def __sub__(self, subtractTile): """returns a copy of self minus subtractTiles. Case of subtractTile (hidden or exposed) is ignored. subtractTile must either be undeclared or part of lastMeld. Exposed melds of length<3 will be hidden.""" # pylint: disable=too-many-branches # If lastMeld is given, it must be first in the list. # Next try undeclared melds, then declared melds assert self.lenOffset == 1 if self.lastTile: if self.lastTile is subtractTile and self.prevHand: return self.prevHand declaredMelds = self.declaredMelds tilesInHand = TileList(self.tilesInHand) boni = MeldList(self.bonusMelds) lastMeld = self.lastMeld if subtractTile.isBonus: for idx, meld in enumerate(boni): if subtractTile is meld[0]: del boni[idx] break else: if lastMeld and lastMeld.isDeclared and ( subtractTile.exposed in lastMeld.exposed): declaredMelds.remove(lastMeld) tilesInHand.extend(lastMeld.concealed) tilesInHand.remove(subtractTile.concealed) for meld in declaredMelds[:]: if len(meld) < 3: declaredMelds.remove(meld) tilesInHand.extend(meld.concealed) # if we robbed a kong, remove that announcement mjPart = '' announcements = self.announcements - set('k') if announcements: mjPart = 'm.' + ''.join(announcements) rest = 'R' + str(tilesInHand) newString = ' '.join(str(x) for x in ( declaredMelds, rest, boni, mjPart)) return Hand(self.player, newString, prevHand=self)
def __init__(self, newContent=None): """init the meld: content can be either - a single string with 2 chars for every tile - a list containing such strings - another meld. Its tiles are not passed. - a list of Tile objects""" if not hasattr(self, '_fixed'): # already defined if I am from cache TileList.__init__(self, newContent) self.case = ''.join('a' if x.islower() else 'A' for x in self) self.key = TileList.key(self) if self.key not in self.cache: self.cache[self.key] = self self.cache[str(self)] = self self.isExposed = self.__isExposed() self.isConcealed = not self.isExposed self.isSingle = self.isPair = self.isChow = self.isPung = False self.isKong = self.isClaimedKong = self.isKnitted = False self.isDragonMeld = len(self) and self[0].isDragon self.isWindMeld = len(self) and self[0].isWind self.isHonorMeld = self.isDragonMeld or self.isWindMeld self.isBonus = len(self) == 1 and self[0].isBonus self.isKnown = len(self) and self[0].isKnown self.__setMeldType() self.isPungKong = self.isPung or self.isKong self.isDeclared = self.isExposed or self.isKong groups = set(x.group.lower() for x in self) if len(groups) == 1: self.group = self[0].group self.lowerGroup = self.group.lower() else: self.group = 'X' self.lowerGroup = 'x' self.isRest = False self.__staticRules = {} # ruleset is key self.__dynamicRules = {} # ruleset is key self.__staticDoublingRules = {} # ruleset is key self.__dynamicDoublingRules = {} # ruleset is key self.__hasRules = None # unknown yet self.__hasDoublingRules = None # unknown yet self.concealed = self.exposed = self.declared = self.exposedClaimed = None # to satisfy pylint self._fixed = True if len(self) < 4: TileList.__setattr__(self, 'concealed', Meld(TileList(x.concealed for x in self))) TileList.__setattr__(self, 'declared', self.concealed) TileList.__setattr__(self, 'exposed', Meld(TileList(x.exposed for x in self))) TileList.__setattr__(self, 'exposedClaimed', self.exposed) else: TileList.__setattr__(self, 'concealed', Meld(TileList(x.concealed for x in self))) TileList.__setattr__( self, 'declared', Meld( TileList([ self[0].exposed, self[1].concealed, self[2].concealed, self[3].exposed ]))) TileList.__setattr__(self, 'exposed', Meld(TileList(x.exposed for x in self))) TileList.__setattr__( self, 'exposedClaimed', Meld( TileList([ self[0].exposed, self[1].exposed, self[2].exposed, self[3].concealed ])))
def __setattr__(self, name, value): if (hasattr(self, '_fixed') and not name.endswith('__hasRules') and not name.endswith('__hasDoublingRules')): raise TypeError TileList.__setattr__(self, name, value)
def __init__(self, newContent=None): """init the meld: content can be either - a single string with 2 chars for every tile - a list containing such strings - another meld. Its tiles are not passed. - a list of Tile objects""" if not hasattr(self, '_fixed'): # already defined if I am from cache TileList.__init__(self, newContent) self.case = ''.join('a' if x.islower() else 'A' for x in self) self.key = TileList.key(self) if self.key not in self.cache: self.cache[self.key] = self self.cache[str(self)] = self self.isExposed = self.__isExposed() self.isConcealed = not self.isExposed self.isSingle = self.isPair = self.isChow = self.isPung = False self.isKong = self.isClaimedKong = self.isKnitted = False self.isDragonMeld = len(self) and self[0].isDragon self.isWindMeld = len(self) and self[0].isWind self.isHonorMeld = self.isDragonMeld or self.isWindMeld self.isBonus = len(self) == 1 and self[0].isBonus self.isKnown = len(self) and self[0].isKnown self.__setMeldType() self.isPungKong = self.isPung or self.isKong self.isDeclared = self.isExposed or self.isKong groups = set(x.group.lower() for x in self) if len(groups) == 1: self.group = self[0].group self.lowerGroup = self.group.lower() else: self.group = 'X' self.lowerGroup = 'x' self.isRest = False self.__staticRules = {} # ruleset is key self.__dynamicRules = {} # ruleset is key self.__staticDoublingRules = {} # ruleset is key self.__dynamicDoublingRules = {} # ruleset is key self.__hasRules = None # unknown yet self.__hasDoublingRules = None # unknown yet self.concealed = self.exposed = self.declared = self.exposedClaimed = None # to satisfy pylint self._fixed = True if len(self) < 4: TileList.__setattr__( self, 'concealed', Meld(TileList(x.concealed for x in self))) TileList.__setattr__(self, 'declared', self.concealed) TileList.__setattr__( self, 'exposed', Meld(TileList(x.exposed for x in self))) TileList.__setattr__(self, 'exposedClaimed', self.exposed) else: TileList.__setattr__( self, 'concealed', Meld(TileList(x.concealed for x in self))) TileList.__setattr__( self, 'declared', Meld(TileList([self[0].exposed, self[1].concealed, self[2].concealed, self[3].exposed]))) TileList.__setattr__( self, 'exposed', Meld(TileList(x.exposed for x in self))) TileList.__setattr__( self, 'exposedClaimed', Meld(TileList([self[0].exposed, self[1].exposed, self[2].exposed, self[3].concealed])))
def setUp(self): lv = LoadValues("test.txt", groups=True) self.tl = TileList() for tile in lv.raw_values: T = Tile(tile) self.tl.add_tile(T)
class Hand(object): """represent the hand to be evaluated. lenOffset is <0 for a short hand 0 for a correct calling hand 1 for a correct winner hand or a long loser hand >1 for a long winner hand Of course ignoring bonus tiles and respecting kong replacement tiles. if there are no kongs, 13 tiles will return 0 We assume that long hands never happen. For manual scoring, this should be asserted by the caller after creating the Hand instance. If the Hand has lenOffset 1 but is no winning hand, the Hand instance will not be fully evaluated, it is given Score 0 and hand.won == False. declaredMelds are those which cannot be changed anymore: Chows, Pungs, Kongs. tilesInHand are those not in declaredMelds Only tiles passed in the 'R' substring may be rearranged. mjRule is the one out of mjRules with the highest resulting score. Every hand gets an mjRule even it is not a wining hand, it is the one which was used for rearranging the hiden tiles to melds. suits include dragons and winds.""" # pylint: disable=too-many-instance-attributes indent = 0 class __NotWon(UserWarning): # pylint: disable=invalid-name """should be won but is not a winning hand""" def __new__(cls, player, string, prevHand=None): # pylint: disable=unused-argument """since a Hand instance is never changed, we can use a cache""" cache = player.handCache cacheKey = string if cacheKey in cache: result = cache[cacheKey] player.cacheHits += 1 return result player.cacheMisses += 1 result = object.__new__(cls) cache[cacheKey] = result return result def __init__(self, player, string, prevHand=None): """evaluate string for player. rules are to be applied in any case""" if hasattr(self, 'string'): # I am from cache return # shortcuts for speed: self._player = weakref.ref(player) self.ruleset = player.game.ruleset self.intelligence = player.intelligence if player else AIDefault() self.string = string self.__robbedTile = Tile.unknown self.prevHand = prevHand self.__won = None self.__score = None self.__callingHands = None self.__mjRule = None self.ruleCache = {} self.__lastTile = None self.__lastSource = TileSource.Unknown self.__announcements = set() self.__lastMeld = 0 self.__lastMelds = MeldList() self.tiles = None self.melds = MeldList() self.bonusMelds = MeldList() self.usedRules = [] self.__rest = TileList() self.__arranged = None self.__parseString(string) self.__won = self.lenOffset == 1 and player.mayWin if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): self.debug(fmt('{callers}', callers=callers(exclude=['__init__']))) Hand.indent += 1 _hideString = string self.debug(fmt('New Hand {_hideString} {self.lenOffset}')) try: self.__arrange() self.__calculate() self.__arranged = True except Hand.__NotWon as notwon: if Debug.mahJongg: self.debug(fmt(str(notwon))) self.__won = False self.__score = Score() finally: self._fixed = True if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): _hideSelf = str(self) _hideScore = str(self.score) self.debug(fmt( 'Fixing {_hideSelf} {self.won} {_hideScore}')) Hand.indent -= 1 def __parseString(self, inString): """parse the string passed to Hand()""" # pylint: disable=too-many-branches tileStrings = [] for part in inString.split(): partId = part[0] if partId == 'm': if len(part) > 1: try: self.__lastSource = TileSource.byChar[part[1]] except KeyError: raise Exception('{} has unknown lastTile {}'.format(inString, part[1])) if len(part) > 2: self.__announcements = set(part[2]) elif partId == 'L': if len(part[1:]) > 8: raise Exception( 'last tile cannot complete a kang:' + inString) if len(part) > 3: self.__lastMeld = Meld(part[3:]) self.__lastTile = Tile(part[1:3]) else: if part != 'R': tileStrings.append(part) self.bonusMelds, tileStrings = self.__separateBonusMelds(tileStrings) tileString = ' '.join(tileStrings) self.tiles = TileList(tileString.replace(' ', '').replace('R', '')) self.tiles.sort() for part in tileStrings[:]: if part[:1] != 'R': self.melds.append(Meld(part)) tileStrings.remove(part) self.values = tuple(x.value for x in self.tiles) self.suits = set(x.lowerGroup for x in self.tiles) self.declaredMelds = MeldList(x for x in self.melds if x.isDeclared) declaredTiles = list(sum((x for x in self.declaredMelds), [])) self.tilesInHand = TileList(x for x in self.tiles if x not in declaredTiles) self.lenOffset = (len(self.tiles) - 13 - sum(x.isKong for x in self.melds)) assert len(tileStrings) < 2, tileStrings self.__rest = TileList() if len(tileStrings): self.__rest.extend(TileList(tileStrings[0][1:])) last = self.__lastTile if last and not last.isBonus: assert last in self.tiles, \ 'lastTile %s is not in hand %s' % (last, str(self)) if self.__lastSource is TileSource.RobbedKong: assert self.tiles.count(last.exposed) + \ self.tiles.count(last.concealed) == 1, ( 'Robbing kong: I cannot have ' 'lastTile %s more than once in %s' % ( last, ' '.join(self.tiles))) @property def arranged(self): """readonly""" return self.__arranged @property def player(self): """weakref""" return self._player() @property def ownWind(self): """for easier usage""" return self.player.wind @property def roundWind(self): """for easier usage""" return self.player.game.roundWind def __calculate(self): """apply rules, calculate score""" assert not self.__rest, ( 'Hand.__calculate expects there to be no rest tiles: %s' % self) oldWon = self.__won self.__applyRules() if len(self.lastMelds) > 1: self.__applyBestLastMeld() if self.__won != oldWon: # if not won after all, this might be a long hand. # So we might even have to unapply meld rules and # bonus points. Instead just recompute all again. # This should only happen with scoring manual games # and with scoringtest - normally kajongg would not # let you declare an invalid mah jongg self.__applyRules() def hasTiles(self): """tiles are assigned to this hand""" return self.tiles or self.bonusMelds @property def mjRule(self): """getter""" return self.__mjRule @mjRule.setter def mjRule(self, value): """changing mjRule must reset score""" if self.__mjRule != value: self.__mjRule = value self.__score = None @property def lastTile(self): """compute and cache, readonly""" return self.__lastTile @property def lastSource(self): """compute and cache, readonly""" return self.__lastSource @property def announcements(self): """compute and cache, readonly""" return self.__announcements @property def score(self): """calculate it first if not yet done""" if self.__score is None and self.__arranged is not None: self.__score = Score() self.__calculate() return self.__score @property def lastMeld(self): """compute and cache, readonly""" if self.__lastMeld == 0: self.__setLastMeld() return self.__lastMeld @property def lastMelds(self): """compute and cache, readonly""" if self.__lastMeld == 0: self.__setLastMeld() return self.__lastMelds @property def won(self): """do we really have a winner hand?""" return self.__won def debug(self, msg): """try to use Game.debug so we get a nice prefix""" idPrefix = Fmt.num_encode(hash(self)) if self.prevHand: idPrefix += '<{}'.format(Fmt.num_encode(hash(self.prevHand))) idPrefix = 'Hand({})'.format(idPrefix) self.player.game.debug(' '.join([dbgIndent(self, self.prevHand), idPrefix, msg])) def __applyRules(self): """find out which rules apply, collect in self.usedRules""" self.usedRules = [] for meld in chain(self.melds, self.bonusMelds): self.usedRules.extend(UsedRule(x, meld) for x in meld.rules(self)) for rule in self.ruleset.handRules: if rule.appliesToHand(self): self.usedRules.append(UsedRule(rule)) self.__score = self.__totalScore() self.ruleCache.clear() # do the rest only if we know all tiles of the hand if Tile.unknown in self.string: return if self.__won: matchingMJRules = self.__maybeMahjongg() if not matchingMJRules: self.__score = Score() raise Hand.__NotWon('no matching MJ Rule') self.__mjRule = matchingMJRules[0] self.usedRules.append(UsedRule(self.__mjRule)) self.usedRules.extend(self.matchingWinnerRules()) self.__score = self.__totalScore() else: # not self.won loserRules = self.__matchingRules(self.ruleset.loserRules) if loserRules: self.usedRules.extend(list(UsedRule(x) for x in loserRules)) self.__score = self.__totalScore() self.__checkHasExclusiveRules() def matchingWinnerRules(self): """returns a list of matching winner rules""" matching = list( UsedRule(x) for x in self.__matchingRules(self.ruleset.winnerRules)) limitRule = self.maxLimitRule(matching) return [limitRule] if limitRule else matching def __checkHasExclusiveRules(self): """if we have one, remove all others""" exclusive = list(x for x in self.usedRules if 'absolute' in x.rule.options) if exclusive: self.usedRules = exclusive self.__score = self.__totalScore() if self.__won and not bool(self.__maybeMahjongg()): raise Hand.__NotWon(fmt('exclusive rule {exclusive} does not win')) def __setLastMeld(self): """sets the shortest possible last meld. This is not yet the final choice, see __applyBestLastMeld""" self.__lastMeld = None if self.lastTile and self.__won: if self.mjRule: self.__lastMelds = self.mjRule.computeLastMelds(self) if self.__lastMelds: # syncHandBoard may return nothing if len(self.__lastMelds) == 1: self.__lastMeld = self.__lastMelds[0] else: totals = sorted( (len(x), idx) for idx, x in enumerate(self.__lastMelds)) self.__lastMeld = self.__lastMelds[totals[0][1]] if not self.__lastMeld: self.__lastMeld = self.lastTile.single self.__lastMelds = MeldList(self.__lastMeld) def __applyBestLastMeld(self): """select the last meld giving the highest score (only winning variants)""" assert len(self.lastMelds) > 1 totals = [] prev = self.lastMeld for rule in self.usedRules: assert isinstance(rule, UsedRule) for lastMeld in self.lastMelds: self.__lastMeld = lastMeld try: self.__applyRules() totals.append((self.__totalScore().total(), lastMeld)) except Hand.__NotWon: pass if totals: totals = sorted(totals) # sort by totalScore maxScore = totals[-1][0] totals = list(x[1] for x in totals if x[0] == maxScore) # now we have a list of only lastMelds reaching maximum score if prev not in totals or self.__lastMeld not in totals: if Debug.explain and prev not in totals: if not self.player.game.belongsToRobotPlayer(): self.debug(fmt( 'replaced last meld {prev} with {totals[0]}')) self.__lastMeld = totals[0] self.__applyRules() def chancesToWin(self): """count the physical tiles that make us win and still seem availabe""" assert self.lenOffset == 0 result = [] for completedHand in self.callingHands: result.extend( [completedHand.lastTile] * (self.player.tileAvailable(completedHand.lastTile, self))) return result def newString(self, melds=1, rest=1, lastSource=1, announcements=1, lastTile=1, lastMeld=1): """create string representing a hand. Default is current Hand, but every part can be overridden or excluded by passing None""" if melds == 1: melds = chain(self.melds, self.bonusMelds) if rest == 1: rest = self.__rest if lastSource == 1: lastSource = self.lastSource if announcements == 1: announcements = self.announcements if lastTile == 1: lastTile = self.lastTile if lastMeld == 1: lastMeld = self.__lastMeld parts = list(str(x) for x in sorted(melds)) if rest: parts.append('R' + ''.join(str(x) for x in sorted(rest))) if lastSource or announcements: parts.append('m{}{}'.format( self.lastSource.char, ''.join(self.announcements))) if lastTile: parts.append('L{}{}'.format(lastTile, lastMeld if lastMeld else '')) return ' '.join(parts).strip() def __add__(self, addTile): """returns a new Hand built from this one plus addTile""" assert addTile.isConcealed, 'addTile %s should be concealed:' % addTile # combine all parts about hidden tiles plus the new one to one part # because something like DrDrS8S9 plus S7 will have to be reordered # anyway newString = self.newString( melds=chain(self.declaredMelds, self.bonusMelds), rest=self.tilesInHand + [addTile], lastSource=None, lastTile=addTile, lastMeld=None ) return Hand(self.player, newString, prevHand=self) def __sub__(self, subtractTile): """returns a copy of self minus subtractTiles. Case of subtractTile (hidden or exposed) is ignored. subtractTile must either be undeclared or part of lastMeld. Exposed melds of length<3 will be hidden.""" # pylint: disable=too-many-branches # If lastMeld is given, it must be first in the list. # Next try undeclared melds, then declared melds assert self.lenOffset == 1 if self.lastTile: if self.lastTile is subtractTile and self.prevHand: return self.prevHand declaredMelds = self.declaredMelds tilesInHand = TileList(self.tilesInHand) boni = MeldList(self.bonusMelds) lastMeld = self.lastMeld if subtractTile.isBonus: for idx, meld in enumerate(boni): if subtractTile is meld[0]: del boni[idx] break else: if lastMeld and lastMeld.isDeclared and ( subtractTile.exposed in lastMeld.exposed): declaredMelds.remove(lastMeld) tilesInHand.extend(lastMeld.concealed) tilesInHand.remove(subtractTile.concealed) for meld in declaredMelds[:]: if len(meld) < 3: declaredMelds.remove(meld) tilesInHand.extend(meld.concealed) # if we robbed a kong, remove that announcement mjPart = '' announcements = self.announcements - set('k') if announcements: mjPart = 'm.' + ''.join(announcements) rest = 'R' + str(tilesInHand) newString = ' '.join(str(x) for x in ( declaredMelds, rest, boni, mjPart)) return Hand(self.player, newString, prevHand=self) def manualRuleMayApply(self, rule): """returns True if rule has selectable() and applies to this hand""" if self.__won and rule in self.ruleset.loserRules: return False if not self.__won and rule in self.ruleset.winnerRules: return False return rule.selectable(self) or rule.appliesToHand(self) # needed for activated rules @property def callingHands(self): """the hand is calling if it only needs one tile for mah jongg. Returns all hands which would only need one tile. If mustBeAvailable is True, make sure the missing tile might still be available. """ if self.__callingHands is None: self.__callingHands = self.__findAllCallingHands() return self.__callingHands def __findAllCallingHands(self): """always try to find all of them""" result = [] string = self.string if ' x' in string or self.lenOffset: return result candidates = [] for rule in self.ruleset.mjRules: cand = rule.winningTileCandidates(self) if Debug.hand and cand: # Py2 and Py3 show sets differently candis = ''.join(str(x) for x in sorted(cand)) # pylint: disable=unused-variable self.debug(fmt('callingHands found {candis} for {rule}')) candidates.extend(x.concealed for x in cand) for tile in sorted(set(candidates)): if sum(x.exposed == tile.exposed for x in self.tiles) == 4: continue hand = self + tile if hand.won: result.append(hand) if Debug.hand: _hiderules = ', '.join(set(x.mjRule.name for x in result)) if _hiderules: self.debug(fmt('Is calling {_hiderules}')) return result @property def robbedTile(self): """cache this here for use in rulecode""" if self.__robbedTile is Tile.unknown: self.__robbedTile = None if self.player.game.moves: # scoringtest does not (yet) simulate this lastMove = self.player.game.moves[-1] if (lastMove.message == Message.DeclaredKong and lastMove.player != self.player): self.__robbedTile = lastMove.meld[1] # we want it concealed only for a hidden Kong return self.__robbedTile def __maybeMahjongg(self): """check if this is a mah jongg hand. Return a sorted list of matching MJ rules, highest total first. If no rule matches, return None""" if self.lenOffset == 1 and self.player.mayWin: matchingMJRules = [x for x in self.ruleset.mjRules if x.appliesToHand(self)] if matchingMJRules: if self.robbedTile and self.robbedTile.isConcealed: # Millington 58: robbing hidden kong is only # allowed for 13 orphans matchingMJRules = [ x for x in matchingMJRules if 'mayrobhiddenkong' in x.options] result = sorted(matchingMJRules, key=lambda x: -x.score.total()) if Debug.mahJongg: self.debug(fmt('{callers} Found {matchingMJRules}', callers=callers())) return result def __arrangements(self): """find all legal arrangements. Returns a list of tuples with the mjRule and a list of concealed melds""" self.__rest.sort() result = [] stdMJ = self.ruleset.standardMJRule if self.mjRule: rules = [self.mjRule] else: rules = self.ruleset.mjRules for mjRule in rules: if ((self.lenOffset == 1 and mjRule.appliesToHand(self)) or (self.lenOffset < 1 and mjRule.shouldTry(self))): if self.__rest: for melds, rest2 in mjRule.rearrange(self, self.__rest[:]): if rest2: melds = list(melds) restMelds, _ = next( stdMJ.rearrange(self, rest2[:])) melds.extend(restMelds) result.append((mjRule, melds)) if not result: result.extend( (stdMJ, x[0]) for x in stdMJ.rearrange(self, self.__rest[:])) return result def __arrange(self): """work hard to always return the variant with the highest Mah Jongg value.""" if any(not x.isKnown for x in self.__rest): melds, rest = divmod(len(self.__rest), 3) self.melds.extend([Tile.unknown.pung] * melds) if rest: self.melds.append(Meld(Tile.unknown * rest)) self.__rest = [] if not self.__rest: self.melds.sort() mjRules = self.__maybeMahjongg() if self.won: if not mjRules: # how could this ever happen? raise Hand.__NotWon('Long Hand with no rest') self.mjRule = mjRules[0] return wonHands = [] lostHands = [] for mjRule, melds in self.__arrangements(): allMelds = self.melds[:] + list(melds) lastTile = self.lastTile if self.lastSource and self.lastSource.isDiscarded: lastTile = lastTile.exposed lastMelds = sorted( (x for x in allMelds if not x.isDeclared and lastTile.concealed in x), key=lambda x: len(x)) # pylint: disable=unnecessary-lambda if lastMelds: allMelds.remove(lastMelds[0]) allMelds.append(lastMelds[0].exposed) _ = self.newString( chain(allMelds, self.bonusMelds), rest=None, lastTile=lastTile, lastMeld=None) tryHand = Hand(self.player, _, prevHand=self) if tryHand.won: tryHand.mjRule = mjRule wonHands.append((mjRule, melds, tryHand)) else: lostHands.append((mjRule, melds, tryHand)) # we prefer a won Hand even if a lost Hand might have a higher score tryHands = wonHands if wonHands else lostHands bestRule, bestVariant, _ = max(tryHands, key=lambda x: x[2]) if wonHands: self.mjRule = bestRule self.melds.extend(bestVariant) self.melds.sort() self.__rest = [] self.ruleCache.clear() assert sum(len(x) for x in self.melds) == len(self.tiles), ( '%s != %s' % (self.melds, self.tiles)) def __gt__(self, other): """compares hand values""" assert self.player == other.player if not other.arranged: return True if self.won and not (other.arranged and other.won): return True elif not (self.arranged and self.won) and other.won: return False else: return (self.intelligence.handValue(self) > self.intelligence.handValue(other)) def __lt__(self, other): """compares hand values""" return other.__gt__(self) def __eq__(self, other): """compares hand values""" assert self.player == other.player return self.string == other.string def __ne__(self, other): """compares hand values""" assert self.player == other.player return self.string != other.string def __matchingRules(self, rules): """return all matching rules for this hand""" return list(rule for rule in rules if rule.appliesToHand(self)) @staticmethod def maxLimitRule(usedRules): """returns the rule with the highest limit score or None""" result = None maxLimit = 0 usedRules = list(x for x in usedRules if x.rule.score.limits) for usedRule in usedRules: score = usedRule.rule.score if score.limits > maxLimit: maxLimit = score.limits result = usedRule return result def __totalScore(self): """use all used rules to compute the score""" maxRule = self.maxLimitRule(self.usedRules) maxLimit = 0.0 pointsTotal = sum((x.rule.score for x in self.usedRules), Score(ruleset=self.ruleset)) if maxRule: maxLimit = maxRule.rule.score.limits if (maxLimit >= 1.0 or maxLimit * self.ruleset.limit > pointsTotal.total()): self.usedRules = [maxRule] return Score(ruleset=self.ruleset, limits=maxLimit) return pointsTotal def total(self): """total points of hand""" return self.score.total() @staticmethod def __separateBonusMelds(tileStrings): """One meld per bonus tile. Others depend on that.""" bonusMelds = MeldList() for tileString in tileStrings[:]: if len(tileString) == 2: tile = Tile(tileString) if tile.isBonus: bonusMelds.append(tile.single) tileStrings.remove(tileString) return bonusMelds, tileStrings def explain(self): """explain what rules were used for this hand""" usedRules = self.player.sortRulesByX(self.usedRules) result = [x.rule.explain(x.meld) for x in usedRules if x.rule.score.points] result.extend( [x.rule.explain(x.meld) for x in usedRules if x.rule.score.doubles]) result.extend( [x.rule.explain(x.meld) for x in usedRules if not x.rule.score.points and not x.rule.score.doubles]) if any(x.rule.debug for x in usedRules): result.append(str(self)) return result def doublesEstimate(self, discard=None): """this is only an estimate because it only uses meldRules and handRules, but not things like mjRules, winnerRules, loserRules""" result = 0 if discard and self.tiles.count(discard) == 2: melds = chain(self.melds, self.bonusMelds, [discard.exposed.pung]) else: melds = chain(self.melds, self.bonusMelds) for meld in melds: result += sum(x.score.doubles for x in meld.doublingRules(self)) for rule in self.ruleset.doublingHandRules: if rule.appliesToHand(self): result += rule.score.doubles return result def __str__(self): """hand as a string""" return self.newString() def __unicode__(self): """hand as a string""" return self.newString() def __repr__(self): """the default representation""" return 'Hand(%s)' % str(self) def __hash__(self): """used for debug logging to identify the hand""" if not hasattr(self, 'string'): return 0 md5sum = md5() if isPython3: md5sum.update(self.player.name.encode('utf-8')) md5sum.update(self.string.encode()) else: md5sum.update(self.player.name.encode('utf-8')) md5sum.update(self.string) digest = md5sum.digest() assert len(digest) == 16 result = 0 if isPython3: for part in range(4): result = (result << 8) + digest[part] else: for part in range(4): result = (result << 8) + ord(digest[part]) return result
def tiles(self): """flat view of all tiles in all melds""" return TileList(sum(self, []))
class Hand(StrMixin): """represent the hand to be evaluated. lenOffset is <0 for a short hand 0 for a correct calling hand 1 for a correct winner hand or a long loser hand >1 for a long winner hand Of course ignoring bonus tiles and respecting kong replacement tiles. if there are no kongs, 13 tiles will return 0 We assume that long hands never happen. For manual scoring, this should be asserted by the caller after creating the Hand instance. If the Hand has lenOffset 1 but is no winning hand, the Hand instance will not be fully evaluated, it is given Score 0 and hand.won == False. declaredMelds are those which cannot be changed anymore: Chows, Pungs, Kongs. tilesInHand are those not in declaredMelds Only tiles passed in the 'R' substring may be rearranged. mjRule is the one out of mjRules with the highest resulting score. Every hand gets an mjRule even it is not a wining hand, it is the one which was used for rearranging the hiden tiles to melds. suits include dragons and winds.""" # pylint: disable=too-many-instance-attributes indent = 0 class __NotWon(UserWarning): # pylint: disable=invalid-name """should be won but is not a winning hand""" def __new__(cls, player, string, prevHand=None): # pylint: disable=unused-argument """since a Hand instance is never changed, we can use a cache""" cache = player.handCache cacheKey = string if cacheKey in cache: result = cache[cacheKey] player.cacheHits += 1 return result player.cacheMisses += 1 result = object.__new__(cls) cache[cacheKey] = result return result def __init__(self, player, string, prevHand=None): """evaluate string for player. rules are to be applied in any case""" if hasattr(self, 'string'): # I am from cache return # shortcuts for speed: self._player = weakref.ref(player) self.ruleset = player.game.ruleset self.intelligence = player.intelligence if player else AIDefault() self.string = string self.__robbedTile = Tile.unknown self.prevHand = prevHand self.__won = None self.__score = None self.__callingHands = None self.__mjRule = None self.ruleCache = {} self.__lastTile = None self.__lastSource = TileSource.Unknown self.__announcements = set() self.__lastMeld = 0 self.__lastMelds = MeldList() self.tiles = None self.melds = MeldList() self.bonusMelds = MeldList() self.usedRules = [] self.__rest = TileList() self.__arranged = None self.__parseString(string) self.__won = self.lenOffset == 1 and player.mayWin if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): self.debug(fmt('{callers}', callers=callers(exclude=['__init__']))) Hand.indent += 1 self.debug('New Hand {} {}'.format(string, self.lenOffset)) try: self.__arrange() self.__calculate() self.__arranged = True except Hand.__NotWon as notwon: if Debug.mahJongg: self.debug(fmt(str(notwon))) self.__won = False self.__score = Score() finally: self._fixed = True if Debug.hand or (Debug.mahJongg and self.lenOffset == 1): self.debug('Fixing {} {} {}'.format(self, self.won, self.score)) Hand.indent -= 1 def __parseString(self, inString): """parse the string passed to Hand()""" # pylint: disable=too-many-branches tileStrings = [] for part in inString.split(): partId = part[0] if partId == 'm': if len(part) > 1: try: self.__lastSource = TileSource.byChar[part[1]] except KeyError: raise Exception('{} has unknown lastTile {}'.format(inString, part[1])) if len(part) > 2: self.__announcements = set(part[2]) elif partId == 'L': if len(part[1:]) > 8: raise Exception( 'last tile cannot complete a kang:' + inString) if len(part) > 3: self.__lastMeld = Meld(part[3:]) self.__lastTile = Tile(part[1:3]) else: if part != 'R': tileStrings.append(part) self.bonusMelds, tileStrings = self.__separateBonusMelds(tileStrings) tileString = ' '.join(tileStrings) self.tiles = TileList(tileString.replace(' ', '').replace('R', '')) self.tiles.sort() for part in tileStrings[:]: if part[:1] != 'R': self.melds.append(Meld(part)) tileStrings.remove(part) self.values = tuple(x.value for x in self.tiles) self.suits = set(x.lowerGroup for x in self.tiles) self.declaredMelds = MeldList(x for x in self.melds if x.isDeclared) declaredTiles = list(sum((x for x in self.declaredMelds), [])) self.tilesInHand = TileList(x for x in self.tiles if x not in declaredTiles) self.lenOffset = (len(self.tiles) - 13 - sum(x.isKong for x in self.melds)) assert len(tileStrings) < 2, tileStrings self.__rest = TileList() if len(tileStrings): self.__rest.extend(TileList(tileStrings[0][1:])) last = self.__lastTile if last and not last.isBonus: assert last in self.tiles, \ 'lastTile %s is not in hand %s' % (last, str(self)) if self.__lastSource is TileSource.RobbedKong: assert self.tiles.count(last.exposed) + \ self.tiles.count(last.concealed) == 1, ( 'Robbing kong: I cannot have ' 'lastTile %s more than once in %s' % ( last, ' '.join(self.tiles))) @property def arranged(self): """readonly""" return self.__arranged @property def player(self): """weakref""" return self._player() @property def ownWind(self): """for easier usage""" return self.player.wind @property def roundWind(self): """for easier usage""" return self.player.game.roundWind def __calculate(self): """apply rules, calculate score""" assert not self.__rest, ( 'Hand.__calculate expects there to be no rest tiles: %s' % self) oldWon = self.__won self.__applyRules() if len(self.lastMelds) > 1: self.__applyBestLastMeld() if self.__won != oldWon: # if not won after all, this might be a long hand. # So we might even have to unapply meld rules and # bonus points. Instead just recompute all again. # This should only happen with scoring manual games # and with scoringtest - normally kajongg would not # let you declare an invalid mah jongg self.__applyRules() def hasTiles(self): """tiles are assigned to this hand""" return self.tiles or self.bonusMelds @property def mjRule(self): """getter""" return self.__mjRule @mjRule.setter def mjRule(self, value): """changing mjRule must reset score""" if self.__mjRule != value: self.__mjRule = value self.__score = None @property def lastTile(self): """compute and cache, readonly""" return self.__lastTile @property def lastSource(self): """compute and cache, readonly""" return self.__lastSource @property def announcements(self): """compute and cache, readonly""" return self.__announcements @property def score(self): """calculate it first if not yet done""" if self.__score is None and self.__arranged is not None: self.__score = Score() self.__calculate() return self.__score @property def lastMeld(self): """compute and cache, readonly""" if self.__lastMeld == 0: self.__setLastMeld() return self.__lastMeld @property def lastMelds(self): """compute and cache, readonly""" if self.__lastMeld == 0: self.__setLastMeld() return self.__lastMelds @property def won(self): """do we really have a winner hand?""" return self.__won def debug(self, msg): """try to use Game.debug so we get a nice prefix""" idPrefix = Fmt.num_encode(hash(self)) if self.prevHand: idPrefix += '<{}'.format(Fmt.num_encode(hash(self.prevHand))) idPrefix = 'Hand({})'.format(idPrefix) self.player.game.debug(' '.join([dbgIndent(self, self.prevHand), idPrefix, msg])) def __applyRules(self): """find out which rules apply, collect in self.usedRules""" self.usedRules = [] for meld in chain(self.melds, self.bonusMelds): self.usedRules.extend(UsedRule(x, meld) for x in meld.rules(self)) for rule in self.ruleset.handRules: if rule.appliesToHand(self): self.usedRules.append(UsedRule(rule)) self.__score = self.__totalScore() self.ruleCache.clear() # do the rest only if we know all tiles of the hand if Tile.unknown in self.string: return if self.__won: matchingMJRules = self.__maybeMahjongg() if not matchingMJRules: self.__score = Score() raise Hand.__NotWon('no matching MJ Rule') self.__mjRule = matchingMJRules[0] self.usedRules.append(UsedRule(self.__mjRule)) self.usedRules.extend(self.matchingWinnerRules()) self.__score = self.__totalScore() else: # not self.won loserRules = self.__matchingRules(self.ruleset.loserRules) if loserRules: self.usedRules.extend(list(UsedRule(x) for x in loserRules)) self.__score = self.__totalScore() self.__checkHasExclusiveRules() def matchingWinnerRules(self): """returns a list of matching winner rules""" matching = list( UsedRule(x) for x in self.__matchingRules(self.ruleset.winnerRules)) limitRule = self.maxLimitRule(matching) return [limitRule] if limitRule else matching def __checkHasExclusiveRules(self): """if we have one, remove all others""" exclusive = list(x for x in self.usedRules if 'absolute' in x.rule.options) if exclusive: self.usedRules = exclusive self.__score = self.__totalScore() if self.__won and not bool(self.__maybeMahjongg()): raise Hand.__NotWon(fmt('exclusive rule {exclusive} does not win')) def __setLastMeld(self): """sets the shortest possible last meld. This is not yet the final choice, see __applyBestLastMeld""" self.__lastMeld = None if self.lastTile and self.__won: if self.mjRule: self.__lastMelds = self.mjRule.computeLastMelds(self) if self.__lastMelds: # syncHandBoard may return nothing if len(self.__lastMelds) == 1: self.__lastMeld = self.__lastMelds[0] else: totals = sorted( (len(x), idx) for idx, x in enumerate(self.__lastMelds)) self.__lastMeld = self.__lastMelds[totals[0][1]] if not self.__lastMeld: self.__lastMeld = self.lastTile.single self.__lastMelds = MeldList(self.__lastMeld) def __applyBestLastMeld(self): """select the last meld giving the highest score (only winning variants)""" assert len(self.lastMelds) > 1 totals = [] prev = self.lastMeld for rule in self.usedRules: assert isinstance(rule, UsedRule) for lastMeld in self.lastMelds: self.__lastMeld = lastMeld try: self.__applyRules() totals.append((self.__totalScore().total(), lastMeld)) except Hand.__NotWon: pass if totals: totals = sorted(totals) # sort by totalScore maxScore = totals[-1][0] totals = list(x[1] for x in totals if x[0] == maxScore) # now we have a list of only lastMelds reaching maximum score if prev not in totals or self.__lastMeld not in totals: if Debug.explain and prev not in totals: if not self.player.game.belongsToRobotPlayer(): self.debug(fmt( 'replaced last meld {prev} with {totals[0]}')) self.__lastMeld = totals[0] self.__applyRules() def chancesToWin(self): """count the physical tiles that make us win and still seem available""" assert self.lenOffset == 0 result = [] for completedHand in self.callingHands: result.extend( [completedHand.lastTile] * (self.player.tileAvailable(completedHand.lastTile, self))) return result def newString(self, melds=1, rest=1, lastSource=1, announcements=1, lastTile=1, lastMeld=1): """create string representing a hand. Default is current Hand, but every part can be overridden or excluded by passing None""" if melds == 1: melds = chain(self.melds, self.bonusMelds) if rest == 1: rest = self.__rest if lastSource == 1: lastSource = self.lastSource if announcements == 1: announcements = self.announcements if lastTile == 1: lastTile = self.lastTile if lastMeld == 1: lastMeld = self.__lastMeld parts = list(str(x) for x in sorted(melds)) if rest: parts.append('R' + ''.join(str(x) for x in sorted(rest))) if lastSource or announcements: parts.append('m{}{}'.format( self.lastSource.char, ''.join(self.announcements))) if lastTile: parts.append('L{}{}'.format(lastTile, lastMeld if lastMeld else '')) return ' '.join(parts).strip() def __add__(self, addTile): """returns a new Hand built from this one plus addTile""" assert addTile.isConcealed, 'addTile %s should be concealed:' % addTile # combine all parts about hidden tiles plus the new one to one part # because something like DrDrS8S9 plus S7 will have to be reordered # anyway newString = self.newString( melds=chain(self.declaredMelds, self.bonusMelds), rest=self.tilesInHand + [addTile], lastSource=None, lastTile=addTile, lastMeld=None ) return Hand(self.player, newString, prevHand=self) def __sub__(self, subtractTile): """returns a copy of self minus subtractTiles. Case of subtractTile (hidden or exposed) is ignored. subtractTile must either be undeclared or part of lastMeld. Exposed melds of length<3 will be hidden.""" # pylint: disable=too-many-branches # If lastMeld is given, it must be first in the list. # Next try undeclared melds, then declared melds assert self.lenOffset == 1 if self.lastTile: if self.lastTile is subtractTile and self.prevHand: return self.prevHand declaredMelds = self.declaredMelds tilesInHand = TileList(self.tilesInHand) boni = MeldList(self.bonusMelds) lastMeld = self.lastMeld if subtractTile.isBonus: for idx, meld in enumerate(boni): if subtractTile is meld[0]: del boni[idx] break else: if lastMeld and lastMeld.isDeclared and ( subtractTile.exposed in lastMeld.exposed): declaredMelds.remove(lastMeld) tilesInHand.extend(lastMeld.concealed) tilesInHand.remove(subtractTile.concealed) for meld in declaredMelds[:]: if len(meld) < 3: declaredMelds.remove(meld) tilesInHand.extend(meld.concealed) # if we robbed a kong, remove that announcement mjPart = '' announcements = self.announcements - set('k') if announcements: mjPart = 'm.' + ''.join(announcements) rest = 'R' + str(tilesInHand) newString = ' '.join(str(x) for x in ( declaredMelds, rest, boni, mjPart)) return Hand(self.player, newString, prevHand=self) def manualRuleMayApply(self, rule): """returns True if rule has selectable() and applies to this hand""" if self.__won and rule in self.ruleset.loserRules: return False if not self.__won and rule in self.ruleset.winnerRules: return False return rule.selectable(self) or rule.appliesToHand(self) # needed for activated rules @property def callingHands(self): """the hand is calling if it only needs one tile for mah jongg. Returns all hands which would only need one tile. If mustBeAvailable is True, make sure the missing tile might still be available. """ if self.__callingHands is None: self.__callingHands = self.__findAllCallingHands() return self.__callingHands def __findAllCallingHands(self): """always try to find all of them""" result = [] string = self.string if ' x' in string or self.lenOffset: return result candidates = [] for rule in self.ruleset.mjRules: cand = rule.winningTileCandidates(self) if Debug.hand and cand: # Py2 and Py3 show sets differently candis = ''.join(str(x) for x in sorted(cand)) # pylint: disable=unused-variable self.debug('callingHands found {} for {}'.format(candis, rule)) candidates.extend(x.concealed for x in cand) for tile in sorted(set(candidates)): if sum(x.exposed == tile.exposed for x in self.tiles) == 4: continue hand = self + tile if hand.won: result.append(hand) if Debug.hand: _hiderules = ', '.join(set(x.mjRule.name for x in result)) if _hiderules: self.debug(fmt('Is calling {_hiderules}')) return result @property def robbedTile(self): """cache this here for use in rulecode""" if self.__robbedTile is Tile.unknown: self.__robbedTile = None if self.player.game.moves: # scoringtest does not (yet) simulate this lastMove = self.player.game.moves[-1] if (lastMove.message == Message.DeclaredKong and lastMove.player != self.player): self.__robbedTile = lastMove.meld[1] # we want it concealed only for a hidden Kong return self.__robbedTile def __maybeMahjongg(self): """check if this is a mah jongg hand. Return a sorted list of matching MJ rules, highest total first. If no rule matches, return None""" if self.lenOffset == 1 and self.player.mayWin: matchingMJRules = [x for x in self.ruleset.mjRules if x.appliesToHand(self)] if matchingMJRules: if self.robbedTile and self.robbedTile.isConcealed: # Millington 58: robbing hidden kong is only # allowed for 13 orphans matchingMJRules = [ x for x in matchingMJRules if 'mayrobhiddenkong' in x.options] result = sorted(matchingMJRules, key=lambda x: -x.score.total()) if Debug.mahJongg: self.debug(fmt('{callers} Found {matchingMJRules}', callers=callers())) return result def __arrangements(self): """find all legal arrangements. Returns a list of tuples with the mjRule and a list of concealed melds""" self.__rest.sort() result = [] stdMJ = self.ruleset.standardMJRule if self.mjRule: rules = [self.mjRule] else: rules = self.ruleset.mjRules for mjRule in rules: if ((self.lenOffset == 1 and mjRule.appliesToHand(self)) or (self.lenOffset < 1 and mjRule.shouldTry(self))): if self.__rest: for melds, rest2 in mjRule.rearrange(self, self.__rest[:]): if rest2: melds = list(melds) restMelds, _ = next( stdMJ.rearrange(self, rest2[:])) melds.extend(restMelds) result.append((mjRule, melds)) if not result: result.extend( (stdMJ, x[0]) for x in stdMJ.rearrange(self, self.__rest[:])) return result def __arrange(self): """work hard to always return the variant with the highest Mah Jongg value.""" if any(not x.isKnown for x in self.__rest): melds, rest = divmod(len(self.__rest), 3) self.melds.extend([Tile.unknown.pung] * melds) if rest: self.melds.append(Meld(Tile.unknown * rest)) self.__rest = [] if not self.__rest: self.melds.sort() mjRules = self.__maybeMahjongg() if self.won: if not mjRules: # how could this ever happen? raise Hand.__NotWon('Long Hand with no rest') self.mjRule = mjRules[0] return wonHands = [] lostHands = [] for mjRule, melds in self.__arrangements(): allMelds = self.melds[:] + list(melds) lastTile = self.lastTile if self.lastSource and self.lastSource.isDiscarded: lastTile = lastTile.exposed lastMelds = sorted( (x for x in allMelds if not x.isDeclared and lastTile.concealed in x), key=lambda x: len(x)) # pylint: disable=unnecessary-lambda if lastMelds: allMelds.remove(lastMelds[0]) allMelds.append(lastMelds[0].exposed) _ = self.newString( chain(allMelds, self.bonusMelds), rest=None, lastTile=lastTile, lastMeld=None) tryHand = Hand(self.player, _, prevHand=self) if tryHand.won: tryHand.mjRule = mjRule wonHands.append((mjRule, melds, tryHand)) else: lostHands.append((mjRule, melds, tryHand)) # we prefer a won Hand even if a lost Hand might have a higher score tryHands = wonHands if wonHands else lostHands bestRule, bestVariant, _ = max(tryHands, key=lambda x: x[2]) if wonHands: self.mjRule = bestRule self.melds.extend(bestVariant) self.melds.sort() self.__rest = [] self.ruleCache.clear() assert sum(len(x) for x in self.melds) == len(self.tiles), ( '%s != %s' % (self.melds, self.tiles)) def __gt__(self, other): """compares hand values""" assert self.player == other.player if not other.arranged: return True if self.won and not (other.arranged and other.won): return True elif not (self.arranged and self.won) and other.won: return False else: return (self.intelligence.handValue(self) > self.intelligence.handValue(other)) def __lt__(self, other): """compares hand values""" return other.__gt__(self) def __eq__(self, other): """compares hand values""" assert self.player == other.player return self.string == other.string def __ne__(self, other): """compares hand values""" assert self.player == other.player return self.string != other.string def __matchingRules(self, rules): """return all matching rules for this hand""" return list(rule for rule in rules if rule.appliesToHand(self)) @staticmethod def maxLimitRule(usedRules): """returns the rule with the highest limit score or None""" result = None maxLimit = 0 usedRules = list(x for x in usedRules if x.rule.score.limits) for usedRule in usedRules: score = usedRule.rule.score if score.limits > maxLimit: maxLimit = score.limits result = usedRule return result def __totalScore(self): """use all used rules to compute the score""" maxRule = self.maxLimitRule(self.usedRules) maxLimit = 0.0 pointsTotal = sum((x.rule.score for x in self.usedRules), Score(ruleset=self.ruleset)) if maxRule: maxLimit = maxRule.rule.score.limits if (maxLimit >= 1.0 or maxLimit * self.ruleset.limit > pointsTotal.total()): self.usedRules = [maxRule] return Score(ruleset=self.ruleset, limits=maxLimit) return pointsTotal def total(self): """total points of hand""" return self.score.total() @staticmethod def __separateBonusMelds(tileStrings): """One meld per bonus tile. Others depend on that.""" bonusMelds = MeldList() for tileString in tileStrings[:]: if len(tileString) == 2: tile = Tile(tileString) if tile.isBonus: bonusMelds.append(tile.single) tileStrings.remove(tileString) return bonusMelds, tileStrings def explain(self): """explain what rules were used for this hand""" usedRules = self.player.sortRulesByX(self.usedRules) result = [x.rule.explain(x.meld) for x in usedRules if x.rule.score.points] result.extend( [x.rule.explain(x.meld) for x in usedRules if x.rule.score.doubles]) result.extend( [x.rule.explain(x.meld) for x in usedRules if not x.rule.score.points and not x.rule.score.doubles]) if any(x.rule.debug for x in usedRules): result.append(str(self)) return result def doublesEstimate(self, discard=None): """this is only an estimate because it only uses meldRules and handRules, but not things like mjRules, winnerRules, loserRules""" result = 0 if discard and self.tiles.count(discard) == 2: melds = chain(self.melds, self.bonusMelds, [discard.exposed.pung]) else: melds = chain(self.melds, self.bonusMelds) for meld in melds: result += sum(x.score.doubles for x in meld.doublingRules(self)) for rule in self.ruleset.doublingHandRules: if rule.appliesToHand(self): result += rule.score.doubles return result def __str__(self): """hand as a string""" return self.newString() def __hash__(self): """used for debug logging to identify the hand""" if not hasattr(self, 'string'): return 0 md5sum = md5() md5sum.update(self.player.name.encode('utf-8')) md5sum.update(self.string.encode()) digest = md5sum.digest() assert len(digest) == 16 result = 0 for part in range(4): result = (result << 8) + digest[part] return result