def __sub__(self, tiles): """returns a copy of self minus tiles. Case of tiles (hidden or exposed) is ignored. If the tile is not hidden but found in an exposed meld, this meld will be hidden with the tile removed from it. Exposed melds of length<3 will also be hidden.""" # pylint: disable=R0912 # pylint says too many branches if not isinstance(tiles, list): tiles = list([tiles]) hidden = 'R' + ''.join(self.tileNamesInHand) # exposed is a deep copy of declaredMelds. If lastMeld is given, it # must be first in the list. exposed = (Meld(x) for x in self.declaredMelds) if self.lastMeld: exposed = sorted(exposed, key=lambda x: (x.pairs != self.lastMeld.pairs, meldKey(x))) else: exposed = sorted(exposed, key=meldKey) bonus = sorted(Meld(x) for x in self.bonusMelds) for tile in tiles: assert isinstance(tile, str) and len(tile) == 2, 'Hand.__sub__:%s' % tiles if tile.capitalize() in hidden: hidden = hidden.replace(tile.capitalize(), '', 1) elif tile[0] in 'fy': # bonus tile for idx, meld in enumerate(bonus): if tile == meld.pairs[0]: del bonus[idx] break else: for idx, meld in enumerate(exposed): if tile.lower() in meld.pairs: del meld.pairs[meld.pairs.index(tile.lower())] del exposed[idx] meld.conceal() hidden += meld.joined break for idx, meld in enumerate(exposed): if len(meld.pairs) < 3: del exposed[idx] meld.conceal() hidden += meld.joined mjStr = self.mjStr if self.lastTile in tiles: parts = mjStr.split() newParts = [] for idx, part in enumerate(parts): if part[0] == 'M': part = 'm' + part[1:] if len(part) > 3 and part[3] == 'k': part = part[:3] elif part[0] == 'L': continue newParts.append(part) mjStr = ' '.join(newParts) newString = ' '.join([hidden, meldsContent(exposed), meldsContent(bonus), mjStr]) return Hand.cached(self, newString, self.computedRules)
def declaredMahJongg(self, concealed, withDiscard, lastTile, lastMeld): """player declared mah jongg. Determine last meld, show concealed tiles grouped to melds""" assert not isinstance(lastTile, Tile) lastMeld = Meld(lastMeld) # do not change the original! self.game.winner = self if withDiscard: self.lastTile = withDiscard self.lastMeld = lastMeld assert withDiscard == self.game.lastDiscard.element, 'withDiscard: %s lastDiscard: %s' % ( withDiscard, self.game.lastDiscard.element) self.addConcealedTiles(self.game.lastDiscard) melds = [Meld(x) for x in concealed.split()] if self.lastSource != 'k': # robbed the kong self.lastSource = 'd' # the last claimed meld is exposed assert lastMeld in melds, '%s: concealed=%s melds=%s lastMeld=%s lastTile=%s withDiscard=%s' % ( self.__concealedTileNames, concealed, meldsContent(melds), ''.join(lastMeld.pairs), lastTile, withDiscard) melds.remove(lastMeld) self.lastTile = self.lastTile.lower() lastMeld.pairs.toLower() self._exposedMelds.append(lastMeld) for tileName in lastMeld.pairs: self.visibleTiles[tileName] += 1 else: melds = [Meld(x) for x in concealed.split()] self.lastTile = lastTile self.lastMeld = lastMeld self._concealedMelds = melds self.__concealedTileNames = [] self.__hand = None self.syncHandBoard()
def __maySayMahjongg(self, move): """returns answer arguments for the server if calling or declaring Mah Jongg is possible""" game = self.game myself = game.myself robbableTile = withDiscard = None if move.message == Message.DeclaredKong: withDiscard = move.source[0].capitalize() if move.player != myself: robbableTile = move.exposedMeld.pairs[ 1] # we want it capitalized for a hidden Kong elif move.message == Message.AskForClaims: withDiscard = game.lastDiscard.element hand = myself.computeHand(withTile=withDiscard, robbedTile=robbableTile, asWinner=True) if hand.won: if Debug.robbingKong: if move.message == Message.DeclaredKong: game.debug('%s may rob the kong from %s/%s' % \ (myself, move.player, move.exposedMeld.joined)) if Debug.mahJongg: game.debug( '%s may say MJ:%s, active=%s' % (myself, list(x for x in game.players), game.activePlayer)) return (meldsContent(hand.hiddenMelds), withDiscard, list(hand.lastMeld.pairs))
def __split(self, rest): """work hard to always return the variant with the highest Mah Jongg value. Adds melds to self.melds. only one special mjRule may try to rearrange melds. A rest will be rearranged by standard rules.""" if 'Xy' in rest: # hidden tiles of other players: self.melds.extend(self.splitRegex(rest)) return arrangements = [] for mjRule in self.ruleset.mjRules: func = mjRule.function if func.__class__.__name__ == 'StandardMahJongg': stdMJ = func if self.mjRule: rules = [self.mjRule] else: rules = self.ruleset.mjRules for mjRule in rules: func = mjRule.function if func != stdMJ and hasattr(func, 'rearrange'): if ((self.lenOffset == 1 and func.appliesToHand(self)) or (self.lenOffset < 1 and func.shouldTry(self))): melds, pairs = func.rearrange(self, rest[:]) if melds: arrangements.append((mjRule, melds, pairs)) if arrangements: # TODO: we should know for each arrangement how many tiles for MJ are still needed. # If len(pairs) == 4, one or up to three might be needed. That would allow for better AI. # TODO: if hand just completed and we did not win, only try stdmj arrangement = sorted(arrangements, key=lambda x: len(x[2]))[0] self.melds.extend(arrangement[1]) self.melds.extend([Meld(x) for x in arrangement[2]]) assert len(''.join(x.joined for x in self.melds)) == len( self.tileNames) * 2, '%s != %s' % (meldsContent( self.melds), self.tileNames) else: # stdMJ is special because it might build more than one pair # the other special hands would put that into the rest # if the above TODO is done, stdMJ does not have to be special anymore melds, _ = stdMJ.rearrange(self, rest[:]) self.melds.extend(melds) assert len(''.join(x.joined for x in self.melds)) == len( self.tileNames) * 2, '%s != %s' % (meldsContent( self.melds), self.tileNames)
def __split(self, rest): """work hard to always return the variant with the highest Mah Jongg value. Adds melds to self.melds. only one special mjRule may try to rearrange melds. A rest will be rearranged by standard rules.""" if 'Xy' in rest: # hidden tiles of other players: self.melds.extend(self.splitRegex(rest)) return arrangements = [] for mjRule in self.ruleset.mjRules: func = mjRule.function if func.__class__.__name__ == 'StandardMahJongg': stdMJ = func if self.mjRule: rules = [self.mjRule] else: rules = self.ruleset.mjRules for mjRule in rules: func = mjRule.function if func != stdMJ and hasattr(func, 'rearrange'): if ((self.lenOffset == 1 and func.appliesToHand(self)) or (self.lenOffset < 1 and func.shouldTry(self))): melds, pairs = func.rearrange(self, rest[:]) if melds: arrangements.append((mjRule, melds, pairs)) if arrangements: # TODO: we should know for each arrangement how many tiles for MJ are still needed. # If len(pairs) == 4, one or up to three might be needed. That would allow for better AI. # TODO: if hand just completed and we did not win, only try stdmj arrangement = sorted(arrangements, key=lambda x: len(x[2]))[0] self.melds.extend(arrangement[1]) self.melds.extend([Meld(x) for x in arrangement[2]]) assert len(''.join(x.joined for x in self.melds)) == len(self.tileNames) * 2, '%s != %s' % ( meldsContent(self.melds), self.tileNames) else: # stdMJ is special because it might build more than one pair # the other special hands would put that into the rest # if the above TODO is done, stdMJ does not have to be special anymore melds, _ = stdMJ.rearrange(self, rest[:]) self.melds.extend(melds) assert len(''.join(x.joined for x in self.melds)) == len(self.tileNames) * 2, '%s != %s' % ( meldsContent(self.melds), self.tileNames)
def __init__(self, ruleset, string, computedRules=None, robbedTile=None): """evaluate string using ruleset. rules are to be applied in any case. ruleset can be Hand, Game or Ruleset.""" # silence pylint. This method is time critical, so do not split it into smaller methods # pylint: disable=R0902,R0914,R0912,R0915 if isinstance(ruleset, Hand): self.ruleset = ruleset.ruleset self.player = ruleset.player self.computedRules = ruleset.computedRules elif isinstance(ruleset, Ruleset): self.ruleset = ruleset self.player = None else: self.player = ruleset self.ruleset = self.player.game.ruleset self.string = string self.robbedTile = robbedTile if computedRules is not None and not isinstance(computedRules, list): computedRules = list([computedRules]) self.computedRules = computedRules or [] self.__won = False self.mjStr = '' self.mjRule = None self.ownWind = None self.roundWind = None tileStrings = [] mjStrings = [] haveM = False splits = self.string.split() for part in splits: partId = part[0] if partId in 'Mmx': haveM = True self.ownWind = part[1] self.roundWind = part[2] mjStrings.append(part) self.__won = partId == 'M' elif partId == 'L': if len(part[1:]) > 8: raise Exception('last tile cannot complete a kang:' + self.string) mjStrings.append(part) else: tileStrings.append(part) if not haveM: raise Exception('Hand got string without mMx: %s', self.string) self.mjStr = ' '.join(mjStrings) self.__lastTile = self.__lastSource = self.__announcements = '' self.__lastMeld = 0 self.__lastMelds = [] self.hiddenMelds = [] self.declaredMelds = [] self.melds = [] tileString = ' '.join(tileStrings) self.bonusMelds, tileString = self.__separateBonusMelds(tileString) self.tileNames = Pairs(tileString.replace(' ','').replace('R', '')) self.tileNames.sort() self.values = ''.join(x[1] for x in self.tileNames) self.suits = set(x[0].lower() for x in self.tileNames) self.lenOffset = self.__computeLenOffset(tileString) self.dragonMelds, self.windMelds = self.__computeDragonWindMelds(tileString) self.__separateMelds(tileString) self.hiddenMelds = sorted(self.hiddenMelds, key=meldKey) self.tileNamesInHand = sum((x.pairs for x in self.hiddenMelds), []) self.sortedMeldsContent = meldsContent(self.melds) if self.bonusMelds: self.sortedMeldsContent += ' ' + meldsContent(self.bonusMelds) self.usedRules = [] self.score = None oldWon = self.won self.__applyRules() if len(self.lastMelds) > 1: self.__applyBestLastMeld() if self.won != oldWon: # if not won after all, this might be a long hand. # So we might even have to unapply meld rules and # bonus points. Instead just recompute all again. # This should only happen with scoring manual games # and with scoringtest - normally kajongg would not # let you declare an invalid mah jongg self.__applyRules()
def __init__(self, ruleset, string, computedRules=None, robbedTile=None): """evaluate string using ruleset. rules are to be applied in any case. ruleset can be Hand, Game or Ruleset.""" # silence pylint. This method is time critical, so do not split it into smaller methods # pylint: disable=R0902,R0914,R0912,R0915 if isinstance(ruleset, Hand): self.ruleset = ruleset.ruleset self.player = ruleset.player self.computedRules = ruleset.computedRules elif isinstance(ruleset, Ruleset): self.ruleset = ruleset self.player = None else: self.player = ruleset self.ruleset = self.player.game.ruleset self.string = string self.robbedTile = robbedTile if computedRules is not None and not isinstance(computedRules, list): computedRules = list([computedRules]) self.computedRules = computedRules or [] self.__won = False self.mjStr = '' self.mjRule = None self.ownWind = None self.roundWind = None tileStrings = [] mjStrings = [] haveM = False splits = self.string.split() for part in splits: partId = part[0] if partId in 'Mmx': haveM = True self.ownWind = part[1] self.roundWind = part[2] mjStrings.append(part) self.__won = partId == 'M' elif partId == 'L': if len(part[1:]) > 8: raise Exception('last tile cannot complete a kang:' + self.string) mjStrings.append(part) else: tileStrings.append(part) if not haveM: raise Exception('Hand got string without mMx: %s', self.string) self.mjStr = ' '.join(mjStrings) self.__lastTile = self.__lastSource = self.__announcements = '' self.__lastMeld = 0 self.__lastMelds = [] self.hiddenMelds = [] self.declaredMelds = [] self.melds = [] tileString = ' '.join(tileStrings) self.bonusMelds, tileString = self.__separateBonusMelds(tileString) self.tileNames = Pairs(tileString.replace(' ', '').replace('R', '')) self.tileNames.sort() self.values = ''.join(x[1] for x in self.tileNames) self.suits = set(x[0].lower() for x in self.tileNames) self.lenOffset = self.__computeLenOffset(tileString) self.dragonMelds, self.windMelds = self.__computeDragonWindMelds( tileString) self.__separateMelds(tileString) self.hiddenMelds = sorted(self.hiddenMelds, key=meldKey) self.tileNamesInHand = sum((x.pairs for x in self.hiddenMelds), []) self.sortedMeldsContent = meldsContent(self.melds) if self.bonusMelds: self.sortedMeldsContent += ' ' + meldsContent(self.bonusMelds) self.usedRules = [] self.score = None oldWon = self.won self.__applyRules() if len(self.lastMelds) > 1: self.__applyBestLastMeld() if self.won != oldWon: # if not won after all, this might be a long hand. # So we might even have to unapply meld rules and # bonus points. Instead just recompute all again. # This should only happen with scoring manual games # and with scoringtest - normally kajongg would not # let you declare an invalid mah jongg self.__applyRules()
def __sub__(self, tiles): """returns a copy of self minus tiles. Case of tiles (hidden or exposed) is ignored. If the tile is not hidden but found in an exposed meld, this meld will be hidden with the tile removed from it. Exposed melds of length<3 will also be hidden.""" # pylint: disable=R0912 # pylint says too many branches if not isinstance(tiles, list): tiles = list([tiles]) hidden = 'R' + ''.join(self.tileNamesInHand) # exposed is a deep copy of declaredMelds. If lastMeld is given, it # must be first in the list. exposed = (Meld(x) for x in self.declaredMelds) if self.lastMeld: exposed = sorted(exposed, key=lambda x: (x.pairs != self.lastMeld.pairs, meldKey(x))) else: exposed = sorted(exposed, key=meldKey) bonus = sorted(Meld(x) for x in self.bonusMelds) for tile in tiles: assert isinstance( tile, str) and len(tile) == 2, 'Hand.__sub__:%s' % tiles if tile.capitalize() in hidden: hidden = hidden.replace(tile.capitalize(), '', 1) elif tile[0] in 'fy': # bonus tile for idx, meld in enumerate(bonus): if tile == meld.pairs[0]: del bonus[idx] break else: for idx, meld in enumerate(exposed): if tile.lower() in meld.pairs: del meld.pairs[meld.pairs.index(tile.lower())] del exposed[idx] meld.conceal() hidden += meld.joined break for idx, meld in enumerate(exposed): if len(meld.pairs) < 3: del exposed[idx] meld.conceal() hidden += meld.joined mjStr = self.mjStr if self.lastTile in tiles: parts = mjStr.split() newParts = [] for idx, part in enumerate(parts): if part[0] == 'M': part = 'm' + part[1:] if len(part) > 3 and part[3] == 'k': part = part[:3] elif part[0] == 'L': continue newParts.append(part) mjStr = ' '.join(newParts) newString = ' '.join( [hidden, meldsContent(exposed), meldsContent(bonus), mjStr]) return Hand.cached(self, newString, self.computedRules)