diff --git a/Makefile b/Makefile index b6a55edce75790912f8cf2a2db08c48768f90b3c..b5f80cd6a3145449f796c53a5bfd1f73572c5689 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ reset-db: flask --app aocp init-db .PHONY: test -test: test-validation test-aoc-py test-verifier +test: test-validation test-aoc-py test-verifier test-score-py .PHONY: test-validation test-validation: @@ -35,3 +35,7 @@ test-validation: .PHONY: test-aoc-py test-aoc-py: flask --app aocp --debug test-aoc-py + +.PHONY: test-score-py +test-score-py: + flask --app aocp --debug test-score-py diff --git a/aocp/__init__.py b/aocp/__init__.py index 195e0ddf3c5c5f9d5dd2f5d5ea8471af8ef0426b..bc803f38a1ffc7e9607ce483b80a14925c97e677 100644 --- a/aocp/__init__.py +++ b/aocp/__init__.py @@ -2,6 +2,8 @@ from . import db from . import auth from . import verifier from . import aoc +from . import score + import os import json @@ -100,4 +102,7 @@ def create_app(test_config=None): # Initialize verifier verifier.init_app(app) + # Initialize score + score.init_app(app) + return app diff --git a/aocp/aoc.py b/aocp/aoc.py index 5337ba94b83dbb105559a0f70f996ceb04167a58..30eed426e241721c1261fa4138bf58798760f077 100755 --- a/aocp/aoc.py +++ b/aocp/aoc.py @@ -1,10 +1,10 @@ -from typing import Literal +from typing import Tuple, cast import dateparser -from datetime import date, datetime +from datetime import datetime, timezone +from zoneinfo import ZoneInfo from enum import Enum from flask import current_app import requests -from flask import app import re import click import requests @@ -102,3 +102,27 @@ def init_app(app): Gets called by __init__.py on initialization of the flask App. """ app.cli.add_command(test_aoc_py_command) + +def get_aoc_day(timestamp : datetime = datetime.now(tz=timezone.utc), year : int|None = None)-> Tuple[int, int]: + """ + Returns a tuple: + - The current AOC Day (1-25), or 0 if the event hasn't started yet, or 26 if the event is already over. + - The hour (0-23) of the AOC Day of the timestamp. + + Parameters: + timestamp -- Overwrite the "current" timestamp. Must be timezone aware. + year -- Overwrite the AOC year. If None the YEAR from the config is used. + """ + timestamp = timestamp.astimezone(ZoneInfo("EST")) # convert to EST time + hour = timestamp.hour + if year is None: + year = cast(int, current_app.config["YEAR"]) + if timestamp.year > year: + return 26, hour + if timestamp.year < year: + return 0, hour + if timestamp.month < 12: + return 0, hour + return min(timestamp.day, 26), hour + + diff --git a/aocp/score.py b/aocp/score.py index 63b55b1db5d33e2803c893b482735008f25bf84b..30097c3292af4dbd94336e2332e3cb242653061f 100644 --- a/aocp/score.py +++ b/aocp/score.py @@ -1,48 +1,147 @@ from datetime import datetime -from zoneinfo import ZoneInfo +from . import db as database +import click +from . import aoc +from flask import current_app +from . import db as database +from . import verifier +from typing import Tuple, cast -# timedelta is hour after conversion! -# return 0 points if not current day - -def calculate_points(day:int, timedelta:int, is_first:bool)->int: +def calculate_points(day:int, timestamp:datetime, is_first:bool)->int: """ Calculates the points based on day, time and if the solver was first. - - + Parameters: - day -- Day of the solution. - timedelta -- Hour of the solution. - is_first -- Boolean (True if person was first to solve problem). - + day -- Day that the solution was submitted for. + timestamp -- Actual timestamp of the solution. + is_first -- Boolean (True if person was first to solve problem on the day). + Returns: - score -- the score + score -- the score or zero if the submission was made on the wrong day or outside of the event """ - # get "base points" depending on day - base_points = 1+ day // 5 # base points increase by 1 every 5 days. - # bonus time points: (all ranges inclusive) - # 0-8: 2 points - # 8-16: 1 point - # 16-24: 0 points - bonus_time_points = (24-timedelta) // 8 - if is_first: # bonus point if person is first to solve on given day. - bonus_first = 1 - else: - bonus_first = 0 - score = base_points + bonus_time_points + bonus_first + sub_day, sub_hour = aoc.get_aoc_day(timestamp) + + if sub_day != day or sub_day <= 0 or sub_day >= 26: + return 0 # Submission was made on the wrong day or outside of the event + + score = 0 + + # base points by day: (all ranges inclusive) + # 1-5: 1 point + # 6-10: 2 points + # 11-15: 3 points + # 16-20: 4 points + # 21-25: 5 points + score += (day-1) // 5 + + # bonus time points by hour since start of day: (all ranges inclusive) + # 0-7: 2 points + # 8-15: 1 point + # 16-23: 0 points + score += 2 - (sub_hour // 8) + + # bonus point if person is first to solve on given day. + if is_first: + score += 1 + return score - -def is_first_solve(sub_id:tuple[int,int,int],day:int): - # incomplete function - first = get_first(day) - return sub_id == first - -def get_datetime_info(timestamp:datetime): - conv_time = timestamp.astimezone(ZoneInfo("EST")) # convert to EST time - day = conv_time.day - hour = conv_time.hour - return day,hour - -def get_first(day): - # missing function - pass - # query all "CORRECT"s of <DAY> and return the SUBMISSION ID first one + +def get_first(day:int)->str|None: + """ + takes an AOC day (1-25) as an integer and checks who was the first to solve the puzzle on that day. + returns: the userid of the first solver or None if there are no solvers for the given day. + """ + corrects = database.get_db().fetch_submissions(status="CORRECT",day=day) + if corrects.empty: + return None + corrects = corrects.sort_values(by="timestamp") + userid = str(corrects.head(1).index[0][0]) + return userid + +def get_leaderboard(groupid:str|None = None)->Tuple[dict,list[dict]]: + """ + Get the compiled data for building the leaderboard. + + Is a user has made no relevant correct submissions, they will not appear in the result. + + Parameters: + groupid -- If set, only fetch the data for building the leaderboard for this group. + + Returns a tuple: + - A dict of the solves and score for each person: + { + uid: { + uid:str + total:int + days:{ + day:{first:bool,score:int,solved:bool} + } + } + } + - The leaderboard as a list of the person dicts sorted by 'total': + [ + { + uid:str + total:int + days:{ + day:{first:bool,score:int,solved:bool} + } + }, + ... + ] + """ + solves = {} + df = database.get_db().fetch_submissions(status="CORRECT", groupid=groupid) + #df = df.sort_values(by=["userid","day"]) + first_solvers = {day:get_first(day) for day in range(1,26)} + + for index,sub in df.iterrows(): + (uid,day,_) = cast(tuple,index) + first = bool(first_solvers[day] == uid) + points = calculate_points(day=day,timestamp=sub.timestamp,is_first=first) + #print(uid," ",day," ",points) + if uid not in solves: + solves[uid]={"userid":uid,"total":0,"days":{day:{"first":False,"score":0,"solved":False} for day in range(1,26)}} + solves[uid]["days"][day]["score"] = points + solves[uid]["days"][day]["first"] = first + solves[uid]["days"][day]["solved"] = True + solves[uid]["total"] += points + + leaderboard = list(solves.values()) + leaderboard = sorted(leaderboard, key=lambda d: d['total']) + + return solves,leaderboard + +def test_user_submissions(): + userid_peter, _ = database.get_db().login_or_register_user("123", "peter@example.com") + database.get_db().add_submission(userid_peter, 3, "Hallo") + database.get_db().add_submission(userid_peter, 3, "Welt") + database.get_db().add_submission(userid_peter, 3, "Yeet") + database.get_db().add_submission(userid_peter, 3, "123") +@click.command('test-score-py') +def test_score_py_command(): + click.echo('Adding Submissions') + #submit_answer(2022,1,1,"30") + database.get_db().set_day_puzzle(day=3,puzzle=b'203nsk20') + database.get_db().set_day_puzzle(day=7,puzzle=b'2138') + database.get_db().set_day_puzzle(day=20,puzzle=b'abcabca') + database.get_db().set_day_puzzle(day=1,puzzle=b'aksjndaks') + database.get_db().set_day_puzzle(day=21,puzzle=b'test') + database.get_db().set_day_puzzle(day=24,puzzle=b'test2') + database.get_db().set_day_solution(3,solution="123") + database.get_db().set_day_solution(7,solution="13") + database.get_db().set_day_solution(20,solution="abcabca") + database.get_db().set_day_solution(21,solution="12") + database.get_db().set_day_solution(24,solution="10") + test_user_submissions() + click.echo('Verifying...') + current_app.logger.debug(verifier.verify()) + click.echo('testing get-leaderboard') + solves, leaderboard = get_leaderboard() + current_app.logger.debug("Solves:") + current_app.logger.debug(solves) + current_app.logger.debug("Leaderboard:") + current_app.logger.debug(leaderboard) +def init_app(app): + app.cli.add_command(test_score_py_command) +