File indexing completed on 2024-04-28 07:51:09

0001 # -*- coding: utf-8 -*-
0002 
0003 """
0004 Copyright (C) 2008-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0005 
0006 SPDX-License-Identifier: GPL-2.0
0007 
0008 """
0009 
0010 import weakref
0011 from collections import defaultdict
0012 
0013 from log import logException, logWarning
0014 from mi18n import i18n, i18nc, i18nE
0015 from common import IntDict, Debug
0016 from common import StrMixin, Internal
0017 from wind import East
0018 from query import Query
0019 from tile import Tile, TileList, elements
0020 from tilesource import TileSource
0021 from meld import Meld, MeldList
0022 from permutations import Permutations
0023 from message import Message
0024 from hand import Hand
0025 from intelligence import AIDefaultAI
0026 
0027 
0028 class Players(list, StrMixin):
0029 
0030     """a list of players where the player can also be indexed by wind.
0031     The position in the list defines the place on screen. First is on the
0032     screen bottom, second on the right, third top, forth left"""
0033 
0034     allNames = {}
0035     allIds = {}
0036     humanNames = {}
0037 
0038     def __init__(self, players=None):
0039         list.__init__(self)
0040         if players:
0041             self.extend(players)
0042 
0043     def __getitem__(self, index):
0044         """allow access by idx or by wind"""
0045         for player in self:
0046             if player.wind == index:
0047                 return player
0048         return list.__getitem__(self, index)
0049 
0050     def __str__(self):
0051         return ', '.join('%s: %s' % (x.name, x.wind) for x in self)
0052 
0053     def byId(self, playerid):
0054         """lookup the player by id"""
0055         for player in self:
0056             if player.nameid == playerid:
0057                 return player
0058         logException("no player has id %d" % playerid)
0059         return None
0060 
0061     def byName(self, playerName):
0062         """lookup the player by name"""
0063         for player in self:
0064             if player.name == playerName:
0065                 return player
0066         logException(
0067             "no player has name %s - we have %s" %
0068             (playerName, [x.name for x in self]))
0069         return None
0070 
0071     @staticmethod
0072     def load():
0073         """load all defined players into self.allIds and self.allNames"""
0074         Players.allIds = {}
0075         Players.allNames = {}
0076         for nameid, name in Query("select id,name from player").records:
0077             Players.allIds[name] = nameid
0078             Players.allNames[nameid] = name
0079             if not name.startswith('Robot'):
0080                 Players.humanNames[nameid] = name
0081 
0082     @staticmethod
0083     def createIfUnknown(name):
0084         """create player in database if not there yet"""
0085         if not Internal.db:
0086             # kajonggtest
0087             nameid = len(Players.allIds) + 1
0088             Players.allIds[name] = nameid
0089             Players.allNames[nameid] = name
0090             if not name.startswith('Robot'):
0091                 Players.humanNames[nameid] = name
0092 
0093         if name not in Players.allNames.values():
0094             Players.load()  # maybe somebody else already added it
0095             if name not in Players.allNames.values():
0096                 Query("insert or ignore into player(name) values(?)", (name,))
0097                 Players.load()
0098         assert name in Players.allNames.values(), '%s not in %s' % (
0099             name, Players.allNames.values())
0100 
0101     def translatePlayerNames(self, names):
0102         """for a list of names, translates those names which are english
0103         player names into the local language"""
0104         known = {x.name for x in self}
0105         return [self.byName(x).localName if x in known else x for x in names]
0106 
0107 
0108 class Player(StrMixin):
0109 
0110     """
0111     all player related attributes without GUI stuff.
0112     concealedTiles: used during the hand for all concealed tiles, ungrouped.
0113     concealedMelds: is empty during the hand, will be valid after end of hand,
0114     containing the concealed melds as the player presents them.
0115 
0116     @todo: Now that Player() always calls createIfUnknown, test defining new
0117     players and adding new players to server
0118     """
0119     # pylint: disable=too-many-instance-attributes,too-many-public-methods
0120 
0121     def __init__(self, game, name):
0122         """
0123         Initialize a player for a give game.
0124 
0125         @type game: L{Game} or None.
0126         @param game: The game this player is part of. May be None.
0127         """
0128         if game:
0129             self._game = weakref.ref(game)
0130         else:
0131             self._game = None
0132         self.__balance = 0
0133         self.__payment = 0
0134         self.wonCount = 0
0135         self.__name = ''
0136         Players.createIfUnknown(name)
0137         self.name = name
0138         self.wind = East
0139         self.intelligence = AIDefaultAI(self)
0140         self.visibleTiles = IntDict(game.visibleTiles) if game else IntDict()
0141         self.handCache = {}
0142         self.cacheHits = 0
0143         self.cacheMisses = 0
0144         self.__lastSource = TileSource.Unknown
0145         self.clearHand()
0146         self.handBoard = None
0147 
0148     def __lt__(self, other):
0149         """Used for sorting"""
0150         if not other:
0151             return False
0152         return self.name < other.name
0153 
0154     def clearCache(self):
0155         """clears the cache with Hands"""
0156         if Debug.hand and self.handCache:
0157             self.game.debug(
0158                 '%s: cache hits:%d misses:%d' %
0159                 (self, self.cacheHits, self.cacheMisses))
0160         self.handCache.clear()
0161         Permutations.cache.clear()
0162         self.cacheHits = 0
0163         self.cacheMisses = 0
0164 
0165     @property
0166     def name(self):
0167         """
0168         The name of the player, can be changed only once.
0169 
0170         @type: C{str}
0171         """
0172         return self.__name
0173 
0174     @name.setter
0175     def name(self, value):
0176         """write once"""
0177         assert self.__name == ''
0178         assert value
0179         assert isinstance(value, str), 'Player.name must be str but not {}'.format(type(value))
0180         self.__name = value
0181 
0182     @property
0183     def game(self):
0184         """hide the fact that this is a weakref"""
0185         return self._game() if self._game else None
0186 
0187     def clearHand(self):
0188         """clear player attributes concerning the current hand"""
0189         self._concealedTiles = []
0190         self._exposedMelds = []
0191         self._concealedMelds = []
0192         self._bonusTiles = []
0193         self.discarded = []
0194         self.visibleTiles.clear()
0195         self.newHandContent = None
0196         self.originalCallingHand = None
0197         self.__lastTile = None
0198         self.lastSource = TileSource.Unknown
0199         self.lastMeld = Meld()
0200         self.__mayWin = True
0201         self.__payment = 0
0202         self.originalCall = False
0203         self.dangerousTiles = list()
0204         self.claimedNoChoice = False
0205         self.playedDangerous = False
0206         self.usedDangerousFrom = None
0207         self.isCalling = False
0208         self.clearCache()
0209         self._hand = None
0210 
0211     @property
0212     def lastTile(self):
0213         """temp for debugging"""
0214         return self.__lastTile
0215 
0216     @lastTile.setter
0217     def lastTile(self, value):
0218         """temp for debugging"""
0219         assert isinstance(value, (Tile, type(None))), value
0220         self.__lastTile = value
0221         if value is None:
0222             self.lastMeld = Meld()
0223 
0224     def invalidateHand(self):
0225         """some source for the computation of current hand changed"""
0226         self._hand = None
0227 
0228     @property
0229     def hand(self):
0230         """readonly: the current Hand. Compute if invalidated."""
0231         if not self._hand:
0232             self._hand = self.__computeHand()
0233         elif Debug.hand:
0234             _ = self.__computeHand()
0235             assert self._hand == self.__computeHand(), '{} != {}'.format(_, self._hand)
0236         return self._hand
0237 
0238     @property
0239     def bonusTiles(self):
0240         """a readonly tuple"""
0241         return tuple(self._bonusTiles)
0242 
0243     @property
0244     def concealedTiles(self):
0245         """a readonly tuple"""
0246         return tuple(self._concealedTiles)
0247 
0248     @property
0249     def exposedMelds(self):
0250         """a readonly tuple"""
0251         return tuple(self._exposedMelds)
0252 
0253     @property
0254     def concealedMelds(self):
0255         """a readonly tuple"""
0256         return tuple(self._concealedMelds)
0257 
0258     @property
0259     def mayWin(self):
0260         """winning possible?"""
0261         return self.__mayWin
0262 
0263     @mayWin.setter
0264     def mayWin(self, value):
0265         """winning possible?"""
0266         if self.__mayWin != value:
0267             self.__mayWin = value
0268             self._hand = None
0269 
0270     @property
0271     def lastSource(self):
0272         """the source of the last tile the player got"""
0273         return self.__lastSource
0274 
0275     @lastSource.setter
0276     def lastSource(self, value):
0277         """the source of the last tile the player got"""
0278         if value is TileSource.LivingWallDiscard and not self.game.wall.living:
0279             value = TileSource.LivingWallEndDiscard
0280         if value is TileSource.LivingWall and not self.game.wall.living:
0281             value = TileSource.LivingWallEnd
0282         if self.__lastSource != value:
0283             self.__lastSource = value
0284             self._hand = None
0285 
0286     @property
0287     def nameid(self):
0288         """the name id of this player"""
0289         return Players.allIds[self.name]
0290 
0291     @property
0292     def localName(self):
0293         """the localized name of this player"""
0294         return i18nc('kajongg, name of robot player, to be translated', self.name)
0295 
0296     @property
0297     def handTotal(self):
0298         """the hand total of this player for the final scoring"""
0299         return 0 if not self.game.winner else self.hand.total()
0300 
0301     @property
0302     def balance(self):
0303         """the balance of this player"""
0304         return self.__balance
0305 
0306     @balance.setter
0307     def balance(self, balance):
0308         """the balance of this player"""
0309         self.__balance = balance
0310         self.__payment = 0
0311 
0312     def getsPayment(self, payment):
0313         """make a payment to this player"""
0314         self.__balance += payment
0315         self.__payment += payment
0316 
0317     @property
0318     def payment(self):
0319         """the payments for the current hand"""
0320         return self.__payment
0321 
0322     @payment.setter
0323     def payment(self, payment):
0324         """the payments for the current hand"""
0325         assert payment == 0
0326         self.__payment = 0
0327 
0328     def __str__(self):
0329         return '{name:<10} {wind}'.format(name=self.name[:10], wind=self.wind)
0330 
0331     def pickedTile(self, deadEnd, tileName=None):
0332         """got a tile from wall"""
0333         self.game.activePlayer = self
0334         tile = self.game.wall.deal([tileName], deadEnd=deadEnd)[0]
0335         if hasattr(tile, 'tile'):
0336             self.lastTile = tile.tile
0337         else:
0338             self.lastTile = tile
0339         self.addConcealedTiles([tile])
0340         if deadEnd:
0341             self.lastSource = TileSource.DeadWall
0342         else:
0343             self.game.lastDiscard = None
0344             self.lastSource = TileSource.LivingWall
0345         return self.lastTile
0346 
0347     def removeTile(self, tile):
0348         """remove from my tiles"""
0349         if tile.isBonus:
0350             self._bonusTiles.remove(tile)
0351         else:
0352             try:
0353                 self._concealedTiles.remove(tile)
0354             except ValueError as _:
0355                 raise Exception('removeTile(%s): tile not in concealed %s' %
0356                                 (tile, ''.join(self._concealedTiles))) from _
0357         if tile is self.lastTile:
0358             self.lastTile = None
0359         self._hand = None
0360 
0361     def addConcealedTiles(self, tiles, animated=False):  # pylint: disable=unused-argument
0362         """add to my tiles"""
0363         assert tiles
0364         for tile in tiles:
0365             if tile.isBonus:
0366                 self._bonusTiles.append(tile)
0367             else:
0368                 assert tile.isConcealed, '%s data=%s' % (tile, tiles)
0369                 self._concealedTiles.append(tile)
0370         self._hand = None
0371 
0372     def syncHandBoard(self, adding=None):
0373         """virtual: synchronize display"""
0374 
0375     def colorizeName(self):
0376         """virtual: colorize Name on wall"""
0377 
0378     def getsFocus(self, unusedResults=None):
0379         """virtual: player gets focus on his hand"""
0380 
0381     def mjString(self):
0382         """compile hand info into a string as needed by the scoring engine"""
0383         announcements = 'a' if self.originalCall else ''
0384         return ''.join(['m', self.lastSource.char, ''.join(announcements)])
0385 
0386     def makeTileKnown(self, tile):
0387         """used when somebody else discards a tile"""
0388         assert not self._concealedTiles[0].isKnown
0389         self._concealedTiles[0] = tile
0390         self._hand = None
0391 
0392     def __computeLastInfo(self):
0393         """compile info about last tile and last meld into a list of strings"""
0394         result = []
0395         if self.lastTile:
0396 # TODO assert, dass lastTile in _concealedTiles oder in _exposedMelds ist
0397 # and (self.lastTile in self._concealedTiles or self.lastTile in :
0398             result.append(
0399                 'L%s%s' %
0400                 (self.lastTile, self.lastMeld if self.lastMeld else ''))
0401         return result
0402 
0403     def __computeHand(self):
0404         """return Hand for this player"""
0405         assert not (self._concealedMelds and self._concealedTiles)
0406         melds = list()
0407         melds.extend(str(x) for x in self._exposedMelds)
0408         melds.extend(str(x) for x in self._concealedMelds)
0409         if self._concealedTiles:
0410             melds.append('R' + ''.join(str(x) for x in sorted(self._concealedTiles)))
0411         melds.extend(str(x) for x in self._bonusTiles)
0412         melds.append(self.mjString())
0413         melds.extend(self.__computeLastInfo())
0414         return Hand(self, ' '.join(melds))
0415 
0416     def _computeHandWithDiscard(self, discard):
0417         """what if"""
0418         lastSource = self.lastSource # TODO: recompute
0419         save = (self.lastTile, self.lastSource)
0420         try:
0421             self.lastSource = lastSource
0422             if discard:
0423                 self.lastTile = discard
0424                 self._concealedTiles.append(discard)
0425             return self.__computeHand()
0426         finally:
0427             self.lastTile, self.lastSource = save
0428             if discard:
0429                 self._concealedTiles = self._concealedTiles[:-1]
0430 
0431     def scoringString(self):
0432         """helper for HandBoard.__str__"""
0433         if self._concealedMelds:
0434             parts = [str(x) for x in self._concealedMelds + self._exposedMelds]
0435         else:
0436             parts = [''.join(self._concealedTiles)]
0437             parts.extend([str(x) for x in self._exposedMelds])
0438         parts.extend(str(x) for x in self._bonusTiles)
0439         return ' '.join(parts)
0440 
0441     def sortRulesByX(self, rules):  # pylint: disable=no-self-use
0442         """if this game has a GUI, sort rules by GUI order"""
0443         return rules
0444 
0445     def others(self):
0446         """a list of the other 3 players"""
0447         return (x for x in self.game.players if x != self)
0448 
0449     def tileAvailable(self, tileName, hand):
0450         """a count of how often tileName might still appear in the game
0451         supposing we have hand"""
0452         lowerTile = tileName.exposed
0453         upperTile = tileName.concealed
0454         visible = self.game.discardedTiles.count([lowerTile])
0455         if visible:
0456             if hand.lenOffset == 0 and self.game.lastDiscard and lowerTile is self.game.lastDiscard.exposed:
0457                 # the last discarded one is available to us since we can claim
0458                 # it
0459                 visible -= 1
0460         visible += sum(x.visibleTiles.count([lowerTile, upperTile])
0461                        for x in self.others())
0462         visible += sum(x.exposed == lowerTile for x in hand.tiles)
0463         return 4 - visible
0464 
0465     def violatesOriginalCall(self, discard=None):
0466         """called if discarding discard violates the Original Call"""
0467         if not self.originalCall or not self.mayWin:
0468             return False
0469         if self.lastTile.exposed != discard.exposed:
0470             if Debug.originalCall:
0471                 self.game.debug(
0472                     '%s would violate OC with %s, lastTile=%s' %
0473                     (self, discard, self.lastTile))
0474             return True
0475         return False
0476 
0477 
0478 class PlayingPlayer(Player):
0479 
0480     """a player in a computer game as opposed to a ScoringPlayer"""
0481     # pylint: disable=too-many-public-methods
0482     # too many public methods
0483 
0484     def __init__(self, game, name):
0485         self.sayable = {}               # recompute for each move, use as cache
0486         Player.__init__(self, game, name)
0487 
0488     def popupMsg(self, msg):
0489         """virtual: show popup on display"""
0490 
0491     def hidePopup(self):
0492         """virtual: hide popup on display"""
0493 
0494     def speak(self, txt):
0495         """only a visible playing player can speak"""
0496 
0497     def declaredMahJongg(self, concealed, withDiscard, lastTile, lastMeld):
0498         """player declared mah jongg. Determine last meld, show concealed tiles grouped to melds"""
0499         if Debug.mahJongg:
0500             self.game.debug('{} declared MJ: concealed={}, withDiscard={}, lastTile={},lastMeld={}'.format(
0501                 self, concealed, withDiscard, lastTile, lastMeld))
0502             self.game.debug('  with hand being {}'.format(self.hand))
0503         melds = concealed[:]
0504         self.game.winner = self
0505         assert lastMeld in melds, \
0506             'lastMeld %s not in melds: concealed=%s: melds=%s lastTile=%s withDiscard=%s' % (
0507                 lastMeld, self._concealedTiles, melds, lastTile, withDiscard)
0508         if withDiscard:
0509             PlayingPlayer.addConcealedTiles(
0510                 self,
0511                 [withDiscard])  # this should NOT invoke syncHandBoard
0512             if len(list(self.game.lastMoves(only=(Message.Discard, )))) == 1:
0513                 self.lastSource = TileSource.East14th
0514             elif self.lastSource is not TileSource.RobbedKong:
0515                 self.lastSource = TileSource.LivingWallDiscard
0516             # the last claimed meld is exposed
0517             melds.remove(lastMeld)
0518             lastTile = withDiscard.exposed
0519             lastMeld = lastMeld.exposed
0520             self._exposedMelds.append(lastMeld)
0521             for tileName in lastMeld:
0522                 self.visibleTiles[tileName] += 1
0523         self.lastTile = lastTile
0524         self.lastMeld = lastMeld
0525         self._concealedMelds = melds
0526         self._concealedTiles = []
0527         self._hand = None
0528         if Debug.mahJongg:
0529             self.game.debug('  hand becomes {}'.format(self.hand))
0530             self._hand = None
0531 
0532     def __possibleChows(self):
0533         """return a unique list of lists with possible claimable chow combinations"""
0534         if self.game.lastDiscard is None:
0535             return []
0536         exposedChows = [x for x in self._exposedMelds if x.isChow]
0537         if len(exposedChows) >= self.game.ruleset.maxChows:
0538             return []
0539         tile = self.game.lastDiscard
0540         within = TileList(self.concealedTiles[:])
0541         within.append(tile)
0542         return within.hasChows(tile)
0543 
0544     def __possibleKongs(self):
0545         """return a unique list of lists with possible kong combinations"""
0546         kongs = []
0547         if self == self.game.activePlayer:
0548             # declaring a kong
0549             for tileName in sorted({x for x in self._concealedTiles if not x.isBonus}):
0550                 if self._concealedTiles.count(tileName) == 4:
0551                     kongs.append(tileName.kong)
0552                 elif self._concealedTiles.count(tileName) == 1 and \
0553                         tileName.exposed.pung in self._exposedMelds:
0554                     # the result will be an exposed Kong but the 4th tile
0555                     # came from the wall, so we use the form aaaA
0556                     kongs.append(tileName.kong.exposedClaimed)
0557         if self.game.lastDiscard:
0558             # claiming a kong
0559             discardTile = self.game.lastDiscard.concealed
0560             if self._concealedTiles.count(discardTile) == 3:
0561                 # discard.kong.concealed is aAAa but we need AAAA
0562                 kongs.append(Meld(discardTile * 4))
0563         return kongs
0564 
0565     def __maySayChow(self, unusedMove):
0566         """return answer arguments for the server if calling chow is possible.
0567         returns the meld to be completed"""
0568         return self.__possibleChows() if self == self.game.nextPlayer() else None
0569 
0570     def __maySayPung(self, unusedMove):
0571         """return answer arguments for the server if calling pung is possible.
0572         returns the meld to be completed"""
0573         lastDiscard = self.game.lastDiscard
0574         if self.game.lastDiscard:
0575             assert lastDiscard.isConcealed, lastDiscard
0576             if self.concealedTiles.count(lastDiscard) >= 2:
0577                 return MeldList([lastDiscard.pung])
0578         return None
0579 
0580     def __maySayKong(self, unusedMove):
0581         """return answer arguments for the server if calling or declaring kong is possible.
0582         returns the meld to be completed or to be declared"""
0583         return self.__possibleKongs()
0584 
0585     def __maySayMahjongg(self, move):
0586         """return answer arguments for the server if calling or declaring Mah Jongg is possible"""
0587         game = self.game
0588         if move.message == Message.DeclaredKong:
0589             withDiscard = move.meld[0].concealed
0590         elif move.message == Message.AskForClaims:
0591             withDiscard = game.lastDiscard
0592         else:
0593             withDiscard = None
0594         hand = self._computeHandWithDiscard(withDiscard)
0595         if hand.won:
0596             if Debug.robbingKong:
0597                 if move.message == Message.DeclaredKong:
0598                     game.debug('%s may rob the kong from %s/%s' %
0599                                (self, move.player, move.exposedMeld))
0600             if Debug.mahJongg:
0601                 game.debug('%s may say MJ:%s, active=%s' % (
0602                     self, list(x for x in game.players), game.activePlayer))
0603                 game.debug('  with hand {}'.format(hand))
0604             return MeldList(x for x in hand.melds if not x.isDeclared), withDiscard, hand.lastMeld
0605         return None
0606 
0607     def __maySayOriginalCall(self, unusedMove):
0608         """return True if Original Call is possible"""
0609         for tileName in sorted(set(self.concealedTiles)):
0610             newHand = self.hand - tileName
0611             if newHand.callingHands:
0612                 if Debug.originalCall:
0613                     self.game.debug(
0614                         '%s may say Original Call by discarding %s from %s' %
0615                         (self, tileName, self.hand))
0616                 return True
0617         return False
0618 
0619     sayables = {
0620         Message.Pung: __maySayPung,
0621         Message.Kong: __maySayKong,
0622         Message.Chow: __maySayChow,
0623         Message.MahJongg: __maySayMahjongg,
0624         Message.OriginalCall: __maySayOriginalCall}
0625 
0626     def computeSayable(self, move, answers):
0627         """find out what the player can legally say with this hand"""
0628         self.sayable = {}
0629         for message in Message.defined.values():
0630             if message in answers and message in self.sayables:
0631                 self.sayable[message] = self.sayables[message](self, move)
0632             else:
0633                 self.sayable[message] = True
0634 
0635     def maybeDangerous(self, msg):
0636         """could answering with msg lead to dangerous game?
0637         If so return a list of resulting melds
0638         where a meld is represented by a list of 2char strings"""
0639         if msg in (Message.Chow, Message.Pung, Message.Kong):
0640             return [x for x in self.sayable[msg] if self.mustPlayDangerous(x)]
0641         return []
0642 
0643     def hasConcealedTiles(self, tiles, within=None):
0644         """do I have those concealed tiles?"""
0645         if within is None:
0646             within = self._concealedTiles
0647         within = within[:]
0648         for tile in tiles:
0649             if tile not in within:
0650                 return False
0651             within.remove(tile)
0652         return True
0653 
0654     def showConcealedTiles(self, tiles, show=True):
0655         """show or hide tiles"""
0656         if not self.game.playOpen and self != self.game.myself:
0657             if not isinstance(tiles, (list, tuple)):
0658                 tiles = [tiles]
0659             assert len(tiles) <= len(self._concealedTiles), \
0660                 '%s: showConcealedTiles %s, we have only %s' % (
0661                     self, tiles, self._concealedTiles)
0662             for tileName in tiles:
0663                 src, dst = (Tile.unknown, tileName) if show else (
0664                     tileName, Tile.unknown)
0665                 assert src != dst, (
0666                     self, src, dst, tiles, self._concealedTiles)
0667                 if src not in self._concealedTiles:
0668                     logException('%s: showConcealedTiles(%s): %s not in %s.' %
0669                                  (self, tiles, src, self._concealedTiles))
0670                 idx = self._concealedTiles.index(src)
0671                 self._concealedTiles[idx] = dst
0672             if self.lastTile and not self.lastTile.isKnown:
0673                 self.lastTile = None
0674             self._hand = None
0675             self.syncHandBoard()
0676 
0677     def showConcealedMelds(self, concealedMelds, ignoreDiscard=None):
0678         """the server tells how the winner shows and melds his
0679         concealed tiles. In case of error, return message and arguments"""
0680         for meld in concealedMelds:
0681             for tile in meld:
0682                 if tile == ignoreDiscard:
0683                     ignoreDiscard = None
0684                 else:
0685                     if tile not in self._concealedTiles:
0686                         msg = i18nE(
0687                             '%1 claiming MahJongg: She does not really have tile %2')
0688                         return msg, self.name, tile
0689                     self._concealedTiles.remove(tile)
0690             if meld.isConcealed and not meld.isKong:
0691                 self._concealedMelds.append(meld)
0692             else:
0693                 self._exposedMelds.append(meld)
0694         if self._concealedTiles:
0695             msg = i18nE(
0696                 '%1 claiming MahJongg: She did not pass all concealed tiles to the server')
0697             return msg, self.name
0698         self._hand = None
0699         return None
0700 
0701     def robTileFrom(self, tile):
0702         """used for robbing the kong from this player"""
0703         assert tile.isConcealed
0704         tile = tile.exposed
0705         for meld in self._exposedMelds:
0706             if tile in meld:
0707                 meld = meld.without(tile)
0708                 self.visibleTiles[tile] -= 1
0709                 break
0710         else:
0711             raise Exception('robTileFrom: no meld found with %s' % tile)
0712         self.game.lastDiscard = tile.concealed
0713         self.lastTile = None  # our lastTile has just been robbed
0714         self._hand = None
0715 
0716     def robsTile(self):
0717         """True if the player is robbing a tile"""
0718         self.lastSource = TileSource.RobbedKong
0719 
0720     def scoreMatchesServer(self, score):
0721         """do we compute the same score as the server does?"""
0722         if score is None:
0723             return True
0724         if any(not x.isKnown for x in self._concealedTiles):
0725             return True
0726         if str(self.hand) == score:
0727             return True
0728         self.game.debug('%s localScore:%s' % (self, self.hand))
0729         self.game.debug('%s serverScore:%s' % (self, score))
0730         logWarning(
0731             'Game %s: client and server disagree about scoring, see logfile for details' %
0732             self.game.seed)
0733         return False
0734 
0735     def mustPlayDangerous(self, exposing=None):
0736         """]
0737         True if the player has no choice, otherwise False.
0738 
0739         @param exposing: May be a meld which will be exposed before we might
0740         play dangerous.
0741         @type exposing: L{Meld}
0742         @rtype: C{Boolean}
0743         """
0744         if self == self.game.activePlayer and exposing and len(exposing) == 4:
0745             # declaring a kong is never dangerous because we get
0746             # an unknown replacement
0747             return False
0748         afterExposed = [x.exposed for x in self._concealedTiles]
0749         if exposing:
0750             exposing = exposing[:]
0751             if self.game.lastDiscard:
0752                 # if this is about claiming a discarded tile, ignore it
0753                 # the player who discarded it is responsible
0754                 exposing.remove(self.game.lastDiscard)
0755             for tile in exposing:
0756                 if tile.exposed in afterExposed:
0757                     # the "if" is needed for claimed pung
0758                     afterExposed.remove(tile.exposed)
0759         return all(self.game.dangerousFor(self, x) for x in afterExposed)
0760 
0761     def exposeMeld(self, meldTiles, calledTile=None):
0762         """exposes a meld with meldTiles: removes them from concealedTiles,
0763         adds the meld to exposedMelds and returns it
0764         calledTile: we got the last tile for the meld from discarded, otherwise
0765         from the wall"""
0766         game = self.game
0767         game.activePlayer = self
0768         allMeldTiles = meldTiles[:]
0769         if calledTile:
0770             assert isinstance(calledTile, Tile), calledTile
0771             allMeldTiles.append(calledTile)
0772         if len(allMeldTiles) == 4 and allMeldTiles[0].isExposed:
0773             tile0 = allMeldTiles[0].exposed
0774             # we are adding a 4th tile to an exposed pung
0775             self._exposedMelds = [
0776                 x for x in self._exposedMelds if x != tile0.pung]
0777             meld = tile0.kong
0778             if allMeldTiles[3] not in self._concealedTiles:
0779                 game.debug(
0780                     't3 %s not in conc %s' %
0781                     (allMeldTiles[3], self._concealedTiles))
0782             self._concealedTiles.remove(allMeldTiles[3])
0783             self.visibleTiles[tile0] += 1
0784         else:
0785             allMeldTiles = sorted(allMeldTiles)  # needed for Chow
0786             meld = Meld(allMeldTiles)
0787             for meldTile in meldTiles:
0788                 self._concealedTiles.remove(meldTile)
0789             for meldTile in allMeldTiles:
0790                 self.visibleTiles[meldTile.exposed] += 1
0791             meld = meld.exposedClaimed if calledTile else meld.declared
0792         if self.lastTile in allMeldTiles:
0793             self.lastTile = self.lastTile.exposed
0794         self._exposedMelds.append(meld)
0795         self._hand = None
0796         game.computeDangerous(self)
0797         return meld
0798 
0799     def findDangerousTiles(self):
0800         """update the list of dangerous tile"""
0801         pName = self.localName
0802         dangerous = list()
0803         expMeldCount = len(self._exposedMelds)
0804         if expMeldCount >= 3:
0805             if all(x in elements.greenHandTiles for x in self.visibleTiles):
0806                 dangerous.append((elements.greenHandTiles,
0807                                   i18n('Player %1 has 3 or 4 exposed melds, all are green', pName)))
0808             group = list(defaultdict.keys(self.visibleTiles))[0].group
0809             # see https://www.logilab.org/ticket/23986
0810             assert group.islower(), self.visibleTiles
0811             if group in Tile.colors:
0812                 if all(x.group == group for x in self.visibleTiles):
0813                     suitTiles = {Tile(group, x) for x in Tile.numbers}
0814                     if self.visibleTiles.count(suitTiles) >= 9:
0815                         dangerous.append(
0816                             (suitTiles, i18n('Player %1 may try a True Color Game', pName)))
0817                 elif all(x.value in Tile.terminals for x in self.visibleTiles):
0818                     dangerous.append((elements.terminals,
0819                                       i18n('Player %1 may try an All Terminals Game', pName)))
0820         if expMeldCount >= 2:
0821             windMelds = sum(self.visibleTiles[x] >= 3 for x in elements.winds)
0822             dragonMelds = sum(
0823                 self.visibleTiles[x] >= 3 for x in elements.dragons)
0824             windsDangerous = dragonsDangerous = False
0825             if windMelds + dragonMelds == expMeldCount and expMeldCount >= 3:
0826                 windsDangerous = dragonsDangerous = True
0827             windsDangerous = windsDangerous or windMelds >= 3
0828             dragonsDangerous = dragonsDangerous or dragonMelds >= 2
0829             if windsDangerous:
0830                 dangerous.append(
0831                     ({x for x in elements.winds if x not in self.visibleTiles},
0832                      i18n('Player %1 exposed many winds', pName)))
0833             if dragonsDangerous:
0834                 dangerous.append(
0835                     ({x for x in elements.dragons if x not in self.visibleTiles},
0836                      i18n('Player %1 exposed many dragons', pName)))
0837         self.dangerousTiles = dangerous
0838         if dangerous and Debug.dangerousGame:
0839             self.game.debug('dangerous:%s' % dangerous)