File indexing completed on 2024-04-14 03:59:11

0001 # -*- coding: utf-8 -*-
0002 
0003 """
0004 Copyright (C) 2010-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0005 
0006 SPDX-License-Identifier: GPL-2.0
0007 
0008 """
0009 
0010 import datetime
0011 
0012 from log import logWarning, logException, logDebug
0013 from mi18n import i18n, i18nc, i18ncE
0014 from sound import Voice
0015 from tile import Tile, TileList
0016 from meld import Meld, MeldList
0017 from common import Internal, Debug, Options, StrMixin
0018 from wind import Wind
0019 from dialogs import Sorry
0020 
0021 # pylint: disable=super-init-not-called
0022 # multiple inheritance: pylint thinks ServerMessage.__init__ does not get called.
0023 # this is no problem: ServerMessage has no __init__ and its parent Message.__init__
0024 # will be called anyway
0025 
0026 
0027 class Message:
0028 
0029     """those are the message types between client and server. They have no state
0030     i.e. they never hold real attributes. They only describe the message and actions upon it"""
0031 
0032     defined = {}
0033 
0034     def __init__(self, name=None, shortcut=None):
0035         """those are the english values"""
0036         self.name = name or self.__class__.__name__.replace('Message', '')
0037         self.__i18nName = None
0038         self.shortcut = shortcut
0039         # do not use a numerical value because that could easier
0040         # change with software updates
0041         className = self.__class__.__name__.replace('Message', '')
0042         msgName = self.name.replace(' ', '')
0043         assert className == msgName, '%s != %s' % (className, msgName)
0044 
0045     @property
0046     def i18nName(self):
0047         """only translate when needed - most messages never need this"""
0048         if self.__i18nName is None:
0049             self.__i18nName = i18nc('kajongg', self.name)
0050         return self.__i18nName
0051 
0052     def __str__(self):
0053         return self.name
0054 
0055     def __repr__(self):
0056         return 'Message.{}'.format(self.name)
0057 
0058     def __lt__(self, other):
0059         return self.__class__.__name__ < other.__class__.__name__
0060 
0061     @staticmethod
0062     def jelly(key, value):
0063         """serialize value for wire transfer. The twisted.pb mechanism with
0064         pb.Copyable is too much overhead"""
0065         # pylint: disable=too-many-return-statements
0066         cls = value.__class__
0067         if cls in (Tile, TileList, Meld, MeldList):
0068             return str(value)
0069         if isinstance(value, Wind):
0070             return str(value)
0071         if isinstance(value, Message):
0072             return value.name
0073         if isinstance(value, (list, tuple)):
0074             if isinstance(value, tuple) and isinstance(value[0], Message):
0075                 if value[1] is None or value[1] == []:
0076                     return value[0].name
0077             return type(value)([Message.jelly(key, x) for x in value])
0078         if isinstance(value, dict):
0079             return {Message.jelly('key', x): Message.jelly('value', y) for x, y in value.items()}
0080         if not isinstance(value, (int, bytes, str, float, type(None))):
0081             raise Exception(
0082                 'callRemote got illegal arg: %s %s(%s)' %
0083                 (key, type(value), str(value)))
0084         return value
0085 
0086     @staticmethod
0087     def jellyAll(args, kwargs):
0088         """serialize args and kwargs for wire transfer. The twisted.pb mechanism with
0089         pb.Copyable is too much overhead"""
0090         args2 = Message.jelly('args', args)
0091         kwargs2 = {}
0092         for key, value in kwargs.items():
0093             kwargs2[key] = Message.jelly(key, value)
0094         return args2, kwargs2
0095 
0096 
0097 class ServerMessage(Message):
0098 
0099     """those classes are used for messages from server to client"""
0100     # if sendScore is True, this message will send info about player scoring,
0101     # so the clients can compare
0102     sendScore = False
0103     needsGame = True   # message only applies to an existing game
0104 
0105     def clientAction(self, client, move): # pylint: disable=unused-argument
0106         """default client action: none - this is a virtual class"""
0107         logException(
0108             'clientAction is not defined for %s. msg:%s' %
0109             (self, move))
0110 
0111 
0112 class ClientMessage(Message):
0113 
0114     """those classes are used for messages from client to server"""
0115 
0116     def __init__(self, name=None, shortcut=None):
0117         Message.__init__(self, name, shortcut)
0118 
0119     def buttonCaption(self):
0120         """localized, with a & for the shortcut"""
0121         i18nShortcut = i18nc(
0122             'kajongg game dialog:Key for ' +
0123             self.name,
0124             self.shortcut)
0125         return self.i18nName.replace(i18nShortcut, '&' + i18nShortcut, 1)
0126 
0127     def toolTip(self, button, tile): # pylint: disable=unused-argument
0128         """return text and warning flag for button and text for tile for button and text for tile"""
0129         txt = 'toolTip is not defined for %s' % self.name
0130         logWarning(txt)
0131         return txt, True, ''
0132 
0133     def serverAction(self, table, msg): # pylint: disable=unused-argument
0134         """default server action: none - this is a virtual class"""
0135         logException(
0136             'serverAction is not defined for %s. msg:%s' %
0137             (self, msg))
0138 
0139     @staticmethod
0140     def isActivePlayer(table, msg):
0141         """helper: does the message come from the active player?"""
0142         if msg.player == table.game.activePlayer:
0143             return True
0144         errMsg = '%s said %s but is not the active player' % (
0145             msg.player, msg.answer.i18nName)
0146         table.abort(errMsg)
0147         return False
0148 
0149 
0150 class MessageAbort(ClientMessage):
0151 
0152     """If a client aborts, the server will set the answer for all open requests
0153     to Message.AbortMessage"""
0154 
0155 
0156 class NotifyAtOnceMessage(ClientMessage):
0157 
0158     """those classes are for messages from clients that might have to be relayed to the
0159     other clients right away.
0160 
0161     Example: If a client says Pung, it sends Message.Pung with the flag 'notifying=True'.
0162     This is relayed to the other 3 clients, helping them in their thinking. When the
0163     server decides that the Pung is actually to be executed, it sends Message.Pung
0164     to all 4 clients, but without 'notifying=True'"""
0165 
0166     sendScore = False
0167 
0168     def __init__(self, name=None, shortcut=None):
0169         ClientMessage.__init__(self, name, shortcut)
0170 
0171     def notifyAction(self, client, move): # pylint: disable=unused-argument
0172         """the default action for immediate notifications"""
0173         move.player.popupMsg(self)
0174 
0175     @classmethod
0176     def receivers(cls, request):
0177         """who should get the notification? Default is all players except the
0178         player who triggered us"""
0179         # default: tell all except the source of the notification
0180         game = request.block.table.game
0181         if game:
0182             return [x for x in game.players if x != request.player]
0183         return []
0184 
0185 
0186 class PungChowMessage(NotifyAtOnceMessage):
0187 
0188     """common code for Pung and Chow"""
0189 
0190     def __init__(self, name=None, shortcut=None):
0191         NotifyAtOnceMessage.__init__(self, name=name, shortcut=shortcut)
0192 
0193     def toolTip(self, button, tile): # pylint: disable=unused-argument
0194         """for the action button which will send this message"""
0195         myself = button.client.game.myself
0196         maySay = myself.sayable[self]
0197         if not maySay:
0198             return '', False, ''
0199         txt = []
0200         warn = False
0201         if myself.originalCall and myself.mayWin:
0202             warn = True
0203             txt.append(i18n('saying %1 violates Original Call',
0204                             self.i18nName))
0205         dangerousMelds = myself.maybeDangerous(self)
0206         if dangerousMelds:
0207             lastDiscard = myself.game.lastDiscard
0208             warn = True
0209             if Debug.dangerousGame and len(dangerousMelds) != len(maySay):
0210                 button.client.game.debug(
0211                     'only some claimable melds are dangerous: %s' %
0212                     dangerousMelds)
0213             if len(dangerousMelds) == 1:
0214                 txt.append(i18n(
0215                     'claiming %1 is dangerous because you will have to discard a dangerous tile',
0216                     lastDiscard.name()))
0217             else:
0218                 for meld in dangerousMelds:
0219                     txt.append(i18n(
0220                         'claiming %1 for %2 is dangerous because you will have to discard a dangerous tile',
0221                         lastDiscard.name(), str(meld)))
0222         if not txt:
0223             txt = [i18n('You may say %1', self.i18nName)]
0224         return '<br><br>'.join(txt), warn, ''
0225 
0226 
0227 class MessagePung(PungChowMessage, ServerMessage):
0228 
0229     """somebody said pung and gets the tile"""
0230 
0231     def __init__(self):
0232         PungChowMessage.__init__(self,
0233                                  name=i18ncE('kajongg', 'Pung'),
0234                                  shortcut=i18ncE('kajongg game dialog:Key for Pung', 'P'))
0235 
0236     def serverAction(self, table, msg):
0237         """the server mirrors that and tells all others"""
0238         table.claimTile(msg.player, self, msg.args[0], Message.Pung)
0239 
0240     def clientAction(self, client, move):
0241         """mirror pung call"""
0242         return client.claimed(move)
0243 
0244 
0245 class MessageKong(NotifyAtOnceMessage, ServerMessage):
0246 
0247     """somebody said kong and gets the tile"""
0248 
0249     def __init__(self):
0250         NotifyAtOnceMessage.__init__(self,
0251                                      name=i18ncE('kajongg', 'Kong'),
0252                                      shortcut=i18ncE('kajongg game dialog:Key for Kong', 'K'))
0253 
0254     def serverAction(self, table, msg):
0255         """the server mirrors that and tells all others"""
0256         if table.game.lastDiscard:
0257             table.claimTile(msg.player, self, msg.args[0], Message.Kong)
0258         else:
0259             table.declareKong(msg.player, Meld(msg.args[0]))
0260 
0261     def toolTip(self, button, tile):
0262         """for the action button which will send this message"""
0263         myself = button.client.game.myself
0264         maySay = myself.sayable[self]
0265         if not maySay:
0266             return '', False, ''
0267         txt = []
0268         warn = False
0269         if myself.originalCall and myself.mayWin:
0270             warn = True
0271             txt.append(
0272                 i18n('saying Kong for %1 violates Original Call',
0273                      Tile(maySay[0][0]).name()))
0274         if not txt:
0275             txt = [i18n('You may say Kong for %1', Tile(maySay[0][0]).name())]
0276         return '<br><br>'.join(txt), warn, ''
0277 
0278     def clientAction(self, client, move):
0279         """mirror kong call"""
0280         return client.claimed(move) if client.game.lastDiscard else client.declared(move)
0281 
0282 
0283 class MessageChow(PungChowMessage, ServerMessage):
0284 
0285     """somebody said chow and gets the tile"""
0286 
0287     def __init__(self):
0288         PungChowMessage.__init__(self,
0289                                  name=i18ncE('kajongg', 'Chow'),
0290                                  shortcut=i18ncE('kajongg game dialog:Key for Chow', 'C'))
0291 
0292     def serverAction(self, table, msg):
0293         """the server mirrors that and tells all others"""
0294         if table.game.nextPlayer() != msg.player:
0295             table.abort(
0296                 'player %s illegally said Chow, only %s may' %
0297                 (msg.player, table.game.nextPlayer()))
0298         else:
0299             table.claimTile(msg.player, self, msg.args[0], Message.Chow)
0300 
0301     def clientAction(self, client, move):
0302         """mirror chow call"""
0303         return client.claimed(move)
0304 
0305 
0306 class MessageBonus(ClientMessage):
0307 
0308     """the client says he got a bonus"""
0309 
0310     def serverAction(self, table, msg):
0311         """the server mirrors that"""
0312         if self.isActivePlayer(table, msg):
0313             table.pickTile()
0314 
0315 
0316 class MessageMahJongg(NotifyAtOnceMessage, ServerMessage):
0317 
0318     """somebody sayd mah jongg and wins"""
0319     sendScore = True
0320 
0321     def __init__(self):
0322         NotifyAtOnceMessage.__init__(self,
0323                                      name=i18ncE('kajongg', 'Mah Jongg'),
0324                                      shortcut=i18ncE('kajongg game dialog:Key for Mah Jongg', 'M'))
0325 
0326     def serverAction(self, table, msg):
0327         """the server mirrors that and tells all others"""
0328         table.claimMahJongg(msg)
0329 
0330     def toolTip(self, button, tile): # pylint: disable=unused-argument
0331         """return text and warning flag for button and text for tile"""
0332         return i18n('Press here and you win'), False, ''
0333 
0334     def clientAction(self, client, move):
0335         """mirror the mahjongg action locally. Check if the balances are correct."""
0336         return move.player.declaredMahJongg(move.melds, move.withDiscardTile,
0337                                             move.lastTile, move.lastMeld)
0338 
0339 
0340 class MessageOriginalCall(NotifyAtOnceMessage, ServerMessage):
0341 
0342     """somebody made an original call"""
0343 
0344     def __init__(self):
0345         NotifyAtOnceMessage.__init__(self,
0346                                      name=i18ncE('kajongg', 'Original Call'),
0347                                      shortcut=i18ncE('kajongg game dialog:Key for Original Call', 'O'))
0348 
0349     def serverAction(self, table, msg):
0350         """the server tells all others"""
0351         table.clientDiscarded(msg)
0352 
0353     def toolTip(self, button, tile):
0354         """for the action button which will send this message"""
0355         assert isinstance(tile, Tile), tile
0356         myself = button.client.game.myself
0357         isCalling = bool((myself.hand - tile).callingHands)
0358         if not isCalling:
0359             txt = i18n(
0360                 'discarding %1 and declaring Original Call makes this hand unwinnable',
0361                 tile.name())
0362             return txt, True, txt
0363         return (i18n(
0364             'Discard a tile, declaring Original Call meaning you need only one '
0365             'tile to complete the hand and will not alter the hand in any way (except bonus tiles)'),
0366                 False, '')
0367 
0368     def clientAction(self, client, move):
0369         """mirror the original call"""
0370         player = move.player
0371         if client.thatWasMe(player):
0372             player.originalCallingHand = player.hand
0373             if Debug.originalCall:
0374                 logDebug(
0375                     '%s gets originalCallingHand:%s' %
0376                     (player, player.originalCallingHand))
0377         player.originalCall = True
0378         client.game.addCsvTag('originalCall')
0379         return client.ask(move, [Message.OK])
0380 
0381 
0382 class MessageDiscard(ClientMessage, ServerMessage):
0383 
0384     """somebody discarded a tile"""
0385  #   sendScore = True
0386 
0387     def __init__(self):
0388         ClientMessage.__init__(self,
0389                                name=i18ncE('kajongg', 'Discard'),
0390                                shortcut=i18ncE('kajongg game dialog:Key for Discard', 'D'))
0391 
0392     def serverAction(self, table, msg):
0393         """the server mirrors that action"""
0394         table.clientDiscarded(msg)
0395 
0396     def toolTip(self, button, tile):
0397         """for the action button which will send this message"""
0398         assert isinstance(tile, Tile), tile
0399         game = button.client.game
0400         myself = game.myself
0401         txt = []
0402         warn = False
0403         if myself.violatesOriginalCall(tile):
0404             txt.append(
0405                 i18n('discarding %1 violates Original Call', tile.name()))
0406             warn = True
0407         if game.dangerousFor(myself, tile):
0408             txt.append(i18n('discarding %1 is Dangerous Game', tile.name()))
0409             warn = True
0410         if not txt:
0411             txt = [i18n('discard the least useful tile')]
0412         txtStr = '<br><br>'.join(txt)
0413         return txtStr, warn, txtStr
0414 
0415     def clientAction(self, client, move):
0416         """execute the discard locally"""
0417         if client.isHumanClient() and Internal.scene:
0418             move.player.handBoard.setEnabled(False)
0419         move.player.speak(move.tile)
0420         return client.game.hasDiscarded(move.player, move.tile)
0421 
0422 
0423 class MessageProposeGameId(ServerMessage):
0424 
0425     """the game server proposes a new game id. We check if it is available
0426     in our local data base - we want to use the same gameid everywhere"""
0427     needsGame = False
0428 
0429     def clientAction(self, client, move):
0430         """ask the client"""
0431         # move.playerNames are the players in seating order
0432         # we cannot just use table.playerNames - the seating order is now
0433         # different (random)
0434         return client.reserveGameId(move.gameid)
0435 
0436 
0437 class MessageTableChanged(ServerMessage):
0438 
0439     """somebody joined or left a table"""
0440     needsGame = False
0441 
0442     def clientAction(self, client, move):
0443         """update our copy"""
0444         return client.tableChanged(move.source)
0445 
0446 
0447 class MessageReadyForGameStart(ServerMessage):
0448 
0449     """the game server asks us if we are ready for game start"""
0450     needsGame = False
0451 
0452     def clientAction(self, client, move):
0453         """ask the client"""
0454         def hideTableList(result):
0455             """hide it only after player says I am ready"""
0456             # set scene.game first, otherwise tableList.hide()
0457             # sees no current game and logs out
0458             if result == Message.OK:
0459                 if client.game and Internal.scene:
0460                     Internal.scene.game = client.game
0461             if result == Message.OK and client.tableList and client.tableList.isVisible():
0462                 if Debug.table:
0463                     logDebug(
0464                         '%s hiding table list because game started' %
0465                         client.name)
0466                 client.tableList.hide()
0467             return result
0468         # move.playerNames are the players in seating order
0469         # we cannot just use table.playerNames - the seating order is now
0470         # different (random)
0471         return client.readyForGameStart(
0472             move.tableid, move.gameid,
0473             move.wantedGame, move.playerNames, shouldSave=move.shouldSave).addCallback(hideTableList)
0474 
0475 
0476 class MessageNoGameStart(NotifyAtOnceMessage):
0477 
0478     """the client says he does not want to start the game after all"""
0479     needsGame = False
0480 
0481     def notifyAction(self, client, move):
0482         if client.beginQuestion or client.game:
0483             Sorry(i18n('%1 is not ready to start the game', move.player.name))
0484         if client.beginQuestion:
0485             client.beginQuestion.cancel()
0486         elif client.game:
0487             return client.game.close()
0488         return None
0489 
0490     @classmethod
0491     def receivers(cls, request):
0492         """notification is not needed for those who already said no game"""
0493         return [x.player for x in request.block.requests if x.answer != Message.NoGameStart]
0494 
0495 
0496 class MessageReadyForHandStart(ServerMessage):
0497 
0498     """the game server asks us if we are ready for a new hand"""
0499 
0500     def clientAction(self, client, move):
0501         """ask the client"""
0502         return client.readyForHandStart(move.playerNames, move.rotateWinds)
0503 
0504 
0505 class MessageInitHand(ServerMessage):
0506 
0507     """the game server tells us to prepare a new hand"""
0508 
0509     def clientAction(self, client, move):
0510         """prepare a new hand"""
0511         client.game.divideAt = move.divideAt
0512         client.game.wall.divide()
0513         if hasattr(client, 'shutdownHumanClients'):
0514             client.shutdownHumanClients(exception=client)
0515         scene = Internal.scene
0516         if scene:
0517             scene.mainWindow.setWindowTitle(
0518                 i18n(
0519                     'Kajongg <numid>%1</numid>',
0520                     client.game.handId.seed))
0521             scene.discardBoard.setRandomPlaces(client.game.randomGenerator)
0522         client.game.initHand()
0523 
0524 
0525 class MessageSetConcealedTiles(ServerMessage):
0526 
0527     """the game server assigns tiles to player"""
0528 
0529     def clientAction(self, client, move):
0530         """set tiles for player"""
0531         return move.player.addConcealedTiles(client.game.wall.deal(move.tiles), animated=False)
0532 
0533 
0534 class MessageShowConcealedTiles(ServerMessage):
0535 
0536     """the game server assigns tiles to player"""
0537 
0538     def clientAction(self, client, move):
0539         """set tiles for player"""
0540         return move.player.showConcealedTiles(move.tiles, move.show)
0541 
0542 
0543 class MessageSaveHand(ServerMessage):
0544 
0545     """the game server tells us to save the hand"""
0546 
0547     def clientAction(self, client, move):
0548         """save the hand"""
0549         return client.game.saveHand()
0550 
0551 
0552 class MessageAskForClaims(ServerMessage):
0553 
0554     """the game server asks us if we want to claim a tile"""
0555 
0556     def clientAction(self, client, move):
0557         """ask the player"""
0558         if client.thatWasMe(move.player):
0559             raise Exception(
0560                 'Server asked me(%s) for claims but I just discarded that tile!' %
0561                 move.player)
0562         return client.ask(move, [Message.NoClaim, Message.Chow, Message.Pung, Message.Kong, Message.MahJongg])
0563 
0564 
0565 class MessagePickedTile(ServerMessage):
0566 
0567     """the game server tells us who picked a tile"""
0568 
0569     def clientAction(self, client, move):
0570         """mirror the picked tile"""
0571         assert move.player.pickedTile(move.deadEnd, tileName=move.tile) == move.tile, \
0572             (move.player.lastTile, move.tile)
0573         if client.thatWasMe(move.player):
0574             return (Message.Bonus, move.tile) if move.tile.isBonus else client.myAction(move)
0575         return None
0576 
0577 
0578 class MessageActivePlayer(ServerMessage):
0579 
0580     """the game server tells us whose turn it is"""
0581 
0582     def clientAction(self, client, move):
0583         """set the active player"""
0584         client.game.activePlayer = move.player
0585 
0586 
0587 class MessageViolatesOriginalCall(ServerMessage):
0588 
0589     """the game server tells us who violated an original call"""
0590 
0591     def __init__(self):
0592         ServerMessage.__init__(
0593             self,
0594             name=i18ncE('kajongg',
0595                         'Violates Original Call'))
0596 
0597     def clientAction(self, client, move):
0598         """violation: player may not say mah jongg"""
0599         move.player.popupMsg(self)
0600         move.player.mayWin = False
0601         if Debug.originalCall:
0602             logDebug('%s: cleared mayWin' % move.player)
0603         return client.ask(move, [Message.OK])
0604 
0605 
0606 class MessageVoiceId(ServerMessage):
0607 
0608     """we got a voice id from the server. If we have no sounds for
0609     this voice, ask the server"""
0610 
0611     def clientAction(self, client, move):
0612         """the server gave us a voice id about another player"""
0613         if Internal.Preferences.useSounds and Options.gui:
0614             move.player.voice = Voice.locate(move.source)
0615             if not move.player.voice:
0616                 return Message.ClientWantsVoiceData, move.source
0617         return None
0618 
0619 
0620 class MessageVoiceData(ServerMessage):
0621 
0622     """we got voice sounds from the server, assign them to the player voice"""
0623 
0624     def clientAction(self, client, move):
0625         """server sent us voice sounds about somebody else"""
0626         move.player.voice = Voice(move.md5sum, move.source)
0627         if Debug.sound:
0628             logDebug('%s gets voice data %s from server, language=%s' % (
0629                 move.player, move.player.voice, move.player.voice.language()))
0630 
0631 
0632 class MessageAssignVoices(ServerMessage):
0633 
0634     """The server tells us that we now got all voice data available"""
0635 
0636     def clientAction(self, client, move):
0637         if Internal.Preferences.useSounds and Options.gui:
0638             client.game.assignVoices()
0639 
0640 
0641 class MessageClientWantsVoiceData(ClientMessage):
0642 
0643     """This client wants voice sounds"""
0644 
0645 
0646 class MessageServerWantsVoiceData(ServerMessage):
0647 
0648     """The server wants voice sounds from a client"""
0649 
0650     def clientAction(self, client, move):
0651         """send voice sounds as requested to server"""
0652         if Debug.sound:
0653             logDebug('%s: send wanted voice data %s to server' % (
0654                 move.player, move.player.voice))
0655         return Message.ServerGetsVoiceData, move.player.voice.archiveContent
0656 
0657 
0658 class MessageServerGetsVoiceData(ClientMessage):
0659 
0660     """The server gets voice sounds from a client"""
0661 
0662     def serverAction(self, table, msg): # pylint: disable=unused-argument
0663         """save voice sounds on the server"""
0664         voice = msg.player.voice
0665         voice.archiveContent = msg.args[0]
0666         if Debug.sound:
0667             if voice.oggFiles():
0668                 logDebug('%s: server got wanted voice data %s' % (
0669                     msg.player, voice))
0670             else:
0671                 logDebug('%s: server got empty voice data %s (arg0=%s)' % (
0672                     msg.player, voice, repr(msg.args[0][:100])))
0673 
0674 
0675 class MessageDeclaredKong(ServerMessage):
0676 
0677     """the game server tells us who declared a kong"""
0678 
0679     def clientAction(self, client, move):
0680         """mirror the action locally"""
0681         prompts = None
0682         if not client.thatWasMe(move.player):
0683             if len(move.meld) != 4 or move.meld[0].isConcealed:
0684                 # do not do this when adding a 4th tile to an exposed pung
0685                 move.player.showConcealedTiles(move.meld)
0686             else:
0687                 move.player.showConcealedTiles(move.meld[3])
0688             prompts = [Message.NoClaim, Message.MahJongg]
0689         move.exposedMeld = move.player.exposeMeld(move.meld)
0690         return client.ask(move, prompts) if prompts else None
0691 
0692 
0693 class MessageRobbedTheKong(NotifyAtOnceMessage, ServerMessage):
0694 
0695     """the game server tells us who robbed the kong"""
0696 
0697     def clientAction(self, client, move):
0698         """mirror the action locally"""
0699         prevMove = next(client.game.lastMoves(only=[Message.DeclaredKong]))
0700         prevMove.player.robTileFrom(prevMove.meld[0].concealed)
0701         move.player.robsTile()
0702         client.game.addCsvTag(
0703             'robbedKong%s' % prevMove.meld[1],
0704             forAllPlayers=True)
0705 
0706 
0707 class MessageCalling(ServerMessage):
0708 
0709     """the game server tells us who announced a calling hand"""
0710 
0711     def clientAction(self, client, move):
0712         """tell user and save this information locally"""
0713         move.player.popupMsg(self)
0714         move.player.isCalling = True
0715         return client.ask(move, [Message.OK])
0716 
0717 
0718 class MessageDangerousGame(ServerMessage):
0719 
0720     """the game server tells us who played dangerous game"""
0721 
0722     def __init__(self):
0723         ServerMessage.__init__(self, name=i18ncE('kajongg', 'Dangerous Game'))
0724 
0725     def clientAction(self, client, move):
0726         """mirror the dangerous game action locally"""
0727         move.player.popupMsg(self)
0728         move.player.playedDangerous = True
0729         return client.ask(move, [Message.OK])
0730 
0731 
0732 class MessageNoChoice(ServerMessage):
0733 
0734     """the game server tells us who had no choice avoiding dangerous game"""
0735 
0736     def __init__(self):
0737         ServerMessage.__init__(self, name=i18ncE('kajongg', 'No Choice'))
0738         self.move = None
0739 
0740     def clientAction(self, client, move):
0741         """mirror the no choice action locally"""
0742         self.move = move
0743         move.player.popupMsg(self)
0744         move.player.claimedNoChoice = True
0745         move.player.showConcealedTiles(move.tiles)
0746         # otherwise we have a visible artifact of the discarded tile.
0747         # Only when animations are disabled. Why?
0748 #        Internal.mainWindow.centralView.resizeEvent(None)
0749         return client.ask(move, [Message.OK]).addCallback(self.hideConcealedAgain)
0750 
0751     def hideConcealedAgain(self, result):
0752         """only show them for explaining the 'no choice'"""
0753         self.move.player.showConcealedTiles(self.move.tiles, False)
0754         return result
0755 
0756 
0757 class MessageUsedDangerousFrom(ServerMessage):
0758 
0759     """the game server tells us somebody claimed a dangerous tile"""
0760 
0761     def clientAction(self, client, move):
0762         fromPlayer = client.game.playerByName(move.source)
0763         move.player.usedDangerousFrom = fromPlayer
0764         if Debug.dangerousGame:
0765             logDebug('%s claimed a dangerous tile discarded by %s' %
0766                      (move.player, fromPlayer))
0767 
0768 
0769 class MessageDraw(ServerMessage):
0770 
0771     """the game server tells us nobody said mah jongg"""
0772     sendScore = True
0773 
0774 
0775 class MessageError(ServerMessage):
0776 
0777     """a client errors"""
0778     needsGame = False
0779 
0780     def clientAction(self, client, move):
0781         """show the error message from server"""
0782         return logWarning(move.source)
0783 
0784 
0785 class MessageNO(ClientMessage):
0786 
0787     """a client says no"""
0788 
0789 
0790 class MessageOK(ClientMessage):
0791 
0792     """a client says OK"""
0793 
0794     def __init__(self):
0795         ClientMessage.__init__(self,
0796                                name=i18ncE('kajongg', 'OK'),
0797                                shortcut=i18ncE('kajongg game dialog:Key for OK', 'O'))
0798 
0799     def toolTip(self, button, tile): # pylint: disable=unused-argument
0800         """return text and warning flag for button and text for tile for button and text for tile"""
0801         return i18n('Confirm that you saw the message'), False, ''
0802 
0803 
0804 class MessageNoClaim(NotifyAtOnceMessage, ServerMessage):
0805 
0806     """A player explicitly says he will not claim a tile"""
0807 
0808     def __init__(self):
0809         NotifyAtOnceMessage.__init__(self,
0810                                      name=i18ncE('kajongg', 'No Claim'),
0811                                      shortcut=i18ncE('kajongg game dialog:Key for No claim', 'N'))
0812 
0813     def toolTip(self, button, tile): # pylint: disable=unused-argument
0814         """return text and warning flag for button and text for tile for button and text for tile"""
0815         return i18n('You cannot or do not want to claim this tile'), False, ''
0816 
0817     @classmethod
0818     def receivers(cls, request):
0819         """no Claim notifications are not needed for those who already answered"""
0820         return [x.player for x in request.block.requests if x.answer is None]
0821 
0822 
0823 def __scanSelf():
0824     """for every message defined in this module which can actually be used for traffic,
0825     generate a class variable Message.msg where msg is the name (without spaces)
0826     of the message. Example: 'Message.NoClaim'.
0827     Those will be used as stateless constants. Also add them to dict Message.defined, but with spaces."""
0828     if Message.defined:
0829         return
0830     for glob in globals().values():
0831         if hasattr(glob, "__mro__"):
0832             if glob.__mro__[-2] == Message and len(glob.__mro__) > 2:
0833                 if glob.__name__.startswith('Message'):
0834                     try:
0835                         msg = glob()
0836                     except Exception:
0837                         logDebug('cannot instantiate %s' % glob.__name__)
0838                         raise
0839                     type.__setattr__(
0840                         Message, msg.name.replace(' ', ''), msg)
0841                     Message.defined[msg.name] = msg
0842 
0843 
0844 class ChatMessage(StrMixin):
0845 
0846     """holds relevant info about a chat message"""
0847 
0848     def __init__(self, tableid, fromUser=None,
0849                  message=None, isStatusMessage=False):
0850         if isinstance(tableid, tuple):
0851             self.tableid, hour, minute, second, self.fromUser, self.message, self.isStatusMessage = tableid
0852             self.timestamp = datetime.time(
0853                 hour=hour,
0854                 minute=minute,
0855                 second=second)
0856         else:
0857             self.tableid = tableid
0858             self.fromUser = fromUser
0859             self.message = message
0860             self.isStatusMessage = isStatusMessage
0861             self.timestamp = datetime.datetime.utcnow().time()
0862 
0863     def localtimestamp(self):
0864         """convert from UTC to local"""
0865         now = datetime.datetime.now()
0866         utcnow = datetime.datetime.utcnow()
0867         result = datetime.datetime.combine(
0868             datetime.date.today(),
0869             self.timestamp)
0870         return result + (now - utcnow)
0871 
0872     def __str__(self):
0873         local = self.localtimestamp()
0874         # pylint: disable=no-member
0875         # pylint says something about NotImplemented, check with later versions
0876         _ = i18n(self.message)
0877         if self.isStatusMessage:
0878             _ = '[{}]'.format(_)
0879         return '%02d:%02d:%02d %s: %s' % (
0880             local.hour,
0881             local.minute,
0882             local.second,
0883             self.fromUser,
0884             i18n(self.message))
0885 
0886     def asList(self):
0887         """encode me for network transfer"""
0888         return (
0889             self.tableid, self.timestamp.hour, self.timestamp.minute, self.timestamp.second,
0890             self.fromUser, self.message, self.isStatusMessage)
0891 
0892 __scanSelf()