diff --git a/aiarena/api/views.py b/aiarena/api/views.py index eb4c1c6c..c1097d75 100644 --- a/aiarena/api/views.py +++ b/aiarena/api/views.py @@ -443,10 +443,10 @@ class ResultSerializer(serializers.ModelSerializer): bot2_name = serializers.SerializerMethodField() def get_bot1_name(self, obj): - return obj.match.participants[0].bot.name + return obj.match.participant1.bot.name def get_bot2_name(self, obj): - return obj.match.participants[1].bot.name + return obj.match.participant2.bot.name class Meta: model = Result diff --git a/aiarena/core/api/bot_statistics.py b/aiarena/core/api/bot_statistics.py index c1351c25..9dc6c759 100644 --- a/aiarena/core/api/bot_statistics.py +++ b/aiarena/core/api/bot_statistics.py @@ -7,6 +7,7 @@ import pandas as pd from django.db import connection from django.db.models import Max +from django_pglocks import advisory_lock from pytz import utc from aiarena.core.models import MatchParticipation, CompetitionParticipation, Bot, Map, Match, Result @@ -20,18 +21,29 @@ def update_stats_based_on_result(bot: CompetitionParticipation, result: Result, """This method updates a bot's existing stats based on a single result. This can be done much quicker that regenerating a bot's entire set of stats""" - if result.type not in BotStatistics._ignored_result_types: - bot.lock_me() - BotStatistics._update_global_statistics(bot, result) - BotStatistics._update_matchup_stats(bot, opponent, result) - BotStatistics._update_map_stats(bot, result) + if result.type not in BotStatistics._ignored_result_types and bot.competition.indepth_bot_statistics_enabled: + with advisory_lock(f'stats_lock_{bot.id}') as acquired: + if not acquired: + raise Exception('Could not acquire lock on bot statistics for competition participation ' + + str(bot.id)) + BotStatistics._update_global_statistics(bot, result) + BotStatistics._update_matchup_stats(bot, opponent, result) + BotStatistics._update_map_stats(bot, result) @staticmethod def recalculate_stats(sp: CompetitionParticipation): """This method entirely recalculates a bot's set of stats.""" - BotStatistics._recalculate_global_statistics(sp) - BotStatistics._recalculate_matchup_stats(sp) - BotStatistics._recalculate_map_stats(sp) + + with advisory_lock(f'stats_lock_{sp.id}') as acquired: + if not acquired: + raise Exception('Could not acquire lock on bot statistics for competition participation ' + + str(sp.id)) + + BotStatistics._recalculate_global_statistics(sp) + + if sp.competition.indepth_bot_statistics_enabled: + BotStatistics._recalculate_matchup_stats(sp) + BotStatistics._recalculate_map_stats(sp) # ignore these result types for the purpose of statistics generation _ignored_result_types = ['MatchCancelled', 'InitializationError', 'Error'] @@ -48,28 +60,29 @@ def _recalculate_global_statistics(sp: CompetitionParticipation): match__round__competition=sp.competition ).count() sp.win_perc = sp.win_count / sp.match_count * 100 - sp.loss_count = MatchParticipation.objects.filter(bot=sp.bot, result='loss', - match__round__competition=sp.competition - ).count() - sp.loss_perc = sp.loss_count / sp.match_count * 100 - sp.tie_count = MatchParticipation.objects.filter(bot=sp.bot, result='tie', - match__round__competition=sp.competition - ).count() - sp.tie_perc = sp.tie_count / sp.match_count * 100 - sp.crash_count = MatchParticipation.objects.filter(bot=sp.bot, result='loss', result_cause__in=['crash', - 'timeout', - 'initialization_failure'], - match__round__competition=sp.competition - ).count() - sp.crash_perc = sp.crash_count / sp.match_count * 100 - - sp.highest_elo = MatchParticipation.objects.filter(bot=sp.bot, - match__result__isnull=False, - match__round__competition=sp.competition) \ - .exclude(match__result__type__in=BotStatistics._ignored_result_types) \ - .aggregate(Max('resultant_elo'))['resultant_elo__max'] - - BotStatistics._generate_graphs(sp) + + if sp.competition.indepth_bot_statistics_enabled: + sp.loss_count = MatchParticipation.objects.filter(bot=sp.bot, result='loss', + match__round__competition=sp.competition + ).count() + sp.loss_perc = sp.loss_count / sp.match_count * 100 + sp.tie_count = MatchParticipation.objects.filter(bot=sp.bot, result='tie', + match__round__competition=sp.competition + ).count() + sp.tie_perc = sp.tie_count / sp.match_count * 100 + sp.crash_count = MatchParticipation.objects.filter(bot=sp.bot, result='loss', result_cause__in=['crash', + 'timeout', + 'initialization_failure'], + match__round__competition=sp.competition + ).count() + sp.crash_perc = sp.crash_count / sp.match_count * 100 + + sp.highest_elo = MatchParticipation.objects.filter(bot=sp.bot, + match__result__isnull=False, + match__round__competition=sp.competition) \ + .exclude(match__result__type__in=BotStatistics._ignored_result_types) \ + .aggregate(Max('resultant_elo'))['resultant_elo__max'] + BotStatistics._generate_graphs(sp) sp.save() @staticmethod diff --git a/aiarena/core/management/commands/generatestats.py b/aiarena/core/management/commands/generatestats.py index 4201ed1f..ea661685 100644 --- a/aiarena/core/management/commands/generatestats.py +++ b/aiarena/core/management/commands/generatestats.py @@ -47,7 +47,6 @@ def handle(self, *args, **options): competition.statistics_finalized = True competition.save() for sp in CompetitionParticipation.objects.filter(competition_id=competition.id): - sp.lock_me() self.stdout.write(f'Generating current competition stats for bot {sp.bot_id}...') BotStatistics.recalculate_stats(sp) else: diff --git a/aiarena/core/management/commands/seed.py b/aiarena/core/management/commands/seed.py index 79a77c13..6b5724cd 100644 --- a/aiarena/core/management/commands/seed.py +++ b/aiarena/core/management/commands/seed.py @@ -116,10 +116,13 @@ def run_seed(self, num_acs: int, matches): competition1.target_division_size = 2 competition1.n_placements = 2 competition1.rounds_per_cycle = 1 + competition1.indepth_bot_statistics_enabled = True competition1.save() client.open_competition(competition1.id) competition2 = client.create_competition('Competition 2', 'L', gamemode.id) + competition2.indepth_bot_statistics_enabled = True + competition2.save() client.open_competition(competition2.id) competition3 = client.create_competition('Competition 3 - Terran Only', 'L', gamemode.id, {terran.id}) diff --git a/aiarena/core/migrations/0068_auto_20230116_0309.py b/aiarena/core/migrations/0068_auto_20230116_0309.py index c21ed19e..cab10140 100644 --- a/aiarena/core/migrations/0068_auto_20230116_0309.py +++ b/aiarena/core/migrations/0068_auto_20230116_0309.py @@ -7,7 +7,7 @@ def mark_participated_in_most_recent_round(apps, schema_editor): - for competition in Competition.objects.all(): + for competition in Competition.objects.all().only('id'): last_round = Ladders.get_most_recent_round(competition) if last_round is not None: bot_ids = Match.objects.select_related('match_participation').filter(round=last_round).values('matchparticipation__bot__id').distinct() diff --git a/aiarena/core/migrations/0070_competition_indepth_bot_statistics_enabled.py b/aiarena/core/migrations/0070_competition_indepth_bot_statistics_enabled.py new file mode 100644 index 00000000..3645b477 --- /dev/null +++ b/aiarena/core/migrations/0070_competition_indepth_bot_statistics_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-05-15 12:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0069_auto_20230122_1819'), + ] + + operations = [ + migrations.AddField( + model_name='competition', + name='indepth_bot_statistics_enabled', + field=models.BooleanField(default=True), + ), + ] diff --git a/aiarena/core/models/competition.py b/aiarena/core/models/competition.py index 1c0b722c..622b3389 100644 --- a/aiarena/core/models/competition.py +++ b/aiarena/core/models/competition.py @@ -67,6 +67,8 @@ class Competition(models.Model, LockableModelMixin): """Marks that this competition's statistics have been finalized and therefore cannot be modified.""" competition_finalized = models.BooleanField(default=False) """Marks that this competition has been finalized, and it's round and match data purged.""" + indepth_bot_statistics_enabled = models.BooleanField(default=True) + """Whether to generate and display indepth bot statistics for this competition.""" def __str__(self): return self.name diff --git a/aiarena/core/models/user.py b/aiarena/core/models/user.py index 5e2c1f45..e5e9ff8b 100644 --- a/aiarena/core/models/user.py +++ b/aiarena/core/models/user.py @@ -57,6 +57,12 @@ def get_absolute_url(self): def as_html_link(self): return mark_safe('{1}'.format(self.get_absolute_url, escape(self.__str__()))) + @cached_property + def as_truncated_html_link(self): + name = escape(self.__str__()) + limit = 20 + return mark_safe(f'{(name[:limit-3] + "...") if len(name) > limit else name}') + BOTS_LIMIT_MAP = { "none": config.MAX_USER_BOT_PARTICIPATIONS_ACTIVE_FREE_TIER, "bronze": config.MAX_USER_BOT_PARTICIPATIONS_ACTIVE_BRONZE_TIER, diff --git a/aiarena/core/tests/testing_utils.py b/aiarena/core/tests/testing_utils.py index f4837078..993bd6de 100644 --- a/aiarena/core/tests/testing_utils.py +++ b/aiarena/core/tests/testing_utils.py @@ -143,6 +143,8 @@ def open_competition(self, competition_id: int): 'name': competition.name, # required by the form 'type': competition.type, # required by the form 'game_mode': competition.game_mode_id, # required by the form + # if this isn't set here, it reverts to false - I don't understand why :( + 'indepth_bot_statistics_enabled': competition.indepth_bot_statistics_enabled, } response = self.django_client.post(url, data) diff --git a/aiarena/frontend/templates/bot.html b/aiarena/frontend/templates/bot.html index d32194d7..77790a32 100644 --- a/aiarena/frontend/templates/bot.html +++ b/aiarena/frontend/templates/bot.html @@ -173,7 +173,11 @@