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):