2020 rewrite

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

View File

@@ -0,0 +1,99 @@
import sqlite3
import backends.entities.Players as Players
import backends.trueskillWrapper as trueskill
# setup
# create TABLE players (id TEXT PRIMARY KEY, name TEXT, lastgame TEXT, wins INTEGER, mu REAL, sigma REAL, game INTEGER);
DATABASE = "players.sqlite"
def getPlayer(playerId):
conn = sqlite3.connect("players.sqlite")
try:
cursor = conn.cursor()
cursor.execute("SELECT * FROM players WHERE id = ?", (playerId,))
rows = cursor.fetchall()
if len(rows) < 1:
return None
else:
playerId, playerName, lastGame, wins, mu, sigma, games = rows[0]
return Players.PlayerInDatabase(playerId, playerName,
trueskill.newRating(mu=mu, sigma=sigma), wins, games)
finally:
conn.close()
def getOrCreatePlayer(player):
playerInDb = getPlayer(player.id)
if not playerInDb:
return savePlayerToDatabase(player)
else:
return playerInDb
def getMultiplePlayers(playerIdList):
return [ getPlayer(p) for p in playerIdList ]
def savePlayerToDatabase(player, incrementWins=0):
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
if not player.rating:
player.rating = trueskill.newRating()
playerFromDatabase = getPlayer(player.id)
if playerFromDatabase:
cursor.execute('''UPDATE players SET id = ?,
name = ?,
lastgame = ?,
wins = ?,
mu = ?,
sigma = ?,
games = ?
WHERE id = ?''',
(player.id, player.name, None, playerFromDatabase.wins + incrementWins,
player.rating.mu, player.rating.sigma, playerFromDatabase.games + 1, player.id))
else:
cursor.execute("INSERT INTO players VALUES (?,?,?,?,?,?,?)",
(player.id, player.name, None, 0,
player.rating.mu, player.rating.sigma, 0))
conn.commit()
conn.close()
return getPlayer(player.id)
def saveMultiplePlayersToDatabase(playerList, incrementWins=0):
for p in playerList:
savePlayerToDatabase(p, incrementWins)
## open leaderboard functions ##
def findPlayerByName(playerName):
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
playerNamePrepared = "%{}%".format(playerName.replace("%", "%%"))
cursor.execute("SELECT * FROM players WHERE name == ?", (playerName,))
rows = cursor.fetchall()
playerRow = None
if len(rows) < 1:
cursor.execute("SELECT * FROM players WHERE name LIKE ?", (playerNamePrepared,))
rows = cursor.fetchall()
if len(rows) < 1:
return None
playerRow = rows[0]
else:
playerRow = rows[0]
playerId, playerName, lastGame, wins, mu, sigma, games = playerRow
conn.close()
return Players.PlayerInDatabase(playerId, playerName,
trueskill.newRating(mu=mu, sigma=sigma), wins, games)
def getTotalEntries(playerName):
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
playerNamePrepared = "%{}%".format(playerName.replace("%", "%%"))
cursor.execute("select count(*) from players")
count = cursor.fetchone()
conn.close()
return count
def getRankRange(start, end):
pass

View File

