mirror of
https://github.com/FAUSheppy/skillbird
synced 2025-12-06 06:51:34 +01:00
2020 rewrite
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,7 @@
|
|||||||
cache
|
cache
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite.clean
|
||||||
|
*.txt
|
||||||
*.log
|
*.log
|
||||||
*.swp
|
*.swp
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
195
NetworkParser.py
195
NetworkParser.py
@@ -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<len(player):
|
|
||||||
teams[count%2] += [players[count]]
|
|
||||||
if len(players) % 2 == 1 and count == len(players)-1:
|
|
||||||
if TS.quality(teams[0] + [p],teams[1]) > 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
|
|
||||||
23
PSQL.py
23
PSQL.py
@@ -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 []
|
|
||||||
90
Player.py
90
Player.py
@@ -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)
|
|
||||||
142
README.md
142
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.
|
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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Adaption for your own Data
|
# Data Transmission
|
||||||
### Data Requirements
|
## /event-blob
|
||||||
To work correctly you data must have the following fields:
|
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.
|
||||||
- unique player id or name
|
|
||||||
- player(s) in winning and losing team
|
|
||||||
|
|
||||||
You may also have the following informations:
|
{
|
||||||
- data/time of the game (you cannot use the cached-rebuild feature without this)
|
events : [ { ... }, { ... } ]
|
||||||
- time players spent playing compared to the full length of the game
|
|
||||||
- teamchanges of players
|
|
||||||
- different maps
|
|
||||||
|
|
||||||
### 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
|
### ActivePlayersEvent
|
||||||
0x42,plugin_unloaded
|
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 current map
|
{
|
||||||
0x42,mapname,MAP_NAME
|
"etype" : "active_players",
|
||||||
|
"timestamp" : ISO_TIMESTAMP,
|
||||||
|
"players" : [
|
||||||
|
{
|
||||||
|
"id" : UNIQUE_PLAYER_ID,
|
||||||
|
"name" : COLOQUIAL_NAME,
|
||||||
|
"team" : NUMERIC_TEAM_IDENTIIER
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
# start a round
|
### WinnerInformationEvent
|
||||||
0x42,round_start_active,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID....
|
Event annotating who won a round. Any single round *MUST* only have one such Event.
|
||||||
|
|
||||||
# record team changes (ct) or disconnects (dc) and the team composition after it
|
{
|
||||||
# the backend will handle those accordingly
|
"etype" : "winner",
|
||||||
0x42,ct,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID....
|
"timestamp" : ISO_TIMESTAMP,
|
||||||
0x42,dc,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID....
|
"winnerTeam" : NUMERIC_TEAM_IDENTIIER
|
||||||
|
}
|
||||||
|
|
||||||
# declare the team-composition at the end of the round
|
### MapInformationEvent
|
||||||
# the backend will use this information for sanity checks
|
Optional event to annotate the map that was played on. Each individual round must only have one such event.
|
||||||
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" : "map",
|
||||||
|
"timestamp" : ISO_TIMESTAMP,
|
||||||
|
"map" : MAP_NAME
|
||||||
|
}
|
||||||
|
|
||||||
## Usage
|
## /submitt-round
|
||||||
usage: startInsurgency.py [-h] [--parse-only] [--start-at-end] [--no-follow]
|
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*.
|
||||||
[--one-thread] [--cache-file CACHEFILE]
|
|
||||||
FILE [FILE ...]
|
|
||||||
|
|
||||||
positional arguments:
|
{
|
||||||
FILE one or more logfiles to parse
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
optional arguments:
|
The player struct in the winner/loser-array must look like this:
|
||||||
-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
|
|
||||||
|
|
||||||
## 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).
|
"playerId" : PLAYER_ID_STR,
|
||||||
|
"playerName" : PLAYER_NAME,
|
||||||
|
"isFake" : BOOLEAN,
|
||||||
|
"activeTime" : ACTIVE_TIME_IN_SECONDS,
|
||||||
|
}
|
||||||
|
|
||||||
### Quality
|
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*.
|
||||||
Get the balance quality of the current team composition.
|
|
||||||
|
|
||||||
Input: quality,LIST_OF_PLAYERS_TEAM_1|LIST_OF_PLAYERS_TEAM_2
|
|
||||||
Output: float between 0 and 100
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
### Player
|
|
||||||
Return rating information about a player-ID
|
|
||||||
|
|
||||||
Input: quality,PLAYER_ID
|
|
||||||
Output: string: rating information
|
|
||||||
|
|
||||||
|
|
||||||
### Find
|
|
||||||
Fuzzy search for the name or ID of a player
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## Related projects
|
## Related projects
|
||||||
- [skillbird-sourcemod](https://github.com/FAUSheppy/skillbird-sourcemod) Sourcemod plugin that produces the necessary output for Source-based servers.
|
- [skillbird-sourcemod](https://github.com/FAUSheppy/skillbird-sourcemod) Sourcemod plugin that produces the necessary output for Source-based servers.
|
||||||
|
|||||||
150
Round.py
150
Round.py
@@ -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)
|
|
||||||
@@ -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() ]
|
|
||||||
@@ -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")
|
|
||||||
105
helper_scripts/test.py
Executable file
105
helper_scripts/test.py
Executable file
@@ -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})
|
||||||
105
helper_scripts/test_single.py
Executable file
105
helper_scripts/test_single.py
Executable file
@@ -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)
|
||||||
61
httpAPI.py
61
httpAPI.py
@@ -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 "<br>".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))
|
|
||||||
@@ -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}
|
|
||||||
@@ -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)]
|
|
||||||
@@ -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
|
|
||||||
142
python/Round.py
Normal file
142
python/Round.py
Normal file
@@ -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)
|
||||||
99
python/backends/database.py
Normal file
99
python/backends/database.py
Normal file
@@ -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
|
||||||
67
python/backends/entities/Players.py
Normal file
67
python/backends/entities/Players.py
Normal file
@@ -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)
|
||||||
219
python/backends/eventStream.py
Normal file
219
python/backends/eventStream.py
Normal file
@@ -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))
|
||||||
85
python/backends/trueskillWrapper.py
Normal file
85
python/backends/trueskillWrapper.py
Normal file
@@ -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()
|
||||||
122
python/httpAPI.py
Normal file
122
python/httpAPI.py
Normal file
@@ -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())
|
||||||
33
python/init.py
Executable file
33
python/init.py
Executable file
@@ -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)
|
||||||
2
req.txt
2
req.txt
@@ -1,4 +1,4 @@
|
|||||||
flask
|
flask
|
||||||
fuzzywuzzy
|
fuzzywuzzy
|
||||||
trueskill
|
trueskill
|
||||||
psycopg2-binary
|
sqlite3
|
||||||
|
|||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user