Skip to content

Commit

Permalink
Merge pull request #390 from aiarena/staging
Browse files Browse the repository at this point in the history
Release v1.6.8
  • Loading branch information
lladdy authored Mar 20, 2022
2 parents cfe93f8 + 8fd79dd commit a478efc
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 69 deletions.
1 change: 1 addition & 0 deletions aiarena/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
router.register(r'bots', publicapi_views.BotViewSet, basename='api_bot')
router.register(r'competitions', publicapi_views.CompetitionViewSet, basename='api_competition')
router.register(r'competition-bot-matchup-stats', publicapi_views.CompetitionBotMatchupStatsViewSet, basename='api_competitionbotmatchupstats')
router.register(r'competition-bot-map-stats', publicapi_views.CompetitionBotMapStatsViewSet, basename='api_competitionbotmapstats')
router.register(r'competition-participations', publicapi_views.CompetitionParticipationViewSet, basename='api_competitionparticipation')
router.register(r'discord-users', publicapi_views.DiscordUserViewSet, basename='api_discord_user')
router.register(r'games', publicapi_views.GameViewSet, basename='api_game')
Expand Down
26 changes: 25 additions & 1 deletion aiarena/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from aiarena.api.view_filters import BotFilter, MatchParticipationFilter, ResultFilter, MatchFilter
from aiarena.core.models import Match, Result, Bot, Map, User, Round, MatchParticipation, CompetitionParticipation, \
Competition, MatchTag, Game, GameMode, MapPool, CompetitionBotMatchupStats, News, Trophy
Competition, MatchTag, Game, GameMode, MapPool, CompetitionBotMatchupStats, CompetitionBotMapStats, News, Trophy
from aiarena.core.permissions import IsServiceOrAdminUser

logger = logging.getLogger(__name__)
Expand All @@ -35,6 +35,9 @@
competition_bot_matchup_stats_include_fields = 'bot', 'opponent', 'match_count', 'win_count', 'win_perc', 'loss_count', \
'loss_perc', 'tie_count', 'tie_perc', 'crash_count', 'crash_perc', \
'updated',
competition_bot_map_stats_include_fields = 'bot', 'map', 'match_count', 'win_count', 'win_perc', 'loss_count', \
'loss_perc', 'tie_count', 'tie_perc', 'crash_count', 'crash_perc', \
'updated',
competition_participation_include_fields = 'id', 'competition', 'bot', 'elo', 'match_count', 'win_perc', 'win_count', \
'loss_perc', 'loss_count', 'tie_perc', 'tie_count', 'crash_perc', \
'crash_count', 'elo_graph', 'highest_elo', 'slug', 'active', \
Expand Down Expand Up @@ -213,6 +216,27 @@ class CompetitionBotMatchupStatsViewSet(viewsets.ReadOnlyModelViewSet):
search_fields = competition_bot_matchup_stats_include_fields
ordering_fields = competition_bot_matchup_stats_include_fields

# !ATTENTION! IF YOU CHANGE THE API ANNOUNCE IT TO USERS

class CompetitionBotMapStatsSerializer(serializers.ModelSerializer):
class Meta:
model = CompetitionBotMapStats
fields = competition_bot_map_stats_include_fields

# !ATTENTION! IF YOU CHANGE THE API ANNOUNCE IT TO USERS

class CompetitionBotMapStatsViewSet(viewsets.ReadOnlyModelViewSet):
"""
CompetitionBotMapStats data view
"""
queryset = CompetitionBotMapStats.objects.all()
serializer_class = CompetitionBotMapStatsSerializer

filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = competition_bot_map_stats_include_fields
search_fields = competition_bot_map_stats_include_fields
ordering_fields = competition_bot_map_stats_include_fields


# !ATTENTION! IF YOU CHANGE THE API ANNOUNCE IT TO USERS

Expand Down
34 changes: 34 additions & 0 deletions aiarena/core/migrations/0046_competitionbotmapstats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.2.9 on 2022-03-09 11:48

import aiarena.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('core', '0045_auto_20220222_2343'),
]

