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())