mirror of
https://github.com/FAUSheppy/skillbird
synced 2025-12-06 06:51:34 +01:00
2020 rewrite
This commit is contained in:
99
python/backends/database.py
Normal file
99
python/backends/database.py
Normal 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
|
||||
67
python/backends/entities/Players.py
Normal file
67
python/backends/entities/Players.py
Normal 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)
|
||||
219
python/backends/eventStream.py
Normal file
219
python/backends/eventStream.py
Normal 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))
|
||||
41
python/backends/genericFFA.py
Normal file
41
python/backends/genericFFA.py
Normal 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)
|
||||
85
python/backends/trueskillWrapper.py
Normal file
85
python/backends/trueskillWrapper.py
Normal 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()
|
||||
Reference in New Issue
Block a user