def addUser(self, user): """add user to this table""" if user.name in list(x.name for x in self.users): raise srvError(pb.Error, i18nE('You already joined this table')) if len(self.users) == self.maxSeats(): raise srvError(pb.Error, i18nE('All seats are already taken')) self.users.append(user) if Debug.table: logDebug('%s seated on table %s' % (user.name, self)) self.sendChatMessage( ChatMessage(self.tableid, user.name, i18nE('takes a seat'), isStatusMessage=True))
def claimTile(self, player, claim, meldTiles, nextMessage): """a player claims a tile for pung, kong or chow. meldTiles contains the claimed tile, concealed""" if not self.running: return lastDiscard = self.game.lastDiscard # if we rob a tile, self.game.lastDiscard has already been set to the # robbed tile hasTiles = Meld(meldTiles[:]) discardingPlayer = self.game.activePlayer hasTiles = hasTiles.without(lastDiscard) meld = Meld(meldTiles) if len(meld) != 4 and not (meld.isPair or meld.isPungKong or meld.isChow): msg = i18nE('%1 wrongly said %2 for meld %3') self.abort(msg, player.name, claim.name, str(meld)) return if not player.hasConcealedTiles(hasTiles): msg = i18nE( '%1 wrongly said %2: claims to have concealed tiles %3 but only has %4' ) self.abort(msg, player.name, claim.name, ' '.join(hasTiles), ''.join(player.concealedTiles)) return # update our internal state before we listen to the clients again self.game.discardedTiles[lastDiscard.exposed] -= 1 self.game.activePlayer = player if lastDiscard: player.lastTile = lastDiscard.exposed player.lastSource = TileSource.LivingWallDiscard player.exposeMeld(hasTiles, lastDiscard) self.game.lastDiscard = None block = DeferredBlock(self) if (nextMessage != Message.Kong and self.game.dangerousFor(discardingPlayer, lastDiscard) and discardingPlayer.playedDangerous): player.usedDangerousFrom = discardingPlayer if Debug.dangerousGame: logDebug('%s claims dangerous tile %s discarded by %s' % (player, lastDiscard, discardingPlayer)) block.tellAll(player, Message.UsedDangerousFrom, source=discardingPlayer.name) block.tellAll(player, nextMessage, meld=meld) if claim == Message.Kong: block.callback(self.pickKongReplacement) else: block.callback(self.moved)
def nextHand(self, dummyResults): """next hand: maybe rotate""" if not self.running: return DeferredBlock.garbageCollection() for block in DeferredBlock.blocks: if block.table == self: logError('request left from previous hand: %s' % block.outstandingStr()) token = self.game.handId.prompt( withAI=False) # we need to send the old token until the # clients started the new hand rotateWinds = self.game.maybeRotateWinds() if self.game.finished(): self.server.removeTable(self, 'gameOver', i18nE('Game <numid>%1</numid> is over!'), self.game.seed) if Debug.process and os.name != 'nt': logDebug('MEM:%s' % resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) return self.game.sortPlayers() playerNames = list((x.wind, x.name) for x in self.game.players) self.tellAll(None, Message.ReadyForHandStart, self.startHand, playerNames=playerNames, rotateWinds=rotateWinds, token=token)
def _lookupTable(self, tableid): """return table by id or raise exception""" if tableid not in self.tables: raise srvError(pb.Error, i18nE('table with id <numid>%1</numid> not found'), tableid) return self.tables[tableid]
def _checkedPassword(matched, userid): """after the password has been checked""" if not matched: return fail( credError.UnauthorizedLogin(srvMessage( i18nE('Wrong password')))) return userid
def delUser(self, user): """remove user from this table""" if user in self.users: self.running = False self.users.remove(user) self.sendChatMessage( ChatMessage(self.tableid, user.name, i18nE('leaves the table'), isStatusMessage=True)) if user is self.owner: # silently pass ownership if self.users: self.owner = self.users[0] if Debug.table: logDebug('%s leaves table %d, %s is the new owner' % (user.name, self.tableid, self.owner)) else: if Debug.table: logDebug('%s leaves table %d, table is now empty' % (user.name, self.tableid)) else: if Debug.table: logDebug('%s leaves table %d, %s stays owner' % (user.name, self.tableid, self.owner))
def __failed(self, result, request): """a user did not or not correctly answer""" if request in self.requests: self.removeRequest(request) if result.type in [pb.PBConnectionLost]: msg = i18nE('The game server lost connection to player %1') self.table.abort(msg, request.user.name) else: msg = i18nE('Error for player %1: %2\n%3') if hasattr(result, 'traceback'): traceBack = result.traceback else: try: traceBack = result.getBriefTraceback() except BaseException as exc: traceBack = 'twisted cannot give us a traceback:{}'.format( exc) self.table.abort(msg, request.user.name, result.getErrorMessage(), traceBack)
def logout(self, user): """remove user from all tables""" if user not in self.srvUsers: return self.srvUsers.remove(user) for tableid in self.tablesWith(user): self.leaveTable(user, tableid, i18nE('Player %1 has logged out'), user.name) # wait a moment. We want the leaveTable message to arrive everywhere before # we say serverDisconnects. Sometimes the order was reversed. reactor.callLater(1, self.__logout2, user)
def perspective_setClientProperties(self, dbIdent, voiceId, maxGameId, clientVersion=None): """perspective_* methods are to be called remotely""" self.pinged() self.dbIdent = dbIdent self.voiceId = voiceId self.maxGameId = maxGameId serverVersion = Internal.defaultPort if clientVersion != serverVersion: # we assume that versions x.y.* are compatible if clientVersion is None: # client passed no version info return fail( srvError( pb.Error, i18nE( 'Your client has a version older than 4.9.0 but you need %1 for this server' ), serverVersion)) else: commonDigits = len([ x for x in zip(clientVersion.split(b'.'), serverVersion.split(b'.')) if x[0] == x[1] ]) if commonDigits < 2: return fail( srvError( pb.Error, i18nE( 'Your client has version %1 but you need %2 for this server' ), clientVersion or '<4.9.0', '.'.join(serverVersion.split('.')[:2]) + '.*')) if Debug.table: logDebug( 'client has dbIdent={} voiceId={} maxGameId={} clientVersion {}' .format(self.dbIdent, self.voiceId, self.maxGameId, clientVersion)) self.server.sendTables(self)
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 meld in concealedMelds: for tile in meld: if tile == ignoreDiscard: ignoreDiscard = None else: if tile not in self._concealedTiles: msg = i18nE( '%1 claiming MahJongg: She does not really have tile %2') return msg, self.name, tile self._concealedTiles.remove(tile) if meld.isConcealed and not meld.isKong: self._concealedMelds.append(meld) else: self._exposedMelds.append(meld) if self._concealedTiles: msg = i18nE( '%1 claiming MahJongg: She did not pass all concealed tiles to the server') return msg, self.name self._hand = None
def tell(self, about, receivers, command, **kwargs): """send info about player 'about' to users 'receivers'""" def encodeKwargs(): """those values are classes like Meld, Tile etc. Convert to bytes""" for keyword in kwargs: if any(keyword.lower().endswith(x) for x in ('tile', 'tiles', 'meld', 'melds')): if kwargs[keyword] is not None: kwargs[keyword] = str(kwargs[keyword]) encodeKwargs() if about.__class__.__name__ == 'User': about = self.playerForUser(about) if not isinstance(receivers, list): receivers = list([receivers]) assert receivers, 'DeferredBlock.tell(%s) has no receiver' % command self.__enrichMessage(self.table.game, about, command, kwargs) aboutName = about.name if about else None if self.table.running and len(receivers) in [1, 4]: # messages are either identical for all 4 players # or identical for 3 players and different for 1 player. And # we want to capture each message exactly once. self.table.game.appendMove(about, command, kwargs) localDeferreds = [] for rec in self.__convertReceivers(receivers): isClient = rec.__class__.__name__.endswith('Client') if Debug.traffic and not isClient: message = '-> {receiver:<15} about {about} {command}{kwargs}'.format( receiver=rec.name[:15], about=about, command=command, kwargs=Move.prettyKwargs(kwargs)) logDebug(message) if isClient: defer = Deferred() defer.addCallback(rec.remote_move, command, **kwargs) else: defer = self.table.server.callRemote(rec, 'move', aboutName, command.name, **kwargs) if defer: defer.command = command.name defer.notifying = 'notifying' in kwargs self.__addRequest(defer, rec, about) else: msg = i18nE('The game server lost connection to player %1') self.table.abort(msg, rec.name) if isClient: localDeferreds.append(defer) for defer in localDeferreds: defer.callback(aboutName) # callback needs an argument !
def claimMahJongg(self, msg): """a player claims mah jongg. Check this and if correct, tell all. Otherwise abort game, kajongg client is faulty""" if not self.running: return player = msg.player concealedMelds = MeldList(msg.args[0]) withDiscard = Tile(msg.args[1]) if msg.args[1] else None lastMeld = Meld(msg.args[2]) if self.game.ruleset.mustDeclareCallingHand: assert player.isCalling, '%s %s %s says MJ but never claimed: concmelds:%s withdiscard:%s lastmeld:%s' % ( self.game.handId, player.hand, player, concealedMelds, withDiscard, lastMeld) discardingPlayer = self.game.activePlayer lastMove = next(self.game.lastMoves(withoutNotifications=True)) robbedTheKong = lastMove.message == Message.DeclaredKong if robbedTheKong: player.robsTile() withDiscard = lastMove.meld[0].concealed lastMove.player.robTileFrom(withDiscard) msgArgs = player.showConcealedMelds(concealedMelds, withDiscard) if msgArgs: self.abort(*msgArgs) return player.declaredMahJongg(concealedMelds, withDiscard, player.lastTile, lastMeld) if not player.hand.won: msg = i18nE('%1 claiming MahJongg: This is not a winning hand: %2') self.abort(msg, player.name, player.hand.string) return block = DeferredBlock(self) if robbedTheKong: block.tellAll(player, Message.RobbedTheKong, tile=withDiscard) if (player.lastSource is TileSource.LivingWallDiscard and self.game.dangerousFor(discardingPlayer, player.lastTile) and discardingPlayer.playedDangerous): player.usedDangerousFrom = discardingPlayer if Debug.dangerousGame: logDebug('%s wins with dangerous tile %s from %s' % (player, self.game.lastDiscard, discardingPlayer)) block.tellAll(player, Message.UsedDangerousFrom, source=discardingPlayer.name) block.tellAll(player, Message.MahJongg, melds=concealedMelds, lastTile=player.lastTile, lastMeld=lastMeld, withDiscardTile=withDiscard) block.callback(self.endHand)
def readyForGameStart(self, user): """the table initiator told us he wants to start the game""" if len(self.users) < self.maxSeats() and self.owner != user: raise srvError( pb.Error, i18nE('Only the initiator %1 can start this game, you are %2'), self.owner.name, user.name) if self.suspendedAt: self.__connectPlayers() self.__checkDbIdents() self.initGame() else: self.game = self.__prepareNewGame() self.__connectPlayers() self.__checkDbIdents() self.proposeGameId(self.calcGameId())
def declareKong(self, player, meldTiles): """player declares a Kong, meldTiles is a list""" kongMeld = Meld(meldTiles) if not player.hasConcealedTiles(kongMeld) and kongMeld[ 0].exposed.pung not in player.exposedMelds: msg = i18nE('declareKong:%1 wrongly said Kong for meld %2') args = (player.name, str(kongMeld)) logDebug(i18n(msg, *args)) logDebug('declareKong:concealedTiles:%s' % ''.join(player.concealedTiles)) logDebug('declareKong:concealedMelds:%s' % ' '.join(str(x) for x in player.concealedMelds)) logDebug('declareKong:exposedMelds:%s' % ' '.join(str(x) for x in player.exposedMelds)) self.abort(msg, *args) return player.exposeMeld(kongMeld) self.tellAll(player, Message.DeclaredKong, self.pickKongReplacement, meld=kongMeld)
def chat(self): """chat. Only generate ChatWindow after the message has successfully been sent to the server. Because the server might have gone away.""" def initChat(_): """now that we were able to send the message to the server instantiate the chat window""" table.chatWindow = ChatWindow(table=table) table.chatWindow.receiveLine(msg) table = self.selectedTable() if not table.chatWindow: line = i18nE('opens a chat window') msg = ChatMessage(table.tableid, table.client.name, line, isStatusMessage=True) table.client.sendChat(msg).addCallback(initChat).addErrback( self.client.tableError) elif table.chatWindow.isVisible(): table.chatWindow.hide() else: table.chatWindow.show()
def __init__(self, name=None): PredefinedRuleset.__init__(self, name or i18nE('Classical Chinese standard'))
class Score(StrMixin): """holds all parts contributing to a score. It has two use cases: 1. for defining what a rules does: either points or doubles or limits, holding never more than one unit 2. for summing up the scores of all rules: Now more than one of the units can be in use. If a rule should want to set more than one unit, split it into two rules. For the first use case only we have the attributes value and unit""" __hash__ = None def __init__(self, points=0, doubles=0, limits=0, ruleset=None): self.points = 0 # define the types for those values self.doubles = 0 self.limits = 0.0 self.ruleset = ruleset self.points = type(self.points)(points) self.doubles = type(self.doubles)(doubles) self.limits = type(self.limits)(limits) unitNames = { i18nE('points'): 0, i18ncE('kajongg', 'doubles'): 50, i18ncE('kajongg', 'limits'): 9999 } def clear(self): """set all to 0""" self.points = self.doubles = self.limits = 0 def change(self, unitName, value): """sets value for unitName. If changed, return True""" oldValue = self.__getattribute__(unitName) newValue = type(oldValue)(value) if newValue == oldValue: return False, None if newValue: if unitName == 'points': if self.doubles: return False, 'Cannot have points and doubles' if unitName == 'doubles': if self.points: return False, 'Cannot have points and doubles' self.__setattr__(unitName, newValue) return True, None def __str__(self): """make score printable""" parts = [] if self.points: parts.append('points=%d' % self.points) if self.doubles: parts.append('doubles=%d' % self.doubles) if self.limits: parts.append('limits=%f' % self.limits) return ' '.join(parts) def i18nStr(self): """make score readable for humans, i18n""" parts = [] if self.points: parts.append(i18nc('Kajongg', '%1 points', self.points)) if self.doubles: parts.append(i18nc('Kajongg', '%1 doubles', self.doubles)) if self.limits: limits = str(self.limits) if limits.endswith('.0'): limits = limits[-2:] parts.append(i18nc('Kajongg', '%1 limits', limits)) return ' '.join(parts) def __eq__(self, other): """ == comparison """ assert isinstance(other, Score) return self.points == other.points and self.doubles == other.doubles and self.limits == other.limits def __ne__(self, other): """ != comparison """ return self.points != other.points or self.doubles != other.doubles or self.limits != other.limits def __lt__(self, other): return self.total() < other.total() def __le__(self, other): return self.total() <= other.total() def __gt__(self, other): return self.total() > other.total() def __ge__(self, other): return self.total() >= other.total() def __add__(self, other): """implement adding Score""" return Score(self.points + other.points, self.doubles + other.doubles, max(self.limits, other.limits), self.ruleset or other.ruleset) def total(self): """the total score""" score = int(self.points * (2**self.doubles)) if self.limits: if self.limits >= 1: self.points = self.doubles = 0 elif self.limits * self.ruleset.limit >= score: self.points = self.doubles = 0 else: self.limits = 0 if self.limits: return int(round(self.limits * self.ruleset.limit)) if score and not self.ruleset.roofOff: score = min(score, self.ruleset.limit) return score def __int__(self): """the total score""" return self.total() def __nonzero__(self): """for bool() conversion""" return self.points != 0 or self.doubles != 0 or self.limits != 0
def __init__(self, name=None): ClassicalChinese.__init__(self, name or i18nE('Classical Chinese BMJA'))