Skip to content

Commit

Permalink
feat: Sort highscore leaderboard by recent popularity
Browse files Browse the repository at this point in the history
  • Loading branch information
NicholasBottone committed Oct 5, 2024
1 parent 16077f6 commit 41f8078
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 42 deletions.
84 changes: 54 additions & 30 deletions highscores/views.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
from collections import Counter
from datetime import datetime, timedelta
from typing import Callable, Optional, Type
from discordoauth2.models import User
from django.http.response import HttpResponseRedirect
from django.http import HttpResponse, HttpRequest
from django.shortcuts import render

from django.contrib.auth.decorators import login_required
from django.db.models import Sum, Max
from django.utils.timezone import make_aware
from datetime import datetime
from collections import Counter
from django.db.models import Sum, Max, F, Count, Prefetch
from django.db.models.functions import Coalesce
from django.core.cache import cache
from django.db.models import Count, F, Max, Prefetch, Q, Sum
from django.db.models.functions import Coalesce
from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseRedirect
from django.shortcuts import render
from django.utils import timezone

from discordoauth2.models import User

from .forms import ScoreForm, get_score_form
from .lib import extract_form_data, game_slug_to_submit_func
from .models import Leaderboard, Score
from .forms import ScoreForm, get_score_form

COMBINED_LEADERBOARD_PAGE = "highscores/combined_leaderboard.html"
SUBMIT_PAGE = "highscores/submit.html"
Expand All @@ -24,7 +25,13 @@


def home(request: HttpRequest) -> HttpResponse:
leaderboards = Leaderboard.objects.all().order_by('-id')
leaderboards = Leaderboard.objects.annotate(
score_count=Count(
'score',
filter=Q(score__approved=True) & Q(
score__time_set__gte=timezone.now() - timedelta(days=7))
)
).order_by('-score_count') # Order by most scores set in the last 7 days

# Create a dictionary mapping game name to array of leaderboards
leaderboards_dict = {}
Expand Down Expand Up @@ -72,7 +79,8 @@ def world_records(request: HttpRequest) -> HttpResponse:
# Collect the highest scores for each leaderboard
world_records = []
for leaderboard in leaderboards:
highest_score = Score.objects.filter(leaderboard=leaderboard, approved=True).order_by('-score', 'time_set').first()
highest_score = Score.objects.filter(
leaderboard=leaderboard, approved=True).order_by('-score', 'time_set').first()
if highest_score:
highest_score.robot_name = leaderboard.name # Include robot name
world_records.append(highest_score)
Expand All @@ -81,23 +89,26 @@ def world_records(request: HttpRequest) -> HttpResponse:
world_records.sort(key=lambda x: x.time_set)

# Calculate how long each record has been active
now = make_aware(datetime.now())
now = timezone.now()
for record in world_records:
time_set = record.time_set
active_duration = now - time_set

years, remainder = divmod(active_duration.total_seconds(), 31536000) # 60*60*24*365
years, remainder = divmod(
active_duration.total_seconds(), 31536000) # 60*60*24*365
months, remainder = divmod(remainder, 2592000) # 60*60*24*30
days = remainder / 86400 # 60*60*24

record.active_for = f"{int(years)} years, {int(months)} months, {days:.1f} days"

# Count the number of records per player
player_counts = Counter(record.player.username for record in world_records)
player_counts = sorted(player_counts.items(), key=lambda x: x[1], reverse=True)
player_counts = sorted(player_counts.items(),
key=lambda x: x[1], reverse=True)

return render(request, WR_PAGE, {"world_records": world_records, "player_counts": player_counts})


def leaderboard_combined(request: HttpRequest, game_slug: str) -> HttpResponse:
cache_key = f'leaderboard_combined_{game_slug}'
context = cache.get(cache_key)
Expand All @@ -111,7 +122,8 @@ def leaderboard_combined(request: HttpRequest, game_slug: str) -> HttpResponse:
leaderboards = Leaderboard.objects.filter(game_slug=game_slug)

# Prefetch related scores for all leaderboards
scores_prefetch = Prefetch('score_set', queryset=Score.objects.filter(approved=True).select_related('player'))
scores_prefetch = Prefetch('score_set', queryset=Score.objects.filter(
approved=True).select_related('player'))
leaderboards = leaderboards.prefetch_related(scores_prefetch)

player_percentiles = {}
Expand All @@ -138,20 +150,26 @@ def leaderboard_combined(request: HttpRequest, game_slug: str) -> HttpResponse:
if len(player_percentiles[player_id]) < leaderboards.count():
player_percentiles[player_id].append(0.0)

