Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

leaderboard: round only the final score + optimizations #1

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 57 additions & 12 deletions web/proboj/games/leaderboard.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
2 changes: 1 addition & 1 deletion web/proboj/games/templates/games/leaderboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ <h2 class="font-semibold text-3xl mb-4">Leaderboard</h2>

<tbody class="divide-y divide-green-800/30 dark:divide-green-800/70">
{% for score in scores %}
<tr class="group even:bg-green-50 dark:even:bg-green-950 {% if score.0.user == user %}text-yellow-400{% endif %}">
<tr class="group even:bg-green-50 dark:even:bg-green-950 {% if score.0.user.username == user.username %}text-yellow-400{% endif %}">
<td class="p-2">
{{ forloop.counter }}.
</td>
Expand Down
77 changes: 54 additions & 23 deletions web/proboj/games/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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


Expand All @@ -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])
],
}
)
Expand Down Expand Up @@ -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])
],
}
)
Expand Down
12 changes: 12 additions & 0 deletions web/proboj/matches/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down