3 Commits

Author SHA1 Message Date
Yannik Schmidt
159871be47 Lol 2021-12-20 19:55:51 +01:00
Yannik Schmidt
6e677c663b cleanups 2021-12-20 19:07:55 +01:00
Yannik Schmidt
12c001ac71 lol 2021-12-20 15:15:35 +01:00
9 changed files with 147 additions and 473 deletions

View File

@@ -1,63 +0,0 @@
import json
import datetime
import player
import datetime
class MapSummary:
def __init__(self, rounds):
'''Create a map MapSummary from a Round-Array'''
self.securityWins = 0
self.insurgentWins = 0
self.times = []
self.predictions = []
self.totalGames = 0
self.confidence = []
self.mapName = None
for r in rounds:
self.mapName = r.mapName
self.totalGames += 1
if r.winnerSideString == "Insurgent":
self.insurgentWins += 1
else:
self.securityWins += 1
self.predictions += [r.numericPrediction]
self.confidence += [r.confidence]
self.times += [r.duration]
self.insurgentWinPercent = ""
self.securityWinPercent = ""
self.ratingSystemDeviation = "-"
self.averageTime = ""
try:
self.insurgentWinPercent = self.insurgentWins / self.totalGames*100
self.securityWinPercent = self.securityWins / self.totalGames*100
averageSeconds = sum([t.total_seconds() for t in self.times]) / len(self.times)
self.averageTime = datetime.timedelta(seconds=int(averageSeconds))
mapper = [ 1 if x == 0 else -1 for x in self.predictions ]
reverseMapper = [ 1 if x == 0 else 0 for x in self.predictions ]
self.ratingSystemDeviation = 0
confidenceCutoff = 60
confidenceTupels = list(filter(lambda x: x[1] > confidenceCutoff,
zip(reverseMapper, self.confidence)))
mapperTupels = list(filter(lambda x: x[1] > confidenceCutoff,
zip(mapper, self.confidence)))
for i in range(0, len(mapperTupels)):
self.ratingSystemDeviation += mapperTupels[i][0] * max(100, 50+mapperTupels[i][1])
self.ratingSystemDeviation /= len(mapperTupels)
self.predictionCorrectPercentage = sum([x[0] for x in confidenceTupels])
self.predictionCorrectPercentage /= len(confidenceTupels)
self.predictionCorrectPercentage *= 100
self.predictionCorrectPercentage = round(self.predictionCorrectPercentage)
except ZeroDivisionError:
pass

View File

@@ -1,60 +0,0 @@
import json
import datetime
import player
import json
import os
class Round:
def __init__(self, dbRow):
'''Create Round Object from cursor database row'''
timestamp, winners, losers, winnerSide, mapName, duration, prediction, confidence = dbRow
startTime = datetime.datetime.fromtimestamp(int(float(timestamp)))
winnersParsed = json.loads(winners)
losersParsed = json.loads(losers)
self.startTime = startTime
self.id = int(float(timestamp))
self.winners = [ player.playerFromDict(wp, int(duration)) for wp in winnersParsed ]
self.losers = [ player.playerFromDict(lp, int(duration)) for lp in losersParsed ]
self.winnerSide = winnerSide
self.duration = datetime.timedelta(seconds=int(duration))
self.blacklist = False
blacklistNames = []
blacklistFile = "blacklist.json"
if os.path.isfile(blacklistFile):
with open(blacklistFile) as f:
blacklistNames = json.load(f)["blacklist"]
for name in blacklistNames:
for p in self.winners:
if p.name == name:
self.blacklist = True
for p in self.losers:
if p.name == name:
self.blacklist = True
if winnerSide == 1:
self.winnerSideString = "Red"
self.loserSideString = "Blue"
else:
self.winnerSideString = "Blue"
self.loserSideString = "Red"
if mapName:
self.mapName = mapName
else:
self.mapName = "unavailiable"
self.numericPrediction = prediction
self.confidence = (int(confidence * 100) - 50)*2
self.quality = int(150 - self.confidence)
if self.confidence < 50:
self.prediction = "-"
elif prediction == 0:
self.prediction = self.winnerSideString
elif prediction == 1:
self.prediction = self.loserSideString
else:
self.prediction = "Error"

68
balance.py Normal file
View File

