From cd130392094410e7f311318164e1a2f8b6f877b4 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Thu, 21 Dec 2023 19:07:43 +0100
Subject: [PATCH 1/3] use mean submission seconds for tiebreak

---
 aocp/aoc.py     | 14 ++++++++++++++
 aocp/context.py |  4 +++-
 aocp/score.py   | 13 ++++++++++++-
 3 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/aocp/aoc.py b/aocp/aoc.py
index 0224470..472abc5 100755
--- a/aocp/aoc.py
+++ b/aocp/aoc.py
@@ -159,3 +159,17 @@ def get_aoc_day(timestamp : datetime|None = None, year : int|None = None)-> Tupl
         return 0, hour
     return min(timestamp.day, 26), hour
 
+def get_aoc_submission_seconds(timestamp: datetime) -> int:
+    """
+    Get the number of seconds since the start of the AOC Day a submission was made in.
+
+    This function is useful for tiebreaking.
+
+    Parameters:
+    timestamp -- The timestamp of the submission. Must be timezone aware.
+    """
+    timestamp = timestamp.astimezone(ZoneInfo("EST")) # convert to EST time
+    daystart = datetime(year=timestamp.year, month=timestamp.month, day=timestamp.day,hour=0,minute=0,second=0, tzinfo=timestamp.tzinfo)
+    return (daystart - timestamp).seconds
+
+
diff --git a/aocp/context.py b/aocp/context.py
index 46011bd..d37f6b3 100644
--- a/aocp/context.py
+++ b/aocp/context.py
@@ -57,16 +57,18 @@ class ContextReq(Flag):
         userid: {
             userid:str
             total:int
+            meansubmissionseconds:int
             days:{
                 day:{first:bool,score:int,solved:bool}
             }
         }
     }
-    Include 'leaderboard' field in the context, which is the leaderboard as a list of the person dicts sorted (descending) by 'total':
+    Include 'leaderboard' field in the context, which is the leaderboard as a list of the person dicts sorted (descending) by 'total' (tiebreaks are done using 'meansubmissionseconds'):
     [
         {
             userid:str
             total:int
+            meansubmissionseconds:int
             days:{
                 day:{first:bool,score:int,solved:bool}
             }
diff --git a/aocp/score.py b/aocp/score.py
index 1f49de7..5c4a298 100644
--- a/aocp/score.py
+++ b/aocp/score.py
@@ -74,16 +74,18 @@ def get_leaderboard(groupid:str|None = None)->Tuple[dict,list[dict]]:
         userid: {
             userid:str
             total:int
+            meansubmissionseconds:int
             days:{
                 day:{first:bool,score:int,solved:bool,groupid:str|None}
             }
         }
     }
-    - The leaderboard as a list of the person dicts sorted (descending) by 'total':
+    - The leaderboard as a list of the person dicts sorted (descending) by 'total' (tiebreaks are done using 'meansubmissionseconds'):
     [
         {
             userid:str
             total:int
+            meansubmissionseconds:int
             days:{
                 day:{first:bool,score:int,solved:bool,groupid:str|None}
             }
@@ -109,7 +111,16 @@ def get_leaderboard(groupid:str|None = None)->Tuple[dict,list[dict]]:
         solves[uid]["days"][day]["groupid"] = sub.groupid
         solves[uid]["total"] += points
 
+    # Determine mean submission seconds for each person
+    meanseconds = df["timestamp"].map(aoc.get_aoc_submission_seconds).groupby(level="userid").mean()
+    for uid, meansecond in meanseconds.items():
+        solves[uid]["meansubmissionseconds"] = int(meansecond)
+
+    # Build leaderboard
     leaderboard = list(solves.values())
+    # Tiebreak via mean submission seconds
+    leaderboard = sorted(leaderboard, key=lambda d: d['meansubmissionseconds'], reverse=True)
+    # Sort by total points
     leaderboard = sorted(leaderboard, key=lambda d: d['total'], reverse=True)
 
     return solves,leaderboard
-- 
GitLab


From ab2d1fcabdfb7aab3d9f400225a90ebadb742957 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Thu, 21 Dec 2023 19:09:42 +0100
Subject: [PATCH 2/3] fixed tiebreak direction

---
 aocp/score.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/aocp/score.py b/aocp/score.py
index 5c4a298..0fbe21a 100644
--- a/aocp/score.py
+++ b/aocp/score.py
@@ -119,7 +119,7 @@ def get_leaderboard(groupid:str|None = None)->Tuple[dict,list[dict]]:
     # Build leaderboard
     leaderboard = list(solves.values())
     # Tiebreak via mean submission seconds
-    leaderboard = sorted(leaderboard, key=lambda d: d['meansubmissionseconds'], reverse=True)
+    leaderboard = sorted(leaderboard, key=lambda d: d['meansubmissionseconds'])
     # Sort by total points
     leaderboard = sorted(leaderboard, key=lambda d: d['total'], reverse=True)
 
-- 
GitLab


From 8ca3df109c700006f81b820ef39569c4e0870636 Mon Sep 17 00:00:00 2001
From: Jake <j.vondoemming@stud.uni-goettingen.de>
Date: Thu, 21 Dec 2023 19:13:21 +0100
Subject: [PATCH 3/3] added tiebreak in rules

---
 aocp/templates/rules.html | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/aocp/templates/rules.html b/aocp/templates/rules.html
index ea285e9..952b17d 100644
--- a/aocp/templates/rules.html
+++ b/aocp/templates/rules.html
@@ -35,6 +35,8 @@
 		<li>Wenn man die erste Person ist die den Tag löst, gibt es einen weiteren Bonuspunkt.</li>
 	</ol>
 	<p>Wenn man einen Tag erst nach 24 Stunden löst, gibt es für den Tag insgesamt genau 0 Punkte.</p>
+	<h4>Tiebreak</h4>
+	<p>Bei Punktegleichstand wird im Leaderboard nach durchschnittlicher Abgabezeit der korrekten Einreichungen sortiert. Wer im Durchschnitt früher abgibt wird also bei Punktegleichstand im Leaderboard weiter oben stehen.</p>
 	<h3>Preise</h3>
 	<p>Am Ende des Adventskalenders werden an die Leute mit den meisten Punkten auf unserem globalen Leaderboard Preise vergeben.</p>
 	<p>Welche Preise es genau gibt und wie viele wird in den nächsten Tagen noch bekannt gegeben.</p>
-- 
GitLab