diff --git a/web/proboj/games/leaderboard.py b/web/proboj/games/leaderboard.py index a311c30..96cccb2 100644 --- a/web/proboj/games/leaderboard.py +++ b/web/proboj/games/leaderboard.py @@ -1,10 +1,13 @@ from collections import defaultdict from django.core.cache import cache +from django.db.models import Prefetch from django.utils import timezone +from proboj.bots.models import Bot, BotVersion from proboj.games.models import Game from proboj.matches.models import Match, MatchBot +from proboj.users.models import User def get_leaderboard(game: Game): @@ -16,27 +19,69 @@ def get_leaderboard(game: Game): Match.objects.filter(game=game, finished_at__isnull=False, failed=False) .order_by("finished_at") .prefetch_related( - "matchbot_set", - "matchbot_set__bot_version", - "matchbot_set__bot_version__bot", - "matchbot_set__bot_version__bot__user", + Prefetch( + "matchbot_set", + queryset=MatchBot.objects.filter(score__gt=0) + .order_by() + .only("score", "match_id", "bot_version"), + ), + Prefetch( + "matchbot_set__bot_version", + queryset=BotVersion.objects.filter().order_by().only("bot_id"), + ), + Prefetch( + "matchbot_set__bot_version__bot", + queryset=Bot.objects.filter().order_by().only("name", "user_id"), + ), + Prefetch( + "matchbot_set__bot_version__bot__user", + queryset=User.objects.filter() + .order_by() + .only("username", "first_name", "last_name"), + ), ) ) if game.score_reset_at and game.score_reset_at < timezone.now(): matches = matches.filter(finished_at__gte=game.score_reset_at) - scores = defaultdict(lambda: 0) + matches = list( + matches.values_list( + "id", + "matchbot__bot_version__bot_id", + "matchbot__score", + "matchbot__bot_version__bot__name", + "matchbot__bot_version__bot__user__username", + "matchbot__bot_version__bot__user__first_name", + "matchbot__bot_version__bot__user__last_name", + ) + ) + + scores = defaultdict(lambda: 0.0) + bots = {} + + last_id = matches[0][0] + for match in matches: - for k in scores.keys(): - scores[k] = round(scores[k] * 0.999) + if match[0] != last_id: + last_id = match[0] + for k in scores.keys(): + scores[k] *= 0.999 + + if match[2] is not None: + if match[1] not in bots: + bots[match[1]] = { + "name": match[3], + "user": { + "username": match[4], + "first_name": match[5], + "last_name": match[6], + }, + } - for mbot in match.matchbot_set.all(): - mbot: MatchBot - if mbot.score is not None: - scores[mbot.bot_version.bot] += mbot.score + scores[match[1]] += match[2] - leaderboard = list(scores.items()) + leaderboard = [(bots[x[0]], round(x[1])) for x in scores.items()] leaderboard.sort(key=lambda x: -x[1]) cache.set(key, leaderboard, 60 * 5) return leaderboard diff --git a/web/proboj/games/templates/games/leaderboard.html b/web/proboj/games/templates/games/leaderboard.html index b19ea8f..fbf1148 100644 --- a/web/proboj/games/templates/games/leaderboard.html +++ b/web/proboj/games/templates/games/leaderboard.html @@ -27,7 +27,7 @@

Leaderboard

