13 Commits

Author SHA1 Message Date
3f964ddb55 update: change some medals
Some checks failed
Container Build for open-web-leaderboard / docker (push) Failing after 12s
2024-10-10 23:43:25 +02:00
6491afc272 add: req.txt
Some checks failed
Container Build for open-web-leaderboard / docker (push) Failing after 2s
2024-09-27 05:47:56 +02:00
756b24a447 add: build schedule 1/week 2024-09-27 05:44:39 +02:00
5f7713daaf docu: info about python-valve integration 2024-07-21 23:57:16 +02:00
ee14c3fd7e feat: add build & update to latest python 2024-07-21 23:49:12 +02:00
Yannik Schmidt
c32155fd40 fix typo in variable name 2022-03-07 01:32:57 +01:00
Yannik Schmidt
defcf5671d implement simple medals 2021-07-24 14:17:35 +02:00
2133249947 Merge branch 'master' of github.com:FAUSheppy/open-web-leaderboard 2021-07-24 13:25:42 +02:00
db3d2bb57e remove footer in player display 2021-07-24 13:25:29 +02:00
c5b7963fff improve livegame display 2021-07-24 13:25:20 +02:00
23fa7f9862 fade & whitespace 2021-07-24 13:24:31 +02:00
8f7b1b47ac add cache header to static 2021-07-24 13:23:05 +02:00
bdd7d9bf01 fix server player query 2021-03-25 17:52:32 +01:00
42 changed files with 712 additions and 1744 deletions

34
.github/workflows/main.yaml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Container Build for open-web-leaderboard
on:
push:
branches:
- "master"
schedule:
- cron: "0 2 * * 0"
jobs:
docker:
runs-on: ubuntu-latest
environment:
name: prod
steps:
- uses: actions/checkout@v3
-
name: Checkout
uses: actions/checkout@v3
-
name: Login to Docker Registry
uses: docker/login-action@v2
with:
registry: ${{ secrets.REGISTRY }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }}
-
name: open-web-leaderboard
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: "${{ secrets.REGISTRY }}/atlantishq/open-web-leaderboard:latest"

13
Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM alpine
RUN apk add --no-cache py3-pip
RUN python3 -m pip install --no-cache-dir --break-system-packages waitress
COPY req.txt .
RUN python3 -m pip install --no-cache-dir --break-system-packages -r req.txt
RUN mkdir /app
WORKDIR /app
COPY ./ .
ENTRYPOINT ["waitress-serve"]
CMD ["--host", "0.0.0.0", "--port", "5000", "--call", "app:createApp"]

63
MapSummary.py Normal file
View File

@@ -0,0 +1,63 @@
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