@@ -0,0 +1,67 @@
import datetime as dt
import json
class Player:
def __init__(self, playerId, name, rating=None):
self.rating = rating
self.id = playerId
self.name = name
def __hash__(self):
return hash(self.id)
def __eq__(self, other):
if not other:
return False
elif isinstance(other, str):
return self.id == other
elif isinstance(other, Player):
return self.id == other.id
else:
raise TypeError("Player: Unsupported equals with types {} and {}".format(type(other),type(self)))
def __str__(self):
return "Player: {}, ID: {}".format(self.name, self.id)
class PlayerInRound(Player):
def __init__(self, playerId, name, team, timestamp, is_fake=False):
self.name = name
self.id = playerId
self.team = int(team)
self.active = True
self.rating = None
self.activeTime = dt.timedelta(0)
self.is_fake = is_fake
self.timestamp = timestamp
def __str__(self):
return "PlayerInRound: N: {} ID: {} Team: {}".format(self.name, self.id, self.team)
class PlayerInDatabase(Player):
def __init__(self, playerId, name, rating, wins, games):
self.id = playerId
self.name = name
self.rating = rating
self.lastUpdate = dt.datetime.now()
self.wins = wins
self.games = games
def winratio(self):
if self.games == 0:
return "---"
return str(int(self.wins*100/self.games))
def getName(self):
return self.name.encode('utf-8')[:25].decode('utf-8','ignore').rstrip(" ")
def toJson(self):
retDict = { "name" : self.name,
"id" : self.id,
"rating-mu" : self.rating.mu,
"rating-sigma" : self.rating.mu,
"games" : self.games,
"wins" : self.wins,
"last-game" : self.lastUpdate.isoformat()}
return json.dumps(retDict)

View File