average_percentiles = {player_id: sum(percentiles) / len(percentiles) for player_id, percentiles in player_percentiles.items()}
sorted_average_percentiles = sorted(average_percentiles.items(), key=lambda x: x[1], reverse=True)
average_percentiles = {player_id: sum(percentiles) / len(percentiles)
for player_id, percentiles in player_percentiles.items()}
sorted_average_percentiles = sorted(
average_percentiles.items(), key=lambda x: x[1], reverse=True)

context = []
i = 1
player_objects = User.objects.filter(id__in=all_players).in_bulk()
for player_id, avg_percentile in sorted_average_percentiles:
player = player_objects[player_id]
total_score = Score.objects.filter(player=player, leaderboard__game_slug=game_slug, approved=True).aggregate(total_score=Sum('score'))['total_score']
last_time_set = Score.objects.filter(player=player, leaderboard__game_slug=game_slug, approved=True).aggregate(last_time_set=Max('time_set'))['last_time_set']
context.append([i, {'player': player, 'average_percentile': avg_percentile, 'score': total_score, 'time_set': last_time_set}])
total_score = Score.objects.filter(player=player, leaderboard__game_slug=game_slug, approved=True).aggregate(
total_score=Sum('score'))['total_score']
last_time_set = Score.objects.filter(player=player, leaderboard__game_slug=game_slug, approved=True).aggregate(
last_time_set=Max('time_set'))['last_time_set']
context.append([i, {'player': player, 'average_percentile': avg_percentile,
'score': total_score, 'time_set': last_time_set}])
i += 1

cache.set(cache_key, {"ls": context, "game_name": game_name}, 300) # Cache for 5 minutes
# Cache for 5 minutes
cache.set(cache_key, {"ls": context, "game_name": game_name}, 300)
return render(request, COMBINED_LEADERBOARD_PAGE, {"ls": context, "game_name": game_name})


Expand All @@ -172,6 +190,7 @@ def submit_form_view(request: HttpRequest, form_class: Type[ScoreForm], submit_f

return render(request, SUBMIT_ACCEPTED_PAGE, {})


def overall_singleplayer_leaderboard(request: HttpRequest) -> HttpResponse:
cache_key = 'overall_singleplayer_leaderboard'
context = cache.get(cache_key)
Expand All @@ -181,7 +200,8 @@ def overall_singleplayer_leaderboard(request: HttpRequest) -> HttpResponse:
leaderboards = Leaderboard.objects.all()

# Prefetch related scores for all leaderboards
scores_prefetch = Prefetch('score_set', queryset=Score.objects.filter(approved=True).select_related('player'))
scores_prefetch = Prefetch('score_set', queryset=Score.objects.filter(
approved=True).select_related('player'))
leaderboards = leaderboards.prefetch_related(scores_prefetch)

player_percentiles = {}
Expand All @@ -208,24 +228,28 @@ def overall_singleplayer_leaderboard(request: HttpRequest) -> HttpResponse:
if len(player_percentiles[player_id]) < leaderboards.count():
player_percentiles[player_id].append(0.0)

average_percentiles = {player_id: sum(percentiles) / len(percentiles) for player_id, percentiles in player_percentiles.items()}
sorted_average_percentiles = sorted(average_percentiles.items(), key=lambda x: x[1], reverse=True)
average_percentiles = {player_id: sum(percentiles) / len(percentiles)
for player_id, percentiles in player_percentiles.items()}
sorted_average_percentiles = sorted(
average_percentiles.items(), key=lambda x: x[1], reverse=True)

context = []
i = 1
player_objects = User.objects.filter(id__in=all_players).in_bulk()
for player_id, avg_percentile in sorted_average_percentiles:
player = player_objects[player_id]
total_score = Score.objects.filter(player=player, approved=True).aggregate(total_score=Sum('score'))['total_score']
last_time_set = Score.objects.filter(player=player, approved=True).aggregate(last_time_set=Max('time_set'))['last_time_set']
context.append([i, {'player': player, 'average_percentile': avg_percentile, 'score': total_score, 'time_set': last_time_set}])
total_score = Score.objects.filter(player=player, approved=True).aggregate(
total_score=Sum('score'))['total_score']
last_time_set = Score.objects.filter(player=player, approved=True).aggregate(
last_time_set=Max('time_set'))['last_time_set']
context.append([i, {'player': player, 'average_percentile': avg_percentile,
'score': total_score, 'time_set': last_time_set}])
i += 1

cache.set(cache_key, {"ls": context}, 300) # Cache for 5 minutes
return render(request, "highscores/overall_singleplayer_leaderboard.html", {"ls": context})



@login_required(login_url='/login')
def submit_form(request: HttpRequest, game_slug: str) -> HttpResponse:
return submit_form_view(request, get_score_form(game_slug), game_slug_to_submit_func[game_slug])
33 changes: 21 additions & 12 deletions ranked/views.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
from django.shortcuts import render, HttpResponseRedirect
from django.db.models import Max, Min, F, Q, Count, ExpressionWrapper, FloatField, Case, When, Value
from django.utils import timezone
from datetime import datetime, timedelta
import math
from datetime import datetime, timedelta

from django.db.models import (Case, Count, ExpressionWrapper, F, FloatField,
Max, Min, Q, Value, When)
from django.db.models.functions import Exp
from django.shortcuts import HttpResponseRedirect, render
from django.utils import timezone

from .models import EloHistory, GameMode, PlayerElo
from .templatetags.rank_filter import mmr_to_rank
from django.db.models.functions import Exp

# Create your views here.


def ranked_home(request):
game_modes = GameMode.objects.annotate(
match_count=Count(
'match',
filter=Q(match__time__gte=timezone.now() - timedelta(days=7))
)
).order_by('-match_count')
).order_by('-match_count') # Order by most matches played in the last 7 days

