Compare commits

35 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
Yannik Schmidt
5d98016040 fix various problem reported by users 2021-09-06 10:57:01 +02:00
Yannik Schmidt
6a2ff6a72f reset color on bad request 2021-07-27 13:56:40 +02:00
Yannik Schmidt
ee61d1bb63 update 2021-07-26 23:25:06 +02:00
Yannik Schmidt
5c8191f4cf new rounds 2021-07-26 21:19:43 +02:00
Yannik Schmidt
5dc1d16447 remove leaderboard from navbar 2021-07-26 21:18:27 +02:00
Yannik Schmidt
eb967900d0 more api fixes 2021-07-26 21:07:57 +02:00
Yannik Schmidt
bcb460bc24 implement basic queries from api 2021-07-25 03:04:24 +02:00
Yannik Schmidt
02be6031a2 prepare api query 2021-07-24 13:22:25 +02:00
a8da2b71bc add rounds 2021-06-07 21:06:27 +02:00
7f8af681dd fix some types 2021-06-07 18:25:00 +02:00
25ea0e03b5 implement simple balance by rating 2021-06-07 17:50:28 +02:00
7f845cc17c add new line 2021-06-06 15:03:03 +02:00
6a5b150cb8 add help 2021-06-06 15:01:54 +02:00
82680cc0db fix url 2021-06-04 20:22:54 +02:00
bdd24d68fa redirect to id url if none given 2021-06-04 20:21:50 +02:00
cab9dceab7 some minor improvements 2021-06-04 19:08:11 +02:00
ead1a05c2e implement autoform clear 2021-06-04 18:38:49 +02:00
77e58305da tabs -> spaces 2021-06-04 18:32:41 +02:00
9cdee90a7f implement role submissioN 2021-06-04 18:32:10 +02:00
0a1675c637 allow lol names with length 3 2021-06-03 21:29:02 +02:00
f6ce5a9533 support non-full teams 2021-06-03 21:11:26 +02:00
8f8eba81fa implement spinner to show loading status 2021-06-03 21:06:30 +02:00
3a028607b4 finish better algo serverside 2021-06-03 21:02:18 +02:00
ca8dfb5208 remove old code 2021-06-03 21:02:04 +02:00
Yannik Schmidt
3df3ade848 change algorithm 2021-06-03 20:38:46 +02:00
2ce48a7147 allow * internally 2021-06-03 20:22:11 +02:00
590763c613 implement multiline input copy 2021-06-02 22:32:57 +02:00
5b8ab6fa78 remove console.logs 2021-06-02 01:12:54 +02:00
1641332661 implement balance tool basic func done 2021-06-02 01:00:23 +02:00
bf31cb59e8 more features 2021-06-01 23:41:13 +02:00
a52bd66aed basic balance input 2021-06-01 19:41:34 +02:00
54f02c978a ESE custom modifications 2021-06-01 15:53:38 +02:00
41 changed files with 1743 additions and 688 deletions

View File

@@ -1,32 +0,0 @@
name: Container Build for open-web-leaderboard
on:
push:
branches:
- "master"
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"

View File

@@ -1,13 +0,0 @@
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"]

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

@@ -33,7 +33,6 @@ Players can be blacklisted by name via a *blacklist.json* file in the project ro
} }
# Adding servers for player count live info # 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: Source-Servers can be added via the *servers.json*-file:
[ [

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 == 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 Normal file
View File

@@ -0,0 +1,113 @@
#!/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,5 +1,3 @@
import server import server
def createApp(envivorment=None, start_response=None): def createApp(envivorment=None, start_response=None):
with server.app.app_context():
server.create_app()
return server.app return server.app

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

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

View File

@@ -1,93 +0,0 @@
import flask
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" },
"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-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" : "Played {} games above 3000 rating",
"color" : "red",
"text-color" : "black" },
}
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 flask.Markup(tmp)
def getMedals(ratingList, gamesPlayed, currentRating):
'''Get Medals this player should have'''
medals = []
if 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 > 100:
medals += [medalGen(medalDict["rating-2k-2"], games2k)]
elif games2k > 0:
medals += [medalGen(medalDict["rating-2k-1"], games2k)]
if games3k > 200:
medals += [medalGen(medalDict["rating-3k-3"], games3k)]
if games3k > 50:
medals += [medalGen(medalDict["rating-3k-2"], games3k)]
elif games3k > 5:
medals += [medalGen(medalDict["rating-3k-1"], games3k)]
if currentRating > 1500:
medals += [medalGen(medalDict["rating-cur-1"])]
return medals

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)

