Пример #1
0
    def __init__(self, names, ruleset, gameid=None,
                 wantedGame=None, 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'
        for wind, name in names:
            assert isinstance(wind, Wind), 'Game.__init__ expects Wind objects'
            assert isinstance(name, (str, unicode)), 'Game.__init__: name must be string and not {}'.format(type(name))
        self.players = Players()
        # if we fail later on in init, at least we can still close the program
        self.myself = None
        # the player using this client instance for talking to the server
        self.__shouldSave = False
        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.randomGenerator = CountingRandom(self)
        self.wantedGame = wantedGame
        self._setHandSeed()
        self.activePlayer = None
        self.__winner = None
        self.moves = []
        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'
        self.wall = self.wallClass(self)
        self.assignPlayers(names)
        if self.belongsToGameServer():
            self.__shufflePlayers()
        self._scanGameOption()
        for player in self.players:
            player.clearHand()
Пример #2
0
    def __init__(self,
                 names,
                 ruleset,
                 gameid=None,
                 wantedGame=None,
                 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'
        for wind, name in names:
            assert isinstance(wind, Wind), 'Game.__init__ expects Wind objects'
            assert isinstance(
                name,
                str), 'Game.__init__: name must be string and not {}'.format(
                    type(name))
        self.players = Players()
        # if we fail later on in init, at least we can still close the program
        self.myself = None
        # the player using this client instance for talking to the server
        self.__shouldSave = False
        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.wantedGame = wantedGame
        self.moves = []
        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.randomGenerator = CountingRandom(self)
        self._setHandSeed()
        self.activePlayer = None
        self.__winner = None
        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'
        self.wall = self.wallClass(self)
        self.assignPlayers(names)
        if self.belongsToGameServer():
            self.__shufflePlayers()
        self._scanGameOption()
        for player in self.players:
            player.clearHand()
Пример #3
0
class Game:
    """the game without GUI"""
    # pylint: disable=too-many-instance-attributes
    playerClass = Player
    wallClass = Wall

    def __init__(self,
                 names,
                 ruleset,
                 gameid=None,
                 wantedGame=None,
                 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'
        for wind, name in names:
            assert isinstance(wind, Wind), 'Game.__init__ expects Wind objects'
            assert isinstance(
                name,
                str), 'Game.__init__: name must be string and not {}'.format(
                    type(name))
        self.players = Players()
        # if we fail later on in init, at least we can still close the program
        self.myself = None
        # the player using this client instance for talking to the server
        self.__shouldSave = False
        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.wantedGame = wantedGame
        self.moves = []
        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.randomGenerator = CountingRandom(self)
        self._setHandSeed()
        self.activePlayer = None
        self.__winner = None
        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'
        self.wall = self.wallClass(self)
        self.assignPlayers(names)
        if self.belongsToGameServer():
            self.__shufflePlayers()
        self._scanGameOption()
        for player in self.players:
            player.clearHand()

    @property
    def shouldSave(self):
        """as a property"""
        return self.__shouldSave

    @shouldSave.setter
    def shouldSave(self, value):
        """if activated, save start time"""
        if value and not self.__shouldSave:
            self.saveStartTime()
        self.__shouldSave = value

    @property
    def handId(self):
        """current position in game"""
        result = HandId(self)
        if result != self._currentHandId:
            self._prevHandId = self._currentHandId
            self._currentHandId = result
        return result

    @property
    def client(self):
        """hide weakref"""
        if self._client:
            return self._client()

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

    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
        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 value.isExposed:
                raise Exception('lastDiscard is exposed:%s' % value)

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

    @property
    def roundWind(self):
        """the round wind for Hand"""
        return Wind.all[self.roundsFinished % 4]

    @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.prompt(withSeed=False)))

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

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

    def close(self):
        """log off from the server and return a Deferred"""
        self.wall = None
        self.lastDiscard = None

    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.

        @param playerNames: A list of 4 tuples. Each tuple holds wind and name.
        @type playerNames: The tuple contents must be C{str}
        @todo: Can we pass L{Players} instead of that tuple list?
        """
        if not self.players:
            self.players = Players()
            for idx in range(4):
                # append each separately: Until they have names, the current length of players
                # is used to assign one of the four walls to the player
                self.players.append(self.playerClass(self,
                                                     playerNames[idx][1]))
        for wind, name in playerNames:
            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, Wind.all4):
            player.wind = wind

    def __exchangeSeats(self):
        """execute seat exchanges according to the rules"""
        winds = list(
            x
            for x in self.shiftRules.split(',')[(self.roundsFinished - 1) % 4])
        players = list(self.players[Wind(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

    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: x.wind)
        self.activePlayer = self.players[East]
        if Internal.scene:
            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]
                    player.sideText.board = player.front
                # we want names to move simultaneously
                self.players[1].sideText.refreshAll()

    @staticmethod
    def _newGameId():
        """write a new entry in the game table
        and returns the game id of that new entry"""
        return Query("insert into game(seed) values(0)").cursor.lastrowid

    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=?", tuple(args))

    def __useRuleset(self, ruleset):
        """use a copy of ruleset for this game, reusing an existing copy"""
        self.ruleset = ruleset
        self.ruleset.load()
        if Internal.db:
            # only if we have a DB open. False in scoringtest.py
            query = Query('select id from ruleset where id>0 and hash=?',
                          (self.ruleset.hash, ))
            if query.records:
                # reuse that ruleset
                self.ruleset.rulesetId = query.records[0][0]
            else:
                # generate a new ruleset
                self.ruleset.save()

    @property
    def seed(self):  # TODO: move this to PlayingGame
        """extract it from wantedGame. Set wantedGame if empty."""
        if not self.wantedGame:
            self.wantedGame = str(int(self.randomGenerator.random() * 10**9))
        return int(self.wantedGame.split('/')[0])

    def _setHandSeed(self):  # TODO: move this to PlayingGame
        """set seed to a reproducible value, independent of what happened
        in previous hands/rounds.
        This makes it easier to reproduce game situations
        in later hands without having to exactly replay all previous hands"""
        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()
        if self.finished():
            if Options.rounds:
                self.close().addCallback(Internal.mainWindow.close)
            else:
                self.close()

    def initHand(self):
        """directly before starting"""
        self.dangerousTiles = list()
        self.discardedTiles.clear()
        assert self.visibleTiles.count() == 0
        if Internal.scene:
            # TODO: why not self.scene?
            Internal.scene.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()
        logMessage = ''
        for player in self.players:
            if player.hand:
                manualrules = '||'.join(x.rule.name
                                        for x in player.hand.usedRules)
            else:
                manualrules = i18n('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), self.roundWind.char,
                 player.wind, player.handTotal, player.payment, player.balance,
                 self.rotated, self.notRotated),
                (player.hand.string, manualrules))
            logMessage += '{player:<12} {hand:>4} {total:>5} {won} | '.format(
                player=str(player)[:12],
                hand=player.handTotal,
                total=player.balance,
                won='WON' if player == self.winner else '   ')
            for usedRule in player.hand.usedRules:
                rule = usedRule.rule
                if rule.score.limits:
                    self.addCsvTag(rule.name.replace(' ', ''))
        if Debug.scores:
            self.debug(logMessage)

    def maybeRotateWinds(self):
        """rules which make winds rotate"""
        result = list(x for x in self.ruleset.filterRules('rotate')
                      if x.rotate(self))
        if result:
            if Debug.explain:
                if not self.belongsToRobotPlayer():
                    self.debug(','.join(x.name for x in 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:
            self.roundsFinished += 1
            self.rotated = 0
            self.roundHandCount = 0
        if self.finished():
            endtime = datetime.datetime.now().replace(
                microsecond=0).isoformat()
            with Internal.db as transaction:
                transaction.execute(
                    '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()
            if Internal.scene:
                with AnimationSpeed(Speeds.windMarker):
                    self.wall.showWindMarkers()

    def debug(self, msg, btIndent=None, prevHandId=False):
        """
        Log a debug message.

        @param msg: The message.
        @type msg: A string.
        @param btIndent: If given, message is indented by
        depth(backtrace)-btIndent
        @type btIndent: C{int}
        @param prevHandId: If True, do not use current handId but previous
        @type prevHandId: C{bool}
        """
        if self.belongsToRobotPlayer():
            prefix = 'R'
        elif self.belongsToHumanPlayer():
            prefix = 'C'
        elif self.belongsToGameServer():
            prefix = 'S'
        else:
            logDebug(msg, btIndent=btIndent)
            return
        handId = self._prevHandId if prevHandId else self.handId
        handId = handId.prompt(withMoveCount=True)
        logDebug('%s%s: %s' % (prefix, handId, msg),
                 withGamePrefix=False,
                 btIndent=btIndent)

    @staticmethod
    def __getName(playerid):
        """get name for playerid
        """
        try:
            return Players.allNames[playerid]
        except KeyError:
            return i18n('Player %1 not known', playerid)

    @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'
        records = Query(
            "select p0,p1,p2,p3,ruleset,seed from game where id = ?",
            (gameid, )).records
        if not records:
            return None
        qGameRecord = records[0]
        rulesetId = qGameRecord[4] or 1
        ruleset = Ruleset.cached(rulesetId)
        Players.load()  # we want to make sure we have the current definitions
        records = Query(
            "select hand,rotated from score where game=? and hand="
            "(select max(hand) from score where game=?)",
            (gameid, gameid)).records
        if records:
            qLastHandRecord = records[0]
        else:
            qLastHandRecord = tuple([0, 0])
        qScoreRecords = Query(
            "select player, wind, balance, won, prevailing from score "
            "where game=? and hand=?", (gameid, qLastHandRecord[0])).records
        if not qScoreRecords:
            # this should normally not happen
            qScoreRecords = list([
                tuple([qGameRecord[wind], wind.char, 0, False, East.char])
                for wind in Wind.all4
            ])
        if len(qScoreRecords) != 4:
            logError('game %d inconsistent: There should be exactly '
                     '4 score records for the last hand' % gameid)

        # after loading SQL, prepare values.

        # 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?
        if len(set(x[4] for x in qScoreRecords)) != 1:
            logError('game %d inconsistent: All score records for the same '
                     'hand must have the same prevailing wind' % gameid)

        players = list(
            tuple([Wind(x[1]), Game.__getName(x[0])]) for x in qScoreRecords)

        # create the game instance.
        game = cls(players,
                   ruleset,
                   gameid=gameid,
                   client=client,
                   wantedGame=qGameRecord[5])
        game.handctr, game.rotated = qLastHandRecord

        for record in qScoreRecords:
            playerid = record[0]
            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])
            if record[3]:
                game.winner = player
        game.roundsFinished = Wind(qScoreRecords[0][4]).__index__()
        game.handctr += 1
        game.notRotated += 1
        game.maybeRotateWinds()
        game.sortPlayers()
        with AnimationSpeed(Speeds.windMarker):
            animateAndDo(game.wall.decorate4)
        return game

    def finished(self):
        """The game is over after minRounds completed rounds. Also,
        check if we reached the second handId defined by --game.
        If we did, the game is over too"""
        last = HandId(self, self.wantedGame, 1)
        if self.handId > last:
            return True
        if Options.rounds:
            return self.roundsFinished >= Options.rounds
        elif 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 == East 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 == East or player2.wind == East:
                        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"""
        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)
