forked from codechefPesuecc/CodeChef-PESUECC-Chapter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathleaderboard.ts
More file actions
146 lines (134 loc) · 4.91 KB
/
Copy pathleaderboard.ts
File metadata and controls
146 lines (134 loc) · 4.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { and, eq } from "drizzle-orm";
import { getDb } from "@/server/db";
import { submissions, users, challenges } from "@/server/db/schema";
import { getDailyChallenge, istYearMonth } from "@/lib/challenges";
import { scoreChallenge, type ScoreInput } from "@/lib/scoring";
/**
* Leaderboards derived from persisted submissions (the scoring rules live in
* `@/lib/scoring`). Today's Problem of the Day ranks live solves by finish order —
* the server timestamp of the first Accepted submission, so it can't be spoofed —
* and freezes on its own at IST midnight once a newer problem becomes the POTD.
* Solving a past problem earns a flat base score.
*/
export interface LeaderRow {
rank: number | null; // null = flagged / out of the ranked positions
// Public board identity: SRN if the student has one, else PRN. The login handle
// (username) and email are intentionally not exposed on the board.
display: string;
points: number;
flagged: boolean;
solved?: number; // month / all-time
language?: string; // today
timeSeconds?: number | null; // today (server-computed solve duration)
}
/** Today's problem: finish-order standings with the speed-bounty points. Only
* live (ranked) accepted solves count — a past-problem practice solve of the same
* slug never appears here. */
export async function todayLeaderboard(): Promise<LeaderRow[]> {
const daily = await getDailyChallenge();
if (!daily) return [];
const db = getDb();
const rows = await db
.select({
userId: submissions.userId,
createdAt: submissions.createdAt,
flags: submissions.flags,
elapsedSeconds: submissions.elapsedSeconds,
language: submissions.language,
srn: users.srn,
prn: users.prn,
})
.from(submissions)
.innerJoin(users, eq(submissions.userId, users.id))
.where(
and(
eq(submissions.challengeSlug, daily.slug),
eq(submissions.status, "AC"),
eq(submissions.ranked, true),
),
);
const displayById = new Map(rows.map((r) => [r.userId, r.srn ?? r.prn]));
// Earliest live AC per user carries the language + solve time we display.
const firstByUser = new Map<string, (typeof rows)[number]>();
for (const r of rows) {
const cur = firstByUser.get(r.userId);
if (!cur || r.createdAt < cur.createdAt) firstByUser.set(r.userId, r);
}
const scored = scoreChallenge(
rows.map((r) => ({
userId: r.userId,
createdAt: r.createdAt,
flags: r.flags,
ranked: true,
})),
);
const out: LeaderRow[] = [...scored.entries()].map(([userId, s]) => {
const first = firstByUser.get(userId);
return {
rank: s.rank,
display: displayById.get(userId) ?? "unknown",
points: s.points,
flagged: s.flagged,
language: first?.language,
timeSeconds: first?.elapsedSeconds ?? null,
};
});
out.sort((a, b) => (a.rank ?? Infinity) - (b.rank ?? Infinity));
return out;
}
/**
* Month / all-time: sum of each user's per-challenge award. Live solvers get their
* speed-bounty points; late (practice) solvers get the flat base score. Month uses
* each challenge's own IST date, compared against the current IST month.
*/
export async function aggregateLeaderboard(scope: "month" | "all"): Promise<LeaderRow[]> {
const db = getDb();
const rows = await db
.select({
userId: submissions.userId,
challengeSlug: submissions.challengeSlug,
createdAt: submissions.createdAt,
flags: submissions.flags,
ranked: submissions.ranked,
date: challenges.date,
srn: users.srn,
prn: users.prn,
})
.from(submissions)
.innerJoin(users, eq(submissions.userId, users.id))
.innerJoin(challenges, eq(submissions.challengeSlug, challenges.slug))
.where(eq(submissions.status, "AC"));
const displayById = new Map(rows.map((r) => [r.userId, r.srn ?? r.prn]));
const bySlug = new Map<string, { date: string; acs: ScoreInput[] }>();
for (const r of rows) {
const group = bySlug.get(r.challengeSlug) ?? { date: r.date, acs: [] };
group.acs.push({
userId: r.userId,
createdAt: r.createdAt,
flags: r.flags,
ranked: r.ranked,
});
bySlug.set(r.challengeSlug, group);
}
const ym = istYearMonth();
const totals = new Map<string, { points: number; solved: number }>();
for (const [, group] of bySlug) {
if (scope === "month" && !group.date.startsWith(ym)) continue;
for (const [userId, s] of scoreChallenge(group.acs)) {
const t = totals.get(userId) ?? { points: 0, solved: 0 };
t.points += s.points;
t.solved += 1;
totals.set(userId, t);
}
}
const out: LeaderRow[] = [...totals.entries()].map(([userId, t]) => ({
rank: 0,
display: displayById.get(userId) ?? "unknown",
points: t.points,
solved: t.solved,
flagged: false,
}));
out.sort((a, b) => b.points - a.points || (b.solved ?? 0) - (a.solved ?? 0));
out.forEach((r, i) => (r.rank = i + 1));
return out;
}