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 argparse
import datetime
import flask_caching as fcache
import itertools
import json
import os
import MapSummary
import random
import secrets
import riotwatcher
import time
import statistics
from database import DatabaseConnection
import api
from constants import *
app = flask.Flask("open-leaderboard")
WATCHER = None
KEY = None
if os.path.isfile("config.py"):
app.config.from_object("config")
cache = fcache.Cache(app, config={'CACHE_TYPE': 'simple'})
cache.init_app(app)
SEGMENT=100
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):
if computedMax > 0 and computedMin > 0:
return (0, 4000)
else:
return (computedMin - 100, 4000)
class PlayerInDatabase(db.Model):
__tablename__ = "players"
player = Column(String, primary_key=True)
rating = Column(Integer)
ratingFix = Column(Integer)
lastUpdated = Column(Integer)
@app.route("/players-online")
def playersOnline():
'''Calc and return the online players'''
class Submission(db.Model):
__tablename__ = "submissions"
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
error = ""
def toDict():
pass
for s in SERVERS:
try:
with valve.source.a2s.ServerQuerier((args.host, args.port)) as server:
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
def affinityFor(self, posIndex):
prio = getattr(self, DATABASE_PRIO_NAMES[posIndex])
return prio
class Player:
def __init__(self, name, prio):
self.name = name
self.prio = prio
self.name = name
self.prio = prio
# TODO
submission = dict()
@app.route("/role-submission", methods=['GET', 'POST'])
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 not ident in submission:
submission.update({ ident : [] })
tmp = dict()
tmp.update({ "name" : flask.request.form["playername"] })
for p in positions:
tmp.update({ p : flask.request.form["prio_{}".format(p)] })
submissionQuery = db.session.query(PlayerInDatabase)
identKey = "{}-{}".format(submissionId, player)
submission = submissionQuery.filter(PlayerInDatabase.ident == identKey).first()
existed = False
for pl in submission[ident]:
if pl["name"] == tmp["name"]:
for p in positions:
pl.update({ p : flask.request.form["prio_{}".format(p)] })
existed = True
break;
if not submission:
submission = Submission(identKey, submissionId, player, -1, -1, -1, -1, -1)
if not existed:
submission[ident] += [tmp]
for i in range(0, 5):
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:
return flask.render_template("role_submission.html",
positions=positions,
ident=ident)
return flask.render_template("role_submission.html", positions=positions, ident=ident)
@app.route("/balance-tool-data")
def balanceToolData():
ident = flask.request.args.get("id")
retDict = dict()
if not ident in submission:
return flask.Response(json.dumps({ "no-data" : False }), 200, mimetype='application/json')
retDict.update({ "submissions" : submission[ident] })
return flask.Response(json.dumps(retDict), 200, mimetype='application/json')
submissionId = flask.request.args.get("id")
submissionsQuery = db.session.query(PlayerInDatabase)
submissions = submissionsQuery.filter(PlayerInDatabase.submissionId == submissionId).all()
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("/balance-tool", methods=['GET', 'POST'])
def balanceTool():
positions=["Top", "Jungle", "Mid", "Bottom", "Support"]
#db = DatabaseConnection(app.config["DB_PATH"])
if flask.request.method == 'POST':
@@ -345,9 +257,6 @@ def balanceTool():
positions=positions,
sides=["left", "right"],
ident=ident)
@app.route("/get-cache")
def getCacheLoc():
return (json.dumps(api.getCache()), 200)
@app.route("/player-api")
def playerApi():
@@ -365,140 +274,9 @@ def playerApiCache():
else:
return ("Nope", 404)
@app.route("/player")
def player():
'''Show Info about Player'''
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("/get-cache")
def getCacheLoc():
return (json.dumps(api.getCache()), 200)
@app.route('/static/<path:path>')
def send_js(path):
@@ -508,22 +286,22 @@ def send_js(path):
def init():
global WATCHER
app.config["DB"] = db
db.create_all()
with open("key.txt","r") as f:
key = f.read().strip()
WATCHER = riotwatcher.LolWatcher(key)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Start open-leaderboard', \
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--interface', default="localhost", \
help='Interface on which flask (this server) will take requests on')
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)')
parser.add_argument('--interface', default="localhost")
parser.add_argument('--port', default="5002")
args = parser.parse_args()
app.config["DB_PATH"] = args.skillbird_db
app.config["TEMPLATES_AUTO_RELOAD"] = True
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
);