diff --git a/highscores/views.py b/highscores/views.py index 7e9556d..545af0e 100644 --- a/highscores/views.py +++ b/highscores/views.py @@ -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" @@ -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 = {} @@ -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) @@ -81,12 +89,13 @@ 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 @@ -94,10 +103,12 @@ def world_records(request: HttpRequest) -> HttpResponse: # 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) @@ -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 = {} @@ -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}) @@ -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) @@ -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 = {} @@ -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]) diff --git a/ranked/views.py b/ranked/views.py index c5f484e..efa1899 100644 --- a/ranked/views.py +++ b/ranked/views.py @@ -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 = {} @@ -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) @@ -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'), @@ -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() ) ), @@ -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'] @@ -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, @@ -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')) @@ -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 @@ -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)))