{% for score in scores %} - + {{ forloop.counter }}. diff --git a/web/proboj/games/views.py b/web/proboj/games/views.py index 0e658d5..7616502 100644 --- a/web/proboj/games/views.py +++ b/web/proboj/games/views.py @@ -3,6 +3,7 @@ from datetime import datetime from django.conf import settings +from django.db.models import Prefetch from django.http import HttpResponseRedirect, JsonResponse from django.urls import reverse from django.utils import timezone @@ -11,12 +12,13 @@ from django.views import View from django.views.decorators.cache import cache_page from django.views.generic import DetailView, ListView, TemplateView +from django.views.generic import DetailView, ListView, TemplateView -from proboj.bots.models import Bot +from proboj.bots.models import Bot, BotVersion from proboj.games.leaderboard import get_leaderboard from proboj.games.mixins import GameMixin from proboj.games.models import Game, Page -from proboj.matches.models import Match +from proboj.matches.models import Match, MatchBot class HomeView(ListView): @@ -73,10 +75,11 @@ def get_context_data(self, **kwargs): reverse("game_autoplay", kwargs={"game": self.game.id}) + f"?since={match.finished_at.timestamp()}" ) - ctx[ - "observer" - ] = f"{settings.OBSERVER_URL}/{self.game.id}/?" + urllib.parse.urlencode( - {"file": observer_file, "autoplay": "1", "back": return_url} + ctx["observer"] = ( + f"{settings.OBSERVER_URL}/{self.game.id}/?" + + urllib.parse.urlencode( + {"file": observer_file, "autoplay": "1", "back": return_url} + ) ) return ctx @@ -94,29 +97,55 @@ def get_context_data(self, **kwargs): def get_scores_and_timestamps(game, bots, scale=True): timestamps = [] - datapoints: dict[int, list[int]] = defaultdict(lambda: []) - total_score = defaultdict(lambda: 0) + datapoints: dict[int, list[float]] = defaultdict(lambda: [0.0]) matches = ( Match.objects.filter(game=game, is_finished=True, failed=False) - .prefetch_related("matchbot_set", "matchbot_set__bot_version") + .prefetch_related( + Prefetch( + "matchbot_set", + queryset=MatchBot.objects.filter(score__gt=0) + .order_by() + .only("score", "match_id", "bot_version"), + ), + Prefetch( + "matchbot_set__bot_version", + queryset=BotVersion.objects.filter().order_by().only("bot_id"), + ), + ) .order_by("finished_at") + .only("finished_at") ) + if game.score_reset_at and game.score_reset_at < timezone.now(): matches = matches.filter(finished_at__gte=game.score_reset_at) + + multiplier = 0.999 if scale else 1 + + matches = list( + matches.values( + "id", + "finished_at", + "matchbot__bot_version__bot_id", + "matchbot__score", + ) + ) + + last_id = matches[0]["id"] + timestamps.append(matches[0]["finished_at"].strftime("%Y-%m-%d %H:%M:%S.%f")) + for match in matches: - timestamps.append(match.finished_at.strftime("%Y-%m-%d %H:%M:%S.%f")) - for bot in bots: - if scale: - total_score[bot.id] = round(total_score[bot.id] * 0.999) - datapoints[bot.id].append(total_score[bot.id]) - - for bot in match.matchbot_set.all(): - if not bot.score: - continue - bot_id = bot.bot_version.bot_id - total_score[bot_id] += bot.score - datapoints[bot_id][-1] = total_score[bot_id] + if match["id"] != last_id: + for bot in bots: + datapoints[bot.id].append(datapoints[bot.id][-1] * multiplier) + timestamps.append(match["finished_at"].strftime("%Y-%m-%d %H:%M:%S.%f")) + last_id = match["id"] + + if match["matchbot__score"] is not None: + datapoints[match["matchbot__bot_version__bot_id"]][-1] += match[ + "matchbot__score" + ] + return datapoints, timestamps @@ -134,7 +163,8 @@ def get(self, request, *args, **kwargs): "type": "line", "symbol": "none", "data": [ - [timestamps[i], d] for i, d in enumerate(datapoints[bot.id]) + [timestamps[i], round(d)] + for i, d in enumerate(datapoints[bot.id]) ], } ) @@ -167,7 +197,8 @@ def get(self, request, *args, **kwargs): "type": "line", "symbol": "none", "data": [ - [timestamps[i], d] for i, d in enumerate(derivations[bot.id]) + [timestamps[i], round(d, 2)] + for i, d in enumerate(derivations[bot.id]) ], } ) diff --git a/web/proboj/matches/admin.py b/web/proboj/matches/admin.py index a011cf6..7f85e8f 100644 --- a/web/proboj/matches/admin.py +++ b/web/proboj/matches/admin.py @@ -6,6 +6,18 @@ class MatchBotInline(admin.TabularInline): model = MatchBot + # select related bot, as it is fetched for each BotVersion + # (to retrieve BotVersion.__str__) and this is called for every + # match bot in match admin + def get_formset(self, request, obj=None, **kwargs): + formset = super(MatchBotInline, self).get_formset(request, obj, **kwargs) + + queryset = formset.form.base_fields["bot_version"].queryset + queryset = queryset.select_related("bot") + formset.form.base_fields["bot_version"].queryset = queryset + + return formset + @admin.action(description="Enqueue selected matches") def enqueue_match(modeladmin, request, queryset):