@@ -33,6 +33,7 @@ Players can be blacklisted by name via a *blacklist.json* file in the project ro
}
# Adding servers for player count live info
**THIS FEATURE IS DISABLED BECAUSE py-valve DOES NOT SUPPORT PYTHON>3.9**
Source-Servers can be added via the *servers.json*-file:
[

60
Round.py Normal file
View File

@@ -0,0 +1,60 @@
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 == 2:
self.winnerSideString = "Security"
self.loserSideString = "Insurgent"
else:
self.winnerSideString = "Insurgent"
self.loserSideString = "Security"
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"

113
api.py
View File

@@ -1,113 +0,0 @@
#!/usr/bin/python3
import riotwatcher
import json
import argparse
import os
import time
from jinja2 import Environment, FileSystemLoader
import requests
import datetime as dt
import sqlite3
REGION = "euw1"
DEFAULT_RATING = 1200
def tierToNumber(tier):
ratingmap = { 'CHALLENGER' : 4500,
'GRANDMASTER': 4000,
'MASTER' : 3500,
'DIAMOND' : 3000,
'PLATINUM' : 2500,
'GOLD' : 1500,
'SILVER' : 1000,
'BRONZE' : 500,
'IRON' : 0 }
return ratingmap[tier]
def divisionToNumber(division):
divisionmap = { "I" : 300,
"II" : 200,
"III" : 100,
"IV" : 0 }
return divisionmap[division]
DATABASE = "rating_cache.sqlite"
def checkPlayerKnown(playerName):
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
backlog = dt.datetime.now() - dt.timedelta(days=7)
query = '''SELECT * from players where playerName = ? LIMIT 1;'''
cursor.execute(query, (playerName,))
try:
playerName, rating, lastUpdated = cursor.fetchone()
except TypeError:
print("sqlite cache '{}' not found".format(playerName))
return None
conn.close()
return (playerName, rating, lastUpdated)
def addToDB(playerName, rating):
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
cursor.execute("INSERT INTO players VALUES(?,?,?);",(
playerName,
rating,
dt.datetime.now().timestamp()))
conn.commit()
print("Added {}".format(playerName))
conn.close()
def getPlayerRatingFromApi(playerName, WATCHER):
if not playerName:
return DEFAULT_RATING
tupel = checkPlayerKnown(playerName)
if tupel:
return tupel[1]
while(True):
try:
pTmp = WATCHER.summoner.by_name(REGION, playerName)
except requests.exceptions.HTTPError as e:
# not found #
if e.response.status_code == 404:
addToDB(playerName, DEFAULT_RATING)
return DEFAULT_RATING
# rate limit
elif e.response.status_code == 429:
print("Ratelimit reached")
#time.sleep(120)
#continue
return DEFAULT_RATING
else:
raise e
if not pTmp:
addToDB(playerName, 0)
return DEFAULT_RATING
computed = DEFAULT_RATING
try:
pInfo = WATCHER.league.by_summoner(REGION, pTmp["id"])
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
print("Ratelimit reached")
return DEFAULT_RATING
#time.sleep(120)
#continue
else:
raise e
for queue in pInfo:
if queue["queueType"] != "RANKED_SOLO_5x5":
continue
computed = tierToNumber(queue["tier"]) + divisionToNumber(queue["rank"]) + \
int(queue["leaguePoints"])
print(computed)
addToDB(playerName, computed)
return computed

2
app.py
View File

@@ -1,3 +1,5 @@
import server
def createApp(envivorment=None, start_response=None):
with server.app.app_context():
server.create_app()
return server.app

View File

@@ -1,68 +0,0 @@
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

1
config.py Normal file
View File

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

2
config.py.example Normal file
View File

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

View File

@@ -1,8 +0,0 @@
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

@@ -31,7 +31,7 @@ class DatabaseConnection:
'''Get the total number of players in the database'''
cursor = self.connPlayers.cursor()
cursor.execute("SELECT Count(*) FROM players")
cursor.execute("SELECT Count(*) FROM players where games >= 10 and not lastgame is null")
count = cursor.fetchone()[0]
return count
@@ -79,8 +79,8 @@ class DatabaseConnection:
cursor = self.connPlayers.cursor()
limit = end - start
print(limit, start)
sqlQuery = '''Select * FROM players where games >= 1
sqlQuery = '''Select * FROM players where games >= 10
and not lastgame is null
ORDER BY (mu - 2*sigma) DESC LIMIT ? OFFSET ?'''
cursor.execute(sqlQuery, (limit, start))
rows = cursor.fetchall()
@@ -121,8 +121,11 @@ class DatabaseConnection:
can't and shouldn't be used to identify a player'''
cursor = self.connPlayers.cursor()
cursor.execute('''SELECT COUNT(*) from players
where (mu-2*sigma) > (?-2*?);''',
if(player.games < 10):
return -1
cursor.execute('''SELECT COUNT(*) from players where games >= 10
and not lastgame is null
and (mu-2*sigma) > (?-2*?);''',
(player.mu, player.sigma))
rank = cursor.fetchone()[0]
return rank
@@ -178,9 +181,9 @@ class DatabaseConnection:
WHERE timestamp < ? AND id = ?''',
(roundObj.startTime.timestamp(), p.playerId))
#if(cursorHist.fetchone()[0] < 10):
# p.ratingChangeString = "Placements"
# continue
if(cursorHist.fetchone()[0] < 10):
p.ratingChangeString = "Placements"
continue
cursorHist.execute('''SELECT mu,sima FROM playerHistoricalData
WHERE timestamp < ? AND id = ? order by timestamp DESC LIMIT 1 ''',

110
medals.py Normal file
View File

@@ -0,0 +1,110 @@
import flask
import markupsafe
medalDict = { "games-played-1" : { "name" : "Tourist",
"text" : "Played {} games on this server",
"color" : "white",
"text-color" : "black" },
"games-played-2" : { "name" : "Enlisted",
"text" : "Played {} games on this server",
"color": "green",
"text-color" : "white" },
"games-played-3" : { "name" : "Veteran",
"text" : "Played {} games on this server",
"color" : "yellow",
"text-color" : "black" },
"games-played-4" : { "name" : "Alpha Chad",
"text" : "Played {} games on this server",
"color" : "black",
"text-color" : "red" },
"rating-cur-1" : { "name" : "Slightly skilled",
"text" : "Rated above 1500",
"color" : "beige",
"text-color" : "black" },
"rating-2k-1" : { "name" : "Contender",
"text" : "Played {} games above 2000 rating",
"color" : "coral",
"text-color" : "black" },
"rating-2k-2" : { "name" : "Known Contender",
"text" : "Played {} games above 2000 rating",
"color" : "lightgreen",
"text-color" : "black" },
"rating-2k-3" : { "name" : "Epic Backup",
"text" : "Played {} games above 2000 rating",
"color" : "darkgreen",
"text-color" : "white" },
"rating-3k-1" : { "name" : "Epic",
"text" : "Played {} games above 3000 rating",
"color" : "lightblue",
"text-color" : "black" },
"rating-3k-2" : { "name" : "Legend",
"text" : "Played {} games above 3000 rating",
"color" : "orange",
"text-color" : "black" },
"rating-3k-3" : { "name" : "All Along The Watchtower",
"text" : "???",
"color" : "red",
"text-color" : "black" },
"rating-3k-4" : { "name" : "Highlander",
"text" : "There can only be one.",
"color" : "darkred",
"text-color" : "white" }
}
def medalGen(medal, formatInsert=None):
'''Gen HTML for metal'''
html = '\
<div style="background-color: {bg} !important; color: {color} !important;"\
class="btn btn-secondary"\
data-toggle="tooltip" data-placement="top" title="{tooltip}">\
{text}\
</div>\
'
tmp = html.format(bg=medal["color"], tooltip=medal["text"], text=medal["name"],
color=medal["text-color"])
if formatInsert:
tmp = tmp.format(formatInsert)
return markupsafe.Markup(tmp)
def getMedals(ratingList, gamesPlayed, currentRating):
'''Get Medals this player should have'''
medals = []
if gamesPlayed > 2000:
medals += [medalGen(medalDict["games-played-4"], gamesPlayed)]
elif gamesPlayed > 500:
medals += [medalGen(medalDict["games-played-3"], gamesPlayed)]
elif gamesPlayed > 100:
medals += [medalGen(medalDict["games-played-2"], gamesPlayed)]
elif gamesPlayed > 50:
medals += [medalGen(medalDict["games-played-1"], gamesPlayed)]
games2k = len(list(filter(lambda x: x > 2000, ratingList)))
games3k = len(list(filter(lambda x: x > 3000, ratingList)))
if games2k > 1000:
medals += [medalGen(medalDict["rating-2k-3"], games2k)]
elif games2k > 100:
medals += [medalGen(medalDict["rating-2k-2"], games2k)]
elif games2k > 0:
medals += [medalGen(medalDict["rating-2k-1"], games2k)]
if games3k > 350:
medals += [medalGen(medalDict["rating-3k-4"], games3k)]
if games3k > 200:
medals += [medalGen(medalDict["rating-3k-3"], games3k)]
if games3k > 50:
medals += [medalGen(medalDict["rating-3k-2"], games3k)]
if games3k > 5:
medals += [medalGen(medalDict["rating-3k-1"], games3k)]
if currentRating > 1500:
medals += [medalGen(medalDict["rating-cur-1"])]
return medals

49
player.py Normal file
View File

@@ -0,0 +1,49 @@
#!/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)

4
req.txt Normal file
View File

@@ -0,0 +1,4 @@
python-valve
pytz
Flask-Caching
Flask

BIN
rounds.sqlite Normal file

Binary file not shown.

View File

@@ -1,50 +0,0 @@
{
"map": "SR",
"winner-side": "1",
"winners": [
{
"playerId": "Phirop",
"isFake": false
},
{
"playerId": "xXxMarethyuxXx",
"isFake": false
},
{
"playerId": "iTrash",
"isFake": false
},
{
"playerId": "JuckF Bierhuhn",
"isFake": false
},
{
"playerId": "ENTAC",
"isFake": false
}
],
"losers": [
{
"playerId": "TryhardYordle",
"isFake": false
},
{
"playerId": "ChessGM lbfxd",
"isFake": false
},
{
"playerId": "TheSlapstick",
"isFake": false
},
{
"playerId": "Vyne",
"isFake": false
},
{
"playerId": "ESE Felix",
"isFake": false
}
],
"duration": 1456,
"startTime": "2021-04-25T22:00+02:00"
}

View File

@@ -1,50 +0,0 @@
{
"map": "SR",
"winner-side": "0",
"winners": [
{
"playerId": "ESE Felix",
"isFake": false
},
{
"playerId": "Vyne",
"isFake": false
},
{
"playerId": "ESE Kniveless",
"isFake": false
},
{
"playerId": "JuckF Bierhuhn",
"isFake": false
},
{
"playerId": "ENTAC",
"isFake": false
}
],
"losers": [
{
"playerId": "TryhardYordle",
"isFake": false
},
{
"playerId": "TheSlapstick",
"isFake": false
},
{
"playerId": "xXxMarethyuxXx",
"isFake": false
},
{
"playerId": "ChessGM lbfxd",
"isFake": false
},
{
"playerId": "Phirop",
"isFake": false
}
],
"duration": 1932,
"startTime": "2021-04-25T21:00+02:00"
}

View File

@@ -1,50 +0,0 @@
{
"map": "SR",
"winner-side": "1",
"winners": [
{
"playerId": "Phirop",
"isFake": false
},
{
"playerId": "Vyne",
"isFake": false
},
{
"playerId": "ESE Felix",
"isFake": false
},
{
"playerId": "ChessGM lbfxd",
"isFake": false
},
{
"playerId": "TheSlapstick",
"isFake": false
}
],
"losers": [
{
"playerId": "ENTAC",
"isFake": false
},
{
"playerId": "xXxMarethyuxXx",
"isFake": false
},
{
"playerId": "ESE Kniveless",
"isFake": false
},
{
"playerId": "JuckF Bierhuhn",
"isFake": false
},
{
"playerId": "TryhardYordle",
"isFake": false
}
],
"duration": 2017,
"startTime": "2021-04-25T20:00+02:00"
}

View File

@@ -1,71 +0,0 @@
{
"map" : "SR",
"winner-side" : 1,
"winners" : [
{
"playerId" : "TryhardYordle",
"playerName" : "TryhardYordle",
"isFake" : false,
"activeTime" : 1971
},
{
"playerId" : "ESE Schmohi",
"playerName" : "ESE Schmohi",
"isFake" : false,
"activeTime" : 1971
},
{
"playerId" : "Area 522",
"playerName" : "Area 522",
"isFake" : false,
"activeTime" : 1971
},
{
"playerId" : "iTrash",
"playerName" : "iTrash",
"isFake" : false,
"activeTime" : 1971
},
{
"playerId" : "Téa Jay",
"playerName" : "Téa Jay",
"isFake" : false,
"activeTime" : 1971
}
],
"losers" : [
{
"playerId" : "Phirop",
"playerName" : "Phirop",
"isFake" : false,
"activeTime" : 1971
},
{
"playerId" : "ENTAC",
"playerName" : "ENTAC",
"isFake" : false,
"activeTime" : 1971
},
{
"playerId" : "BoringBookCover",
"playerName" : "BoringBookCover",
"isFake" : false,
"activeTime" : 1971
},
{
"playerId" : "ESE Litzuck",
"playerName" : "ESE Litzuck",
"isFake" : false,
"activeTime" : 1971
},
{
"playerId" : "ESE MarsUltor",
"playerName" : "ESE MarsUltor",
"isFake" : false,
"activeTime" : 1971
}
],
"duration" : 1971,
"startTime" : "2021-05-24T20:00+02:00"
}

View File

@@ -1,72 +0,0 @@
{
"map" : "SR",
"winner-side" : 0,
"winners" : [
{
"playerId" : "Vyne",
"playerName" : "Vyne",
"isFake" : false,
"activeTime" : 1927
},
{
"playerId" : "ESE Kniveless",
"playerName" : "ESE Kniveless",
"isFake" : false,
"activeTime" : 1927
},
{
"playerId" : "ESE Vinnie",
"playerName" : "ESE Vinnie",
"isFake" : false,
"activeTime" : 1927
},
{
"playerId" : "ESE Litzuck",
"playerName" : "ESE Litzuck",
"isFake" : false,
"activeTime" : 1927
},
{
"playerId" : "ESE MarsUltor",
"playerName" : "ESE MarsUltor",
"isFake" : false,
"activeTime" : 1927
}
],
"losers" : [
{
"playerId" : "ESE Schmohi",
"playerName" : "ESE Schmohi",
"isFake" : false,
"activeTime" : 1927
},
{
"playerId" : "ESE Nemesis",
"playerName" : "ESE Nemesis",
"isFake" : false,
"activeTime" : 1927
},
{
"playerId" : "ENTAC",
"playerName" : "ENTAC",
"isFake" : false,
"activeTime" : 1927
},
{
"playerId" : "Area 522",
"playerName" : "Area 552",
"isFake" : false,
"activeTime" : 1927
},
{
"playerId" : "Subsidiary",
"playerName" : "Subsidiary",
"isFake" : false,
"activeTime" : 1927
}
],
"duration" : 1927,
"startTime" : "2021-05-31T20:00+02:00"
}

View File

@@ -1,72 +0,0 @@
{
"map" : "SR",
"winner-side" : 1,
"winners" : [
{
"playerId" : "Vyne",
"playerName" : "Vyne",
"isFake" : false,
"activeTime" : 1864
},
{
"playerId" : "ESE Kniveless",
"playerName" : "ESE Kniveless",
"isFake" : false,
"activeTime" : 1864
},
{
"playerId" : "ESE Vinnie",
"playerName" : "ESE Vinnie",
"isFake" : false,
"activeTime" : 1864
},
{
"playerId" : "ESE Litzuck",
"playerName" : "ESE Litzuck",
"isFake" : false,
"activeTime" : 1864
},
{
"playerId" : "ESE MarsUltor",
"playerName" : "ESE MarsUltor",
"isFake" : false,
"activeTime" : 1864
}
],
"losers" : [
{
"playerId" : "Muchlove the One",
"playerName" : "Muchlove the One",
"isFake" : false,
"activeTime" : 1864
},
{
"playerId" : "ESE Nemesis",
"playerName" : "ESE Nemesis",
"isFake" : false,
"activeTime" : 1864
},
{
"playerId" : "ENTAC",
"playerName" : "ENTAC",
"isFake" : false,
"activeTime" : 1864
},
{
"playerId" : "Area 522",
"playerName" : "Area 552",
"isFake" : false,
"activeTime" : 1864
},
{
"playerId" : "Subsidiary",
"playerName" : "Subsidiary",
"isFake" : false,
"activeTime" : 1864
}
],
"duration" : 1864,
"startTime" : "2021-05-31T21:00+02:00"
}

View File

@@ -1,71 +0,0 @@
{
"map" : "SR",
"winner-side" : 0,
"winners" : [
{
"playerId" : "Vyne",
"playerName" : "Vyne",
"isFake" : false,
"activeTime" : 1450
},
{
"playerId" : "ESE Kniveless",
"playerName" : "ESE Kniveless",
"isFake" : false,
"activeTime" : 1450
},
{
"playerId" : "Muchlove the One",
"playerName" : "Muchlove the One",
"isFake" : false,
"activeTime" : 1450
},
{
"playerId" : "ESE Litzuck",
"playerName" : "ESE Litzuck",
"isFake" : false,
"activeTime" : 1450
},
{
"playerId" : "ENTAC",
"playerName" : "ENTAC",
"isFake" : false,
"activeTime" : 1450
}
],
"losers" : [
{
"playerId" : "ESE MarsUltor",
"playerName" : "ESE MarsUltor",
"isFake" : false,
"activeTime" : 1450
},
{
"playerId" : "ESE Nemesis",
"playerName" : "ESE Nemesis",
"isFake" : false,
"activeTime" : 1450
},
{
"playerId" : "Area 522",
"playerName" : "Area 552",
"isFake" : false,
"activeTime" : 1450
},
{
"playerId" : "Takki",
"playerName" : "Takki",
"isFake" : false,
"activeTime" : 1450
},
{
"playerId" : "ESE Vinnie",
"playerName" : "ESE Vinnie",
"isFake" : false,
"activeTime" : 1450
}
],
"duration" : 1450,
"startTime" : "2021-05-31T23:00+02:00"
}

View File

@@ -1,50 +0,0 @@
{
"map": "SR",
"winner-side": "0",
"winners": [
{
"playerId": "Phirop",
"isFake": false
},
{
"playerId": "ENTAC",
"isFake": false
},
{
"playerId": "Victor Lustig",
"isFake": false
},
{
"playerId": "TSo",
"isFake": false
},
{
"playerId": "ESE MarsUltor",
"isFake": false
}
],
"losers": [
{
"playerId": "Téa Jay",
"isFake": false
},
{
"playerId": "Heinz sama",
"isFake": false
},
{
"playerId": "Area 522",
"isFake": false
},
{
"playerId": "Byndeskanzler",
"isFake": false
},
{
"playerId": "ESE Schmohi",
"isFake": false
}
],
"duration": 2019,
"startTime": "2021-06-07T20:00+02:00"
}

View File

@@ -1,50 +0,0 @@
{
"map": "SR",
"winner-side": "1",
"winners": [
{
"playerId": "ESE TomTom",
"isFake": false
},
{
"playerId": "Byndeskanzler",
"isFake": false
},
{
"playerId": "Area 522",
"isFake": false
},
{
"playerId": "Heinz sama",
"isFake": false
},
{
"playerId": "Téa Jay",
"isFake": false
}
],
"losers": [
{
"playerId": "Phirop",
"isFake": false
},
{
"playerId": "lbfxd",
"isFake": false
},
{
"playerId": "Victor Lustig",
"isFake": false
},
{
"playerId": "ESE MarsUltor",
"isFake": false
},
{
"playerId": "ENTAC",
"isFake": false
}
],
"duration": 1978,
"startTime": "2021-07-05T20:00+02:00"
}

View File

@@ -1,50 +0,0 @@
{
"map": "SR",
"winner-side": "1",
"winners": [
{
"playerId": "ESE TomTom",
"isFake": false
},
{
"playerId": "ENTAC",
"isFake": false
},
{
"playerId": "Area 522",
"isFake": false
},
{
"playerId": "Heinz sama",
"isFake": false
},
{
"playerId": "Téa Jay",
"isFake": false
}
],
"losers": [
{
"playerId": "Phirop",
"isFake": false
},
{
"playerId": "lbfxd",
"isFake": false
},
{
"playerId": "Victor Lustig",
"isFake": false
},
{
"playerId": "ESE MarsUltor",
"isFake": false
},
{
"playerId": "Byndeskanzler",
"isFake": false
}
],
"duration": 1978,
"startTime": "2021-07-05T21:00+02:00"
}

View File

@@ -1,50 +0,0 @@
{
"map": "SR",
"winner-side": "0",
"winners": [
{
"playerId": "Phirop",
"isFake": false
},
{
"playerId": "ENTAC",
"isFake": false
},
{
"playerId": "Heinz sama",
"isFake": false
},
{
"playerId": "Victor Lustig",
"isFake": false
},
{
"playerId": "ESE MarsUltor",
"isFake": false
}
],
"losers": [
{
"playerId": "Area 522",
"isFake": false
},
{
"playerId": "lbfxd",
"isFake": false
},
{
"playerId": "Téa Jay",
"isFake": false
},
{
"playerId": "Byndeskanzler",
"isFake": false
},
{
"playerId": "ESE TomTom",
"isFake": false
}
],
"duration": 1804,
"startTime": "2021-07-05T22:00+02:00"
}

View File

@@ -1,7 +0,0 @@
curl -X POST -H "Content-Type: application/json" -d @2021-04-26-round-01.json http://localhost:10010/submitt-round
curl -X POST -H "Content-Type: application/json" -d @2021-04-26-round-02.json http://localhost:10010/submitt-round
curl -X POST -H "Content-Type: application/json" -d @2021-04-26-round-03.json http://localhost:10010/submitt-round
curl -X POST -H "Content-Type: application/json" -d @2021-05-24-round-01.json http://localhost:10010/submitt-round
curl -X POST -H "Content-Type: application/json" -d @2021-05-31-round-01.json http://localhost:10010/submitt-round
curl -X POST -H "Content-Type: application/json" -d @2021-05-31-round-02.json http://localhost:10010/submitt-round
curl -X POST -H "Content-Type: application/json" -d @2021-05-31-round-03.json http://localhost:10010/submitt-round

View File

@@ -1,13 +0,0 @@
1
24:16
TryhardYordle
ChessGM lbfxd
TheSlapstick
Vyne
ESE Felix
Phirop
xXxMarethyuxXx
iTrash
JuckF Bierhuhn
ENTAC
2021-04-25T22:00+02:00

View File

@@ -1,44 +0,0 @@
#!/usr/bin/python3
winnerside = input("Winnerseide (0/1 , 0=bue, 1=red): ")
duration = input("Duration: ")
duration = int(duration.split(":")[0])*60+int(duration.split(":")[1])
print("Blue Team")
blueTeam = []
for i in range(5):
pname = input("Name: ")
p = dict()
p.update({"playerId" : pname})
p.update({"isFake" : False})
blueTeam += [p]
print("Red Team")
redTeam = []
for i in range(5):
pname = input("Name: ")
p = dict()
p.update({"playerId" : pname})
p.update({"isFake" : False})
redTeam += [p]
if winnerside == 0:
winners = blueTeam
losers = redTeam
else:
winners = redTeam
losers = blueTeam
startTime = input("Start Time: ")
retDict = {}
retDict.update({ "map" : "SR" })
retDict.update({ "winner-side" : winnerside })
retDict.update({ "winners" : winners})
retDict.update({ "losers" : losers})
retDict.update({ "duration" : duration })
retDict.update({ "startTime" : startTime})
import json
print()
print(json.dumps(retDict, indent=4, sort_keys=False))

536
server.py
View File

@@ -1,307 +1,323 @@
#!/usr/bin/python3
import medals
import flask
import requests
import argparse
import datetime
import itertools
import flask_caching as fcache
import json
import os
import random
import secrets
import riotwatcher
import time
import statistics
import api
import MapSummary
from constants import *
from database import DatabaseConnection
#import valve.source.a2s
#from valve.source import NoResponseError
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)
class PlayerInDatabase(db.Model):
__tablename__ = "players"
player = Column(String, primary_key=True)
rating = Column(Integer)
ratingFix = Column(Integer)
lastUpdated = Column(Integer)
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)
def toDict():
pass
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
@app.route("/role-submission", methods=['GET', 'POST'])
def roleSubmissionTool():
submissionId = flask.request.args.get("id")
player = flask.request.args.get("player")
if flask.request.method == 'POST':
submissionQuery = db.session.query(PlayerInDatabase)
identKey = "{}-{}".format(submissionId, player)
submission = submissionQuery.filter(PlayerInDatabase.ident == identKey).first()
if not submission:
submission = Submission(identKey, submissionId, player, -1, -1, -1, -1, -1)
for i in range(0, 5):
formKey = "prio_" + POSITIONS_NAMES[i]
setattr(submission, DATABASE_PRIO_NAMES[i], flask.request.form[formKey])
db.session.merge(submission)
db.session.commit()
return flask.redirect("/balance-tool?id=" + ident)
def prettifyMinMaxY(computedMin, computedMax):
if computedMax > 0 and computedMin > 0:
return (0, 4000)
else:
return flask.render_template("role_submission.html", positions=positions, ident=ident)
return (computedMin - 100, 4000)
@app.route("/balance-tool-data")
def balanceToolData():
@app.route("/players-online")
def playersOnline():
'''Calc and return the online players'''
submissionId = flask.request.args.get("id")
submissionsQuery = db.session.query(PlayerInDatabase)
submissions = submissionsQuery.filter(PlayerInDatabase.submissionId == submissionId).all()
playerTotal = 0
error = ""
if not submissions:
return flask.Response(json.dumps({ "no-data" : False }), HTTP_OK, mimetype=TYPE_JSON)
for s in SERVERS:
try:
pass
#with valve.source.a2s.ServerQuerier((s["host"], s["port"])) as server:
# playerTotal += int(server.info()["player_count"])
except NoResponseError:
error = "Server Unreachable"
except Exception as e:
error = str(e)
retDict.update()
retDict = { "player_total" : playerTotal, "error" : error }
return flask.Response(json.dumps(retDict), 200, mimetype='application/json')
dicts = [ s.toDict() for s in submissions ]
return flask.Response(json.dumps({ "submissions" : dicts }), HTTP_OK, mimetype=TYPE_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=7)
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
@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
# data for medals #
medalsRatingList = []
if histData:
datapoints = histData[playerId]
if datapoints:
tickCounter = 10
for dpk in datapoints.keys():
# timestamp #
t = datetime.datetime.fromtimestamp(int(float(dpk)))
tsMs = str(int(t.timestamp() * 1000))
computedRating = int(datapoints[dpk]["mu"]) - 2*int(datapoints[dpk]["sigma"])
# for medals #
medalsRatingList += [computedRating]
# for moment js #
ratingString = str(computedRating)
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)
medalsList = medals.getMedals(medalsRatingList, player.games, player.rating)
# 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, medals=medalsList)
@app.route('/leaderboard')
@app.route('/')
@app.route("/balance-tool", methods=['GET', 'POST'])
def balanceTool():
@cache.cached(timeout=600, query_string=True)
def leaderboard():
'''Show main leaderboard page with range dependant on parameters'''
if flask.request.method == 'POST':
# parse parameters #
page = flask.request.args.get("page")
playerName = flask.request.args.get("string")
db = DatabaseConnection(app.config["DB_PATH"])
players = []
threshold = 0.7
for k,v in flask.request.json.items():
if k == "acceptable-solution-threshold":
threshold = v
continue
for i in range(5):
if v[i] in positions:
v[i] = 5
else:
v[i] = int(v[i])
p = Player(k, v)
players += [p]
# theoretical minnimum #
theoMin = sum([ min(p.prio) for p in players ])
permutations = itertools.permutations(players)
best = 100
bestOption = None
alternateOptions = []
alternateOptionsAboveThreshold = []
for option in permutations:
cur = 0
for i in range(len(option)):
cur += option[i].prio[i%5]
if theoMin/cur > threshold:
alternateOptionsAboveThreshold.append(list(option))
qualityCur = int(theoMin/cur*100)
if cur < best:
best = cur
bestOption = list(option)
alternateOptions = []
alternateOptions.append(list(option))
print("Option Found Quality: {}%".format(str(qualityCur)))
elif cur == best or qualityCur > threshold*100:
alternateOptions.append(list(option))
alternateOptions += alternateOptionsAboveThreshold
retDict = { "left" : {}, "right" : {} }
bestOption = list(bestOption)
if len(bestOption) < 10:
for x in range(10-len(bestOption)):
bestOption += [Player("", [0,0,0,0,0])]
for o in alternateOptions:
for x in range(10-len(o)):
o += [Player("", [0,0,0,0,0])]
# fix options with rating #
bestOptionWithRating = None
bestOptionRatings = None
# alternate options rundown positional diff #
posCurrDiff = 100000
for o in alternateOptions:
firstHalf = o[:5]
secondHalf = o[5:]
firstHalfVal = [0, 0, 0, 0, 0]
secondHalfVal = [0, 0, 0, 0, 0]
countFirstHalf = 0
for pil in firstHalf:
if pil:
firstHalfVal[countFirstHalf] = api.getPlayerRatingFromApi(pil.name, WATCHER)
#print(pil.name, firstHalfVal[countFirstHalf])
countFirstHalf += 1
countSecondHalf = 0
for pil in secondHalf:
if pil:
secondHalfVal[countSecondHalf] = api.getPlayerRatingFromApi(pil.name, WATCHER)
#print(pil.name, secondHalfVal[countSecondHalf])
countSecondHalf += 1
posDiff = abs(statistics.median(firstHalfVal) - statistics.median(secondHalfVal))
# check if posdiff is better #
if posDiff < posCurrDiff:
bestOptionWithRating = o
bestOptionRatings = firstHalfVal + secondHalfVal
qualityRatings = -1
# find the best permutation of this solution #
for i in range(0,5):
teamDiff = abs(sum(firstHalfVal) - sum(secondHalfVal))
# first flip
tmp = firstHalfVal[i]
firstHalfVal[i] = secondHalfVal[i]
secondHalfVal[i] = tmp
teamDiffNew = abs(sum(firstHalfVal) - sum(secondHalfVal))
# if new is not better #
if not (teamDiffNew < teamDiff):
# flip it back #
tmp = firstHalfVal[i]
firstHalfVal[i] = secondHalfVal[i]
secondHalfVal[i] = tmp
# else flip the names too #
else:
tmp = firstHalf[i]
firstHalf[i] = secondHalf[i]
secondHalf[i] = tmp
# and reset the option #
bestOptionWithRating = firstHalf + secondHalf
bestOptionRatings = firstHalfVal + secondHalfVal
qualityRatings = min(sum(firstHalfVal)/sum(secondHalfVal),
sum(secondHalfVal)/sum(firstHalfVal))
for i in range(5):
retDict["left"].update( { positions[i] : bestOptionWithRating[i].name })
retDict["right"].update({ positions[i] : bestOptionWithRating[i+5].name })
import sys
print(flask.request.json, file=sys.stderr)
print(retDict, file=sys.stderr)
renderContent = flask.render_template("balance_response_partial.html", d=retDict,
reqJson=flask.request.json,
positions=positions,
ratings=bestOptionRatings,
qualityPositions=int(theoMin/best*100),
qualityRatings=int(qualityRatings*100))
return flask.Response(
json.dumps({ "content": renderContent }), 200, mimetype='application/json')
if page:
pageInt = int(page)
if pageInt < 0:
pageInt = 0
start = SEGMENT * int(page)
else:
givenIdent = flask.request.args.get("id")
if givenIdent:
ident = givenIdent
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:
ident = secrets.token_urlsafe(16)
return flask.redirect("/balance-tool?id={}".format(ident))
return flask.render_template("json_builder.html",
positions=positions,
sides=["left", "right"],
ident=ident)
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
@app.route("/player-api")
def playerApi():
result = api.getPlayerRatingFromApi(flask.request.args.get("id"), WATCHER)
if result:
return ("OK", 200)
else:
return ("Nope", 404)
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
reachedEnd = True
@app.route("/player-api-cache")
def playerApiCache():
result = api.checkPlayerKnown(flask.request.args.get("id"))
if result and result[1] != 0:
return ("OK", 200)
else:
return ("Nope", 404)
@app.route("/get-cache")
def getCacheLoc():
return (json.dumps(api.getCache()), 200)
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>')
def send_js(path):
return send_from_directory('static', path)
@app.before_first_request
def init():
response = send_from_directory('static', path)
response.headers['Cache-Control'] = "max-age=2592000"
return response
global WATCHER
app.config["DB"] = db
db.create_all()
def create_app():
with open("key.txt","r") as f:
key = f.read().strip()
WATCHER = riotwatcher.LolWatcher(key)
global SERVERS
SERVERS_FILE = "servers.json"
if os.path.isfile(SERVERS_FILE):
with open(SERVERS_FILE) as f:
SERVERS = json.load(f)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Start open-leaderboard', \
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--interface', default="localhost")
parser.add_argument('--port', default="5002")
args = parser.parse_args()
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=True,
help='skillbird database (overrides web connection if set)')
app.config["TEMPLATES_AUTO_RELOAD"] = True
with app.app_context():
create_app()
args = parser.parse_args()
app.config["DB_PATH"] = args.skillbird_db
app.run(host=args.interface, port=args.port)

View File

@@ -1,300 +0,0 @@
positions = [ "Top", "Jungle", "Mid", "Bottom", "Support" ]
acceptedParser = [ "top", "jungle", "mid", "sup" , "bot", "adc", "support", "bottom", "*" ]
sides = [ "left", "right"]
var checkPlayerFunc = function checkPlayer() {
if(this.value == ""){
return
}
url = "/player-api-cache?id=" + this.value
fetch(url).then(r => {
if(r.status == 200){
this.style.background = "#74bb74"
}else{
this.style.background = "#d25252"
}
})
}
var checkPlayerApiFunc = function checkPlayer() {
if(this.value == ""){
return
}
url = "/player-api?id=" + this.value
fetch(url).then(r => {
if(r.status == 200){
this.style.background = "#74bb74"
}else{
//this.style.background = "#d25252"
}
})
}
var fastPosChangedFunc = function fastPosChanged() {
accepted = [ "top", "jungle", "mid", "sup" , "bot" ]
uniqArr = []
prioArray = [5, 5, 5, 5, 5]
/* commence cleanup */
clean = this.value.replaceAll(" ", "").toLocaleLowerCase()
clean = clean.replace("support", "sup")
clean = clean.replace("adc", "bot")
clean = clean.replace("bottom", "bot")
retVal = true
if(clean.includes("<")){
console.log("Not accepted (includes <)")
retVal = false
}
list = clean.split(">")
cur = 1
list.forEach(el => {
if(el.includes("=")){
listEq = el.split("=")
listEq.forEach(sub => {
if(accepted.includes(sub) && !uniqArr.includes(sub)){
prioArray[accepted.indexOf(sub)] = cur
uniqArr += [sub]
}else{
console.log("Not accepted (=): " + sub)
retVal = false
}
})
cur++
}else{
if(accepted.includes(el) && !uniqArr.includes(el)){
prioArray[accepted.indexOf(el)] = cur
uniqArr += [el]
cur++
}else{
console.log("Not accepted (>): " + el)
retVal = false
}
}
})
for(i = 0; i<5; i++){
arr = this.id.split("-")
pNr = arr[arr.length-1]
side = this.id.includes("left") ? "left" : "right"
roleSubmission = document.getElementById("fastpos-submission")
if(roleSubmission){
string = "prio_" + accepted[i]
}else {
string = "prio-" + side + "-" + accepted[i] + "-" + pNr
}
string = string.replace("top","Top")
string = string.replace("jungle","Jungle")
string = string.replace("mid","Mid")
string = string.replace("sup","Support")
string = string.replace("bot","Bottom")
selector = document.getElementById(string)
selector.value = prioArray[i]
selector.style.background = "lightblue"
}
/* allow some basic shit */
if(clean == "*" || clean == ""){
retVal = true
}
if(retVal){
this.style.background = "#74bb74"
}else{
this.style.background = "#d25252"
}
}
function balance(){
cont = document.getElementById("response-container")
cont.innerHTML = ""
cont.style.color = "black";
sides = ["left", "right"]
blue = [ "", "", "", "", ""]
red = [ "", "", "", "", ""]
dictToBeSorted = {
0 : [],
1 : [],
2 : [],
3 : [],
4 : []
}
filler = []
impossible = []
dictAll = {}
for(sid = 0; sid < 2; sid++){
for(id = 0; id < 5; id++){
var pname = "undef"
var prioList = [5, 5, 5, 5, 5]
stringPid = `playername-${sides[sid]}-${id}`
pnameObj = document.getElementById(stringPid)
pname = pnameObj.value
for(acc = 0; acc < 5; acc++){
stringSelectorId = `prio-${sides[sid]}-${positions[acc]}-${id}`
selObj = document.getElementById(stringSelectorId)
prioList[acc] = selObj.value
}
dictAll[pname] = prioList
}
}
prioBalanceCheckbox = document.getElementById("prio-balance")
if(prioBalanceCheckbox.checked){
dictAll["acceptable-solution-threshold"] = 0.5
}else{
dictAll["acceptable-solution-threshold"] = 0.7
}
jsonData = JSON.stringify(dictAll, null, 4);
/* transmitt */
spinner = document.getElementById("loading")
spinner.style.display = "block";
fetch(window.location.href, {
method: 'post',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json' },
body: jsonData
}).then(r => r.json()).then(j => {
spinner.style.display = "none";
cont.innerHTML = j["content"]
}).catch(err => {
spinner.style.display = "none";
cont.style.color = "red";
cont.innerHTML = "Error - request failed."
})
}
function parseMultiline(){
var names = []
var prioStrings = []
field = document.getElementById("multi-line-copy")
lines = field.value.split("\n")
lines.forEach(l => {
lowestIndex = 100
acceptedParser.forEach( p => {
i = l.indexOf(" " + p)
if(i >= 3 && i < lowestIndex){
lowestIndex = i
}
})
if(lowestIndex != 100){
name = l.substring(0, lowestIndex)
prioStr = l.substring(lowestIndex)
names.push(name)
prioStrings.push(prioStr)
}
})
count = 0
sides.forEach(s => {
for(i = 0; i<5; i++){
pObjField = document.getElementById("playername-" + s + "-" + i)
prObjField = document.getElementById("check-" + s + "-fastpos-" + i)
if(count >= names.length){
pObjField.value = ""
prObjField.value = ""
}else{
pObjField.value = names[count]
prObjField.value = prioStrings[count]
}
count++;
}
})
const inputEvent = new Event("input")
fastPosFields.forEach(el => el.dispatchEvent(inputEvent))
balance()
}
function queryForPlayerData(){
ident = document.getElementById("ident-field").innerHTML
fetch("/balance-tool-data?id=" + ident).then(r => r.json()).then(j => {
if("no-data" in j){
return
}
j["submissions"].forEach(el => {
console.log(el)
breakToSubmissions = false
for(sid = 0; sid < 2; sid++){
if(breakToSubmissions){
break;
}
for(id = 0; id < 5; id++){
stringPid = `playername-${sides[sid]}-${id}`
pnameObj = document.getElementById(stringPid)
if(pnameObj.value == "" || pnameObj.value == el["name"]){
pnameObj.value = el["name"]
for(acc = 0; acc < 5; acc++){
stringSelectorId = `prio-${sides[sid]}-${positions[acc]}-${id}`
selObj = document.getElementById(stringSelectorId)
selObj.value = el[positions[acc]]
}
breakToSubmissions = true
break;
}
}
}
})
})
}
function copy() {
ident = document.getElementById("ident-field").innerHTML
path = "/role-submission?id="
copyText = window.location.protocol + "//" + window.location.hostname + path + ident
navigator.clipboard.writeText(copyText)
document.getElementById("copyLink").innerHTML = "Copied!"
setTimeout(() => {
document.getElementById("copyLink").innerHTML = "Copy Submission Link" }, 500);
}
fastPosFields = document.getElementsByClassName("fastpos")
playerNameFields = document.getElementsByClassName("pname")
playerNameFields.forEach(el => el.addEventListener('input', checkPlayerFunc));
playerNameFields.forEach(el => el.addEventListener('focus', checkPlayerFunc));
fastPosFields.forEach(el => el.addEventListener('input', fastPosChangedFunc));
//fastPosFields.forEach(el => el.addEventListener('focus', fastPosChangedFunc));
fastposSubmission = document.getElementById("fastpos-submission")
if(fastposSubmission){
fastposSubmission.addEventListener("input", fastPosChangedFunc)
fastposSubmission.addEventListener("focus", fastPosChangedFunc)
}
setInterval(queryForPlayerData(), 3000)
formContainer = document.getElementById("form-container")
if(formContainer){
//formContainer.reset()
}
function resetAll(){
formContainer = document.getElementById("form-container")
formContainer.reset()
}

View File

@@ -8,12 +8,6 @@ body{
}
.border-special{
border-style: outset;
border-width: 0.2px;
padding: 20px;
}
.top-bar{
background-color: orange;
padding-bottom: 10px;
@@ -70,14 +64,14 @@ body{
font-size: 5vw;
margin-right: 2.5%;
}
.input-field{
margin-top: 2vw;
float: left;
font-size: 6vw;
margin-right: 10px;
}
.input-field-number{
display: none;
}
@@ -87,7 +81,7 @@ body{
}
}
/* ############# PLAYER INFORMATION IN LINES ############# */
/* ############# PLAYER INFORMATION IN LINES ############# */
.playerRank{
margin-left:1%;
float: left;
@@ -149,7 +143,7 @@ body{
}
.line-odd{
width: 100%;
width: 100%;
color: black;
background-color: lightgrey !important;
overflow: hidden;
@@ -173,7 +167,7 @@ body{
font-size: 4.5vw;
width: 100%;
}
.colum-names{
white-space: nowrap;
overflow: hidden;
@@ -205,14 +199,14 @@ body{
width: 15%;
text-align: left;
}
.playerWinratio{
/* 19% is just enought to cut the last letter */
width: 19%;
}
}
/* ######################## FOOTER ####################### */
/* ######################## FOOTER ####################### */
.footer{
text-align: center;
font-weight: bold;
@@ -223,7 +217,6 @@ body{
bottom: 0;
left: 0;
height: 30px;
}
.footerLink{
@@ -247,7 +240,6 @@ body{
.footerLink{
font-size: 4vw;
}
.mid{
margin-left: 2.5%;
margin-right: 2.5%;
@@ -264,3 +256,11 @@ body{
font-style: italic;
font-weight: bold;
}
@keyframes fadeIn {
from { opacity: 0.3; }
}
.animate-flicker {
animation: fadeIn 2s alternate;
}

View File

@@ -1,41 +0,0 @@
<div class="my-3">
<h3> Suggestion </h3>
<p>Postion Wishes: {{ qualityPositions }}% fullfilled /
Teambalance: {{ qualityRatings }}% </p>
</div>
{% for x in range(5) %}
<div class="row">
{% set leftP = d["left"][positions[x]] %}
{% set rightP = d["right"][positions[x]] %}
<div class="col-sm">
{{ positions[x] }}
</div>
<div class="col-sm"
{% if reqJson.get(leftP) and reqJson.get(leftP)[x] | int >= 4 %}
style="background: red;"
{% endif %}>
{{ leftP }} ({{ ratings[x] }})
</div>
<div class="col-sm"
{% if reqJson.get(rightP) and reqJson.get(rightP)[x] | int >= 4 %}
style="background: red;"
{% endif %}>
{{ rightP }} ({{ ratings[x+5] }})
</div>
</div>
{% endfor %}
<div class="mt-3">
<h3>For copying into Lobby:</h3>
<div class="mt-2">
<div class="row">
<textarea style="padding-left: 10px !important; padding-top: 10px !important" rows="16" cols= 100>
Left Side Team
{% for x in range(5) %}
{% set leftP = d["left"][positions[x]] %}{{ leftP }} left team {{ positions[x] }}{% endfor %}
Right Side Team
{% for x in range(5) %}
{% set rightP = d["right"][positions[x]] %}{{ rightP }} right team {{ positions[x] }}{% endfor %}
</textarea>
</div>

View File

@@ -10,6 +10,29 @@
{% include 'navbar_leaderboard.html' %}
<div class="container mt-3 mb-3" role="main">
<div id="playerDisplay" class="playerDisplay mb-3 mt-2">
<script>
function players(){
//document.getElementById("playerDisplay").classList.remove("animate-flicker")
fetch("/players-online").then(
response => response.json()
).then(
data => {
if(data["error"] == ""){
document.getElementById("playerDisplay").innerHTML = "Players Online: " + data["player_total"]
if(parseInt(data["player_total"]) == 0){
//document.getElementById("playerDisplay").classList.add("animate-flicker")
}
}else{
document.getElementById("playerDisplay").innerHTML = "Players Online: (error)" + data["error"]
}
}
)
}
players()
setInterval(players, 10000)
</script>
</div>
<table id="tableMain" class="table table-striped table-bordered table-sm"
cellspacing="0">
<thead>

View File

@@ -1,9 +1,9 @@
<div class="footer-copyright text-center py-3 bg-dark" role="contentinfo">
<a style="color: rgba(255,255,255,.5);"
class="footerLink" href="https://atlantishq.de/impressum">Impressum/Legal</a>
class="footerLink" href="https://blog.atlantishq.de/about">Impressum/Legal</a>
<a style="color: rgba(255,255,255,.5);"
class="footerLink mid" href="https://esports-erlangen.de">
ESE Website</a>
class="footerLink mid" href="steam://connect/athq.de/:27026">
Join the Server!</a>
<a style="color: rgba(255,255,255,.5);"
class="footerLink" href="https://github.com/FAUSheppy/skillbird">Star on GitHub</a>
</div>

View File

@@ -1,150 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Balance and Submission Tool</title>
<meta name="Description" content="Sheppy is awesome?">
<script defer src="/static/balance.js"></script>
{% include 'default_head_content.html' %}
</head>
<body class="bg-special">
{% include 'navbar.html' %}
<div id="ident-field" style="display: none;">{{ ident }}</div>
<div class="container mt-3 mb-3" role="main">
<div class="row">
<div class="col-sm">
<h1>Balance Tool</h1>
</div>
</div>
<div class="row">
<div class="col">
<button type="button" class="mb-3 btn btn-secondary" onclick="balance()">
Find Teams
</button>
</br>
<button id="copyLink" type="button"
class="mb-3 btn btn-secondary" onclick="copy()">
Copy Submission Link
</button>
<button type="button"
class="mb-3 btn btn-secondary" onclick="queryForPlayerData()">
Check Submissions
</button>
</br>
{% if True %}
<button class="btn btn-primary" type="button" data-toggle="collapse"
data-target="#hilfe" aria-expanded="false"
aria-controls="hilfe">
Hilfe Anzeigen
</button>
<button type="button"
class="mb-3 btn btn-secondary" onclick="resetAll()">
Reset all
</button></br>
<div style="display: none;" class="form-check mt-3">
<input class="form-check-input" type="checkbox" value=""
id="prio-balance">
<label class="form-check-label" for="flexCheckChecked">
Prioritize balance over positional preferences
<p style="color: red;">(experimental, slow af)</p>
</label>
</div>
<div id="hilfe" class="collapse mt-5 border-special">
<h4>Priorität</h4>
<p>
1 = höchste Priorität
5 = niedrigste Priorität
</p>
<hr class="my-3">
<h4>Fast Position</h4>
<p>
<b>Positionen können in den Kurzschreibweisen: top, mid, bot, jungle, support mit "=" und ">" angegeben werden. Zum Beispiel:</b></br></br>
top > mid > top = adc</br>
mid = top > bot</br>
support > jungle</br>
etc..</br>
</p>
<hr class="my-3">
<h4>Submission Link</h4>
<p>
Mit diesem Link können Spieler eine Positionspräferenz selbst übermitteln, einfach diesen Link in den Chat pasten.
</p>
<hr class="my-3">
<h4>Find Teams</h4>
<p>
Sobald alle eingetragen sind einmal auf "Find Teams" klicken und auf den Vorschlag warten.
</p>
<hr class="my-3">
<h4>Multiline Input</h4>
<p>
Jeder Kann seine Rollen im Schnellformat in den Chat schreiben, in das Feld unten kopieren und Button drücken (überschreibt existierende Einträge).
</p>
</div>
{% endif %}
</div>
</div>
<div class="spinner-border" id="loading" style="display: none;" role="status">
<span class="sr-only">Loading...</span>
</div>
<div id="response-container" class="mt-3 mb-3">
</div>
<hr>
<form id="form-container" class="row">
{% for side in sides %}
<div class="col-sm">
{% for field in range(5) %}
<div id="{{ side }}-{{ field }}" class="row mt-2 mb-2">
<div class="col-sm">
<!-- player name field -->
<input class="form-control pname" type="text" placeholder="Player"
id="playername-{{ side }}-{{ field }}">
<!-- fast postition slection field -->
<input class="form-control fastpos" type="text"
placeholder="top > mid = bot"
id="check-{{ side }}-fastpos-{{ field }}">
</div>
<div class="col-sm">
<!-- checkboxes for pos -->
{% for pos in positions %}
<select id="prio-{{ side }}-{{ pos }}-{{ field }}"
class="form-select">
<option selected>{{ pos }}</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
{% endfor %}
</div>
</div>
<hr>
{% endfor %}
</div>
{% endfor %}
</form>
<hr>
<textarea type="text" id="multi-line-copy" rows="10"
placeholder="Have everybody write their roles in the form of 'top > mid = sup = jungle >adc' in the chat and then copy it in here and hit 'Use Multiline Input' :)"
class="form-control md-textarea"></textarea>
<button type="button" class="mb-3 btn btn-secondary" onclick="parseMultiline()">
Use Multiline Input..
</button>
<hr>
</div>
{% include 'footer.html' %}
</body>
</html>

View File

@@ -10,13 +10,28 @@
<!-- left side -->
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/">New Balance</a>
<a class="nav-link" href="/">Leaderboard</a>
</li>
<!--
<li class="nav-item">
<a class="nav-link" href="/rounds">Rounds</a>
</li>
-->
<li class="nav-item">
<a class="nav-link" href="/maps">Maps</a>
</li>
</ul>
<ul class="navbar-nav right">
<li class="nav-item">
<a class="nav-link" href="steam://connect/athq.de/:27026">Server 1</a>
</li>
<li class="nav-item mr-4">
<a class="nav-link" href="steam://connect/athq.de/:27015">Server 2</a>
</li>
<!--
<li class="nav-item">
<a class="navbar-brand hover-to-75 patreon" style="position: unset !important;" href="https://www.patreon.com/erlangen_sheppy">Support me
</a>
</li>
-->
</ul>
</div>
</nav>

View File

@@ -18,8 +18,10 @@
<li class="nav-item">
<a id="button-forward" class="nav-link" href="/?page={{ nextPageNumber }}">Forward</a>
</li>
<li class="nav-item">
<a id="button-forward" class="nav-link" href="/balance-tool">Balance Tool</a>
<li class="nav-item" {% if not gameInProgress %} style="opacity: 0.5" {% endif %}>
<a id="button-forward" class="nav-link" href="/livegames">Livegames
<div style="font-size: small; color: red; opacity: 1; float: right; margin-left: 10px;">(experimental)</div>
</a>
</li>
</ul>
<ul class="navbar-nav">
@@ -31,12 +33,15 @@
<li class="nav-item right mr-2">
<a class="nav-link" href="/rounds">Rounds</a>
</li>
<li class="nav-item right mr-2">
<a class="nav-link" href="/maps">Maps</a>
</li>
<li class="nav-item right">
<input id="getPlayer" type="text" aria-label="search for player" class="mt-1"
placeholder="search player...">
</li>
{{ findPlayer }}
<!-- <li class="nav-item right">
<!--- <li class="nav-item right">
<input id="gotoRank" type="number" aria-label="goto rank"
placeholder="goto rank...">
</li> -->

View File

@@ -14,17 +14,28 @@
</h1>
<h3>
Rating: <i>{{ player.rating }}</i> <br>
Rank: {{ player.rank }}
{% if player.rank == 0 %}
<i><small>Missing {{ 10 - player.games }} placement games!</small></i>
{% elif not player.lastGame %}
<i><small>Must play a game again before being assigned a rank!</small></i>
{% else %}
Rank: {{ player.rank }}
{%endif%}
</h3>
</div>
<div class="plot-container">
<canvas id="lineChart">
</canvas>
</div>
<div class="mt-3 medal-container">
{% for m in medals %}
{{ m }}
{% endfor %}
</div>
<p class="mt-5 mb-3">
</p>
</div>
{% include 'footer.html' %}
<!-- {% include 'footer.html' %}-->
<script defer>
var canvas = document.getElementById("lineChart")
var ctx = canvas.getContext('2d');

View File

@@ -1,70 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Balance and Submission Tool</title>
<meta name="Description" content="Sheppy is awesome?">
<script defer src="/static/balance.js"></script>
{% include 'default_head_content.html' %}
</head>
<body class="bg-special">
{% include 'navbar.html' %}
<div class="container mt-3 mb-3" role="main">
<div class="row">
<div class="col-sm">
<h1>Role Preference Submission Tool</h1>
</div>
</div>
<!-- player name field -->
<form action="/role-submission?id={{ ident }}" method="POST">
<p style="color: red; font-weight: bold"> PLEASE USE YOUR _CORRECTLY SPELLED_ LoL-NAME!!</p>
<input class="form-control pname m-3" type="text" placeholder="Player"
name="playername" id="playername">
<!-- fast postition slection field -->
<input class="form-control fastpos m-3" type="text"
placeholder="Fast Position Input"
id="fastpos-submission">
<div class="col-sm m-3" style="min-width: 300px;">
<!-- checkboxes for pos -->
{% for pos in positions %}
<div>
<label style="min-width: 100px;"
for="prio_{{ pos }}">{{ pos }}</label>
<select name="prio_{{ pos }}" id="prio_{{ pos }}"
class="form-select ml-3 mr-3">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</div>
{% endfor %}
</div>
<input type="submit" value="Submit">
<div class="mt-5">
<h4>Priorität</h4>
<p>
1 = höchste </br>
5 = niedrigste </br>
</p>
<hr class="my-3">
<h4>Fast-Position</h4>
<p>
<b>Positionen können in den Kurzschreibweisen: top, mid, bot, jungle, support mit "=" und ">" angegeben werden. Zum Beispiel:</b></br></br>
top > mid > top = adc</br>
mid = top > bot</br>
support > jungle</br>
etc..</br>
</p>
</div>
</form>
</div>
{% include 'footer.html' %}
</body>
</html>

View File

@@ -41,6 +41,9 @@
<div class="col-sm" style="overflow: hidden;">
<a href="/player?id={{ p.playerId }}">{{ p.name }}</a>
</div>
<div class="col-sm">
{{ p.participation }}%
</div>
<div class="col-sm">
{% if not r.invalid %}
<small style="color: green;">
@@ -57,6 +60,9 @@
<div class="col-sm" style="overflow: hidden;">
<a href="/player?id={{ p.playerId }}">{{ p.name }}</a>
</div>
<div class="col-sm">
{{ p.participation }}%
</div>
<div class="col-sm">
{% if not r.invalid %}
<small style="color: red;">