operations = [
migrations.CreateModel(
name='CompetitionBotMapStats',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('match_count', models.IntegerField(blank=True, null=True)),
('win_count', models.IntegerField(blank=True, null=True)),
('win_perc', models.FloatField(blank=True, null=True, validators=[aiarena.core.validators.validate_not_nan, aiarena.core.validators.validate_not_inf])),
('loss_count', models.IntegerField(blank=True, null=True)),
('loss_perc', models.FloatField(blank=True, null=True, validators=[aiarena.core.validators.validate_not_nan, aiarena.core.validators.validate_not_inf])),
('tie_count', models.IntegerField(blank=True, null=True)),
('tie_perc', models.FloatField(blank=True, null=True, validators=[aiarena.core.validators.validate_not_nan, aiarena.core.validators.validate_not_inf])),
('crash_count', models.IntegerField(blank=True, null=True)),
('crash_perc', models.FloatField(blank=True, null=True, validators=[aiarena.core.validators.validate_not_nan, aiarena.core.validators.validate_not_inf])),
('updated', models.DateTimeField(default=django.utils.timezone.now)),
('bot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='competition_map_stats', to='core.competitionparticipation')),
('map', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.map')),
],
),
]
1 change: 1 addition & 0 deletions aiarena/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .bot import Bot
from .competition import Competition
from .competition_bot_matchup_stats import CompetitionBotMatchupStats
from .competition_bot_map_stats import CompetitionBotMapStats
from .competition_participation import CompetitionParticipation
from .game import Game
from .game_mode import GameMode
Expand Down
33 changes: 33 additions & 0 deletions aiarena/core/models/competition_bot_map_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import logging

from django.db import models
from django.utils import timezone
from aiarena.core.validators import validate_not_nan, validate_not_inf
from .competition_participation import CompetitionParticipation
from .map import Map

logger = logging.getLogger(__name__)


class CompetitionBotMapStats(models.Model):
bot = models.ForeignKey(CompetitionParticipation, on_delete=models.CASCADE, related_name='competition_map_stats')
map = models.ForeignKey(Map, on_delete=models.CASCADE)
match_count = models.IntegerField(blank=True, null=True)
win_count = models.IntegerField(blank=True, null=True)
win_perc = models.FloatField(blank=True, null=True, validators=[validate_not_nan, validate_not_inf])
loss_count = models.IntegerField(blank=True, null=True)
loss_perc = models.FloatField(blank=True, null=True, validators=[validate_not_nan, validate_not_inf])
tie_count = models.IntegerField(blank=True, null=True)
tie_perc = models.FloatField(blank=True, null=True, validators=[validate_not_nan, validate_not_inf])
crash_count = models.IntegerField(blank=True, null=True)
crash_perc = models.FloatField(blank=True, null=True, validators=[validate_not_nan, validate_not_inf])
updated = models.DateTimeField(default=timezone.now) # populate fields before first save

def save(self, *args, **kwargs):
# update time pre save
self.updated = timezone.now()
# now we call django's save protocol
super(CompetitionBotMapStats, self).save(*args, **kwargs)

def __str__(self):
return str(self.bot) + ' on ' + str(self.map)
151 changes: 95 additions & 56 deletions aiarena/core/stats/stats_generator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io

from datetime import datetime

import matplotlib.dates as mdates
Expand All @@ -9,7 +10,8 @@

from aiarena.core.models import MatchParticipation, CompetitionParticipation, Bot
from aiarena.core.models.competition_bot_matchup_stats import CompetitionBotMatchupStats

from aiarena.core.models.competition_bot_map_stats import CompetitionBotMapStats
from aiarena.core.models.competition import Competition

class StatsGenerator:

Expand Down Expand Up @@ -59,6 +61,7 @@ def update_stats(sp: CompetitionParticipation):
sp.save()

StatsGenerator._update_matchup_stats(sp)
StatsGenerator._update_map_stats(sp)

@staticmethod
def _update_matchup_stats(sp: CompetitionParticipation):
Expand All @@ -72,17 +75,17 @@ def _update_matchup_stats(sp: CompetitionParticipation):
sp)

if matchup_stats.match_count != 0:
matchup_stats.win_count = StatsGenerator._calculate_win_count(cursor, competition_participation, sp)
matchup_stats.win_count = StatsGenerator._calculate_matchup_win_count(cursor, competition_participation, sp)
matchup_stats.win_perc = matchup_stats.win_count / matchup_stats.match_count * 100