Binary file not shown.

View File

@@ -0,0 +1,50 @@
{
"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

@@ -0,0 +1,50 @@
{
"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

@@ -0,0 +1,50 @@
{
"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

@@ -0,0 +1,71 @@
{
"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

@@ -0,0 +1,72 @@
{
"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

@@ -0,0 +1,72 @@
{
"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

@@ -0,0 +1,71 @@
{
"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

@@ -0,0 +1,50 @@
{
"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

@@ -0,0 +1,50 @@
{
"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

@@ -0,0 +1,50 @@
{
"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

@@ -0,0 +1,50 @@
{
"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"
}

7
rounds/all.sh Normal file
View File

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

13
rounds/data Normal file
View File

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

44
rounds/gen.py Executable file
View File

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

534
server.py
View File

@@ -1,323 +1,307 @@
#!/usr/bin/python3 #!/usr/bin/python3
import medals
import flask import flask
import requests import requests
import argparse import argparse
import datetime import datetime
import flask_caching as fcache import itertools
import json import json
import os import os
import MapSummary import random
import secrets
import riotwatcher
import time
import statistics
import api
from database import DatabaseConnection from constants import *
#import valve.source.a2s
#from valve.source import NoResponseError
app = flask.Flask("open-leaderboard") app = flask.Flask("open-leaderboard")
WATCHER = 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)
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)
else: else:
return (computedMin - 100, 4000) return flask.render_template("role_submission.html", positions=positions, ident=ident)
@app.route("/players-online") @app.route("/balance-tool-data")
def playersOnline(): def balanceToolData():
'''Calc and return the online players'''
playerTotal = 0 submissionId = flask.request.args.get("id")
error = "" submissionsQuery = db.session.query(PlayerInDatabase)
submissions = submissionsQuery.filter(PlayerInDatabase.submissionId == submissionId).all()
for s in SERVERS: if not submissions:
try: return flask.Response(json.dumps({ "no-data" : False }), HTTP_OK, mimetype=TYPE_JSON)
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 = { "player_total" : playerTotal, "error" : error } retDict.update()
return flask.Response(json.dumps(retDict), 200, mimetype='application/json')
@app.route("/round-info") dicts = [ s.toDict() for s in submissions ]
def singleRound(): return flask.Response(json.dumps({ "submissions" : dicts }), HTTP_OK, mimetype=TYPE_JSON)
'''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('/')
@cache.cached(timeout=600, query_string=True) @app.route("/balance-tool", methods=['GET', 'POST'])
def leaderboard(): def balanceTool():
'''Show main leaderboard page with range dependant on parameters'''
# parse parameters # if flask.request.method == 'POST':
page = flask.request.args.get("page")
playerName = flask.request.args.get("string")
db = DatabaseConnection(app.config["DB_PATH"])
if page: players = []
pageInt = int(page) threshold = 0.7
if pageInt < 0: for k,v in flask.request.json.items():
pageInt = 0 if k == "acceptable-solution-threshold":
start = SEGMENT * int(page) threshold = v
else: continue
pageInt = 0 for i in range(5):
start = 0 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 ])
# handle find player request # permutations = itertools.permutations(players)
cannotFindPlayer = ""
searchName = "" 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])]
playerList = None # fix options with rating #
doNotComputeRank = True bestOptionWithRating = None
if playerName: bestOptionRatings = None
playersInLeaderboard = db.findPlayerByName(playerName)
if not playersInLeaderboard: # alternate options rundown positional diff #
cannotFindPlayer = flask.Markup("<div class=noPlayerFound>No player of that name</div>") posCurrDiff = 100000
start = 0 for o in alternateOptions:
else: firstHalf = o[:5]
if len(playersInLeaderboard) == 1: secondHalf = o[5:]
rank = playersInLeaderboard[0].rank
if(playersInLeaderboard[0].games < 10): firstHalfVal = [0, 0, 0, 0, 0]
return flask.redirect("/player?id={}".format(playersInLeaderboard[0].playerId)) secondHalfVal = [0, 0, 0, 0, 0]
searchName = playersInLeaderboard[0].name
start = rank - (rank % SEGMENT) countFirstHalf = 0
else: for pil in firstHalf:
playerList = playersInLeaderboard if pil:
for p in playerList: firstHalfVal[countFirstHalf] = api.getPlayerRatingFromApi(pil.name, WATCHER)
if p.rank == -1: #print(pil.name, firstHalfVal[countFirstHalf])
p.rankStr = "N/A" 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: else:
p.rankStr = str(p.rank) tmp = firstHalf[i]
doNotComputeRank = False 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))
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
playerList = db.getRankRange(start, end)
for i in range(5):
endOfBoardIndicator = "" retDict["left"].update( { positions[i] : bestOptionWithRating[i].name })
if reachedEnd: retDict["right"].update({ positions[i] : bestOptionWithRating[i+5].name })
endOfBoardHtml = "<div id='eof' class=endOfBoardIndicator> - - - End of Board - - - </div>"
endOfBoardIndicator = flask.Markup(endOfBoardHtml) import sys
print(flask.request.json, file=sys.stderr)
# fix <100 player start at 0 # print(retDict, file=sys.stderr)
if maxEntry <= 100: renderContent = flask.render_template("balance_response_partial.html", d=retDict,
start = max(start, 0) reqJson=flask.request.json,
positions=positions,
finalResponse = flask.render_template("base.html", playerList=playerList, ratings=bestOptionRatings,
doNotComputeRank=doNotComputeRank, qualityPositions=int(theoMin/best*100),
start=start, qualityRatings=int(qualityRatings*100))
endOfBoardIndicator=endOfBoardIndicator, return flask.Response(
findPlayer=cannotFindPlayer, json.dumps({ "content": renderContent }), 200, mimetype='application/json')
searchName=searchName, else:
nextPageNumber=int(pageInt)+1, givenIdent = flask.request.args.get("id")
prevPageNumber=int(pageInt)-1) if givenIdent:
return finalResponse ident = givenIdent
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)
@app.route("/player-api")
def playerApi():
result = api.getPlayerRatingFromApi(flask.request.args.get("id"), WATCHER)
if result:
return ("OK", 200)
else:
return ("Nope", 404)
@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)
@app.route('/static/<path:path>') @app.route('/static/<path:path>')
def send_js(path): def send_js(path):
return send_from_directory('static', path)
response = send_from_directory('static', path) @app.before_first_request
response.headers['Cache-Control'] = "max-age=2592000" def init():
return response
global WATCHER
def create_app(): app.config["DB"] = db
db.create_all()
global SERVERS with open("key.txt","r") as f:
key = f.read().strip()
SERVERS_FILE = "servers.json" WATCHER = riotwatcher.LolWatcher(key)
if os.path.isfile(SERVERS_FILE):
with open(SERVERS_FILE) as f:
SERVERS = json.load(f)
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=True,
help='skillbird database (overrides web connection if set)')
with app.app_context():
create_app()
args = parser.parse_args() 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) app.run(host=args.interface, port=args.port)

300
static/balance.js Normal file
View File

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

View File

@@ -0,0 +1,41 @@
<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,29 +10,6 @@
{% include 'navbar_leaderboard.html' %} {% include 'navbar_leaderboard.html' %}
<div class="container mt-3 mb-3" role="main"> <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" <table id="tableMain" class="table table-striped table-bordered table-sm"
cellspacing="0"> cellspacing="0">
<thead> <thead>

View File

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

150
templates/json_builder.html Normal file
View File

@@ -0,0 +1,150 @@
<!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,28 +10,13 @@
<!-- left side --> <!-- left side -->
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/">Leaderboard</a> <a class="nav-link" href="/">New Balance</a>
</li> </li>
<!--
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/rounds">Rounds</a> <a class="nav-link" href="/rounds">Rounds</a>
</li> </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> </ul>
</div> </div>
</nav> </nav>

View File

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

View File

@@ -14,28 +14,17 @@
</h1> </h1>
<h3> <h3>
Rating: <i>{{ player.rating }}</i> <br> Rating: <i>{{ player.rating }}</i> <br>
{% if player.rank == 0 %} Rank: {{ player.rank }}
<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> </h3>
</div> </div>
<div class="plot-container"> <div class="plot-container">
<canvas id="lineChart"> <canvas id="lineChart">
</canvas> </canvas>
</div> </div>
<div class="mt-3 medal-container">
{% for m in medals %}
{{ m }}
{% endfor %}
</div>
<p class="mt-5 mb-3"> <p class="mt-5 mb-3">
</p> </p>
</div> </div>
<!-- {% include 'footer.html' %}--> {% include 'footer.html' %}
<script defer> <script defer>
var canvas = document.getElementById("lineChart") var canvas = document.getElementById("lineChart")
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');

View File

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