Exemplo n.º 1
0
 def __init__(self):
     Client.__init__(self)
     HumanClient.humanClients.append(self)
     self.table = None
     self.ruleset = None
     self.beginQuestion = None
     self.tableList = TableList(self)
     Connection(self).login().addCallbacks(
         self.__loggedIn,
         self.__loginFailed)
Exemplo n.º 2
0
 def __init__(self):
     aiClass = self.__findAI([intelligence, altint], Options.AI)
     if not aiClass:
         raise Exception('intelligence %s is undefined' % Options.AI)
     Client.__init__(self, intelligence=aiClass)
     HumanClient.humanClients.append(self)
     self.table = None
     self.ruleset = None
     self.beginQuestion = None
     self.tableList = TableList(self)
     Connection(self).login().addCallbacks(self.__loggedIn, self.__loginFailed)
Exemplo n.º 3
0
 def __init__(self):
     Client.__init__(self)
     HumanClient.humanClients.append(self)
     self.table = None
     self.ruleset = None
     self.beginQuestion = None
     self.tableList = TableList(self)
     Connection(self).login().addCallbacks(
         self.__loggedIn,
         self.__loginFailed)
Exemplo n.º 4
0
 def showTableList(self, dummy=None):
     """allocate it if needed"""
     if not self.tableList:
         self.tableList = TableList(self)
     self.tableList.loadTables(self.tables)
     self.tableList.activateWindow()
