2020 rewrite

This commit is contained in:
2020-06-15 00:50:54 +02:00
parent 99fa086a3f
commit fc5a825981
26 changed files with 1035 additions and 1541 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,7 @@
cache
*.sqlite
*.sqlite.clean
*.txt
*.log
*.swp
__pycache__/

View File

@@ -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()

View File

@@ -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

View File

@@ -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
View File

@@ -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 []

View File

@@ -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
View File

@@ -1,111 +1,77 @@
## What is "Skillbird" ?
# What is "Skillbird" ?
Skillbird is a framework around the python-Trueskill library, which can parse files of versus games to calculate a rating, matchmaking suggestions for future games or create predictions for the outcome of a game with certain team compositions.
## Web Interface
# Web Interface
The [Open-web-leaderboard](https://github.com/FAUSheppy/open-web-leaderboard) can be used for visualization. If you leave all settings at default, it should work out of the box.
![open-web-leaderboard](https://media.atlantishq.de/leaderboard-github-picture.png)
## Adaption for your own Data
### Data Requirements
To work correctly you data must have the following fields:
- unique player id or name
- player(s) in winning and losing team
# Data Transmission
## /event-blob
Your server may collect certain events during a match of two teams and columinatively report them to the server, which will then evalute those event into a single Round. The events must be submitted as a *JSON-list* with *Content-Type application/json* in a field called *"events"*. Event must be dictionary-like *JSON*-objects as described below.
You may also have the following informations:
- data/time of the game (you cannot use the cached-rebuild feature without this)
- time players spent playing compared to the full length of the game
- teamchanges of players
- different maps
{
events : [ { ... }, { ... } ]
### Data Input
If you use the official source-plugin and it's output, you don't have to do anything. Alternatively can write your own parser (see the skillbird-examples project), or you can conform to the Source-format which supports the following log-lines. None of the input values may contain any of the separators used (**pipe** and **comma**) or the line identifier (**0x42**).
}
# reset state
0x42,plugin_unloaded
# declare the current map
0x42,mapname,MAP_NAME
# start a round
0x42,round_start_active,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID....
# record team changes (ct) or disconnects (dc) and the team composition after it
# the backend will handle those accordingly
0x42,ct,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID....
0x42,dc,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID....
### ActivePlayersEvent
This events lists all players currently in the game, it must be fired whenever a new player connects, changes team or disconnects, but may also be fired at any other point. The *NUMERIC_TEAM_IDENTIIER* must be 0 for *no team*, 1 for *observers* or 2/3 respectively for players actually in one of the teams.
# declare the team-composition at the end of the round
# the backend will use this information for sanity checks
0x42,round_end_active,PLAYER_ID|PLAYER_NAME|TEAMI_D,PLAYER_ID....
# name the winning team and end the round
0x42,winner,WINNING_TEAM_ID
{
"etype" : "active_players",
"timestamp" : ISO_TIMESTAMP,
"players" : [
{
"id" : UNIQUE_PLAYER_ID,
"name" : COLOQUIAL_NAME,
"team" : NUMERIC_TEAM_IDENTIIER
}
...
]
}
## Usage
usage: startInsurgency.py [-h] [--parse-only] [--start-at-end] [--no-follow]
[--one-thread] [--cache-file CACHEFILE]
FILE [FILE ...]
positional arguments:
FILE one or more logfiles to parse
optional arguments:
-h, --help show this help message and exit
--parse-only, -po only parse, do not listen for queries
--start-at-end, -se start at the end of each file (overwrites no-follow)
--no-follow, -nf wait for changes on the files (does not imply start-
at-end)
--one-thread run everything in main thread (implies no-follow)
--cache-file CACHEFILE
A cache file which makes restarting the system fast
### WinnerInformationEvent
Event annotating who won a round. Any single round *MUST* only have one such Event.
## Query Options
Skillbird has a TCP-Query interface which supports the following queries. The separator for player-IDs is always a**comma** and the separator for for teams is always a **pipe** as before, those special characters may not be contained in any of the actual input values. A HTTP-api is work in progress (at the start of this project the interface was only intended for sourcemodplugins).
{
"etype" : "winner",
"timestamp" : ISO_TIMESTAMP,
"winnerTeam" : NUMERIC_TEAM_IDENTIIER
}
### Quality
Get the balance quality of the current team composition.
### MapInformationEvent
Optional event to annotate the map that was played on. Each individual round must only have one such event.
Input: quality,LIST_OF_PLAYERS_TEAM_1|LIST_OF_PLAYERS_TEAM_2
Output: float between 0 and 100
{
"etype" : "map",
"timestamp" : ISO_TIMESTAMP,
"map" : MAP_NAME
}
### Balance
Return a balance suggestion for a list of players.
Input: quality,LIST_OF_PLAYER_IDs
Output: string LIST_OF_PLAYERS_TEAM_1|LIST_OF_PLAYERS_TEAM_2
## /submitt-round
Your may transmitt a json dictionary representing an actuall round, this is intended more for backups and manual inputs rather than production use, it basicly skips *backends.eventStream.parse* and goes directly to *backends.trueskill.evaluateRound*.
### Player
Return rating information about a player-ID
{
"map" : MAP_STRING_OR_NULL,
"winner-side" : NUMERIC_TEAM_ID_OR_NULL,
"winners" : [ player, player, ... ],
"losers" : [ player, player, ..., ],
"duration" : DURATION_OF_ROUND_IN_SECONDS,
"startTime" : ISO_TIMESTAMP_OR_NULL
}
Input: quality,PLAYER_ID
Output: string: rating information
The player struct in the winner/loser-array must look like this:
### Find
Fuzzy search for the name or ID of a player
{
"playerId" : PLAYER_ID_STR,
"playerName" : PLAYER_NAME,
"isFake" : BOOLEAN,
"activeTime" : ACTIVE_TIME_IN_SECONDS,
}
Input: find,string
Output: string: rating information
### Force rank reload
For the reload of player ranks, which are usually updated every 5 minutes immediately.
Input: forceRankReload
Output: string: "OK" (if successful)
### Dump
Reload the player ranks cache and dump the entire contents.
Input: dump
Output: string: all players, their ratings and their ranks
### Stats
Return some statistics about the system
Input: stats
Output: string: general information
You cannot ommit the duration/active\_time fields, if you don't need or want to use them set them to *1*. If the winner side is unkown or your game is symetrical (meaning there is no possible advantage for one side or the other, set it to *-1*.
## Related projects
- [skillbird-sourcemod](https://github.com/FAUSheppy/skillbird-sourcemod) Sourcemod plugin that produces the necessary output for Source-based servers.

150
Round.py
View File

@@ -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)

View File

@@ -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() ]

View File

@@ -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
View 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
View 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)

View File

@@ -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))

View File

@@ -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}

View File

@@ -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)]

View File

@@ -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
View 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)

View 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

View 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)

View 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))

View 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
View 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
View 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)

View File

@@ -1,4 +1,4 @@
flask
fuzzywuzzy
trueskill
psycopg2-binary
sqlite3

View File

@@ -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)