File indexing completed on 2024-04-28 07:51:08
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()