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

0001 # -*- coding: utf-8 -*-
0002 
0003 """
0004 Copyright (C) 2009-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0005 
0006 SPDX-License-Identifier: GPL-2.0
0007 
0008 
0009 The DBPasswordChecker is based on an example from the book
0010 Twisted Network Programming Essentials by Abe Fettig, 2006
0011 O'Reilly Media, Inc., ISBN 0-596-10032-9
0012 """
0013 
0014 import os
0015 import random
0016 import traceback
0017 from itertools import chain
0018 from twisted.spread import pb
0019 
0020 from common import Debug, Internal, StrMixin
0021 from wind import Wind
0022 from tilesource import TileSource
0023 from util import Duration
0024 from message import Message, ChatMessage
0025 from log import logDebug, logError
0026 from mi18n import i18nE, i18n, i18ncE
0027 from deferredutil import DeferredBlock
0028 from tile import Tile, TileList, elements
0029 from meld import Meld, MeldList
0030 from query import Query
0031 from client import Client, Table
0032 from wall import WallEmpty
0033 from sound import Voice
0034 from servercommon import srvError
0035 from user import User
0036 from game import PlayingGame
0037 
0038 if os.name != 'nt':
0039     import resource
0040 
0041 
0042 class ServerGame(PlayingGame):
0043 
0044     """the central game instance on the server"""
0045     # pylint: disable=too-many-arguments, too-many-public-methods
0046 
0047     def __init__(self, names, ruleset, gameid=None, wantedGame=None,
0048                  client=None, playOpen=False, autoPlay=False):
0049         PlayingGame.__init__(
0050             self,
0051             names,
0052             ruleset,
0053             gameid,
0054             wantedGame,
0055             client,
0056             playOpen,
0057             autoPlay)
0058         self.shouldSave = True
0059 
0060     def throwDices(self):
0061         """set random living and kongBox
0062         sets divideAt: an index for the wall break"""
0063         self.wall.tiles.sort()
0064         self.randomGenerator.shuffle(self.wall.tiles)
0065         PlayingGame.throwDices(self)
0066 
0067     def initHand(self):
0068         """Happens only on server: every player gets 13 tiles (including east)"""
0069         self.throwDices()
0070         self.wall.divide()
0071         for player in self.players:
0072             player.clearHand()
0073             # 13 tiles at least, with names as given by wall
0074             # compensate boni
0075             while len(player.concealedTiles) != 13:
0076                 player.addConcealedTiles(self.wall.deal())
0077         PlayingGame.initHand(self)
0078 
0079 
0080 class ServerTable(Table, StrMixin):
0081 
0082     """a table on the game server"""
0083     # pylint: disable=too-many-arguments
0084 
0085     def __init__(self, server, owner, ruleset, suspendedAt,
0086                  playOpen, autoPlay, wantedGame, tableId=None):
0087         if tableId is None:
0088             tableId = server.generateTableId()
0089         Table.__init__(
0090             self,
0091             tableId,
0092             ruleset,
0093             suspendedAt,
0094             False,
0095             playOpen,
0096             autoPlay,
0097             wantedGame)
0098         self.server = server
0099         self.owner = owner
0100         self.users = [owner] if owner else []
0101         self.remotes = {}   # maps client connections to users
0102         self.game = None
0103         self.client = None
0104         server.tables[self.tableid] = self
0105         if Debug.table:
0106             logDebug('new table %s' % self)
0107 
0108     def hasName(self, name):
0109         """return True if one of the players in the game is named 'name'"""
0110         return bool(self.game) and any(x.name == name for x in self.game.players)
0111 
0112     def asSimpleList(self, withFullRuleset=False):
0113         """return the table attributes to be sent to the client"""
0114         game = self.game
0115         onlineNames = [x.name for x in self.users]
0116         if self.suspendedAt:
0117             names = tuple(x.name for x in game.players)
0118         else:
0119             names = tuple(x.name for x in self.users)
0120         online = tuple(bool(x in onlineNames) for x in names)
0121         if game:
0122             endValues = game.handctr, {x.wind.char: x.balance for x in game.players}
0123         else:
0124             endValues = None
0125         return list([
0126             self.tableid,
0127             self.ruleset.toList() if withFullRuleset else self.ruleset.hash,
0128             game.gameid if game else None, self.suspendedAt, self.running,
0129             self.playOpen, self.autoPlay, self.wantedGame, names, online, endValues])
0130 
0131     def maxSeats(self):
0132         """for a new game: 4. For a suspended game: The
0133         number of humans before suspending"""
0134         result = 4
0135         if self.suspendedAt:
0136             result -= sum(x.name.startswith('Robot ')
0137                           for x in self.game.players)
0138         return result
0139 
0140     def sendChatMessage(self, chatLine):
0141         """sends a chat messages to all clients"""
0142         if Debug.chat:
0143             logDebug('server sends chat msg %s' % chatLine)
0144         if self.suspendedAt and self.game:
0145             chatters = []
0146             for player in self.game.players:
0147                 chatters.extend(
0148                     x for x in self.server.srvUsers if x.name == player.name)
0149         else:
0150             chatters = self.users
0151         for other in chatters:
0152             self.server.callRemote(other, 'chat', chatLine.asList())
0153 
0154     def addUser(self, user):
0155         """add user to this table"""
0156         if user.name in (x.name for x in self.users):
0157             raise srvError(pb.Error, i18nE('You already joined this table'))
0158         if len(self.users) == self.maxSeats():
0159             raise srvError(pb.Error, i18nE('All seats are already taken'))
0160         self.users.append(user)
0161         if Debug.table:
0162             logDebug('%s seated on table %s' % (user.name, self))
0163         self.sendChatMessage(ChatMessage(self.tableid, user.name,
0164                                          i18nE('takes a seat'), isStatusMessage=True))
0165 
0166     def delUser(self, user):
0167         """remove user from this table"""
0168         if user in self.users:
0169             self.running = False
0170             self.users.remove(user)
0171             self.sendChatMessage(ChatMessage(self.tableid, user.name,
0172                                              i18nE('leaves the table'), isStatusMessage=True))
0173             if user is self.owner:
0174                 # silently pass ownership
0175                 if self.users:
0176                     self.owner = self.users[0]
0177                     if Debug.table:
0178                         logDebug('%s leaves table %d, %s is the new owner' % (
0179                             user.name, self.tableid, self.owner))
0180                 else:
0181                     if Debug.table:
0182                         logDebug('%s leaves table %d, table is now empty' % (
0183                             user.name, self.tableid))
0184             else:
0185                 if Debug.table:
0186                     logDebug('%s leaves table %d, %s stays owner' % (
0187                         user.name, self.tableid, self.owner))
0188 
0189     def __str__(self):
0190         """for debugging output"""
0191         if self.users:
0192             onlineNames = [x.name + ('(Owner)' if self.owner and x == self.owner.name else '')
0193                            for x in self.users]
0194         else:
0195             onlineNames = ['no users yet']
0196         offlineString = ''
0197         if self.game:
0198             offlineNames = [x.name for x in self.game.players if x.name not in onlineNames
0199                             and not x.name.startswith('Robot')]
0200             if offlineNames:
0201                 offlineString = ' offline:' + ','.join(offlineNames)
0202         return '%d(%s%s)' % (self.tableid, ','.join(onlineNames), offlineString)
0203 
0204     def calcGameId(self):
0205         """based upon the max gameids we got from the clients, propose
0206         a new one, we want to use the same gameid in all data bases"""
0207         serverMaxGameId = Query('select max(id) from game').records[0][0]
0208         serverMaxGameId = int(serverMaxGameId) if serverMaxGameId else 0
0209         gameIds = [x.maxGameId for x in self.users]
0210         gameIds.append(serverMaxGameId)
0211         return max(gameIds) + 1
0212 
0213     def __prepareNewGame(self):
0214         """return a new game object"""
0215         names = [x.name for x in self.users]
0216         # the server and all databases save the english name but we
0217         # want to make sure a translation exists for the client GUI
0218         robotNames = [
0219             i18ncE(
0220                 'kajongg, name of robot player, to be translated',
0221                 'Robot 1'),
0222             i18ncE(
0223                 'kajongg, name of robot player, to be translated',
0224                 'Robot 2'),
0225             i18ncE('kajongg, name of robot player, to be translated', 'Robot 3')]
0226         while len(names) < 4:
0227             names.append(robotNames[3 - len(names)])
0228         names = [tuple([Wind.all4[idx], name]) for idx, name in enumerate(names)]
0229         self.client = Client()
0230                              # Game has a weakref to client, so we must keep
0231                              # it!
0232         return ServerGame(names, self.ruleset, client=self.client,
0233                           playOpen=self.playOpen, autoPlay=self.autoPlay, wantedGame=self.wantedGame)
0234 
0235     def userForPlayer(self, player):
0236         """finds the table user corresponding to player"""
0237         for result in self.users:
0238             if result.name == player.name:
0239                 return result
0240         return None
0241 
0242     def __connectPlayers(self):
0243         """connects client instances with the game players"""
0244         game = self.game
0245         for player in game.players:
0246             remote = self.userForPlayer(player)
0247             if not remote:
0248                 # we found a robot player, its client runs in this server
0249                 # process
0250                 remote = Client(player.name)
0251                 remote.table = self
0252             self.remotes[player] = remote
0253 
0254     def __checkDbIdents(self):
0255         """for 4 players, we have up to 4 data bases:
0256         more than one player might use the same data base.
0257         However the server always needs to use its own data base.
0258         If a data base is used by more than one client, only one of
0259         them should update. Here we set shouldSave for all players,
0260         while the server always saves"""
0261         serverIdent = Internal.db.identifier
0262         dbIdents = set()
0263         game = self.game
0264         for player in game.players:
0265             player.shouldSave = False
0266             if isinstance(self.remotes[player], User):
0267                 dbIdent = self.remotes[player].dbIdent
0268                 assert dbIdent != serverIdent, \
0269                     'client and server try to use the same database:%s' % \
0270                     Internal.db.path
0271                 player.shouldSave = dbIdent not in dbIdents
0272                 dbIdents.add(dbIdent)
0273 
0274     def readyForGameStart(self, user):
0275         """the table initiator told us he wants to start the game"""
0276         if len(self.users) < self.maxSeats() and self.owner != user:
0277             raise srvError(pb.Error,
0278                            i18nE(
0279                                'Only the initiator %1 can start this game, you are %2'),
0280                            self.owner.name, user.name)
0281         if self.suspendedAt:
0282             self.__connectPlayers()
0283             self.__checkDbIdents()
0284             self.initGame()
0285         else:
0286             self.game = self.__prepareNewGame()
0287             self.__connectPlayers()
0288             self.__checkDbIdents()
0289             self.proposeGameId(self.calcGameId())
0290         # TODO: remove table for all other srvUsers out of sight
0291 
0292     def proposeGameId(self, gameid):
0293         """server proposes an id to the clients ands waits for answers"""
0294         while True:
0295             query = Query('insert into game(id,seed) values(?,?)',
0296                           (gameid, 'proposed'), mayFail=True, failSilent=True)
0297             if not query.failure:
0298                 break
0299             gameid += random.randrange(1, 100)
0300         block = DeferredBlock(self)
0301         for player in self.game.players:
0302             if player.shouldSave and isinstance(self.remotes[player], User):
0303                 # do not ask robot players, they use the server data base
0304                 block.tellPlayer(player, Message.ProposeGameId, gameid=gameid)
0305         block.callback(self.collectGameIdAnswers, gameid)
0306 
0307     def collectGameIdAnswers(self, requests, gameid):
0308         """clients answered if the proposed game id is free"""
0309         if requests:
0310             # when errors happen, there might be no requests left
0311             for msg in requests:
0312                 if msg.answer == Message.NO:
0313                     self.proposeGameId(gameid + 1)
0314                     return
0315                 if msg.answer != Message.OK:
0316                     raise srvError(
0317                         pb.Error,
0318                         'collectGameIdAnswers got neither NO nor OK')
0319             self.game.gameid = gameid
0320             self.initGame()
0321 
0322     def initGame(self):
0323         """ask clients if they are ready to start"""
0324         game = self.game
0325         game.saveStartTime()
0326         block = DeferredBlock(self)
0327         for player in game.players:
0328             block.tellPlayer(
0329                 player, Message.ReadyForGameStart, tableid=self.tableid,
0330                 gameid=game.gameid, shouldSave=player.shouldSave,
0331                 wantedGame=game.wantedGame, playerNames=[(x.wind, x.name) for x in game.players])
0332         block.callback(self.startGame)
0333 
0334     def startGame(self, requests):
0335         """if all players said ready, start the game"""
0336         for user in self.users:
0337             userRequests = [x for x in requests if x.user == user]
0338             if not userRequests or userRequests[0].answer == Message.NoGameStart:
0339                 if Debug.table:
0340                     if not userRequests:
0341                         logDebug(
0342                             'Server.startGame: found no request for user %s' %
0343                             user.name)
0344                     else:
0345                         logDebug(
0346                             'Server.startGame: %s said NoGameStart' %
0347                             user.name)
0348                 self.game = None
0349                 return
0350         if Debug.table:
0351             logDebug('Game starts on table %s' % self)
0352         elementIter = iter(elements.all(self.game.ruleset))
0353         wallSize = len(self.game.wall.tiles)
0354         self.game.wall.tiles = []
0355         for _ in range(wallSize):
0356             self.game.wall.tiles.append(next(elementIter).concealed)
0357         assert isinstance(self.game, ServerGame), self.game
0358         self.running = True
0359         self.__adaptOtherTables()
0360         self.sendVoiceIds()
0361 
0362     def __adaptOtherTables(self):
0363         """if the players on this table also reserved seats on other tables, clear them
0364         make running table invisible for other users"""
0365         for user in self.users:
0366             for tableid in self.server.tablesWith(user):
0367                 if tableid != self.tableid:
0368                     self.server.leaveTable(user, tableid)
0369         foreigners = [x for x in self.server.srvUsers if x not in self.users]
0370         if foreigners:
0371             if Debug.table:
0372                 logDebug(
0373                     'make running table %s invisible for %s' %
0374                     (self, ','.join(str(x) for x in foreigners)))
0375             for srvUser in foreigners:
0376                 self.server.callRemote(
0377                     srvUser,
0378                     'tableRemoved',
0379                     self.tableid,
0380                     '')
0381 
0382     def sendVoiceIds(self):
0383         """tell each player what voice ids the others have. By now the client has a Game instance!"""
0384         humanPlayers = [
0385             x for x in self.game.players if isinstance(self.remotes[x], User)]
0386         if len(humanPlayers) < 2 or not any(self.remotes[x].voiceId for x in humanPlayers):
0387             # no need to pass around voice data
0388             self.assignVoices()
0389             return
0390         block = DeferredBlock(self)
0391         for player in humanPlayers:
0392             remote = self.remotes[player]
0393             if remote.voiceId:
0394                 # send it to other human players:
0395                 others = [x for x in humanPlayers if x != player]
0396                 if Debug.sound:
0397                     logDebug('telling other human players that %s has voiceId %s' % (
0398                         player.name, remote.voiceId))
0399                 block.tell(
0400                     player,
0401                     others,
0402                     Message.VoiceId,
0403                     source=remote.voiceId)
0404         block.callback(self.collectVoiceData)
0405 
0406     def collectVoiceData(self, requests):
0407         """collect voices of other players"""
0408         if not self.running:
0409             return
0410         block = DeferredBlock(self)
0411         voiceDataRequests = []
0412         for request in requests:
0413             if request.answer == Message.ClientWantsVoiceData:
0414                 # another human player requests sounds for voiceId
0415                 voiceId = request.args[0]
0416                 voiceFor = [x for x in self.game.players if isinstance(self.remotes[x], User)
0417                             and self.remotes[x].voiceId == voiceId][0]
0418                 voiceFor.voice = Voice(voiceId)
0419                 if Debug.sound:
0420                     logDebug(
0421                         'client %s wants voice data %s for %s' %
0422                         (request.user.name, request.args[0], voiceFor))
0423                 voiceDataRequests.append((request.user, voiceFor))
0424                 if not voiceFor.voice.oggFiles():
0425                     # the server does not have it, ask the client with that
0426                     # voice
0427                     block.tell(
0428                         voiceFor,
0429                         voiceFor,
0430                         Message.ServerWantsVoiceData)
0431         block.callback(self.sendVoiceData, voiceDataRequests)
0432 
0433     def sendVoiceData(self, requests, voiceDataRequests):
0434         """sends voice sounds to other human players"""
0435         self.processAnswers(requests)
0436         block = DeferredBlock(self)
0437         for voiceDataRequester, voiceFor in voiceDataRequests:
0438             # this player requested sounds for voiceFor
0439             voice = voiceFor.voice
0440             content = voice.archiveContent
0441             if content:
0442                 if Debug.sound:
0443                     logDebug(
0444                         'server got voice data %s for %s from client' %
0445                         (voiceFor.voice, voiceFor.name))
0446                 block.tell(
0447                     voiceFor,
0448                     voiceDataRequester,
0449                     Message.VoiceData,
0450                     md5sum=voice.md5sum,
0451                     source=content)
0452             elif Debug.sound:
0453                 logDebug('server got empty voice data %s for %s from client' % (
0454                     voice, voiceFor.name))
0455         block.callback(self.assignVoices)
0456 
0457     def assignVoices(self, unusedResults=None):
0458         """now all human players have all voice data needed"""
0459         humanPlayers = [
0460             x for x in self.game.players if isinstance(self.remotes[x], User)]
0461         block = DeferredBlock(self)
0462         block.tell(None, humanPlayers, Message.AssignVoices)
0463         block.callback(self.startHand)
0464 
0465     def pickTile(self, unusedResults=None, deadEnd=False):
0466         """the active player gets a tile from wall. Tell all clients."""
0467         if not self.running:
0468             return
0469         player = self.game.activePlayer
0470         try:
0471             tile = player.pickedTile(deadEnd)
0472         except WallEmpty:
0473             self.endHand()
0474         else:
0475             self.game.lastDiscard = None
0476             block = DeferredBlock(self)
0477             block.tellPlayer(
0478                 player,
0479                 Message.PickedTile,
0480                 tile=tile,
0481                 deadEnd=deadEnd)
0482             showTile = tile if tile.isBonus or self.game.playOpen else Tile.unknown
0483             block.tellOthers(
0484                 player,
0485                 Message.PickedTile,
0486                 tile=showTile,
0487                 deadEnd=deadEnd)
0488             block.callback(self.moved)
0489 
0490     def pickKongReplacement(self, requests=None):
0491         """the active player gets a tile from the dead end. Tell all clients."""
0492         requests = self.prioritize(requests)
0493         if requests and requests[0].answer == Message.MahJongg:
0494             # somebody robs my kong
0495             requests[0].answer.serverAction(self, requests[0])
0496         else:
0497             self.pickTile(requests, deadEnd=True)
0498 
0499     def clientDiscarded(self, msg):
0500         """client told us he discarded a tile. Check for consistency and tell others."""
0501         if not self.running:
0502             return
0503         player = msg.player
0504         assert player == self.game.activePlayer
0505         tile = Tile(msg.args[0])
0506         if tile not in player.concealedTiles:
0507             self.abort(
0508                 'player %s discarded %s but does not have it' %
0509                 (player, tile))
0510             return
0511         dangerousText = self.game.dangerousFor(player, tile)
0512         mustPlayDangerous = player.mustPlayDangerous()
0513         violates = player.violatesOriginalCall(tile)
0514         self.game.hasDiscarded(player, tile)
0515         block = DeferredBlock(self)
0516         block.tellAll(player, Message.Discard, tile=tile)
0517         block.callback(self._clientDiscarded2, msg, dangerousText, mustPlayDangerous, violates)
0518 
0519     def _clientDiscarded2(self, unusedResults, msg, dangerousText, mustPlayDangerous, violates):
0520         """client told us he discarded a tile. Continue, check for violating original call"""
0521         block = DeferredBlock(self)
0522         player = msg.player
0523         if violates:
0524             if Debug.originalCall:
0525                 tile = Tile(msg.args[0])
0526                 logDebug('%s just violated OC with %s' % (player, tile))
0527             player.mayWin = False
0528             block.tellAll(player, Message.ViolatesOriginalCall)
0529         block.callback(self._clientDiscarded3, msg, dangerousText, mustPlayDangerous)
0530 
0531     def _clientDiscarded3(self, unusedResults, msg, dangerousText, mustPlayDangerous):
0532         """client told us he discarded a tile. Continue, check for calling"""
0533         block = DeferredBlock(self)
0534         player = msg.player
0535         if self.game.ruleset.mustDeclareCallingHand and not player.isCalling:
0536             if player.hand.callingHands:
0537                 player.isCalling = True
0538                 block.tellAll(player, Message.Calling)
0539         block.callback(self._clientDiscarded4, msg, dangerousText, mustPlayDangerous)
0540 
0541     def _clientDiscarded4(self, unusedResults, msg, dangerousText, mustPlayDangerous):
0542         """client told us he discarded a tile. Continue, check for dangerous game"""
0543         block = DeferredBlock(self)
0544         player = msg.player
0545         if dangerousText:
0546             if mustPlayDangerous and not player.lastSource.isDiscarded:
0547                 if Debug.dangerousGame:
0548                     tile = Tile(msg.args[0])
0549                     logDebug('%s claims no choice. Discarded %s, keeping %s. %s' %
0550                              (player, tile, ''.join(player.concealedTiles), ' / '.join(dangerousText)))
0551                 player.claimedNoChoice = True
0552                 block.tellAll(
0553                     player,
0554                     Message.NoChoice,
0555                     tiles=TileList(player.concealedTiles))
0556             else:
0557                 player.playedDangerous = True
0558                 if Debug.dangerousGame:
0559                     tile = Tile(msg.args[0])
0560                     logDebug('%s played dangerous. Discarded %s, keeping %s. %s' %
0561                              (player, tile, ''.join(player.concealedTiles), ' / '.join(dangerousText)))
0562                 block.tellAll(
0563                     player,
0564                     Message.DangerousGame,
0565                     tiles=TileList(player.concealedTiles))
0566         if msg.answer == Message.OriginalCall:
0567             player.isCalling = True
0568             block.callback(self.clientMadeOriginalCall, msg)
0569         else:
0570             block.callback(self._askForClaims, msg)
0571 
0572     def clientMadeOriginalCall(self, unusedResults, msg):
0573         """first tell everybody about original call
0574         and then treat the implicit discard"""
0575         msg.player.originalCall = True
0576         if Debug.originalCall:
0577             logDebug('server.clientMadeOriginalCall: %s' % msg.player)
0578         block = DeferredBlock(self)
0579         block.tellAll(msg.player, Message.OriginalCall)
0580         block.callback(self._askForClaims, msg)
0581 
0582     def startHand(self, unusedResults=None):
0583         """all players are ready to start a hand, so do it"""
0584         if self.running:
0585             self.game.prepareHand()
0586             self.game.initHand()
0587             block = self.tellAll(None, Message.InitHand,
0588                                  divideAt=self.game.divideAt)
0589             block.callback(self.divided)
0590 
0591     def divided(self, unusedResults=None):
0592         """the wall is now divided for all clients"""
0593         if not self.running:
0594             return
0595         block = DeferredBlock(self)
0596         for clientPlayer in self.game.players:
0597             for player in self.game.players:
0598                 if player == clientPlayer or self.game.playOpen:
0599                     tiles = player.concealedTiles
0600                 else:
0601                     tiles = TileList(Tile.unknown * 13)
0602                 block.tell(player, clientPlayer, Message.SetConcealedTiles,
0603                            tiles=TileList(chain(tiles, player.bonusTiles)))
0604         block.callback(self.dealt)
0605 
0606     def endHand(self, unusedResults=None):
0607         """hand is over, show all concealed tiles to all players"""
0608         if not self.running:
0609             return
0610         if self.game.playOpen:
0611             self.saveHand()
0612         else:
0613             block = DeferredBlock(self)
0614             for player in self.game.players:
0615                 # there might be no winner, winner.others() would be wrong
0616                 if player != self.game.winner:
0617                     # the winner tiles are already shown in claimMahJongg
0618                     block.tellOthers(
0619                         player, Message.ShowConcealedTiles, show=True,
0620                         tiles=TileList(player.concealedTiles))
0621             block.callback(self.saveHand)
0622 
0623     def saveHand(self, unusedResults=None):
0624         """save the hand to the database and proceed to next hand"""
0625         if not self.running:
0626             return
0627         self.tellAll(None, Message.SaveHand, self.nextHand)
0628         self.game.saveHand()
0629 
0630     def nextHand(self, unusedResults):
0631         """next hand: maybe rotate"""
0632         if not self.running:
0633             return
0634         DeferredBlock.garbageCollection()
0635         for block in DeferredBlock.blocks:
0636             if block.table == self:
0637                 logError(
0638                     'request left from previous hand: %s' %
0639                     block.outstandingStr())
0640         token = self.game.handId.prompt(
0641             withAI=False)  # we need to send the old token until the
0642                                    # clients started the new hand
0643         rotateWinds = self.game.maybeRotateWinds()
0644         if self.game.finished():
0645             self.server.removeTable(
0646                 self,
0647                 'gameOver',
0648                 i18nE('Game <numid>%1</numid> is over!'),
0649                 self.game.seed)
0650             if Debug.process and os.name != 'nt':
0651                 logDebug(
0652                     'MEM:%s' %
0653                     resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
0654             return
0655         self.game.sortPlayers()
0656         playerNames = [(x.wind, x.name) for x in self.game.players]
0657         self.tellAll(None, Message.ReadyForHandStart, self.startHand,
0658                      playerNames=playerNames, rotateWinds=rotateWinds, token=token)
0659 
0660     def abort(self, message, *args):
0661         """abort the table. Reason: message/args"""
0662         self.server.removeTable(self, 'abort', message, *args)
0663 
0664     def claimTile(self, player, claim, meldTiles, nextMessage):
0665         """a player claims a tile for pung, kong or chow.
0666         meldTiles contains the claimed tile, concealed"""
0667         if not self.running:
0668             return
0669         lastDiscard = self.game.lastDiscard
0670         # if we rob a tile, self.game.lastDiscard has already been set to the
0671         # robbed tile
0672         hasTiles = Meld(meldTiles[:])
0673         discardingPlayer = self.game.activePlayer
0674         hasTiles = hasTiles.without(lastDiscard)
0675         meld = Meld(meldTiles)
0676         if len(meld) != 4 and not (meld.isPair or meld.isPungKong or meld.isChow):
0677             msg = i18nE('%1 wrongly said %2 for meld %3')
0678             self.abort(msg, player.name, claim.name, str(meld))
0679             return
0680         if not player.hasConcealedTiles(hasTiles):
0681             msg = i18nE(
0682                 '%1 wrongly said %2: claims to have concealed tiles %3 but only has %4')
0683             self.abort(
0684                 msg,
0685                 player.name,
0686                 claim.name,
0687                 ' '.join(hasTiles),
0688                 ''.join(player.concealedTiles))
0689             return
0690         # update our internal state before we listen to the clients again
0691         self.game.discardedTiles[lastDiscard.exposed] -= 1
0692         self.game.activePlayer = player
0693         if lastDiscard:
0694             player.lastTile = lastDiscard.exposed
0695             player.lastSource = TileSource.LivingWallDiscard
0696         player.exposeMeld(hasTiles, lastDiscard)
0697         self.game.lastDiscard = None
0698         block = DeferredBlock(self)
0699         if (nextMessage != Message.Kong
0700                 and self.game.dangerousFor(discardingPlayer, lastDiscard)
0701                 and discardingPlayer.playedDangerous):
0702             player.usedDangerousFrom = discardingPlayer
0703             if Debug.dangerousGame:
0704                 logDebug('%s claims dangerous tile %s discarded by %s' %
0705                          (player, lastDiscard, discardingPlayer))
0706             block.tellAll(
0707                 player,
0708                 Message.UsedDangerousFrom,
0709                 source=discardingPlayer.name)
0710         block.tellAll(player, nextMessage, meld=meld)
0711         if claim == Message.Kong:
0712             block.callback(self.pickKongReplacement)
0713         else:
0714             block.callback(self.moved)
0715 
0716     def declareKong(self, player, meldTiles):
0717         """player declares a Kong, meldTiles is a list"""
0718         kongMeld = Meld(meldTiles)
0719         if not player.hasConcealedTiles(kongMeld) and kongMeld[0].exposed.pung not in player.exposedMelds:
0720             msg = i18nE('declareKong:%1 wrongly said Kong for meld %2')
0721             args = (player.name, str(kongMeld))
0722             logDebug(i18n(msg, *args))
0723             logDebug(
0724                 'declareKong:concealedTiles:%s' %
0725                 ''.join(player.concealedTiles))
0726             logDebug('declareKong:concealedMelds:%s' %
0727                      ' '.join(str(x) for x in player.concealedMelds))
0728             logDebug('declareKong:exposedMelds:%s' %
0729                      ' '.join(str(x) for x in player.exposedMelds))
0730             self.abort(msg, *args)
0731             return
0732         player.exposeMeld(kongMeld)
0733         self.tellAll(
0734             player,
0735             Message.DeclaredKong,
0736             self.pickKongReplacement,
0737             meld=kongMeld)
0738 
0739     def claimMahJongg(self, msg):
0740         """a player claims mah jongg. Check this and
0741         if correct, tell all. Otherwise abort game, kajongg client is faulty"""
0742         if not self.running:
0743             return
0744         player = msg.player
0745         concealedMelds = MeldList(msg.args[0])
0746         withDiscard = Tile(msg.args[1]) if msg.args[1] else None
0747         lastMeld = Meld(msg.args[2])
0748         if self.game.ruleset.mustDeclareCallingHand:
0749             assert player.isCalling, '%s %s %s says MJ but never claimed: concmelds:%s withdiscard:%s lastmeld:%s' % (
0750                 self.game.handId, player.hand, player, concealedMelds, withDiscard, lastMeld)
0751         discardingPlayer = self.game.activePlayer
0752         lastMove = next(self.game.lastMoves(withoutNotifications=True))
0753         robbedTheKong = lastMove.message == Message.DeclaredKong
0754         if robbedTheKong:
0755             player.robsTile()
0756             withDiscard = lastMove.meld[0].concealed
0757             lastMove.player.robTileFrom(withDiscard)
0758         msgArgs = player.showConcealedMelds(concealedMelds, withDiscard)
0759         if msgArgs:
0760             self.abort(*msgArgs)
0761             return
0762         player.declaredMahJongg(
0763             concealedMelds,
0764             withDiscard,
0765             player.lastTile,
0766             lastMeld)
0767         if not player.hand.won:
0768             msg = i18nE('%1 claiming MahJongg: This is not a winning hand: %2')
0769             self.abort(msg, player.name, player.hand.string)
0770             return
0771         block = DeferredBlock(self)
0772         if robbedTheKong:
0773             block.tellAll(player, Message.RobbedTheKong, tile=withDiscard)
0774         if (player.lastSource is TileSource.LivingWallDiscard
0775                 and self.game.dangerousFor(discardingPlayer, player.lastTile)
0776                 and discardingPlayer.playedDangerous):
0777             player.usedDangerousFrom = discardingPlayer
0778             if Debug.dangerousGame:
0779                 logDebug('%s wins with dangerous tile %s from %s' %
0780                          (player, self.game.lastDiscard, discardingPlayer))
0781             block.tellAll(
0782                 player,
0783                 Message.UsedDangerousFrom,
0784                 source=discardingPlayer.name)
0785         block.tellAll(
0786             player, Message.MahJongg, melds=concealedMelds, lastTile=player.lastTile,
0787             lastMeld=lastMeld, withDiscardTile=withDiscard)
0788         block.callback(self.endHand)
0789 
0790     def dealt(self, unusedResults):
0791         """all tiles are dealt, ask east to discard a tile"""
0792         if self.running:
0793             self.tellAll(
0794                 self.game.activePlayer,
0795                 Message.ActivePlayer,
0796                 self.pickTile)
0797 
0798     def nextTurn(self):
0799         """the next player becomes active"""
0800         if self.running:
0801             # the player might just have disconnected
0802             self.game.nextTurn()
0803             self.tellAll(
0804                 self.game.activePlayer,
0805                 Message.ActivePlayer,
0806                 self.pickTile)
0807 
0808     def prioritize(self, requests):
0809         """return only requests we want to execute"""
0810         if not self.running:
0811             return None
0812         answers = [
0813             x for x in requests if x.answer not in [
0814                 Message.NoClaim,
0815                 Message.OK,
0816                 None]]
0817         if len(answers) > 1:
0818             claims = [
0819                 Message.MahJongg,
0820                 Message.Kong,
0821                 Message.Pung,
0822                 Message.Chow]
0823             for claim in claims:
0824                 if claim in [x.answer for x in answers]:
0825                     # ignore claims with lower priority:
0826                     answers = [
0827                         x for x in answers if x.answer == claim or x.answer not in claims]
0828                     break
0829         mjAnswers = [x for x in answers if x.answer == Message.MahJongg]
0830         if len(mjAnswers) > 1:
0831             mjPlayers = [x.player for x in mjAnswers]
0832             nextPlayer = self.game.nextPlayer()
0833             while nextPlayer not in mjPlayers:
0834                 nextPlayer = self.game.nextPlayer(nextPlayer)
0835             answers = [
0836                 x for x in answers if x.player == nextPlayer or x.answer != Message.MahJongg]
0837         return answers
0838 
0839     def _askForClaims(self, unusedRequests, unusedMsg):
0840         """ask all players if they want to claim"""
0841         if self.running:
0842             self.tellOthers(
0843                 self.game.activePlayer,
0844                 Message.AskForClaims,
0845                 self.moved)
0846 
0847     def processAnswers(self, requests):
0848         """a player did something"""
0849         if not self.running:
0850             return None
0851         answers = self.prioritize(requests)
0852         if not answers:
0853             return None
0854         for answer in answers:
0855             msg = '<-  %s' % answer
0856             if Debug.traffic:
0857                 logDebug(msg)
0858             with Duration(msg):
0859                 answer.answer.serverAction(self, answer)
0860         return answers
0861 
0862     def moved(self, requests):
0863         """a player did something"""
0864         if Debug.stack:
0865             stck = traceback.extract_stack()
0866             if len(stck) > 30:
0867                 logDebug('stack size:%d' % len(stck))
0868                 logDebug(stck)
0869         answers = self.processAnswers(requests)
0870         if not answers:
0871             self.nextTurn()
0872 
0873     def tellAll(self, player, command, callback=None, **kwargs):
0874         """tell something about player to all players"""
0875         block = DeferredBlock(self)
0876         block.tellAll(player, command, **kwargs)
0877         block.callback(callback)
0878         return block
0879 
0880     def tellOthers(self, player, command, callback=None, **kwargs):
0881         """tell something about player to all other players"""
0882         block = DeferredBlock(self)
0883         block.tellOthers(player, command, **kwargs)
0884         block.callback(callback)
0885         return block