# -*- coding: utf-8 -*-

"""
Copyright (C) 2008-2012 Wolfgang Rohdewald <wolfgang@rohdewald.de>

kajongg is free software you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""

import datetime
import weakref
from random import Random
from collections import defaultdict
from twisted.internet.defer import succeed
from util import logError, logWarning, logException, logDebug, m18n, stack
from common import WINDS, Internal, IntDict, Debug
from query import Transaction, Query
from rule import Ruleset
from tile import Tile, elements
from meld import tileKey
from hand import Hand
from sound import Voice
from wall import Wall
from move import Move
from player import Players, Player, PlayingPlayer

class CountingRandom(Random):
    """counts how often random() is called and prints debug info"""
    def __init__(self, game, value=None):
        self._game = weakref.ref(game)
        Random.__init__(self, value)
        self.count = 0

    @property
    def game(self):
        """hide the fact that game is a weakref"""
        return self._game()

    def random(self):
        """the central randomizator"""
        self.count += 1
        return Random.random(self)
    def seed(self, newSeed=None):
        self.count = 0
        if Debug.random:
            self.game.debug('Random gets seed %s' % newSeed)
        Random.seed(self, newSeed)
    def shuffle(self, listValue, func=None, intType=int):
        oldCount = self.count
        Random.shuffle(self, listValue, func, intType)
        if Debug.random:
            self.game.debug('%d calls to random by Random.shuffle from %s' % (
                self.count - oldCount, stack('')[-2]))
    def randrange(self, start, stop=None, step=1, intType=int, default=None, maxWidth=9007199254740992L):
        oldCount = self.count
        result = Random.randrange(self, start, stop, step, intType, default, maxWidth)
        if Debug.random:
            self.game.debug('%d calls to random by Random.randrange(%d,%s) from %s' % (
                self.count - oldCount, start, stop, stack('')[-2]))
        return result
    def choice(self, fromList):
        if len(fromList) == 1:
            return fromList[0]
        oldCount = self.count
        result = Random.choice(self, fromList)
        if Debug.random:
            self.game.debug('%d calls to random by Random.choice(%s) from %s' % (
                self.count - oldCount, str([str(x) for x in fromList]), stack('')[-2]))
        return result
    def sample(self, population, wantedLength):
        oldCount = self.count
        result = Random.sample(self, population, wantedLength)
        if Debug.random:
            self.game.debug('%d calls to random by Random.sample(x, %d) from %s' % (
                self.count - oldCount, wantedLength, stack('')[-2]))
        return result

class Game(object):
    """the game without GUI"""
    # pylint: disable=too-many-instance-attributes
    playerClass = Player

    def __del__(self):
        """break reference cycles"""
        self.clearHand()
        if self.players:
            for player in self.players[:]:
                self.players.remove(player)
                del player
            self.players = []
        self.__activePlayer = None
        self.prevActivePlayer = None
        self.__winner = None
        self.myself = None
        if self.client:
            self.client.game = None
        self.client = None

    def __init__(self, names, ruleset, gameid=None, wantedGame=None, shouldSave=True, client=None):
        """a new game instance. May be shown on a field, comes from database if gameid is set

        Game.lastDiscard is the tile last discarded by any player. It is reset to None when a
        player gets a tile from the living end of the wall or after he claimed a discard.
        """
        # pylint: disable=too-many-statements
        assert self.__class__ != Game, 'Do not directly instantiate Game'
        self.players = Players() # if we fail later on in init, at least we can still close the program
        self._client = None
        self.client = client
        self.rotated = 0
        self.notRotated = 0 # counts hands since last rotation
        self.ruleset = None
        self.roundsFinished = 0
        self._currentHandId = None
        self._prevHandId = None
        self.seed = 0
        self.randomGenerator = CountingRandom(self)
        if self.isScoringGame():
            self.wantedGame = str(wantedGame)
            self.seed = wantedGame
        else:
            self.wantedGame = wantedGame
            _ = int(wantedGame.split('/')[0]) if wantedGame else 0
            self.seed = _ or int(self.randomGenerator.random() * 10**9)
        self.shouldSave = shouldSave
        self._setHandSeed()
        self.activePlayer = None
        self.__winner = None
        self.moves = []
        self.myself = None   # the player using this client instance for talking to the server
        self.gameid = gameid
        self.playOpen = False
        self.autoPlay = False
        self.handctr = 0
        self.roundHandCount = 0
        self.handDiscardCount = 0
        self.divideAt = None
        self.__lastDiscard = None # always uppercase
        self.visibleTiles = IntDict()
        self.discardedTiles = IntDict(self.visibleTiles) # tile names are always lowercase
        self.dangerousTiles = list()
        self.csvTags = []
        self._setGameId()
        self.__useRuleset(ruleset)
        # shift rules taken from the OEMC 2005 rules
        # 2nd round: S and W shift, E and N shift
        self.shiftRules = 'SWEN,SE,WE'
        field = Internal.field
        if field:
            field.game = self
            field.startingGame = False
            field.showWall()  # sets self.wall
        else:
            self.wall = Wall(self)
        self.assignPlayers(names)
        if self.belongsToGameServer():
            self.__shufflePlayers()
        self._scanGameOption()
        if self.shouldSave:
            self.saveStartTime()

    def clearHand(self):
        """empty all data"""
        if self.moves:
            for move in self.moves:
                del move
        self.moves = []
        for player in self.players:
            player.clearHand()
        self.__winner = None
        self.__activePlayer = None
        self.prevActivePlayer = None
        Hand.clearCache(self)
        self.dangerousTiles = list()
        self.discardedTiles.clear()
        assert self.visibleTiles.count() == 0

    def _scanGameOption(self):
        """this is only done for PlayingGame"""
        pass

    @property
    def lastDiscard(self):
        """hide weakref"""
        return self.__lastDiscard
    @lastDiscard.setter
    def lastDiscard(self, value):
        """hide weakref"""
        self.__lastDiscard = value
        if value is not None:
            assert isinstance(value, Tile), value
            if not value.istitle():
                raise Exception('lastDiscard is lower:%s' % value)

    @property
    def client(self):
        """hide weakref"""
        if self._client is not None:
            return self._client()
    @client.setter
    def client(self, value):
        """hide weakref"""
        if value is None:
            self._client = None
        else:
            self._client = weakref.ref(value)

    @property
    def winner(self):
        """the name of the game server this game is attached to"""
        return self.__winner

    @winner.setter
    def winner(self, value):
        """the name of the game server this game is attached to"""
        if self.__winner != value:
            if self.__winner:
                self.__winner.invalidateHand()
            self.__winner = value
            if value:
                value.invalidateHand()

    def addCsvTag(self, tag, forAllPlayers=False):
        """tag will be written to tag field in csv row"""
        if forAllPlayers or self.belongsToHumanPlayer():
            self.csvTags.append('%s/%s' % (tag, self.handId()))

    def isFirstHand(self):
        """as the name says"""
        return self.roundHandCount == 0 and self.roundsFinished == 0

    def handId(self, withAI=True, withMoveCount=False):
        """identifies the hand for window title and scoring table"""
        aiVariant = ''
        if withAI and self.belongsToHumanPlayer():
            aiName = self.client.intelligence.name()
            if aiName != 'Default':
                aiVariant = aiName + '/'
        num = self.notRotated
        charId = ''
        while num:
            charId = chr(ord('a') + (num-1) % 26) + charId
            num = (num-1) / 26
        if self.finished():
            wind = 'X'
        else:
            wind = WINDS[self.roundsFinished]
        result = '%s%s/%s%s%s' % (aiVariant, self.seed, wind, self.rotated + 1, charId)
        if withMoveCount:
            result += '/moves:%d' % len(self.moves)
        if result != self._currentHandId:
            self._prevHandId = self._currentHandId
            self._currentHandId = result
        return result

    def _setGameId(self):
        """virtual"""
        assert not self # we want it to fail, and quiten pylint

    def close(self):
        """log off from the server and return a Deferred"""
        self.wall = None
        self.lastDiscard = None
        if self.client:
            client = self.client
            self.client = None
            result = client.logout()
            client.delete()
        else:
            result = succeed(None)
        return result

    def playerByName(self, playerName):
        """return None or the matching player"""
        if playerName is None:
            return None
        for myPlayer in self.players:
            if myPlayer.name == playerName:
                return myPlayer
        logException('Move references unknown player %s' % playerName)

    def losers(self):
        """the 3 or 4 losers: All players without the winner"""
        return list([x for x in self.players if x is not self.__winner])

    def belongsToRobotPlayer(self):
        """does this game instance belong to a robot player?"""
        return self.client and self.client.isRobotClient()

    def belongsToHumanPlayer(self):
        """does this game instance belong to a human player?"""
        return self.client and self.client.isHumanClient()

    def belongsToGameServer(self):
        """does this game instance belong to the game server?"""
        return self.client and self.client.isServerClient()

    @staticmethod
    def isScoringGame():
        """are we scoring a manual game?"""
        return False

    def belongsToPlayer(self):
        """does this game instance belong to a player (as opposed to the game server)?"""
        return self.belongsToRobotPlayer() or self.belongsToHumanPlayer()

    def assignPlayers(self, playerNames):
        """the server tells us the seating order and player names"""
        pairs = []
        for idx, pair in enumerate(playerNames):
            if isinstance(pair, basestring):
                wind, name = WINDS[idx], pair
            else:
                wind, name = pair
            pairs.append((wind, name))

        if not self.players:
            for _ in range(4):
                self.players.append(self.playerClass(self))
            for idx, pair in enumerate(pairs):
                wind, name = pair
                player = self.players[idx]
                Players.createIfUnknown(name)
                player.wind = wind
                player.name = name
        else:
            for idx, pair in enumerate(playerNames):
                wind, name = pair
                self.players.byName(name).wind = wind
        if self.client and self.client.name:
            self.myself = self.players.byName(self.client.name)
        self.sortPlayers()

    def __shufflePlayers(self):
        """assign random seats to the players and assign winds"""
        self.players.sort(key=lambda x:x.name)
        self.randomGenerator.shuffle(self.players)
        for player, wind in zip(self.players, WINDS):
            player.wind = wind

    def __exchangeSeats(self):
        """execute seat exchanges according to the rules"""
        winds = self.shiftRules.split(',')[(self.roundsFinished-1) % 4]
        players = list(self.players[x] for x in winds)
        pairs = list(players[x:x+2] for x in range(0, len(winds), 2))
        for playerA, playerB in self._mustExchangeSeats(pairs):
            playerA.wind, playerB.wind = playerB.wind, playerA.wind
        self.sortPlayers()

    def _mustExchangeSeats(self, pairs):
        """filter: which player pairs should really swap places?"""
        # pylint: disable=no-self-use
        return pairs

    def sortPlayers(self):
        """sort by wind order. Place ourself at bottom (idx=0)"""
        self.players.sort(key=lambda x: 'ESWN'.index(x.wind))
        self.activePlayer = self.players['E']
        if Internal.field:
            if self.belongsToHumanPlayer():
                while self.players[0] != self.myself:
                    self.players = Players(self.players[1:] + self.players[:1])
                for idx, player in enumerate(self.players):
                    player.front = self.wall[idx]

    @staticmethod
    def _newGameId():
        """write a new entry in the game table
        and returns the game id of that new entry"""
        with Transaction():
            query = Query("insert into game(seed) values(0)")
            gameid, gameidOK = query.query.lastInsertId().toInt()
        assert gameidOK
        return gameid

    def saveStartTime(self):
        """save starttime for this game"""
        starttime = datetime.datetime.now().replace(microsecond=0).isoformat()
        args = list([starttime, self.seed, int(self.autoPlay), self.ruleset.rulesetId])
        args.extend([p.nameid for p in self.players])
        args.append(self.gameid)
        Query("update game set starttime=?,seed=?,autoplay=?," \
                "ruleset=?,p0=?,p1=?,p2=?,p3=? where id=?", args)

    def __useRuleset(self, ruleset):
        """use a copy of ruleset for this game, reusing an existing copy"""
        self.ruleset = ruleset
        self.ruleset.load()
        query = Query('select id from ruleset where id>0 and hash="%s"' % \
            self.ruleset.hash)
        if query.records:
            # reuse that ruleset
            self.ruleset.rulesetId = query.records[0][0]
        else:
            # generate a new ruleset
            self.ruleset.save(copy=True, minus=False)

    def _setHandSeed(self):
        """set seed to a reproducable value, independent of what happend
        in previous hands/rounds.
        This makes it easier to reproduce game situations
        in later hands without having to exactly replay all previous hands"""
        if self.seed is not None:
            seedFactor = (self.roundsFinished + 1) * 10000 + self.rotated * 1000 + self.notRotated * 100
            self.randomGenerator.seed(self.seed * seedFactor)

    def prepareHand(self):
        """prepare a game hand"""
        self.clearHand()

    def initHand(self):
        """directly before starting"""
        Hand.clearCache(self)
        self.dangerousTiles = list()
        self.discardedTiles.clear()
        assert self.visibleTiles.count() == 0
        if Internal.field:
            Internal.field.prepareHand()
        self._setHandSeed()

    def saveHand(self):
        """save hand to database, update score table and balance in status line"""
        self.__payHand()
        self._saveScores()
        self.handctr += 1
        self.notRotated += 1
        self.roundHandCount += 1
        self.handDiscardCount = 0

    def _saveScores(self):
        """save computed values to database, update score table and balance in status line"""
        scoretime = datetime.datetime.now().replace(microsecond=0).isoformat()
        for player in self.players:
            if player.hand:
                manualrules = '||'.join(x.rule.name for x in player.hand.usedRules)
            else:
                manualrules = m18n('Score computed manually')
            Query("INSERT INTO SCORE "
                "(game,hand,data,manualrules,player,scoretime,won,prevailing,wind,"
                "points,payments, balance,rotated,notrotated) "
                "VALUES(%d,%d,?,?,%d,'%s',%d,'%s','%s',%d,%d,%d,%d,%d)" % \
                (self.gameid, self.handctr, player.nameid,
                    scoretime, int(player == self.__winner),
                    WINDS[self.roundsFinished % 4], player.wind, player.handTotal,
                    player.payment, player.balance, self.rotated, self.notRotated),
                list([player.hand.string, manualrules]))
            if Debug.scores:
                self.debug('%s: handTotal=%s balance=%s %s' % (
                    player,
                    player.handTotal, player.balance, 'won' if player == self.winner else ''))
            for usedRule in player.hand.usedRules:
                rule = usedRule.rule
                if rule.score.limits:
                    tag = rule.function.__class__.__name__
                    if hasattr(rule.function, 'limitHand'):
                        tag = rule.function.limitHand.__class__.__name__
                    self.addCsvTag(tag)

    def maybeRotateWinds(self):
        """rules which make winds rotate"""
        result = list(x for x in self.ruleset.filterFunctions('rotate') if x.rotate(self))
        if result:
            if Debug.explain:
                if not self.belongsToRobotPlayer():
                    self.debug(result, prevHandId=True)
            self.rotateWinds()
        return bool(result)

    def rotateWinds(self):
        """rotate winds, exchange seats. If finished, update database"""
        self.rotated += 1
        self.notRotated = 0
        if self.rotated == 4:
            if not self.finished():
                self.roundsFinished += 1
            self.rotated = 0
            self.roundHandCount = 0
        if self.finished():
            endtime = datetime.datetime.now().replace(microsecond=0).isoformat()
            with Transaction():
                Query('UPDATE game set endtime = "%s" where id = %d' % \
                    (endtime, self.gameid))
        elif not self.belongsToPlayer():
            # the game server already told us the new placement and winds
            winds = [player.wind for player in self.players]
            winds = winds[3:] + winds[0:3]
            for idx, newWind in enumerate(winds):
                self.players[idx].wind = newWind
            if self.roundsFinished % 4 and self.rotated == 0:
                # exchange seats between rounds
                self.__exchangeSeats()

    def debug(self, msg, btIndent=None, prevHandId=False):
        """prepend game id"""
        if self.belongsToRobotPlayer():
            prefix = 'R'
        elif self.belongsToHumanPlayer():
            prefix = 'C'
        elif self.belongsToGameServer():
            prefix = 'S'
        else:
            logDebug(msg, btIndent=btIndent)
            return
        logDebug('%s%s: %s' % (prefix, self._prevHandId if prevHandId else self.handId(), msg),
            withGamePrefix=False, btIndent=btIndent)

    @staticmethod
    def __getNames(record):
        """get name ids from record
        and return the names"""
        names = []
        for idx in range(4):
            nameid = record[idx]
            try:
                name = Players.allNames[nameid]
            except KeyError:
                name = m18n('Player %1 not known', nameid)
            names.append(name)
        return names

    @classmethod
    def loadFromDB(cls, gameid, client=None):
        """load game by game id and return a new Game instance"""
        Internal.logPrefix = 'S' if Internal.isServer else 'C'
        qGame = Query("select p0,p1,p2,p3,ruleset,seed from game where id = %d" % gameid)
        if not qGame.records:
            return None
        rulesetId = qGame.records[0][4] or 1
        ruleset = Ruleset.cached(rulesetId)
        Players.load() # we want to make sure we have the current definitions
        game = cls(Game.__getNames(qGame.records[0]), ruleset, gameid=gameid,
                client=client, wantedGame=qGame.records[0][5])
        qLastHand = Query("select hand,rotated from score where game=%d and hand="
            "(select max(hand) from score where game=%d)" % (gameid, gameid))
        if qLastHand.records:
            (game.handctr, game.rotated) = qLastHand.records[0]

        qScores = Query("select player, wind, balance, won, prevailing from score "
            "where game=%d and hand=%d" % (gameid, game.handctr))
        # default value. If the server saved a score entry but our client did not,
        # we get no record here. Should we try to fix this or exclude such a game from
        # the list of resumable games?
        prevailing = 'E'
        for record in qScores.records:
            playerid = record[0]
            wind = str(record[1])
            player = game.players.byId(playerid)
            if not player:
                logError(
                'game %d inconsistent: player %d missing in game table' % \
                    (gameid, playerid))
            else:
                player.getsPayment(record[2])
                player.wind = wind
            if record[3]:
                game.winner = player
            prevailing = record[4]
        game.roundsFinished = WINDS.index(prevailing)
        game.handctr += 1
        game.notRotated += 1
        game.maybeRotateWinds()
        game.sortPlayers()
        game.wall.decorate()
        return game

    def finished(self):
        """The game is over after minRounds completed rounds"""
        if self.ruleset:
            # while initialising Game, ruleset might be None
            return self.roundsFinished >= self.ruleset.minRounds

    def __payHand(self):
        """pay the scores"""
        # pylint: disable=too-many-branches
        # too many branches
        winner = self.__winner
        if winner:
            winner.wonCount += 1
            guilty = winner.usedDangerousFrom
            if guilty:
                payAction = self.ruleset.findUniqueOption('payforall')
            if guilty and payAction:
                if Debug.dangerousGame:
                    self.debug('%s: winner %s. %s pays for all' % \
                                (self.handId(), winner, guilty))
                guilty.hand.usedRules.append((payAction, None))
                score = winner.handTotal
                score = score * 6 if winner.wind == 'E' else score * 4
                guilty.getsPayment(-score)
                winner.getsPayment(score)
                return

        for player1 in self.players:
            if Debug.explain:
                if not self.belongsToRobotPlayer():
                    self.debug('%s: %s' % (player1, player1.hand.string))
                    for line in player1.hand.explain():
                        self.debug('   %s' % (line))
            for player2 in self.players:
                if id(player1) != id(player2):
                    if player1.wind == 'E' or player2.wind == 'E':
                        efactor = 2
                    else:
                        efactor = 1
                    if player2 != winner:
                        player1.getsPayment(player1.handTotal * efactor)
                    if player1 != winner:
                        player1.getsPayment(-player2.handTotal * efactor)

    def lastMoves(self, only=None, without=None, withoutNotifications=False):
        """filters and yields the moves in reversed order"""
        for idx in range(len(self.moves)-1, -1, -1):
            move = self.moves[idx]
            if withoutNotifications and move.notifying:
                continue
            if only:
                if move.message in only:
                    yield move
            elif without:
                if move.message not in without:
                    yield move
            else:
                yield move

    def throwDices(self):
        """sets random living and kongBox
        sets divideAt: an index for the wall break"""
        if self.belongsToGameServer():
            self.wall.tiles.sort(key=tileKey)
            self.randomGenerator.shuffle(self.wall.tiles)
        breakWall = self.randomGenerator.randrange(4)
        sideLength = len(self.wall.tiles) // 4
        # use the sum of four dices to find the divide
        self.divideAt = breakWall * sideLength + \
            sum(self.randomGenerator.randrange(1, 7) for idx in range(4))
        if self.divideAt % 2 == 1:
            self.divideAt -= 1
        self.divideAt %= len(self.wall.tiles)

class PlayingGame(Game):
    """this game is played using the computer"""
    # pylint: disable=too-many-arguments,too-many-public-methods,too-many-instance-attributes
    playerClass = PlayingPlayer

    def __init__(self, names, ruleset, gameid=None, wantedGame=None, shouldSave=True, \
            client=None, playOpen=False, autoPlay=False):
        """a new game instance, comes from database if gameid is set"""
        self.__activePlayer = None
        self.prevActivePlayer = None
        self.defaultNameBrush = None
        Game.__init__(self, names, ruleset, gameid,
            wantedGame=wantedGame, shouldSave=shouldSave, client=client)
        self.playOpen = playOpen
        self.autoPlay = autoPlay
        myself = self.myself
        if self.belongsToHumanPlayer() and myself:
            myself.voice = Voice.locate(myself.name)
            if myself.voice:
                if Debug.sound:
                    logDebug('myself %s gets voice %s' % (myself.name, myself.voice))
            else:
                if Debug.sound:
                    logDebug('myself %s gets no voice'% (myself.name))

    def close(self):
        """log off from the server and return a Deferred"""
        Internal.autoPlay = False # do that only for the first game
        return Game.close(self)

    def _setGameId(self):
        """do nothing, we already went through the game id reservation"""
        pass

    @property
    def activePlayer(self):
        """the turn is on this player"""
        return self.__activePlayer

    @activePlayer.setter
    def activePlayer(self, player):
        """the turn is on this player"""
        if self.__activePlayer != player:
            self.prevActivePlayer = self.__activePlayer
            if self.prevActivePlayer:
                self.prevActivePlayer.hidePopup()
            self.__activePlayer = player
            if Internal.field: # mark the name of the active player in blue
                for player in self.players:
                    player.colorizeName()

    def prepareHand(self):
        """prepares the next hand"""
        Game.prepareHand(self)
        if self.finished():
            self.close()
        else:
            self.sortPlayers()
            self.hidePopups()
            self._setHandSeed()
            self.wall.build()

    def hidePopups(self):
        """hide all popup messages"""
        for player in self.players:
            player.hidePopup()

    def saveStartTime(self):
        """write a new entry in the game table with the selected players"""
        if not self.gameid:
            # in server.__prepareNewGame, gameid is None here
            return
        records = Query("select seed from game where id=?", list([self.gameid])).records
        assert records, 'self.gameid: %s' % self.gameid
        seed = records[0][0]

        if not Internal.isServer and self.client:
            host = self.client.connection.url
        else:
            host = None

        if seed == 'proposed' or seed == host:
            # we reserved the game id by writing a record with seed == host
            Game.saveStartTime(self)

    def _saveScores(self):
        """save computed values to database, update score table and balance in status line"""
        if self.shouldSave:
            if self.belongsToRobotPlayer():
                assert False, 'shouldSave must not be True for robot player'
            Game._saveScores(self)

    def nextPlayer(self, current=None):
        """returns the player after current or after activePlayer"""
        if not current:
            current = self.activePlayer
        pIdx = self.players.index(current)
        return self.players[(pIdx + 1) % 4]

    def nextTurn(self):
        """move activePlayer"""
        self.activePlayer = self.nextPlayer()

    def initialDeal(self):
        """Happens only on server: every player gets 13 tiles (including east)"""
        self.throwDices()
        self.wall.divide()
        for player in self.players:
            player.clearHand()
            # 13 tiles at least, with names as given by wall
            player.addConcealedTiles(self.wall.deal([None] * 13))
            # compensate boni
            while len(player.concealedTileNames) != 13:
                player.addConcealedTiles(self.wall.deal())

    def __concealedTileName(self, tileName):
        """tileName has been discarded, by which name did we know it?"""
        player = self.activePlayer
        if self.myself and player != self.myself and not self.playOpen:
            # we are human and server tells us another player discarded a tile. In our
            # game instance, tiles in handBoards of other players are unknown
            player.makeTileKnown(tileName)
            result = 'Xy'
        else:
            result = tileName
        if not tileName in player.concealedTileNames:
            raise Exception('I am %s. Player %s is told to show discard of tile %s but does not have it, he has %s' % \
                           (self.myself.name if self.myself else 'None',
                            player.name, result, player.concealedTileNames))
        return result

    def hasDiscarded(self, player, tileName):
        """discards a tile from a player board"""
        # pylint: disable=too-many-branches
        # too many branches
        assert isinstance(tileName, Tile)
        if player != self.activePlayer:
            raise Exception('Player %s discards but %s is active' % (player, self.activePlayer))
        self.discardedTiles[tileName.lower()] += 1
        player.discarded.append(tileName)
        self.__concealedTileName(tileName) # has side effect, needs to be called
        if Internal.field:
            player.handBoard.discard(tileName)
        self.lastDiscard = Tile(tileName)
        player.removeTile(self.lastDiscard)
        if any(tileName.lower() in x[0] for x in self.dangerousTiles):
            self.computeDangerous()
        else:
            self._endWallDangerous()
        self.handDiscardCount += 1

    def checkTarget(self):
        """check if we reached the point defined by --game.
        If we did, disable autoPlay"""
        parts = self.wantedGame.split('/')
        if len(parts) > 1:
            discardCount = int(parts[2]) if len(parts) > 2 else 0
            if self.handId().split('/')[-1] == parts[1] \
               and self.handDiscardCount >= int(discardCount):
                self.autoPlay = False
                self.wantedGame = parts[0] # --game has been processed
                if Internal.field: # mark the name of the active player in blue
                    Internal.field.actionAutoPlay.setChecked(False)

    def saveHand(self):
        """server told us to save this hand"""
        for player in self.players:
            assert player.hand.won == (player == self.winner)
        Game.saveHand(self)

    def _mustExchangeSeats(self, pairs):
        """filter: which player pairs should really swap places?"""
        if self.belongsToPlayer():
            # if we are a client in a remote game, the server swaps and tells us the new places
            return []
        else:
            return pairs

    def _scanGameOption(self):
        """scan the --game option and go to start of wanted hand"""
        if not '/' in self.wantedGame:
            return
        part = self.wantedGame.split('/')[1]
        if part[0] not in 'ESWN':
            logException('--game option with / must specify the round wind')
        roundsFinished = 'ESWN'.index(part[0])
        if roundsFinished > self.ruleset.minRounds:
            logWarning('Ruleset %s has %d minimum rounds but you want round %d(%s)' % (
                self.ruleset.name, self.ruleset.minRounds, roundsFinished + 1, part[0]))
            return self.ruleset.minRounds, 0
        rotations = int(part[1]) - 1
        notRotated = 0
        if rotations > 3:
            logWarning('You want %d rotations, reducing to maximum of 3' % rotations)
            return roundsFinished, 3, 0
        for char in part[2:]:
            if char < 'a':
                logWarning('you want %s, changed to a' % char)
                char = 'a'
            if char > 'z':
                logWarning('you want %s, changed to z' % char)
                char = 'z'
            notRotated = notRotated * 26 + ord(char) - ord('a') + 1
        for _ in range(roundsFinished * 4 + rotations):
            self.rotateWinds()
        self.notRotated = notRotated

    def assignVoices(self):
        """now we have all remote user voices"""
        assert self.belongsToHumanPlayer()
        available = Voice.availableVoices()[:]
        # available is without transferred human voices
        for player in self.players:
            if player.voice and player.voice.oggFiles():
                # remote human player sent her voice, or we are human and have a voice
                if Debug.sound and player != self.myself:
                    logDebug('%s got voice from opponent: %s' % (player.name, player.voice))
            else:
                player.voice = Voice.locate(player.name)
                if player.voice:
                    if Debug.sound:
                        logDebug('%s has own local voice %s' % (player.name, player.voice))
            if player.voice:
                for voice in Voice.availableVoices():
                    if voice in available and voice.md5sum == player.voice.md5sum:
                        # if the local voice is also predefined,
                        # make sure we do not use both
                        available.remove(voice)
        # for the other players use predefined voices in preferred language. Only if
        # we do not have enough predefined voices, look again in locally defined voices
        predefined = [x for x in available if x.language() != 'local']
        predefined.extend(available)
        for player in self.players:
            if player.voice is None and predefined:
                player.voice = predefined.pop(0)
                if Debug.sound:
                    logDebug('%s gets one of the still available voices %s' % (player.name, player.voice))

    def dangerousFor(self, forPlayer, tile):
        """returns a list of explaining texts if discarding tile
        would be Dangerous game for forPlayer. One text for each
        reason - there might be more than one"""
        assert isinstance(tile, Tile), tile
        tile = tile.lower()
        result = []
        for dang, txt in self.dangerousTiles:
            if tile in dang:
                result.append(txt)
        for player in forPlayer.others():
            for dang, txt in player.dangerousTiles:
                if tile in dang:
                    result.append(txt)
        return result

    def computeDangerous(self, playerChanged=None):
        """recompute gamewide dangerous tiles. Either for playerChanged or for all players"""
        self.dangerousTiles = list()
        if playerChanged:
            playerChanged.findDangerousTiles()
        else:
            for player in self.players:
                player.findDangerousTiles()
        self._endWallDangerous()

    def _endWallDangerous(self):
        """if end of living wall is reached, declare all invisible tiles as dangerous"""
        if len(self.wall.living) <=5:
            allTiles = [x for x in defaultdict.keys(elements.occurrence) if not x.isBonus()]
            for tile in allTiles:
                assert isinstance(tile, Tile), tile
            # see http://www.logilab.org/ticket/23986
            invisibleTiles = set(x for x in allTiles if x not in self.visibleTiles)
            msg = m18n('Short living wall: Tile is invisible, hence dangerous')
            self.dangerousTiles = list(x for x in self.dangerousTiles if x[1] != msg)
            self.dangerousTiles.append((invisibleTiles, msg))

    def appendMove(self, player, command, kwargs):
        """append a Move object to self.moves"""
        self.moves.append(Move(player, command, kwargs))
