Skip to content

Commit

Permalink
Merge pull request #582 from aiarena/staging
Browse files Browse the repository at this point in the history
Release v1.10.5
  • Loading branch information
lladdy authored May 18, 2023
2 parents 043e876 + dcb4c8d commit 3944132
Show file tree
Hide file tree
Showing 15 changed files with 114 additions and 39 deletions.
4 changes: 2 additions & 2 deletions aiarena/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 43 additions & 30 deletions aiarena/core/api/bot_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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']
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion aiarena/core/management/commands/generatestats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions aiarena/core/management/commands/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
2 changes: 1 addition & 1 deletion aiarena/core/migrations/0068_auto_20230116_0309.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
),
]
2 changes: 2 additions & 0 deletions aiarena/core/models/competition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions aiarena/core/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ def get_absolute_url(self):
def as_html_link(self):
return mark_safe('<a href="{0}">{1}</a>'.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'<a href="{self.get_absolute_url}">{(name[:limit-3] + "...") if len(name) > limit else name}</a>')

BOTS_LIMIT_MAP = {
"none": config.MAX_USER_BOT_PARTICIPATIONS_ACTIVE_FREE_TIER,
"bronze": config.MAX_USER_BOT_PARTICIPATIONS_ACTIVE_BRONZE_TIER,
Expand Down
2 changes: 2 additions & 0 deletions aiarena/core/tests/testing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions aiarena/frontend/templates/bot.html
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,11 @@
<td>--</td>
{% endif %}
<td>{{ competition_participation.elo }}</td>
{% if competition_participation.competition.indepth_bot_statistics_enabled %}
<td><a href="{% url 'bot_competition_stats' competition_participation.id competition_participation.slug %}">Stats</a></td>
{% else %}
<td>--</td>
{% endif %}
</tr>
{% endfor %}
{% else %}
Expand Down
7 changes: 5 additions & 2 deletions aiarena/frontend/templates/competition.html
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@
<td><strong>Type</strong></td>
<td><strong>Win %</strong></td>
<td><strong>ELO</strong></td>
{% if competition.indepth_bot_statistics_enabled %}
<td></td>
{% endif %}
</tr>
</thead>
<tbody>
Expand All @@ -112,7 +114,7 @@
{{ participant.bot.as_truncated_html_link }}
</td>
<td>{{ participant.bot.plays_race.get_label_display }}</td>
<td>{{ participant.bot.user.as_html_link }}</td>
<td>{{ participant.bot.user.as_truncated_html_link }}</td>
<td>{{ participant.bot.type }}</td>
{% if participant.win_perc %}
<td>{{ participant.win_perc|floatformat:2 }}</td>
Expand Down Expand Up @@ -153,8 +155,9 @@
{% endif %}
{% endif %}
</td>
{% if competition.indepth_bot_statistics_enabled %}
<td><a href="{% url 'bot_competition_stats' participant.id participant.slug %}">Stats</a></td>

{% endif %}
</tr>
{% endfor %}
{% else %}
Expand Down
2 changes: 1 addition & 1 deletion aiarena/frontend/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
{# newly created bots have same update time as its creation time #}
{% if event.is_created_event %}
<td nowrap>{{ event.bot_zip_updated|naturaltime|shorten_naturaltime }}</td>
<td style="text-align: left">{{ event.user.as_html_link }} uploaded a new bot: {{ event.as_truncated_html_link }}.</td>
<td style="text-align: left">{{ event.user.as_truncated_html_link }} uploaded a new bot: {{ event.as_truncated_html_link }}.</td>
{% else %}
<td nowrap>{{ event.bot_zip_updated|naturaltime|shorten_naturaltime }}</td>
<td style="text-align: left">Bot {{ event.as_truncated_html_link }} was updated.</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.16 on 2023-02-19 06:01

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('patreon', '0005_patreonunlinkeddiscorduid'),
]

operations = [
migrations.AddField(
model_name='patreonaccountbind',
name='last_refresh_attempt_current_user_json',
field=models.TextField(blank=True, null=True),
),
]
6 changes: 6 additions & 0 deletions aiarena/patreon/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class PatreonAccountBind(models.Model):
last_token_refresh_failure_message = models.TextField(blank=True, null=True)
"""The exception text provided with the latest refresh failure"""
patreon_user_id = models.CharField(max_length=64, blank=True, null=True)
"""The user id of the patreon user"""
last_refresh_attempt_current_user_json = models.TextField(blank=True, null=True)
"""The JSON returned from Patreon's current_user endpoint on the last refresh attempt."""

def update_tokens(self):
self.last_token_refresh_attempt = timezone.now()
Expand All @@ -40,7 +43,10 @@ def update_tokens(self):

def update_user_patreon_tier(self):
api_client = PatreonApi(self.access_token)

user = api_client.current_user()
self.last_refresh_attempt_current_user_json = str(user)

patreon_level = 'none'
if self.has_pledge(user):
patreon_level = self.get_pledge_reward_name(user, self.get_pledge_reward_id(user)).lower()
Expand Down
5 changes: 3 additions & 2 deletions pip/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ django-filter==22.1
django-private-storage==3.0
django-registration-redux==2.10
django-avatar==5.0.0
djangorestframework==3.11.1
djangorestframework==3.14.0
wheel==0.38.4
psycopg2-binary==2.9.5
pinax-theme-bootstrap==8.0.1
Expand All @@ -21,9 +21,10 @@ django-robots==4.0
django-random-queryset==0.1.3
django-extensions==3.2.1
django-grappelli==2.15.3
docutils==0.18.1
docutils==0.20
drf-yasg==1.21.5
django-redis==4.12.1
django-select2==7.5.0
django-tables2==2.3.4
django-debug-toolbar==3.2.2
django-pglocks==1.0.4

0 comments on commit 3944132

Please sign in to comment.