@@ -0,0 +1,219 @@
import abc
import dateutil.parser
import datetime as dt
import Round
import backends.entities.Players as Players
NO_TEAM = 0
OBSERVERS = 1
SECURITY = 2
INSURGENT = 3
def _getKey(dic, key):
'''Helper function'''
tmp = list(dic)
return tmp[tmp.index(key)]
def parse(events):
'''Parse a string blob representing a full round'''
eventsParsed = []
for eventJsonObject in events:
eventsParsed += [parseEventString(eventJsonObject)]
es = EventSeries(eventsParsed)
return Round.Round(es.winnerTeam, es.loserTeam, es.map, es.duration, es.startTime, es.winnerTeamId)
def parseEventString(event):
'''Take a dictionary representing an event and return an actual event object'''
TEAMCHANGE = ["teamchange"]
ACTIVE_PLAYERS = ["ct","dc","round_start_active","round_end_active","tc","active_players"]
DISCONNECT = ["disconnect"]
WINNER_INFO = ["winner"]
MAP_INFO = ["mapname", "map"]
IGNORE = ["map_start_active","start","plugin unloaded"]
print(event)
etype = event["etype"]
if False:
pass
# elif etype in TEAMCHANGE:
# return TeamchangeEvent(event)
elif etype in ACTIVE_PLAYERS:
return ActivePlayersEvent(event)
# elif etype in DISCONNECT:
# return DisconnectEvent(event)
elif etype in WINNER_INFO:
return WinnerInformationEvent(event)
elif etype in MAP_INFO:
return MapInformationEvent(event)
elif etype in IGNORE:
pass
class Event(abc.ABC):
'''Abstract class all events inherit from'''
pass
#class DisconnectEvent(Event):
# '''Event describing a disconnect by an individual player'''
#
# def __init__(self, event):
# self.timestamp = dt.datetime.fromtimestamp(event["timestamp"])
# self.player = event["playerId"]
#
#class TeamchangeEvent(Event):
# '''Event describing a teamchange by an individual player'''
#
# def __init__(self, event):
# self.timestamp = dt.datetime.fromtimestamp(event["timestamp"])
# self.player = event["playerId"]
# self.old_team = event["previousTeam"]
class ActivePlayersEvent(Event):
'''Event describing as absolute values the currently active player'''
def __init__(self, event):
self.timestamp = dt.datetime.fromtimestamp(event["timestamp"])
self.players = [ Players.PlayerInRound(p["id"], p["name"], p["team"], self.timestamp)
for p in event["players"] ]
class WinnerInformationEvent(Event):
'''Event describing which team has won the game'''
def __init__(self, event):
self.timestamp = dt.datetime.fromtimestamp(event["timestamp"])
self.winner = event["winnerTeam"]
class MapInformationEvent(Event):
'''Event describing the current map'''
def __init__(self, event):
self.timestamp = dt.datetime.fromtimestamp(event["timestamp"])
self.map = event["map"]
class EventSeries():
def __init__(self, events):
self.events = events
self.winnerTeam = None
self.winnerTeamId = -1
self.loserTeam = None
self.loserTeamId = -1
self.map = ""
lastEvent = max(events, key=lambda e: e.timestamp)
firstEvent = min(events, key=lambda e: e.timestamp)
self.duration = lastEvent.timestamp - firstEvent.timestamp
self.startTime = firstEvent.timestamp
self.teamA = []
self.teamAId = 2
self.teamB = []
self.teamBId = 3
for e in events:
if type(e) == ActivePlayersEvent:
for playerInRound in e.players:
## Case 1: Player isn't in any team yet
if playerInRound not in self.teamA and playerInRound not in self.teamB:
playerInRound.active = True
if playerInRound.team == self.teamAId:
self.teamA += [playerInRound]
else:
self.teamB += [playerInRound]
## Case 2: Player is in the wrong team
if playerInRound not in self._teamFromId(playerInRound.team):
index = self._teamFromId(playerInRound.team, inverted=True).index(playerInRound)
playerInEventSeries = self._teamFromId(playerInRound.team, inverted=True)[index]
# update playtime #
playerInEventSeries.active = False
playerInEventSeries.activeTime += e.timestamp - playerInEventSeries.timestamp
# add player to correct team #
playerInRound.active = True
if playerInRound.team == self.teamAId:
self.teamA += [playerInRound]
else:
self.teamB += [playerInRound]
## Case 3: Player is already in the correct team
else:
index = self._teamFromId(playerInRound.team).index(playerInRound)
playerInEventSeries = self._teamFromId(playerInRound.team)[index]
# update playtime #
if playerInEventSeries.active:
playerInEventSeries.activeTime += e.timestamp - playerInEventSeries.timestamp
playerInEventSeries.timestamp = e.timestamp
playerInEventSeries.active = True
# mark all missing players as inactive and update their play times #
for playerInEventSeries in self.teamA + self.teamB:
if playerInEventSeries not in e.players:
# update playtime #
playerInEventSeries.active = False
playerInEventSeries.activeTime += e.timestamp - playerInEventSeries.timestamp
elif type(e) == WinnerInformationEvent:
self.winnerTeamId = int(e.winner)
self.winnerTeam = self._teamFromId(self.winnerTeamId)
self.loserTeam = self._teamFromId(self.winnerTeamId, inverted=True)
elif type(e) == MapInformationEvent:
self.map = e.map
### normalize teamchanges
toBeRemovedFromLosers = [] # cannot change iteable during iteration
toBeRemovedFromWinners = []
for playerInEventSeries in self.winnerTeam:
if playerInEventSeries in self.loserTeam:
# get active time in both teams #
playerLoserTeamIndex = self.loserTeam.index(playerInEventSeries)
loserTeamActiveTime = self.loserTeam[playerLoserTeamIndex].activeTime
winnerTeamActiveTime = playerInEventSeries.activeTime
# substract the smaller active time and mark player #
# to be removed from the team he played less on #
if winnerTeamActiveTime > loserTeamActiveTime:
toBeRemovedFromLosers += [playerInEventSeries]
playerInEventSeries.activeTime - loserTeamActiveTime
else:
toBeRemovedFromWinners += [playerInEventSeries]
self.loserTeam[playerLoserTeamIndex].activeTime -= winnerTeamActiveTime
# after iteration actually remove the players #
for player in toBeRemovedFromWinners:
self.winnerTeam.remove(player)
for player in toBeRemovedFromLosers:
self.loserTeam.remove(player)
def _teamFromId(self, teamId, inverted=False):
'''Return the attribute array representing the teamId in this event series
or a dummy array for observers and no-team
Inverted: return the team NOT beloning to the teamId'''
if inverted:
teamId = ( teamId + 1 ) % 2 +2 # 2 => 3 and 3 => 2, 0/1 don't matter
if teamId == OBSERVERS or teamId == NO_TEAM:
return []
elif teamId == 2:
return self.teamA;
elif teamId == 3:
return self.teamB;
else:
errorMsg = "TeamID must be 0 - NoTeam, 1 - Observers, 2 - Security or 3 - Insurgent, but was {}"
raise ValueError(errorMsg.format(teamId))