Пример #4
0
class Game(object):

    """the game without GUI"""
    # pylint: disable=too-many-instance-attributes
    playerClass = Player
    wallClass = Wall

    def __init__(self, names, ruleset, gameid=None,
                 wantedGame=None, 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'
        for wind, name in names:
            assert isinstance(wind, Wind), 'Game.__init__ expects Wind objects'
            assert isinstance(name, (str, unicode)), 'Game.__init__: name must be string and not {}'.format(type(name))
        self.players = Players()
        # if we fail later on in init, at least we can still close the program
        self.myself = None
        # the player using this client instance for talking to the server
        self.__shouldSave = False
        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.randomGenerator = CountingRandom(self)
        self.wantedGame = wantedGame
        self._setHandSeed()
        self.activePlayer = None
        self.__winner = None
        self.moves = []
        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'
        self.wall = self.wallClass(self)
        self.assignPlayers(names)
        if self.belongsToGameServer():
            self.__shufflePlayers()
        self._scanGameOption()
        for player in self.players:
            player.clearHand()

    @property
    def shouldSave(self):
        """as a property"""
        return self.__shouldSave

    @shouldSave.setter
    def shouldSave(self, value):
        """if activated, save start time"""
        if value and not self.__shouldSave:
            self.saveStartTime()
        self.__shouldSave = value

    @property
    def handId(self):
        """current position in game"""
        result = HandId(self)
        if result != self._currentHandId:
            self._prevHandId = self._currentHandId
            self._currentHandId = result
        return result

    @property
    def client(self):
        """hide weakref"""
        if self._client:
            return self._client()

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

    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
        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 value.isExposed:
                raise Exception('lastDiscard is exposed:%s' % value)

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

    @property
    def roundWind(self):
        """the round wind for Hand"""
        return Wind.all[self.roundsFinished % 4]

    @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.prompt(withSeed=False)))

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

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

    def close(self):
        """log off from the server and return a Deferred"""
        self.wall = None
        self.lastDiscard = None

    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.

        @param playerNames: A list of 4 tuples. Each tuple holds wind and name.
        @type playerNames: The tuple contents must be C{unicode}
        @todo: Can we pass L{Players} instead of that tuple list?
        """
        if not self.players:
            self.players = Players(self.playerClass(
                self, playerNames[x][1]) for x in range(4))
        for wind, name in playerNames:
            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, Wind.all4):
            player.wind = wind

    def __exchangeSeats(self):
        """execute seat exchanges according to the rules"""
        winds = list(x for x in self.shiftRules.split(',')[(self.roundsFinished - 1) % 4])
        players = list(self.players[Wind(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

    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: x.wind)
        self.activePlayer = self.players[East]
        if Internal.scene:
            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"""
        return Query("insert into game(seed) values(0)").cursor.lastrowid

    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=?", tuple(args))

    def __useRuleset(self, ruleset):
        """use a copy of ruleset for this game, reusing an existing copy"""
        self.ruleset = ruleset
        self.ruleset.load()
        if Internal.db:
            # only if we have a DB open. False in scoringtest.py
            query = Query(
                'select id from ruleset where id>0 and hash=?',
                (self.ruleset.hash,))
            if query.records:
                # reuse that ruleset
                self.ruleset.rulesetId = query.records[0][0]
            else:
                # generate a new ruleset
                self.ruleset.save()

    @property
    def seed(self):  # TODO: move this to PlayingGame
        """extract it from wantedGame. Set wantedGame if empty."""
        if not self.wantedGame:
            self.wantedGame = str(int(self.randomGenerator.random() * 10 ** 9))
        return int(self.wantedGame.split('/')[0])

    def _setHandSeed(self):  # TODO: move this to PlayingGame
        """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"""
        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()
        if self.finished():
            if Options.rounds:
                self.close().addCallback(Internal.mainWindow.close)
            else:
                self.close()

    def initHand(self):
        """directly before starting"""
        self.dangerousTiles = list()
        self.discardedTiles.clear()
        assert self.visibleTiles.count() == 0
        if Internal.scene:
            # TODO: why not self.scene?
            Internal.scene.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()
        logMessage = u''
        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),
                 self.roundWind.char, player.wind,
                 player.handTotal, player.payment, player.balance,
                 self.rotated, self.notRotated),
                (player.hand.string, manualrules))
            logMessage += u'{player:<12} {hand:>4} {total:>5} {won} | '.format(
                player=unicode(player)[:12], hand=player.handTotal,
                total=player.balance,
                won='WON' if player == self.winner else '   ')
            for usedRule in player.hand.usedRules:
                rule = usedRule.rule
                if rule.score.limits:
                    self.addCsvTag(rule.name.replace(' ', ''))
        if Debug.scores:
            self.debug(logMessage)

    def maybeRotateWinds(self):
        """rules which make winds rotate"""
        result = list(x for x in self.ruleset.filterRules('rotate')
                      if x.rotate(self))
        if result:
            if Debug.explain:
                if not self.belongsToRobotPlayer():
                    self.debug(u','.join(x.name for x in 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:
            self.roundsFinished += 1
            self.rotated = 0
            self.roundHandCount = 0
        if self.finished():
            endtime = datetime.datetime.now().replace(
                microsecond=0).isoformat()
            with Internal.db as transaction:
                transaction.execute(
                    '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):
        """
        Log a debug message.

        @param msg: The message.
        @type msg: A string.
        @param btIndent: If given, message is indented by
        depth(backtrace)-btIndent
        @type btIndent: C{int}
        @param prevHandId: If True, do not use current handId but previous
        @type prevHandId: C{bool}
        """
        if self.belongsToRobotPlayer():
            prefix = u'R'
        elif self.belongsToHumanPlayer():
            prefix = u'C'
        elif self.belongsToGameServer():
            prefix = u'S'
        else:
            logDebug(msg, btIndent=btIndent)
            return
        handId = self._prevHandId if prevHandId else self.handId
        handId = unicodeString(handId.prompt(withMoveCount=True))
        logDebug(
            u'%s%s: %s' % (prefix, handId, unicodeString(msg)),
            withGamePrefix=False,
            btIndent=btIndent)

    @staticmethod
    def __getName(playerid):
        """get name for playerid
        """
        try:
            return Players.allNames[playerid]
        except KeyError:
            return m18n('Player %1 not known', playerid)

    @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'
        records = Query(
            "select p0,p1,p2,p3,ruleset,seed from game where id = ?",
            (gameid,)).records
        if not records:
            return None
        qGameRecord = records[0]
        rulesetId = qGameRecord[4] or 1
        ruleset = Ruleset.cached(rulesetId)
        Players.load()  # we want to make sure we have the current definitions
        records = Query(
            "select hand,rotated from score where game=? and hand="
            "(select max(hand) from score where game=?)",
            (gameid, gameid)).records
        if records:
            qLastHandRecord = records[0]
        else:
            qLastHandRecord = tuple([0, 0])
        qScoreRecords = Query(
            "select player, wind, balance, won, prevailing from score "
            "where game=? and hand=?",
            (gameid, qLastHandRecord[0])).records
        if not qScoreRecords:
            # this should normally not happen
            qScoreRecords = list([
                tuple([qGameRecord[wind], wind.char, 0, False, East.char])
                for wind in Wind.all4])
        if len(qScoreRecords) != 4:
            logError(u'game %d inconsistent: There should be exactly '
                     '4 score records for the last hand' % gameid)

        # after loading SQL, prepare values.

        # 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?
        if len(set(x[4] for x in qScoreRecords)) != 1:
            logError(u'game %d inconsistent: All score records for the same '
                     'hand must have the same prevailing wind' % gameid)

        players = list(tuple([Wind(x[1]), Game.__getName(x[0])])
                       for x in qScoreRecords)

        # create the game instance.
        game = cls(players, ruleset, gameid=gameid, client=client,
                   wantedGame=qGameRecord[5])
        game.handctr, game.rotated = qLastHandRecord

        for record in qScoreRecords:
            playerid = record[0]
            player = game.players.byId(playerid)
            if not player:
                logError(
                    u'game %d inconsistent: player %d missing in game table' %
                    (gameid, playerid))
            else:
                player.getsPayment(record[2])
            if record[3]:
                game.winner = player
        game.roundsFinished = Wind(qScoreRecords[0][4]).__index__()
        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. Also,
        check if we reached the second handId defined by --game.
        If we did, the game is over too"""
        last = HandId(self, self.wantedGame, 1)
        if self.handId > last:
            return True
        if Options.rounds:
            return self.roundsFinished >= Options.rounds
        elif 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 == East 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 == East or player2.wind == East:
                        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"""
        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)