class Game(object): """the game without GUI""" # pylint: disable=R0902 # pylint we need more than 10 instance attributes 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=R0915 # pylint we need more than 50 statements 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() if not self.isScoringGame() and '/' in self.wantedGame: roundsFinished, rotations, notRotated = self.__scanGameOption(self.wantedGame) for _ in range(roundsFinished * 4 + rotations): self.rotateWinds() self.notRotated = notRotated if self.shouldSave: self.saveNewGame() if field: self.__initVisiblePlayers() field.updateGUI() self.wall.decorate() @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) def __scanGameOption(self, wanted): """scan the --game option. Return roundsFinished, rotations, notRotated""" part = wanted.split('/')[1] 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 return roundsFinished, rotations, notRotated @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""" Internal.autoPlay = False # do that only for the first game self.__hideGame() if self.client: client = self.client self.client = None result = client.logout() client.delete() else: result = succeed(None) return result def __hideGame(self): """remove all visible traces of the current game""" field = Internal.field if isAlive(field): field.setWindowTitle('Kajongg') if field: field.discardBoard.hide() field.selectorBoard.tiles = [] field.selectorBoard.allSelectorTiles = [] if isAlive(field.centralScene): field.centralScene.removeTiles() field.clientDialog = None for player in self.players: if player.handBoard: player.clearHand() player.handBoard.hide() if self.wall: self.wall.hide() self.wall = None self.lastDiscard = None if field: field.actionAutoPlay.setChecked(False) field.startingGame = False field.game = None field.updateGUI() def __initVisiblePlayers(self): """make players visible""" for idx, player in enumerate(self.players): player.front = self.wall[idx] player.clearHand() player.handBoard.setVisible(True) scoring = self.isScoringGame() player.handBoard.setEnabled(scoring or \ (self.belongsToHumanPlayer() and player == self.myself)) player.handBoard.showMoveHelper(scoring) Internal.field.adjustView() def setConcealedTiles(self, allPlayerTiles): """when starting the hand. tiles is one string""" with Animated(False): for playerName, tileNames in allPlayerTiles: player = self.playerByName(playerName) player.addConcealedTiles(self.wall.deal(tileNames)) 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)) field = Internal.field if not self.players: if field: self.players = field.genPlayers() else: self.players = Players([Player(self) for idx in range(4)]) 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 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 __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""" windPairs = self.shiftRules.split(',')[(self.roundsFinished-1) % 4] while len(windPairs): windPair = windPairs[0:2] windPairs = windPairs[2:] swappers = list(self.players[windPair[x]] for x in (0, 1)) if self.belongsToPlayer(): # we are a client in a remote game, the server swaps and tells us the new places shouldSwap = False elif self.isScoringGame(): # we play a manual game and do only the scoring shouldSwap = Internal.field.askSwap(swappers) else: # we are the game server. Always swap in remote games. # do not do assert self.belongsToGameServer() here because # self.client might not yet be set - this code is called for all # suspended games but self.client is assigned later shouldSwap = True if shouldSwap: swappers[0].wind, swappers[1].wind = swappers[1].wind, swappers[0].wind self.sortPlayers() def sortPlayers(self): """sort by wind order. If we are in a remote game, place ourself at bottom (idx=0)""" players = self.players if Internal.field: fieldAttributes = list([(p.handBoard, p.front) for p in players]) players.sort(key=lambda x: 'ESWN'.index(x.wind)) if self.belongsToHumanPlayer(): myName = self.myself.name while players[0].name != myName: values0 = players[0].values for idx in range(4, 0, -1): this, prev = players[idx % 4], players[idx - 1] this.values = prev.values players[1].values = values0 self.myself = players[0] if Internal.field: for idx, player in enumerate(players): player.handBoard, player.front = fieldAttributes[idx] player.handBoard.player = player self.activePlayer = self.players['E'] @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 saveNewGame(self): """write a new entry in the game table with the selected players""" if self.gameid is None: return if not self.isScoringGame(): records = Query("select seed from game where id=?", list([self.gameid])).records assert records if not records: return seed = records[0][0] if not Internal.isServer and self.client: host = self.client.connection.url else: host = None if self.isScoringGame() or seed == 'proposed' or seed == host: # we reserved the game id by writing a record with seed == hostname 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) with Transaction(): Query("update game set starttime=?,seed=?,autoplay=?," \ "ruleset=?,p0=?,p1=?,p2=?,p3=? where id=?", args) if not Internal.isServer: Query('update server set lastruleset=? where url=?', list([self.ruleset.rulesetId, host])) 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 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 prepareHand(self): """prepares the next hand""" self.clearHand() if self.finished(): self.close() else: if not self.isScoringGame(): self.sortPlayers() self.hidePopups() self.__setHandSeed() self.wall.build() 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 hidePopups(self): """hide all popup messages""" for player in self.players: player.hidePopup() 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 __needSave(self): """do we need to save this game?""" if self.isScoringGame(): return True elif self.belongsToRobotPlayer(): return False else: return self.shouldSave # as the server told us def __saveScores(self): """save computed values to database, update score table and balance in status line""" if not self.__needSave(): return 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 savePenalty(self, player, offense, amount): """save computed values to database, update score table and balance in status line""" if not self.__needSave(): return scoretime = datetime.datetime.now().replace(microsecond=0).isoformat() with Transaction(): Query("INSERT INTO SCORE " "(game,penalty,hand,data,manualrules,player,scoretime," "won,prevailing,wind,points,payments, balance,rotated,notrotated) " "VALUES(%d,1,%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, 0, amount, player.balance, self.rotated, self.notRotated), list([player.hand.string, offense.name])) if Internal.field: Internal.field.updateGUI() 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=R0912 # 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) 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""" if isinstance(tile, Tile): tile = tile.element 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 x[0] not in 'fy'] # 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))