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 Player(StrMixin): """ all player related attributes without GUI stuff. concealedTiles: used during the hand for all concealed tiles, ungrouped. concealedMelds: is empty during the hand, will be valid after end of hand, containing the concealed melds as the player presents them. @todo: Now that Player() always calls createIfUnknown, test defining new players and adding new players to server """ # pylint: disable=too-many-instance-attributes,too-many-public-methods def __init__(self, game, name): """ Initialize a player for a give game. @type game: L{Game} or None. @param game: The game this player is part of. May be None. """ if game: self._game = weakref.ref(game) else: self._game = None self.__balance = 0 self.__payment = 0 self.wonCount = 0 self.__name = '' Players.createIfUnknown(name) self.name = name self.wind = East self.intelligence = AIDefault(self) self.visibleTiles = IntDict(game.visibleTiles) if game else IntDict() self.handCache = {} self.cacheHits = 0 self.cacheMisses = 0 self.__lastSource = TileSource.East14th self.clearHand() self.handBoard = None def __lt__(self, other): """Used for sorting""" if not other: return False return self.name < other.name def clearCache(self): """clears the cache with Hands""" if Debug.hand and len(self.handCache): self.game.debug( '%s: cache hits:%d misses:%d' % (self, self.cacheHits, self.cacheMisses)) self.handCache.clear() Permutations.cache.clear() self.cacheHits = 0 self.cacheMisses = 0 @property def name(self): """ The name of the player, can be changed only once. @type: C{str} """ return self.__name @name.setter def name(self, value): """write once""" assert self.__name == '' assert value assert isinstance(value, str), 'Player.name must be str but not {}'.format(type(value)) self.__name = value @property def game(self): """hide the fact that this is a weakref""" if self._game: return self._game() def clearHand(self): """clear player attributes concerning the current hand""" self._concealedTiles = [] self._exposedMelds = [] self._concealedMelds = [] self._bonusTiles = [] self.discarded = [] self.visibleTiles.clear() self.newHandContent = None self.originalCallingHand = None self.__lastTile = None self.lastSource = TileSource.East14th self.lastMeld = Meld() self.__mayWin = True self.__payment = 0 self.originalCall = False self.dangerousTiles = list() self.claimedNoChoice = False self.playedDangerous = False self.usedDangerousFrom = None self.isCalling = False self.clearCache() self._hand = None @property def lastTile(self): """temp for debugging""" return self.__lastTile @lastTile.setter def lastTile(self, value): """temp for debugging""" assert isinstance(value, (Tile, type(None))), value self.__lastTile = value def invalidateHand(self): """some source for the computation of current hand changed""" self._hand = None @property def hand(self): """readonly: the current Hand. Compute if invalidated.""" if not self._hand: self._hand = self.__computeHand() elif Debug.hand: _ = self.__computeHand() assert self._hand == self.__computeHand(), '{} != {}'.format(_, self._hand) return self._hand @property def bonusTiles(self): """a readonly tuple""" return tuple(self._bonusTiles) @property def concealedTiles(self): """a readonly tuple""" return tuple(self._concealedTiles) @property def exposedMelds(self): """a readonly tuple""" return tuple(self._exposedMelds) @property def concealedMelds(self): """a readonly tuple""" return tuple(self._concealedMelds) @property def mayWin(self): """winning possible?""" return self.__mayWin @mayWin.setter def mayWin(self, value): """winning possible?""" if self.__mayWin != value: self.__mayWin = value self._hand = None @property def lastSource(self): """the source of the last tile the player got""" return self.__lastSource @lastSource.setter def lastSource(self, value): """the source of the last tile the player got""" if value is TileSource.LivingWallDiscard and not self.game.wall.living: value = TileSource.LivingWallEndDiscard if value is TileSource.LivingWall and not self.game.wall.living: value = TileSource.LivingWallEnd if self.__lastSource != value: self.__lastSource = value self._hand = None @property def nameid(self): """the name id of this player""" return Players.allIds[self.name] @property def localName(self): """the localized name of this player""" return i18nc('kajongg, name of robot player, to be translated', self.name) @property def handTotal(self): """the hand total of this player for the final scoring""" if not self.game.winner: return 0 else: return self.hand.total() @property def balance(self): """the balance of this player""" return self.__balance @balance.setter def balance(self, balance): """the balance of this player""" self.__balance = balance self.__payment = 0 def getsPayment(self, payment): """make a payment to this player""" self.__balance += payment self.__payment += payment @property def payment(self): """the payments for the current hand""" return self.__payment @payment.setter def payment(self, payment): """the payments for the current hand""" assert payment == 0 self.__payment = 0 def __str__(self): return '{name:<10} {wind}'.format(name=self.name[:10], wind=self.wind) def pickedTile(self, deadEnd, tileName=None): """got a tile from wall""" self.game.activePlayer = self tile = self.game.wall.deal([tileName], deadEnd=deadEnd)[0] if hasattr(tile, 'tile'): self.lastTile = tile.tile else: self.lastTile = tile self.addConcealedTiles([tile]) if deadEnd: self.lastSource = TileSource.DeadWall else: self.game.lastDiscard = None self.lastSource = TileSource.LivingWall return self.lastTile def removeTile(self, tile): """remove from my tiles""" if tile.isBonus: self._bonusTiles.remove(tile) else: try: self._concealedTiles.remove(tile) except ValueError: raise Exception('removeTile(%s): tile not in concealed %s' % (tile, ''.join(self._concealedTiles))) if tile is self.lastTile: self.lastTile = None self._hand = None def addConcealedTiles(self, tiles, animated=False): # pylint: disable=unused-argument """add to my tiles""" assert len(tiles) for tile in tiles: if tile.isBonus: self._bonusTiles.append(tile) else: assert tile.isConcealed, '%s data=%s' % (tile, tiles) self._concealedTiles.append(tile) self._hand = None def syncHandBoard(self, adding=None): """virtual: synchronize display""" pass def colorizeName(self): """virtual: colorize Name on wall""" pass def getsFocus(self, dummyResults=None): """virtual: player gets focus on his hand""" pass def mjString(self): """compile hand info into a string as needed by the scoring engine""" announcements = 'a' if self.originalCall else '' return ''.join(['m', self.lastSource.char, ''.join(announcements)]) def makeTileKnown(self, tileName): """used when somebody else discards a tile""" assert not self._concealedTiles[0].isKnown self._concealedTiles[0] = tileName self._hand = None def __computeHand(self): """returns Hand for this player""" assert not (self._concealedMelds and self._concealedTiles) melds = list() melds.extend(str(x) for x in self._exposedMelds) melds.extend(str(x) for x in self._concealedMelds) if self._concealedTiles: melds.append('R' + ''.join(str(x) for x in sorted(self._concealedTiles))) melds.extend(str(x) for x in self._bonusTiles) melds.append(self.mjString()) if self.lastTile: melds.append( 'L%s%s' % (self.lastTile, self.lastMeld if self.lastMeld else '')) return Hand(self, ' '.join(melds)) def _computeHandWithDiscard(self, discard): """what if""" lastSource = self.lastSource # TODO: recompute save = (self.lastTile, self.lastSource) try: self.lastSource = lastSource if discard: self.lastTile = discard self._concealedTiles.append(discard) return self.__computeHand() finally: self.lastTile, self.lastSource = save if discard: self._concealedTiles = self._concealedTiles[:-1] def scoringString(self): """helper for HandBoard.__str__""" if self._concealedMelds: parts = [str(x) for x in self._concealedMelds + self._exposedMelds] else: parts = [''.join(self._concealedTiles)] parts.extend([str(x) for x in self._exposedMelds]) parts.extend(str(x) for x in self._bonusTiles) return ' '.join(parts) def sortRulesByX(self, rules): # pylint: disable=no-self-use """if this game has a GUI, sort rules by GUI order""" return rules def others(self): """a list of the other 3 players""" return (x for x in self.game.players if x != self) def tileAvailable(self, tileName, hand): """a count of how often tileName might still appear in the game supposing we have hand""" lowerTile = tileName.exposed upperTile = tileName.concealed visible = self.game.discardedTiles.count([lowerTile]) if visible: if hand.lenOffset == 0 and self.game.lastDiscard and lowerTile is self.game.lastDiscard.exposed: # the last discarded one is available to us since we can claim # it visible -= 1 visible += sum(x.visibleTiles.count([lowerTile, upperTile]) for x in self.others()) visible += sum(x.exposed == lowerTile for x in hand.tiles) return 4 - visible def violatesOriginalCall(self, discard=None): """called if discarding discard violates the Original Call""" if not self.originalCall or not self.mayWin: return False if self.lastTile.exposed != discard.exposed: if Debug.originalCall: self.game.debug( '%s would violate OC with %s, lastTile=%s' % (self, discard, self.lastTile)) return True return False
class Player(object): """all player related attributes without GUI stuff. concealedTileNames: used during the hand for all concealed tiles, ungrouped. concealedMelds: is empty during the hand, will be valid after end of hand, containing the concealed melds as the player presents them.""" # pylint: disable=R0902 # pylint we need more than 10 instance attributes # pylint: disable=R0904 # pylint we need more than 40 public methods def __init__(self, game): if game: self._game = weakref.ref(game) else: self._game = None self.__balance = 0 self.__payment = 0 self.wonCount = 0 self.name = '' self.wind = WINDS[0] self.visibleTiles = IntDict(game.visibleTiles) if game else IntDict() self.clearHand() self.__lastSource = '1' # no source: blessing from heaven or earth self.voice = None self.handBoard = None def __del__(self): """break reference cycles""" self.clearHand() @property def game(self): """hide the fact that this is a weakref""" if self._game: return self._game() def speak(self, text): """speak if we have a voice""" pass def clearHand(self): """clear player attributes concerning the current hand""" self.__concealedTileNames = [] self._exposedMelds = [] self._concealedMelds = [] self.__bonusTiles = [] self.discarded = [] self.visibleTiles.clear() self.newHandContent = None self.originalCallingHand = None self.lastTile = None self.lastSource = '1' self.lastMeld = Meld() self.__mayWin = True self.__payment = 0 self.originalCall = False self.dangerousTiles = list() self.claimedNoChoice = False self.playedDangerous = False self.usedDangerousFrom = None self.isCalling = False self.__hand = None def invalidateHand(self): """some source for the computation of current hand changed""" self.__hand = None @property def hand(self): """a readonly tuple""" if not self.__hand: self.__hand = self.computeHand() return self.__hand @property def bonusTiles(self): """a readonly tuple""" return tuple(self.__bonusTiles) @property def concealedTileNames(self): """a readonly tuple""" return tuple(self.__concealedTileNames) @property def exposedMelds(self): """a readonly tuple""" return tuple(self._exposedMelds) @property def concealedMelds(self): """a readonly tuple""" return tuple(self._concealedMelds) @property def mayWin(self): """winning possible?""" return self.__mayWin @mayWin.setter def mayWin(self, value): """winning possible?""" if self.__mayWin != value: self.__mayWin = value self.__hand = None @property def lastSource(self): """the source of the last tile the player got""" return self.__lastSource @lastSource.setter def lastSource(self, lastSource): """the source of the last tile the player got""" self.__lastSource = lastSource if lastSource == 'd' and not self.game.wall.living: self.__lastSource = 'Z' if lastSource == 'w' and not self.game.wall.living: self.__lastSource = 'z' @property def nameid(self): """the name id of this player""" return Players.allIds[self.name] @property def localName(self): """the localized name of this player""" return m18nc('kajongg, name of robot player, to be translated', self.name) @property def handTotal(self): """the hand total of this player""" if not self.game.isScoringGame() and not self.game.winner: return 0 return self.hand.total() @property def balance(self): """the balance of this player""" return self.__balance @balance.setter def balance(self, balance): """the balance of this player""" self.__balance = balance self.__payment = 0 @property def values(self): """the values that are still needed after ending a hand""" return self.name, self.wind, self.balance, self.voice @values.setter def values(self, values): """the values that are still needed after ending a hand""" self.name = values[0] self.wind = values[1] self.balance = values[2] self.voice = values[3] def getsPayment(self, payment): """make a payment to this player""" self.__balance += payment self.__payment += payment @property def payment(self): """the payments for the current hand""" return self.__payment @payment.setter def payment(self, payment): """the payments for the current hand""" assert payment == 0 self.__payment = 0 def __repr__(self): return u'{name:<10} {wind}'.format(name=self.name[:10], wind=self.wind) def __unicode__(self): return u'{name:<10} {wind}'.format(name=self.name[:10], wind=self.wind) def pickedTile(self, deadEnd, tileName=None): """got a tile from wall""" self.game.activePlayer = self tile = self.game.wall.deal([tileName], deadEnd=deadEnd)[0] self.lastTile = tile.element self.addConcealedTiles(tile) if deadEnd: self.lastSource = 'e' else: self.game.lastDiscard = None self.lastSource = 'w' return tile def addConcealedTiles(self, data): """add to my tiles and sync the hand board""" assert isinstance(data, (Tile, list)), data assert not self.game.isScoringGame() if isinstance(data, Tile): data = list([data]) for tile in data: assert isinstance(tile, Tile) tileName = tile.element if tile.isBonus(): self.__bonusTiles.append(tile) else: assert tileName.istitle(), '%s data=%s' % (tile, data) self.__concealedTileNames.append(tileName) self.__hand = None if data: self.syncHandBoard(adding=data) def addMeld(self, meld): """add meld to this hand in a scoring game also used for the Game instance maintained by the server""" if len(meld.tiles) == 1 and meld[0].isBonus(): self.__bonusTiles.append(meld[0]) elif meld.state == CONCEALED and not meld.isKong(): self._concealedMelds.append(meld) else: self._exposedMelds.append(meld) self.__hand = None def remove(self, tile=None, meld=None): """remove from my melds or tiles""" tiles = [tile] if tile else meld.tiles if len(tiles) == 1 and tiles[0].isBonus(): self.__bonusTiles.remove(tiles[0]) self.__hand = None self.syncHandBoard() return if tile: assert not meld, (str(tile), str(meld)) assert not self.game.isScoringGame() tileName = tile.element try: self.__concealedTileNames.remove(tileName) except ValueError: raise Exception('removeTiles(%s): tile not in concealed %s' % \ (tileName, ''.join(self.__concealedTileNames))) else: self.removeMeld(meld) self.__hand = None self.syncHandBoard() def removeMeld(self, meld): """remove a meld from this hand in a scoring game""" assert self.game.isScoringGame() for melds in [self._concealedMelds, self._exposedMelds]: for idx, myTile in enumerate(melds): if id(myTile) == id(meld): melds.pop(idx) self.__hand = None def hasConcealedTiles(self, tileNames, within=None): """do I have those concealed tiles?""" if within is None: within = self.__concealedTileNames within = within[:] for tileName in tileNames: if tileName not in within: return False within.remove(tileName) return True def showConcealedTiles(self, tileNames, show=True): """show or hide tileNames""" if not self.game.playOpen and self != self.game.myself: if not isinstance(tileNames, (list, tuple)): tileNames = [tileNames] assert len(tileNames) <= len(self.__concealedTileNames), \ '%s: showConcealedTiles %s, we have only %s' % (self, tileNames, self.__concealedTileNames) for tileName in tileNames: src, dst = ('Xy', tileName) if show else (tileName, 'Xy') assert src != dst, (self, src, dst, tileNames, self.__concealedTileNames) if not src in self.__concealedTileNames: logException( '%s: showConcealedTiles(%s): %s not in %s.' % \ (self, tileNames, src, self.__concealedTileNames)) idx = self.__concealedTileNames.index(src) self.__concealedTileNames[idx] = dst self.__hand = None self.syncHandBoard() def showConcealedMelds(self, concealedMelds, ignoreDiscard=None): """the server tells how the winner shows and melds his concealed tiles. In case of error, return message and arguments""" for part in concealedMelds.split(): meld = Meld(part) for pair in meld.pairs: if pair == ignoreDiscard: ignoreDiscard = None else: if not pair in self.__concealedTileNames: msg = m18nE('%1 claiming MahJongg: She does not really have tile %2') return msg, self.name, pair self.__concealedTileNames.remove(pair) self.addMeld(meld) if self.__concealedTileNames: msg = m18nE('%1 claiming MahJongg: She did not pass all concealed tiles to the server') return msg, self.name self.__hand = None def hasExposedPungOf(self, tileName): """do I have an exposed Pung of tileName?""" for meld in self._exposedMelds: if meld.pairs == [tileName.lower()] * 3: return True return False def robTile(self, tileName): """used for robbing the kong""" assert tileName.istitle() tileName = tileName.lower() for meld in self._exposedMelds: if tileName in meld.pairs: meld.pairs.remove(tileName) meld.meldtype = PUNG self.visibleTiles[tileName] -= 1 break else: raise Exception('robTile: no meld found with %s' % tileName) if Internal.field: hbTiles = self.handBoard.tiles self.game.lastDiscard = [x for x in hbTiles if x.element == tileName][-1] # remove from board of robbed player, otherwise syncHandBoard would # not fix display for the robbed player self.game.lastDiscard.setBoard(None) self.syncHandBoard() else: self.game.lastDiscard = Tile(tileName) self.game.lastDiscard.element = self.game.lastDiscard.upper() def scoreMatchesServer(self, score): """do we compute the same score as the server does?""" if score is None: return True if 'Xy' in self.__concealedTileNames: return True if str(self.hand) == score: return True self.game.debug('%s localScore:%s' % (self, self.hand)) self.game.debug('%s serverScore:%s' % (self, score)) logWarning('Game %s: client and server disagree about scoring, see logfile for details' % self.game.seed) return False def mustPlayDangerous(self, exposing=None): """returns True if the player has no choice, otherwise False. Exposing may be a meld which will be exposed before we might play dangerous""" if self == self.game.activePlayer and exposing and len(exposing) == 4: # declaring a kong is never dangerous because we get # an unknown replacement return False afterExposed = list(x.lower() for x in self.__concealedTileNames) if exposing: exposing = exposing[:] if self.game.lastDiscard: # if this is about claiming a discarded tile, ignore it # the player who discarded it is responsible exposing.remove(self.game.lastDiscard.element) for tileName in exposing: if tileName.lower() in afterExposed: # the "if" is needed for claimed pung afterExposed.remove(tileName.lower()) return all(self.game.dangerousFor(self, x) for x in afterExposed) def exposeMeld(self, meldTiles, calledTile=None): """exposes a meld with meldTiles: removes them from concealedTileNames, adds the meld to exposedMelds and returns it calledTile: we got the last tile for the meld from discarded, otherwise from the wall""" game = self.game game.activePlayer = self allMeldTiles = meldTiles[:] if calledTile: allMeldTiles.append(calledTile.element if isinstance(calledTile, Tile) else calledTile) if len(allMeldTiles) == 4 and allMeldTiles[0].islower(): tile0 = allMeldTiles[0].lower() # we are adding a 4th tile to an exposed pung self._exposedMelds = [meld for meld in self._exposedMelds if meld.pairs != [tile0] * 3] meld = Meld(tile0 * 4) self.__concealedTileNames.remove(allMeldTiles[3]) self.visibleTiles[tile0] += 1 else: allMeldTiles = sorted(allMeldTiles) # needed for Chow meld = Meld(allMeldTiles) for meldTile in meldTiles: self.__concealedTileNames.remove(meldTile) for meldTile in allMeldTiles: self.visibleTiles[meldTile.lower()] += 1 meld.expose(bool(calledTile)) self._exposedMelds.append(meld) self.__hand = None game.computeDangerous(self) adding = [calledTile] if calledTile else None self.syncHandBoard(adding=adding) return meld def findDangerousTiles(self): """update the list of dangerous tile""" pName = self.localName dangerous = list() expMeldCount = len(self._exposedMelds) if expMeldCount >= 3: if all(x in elements.greenHandTiles for x in self.visibleTiles): dangerous.append((elements.greenHandTiles, m18n('Player %1 has 3 or 4 exposed melds, all are green', pName))) color = defaultdict.keys(self.visibleTiles)[0][0] # see http://www.logilab.org/ticket/23986 assert color.islower(), self.visibleTiles if color in 'sbc': if all(x[0] == color for x in self.visibleTiles): suitTiles = set([color+x for x in '123456789']) if self.visibleTiles.count(suitTiles) >= 9: dangerous.append((suitTiles, m18n('Player %1 may try a True Color Game', pName))) elif all(x[1] in '19' for x in self.visibleTiles): dangerous.append((elements.terminals, m18n('Player %1 may try an All Terminals Game', pName))) if expMeldCount >= 2: windMelds = sum(self.visibleTiles[x] >=3 for x in elements.winds) dragonMelds = sum(self.visibleTiles[x] >=3 for x in elements.dragons) windsDangerous = dragonsDangerous = False if windMelds + dragonMelds == expMeldCount and expMeldCount >= 3: windsDangerous = dragonsDangerous = True windsDangerous = windsDangerous or windMelds >= 3 dragonsDangerous = dragonsDangerous or dragonMelds >= 2 if windsDangerous: dangerous.append((set(x for x in elements.winds if x not in self.visibleTiles), m18n('Player %1 exposed many winds', pName))) if dragonsDangerous: dangerous.append((set(x for x in elements.dragons if x not in self.visibleTiles), m18n('Player %1 exposed many dragons', pName))) self.dangerousTiles = dangerous if dangerous and Debug.dangerousGame: self.game.debug('dangerous:%s' % dangerous) def popupMsg(self, msg): """virtual: show popup on display""" pass def hidePopup(self): """virtual: hide popup on display""" pass def syncHandBoard(self, adding=None): """virtual: synchronize display""" pass def colorizeName(self): """virtual: colorize Name on wall""" pass def getsFocus(self, dummyResults=None): """virtual: player gets focus on his hand""" pass def mjString(self, asWinner=False): """compile hand info into a string as needed by the scoring engine""" game = self.game assert game winds = self.wind.lower() + 'eswn'[game.roundsFinished % 4] wonChar = 'm' lastSource = '' declaration = '' if asWinner or self == game.winner: wonChar = 'M' lastSource = self.lastSource if self.originalCall: declaration = 'a' if not self.mayWin: wonChar = 'x' return ''.join([wonChar, winds, lastSource, declaration]) def makeTileKnown(self, tileName): """used when somebody else discards a tile""" assert self.__concealedTileNames[0] == 'Xy' self.__concealedTileNames[0] = tileName self.__hand = None def computeHand(self, withTile=None, robbedTile=None, dummy=None, asWinner=False): """returns Hand for this player""" assert not (self._concealedMelds and self.__concealedTileNames) assert not isinstance(self.lastTile, Tile) assert not isinstance(withTile, Tile) melds = ['R' + ''.join(self.__concealedTileNames)] if withTile: melds[0] += withTile melds.extend(x.joined for x in self._exposedMelds) melds.extend(x.joined for x in self._concealedMelds) melds.extend(''.join(x.element) for x in self.__bonusTiles) mjString = self.mjString(asWinner) melds.append(mjString) if mjString.startswith('M') and (withTile or self.lastTile): melds.append('L%s%s' % (withTile or self.lastTile, self.lastMeld.joined)) return Hand.cached(self, ' '.join(melds), robbedTile=robbedTile) def computeNewHand(self): """returns the new hand. Same as current unless we need to discard. In that case, make an educated guess about the discard. For player==game.myself, use the focussed tile.""" hand = self.hand if hand and hand.tileNames and self.__concealedTileNames: if hand.lenOffset == 1 and not hand.won: if self == self.game.myself: removeTile = self.handBoard.focusTile.element elif self.lastTile: removeTile = self.lastTile else: removeTile = self.__concealedTileNames[0] assert removeTile[0] not in 'fy', 'hand:%s remove:%s lastTile:%s' % ( hand, removeTile, self.lastTile) hand -= removeTile assert not hand.lenOffset return hand def possibleChows(self, tileName=None, within=None): """returns a unique list of lists with possible claimable chow combinations""" if self.game.lastDiscard is None: return [] exposedChows = [x for x in self._exposedMelds if x.isChow()] if len(exposedChows) >= self.game.ruleset.maxChows: return [] if tileName is None: tileName = self.game.lastDiscard.element if within is None: within = self.__concealedTileNames within = within[:] within.append(tileName) return hasChows(tileName, within) def exposedChows(self): """returns a list of exposed chows""" return [x for x in self._exposedMelds if x.isChow()] def possibleKongs(self): """returns a unique list of lists with possible kong combinations""" kongs = [] if self == self.game.activePlayer: # declaring a kong for tileName in set([x for x in self.__concealedTileNames if x[0] not in 'fy']): if self.__concealedTileNames.count(tileName) == 4: kongs.append([tileName] * 4) elif self.__concealedTileNames.count(tileName) == 1 and \ tileName.lower() * 3 in list(x.joined for x in self._exposedMelds): kongs.append([tileName.lower()] * 3 + [tileName]) if self.game.lastDiscard: # claiming a kong discardName = self.game.lastDiscard.element.capitalize() if self.__concealedTileNames.count(discardName) == 3: kongs.append([discardName] * 4) return kongs def declaredMahJongg(self, concealed, withDiscard, lastTile, lastMeld): """player declared mah jongg. Determine last meld, show concealed tiles grouped to melds""" assert not isinstance(lastTile, Tile) lastMeld = Meld(lastMeld) # do not change the original! self.game.winner = self if withDiscard: self.lastTile = withDiscard self.lastMeld = lastMeld assert withDiscard == self.game.lastDiscard.element, 'withDiscard: %s lastDiscard: %s' % ( withDiscard, self.game.lastDiscard.element) self.addConcealedTiles(self.game.lastDiscard) melds = [Meld(x) for x in concealed.split()] if self.lastSource != 'k': # robbed the kong self.lastSource = 'd' # the last claimed meld is exposed assert lastMeld in melds, '%s: concealed=%s melds=%s lastMeld=%s lastTile=%s withDiscard=%s' % ( self.__concealedTileNames, concealed, meldsContent(melds), ''.join(lastMeld.pairs), lastTile, withDiscard) melds.remove(lastMeld) self.lastTile = self.lastTile.lower() lastMeld.pairs.toLower() self._exposedMelds.append(lastMeld) for tileName in lastMeld.pairs: self.visibleTiles[tileName] += 1 else: melds = [Meld(x) for x in concealed.split()] self.lastTile = lastTile self.lastMeld = lastMeld self._concealedMelds = melds self.__concealedTileNames = [] self.__hand = None self.syncHandBoard() def scoringString(self): """helper for HandBoard.__str__""" if self._concealedMelds: parts = [x.joined for x in self._concealedMelds + self._exposedMelds] else: parts = [''.join(self.__concealedTileNames)] parts.extend([x.joined for x in self._exposedMelds]) parts.extend(''.join(x.element) for x in self.__bonusTiles) return ' '.join(parts) def others(self): """a list of the other 3 players""" return (x for x in self.game.players if x != self) def tileAvailable(self, tileName, hand): """a count of how often tileName might still appear in the game supposing we have hand""" visible = self.game.discardedTiles.count([tileName.lower()]) for player in self.others(): visible += player.visibleTiles.count([tileName.capitalize()]) visible += player.visibleTiles.count([tileName.lower()]) for pair in hand.tileNames: if pair.lower() == tileName.lower(): visible += 1 return 4 - visible def violatesOriginalCall(self, tileName=None): """called if discarding tileName (default=just discarded tile) violates the Original Call""" if not self.originalCall or not self.mayWin: return False if tileName is None: if len(self.discarded) < 2: return False tileName = self.discarded[-1] if self.lastTile.lower() != tileName.lower(): if Debug.originalCall: self.game.debug('%s would violate OC with %s, lastTile=%s' % (self, tileName, self.lastTile)) return True return False
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))
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)
class Player(StrMixin): """ all player related attributes without GUI stuff. concealedTiles: used during the hand for all concealed tiles, ungrouped. concealedMelds: is empty during the hand, will be valid after end of hand, containing the concealed melds as the player presents them. @todo: Now that Player() always calls createIfUnknown, test defining new players and adding new players to server """ # pylint: disable=too-many-instance-attributes,too-many-public-methods def __init__(self, game, name): """ Initialize a player for a give game. @type game: L{Game} or None. @param game: The game this player is part of. May be None. """ if game: self._game = weakref.ref(game) else: self._game = None self.__balance = 0 self.__payment = 0 self.wonCount = 0 self.__name = '' Players.createIfUnknown(name) self.name = name self.wind = East self.intelligence = AIDefault(self) self.visibleTiles = IntDict(game.visibleTiles) if game else IntDict() self.handCache = {} self.cacheHits = 0 self.cacheMisses = 0 self.__lastSource = TileSource.East14th self.clearHand() self.handBoard = None def __lt__(self, other): """Used for sorting""" if not other: return False return self.name < other.name def clearCache(self): """clears the cache with Hands""" if Debug.hand and len(self.handCache): self.game.debug( '%s: cache hits:%d misses:%d' % (self, self.cacheHits, self.cacheMisses)) self.handCache.clear() Permutations.cache.clear() self.cacheHits = 0 self.cacheMisses = 0 @property def name(self): """ The name of the player, can be changed only once. @type: C{unicode} """ return self.__name @name.setter def name(self, value): """write once""" assert self.__name == '' assert value assert isinstance(value, unicode), 'Player.name must be unicode but not {}'.format(type(value)) self.__name = value @property def game(self): """hide the fact that this is a weakref""" if self._game: return self._game() def clearHand(self): """clear player attributes concerning the current hand""" self._concealedTiles = [] self._exposedMelds = [] self._concealedMelds = [] self._bonusTiles = [] self.discarded = [] self.visibleTiles.clear() self.newHandContent = None self.originalCallingHand = None self.__lastTile = None self.lastSource = TileSource.East14th self.lastMeld = Meld() self.__mayWin = True self.__payment = 0 self.originalCall = False self.dangerousTiles = list() self.claimedNoChoice = False self.playedDangerous = False self.usedDangerousFrom = None self.isCalling = False self.clearCache() self._hand = None @property def lastTile(self): """temp for debugging""" return self.__lastTile @lastTile.setter def lastTile(self, value): """temp for debugging""" assert isinstance(value, (Tile, type(None))), value self.__lastTile = value def invalidateHand(self): """some source for the computation of current hand changed""" self._hand = None @property def hand(self): """readonly: the current Hand. Compute if invalidated.""" if not self._hand: self._hand = self.__computeHand() elif Debug.hand: _ = self.__computeHand() assert self._hand == self.__computeHand(), '{} != {}'.format(_, self._hand) return self._hand @property def bonusTiles(self): """a readonly tuple""" return tuple(self._bonusTiles) @property def concealedTiles(self): """a readonly tuple""" return tuple(self._concealedTiles) @property def exposedMelds(self): """a readonly tuple""" return tuple(self._exposedMelds) @property def concealedMelds(self): """a readonly tuple""" return tuple(self._concealedMelds) @property def mayWin(self): """winning possible?""" return self.__mayWin @mayWin.setter def mayWin(self, value): """winning possible?""" if self.__mayWin != value: self.__mayWin = value self._hand = None @property def lastSource(self): """the source of the last tile the player got""" return self.__lastSource @lastSource.setter def lastSource(self, value): """the source of the last tile the player got""" if value is TileSource.LivingWallDiscard and not self.game.wall.living: value = TileSource.LivingWallEndDiscard if value is TileSource.LivingWall and not self.game.wall.living: value = TileSource.LivingWallEnd if self.__lastSource != value: self.__lastSource = value self._hand = None @property def nameid(self): """the name id of this player""" return Players.allIds[self.name] @property def localName(self): """the localized name of this player""" return m18nc('kajongg, name of robot player, to be translated', self.name) @property def handTotal(self): """the hand total of this player for the final scoring""" if not self.game.winner: return 0 else: return self.hand.total() @property def balance(self): """the balance of this player""" return self.__balance @balance.setter def balance(self, balance): """the balance of this player""" self.__balance = balance self.__payment = 0 def getsPayment(self, payment): """make a payment to this player""" self.__balance += payment self.__payment += payment @property def payment(self): """the payments for the current hand""" return self.__payment @payment.setter def payment(self, payment): """the payments for the current hand""" assert payment == 0 self.__payment = 0 def __unicode__(self): return u'{name:<10} {wind}'.format(name=self.name[:10], wind=self.wind) def pickedTile(self, deadEnd, tileName=None): """got a tile from wall""" self.game.activePlayer = self tile = self.game.wall.deal([tileName], deadEnd=deadEnd)[0] if hasattr(tile, 'tile'): self.lastTile = tile.tile else: self.lastTile = tile self.addConcealedTiles([tile]) if deadEnd: self.lastSource = TileSource.DeadWall else: self.game.lastDiscard = None self.lastSource = TileSource.LivingWall return self.lastTile def removeTile(self, tile): """remove from my tiles""" if tile.isBonus: self._bonusTiles.remove(tile) else: try: self._concealedTiles.remove(tile) except ValueError: raise Exception('removeTile(%s): tile not in concealed %s' % (tile, ''.join(self._concealedTiles))) if tile is self.lastTile: self.lastTile = None self._hand = None def addConcealedTiles(self, tiles, animated=False): # pylint: disable=unused-argument """add to my tiles""" assert len(tiles) for tile in tiles: if tile.isBonus: self._bonusTiles.append(tile) else: assert tile.isConcealed, '%s data=%s' % (tile, tiles) self._concealedTiles.append(tile) self._hand = None def syncHandBoard(self, adding=None): """virtual: synchronize display""" pass def colorizeName(self): """virtual: colorize Name on wall""" pass def getsFocus(self, dummyResults=None): """virtual: player gets focus on his hand""" pass def mjString(self): """compile hand info into a string as needed by the scoring engine""" announcements = 'a' if self.originalCall else '' return ''.join(['m', self.lastSource.char, ''.join(announcements)]) def makeTileKnown(self, tileName): """used when somebody else discards a tile""" assert not self._concealedTiles[0].isKnown self._concealedTiles[0] = tileName self._hand = None def __computeHand(self): """returns Hand for this player""" assert not (self._concealedMelds and self._concealedTiles) melds = list() melds.extend(str(x) for x in self._exposedMelds) melds.extend(str(x) for x in self._concealedMelds) if self._concealedTiles: melds.append('R' + ''.join(str(x) for x in sorted(self._concealedTiles))) melds.extend(str(x) for x in self._bonusTiles) melds.append(self.mjString()) if self.lastTile: melds.append( 'L%s%s' % (self.lastTile, self.lastMeld if self.lastMeld else '')) return Hand(self, ' '.join(melds)) def _computeHandWithDiscard(self, discard): """what if""" lastSource = self.lastSource # TODO: recompute save = (self.lastTile, self.lastSource) try: self.lastSource = lastSource if discard: self.lastTile = discard self._concealedTiles.append(discard) return self.__computeHand() finally: self.lastTile, self.lastSource = save if discard: self._concealedTiles = self._concealedTiles[:-1] def scoringString(self): """helper for HandBoard.__str__""" if self._concealedMelds: parts = [str(x) for x in self._concealedMelds + self._exposedMelds] else: parts = [''.join(self._concealedTiles)] parts.extend([str(x) for x in self._exposedMelds]) parts.extend(str(x) for x in self._bonusTiles) return ' '.join(parts) def sortRulesByX(self, rules): # pylint: disable=no-self-use """if this game has a GUI, sort rules by GUI order""" return rules def others(self): """a list of the other 3 players""" return (x for x in self.game.players if x != self) def tileAvailable(self, tileName, hand): """a count of how often tileName might still appear in the game supposing we have hand""" lowerTile = tileName.exposed upperTile = tileName.concealed visible = self.game.discardedTiles.count([lowerTile]) if visible: if hand.lenOffset == 0 and self.game.lastDiscard and lowerTile is self.game.lastDiscard.exposed: # the last discarded one is available to us since we can claim # it visible -= 1 visible += sum(x.visibleTiles.count([lowerTile, upperTile]) for x in self.others()) visible += sum(x.exposed == lowerTile for x in hand.tiles) return 4 - visible def violatesOriginalCall(self, discard=None): """called if discarding discard violates the Original Call""" if not self.originalCall or not self.mayWin: return False if self.lastTile.exposed != discard.exposed: if Debug.originalCall: self.game.debug( '%s would violate OC with %s, lastTile=%s' % (self, discard, self.lastTile)) return True return False