Exemplo n.º 5
0
class HumanClient(Client):
    """a human client"""
    # pylint: disable=R0904
    humanClients = []
    def __init__(self):
        aiClass = self.__findAI([intelligence, altint], Options.AI)
        if not aiClass:
            raise Exception('intelligence %s is undefined' % Options.AI)
        Client.__init__(self, intelligence=aiClass)
        HumanClient.humanClients.append(self)
        self.table = None
        self.ruleset = None
        self.beginQuestion = None
        self.tableList = TableList(self)
        Connection(self).login().addCallbacks(self.__loggedIn, self.__loginFailed)

    @staticmethod
    def shutdownHumanClients(exception=None):
        """close connections to servers except maybe one"""
        clients = HumanClient.humanClients
        def done():
            """return True if clients is cleaned"""
            return len(clients) == 0 or (exception and clients == [exception])
        def disconnectedClient(dummyResult, client):
            """now the client is really disconnected from the server"""
            if client in clients:
                # HumanClient.serverDisconnects also removes it!
                clients.remove(client)
        if isinstance(exception, Failure):
            logException(exception)
        for client in clients[:]:
            if client.tableList:
                client.tableList.hide()
        if done():
            return succeed(None)
        deferreds = []
        for client in clients[:]:
            if client != exception and client.connection:
                deferreds.append(client.logout().addCallback(disconnectedClient, client))
        return DeferredList(deferreds)

    def __loggedIn(self, connection):
        """callback after the server answered our login request"""
        self.connection = connection
        self.ruleset = connection.ruleset
        self.name = connection.username
        self.tableList.show()
        voiceId = None
        if Preferences.uploadVoice:
            voice = Voice.locate(self.name)
            if voice:
                voiceId = voice.md5sum
            if Debug.sound and voiceId:
                logDebug('%s sends own voice %s to server' % (self.name, voiceId))
        maxGameId = Query('select max(id) from game').records[0][0]
        maxGameId = int(maxGameId) if maxGameId else 0
        self.callServer('setClientProperties',
            Internal.dbIdent,
            voiceId, maxGameId, Internal.version).addCallbacks(self.__initTableList, self.__versionError)

    def __initTableList(self, dummy):
        """first load of the list. Process options like --demo, --table, --join"""
        self.showTableList()
        if SingleshotOptions.table:
            Internal.autoPlay = False
            self.__requestNewTableFromServer(SingleshotOptions.table).addCallback(
                self.__showTables).addErrback(self.tableError)
            if Debug.table:
                logDebug('%s: --table lets us open an new table %d' % (self.name, SingleshotOptions.table))
            SingleshotOptions.table = False
        elif SingleshotOptions.join:
            Internal.autoPlay = False
            self.callServer('joinTable', SingleshotOptions.join).addCallback(
                self.__showTables).addErrback(self.tableError)
            if Debug.table:
                logDebug('%s: --join lets us join table %s' % (self.name, self._tableById(SingleshotOptions.join)))
            SingleshotOptions.join = False
        elif not self.game and (Internal.autoPlay or (not self.tables and self.hasLocalServer())):
            self.__requestNewTableFromServer().addCallback(self.__newLocalTable).addErrback(self.tableError)
        else:
            self.__showTables()

    @staticmethod
    def __loginFailed(dummy):
        """as the name says"""
        Internal.field.startingGame = False

    @staticmethod
    def __findAI(modules, aiName):
        """list of all alternative AIs defined in altint.py"""
        for modul in modules:
            for key, value in modul.__dict__.items():
                if key == 'AI' + aiName:
                    return value

    def isRobotClient(self):
        """avoid using isinstance, it would import too much for kajonggserver"""
        return False

    @staticmethod
    def isHumanClient():
        """avoid using isinstance, it would import too much for kajonggserver"""
        return True

    def isServerClient(self):
        """avoid using isinstance, it would import too much for kajonggserver"""
        return False

    def hasLocalServer(self):
        """True if we are talking to a Local Game Server"""
        return self.connection and self.connection.useSocket

    def __updateTableList(self):
        """if it exists"""
        if self.tableList:
            self.tableList.loadTables(self.tables)

    def __showTables(self, dummy=None):
        """load and show tables. We may be used as a callback. In that case,
        clientTables is the id of a new table - which we do not need here"""
        self.tableList.loadTables(self.tables)
        self.tableList.show()

    def showTableList(self, dummy=None):
        """allocate it if needed"""
        if not self.tableList:
            self.tableList = TableList(self)
        self.tableList.loadTables(self.tables)
        self.tableList.activateWindow()

    def remote_tableRemoved(self, tableid, message, *args):
        """update table list"""
        Client.remote_tableRemoved(self, tableid, message, *args)
        self.__updateTableList()
        if message:
            if not self.name in args or not message.endswith('has logged out'):
                logWarning(m18n(message, *args))

    def __receiveTables(self, tables):
        """now we already know all rulesets for those tables"""
        Client.remote_newTables(self, tables)
        if not Internal.autoPlay:
            if self.hasLocalServer():
                # when playing a local game, only show pending tables with
                # previously selected ruleset
                self.tables = list(x for x in self.tables if x.ruleset == self.ruleset)
        if len(self.tables):
            self.__updateTableList()

    def remote_newTables(self, tables):
        """update table list"""
        assert len(tables)
        def gotRulesets(result):
            """the server sent us the wanted ruleset definitions"""
            for ruleset in result:
                Ruleset.cached(ruleset).save(copy=True) # make it known to the cache and save in db
            return tables
        rulesetHashes = set(x[1] for x in tables)
        needRulesets = list(x for x in rulesetHashes if not Ruleset.hashIsKnown(x))
        if needRulesets:
            self.callServer('needRulesets', needRulesets).addCallback(gotRulesets).addCallback(self.__receiveTables)
        else:
            self.__receiveTables(tables)

    @staticmethod
    def remote_needRuleset(ruleset):
        """server only knows hash, needs full definition"""
        result = Ruleset.cached(ruleset)
        assert result and result.hash == ruleset
        return result.toList()

    def tableChanged(self, table):
        """update table list"""
        oldTable, newTable = Client.tableChanged(self, table)
        if oldTable and oldTable == self.table:
            # this happens if a table has more than one human player and
            # one of them leaves the table. In that case, the other players
            # need this code.
            self.table = newTable
            if len(newTable.playerNames) == 3:
                # only tell about the first player leaving, because the
                # others will then automatically leave too
                for name in oldTable.playerNames:
                    if name != self.name and not newTable.isOnline(name):
                        def sorried(dummy):
                            """user ack"""
                            game = self.game
                            if game:
                                self.game = None
                                return game.close()
                        if self.beginQuestion:
                            self.beginQuestion.cancel()
                        Sorry(m18n('Player %1 has left the table', name)).addCallback(
                            sorried).addCallback(self.showTableList)
                        break
        self.__updateTableList()

    def remote_chat(self, data):
        """others chat to me"""
        chatLine = ChatMessage(data)
        if Debug.chat:
            logDebug('got chatLine: %s' % chatLine)
        table = self._tableById(chatLine.tableid)
        if not chatLine.isStatusMessage and not table.chatWindow:
            ChatWindow(table)
        if table.chatWindow:
            table.chatWindow.receiveLine(chatLine)

    def readyForGameStart(self, tableid, gameid, wantedGame, playerNames, shouldSave=True):
        """playerNames are in wind order ESWN"""
        def answered(result):
            """callback, called after the client player said yes or no"""
            self.beginQuestion = None
            if self.connection and result:
                # still connected and yes, we are
                Client.readyForGameStart(self, tableid, gameid, wantedGame, playerNames, shouldSave)
                return Message.OK
            else:
                return Message.NoGameStart
        def cancelled(dummy):
            """the user does not want to start now. Back to table list"""
            if Debug.table:
                logDebug('%s: Readyforgamestart returns Message.NoGameStart for table %s' % (
                    self.name, self._tableById(tableid)))
            self.table = None
            self.beginQuestion = None
            if self.tableList:
                self.__updateTableList()
                self.tableList.show()
            return Message.NoGameStart
        if sum(not x[1].startswith('Robot ') for x in playerNames) == 1:
            # we play against 3 robots and we already told the server to start: no need to ask again
            return Client.readyForGameStart(self, tableid, gameid, wantedGame, playerNames, shouldSave)
        assert not self.table
        assert self.tables
        self.table = self._tableById(tableid)
        if not self.table:
            raise pb.Error('client.readyForGameStart: tableid %d unknown' % tableid)
        msg = m18n("The game on table <numid>%1</numid> can begin. Are you ready to play now?", tableid)
        self.beginQuestion = QuestionYesNo(msg, modal=False, caption=self.name).addCallback(
            answered).addErrback(cancelled)
        return self.beginQuestion

    def readyForHandStart(self, playerNames, rotateWinds):
        """playerNames are in wind order ESWN. Never called for first hand."""
        def answered(dummy=None):
            """called after the client player said yes, I am ready"""
            if self.connection:
                return Client.readyForHandStart(self, playerNames, rotateWinds)
        if not self.connection:
            # disconnected meanwhile
            return
        if Internal.field:
            # update the balances in the status bar:
            Internal.field.updateGUI()
        assert not self.game.isFirstHand()
        return Information(m18n("Ready for next hand?"), modal=False).addCallback(answered)

    def ask(self, move, answers):
        """server sends move. We ask the user. answers is a list with possible answers,
        the default answer being the first in the list."""
        if not Internal.field:
            return Client.ask(self, move, answers)
        self._computeSayable(move, answers)
        deferred = Deferred()
        deferred.addCallback(self.__askAnswered)
        deferred.addErrback(self.__answerError, move, answers)
        iAmActive = self.game.myself == self.game.activePlayer
        self.game.myself.handBoard.setEnabled(iAmActive)
        field = Internal.field
        oldDialog = field.clientDialog
        if oldDialog and not oldDialog.answered:
            raise Exception('old dialog %s:%s is unanswered, new Dialog: %s/%s' % (
                str(oldDialog.move),
                str([x.name for x in oldDialog.buttons]),
                str(move), str(answers)))
        if not oldDialog or not oldDialog.isVisible():
            # always build a new dialog because if we change its layout before
            # reshowing it, sometimes the old buttons are still visible in which
            # case the next dialog will appear at a lower position than it should
            field.clientDialog = ClientDialog(self, field.centralWidget())
        assert field.clientDialog.client is self
        field.clientDialog.askHuman(move, answers, deferred)
        return deferred

    def __selectChow(self, chows):
        """which possible chow do we want to expose?
        Since we might return a Deferred to be sent to the server,
        which contains Message.Chow plus selected Chow, we should
        return the same tuple here"""
        if self.game.autoPlay:
            return Message.Chow, self.intelligence.selectChow(chows)
        if len(chows) == 1:
            return Message.Chow, chows[0]
        if Preferences.propose:
            propose = self.intelligence.selectChow(chows)
        else:
            propose = None
        deferred = Deferred()
        selDlg = SelectChow(chows, propose, deferred)
        assert selDlg.exec_()
        return deferred

    def __selectKong(self, kongs):
        """which possible kong do we want to declare?"""
        if self.game.autoPlay:
            return Message.Kong, self.intelligence.selectKong(kongs)
        if len(kongs) == 1:
            return Message.Kong, kongs[0]
        deferred = Deferred()
        selDlg = SelectKong(kongs, deferred)
        assert selDlg.exec_()
        return deferred

    def __askAnswered(self, answer):
        """the user answered our question concerning move"""
        if not self.game:
            return Message.NoClaim
        myself = self.game.myself
        if answer in [Message.Discard, Message.OriginalCall]:
            # do not remove tile from hand here, the server will tell all players
            # including us that it has been discarded. Only then we will remove it.
            myself.handBoard.setEnabled(False)
            return answer, myself.handBoard.focusTile.element
        args = self.sayable[answer]
        assert args
        if answer == Message.Chow:
            return self.__selectChow(args)
        if answer == Message.Kong:
            return self.__selectKong(args)
        self.game.hidePopups()
        if args is True or args == []:
            # this does not specify any tiles, the server does not need this. Robot players
            # also return None in this case.
            return answer
        else:
            return answer, args

    def __answerError(self, answer, move, answers):
        """an error happened while determining the answer to server"""
        logException('%s %s %s %s' % (self.game.myself.name if self.game else 'NOGAME', answer, move, answers))

    def remote_abort(self, tableid, message, *args):
        """the server aborted this game"""
        if self.table and self.table.tableid == tableid:
            # translate Robot to Roboter:
            if self.game:
                args = self.game.players.translatePlayerNames(args)
            logWarning(m18n(message, *args))
            if self.game:
                self.game.close()
                if self.game.autoPlay:
                    if Internal.field:
                        Internal.field.close()

    def remote_gameOver(self, tableid, message, *args):
        """the game is over"""
        def yes(dummy):
            """now that the user clicked the 'game over' prompt away, clean up"""
            if self.game:
                self.game.rotateWinds()
                if Options.csv:
                    gameWinner = max(self.game.players, key=lambda x: x.balance)
                    writer = csv.writer(open(Options.csv,'a'), delimiter=';')
                    if Debug.process:
                        self.game.csvTags.append('MEM:%s' % resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
                    row = [Options.AI, str(self.game.seed), ','.join(self.game.csvTags)]
                    for player in sorted(self.game.players, key=lambda x: x.name):
                        row.append(player.name.encode('utf-8'))
                        row.append(player.balance)
                        row.append(player.wonCount)
                        row.append(1 if player == gameWinner else 0)
                    writer.writerow(row)
                    del writer
                if self.game.autoPlay and Internal.field:
                    Internal.field.close()
                else:
                    self.game.close().addCallback(Client.quitProgram)
        assert self.table and self.table.tableid == tableid
        if Internal.field:
            # update the balances in the status bar:
            Internal.field.updateGUI()
        logInfo(m18n(message, *args), showDialog=True).addCallback(yes)

    def remote_serverDisconnects(self, result=None):
        """we logged out or or lost connection to the server.
        Remove visual traces depending on that connection."""
        if Debug.connections and result:
            logDebug('server %s disconnects: %s' % (self.connection.url, result))
        self.connection = None
        game = self.game
        self.game = None # avoid races: messages might still arrive
        if self.tableList:
            self.tableList.hide()
            self.tableList = None
        if self in HumanClient.humanClients:
            HumanClient.humanClients.remove(self)
        if self.beginQuestion:
            self.beginQuestion.cancel()
        field = Internal.field
        if field and game and field.game == game:
            game.close() # TODO: maybe issue a Sorry first?

    def serverDisconnected(self, dummyReference):
        """perspective calls us back"""
        if self.connection and (Debug.traffic or Debug.connections):
            logDebug('perspective notifies disconnect: %s' % self.connection.url)
        self.remote_serverDisconnects()

    @staticmethod
    def __versionError(err):
        """log the twisted error"""
        logWarning(err.getErrorMessage())
        Internal.field.abortGame()
        return err

    @staticmethod
    def __wantedGame():
        """find out which game we want to start on the table"""
        result = SingleshotOptions.game
        if not result or result == '0':
            result = str(int(random.random() * 10**9))
        SingleshotOptions.game = None
        return result

    def tableError(self, err):
        """log the twisted error"""
        if not self.connection:
            # lost connection to server
            if self.tableList:
                self.tableList.hide()
                self.tableList = None
        else:
            logWarning(err.getErrorMessage())

    def __newLocalTable(self, newId):
        """we just got newId from the server"""
        return self.callServer('startGame', newId).addErrback(self.tableError)

    def __requestNewTableFromServer(self, tableid=None, ruleset=None):
        """as the name says"""
        if ruleset is None:
            ruleset = self.ruleset
        return self.callServer('newTable', ruleset.hash, Options.playOpen,
            Internal.autoPlay, self.__wantedGame(), tableid).addErrback(self.tableError)

    def newTable(self):
        """TableList uses me as a slot"""
        if Options.ruleset:
            ruleset = Options.ruleset
        elif self.hasLocalServer():
            ruleset = self.ruleset
        else:
            selectDialog = SelectRuleset(self.connection.url)
            if not selectDialog.exec_():
                return
            ruleset = selectDialog.cbRuleset.current
        deferred = self.__requestNewTableFromServer(ruleset=ruleset)
        if self.hasLocalServer():
            deferred.addCallback(self.__newLocalTable)
        self.tableList.requestedNewTable = True

    def joinTable(self, table=None):
        """join a table"""
        if not isinstance(table, ClientTable):
            table = self.tableList.selectedTable()
        self.callServer('joinTable', table.tableid).addErrback(self.tableError)

    def logout(self, dummyResult=None):
        """clean visual traces and logout from server"""
        def loggedout(result, connection):
            """TODO: do we need this?"""
            connection.connector.disconnect()
            return result
        if self.connection:
            conn = self.connection
            self.connection = None
            return self.callServer('logout').addCallback(loggedout, conn)
        else:
            return succeed(None)

    def callServer(self, *args):
        """if we are online, call server"""
        if self.connection:
            if args[0] is None:
                args = args[1:]
            try:
                if Debug.traffic:
                    if self.game:
                        self.game.debug('callServer(%s)' % repr(args))
                    else:
                        logDebug('callServer(%s)' % repr(args))
                def callServerError(result):
                    """if serverDisconnected has been called meanwhile, just ignore msg about
                    connection lost in a non-clean fashion"""
                    if self.connection:
                        return result
                return self.connection.perspective.callRemote(*args).addErrback(callServerError)
            except pb.DeadReferenceError:
                logWarning(m18n('The connection to the server %1 broke, please try again later.',
                                  self.connection.url))
                self.remote_serverDisconnects()
                return succeed(None)
        else:
            return succeed(None)

    def sendChat(self, chatLine):
        """send chat message to server"""
        return self.callServer('chat', chatLine.asList())
Exemplo n.º 6
0
 def showTableList(self, dummy=None):
     """allocate it if needed"""
     if not self.tableList:
         self.tableList = TableList(self)
     self.tableList.loadTables(self.tables)
     self.tableList.activateWindow()
Exemplo n.º 7
0
class HumanClient(Client):

    """a human client"""
    # pylint: disable=too-many-public-methods
    humanClients = []

    def __init__(self):
        Client.__init__(self)
        HumanClient.humanClients.append(self)
        self.table = None
        self.ruleset = None
        self.beginQuestion = None
        self.tableList = TableList(self)
        Connection(self).login().addCallbacks(
            self.__loggedIn,
            self.__loginFailed)

    @staticmethod
    def shutdownHumanClients(exception=None):
        """close connections to servers except maybe one"""
        clients = HumanClient.humanClients

        def done():
            """return True if clients is cleaned"""
            return len(clients) == 0 or (exception and clients == [exception])

        def disconnectedClient(dummyResult, client):
            """now the client is really disconnected from the server"""
            if client in clients:
                # HumanClient.serverDisconnects also removes it!
                clients.remove(client)
        if isinstance(exception, Failure):
            logException(exception)
        for client in clients[:]:
            if client.tableList:
                client.tableList.hide()
        if done():
            return succeed(None)
        deferreds = []
        for client in clients[:]:
            if client != exception and client.connection:
                deferreds.append(
                    client.logout(
                    ).addCallback(
                        disconnectedClient,
                        client))
        return DeferredList(deferreds)

    def __loggedIn(self, connection):
        """callback after the server answered our login request"""
        self.connection = connection
        self.ruleset = connection.ruleset
        self.name = connection.username
        self.tableList.show()
        voiceId = None
        if Internal.Preferences.uploadVoice:
            voice = Voice.locate(self.name)
            if voice:
                voiceId = voice.md5sum
            if Debug.sound and voiceId:
                logDebug(
                    u'%s sends own voice %s to server' %
                    (self.name, voiceId))
        maxGameId = Query('select max(id) from game').records[0][0]
        maxGameId = int(maxGameId) if maxGameId else 0
        self.callServer('setClientProperties',
                        Internal.db.identifier,
                        voiceId, maxGameId,
                        Internal.defaultPort).addCallbacks(self.__initTableList, self.__versionError)

    def __initTableList(self, dummy):
        """first load of the list. Process options like --demo, --table, --join"""
        self.showTableList()
        if SingleshotOptions.table:
            Internal.autoPlay = False
            self.__requestNewTableFromServer(SingleshotOptions.table).addCallback(
                self.__showTables).addErrback(self.tableError)
            if Debug.table:
                logDebug(
                    u'%s: --table lets us open an new table %d' %
                    (self.name, SingleshotOptions.table))
            SingleshotOptions.table = False
        elif SingleshotOptions.join:
            Internal.autoPlay = False
            self.callServer('joinTable', SingleshotOptions.join).addCallback(
                self.__showTables).addErrback(self.tableError)
            if Debug.table:
                logDebug(
                    u'%s: --join lets us join table %s' %
                    (self.name, self._tableById(SingleshotOptions.join)))
            SingleshotOptions.join = False
        elif not self.game and (Internal.autoPlay or (not self.tables and self.hasLocalServer())):
            self.__requestNewTableFromServer().addCallback(
                self.__newLocalTable).addErrback(self.tableError)
        else:
            self.__showTables()

    @staticmethod
    def __loginFailed(dummy):
        """as the name says"""
        if Internal.scene:
            Internal.scene.startingGame = False

    def isRobotClient(self):
        """avoid using isinstance, it would import too much for kajonggserver"""
        return False

    @staticmethod
    def isHumanClient():
        """avoid using isinstance, it would import too much for kajonggserver"""
        return True

    def isServerClient(self):
        """avoid using isinstance, it would import too much for kajonggserver"""
        return False

    def hasLocalServer(self):
        """True if we are talking to a Local Game Server"""
        return self.connection and self.connection.url.isLocalHost

    def __updateTableList(self):
        """if it exists"""
        if self.tableList:
            self.tableList.loadTables(self.tables)

    def __showTables(self, dummy=None):
        """load and show tables. We may be used as a callback. In that case,
        clientTables is the id of a new table - which we do not need here"""
        self.tableList.loadTables(self.tables)
        self.tableList.show()

    def showTableList(self, dummy=None):
        """allocate it if needed"""
        if not self.tableList:
            self.tableList = TableList(self)
        self.tableList.loadTables(self.tables)
        self.tableList.activateWindow()

    def remote_tableRemoved(self, tableid, message, *args):
        """update table list"""
        Client.remote_tableRemoved(self, tableid, message, *args)
        self.__updateTableList()
        if message:
            if self.name not in args or not message.endswith('has logged out'):
                logWarning(m18n(message, *args))

    def __receiveTables(self, tables):
        """now we already know all rulesets for those tables"""
        Client.remote_newTables(self, tables)
        if not Internal.autoPlay:
            if self.hasLocalServer():
                # when playing a local game, only show pending tables with
                # previously selected ruleset
                self.tables = list(
                    x for x in self.tables if x.ruleset == self.ruleset)
        if len(self.tables):
            self.__updateTableList()

    def remote_newTables(self, tables):
        """update table list"""
        assert len(tables)

        def gotRulesets(result):
            """the server sent us the wanted ruleset definitions"""
            for ruleset in result:
                Ruleset.cached(ruleset).save()  # make it known to the cache and save in db
            return tables
        rulesetHashes = set(x[1] for x in tables)
        needRulesets = list(
            x for x in rulesetHashes if not Ruleset.hashIsKnown(x))
        if needRulesets:
            self.callServer(
                'needRulesets',
                needRulesets).addCallback(
                    gotRulesets).addCallback(
                        self.__receiveTables)
        else:
            self.__receiveTables(tables)

    @staticmethod
    def remote_needRuleset(ruleset):
        """server only knows hash, needs full definition"""
        result = Ruleset.cached(ruleset)
        assert result and result.hash == ruleset
        return result.toList()

    def tableChanged(self, table):
        """update table list"""
        oldTable, newTable = Client.tableChanged(self, table)
        if oldTable and oldTable == self.table:
            # this happens if a table has more than one human player and
            # one of them leaves the table. In that case, the other players
            # need this code.
            self.table = newTable
            if len(newTable.playerNames) == 3:
                # only tell about the first player leaving, because the
                # others will then automatically leave too
                for name in oldTable.playerNames:
                    if name != self.name and not newTable.isOnline(name):
                        def sorried(dummy):
                            """user ack"""
                            game = self.game
                            if game:
                                self.game = None
                                return game.close()
                        if self.beginQuestion:
                            self.beginQuestion.cancel()
                        Sorry(m18n('Player %1 has left the table', name)).addCallback(
                            sorried).addCallback(self.showTableList)
                        break
        self.__updateTableList()

    def remote_chat(self, data):
        """others chat to me"""
        chatLine = ChatMessage(data)
        if Debug.chat:
            logDebug(u'got chatLine: %s' % chatLine)
        table = self._tableById(chatLine.tableid)
        if not chatLine.isStatusMessage and not table.chatWindow:
            ChatWindow(table)
        if table.chatWindow:
            table.chatWindow.receiveLine(chatLine)

    def readyForGameStart(
            self, tableid, gameid, wantedGame, playerNames, shouldSave=True,
            gameClass=None):
        """playerNames are in wind order ESWN"""
        if gameClass is None:
            if Options.gui:
                gameClass = VisiblePlayingGame
            else:
                gameClass = PlayingGame

        def clientReady():
            """macro"""
            return Client.readyForGameStart(
                self, tableid, gameid, wantedGame, playerNames,
                shouldSave, gameClass)

        def answered(result):
            """callback, called after the client player said yes or no"""
            self.beginQuestion = None
            if self.connection and result:
                # still connected and yes, we are
                return clientReady()
            else:
                return Message.NoGameStart

        def cancelled(dummy):
            """the user does not want to start now. Back to table list"""
            if Debug.table:
                logDebug(u'%s: Readyforgamestart returns Message.NoGameStart for table %s' % (
                    self.name, self._tableById(tableid)))
            self.table = None
            self.beginQuestion = None
            if self.tableList:
                self.__updateTableList()
                self.tableList.show()
            return Message.NoGameStart
        if sum(not x[1].startswith(u'Robot ') for x in playerNames) == 1:
            # we play against 3 robots and we already told the server to start:
            # no need to ask again
            return clientReady()
        assert not self.table
        assert self.tables
        self.table = self._tableById(tableid)
        if not self.table:
            raise pb.Error(
                'client.readyForGameStart: tableid %d unknown' %
                tableid)
        msg = m18n(
            "The game on table <numid>%1</numid> can begin. Are you ready to play now?",
            tableid)
        self.beginQuestion = QuestionYesNo(msg, modal=False, caption=self.name).addCallback(
            answered).addErrback(cancelled)
        return self.beginQuestion

    def readyForHandStart(self, playerNames, rotateWinds):
        """playerNames are in wind order ESWN. Never called for first hand."""
        def answered(dummy=None):
            """called after the client player said yes, I am ready"""
            if self.connection:
                return Client.readyForHandStart(self, playerNames, rotateWinds)
        if not self.connection:
            # disconnected meanwhile
            return
        if Options.gui:
            # update the balances in the status bar:
            Internal.mainWindow.updateGUI()
        assert not self.game.isFirstHand()
        return Information(m18n("Ready for next hand?"), modal=usingKDE).addCallback(answered)

    def ask(self, move, answers):
        """server sends move. We ask the user. answers is a list with possible answers,
        the default answer being the first in the list."""
        if not Options.gui:
            return Client.ask(self, move, answers)
        self.game.myself.computeSayable(move, answers)
        deferred = Deferred()
        deferred.addCallback(self.__askAnswered)
        deferred.addErrback(self.__answerError, move, answers)
        iAmActive = self.game.myself == self.game.activePlayer
        self.game.myself.handBoard.setEnabled(iAmActive)
        scene = Internal.scene
        oldDialog = scene.clientDialog
        if oldDialog and not oldDialog.answered:
            raise Exception('old dialog %s:%s is unanswered, new Dialog: %s/%s' % (
                str(oldDialog.move),
                str([x.message.name for x in oldDialog.buttons]),
                str(move), str(answers)))
        if not oldDialog or not oldDialog.isVisible():
            # always build a new dialog because if we change its layout before
            # reshowing it, sometimes the old buttons are still visible in which
            # case the next dialog will appear at a lower position than it
            # should
            scene.clientDialog = ClientDialog(
                self,
                scene.mainWindow.centralWidget())
        assert scene.clientDialog.client is self
        scene.clientDialog.askHuman(move, answers, deferred)
        return deferred

    def __selectChow(self, chows):
        """which possible chow do we want to expose?
        Since we might return a Deferred to be sent to the server,
        which contains Message.Chow plus selected Chow, we should
        return the same tuple here"""
        intelligence = self.game.myself.intelligence
        if self.game.autoPlay:
            return Message.Chow, intelligence.selectChow(chows)
        if len(chows) == 1:
            return Message.Chow, chows[0]
        if Internal.Preferences.propose:
            propose = intelligence.selectChow(chows)
        else:
            propose = None
        deferred = Deferred()
        selDlg = SelectChow(chows, propose, deferred)
        assert selDlg.exec_()
        return deferred

    def __selectKong(self, kongs):
        """which possible kong do we want to declare?"""
        if self.game.autoPlay:
            return Message.Kong, self.game.myself.intelligence.selectKong(kongs)
        if len(kongs) == 1:
            return Message.Kong, kongs[0]
        deferred = Deferred()
        selDlg = SelectKong(kongs, deferred)
        assert selDlg.exec_()
        return deferred

    def __askAnswered(self, answer):
        """the user answered our question concerning move"""
        if not self.game:
            return Message.NoClaim
        myself = self.game.myself
        if answer in [Message.Discard, Message.OriginalCall]:
            # do not remove tile from hand here, the server will tell all players
            # including us that it has been discarded. Only then we will remove
            # it.
            myself.handBoard.setEnabled(False)
            return answer, myself.handBoard.focusTile.tile
        args = self.game.myself.sayable[answer]
        assert args
        if answer == Message.Chow:
            return self.__selectChow(args)
        if answer == Message.Kong:
            return self.__selectKong(args)
        self.game.hidePopups()
        if args is True or args == []:
            # this does not specify any tiles, the server does not need this. Robot players
            # also return None in this case.
            return answer
        else:
            return answer, args

    def __answerError(self, answer, move, answers):
        """an error happened while determining the answer to server"""
        logException(
            '%s %s %s %s' %
            (self.game.myself.name if self.game else 'NOGAME', answer, move, answers))

    def remote_abort(self, tableid, message, *args):
        """the server aborted this game"""
        message = nativeString(message)
        args = nativeStringArgs(args)
        if self.table and self.table.tableid == tableid:
            # translate Robot to Roboter:
            if self.game:
                args = self.game.players.translatePlayerNames(args)
            logWarning(m18n(message, *args))
            if self.game:
                self.game.close()

    def remote_gameOver(self, tableid, message, *args):
        """the game is over"""
        def yes(dummy):
            """now that the user clicked the 'game over' prompt away, clean up"""
            if self.game:
                self.game.rotateWinds()
                self.game.close().addCallback(Internal.mainWindow.close)
        assert self.table and self.table.tableid == tableid
        if Internal.scene:
            # update the balances in the status bar:
            Internal.scene.mainWindow.updateGUI()
        Information(m18n(message, *args)).addCallback(yes)

    def remote_serverDisconnects(self, result=None):
        """we logged out or or lost connection to the server.
        Remove visual traces depending on that connection."""
        if Debug.connections and result:
            logDebug(
                u'server %s disconnects: %s' %
                (self.connection.url, result))
        self.connection = None
        game = self.game
        self.game = None  # avoid races: messages might still arrive
        if self.tableList:
            self.tableList.hide()
            self.tableList = None
        if self in HumanClient.humanClients:
            HumanClient.humanClients.remove(self)
        if self.beginQuestion:
            self.beginQuestion.cancel()
        scene = Internal.scene
        if scene and game and scene.game == game:
            scene.game = None

    def serverDisconnected(self, dummyReference):
        """perspective calls us back"""
        if self.connection and (Debug.traffic or Debug.connections):
            logDebug(
                u'perspective notifies disconnect: %s' %
                self.connection.url)
        self.remote_serverDisconnects()

    @staticmethod
    def __versionError(err):
        """log the twisted error"""
        logWarning(err.getErrorMessage())
        if Internal.game:
            Internal.game.close()
            Internal.game = None
        return err

    @staticmethod
    def __wantedGame():
        """find out which game we want to start on the table"""
        result = SingleshotOptions.game
        if not result or result == '0':
            result = str(int(random.random() * 10 ** 9))
        SingleshotOptions.game = None
        return result

    def tableError(self, err):
        """log the twisted error"""
        if not self.connection:
            # lost connection to server
            if self.tableList:
                self.tableList.hide()
                self.tableList = None
        else:
            logWarning(err.getErrorMessage())

    def __newLocalTable(self, newId):
        """we just got newId from the server"""
        return self.callServer('startGame', newId).addErrback(self.tableError)

    def __requestNewTableFromServer(self, tableid=None, ruleset=None):
        """as the name says"""
        if ruleset is None:
            ruleset = self.ruleset
        self.connection.ruleset = ruleset  # side effect: saves ruleset as last used for server
        return self.callServer('newTable', ruleset.hash, Options.playOpen,
                               Internal.autoPlay, self.__wantedGame(), tableid).addErrback(self.tableError)

    def newTable(self):
        """TableList uses me as a slot"""
        if Options.ruleset:
            ruleset = Options.ruleset
        elif self.hasLocalServer():
            ruleset = self.ruleset
        else:
            selectDialog = SelectRuleset(self.connection.url)
            if not selectDialog.exec_():
                return
            ruleset = selectDialog.cbRuleset.current
        deferred = self.__requestNewTableFromServer(ruleset=ruleset)
        if self.hasLocalServer():
            deferred.addCallback(self.__newLocalTable)
        self.tableList.requestedNewTable = True

    def joinTable(self, table=None):
        """join a table"""
        if not isinstance(table, ClientTable):
            table = self.tableList.selectedTable()
        self.callServer('joinTable', table.tableid).addErrback(self.tableError)

    def logout(self, dummyResult=None):
        """clean visual traces and logout from server"""
        def loggedout(result, connection):
            """end the connection from client side"""
            connection.connector.disconnect()
            return result
        if self.connection:
            conn = self.connection
            self.connection = None
            return self.callServer('logout').addCallback(loggedout, conn)
        else:
            return succeed(None)

    def __logCallServer(self, *args):
        """for Debug.traffic"""
        debugArgs = list(args[:])
        if Debug.neutral:
            if debugArgs[0] == 'ping':
                return
            if debugArgs[0] == 'setClientProperties':
                debugArgs[1] = 'DBID'
                debugArgs[3] = 'GAMEID'
                if debugArgs[4] >= 8300:
                    debugArgs[4] -= 300
        if self.game:
            self.game.debug('callServer(%s)' % repr(debugArgs))
        else:
            logDebug(u'callServer(%s)' % repr(debugArgs))

    def callServer(self, *args):
        """if we are online, call server"""
        if self.connection:
            if args[0] is None:
                args = args[1:]
            try:
                if Debug.traffic:
                    self.__logCallServer(*args)

                def callServerError(result):
                    """if serverDisconnected has been called meanwhile, just ignore msg about
                    connection lost in a non-clean fashion"""
                    if self.connection:
                        return result
                return self.connection.perspective.callRemote(*args).addErrback(callServerError)
            except pb.DeadReferenceError:
                logWarning(
                    m18n(
                        'The connection to the server %1 broke, please try again later.',
                        self.connection.url))
                self.remote_serverDisconnects()
                return succeed(None)
        else:
            return succeed(None)

    def sendChat(self, chatLine):
        """send chat message to server"""
        return self.callServer('chat', chatLine.asList())