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)