Ticket #3022: LobbyBotSplit.patch
File LobbyBotSplit.patch, 61.3 KB (added by , 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 20 import logging, time, traceback 21 from optparse import OptionParser 22 23 import sleekxmpp 24 from sleekxmpp.stanza import Iq 25 from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin, ET 26 from sleekxmpp.xmlstream.handler import Callback 27 from sleekxmpp.xmlstream.matcher import StanzaPath 28 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 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. ## 277 class 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 ## 371 class 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 ## 383 class 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 ## 400 class 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 ## 421 class 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 ## 440 class 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 ## 710 if __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
61 61 62 62 Run XpartaMuPP - XMPP Multiplayer Game Manager 63 63 ============================================== 64 You need to have python 3 and SleekXmpp installed 64 You need to have python 3 and SleekXmpp installed (tested for 1.0-beta5) 65 65 $ sudo apt-get install python3 python3-sleekxmpp 66 66 67 67 If you would like to run the leaderboard database, -
source/tools/XpartaMuPP/XpartaMuPP.py
1 1 #!/usr/bin/env python3 2 2 # -*- coding: utf-8 -*- 3 """Copyright (C) 201 4Wildfire Games.3 """Copyright (C) 2016 Wildfire Games. 4 4 * This file is part of 0 A.D. 5 5 * 6 6 * 0 A.D. is free software: you can redistribute it and/or modify … … 26 26 from sleekxmpp.xmlstream.handler import Callback 27 27 from sleekxmpp.xmlstream.matcher import StanzaPath 28 28 29 from sqlalchemy import func30 31 from LobbyRanking import session as db, Game, Player, PlayerInfo32 from ELO import get_rating_adjustment33 # Rating that new players should be inserted into the34 # database with, before they've played any games.35 leaderboard_default_rating = 120036 37 ## Class that contains and manages leaderboard data ##38 class LeaderboardList():39 def __init__(self, room):40 self.room = room41 self.lastRated = ""42 43 def getProfile(self, JID):44 """45 Retrieves the profile for the specified JID46 """47 stats = {}48 player = db.query(Player).filter(Player.jid.ilike(str(JID)))49 if not player.first():50 return51 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().id58 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 break64 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 stats69 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 of74 the Player model, or the one that already75 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 player83 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 None89 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 None95 players.delete()96 return player97 98 def addGame(self, gamereport):99 """100 Adds a game to the database and updates the data101 on a player(JID) from game results.102 Returns the created Game object, or None if103 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 None111 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 in116 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 | miscStats145 playerInfos = []146 for player in players:147 jid = player.jid148 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 game160 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 in167 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 False173 if len(dict.keys(gamereport['playerStates'])) != 2:174 return False175 return True176 177 def rateGame(self, game):178 """179 Takes a game with 2 players and alters their ratings180 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 -1191 # Player's ratings are -1 unless they have played a rated game.192 if player1.rating == -1:193 player1.rating = leaderboard_default_rating194 if player2.rating == -1:195 player2.rating = leaderboard_default_rating196 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_adjustment1213 player2.rating += rating_adjustment2214 if not player1.highest_rating:215 player1.highest_rating = -1216 if not player2.highest_rating:217 player2.highest_rating = -1218 if player1.rating > player1.highest_rating:219 player1.highest_rating = player1.rating220 if player2.rating > player2.highest_rating:221 player2.highest_rating = player2.rating222 db.commit()223 return self224 225 def getLastRatedMessage(self):226 """227 Gets the string of the last rated game. Triggers an update228 chat for the bot.229 """230 return self.lastRated231 232 def addAndRateGame(self, gamereport):233 """234 Calls addGame and if the game has only two235 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 game244 245 def getBoard(self):246 """247 Returns a dictionary of player rankings to248 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 board255 def getRatingList(self, nicks):256 """257 Returns a rating list of players258 currently in the lobby by nick259 because the client can't link260 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 ratinglist273 274 29 ## Class to tracks all games in the lobby ## 275 30 class GameList(): 276 31 def __init__(self): … … 308 63 self.gameList[JID]['nbp'] = data['nbp'] 309 64 self.gameList[JID]['state'] = 'running' 310 65 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 = leaderboard316 self.interimReportTracker = []317 self.interimJIDTracker = []66 ## Class for custom player stanza extension ## 67 class PlayerXmppPlugin(ElementBase): 68 name = 'query' 69 namespace = 'jabber:iq:player' 70 interfaces = set(('online')) 71 sub_interfaces = interfaces 72 plugin_attrib = 'player' 318 73 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) 327 77 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] * numPlayers335 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] = JIDs346 347 self.checkFull()348 349 def expandReport(self, rawGameReport, JIDs):350 """351 Takes an raw game report and re-formats it into352 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]] = part366 processedGameReport[key] = statToJID367 return processedGameReport368 369 def checkFull(self):370 """371 Searches internal database to check if enough372 reports have been submitted to add a game to373 the leaderboard. If so, the report will be374 interpolated and addAndRateGame will be375 called with the result.376 """377 i = 0378 length = len(self.interimReportTracker)379 while(i < length):380 numPlayers = self.getNumPlayers(self.interimReportTracker[i])381 numReports = 0382 for JID in self.interimJIDTracker[i]:383 if JID != None:384 numReports += 1385 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 -= 1390 else:391 i += 1392 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(","))-1404 # Return -1 in case of failure.405 return -1406 407 78 ## Class for custom gamelist stanza extension ## 408 79 class GameListXmppPlugin(ElementBase): 409 80 name = 'query' … … 431 102 class BoardListXmppPlugin(ElementBase): 432 103 name = 'query' 433 104 namespace = 'jabber:iq:boardlist' 434 interfaces = set(('board', 'command' ))105 interfaces = set(('board', 'command', 'recipient')) 435 106 sub_interfaces = interfaces 436 107 plugin_attrib = 'boardlist' 437 108 def addCommand(self, command): 438 109 commandXml = ET.fromstring("<command>%s</command>" % command) 439 110 self.xml.append(commandXml) 111 def addRecipient(self, recipient): 112 recipientXml = ET.fromstring("<recipient>%s</recipient>" % recipient) 113 self.xml.append(recipientXml) 440 114 def addItem(self, name, rating): 441 115 itemXml = ET.Element("board", {"name": name, "rating": rating}) 442 116 self.xml.append(itemXml) … … 446 120 name = 'report' 447 121 namespace = 'jabber:iq:gamereport' 448 122 plugin_attrib = 'gamereport' 449 interfaces = ('game' )123 interfaces = ('game', 'sender') 450 124 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) 452 131 def getGame(self): 453 132 """ 454 133 Required to parse incoming stanzas with this … … 464 143 class ProfileXmppPlugin(ElementBase): 465 144 name = 'query' 466 145 namespace = 'jabber:iq:profile' 467 interfaces = set(('profile', 'command' ))146 interfaces = set(('profile', 'command', 'recipient')) 468 147 sub_interfaces = interfaces 469 148 plugin_attrib = 'profile' 470 149 def addCommand(self, command): 471 150 commandXml = ET.fromstring("<command>%s</command>" % command) 472 151 self.xml.append(commandXml) 152 def addRecipient(self, recipient): 153 recipientXml = ET.fromstring("<recipient>%s</recipient>" % recipient) 154 self.xml.append(recipientXml) 473 155 def addItem(self, player, rating, highestRating, rank, totalGamesPlayed, wins, losses): 474 156 itemXml = ET.Element("profile", {"player": player, "rating": rating, "highestRating": highestRating, 475 157 "rank" : rank, "totalGamesPlayed" : totalGamesPlayed, "wins" : wins, … … 481 163 """ 482 164 A simple list provider 483 165 """ 484 def __init__(self, sjid, password, room, nick ):166 def __init__(self, sjid, password, room, nick, ratingsbot): 485 167 sleekxmpp.ClientXMPP.__init__(self, sjid, password) 486 168 self.sjid = sjid 487 169 self.room = room 488 170 self.nick = nick 489 171 490 self.rating ListUpdate = False172 self.ratingsBot = ratingsbot 491 173 # Game collection 492 174 self.gameList = GameList() 493 175 494 # Init leaderboard object495 self.leaderboard = LeaderboardList(room)496 497 # gameReport to leaderboard abstraction498 self.reportManager = ReportManager(self.leaderboard)499 500 176 # Store mapping of nicks and XmppIDs, attached via presence stanza 501 177 self.nicks = {} 502 178 503 179 self.lastLeft = "" 504 180 181 register_stanza_plugin(Iq, PlayerXmppPlugin) 505 182 register_stanza_plugin(Iq, GameListXmppPlugin) 506 183 register_stanza_plugin(Iq, BoardListXmppPlugin) 507 184 register_stanza_plugin(Iq, GameReportXmppPlugin) 508 185 register_stanza_plugin(Iq, ProfileXmppPlugin) 509 186 187 self.register_handler(Callback('Iq Player', 188 StanzaPath('iq/player'), 189 self.iqhandler, 190 instream=True)) 510 191 self.register_handler(Callback('Iq Gamelist', 511 192 StanzaPath('iq/gamelist'), 512 193 self.iqhandler, … … 519 200 StanzaPath('iq/gamereport'), 520 201 self.iqhandler, 521 202 instream=True)) 522 523 203 self.register_handler(Callback('Iq Profile', 524 204 StanzaPath('iq/profile'), 525 205 self.iqhandler, … … 543 223 """ 544 224 Process presence stanza from a chat room. 545 225 """ 546 self.ratingListUpdate = True 226 if self.ratingsBot in self.nicks: 227 self.relayRatingListRequest(self.ratingsBot) 228 self.relayPlayerOnline(presence['muc']['jid']) 547 229 if presence['muc']['nick'] != self.nick: 548 230 # If it doesn't already exist, store player JID mapped to their nick. 549 231 if str(presence['muc']['jid']) not in self.nicks: … … 551 233 # Check the jid isn't already in the lobby. 552 234 # Send Gamelist to new player. 553 235 self.sendGameList(presence['muc']['jid']) 554 # Following two calls make sqlalchemy complain about using objects in the555 # 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'])560 236 logging.debug("Client '%s' connected with a nick of '%s'." %(presence['muc']['jid'], presence['muc']['nick'])) 561 237 562 238 def muc_offline(self, presence): … … 607 283 logging.error("Failed to process gamelist request from %s" % iq['from'].bare) 608 284 elif 'boardlist' in iq.plugins: 609 285 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) 619 291 elif 'profile' in iq.plugins: 620 292 command = iq['profile']['command'] 621 293 try: 622 self. sendProfile(iq['from'], command)294 self.relayProfileRequest(iq['from'], command) 623 295 except: 624 try: 625 self.sendProfileNotFound(iq['from'], command) 626 except: 627 logging.debug("No record found for %s" % command) 296 pass 628 297 else: 629 298 logging.error("Unknown 'get' type stanza request from %s" % iq['from'].bare) 630 299 elif iq['type'] == 'result': … … 631 300 """ 632 301 Iq successfully received 633 302 """ 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 635 312 elif iq['type'] == 'set': 636 313 if 'gamelist' in iq.plugins: 637 314 """ … … 670 347 Client is reporting end of game statistics 671 348 """ 672 349 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']) 679 351 except: 680 352 traceback.print_exc() 681 353 logging.error("Failed to update game statistics for %s" % iq['from'].bare) 682 354 else: 683 355 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 = False687 356 688 357 def sendGameList(self, to = ""): 689 358 """ … … 736 405 except: 737 406 logging.error("Failed to send game list") 738 407 739 def sendBoardList(self, to = ""):408 def relayBoardListRequest(self, recipient): 740 409 """ 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. 744 411 """ 745 ## Pull leaderboard data and add it to the stanza746 board = self.leaderboard.getBoard()747 412 stz = BoardListXmppPlugin() 748 413 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) 753 417 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") 775 475 776 def sendRatingList(self, to = ""):476 def relayPlayerOnline(self, jid): 777 477 """ 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 780 524 to all clients. 781 525 """ 782 ## Pull rating list data and add it to the stanza783 ratinglist = self.leaderboard.getRatingList(self.nicks)784 stz = BoardListXmppPlugin()785 526 iq = self.Iq() 786 527 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 792 535 for JID in list(self.nicks): 793 536 ## Set additional IQ attributes 794 537 iq['to'] = JID … … 798 541 except: 799 542 logging.error("Failed to send rating list") 800 543 else: 801 # # Check recipient exists544 # Leaderboard 802 545 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)) 804 547 return 805 548 ## Set additional IQ attributes 806 549 iq['to'] = to … … 808 551 try: 809 552 iq.send(block=False, now=True) 810 553 except: 811 logging.error("Failed to send ratinglist")554 logging.error("Failed to send leaderboard list") 812 555 813 def sendProfile(self, to, player):556 def relayProfile(self, data, player, to): 814 557 """ 815 Send the p rofile to a specified player.558 Send the player profile to a specified target. 816 559 """ 817 560 if to == "": 818 logging.error("Failed to send profile ")561 logging.error("Failed to send profile, target unspecified") 819 562 return 820 563 821 online = False;822 ## Pull stats and add it to the stanza823 for JID in list(self.nicks):824 if self.nicks[JID] == player:825 stats = self.leaderboard.getProfile(JID)826 online = True827 break828 829 if online == False:830 stats = self.leaderboard.getProfile(player + "@" + str(to).split('@')[1])831 stz = ProfileXmppPlugin()832 564 iq = self.Iq() 833 565 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) 838 567 ## Check recipient exists 839 568 if str(to) not in self.nicks: 840 569 logging.error("No player with the XmPP ID '%s' known to send profile to" % str(to)) … … 850 579 traceback.print_exc() 851 580 logging.error("Failed to send profile") 852 581 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 exists866 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 return869 870 ## Set additional IQ attributes871 iq['to'] = to872 873 ## Try sending the stanza874 try:875 iq.send(block=False, now=True)876 except:877 traceback.print_exc()878 logging.error("Failed to send profile")879 880 582 ## Main Program ## 881 583 if __name__ == '__main__': 882 584 # Setup the command line arguments. … … 909 611 optp.add_option('-r', '--room', help='set muc room to join', 910 612 action='store', dest='xroom', 911 613 default="arena") 614 optp.add_option('-e', '--elo', help='set rating bot username', 615 action='store', dest='xratingsbot', 616 default="ratings") 912 617 913 618 opts, args = optp.parse_args() 914 619 … … 917 622 format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 918 623 919 624 # 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') 921 626 xmpp.register_plugin('xep_0030') # Service Discovery 922 627 xmpp.register_plugin('xep_0004') # Data Forms 923 628 xmpp.register_plugin('xep_0045') # Multi-User Chat # used