File indexing completed on 2024-04-21 04:01:49
0001 # -*- coding: utf-8 -*- 0002 0003 """ 0004 Copyright (C) 2008-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de> 0005 0006 SPDX-License-Identifier: GPL-2.0 0007 0008 """ 0009 0010 import datetime 0011 import weakref 0012 import os 0013 import sys 0014 from collections import defaultdict 0015 from functools import total_ordering 0016 0017 from twisted.internet.defer import succeed 0018 from util import gitHead 0019 from kajcsv import CsvRow 0020 from rand import CountingRandom 0021 from log import logError, logWarning, logException, logDebug, i18n 0022 from common import Internal, IntDict, Debug, Options 0023 from common import StrMixin, Speeds 0024 from wind import Wind, East 0025 from query import Query 0026 from rule import Ruleset 0027 from tile import Tile, elements 0028 from tilesource import TileSource 0029 from sound import Voice 0030 from wall import Wall 0031 from move import Move 0032 from player import Players, Player, PlayingPlayer 0033 from animation import animateAndDo, AnimationSpeed, ParallelAnimationGroup 0034 0035 if os.name != 'nt': 0036 import resource 0037 0038 0039 @total_ordering 0040 class HandId(StrMixin): 0041 0042 """handle a string representing a hand Id""" 0043 0044 def __init__(self, game, string=None, stringIdx=0): 0045 self.game = game 0046 self.seed = game.seed 0047 self.roundsFinished = self.rotated = self.notRotated = 0 0048 self.moveCount = 0 0049 if string is None: 0050 self.roundsFinished = game.roundsFinished 0051 self.rotated = game.rotated 0052 self.notRotated = game.notRotated 0053 self.moveCount = len(game.moves) 0054 else: 0055 self.__scanHandId(string, stringIdx) 0056 assert self.rotated < 4, self 0057 0058 def goto(self): 0059 """advance game to self""" 0060 for _ in range(self.roundsFinished * 4 + self.rotated): 0061 self.game.rotateWinds() 0062 self.game.notRotated = self.notRotated 0063 0064 def __scanHandId(self, string, stringIdx): 0065 """get the --game option. 0066 stringIdx 0 is the part in front of .. 0067 stringIdx 1 is the part after .. 0068 """ 0069 # pylint: disable=too-many-return-statements,too-many-branches 0070 if not string: 0071 return 0072 seed = int(string.split('/')[0]) 0073 assert self.seed is None or self.seed == seed, string 0074 self.seed = seed 0075 if '/' not in string: 0076 if stringIdx == 1: 0077 self.roundsFinished = 100 0078 return 0079 string1 = string.split('/')[1] 0080 if not string1: 0081 logException('--game=%s must specify the wanted round' % string) 0082 parts = string1.split('..') 0083 if len(parts) == 2: 0084 if stringIdx == 0 and parts[0] == '': 0085 return 0086 if stringIdx == 1 and parts[1] == '': 0087 self.roundsFinished = 100 0088 return 0089 handId = parts[min(stringIdx, len(parts) - 1)] 0090 if handId[0].lower() not in 'eswn': 0091 logException('--game=%s must specify the round wind' % string) 0092 handWind = Wind(handId[0]) 0093 ruleset = self.game.ruleset 0094 self.roundsFinished = handWind.__index__() 0095 if self.roundsFinished > ruleset.minRounds: 0096 logWarning( 0097 'Ruleset %s has %d minimum rounds but you want round %d(%s)' 0098 % (ruleset.name, ruleset.minRounds, self.roundsFinished + 1, 0099 handWind)) 0100 self.roundsFinished = ruleset.minRounds 0101 return 0102 self.rotated = int(handId[1]) - 1 0103 if self.rotated > 3: 0104 logWarning( 0105 'You want %d rotations, reducing to maximum of 3' % 0106 self.rotated) 0107 self.rotated = 3 0108 return 0109 for char in handId[2:]: 0110 if char < 'a': 0111 logWarning('you want %s, changed to a' % char) 0112 char = 'a' 0113 if char > 'z': 0114 logWarning('you want %s, changed to z' % char) 0115 char = 'z' 0116 self.notRotated = self.notRotated * 26 + ord(char) - ord('a') + 1 0117 return 0118 0119 def prompt(self, withSeed=True, withAI=True, withMoveCount=False): 0120 """ 0121 Identifies the hand for window title and scoring table. 0122 0123 @param withSeed: If set, include the seed used for the 0124 random generator. 0125 @type withSeed: C{Boolean} 0126 @param withAI: If set and AI != DefaultAI: include AI name for 0127 human players. 0128 @type withAI: C{Boolean} 0129 @param withMoveCount: If set, include the current count of moves. 0130 @type withMoveCount: C{Boolean} 0131 @return: The prompt. 0132 @rtype: C{str} 0133 """ 0134 aiVariant = '' 0135 if withAI and self.game.belongsToHumanPlayer(): 0136 if self.game.myself: 0137 aiName = self.game.myself.intelligence.name() 0138 else: 0139 aiName = 'DefaultAI' 0140 if aiName != 'DefaultAI': 0141 aiVariant = aiName + '/' 0142 num = self.notRotated 0143 assert isinstance(num, int), num 0144 charId = '' 0145 while num: 0146 charId = chr(ord('a') + (num - 1) % 26) + charId 0147 num = (num - 1) // 26 0148 if not charId: 0149 charId = ' ' # align to the most common case 0150 wind = Wind.all4[self.roundsFinished % 4] 0151 if withSeed: 0152 seed = str(self.seed) 0153 else: 0154 seed = '' 0155 delim = '/' if withSeed or withAI else '' 0156 result = '%s%s%s%s%s%s' % ( 0157 aiVariant, seed, delim, wind, self.rotated + 1, charId) 0158 if withMoveCount: 0159 result += '/%3d' % self.moveCount 0160 return result 0161 0162 def token(self): 0163 """server and client use this for checking if they talk about 0164 the same thing""" 0165 return self.prompt(withAI=False) 0166 0167 def __str__(self): 0168 return self.prompt() 0169 0170 def __eq__(self, other): 0171 return (other 0172 and (self.roundsFinished, self.rotated, self.notRotated) == 0173 (other.roundsFinished, other.rotated, other.notRotated)) 0174 0175 def __ne__(self, other): 0176 return not self == other # pylint: disable=unneeded-not 0177 0178 def __lt__(self, other): 0179 return (self.roundsFinished, self.rotated, self.notRotated) < ( 0180 other.roundsFinished, other.rotated, other.notRotated) 0181 0182 0183 class Game: 0184 0185 """the game without GUI""" 0186 # pylint: disable=too-many-instance-attributes 0187 playerClass = Player 0188 wallClass = Wall 0189 0190 def __init__(self, names, ruleset, gameid=None, 0191 wantedGame=None, client=None): 0192 """a new game instance. May be shown on a field, comes from database 0193 if gameid is set. 0194 0195 Game.lastDiscard is the tile last discarded by any player. It is 0196 reset to None when a player gets a tile from the living end of the 0197 wall or after he claimed a discard. 0198 """ 0199 # pylint: disable=too-many-statements 0200 assert self.__class__ != Game, 'Do not directly instantiate Game' 0201 for wind, name in names: 0202 assert isinstance(wind, Wind), 'Game.__init__ expects Wind objects' 0203 assert isinstance(name, str), 'Game.__init__: name must be string and not {}'.format(type(name)) 0204 self.players = Players() 0205 # if we fail later on in init, at least we can still close the program 0206 self.myself = None 0207 # the player using this client instance for talking to the server 0208 self.__shouldSave = False 0209 self._client = None 0210 self.client = client 0211 self.rotated = 0 0212 self.notRotated = 0 # counts hands since last rotation 0213 self.ruleset = None 0214 self.roundsFinished = 0 0215 self._currentHandId = None 0216 self._prevHandId = None 0217 self.wantedGame = wantedGame 0218 self.moves = [] 0219 self.gameid = gameid 0220 self.playOpen = False 0221 self.autoPlay = False 0222 self.handctr = 0 0223 self.roundHandCount = 0 0224 self.handDiscardCount = 0 0225 self.divideAt = None 0226 self.__lastDiscard = None # always uppercase 0227 self.visibleTiles = IntDict() 0228 self.discardedTiles = IntDict(self.visibleTiles) 0229 # tile names are always lowercase 0230 self.dangerousTiles = list() 0231 self.csvTags = [] 0232 self.randomGenerator = CountingRandom(self) 0233 self._setHandSeed() 0234 self.activePlayer = None 0235 self.__winner = None 0236 self._setGameId() 0237 self.__useRuleset(ruleset) 0238 # shift rules taken from the OEMC 2005 rules 0239 # 2nd round: S and W shift, E and N shift 0240 self.shiftRules = 'SWEN,SE,WE' 0241 self.wall = self.wallClass(self) 0242 self.assignPlayers(names) 0243 if self.belongsToGameServer(): 0244 self.__shufflePlayers() 0245 self._scanGameOption() 0246 for player in self.players: 0247 player.clearHand() 0248 0249 @property 0250 def shouldSave(self): 0251 """as a property""" 0252 return self.__shouldSave 0253 0254 @shouldSave.setter 0255 def shouldSave(self, value): 0256 """if activated, save start time""" 0257 if value and not self.__shouldSave: 0258 self.saveStartTime() 0259 self.__shouldSave = value 0260 0261 @property 0262 def handId(self): 0263 """current position in game""" 0264 result = HandId(self) 0265 if result != self._currentHandId: 0266 self._prevHandId = self._currentHandId 0267 self._currentHandId = result 0268 return result 0269 0270 @property 0271 def client(self): 0272 """hide weakref""" 0273 return self._client() if self._client else None 0274 0275 @client.setter 0276 def client(self, value): 0277 """hide weakref""" 0278 if value: 0279 self._client = weakref.ref(value) 0280 else: 0281 self._client = None 0282 0283 def clearHand(self): 0284 """empty all data""" 0285 if self.moves: 0286 for move in self.moves: 0287 del move 0288 self.moves = [] 0289 for player in self.players: 0290 player.clearHand() 0291 self.__winner = None 0292 self.__activePlayer = None 0293 self.prevActivePlayer = None 0294 self.dangerousTiles = list() 0295 self.discardedTiles.clear() 0296 assert self.visibleTiles.count() == 0 0297 0298 def _scanGameOption(self): 0299 """this is only done for PlayingGame""" 0300 0301 @property 0302 def lastDiscard(self): 0303 """hide weakref""" 0304 return self.__lastDiscard 0305 0306 @lastDiscard.setter 0307 def lastDiscard(self, value): 0308 """hide weakref""" 0309 self.__lastDiscard = value 0310 if value is not None: 0311 assert isinstance(value, Tile), value 0312 if value.isExposed: 0313 raise Exception('lastDiscard is exposed:%s' % value) 0314 0315 @property 0316 def winner(self): 0317 """the name of the game server this game is attached to""" 0318 return self.__winner 0319 0320 @property 0321 def roundWind(self): 0322 """the round wind for Hand""" 0323 return Wind.all[self.roundsFinished % 4] 0324 0325 @winner.setter 0326 def winner(self, value): 0327 """the name of the game server this game is attached to""" 0328 if self.__winner != value: 0329 if self.__winner: 0330 self.__winner.invalidateHand() 0331 self.__winner = value 0332 if value: 0333 value.invalidateHand() 0334 0335 def addCsvTag(self, tag, forAllPlayers=False): 0336 """tag will be written to tag field in csv row""" 0337 if forAllPlayers or self.belongsToHumanPlayer(): 0338 self.csvTags.append('%s/%s' % 0339 (tag, self.handId.prompt(withSeed=False))) 0340 0341 def isFirstHand(self): 0342 """as the name says""" 0343 return self.roundHandCount == 0 and self.roundsFinished == 0 0344 0345 def _setGameId(self): 0346 """virtual""" 0347 assert not self # we want it to fail, and quieten pylint 0348 0349 def close(self): 0350 """log off from the server and return a Deferred""" 0351 self.wall = None 0352 self.lastDiscard = None 0353 if Options.gui: 0354 ParallelAnimationGroup.cancelAll() 0355 0356 def playerByName(self, playerName): 0357 """return None or the matching player""" 0358 if playerName is None: 0359 return None 0360 for myPlayer in self.players: 0361 if myPlayer.name == playerName: 0362 return myPlayer 0363 logException('Move references unknown player %s' % playerName) 0364 return None 0365 0366 def losers(self): 0367 """the 3 or 4 losers: All players without the winner""" 0368 return list(x for x in self.players if x is not self.__winner) 0369 0370 def belongsToRobotPlayer(self): 0371 """does this game instance belong to a robot player?""" 0372 return self.client and self.client.isRobotClient() 0373 0374 def belongsToHumanPlayer(self): 0375 """does this game instance belong to a human player?""" 0376 return self.client and self.client.isHumanClient() 0377 0378 def belongsToGameServer(self): 0379 """does this game instance belong to the game server?""" 0380 return self.client and self.client.isServerClient() 0381 0382 @staticmethod 0383 def isScoringGame(): 0384 """are we scoring a manual game?""" 0385 return False 0386 0387 def belongsToPlayer(self): 0388 """does this game instance belong to a player 0389 (as opposed to the game server)?""" 0390 return self.belongsToRobotPlayer() or self.belongsToHumanPlayer() 0391 0392 def assignPlayers(self, playerNames): 0393 """ 0394 The server tells us the seating order and player names. 0395 0396 @param playerNames: A list of 4 tuples. Each tuple holds wind and name. 0397 @type playerNames: The tuple contents must be C{str} 0398 @todo: Can we pass L{Players} instead of that tuple list? 0399 """ 0400 if not self.players: 0401 self.players = Players() 0402 for idx in range(4): 0403 # append each separately: Until they have names, the current length of players 0404 # is used to assign one of the four walls to the player 0405 self.players.append(self.playerClass( 0406 self, playerNames[idx][1])) 0407 for wind, name in playerNames: 0408 self.players.byName(name).wind = wind 0409 if self.client and self.client.name: 0410 self.myself = self.players.byName(self.client.name) 0411 self.sortPlayers() 0412 0413 def __shufflePlayers(self): 0414 """assign random seats to the players and assign winds""" 0415 self.players.sort(key=lambda x: x.name) 0416 self.randomGenerator.shuffle(self.players) 0417 for player, wind in zip(self.players, Wind.all4): 0418 player.wind = wind 0419 0420 def __exchangeSeats(self): 0421 """execute seat exchanges according to the rules""" 0422 winds = list(x for x in self.shiftRules.split(',')[(self.roundsFinished - 1) % 4]) 0423 players = [self.players[Wind(x)] for x in winds] 0424 pairs = [players[x:x + 2] for x in range(0, len(winds), 2)] 0425 for playerA, playerB in self._mustExchangeSeats(pairs): 0426 playerA.wind, playerB.wind = playerB.wind, playerA.wind 0427 0428 def _mustExchangeSeats(self, pairs): 0429 """filter: which player pairs should really swap places?""" 0430 # pylint: disable=no-self-use 0431 return pairs 0432 0433 def sortPlayers(self): 0434 """sort by wind order. Place ourself at bottom (idx=0)""" 0435 self.players.sort(key=lambda x: x.wind) 0436 self.activePlayer = self.players[East] 0437 if Internal.scene: 0438 if self.belongsToHumanPlayer(): 0439 while self.players[0] != self.myself: 0440 self.players = Players(self.players[1:] + self.players[:1]) 0441 for idx, player in enumerate(self.players): 0442 player.front = self.wall[idx] 0443 player.sideText.board = player.front 0444 # we want names to move simultaneously 0445 self.players[1].sideText.refreshAll() 0446 0447 @staticmethod 0448 def _newGameId(): 0449 """write a new entry in the game table 0450 and returns the game id of that new entry""" 0451 return Query("insert into game(seed) values(0)").cursor.lastrowid 0452 0453 def saveStartTime(self): 0454 """save starttime for this game""" 0455 starttime = datetime.datetime.now().replace(microsecond=0).isoformat() 0456 args = list([starttime, self.seed, int(self.autoPlay), 0457 self.ruleset.rulesetId]) 0458 args.extend([p.nameid for p in self.players]) 0459 args.append(self.gameid) 0460 Query("update game set starttime=?,seed=?,autoplay=?," 0461 "ruleset=?,p0=?,p1=?,p2=?,p3=? where id=?", tuple(args)) 0462 0463 def __useRuleset(self, ruleset): 0464 """use a copy of ruleset for this game, reusing an existing copy""" 0465 self.ruleset = ruleset 0466 self.ruleset.load() 0467 if Internal.db: 0468 # only if we have a DB open. False in scoringtest.py 0469 query = Query( 0470 'select id from ruleset where id>0 and hash=?', 0471 (self.ruleset.hash,)) 0472 if query.records: 0473 # reuse that ruleset 0474 self.ruleset.rulesetId = query.records[0][0] 0475 else: 0476 # generate a new ruleset 0477 self.ruleset.save() 0478 0479 @property 0480 def seed(self): # TODO: move this to PlayingGame 0481 """extract it from wantedGame. Set wantedGame if empty.""" 0482 if not self.wantedGame: 0483 self.wantedGame = str(int(self.randomGenerator.random() * 10 ** 9)) 0484 return int(self.wantedGame.split('/')[0]) 0485 0486 def _setHandSeed(self): # TODO: move this to PlayingGame 0487 """set seed to a reproducible value, independent of what happened 0488 in previous hands/rounds. 0489 This makes it easier to reproduce game situations 0490 in later hands without having to exactly replay all previous hands""" 0491 seedFactor = ((self.roundsFinished + 1) * 10000 0492 + self.rotated * 1000 0493 + self.notRotated * 100) 0494 self.randomGenerator.seed(self.seed * seedFactor) 0495 0496 def prepareHand(self): 0497 """prepare a game hand""" 0498 self.clearHand() 0499 if self.finished(): 0500 if Options.rounds: 0501 self.close().addCallback(Internal.mainWindow.close) 0502 else: 0503 self.close() 0504 0505 def initHand(self): 0506 """directly before starting""" 0507 self.dangerousTiles = list() 0508 self.discardedTiles.clear() 0509 assert self.visibleTiles.count() == 0 0510 if Internal.scene: 0511 # TODO: why not self.scene? 0512 Internal.scene.prepareHand() 0513 self._setHandSeed() 0514 0515 def saveHand(self): 0516 """save hand to database, 0517 update score table and balance in status line""" 0518 self.__payHand() 0519 self._saveScores() 0520 self.handctr += 1 0521 self.notRotated += 1 0522 self.roundHandCount += 1 0523 self.handDiscardCount = 0 0524 0525 def _saveScores(self): 0526 """save computed values to database, 0527 update score table and balance in status line""" 0528 scoretime = datetime.datetime.now().replace(microsecond=0).isoformat() 0529 logMessage = '' 0530 for player in self.players: 0531 if player.hand: 0532 manualrules = '||'.join(x.rule.name 0533 for x in player.hand.usedRules) 0534 else: 0535 manualrules = i18n('Score computed manually') 0536 Query( 0537 "INSERT INTO SCORE " 0538 "(game,hand,data,manualrules,player,scoretime,won,prevailing," 0539 "wind,points,payments, balance,rotated,notrotated) " 0540 "VALUES(%d,%d,?,?,%d,'%s',%d,'%s','%s',%d,%d,%d,%d,%d)" % 0541 (self.gameid, self.handctr, player.nameid, 0542 scoretime, int(player == self.__winner), 0543 self.roundWind.char, player.wind, 0544 player.handTotal, player.payment, player.balance, 0545 self.rotated, self.notRotated), 0546 (player.hand.string, manualrules)) 0547 logMessage += '{player:<12} {hand:>4} {total:>5} {won} | '.format( 0548 player=str(player)[:12], hand=player.handTotal, 0549 total=player.balance, 0550 won='WON' if player == self.winner else ' ') 0551 for usedRule in player.hand.usedRules: 0552 rule = usedRule.rule 0553 if rule.score.limits: 0554 self.addCsvTag(rule.name.replace(' ', '')) 0555 if Debug.scores: 0556 self.debug(logMessage) 0557 0558 def maybeRotateWinds(self): 0559 """rules which make winds rotate""" 0560 result = [x for x in self.ruleset.filterRules('rotate') if x.rotate(self)] 0561 if result: 0562 if Debug.explain: 0563 if not self.belongsToRobotPlayer(): 0564 self.debug(','.join(x.name for x in result), prevHandId=True) 0565 self.rotateWinds() 0566 return bool(result) 0567 0568 def rotateWinds(self): 0569 """rotate winds, exchange seats. If finished, update database""" 0570 self.rotated += 1 0571 self.notRotated = 0 0572 if self.rotated == 4: 0573 self.roundsFinished += 1 0574 self.rotated = 0 0575 self.roundHandCount = 0 0576 if self.finished(): 0577 endtime = datetime.datetime.now().replace( 0578 microsecond=0).isoformat() 0579 with Internal.db as transaction: 0580 transaction.execute( 0581 'UPDATE game set endtime = "%s" where id = %d' % 0582 (endtime, self.gameid)) 0583 elif not self.belongsToPlayer(): 0584 # the game server already told us the new placement and winds 0585 winds = [player.wind for player in self.players] 0586 winds = winds[3:] + winds[0:3] 0587 for idx, newWind in enumerate(winds): 0588 self.players[idx].wind = newWind 0589 if self.roundsFinished % 4 and self.rotated == 0: 0590 # exchange seats between rounds 0591 self.__exchangeSeats() 0592 if Internal.scene: 0593 with AnimationSpeed(Speeds.windMarker): 0594 self.wall.showWindMarkers() 0595 0596 def debug(self, msg, btIndent=None, prevHandId=False): 0597 """ 0598 Log a debug message. 0599 0600 @param msg: The message. 0601 @type msg: A string. 0602 @param btIndent: If given, message is indented by 0603 depth(backtrace)-btIndent 0604 @type btIndent: C{int} 0605 @param prevHandId: If True, do not use current handId but previous 0606 @type prevHandId: C{bool} 0607 """ 0608 if self.belongsToRobotPlayer(): 0609 prefix = 'R' 0610 elif self.belongsToHumanPlayer(): 0611 prefix = 'C' 0612 elif self.belongsToGameServer(): 0613 prefix = 'S' 0614 else: 0615 logDebug(msg, btIndent=btIndent) 0616 return 0617 handId = self._prevHandId if prevHandId else self.handId 0618 handId = handId.prompt(withMoveCount=True) 0619 logDebug( 0620 '%s%s: %s' % (prefix, handId, msg), 0621 withGamePrefix=False, 0622 btIndent=btIndent) 0623 0624 @staticmethod 0625 def __getName(playerid): 0626 """get name for playerid 0627 """ 0628 try: 0629 return Players.allNames[playerid] 0630 except KeyError: 0631 return i18n('Player %1 not known', playerid) 0632 0633 @classmethod 0634 def loadFromDB(cls, gameid, client=None): 0635 """load game by game id and return a new Game instance""" 0636 Internal.logPrefix = 'S' if Internal.isServer else 'C' 0637 records = Query( 0638 "select p0,p1,p2,p3,ruleset,seed from game where id = ?", 0639 (gameid,)).records 0640 if not records: 0641 return None 0642 qGameRecord = records[0] 0643 rulesetId = qGameRecord[4] or 1 0644 ruleset = Ruleset.cached(rulesetId) 0645 Players.load() # we want to make sure we have the current definitions 0646 records = Query( 0647 "select hand,rotated from score where game=? and hand=" 0648 "(select max(hand) from score where game=?)", 0649 (gameid, gameid)).records 0650 if records: 0651 qLastHandRecord = records[0] 0652 else: 0653 qLastHandRecord = tuple([0, 0]) 0654 qScoreRecords = Query( 0655 "select player, wind, balance, won, prevailing from score " 0656 "where game=? and hand=?", 0657 (gameid, qLastHandRecord[0])).records 0658 if not qScoreRecords: 0659 # this should normally not happen 0660 qScoreRecords = list( 0661 tuple([qGameRecord[wind], wind.char, 0, False, East.char]) 0662 for wind in Wind.all4) 0663 if len(qScoreRecords) != 4: 0664 logError('game %d inconsistent: There should be exactly ' 0665 '4 score records for the last hand' % gameid) 0666 0667 # after loading SQL, prepare values. 0668 0669 # default value. If the server saved a score entry but our client 0670 # did not, we get no record here. Should we try to fix this or 0671 # exclude such a game from the list of resumable games? 0672 if len({x[4] for x in qScoreRecords}) != 1: 0673 logError('game %d inconsistent: All score records for the same ' 0674 'hand must have the same prevailing wind' % gameid) 0675 0676 players = [tuple([Wind(x[1]), Game.__getName(x[0])]) for x in qScoreRecords] 0677 0678 # create the game instance. 0679 game = cls(players, ruleset, gameid=gameid, client=client, 0680 wantedGame=qGameRecord[5]) 0681 game.handctr, game.rotated = qLastHandRecord 0682 0683 for record in qScoreRecords: 0684 playerid = record[0] 0685 player = game.players.byId(playerid) 0686 if not player: 0687 logError( 0688 'game %d inconsistent: player %d missing in game table' % 0689 (gameid, playerid)) 0690 else: 0691 player.getsPayment(record[2]) 0692 if record[3]: 0693 game.winner = player 0694 game.roundsFinished = Wind(qScoreRecords[0][4]).__index__() 0695 game.handctr += 1 0696 game.notRotated += 1 0697 game.maybeRotateWinds() 0698 game.sortPlayers() 0699 with AnimationSpeed(Speeds.windMarker): 0700 animateAndDo(game.wall.decorate4) 0701 return game 0702 0703 def finished(self): 0704 """The game is over after minRounds completed rounds. Also, 0705 check if we reached the second handId defined by --game. 0706 If we did, the game is over too""" 0707 last = HandId(self, self.wantedGame, 1) 0708 if self.handId > last: 0709 return True 0710 if Options.rounds: 0711 return self.roundsFinished >= Options.rounds 0712 if self.ruleset: 0713 # while initialising Game, ruleset might be None 0714 return self.roundsFinished >= self.ruleset.minRounds 0715 return None 0716 0717 def __payHand(self): 0718 """pay the scores""" 0719 # pylint: disable=too-many-branches 0720 # too many branches 0721 winner = self.__winner 0722 if winner: 0723 winner.wonCount += 1 0724 guilty = winner.usedDangerousFrom 0725 if guilty: 0726 payAction = self.ruleset.findUniqueOption('payforall') 0727 if guilty and payAction: 0728 if Debug.dangerousGame: 0729 self.debug('%s: winner %s. %s pays for all' % 0730 (self.handId, winner, guilty)) 0731 guilty.hand.usedRules.append((payAction, None)) 0732 score = winner.handTotal 0733 score = score * 6 if winner.wind == East else score * 4 0734 guilty.getsPayment(-score) 0735 winner.getsPayment(score) 0736 return 0737 0738 for player1 in self.players: 0739 if Debug.explain: 0740 if not self.belongsToRobotPlayer(): 0741 self.debug('%s: %s' % (player1, player1.hand.string)) 0742 for line in player1.hand.explain(): 0743 self.debug(' %s' % (line)) 0744 for player2 in self.players: 0745 if id(player1) != id(player2): 0746 if East in (player1.wind, player2.wind): 0747 efactor = 2 0748 else: 0749 efactor = 1 0750 if player2 != winner: 0751 player1.getsPayment(player1.handTotal * efactor) 0752 if player1 != winner: 0753 player1.getsPayment(-player2.handTotal * efactor) 0754 0755 def lastMoves(self, only=None, without=None, withoutNotifications=False): 0756 """filters and yields the moves in reversed order""" 0757 for idx in range(len(self.moves) - 1, -1, -1): 0758 move = self.moves[idx] 0759 if withoutNotifications and move.notifying: 0760 continue 0761 if only: 0762 if move.message in only: 0763 yield move 0764 elif without: 0765 if move.message not in without: 0766 yield move 0767 else: 0768 yield move 0769 0770 def throwDices(self): 0771 """set random living and kongBox 0772 sets divideAt: an index for the wall break""" 0773 breakWall = self.randomGenerator.randrange(4) 0774 sideLength = len(self.wall.tiles) // 4 0775 # use the sum of four dices to find the divide 0776 self.divideAt = breakWall * sideLength + \ 0777 sum(self.randomGenerator.randrange(1, 7) for idx in range(4)) 0778 if self.divideAt % 2 == 1: 0779 self.divideAt -= 1 0780 self.divideAt %= len(self.wall.tiles) 0781 0782 0783 class PlayingGame(Game): 0784 0785 """this game is played using the computer""" 0786 # pylint: disable=too-many-arguments,too-many-public-methods 0787 # pylint: disable=too-many-instance-attributes 0788 playerClass = PlayingPlayer 0789 0790 def __init__(self, names, ruleset, gameid=None, wantedGame=None, 0791 client=None, playOpen=False, autoPlay=False): 0792 """a new game instance, comes from database if gameid is set""" 0793 self.__activePlayer = None 0794 self.prevActivePlayer = None 0795 self.defaultNameBrush = None 0796 Game.__init__(self, names, ruleset, 0797 gameid, wantedGame=wantedGame, client=client) 0798 self.players['E'].lastSource = TileSource.East14th 0799 self.playOpen = playOpen 0800 self.autoPlay = autoPlay 0801 myself = self.myself 0802 if self.belongsToHumanPlayer() and myself: 0803 myself.voice = Voice.locate(myself.name) 0804 if myself.voice: 0805 if Debug.sound: 0806 logDebug('myself %s gets voice %s' % ( 0807 myself.name, myself.voice)) 0808 else: 0809 if Debug.sound: 0810 logDebug('myself %s gets no voice' % (myself.name)) 0811 0812 def writeCsv(self): 0813 """write game summary to Options.csv""" 0814 if self.finished() and Options.csv: 0815 gameWinner = max(self.players, key=lambda x: x.balance) 0816 if Debug.process and os.name != 'nt': 0817 self.csvTags.append('MEM:%s' % resource.getrusage( 0818 resource.RUSAGE_SELF).ru_maxrss) 0819 if Options.rounds: 0820 self.csvTags.append('ROUNDS:%s' % Options.rounds) 0821 _ = CsvRow.fields 0822 row = [''] * CsvRow.fields.PLAYERS 0823 row[_.GAME] = str(self.seed) 0824 row[_.RULESET] = self.ruleset.name 0825 row[_.AI] = Options.AI 0826 row[_.COMMIT] = gitHead() 0827 row[_.PY_VERSION] = '{}.{}'.format(*sys.version_info[:2]) 0828 row[_.TAGS] = ','.join(self.csvTags) 0829 for player in sorted(self.players, key=lambda x: x.name): 0830 row.append(player.name) 0831 row.append(player.balance) 0832 row.append(player.wonCount) 0833 row.append(1 if player == gameWinner else 0) 0834 CsvRow(row).write() 0835 0836 def close(self): 0837 """log off from the server and return a Deferred""" 0838 Game.close(self) 0839 self.writeCsv() 0840 Internal.autoPlay = False # do that only for the first game 0841 if self.client: 0842 client = self.client 0843 self.client = None 0844 result = client.logout() 0845 else: 0846 result = succeed(None) 0847 return result 0848 0849 def _setGameId(self): 0850 """do nothing, we already went through the game id reservation""" 0851 0852 @property 0853 def activePlayer(self): 0854 """the turn is on this player""" 0855 return self.__activePlayer 0856 0857 @activePlayer.setter 0858 def activePlayer(self, player): 0859 """the turn is on this player""" 0860 if self.__activePlayer != player: 0861 self.prevActivePlayer = self.__activePlayer 0862 if self.prevActivePlayer: 0863 self.prevActivePlayer.hidePopup() 0864 self.__activePlayer = player 0865 if Internal.scene: # mark the name of the active player in blue 0866 for _ in self.players: 0867 _.colorizeName() 0868 0869 def prepareHand(self): 0870 """prepares the next hand""" 0871 Game.prepareHand(self) 0872 if not self.finished(): 0873 self.sortPlayers() 0874 self.hidePopups() 0875 self._setHandSeed() 0876 self.wall.build(shuffleFirst=True) 0877 0878 def hidePopups(self): 0879 """hide all popup messages""" 0880 for player in self.players: 0881 player.hidePopup() 0882 0883 def saveStartTime(self): 0884 """write a new entry in the game table with the selected players""" 0885 if not self.gameid: 0886 # in server.__prepareNewGame, gameid is None here 0887 return 0888 records = Query("select seed from game where id=?", ( 0889 self.gameid,)).records 0890 assert records, 'self.gameid: %s' % self.gameid 0891 seed = records[0][0] 0892 0893 if not Internal.isServer and self.client: 0894 host = self.client.connection.url 0895 else: 0896 host = None 0897 0898 if seed in ('proposed', host): 0899 # we reserved the game id by writing a record with seed == host 0900 Game.saveStartTime(self) 0901 0902 def _saveScores(self): 0903 """save computed values to database, update score table 0904 and balance in status line""" 0905 if self.shouldSave: 0906 if self.belongsToRobotPlayer(): 0907 assert False, 'shouldSave must not be True for robot player' 0908 Game._saveScores(self) 0909 0910 def nextPlayer(self, current=None): 0911 """return the player after current or after activePlayer""" 0912 if not current: 0913 current = self.activePlayer 0914 pIdx = self.players.index(current) 0915 return self.players[(pIdx + 1) % 4] 0916 0917 def nextTurn(self): 0918 """move activePlayer""" 0919 self.activePlayer = self.nextPlayer() 0920 0921 def __concealedTileName(self, tileName): 0922 """tileName has been discarded, by which name did we know it?""" 0923 player = self.activePlayer 0924 if self.myself and player != self.myself and not self.playOpen: 0925 # we are human and server tells us another player discarded 0926 # a tile. In our game instance, tiles in handBoards of other 0927 # players are unknown 0928 player.makeTileKnown(tileName) 0929 result = Tile.unknown 0930 else: 0931 result = tileName 0932 if tileName not in player.concealedTiles: 0933 raise Exception('I am %s. Player %s is told to show discard ' 0934 'of tile %s but does not have it, he has %s' % 0935 (self.myself.name if self.myself else 'None', 0936 player.name, result, player.concealedTiles)) 0937 return result 0938 0939 def hasDiscarded(self, player, tileName): 0940 """discards a tile from a player board""" 0941 # pylint: disable=too-many-branches 0942 # too many branches 0943 assert isinstance(tileName, Tile) 0944 if player != self.activePlayer: 0945 raise Exception('Player %s discards but %s is active' % ( 0946 player, self.activePlayer)) 0947 self.discardedTiles[tileName.exposed] += 1 0948 player.discarded.append(tileName) 0949 self.__concealedTileName(tileName) 0950 # the above has side effect, needs to be called 0951 if Internal.scene: 0952 player.handBoard.discard(tileName) 0953 self.lastDiscard = Tile(tileName) 0954 player.removeTile(self.lastDiscard) 0955 if any(tileName.exposed in x[0] for x in self.dangerousTiles): 0956 self.computeDangerous() 0957 else: 0958 self._endWallDangerous() 0959 self.handDiscardCount += 1 0960 0961 def saveHand(self): 0962 """server told us to save this hand""" 0963 for player in self.players: 0964 handWonMatches = player.hand.won == (player == self.winner) 0965 assert handWonMatches, 'hand.won:%s winner:%s' % ( 0966 player.hand.won, player == self.winner) 0967 Game.saveHand(self) 0968 0969 def _mustExchangeSeats(self, pairs): 0970 """filter: which player pairs should really swap places?""" 0971 # if we are a client in a remote game, the server swaps and tells 0972 # us the new places 0973 return [] if self.belongsToPlayer() else pairs 0974 0975 def _scanGameOption(self): 0976 """scan the --game option and go to start of wanted hand""" 0977 if '/' in self.wantedGame: 0978 first, last = (HandId(self, self.wantedGame, x) for x in (0, 1)) 0979 if first > last: 0980 raise UserWarning('{}..{} is a negative range'.format( 0981 first, last)) 0982 HandId(self, self.wantedGame).goto() 0983 0984 def assignVoices(self): 0985 """now we have all remote user voices""" 0986 assert self.belongsToHumanPlayer() 0987 available = Voice.availableVoices()[:] 0988 # available is without transferred human voices 0989 for player in self.players: 0990 if player.voice and player.voice.oggFiles(): 0991 # remote human player sent her voice, or we are human 0992 # and have a voice 0993 if Debug.sound and player != self.myself: 0994 logDebug('%s got voice from opponent: %s' % ( 0995 player.name, player.voice)) 0996 else: 0997 player.voice = Voice.locate(player.name) 0998 if player.voice: 0999 if Debug.sound: 1000 logDebug('%s has own local voice %s' % ( 1001 player.name, player.voice)) 1002 if player.voice: 1003 for voice in Voice.availableVoices(): 1004 if (voice in available 1005 and voice.md5sum == player.voice.md5sum): 1006 # if the local voice is also predefined, 1007 # make sure we do not use both 1008 available.remove(voice) 1009 # for the other players use predefined voices in preferred language. 1010 # Only if we do not have enough predefined voices, look again in 1011 # locally defined voices 1012 predefined = [x for x in available if x.language() != 'local'] 1013 predefined.extend(available) 1014 for player in self.players: 1015 if player.voice is None and predefined: 1016 player.voice = predefined.pop(0) 1017 if Debug.sound: 1018 logDebug( 1019 '%s gets one of the still available voices %s' % ( 1020 player.name, player.voice)) 1021 1022 def dangerousFor(self, forPlayer, tile): 1023 """return a list of explaining texts if discarding tile 1024 would be Dangerous game for forPlayer. One text for each 1025 reason - there might be more than one""" 1026 assert isinstance(tile, Tile), tile 1027 tile = tile.exposed 1028 result = [] 1029 for dang, txt in self.dangerousTiles: 1030 if tile in dang: 1031 result.append(txt) 1032 for player in forPlayer.others(): 1033 for dang, txt in player.dangerousTiles: 1034 if tile in dang: 1035 result.append(txt) 1036 return result 1037 1038 def computeDangerous(self, playerChanged=None): 1039 """recompute gamewide dangerous tiles. Either for playerChanged or 1040 for all players""" 1041 self.dangerousTiles = list() 1042 if playerChanged: 1043 playerChanged.findDangerousTiles() 1044 else: 1045 for player in self.players: 1046 player.findDangerousTiles() 1047 self._endWallDangerous() 1048 1049 def _endWallDangerous(self): 1050 """if end of living wall is reached, declare all invisible tiles 1051 as dangerous""" 1052 if len(self.wall.living) <= 5: 1053 allTiles = [x for x in defaultdict.keys(elements.occurrence) 1054 if not x.isBonus] 1055 for tile in allTiles: 1056 assert isinstance(tile, Tile), tile 1057 # see https://www.logilab.org/ticket/23986 1058 invisibleTiles = {x for x in allTiles if x not in self.visibleTiles} 1059 msg = i18n('Short living wall: Tile is invisible, hence dangerous') 1060 self.dangerousTiles = [x for x in self.dangerousTiles if x[1] != msg] 1061 self.dangerousTiles.append((invisibleTiles, msg)) 1062 1063 def appendMove(self, player, command, kwargs): 1064 """append a Move object to self.moves""" 1065 self.moves.append(Move(player, command, kwargs))