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