From 8e9cba5ce9804585e3a6dcde569750fc5e7dee1a Mon Sep 17 00:00:00 2001 From: Kannadan Date: Sun, 16 Jan 2022 12:52:11 +0200 Subject: [PATCH] Player elo calculation update - if game is in current season, only calculate elo for that season - if game is newest, no reason to rollback elo, calculate using current elo - Added column to game table called elo_change - team elo change in games is rounded - added script to sync database with production using open api's - added script to delete all data. --- .gitignore | 1 + delete_data.py | 20 +++++ djangofiles/frisbeer/admin.py | 1 + .../migrations/0035_game_elo_change.py | 23 +++++ djangofiles/frisbeer/models.py | 6 ++ djangofiles/frisbeer/signals.py | 87 +++++++++++++++---- setup_test_database.py | 7 +- sync_database.py | 84 ++++++++++++++++++ 8 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 delete_data.py create mode 100644 djangofiles/frisbeer/migrations/0035_game_elo_change.py create mode 100644 sync_database.py diff --git a/.gitignore b/.gitignore index b02b4e7..79862d0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ db.sqlite3 *.pyc log.txt +.vscode \ No newline at end of file diff --git a/delete_data.py b/delete_data.py new file mode 100644 index 0000000..021949c --- /dev/null +++ b/delete_data.py @@ -0,0 +1,20 @@ +import os +import sys + +''' + Simply deletes all data from tables +''' + +sys.path.append(os.path.join(os.path.dirname(__file__), 'djangofiles')) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', "server.settings") + +import django +django.setup() +from frisbeer.models import * + +Game.objects.all().delete() +Player.objects.all().delete() +Season.objects.all().delete() +Location.objects.all().delete() +GamePlayerRelation.objects.all().delete() +Team.objects.all().delete() diff --git a/djangofiles/frisbeer/admin.py b/djangofiles/frisbeer/admin.py index 7d02871..2439d7e 100644 --- a/djangofiles/frisbeer/admin.py +++ b/djangofiles/frisbeer/admin.py @@ -8,6 +8,7 @@ class PlayerInGameInline(admin.TabularInline): class GameAdmin(admin.ModelAdmin): inlines = [PlayerInGameInline, ] + exclude = ('elo_change', '_rules') def get_changeform_initial_data(self, request): return {'season': Season.current().id } diff --git a/djangofiles/frisbeer/migrations/0035_game_elo_change.py b/djangofiles/frisbeer/migrations/0035_game_elo_change.py new file mode 100644 index 0000000..4c7afbc --- /dev/null +++ b/djangofiles/frisbeer/migrations/0035_game_elo_change.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.14 on 2021-12-27 21:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('frisbeer', '0034_season_rules'), + ] + + operations = [ + migrations.AddField( + model_name='game', + name='elo_change', + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name='season', + name='score_algorithm', + field=models.CharField(choices=[('2017', 'Season 2017'), ('2018', 'Season 2018'), ('elo', 'Elo'), ('top_elo', 'Best elo')], max_length=255), + ), + ] diff --git a/djangofiles/frisbeer/models.py b/djangofiles/frisbeer/models.py index 1bc75d5..647741e 100644 --- a/djangofiles/frisbeer/models.py +++ b/djangofiles/frisbeer/models.py @@ -195,6 +195,7 @@ class Game(models.Model): team1_score = models.IntegerField(default=0, choices=((0, 0), (1, 1), (2, 2))) team2_score = models.IntegerField(default=0, choices=((0, 0), (1, 1), (2, 2))) + elo_change = models.IntegerField(default=0) state = models.IntegerField(choices=game_state_choices, default=PENDING, help_text="0: pending - the game has been proposed but is still missing players. " "1: ready - the game can be played now. Setting this state creates teams. " @@ -230,6 +231,11 @@ def team2(self): def save(self, *args, **kwargs): if not self.season: self.season = Season.current() + # Auto approve games + # if self.id: + # old_game = Game.objects.get(pk=self.id) + # if old_game.state == Game.READY and self.state == Game.PLAYED: + # self.state = Game.APPROVED super().save(*args, **kwargs) def __str__(self): diff --git a/djangofiles/frisbeer/signals.py b/djangofiles/frisbeer/signals.py index 99fc6c2..6439448 100644 --- a/djangofiles/frisbeer/signals.py +++ b/djangofiles/frisbeer/signals.py @@ -20,17 +20,18 @@ def update_statistics(sender, instance, **kwargs): logging.debug("Game was saved, but hasn't been played yet. Sender %s, instance %s", sender, instance) return - update_elo() - update_score() - calculate_ranks() - update_team_score() + update_elo(instance) # 18 seconds + update_score() # 2 seconds + calculate_ranks() # 1 second + update_team_score(instance) # 9 seconds -def update_elo(): +def update_elo(initialiser=None): """ Calculate new elos for all players. - Update is done for all players because matches are possibly added in non-chronological order + Update is done for all players if game that initialized update is from past season. + For current season games, player elos are reverted to the state of the initializer game and recalculated from there """ logging.info("Updating elos (mabby)") @@ -39,9 +40,7 @@ def _elo_decay(): # Halves the distance from median elo for all players Player.objects.all().update(elo=(F('elo') - 1500) / 2 + 1500, season_best=0) - games = Game.objects.filter(state=Game.APPROVED).order_by("date") - - Player.objects.all().update(elo=1500, season_best=0) + games = rollback_player_elo(initialiser) season = None @@ -63,9 +62,10 @@ def _elo_decay(): # We only need to calculate elo change for one team, since elo change is the same for all players # and symmetrical between losing and winning sides - team1_elo_change = (game.team1_score * calculate_elo_change(team1_pregame_elo, team2_pregame_elo, True) - + game.team2_score * calculate_elo_change(team1_pregame_elo, team2_pregame_elo, False)) + team1_elo_change = round((game.team1_score * calculate_elo_change(team1_pregame_elo, team2_pregame_elo, True) + + game.team2_score * calculate_elo_change(team1_pregame_elo, team2_pregame_elo, False))) + game.elo_change = team1_elo_change for player in team1: player.elo += team1_elo_change # logging.debug("{0} elo changed {1:0.2f}".format(player.name, team1_elo_change)) @@ -78,11 +78,64 @@ def _elo_decay(): if player.elo > player.season_best: player.season_best = player.elo player.save() - + Game.objects.bulk_update(games, ["elo_change"]) # New season has begun, but no games yet played -> decay if season != Season.current(): _elo_decay() +def rollback_player_elo(game): + """ + Runs games back in time and reverts elo changes caused by them to players + """ + if game != None and game.season == Season.current(): + games = Game.objects.filter(state=Game.APPROVED).filter(date__gte=game.date).order_by("date") + if len(games) > 1: + games = Game.objects.filter(season_id=Season.current().id, state=Game.APPROVED).order_by("date") + Player.objects.all().update(season_best=0) + for game in reversed(games): + if not game.can_score(): + continue + team1 = [r.player for r in list(game.gameplayerrelation_set.filter(team=1))] + team2 = [r.player for r in list(game.gameplayerrelation_set.filter(team=2))] + + team1_elo_change = game.elo_change * -1 + for player in team1: + old_elo = player.elo + player.elo += team1_elo_change + # logging.debug("{0} elo rollback {1:0.2f}. New: {2}".format(player.name, team1_elo_change, player.elo)) + if old_elo == player.season_best and player.elo < old_elo: + # Only really applicable if only one game is being reversed. Would need individual player history to do proberly + player.season_best = player.elo + player.save() + for player in team2: + player.elo -= team1_elo_change + if old_elo == player.season_best and player.elo < old_elo: + player.season_best = player.elo + # logging.debug("{0} elo rollback {1:0.2f}. New: {2}".format(player.name, -team1_elo_change, player.elo)) + player.save() + return games + else: + Player.objects.all().update(elo=1500, season_best=0) + return Game.objects.filter(state=Game.APPROVED).order_by("date") + +def get_team_games(game): + """ + Gets games for team elo update + If game in question is the newest, just use that, otherwise count whole season elos + """ + season = Season.current() + if game != None: + games = Game.objects.filter(season=season, state=Game.APPROVED).filter(date__gte=game.date).order_by("date") + if len(games) == 1: + teams = GameTeamRelation.objects.filter(game_id=games[0].id) + if len(teams) == 0: + # new game, just do that + return games + games = Game.objects.filter(season=season, state=Game.APPROVED).order_by("date") + Team.objects.filter(virtual=True).delete() + Team.objects.all().update(elo=1500, season_best=0) + return games + def update_score(): logging.info("Updating scores (mabby)") @@ -119,11 +172,10 @@ def update_score(): BACKUP_PENALTY_PERCENT = 22.45 -def update_team_score(): - Team.objects.filter(virtual=True).delete() - Team.objects.all().update(elo=1500, season_best=0) +def update_team_score(initialiser=None): season = Season.current() - games = Game.objects.filter(season=season, state=Game.APPROVED).order_by("date") + + games = get_team_games(initialiser) for game in games: if not game.can_score(): @@ -134,8 +186,7 @@ def update_team_score(): GameTeamRelation.objects.update_or_create(side=1, game=game, defaults={'team': team1}) GameTeamRelation.objects.update_or_create(side=2, game=game, defaults={'team': team2}) - team1_elo_change = (game.team1_score * calculate_elo_change(team1.elo, team2.elo, True) + - game.team2_score * calculate_elo_change(team1.elo, team2.elo, False)) + team1_elo_change = game.elo_change team2_elo_change = -team1_elo_change diff --git a/setup_test_database.py b/setup_test_database.py index 3cc50c2..86369fa 100644 --- a/setup_test_database.py +++ b/setup_test_database.py @@ -22,14 +22,13 @@ player = Player(name=name) player.save() players.add(player) - Game.objects.all().delete() - +playerList = list(players) for i in range(10): g = Game(name="Testipeli {}".format(i), season=Season.current()) g.save() - t1 = set(random.sample(players, 3)) - t2 = random.sample(players - t1, 3) + t1 = set(random.sample(playerList, 3)) + t2 = random.sample(list(players - t1), 3) for player in t1: GamePlayerRelation(player=player, game=g, team=1).save() for player in t2: diff --git a/sync_database.py b/sync_database.py new file mode 100644 index 0000000..6279f58 --- /dev/null +++ b/sync_database.py @@ -0,0 +1,84 @@ +import os +import sys +import random +import requests +import datetime + +from django.db import IntegrityError + +''' + Srcipt to copy games, locations and player stats from production environment using the open api's +''' + +sys.path.append(os.path.join(os.path.dirname(__file__), 'djangofiles')) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', "server.settings") +r = requests.get('http://api.frisbeer.win/API/players') +ExternalPlayers = r.json() +r = requests.get('http://api.frisbeer.win/API/locations') +ExternalLocations = r.json() +r = requests.get('http://api.frisbeer.win/API/games') +ExternalGames = r.json() + +import django + +django.setup() + +from frisbeer.models import * +from django.contrib.auth.models import User +from djangofiles.frisbeer.signals import update_elo, update_score, calculate_ranks, update_team_score + +Player.objects.all().delete() +fields = ('id', 'name', 'score', 'rank', 'season_best') +for player in ExternalPlayers: + temp = Player(name=player["name"], score=1500) + temp.save() + +Game.objects.all().delete() +Season.objects.all().delete() +Location.objects.all().delete() +GamePlayerRelation.objects.all().delete() +Team.objects.all().delete() +year = 2017 +rules = GameRules.objects.get(id=1) +Srules = SeasonRules.objects.get(id=4) +print("starting season loop") +for i in range(datetime.datetime.now().year - year + 1): + start = datetime.datetime(year, 1, 1) + end = datetime.datetime(year, 12, 31) + s = Season(id=i+2, name=year, start_date=start, end_date=end, game_rules=rules, rules=Srules) + s.save() + year = year + 1 +print("starting location loop") +for location in ExternalLocations: + l = Location(id=location["id"], name=location["name"], longitude=location["longitude"], latitude=location["latitude"]) + l.save() + + +print("starting game loop") +for game in ExternalGames: + if game["location"] == None: + loc = None + else: + loc = Location.objects.get(id=game["location"]) + g = Game(name=game["name"], season=Season.objects.get(id=game["season"]), location=loc, date=game["date"], + team1_score=game["team1_score"], team2_score=game["team2_score"], state=game["state"]) + g.save() + t1 = [d for d in game["players"] if d['team'] in [1]] + t2 = [d for d in game["players"] if d['team'] in [2]] + t0 = [d for d in game["players"] if d['team'] in [0]] + for player in t1: + temp = Player.objects.get(name=player["name"]) + GamePlayerRelation(player=temp, game=g, team=1).save() + for player in t2: + temp = Player.objects.get(name=player["name"]) + GamePlayerRelation(player=temp, game=g, team=2).save() + for player in t0: + temp = Player.objects.get(name=player["name"]) + GamePlayerRelation(player=temp, game=g, team=0).save() + +print("updating values") +update_elo() +update_score() +calculate_ranks() +update_team_score() +