# Create a dictionary mapping game name to array of game modes
game_dict = {}
Expand All @@ -28,6 +31,7 @@ def ranked_home(request):
context = {'games': game_dict}
return render(request, 'ranked/ranked_home.html', context)


def leaderboard(request, name):
gamemode = GameMode.objects.filter(short_code=name)

Expand All @@ -36,7 +40,8 @@ def leaderboard(request, name):

gamemode = gamemode[0]

players = PlayerElo.objects.filter(game_mode=gamemode, matches_played__gt=20)
players = PlayerElo.objects.filter(
game_mode=gamemode, matches_played__gt=20)
players = players.annotate(
time_delta=ExpressionWrapper(
datetime.now(timezone.utc) - F('last_match_played_time'),
Expand All @@ -45,13 +50,14 @@ def leaderboard(request, name):
)

players = players.annotate(
mmr = ExpressionWrapper(
mmr=ExpressionWrapper(
Case(
# When time_delta > 168
When(
time_delta__gt=168,
then=ExpressionWrapper(
150 * Exp(-0.00175 * (F('time_delta') - Value(168))) + F('elo') - 150,
150 * Exp(-0.00175 * (F('time_delta') - \
Value(168))) + F('elo') - 150,
output_field=FloatField()
)
),
Expand All @@ -66,7 +72,6 @@ def leaderboard(request, name):
)
)


# Get highest and lowest MMR values
highest_mmr = players.aggregate(Max('mmr'))['mmr__max']
lowest_mmr = players.aggregate(Min('mmr'))['mmr__min']
Expand All @@ -81,7 +86,8 @@ def leaderboard(request, name):
})

# Sort players_with_rank by MMR in descending order
players_with_rank = sorted(players_with_rank, key=lambda x: x['player'].mmr, reverse=True)
players_with_rank = sorted(
players_with_rank, key=lambda x: x['player'].mmr, reverse=True)

context = {
'leaderboard_code': gamemode.short_code,
Expand All @@ -91,6 +97,7 @@ def leaderboard(request, name):

return render(request, "ranked/leaderboard.html", context)


def player_info(request, name, player_id):
if not player_id.isdigit():
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/ranked'))
Expand All @@ -106,7 +113,8 @@ def player_info(request, name, player_id):

# Handle cases where last_match_played_time is None
if player.last_match_played_time:
delta_hours = (timezone.now() - player.last_match_played_time).total_seconds() / 3600
delta_hours = (timezone.now() -
player.last_match_played_time).total_seconds() / 3600
else:
# Define a default value or handle as needed
# For example, setting delta_hours to 0 or a specific number
Expand All @@ -122,5 +130,6 @@ def player_info(request, name, player_id):
'elo_history': elo_history, 'match_labels': match_labels}
return render(request, 'ranked/player_info.html', context)


def mmr_calc(elo, matches_played, delta_hours):
return elo * 2 / ((1 + pow(math.e, 1/168 * pow(delta_hours, 0.63))) * (1 + pow(math.e, -0.33 * matches_played)))

0 comments on commit 41f8078

Please sign in to comment.