diff --git a/.gitignore b/.gitignore index 5e86b66..390b3b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ cache +*.sqlite +*.sqlite.clean +*.txt *.log *.swp __pycache__/ diff --git a/FileReader.py b/FileReader.py deleted file mode 100644 index 331b23f..0000000 --- a/FileReader.py +++ /dev/null @@ -1,53 +0,0 @@ -import TrueSkillWrapper as TS -import time -import threading -import insurgencyParsing as iparse - -DATE_LENGTH = 15 - -def readfile(filename, start_at_end, exit_on_eof, parsingBackend, startAtTime, cacheFile, cpus=1): - - f = open(filename) - if start_at_end: - f.seek(0,2) - - if startAtTime: - while True: - line = f.readline() - try: - dt = parsingBackend.parseDate(line) - if not dt: - break - if dt > startAtTime: - break - except IndexError: - pass - except ValueError: - pass - - try: - if cpus > 1: - raise NotImplementedError("Multiprocessing not implemeted yet") - else: - if callable(parsingBackend): - parsingBackend(f, exit_on_eof, start_at_end, cacheFile) - else: - parsingBackend.parse(f, exit_on_eof, start_at_end, cacheFile) - except TypeError: - raise RuntimeError("parsingBackend musst be callable or have .parse() callable") - - f.close() - -def readfiles(filenames, start_at_end, nofollow, oneThread, cacheFile, parsingBackend=iparse): - if cacheFile: - startAtTime = parsingBackend.loadCache(cacheFile) - print(startAtTime) - else: - startAtTime = None - for f in filenames: - if oneThread: - readfile(f, start_at_end, nofollow, parsingBackend, startAtTime, cacheFile) - else: - threading.Thread(target=readfile,args=\ - ( f, start_at_end, nofollow, \ - parsingBackend, startAtTime, cacheFile, )).start() diff --git a/NetworkListener.py b/NetworkListener.py deleted file mode 100644 index 670ba29..0000000 --- a/NetworkListener.py +++ /dev/null @@ -1,34 +0,0 @@ -import socket -from threading import Thread -import NetworkParser - -TCP_IP = '127.0.0.1' -TCP_PORT = 7040 -# must be same as smmod -BUFFER_SIZE = 1024 - -def listen(): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind((TCP_IP, TCP_PORT)) - s.listen(5) - print("TCP listener on {}:{}".format(TCP_IP, TCP_PORT)) - while True: - conn, addr = s.accept(); - Thread(target=t_listen,args=(conn,)).start(); - -def t_listen(conn): - while True: - try: - data = conn.recv(BUFFER_SIZE).decode('utf-8') - # data is only None if the connection is seperated - if not data: - break - ret = NetworkParser.handleInput(data) - if not ret: - ret = "Rating Backend Error" - if type(ret) == str: - ret = ret.encode("utf-8") - conn.send(ret) - except IOError: - pass diff --git a/NetworkParser.py b/NetworkParser.py deleted file mode 100644 index 79da018..0000000 --- a/NetworkParser.py +++ /dev/null @@ -1,195 +0,0 @@ -import socket -import TrueSkillWrapper as TS -import Player -import StorrageBackend as SB -from threading import Thread -import PSQL - -def handleInput(data): - no_log_in_console = False - data, ident = get_event_ident(data) - tmp = '' - print(data) - if data.startswith("quality,"): - t1, t2 = parse_teams(data.lstrip("quality,")) - tmp = TS.quality(t1.values(),t2.values(),t1.keys(), t2.keys()) - elif data.startswith("balance,"): - s = data.lstrip("balance,") - tmp = TS.balance(parse_players(s), get_buddies(s)) - elif data.startswith("player,"): - # legacy format support - p = Player.DummyPlayer(data.lstrip("player,").rstrip("\n")) - tmp = TS.get_player_rating(p) - elif data.startswith("find,"): - tmp = find_player(data.rstrip("\n").lstrip("find,")) - elif data.startswith("buddies,"): - tmp = str(get_buddies()) - elif data.startswith("forceRankReload"): - SB.updatePlayerRanks(force=True) - tmp = "Updated" - elif data.startswith("dump"): - no_log_in_console = True - topN = 0 - if "," in data: - topN = int(data.split(",")[1]) - tmp = SB.dumpRatings(topN) - elif data.startswith("stats"): - tmp = "Clean: {}\nDirty: {}\n".format(TS.clean_rounds,TS.dirty_rounds) - elif data.startswith("getteam,"): - tmp = get_team(data.split("getteam,")[1]); - elif data.startswith("rebuildteam,"): - tmp = get_rebuild_team(data.split("rebuildteam,")[1]); - else: - print("wtf input: "+data) - if tmp == '': - return ("EMPTY") - ret = str(ident+str(tmp)).encode('utf-8') - if not no_log_in_console and ret: - print(ret) - return ret - -def get_event_ident(data): - if data.startswith("player_connected"): - return (data.strip("player_connected"),"player_connected") - else: - return (data,"") - -def get_team(string): - - # variables # - ret = "BALANCE_SINGLE,{},{}" - sid = string.split(",")[0] - rest = string.split(",")[1:] - team1 = [] - team2 = [] - - # parse # - for pair in rest: - p = DummyPlayer(pair.split("|")[0]); - if pair.split("|")[2] == "2": - team1 += [p] - else: - team2 += [p] - - # Prevent totally imbalanced teams # - if len(team1) == 0: - return ret.format(sid,"2") - elif len(team2) == 0: - return ret.format(sid,"3") - elif len(team1) > len(team2) + 2: - return ret.format(sid,"2") - elif len(team1) +2 < len(team2): - return ret.format(sid,"3") - - # balance # - if TS.quality(team1 + [p],team2) > TS.quality(team1,team2 + [p]): - return ret.format(sid,"2") - else: - return ret.format(sid,"3") - -def get_rebuild_team(string): - # parse # - players = [] - teams = ([],[]) - for pair in string.split(","): - p = DummyPlayer(pair.split("|")[0]); - players += [p] - players = list(map(players,lambda p: SB.known_players[p])) - players = sorted(players,key=lambda x: TS.getEnviroment().expose(x.rating),reverse=True) - count = 0 - - # initial # - while count TS.quality(teams[0],teams[1] + [p]): - teams[0] += [players[count]] - else: - teams[1] += [players[count]] - - # iterate # - count = 0 - while count < min(len(teams[0]),len(teams[1])): - old_q = TS.quality(teams[0],teams[1]) - p0 = teams[0][count] - p1 = teams[1][count] - # basicly if not better, reset # - if old_q > TS.quality(teams[0].remove(p0)+[p1],teams[1].remove(p1)+[p0]): - teams[0].remove(p1) - teams[0] += [p0] - teams[1].remove(p0) - teams[1] += [p1] - ret = "BALANCE_REBUILD," - for p in teams[0]: - ret += str(p.steamid)+"|2," - for p in teams[1]: - ret += str(p.steamid)+"|3," - - return ret.rstrip(","); - -def get_buddies(players): - # [[p1,p2,p3 ],...] - already_found = [] - ret = [] - for sid in players: - p = Player.DummyPlayer(sid) - if p in already_found: - continue - tmp = PSQL.query_buddies(p) - already_found += tmp - ret += [tmp] - return ret - -def parse_teams(data): - # TEAM_1,Team_2 - # TEAM_X = STEAMID|STEAMID,... - # return ({p1:p1.rating,p2:p2.rating,...},..) - ret = (dict(),dict()) - team1, team2 = data.split(",") - team1 = team1.split("|") - team2 = team2.split("|") - for sid in team1: - sid = sid.strip() - tmp = Player.DummyPlayer(sid, sid) - if tmp in SB.known_players: - ret[0].update({SB.known_players[tmp]:Storrage.known_players[tmp].rating}) - else: - ret[0].update({tmp:TS.newRating()}) - for sid in team2: - sid = sid.strip() - tmp = Player.DummyPlayer(sid, sid) - if tmp in SB.known_players: - ret[1].update({SB.known_players[tmp]:Storrage.known_players[tmp].rating}) - else: - ret[1].update({tmp:TS.newRating()}) - return ret - -def parse_players(data, lol=False): - # p1|p2|p3|... - ret = [] - players = data.strip("\n").split("|") - if len(players) == 1 and players[0] == '': - return None - players = players_new - for sid in players: - if lol: - tmp = Player.DummyPlayer(str(sid[0]),sid[1]) - else: - tmp = Player.DummyPlayer(str(sid), str(sid)) - if tmp in SB.known_players: - ret += [SB.known_players[tmp]] - else: - ret += [tmp] - return ret - -def find_player(string): - if string.isdigit(): - if string in SB.known_players: - return TS.get_player_rating(string, string) - else: - tmp = SB.fuzzy_find_player(string) - string = "" - for tup in tmp: - p = tup[1] - string += "{}\n".format(TS.get_player_rating(p, p.name)) - return string diff --git a/PSQL.py b/PSQL.py deleted file mode 100644 index 1e12b73..0000000 --- a/PSQL.py +++ /dev/null @@ -1,23 +0,0 @@ -import psycopg2 -def save(dname,user,host,password,data): - conn = psycopg2.connect("dbname={} user={} host={} password={}".format(dname,user,host,password)) - cur = conn.cursor() - tup = () - if type(data) == dict: - for player in data: - d = dict() - d.update("steamid",p.steamid) - d.update("name",p.name) - d.update("wins",p.wins) - d.update("games",p.games) - d.update("mu",p.rating.mu) - d.update("sigma",p.rating.sigma) - tup += (d,) - cur.executemany("""INSERT INTO ratings(steamid,name,wins,games,mu,sigma) \ - VALUES (%(steamid)s,%(name)s,%(wins)s,%(games)s,%(mu)s,%(sigma)s)""",d) - elif isinstance(data,Event): - cur.exceutemany("""INSERT INTO events(etype,timestamp,line) \ - VALUES (%(etype)s,%(timestamp)s,%(line)s)""",data) - -def query_buddies(steamid): - return [] diff --git a/Player.py b/Player.py deleted file mode 100644 index cb5d31d..0000000 --- a/Player.py +++ /dev/null @@ -1,90 +0,0 @@ -import datetime -import TrueSkillWrapper as TS - -class Player: - def __init__(self, steamid, name, rating=None): - if rating: - self.rating = rating - else: - self.rating = TS.newRating() - self.steamid = steamid - self.cur_name = name - def __hash__(self): - return hash(self.steamid) - def __eq__(self, other): - if not other: - return False - if isinstance(other,str): - return self.steamid == other - elif isinstance(other,Player): - return self.steamid == other.steamid - else: - raise TypeError("Unsupported Equals with types {} and {}".format(type(other),type(self))) - def __str__(self): - return "Player: {}, ID: {}".format(self.name, self.steamid) - -class DummyPlayer(Player): - def __init__(self, steamid, name="PLACEHOLDER", rating=None): - self.rating = TS.newRating() - if rating: - self.rating = rating - self.name = name - self.cur_name = name - super().__init__(steamid, name) - -class PlayerInRound(Player): - def __init__(self, steamid, name, team, timestamp=None): - self.name = name - self.cur_name = name - self.steamid = steamid - self.rating = TS.newRating() - self.active_time = datetime.timedelta(0) - if type(team) != int or team > 3 or team < 0: - raise Exception("Invalid TeamID '{}', must be 0-3 (inkl.)".format(team)) - self.team = int(team) - self.active = True # unset when player has disconnected or changed team - if not timestamp: - self.is_fake = True - else: - self.is_fake = False - self.timestamp = timestamp - def __str__(self): - return "TMP-Player: {}, ID: {}, active: {}".format(self.cur_name,self.steamid,self.active_time) - def serialize(self): - return "{}0x42{}0x42{}0x42{}".format(self.name, self.steamid, \ - self.team, self.active_time.seconds) - - def deserialize(string): - name, steamid, team, active_time = string.split("0x42") - active_time = datetime.timedelta(seconds=int(active_time)) - - team = int(team) - return PlayerInRound(steamid, name, team, active_time) - -class PlayerForDatabase(Player): - def __init__(self,steamid, name, rating, lastUpdate=None, player=None): - if player: - self.steamid = player.steamid - self.name = player.name - self.rating = player.rating - else: - self.steamid = steamid - self.name = name - self.rating = rating - self.lastUpdate = lastUpdate - self.games = 0 - self.wins = 0 - def winratio(self): - if self.games == 0: - return "---" - return str(int(self.wins*100/self.games)) - def get_name(self): - return self.name.encode('utf-8')[:25].decode('utf-8','ignore').rstrip(" ") - - def serialize(self): - arr = [str(x) for x in [self.name, self.steamid, self.rating.mu, self.games, self.wins]] - return ",".join(arr) - -class PlayerFromDatabase(PlayerForDatabase): - def __init__(line): - super().__init__(None,None,None) diff --git a/README.md b/README.md index 2b039b5..dfaeb7c 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,77 @@ -## What is "Skillbird" ? +# What is "Skillbird" ? Skillbird is a framework around the python-Trueskill library, which can parse files of versus games to calculate a rating, matchmaking suggestions for future games or create predictions for the outcome of a game with certain team compositions. -## Web Interface +# Web Interface The [Open-web-leaderboard](https://github.com/FAUSheppy/open-web-leaderboard) can be used for visualization. If you leave all settings at default, it should work out of the box. ![open-web-leaderboard](https://media.atlantishq.de/leaderboard-github-picture.png) -## Adaption for your own Data -### Data Requirements -To work correctly you data must have the following fields: -- unique player id or name -- player(s) in winning and losing team +# Data Transmission +## /event-blob +Your server may collect certain events during a match of two teams and columinatively report them to the server, which will then evalute those event into a single Round. The events must be submitted as a *JSON-list* with *Content-Type application/json* in a field called *"events"*. Event must be dictionary-like *JSON*-objects as described below. -You may also have the following informations: -- data/time of the game (you cannot use the cached-rebuild feature without this) -- time players spent playing compared to the full length of the game -- teamchanges of players -- different maps + { + events : [ { ... }, { ... } ] -### Data Input -If you use the official source-plugin and it's output, you don't have to do anything. Alternatively can write your own parser (see the skillbird-examples project), or you can conform to the Source-format which supports the following log-lines. None of the input values may contain any of the separators used (**pipe** and **comma**) or the line identifier (**0x42**). + } - # reset state - 0x42,plugin_unloaded - - # declare the current map - 0x42,mapname,MAP_NAME - - # start a round - 0x42,round_start_active,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID.... - - # record team changes (ct) or disconnects (dc) and the team composition after it - # the backend will handle those accordingly - 0x42,ct,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID.... - 0x42,dc,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID.... +### ActivePlayersEvent +This events lists all players currently in the game, it must be fired whenever a new player connects, changes team or disconnects, but may also be fired at any other point. The *NUMERIC_TEAM_IDENTIIER* must be 0 for *no team*, 1 for *observers* or 2/3 respectively for players actually in one of the teams. - # declare the team-composition at the end of the round - # the backend will use this information for sanity checks - 0x42,round_end_active,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID.... - - # name the winning team and end the round - 0x42,winner,WINNING_TEAM_ID + { + "etype" : "active_players", + "timestamp" : ISO_TIMESTAMP, + "players" : [ + { + "id" : UNIQUE_PLAYER_ID, + "name" : COLOQUIAL_NAME, + "team" : NUMERIC_TEAM_IDENTIIER + } + ... + ] + } -## Usage - usage: startInsurgency.py [-h] [--parse-only] [--start-at-end] [--no-follow] - [--one-thread] [--cache-file CACHEFILE] - FILE [FILE ...] - - positional arguments: - FILE one or more logfiles to parse - - optional arguments: - -h, --help show this help message and exit - --parse-only, -po only parse, do not listen for queries - --start-at-end, -se start at the end of each file (overwrites no-follow) - --no-follow, -nf wait for changes on the files (does not imply start- - at-end) - --one-thread run everything in main thread (implies no-follow) - --cache-file CACHEFILE - A cache file which makes restarting the system fast +### WinnerInformationEvent +Event annotating who won a round. Any single round *MUST* only have one such Event. -## Query Options -Skillbird has a TCP-Query interface which supports the following queries. The separator for player-IDs is always a**comma** and the separator for for teams is always a **pipe** as before, those special characters may not be contained in any of the actual input values. A HTTP-api is work in progress (at the start of this project the interface was only intended for sourcemodplugins). + { + "etype" : "winner", + "timestamp" : ISO_TIMESTAMP, + "winnerTeam" : NUMERIC_TEAM_IDENTIIER + } -### Quality -Get the balance quality of the current team composition. +### MapInformationEvent +Optional event to annotate the map that was played on. Each individual round must only have one such event. - Input: quality,LIST_OF_PLAYERS_TEAM_1|LIST_OF_PLAYERS_TEAM_2 - Output: float between 0 and 100 + { + "etype" : "map", + "timestamp" : ISO_TIMESTAMP, + "map" : MAP_NAME + } -### Balance -Return a balance suggestion for a list of players. - - Input: quality,LIST_OF_PLAYER_IDs - Output: string LIST_OF_PLAYERS_TEAM_1|LIST_OF_PLAYERS_TEAM_2 +## /submitt-round +Your may transmitt a json dictionary representing an actuall round, this is intended more for backups and manual inputs rather than production use, it basicly skips *backends.eventStream.parse* and goes directly to *backends.trueskill.evaluateRound*. -### Player -Return rating information about a player-ID + { + "map" : MAP_STRING_OR_NULL, + "winner-side" : NUMERIC_TEAM_ID_OR_NULL, + "winners" : [ player, player, ... ], + "losers" : [ player, player, ..., ], + "duration" : DURATION_OF_ROUND_IN_SECONDS, + "startTime" : ISO_TIMESTAMP_OR_NULL + } - Input: quality,PLAYER_ID - Output: string: rating information - +The player struct in the winner/loser-array must look like this: -### Find -Fuzzy search for the name or ID of a player + { + "playerId" : PLAYER_ID_STR, + "playerName" : PLAYER_NAME, + "isFake" : BOOLEAN, + "activeTime" : ACTIVE_TIME_IN_SECONDS, + } - Input: find,string - Output: string: rating information - -### Force rank reload -For the reload of player ranks, which are usually updated every 5 minutes immediately. - - Input: forceRankReload - Output: string: "OK" (if successful) - -### Dump -Reload the player ranks cache and dump the entire contents. - - Input: dump - Output: string: all players, their ratings and their ranks - -### Stats -Return some statistics about the system - - Input: stats - Output: string: general information +You cannot ommit the duration/active\_time fields, if you don't need or want to use them set them to *1*. If the winner side is unkown or your game is symetrical (meaning there is no possible advantage for one side or the other, set it to *-1*. ## Related projects - [skillbird-sourcemod](https://github.com/FAUSheppy/skillbird-sourcemod) Sourcemod plugin that produces the necessary output for Source-based servers. diff --git a/Round.py b/Round.py deleted file mode 100644 index 1126fd5..0000000 --- a/Round.py +++ /dev/null @@ -1,150 +0,0 @@ -from datetime import timedelta, datetime -import threading -from Player import PlayerInRound -import TrueSkillWrapper as TS - -## A comment on why the login-offset is nessesary ## -## - losing teams tend to have players leaving and joining more rapidly -## - every time a new player joins he has to setup -## - new players are unfamiliar with postions of enemy team -## - new players have to run from spawn -## --> their impact factor must account for that -loginoffset = timedelta(seconds=60) - -class Round: - writeLock = threading.RLock() - def __init__(self, winner_team, loser_team, _map, duration,\ - starttime, winner_side=None, cache=None): - self.winners = winner_team - self.losers = loser_team - self.winner_side = winner_side - self.map = _map - self.duration = duration - self.start = starttime - #self.add_fake_players() - if cache: - Round.writeLock.acquire() - with open(cache, "a") as f: - f.write(self.serialize()) - f.write("\n") - f.flush() - Round.writeLock.release() - - - def normalized_playtimes(self): - '''returns a dict-Object with {key=(teamid,player):value=player_time_played/total_time_of_round}''' - np = dict() - - for p in self.winners: - if self.duration == None: - d = 1.0 - else: - d = (p.active_time-loginoffset)/self.duration - if d < -1: - print("lol") - if d < 0: - d = 0.0 - elif d > 1: - d = 1.0 - np.update({(0,p):d}) - for p in self.losers: - if self.duration == None: - d = 1.0 - else: - d = (p.active_time-loginoffset)/self.duration - if d < 0: - d = 0.0 - elif d > 1: - d = 1.0 - np.update({(1,p):d}) - - return np - - def add_fake_players(self): - ''' adds map/side specific player to account for asynchronous gameplay ''' - if not self.map: - #print("Warning: No map info, cannot add fake players.") - return - ins = self.map+str(2) - sec = self.map+str(3) - - p_ins = PlayerInRound(ins,2,None) - p_sec = PlayerInRound(sec,3,None) - - if p_ins in Storrage.known_players: - p_ins = Storrage.known_players[p_ins] - if p_sec in Storrage.known_players: - p_sec = Storrage.known_players[p_sec] - - if self.winner_side == 2: - self.winners += [p_ins] - self.losers += [p_sec] - else: - self.winners += [p_sec] - self.losers += [p_ins] - - def pt_difference(self): - '''Used to check difference in playtimes per team''' - if self.duration == None: - return 1 - w1 = w2 = 0 - for p in self.winners: - if p.is_fake: - w1 += 1.0 - continue - d = (p.active_time-loginoffset)/self.duration - if d < 0: - d = 0.0 - elif d > 1: - d = 1.0 - w1 += d - for p in self.losers: - d = (p.active_time-loginoffset)/self.duration - if p.is_fake: - w2 += 1.0 - continue - if d < 0: - d = 0.0 - elif d > 1: - d = 1.0 - w2 += d - - # no div0 plox - if min(w1,w2) <= 0: - return 0 - - return max(w1,w2)/min(w1,w2) - - def serialize(self): - # full = winners|losers|winner_side|map|duration|start - # winners/losers = p1,p2,p3 ... - winners = "" - losers = "" - for p in self.winners: - winners += "," + p.serialize() - for p in self.losers: - losers += "," + p.serialize() - - teams = "{}|{}".format(winners, losers) - startTimeStr = self.start.strftime("%y %b %d %H:%M:%S") - - ret = "{}|{}|{}|{}|{}".format(teams, self.winner_side, \ - self.map, self.duration.seconds, startTimeStr) - return ret - - def deserialize(string): - string = string.strip("\n") - winnersStr, losersStr, winner_side, _map, duration, startTimeStr = string.split("|") - winners = dict() - losers = dict() - for pStr in winnersStr.split(","): - if pStr == "": - continue - winners.update({PlayerInRound.deserialize(pStr):TS.newRating()}) - for pStr in losersStr.split(","): - if pStr == "": - continue - losers.update({PlayerInRound.deserialize(pStr):TS.newRating()}) - startTime = datetime.strptime(startTimeStr, "%y %b %d %H:%M:%S") - duration = timedelta(seconds=int(duration)) - return Round(winners, losers, _map, duration, startTime, winner_side) diff --git a/StorrageBackend.py b/StorrageBackend.py deleted file mode 100644 index 573be51..0000000 --- a/StorrageBackend.py +++ /dev/null @@ -1,259 +0,0 @@ -import Player -import TrueSkillWrapper as TS -from fuzzywuzzy import fuzz -from datetime import datetime, timedelta - -known_players = dict() - -# care for cpu load # -player_ranks = dict() -playerRankList = [] -last_rank_update = datetime.now()- timedelta(minutes=5) - -############################################################# -###################### Save/Load File ####################### -############################################################# - -def load_save(): - with open("score.log") as f: - for l in f: - p = Player.PlayerFormDatabase(l) - known_players[p.steamid] = p - -def save_to_file(fname="score.log"): - with open(fname,"w") as f: - for p in known_players.values(): - f.write(p.toCSV()+'\n') - -############################################################# -###################### SyncPlayerDict ####################### -############################################################# - -def sync_from_database(players): - for p in players: - if p in known_players: - p.rating = known_players[p].rating - if type(players) == dict: - players[p] = p.rating - if not p.name or p.name == "PLACEHOLDER": - p.name = known_players[p].name - else: - lastUpdate = datetime.now() - known_players.update(\ - { Player.DummyPlayer(p.steamid, p.name) : \ - Player.PlayerForDatabase(None, None, None, player=p, lastUpdate=lastUpdate)\ - }) - -def sync_to_database(players, win): - for p in players: - known_players[p].rating = players[p] - if win: - known_players[p].wins += 1 - known_players[p].games += 1 - known_players[p].lastUpdate = datetime.now() - - updatePlayerRanks(force=True) - -############################################################# -##################### Handle Rank Cache ##################### -############################################################# - -def updatePlayerRanks(force=False, minGames=10, maxInactivityDays=60): - '''Precalculate player ranks to be used for low performance impact''' - - global last_rank_update - global player_ranks - global playerRankList - - if force or datetime.now() - last_rank_update > timedelta(seconds=240): - last_rank_update = datetime.now() - - # sort players by rank # - sortKey = key = lambda x: TS.getEnviroment().expose(x.rating) - sortedPlayers = sorted(known_players.values(), key=sortKey, reverse=True) - - # filter out inactive and low-game players # - maxInactivityDelta = timedelta(days=maxInactivityDays) - filterKey = lambda p: (last_rank_update - p.lastUpdate) < maxInactivityDelta \ - and p.games >= minGames - filteredPlayers = list(filter(filterKey, sortedPlayers)) - - # assing ranks to player objects # - rankCounter = 1 - for p in filteredPlayers: - if p in player_ranks: - player_ranks[p] = rankCounter - else: - player_ranks.update({p:rankCounter}) - rankCounter += 1 - - # list of players in the right order for a leaderboard # - playerRankList = list(filteredPlayers) - - -############################################################# -################## Write/Load External DB ################### -############################################################# - -def save_event(event): - return - -def save_psql(): - f=open("pass.secret",'r') - pw=f.readline().strip(); - f.close() - PSQL.save("insurgency", "insurgencyUser", "localhost", pw, known_players) - -############################################################# -###################### Python API ########################### -############################################################# - -def getPlayer(pid, name="NOTFOUND"): - return known_player[pid] - -def searchPlayerByName(name): - '''Find a player by his name''' - global player_ranks - - ret = "" - tup_list = [] - TS.lock() - try: - for p in known_players.values(): - sim = fuzz.token_set_ratio(name.lower(),p.name.lower()) - tup_list += [(sim,p)] - tmp = sorted(tup_list, key=lambda x: x[0], reverse=True) - players = list([x[1] for x in filter(lambda x: x[0] > 80, tmp)]) - - # update ranks # - updatePlayerRanks(force=True) - - resultPlayers = [] - for p in players: - try: - resultPlayers += [(p.steamid, p.name, p.rating, player_ranks[p])] - except KeyError: - noRankExplanation = "inactive" - if p.games < 10: - noRankExplanation = "not enough games" - resultPlayers += [(p.steamid, p.name, p.rating, noRankExplanation)] - finally: - TS.unlock() - return resultPlayers - -def getPlayerRank(playerID): - '''Get a players rank''' - - global player_ranks - try: - return str(player_ranks[playerID]) - except KeyError: - return "N/A" - -def getBalancedTeams(players, buddies=None, teamCount=2, useNames=False): - '''Balance a number of players into teams''' - - if teamCount != 2: - raise NotImplementedError("Only supporting balancing into two teams currently") - if not players: - return ValueError("Input contains no players") - - if useNames: - players = [ Player.DummyPlayer(searchPlayerByName(playerID)[0][0]) for playerID in players ] - elif type(players[0]) == str: - players = [ Player.DummyPlayer(playerID) for playerID in players] - - sync_from_database(players) - - sortedPlayers = sorted(players, key=lambda x: x.rating.mu, reverse=True) - team1Rating = 0 - team2Rating = 0 - ret = "" - - for p in sortedPlayers: - if team1Rating <= team2Rating: - ret += "{}|{},".format(p.name, 2) - team1Rating += p.rating.mu - else: - ret += "{}|{},".format(p.name, 3) - team2Rating += p.rating.mu - return ret - -def qualityForTeams(teamArray, useNames=False): - '''Get quality for number of teams with players''' - - if not teamArray or len(teamArray) < 2 or not teamArray[0]: - raise ValueError("Team Array must be more than one team with more than one player each") - - if useNames: - teamArray = [ [ Player.DummyPlayer(searchPlayerByName(playerID)[0][0]) - for playerID in team ] for team in teamArray ] - elif type(teamArray[0][0]) == str: - teamArray = [ [ Player.DummyPlayer(playerID) for playerID in team ] for team in teamArray ] - - for team in teamArray: - sync_from_database(team) - - teamAsRatings = [ [ player.rating for player in team ] for team in teamArray ] - teamAsNames = [ [ player.name for player in team ] for team in teamArray ] - - # TODO: implement for !=2 teams # - if len(teamArray) != 2: - raise NotImplementedError("Quality is only supported for exactly two teams") - - return qualityForRatings(teamAsRatings[0], teamAsRatings[1], teamAsNames[0], teamAsNames[1]) - -def qualityForRatings(team1, team2, names1 = [""], names2 = [""]): - '''Get Quality for two arrays containing TrueSkill.Rating objects''' - - mu1 = sum(r.mu for r in team1) - mu2 = sum(r.mu for r in team2) - mu_tot = mu1 + mu2 - sig1 = sum(r.sigma for r in team1) - sig2 = sum(r.sigma for r in team2) - sigtot = sig1 + sig2 - - # print(team1, names1, team2, names2) - - diff = abs(mu1 - mu2) - percent = 50 + diff/mu_tot*100 - - if percent > 100: - percent = 100 - - if mu1 > mu2: - string = "{} win at {:.2f}% - {:.2f} to {:.2f}".format(names1, percent, mu1, mu2 ) - else: - string = "{} win at {:.2f}% - {:.2f} to {:.2f}".format(names2, percent, mu2, mu1 ) - - return string - -def getRankListLength(revalidateRanks=False): - '''Get the total number of entries in the ranking''' - - global playerRankList - updatePlayerRanks(revalidateRanks) - return len(playerRankList) - -def getRankRange(start, end, revalidateRanks=False): - '''Returns a list of player, optionally flushing the ranks-cache first''' - - global playerRankList - - updatePlayerRanks(revalidateRanks) - if start > len(playerRankList) or start >= end: - return [] - return playerRankList[start:end] - -def rankHasChanged(time): - '''Indicates to a http-querier if the availiable data has changed''' - - if last_rank_update > time: - return "True" - else: - return "False" - -def debugInformation(): - '''Dump a string of debugging information (this may take some times)''' - - return [ "{} {} {}".format(p.steamid, p.name, p.rating) for p in known_players.values() ] diff --git a/TrueSkillWrapper.py b/TrueSkillWrapper.py deleted file mode 100644 index 9dc2f9b..0000000 --- a/TrueSkillWrapper.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/python3 -from trueskill import TrueSkill, Rating -import StorrageBackend -import threading -import Player - -env = TrueSkill(draw_probability=0, mu=1500, sigma=833, tau=40, backend='mpmath') -env.make_as_global() -updateLock = threading.RLock() -dirty_rounds = 0 -clean_rounds = 0 - -##################################################### -################ HANDLE RATING INPUT ################ -##################################################### - -def evaluate_round(r): - global clean_rounds - global dirty_rounds - - updateLock.acquire() - try: - if not r: - dirty_rounds += 1 - return - if r.pt_difference() >= 2.1 or r.pt_difference() == 0: - dirty_rounds += 1 - raise Warning("%s | Info: Teams too imbalanced, not rated (1:%.2f)"%(r.start+r.duration,r.pt_difference())) - weights = r.normalized_playtimes() - tmp = rate_teams_simple(r.winners,r.losers,weights) - if not tmp: - dirty_rounds += 1 - return - finally: - updateLock.release() - - -def rate_teams_simple(winner_team, loser_team, weights=None): - global clean_rounds - groups = [winner_team] + [loser_team] - - if len(groups[1]) == 0 or len(groups[0]) ==0 : - print("WARNING: Groups were {} - {} INCOMPLETE, SKIP".format(len(groups[0]),len(groups[1]))) - return False - - rated = env.rate(groups,weights=weights) - StorrageBackend.sync_to_database(rated[0],True) - StorrageBackend.sync_to_database(rated[1],False) - clean_rounds += 1 - return True - -def rate_ffa(players): - '''Takes a list of players in reverse finishing order (meaning best first) - perform a truskill evaluation and write it to database''' - - # one player doesnt make sense # - if len(players) <= 1: - return False - - # create list of dicts for trueskill-library # - playerRatingTupelDicts = [] - for p in players: - playerRatingTupelDicts += [{p:p.rating}] - - # generate ranks - ranks = [ i for i in range(0, len(playerRatingTupelDicts))] - - # rate and safe to database # - rated = env.rate(playerRatingTupelDicts) - - # create sync dict # - # first player is handled seperately # - allPlayer = dict() - for playerRatingDict in rated[1:]: - allPlayer.update(playerRatingDict) - - # only first player gets win # - StorrageBackend.sync_to_database(rated[0], True) - StorrageBackend.sync_to_database(allPlayer, False) - - for p in allPlayer.keys(): - print(p) - -##################################################### -################### LOCK/GETTER #################### -##################################################### - -def lock(): - updateLock.acquire() - -def unlock(): - updateLock.release() - -def newRating(mu=None): - if mu: - return Rating(mu=mu, sigma=env.sigma) - return env.create_rating() - -def getEnviroment(): - return env - - -def balance(players, buddies=None): - raise NotImplementedError() - -def get_player_rating(sid, name="NOTFOUND"): - raise NotImplementedError() - -##################################################### -############### DEBUGGING FUNCTIONS ################# -##################################################### - -def playerDebug(sid,rated,groups): - - print("winner") - for key in groups[0]: - #if key.steamid == sid: - print(key.name,key.rating) - print("loser") - for key in groups[1]: - #if key.steamid == sid: - print(key.name,key.rating) - - p = Player.DummyPlayer(sid) - if p in rated[0]: - if p in groups[0]: - print("Before: {}".format(groups[0][p])) - print("After: {}".format(rated[0][p])) - else: - print("Before: {}".format(groups[1][p])) - print("After: {}".format(rated[0][p])) - if p in rated[1]: - if p in groups[0]: - print("Before: {}".format(groups[0][p])) - print("After: {}".format(rated[1][p])) - else: - print("Before: {}".format(groups[1][p])) - print("After: {}".format(rated[1][p])) - print("\n") diff --git a/helper_scripts/test.py b/helper_scripts/test.py new file mode 100755 index 0000000..c4b40d9 --- /dev/null +++ b/helper_scripts/test.py @@ -0,0 +1,105 @@ +#!/usr/bin/python3 +import sys +import argparse +import datetime as dt +import json +import requests +import random + +possiblePlayers = { +"KD0FF8NC" : None, +"SA5BPTYB" : None, +"KBXGPV20" : None, +"1TNLXYO9" : None, +"S4M3Z7AT" : None, +"FO4V590D" : None, +"A116P0TY" : None, +"8OPBPAZ1" : None, +"QLIEC5F9" : None, +"TYSXZB6S" : None, +"M6Y54UI2" : None, +"AEB7EHLC" : None, +"OY8ZQEYJ" : None, +"LJ5BRST2" : None, +"CBR279YR" : None, +"GSI4J4D3" : None, +"YHTACQB8" : None, +"LD2I5Z52" : None, +"GB4ZC8Z7" : None, +"RVT3KQZC" : None, +"8L8UWEJE" : None, +"VUBITUD2" : None, +"SCG4LCK9" : None, +"VVYP0AOE" : None, +"8QFDCKB4" : None, +"P4WFV6P4" : None, +"6NDWTM7O" : None, +"7JU4HSEQ" : None, +"AE3V6PFV" : None, +"0OPM7MAZ" : None, +"160Y4R9T" : None, +"TZ8TLYO5" : None, +"5RHPGONF" : None, +"AEVXADMM" : None, +"VG70J1PR" : None, +"TWWQXOTR" : None, +"PW3X67XA" : None, +"DLGGHKWZ" : None, +"4QZVPKTG" : None, +"I75UFRUC" : None, +"6CY0QDF1" : None, +"SNHDK5V3" : None, +"91URSK04" : None, +"P1S9GSVY" : None, +"FZOY6QCW" : None, +"S0EVQ1S9" : None, +"EL3MU8YE" : None, +"5M3S90WK" : None, +"TUUYU1J5" : None, +"UFT70NL1" : None } + +def simulatedTime(timeSimMultiplier): + return dt.datetime.isoformat(dt.datetime.now() + dt.timedelta(minutes=1) * timeSimMultiplier ) + + +def genActivePlayersEvent(timeSimMultiplier): + + ## pick some random players and generate json # + players = [] + tmp = list(possiblePlayers.keys()) + random.shuffle(tmp) + for x in range(0,10): + playerDict = dict() + curPlayer = tmp[x] + playerDict.update({ "id" : possiblePlayers[curPlayer] }) + playerDict.update({ "name" : curPlayer }) + playerDict.update({ "team" : (x % 2) + 2 }) + players += [playerDict] + + return { "etype" : "active_players", "timestamp" : simulatedTime(timeSimMultiplier), "players" : players } + +def submitt(jsonLikeObject): + #print(json.dumps(jsonLikeObject, indent=2)) + requests.post("http://localhost:{}/event-blob".format(args.target_port), json=jsonLikeObject) + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description='Insurgency rating python backend server') + parser.add_argument('--target-port', type=int, default=5000, help="HTTP API Port") + args = parser.parse_args() + + # generate uniq playerIds + idCount = 0 + for p in possiblePlayers.keys(): + possiblePlayers[p] = idCount + idCount += 1 + + for r in range(1,1000): + print(r) + winner = random.randint(2,3) + events = [] + events += [{ "etype" : "map", "timestamp" : simulatedTime(r), "map" : "kappa" }] + events += [{ "etype" : "winner", "timestamp" : simulatedTime(r+1), "winnerTeam" : winner }] + for activityEvent in range(1,10): + events += [genActivePlayersEvent((r*activityEvent)+2)] + submitt({"events" : events}) diff --git a/helper_scripts/test_single.py b/helper_scripts/test_single.py new file mode 100755 index 0000000..55b5954 --- /dev/null +++ b/helper_scripts/test_single.py @@ -0,0 +1,105 @@ +#!/usr/bin/python3 +import sys +import argparse +import datetime as dt +import json +import requests +import random + +possiblePlayers = { +"KD0FF8NC" : None, +"SA5BPTYB" : None, +"KBXGPV20" : None, +"1TNLXYO9" : None, +"S4M3Z7AT" : None, +"FO4V590D" : None, +"A116P0TY" : None, +"8OPBPAZ1" : None, +"QLIEC5F9" : None, +"TYSXZB6S" : None, +"M6Y54UI2" : None, +"AEB7EHLC" : None, +"OY8ZQEYJ" : None, +"LJ5BRST2" : None, +"CBR279YR" : None, +"GSI4J4D3" : None, +"YHTACQB8" : None, +"LD2I5Z52" : None, +"GB4ZC8Z7" : None, +"RVT3KQZC" : None, +"8L8UWEJE" : None, +"VUBITUD2" : None, +"SCG4LCK9" : None, +"VVYP0AOE" : None, +"8QFDCKB4" : None, +"P4WFV6P4" : None, +"6NDWTM7O" : None, +"7JU4HSEQ" : None, +"AE3V6PFV" : None, +"0OPM7MAZ" : None, +"160Y4R9T" : None, +"TZ8TLYO5" : None, +"5RHPGONF" : None, +"AEVXADMM" : None, +"VG70J1PR" : None, +"TWWQXOTR" : None, +"PW3X67XA" : None, +"DLGGHKWZ" : None, +"4QZVPKTG" : None, +"I75UFRUC" : None, +"6CY0QDF1" : None, +"SNHDK5V3" : None, +"91URSK04" : None, +"P1S9GSVY" : None, +"FZOY6QCW" : None, +"S0EVQ1S9" : None, +"EL3MU8YE" : None, +"5M3S90WK" : None, +"TUUYU1J5" : None, +"UFT70NL1" : None } + +def simulatedTime(timeSimMultiplier): + return dt.datetime.isoformat(dt.datetime.now() + dt.timedelta(minutes=1) * timeSimMultiplier ) + + +def genActivePlayersEvent(timeSimMultiplier): + + ## pick some random players and generate json # + players = [] + tmp = list(possiblePlayers.keys()) + random.shuffle(tmp) + for x in range(0,10): + playerDict = dict() + curPlayer = tmp[x] + playerDict.update({ "id" : possiblePlayers[curPlayer] }) + playerDict.update({ "name" : curPlayer }) + playerDict.update({ "team" : (x % 2) + 2 }) + players += [playerDict] + + return { "etype" : "active_players", "timestamp" : simulatedTime(timeSimMultiplier), "players" : players } + +def submitt(jsonLikeObject, session): + #print(json.dumps(jsonLikeObject, indent=2)) + requests.post("http://localhost:{}/single-event?session=".format(args.target_port, session), + json=jsonLikeObject) + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description='Insurgency rating python backend server') + parser.add_argument('--target-port', type=int, default=5000, help="HTTP API Port") + args = parser.parse_args() + + # generate uniq playerIds + idCount = 0 + for p in possiblePlayers.keys(): + possiblePlayers[p] = idCount + idCount += 1 + + for r in range(1,1000): + winner = random.randint(2,3) + submitt({ "etype" : "map", "timestamp" : simulatedTime(r), "map" : "kappa" }, r) + for activityEvent in range(1,10): + submitt(genActivePlayersEvent((r*activityEvent)+2), r) + submitt({ "etype" : "winner", "timestamp" : simulatedTime(r*activityEvent+2+1), + "winnerTeam" : winner }, r) + submitt({ "etype" : "round_end", "timestamp" : simulatedTime(r*activityEvent+2+1) }, r) diff --git a/httpAPI.py b/httpAPI.py deleted file mode 100644 index d28f089..0000000 --- a/httpAPI.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/python3 -import StorrageBackend as SB -import flask - - -app = flask.Flask("skillbird") - -################## HTML HELPER ######################## -def invalidParameters(*args): - return "500 - Invalid {}".format(args) - -######################################################## - -@app.route('/dumpdebug') -def dumpDebug(): - return "
".join(SB.debugInformation()) - -@app.route('/getplayer') -def getPlayer(): - playerName = flask.request.args.get("name") - return str(SB.searchPlayerByName(playerName)) - -@app.route('/getmaxentries') -def getMaxEntries(): - return str(SB.getRankListLength()) - -@app.route('/rankrange') -def getRankRange(): - try: - start = int(flask.request.args.get("start")) - end = int(flask.request.args.get("end")) - if end - start <= 0 or end - start > 100: - raise ValueError() - except ValueError: - return invalidParameters(start, end) - - players = SB.getRankRange(start, end) - return "\n".join([p.serialize() for p in players]) - -@app.route('/haschanged') -def hasChanged(): - string = flask.request.args.get("time") - # TODO get time with timezone - return SB.rankHasChanged(localizedTime) - -@app.route('/getbalancedteams') -def getBalancedTeams(): - players = flask.request.args.get("players").split(",") - useNames = flask.request.args.get("names") - return SB.getBalancedTeams(players, useNames=bool(useNames)) - -@app.route('/quality') -def quality(): - '''Get a game quality estimate for two or more given teams''' - string = flask.request.args.get("playerswithteams") - useNames = flask.request.args.get("names") - teams = string.split("|") - if len(teams) < 2: - flask.abort("Invalid input string: {}".format(string)) - teams = [ x.split(",") for x in teams ] - return SB.qualityForTeams(teams, useNames=bool(useNames)) diff --git a/insurgencyEvent.py b/insurgencyEvent.py deleted file mode 100644 index dedf8fa..0000000 --- a/insurgencyEvent.py +++ /dev/null @@ -1,70 +0,0 @@ -import Player -from datetime import datetime, timedelta - -class Event: - def __init__(self,timestamp,_map=None): - self.map = _map - self.timestamp = timestamp - def serialize(self): - raise NotImplementedError() - -class DisconnectEvent(Event): - def __init__(self,player,timestamp,line): - self.timestamp = timestamp - self.player = player - self.string = line - def serialize(self): - return {"etype":"DCE","timestamp":self.timestamp.strftime(),"string":self.string} - -class TeamchangeEvent(Event): - def __init__(self,player,old_team,timestamp,line): - self.timestamp = timestamp - self.player = player - self.old_team = int(old_team) - self.string = line - def serialize(self): - return {"etype":"TCE","timestamp":self.timestamp.strftime(),"string":self.string} - -class ActivePlayersEvent(Event): - def __init__(self,player_str,timestamp): - self.timestamp = timestamp - self.players = [] - self.string = player_str - try: - for s in player_str.split(","): - if not s or len(s.split("|"))==1: - continue - steamid = s.split("|")[1] - name = s.split("|")[2] - team = int(s.split("|")[3]) - self.players += [Player.PlayerInRound(steamid,name,team,self.timestamp)] - except IndexError: - print("ERROR: CANNOT PARSE LOGLINE: {}".format(player_str)) - print("WARNING: EVENT WILL BE USED IN INCOMPLETE STATE") - - def serialize(self): - return {"etype":"APE","timestamp":self.timestamp.strftime(),"string":self.string} - -class WinnerInformationEvent(Event): - def __init__(self,winner_side,timestamp,line): - self.timestamp = timestamp - self.winner = winner_side - self.string = line - def serialize(self): - return {"etype":"WIE","timestamp":self.timestamp.strftime(),"string":self.string} - -class MapInformationEvent(Event): - def __init__(self,_map,timestamp,line): - self.timestamp = timestamp - self.map = _map - self.string = line - def serialize(self): - return {"etype":"MIE","timestamp":self.timestamp.strftime(),"string":self.string} - -class MapInformationEvent(Event): - def __init__(self,_map,timestamp,line): - self.timestamp = timestamp - self.map = _map - self.string = line - def serialize(self): - return {"etype":"MIE","timestamp":self.timestamp.strftime(),"string":self.string} diff --git a/insurgencyEventSeries.py b/insurgencyEventSeries.py deleted file mode 100644 index ab727d5..0000000 --- a/insurgencyEventSeries.py +++ /dev/null @@ -1,106 +0,0 @@ -import insurgencyEvent as Event -from datetime import timedelta - -NO_TEAM = 0 -OBSERVERS = 1 -SECURITY = 2 -INSURGENT = 3 - -class EventSeries(list): - def __init__(self): - self.winner_side_cache = None - self.loser_side_cache = None - self.map_cache = None - self.security_cache = dict() - self.insurgent_cache = dict() - - def _cache_teams(self): - for e in self: - if type(e) == Event.ActivePlayersEvent: - # TODO deal with players that are missing without a teamchange or dc event # - for p in e.players: - if p not in self._team_from_id(p.team): - self._team_from_id(p.team).update({p:p.rating}) - else: - tmp_team = list(self._team_from_id(p.team)) - tmp_player = tmp_team[tmp_team.index(p)] - ## Add active time if player was active last event ## - if tmp_player.active: - tmp_player.active_time += e.timestamp - tmp_player.timestamp - tmp_player.timestamp = e.timestamp - tmp_player.active = True - - ## set player.active to false for disconnect or teamchange, it will be set to true at the next event that player is seen in a team ## - elif type(e) == Event.DisconnectEvent: - if e.player in self.security_cache and get_key(self.security_cache,e.player).active: - get_key(self.security_cache,e.player).active_time += e.timestamp - get_key(self.security_cache,e.player).timestamp - get_key(self.security_cache,e.player).active = False - elif e.player in self.insurgent_cache and get_key(self.insurgent_cache,e.player).active: - get_key(self.insurgent_cache,e.player).active_time += e.timestamp - get_key(self.insurgent_cache,e.player).timestamp - get_key(self.insurgent_cache,e.player).active = False - elif type(e) == Event.TeamchangeEvent: - if e.player in self._team_from_id(e.old_team): - get_key(self._team_from_id(e.old_team),e.player).active_time += e.timestamp-get_key(self._team_from_id(e.old_team),e.player).timestamp - get_key(self._team_from_id(e.old_team),e.player).active = False - - def _find_winner(self): - time = "NO_TIME_FOUND" - for e in self: - time = e.timestamp#.strftime("%d-%m-%Y %H:%M:%S") - if type(e) == Event.WinnerInformationEvent: - if self.winner_side_cache != None: - raise Warning("%s | Info: More than one Winner in series, skipping Round."%time) - self.winner_side_cache = int(e.winner) - self.loser_side_cache = ( ( ( int(e.winner) - 2 ) + 1 ) % 2) + 2 #löl - if self.winner_side_cache: - return self.winner_side_cache - else: - raise Warning("%s | Info: No winner found in series, skipping Round."%time) - - def _team_from_id(self,tid): - if tid == OBSERVERS or tid == NO_TEAM: - return dict() - elif tid == SECURITY: - return self.security_cache; - elif tid == INSURGENT: - return self.insurgent_cache; - else: - raise ValueError("TeamID must be 0 - NoTeam, 1 - Observers, 2 - Security or 3 - Insurgent, but was {}".format(tid)) - - def get_duration(self): - key = lambda x: x.timestamp - max_ = max(self,key=key) - min_ = min(self,key=key) - ret = max_.timestamp-min_.timestamp - if ret > timedelta(seconds=60*30): - raise Warning("%s | Info: Round Length was %s, too long, ignoring."%(min_.timestamp,ret)) - if ret < timedelta(seconds=60*3): - raise Warning("%s | Info: Round Length was %s, too short, ignoring."%(min_.timestamp,ret)) - return ret - - def get_starttime(self): - key = lambda x: x.timestamp - return min(self,key=key).timestamp - - def get_winners(self): - if not self.security_cache or not self.insurgent_cache: - self._cache_teams() - self._find_winner() - return self._team_from_id(self.winner_side_cache) - - def get_losers(self): - if not self.security_cache or not self.insurgent_cache: - self._cache_teams() - self._find_winner() - return self._team_from_id(self.loser_side_cache) - - def get_map(self): - if self.map_cache == None: - for e in self: - if type(e) == Event.MapInformationEvent: - self.map_cache = e.map - return self.map_cache - -def get_key(dic,key): - tmp = list(dic) - return tmp[tmp.index(key)] diff --git a/insurgencyParsing.py b/insurgencyParsing.py deleted file mode 100644 index 9e436cb..0000000 --- a/insurgencyParsing.py +++ /dev/null @@ -1,215 +0,0 @@ -# insurgency specific -import insurgencyEvent as Event -import StorrageBackend as SB -import TrueSkillWrapper as TS -from insurgencyEventSeries import EventSeries - -# general -import Player -import Round -from datetime import datetime -import time - -def is_round_end(line): - return "0x42,round_end_active" in line -def is_plugin_output(line): - return "0x42" in line -def is_winner_event(line): - return "0x42,winner" in line -def get_key(dic,key): - tmp = list(dic) - return tmp[tmp.index(key)] - -def loadCache(cacheFile): - rounds = [] - if not cacheFile: - return None - with open(cacheFile, "r") as f: - for line in f: - rounds += [Round.Round.deserialize(line)] - - # return if file was empty # - if len(rounds) == 0: - return None - - # put them in right order # - rounds = sorted(rounds, key=lambda r: r.start) - - # parse Rounds # - for r in rounds: - try: - SB.sync_from_database(r.winners) - SB.sync_from_database(r.losers) - TS.evaluate_round(r) - except Warning: - pass - - # find newest # - lastRoundByDate = rounds[-1].start - return lastRoundByDate - - - -def parse(f, exit_on_eof=True, start_at_end=False, cacheFile=None): - last_round_end = None - seek_start = True - round_lines = [] - last_line_was_winner = False - lineCount = 0 - startTime = datetime.now() - - while True: - old_line_nr = f.tell() - line = f.readline() - - # if no line or incomplete line, sleep and try again # - if not line or not line.strip("\n"): - if exit_on_eof: - return - time.sleep(5000) - continue - elif not line.endswith("\n"): - f.seek(old_line_nr) - time.sleep(5000) - continue - - lineCount += 1 - if lineCount % 100000 == 0: - diff = datetime.now() - startTime - print("At Line: {} Tot: {} Per 100k:{}".format(\ - lineCount, diff, diff/(lineCount/100000))) - - if seek_start and not "round_start_active" in line and line: - continue - elif "round_start_active" in line: - seek_start = False - elif "plugin unloaded" in line: - round_lines = [] - seek_start = True - continue - - evalRound = False - # ad line and stop if it was round end # - round_lines += [line] - if is_round_end(line): - last_round_end = line - evalRound = True - elif is_winner_event(line): - last_line_was_winner = True - - # parse and evaluate round # - if evalRound: - nextRound = parseRoundFromLines(round_lines, cacheFile) - round_lines = [] - evalRound = False - if nextRound: - try: - TS.evaluate_round(nextRound) - except Warning as e: - pass - - -def parseRoundFromLines(r, cacheFile=None): - - # get an event series # - es = EventSeries() - for l in r: - if is_plugin_output(l): - e = parse_line_to_event(l) - if e != None: - es += [e] - - # get players with teams # - try: - winners = es.get_winners() - losers = es.get_losers() - except Warning as e: - TS.dirty_rounds += 1 - return None - - # deal with teamchanges # - losers_pop = [] - winners_pop = [] - for p in winners: - if p in losers: - if get_key(losers,p).active_time < get_key(winners,p).active_time: - get_key(winners,p).active_time -= get_key(losers,p).active_time - losers_pop += [p] - else: - get_key(losers,p).active_time -= get_key(winners,p).active_time - winners_pop += [p] - - # we cannot change dict during iteration # - for p in losers_pop: - losers.pop(p) - for p in winners_pop: - winners.pop(p) - - # get ratings if there are any yet # - SB.sync_from_database(winners) - SB.sync_from_database(losers) - - try: - es.get_duration() - except Warning as e: - TS.dirty_rounds += 1 - return None - return Round.Round(winners, losers, es.get_map(), es.get_duration(), \ - es.get_starttime(), cache=cacheFile) - -def create_event(etype,line,timestamp): - TEAMCHANGE = ["teamchange"] - ACTIVE_PLAYERS = ["ct","dc","round_start_active","round_end_active","tc"] - DISCONNECT = ["disconnect"] - WINNER_INFO = ["winner"] - MAP_INFO = ["mapname"] - IGNORE = ["map_start_active","start","plugin unloaded"] - - if etype in TEAMCHANGE: - player = Player.DummyPlayer(line.split(",")[1]) - old_team = line.split(",")[2] - return Event.TeamchangeEvent(player,old_team,timestamp,line) - - elif etype in ACTIVE_PLAYERS: - return Event.ActivePlayersEvent(line,timestamp) - - elif etype in DISCONNECT: - player = Player.DummyPlayer(line.split(",")[1]) - return Event.DisconnectEvent(player,timestamp,line) - - elif etype in WINNER_INFO: - winner_side = line.split(",")[1] - return Event.WinnerInformationEvent(winner_side,timestamp,line) - - elif etype in MAP_INFO: - return Event.MapInformationEvent(line.split(",")[1],timestamp,line) - - elif etype in IGNORE: - pass - - else: - raise RuntimeError("Cannot create event from logline. (etype was: '{}')".format(etype)) - -def parseDate(l): - if ": L " in l.split("0x42")[0]: - timestamp = datetime.strptime(l.split(": L ")[1].split(": [")[0],"%m/%d/%Y - %H:%M:%S") - else: - timestamp = datetime.strptime(l.split(": [ints_logging.smx]")[0],"L %m/%d/%Y - %H:%M:%S") - return timestamp - - -def parse_line_to_event(l): - tmp = l.split("0x42,")[1].strip("\n") - etype = tmp.split(",")[0].split("|")[0] - try: - timestamp = parseDate(l) - event = create_event(etype,tmp,timestamp) - except ValueError: - print(" ---- NO TIME ---- | WARNING: Failed to parse time for event, SKIP") - return None - except RuntimeError as e: - print("Failed to parse Event in line, skipping: {}".format(str(e))) - return None - - SB.save_event(event); - return event diff --git a/python/Round.py b/python/Round.py new file mode 100644 index 0000000..312e0c1 --- /dev/null +++ b/python/Round.py @@ -0,0 +1,142 @@ +import json +import datetime as dt +import dateutil.parser +import backends.entities.Players as Players +import backends.database as db + +## A comment on why the login-offset is nessesary ## +## - losing teams tend to have players leaving and joining more rapidly +## - every time a new player joins he has to setup +## - new players are unfamiliar with postions of enemy team +## - new players have to run from spawn +## --> their impact factor (which is calculated from their active time) must account for that +loginoffset = dt.timedelta(seconds=60) + +class Round: + + def __init__(self, winnerTeam, loserTeam, _map, duration, startTime, winnerSide): + + self.winners = winnerTeam + self.losers = loserTeam + self.winnerSide = winnerSide + self.map = _map + self.duration = duration + self.start = startTime + + ### Sync players from Databse ### + for p in self.winners + self.losers: + playerInDB = db.getOrCreatePlayer(p) + p.rating = playerInDB.rating + + + + def normalized_playtimes(self): + '''returns a dict-Object with {key=(teamid,player):value=player_time_played/total_time_of_round}''' + + np = dict() + for p in self.winners: + if self.duration == None: + d = 1.0 + else: + d = (p.activeTime-loginoffset)/self.duration + if d < -1: + raise AssertionError("Normalized Playtime was less than -1 ??") + if d < 0: + d = 0.0 + elif d > 1: + d = 1.0 + np.update({(0,p):d}) + + for p in self.losers: + if self.duration == None: + d = 1.0 + else: + d = (p.activeTime-loginoffset)/self.duration + if d < 0: + d = 0.0 + elif d > 1: + d = 1.0 + np.update({(1,p):d}) + + return np + + def pt_difference(self): + '''Used to check difference in playtimes per team''' + + if self.duration == None: + return 1 + w1 = w2 = 0 + for p in self.winners: + if p.is_fake: + w1 += 1.0 + continue + d = (p.activeTime-loginoffset)/self.duration + if d < 0: + d = 0.0 + elif d > 1: + d = 1.0 + w1 += d + for p in self.losers: + d = (p.activeTime-loginoffset)/self.duration + if p.is_fake: + w2 += 1.0 + continue + if d < 0: + d = 0.0 + elif d > 1: + d = 1.0 + w2 += d + + # no div0 plox + if min(w1,w2) <= 0: + return 0 + + return max(w1,w2)/min(w1,w2) + + def toJson(self): + winnersList = [] + losersList = [] + for w in self.winners: + winnersList += [{ "playerId" : w.id, + "playerName" : w.name, + "isFake" : w.is_fake, + "activeTime" : w.activeTime.total_seconds() }] + + for w in self.losers: + losersList += [{ "playerId" : w.id, + "playerName" : w.name, + "isFake" : w.is_fake, + "activeTime" : w.activeTime.total_seconds() }] + + retDict = { "winners" : winnersList, + "losers" : losersList, + "startTime" : self.start.isoformat(), + "duration" : self.duration.total_seconds(), + "map" : self.map, + "winner-side" : self.winnerSide } + + return json.dumps(retDict) + +def fromJson(jsonDict): + winnersList = [] + losersList = [] + + timestamp = dateutil.parser.isoparse(jsonDict["startTime"]) + winnerTeam = jsonDict.get("winner-side") + if not winnerTeam: + winnerTeam = -1 + loserTeam = -2 + else: + loserTeam = (winnerTeam % 2) + 2 + + for p in jsonDict["winners"]: + pObj = Players.PlayerInRound(p["playerId"], p["playerName"], winnerTeam, timestamp) + pObj.activeTime = int(p["activeTime"]) + winnersList += [pObj] + + for p in jsonDict["losers"]: + pObj = Players.PlayerInRound(p["playerId"], p["playerName"], loserTeam, timestamp) + pObj.activeTime = int(p["activeTime"]) + losersList += [pObj] + + return Round(winnersList, losersList, jsonDict["duration"], jsonDict["map"], timestamp, winnerTeam) diff --git a/python/backends/database.py b/python/backends/database.py new file mode 100644 index 0000000..6c92a03 --- /dev/null +++ b/python/backends/database.py @@ -0,0 +1,99 @@ +import sqlite3 +import backends.entities.Players as Players +import backends.trueskillWrapper as trueskill + +# setup +# create TABLE players (id TEXT PRIMARY KEY, name TEXT, lastgame TEXT, wins INTEGER, mu REAL, sigma REAL, game INTEGER); + +DATABASE = "players.sqlite" + +def getPlayer(playerId): + conn = sqlite3.connect("players.sqlite") + try: + cursor = conn.cursor() + cursor.execute("SELECT * FROM players WHERE id = ?", (playerId,)) + rows = cursor.fetchall() + if len(rows) < 1: + return None + else: + playerId, playerName, lastGame, wins, mu, sigma, games = rows[0] + + return Players.PlayerInDatabase(playerId, playerName, + trueskill.newRating(mu=mu, sigma=sigma), wins, games) + finally: + conn.close() + +def getOrCreatePlayer(player): + playerInDb = getPlayer(player.id) + if not playerInDb: + return savePlayerToDatabase(player) + else: + return playerInDb + + +def getMultiplePlayers(playerIdList): + return [ getPlayer(p) for p in playerIdList ] + +def savePlayerToDatabase(player, incrementWins=0): + conn = sqlite3.connect(DATABASE) + cursor = conn.cursor() + + if not player.rating: + player.rating = trueskill.newRating() + playerFromDatabase = getPlayer(player.id) + if playerFromDatabase: + cursor.execute('''UPDATE players SET id = ?, + name = ?, + lastgame = ?, + wins = ?, + mu = ?, + sigma = ?, + games = ? + WHERE id = ?''', + (player.id, player.name, None, playerFromDatabase.wins + incrementWins, + player.rating.mu, player.rating.sigma, playerFromDatabase.games + 1, player.id)) + else: + cursor.execute("INSERT INTO players VALUES (?,?,?,?,?,?,?)", + (player.id, player.name, None, 0, + player.rating.mu, player.rating.sigma, 0)) + conn.commit() + conn.close() + return getPlayer(player.id) + +def saveMultiplePlayersToDatabase(playerList, incrementWins=0): + for p in playerList: + savePlayerToDatabase(p, incrementWins) + +## open leaderboard functions ## +def findPlayerByName(playerName): + conn = sqlite3.connect(DATABASE) + cursor = conn.cursor() + playerNamePrepared = "%{}%".format(playerName.replace("%", "%%")) + cursor.execute("SELECT * FROM players WHERE name == ?", (playerName,)) + rows = cursor.fetchall() + playerRow = None + if len(rows) < 1: + cursor.execute("SELECT * FROM players WHERE name LIKE ?", (playerNamePrepared,)) + rows = cursor.fetchall() + if len(rows) < 1: + return None + playerRow = rows[0] + else: + playerRow = rows[0] + + playerId, playerName, lastGame, wins, mu, sigma, games = playerRow + conn.close() + return Players.PlayerInDatabase(playerId, playerName, + trueskill.newRating(mu=mu, sigma=sigma), wins, games) + +def getTotalEntries(playerName): + conn = sqlite3.connect(DATABASE) + cursor = conn.cursor() + playerNamePrepared = "%{}%".format(playerName.replace("%", "%%")) + cursor.execute("select count(*) from players") + count = cursor.fetchone() + conn.close() + return count + +def getRankRange(start, end): + pass diff --git a/python/backends/entities/Players.py b/python/backends/entities/Players.py new file mode 100644 index 0000000..d69ec4c --- /dev/null +++ b/python/backends/entities/Players.py @@ -0,0 +1,67 @@ +import datetime as dt +import json + +class Player: + def __init__(self, playerId, name, rating=None): + self.rating = rating + self.id = playerId + self.name = name + + def __hash__(self): + return hash(self.id) + + def __eq__(self, other): + if not other: + return False + elif isinstance(other, str): + return self.id == other + elif isinstance(other, Player): + return self.id == other.id + else: + raise TypeError("Player: Unsupported equals with types {} and {}".format(type(other),type(self))) + + def __str__(self): + return "Player: {}, ID: {}".format(self.name, self.id) + +class PlayerInRound(Player): + def __init__(self, playerId, name, team, timestamp, is_fake=False): + self.name = name + self.id = playerId + self.team = int(team) + self.active = True + self.rating = None + self.activeTime = dt.timedelta(0) + self.is_fake = is_fake + self.timestamp = timestamp + + def __str__(self): + return "PlayerInRound: N: {} ID: {} Team: {}".format(self.name, self.id, self.team) + +class PlayerInDatabase(Player): + + def __init__(self, playerId, name, rating, wins, games): + self.id = playerId + self.name = name + self.rating = rating + self.lastUpdate = dt.datetime.now() + self.wins = wins + self.games = games + + def winratio(self): + if self.games == 0: + return "---" + return str(int(self.wins*100/self.games)) + + def getName(self): + return self.name.encode('utf-8')[:25].decode('utf-8','ignore').rstrip(" ") + + def toJson(self): + retDict = { "name" : self.name, + "id" : self.id, + "rating-mu" : self.rating.mu, + "rating-sigma" : self.rating.mu, + "games" : self.games, + "wins" : self.wins, + "last-game" : self.lastUpdate.isoformat()} + + return json.dumps(retDict) diff --git a/python/backends/eventStream.py b/python/backends/eventStream.py new file mode 100644 index 0000000..9f62bd6 --- /dev/null +++ b/python/backends/eventStream.py @@ -0,0 +1,219 @@ +import abc +import dateutil.parser +import datetime as dt + +import Round +import backends.entities.Players as Players + +NO_TEAM = 0 +OBSERVERS = 1 +SECURITY = 2 +INSURGENT = 3 + +def _getKey(dic, key): + '''Helper function''' + tmp = list(dic) + return tmp[tmp.index(key)] + + +def parse(events): + '''Parse a string blob representing a full round''' + + eventsParsed = [] + for eventJsonObject in events: + eventsParsed += [parseEventString(eventJsonObject)] + + es = EventSeries(eventsParsed) + return Round.Round(es.winnerTeam, es.loserTeam, es.map, es.duration, es.startTime, es.winnerTeamId) + +def parseEventString(event): + '''Take a dictionary representing an event and return an actual event object''' + + TEAMCHANGE = ["teamchange"] + ACTIVE_PLAYERS = ["ct","dc","round_start_active","round_end_active","tc","active_players"] + DISCONNECT = ["disconnect"] + WINNER_INFO = ["winner"] + MAP_INFO = ["mapname", "map"] + IGNORE = ["map_start_active","start","plugin unloaded"] + + print(event) + + etype = event["etype"] + if False: + pass +# elif etype in TEAMCHANGE: +# return TeamchangeEvent(event) + elif etype in ACTIVE_PLAYERS: + return ActivePlayersEvent(event) +# elif etype in DISCONNECT: +# return DisconnectEvent(event) + elif etype in WINNER_INFO: + return WinnerInformationEvent(event) + elif etype in MAP_INFO: + return MapInformationEvent(event) + elif etype in IGNORE: + pass + +class Event(abc.ABC): + '''Abstract class all events inherit from''' + pass + +#class DisconnectEvent(Event): +# '''Event describing a disconnect by an individual player''' +# +# def __init__(self, event): +# self.timestamp = dt.datetime.fromtimestamp(event["timestamp"]) +# self.player = event["playerId"] +# +#class TeamchangeEvent(Event): +# '''Event describing a teamchange by an individual player''' +# +# def __init__(self, event): +# self.timestamp = dt.datetime.fromtimestamp(event["timestamp"]) +# self.player = event["playerId"] +# self.old_team = event["previousTeam"] + +class ActivePlayersEvent(Event): + '''Event describing as absolute values the currently active player''' + + def __init__(self, event): + self.timestamp = dt.datetime.fromtimestamp(event["timestamp"]) + self.players = [ Players.PlayerInRound(p["id"], p["name"], p["team"], self.timestamp) + for p in event["players"] ] + +class WinnerInformationEvent(Event): + '''Event describing which team has won the game''' + + def __init__(self, event): + self.timestamp = dt.datetime.fromtimestamp(event["timestamp"]) + self.winner = event["winnerTeam"] + +class MapInformationEvent(Event): + '''Event describing the current map''' + + def __init__(self, event): + self.timestamp = dt.datetime.fromtimestamp(event["timestamp"]) + self.map = event["map"] + +class EventSeries(): + def __init__(self, events): + + self.events = events + self.winnerTeam = None + self.winnerTeamId = -1 + self.loserTeam = None + self.loserTeamId = -1 + self.map = "" + + lastEvent = max(events, key=lambda e: e.timestamp) + firstEvent = min(events, key=lambda e: e.timestamp) + + self.duration = lastEvent.timestamp - firstEvent.timestamp + self.startTime = firstEvent.timestamp + + + + self.teamA = [] + self.teamAId = 2 + self.teamB = [] + self.teamBId = 3 + + for e in events: + if type(e) == ActivePlayersEvent: + for playerInRound in e.players: + + ## Case 1: Player isn't in any team yet + if playerInRound not in self.teamA and playerInRound not in self.teamB: + playerInRound.active = True + if playerInRound.team == self.teamAId: + self.teamA += [playerInRound] + else: + self.teamB += [playerInRound] + + ## Case 2: Player is in the wrong team + if playerInRound not in self._teamFromId(playerInRound.team): + index = self._teamFromId(playerInRound.team, inverted=True).index(playerInRound) + playerInEventSeries = self._teamFromId(playerInRound.team, inverted=True)[index] + + # update playtime # + playerInEventSeries.active = False + playerInEventSeries.activeTime += e.timestamp - playerInEventSeries.timestamp + + # add player to correct team # + playerInRound.active = True + if playerInRound.team == self.teamAId: + self.teamA += [playerInRound] + else: + self.teamB += [playerInRound] + + ## Case 3: Player is already in the correct team + else: + index = self._teamFromId(playerInRound.team).index(playerInRound) + playerInEventSeries = self._teamFromId(playerInRound.team)[index] + + # update playtime # + if playerInEventSeries.active: + playerInEventSeries.activeTime += e.timestamp - playerInEventSeries.timestamp + playerInEventSeries.timestamp = e.timestamp + playerInEventSeries.active = True + + # mark all missing players as inactive and update their play times # + for playerInEventSeries in self.teamA + self.teamB: + if playerInEventSeries not in e.players: + + # update playtime # + playerInEventSeries.active = False + playerInEventSeries.activeTime += e.timestamp - playerInEventSeries.timestamp + + elif type(e) == WinnerInformationEvent: + self.winnerTeamId = int(e.winner) + self.winnerTeam = self._teamFromId(self.winnerTeamId) + self.loserTeam = self._teamFromId(self.winnerTeamId, inverted=True) + elif type(e) == MapInformationEvent: + self.map = e.map + + ### normalize teamchanges + toBeRemovedFromLosers = [] # cannot change iteable during iteration + toBeRemovedFromWinners = [] + + for playerInEventSeries in self.winnerTeam: + if playerInEventSeries in self.loserTeam: + + # get active time in both teams # + playerLoserTeamIndex = self.loserTeam.index(playerInEventSeries) + loserTeamActiveTime = self.loserTeam[playerLoserTeamIndex].activeTime + winnerTeamActiveTime = playerInEventSeries.activeTime + + # substract the smaller active time and mark player # + # to be removed from the team he played less on # + if winnerTeamActiveTime > loserTeamActiveTime: + toBeRemovedFromLosers += [playerInEventSeries] + playerInEventSeries.activeTime - loserTeamActiveTime + else: + toBeRemovedFromWinners += [playerInEventSeries] + self.loserTeam[playerLoserTeamIndex].activeTime -= winnerTeamActiveTime + + # after iteration actually remove the players # + for player in toBeRemovedFromWinners: + self.winnerTeam.remove(player) + for player in toBeRemovedFromLosers: + self.loserTeam.remove(player) + + + def _teamFromId(self, teamId, inverted=False): + '''Return the attribute array representing the teamId in this event series + or a dummy array for observers and no-team + Inverted: return the team NOT beloning to the teamId''' + + if inverted: + teamId = ( teamId + 1 ) % 2 +2 # 2 => 3 and 3 => 2, 0/1 don't matter + + if teamId == OBSERVERS or teamId == NO_TEAM: + return [] + elif teamId == 2: + return self.teamA; + elif teamId == 3: + return self.teamB; + else: + errorMsg = "TeamID must be 0 - NoTeam, 1 - Observers, 2 - Security or 3 - Insurgent, but was {}" + raise ValueError(errorMsg.format(teamId)) diff --git a/backends/genericFFA.py b/python/backends/genericFFA.py similarity index 100% rename from backends/genericFFA.py rename to python/backends/genericFFA.py diff --git a/python/backends/trueskillWrapper.py b/python/backends/trueskillWrapper.py new file mode 100644 index 0000000..fcd3fb5 --- /dev/null +++ b/python/backends/trueskillWrapper.py @@ -0,0 +1,85 @@ +#!/usr/bin/python3 +from trueskill import TrueSkill, Rating + +env = TrueSkill(draw_probability=0, mu=1500, sigma=833, tau=40, backend='mpmath') +env.make_as_global() + +##################################################### +################ HANDLE RATING INPUT ################ +##################################################### + +def evaluateRound(r): + + # do no rate rounds that were too imbalanced # + if r.pt_difference() >= 2.1 or r.pt_difference() == 0: + raise ValueError("Teams too imbalanced: {} (zero=inifinity)".format(r.pt_difference())) + + weights = r.normalized_playtimes() + + # trueskill need groups = [ { key : rating, ... } , { key : rating, ... } ] + # --------------- --------------- + # Team 1 (winners) Team 2 (losers) + groups=[dict(), dict()] + for playerInDatabase in r.winners: + groups[0].update( { playerInDatabase : playerInDatabase.rating } ) + for playerInDatabase in r.losers: + groups[1].update( { playerInDatabase : playerInDatabase.rating } ) + + if len(groups[1]) == 0 or len(groups[0]) ==0 : + raise ValueError("One of the rated Teams was empty") + + rated = env.rate(groups, weights=weights) + return rated + +#def rate_ffa(players): +# '''Takes a list of players in reverse finishing order (meaning best first) +# perform a truskill evaluation and write it to database''' +# +# # one player doesnt make sense # +# if len(players) <= 1: +# return False +# +# # create list of dicts for trueskill-library # +# playerRatingTupelDicts = [] +# for p in players: +# playerRatingTupelDicts += [{p:p.rating}] +# +# # generate ranks +# ranks = [ i for i in range(0, len(playerRatingTupelDicts))] +# +# # rate and safe to database # +# rated = env.rate(playerRatingTupelDicts) +# +# # create sync dict # +# # first player is handled seperately # +# allPlayer = dict() +# for playerRatingDict in rated[1:]: +# allPlayer.update(playerRatingDict) +# +# # only first player gets win # +# StorrageBackend.sync_to_database(rated[0], True) +# StorrageBackend.sync_to_database(allPlayer, False) +# +# for p in allPlayer.keys(): +# print(p) + +##################################################### +################### LOCK/GETTER #################### +##################################################### + +def newRating(mu=None, sigma=None): + if mu: + return Rating(mu=mu, sigma=env.sigma) + elif mu and sigma: + return Rating(mu=mu, sigma=sigma) + else: + return env.create_rating() + +def getEnviroment(): + return env + +def balance(players, buddies=None): + raise NotImplementedError() + +def get_player_rating(sid, name="NOTFOUND"): + raise NotImplementedError() diff --git a/python/httpAPI.py b/python/httpAPI.py new file mode 100644 index 0000000..fda7850 --- /dev/null +++ b/python/httpAPI.py @@ -0,0 +1,122 @@ +#!/usr/bin/python3 + +import backends.database as db +import backends.trueskillWrapper as ts +import backends.eventStream +import Round +import json +import flask +import os +import sys +import datetime as dt + +app = flask.Flask("skillbird") + +def run(port, parser): + app.run(port=port) + +## SERVER QUERIES ### +@app.route('/get-player-rating-msg') +def getPlayer(): + playerId = flask.request.args.get("id") + p = db.getPlayer(playerId) + if not p: + return ("Player not found", 404) + return "{}'s Rating: {}".format(p.name, p.rating.mu - p.rating.sigma) + +#### Open Leaderboard ### +@app.route('/findplayer') +def getFind(): + playerName = flask.request.args.get("name") + p = db.findPlayerbyName(playerName) + return p.toJson() + +@app.route('/getmaxentries') +def getMaxEntries(): + return "{}".format(db.getTotalEntries()) + +@app.route('/rankrange') +def getRankRange(): + try: + start = int(flask.request.args.get("start")) + end = int(flask.request.args.get("end")) + if end - start <= 0 or end - start > 100: + raise ValueError() + except ValueError: + return invalidParameters(start, end) + + players = db.getRankRange(start, end) + return "" + +################# DataSubmission ####################### +@app.route('/submitt-round', methods=["POST"]) +def jsonRound(): + '''Submitt a full Round in the form a json directory as described in the documentation''' + evaluateRound(Round.fromJson(flask.request.json)) + + return ("", 204) + +@app.route('/single-event', methods=["POST"]) +def singleEvent(): + '''Location for submitting single events with a session id, such an event stream must be terminated + with an "round_end" event''' + + # get and check request parameters # + jsonDict = flask.request.json + session = flask.request.args.get("session") + if not session: + return ("Bad session ID", 500) + + # define local names # + ROUND_END_IDENT = "round_end" + PATH_BASE = "data/" + SESSION_FILE_FORMAT = "{}-session-events.txt" + sesionFile = SESSION_FILE_FORMAT.format(session) + fullPath = os.path.join(PATH_BASE, sesionFile) + + # if there is a local session check it's age first and delete it if it is too old # + if os.path.isfile(fullPath): + fileLastModified = dt.datetime.fromtimestamp(os.path.getmtime(fullPath)) + maxAge = dt.timedelta(minutes=30) + if dt.datetime.now() - fileLastModified > maxAge: + print("Removed orphaned session file: {}".format(fullPath), sys.stderr) + os.remove(fullPath) + + if jsonDict["etype"] == ROUND_END_IDENT: + events = [] + with open(fullPath, "r") as f: + for l in f: + events += [json.loads(l)] + os.remove(fullPath) + matchRound = backends.eventStream.parse(events) + evaluateRound(matchRound) + else: + with open(fullPath, "a") as f: + f.write(json.dumps(jsonDict)) + f.write("\n") + + return ("", 204) + + + +@app.route('/event-blob', methods=["POST"]) +def eventBlob(): + '''Location for submitting full rounds in the form of an event array''' + + matchRound = backends.eventStream.parse(flask.request.json["events"]) + evaluateRound(matchRound) + return ("", 204) + +def evaluateRound(matchRound): + '''Run a match round throught the backand (includeing persisting it''' + + # winners/losers are both dictionaries in the form of { PlayerInDatabase : newRating } # + winnersRated, losersRated = ts.evaluateRound(matchRound) + + for playerInDatabase in winnersRated.keys(): + playerInDatabase.rating = winnersRated[playerInDatabase] + for playerInDatabase in losersRated.keys(): + playerInDatabase.rating = losersRated[playerInDatabase] + + db.saveMultiplePlayersToDatabase(winnersRated.keys(), incrementWins=1) + db.saveMultiplePlayersToDatabase(losersRated.keys()) diff --git a/python/init.py b/python/init.py new file mode 100755 index 0000000..e4e2060 --- /dev/null +++ b/python/init.py @@ -0,0 +1,33 @@ +#!/usr/bin/python3 +import sys +import argparse +import httpAPI + +#import backends.genericFFA +import backends.eventStream + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description='Insurgency rating python backend server') + + ### parser backend configuration ### + parser.add_argument('--parser-backend', required=True, choices=['ffa', 'eventStream'], + help="Use the free-for-all parser") + + ### readin configuration ### + parser.add_argument('--http-api-port', type=int, default=5000, help="HTTP API Port") + + args = parser.parse_args() + + ### set parser ### + if args.parser_backend == "ffa": + #parser = backends.genericFFA + raise NotImplementedError() + elif args.parser_backend == "eventStream": + parser = backends.eventStream + else: + print("Unsupported parser: {}".format(args.parser_backend), file=sys.stderr) + sys.exit(1) + + ### set input mode ### + httpAPI.run(args.http_api_port, parser) diff --git a/req.txt b/req.txt index 5a8b8ad..c933b02 100644 --- a/req.txt +++ b/req.txt @@ -1,4 +1,4 @@ flask fuzzywuzzy trueskill -psycopg2-binary \ No newline at end of file +sqlite3 diff --git a/startInsurgency.py b/startInsurgency.py deleted file mode 100755 index 8fdb57a..0000000 --- a/startInsurgency.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/python3 -import sys -import NetworkParser -import FileReader -import argparse -import StorrageBackend -import NetworkListener -import httpAPI -from threading import Thread - -import backends.genericFFA as ffaParser -import insurgencyParsing as iparser - -parser = argparse.ArgumentParser(description='Insurgency rating python backend server') -parser.add_argument('files', metavar='FILE', type=str, nargs='+',\ - help='one or more logfiles to parse') -parser.add_argument('--parse-only','-po',dest='parse_only', action='store_const',\ - const=True, default=False,help="only parse, do not listen for queries") -parser.add_argument('--start-at-end','-se',dest='start_at_end', action='store_const',\ - const=True, default=False, \ - help="start at the end of each file (overwrites no-follow)") -parser.add_argument('--no-follow','-nf',dest='nofollow', action='store_const',\ - const=True, default=False, \ - help="wait for changes on the files (does not imply start-at-end)") -parser.add_argument('--one-thread', dest='oneThread', action='store_const',\ - const=True, default=False, \ - help="run everything in main thread (implies no-follow)") -parser.add_argument('--cache-file', dest='cacheFile',\ - help="A cache file which makes restarting the system fast") -parser.add_argument('--ffa', action='store_const', default=False, const=True, \ - help="Use the free-for-all parser") -parser.add_argument('--http-api-port', default=5000, \ - help="Port to use for http-api port") - -if __name__ == "__main__": - args = parser.parse_args() - if args.cacheFile: - open(args.cacheFile, "a").close() - - if args.ffa: - parser = ffaParser - else: - parser = iparser - - FileReader.readfiles( args.files ,\ - start_at_end=args.start_at_end,\ - nofollow=args.nofollow, \ - oneThread=args.oneThread, \ - cacheFile=args.cacheFile, \ - parsingBackend=parser) - - if not args.parse_only: - print("Starting network-listener(s)") - Thread(target=httpAPI.app.run,kwargs={'port':args.http_api_port}).start() - NetworkListener.listen() - else: - sys.exit(0)