@@ -0,0 +1,68 @@
from constants import *
NO_CHANGE = -1
def shouldSwapBasedOnPrio(teams, curIndex, compareIndex, curTeamIndex):
'''Return a team ID in which to switch with the compare index'''
otherTeam = (curTeamIndex + 1) % 2
curPrio = teams[curTeamIndex].affinityFor(curIndex)
compPrioOtherTeam = teams[otherTeam].affinityFor(compareIndex)
compPrioSameTeam = team[otherTeam].affinityFor(compareIndex)
if curPrio > compPrioSameTeam and compPrioSameTeam > compPrioOtherTeam:
return compPrioSameTeam
elif curPrio > compPrioOtherTeam:
return compPrioOtherTeam
else:
return NO_CHANGE
def swap(teams, teamIndex1, teamIndex2, pos1, pos2):
'''Swap two positions in the same or different teams'''
tmp = teams[teamIndex1][pos1]
teams[teamIndex1][pos1] = teams[teamIndex2][pos2]
teams[teamIndex2][pos2] = tmp
def ratingDiff(team1, team2):
'''Positive if first > seconds, negative if second > first, 0 if equal'''
return sum([p.rating for p in team1]) - sum([p.rating for p in team2])
def balance(players):
# initial teams #
playersByRating = sorted(players, key=lambda p: p.rating)
teams = dict( { 0 : [] }, { 1 : [] } )
for i in range(0, len(playersByRating)):
teams[i%2] = playersByRating[i]
# optimize positions worst case ~8n^2 * 2log(n) #
for teamIndex in teams.keys():
changed = True
while changed:
changed = False
for curIndex in range(0, 5)
for compareIndex in range(curIndex, 5)
if curIndex == compareIndex:
continue
# shouldSwap return -1 for no swap or the team to swap with #
swapTeam = shouldSwapBasedOnPrio(teams, curIndex, compareIndex, teamIndex)
elif VALID_INDEX(swapTeam):
changed = True
swap(teams, teamIndex, swapTeam, curIndex, compareIndex)
# optimize team rating #
changedRating = True
while changedRating:
diff = ratingDiff(teams[0], teams[1])
diffByPos = [ teams[0][i] - teams[1][i] for i in range(0, 5) ]
for i in range(0, diffByPos):
diffHelper = abs(diffByPos[i]-diff)
if diffHelper <
for curIndex in range(0, len(teams[0])):
if rating diff

View File

@@ -1 +0,0 @@
DB_PATH="/home/sheppy-gaming/insurgency-skillbird/python/"

View File

@@ -1,2 +0,0 @@
# rename to config.py and remove this line for this file to have effect
DB_PATH="players.sqlite"

8
constants.py Normal file
View File

@@ -0,0 +1,8 @@
POSITIONS_NAMES = ["Top", "Jungle", "Mid", "Bottom", "Support" ]
DATABASE_PRIO_NAMES = ["prioTop", "prioJungle", "prioMid", "prioBot", "prioSupport" ]
TYPE_JSON = 'application/json'
HTTP_OK = 200
def VALID_INDEX(index):
return index == 0 or index == 1

View File

@@ -1,49 +0,0 @@
#!/usr/bin/python3
import flask
def playerFromDict(d, duration):
p = PlayerInLeaderboard([d["id"], d["name"], None, 0, 0, 0, 0])
p.participation = min(int(d["active_time"]/duration*100), 100)
return p
class PlayerInLeaderboard:
def __init__(self, dbRow):
'''Initialize a player object later to be serialized to HTML'''
playerId, name, lastGame, wins, mu, sigma, games = dbRow
# set relevant values #
self.name = name
self.playerId = playerId
self.mu = mu
self.sigma = sigma
self.rating = int(self.mu) - 2*int(self.sigma)
self.ratingStr = str(self.rating)
self.games = int(games)
self.wins = int(wins)
self.loses = self.games - self.wins
self.rank = None
self.lastGame = lastGame
self.participation = -1
self.muChange = None
self.sigmaChange = None
self.ratingChangeString = "N/A"
# determine winratio #
if self.games == 0:
self.winratio = "N/A"
else:
self.winratio = str(int(self.wins/self.games * 100))
def getLineHTML(self, rank):
'''Build a single line for a specific player in the leaderboard'''
string = flask.render_template("playerLine.html", \
playerRank = rank, \
playerName = self.name, \
playerRating = self.rating, \
playerGames = self.games, \
playerWinratio = self.winratio)
return flask.Markup(string)

364
server.py
View File

