From fc5a825981f3ca3bc3695cc95ed22e0d661052f0 Mon Sep 17 00:00:00 2001 From: Sheppy Date: Mon, 15 Jun 2020 00:50:54 +0200 Subject: [PATCH] 2020 rewrite --- .gitignore | 3 + FileReader.py | 53 ---- NetworkListener.py | 34 --- NetworkParser.py | 195 --------------- PSQL.py | 23 -- Player.py | 90 ------- README.md | 142 ++++------- Round.py | 150 ------------ StorrageBackend.py | 259 -------------------- TrueSkillWrapper.py | 139 ----------- helper_scripts/test.py | 105 ++++++++ helper_scripts/test_single.py | 105 ++++++++ httpAPI.py | 61 ----- insurgencyEvent.py | 70 ------ insurgencyEventSeries.py | 106 -------- insurgencyParsing.py | 215 ---------------- python/Round.py | 142 +++++++++++ python/backends/database.py | 99 ++++++++ python/backends/entities/Players.py | 67 +++++ python/backends/eventStream.py | 219 +++++++++++++++++ {backends => python/backends}/genericFFA.py | 0 python/backends/trueskillWrapper.py | 85 +++++++ python/httpAPI.py | 122 +++++++++ python/init.py | 33 +++ req.txt | 2 +- startInsurgency.py | 57 ----- 26 files changed, 1035 insertions(+), 1541 deletions(-) delete mode 100644 FileReader.py delete mode 100644 NetworkListener.py delete mode 100644 NetworkParser.py delete mode 100644 PSQL.py delete mode 100644 Player.py delete mode 100644 Round.py delete mode 100644 StorrageBackend.py delete mode 100644 TrueSkillWrapper.py create mode 100755 helper_scripts/test.py create mode 100755 helper_scripts/test_single.py delete mode 100644 httpAPI.py delete mode 100644 insurgencyEvent.py delete mode 100644 insurgencyEventSeries.py delete mode 100644 insurgencyParsing.py create mode 100644 python/Round.py create mode 100644 python/backends/database.py create mode 100644 python/backends/entities/Players.py create mode 100644 python/backends/eventStream.py rename {backends => python/backends}/genericFFA.py (100%) create mode 100644 python/backends/trueskillWrapper.py create mode 100644 python/httpAPI.py create mode 100755 python/init.py delete mode 100755 startInsurgency.py 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)