From 89fbd9af28ee01427fbdcc33decbaf3a779b09cc Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:43:26 -0700 Subject: [PATCH] add `scores` property to `Season` --- api/dailies.ts | 14 +- api/discord_app/sub/register.ts | 4 +- api/discord_app/sub/submit.ts | 4 +- api/mod.ts | 1 + api/seasons.ts | 2 +- api/types.ts | 118 ++++++++++++++++ lib/lc/client.ts | 51 ++----- lib/lc/fake_client.ts | 16 +-- .../denokv/denokv_leaderboard_client.ts | 83 +++++++----- .../denokv/denokv_leaderboard_client_test.ts | 10 +- lib/leaderboard/leaderboard_client.ts | 126 +----------------- lib/leaderboard/scores.ts | 34 +++-- lib/leaderboard/scores_test.ts | 12 +- 13 files changed, 243 insertions(+), 232 deletions(-) create mode 100644 api/types.ts diff --git a/api/dailies.ts b/api/dailies.ts index 9dfa4ab..7e13fae 100644 --- a/api/dailies.ts +++ b/api/dailies.ts @@ -1,4 +1,5 @@ import { type APIEmbed } from "lc-dailies/deps.ts"; +import * as api from "lc-dailies/api/mod.ts"; import * as discord from "lc-dailies/lib/discord/mod.ts"; import * as router from "lc-dailies/lib/router/mod.ts"; import * as lc from "lc-dailies/lib/lc/mod.ts"; @@ -85,7 +86,7 @@ async function executeDailyWebhook( const season = seasonID ? await leaderboardClient.getSeason(seasonID) : isSunday - ? await leaderboardClient.getCurrentSeason() + ? await leaderboardClient.getLatestSeason() : null; // Format the webhook embed. @@ -108,12 +109,12 @@ export interface DailyWebhookOptions { /** * question is the daily question. */ - question: lc.DailyQuestion; + question: api.LCQuestion; /** * season is the season to recap. */ - season: leaderboard.Season | null; + season: api.Season | null; } /** @@ -158,13 +159,10 @@ export function makeDailyWebhookEmbeds( /** * formatScores formats the scores of all players in a season. */ -export function formatScores(season: leaderboard.Season): string { - const scores = leaderboard.calculateSeasonScores( - leaderboard.makeDefaultCalculateScoresOptions(season), - ); +export function formatScores(season: api.Season): string { return [ "```", - ...Object.entries(scores) + ...Object.entries(season.scores) .sort(({ 1: scoreA }, { 1: scoreB }) => scoreB - scoreA) .map(([playerID, score], i) => { const player = season.players[playerID]; diff --git a/api/discord_app/sub/register.ts b/api/discord_app/sub/register.ts index 733df7a..c11ec10 100644 --- a/api/discord_app/sub/register.ts +++ b/api/discord_app/sub/register.ts @@ -7,7 +7,7 @@ import { ApplicationCommandOptionType, InteractionResponseType, } from "lc-dailies/deps.ts"; -import type * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts"; +import type * as api from "lc-dailies/api/mod.ts"; export const REGISTER = "register"; export const REGISTER_DESCRIPTION = "Register your Leetcode account"; @@ -68,7 +68,7 @@ export function parseRegisterOptions( * makeRegisterInteractionResponse makes the interaction response for the register subcommand. */ export function makeRegisterInteractionResponse( - r: leaderboard.RegisterResponse, + r: api.RegisterResponse, ): APIInteractionResponse { return { type: InteractionResponseType.ChannelMessageWithSource, diff --git a/api/discord_app/sub/submit.ts b/api/discord_app/sub/submit.ts index 316e23b..d53f05e 100644 --- a/api/discord_app/sub/submit.ts +++ b/api/discord_app/sub/submit.ts @@ -7,7 +7,7 @@ import { ApplicationCommandOptionType, InteractionResponseType, } from "lc-dailies/deps.ts"; -import type * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts"; +import * as api from "lc-dailies/api/mod.ts"; export const SUBMIT = "submit"; export const SUBMIT_DESCRIPTION = @@ -69,7 +69,7 @@ export function parseSubmitOptions( * makeSubmitInteractionResponse makes the interaction response for the register subcommand. */ export function makeSubmitInteractionResponse( - r: leaderboard.SubmitResponse, + r: api.SubmitResponse, ): APIInteractionResponse { return { type: InteractionResponseType.ChannelMessageWithSource, diff --git a/api/mod.ts b/api/mod.ts index cb3bec7..9811231 100644 --- a/api/mod.ts +++ b/api/mod.ts @@ -1 +1,2 @@ export * from "./api.ts"; +export * from "./types.ts"; diff --git a/api/seasons.ts b/api/seasons.ts index bfe1e43..d20e503 100644 --- a/api/seasons.ts +++ b/api/seasons.ts @@ -35,7 +35,7 @@ export function makeSeasonGetHandler( const season = await (seasonID === "latest" - ? leaderboardClient.getCurrentSeason() + ? leaderboardClient.getLatestSeason() : leaderboardClient.getSeason(seasonID)); return new Response(JSON.stringify(season)); }; diff --git a/api/types.ts b/api/types.ts new file mode 100644 index 0000000..e50d796 --- /dev/null +++ b/api/types.ts @@ -0,0 +1,118 @@ +/** + * LCPlayer is a registered player from Leetcode. + */ +export interface LCPlayer { + /** + * discord_user_id is the Discord user ID of the player. + */ + discord_user_id: string; + + /** + * lc_username is the Leetcode username of the player. + */ + lc_username: string; +} + +/** + * LCSubmission is a Leetcode submission. + */ +export interface LCSubmission { + /** + * id is the ID of the submission. + */ + id: string; + + /** + * date is the timestamp of the Leetcode submission. + */ + date: string; +} + +/** + * LCQuestion is a Leetcode question. + */ +export interface LCQuestion { + /** + * name is the name of the daily question. + */ + name: string; + + /** + * date is the date the daily question was posted in the format of YYYY-MM-DD. + */ + date: string; + + /** + * title is the title of the daily question. + */ + title: string; + + /** + * difficulty is the difficulty of the daily question. + */ + difficulty: string; + + /** + * url is the link of the daily question. + */ + url: string; +} + +/** + * Season is a season of the leaderboard. + */ +export interface Season { + /** + * id is the ID of the season. + */ + id: string; + + /** + * start_date is the start date of the season. + */ + start_date: string; + + /** + * scores is the map of scores in the season. + */ + scores: { [discord_user_id: string]: number }; + + /** + * players is the map of players in the season. + */ + players: { [discord_user_id: string]: LCPlayer }; + + /** + * questions is the map of questions in the season. + */ + questions: { [lc_question_name: string]: LCQuestion }; + + /** + * submissions is the map of submissions in the season. + */ + submissions: { + [discord_user_id: string]: { + [lc_question_name: string]: LCSubmission; + }; + }; +} + +/** + * RegisterResponse is the response for the register subcommand. + */ +export interface RegisterResponse { + /** + * ok is whether the registration was successful. + */ + ok: boolean; +} + +/** + * SubmitResponse is the response for the submit subcommand. + */ +export interface SubmitResponse { + /** + * ok is whether the submission was successful. + */ + ok: boolean; +} diff --git a/lib/lc/client.ts b/lib/lc/client.ts index 466d478..5eb8a97 100644 --- a/lib/lc/client.ts +++ b/lib/lc/client.ts @@ -1,46 +1,13 @@ +import type { LCQuestion } from "lc-dailies/api/mod.ts"; import { makeQuestionURL } from "./urls.ts"; import { gql } from "./gql.ts"; -/** - * DailyQuestion is the representation of Leetcode's daily question. - * - * Sample: - * Daily Leetcode Question for 2021-10-20 (date) - * Question: Find Eventual Safe States (question.title) - * Difficulty: Medium (question.difficulty) - * Link: https://leetcode.com/problems/find-eventual-safe-states/ (link) - */ -export interface DailyQuestion { - /** - * name is the name of the daily question. - */ - name: string; - - /** - * date is the date the daily question was posted in the format of YYYY-MM-DD. - */ - date: string; - - /** - * title is the title of the daily question. - */ - title: string; - - /** - * difficulty is the difficulty of the daily question. - */ - difficulty: string; - - /** - * url is the link of the daily question. - */ - url: string; -} +export type { LCQuestion }; /** - * RecentSubmission is the representation of Leetcode's recent submission per user. + * LCSubmission is the representation of Leetcode's recent submission per user. */ -export interface RecentSubmission { +export interface LCSubmission { /** * id is the id details of the submission. */ @@ -77,7 +44,7 @@ export class LCClient { /** * getDailyQuestion gets the daily question from Leetcode. */ - public async getDailyQuestion(): Promise { + public async getDailyQuestion(): Promise { const date = new Date(); const [question] = await this.listDailyQuestions( 1, @@ -98,8 +65,8 @@ export class LCClient { limit: number, asOfYear: number, asOfMonth: number, - ): Promise { - const dailies: DailyQuestion[] = []; + ): Promise { + const dailies: LCQuestion[] = []; let currentYear = asOfYear; let currentMonth = asOfMonth; @@ -153,7 +120,7 @@ export class LCClient { public async getRecentAcceptedSubmissions( username: string, limit: number, - ): Promise { + ): Promise { return await gql( JSON.stringify({ operationName: "recentAcSubmissions", @@ -175,7 +142,7 @@ export class LCClient { timestamp: string; titleSlug: string; }, - ): RecentSubmission => ({ + ): LCSubmission => ({ id: acSubmission.id, name: acSubmission.titleSlug, title: acSubmission.title, diff --git a/lib/lc/fake_client.ts b/lib/lc/fake_client.ts index f688ef2..d4d22bc 100644 --- a/lib/lc/fake_client.ts +++ b/lib/lc/fake_client.ts @@ -1,4 +1,4 @@ -import type { DailyQuestion, LCClient, RecentSubmission } from "./client.ts"; +import type { LCClient, LCQuestion, LCSubmission } from "./client.ts"; export const FAKE_LC_USERNAME = "fake_lc_username"; export const FAKE_LC_QUESTION_NAME = "fake_lc_question_name"; @@ -6,23 +6,23 @@ export const FAKE_LC_QUESTION_TITLE = "fake_lc_question_title"; export const FAKE_LC_QUESTION_URL = "fake_lc_question_url"; export const FAKE_LC_QUESTION_DIFFICULTY = "fake_lc_question_difficulty"; export const FAKE_LC_QUESTION_DATE = "2023-07-31"; -export const FAKE_LC_QUESTION: DailyQuestion = { +export const FAKE_LC_QUESTION: LCQuestion = { name: FAKE_LC_QUESTION_NAME, title: FAKE_LC_QUESTION_TITLE, url: FAKE_LC_QUESTION_URL, difficulty: FAKE_LC_QUESTION_DIFFICULTY, date: FAKE_LC_QUESTION_DATE, }; -export const FAKE_LC_QUESTIONS: DailyQuestion[] = [FAKE_LC_QUESTION]; +export const FAKE_LC_QUESTIONS: LCQuestion[] = [FAKE_LC_QUESTION]; export const FAKE_RECENT_SUBMISSION_ID = "1031839418"; export const FAKE_RECENT_SUBMISSION_TIMESTAMP = "1690761600"; -export const FAKE_RECENT_SUBMISSION: RecentSubmission = { +export const FAKE_RECENT_SUBMISSION: LCSubmission = { id: FAKE_RECENT_SUBMISSION_ID, name: FAKE_LC_QUESTION_NAME, title: FAKE_LC_QUESTION_TITLE, timestamp: FAKE_RECENT_SUBMISSION_TIMESTAMP, }; -export const FAKE_RECENT_SUBMISSIONS: RecentSubmission[] = [ +export const FAKE_RECENT_SUBMISSIONS: LCSubmission[] = [ FAKE_RECENT_SUBMISSION, ]; @@ -38,18 +38,18 @@ export class FakeLCClient implements LCClient { _: number, __: number, ___: number, - ): Promise { + ): Promise { return Promise.resolve(FAKE_LC_QUESTIONS); } public getRecentAcceptedSubmissions( _: string, __: number, - ): Promise { + ): Promise { return Promise.resolve(FAKE_RECENT_SUBMISSIONS); } - public getDailyQuestion(): Promise { + public getDailyQuestion(): Promise { return Promise.resolve(FAKE_LC_QUESTION); } } diff --git a/lib/leaderboard/denokv/denokv_leaderboard_client.ts b/lib/leaderboard/denokv/denokv_leaderboard_client.ts index 39b0819..a46c32b 100644 --- a/lib/leaderboard/denokv/denokv_leaderboard_client.ts +++ b/lib/leaderboard/denokv/denokv_leaderboard_client.ts @@ -1,11 +1,16 @@ import { DAY, ulid, WEEK } from "lc-dailies/deps.ts"; -import type * as lc from "lc-dailies/lib/lc/mod.ts"; -import * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts"; +import type * as api from "lc-dailies/api/mod.ts"; +import type { LeaderboardClient } from "lc-dailies/lib/leaderboard/mod.ts"; +import { + calculateSeasonScores, + makeDefaultCalculateScoresOptions, +} from "lc-dailies/lib/leaderboard/mod.ts"; +import type { LCClient } from "lc-dailies/lib/lc/mod.ts"; /** * DenoKvLeaderboardClient is the client for the leaderboard. */ -export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { +export class DenoKvLeaderboardClient implements LeaderboardClient { public constructor( /** * kv is the key-value store for the leaderboard. @@ -14,7 +19,7 @@ export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { /** * lc is the Leetcode client. */ - private readonly lc: lc.LCClient, + private readonly lc: LCClient, /** * restartMS is the milliseconds to restart the leaderboard. * @@ -25,22 +30,22 @@ export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { ) {} /** - * getCurrentSeasonFromKv reads the current season from Deno KV. + * getLatestSeasonFromKv reads the latest season from Deno KV. */ - private async getCurrentSeasonFromKv(): Promise< - Deno.KvEntryMaybe | null + private async getLatestSeasonFromKv(): Promise< + Deno.KvEntryMaybe | null > { - // Get the current season ID. - const currentSeasonIDResult = await this.kv + // Get the latest season ID. + const latestSeasonIDResult = await this.kv .get([LeaderboardKvPrefix.SEASON_ID]); - if (!currentSeasonIDResult.value) { + if (!latestSeasonIDResult.value) { return null; } // Get the current season. - const seasonResult = await this.kv.get([ + const seasonResult = await this.kv.get([ LeaderboardKvPrefix.SEASONS, - currentSeasonIDResult.value, + latestSeasonIDResult.value, ]); if (!seasonResult.value) { throw new Error("Season not found"); @@ -50,11 +55,11 @@ export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { } /** - * updateCurrentSeason updates the current season in Deno KV. + * updateLatestSeason updates the latest season in Deno KV. */ - private async updateCurrentSeason( - season: leaderboard.Season, - prevSeasonResult: Deno.KvEntryMaybe | null, + private async updateLatestSeason( + season: api.Season, + prevSeasonResult: Deno.KvEntryMaybe | null, ): Promise { // Update the season. const updateSeasonOp = this.kv.atomic(); @@ -86,9 +91,9 @@ export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { public async register( discord_user_id: string, lc_username: string, - ): Promise { + ): Promise { const key: Deno.KvKey = [LeaderboardKvPrefix.PLAYERS, discord_user_id]; - const playerResult = await this.kv.get(key); + const playerResult = await this.kv.get(key); if (playerResult.value) { throw new Error("Player already registered"); } @@ -100,7 +105,7 @@ export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { } // Register the player. - const player: leaderboard.Player = { discord_user_id, lc_username }; + const player: api.LCPlayer = { discord_user_id, lc_username }; const registerResult = await this.kv .atomic() .check(playerResult) @@ -117,10 +122,10 @@ export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { discord_user_id: string, lc_submission_id: string, currentDate = new Date(), - ): Promise { + ): Promise { // Check if the player is registered in our leaderboard. const maybePlayerResult = await this.kv - .get([LeaderboardKvPrefix.PLAYERS, discord_user_id]); + .get([LeaderboardKvPrefix.PLAYERS, discord_user_id]); if (!maybePlayerResult.value) { throw new Error("Player not registered"); } @@ -134,14 +139,14 @@ export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { throw new Error("Submission not found"); } - // The current season. Default to empty season when not found or not current. - // If current date is no longer in the "current season, create a new season. - const maybeSeasonResult = await this.getCurrentSeasonFromKv(); - const isCurrentSeason = !!(maybeSeasonResult?.value) && checkDateInWeek( + // The latest season. Default to empty season when not found or not latest. + // If current date is no longer in the "latest" season, create a new season. + const maybeSeasonResult = await this.getLatestSeasonFromKv(); + const isLatestSeason = !!(maybeSeasonResult?.value) && checkDateInWeek( new Date(maybeSeasonResult.value.start_date).getTime(), currentDate.getTime(), ); - const season = isCurrentSeason + const season = isLatestSeason ? maybeSeasonResult?.value : makeEmptySeason(getStartOfWeek(this.restartMs, currentDate)); @@ -202,13 +207,20 @@ export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { // Add the question to the season if not already in the season. season.questions[recentDailyQuestion.name] ??= recentDailyQuestion; + // Add the calculated scores to the season. + season.scores = calculateSeasonScores(makeDefaultCalculateScoresOptions( + season.players, + season.questions, + season.submissions, + )); + // Update the season in Deno KV. - await this.updateCurrentSeason(season, maybeSeasonResult); + await this.updateLatestSeason(season, maybeSeasonResult); return { ok: true }; } - public async getCurrentSeason(): Promise { - const seasonResult = await this.getCurrentSeasonFromKv(); + public async getLatestSeason(): Promise { + const seasonResult = await this.getLatestSeasonFromKv(); if (!seasonResult?.value) { return null; } @@ -218,18 +230,18 @@ export class DenoKvLeaderboardClient implements leaderboard.LeaderboardClient { public async getSeason( season_id: string, - ): Promise { - const seasonResult = await this.kv.get([ + ): Promise { + const seasonResult = await this.kv.get([ LeaderboardKvPrefix.SEASONS, season_id, ]); return seasonResult.value; } - public async listSeasons(): Promise { - const seasons: leaderboard.Season[] = []; + public async listSeasons(): Promise { + const seasons: api.Season[] = []; const entries = this.kv - .list({ prefix: [LeaderboardKvPrefix.SEASONS] }); + .list({ prefix: [LeaderboardKvPrefix.SEASONS] }); for await (const entry of entries) { seasons.push(entry.value); } @@ -265,10 +277,11 @@ function getStartOfWeek(restartMs = 0, date = new Date()): number { return startOfWeek; } -function makeEmptySeason(startOfWeek: number): leaderboard.Season { +function makeEmptySeason(startOfWeek: number): api.Season { return { id: ulid(startOfWeek), start_date: new Date(startOfWeek).toUTCString(), + scores: {}, players: {}, questions: {}, submissions: {}, diff --git a/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts b/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts index 7092df3..9962bf5 100644 --- a/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts +++ b/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts @@ -1,6 +1,6 @@ import { assertEquals, assertRejects } from "lc-dailies/deps.ts"; import * as fake_lc from "lc-dailies/lib/lc/fake_client.ts"; -import type { Season } from "lc-dailies/lib/leaderboard/mod.ts"; +import type { Season } from "lc-dailies/api/mod.ts"; import { DenoKvLeaderboardClient } from "./denokv_leaderboard_client.ts"; const FAKE_DISCORD_USER_ID = "fake_discord_user_id"; @@ -14,6 +14,9 @@ const FAKE_SEASON: Season = { lc_username: fake_lc.FAKE_LC_USERNAME, }, }, + scores: { + [FAKE_DISCORD_USER_ID]: 50, + }, questions: { [fake_lc.FAKE_LC_QUESTION_NAME]: fake_lc.FAKE_LC_QUESTION, }, @@ -61,8 +64,8 @@ Deno.test("DenoKvLeaderboardClient", async (t) => { }); let seasonID: string | undefined; - await t.step("getCurrentSeason", async () => { - const season = await client.getCurrentSeason(); + await t.step("getLatestSeason", async () => { + const season = await client.getLatestSeason(); seasonID = season?.id; assertSeasonsEqual(season, FAKE_SEASON); }); @@ -87,6 +90,7 @@ function assertSeasonsEqual( expectedSeason: Season, ): void { assertEquals(actualSeason?.start_date, expectedSeason.start_date); + assertEquals(actualSeason?.scores, expectedSeason.scores); assertEquals(actualSeason?.players, expectedSeason.players); assertEquals(actualSeason?.questions, expectedSeason.questions); assertEquals(actualSeason?.submissions, expectedSeason.submissions); diff --git a/lib/leaderboard/leaderboard_client.ts b/lib/leaderboard/leaderboard_client.ts index fe97bee..f57f415 100644 --- a/lib/leaderboard/leaderboard_client.ts +++ b/lib/leaderboard/leaderboard_client.ts @@ -1,116 +1,4 @@ -/** - * Player is a registered player in the leaderboard. - */ -export interface Player { - /** - * discord_user_id is the Discord user ID of the player. - */ - discord_user_id: string; - - /** - * lc_username is the Leetcode username of the player. - */ - lc_username: string; -} - -/** - * Submission is a submission in the leaderboard. - */ -export interface Submission { - /** - * id is the ID of the submission. - */ - id: string; - - /** - * date is the date of the submission. - */ - date: string; -} - -/** - * LCQuestion is a Leetcode question. - */ -export interface LCQuestion { - /** - * name is the name of the daily question. - */ - name: string; - - /** - * date is the date the daily question was posted in the format of YYYY-MM-DD. - */ - date: string; - - /** - * title is the title of the daily question. - */ - title: string; - - /** - * difficulty is the difficulty of the daily question. - */ - difficulty: string; - - /** - * url is the link of the daily question. - */ - url: string; -} - -/** - * Season is a season of the leaderboard. - */ -export interface Season { - /** - * id is the ID of the season. - */ - id: string; - - /** - * start_date is the start date of the season. - */ - start_date: string; - - /** - * players is the map of players in the season. - */ - players: { [discord_user_id: string]: Player }; - - /** - * questions is the map of questions in the season. - */ - questions: { [lc_question_name: string]: LCQuestion }; - - /** - * submissions is the map of submissions in the season. - */ - submissions: { - [discord_user_id: string]: { - [lc_question_name: string]: Submission; - }; - }; -} - -/** - * RegisterResponse is the response for the register subcommand. - */ -export interface RegisterResponse { - /** - * ok is whether the registration was successful. - */ - ok: boolean; -} - -/** - * SubmitResponse is the response for the submit subcommand. - */ -export interface SubmitResponse { - /** - * ok is whether the submission was successful. - */ - ok: boolean; -} +import type * as api from "lc-dailies/api/mod.ts"; /** * LeaderboardClient is the client interface for the leaderboard. @@ -122,7 +10,7 @@ export interface LeaderboardClient { register( discord_user_id: string, lc_username: string, - ): Promise; + ): Promise; /** * submit registers a new submission to the leaderboard. @@ -132,20 +20,20 @@ export interface LeaderboardClient { submit( discord_user_id: string, lc_submission_id: string, - ): Promise; + ): Promise; /** - * getCurrentSeason gets the current season of the leaderboard. + * getLatestSeason gets the latest season. */ - getCurrentSeason(): Promise; + getLatestSeason(): Promise; /** * getSeason gets a season of the leaderboard by ID. */ - getSeason(season_id: string): Promise; + getSeason(season_id: string): Promise; /** * listSeasons gets a list of season IDs of the leaderboard. */ - listSeasons(): Promise; + listSeasons(): Promise; } diff --git a/lib/leaderboard/scores.ts b/lib/leaderboard/scores.ts index a8e6c6c..70290a4 100644 --- a/lib/leaderboard/scores.ts +++ b/lib/leaderboard/scores.ts @@ -1,14 +1,24 @@ import { DAY } from "lc-dailies/deps.ts"; -import type { LCQuestion, Season, Submission } from "./leaderboard_client.ts"; +import type * as api from "lc-dailies/api/mod.ts"; /** * CalculateScoresOptions is the options for calculateScores. */ export interface CalculateScoresOptions { /** - * season is the season to calculate the score for. + * submissions are the submissions in the season. */ - season: Season; + submissions: api.Season["submissions"]; + + /** + * questions are the questions in the season. + */ + questions: api.Season["questions"]; + + /** + * players are the players in the season. + */ + players: api.Season["players"]; /** * possibleHighestScore is the highest possible score. @@ -31,8 +41,8 @@ export interface CalculateScoresOptions { * calculateSubmissionScore calculates the score of a submission. */ export function calculateSubmissionScore( - submission: Submission, - question: LCQuestion, + submission: api.LCSubmission, + question: api.LCQuestion, options: CalculateScoresOptions, ): number { const questionDate = new Date(`${question.date} GMT`); @@ -55,14 +65,14 @@ export function calculatePlayerScore( playerID: string, options: CalculateScoresOptions, ): number { - const submissions = options.season.submissions[playerID]; + const submissions = options.submissions[playerID]; if (!submissions) { return 0; } const scoreSum = Object.entries(submissions) .reduce((score, [questionID, submission]) => { - const question = options.season.questions[questionID]; + const question = options.questions[questionID]; if (!question) { return score; } @@ -85,7 +95,7 @@ export function calculatePlayerScore( export function calculateSeasonScores( options: CalculateScoresOptions, ): Record { - return Object.keys(options.season.players) + return Object.keys(options.players) .reduce((scores, playerID) => { scores[playerID] = calculatePlayerScore(playerID, options); return scores; @@ -96,10 +106,14 @@ export function calculateSeasonScores( * makeDefaultCalculateScoresOptions creates a default CalculateScoresOptions. */ export function makeDefaultCalculateScoresOptions( - season: Season, + players: api.Season["players"], + questions: api.Season["questions"], + submissions: api.Season["submissions"], ): CalculateScoresOptions { return { - season, + players, + questions, + submissions, possibleHighestScore: 100, possibleLowestScore: 50, duration: DAY, diff --git a/lib/leaderboard/scores_test.ts b/lib/leaderboard/scores_test.ts index 1e59400..3862882 100644 --- a/lib/leaderboard/scores_test.ts +++ b/lib/leaderboard/scores_test.ts @@ -62,7 +62,11 @@ Deno.test("calculatePlayerScore calculates the score of a player", () => { assertEquals( calculatePlayerScore( "redacted_discord_id_00", - makeDefaultCalculateScoresOptions(FAKE_SEASON), + makeDefaultCalculateScoresOptions( + FAKE_SEASON.players, + FAKE_SEASON.questions, + FAKE_SEASON.submissions, + ), ), 159, ); @@ -70,7 +74,11 @@ Deno.test("calculatePlayerScore calculates the score of a player", () => { Deno.test("calculateSeasonScores calculates the scores of a season", () => { const seasonScores = calculateSeasonScores( - makeDefaultCalculateScoresOptions(FAKE_SEASON), + makeDefaultCalculateScoresOptions( + FAKE_SEASON.players, + FAKE_SEASON.questions, + FAKE_SEASON.submissions, + ), ); assertEquals(