matchup_stats.loss_count = StatsGenerator._calculate_loss_count(cursor, competition_participation,
matchup_stats.loss_count = StatsGenerator._calculate_matchup_loss_count(cursor, competition_participation,
sp)
matchup_stats.loss_perc = matchup_stats.loss_count / matchup_stats.match_count * 100

matchup_stats.tie_count = StatsGenerator._calculate_tie_count(cursor, competition_participation, sp)
matchup_stats.tie_count = StatsGenerator._calculate_matchup_tie_count(cursor, competition_participation, sp)
matchup_stats.tie_perc = matchup_stats.tie_count / matchup_stats.match_count * 100

matchup_stats.crash_count = StatsGenerator._calculate_crash_count(cursor, competition_participation,
matchup_stats.crash_count = StatsGenerator._calculate_matchup_crash_count(cursor, competition_participation,
sp)
matchup_stats.crash_perc = matchup_stats.crash_count / matchup_stats.match_count * 100
else:
Expand All @@ -93,29 +96,44 @@ def _update_matchup_stats(sp: CompetitionParticipation):

matchup_stats.save()

@staticmethod
def _update_map_stats(sp: CompetitionParticipation):
competition = Competition.objects.get(id=sp.competition.id)
for map in competition.maps.all():
with connection.cursor() as cursor:
map_stats = CompetitionBotMapStats.objects.select_for_update() \
.get_or_create(bot=sp, map=map)[0]

map_stats.match_count = StatsGenerator._calculate_map_count(cursor, map, sp)

if map_stats.match_count != 0:
map_stats.win_count = StatsGenerator._calculate_map_win_count(cursor, map, sp)
map_stats.win_perc = map_stats.win_count / map_stats.match_count * 100

map_stats.loss_count = StatsGenerator._calculate_map_loss_count(cursor, map, sp)
map_stats.loss_perc = map_stats.loss_count / map_stats.match_count * 100

map_stats.tie_count = StatsGenerator._calculate_map_tie_count(cursor, map, sp)
map_stats.tie_perc = map_stats.tie_count / map_stats.match_count * 100

map_stats.crash_count = StatsGenerator._calculate_map_crash_count(cursor, map, sp)
map_stats.crash_perc = map_stats.crash_count / map_stats.match_count * 100
else:
map_stats.win_count = 0
map_stats.loss_count = 0
map_stats.tie_count = 0
map_stats.crash_count = 0

map_stats.save()

@staticmethod
def _run_single_column_query(cursor, query, params):
cursor.execute(query, params)
row = cursor.fetchone()
return row[0]

@staticmethod
def _calculate_matchup_count(cursor, competition_participation, sp):
return StatsGenerator._run_single_column_query(cursor, """
select count(cm.id) as count
from core_match cm
inner join core_matchparticipation bot_p on cm.id = bot_p.match_id
inner join core_matchparticipation opponent_p on cm.id = opponent_p.match_id
inner join core_round cr on cm.round_id = cr.id
inner join core_competition cs on cr.competition_id = cs.id
where cs.id = %s -- make sure it's part of the current competition
and bot_p.bot_id = %s
and opponent_p.bot_id = %s
and bot_p.result is not null and bot_p.result != 'none' -- make sure it's a finished match wih a result
""", [sp.competition_id, sp.bot_id, competition_participation.bot_id])

@staticmethod
def _calculate_win_count(cursor, competition_participation, sp):
def _calculate_matchup_data(cursor, competition_participation, sp, query):
return StatsGenerator._run_single_column_query(cursor, """
select count(cm.id) as count
from core_match cm
Expand All @@ -126,54 +144,75 @@ def _calculate_win_count(cursor, competition_participation, sp):
where cs.id = %s -- make sure it's part of the current competition
and bot_p.bot_id = %s
and opponent_p.bot_id = %s
and bot_p.result = 'win'
""", [sp.competition_id, sp.bot_id, competition_participation.bot_id])
and """ + query,
[sp.competition_id, sp.bot_id, competition_participation.bot_id])

@staticmethod
def _calculate_loss_count(cursor, competition_participation, sp):
return StatsGenerator._run_single_column_query(cursor, """
select count(cm.id) as count
from core_match cm
inner join core_matchparticipation bot_p on cm.id = bot_p.match_id
inner join core_matchparticipation opponent_p on cm.id = opponent_p.match_id
inner join core_round cr on cm.round_id = cr.id
inner join core_competition cs on cr.competition_id = cs.id
where cs.id = %s -- make sure it's part of the current competition
and bot_p.bot_id = %s
and opponent_p.bot_id = %s
and bot_p.result = 'loss'
""", [sp.competition_id, sp.bot_id, competition_participation.bot_id])
def _calculate_matchup_count(cursor, competition_participation, sp):
return StatsGenerator._calculate_matchup_data(cursor, competition_participation, sp,
"bot_p.result is not null and bot_p.result != 'none'")

@staticmethod
def _calculate_tie_count(cursor, competition_participation, sp):
return StatsGenerator._run_single_column_query(cursor, """
select count(cm.id) as count
from core_match cm
inner join core_matchparticipation bot_p on cm.id = bot_p.match_id
inner join core_matchparticipation opponent_p on cm.id = opponent_p.match_id
inner join core_round cr on cm.round_id = cr.id
inner join core_competition cs on cr.competition_id = cs.id
where cs.id = %s -- make sure it's part of the current competition
and bot_p.bot_id = %s
and opponent_p.bot_id = %s
and bot_p.result = 'tie'
""", [sp.competition_id, sp.bot_id, competition_participation.bot_id])
def _calculate_matchup_win_count(cursor, competition_participation, sp):
return StatsGenerator._calculate_matchup_data(cursor, competition_participation, sp,
"bot_p.result = 'win'")

@staticmethod
def _calculate_matchup_loss_count(cursor, competition_participation, sp):
return StatsGenerator._calculate_matchup_data(cursor, competition_participation, sp,
"bot_p.result = 'loss'")

@staticmethod
def _calculate_matchup_tie_count(cursor, competition_participation, sp):
return StatsGenerator._calculate_matchup_data(cursor, competition_participation, sp,
"bot_p.result = 'tie'")

@staticmethod
def _calculate_matchup_crash_count(cursor, competition_participation, sp):
return StatsGenerator._calculate_matchup_data(cursor, competition_participation, sp,
"""bot_p.result = 'loss'
and bot_p.result_cause in ('crash', 'timeout', 'initialization_failure')""")

@staticmethod
def _calculate_crash_count(cursor, competition_participation, sp):
def _calculate_map_data(cursor, map, sp, query):
return StatsGenerator._run_single_column_query(cursor, """
select count(cm.id) as count
from core_match cm
inner join core_matchparticipation bot_p on cm.id = bot_p.match_id
inner join core_matchparticipation opponent_p on cm.id = opponent_p.match_id
inner join core_map map on cm.map_id = map.id
inner join core_round cr on cm.round_id = cr.id
inner join core_competition cs on cr.competition_id = cs.id
where cs.id = %s -- make sure it's part of the current competition
and map.id = %s
and bot_p.bot_id = %s
and opponent_p.bot_id = %s
and bot_p.result = 'loss'
and bot_p.result_cause in ('crash', 'timeout', 'initialization_failure')
""", [sp.competition_id, sp.bot_id, competition_participation.bot_id])
and """ + query,
[sp.competition_id, map.id, sp.bot_id])

@staticmethod
def _calculate_map_count(cursor, map, sp):
return StatsGenerator._calculate_map_data(cursor, map, sp,
"bot_p.result is not null and bot_p.result != 'none'")

@staticmethod
def _calculate_map_win_count(cursor, map, sp):
return StatsGenerator._calculate_map_data(cursor,map, sp,
"bot_p.result = 'win'")

@staticmethod
def _calculate_map_loss_count(cursor, map, sp):
return StatsGenerator._calculate_map_data(cursor,map, sp,
"bot_p.result = 'loss'")

@staticmethod
def _calculate_map_tie_count(cursor, map, sp):
return StatsGenerator._calculate_map_data(cursor,map, sp,
"bot_p.result = 'tie'")

@staticmethod
def _calculate_map_crash_count(cursor, map, sp):
return StatsGenerator._calculate_map_data(cursor,map, sp,
"""bot_p.result = 'loss'
and bot_p.result_cause in ('crash', 'timeout', 'initialization_failure')""")

@staticmethod
def _get_data(bot_id, competition_id):
Expand Down Expand Up @@ -237,7 +276,7 @@ def _generate_plot_images(df, update_date: datetime):
legend.append('Last bot update')
ax1.legend(legend, loc='lower center', fontsize='xx-large')
plt.savefig(plot2, format="png", transparent=True)
plt.cla() # Clears axis in preparation for new graph
plt.close(fig)
return plot1, plot2

@staticmethod
Expand Down
4 changes: 2 additions & 2 deletions aiarena/frontend/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,11 @@ a:hover {
padding: 0;
}

#bot_competition_stats_matchups {
#bot_competition_stats_list {
margin: 0 0 0 73px;
}

#bot_competition_stats_matchups table {
#bot_competition_stats_list table {
margin: 0;
padding: 0;
}
Expand Down
Loading

0 comments on commit a478efc

Please sign in to comment.