Ticket #3022: LobbyBotSplit.patch

File LobbyBotSplit.patch, 61.3 KB (added by scythetwirler, 8 years ago)
  • source/tools/XpartaMuPP/EcheLOn.py

     
     1#!/usr/bin/env python3
     2# -*- coding: utf-8 -*-
     3"""Copyright (C) 2016 Wildfire Games.
     4 * This file is part of 0 A.D.
     5 *
     6 * 0 A.D. is free software: you can redistribute it and/or modify
     7 * it under the terms of the GNU General Public License as published by
     8 * the Free Software Foundation, either version 2 of the License, or
     9 * (at your option) any later version.
     10 *
     11 * 0 A.D. is distributed in the hope that it will be useful,
     12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
     13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     14 * GNU General Public License for more details.
     15 *
     16 * You should have received a copy of the GNU General Public License
     17 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
     18"""
     19
     20import logging, time, traceback
     21from optparse import OptionParser
     22
     23import sleekxmpp
     24from sleekxmpp.stanza import Iq
     25from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin, ET
     26from sleekxmpp.xmlstream.handler import Callback
     27from sleekxmpp.xmlstream.matcher import StanzaPath
     28
     29from sqlalchemy import func
     30
     31from LobbyRanking import session as db, Game, Player, PlayerInfo
     32from ELO import get_rating_adjustment
     33# Rating that new players should be inserted into the
     34# database with, before they've played any games.
     35leaderboard_default_rating = 1200
     36
     37## Class that contains and manages leaderboard data ##
     38class LeaderboardList():
     39  def __init__(self, room):
     40    self.room = room
     41    self.lastRated = ""
     42
     43  def getProfile(self, JID):
     44    """
     45      Retrieves the profile for the specified JID
     46    """
     47    stats = {}
     48    player = db.query(Player).filter(Player.jid.ilike(str(JID)))
     49    if not player.first():
     50      return
     51    if player.first().rating != -1:
     52      stats['rating'] = str(player.first().rating)
     53
     54    if player.first().highest_rating != -1:
     55      stats['highestRating'] = str(player.first().highest_rating)
     56
     57    playerID = player.first().id
     58    players = db.query(Player).filter(Player.rating != -1).order_by(Player.rating.desc()).all()
     59
     60    for rank, user in enumerate(players):
     61      if (user.jid.lower() == JID.lower()):
     62        stats['rank'] = str(rank+1)
     63        break
     64
     65    stats['totalGamesPlayed'] = str(db.query(PlayerInfo).filter_by(player_id=playerID).count())
     66    stats['wins'] = str(db.query(Game).filter_by(winner_id=playerID).count())
     67    stats['losses'] = str(db.query(PlayerInfo).filter_by(player_id=playerID).count() - db.query(Game).filter_by(winner_id=playerID).count())
     68    return stats
     69
     70  def getOrCreatePlayer(self, JID):
     71    """
     72      Stores a player(JID) in the database if they don't yet exist.
     73      Returns either the newly created instance of
     74      the Player model, or the one that already
     75      exists in the database.
     76    """
     77    players = db.query(Player).filter_by(jid=str(JID))
     78    if not players.first():
     79      player = Player(jid=str(JID), rating=-1)
     80      db.add(player)
     81      db.commit()
     82      return player
     83    return players.first()
     84
     85  def removePlayer(self, JID):
     86    """
     87      Remove a player(JID) from database.
     88      Returns the player that was removed, or None
     89      if that player didn't exist.
     90    """
     91    players = db.query(Player).filter_by(jid=JID)
     92    player = players.first()
     93    if not player:
     94      return None
     95    players.delete()
     96    return player
     97
     98  def addGame(self, gamereport):
     99    """
     100      Adds a game to the database and updates the data
     101      on a player(JID) from game results.
     102      Returns the created Game object, or None if
     103      the creation failed for any reason.
     104      Side effects:
     105        Inserts a new Game instance into the database.
     106    """
     107    # Discard any games still in progress.
     108    if any(map(lambda state: state == 'active',
     109               dict.values(gamereport['playerStates']))):
     110      return None
     111
     112    players = map(lambda jid: db.query(Player).filter(Player.jid.ilike(str(jid))).first(),
     113                  dict.keys(gamereport['playerStates']))
     114
     115    winning_jid = list(dict.keys({jid: state for jid, state in
     116                                  gamereport['playerStates'].items()
     117                                  if state == 'won'}))[0]
     118
     119    def get(stat, jid):
     120      return gamereport[stat][jid]
     121
     122    singleStats = {'timeElapsed', 'mapName', 'teamsLocked', 'matchID'}
     123    totalScoreStats = {'economyScore', 'militaryScore', 'totalScore'}
     124    resourceStats = {'foodGathered', 'foodUsed', 'woodGathered', 'woodUsed',
     125            'stoneGathered', 'stoneUsed', 'metalGathered', 'metalUsed', 'vegetarianFoodGathered',
     126            'treasuresCollected', 'lootCollected', 'tributesSent', 'tributesReceived'}
     127    unitsStats = {'totalUnitsTrained', 'totalUnitsLost', 'enemytotalUnitsKilled', 'infantryUnitsTrained',
     128            'infantryUnitsLost', 'enemyInfantryUnitsKilled', 'workerUnitsTrained', 'workerUnitsLost',
     129            'enemyWorkerUnitsKilled', 'femaleUnitsTrained', 'femaleUnitsLost', 'enemyFemaleUnitsKilled',
     130            'cavalryUnitsTrained', 'cavalryUnitsLost', 'enemyCavalryUnitsKilled', 'championUnitsTrained',
     131            'championUnitsLost', 'enemyChampionUnitsKilled', 'heroUnitsTrained', 'heroUnitsLost',
     132            'enemyHeroUnitsKilled', 'shipUnitsTrained', 'shipUnitsLost', 'enemyShipUnitsKilled', 'traderUnitsTrained',
     133            'traderUnitsLost', 'enemyTraderUnitsKilled'}
     134    buildingsStats = {'totalBuildingsConstructed', 'totalBuildingsLost', 'enemytotalBuildingsDestroyed',
     135            'civCentreBuildingsConstructed', 'civCentreBuildingsLost', 'enemyCivCentreBuildingsDestroyed',
     136            'houseBuildingsConstructed', 'houseBuildingsLost', 'enemyHouseBuildingsDestroyed',
     137            'economicBuildingsConstructed', 'economicBuildingsLost', 'enemyEconomicBuildingsDestroyed',
     138            'outpostBuildingsConstructed', 'outpostBuildingsLost', 'enemyOutpostBuildingsDestroyed',
     139            'militaryBuildingsConstructed', 'militaryBuildingsLost', 'enemyMilitaryBuildingsDestroyed',
     140            'fortressBuildingsConstructed', 'fortressBuildingsLost', 'enemyFortressBuildingsDestroyed',
     141            'wonderBuildingsConstructed', 'wonderBuildingsLost', 'enemyWonderBuildingsDestroyed'}
     142    marketStats = {'woodBought', 'foodBought', 'stoneBought', 'metalBought', 'tradeIncome'}
     143    miscStats = {'civs', 'teams', 'percentMapExplored'}
     144
     145    stats = totalScoreStats | resourceStats | unitsStats | buildingsStats | marketStats | miscStats
     146    playerInfos = []
     147    for player in players:
     148      jid = player.jid
     149      playerinfo = PlayerInfo(player=player)
     150      for reportname in stats:
     151        setattr(playerinfo, reportname, get(reportname, jid.lower()))
     152      playerInfos.append(playerinfo)
     153
     154    game = Game(map=gamereport['mapName'], duration=int(gamereport['timeElapsed']), teamsLocked=bool(gamereport['teamsLocked']), matchID=gamereport['matchID'])
     155    game.players.extend(players)
     156    game.player_info.extend(playerInfos)
     157    game.winner = db.query(Player).filter(Player.jid.ilike(str(winning_jid))).first()
     158    db.add(game)
     159    db.commit()
     160    return game
     161 
     162  def verifyGame(self, gamereport):
     163    """
     164      Returns a boolean based on whether the game should be rated.
     165      Here, we can specify the criteria for rated games.
     166    """
     167    winning_jids = list(dict.keys({jid: state for jid, state in
     168                                  gamereport['playerStates'].items()
     169                                  if state == 'won'}))
     170    # We only support 1v1s right now. TODO: Support team games.
     171    if len(winning_jids) * 2 > len(dict.keys(gamereport['playerStates'])):
     172      # More than half the people have won. This is not a balanced team game or duel.
     173      return False
     174    if len(dict.keys(gamereport['playerStates'])) != 2:
     175      return False
     176    return True
     177
     178  def rateGame(self, game):
     179    """
     180      Takes a game with 2 players and alters their ratings
     181      based on the result of the game.
     182      Returns self.
     183      Side effects:
     184        Changes the game's players' ratings in the database.
     185    """
     186    player1 = game.players[0]
     187    player2 = game.players[1]
     188    # TODO: Support draws. Since it's impossible to draw in the game currently,
     189    # the database model, and therefore this code, requires a winner.
     190    # The Elo implementation does not, however.
     191    result = 1 if player1 == game.winner else -1
     192    # Player's ratings are -1 unless they have played a rated game.
     193    if player1.rating == -1:
     194      player1.rating = leaderboard_default_rating
     195    if player2.rating == -1:
     196      player2.rating = leaderboard_default_rating
     197
     198    rating_adjustment1 = int(get_rating_adjustment(player1.rating, player2.rating,
     199      len(player1.games), len(player2.games), result))
     200    rating_adjustment2 = int(get_rating_adjustment(player2.rating, player1.rating,
     201      len(player2.games), len(player1.games), result * -1))
     202    if result == 1:
     203      resultQualitative = "won"
     204    elif result == 0:
     205      resultQualitative = "drew"
     206    else:
     207      resultQualitative = "lost"
     208    name1 = '@'.join(player1.jid.split('@')[:-1])
     209    name2 = '@'.join(player2.jid.split('@')[:-1])
     210    self.lastRated = "A rated game has ended. %s %s against %s. Rating Adjustment: %s (%s -> %s) and %s (%s -> %s)."%(name1,
     211      resultQualitative, name2, name1, player1.rating, player1.rating + rating_adjustment1,
     212      name2, player2.rating, player2.rating + rating_adjustment2)
     213    player1.rating += rating_adjustment1
     214    player2.rating += rating_adjustment2
     215    if not player1.highest_rating:
     216      player1.highest_rating = -1
     217    if not player2.highest_rating:
     218      player2.highest_rating = -1
     219    if player1.rating > player1.highest_rating:
     220      player1.highest_rating = player1.rating
     221    if player2.rating > player2.highest_rating:
     222      player2.highest_rating = player2.rating
     223    db.commit()
     224    return self
     225
     226  def getLastRatedMessage(self):
     227    """
     228      Gets the string of the last rated game. Triggers an update
     229      chat for the bot.
     230    """
     231    return self.lastRated
     232
     233  def addAndRateGame(self, gamereport):
     234    """
     235      Calls addGame and if the game has only two
     236      players, also calls rateGame.
     237      Returns the result of addGame.
     238    """
     239    game = self.addGame(gamereport)
     240    if game and self.verifyGame(gamereport):
     241      self.rateGame(game)
     242    else:
     243      self.lastRated = ""
     244    return game
     245
     246  def getBoard(self):
     247    """
     248      Returns a dictionary of player rankings to
     249        JIDs for sending.
     250    """
     251    board = {}
     252    players = db.query(Player).filter(Player.rating != -1).order_by(Player.rating.desc()).limit(100).all()
     253    for rank, player in enumerate(players):
     254      board[player.jid] = {'name': '@'.join(player.jid.split('@')[:-1]), 'rating': str(player.rating)}
     255    return board
     256  def getRatingList(self, nicks):
     257    """
     258    Returns a rating list of players
     259    currently in the lobby by nick
     260    because the client can't link
     261    JID to nick conveniently.
     262    """
     263    ratinglist = {}
     264    for JID in list(nicks):
     265      players = db.query(Player.jid, Player.rating).filter(Player.jid.ilike(str(JID)))
     266      if players.first():
     267        if players.first().rating == -1:
     268          ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': ''}
     269        else:
     270          ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': str(players.first().rating)}
     271      else:
     272        ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': ''}
     273    return ratinglist
     274
     275## Class which manages different game reports from clients ##
     276##   and calls leaderboard functions as appropriate.       ##
     277class ReportManager():
     278  def __init__(self, leaderboard):
     279    self.leaderboard = leaderboard
     280    self.interimReportTracker = []
     281    self.interimJIDTracker = []
     282
     283  def addReport(self, JID, rawGameReport):
     284    """
     285      Adds a game to the interface between a raw report
     286        and the leaderboard database.
     287    """
     288    # cleanRawGameReport is a copy of rawGameReport with all reporter specific information removed.
     289    cleanRawGameReport = rawGameReport.copy()
     290    del cleanRawGameReport["playerID"]
     291
     292    if cleanRawGameReport not in self.interimReportTracker:
     293      # Store the game.
     294      appendIndex = len(self.interimReportTracker)
     295      self.interimReportTracker.append(cleanRawGameReport)
     296      # Initilize the JIDs and store the initial JID.
     297      numPlayers = self.getNumPlayers(rawGameReport)
     298      JIDs = [None] * numPlayers
     299      if numPlayers - int(rawGameReport["playerID"]) > -1:
     300        JIDs[int(rawGameReport["playerID"])-1] = str(JID).lower()
     301      self.interimJIDTracker.append(JIDs)
     302    else:
     303      # We get the index at which the JIDs coresponding to the game are stored.
     304      index = self.interimReportTracker.index(cleanRawGameReport)
     305      # We insert the new report JID into the ascending list of JIDs for the game.
     306      JIDs = self.interimJIDTracker[index]
     307      if len(JIDs) - int(rawGameReport["playerID"]) > -1:
     308        JIDs[int(rawGameReport["playerID"])-1] = str(JID).lower()
     309      self.interimJIDTracker[index] = JIDs
     310
     311    self.checkFull()
     312
     313  def expandReport(self, rawGameReport, JIDs):
     314    """
     315      Takes an raw game report and re-formats it into
     316        Python data structures leaving JIDs empty.
     317      Returns a processed gameReport of type dict.
     318    """
     319    processedGameReport = {}
     320    for key in rawGameReport:
     321      if rawGameReport[key].find(",") == -1:
     322        processedGameReport[key] = rawGameReport[key]
     323      else:
     324        split = rawGameReport[key].split(",")
     325        # Remove the false split positive.
     326        split.pop()
     327        statToJID = {}
     328        for i, part in enumerate(split):
     329           statToJID[JIDs[i]] = part
     330        processedGameReport[key] = statToJID
     331    return processedGameReport
     332
     333  def checkFull(self):
     334    """
     335      Searches internal database to check if enough
     336        reports have been submitted to add a game to
     337        the leaderboard. If so, the report will be
     338        interpolated and addAndRateGame will be
     339        called with the result.
     340    """
     341    i = 0
     342    length = len(self.interimReportTracker)
     343    while(i < length):
     344      numPlayers = self.getNumPlayers(self.interimReportTracker[i])
     345      numReports = 0
     346      for JID in self.interimJIDTracker[i]:
     347        if JID != None:
     348          numReports += 1
     349      if numReports == numPlayers:
     350        self.leaderboard.addAndRateGame(self.expandReport(self.interimReportTracker[i], self.interimJIDTracker[i]))
     351        del self.interimJIDTracker[i]
     352        del self.interimReportTracker[i]
     353        length -= 1
     354      else:
     355        i += 1
     356        self.leaderboard.lastRated = ""
     357
     358  def getNumPlayers(self, rawGameReport):
     359    """
     360      Computes the number of players in a raw gameReport.
     361      Returns int, the number of players.
     362    """
     363    # Find a key in the report which holds values for multiple players.
     364    for key in rawGameReport:
     365      if rawGameReport[key].find(",") != -1:
     366        # Count the number of values, minus one for the false split positive.
     367        return len(rawGameReport[key].split(","))-1
     368    # Return -1 in case of failure.
     369    return -1
     370## Class for custom player stanza extension ##
     371class PlayerXmppPlugin(ElementBase):
     372  name = 'query'
     373  namespace = 'jabber:iq:player'
     374  interfaces = set(('game', 'online'))
     375  sub_interfaces = interfaces
     376  plugin_attrib = 'player'
     377
     378  def addPlayerOnline(self, player):
     379    playerXml = ET.fromstring("<player>%s</player>" % player)
     380    self.xml.append(playerXml)
     381
     382## Class for custom boardlist and ratinglist stanza extension ##
     383class BoardListXmppPlugin(ElementBase):
     384  name = 'query'
     385  namespace = 'jabber:iq:boardlist'
     386  interfaces = set(('board', 'command', 'recipient'))
     387  sub_interfaces = interfaces
     388  plugin_attrib = 'boardlist'
     389  def addCommand(self, command):
     390    commandXml = ET.fromstring("<command>%s</command>" % command)
     391    self.xml.append(commandXml)
     392  def addRecipient(self, recipient):
     393    recipientXml = ET.fromstring("<recipient>%s</recipient>" % recipient)
     394    self.xml.append(recipientXml)
     395  def addItem(self, name, rating):
     396    itemXml = ET.Element("board", {"name": name, "rating": rating})
     397    self.xml.append(itemXml)
     398
     399## Class for custom gamereport stanza extension ##
     400class GameReportXmppPlugin(ElementBase):
     401  name = 'report'
     402  namespace = 'jabber:iq:gamereport'
     403  plugin_attrib = 'gamereport'
     404  interfaces = ('game', 'sender')
     405  sub_interfaces = interfaces
     406  def addSender(self, sender):
     407    senderXml = ET.fromstring("<sender>%s</sender>" % sender)
     408    self.xml.append(senderXml)
     409  def getGame(self):
     410    """
     411      Required to parse incoming stanzas with this
     412        extension.
     413    """
     414    game = self.xml.find('{%s}game' % self.namespace)
     415    data = {}
     416    for key, item in game.items():
     417      data[key] = item
     418    return data
     419
     420## Class for custom profile ##
     421class ProfileXmppPlugin(ElementBase):
     422  name = 'query'
     423  namespace = 'jabber:iq:profile'
     424  interfaces = set(('profile', 'command', 'recipient'))
     425  sub_interfaces = interfaces
     426  plugin_attrib = 'profile'
     427  def addCommand(self, command):
     428    commandXml = ET.fromstring("<command>%s</command>" % command)
     429    self.xml.append(commandXml)
     430  def addRecipient(self, recipient):
     431    recipientXml = ET.fromstring("<recipient>%s</recipient>" % recipient)
     432    self.xml.append(recipientXml)
     433  def addItem(self, player, rating, highestRating, rank, totalGamesPlayed, wins, losses):
     434    itemXml = ET.Element("profile", {"player": player, "rating": rating, "highestRating": highestRating,
     435                                      "rank" : rank, "totalGamesPlayed" : totalGamesPlayed, "wins" : wins,
     436                                      "losses" : losses})
     437    self.xml.append(itemXml)
     438
     439## Main class which handles IQ data and sends new data ##
     440class EcheLOn(sleekxmpp.ClientXMPP):
     441  """
     442  A simple list provider
     443  """
     444  def __init__(self, sjid, password, room, nick):
     445    sleekxmpp.ClientXMPP.__init__(self, sjid, password)
     446    self.sjid = sjid
     447    self.room = room
     448    self.nick = nick
     449
     450    # Init leaderboard object
     451    self.leaderboard = LeaderboardList(room)
     452
     453    # gameReport to leaderboard abstraction
     454    self.reportManager = ReportManager(self.leaderboard)
     455
     456    # Store mapping of nicks and XmppIDs, attached via presence stanza
     457    self.nicks = {}
     458
     459    self.lastLeft = ""
     460
     461    register_stanza_plugin(Iq, PlayerXmppPlugin)
     462    register_stanza_plugin(Iq, BoardListXmppPlugin)
     463    register_stanza_plugin(Iq, GameReportXmppPlugin)
     464    register_stanza_plugin(Iq, ProfileXmppPlugin)
     465
     466    self.register_handler(Callback('Iq Player',
     467                                       StanzaPath('iq/player'),
     468                                       self.iqhandler,
     469                                       instream=True))
     470    self.register_handler(Callback('Iq Boardlist',
     471                                       StanzaPath('iq/boardlist'),
     472                                       self.iqhandler,
     473                                       instream=True))
     474    self.register_handler(Callback('Iq GameReport',
     475                                       StanzaPath('iq/gamereport'),
     476                                       self.iqhandler,
     477                                       instream=True))
     478    self.register_handler(Callback('Iq Profile',
     479                                       StanzaPath('iq/profile'),
     480                                       self.iqhandler,
     481                                       instream=True))
     482
     483    self.add_event_handler("session_start", self.start)
     484    self.add_event_handler("muc::%s::got_online" % self.room, self.muc_online)
     485    self.add_event_handler("muc::%s::got_offline" % self.room, self.muc_offline)
     486
     487  def start(self, event):
     488    """
     489    Process the session_start event
     490    """
     491    self.plugin['xep_0045'].joinMUC(self.room, self.nick)   
     492    self.send_presence()
     493    self.get_roster()
     494    logging.info("EcheLOn started")
     495
     496  def muc_online(self, presence):
     497    """
     498    Process presence stanza from a chat room.
     499    """
     500    if presence['muc']['nick'] != self.nick:
     501      # If it doesn't already exist, store player JID mapped to their nick.
     502      if str(presence['muc']['jid']) not in self.nicks:
     503        self.nicks[str(presence['muc']['jid'])] = presence['muc']['nick']
     504      # Check the jid isn't already in the lobby.
     505      logging.debug("Client '%s' connected with a nick of '%s'." %(presence['muc']['jid'], presence['muc']['nick']))
     506
     507  def muc_offline(self, presence):
     508    """
     509    Process presence stanza from a chat room.
     510    """
     511    # Clean up after a player leaves
     512    if presence['muc']['nick'] != self.nick:
     513      # Remove them from the local player list.
     514      self.lastLeft = str(presence['muc']['jid'])
     515      if str(presence['muc']['jid']) in self.nicks:
     516        del self.nicks[str(presence['muc']['jid'])]
     517
     518  def iqhandler(self, iq):
     519    """
     520    Handle the custom stanzas
     521      This method should be very robust because we could receive anything
     522    """
     523    if iq['type'] == 'error':
     524      logging.error('iqhandler error' + iq['error']['condition'])
     525      #self.disconnect()
     526    elif iq['type'] == 'get':
     527      """
     528      Request lists.
     529      """
     530      if 'boardlist' in iq.plugins:
     531        command = iq['boardlist']['command']
     532        recipient = iq['boardlist']['recipient']
     533        if command == 'getleaderboard':
     534          try:
     535            self.leaderboard.getOrCreatePlayer(iq['from'])
     536            self.sendBoardList(iq['from'], recipient)
     537          except:
     538            traceback.print_exc()
     539            logging.error("Failed to process leaderboardlist request from %s" % iq['from'].bare)
     540        elif command == 'getratinglist':
     541          try:
     542            self.sendRatingList(iq['from']);
     543          except:
     544            traceback.print_exc()
     545        else:
     546          logging.error("Failed to process boardlist request from %s" % iq['from'].bare)
     547      elif 'profile' in iq.plugins:
     548        command = iq['profile']['command']
     549        recipient = iq['profile']['recipient']
     550        try:
     551          self.sendProfile(iq['from'], command, recipient)
     552        except:
     553          try:
     554            self.sendProfileNotFound(iq['from'], command, recipient)
     555          except:
     556            logging.debug("No record found for %s" % command)
     557      else:
     558        logging.error("Unknown 'get' type stanza request from %s" % iq['from'].bare)
     559    elif iq['type'] == 'result':
     560      """
     561      Iq successfully received
     562      """
     563      pass
     564    elif iq['type'] == 'set':
     565      if 'gamereport' in iq.plugins:
     566        """
     567        Client is reporting end of game statistics
     568        """
     569        try:
     570          self.reportManager.addReport(iq['gamereport']['sender'], iq['gamereport']['game'])
     571          if self.leaderboard.getLastRatedMessage() != "":
     572            self.send_message(mto=self.room, mbody=self.leaderboard.getLastRatedMessage(), mtype="groupchat",
     573              mnick=self.nick)
     574            self.sendRatingList(iq['from'])
     575        except:
     576          traceback.print_exc()
     577          logging.error("Failed to update game statistics for %s" % iq['from'].bare)
     578      elif 'player' in iq.plugins:
     579        player = iq['player']['online']
     580        #try:
     581        self.leaderboard.getOrCreatePlayer(player)
     582        #except:
     583          #logging.debug("Could not create new user %s" % player)
     584    else:
     585       logging.error("Failed to process stanza type '%s' received from %s" % iq['type'], iq['from'].bare)
     586
     587  def sendBoardList(self, to, recipient):
     588    """
     589      Send the whole leaderboard list.
     590      If no target is passed the boardlist is broadcasted
     591        to all clients.
     592    """
     593    ## Pull leaderboard data and add it to the stanza 
     594    board = self.leaderboard.getBoard()
     595    stz = BoardListXmppPlugin()
     596    iq = self.Iq()
     597    iq['type'] = 'result'
     598    for i in board:
     599      stz.addItem(board[i]['name'], board[i]['rating'])
     600    stz.addCommand('boardlist')
     601    stz.addRecipient(recipient)
     602    iq.setPayload(stz)
     603    ## Check recipient exists
     604    if str(to) not in self.nicks:
     605      logging.error("No player with the XmPP ID '%s' known to send boardlist to" % str(to))
     606      return
     607    ## Set additional IQ attributes
     608    iq['to'] = to
     609    ## Try sending the stanza
     610    try:
     611      iq.send(block=False, now=True)
     612    except:
     613      logging.error("Failed to send leaderboard list")
     614
     615  def sendRatingList(self, to):
     616    """
     617      Send the rating list.
     618    """
     619    ## Pull rating list data and add it to the stanza 
     620    ratinglist = self.leaderboard.getRatingList(self.nicks)
     621    stz = BoardListXmppPlugin()
     622    iq = self.Iq()
     623    iq['type'] = 'result'
     624    for i in ratinglist:
     625      stz.addItem(ratinglist[i]['name'], ratinglist[i]['rating'])
     626    stz.addCommand('ratinglist')
     627    iq.setPayload(stz)
     628    ## Check recipient exists
     629    if str(to) not in self.nicks:
     630      logging.error("No player with the XmPP ID '%s' known to send ratinglist to" % str(to))
     631      return
     632    ## Set additional IQ attributes
     633    iq['to'] = to
     634    ## Try sending the stanza
     635    try:
     636      iq.send(block=False, now=True)
     637    except:
     638      logging.error("Failed to send rating list")
     639
     640  def sendProfile(self, to, player, recipient):
     641    """
     642      Send the player profile to a specified target.
     643    """
     644    if to == "":
     645      logging.error("Failed to send profile")
     646      return
     647
     648    online = False;
     649    ## Pull stats and add it to the stanza
     650    for JID in list(self.nicks):
     651      if self.nicks[JID] == player:
     652        stats = self.leaderboard.getProfile(JID)
     653        online = True
     654        break
     655
     656    if online == False:
     657      stats = self.leaderboard.getProfile(player + "@" + str(recipient).split('@')[1])
     658    stz = ProfileXmppPlugin()
     659    iq = self.Iq()
     660    iq['type'] = 'result'
     661
     662    stz.addItem(player, stats['rating'], stats['highestRating'], stats['rank'], stats['totalGamesPlayed'], stats['wins'], stats['losses'])
     663    stz.addCommand(player)
     664    stz.addRecipient(recipient)
     665    iq.setPayload(stz)
     666    ## Check recipient exists
     667    if str(to) not in self.nicks:
     668      logging.error("No player with the XmPP ID '%s' known to send profile to" % str(to))
     669      return
     670
     671    ## Set additional IQ attributes
     672    iq['to'] = to
     673
     674    ## Try sending the stanza
     675    try:
     676      iq.send(block=False, now=True)
     677    except:
     678      traceback.print_exc()
     679      logging.error("Failed to send profile")
     680
     681  def sendProfileNotFound(self, to, player, recipient):
     682    """
     683      Send a profile not-found error to a specified target.
     684    """
     685    stz = ProfileXmppPlugin()
     686    iq = self.Iq()
     687    iq['type'] = 'result'
     688
     689    filler = str(0)
     690    stz.addItem(player, str(-2), filler, filler, filler, filler, filler)
     691    stz.addCommand(player)
     692    stz.addRecipient(recipient)
     693    iq.setPayload(stz)
     694    ## Check recipient exists
     695    if str(to) not in self.nicks:
     696      logging.error("No player with the XmPP ID '%s' known to send profile to" % str(to))
     697      return
     698
     699    ## Set additional IQ attributes
     700    iq['to'] = to
     701
     702    ## Try sending the stanza
     703    try:
     704      iq.send(block=False, now=True)
     705    except:
     706      traceback.print_exc()
     707      logging.error("Failed to send profile")
     708
     709## Main Program ##
     710if __name__ == '__main__':
     711  # Setup the command line arguments.
     712  optp = OptionParser()
     713
     714  # Output verbosity options.
     715  optp.add_option('-q', '--quiet', help='set logging to ERROR',
     716                  action='store_const', dest='loglevel',
     717                  const=logging.ERROR, default=logging.INFO)
     718  optp.add_option('-d', '--debug', help='set logging to DEBUG',
     719                  action='store_const', dest='loglevel',
     720                  const=logging.DEBUG, default=logging.INFO)
     721  optp.add_option('-v', '--verbose', help='set logging to COMM',
     722                  action='store_const', dest='loglevel',
     723                  const=5, default=logging.INFO)
     724
     725  # EcheLOn configuration options
     726  optp.add_option('-m', '--domain', help='set EcheLOn domain',
     727                  action='store', dest='xdomain',
     728                  default="lobby.wildfiregames.com")
     729  optp.add_option('-l', '--login', help='set EcheLOn login',
     730                  action='store', dest='xlogin',
     731                  default="EcheLOn")
     732  optp.add_option('-p', '--password', help='set EcheLOn password',
     733                  action='store', dest='xpassword',
     734                  default="XXXXXX")
     735  optp.add_option('-n', '--nickname', help='set EcheLOn nickname',
     736                  action='store', dest='xnickname',
     737                  default="Ratings")
     738  optp.add_option('-r', '--room', help='set muc room to join',
     739                  action='store', dest='xroom',
     740                  default="arena")
     741
     742  opts, args = optp.parse_args()
     743
     744  # Setup logging.
     745  logging.basicConfig(level=opts.loglevel,
     746                      format='%(asctime)s        %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
     747
     748  # EcheLOn
     749  xmpp = EcheLOn(opts.xlogin+'@'+opts.xdomain+'/CC', opts.xpassword, opts.xroom+'@conference.'+opts.xdomain, opts.xnickname)
     750  xmpp.register_plugin('xep_0030') # Service Discovery
     751  xmpp.register_plugin('xep_0004') # Data Forms
     752  xmpp.register_plugin('xep_0045') # Multi-User Chat    # used
     753  xmpp.register_plugin('xep_0060') # PubSub
     754  xmpp.register_plugin('xep_0199') # XMPP Ping
     755
     756  if xmpp.connect():
     757    xmpp.process(threaded=False)
     758  else:
     759    logging.error("Unable to connect")
  • source/tools/XpartaMuPP/README

     
    6161
    6262Run XpartaMuPP - XMPP Multiplayer Game Manager
    6363==============================================
    64 You need to have python 3 and SleekXmpp installed
     64You need to have python 3 and SleekXmpp installed (tested for 1.0-beta5)
    6565    $ sudo apt-get install python3 python3-sleekxmpp
    6666
    6767If you would like to run the leaderboard database,
  • source/tools/XpartaMuPP/XpartaMuPP.py

     
    11#!/usr/bin/env python3
    22# -*- coding: utf-8 -*-
    3 """Copyright (C) 2014 Wildfire Games.
     3"""Copyright (C) 2016 Wildfire Games.
    44 * This file is part of 0 A.D.
    55 *
    66 * 0 A.D. is free software: you can redistribute it and/or modify
     
    2626from sleekxmpp.xmlstream.handler import Callback
    2727from sleekxmpp.xmlstream.matcher import StanzaPath
    2828
    29 from sqlalchemy import func
    30 
    31 from LobbyRanking import session as db, Game, Player, PlayerInfo
    32 from ELO import get_rating_adjustment
    33 # Rating that new players should be inserted into the
    34 # database with, before they've played any games.
    35 leaderboard_default_rating = 1200
    36 
    37 ## Class that contains and manages leaderboard data ##
    38 class LeaderboardList():
    39   def __init__(self, room):
    40     self.room = room
    41     self.lastRated = ""
    42 
    43   def getProfile(self, JID):
    44     """
    45       Retrieves the profile for the specified JID
    46     """
    47     stats = {}
    48     player = db.query(Player).filter(Player.jid.ilike(str(JID)))
    49     if not player.first():
    50       return
    51     if player.first().rating != -1:
    52       stats['rating'] = str(player.first().rating)
    53 
    54     if player.first().highest_rating != -1:
    55       stats['highestRating'] = str(player.first().highest_rating)
    56 
    57     playerID = player.first().id
    58     players = db.query(Player).filter(Player.rating != -1).order_by(Player.rating.desc()).all()
    59 
    60     for rank, user in enumerate(players):
    61       if (user.jid.lower() == JID.lower()):
    62         stats['rank'] = str(rank+1)
    63         break
    64 
    65     stats['totalGamesPlayed'] = str(db.query(PlayerInfo).filter_by(player_id=playerID).count())
    66     stats['wins'] = str(db.query(Game).filter_by(winner_id=playerID).count())
    67     stats['losses'] = str(db.query(PlayerInfo).filter_by(player_id=playerID).count() - db.query(Game).filter_by(winner_id=playerID).count())
    68     return stats
    69 
    70   def getOrCreatePlayer(self, JID):
    71     """
    72       Stores a player(JID) in the database if they don't yet exist.
    73       Returns either the newly created instance of
    74       the Player model, or the one that already
    75       exists in the database.
    76     """
    77     players = db.query(Player).filter_by(jid=str(JID))
    78     if not players.first():
    79       player = Player(jid=str(JID), rating=-1)
    80       db.add(player)
    81       db.commit()
    82       return player
    83     return players.first()
    84 
    85   def removePlayer(self, JID):
    86     """
    87       Remove a player(JID) from database.
    88       Returns the player that was removed, or None
    89       if that player didn't exist.
    90     """
    91     players = db.query(Player).filter_by(jid=JID)
    92     player = players.first()
    93     if not player:
    94       return None
    95     players.delete()
    96     return player
    97 
    98   def addGame(self, gamereport):
    99     """
    100       Adds a game to the database and updates the data
    101       on a player(JID) from game results.
    102       Returns the created Game object, or None if
    103       the creation failed for any reason.
    104       Side effects:
    105         Inserts a new Game instance into the database.
    106     """
    107     # Discard any games still in progress.
    108     if any(map(lambda state: state == 'active',
    109                dict.values(gamereport['playerStates']))):
    110       return None
    111 
    112     players = map(lambda jid: db.query(Player).filter(Player.jid.ilike(str(jid))).first(),
    113                   dict.keys(gamereport['playerStates']))
    114 
    115     winning_jid = list(dict.keys({jid: state for jid, state in
    116                                   gamereport['playerStates'].items()
    117                                   if state == 'won'}))[0]
    118     def get(stat, jid):
    119       return gamereport[stat][jid]
    120 
    121     singleStats = {'timeElapsed', 'mapName', 'teamsLocked', 'matchID'}
    122     totalScoreStats = {'economyScore', 'militaryScore', 'totalScore'}
    123     resourceStats = {'foodGathered', 'foodUsed', 'woodGathered', 'woodUsed',
    124             'stoneGathered', 'stoneUsed', 'metalGathered', 'metalUsed', 'vegetarianFoodGathered',
    125             'treasuresCollected', 'lootCollected', 'tributesSent', 'tributesReceived'}
    126     unitsStats = {'totalUnitsTrained', 'totalUnitsLost', 'enemytotalUnitsKilled', 'infantryUnitsTrained',
    127             'infantryUnitsLost', 'enemyInfantryUnitsKilled', 'workerUnitsTrained', 'workerUnitsLost',
    128             'enemyWorkerUnitsKilled', 'femaleUnitsTrained', 'femaleUnitsLost', 'enemyFemaleUnitsKilled',
    129             'cavalryUnitsTrained', 'cavalryUnitsLost', 'enemyCavalryUnitsKilled', 'championUnitsTrained',
    130             'championUnitsLost', 'enemyChampionUnitsKilled', 'heroUnitsTrained', 'heroUnitsLost',
    131             'enemyHeroUnitsKilled', 'shipUnitsTrained', 'shipUnitsLost', 'enemyShipUnitsKilled', 'traderUnitsTrained',
    132             'traderUnitsLost', 'enemyTraderUnitsKilled'}
    133     buildingsStats = {'totalBuildingsConstructed', 'totalBuildingsLost', 'enemytotalBuildingsDestroyed',
    134             'civCentreBuildingsConstructed', 'civCentreBuildingsLost', 'enemyCivCentreBuildingsDestroyed',
    135             'houseBuildingsConstructed', 'houseBuildingsLost', 'enemyHouseBuildingsDestroyed',
    136             'economicBuildingsConstructed', 'economicBuildingsLost', 'enemyEconomicBuildingsDestroyed',
    137             'outpostBuildingsConstructed', 'outpostBuildingsLost', 'enemyOutpostBuildingsDestroyed',
    138             'militaryBuildingsConstructed', 'militaryBuildingsLost', 'enemyMilitaryBuildingsDestroyed',
    139             'fortressBuildingsConstructed', 'fortressBuildingsLost', 'enemyFortressBuildingsDestroyed',
    140             'wonderBuildingsConstructed', 'wonderBuildingsLost', 'enemyWonderBuildingsDestroyed'}
    141     marketStats = {'woodBought', 'foodBought', 'stoneBought', 'metalBought', 'tradeIncome'}
    142     miscStats = {'civs', 'teams', 'percentMapExplored'}
    143 
    144     stats = totalScoreStats | resourceStats | unitsStats | buildingsStats | marketStats | miscStats
    145     playerInfos = []
    146     for player in players:
    147       jid = player.jid
    148       playerinfo = PlayerInfo(player=player)
    149       for reportname in stats:
    150         setattr(playerinfo, reportname, get(reportname, jid.lower()))
    151       playerInfos.append(playerinfo)
    152 
    153     game = Game(map=gamereport['mapName'], duration=int(gamereport['timeElapsed']), teamsLocked=bool(gamereport['teamsLocked']), matchID=gamereport['matchID'])
    154     game.players.extend(players)
    155     game.player_info.extend(playerInfos)
    156     game.winner = db.query(Player).filter(Player.jid.ilike(str(winning_jid))).first()
    157     db.add(game)
    158     db.commit()
    159     return game
    160  
    161   def verifyGame(self, gamereport):
    162     """
    163       Returns a boolean based on whether the game should be rated.
    164       Here, we can specify the criteria for rated games.
    165     """
    166     winning_jids = list(dict.keys({jid: state for jid, state in
    167                                   gamereport['playerStates'].items()
    168                                   if state == 'won'}))
    169     # We only support 1v1s right now. TODO: Support team games.
    170     if len(winning_jids) * 2 > len(dict.keys(gamereport['playerStates'])):
    171       # More than half the people have won. This is not a balanced team game or duel.
    172       return False
    173     if len(dict.keys(gamereport['playerStates'])) != 2:
    174       return False
    175     return True
    176 
    177   def rateGame(self, game):
    178     """
    179       Takes a game with 2 players and alters their ratings
    180       based on the result of the game.
    181       Returns self.
    182       Side effects:
    183         Changes the game's players' ratings in the database.
    184     """
    185     player1 = game.players[0]
    186     player2 = game.players[1]
    187     # TODO: Support draws. Since it's impossible to draw in the game currently,
    188     # the database model, and therefore this code, requires a winner.
    189     # The Elo implementation does not, however.
    190     result = 1 if player1 == game.winner else -1
    191     # Player's ratings are -1 unless they have played a rated game.
    192     if player1.rating == -1:
    193       player1.rating = leaderboard_default_rating
    194     if player2.rating == -1:
    195       player2.rating = leaderboard_default_rating
    196 
    197     rating_adjustment1 = int(get_rating_adjustment(player1.rating, player2.rating,
    198       len(player1.games), len(player2.games), result))
    199     rating_adjustment2 = int(get_rating_adjustment(player2.rating, player1.rating,
    200       len(player2.games), len(player1.games), result * -1))
    201     if result == 1:
    202       resultQualitative = "won"
    203     elif result == 0:
    204       resultQualitative = "drew"
    205     else:
    206       resultQualitative = "lost"
    207     name1 = '@'.join(player1.jid.split('@')[:-1])
    208     name2 = '@'.join(player2.jid.split('@')[:-1])
    209     self.lastRated = "A rated game has ended. %s %s against %s. Rating Adjustment: %s (%s -> %s) and %s (%s -> %s)."%(name1,
    210       resultQualitative, name2, name1, player1.rating, player1.rating + rating_adjustment1,
    211       name2, player2.rating, player2.rating + rating_adjustment2)
    212     player1.rating += rating_adjustment1
    213     player2.rating += rating_adjustment2
    214     if not player1.highest_rating:
    215       player1.highest_rating = -1
    216     if not player2.highest_rating:
    217       player2.highest_rating = -1
    218     if player1.rating > player1.highest_rating:
    219       player1.highest_rating = player1.rating
    220     if player2.rating > player2.highest_rating:
    221       player2.highest_rating = player2.rating
    222     db.commit()
    223     return self
    224 
    225   def getLastRatedMessage(self):
    226     """
    227       Gets the string of the last rated game. Triggers an update
    228       chat for the bot.
    229     """
    230     return self.lastRated
    231 
    232   def addAndRateGame(self, gamereport):
    233     """
    234       Calls addGame and if the game has only two
    235       players, also calls rateGame.
    236       Returns the result of addGame.
    237     """
    238     game = self.addGame(gamereport)
    239     if game and self.verifyGame(gamereport):
    240       self.rateGame(game)
    241     else:
    242       self.lastRated = ""
    243     return game
    244 
    245   def getBoard(self):
    246     """
    247       Returns a dictionary of player rankings to
    248         JIDs for sending.
    249     """
    250     board = {}
    251     players = db.query(Player).filter(Player.rating != -1).order_by(Player.rating.desc()).limit(100).all()
    252     for rank, player in enumerate(players):
    253       board[player.jid] = {'name': '@'.join(player.jid.split('@')[:-1]), 'rating': str(player.rating)}
    254     return board
    255   def getRatingList(self, nicks):
    256     """
    257     Returns a rating list of players
    258     currently in the lobby by nick
    259     because the client can't link
    260     JID to nick conveniently.
    261     """
    262     ratinglist = {}
    263     for JID in list(nicks):
    264       players = db.query(Player.jid, Player.rating).filter(Player.jid.ilike(str(JID)))
    265       if players.first():
    266         if players.first().rating == -1:
    267           ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': ''}
    268         else:
    269           ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': str(players.first().rating)}
    270       else:
    271         ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': ''}
    272     return ratinglist
    273 
    27429## Class to tracks all games in the lobby ##
    27530class GameList():
    27631  def __init__(self):
     
    30863        self.gameList[JID]['nbp'] = data['nbp']
    30964        self.gameList[JID]['state'] = 'running'
    31065
    311 ## Class which manages different game reports from clients ##
    312 ##   and calls leaderboard functions as appropriate.       ##
    313 class ReportManager():
    314   def __init__(self, leaderboard):
    315     self.leaderboard = leaderboard
    316     self.interimReportTracker = []
    317     self.interimJIDTracker = []
     66## Class for custom player stanza extension ##
     67class PlayerXmppPlugin(ElementBase):
     68  name = 'query'
     69  namespace = 'jabber:iq:player'
     70  interfaces = set(('online'))
     71  sub_interfaces = interfaces
     72  plugin_attrib = 'player'
    31873
    319   def addReport(self, JID, rawGameReport):
    320     """
    321       Adds a game to the interface between a raw report
    322         and the leaderboard database.
    323     """
    324     # cleanRawGameReport is a copy of rawGameReport with all reporter specific information removed.
    325     cleanRawGameReport = rawGameReport.copy()
    326     del cleanRawGameReport["playerID"]
     74  def addPlayerOnline(self, player):
     75    playerXml = ET.fromstring("<online>%s</online>" % player)
     76    self.xml.append(playerXml)
    32777
    328     if cleanRawGameReport not in self.interimReportTracker:
    329       # Store the game.
    330       appendIndex = len(self.interimReportTracker)
    331       self.interimReportTracker.append(cleanRawGameReport)
    332       # Initilize the JIDs and store the initial JID.
    333       numPlayers = self.getNumPlayers(rawGameReport)
    334       JIDs = [None] * numPlayers
    335       if numPlayers - int(rawGameReport["playerID"]) > -1:
    336         JIDs[int(rawGameReport["playerID"])-1] = str(JID).lower()
    337       self.interimJIDTracker.append(JIDs)
    338     else:
    339       # We get the index at which the JIDs coresponding to the game are stored.
    340       index = self.interimReportTracker.index(cleanRawGameReport)
    341       # We insert the new report JID into the acending list of JIDs for the game.
    342       JIDs = self.interimJIDTracker[index]
    343       if len(JIDs) - int(rawGameReport["playerID"]) > -1:
    344         JIDs[int(rawGameReport["playerID"])-1] = str(JID).lower()
    345       self.interimJIDTracker[index] = JIDs
    346 
    347     self.checkFull()
    348 
    349   def expandReport(self, rawGameReport, JIDs):
    350     """
    351       Takes an raw game report and re-formats it into
    352         Python data structures leaving JIDs empty.
    353       Returns a processed gameReport of type dict.
    354     """
    355     processedGameReport = {}
    356     for key in rawGameReport:
    357       if rawGameReport[key].find(",") == -1:
    358         processedGameReport[key] = rawGameReport[key]
    359       else:
    360         split = rawGameReport[key].split(",")
    361         # Remove the false split positive.
    362         split.pop()
    363         statToJID = {}
    364         for i, part in enumerate(split):
    365            statToJID[JIDs[i]] = part
    366         processedGameReport[key] = statToJID
    367     return processedGameReport
    368 
    369   def checkFull(self):
    370     """
    371       Searches internal database to check if enough
    372         reports have been submitted to add a game to
    373         the leaderboard. If so, the report will be
    374         interpolated and addAndRateGame will be
    375         called with the result.
    376     """
    377     i = 0
    378     length = len(self.interimReportTracker)
    379     while(i < length):
    380       numPlayers = self.getNumPlayers(self.interimReportTracker[i])
    381       numReports = 0
    382       for JID in self.interimJIDTracker[i]:
    383         if JID != None:
    384           numReports += 1
    385       if numReports == numPlayers:
    386         self.leaderboard.addAndRateGame(self.expandReport(self.interimReportTracker[i], self.interimJIDTracker[i]))
    387         del self.interimJIDTracker[i]
    388         del self.interimReportTracker[i]
    389         length -= 1
    390       else:
    391         i += 1
    392         self.leaderboard.lastRated = ""
    393 
    394   def getNumPlayers(self, rawGameReport):
    395     """
    396       Computes the number of players in a raw gameReport.
    397       Returns int, the number of players.
    398     """
    399     # Find a key in the report which holds values for multiple players.
    400     for key in rawGameReport:
    401       if rawGameReport[key].find(",") != -1:
    402         # Count the number of values, minus one for the false split positive.
    403         return len(rawGameReport[key].split(","))-1
    404     # Return -1 in case of failure.
    405     return -1
    406 
    40778## Class for custom gamelist stanza extension ##
    40879class GameListXmppPlugin(ElementBase):
    40980  name = 'query'
     
    431102class BoardListXmppPlugin(ElementBase):
    432103  name = 'query'
    433104  namespace = 'jabber:iq:boardlist'
    434   interfaces = set(('board', 'command'))
     105  interfaces = set(('board', 'command', 'recipient'))
    435106  sub_interfaces = interfaces
    436107  plugin_attrib = 'boardlist'
    437108  def addCommand(self, command):
    438109    commandXml = ET.fromstring("<command>%s</command>" % command)
    439110    self.xml.append(commandXml)
     111  def addRecipient(self, recipient):
     112    recipientXml = ET.fromstring("<recipient>%s</recipient>" % recipient)
     113    self.xml.append(recipientXml)
    440114  def addItem(self, name, rating):
    441115    itemXml = ET.Element("board", {"name": name, "rating": rating})
    442116    self.xml.append(itemXml)
     
    446120  name = 'report'
    447121  namespace = 'jabber:iq:gamereport'
    448122  plugin_attrib = 'gamereport'
    449   interfaces = ('game')
     123  interfaces = ('game', 'sender')
    450124  sub_interfaces = interfaces
    451 
     125  def addSender(self, sender):
     126    senderXml = ET.fromstring("<sender>%s</sender>" % sender)
     127    self.xml.append(senderXml)
     128  def addGame(self, gr):
     129    game = ET.fromstring(str(gr)).find('{%s}game' % self.namespace)
     130    self.xml.append(game)
    452131  def getGame(self):
    453132    """
    454133      Required to parse incoming stanzas with this
     
    464143class ProfileXmppPlugin(ElementBase):
    465144  name = 'query'
    466145  namespace = 'jabber:iq:profile'
    467   interfaces = set(('profile', 'command'))
     146  interfaces = set(('profile', 'command', 'recipient'))
    468147  sub_interfaces = interfaces
    469148  plugin_attrib = 'profile'
    470149  def addCommand(self, command):
    471150    commandXml = ET.fromstring("<command>%s</command>" % command)
    472151    self.xml.append(commandXml)
     152  def addRecipient(self, recipient):
     153    recipientXml = ET.fromstring("<recipient>%s</recipient>" % recipient)
     154    self.xml.append(recipientXml)
    473155  def addItem(self, player, rating, highestRating, rank, totalGamesPlayed, wins, losses):
    474156    itemXml = ET.Element("profile", {"player": player, "rating": rating, "highestRating": highestRating,
    475157                                      "rank" : rank, "totalGamesPlayed" : totalGamesPlayed, "wins" : wins,
     
    481163  """
    482164  A simple list provider
    483165  """
    484   def __init__(self, sjid, password, room, nick):
     166  def __init__(self, sjid, password, room, nick, ratingsbot):
    485167    sleekxmpp.ClientXMPP.__init__(self, sjid, password)
    486168    self.sjid = sjid
    487169    self.room = room
    488170    self.nick = nick
    489171
    490     self.ratingListUpdate = False
     172    self.ratingsBot = ratingsbot
    491173    # Game collection
    492174    self.gameList = GameList()
    493175
    494     # Init leaderboard object
    495     self.leaderboard = LeaderboardList(room)
    496 
    497     # gameReport to leaderboard abstraction
    498     self.reportManager = ReportManager(self.leaderboard)
    499 
    500176    # Store mapping of nicks and XmppIDs, attached via presence stanza
    501177    self.nicks = {}
    502178
    503179    self.lastLeft = ""
    504180
     181    register_stanza_plugin(Iq, PlayerXmppPlugin)
    505182    register_stanza_plugin(Iq, GameListXmppPlugin)
    506183    register_stanza_plugin(Iq, BoardListXmppPlugin)
    507184    register_stanza_plugin(Iq, GameReportXmppPlugin)
    508185    register_stanza_plugin(Iq, ProfileXmppPlugin)
    509186
     187    self.register_handler(Callback('Iq Player',
     188                                       StanzaPath('iq/player'),
     189                                       self.iqhandler,
     190                                       instream=True))
    510191    self.register_handler(Callback('Iq Gamelist',
    511192                                       StanzaPath('iq/gamelist'),
    512193                                       self.iqhandler,
     
    519200                                       StanzaPath('iq/gamereport'),
    520201                                       self.iqhandler,
    521202                                       instream=True))
    522                                        
    523203    self.register_handler(Callback('Iq Profile',
    524204                                       StanzaPath('iq/profile'),
    525205                                       self.iqhandler,
     
    543223    """
    544224    Process presence stanza from a chat room.
    545225    """
    546     self.ratingListUpdate = True
     226    if self.ratingsBot in self.nicks:
     227      self.relayRatingListRequest(self.ratingsBot)
     228    self.relayPlayerOnline(presence['muc']['jid'])
    547229    if presence['muc']['nick'] != self.nick:
    548230      # If it doesn't already exist, store player JID mapped to their nick.
    549231      if str(presence['muc']['jid']) not in self.nicks:
     
    551233      # Check the jid isn't already in the lobby.
    552234      # Send Gamelist to new player.
    553235      self.sendGameList(presence['muc']['jid'])
    554       # Following two calls make sqlalchemy complain about using objects in the
    555       #  incorrect thread. TODO: Figure out how to fix this.
    556       # Send Leaderboard to new player.
    557       #self.sendBoardList(presence['muc']['jid'])
    558       # Register on leaderboard.
    559       #self.leaderboard.getOrCreatePlayer(presence['muc']['jid'])
    560236      logging.debug("Client '%s' connected with a nick of '%s'." %(presence['muc']['jid'], presence['muc']['nick']))
    561237
    562238  def muc_offline(self, presence):
     
    607283          logging.error("Failed to process gamelist request from %s" % iq['from'].bare)
    608284      elif 'boardlist' in iq.plugins:
    609285        command = iq['boardlist']['command']
    610         if command == 'getleaderboard':
    611           try:
    612             self.leaderboard.getOrCreatePlayer(iq['from'])
    613             self.sendBoardList(iq['from'])
    614           except:
    615             traceback.print_exc()
    616             logging.error("Failed to process leaderboardlist request from %s" % iq['from'].bare)
    617         else:
    618           logging.error("Failed to process boardlist request from %s" % iq['from'].bare)
     286        try:
     287          self.relayBoardListRequest(iq['from']);
     288        except:
     289          traceback.print_exc()
     290          logging.error("Failed to process leaderboardlist request from %s" % iq['from'].bare)
    619291      elif 'profile' in iq.plugins:
    620292        command = iq['profile']['command']
    621293        try:
    622           self.sendProfile(iq['from'], command)
     294          self.relayProfileRequest(iq['from'], command)
    623295        except:
    624           try:
    625             self.sendProfileNotFound(iq['from'], command)
    626           except:
    627             logging.debug("No record found for %s" % command)
     296          pass
    628297      else:
    629298        logging.error("Unknown 'get' type stanza request from %s" % iq['from'].bare)
    630299    elif iq['type'] == 'result':
     
    631300      """
    632301      Iq successfully received
    633302      """
    634       pass
     303      if 'boardlist' in iq.plugins:
     304        recipient = iq['boardlist']['recipient']
     305        self.relayBoardList(iq['boardlist'], recipient)
     306      elif 'profile' in iq.plugins:
     307        recipient = iq['profile']['recipient']
     308        player =  iq['profile']['command']
     309        self.relayProfile(iq['profile'], player, recipient)
     310      else:
     311        pass
    635312    elif iq['type'] == 'set':
    636313      if 'gamelist' in iq.plugins:
    637314        """
     
    670347        Client is reporting end of game statistics
    671348        """
    672349        try:
    673           self.reportManager.addReport(iq['from'], iq['gamereport']['game'])
    674           if self.leaderboard.getLastRatedMessage() != "":
    675             self.send_message(mto=self.room, mbody=self.leaderboard.getLastRatedMessage(), mtype="groupchat",
    676               mnick=self.nick)
    677             self.sendBoardList()
    678             self.sendRatingList()
     350          self.relayGameReport(iq['gamereport'], iq['from'])
    679351        except:
    680352          traceback.print_exc()
    681353          logging.error("Failed to update game statistics for %s" % iq['from'].bare)
    682354    else:
    683355       logging.error("Failed to process stanza type '%s' received from %s" % iq['type'], iq['from'].bare)
    684     if self.ratingListUpdate == True:
    685       self.sendRatingList()
    686       self.ratingListUpdate = False
    687356
    688357  def sendGameList(self, to = ""):
    689358    """
     
    736405      except:
    737406        logging.error("Failed to send game list")
    738407
    739   def sendBoardList(self, to = ""):
     408  def relayBoardListRequest(self, recipient):
    740409    """
    741       Send the whole leaderboard list.
    742       If no target is passed the boardlist is broadcasted
    743         to all clients.
     410      Send a boardListRequest to EcheLOn.
    744411    """
    745     ## Pull leaderboard data and add it to the stanza 
    746     board = self.leaderboard.getBoard()
    747412    stz = BoardListXmppPlugin()
    748413    iq = self.Iq()
    749     iq['type'] = 'result'
    750     for i in board:
    751       stz.addItem(board[i]['name'], board[i]['rating'])
    752     stz.addCommand('boardlist')
     414    iq['type'] = 'get'
     415    stz.addCommand('getleaderboard')
     416    stz.addRecipient(recipient)
    753417    iq.setPayload(stz)
    754     if to == "":   
    755       for JID in list(self.nicks):
    756         ## Set additional IQ attributes
    757         iq['to'] = JID
    758         ## Try sending the stanza
    759         try:
    760           iq.send(block=False, now=True)
    761         except:
    762           logging.error("Failed to send leaderboard list")
    763     else:
    764       ## Check recipient exists
    765       if str(to) not in self.nicks:
    766         logging.error("No player with the XmPP ID '%s' known to send boardlist to" % str(to))
    767         return
    768       ## Set additional IQ attributes
    769       iq['to'] = to
    770       ## Try sending the stanza
    771       try:
    772         iq.send(block=False, now=True)
    773       except:
    774         logging.error("Failed to send leaderboard list")
     418    ## Check recipient exists
     419    to = self.ratingsBot
     420    if to not in self.nicks:
     421      logging.error("No player with the XmPP ID '%s' known to send boardlist to" % str(to))
     422      return
     423    ## Set additional IQ attributes
     424    iq['to'] = to
     425    ## Try sending the stanza
     426    try:
     427      iq.send(block=False, now=True)
     428    except:
     429      logging.error("Failed to send leaderboard list request")
     430     
     431  def relayRatingListRequest(self, recipient):
     432    """
     433      Send a ratingListRequest to EcheLOn.
     434    """
     435    stz = BoardListXmppPlugin()
     436    iq = self.Iq()
     437    iq['type'] = 'get'
     438    stz.addCommand('getratinglist')
     439    iq.setPayload(stz)
     440    ## Check recipient exists
     441    to = self.ratingsBot
     442    if to not in self.nicks:
     443      logging.error("No player with the XmPP ID '%s' known to send rating list to" % str(to))
     444      return
     445    ## Set additional IQ attributes
     446    iq['to'] = to
     447    ## Try sending the stanza
     448    try:
     449      iq.send(block=False, now=True)
     450    except:
     451      logging.error("Failed to send rating list request")
     452 
     453  def relayProfileRequest(self, recipient, player):
     454    """
     455      Send a profileRequest to EcheLOn.
     456    """
     457    stz = ProfileXmppPlugin()
     458    iq = self.Iq()
     459    iq['type'] = 'get'
     460    stz.addCommand(player)
     461    stz.addRecipient(recipient)
     462    iq.setPayload(stz)
     463    ## Check recipient exists
     464    to = self.ratingsBot
     465    if to not in self.nicks:
     466      logging.error("No player with the XmPP ID '%s' known to send rating list to" % str(to))
     467      return
     468    ## Set additional IQ attributes
     469    iq['to'] = to
     470    ## Try sending the stanza
     471    try:
     472      iq.send(block=False, now=True)
     473    except:
     474      logging.error("Failed to send profile request")
    775475
    776   def sendRatingList(self, to = ""):
     476  def relayPlayerOnline(self, jid):
    777477    """
    778       Send the rating list.
    779       If no target is passed the rating list is broadcasted
     478      Tells EcheLOn that someone comes online.
     479    """
     480    stz = PlayerXmppPlugin()
     481    iq = self.Iq()
     482    iq['type'] = 'set'
     483    stz.addPlayerOnline(jid)
     484    iq.setPayload(stz)
     485    ## Check recipient exists
     486    to = self.ratingsBot
     487    if to not in self.nicks:
     488      return
     489    ## Set additional IQ attributes
     490    iq['to'] = to
     491    ## Try sending the stanza
     492    try:
     493      iq.send(block=False, now=True)
     494    except:
     495      logging.error("Failed to send player muc online")
     496     
     497  def relayGameReport(self, data, sender):
     498    """
     499      Relay a game report to EcheLOn.
     500    """
     501    stz = GameReportXmppPlugin()
     502    stz.addGame(data)
     503    stz.addSender(sender)
     504    iq = self.Iq()
     505    iq['type'] = 'set'
     506    iq.setPayload(stz)
     507    ## Check recipient exists
     508    to = self.ratingsBot
     509    if to not in self.nicks:
     510      logging.error("No player with the XmPP ID '%s' known to send rating list to" % str(to))
     511      return
     512    ## Set additional IQ attributes
     513    iq['to'] = to
     514    ## Try sending the stanza
     515    try:
     516      iq.send(block=False, now=True)
     517    except:
     518      logging.error("Failed to send game report request")
     519
     520  def relayBoardList(self, boardList, to = ""):
     521    """
     522      Send the whole leaderboard list.
     523      If no target is passed the boardlist is broadcasted
    780524        to all clients.
    781525    """
    782     ## Pull rating list data and add it to the stanza 
    783     ratinglist = self.leaderboard.getRatingList(self.nicks)
    784     stz = BoardListXmppPlugin()
    785526    iq = self.Iq()
    786527    iq['type'] = 'result'
    787     for i in ratinglist:
    788       stz.addItem(ratinglist[i]['name'], ratinglist[i]['rating'])
    789     stz.addCommand('ratinglist')
    790     iq.setPayload(stz)
    791     if to == "":   
     528    """for i in board:
     529      stz.addItem(board[i]['name'], board[i]['rating'])
     530    stz.addCommand('boardlist')"""
     531    iq.setPayload(boardList)
     532    ## Check recipient exists
     533    if to == "": 
     534      # Rating List
    792535      for JID in list(self.nicks):
    793536        ## Set additional IQ attributes
    794537        iq['to'] = JID
     
    798541        except:
    799542          logging.error("Failed to send rating list")
    800543    else:
    801       ## Check recipient exists
     544      # Leaderboard
    802545      if str(to) not in self.nicks:
    803         logging.error("No player with the XmPP ID '%s' known to send ratinglist to" % str(to))
     546        logging.error("No player with the XmPP ID '%s' known to send boardlist to" % str(to))
    804547        return
    805548      ## Set additional IQ attributes
    806549      iq['to'] = to
     
    808551      try:
    809552        iq.send(block=False, now=True)
    810553      except:
    811         logging.error("Failed to send rating list")
     554        logging.error("Failed to send leaderboard list")
    812555
    813   def sendProfile(self, to, player):
     556  def relayProfile(self, data, player, to):
    814557    """
    815       Send the profile to a specified player.
     558      Send the player profile to a specified target.
    816559    """
    817560    if to == "":
    818       logging.error("Failed to send profile")
     561      logging.error("Failed to send profile, target unspecified")
    819562      return
    820563
    821     online = False;
    822     ## Pull stats and add it to the stanza
    823     for JID in list(self.nicks):
    824       if self.nicks[JID] == player:
    825         stats = self.leaderboard.getProfile(JID)
    826         online = True
    827         break
    828 
    829     if online == False:
    830       stats = self.leaderboard.getProfile(player + "@" + str(to).split('@')[1])
    831     stz = ProfileXmppPlugin()
    832564    iq = self.Iq()
    833565    iq['type'] = 'result'
    834 
    835     stz.addItem(player, stats['rating'], stats['highestRating'], stats['rank'], stats['totalGamesPlayed'], stats['wins'], stats['losses'])
    836     stz.addCommand(player)
    837     iq.setPayload(stz)
     566    iq.setPayload(data)
    838567    ## Check recipient exists
    839568    if str(to) not in self.nicks:
    840569      logging.error("No player with the XmPP ID '%s' known to send profile to" % str(to))
     
    850579      traceback.print_exc()
    851580      logging.error("Failed to send profile")
    852581
    853   def sendProfileNotFound(self, to, player):
    854     """
    855       Send a profile not-found error to a specified player.
    856     """
    857     stz = ProfileXmppPlugin()
    858     iq = self.Iq()
    859     iq['type'] = 'result'
    860 
    861     filler = str(0)
    862     stz.addItem(player, str(-2), filler, filler, filler, filler, filler)
    863     stz.addCommand(player)
    864     iq.setPayload(stz)
    865     ## Check recipient exists
    866     if str(to) not in self.nicks:
    867       logging.error("No player with the XmPP ID '%s' known to send profile to" % str(to))
    868       return
    869 
    870     ## Set additional IQ attributes
    871     iq['to'] = to
    872 
    873     ## Try sending the stanza
    874     try:
    875       iq.send(block=False, now=True)
    876     except:
    877       traceback.print_exc()
    878       logging.error("Failed to send profile")
    879 
    880582## Main Program ##
    881583if __name__ == '__main__':
    882584  # Setup the command line arguments.
     
    909611  optp.add_option('-r', '--room', help='set muc room to join',
    910612                  action='store', dest='xroom',
    911613                  default="arena")
     614  optp.add_option('-e', '--elo', help='set rating bot username',
     615                  action='store', dest='xratingsbot',
     616                  default="ratings")
    912617
    913618  opts, args = optp.parse_args()
    914619
     
    917622                      format='%(asctime)s        %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
    918623
    919624  # XpartaMuPP
    920   xmpp = XpartaMuPP(opts.xlogin+'@'+opts.xdomain+'/CC', opts.xpassword, opts.xroom+'@conference.'+opts.xdomain, opts.xnickname)
     625  xmpp = XpartaMuPP(opts.xlogin+'@'+opts.xdomain+'/CC', opts.xpassword, opts.xroom+'@conference.'+opts.xdomain, opts.xnickname, opts.xratingsbot+'@'+opts.xdomain+'/CC')
    921626  xmpp.register_plugin('xep_0030') # Service Discovery
    922627  xmpp.register_plugin('xep_0004') # Data Forms
    923628  xmpp.register_plugin('xep_0045') # Multi-User Chat    # used