View File

@@ -0,0 +1,41 @@
# insurgency specific
import StorrageBackend as SB
import TrueSkillWrapper as TS
# general
import Player
import time
from datetime import datetime
def loadCache(cacheFile):
raise NotImplementedError("This backend does not support caching")
def parse(f, exit_on_eof=True, start_at_end=False, cacheFile=None):
last_round_end = None
seek_start = True
round_lines = []
last_line_was_winner = False
lineCount = 0
startTime = datetime.now()
while True:
old_line_nr = f.tell()
line = f.readline()
# if no line or incomplete line, sleep and try again #
if not line or not line.strip("\n"):
if exit_on_eof:
return
time.sleep(5000)
continue
elif not line.endswith("\n"):
f.seek(old_line_nr)
time.sleep(5000)
continue
players = line.strip("\n").split("|")
playerObjects = [Player.PlayerForDatabase(pname, pname, TS.newRating()) for pname in players]
# get ratings if there are any yet #
SB.sync_from_database(playerObjects)
TS.rate_ffa(playerObjects)

View File

@@ -0,0 +1,85 @@
#!/usr/bin/python3
from trueskill import TrueSkill, Rating
env = TrueSkill(draw_probability=0, mu=1500, sigma=833, tau=40, backend='mpmath')
env.make_as_global()
#####################################################
################ HANDLE RATING INPUT ################
#####################################################
def evaluateRound(r):
# do no rate rounds that were too imbalanced #
if r.pt_difference() >= 2.1 or r.pt_difference() == 0:
raise ValueError("Teams too imbalanced: {} (zero=inifinity)".format(r.pt_difference()))
weights = r.normalized_playtimes()
# trueskill need groups = [ { key : rating, ... } , { key : rating, ... } ]
# --------------- ---------------
# Team 1 (winners) Team 2 (losers)
groups=[dict(), dict()]
for playerInDatabase in r.winners:
groups[0].update( { playerInDatabase : playerInDatabase.rating } )
for playerInDatabase in r.losers:
groups[1].update( { playerInDatabase : playerInDatabase.rating } )
if len(groups[1]) == 0 or len(groups[0]) ==0 :
raise ValueError("One of the rated Teams was empty")
rated = env.rate(groups, weights=weights)
return rated
#def rate_ffa(players):
# '''Takes a list of players in reverse finishing order (meaning best first)
# perform a truskill evaluation and write it to database'''
#
# # one player doesnt make sense #
# if len(players) <= 1:
# return False
#
# # create list of dicts for trueskill-library #
# playerRatingTupelDicts = []
# for p in players:
# playerRatingTupelDicts += [{p:p.rating}]
#
# # generate ranks
# ranks = [ i for i in range(0, len(playerRatingTupelDicts))]
#
# # rate and safe to database #
# rated = env.rate(playerRatingTupelDicts)
#
# # create sync dict #
# # first player is handled seperately #
# allPlayer = dict()
# for playerRatingDict in rated[1:]:
# allPlayer.update(playerRatingDict)
#
# # only first player gets win #
# StorrageBackend.sync_to_database(rated[0], True)
# StorrageBackend.sync_to_database(allPlayer, False)
#
# for p in allPlayer.keys():
# print(p)
#####################################################
################### LOCK/GETTER ####################
#####################################################
def newRating(mu=None, sigma=None):
if mu:
return Rating(mu=mu, sigma=env.sigma)
elif mu and sigma:
return Rating(mu=mu, sigma=sigma)
else:
return env.create_rating()
def getEnviroment():
return env
def balance(players, buddies=None):
raise NotImplementedError()
def get_player_rating(sid, name="NOTFOUND"):
raise NotImplementedError()