File indexing completed on 2024-04-14 03:59:09
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 0010 import random 0011 0012 from twisted.spread import pb 0013 from twisted.python.failure import Failure 0014 from twisted.internet.defer import Deferred, succeed, DeferredList 0015 from qt import Qt, QTimer 0016 from qt import QDialog, QVBoxLayout, QGridLayout, \ 0017 QLabel, QPushButton, QWidget, \ 0018 QProgressBar, QRadioButton, QSpacerItem, QSizePolicy 0019 0020 from kde import KIcon, KDialog 0021 from dialogs import Sorry, Information, QuestionYesNo, KDialogIgnoringEscape 0022 from guiutil import decorateWindow 0023 from log import i18n, logWarning, logException, logDebug 0024 from message import Message, ChatMessage 0025 from chat import ChatWindow 0026 from common import Options, SingleshotOptions, Internal, Debug, isAlive 0027 from query import Query 0028 from board import Board 0029 from client import Client, ClientTable 0030 from tables import TableList, SelectRuleset 0031 from sound import Voice 0032 from login import Connection 0033 from rule import Ruleset 0034 from game import PlayingGame 0035 from visible import VisiblePlayingGame 0036 0037 0038 class SelectChow(KDialogIgnoringEscape): 0039 0040 """asks which of the possible chows is wanted""" 0041 0042 def __init__(self, chows, propose, deferred): 0043 KDialogIgnoringEscape.__init__(self) 0044 decorateWindow(self) 0045 self.setButtons(KDialog.NoButton) 0046 self.chows = chows 0047 self.selectedChow = None 0048 self.deferred = deferred 0049 layout = QVBoxLayout() 0050 label = QLabel(i18n('Which chow do you want to expose?')) 0051 layout.addWidget(label) 0052 layout.setAlignment(label, Qt.AlignHCenter) 0053 self.buttons = [] 0054 for chow in chows: 0055 button = QRadioButton('{}-{}-{}'.format(*(x.value for x in chow))) 0056 self.buttons.append(button) 0057 layout.addWidget(button) 0058 layout.setAlignment(button, Qt.AlignHCenter) 0059 button.toggled.connect(self.toggled) 0060 widget = QWidget(self) 0061 widget.setLayout(layout) 0062 self.setMainWidget(widget) 0063 for idx, chow in enumerate(chows): 0064 if chow == propose: 0065 self.buttons[idx].setFocus() 0066 0067 def toggled(self, unusedChecked): 0068 """a radiobutton has been toggled""" 0069 button = self.sender() 0070 if button.isChecked(): 0071 self.selectedChow = self.chows[self.buttons.index(button)] 0072 self.accept() 0073 self.deferred.callback((Message.Chow, self.selectedChow)) 0074 0075 0076 class SelectKong(KDialogIgnoringEscape): 0077 0078 """asks which of the possible kongs is wanted""" 0079 0080 def __init__(self, kongs, deferred): 0081 KDialogIgnoringEscape.__init__(self) 0082 decorateWindow(self) 0083 self.setButtons(0) 0084 self.kongs = kongs 0085 self.selectedKong = None 0086 self.deferred = deferred 0087 layout = QVBoxLayout() 0088 label = QLabel(i18n('Which kong do you want to declare?')) 0089 layout.addWidget(label) 0090 layout.setAlignment(label, Qt.AlignHCenter) 0091 self.buttons = [] 0092 for kong in kongs: 0093 button = QRadioButton((kong[0].name()), self) 0094 self.buttons.append(button) 0095 layout.addWidget(button) 0096 button.toggled.connect(self.toggled) 0097 widget = QWidget(self) 0098 widget.setLayout(layout) 0099 self.setMainWidget(widget) 0100 0101 def toggled(self, unusedChecked): 0102 """a radiobutton has been toggled""" 0103 button = self.sender() 0104 if button.isChecked(): 0105 self.selectedKong = self.kongs[self.buttons.index(button)] 0106 self.accept() 0107 self.deferred.callback((Message.Kong, self.selectedKong)) 0108 0109 0110 class DlgButton(QPushButton): 0111 0112 """special button for ClientDialog""" 0113 0114 def __init__(self, message, parent): 0115 QPushButton.__init__(self, parent) 0116 self.message = message 0117 self.client = parent.client 0118 self.setMinimumHeight(25) 0119 self.setText(message.buttonCaption()) 0120 0121 def setMeaning(self, uiTile): 0122 """give me caption, shortcut, tooltip, icon""" 0123 txt, warn, _ = self.message.toolTip( 0124 self, uiTile.tile if uiTile else None) 0125 if not txt: 0126 txt = self.message.i18nName # .replace(i18nShortcut, '&'+i18nShortcut, 1) 0127 self.setToolTip(txt) 0128 self.setWarning(warn) 0129 0130 def keyPressEvent(self, event): 0131 """forward horizintal arrows to the hand board""" 0132 key = Board.mapChar2Arrow(event) 0133 if key in [Qt.Key_Left, Qt.Key_Right]: 0134 game = self.client.game 0135 if game and game.activePlayer == game.myself: 0136 game.myself.handBoard.keyPressEvent(event) 0137 self.setFocus() 0138 return 0139 QPushButton.keyPressEvent(self, event) 0140 0141 def setWarning(self, warn): 0142 """if warn, show a warning icon on the button""" 0143 if warn: 0144 self.setIcon(KIcon('dialog-warning')) 0145 else: 0146 self.setIcon(KIcon()) 0147 0148 0149 class ClientDialog(QDialog): 0150 0151 """a simple popup dialog for asking the player what he wants to do""" 0152 0153 def __init__(self, client, parent=None): 0154 QDialog.__init__(self, parent) 0155 decorateWindow(self, i18n('Choose')) 0156 self.setObjectName('ClientDialog') 0157 self.client = client 0158 self.layout = QGridLayout(self) 0159 self.progressBar = QProgressBar() 0160 self.progressBar.setMinimumHeight(25) 0161 self.timer = QTimer() 0162 if not client.game.autoPlay: 0163 self.timer.timeout.connect(self.timeout) 0164 self.deferred = None 0165 self.buttons = [] 0166 self.setWindowFlags(Qt.SubWindow | Qt.WindowStaysOnTopHint) 0167 self.setModal(False) 0168 self.btnHeight = 0 0169 self.answered = False 0170 self.move = None 0171 self.sorry = None 0172 0173 def keyPressEvent(self, event): 0174 """ESC selects default answer""" 0175 if not self.client.game or self.client.game.autoPlay: 0176 return 0177 if event.key() in [Qt.Key_Escape, Qt.Key_Space]: 0178 self.selectButton() 0179 event.accept() 0180 else: 0181 for btn in self.buttons: 0182 if str(event.text()).upper() == btn.message.shortcut: 0183 self.selectButton(btn) 0184 event.accept() 0185 return 0186 QDialog.keyPressEvent(self, event) 0187 0188 def __declareButton(self, message): 0189 """define a button""" 0190 maySay = self.client.game.myself.sayable[message] 0191 if Internal.Preferences.showOnlyPossibleActions and not maySay: 0192 return 0193 btn = DlgButton(message, self) 0194 btn.setAutoDefault(True) 0195 btn.clicked.connect(self.selectedAnswer) 0196 self.buttons.append(btn) 0197 0198 def focusTileChanged(self): 0199 """update icon and tooltip for the discard button""" 0200 if not self.client.game: 0201 return 0202 for button in self.buttons: 0203 button.setMeaning(self.client.game.myself.handBoard.focusTile) 0204 for uiTile in self.client.game.myself.handBoard.lowerHalfTiles(): 0205 txt = [] 0206 for button in self.buttons: 0207 _, _, tileTxt = button.message.toolTip(button, uiTile.tile) 0208 if tileTxt: 0209 txt.append(tileTxt) 0210 uiTile.setToolTip('<br><br>'.join(txt)) 0211 if self.client.game.activePlayer == self.client.game.myself: 0212 Internal.scene.handSelectorChanged( 0213 self.client.game.myself.handBoard) 0214 0215 def checkTiles(self): 0216 """does the logical state match the displayed tiles?""" 0217 for player in self.client.game.players: 0218 player.handBoard.checkTiles() 0219 0220 def messages(self): 0221 """a list of all messages returned by the declared buttons""" 0222 return [x.message for x in self.buttons] 0223 0224 def proposeAction(self): 0225 """either intelligently or first button by default. May also 0226 focus a proposed tile depending on the action.""" 0227 result = self.buttons[0] 0228 game = self.client.game 0229 if game.autoPlay or Internal.Preferences.propose: 0230 answer, parameter = game.myself.intelligence.selectAnswer( 0231 self.messages()) 0232 result = [x for x in self.buttons if x.message == answer][0] 0233 result.setFocus() 0234 if answer in [Message.Discard, Message.OriginalCall]: 0235 for uiTile in game.myself.handBoard.uiTiles: 0236 if uiTile.tile is parameter: 0237 game.myself.handBoard.focusTile = uiTile 0238 return result 0239 0240 def askHuman(self, move, answers, deferred): 0241 """make buttons specified by answers visible. The first answer is default. 0242 The default button only appears with blue border when this dialog has 0243 focus but we always want it to be recognizable. Hence setBackgroundRole.""" 0244 self.move = move 0245 self.deferred = deferred 0246 for answer in answers: 0247 self.__declareButton(answer) 0248 self.focusTileChanged() 0249 self.show() 0250 self.checkTiles() 0251 game = self.client.game 0252 myTurn = game.activePlayer == game.myself 0253 prefButton = self.proposeAction() 0254 if game.autoPlay: 0255 self.selectButton(prefButton) 0256 return 0257 prefButton.setFocus() 0258 0259 self.progressBar.setVisible(not myTurn) 0260 if not myTurn: 0261 msecs = 50 0262 self.progressBar.setMinimum(0) 0263 self.progressBar.setMaximum( 0264 game.ruleset.claimTimeout * 1000 // msecs) 0265 self.progressBar.reset() 0266 self.timer.start(msecs) 0267 0268 def placeInField(self): 0269 """place the dialog at bottom or to the right depending on space.""" 0270 mainWindow = Internal.scene.mainWindow 0271 cwi = mainWindow.centralWidget() 0272 view = mainWindow.centralView 0273 geometry = self.geometry() 0274 if not self.btnHeight: 0275 self.btnHeight = self.buttons[0].height() 0276 vertical = view.width() > view.height() * 1.2 0277 if vertical: 0278 height = (len(self.buttons) + 1) * self.btnHeight * 1.2 0279 width = (cwi.width() - cwi.height()) // 2 0280 geometry.setX(cwi.width() - width) 0281 geometry.setY(min(cwi.height() // 3, cwi.height() - height)) 0282 else: 0283 handBoard = self.client.game.myself.handBoard 0284 if not handBoard: 0285 # we are in the progress of logging out 0286 return 0287 hbLeftTop = view.mapFromScene( 0288 handBoard.mapToScene(handBoard.rect().topLeft())) 0289 hbRightBottom = view.mapFromScene( 0290 handBoard.mapToScene(handBoard.rect().bottomRight())) 0291 width = hbRightBottom.x() - hbLeftTop.x() 0292 height = self.btnHeight 0293 geometry.setY(cwi.height() - height) 0294 geometry.setX(hbLeftTop.x()) 0295 for idx, btn in enumerate(self.buttons + [self.progressBar]): 0296 self.layout.addWidget( 0297 btn, 0298 idx + 0299 1 if vertical else 0, 0300 idx + 0301 1 if not vertical else 0) 0302 idx = len(self.buttons) + 2 0303 spacer = QSpacerItem( 0304 20, 0305 20, 0306 QSizePolicy.Expanding, 0307 QSizePolicy.Expanding) 0308 self.layout.addItem( 0309 spacer, 0310 idx if vertical else 0, 0311 idx if not vertical else 0) 0312 0313 geometry.setWidth(int(width)) 0314 geometry.setHeight(int(height)) 0315 self.setGeometry(geometry) 0316 0317 def showEvent(self, unusedEvent): 0318 """try to place the dialog such that it does not cover interesting information""" 0319 self.placeInField() 0320 0321 def timeout(self): 0322 """the progressboard wants an update""" 0323 pBar = self.progressBar 0324 if isAlive(pBar): 0325 pBar.setValue(pBar.value() + 1) 0326 pBar.setVisible(True) 0327 if pBar.value() == pBar.maximum(): 0328 # timeout: we always return the original default answer, not 0329 # the one with focus 0330 self.selectButton() 0331 pBar.setVisible(False) 0332 0333 def selectButton(self, button=None): 0334 """select default answer. button may also be of type Message.""" 0335 if self.answered: 0336 # sometimes we get this event twice 0337 return 0338 if button is None: 0339 button = self.focusWidget() 0340 if isinstance(button, Message): 0341 assert any(x.message == button for x in self.buttons) 0342 answer = button 0343 else: 0344 answer = button.message 0345 if not self.client.game.myself.sayable[answer]: 0346 self.proposeAction().setFocus() # go back to default action 0347 self.sorry = Sorry(i18n('You cannot say %1', answer.i18nName)) 0348 return 0349 self.timer.stop() 0350 self.answered = True 0351 if self.sorry: 0352 self.sorry.cancel() 0353 self.sorry = None 0354 Internal.scene.clientDialog = None 0355 self.deferred.callback(answer) 0356 0357 def selectedAnswer(self, unusedChecked): 0358 """the user clicked one of the buttons""" 0359 game = self.client.game 0360 if game and not game.autoPlay: 0361 self.selectButton(self.sender()) 0362 0363 0364 class HumanClient(Client): 0365 0366 """a human client""" 0367 # pylint: disable=too-many-public-methods 0368 humanClients = [] 0369 0370 def __init__(self): 0371 Client.__init__(self) 0372 HumanClient.humanClients.append(self) 0373 self.table = None 0374 self.ruleset = None 0375 self.beginQuestion = None 0376 self.tableList = TableList(self) 0377 Connection(self).login().addCallbacks( 0378 self.__loggedIn, 0379 self.__loginFailed) 0380 0381 @staticmethod 0382 def shutdownHumanClients(exception=None): 0383 """close connections to servers except maybe one""" 0384 clients = HumanClient.humanClients 0385 0386 def done(): 0387 """return True if clients is cleaned""" 0388 return len(clients) == 0 or (exception and clients == [exception]) 0389 0390 def disconnectedClient(unusedResult, client): 0391 """now the client is really disconnected from the server""" 0392 if client in clients: 0393 # HumanClient.serverDisconnects also removes it! 0394 clients.remove(client) 0395 if isinstance(exception, Failure): 0396 logException(exception) 0397 for client in clients[:]: 0398 if client.tableList: 0399 client.tableList.hide() 0400 if done(): 0401 return succeed(None) 0402 deferreds = [] 0403 for client in clients[:]: 0404 if client != exception and client.connection: 0405 deferreds.append( 0406 client.logout( 0407 ).addCallback( 0408 disconnectedClient, 0409 client)) 0410 return DeferredList(deferreds) 0411 0412 def __loggedIn(self, connection): 0413 """callback after the server answered our login request""" 0414 self.connection = connection 0415 self.ruleset = connection.ruleset 0416 self.name = connection.username 0417 self.tableList.show() 0418 voiceId = None 0419 if Internal.Preferences.uploadVoice: 0420 voice = Voice.locate(self.name) 0421 if voice: 0422 voiceId = voice.md5sum 0423 if Debug.sound and voiceId: 0424 logDebug( 0425 '%s sends own voice %s to server' % 0426 (self.name, voiceId)) 0427 maxGameId = Query('select max(id) from game').records[0][0] 0428 maxGameId = int(maxGameId) if maxGameId else 0 0429 self.callServer('setClientProperties', 0430 Internal.db.identifier, 0431 voiceId, maxGameId, 0432 Internal.defaultPort).addCallbacks(self.__initTableList, self.__versionError) 0433 0434 def __initTableList(self, unused): 0435 """first load of the list. Process options like --demo, --table, --join""" 0436 self.showTableList() 0437 if SingleshotOptions.table: 0438 Internal.autoPlay = False 0439 self.__requestNewTableFromServer(SingleshotOptions.table).addCallback( 0440 self.__showTables).addErrback(self.tableError) 0441 if Debug.table: 0442 logDebug( 0443 '%s: --table lets us open a new table' % self.name) 0444 SingleshotOptions.table = False 0445 elif SingleshotOptions.join: 0446 Internal.autoPlay = False 0447 self.callServer('joinTable', SingleshotOptions.join).addCallback( 0448 self.__showTables).addErrback(self.tableError) 0449 if Debug.table: 0450 logDebug( 0451 '%s: --join lets us join table %s' % 0452 (self.name, self._tableById(SingleshotOptions.join))) 0453 SingleshotOptions.join = False 0454 elif not self.game and (Internal.autoPlay or (not self.tables and self.hasLocalServer())): 0455 self.__requestNewTableFromServer().addCallback( 0456 self.__newLocalTable).addErrback(self.tableError) 0457 else: 0458 self.__showTables() 0459 0460 @staticmethod 0461 def __loginFailed(unused): 0462 """as the name says""" 0463 if Internal.scene: 0464 Internal.scene.startingGame = False 0465 0466 def isRobotClient(self): 0467 """avoid using isinstance, it would import too much for kajonggserver""" 0468 return False 0469 0470 @staticmethod 0471 def isHumanClient(): 0472 """avoid using isinstance, it would import too much for kajonggserver""" 0473 return True 0474 0475 def isServerClient(self): 0476 """avoid using isinstance, it would import too much for kajonggserver""" 0477 return False 0478 0479 def hasLocalServer(self): 0480 """True if we are talking to a Local Game Server""" 0481 return self.connection and self.connection.url.isLocalHost 0482 0483 def __updateTableList(self): 0484 """if it exists""" 0485 if self.tableList: 0486 self.tableList.loadTables(self.tables) 0487 0488 def __showTables(self, unused=None): 0489 """load and show tables. We may be used as a callback. In that case, 0490 clientTables is the id of a new table - which we do not need here""" 0491 self.tableList.loadTables(self.tables) 0492 self.tableList.show() 0493 0494 def showTableList(self, unused=None): 0495 """allocate it if needed""" 0496 if not self.tableList: 0497 self.tableList = TableList(self) 0498 self.tableList.loadTables(self.tables) 0499 self.tableList.activateWindow() 0500 0501 def remote_tableRemoved(self, tableid, message, *args): 0502 """update table list""" 0503 Client.remote_tableRemoved(self, tableid, message, *args) 0504 self.__updateTableList() 0505 if message: 0506 if self.name not in args or not message.endswith('has logged out'): 0507 logWarning(i18n(message, *args)) 0508 0509 def __receiveTables(self, tables): 0510 """now we already know all rulesets for those tables""" 0511 Client.remote_newTables(self, tables) 0512 if not Internal.autoPlay: 0513 if self.hasLocalServer(): 0514 # when playing a local game, only show pending tables with 0515 # previously selected ruleset 0516 self.tables = [x for x in self.tables if x.ruleset == self.ruleset] 0517 if self.tables: 0518 self.__updateTableList() 0519 0520 def remote_newTables(self, tables): 0521 """update table list""" 0522 assert tables 0523 0524 def gotRulesets(result): 0525 """the server sent us the wanted ruleset definitions""" 0526 for ruleset in result: 0527 Ruleset.cached(ruleset).save() # make it known to the cache and save in db 0528 return tables 0529 rulesetHashes = {x[1] for x in tables} 0530 needRulesets = [x for x in rulesetHashes if not Ruleset.hashIsKnown(x)] 0531 if needRulesets: 0532 self.callServer( 0533 'needRulesets', 0534 needRulesets).addCallback( 0535 gotRulesets).addCallback( 0536 self.__receiveTables) 0537 else: 0538 self.__receiveTables(tables) 0539 0540 @staticmethod 0541 def remote_needRuleset(ruleset): 0542 """server only knows hash, needs full definition""" 0543 result = Ruleset.cached(ruleset) 0544 assert result and result.hash == ruleset 0545 return result.toList() 0546 0547 def tableChanged(self, table): 0548 """update table list""" 0549 oldTable, newTable = Client.tableChanged(self, table) 0550 if oldTable and oldTable == self.table: 0551 # this happens if a table has more than one human player and 0552 # one of them leaves the table. In that case, the other players 0553 # need this code. 0554 self.table = newTable 0555 if len(newTable.playerNames) == 3: 0556 # only tell about the first player leaving, because the 0557 # others will then automatically leave too 0558 for name in oldTable.playerNames: 0559 if name != self.name and not newTable.isOnline(name): 0560 def sorried(unused): 0561 """user ack""" 0562 game = self.game 0563 if game: 0564 self.game = None 0565 return game.close() 0566 return None 0567 if self.beginQuestion: 0568 self.beginQuestion.cancel() 0569 Sorry(i18n('Player %1 has left the table', name)).addCallback( 0570 sorried).addCallback(self.showTableList) 0571 break 0572 self.__updateTableList() 0573 0574 def remote_chat(self, data): 0575 """others chat to me""" 0576 chatLine = ChatMessage(data) 0577 if Debug.chat: 0578 logDebug('got chatLine: %s' % chatLine) 0579 table = self._tableById(chatLine.tableid) 0580 if not chatLine.isStatusMessage and not table.chatWindow: 0581 ChatWindow(table) 0582 if table.chatWindow: 0583 table.chatWindow.receiveLine(chatLine) 0584 0585 def readyForGameStart( 0586 self, tableid, gameid, wantedGame, playerNames, shouldSave=True, 0587 gameClass=None): 0588 """playerNames are in wind order ESWN""" 0589 if gameClass is None: 0590 if Options.gui: 0591 gameClass = VisiblePlayingGame 0592 else: 0593 gameClass = PlayingGame 0594 0595 def clientReady(): 0596 """macro""" 0597 return Client.readyForGameStart( 0598 self, tableid, gameid, wantedGame, playerNames, 0599 shouldSave, gameClass) 0600 0601 def answered(result): 0602 """callback, called after the client player said yes or no""" 0603 self.beginQuestion = None 0604 if self.connection and result: 0605 # still connected and yes, we are 0606 return clientReady() 0607 return Message.NoGameStart 0608 0609 def cancelled(unused): 0610 """the user does not want to start now. Back to table list""" 0611 if Debug.table: 0612 logDebug('%s: Readyforgamestart returns Message.NoGameStart for table %s' % ( 0613 self.name, self._tableById(tableid))) 0614 self.table = None 0615 self.beginQuestion = None 0616 if self.tableList: 0617 self.__updateTableList() 0618 self.tableList.show() 0619 return Message.NoGameStart 0620 if sum(not x[1].startswith('Robot ') for x in playerNames) == 1: 0621 # we play against 3 robots and we already told the server to start: 0622 # no need to ask again 0623 return clientReady() 0624 assert not self.table 0625 assert self.tables 0626 self.table = self._tableById(tableid) 0627 if not self.table: 0628 raise pb.Error( 0629 'client.readyForGameStart: tableid %d unknown' % 0630 tableid) 0631 msg = i18n( 0632 "The game on table <numid>%1</numid> can begin. Are you ready to play now?", 0633 tableid) 0634 self.beginQuestion = QuestionYesNo(msg, modal=False, caption=self.name).addCallback( 0635 answered).addErrback(cancelled) 0636 return self.beginQuestion 0637 0638 def readyForHandStart(self, playerNames, rotateWinds): 0639 """playerNames are in wind order ESWN. Never called for first hand.""" 0640 def answered(unused=None): 0641 """called after the client player said yes, I am ready""" 0642 return Client.readyForHandStart(self, playerNames, rotateWinds) if self.connection else None 0643 if not self.connection: 0644 # disconnected meanwhile 0645 return None 0646 if Options.gui: 0647 # update the balances in the status bar: 0648 Internal.mainWindow.updateGUI() 0649 assert not self.game.isFirstHand() 0650 return Information(i18n("Ready for next hand?"), modal=False).addCallback(answered) 0651 0652 def ask(self, move, answers): 0653 """server sends move. We ask the user. answers is a list with possible answers, 0654 the default answer being the first in the list.""" 0655 if not Options.gui: 0656 return Client.ask(self, move, answers) 0657 self.game.myself.computeSayable(move, answers) 0658 deferred = Deferred() 0659 deferred.addCallback(self.__askAnswered) 0660 deferred.addErrback(self.__answerError, move, answers) 0661 iAmActive = self.game.myself == self.game.activePlayer 0662 self.game.myself.handBoard.setEnabled(iAmActive) 0663 scene = Internal.scene 0664 oldDialog = scene.clientDialog 0665 if oldDialog and not oldDialog.answered: 0666 raise Exception('old dialog %s:%s is unanswered, new Dialog: %s/%s' % ( 0667 str(oldDialog.move), 0668 str([x.message.name for x in oldDialog.buttons]), 0669 str(move), str(answers))) 0670 if not oldDialog or not oldDialog.isVisible(): 0671 # always build a new dialog because if we change its layout before 0672 # reshowing it, sometimes the old buttons are still visible in which 0673 # case the next dialog will appear at a lower position than it 0674 # should 0675 scene.clientDialog = ClientDialog( 0676 self, 0677 scene.mainWindow.centralWidget()) 0678 assert scene.clientDialog.client is self 0679 scene.clientDialog.askHuman(move, answers, deferred) 0680 return deferred 0681 0682 def __selectChow(self, chows): 0683 """which possible chow do we want to expose? 0684 Since we might return a Deferred to be sent to the server, 0685 which contains Message.Chow plus selected Chow, we should 0686 return the same tuple here""" 0687 intelligence = self.game.myself.intelligence 0688 if self.game.autoPlay: 0689 return Message.Chow, intelligence.selectChow(chows) 0690 if len(chows) == 1: 0691 return Message.Chow, chows[0] 0692 if Internal.Preferences.propose: 0693 propose = intelligence.selectChow(chows) 0694 else: 0695 propose = None 0696 deferred = Deferred() 0697 selDlg = SelectChow(chows, propose, deferred) 0698 assert selDlg.exec_() 0699 return deferred 0700 0701 def __selectKong(self, kongs): 0702 """which possible kong do we want to declare?""" 0703 if self.game.autoPlay: 0704 return Message.Kong, self.game.myself.intelligence.selectKong(kongs) 0705 if len(kongs) == 1: 0706 return Message.Kong, kongs[0] 0707 deferred = Deferred() 0708 selDlg = SelectKong(kongs, deferred) 0709 assert selDlg.exec_() 0710 return deferred 0711 0712 def __askAnswered(self, answer): 0713 """the user answered our question concerning move""" 0714 if not self.game: 0715 return Message.NoClaim 0716 myself = self.game.myself 0717 if answer in [Message.Discard, Message.OriginalCall]: 0718 # do not remove tile from hand here, the server will tell all players 0719 # including us that it has been discarded. Only then we will remove 0720 # it. 0721 myself.handBoard.setEnabled(False) 0722 return answer, myself.handBoard.focusTile.tile 0723 args = self.game.myself.sayable[answer] 0724 assert args 0725 if answer == Message.Chow: 0726 return self.__selectChow(args) 0727 if answer == Message.Kong: 0728 return self.__selectKong(args) 0729 self.game.hidePopups() 0730 if args is True or args == []: 0731 # this does not specify any tiles, the server does not need this. Robot players 0732 # also return None in this case. 0733 return answer 0734 return answer, args 0735 0736 def __answerError(self, answer, move, answers): 0737 """an error happened while determining the answer to server""" 0738 logException( 0739 '%s %s %s %s' % 0740 (self.game.myself.name if self.game else 'NOGAME', answer, move, answers)) 0741 0742 def remote_abort(self, tableid, message: str, *args): 0743 """the server aborted this game""" 0744 if self.table and self.table.tableid == tableid: 0745 # translate Robot to Roboter: 0746 if self.game: 0747 args = self.game.players.translatePlayerNames(args) 0748 logWarning(i18n(message, *args)) 0749 if self.game: 0750 self.game.close() 0751 0752 def remote_gameOver(self, tableid, message, *args): 0753 """the game is over""" 0754 def yes(unused): 0755 """now that the user clicked the 'game over' prompt away, clean up""" 0756 if self.game: 0757 self.game.rotateWinds() 0758 self.game.close().addCallback(Internal.mainWindow.close) 0759 assert self.table and self.table.tableid == tableid 0760 if Internal.scene: 0761 # update the balances in the status bar: 0762 Internal.scene.mainWindow.updateGUI() 0763 Information(i18n(message, *args)).addCallback(yes) 0764 0765 def remote_serverDisconnects(self, result=None): 0766 """we logged out or lost connection to the server. 0767 Remove visual traces depending on that connection.""" 0768 if Debug.connections and result: 0769 logDebug( 0770 'server %s disconnects: %s' % 0771 (self.connection.url, result)) 0772 self.connection = None 0773 game = self.game 0774 self.game = None # avoid races: messages might still arrive 0775 if self.tableList: 0776 self.tableList.hide() 0777 self.tableList = None 0778 if self in HumanClient.humanClients: 0779 HumanClient.humanClients.remove(self) 0780 if self.beginQuestion: 0781 self.beginQuestion.cancel() 0782 scene = Internal.scene 0783 if scene and game and scene.game == game: 0784 scene.game = None 0785 if not Options.gui: 0786 Internal.mainWindow.close() 0787 0788 def serverDisconnected(self, unusedReference): 0789 """perspective calls us back""" 0790 if self.connection and (Debug.traffic or Debug.connections): 0791 logDebug( 0792 'perspective notifies disconnect: %s' % 0793 self.connection.url) 0794 self.remote_serverDisconnects() 0795 0796 @staticmethod 0797 def __versionError(err): 0798 """log the twisted error""" 0799 logWarning(err.getErrorMessage()) 0800 if Internal.game: 0801 Internal.game.close() 0802 Internal.game = None 0803 return err 0804 0805 @staticmethod 0806 def __wantedGame(): 0807 """find out which game we want to start on the table""" 0808 result = SingleshotOptions.game 0809 if not result or result == '0': 0810 result = str(int(random.random() * 10 ** 9)) 0811 SingleshotOptions.game = None 0812 return result 0813 0814 def tableError(self, err): 0815 """log the twisted error""" 0816 if not self.connection: 0817 # lost connection to server 0818 if self.tableList: 0819 self.tableList.hide() 0820 self.tableList = None 0821 else: 0822 logWarning(err.getErrorMessage()) 0823 0824 def __newLocalTable(self, newId): 0825 """we just got newId from the server""" 0826 return self.callServer('startGame', newId).addErrback(self.tableError) 0827 0828 def __requestNewTableFromServer(self, tableid=None, ruleset=None): 0829 """as the name says""" 0830 if ruleset is None: 0831 ruleset = self.ruleset 0832 self.connection.ruleset = ruleset # side effect: saves ruleset as last used for server 0833 return self.callServer('newTable', ruleset.hash, Options.playOpen, 0834 Internal.autoPlay, self.__wantedGame(), tableid).addErrback(self.tableError) 0835 0836 def newTable(self): 0837 """TableList uses me as a slot""" 0838 if Options.ruleset: 0839 ruleset = Options.ruleset 0840 elif self.hasLocalServer(): 0841 ruleset = self.ruleset 0842 else: 0843 selectDialog = SelectRuleset(self.connection.url) 0844 if not selectDialog.exec_(): 0845 return 0846 ruleset = selectDialog.cbRuleset.current 0847 deferred = self.__requestNewTableFromServer(ruleset=ruleset) 0848 if self.hasLocalServer(): 0849 deferred.addCallback(self.__newLocalTable) 0850 self.tableList.requestedNewTable = True 0851 0852 def joinTable(self, table=None): 0853 """join a table""" 0854 if not isinstance(table, ClientTable): 0855 table = self.tableList.selectedTable() 0856 self.callServer('joinTable', table.tableid).addErrback(self.tableError) 0857 0858 def logout(self, unusedResult=None): 0859 """clean visual traces and logout from server""" 0860 def loggedout(result, connection): 0861 """end the connection from client side""" 0862 if Debug.connections: 0863 logDebug('server confirmed logout for {}'.format(self)) 0864 connection.connector.disconnect() 0865 return result 0866 if self.connection: 0867 conn = self.connection 0868 self.connection = None 0869 if Debug.connections: 0870 logDebug('sending logout to server for {}'.format(self)) 0871 return self.callServer('logout').addCallback(loggedout, conn) 0872 return succeed(None) 0873 0874 def __logCallServer(self, *args): 0875 """for Debug.traffic""" 0876 debugArgs = list(args[:]) 0877 if Debug.neutral: 0878 if debugArgs[0] == 'ping': 0879 return 0880 if debugArgs[0] == 'setClientProperties': 0881 debugArgs[1] = 'DBID' 0882 debugArgs[3] = 'GAMEID' 0883 if debugArgs[4] >= 8300: 0884 debugArgs[4] -= 300 0885 if self.game: 0886 self.game.debug('callServer(%s)' % repr(debugArgs)) 0887 else: 0888 logDebug('callServer(%s)' % repr(debugArgs)) 0889 0890 def callServer(self, *args): 0891 """if we are online, call server""" 0892 if self.connection: 0893 if args[0] is None: 0894 args = args[1:] 0895 try: 0896 if Debug.traffic: 0897 self.__logCallServer(*args) 0898 0899 def callServerError(result): 0900 """if serverDisconnected has been called meanwhile, just ignore msg about 0901 connection lost in a non-clean fashion""" 0902 return result if self.connection else None 0903 return self.connection.perspective.callRemote(*args).addErrback(callServerError) 0904 except pb.DeadReferenceError: 0905 logWarning( 0906 i18n( 0907 'The connection to the server %1 broke, please try again later.', 0908 self.connection.url)) 0909 self.remote_serverDisconnects() 0910 return succeed(None) 0911 else: 0912 return succeed(None) 0913 0914 def sendChat(self, chatLine): 0915 """send chat message to server""" 0916 return self.callServer('chat', chatLine.asList())