@@ -3,202 +3,114 @@ import flask
import requests import requests
import argparse import argparse
import datetime import datetime
import flask_caching as fcache
import itertools import itertools
import json import json
import os import os
import MapSummary
import random import random
import secrets import secrets
import riotwatcher import riotwatcher
import time import time
import statistics import statistics
from database import DatabaseConnection
import api import api
from constants import *
app = flask.Flask("open-leaderboard") app = flask.Flask("open-leaderboard")
WATCHER = None WATCHER = None
KEY = None KEY = None
if os.path.isfile("config.py"): if os.path.isfile("config.py"):
app.config.from_object("config") app.config.from_object("config")
cache = fcache.Cache(app, config={'CACHE_TYPE': 'simple'})
cache.init_app(app)
SEGMENT=100 SEGMENT=100
SERVERS=list() SERVERS=list()
from sqlalchemy import Column, Integer, String, Boolean, or_, and_
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import func
import sqlalchemy
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
def prettifyMinMaxY(computedMin, computedMax): class PlayerInDatabase(db.Model):
if computedMax > 0 and computedMin > 0: __tablename__ = "players"
return (0, 4000) player = Column(String, primary_key=True)
else: rating = Column(Integer)
return (computedMin - 100, 4000) ratingFix = Column(Integer)
lastUpdated = Column(Integer)
@app.route("/players-online") class Submission(db.Model):
def playersOnline(): __tablename__ = "submissions"
'''Calc and return the online players''' ident = Column(String, primary_key=True)
submissionId = Column(String)
player = Column(String)
prioTop = Column(Integer)
prioJungle = Column(Integer)
prioMid = Column(Integer)
prioBot = Column(Integer)
prioSupport = Column(Integer)
playerTotal = 0 def toDict():
error = "" pass
for s in SERVERS: def affinityFor(self, posIndex):
try: prio = getattr(self, DATABASE_PRIO_NAMES[posIndex])
with valve.source.a2s.ServerQuerier((args.host, args.port)) as server: return prio
playerTotal += int(server.info()["player_count"])
except NoResponseError:
error = "Server Unreachable"
except Exception as e:
error = str(e)
retDict = { "player_total" : playerTotal, "error" : error }
return flask.Response(json.dumps(retDict), 200, mimetype='application/json')
@app.route("/round-info")
def singleRound():
'''Display info about a single round itdentified by it's timestamp'''
timestamp = flask.request.args.get("id")
if not timestamp:
return ("ID Missing", 404)
if not timestamp.endswith(".0"):
timestamp = timestamp + ".0"
db = DatabaseConnection(app.config["DB_PATH"])
r = db.getRoundByTimestamp(timestamp)
if not r:
return ("Round not found", 404)
elif r.blacklist:
return ("Unavailable due to pending GDPR deletion request", 451)
r = db.calcRatingChanges(r)
if not r:
return ("", 404)
r.winners = sorted(r.winners, key=lambda p: p.participation, reverse=True)
r.losers = sorted(r.losers, key=lambda p: p.participation, reverse=True)
return flask.render_template("single_round.html", r=r)
@app.route("/livegames")
def liveGames():
'''Display info about a single round itdentified by it's timestamp'''
db = DatabaseConnection(app.config["DB_PATH"])
rounds = db.getLiveGames()
return flask.render_template("livegames.html", liveGameRounds=rounds, noRounds=not bool(rounds))
@app.route("/maps")
def maps():
'''Show an overview of maps'''
db = DatabaseConnection(app.config["DB_PATH"])
start = datetime.datetime.now() - datetime.timedelta(days=4000)
end = datetime.datetime.now()
rounds = db.roundsBetweenDates(start, end)
distinctMaps = db.distinctMaps()
maps = []
for mapName in [ tupel[0] for tupel in distinctMaps]:
roundsWithMap = list(filter(lambda r: r.mapName == mapName , rounds))
maps += [MapSummary.MapSummary(roundsWithMap)]
allMaps = MapSummary.MapSummary(rounds)
allMaps.mapName = "All Maps*"
maps += [allMaps]
mapsFiltered = filter(lambda x: x.mapName, maps)
return flask.render_template("maps.html", maps=mapsFiltered)
@app.route("/rounds-by-timestamp")
@app.route("/rounds")
def rounds():
'''Show rounds played on the server'''
start = flask.request.args.get("start")
end = flask.request.args.get("end")
if not start or not end:
start = datetime.datetime.now() - datetime.timedelta(days=365)
end = datetime.datetime.now()
else:
start = datetime.datetime.fromtimestamp(start)
end = datetime.datetime.fromtimestamp(end)
db = DatabaseConnection(app.config["DB_PATH"])
rounds = db.roundsBetweenDates(start, end)
return flask.render_template("rounds.html", rounds=rounds)
# get timestamp
# display players
# display rating change
# display outcome
# display map
class Player: class Player:
def __init__(self, name, prio): def __init__(self, name, prio):
self.name = name self.name = name
self.prio = prio self.prio = prio
# TODO
submission = dict()
@app.route("/role-submission", methods=['GET', 'POST']) @app.route("/role-submission", methods=['GET', 'POST'])
def roleSubmissionTool(): def roleSubmissionTool():
positions=["Top", "Jungle", "Mid", "Bottom", "Support" ]
ident = flask.request.args.get("id") submissionId = flask.request.args.get("id")
player = flask.request.args.get("player")
if flask.request.method == 'POST': if flask.request.method == 'POST':
if not ident in submission: submissionQuery = db.session.query(PlayerInDatabase)
submission.update({ ident : [] }) identKey = "{}-{}".format(submissionId, player)
submission = submissionQuery.filter(PlayerInDatabase.ident == identKey).first()
tmp = dict()
tmp.update({ "name" : flask.request.form["playername"] })
for p in positions:
tmp.update({ p : flask.request.form["prio_{}".format(p)] })
existed = False if not submission:
for pl in submission[ident]: submission = Submission(identKey, submissionId, player, -1, -1, -1, -1, -1)
if pl["name"] == tmp["name"]:
for p in positions:
pl.update({ p : flask.request.form["prio_{}".format(p)] })
existed = True
break;
if not existed: for i in range(0, 5):
submission[ident] += [tmp] formKey = "prio_" + POSITIONS_NAMES[i]
setattr(submission, DATABASE_PRIO_NAMES[i], flask.request.form[formKey])
return flask.redirect("/balance-tool?id={}".format(ident)) db.session.merge(submission)
db.session.commit()
return flask.redirect("/balance-tool?id=" + ident)
else: else:
return flask.render_template("role_submission.html", return flask.render_template("role_submission.html", positions=positions, ident=ident)
positions=positions,
ident=ident)
@app.route("/balance-tool-data") @app.route("/balance-tool-data")
def balanceToolData(): def balanceToolData():
ident = flask.request.args.get("id")
retDict = dict() submissionId = flask.request.args.get("id")
if not ident in submission: submissionsQuery = db.session.query(PlayerInDatabase)
return flask.Response(json.dumps({ "no-data" : False }), 200, mimetype='application/json') submissions = submissionsQuery.filter(PlayerInDatabase.submissionId == submissionId).all()
retDict.update({ "submissions" : submission[ident] })
return flask.Response(json.dumps(retDict), 200, mimetype='application/json') if not submissions:
return flask.Response(json.dumps({ "no-data" : False }), HTTP_OK, mimetype=TYPE_JSON)
retDict.update()
dicts = [ s.toDict() for s in submissions ]
return flask.Response(json.dumps({ "submissions" : dicts }), HTTP_OK, mimetype=TYPE_JSON)
@app.route('/') @app.route('/')
@app.route("/balance-tool", methods=['GET', 'POST']) @app.route("/balance-tool", methods=['GET', 'POST'])
def balanceTool(): def balanceTool():
positions=["Top", "Jungle", "Mid", "Bottom", "Support"]
#db = DatabaseConnection(app.config["DB_PATH"])
if flask.request.method == 'POST': if flask.request.method == 'POST':
@@ -345,9 +257,6 @@ def balanceTool():
positions=positions, positions=positions,
sides=["left", "right"], sides=["left", "right"],
ident=ident) ident=ident)
@app.route("/get-cache")
def getCacheLoc():
return (json.dumps(api.getCache()), 200)
@app.route("/player-api") @app.route("/player-api")
def playerApi(): def playerApi():
@@ -365,140 +274,9 @@ def playerApiCache():
else: else:
return ("Nope", 404) return ("Nope", 404)
@app.route("/player") @app.route("/get-cache")
def player(): def getCacheLoc():
'''Show Info about Player''' return (json.dumps(api.getCache()), 200)
playerId = flask.request.args.get("id")
if(not playerId):
return ("", 404)
db = DatabaseConnection(app.config["DB_PATH"])
player = db.getPlayerById(playerId)
if(not player):
return ("", 404)
player.rank = db.getPlayerRank(player)
histData = db.getHistoricalForPlayerId(playerId)
csv_month_year = []
csv_ratings = []
csv_timestamps = []
minRating = 3000
maxRating = 0
if histData:
datapoints = histData[playerId]
if datapoints:
tickCounter = 10
for dpk in datapoints.keys():
t = datetime.datetime.fromtimestamp(int(float(dpk)))
tsMs = str(int(t.timestamp() * 1000))
ratingString = str(int(datapoints[dpk]["mu"]) - 2*int(datapoints[dpk]["sigma"]))
ratingAmored = '{ x : ' + tsMs + ', y : ' + ratingString + '}'
csv_timestamps += [str(tsMs)]
csv_ratings += [ratingAmored]
tickCounter -= 1
if tickCounter <= 0:
tickCounter = 10
csv_month_year += ['new Date({})'.format(tsMs)]
minRating = min(minRating, int(ratingString))
maxRating = max(maxRating, int(ratingString))
yMin, yMax = prettifyMinMaxY(minRating, maxRating)
# change displayed rank to start from 1 :)
player.rank += 1
return flask.render_template("player.html", player=player, CSV_RATINGS=",".join(csv_ratings),
CSV_MONTH_YEAR_OF_RATINGS=",".join(csv_month_year),
CSV_TIMESTAMPS=csv_timestamps,
Y_MIN=yMin, Y_MAX=yMax)
@app.route('/leaderboard')
@cache.cached(timeout=10, query_string=True)
def leaderboard():
'''Show main leaderboard page with range dependant on parameters'''
# parse parameters #
page = flask.request.args.get("page")
playerName = flask.request.args.get("string")
db = DatabaseConnection(app.config["DB_PATH"])
if page:
pageInt = int(page)
if pageInt < 0:
pageInt = 0
start = SEGMENT * int(page)
else:
pageInt = 0
start = 0
# handle find player request #
cannotFindPlayer = ""
searchName = ""
playerList = None
doNotComputeRank = True
if playerName:
playersInLeaderboard = db.findPlayerByName(playerName)
if not playersInLeaderboard:
cannotFindPlayer = flask.Markup("<div class=noPlayerFound>No player of that name</div>")
start = 0
else:
if len(playersInLeaderboard) == 1:
rank = playersInLeaderboard[0].rank
if(playersInLeaderboard[0].games < 10):
return flask.redirect("/player?id={}".format(playersInLeaderboard[0].playerId))
searchName = playersInLeaderboard[0].name
start = rank - (rank % SEGMENT)
else:
playerList = playersInLeaderboard
for p in playerList:
if p.rank == -1:
p.rankStr = "N/A"
else:
p.rankStr = str(p.rank)
doNotComputeRank = False
reachedEnd = False
maxEntry = 0
if not playerList:
# compute range #
end = start + SEGMENT
maxEntry = db.getTotalPlayers()
reachedEnd = False
if end > maxEntry:
start = maxEntry - ( maxEntry % SEGMENT ) - 1
end = maxEntry - 1
print(maxEntry)
reachedEnd = True
playerList = db.getRankRange(start, end)
endOfBoardIndicator = ""
if reachedEnd:
endOfBoardHtml = "<div id='eof' class=endOfBoardIndicator> - - - End of Board - - - </div>"
endOfBoardIndicator = flask.Markup(endOfBoardHtml)
# fix <100 player start at 0 #
if maxEntry <= 100:
start = max(start, 0)
finalResponse = flask.render_template("base.html", playerList=playerList, \
doNotComputeRank=doNotComputeRank, \
start=start, \
endOfBoardIndicator=endOfBoardIndicator, \
findPlayer=cannotFindPlayer, \
searchName=searchName,
nextPageNumber=int(pageInt)+1,
prevPageNumber=int(pageInt)-1)
return finalResponse
@app.route('/static/<path:path>') @app.route('/static/<path:path>')
def send_js(path): def send_js(path):
@@ -508,22 +286,22 @@ def send_js(path):
def init(): def init():
global WATCHER global WATCHER
app.config["DB"] = db
db.create_all()
with open("key.txt","r") as f: with open("key.txt","r") as f:
key = f.read().strip() key = f.read().strip()
WATCHER = riotwatcher.LolWatcher(key) WATCHER = riotwatcher.LolWatcher(key)
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Start open-leaderboard', \ parser = argparse.ArgumentParser(description='Start open-leaderboard', \
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--interface', default="localhost", \ parser.add_argument('--interface', default="localhost")
help='Interface on which flask (this server) will take requests on') parser.add_argument('--port', default="5002")
parser.add_argument('--port', default="5002", \
help='Port on which flask (this server) will take requests on')
parser.add_argument('--skillbird-db', required=False, help='skillbird database (overrides web connection if set)')
args = parser.parse_args() args = parser.parse_args()
app.config["DB_PATH"] = args.skillbird_db
app.config["TEMPLATES_AUTO_RELOAD"] = True app.config["TEMPLATES_AUTO_RELOAD"] = True
app.run(host=args.interface, port=args.port) app.run(host=args.interface, port=args.port)

View File

@@ -1,5 +0,0 @@
CREATE TABLE players(
playerName TEXT PRIMARY KEY,
rating INTEGER,
lastupdated TEXT
);