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