diff --git a/backend/package.json b/backend/package.json index 5cd74ebacf64..c063510eaaaf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@date-fns/utc": "1.2.0", + "@monkeytype/challenges": "workspace:*", "@monkeytype/contracts": "workspace:*", "@monkeytype/funbox": "workspace:*", "@monkeytype/schemas": "workspace:*", diff --git a/backend/src/constants/auto-roles.ts b/backend/src/constants/auto-roles.ts index dd80dc29a7e0..75d6ca13a140 100644 --- a/backend/src/constants/auto-roles.ts +++ b/backend/src/constants/auto-roles.ts @@ -1,38 +1,5 @@ -export default [ - "oneHourWarrior", - "doubleDown", - "tripleTrouble", - "quad", - "trueSimp", - "bigramSalad", - "simp", - "antidiseWhat", - "whatsThisWebsiteCalledAgain", - "developd", - "slowAndSteady", - "speedSpacer", - "iveGotThePower", - "accuracyExpert", - "accuracyMaster", - "accuracyGod", - "jolly", - "gottaCatchEmAll", - "rapGod", - "navySeal", - "rollercoaster", - "oneHourMirror", - "chooChoo", - "earfquake", - "simonSez", - "accountant", - "hidden", - "iCanSeeTheFuture", - "whatAreWordsAtThisPoint", - "specials", - "aeiou", - "asciiWarrior", - "iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS", - "oneNauseousMonkey", - "69", - "englishMaster", -]; +import { getChallenges } from "@monkeytype/challenges"; + +export default getChallenges() + .filter((it) => it.settings?.autoRole) + .map((it) => it.name); diff --git a/frontend/package.json b/frontend/package.json index 24a7fc9a68f7..5a33b1face8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "dependencies": { "@date-fns/utc": "1.2.0", "@leonabcd123/modern-caps-lock": "3.1.3", + "@monkeytype/challenges": "workspace:*", "@monkeytype/contracts": "workspace:*", "@monkeytype/funbox": "workspace:*", "@monkeytype/schemas": "workspace:*", diff --git a/frontend/scripts/check-assets.ts b/frontend/scripts/check-assets.ts index 8ba8e7fa9400..bc74ae38adaf 100644 --- a/frontend/scripts/check-assets.ts +++ b/frontend/scripts/check-assets.ts @@ -2,7 +2,6 @@ * Example usage in root or frontend: * pnpm check-assets (npm run check-assets) * pnpm check-assets -- -- quotes others (npm run check-assets -- -- quotes others) - * pnpm check-assets -- -- challenges sound -p (npm run check-assets -- -- challenges sound -p) */ import * as fs from "fs"; @@ -18,7 +17,6 @@ import { KnownFontName } from "@monkeytype/schemas/fonts"; import { Fonts } from "../src/ts/constants/fonts"; import { themes, ThemeSchema, ThemesList } from "../src/ts/constants/themes"; import { z } from "zod"; -import { ChallengeSchema, Challenge } from "@monkeytype/schemas/challenges"; import { LayoutObject, LayoutObjectSchema } from "@monkeytype/schemas/layouts"; import { QuoteDataSchema, QuoteData } from "@monkeytype/schemas/quotes"; import { clickSoundConfig } from "../src/ts/constants/sounds"; @@ -99,24 +97,6 @@ function findDuplicates(items: T[]): T[] { return Array.from(duplicates); } -async function validateChallenges(): Promise { - const problems = new Problems<"_list.json", never>("Challenges", {}); - - const challengesData = JSON.parse( - fs.readFileSync("./static/challenges/_list.json", { - encoding: "utf8", - flag: "r", - }), - ) as Challenge; - const validationResult = z.array(ChallengeSchema).safeParse(challengesData); - problems.addValidation("_list.json", validationResult); - - console.log(problems.toString()); - if (problems.hasError()) { - throw new Error("challenges with errors"); - } -} - async function validateLayouts(): Promise { const problems = new Problems("Layouts", { _additional: @@ -496,17 +476,10 @@ async function main(): Promise { quotes: [validateQuotes], languages: [validateLanguages], layouts: [validateLayouts], - challenges: [validateChallenges], fonts: [validateFonts], themes: [validateThemes], sounds: [validateSounds], - others: [ - validateChallenges, - validateLayouts, - validateFonts, - validateThemes, - validateSounds, - ], + others: [validateLayouts, validateFonts, validateThemes, validateSounds], }; // flags diff --git a/frontend/src/ts/auth.tsx b/frontend/src/ts/auth.tsx index 52b7231bd230..f73f7a0c6480 100644 --- a/frontend/src/ts/auth.tsx +++ b/frontend/src/ts/auth.tsx @@ -1,4 +1,5 @@ import { PasswordSchema } from "@monkeytype/schemas/users"; +import { typedKeys } from "@monkeytype/util/objects"; import { tryCatch } from "@monkeytype/util/trycatch"; import { FirebaseError } from "firebase/app"; import { @@ -41,7 +42,6 @@ import { } from "./states/notifications"; import { isDevEnvironment } from "./utils/env"; import { createErrorMessage } from "./utils/error"; -import { typedKeys } from "./utils/misc"; import { SnapshotInitError } from "./utils/snapshot-init-error"; import { OneOf } from "./utils/types"; diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts index 71b01937d653..5d479fd2309a 100644 --- a/frontend/src/ts/commandline/commandline-metadata.ts +++ b/frontend/src/ts/commandline/commandline-metadata.ts @@ -13,8 +13,8 @@ import { getActivePage, isAuthenticated } from "../states/core"; import { Fonts } from "../constants/fonts"; import { KnownFontName } from "@monkeytype/schemas/fonts"; import * as UI from "../ui"; -import { typedKeys } from "../utils/misc"; import { Validation } from "../types/validation"; +import { typedKeys } from "@monkeytype/util/objects"; //TODO: remove display property and instead use optionsMetadata from configMetadata // eventually this file should be fully merged into config metadata, probably under the 'commandline' property diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 6780991f828f..ac9a39c45a32 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -12,14 +12,10 @@ import CustomThemesListCommands from "./lists/custom-themes-list"; import PresetsCommands from "./lists/presets"; import FunboxCommands from "./lists/funbox"; import ThemesCommands from "./lists/themes"; -import LoadChallengeCommands, { - update as updateLoadChallengeCommands, -} from "./lists/load-challenge"; +import LoadChallengeCommands from "./lists/load-challenge"; import { Config } from "../config/store"; import { setConfig } from "../config/setters"; -import * as getErrorMessage from "../utils/error"; -import * as JSONData from "../utils/json-data"; import { randomizeTheme } from "../controllers/theme-controller"; import { showModal } from "../states/modals"; import { @@ -41,20 +37,6 @@ import { import { applyConfigFromJson } from "../config/lifecycle"; import { lastEventLog } from "../test/test-state"; -const challengesPromise = JSONData.getChallengeList(); -challengesPromise - .then((challenges) => { - updateLoadChallengeCommands(challenges); - }) - .catch((e: unknown) => { - console.error( - getErrorMessage.createErrorMessage( - e, - "Failed to update challenges commands", - ), - ); - }); - const adsCommands = buildCommands("ads"); export const commands: CommandsSubgroup = { @@ -406,8 +388,6 @@ export function doesListExist(listName: string): boolean { export async function getList( listName: CommandlineListKey | ConfigKey, ): Promise { - await Promise.allSettled([challengesPromise]); - const subGroup = subgroupByConfigKey[listName]; if (subGroup !== undefined) { return subGroup; @@ -451,7 +431,6 @@ export function getTopOfStack(): CommandsSubgroup { let singleList: CommandsSubgroup | undefined; export async function getSingleSubgroup(): Promise { - await Promise.allSettled([challengesPromise]); const singleCommands: Command[] = []; for (const command of commands.list) { const ret = buildSingleListCommands(command); diff --git a/frontend/src/ts/commandline/lists/load-challenge.ts b/frontend/src/ts/commandline/lists/load-challenge.ts index c49f1511a298..a7dfbbfc1524 100644 --- a/frontend/src/ts/commandline/lists/load-challenge.ts +++ b/frontend/src/ts/commandline/lists/load-challenge.ts @@ -1,13 +1,25 @@ -import { navigate } from "../../controllers/route-controller"; +import { getChallenges } from "@monkeytype/challenges"; import * as ChallengeController from "../../controllers/challenge-controller"; +import { navigate } from "../../controllers/route-controller"; import * as TestLogic from "../../test/test-logic"; import { capitalizeFirstLetterOfEachWord } from "../../utils/strings"; import { Command, CommandsSubgroup } from "../types"; -import { Challenge } from "@monkeytype/schemas/challenges"; const subgroup: CommandsSubgroup = { title: "Load challenge...", - list: [], + list: getChallenges() + .filter((it) => it.settings !== undefined) + .map((challenge) => ({ + id: `loadChallenge${capitalizeFirstLetterOfEachWord(challenge.name)}`, + display: challenge.display, + exec: async (): Promise => { + await navigate("/"); + await ChallengeController.setup(challenge.name); + TestLogic.restart({ + nosave: true, + }); + }, + })), }; const commands: Command[] = [ @@ -19,21 +31,4 @@ const commands: Command[] = [ }, ]; -function update(challenges: Challenge[]): void { - challenges.forEach((challenge) => { - subgroup.list.push({ - id: `loadChallenge${capitalizeFirstLetterOfEachWord(challenge.name)}`, - display: challenge.display, - exec: async (): Promise => { - await navigate("/"); - await ChallengeController.setup(challenge.name); - TestLogic.restart({ - nosave: true, - }); - }, - }); - }); -} - export default commands; -export { update }; diff --git a/frontend/src/ts/components/common/AsyncContent.tsx b/frontend/src/ts/components/common/AsyncContent.tsx index 1309377dc4ca..d2b5a4d019f9 100644 --- a/frontend/src/ts/components/common/AsyncContent.tsx +++ b/frontend/src/ts/components/common/AsyncContent.tsx @@ -1,3 +1,4 @@ +import { typedKeys } from "@monkeytype/util/objects"; import { UseQueryResult } from "@tanstack/solid-query"; import { Accessor, @@ -12,7 +13,6 @@ import { import { showErrorNotification } from "../../states/notifications"; import { createErrorMessage } from "../../utils/error"; -import { typedKeys } from "../../utils/misc"; import { LoadingCircle } from "./LoadingCircle"; type AsyncEntry = { diff --git a/frontend/src/ts/components/modals/SimpleModal.tsx b/frontend/src/ts/components/modals/SimpleModal.tsx index d07efad88d59..c7a0337944ae 100644 --- a/frontend/src/ts/components/modals/SimpleModal.tsx +++ b/frontend/src/ts/components/modals/SimpleModal.tsx @@ -1,3 +1,4 @@ +import { typedEntries } from "@monkeytype/util/objects"; import { AnyFieldApi, createForm } from "@tanstack/solid-form"; import { Accessor, @@ -25,7 +26,6 @@ import { SimpleModalInput, } from "../../states/simple-modal"; import { cn } from "../../utils/cn"; -import { typedEntries } from "../../utils/misc"; import { getZodType, unwrapSchema } from "../../utils/zod"; import { AnimatedModal } from "../common/AnimatedModal"; import { Checkbox } from "../ui/form/Checkbox"; diff --git a/frontend/src/ts/components/pages/account/utils.ts b/frontend/src/ts/components/pages/account/utils.ts index 096bfe99631f..e251099295c0 100644 --- a/frontend/src/ts/components/pages/account/utils.ts +++ b/frontend/src/ts/components/pages/account/utils.ts @@ -1,7 +1,8 @@ import { ResultFilters, ResultFiltersSchema } from "@monkeytype/schemas/users"; -import { typedKeys } from "../../../utils/misc"; + import defaultResultFilters from "../../../constants/default-result-filters"; import { sanitize } from "../../../utils/sanitize"; +import { typedKeys } from "@monkeytype/util/objects"; export function mergeWithDefaultFilters( filters: Partial, diff --git a/frontend/src/ts/config/lifecycle.ts b/frontend/src/ts/config/lifecycle.ts index 4f88247173e4..1ef978c67b1a 100644 --- a/frontend/src/ts/config/lifecycle.ts +++ b/frontend/src/ts/config/lifecycle.ts @@ -13,9 +13,10 @@ import { Config, setFullConfigStore } from "./store"; import { getDefaultConfig } from "../constants/default-config"; import { configEvent } from "../events/config"; import { migrateConfig } from "./utils"; -import { promiseWithResolvers, typedKeys } from "../utils/misc"; +import { promiseWithResolvers } from "../utils/misc"; import { setConfig } from "./setters"; import { deleteConfig } from "../ape/config"; +import { typedKeys } from "@monkeytype/util/objects"; export async function applyConfigFromJson(json: string): Promise { try { diff --git a/frontend/src/ts/config/setters.ts b/frontend/src/ts/config/setters.ts index de181296161f..f571baa401be 100644 --- a/frontend/src/ts/config/setters.ts +++ b/frontend/src/ts/config/setters.ts @@ -10,10 +10,11 @@ import { canSetFunboxWithConfig, } from "./funbox-validation"; import * as TestState from "../test/test-state"; -import { typedKeys, triggerResize, escapeHTML } from "../utils/misc"; +import { triggerResize, escapeHTML } from "../utils/misc"; import { camelCaseToWords, capitalizeFirstLetter } from "../utils/strings"; import { Config, setConfigStore } from "./store"; import { FunboxName } from "@monkeytype/schemas/configs"; +import { typedKeys } from "@monkeytype/util/objects"; export function setConfig( key: T, diff --git a/frontend/src/ts/config/utils.ts b/frontend/src/ts/config/utils.ts index 9621b2e717a3..006f7ecd5d85 100644 --- a/frontend/src/ts/config/utils.ts +++ b/frontend/src/ts/config/utils.ts @@ -4,11 +4,11 @@ import type { PartialConfig, FunboxName, } from "@monkeytype/schemas/configs"; -import { typedKeys } from "../utils/misc"; import { sanitize } from "../utils/sanitize"; import * as ConfigSchemas from "@monkeytype/schemas/configs"; import { getDefaultConfig } from "../constants/default-config"; import { Config } from "./store"; +import { typedKeys } from "@monkeytype/util/objects"; /** * migrates possible outdated config and merges with the default config values * @param config partial or possible outdated config diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 0252e8d5e7b2..f963351068ab 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -1,33 +1,24 @@ -import * as Misc from "../utils/misc"; -import * as JSONData from "../utils/json-data"; import { - showNoticeNotification, showErrorNotification, + showNoticeNotification, showSuccessNotification, } from "../states/notifications"; import * as CustomText from "../test/custom-text"; import * as Funbox from "../test/funbox/funbox"; -import { Config } from "../config/store"; import { setConfig } from "../config/setters"; +import { Config } from "../config/store"; import { configEvent } from "../events/config"; import * as TestState from "../test/test-state"; -import { showLoaderBar, hideLoaderBar } from "../states/loader-bar"; -import { CustomTextLimitMode, CustomTextMode } from "@monkeytype/schemas/util"; -import { - Config as ConfigType, - Difficulty, - ThemeName, - FunboxName, -} from "@monkeytype/schemas/configs"; -import { Mode } from "@monkeytype/schemas/shared"; +import { ChallengeSettings, getChallenge } from "@monkeytype/challenges"; +import { ChallengeName } from "@monkeytype/schemas/challenges"; import { CompletedEvent } from "@monkeytype/schemas/results"; +import { typedKeys } from "@monkeytype/util/objects"; +import { hideLoaderBar, showLoaderBar } from "../states/loader-bar"; +import { getLoadedChallenge, setLoadedChallenge } from "../states/test"; import { areUnsortedArraysEqual } from "../utils/arrays"; -import { tryCatch } from "@monkeytype/util/trycatch"; -import { Challenge } from "@monkeytype/schemas/challenges"; import { qs } from "../utils/dom"; -import { getLoadedChallenge, setLoadedChallenge } from "../states/test"; let challengeLoading = false; @@ -44,8 +35,8 @@ export function clearActive(): void { function verifyRequirement( result: CompletedEvent, - requirements: NonNullable, - requirementType: keyof NonNullable, + requirements: NonNullable, + requirementType: keyof NonNullable, ): [boolean, string[]] { let requirementsMet = true; let failReasons: string[] = []; @@ -137,9 +128,9 @@ function verifyRequirement( } } else if (requirementType === "config" && requirements.config) { const requirementValue = requirements.config; - for (const configKey of Misc.typedKeys(requirementValue)) { + for (const configKey of typedKeys(requirementValue)) { const configValue = requirementValue[configKey]; - if (Config[configKey as keyof ConfigType] !== configValue) { + if (Config[configKey] !== configValue) { requirementsMet = false; failReasons.push(`${configKey} not set to ${configValue}`); } @@ -148,7 +139,7 @@ function verifyRequirement( return [requirementsMet, failReasons]; } -export function verify(result: CompletedEvent): string | null { +export function verify(result: CompletedEvent): ChallengeName | null { const loadedChallenge = getLoadedChallenge(); if (loadedChallenge === null) return null; @@ -161,18 +152,18 @@ export function verify(result: CompletedEvent): string | null { return null; } - if (loadedChallenge.requirements === undefined) { + if (loadedChallenge.settings?.requirements === undefined) { showSuccessNotification(`${loadedChallenge.display} challenge passed!`); return loadedChallenge.name || null; } else { let requirementsMet = true; const failReasons: string[] = []; - for (const requirementType of Misc.typedKeys( - loadedChallenge.requirements, + for (const requirementType of typedKeys( + loadedChallenge.settings.requirements, )) { const [passed, requirementFailReasons] = verifyRequirement( result, - loadedChallenge.requirements, + loadedChallenge.settings.requirements, requirementType, ); if (!passed) { @@ -181,7 +172,7 @@ export function verify(result: CompletedEvent): string | null { failReasons.push(...requirementFailReasons); } if (requirementsMet) { - if (loadedChallenge.autoRole) { + if (loadedChallenge.settings.autoRole) { showSuccessNotification( "You will receive a role shortly. Please don't post a screenshot in challenge submissions.", { durationMs: 5000 }, @@ -207,36 +198,26 @@ export function verify(result: CompletedEvent): string | null { } } -export async function setup(challengeName: string): Promise { +export async function setup(challengeName: ChallengeName): Promise { challengeLoading = true; setConfig("funbox", []); - const { data: list, error } = await tryCatch(JSONData.getChallengeList()); - if (error) { - showErrorNotification("Failed to setup challenge", { error }); - setTimeout(() => { - qs("header .config")?.show(); - qs(".page.pageTest")?.show(); - }, 250); - return false; - } + const challenge = getChallenge(challengeName); + const settings = challenge.settings; - const challenge = list.find( - (c) => c.name.toLowerCase() === challengeName.toLowerCase(), - ); let notitext; try { - if (challenge === undefined) { - showNoticeNotification("Challenge not found"); + if (challenge === undefined || settings === undefined) { + showNoticeNotification("Challenge not found or missing settings"); setTimeout(() => { qs("header .config")?.show(); qs(".page.pageTest")?.show(); }, 250); return false; } - if (challenge.type === "customTime") { - setConfig("time", challenge.parameters[0] as number, { + if (settings.type === "customTime") { + setConfig("time", settings.parameters.time, { nosave: true, }); setConfig("mode", "time", { @@ -245,7 +226,7 @@ export async function setup(challengeName: string): Promise { setConfig("difficulty", "normal", { nosave: true, }); - if (challenge.name === "englishMaster") { + if (challengeName === "englishMaster") { setConfig("language", "english_10k", { nosave: true, }); @@ -256,8 +237,8 @@ export async function setup(challengeName: string): Promise { nosave: true, }); } - } else if (challenge.type === "customWords") { - setConfig("words", challenge.parameters[0] as number, { + } else if (settings.type === "customWords") { + setConfig("words", settings.parameters.words, { nosave: true, }); setConfig("mode", "words", { @@ -266,23 +247,21 @@ export async function setup(challengeName: string): Promise { setConfig("difficulty", "normal", { nosave: true, }); - } else if (challenge.type === "customText") { - CustomText.setText((challenge.parameters[0] as string).split(" ")); - CustomText.setMode(challenge.parameters[1] as CustomTextMode); - CustomText.setLimitValue(challenge.parameters[2] as number); - CustomText.setLimitMode(challenge.parameters[3] as CustomTextLimitMode); - CustomText.setPipeDelimiter(challenge.parameters[4] as boolean); + } else if (settings.type === "customText") { + CustomText.setText(settings.parameters.text.split(" ")); + CustomText.setMode(settings.parameters.mode); + CustomText.setLimitValue(settings.parameters.limit); + CustomText.setLimitMode(settings.parameters.limitMode); + CustomText.setPipeDelimiter(settings.parameters.isPipeDelimiter); setConfig("mode", "custom", { nosave: true, }); setConfig("difficulty", "normal", { nosave: true, }); - } else if (challenge.type === "script") { + } else if (settings.type === "script") { showLoaderBar(); - const response = await fetch( - `/challenges/${challenge.parameters[0] as string}`, - ); + const response = await fetch(`/challenges/${settings.parameters.script}`); hideLoaderBar(); if (response.status !== 200) { throw new Error(`${response.status} ${response.statusText}`); @@ -301,13 +280,13 @@ export async function setup(challengeName: string): Promise { setConfig("difficulty", "normal", { nosave: true, }); - if (challenge.parameters[1] !== null) { - setConfig("theme", challenge.parameters[1] as ThemeName); + if (settings.parameters.theme !== undefined) { + setConfig("theme", settings.parameters.theme); } - if (challenge.parameters[2] !== null) { - void Funbox.activate(challenge.parameters[2] as FunboxName[]); + if (settings.parameters.funboxes !== undefined) { + void Funbox.activate(settings.parameters.funboxes); } - } else if (challenge.type === "accuracy") { + } else if (settings.type === "accuracy") { setConfig("time", 0, { nosave: true, }); @@ -317,63 +296,37 @@ export async function setup(challengeName: string): Promise { setConfig("difficulty", "master", { nosave: true, }); - } else if (challenge.type === "funbox") { + } else if (settings.type === "funbox") { setConfig("difficulty", "normal", { nosave: true, }); - if (challenge.parameters[1] === "words") { - setConfig("words", challenge.parameters[2] as number, { + if (settings.parameters.mode === "words") { + setConfig("words", settings.parameters.mode2, { nosave: true, }); - } else if (challenge.parameters[1] === "time") { - setConfig("time", challenge.parameters[2] as number, { + } else if (settings.parameters.mode === "time") { + setConfig("time", settings.parameters.mode2, { nosave: true, }); } - setConfig("mode", challenge.parameters[1] as Mode, { + setConfig("mode", settings.parameters.mode, { nosave: true, }); - if (challenge.parameters[3] !== undefined) { - setConfig("difficulty", challenge.parameters[3] as Difficulty, { + if (settings.parameters.difficulty !== undefined) { + setConfig("difficulty", settings.parameters.difficulty, { nosave: true, }); } if ( - !setConfig("funbox", challenge.parameters[0] as FunboxName[], { + !setConfig("funbox", [settings.parameters.funbox], { nosave: true, }) ) { throw new Error("Can't load challenge with current config"); } - } else if (challenge.type === "other") { - if (challenge.name === "semimak") { - // so can you make a link that sets up 120s, 10k, punct, stop on word, and semimak as the layout? - setConfig("mode", "time", { - nosave: true, - }); - setConfig("time", 120, { - nosave: true, - }); - setConfig("language", "english_10k", { - nosave: true, - }); - setConfig("punctuation", true, { - nosave: true, - }); - setConfig("stopOnError", "word", { - nosave: true, - }); - setConfig("layout", "semimak", { - nosave: true, - }); - setConfig("keymapLayout", "overrideSync", { - nosave: true, - }); - setConfig("keymapMode", "static", { - nosave: true, - }); - } else if (challenge.name === "wingdings") { + } else if (settings.type === "other") { + if (challengeName === "wingdings") { // Ten Words of Pain: 10-word Master mode test using the Wingdings custom font, no keymap setConfig("mode", "words", { nosave: true, @@ -392,7 +345,7 @@ export async function setup(challengeName: string): Promise { }); } } - notitext = challenge.message; + notitext = settings.message; qs("header .config")?.show(); qs(".page.pageTest")?.show(); diff --git a/frontend/src/ts/controllers/chart-controller.ts b/frontend/src/ts/controllers/chart-controller.ts index 636a0c51e09e..01ea9b02b32f 100644 --- a/frontend/src/ts/controllers/chart-controller.ts +++ b/frontend/src/ts/controllers/chart-controller.ts @@ -60,12 +60,13 @@ import { Config } from "../config/store"; import { configEvent } from "../events/config"; import * as Arrays from "../utils/arrays"; import { blendTwoHexColors } from "../utils/colors"; -import { typedKeys } from "../utils/misc"; + import { getTheme } from "../states/theme"; import { Theme } from "../constants/themes"; import { createDebouncedEffectOn } from "../hooks/effects"; import { getWordIndexesForSecond } from "../test/events/stats"; import { lastEventLog } from "../test/test-state"; +import { typedKeys } from "@monkeytype/util/objects"; export class ChartWithUpdateColors< TType extends ChartType = ChartType, diff --git a/frontend/src/ts/controllers/url-handler.tsx b/frontend/src/ts/controllers/url-handler.tsx index 83ec39952c7f..3b5c326f7258 100644 --- a/frontend/src/ts/controllers/url-handler.tsx +++ b/frontend/src/ts/controllers/url-handler.tsx @@ -1,3 +1,4 @@ +import { ChallengeName } from "@monkeytype/schemas/challenges"; import { CustomBackgroundFilter, CustomBackgroundFilterSchema, @@ -317,7 +318,7 @@ export async function loadChallengeFromUrl( ).toLowerCase(); if (getValue === "") return; - ChallengeController.setup(getValue) + ChallengeController.setup(getValue as ChallengeName) .then((result) => { if (result) { restartTest({ diff --git a/frontend/src/ts/states/test.ts b/frontend/src/ts/states/test.ts index 9654775e29f0..367452e23cd9 100644 --- a/frontend/src/ts/states/test.ts +++ b/frontend/src/ts/states/test.ts @@ -1,5 +1,4 @@ import { createSignal, createEffect, createMemo } from "solid-js"; -import { Challenge } from "@monkeytype/schemas/challenges"; import { getConfig } from "../config/store"; import { canQuickRestart } from "../utils/quick-restart"; @@ -8,6 +7,7 @@ import { getActivePage, getCustomTextIndicator } from "./core"; import { QuoteWithTextSplit } from "../types/quotes"; import { CompletedEvent, IncompleteTest } from "@monkeytype/schemas/results"; import { createSignalWithSetters } from "../hooks/createSignalWithSetters"; +import { Challenge } from "@monkeytype/challenges"; export const [wordsHaveNewline, setWordsHaveNewline] = createSignal(false); export const [wordsHaveTab, setWordsHaveTab] = createSignal(false); diff --git a/frontend/src/ts/utils/json-data.ts b/frontend/src/ts/utils/json-data.ts index 7b773eb19a20..0d19df8f0639 100644 --- a/frontend/src/ts/utils/json-data.ts +++ b/frontend/src/ts/utils/json-data.ts @@ -1,9 +1,8 @@ import { Language, LanguageObject } from "@monkeytype/schemas/languages"; -import { Challenge } from "@monkeytype/schemas/challenges"; import { LayoutObject } from "@monkeytype/schemas/layouts"; -import { toHex } from "./strings"; import { languageHashes } from "virtual:language-hashes"; import { isDevEnvironment } from "./env"; +import { toHex } from "./strings"; //pin implementation const fetch = window.fetch; @@ -154,15 +153,6 @@ export class Section { export type FunboxWordOrder = "normal" | "reverse"; -/** - * Fetches the list of challenges from the server. - * @returns A promise that resolves to the list of challenges. - */ -export async function getChallengeList(): Promise { - const data = await cachedFetchJson("/challenges/_list.json"); - return data; -} - /** * Fetches the list of supporters from the server. * @returns A promise that resolves to the list of supporters. diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 6480e38ae000..669d93b3bba5 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -473,18 +473,6 @@ export function getBoundingRectOfElements(elements: HTMLElement[]): DOMRect { }; } -export function typedKeys( - obj: T, -): T extends T ? (keyof T)[] : never { - return Object.keys(obj) as unknown as T extends T ? (keyof T)[] : never; -} - -export function typedEntries( - obj: T, -): { [K in keyof T]: [K, T[K]] }[keyof T][] { - return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][]; -} - export function reloadAfter(seconds: number): void { setTimeout(() => { window.location.reload(); diff --git a/frontend/static/challenges/_list.json b/frontend/static/challenges/_list.json deleted file mode 100644 index a94b95aa6858..000000000000 --- a/frontend/static/challenges/_list.json +++ /dev/null @@ -1,720 +0,0 @@ -[ - { - "name": "oneHourWarrior", - "display": "One Hour Warrior", - "autoRole": true, - "type": "customTime", - "parameters": [3600], - "requirements": { - "time": { - "min": 3600 - } - } - }, - { - "name": "doubleDown", - "display": "Double Down", - "autoRole": true, - "type": "customTime", - "parameters": [7200], - "requirements": { - "time": { - "min": 7200 - } - } - }, - { - "name": "tripleTrouble", - "display": "Triple Trouble", - "autoRole": true, - "type": "customTime", - "parameters": [10800], - "requirements": { - "time": { - "min": 10800 - } - } - }, - { - "name": "quad", - "display": "Quaaaaad", - "autoRole": true, - "type": "customTime", - "parameters": [14400], - "requirements": { - "time": { - "min": 14400 - } - } - }, - { - "name": "8Ball", - "display": "8 Ball", - "type": "customTime", - "parameters": [28800], - "requirements": { - "time": { - "min": 28800 - } - } - }, - { - "name": "theBig12", - "display": "The Big 12", - "type": "customTime", - "parameters": [43200], - "requirements": { - "time": { - "min": 43200 - } - } - }, - { - "name": "1Day", - "display": "1 Day", - "type": "customTime", - "parameters": [86400], - "requirements": { - "time": { - "min": 86400 - } - } - }, - { - "name": "trueSimp", - "display": "True Simp", - "autoRole": true, - "type": "customText", - "parameters": ["miodec", "repeat", 10000, "word", false] - }, - { - "name": "bigramSalad", - "display": "Bigram Salad", - "autoRole": true, - "type": "customText", - "parameters": [ - "to of in it is as at be we he so on an or do if up by my go", - "random", - 100, - "word", - false - ], - "requirements": { - "wpm": { - "min": 100 - } - } - }, - { - "name": "simp", - "display": "Simp", - "autoRole": true, - "type": "customText", - "parameters": ["miodec", "repeat", 1000, "word", false] - }, - { - "name": "antidiseWhat", - "display": "Antidise-what?", - "autoRole": true, - "type": "customText", - "parameters": ["antidisestablishmentarianism", "repeat", 1, "word", false], - "requirements": { - "wpm": { - "min": 200 - } - } - }, - { - "name": "whatsThisWebsiteCalledAgain", - "display": "What's this website called again?", - "autoRole": true, - "type": "customText", - "parameters": ["monkeytype", "repeat", 1000, "word", false] - }, - { - "name": "developd", - "display": "Develop'd", - "autoRole": true, - "type": "customText", - "parameters": ["develop", "repeat", 1000, "word", false] - }, - { - "name": "slowAndSteady", - "display": "Slow and Steady", - "autoRole": true, - "type": "customTime", - "parameters": [300], - "requirements": { - "wpm": { - "exact": 60 - }, - "config": { - "liveSpeedStyle": "off", - "paceCaret": "off" - } - } - }, - { - "name": "speedSpacer", - "display": "Speed Spacer", - "autoRole": true, - "type": "customText", - "parameters": [ - "a b c d e f g h i j k l m n o p q r s t u v w x y z", - "random", - 100, - "word", - false - ], - "requirements": { - "wpm": { - "min": 100 - } - } - }, - { - "name": "iveGotThePower", - "display": "I've got the POWER", - "autoRole": true, - "type": "customText", - "parameters": ["power", "repeat", 10, "word", false], - "requirements": { - "wpm": { - "min": 400 - } - } - }, - { - "name": "accuracyExpert", - "display": "Accuracy Expert", - "autoRole": true, - "type": "accuracy", - "parameters": [], - "message": "Minimum 60wpm and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 60 - }, - "acc": { - "exact": 100 - }, - "afk": { - "max": 5 - }, - "time": { - "min": 600 - } - } - }, - { - "name": "accuracyMaster", - "display": "Accuracy Master", - "autoRole": true, - "type": "accuracy", - "parameters": [], - "message": "Minimum 60wpm and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 60 - }, - "acc": { - "exact": 100 - }, - "afk": { - "max": 5 - }, - "time": { - "min": 1200 - } - } - }, - { - "name": "accuracyGod", - "display": "Accuracy God", - "autoRole": true, - "type": "accuracy", - "parameters": [], - "message": "Minimum 60wpm and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 60 - }, - "acc": { - "exact": 100 - }, - "afk": { - "max": 5 - }, - "time": { - "min": 1800 - } - } - }, - { - "name": "inAGalaxyFarFarAway", - "display": "In a galaxy far far away", - "type": "script", - "parameters": ["episode4.txt", null, ["space_balls"]], - "requirements": { - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "beepBoop", - "display": "Beep Boop", - "type": "script", - "parameters": ["beepboop.txt", null, ["nospace"]], - "message": "Mininum 45 WPM and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 45 - }, - "acc": { - "min": 100 - }, - "funbox": { - "exact": ["nospace"] - } - } - }, - { - "name": "whosYourDaddy", - "display": "Who's your daddy?", - "type": "script", - "parameters": ["episode5.txt", null, ["space_balls"]], - "requirements": { - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "itsATrap", - "display": "It's a trap!", - "type": "script", - "parameters": ["episode6.txt", null, ["space_balls"]], - "requirements": { - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "jolly", - "display": "Jolly", - "autoRole": true, - "type": "script", - "parameters": ["jolly.txt", null, null], - "message": "Minimum 70wpm required.", - "requirements": { - "wpm": { - "min": 70 - } - } - }, - { - "name": "gottaCatchEmAll", - "display": "Gotta catch 'em all", - "autoRole": true, - "type": "script", - "parameters": ["pokemon.txt", null, null] - }, - { - "name": "rapGod", - "display": "Rap God", - "autoRole": true, - "type": "script", - "parameters": ["rapgod.txt", null, null], - "message": "Minimum 85wpm and 90% accuracy required.", - "requirements": { - "wpm": { - "min": 85 - }, - "acc": { - "min": 90 - }, - "afk": { - "max": 5 - } - } - }, - { - "name": "navySeal", - "display": "Navy Seal", - "autoRole": true, - "type": "script", - "parameters": ["navyseal.txt", null, null], - "message": "Minimum 60wpm and 100% accuracy required.", - "requirements": { - "wpm": { - "min": 60 - }, - "acc": { - "exact": 100 - }, - "afk": { - "max": 5 - } - } - }, - { - "name": "littleChef", - "display": "Little Chef", - "type": "script", - "parameters": ["littlechef.txt", null, null] - }, - { - "name": "crosstalk", - "display": "(CROSSTALK)", - "type": "script", - "parameters": ["crosstalk.txt", null, null] - }, - { - "name": "bees", - "display": "Bees!", - "type": "script", - "parameters": ["bees.txt", null, null] - }, - { - "name": "getOffMySwamp", - "display": "Get off my swamp", - "type": "script", - "parameters": ["shrek.txt", null, null] - }, - { - "name": "lookAtMeIAmTheDeveloperNow", - "display": "Look at me. I am the developer now.", - "autoRole": true, - "type": "script", - "parameters": ["sourcecode.txt", null, null] - }, - { - "name": "beLikeWater", - "display": "Be like water", - "type": "funbox", - "parameters": [["layoutfluid"], "time", 60], - "message": "Remember: You need to achieve at least 50 wpm in each layout." - }, - { - "name": "rollercoaster", - "display": "Rollercoaster", - "autoRole": true, - "type": "funbox", - "parameters": [["round_round_baby"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["round_round_baby"] - } - } - }, - { - "name": "oneHourMirror", - "display": "ɿoɿɿim ɿυoʜ ɘno", - "autoRole": true, - "type": "funbox", - "parameters": [["mirror"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["mirror"] - } - } - }, - { - "name": "chooChoo", - "display": "Choo choo", - "autoRole": true, - "type": "funbox", - "parameters": [["choo_choo"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["choo_choo"] - } - } - }, - { - "name": "mnemonist", - "display": "Mnemonist", - "type": "funbox", - "parameters": [["memory"], "words", 25, "master"], - "requirements": { - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "earfquake", - "display": "Earfquake", - "autoRole": true, - "type": "funbox", - "parameters": [["earthquake"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["earthquake"] - } - } - }, - { - "name": "simonSez", - "display": "Simon Sez", - "autoRole": true, - "type": "funbox", - "parameters": [["simon_says"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["simon_says"] - } - } - }, - { - "name": "accountant", - "display": "Accountant", - "autoRole": true, - "type": "funbox", - "parameters": [["58008"], "time", 3600], - "requirements": { - "time": { - "min": 3600 - }, - "funbox": { - "exact": ["58008"] - } - } - }, - { - "name": "hidden", - "display": "Hidden", - "autoRole": true, - "type": "funbox", - "parameters": [["read_ahead"], "time", 60], - "requirements": { - "wpm": { - "min": 100 - }, - "time": { - "min": 60 - }, - "funbox": { - "exact": ["read_ahead"] - }, - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "iCanSeeTheFuture", - "display": "I can see the future", - "autoRole": true, - "type": "funbox", - "parameters": [["read_ahead_hard"], "time", 60], - "requirements": { - "wpm": { - "min": 100 - }, - "time": { - "min": 60 - }, - "funbox": { - "exact": ["read_ahead_hard"] - }, - "config": { - "tapeMode": "off" - } - } - }, - { - "name": "whatAreWordsAtThisPoint", - "display": "What are words at this point?", - "autoRole": true, - "type": "funbox", - "parameters": [["gibberish"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["gibberish"] - } - } - }, - { - "name": "specials", - "display": "Specials", - "autoRole": true, - "type": "funbox", - "parameters": [["specials"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["specials"] - } - } - }, - { - "name": "aeiou", - "display": "Aeiou.", - "autoRole": true, - "type": "funbox", - "parameters": [["tts"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["tts"] - } - } - }, - { - "name": "asciiWarrior", - "display": "ASCII warrior", - "autoRole": true, - "type": "funbox", - "parameters": [["ascii"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["ascii"] - } - } - }, - { - "name": "iKINdaLikEHoWinEFFICIeNtQwErtYIs.", - "display": "i KINda LikE HoW inEFFICIeNt QwErtY Is.", - "autoRole": true, - "type": "funbox", - "parameters": [["sPoNgEcAsE"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["sPoNgEcAsE"] - } - } - }, - { - "name": "oneNauseousMonkey", - "display": "One Nauseous Monkey", - "autoRole": true, - "type": "funbox", - "parameters": [["nausea"], "time", 3600], - "requirements": { - "time": { - "min": 60 - }, - "funbox": { - "exact": ["nausea"] - } - } - }, - { - "name": "thumbWarrior", - "display": "Thumb warrior", - "type": "customTime", - "parameters": [3600] - }, - { - "name": "mouseWarrior", - "display": "Mouse warrior", - "type": "customTime", - "parameters": [3600] - }, - { - "name": "mobileWarrior", - "display": "Mobile warrior", - "type": "customTime", - "parameters": [3600] - }, - { - "name": "69", - "display": "6969696969", - "autoRole": true, - "type": "customTime", - "parameters": [69], - "message": "You need to achieve 69 wpm, 69 raw, 69% accuracy and 69% consistency.", - "requirements": { - "wpm": { - "exact": 69 - }, - "raw": { - "exact": 69 - }, - "acc": { - "exact": 69 - }, - "con": { - "exact": 69 - } - } - }, - { - "name": "upsideDown", - "display": "Upside down", - "type": "customTime", - "parameters": [60] - }, - { - "name": "oneArmedBandit", - "display": "One armed bandit", - "type": "customWords", - "parameters": [10000] - }, - { - "name": "englishMaster", - "display": "English master", - "autoRole": true, - "type": "customTime", - "parameters": [3600], - "requirements": { - "time": { - "min": 3600 - }, - "config": { - "language": "english_10k", - "punctuation": true, - "numbers": true - } - } - }, - { - "name": "feetWarrior", - "display": "Feet warrior", - "type": "customTime", - "parameters": [3600] - }, - { - "name": "wingdings", - "display": "Ten Words of Pain", - "type": "other", - "parameters": [], - "message": "Complete a 10-word Master mode test using the Wingdings custom font. No keymap allowed. Minimum 60 WPM and 100% accuracy required.", - "requirements": { - "acc": { - "exact": 100 - } - } - } -] diff --git a/packages/challenges/.oxlintrc.json b/packages/challenges/.oxlintrc.json new file mode 100644 index 000000000000..f6a8e7c07d0a --- /dev/null +++ b/packages/challenges/.oxlintrc.json @@ -0,0 +1,7 @@ +{ + "ignorePatterns": ["node_modules", "dist", ".turbo"], + "extends": [ + "../oxlint-config/index.jsonc" + // "@monkeytype/oxlint-config" + ] +} diff --git a/packages/challenges/__test__/tsconfig.json b/packages/challenges/__test__/tsconfig.json new file mode 100644 index 000000000000..bc5ae47e535d --- /dev/null +++ b/packages/challenges/__test__/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@monkeytype/typescript-config/base.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["./**/*.ts", "./**/*.spec.ts", "./setup-tests.ts"] +} diff --git a/packages/challenges/package.json b/packages/challenges/package.json new file mode 100644 index 000000000000..da5317a27d41 --- /dev/null +++ b/packages/challenges/package.json @@ -0,0 +1,36 @@ +{ + "name": "@monkeytype/challenges", + "private": true, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "dev": "tsup-node --watch", + "build": "npm run madge && tsup-node", + "test": "vitest run", + "madge": " madge --circular --extensions ts ./src", + "ts-check": "tsc --noEmit", + "lint": "oxlint . --type-aware --type-check", + "lint-fast": "oxlint .", + "george-mapping": "tsx ./scripts/challenge-roles" + }, + "dependencies": { + "@monkeytype/schemas": "workspace:*", + "@monkeytype/util": "workspace:*" + }, + "devDependencies": { + "@monkeytype/tsup-config": "workspace:*", + "@monkeytype/typescript-config": "workspace:*", + "@types/node": "24.9.1", + "madge": "8.0.0", + "oxlint": "1.68.0", + "oxlint-tsgolint": "0.23.0", + "tsup": "8.4.0", + "typescript": "6.0.2", + "vitest": "4.1.0" + } +} diff --git a/packages/challenges/scripts/challenge-roles.ts b/packages/challenges/scripts/challenge-roles.ts new file mode 100644 index 000000000000..80c54edf22c7 --- /dev/null +++ b/packages/challenges/scripts/challenge-roles.ts @@ -0,0 +1,8 @@ +import { getChallenges } from "../src/index"; + +const known = Object.fromEntries( + getChallenges().map((it) => [it.name, it.discordRoleId]), +); + +console.log("roleid mapping"); +console.log(JSON.stringify(known, null, 2)); diff --git a/packages/challenges/src/index.ts b/packages/challenges/src/index.ts new file mode 100644 index 000000000000..1c1374e910df --- /dev/null +++ b/packages/challenges/src/index.ts @@ -0,0 +1,871 @@ +import { ChallengeName } from "@monkeytype/schemas/challenges"; +import { + Config, + Difficulty, + FunboxName, + ThemeName, +} from "@monkeytype/schemas/configs"; +import { Mode } from "@monkeytype/schemas/shared"; +import { CustomTextLimitMode, CustomTextMode } from "@monkeytype/schemas/util"; + +export type Challenge = { + name: ChallengeName; + display: string; + description: string; + isHidden?: boolean; + discordRoleId: string; + category: + | "other" + | "endurance" + | "script" + | "speed" + | "accuracy" + | "funbox" + | "champions" + | "roleCount"; + settings: ChallengeSettings; +}; + +type ChallengeParameter = + | { + type: "customTime"; + parameters: { time: number }; + } + | { type: "customWords"; parameters: { words: number } } + | { + type: "customText"; + parameters: { + text: string; + mode: CustomTextMode; + limit: number; + limitMode: CustomTextLimitMode; + isPipeDelimiter: boolean; + }; + } + | { + type: "script"; + parameters: { + script: string; + theme?: ThemeName; + funboxes?: FunboxName[]; + }; + } + | { type: "accuracy" } + | { + type: "funbox"; + parameters: { + funbox: FunboxName; + difficulty?: Difficulty; + } & ( + | { mode: "time" | "words"; mode2: number } + | { mode: Exclude } + ); + } + | { type: "other" }; + +export type ChallengeSettings = { + autoRole?: boolean; + message?: string; + requirements?: { + wpm?: { min: number } | { exact: number }; + acc?: { min: number } | { exact: number }; + raw?: { exact: number }; + con?: { exact: number }; + afk?: { max: number }; + time?: { min: number }; + funbox?: { exact: FunboxName[] }; + config?: Partial; + }; +} & ChallengeParameter; + +const challenges: Record> = { + "69": { + display: "6969696969", + discordRoleId: "749505965174292511", + category: "other", + description: + "Complete a 69-second test and achieve 69 WPM, 69 raw, 69% accuracy, and 69% consistency.", + settings: { + autoRole: true, + type: "customTime", + message: + "You need to achieve 69 wpm, 69 raw, 69% accuracy and 69% consistency.", + parameters: { time: 69 }, + requirements: { + wpm: { exact: 69 }, + raw: { exact: 69 }, + acc: { exact: 69 }, + con: { exact: 69 }, + }, + }, + }, + oneHourWarrior: { + display: "One Hour Warrior", + discordRoleId: "728371749737201855", + category: "endurance", + description: "Complete a one-hour test.", + settings: { + autoRole: true, + type: "customTime", + parameters: { time: 3600 }, + requirements: { time: { min: 3600 } }, + }, + }, + doubleDown: { + display: "Double Down", + discordRoleId: "732008008514535544", + category: "endurance", + description: "Complete a two-hour test.", + settings: { + autoRole: true, + type: "customTime", + parameters: { time: 7200 }, + requirements: { time: { min: 7200 } }, + }, + }, + tripleTrouble: { + display: "Triple Trouble", + discordRoleId: "732008047618293762", + category: "endurance", + description: "Complete a three-hour test.", + settings: { + autoRole: true, + type: "customTime", + parameters: { time: 10800 }, + requirements: { time: { min: 10800 } }, + }, + }, + quad: { + display: "Quaaaaad", + discordRoleId: "736215666352455801", + category: "endurance", + description: "Complete a four-hour test.", + settings: { + autoRole: true, + type: "customTime", + parameters: { time: 14400 }, + requirements: { time: { min: 14400 } }, + }, + }, + "8Ball": { + display: "8 Ball", + discordRoleId: "736528159956271126", + category: "endurance", + description: "Complete an eight-hour test.", + settings: { + type: "customTime", + parameters: { time: 28800 }, + requirements: { time: { min: 28800 } }, + }, + }, + theBig12: { + display: "The Big 12", + discordRoleId: "740532256388546581", + category: "endurance", + description: "Complete a twelve-hour test.", + settings: { + type: "customTime", + parameters: { time: 43200 }, + requirements: { time: { min: 43200 } }, + }, + }, + "1Day": { + display: "1 Day", + discordRoleId: "751801958511149057", + category: "endurance", + description: "Complete a twenty-four-hour test.", + settings: { + type: "customTime", + parameters: { time: 86400 }, + requirements: { time: { min: 86400 } }, + }, + }, + trueSimp: { + display: "True Simp", + discordRoleId: "744328648211038359", + category: "script", + description: "Type miodec ten thousand times.", + settings: { + autoRole: true, + type: "customText", + parameters: { + text: "miodec", + mode: "repeat", + limit: 10000, + limitMode: "word", + isPipeDelimiter: false, + }, + }, + }, + bigramSalad: { + display: "Bigram Salad", + discordRoleId: "818535054145093652", + category: "speed", + description: + "Get 100 WPM on a randomized, 100-word custom test with the words list: to of in it is as at be we he so on an or do if up by my go.", + settings: { + autoRole: true, + type: "customText", + parameters: { + text: "to of in it is as at be we he so on an or do if up by my go", + mode: "random", + limit: 100, + limitMode: "word", + isPipeDelimiter: false, + }, + requirements: { wpm: { min: 100 } }, + }, + }, + simp: { + display: "Simp", + discordRoleId: "743854992699687023", + category: "script", + description: "Type miodec one thousand times.", + settings: { + autoRole: true, + type: "customText", + parameters: { + text: "miodec", + mode: "repeat", + limit: 1000, + limitMode: "word", + isPipeDelimiter: false, + }, + }, + }, + simpLord: { + display: "Simp Lord", + discordRoleId: "984911956949479445", + category: "script", + description: "Type miodec one hundred thousand times.", + settings: { + type: "customText", + parameters: { + text: "miodec", + mode: "repeat", + limit: 100000, + limitMode: "word", + isPipeDelimiter: false, + }, + }, + }, + antidiseWhat: { + display: "Antidise-what?", + discordRoleId: "782006507360616449", + category: "script", + description: "Get at least 200 wpm typing antidisestablishmentarianism.", + settings: { + autoRole: true, + type: "customText", + parameters: { + text: "antidisestablishmentarianism", + mode: "repeat", + limit: 1, + limitMode: "word", + isPipeDelimiter: false, + }, + requirements: { wpm: { min: 200 } }, + }, + }, + whatsThisWebsiteCalledAgain: { + display: "What's this website called again?", + discordRoleId: "739276161603076116", + category: "script", + description: "Type monkeytype one thousand times.", + settings: { + autoRole: true, + type: "customText", + parameters: { + text: "monkeytype", + mode: "repeat", + limit: 1000, + limitMode: "word", + isPipeDelimiter: false, + }, + }, + }, + developd: { + display: "Develop'd", + discordRoleId: "735964917877964932", + category: "script", + description: "Type develop one thousand times.", + settings: { + autoRole: true, + type: "customText", + parameters: { + text: "develop", + mode: "repeat", + limit: 1000, + limitMode: "word", + isPipeDelimiter: false, + }, + }, + }, + slowAndSteady: { + display: "Slow and Steady", + discordRoleId: "782005061935956008", + category: "speed", + description: + "Complete a 5-minute test with exactly 60 WPM without using the live WPM or pace caret.", + settings: { + autoRole: true, + type: "customTime", + parameters: { time: 300 }, + requirements: { + wpm: { exact: 60 }, + config: { liveSpeedStyle: "off", paceCaret: "off" }, + }, + }, + }, + speedSpacer: { + display: "Speed Spacer", + discordRoleId: "755244049446731856", + category: "speed", + description: + "Get 100 wpm on a randomised custom test with the input: a b c d e f g h i j k l m n o p q r s t u v w x y z (the alphabet) and a word count of 100.", + settings: { + autoRole: true, + type: "customText", + parameters: { + text: "a b c d e f g h i j k l m n o p q r s t u v w x y z", + mode: "random", + limit: 100, + limitMode: "word", + isPipeDelimiter: false, + }, + requirements: { wpm: { min: 100 } }, + }, + }, + iveGotThePower: { + display: "I've got the POWER", + discordRoleId: "764879734873915402", + category: "speed", + description: "Get 400 WPM while typing power 10 times.", + settings: { + autoRole: true, + type: "customText", + parameters: { + text: "power", + mode: "repeat", + limit: 10, + limitMode: "word", + isPipeDelimiter: false, + }, + requirements: { wpm: { min: 400 } }, + }, + }, + accuracyExpert: { + display: "Accuracy Expert", + discordRoleId: "751168451263070259", + category: "accuracy", + description: "Complete a 10-minute Master mode test.", + settings: { + autoRole: true, + type: "accuracy", + message: "Minimum 60wpm and 100% accuracy required.", + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + time: { min: 600 }, + }, + }, + }, + accuracyMaster: { + display: "Accuracy Master", + discordRoleId: "751168567432708239", + category: "accuracy", + description: "Complete a 20-minute Master mode test.", + settings: { + autoRole: true, + type: "accuracy", + message: "Minimum 60wpm and 100% accuracy required.", + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + time: { min: 1200 }, + }, + }, + }, + accuracyGod: { + display: "Accuracy God", + discordRoleId: "751168657626890361", + category: "accuracy", + description: "Complete a 30-minute Master mode test.", + settings: { + autoRole: true, + type: "accuracy", + message: "Minimum 60wpm and 100% accuracy required.", + requirements: { + wpm: { min: 60 }, + acc: { exact: 100 }, + afk: { max: 5 }, + time: { min: 1800 }, + }, + }, + }, + inAGalaxyFarFarAway: { + display: "In a galaxy far, far away", + discordRoleId: "740004324301602907", + category: "script", + description: + "Type out the entire Star Wars Episode 4 script with punctuation while watching the movie simultaneously.", + settings: { + type: "script", + parameters: { script: "episode4.txt", funboxes: ["space_balls"] }, + requirements: { config: { tapeMode: "off" } }, + }, + }, + beepBoop: { + display: "Beep Boop", + discordRoleId: "813076265145729024", + category: "script", + description: + "Type the beepboop script with 100% accuracy and at least 45 WPM.", + settings: { + type: "script", + message: "Mininum 45 WPM and 100% accuracy required.", + parameters: { script: "beepboop.txt", funboxes: ["nospace"] }, + requirements: { + wpm: { min: 45 }, + acc: { min: 100 }, + funbox: { exact: ["nospace"] }, + }, + }, + }, + whosYourDaddy: { + display: "Who's your daddy?", + discordRoleId: "742171915405361204", + category: "script", + description: + "Type out the entire Star Wars Episode 5 script with punctuation while watching the movie simultaneously.", + settings: { + type: "script", + parameters: { script: "episode5.txt", funboxes: ["space_balls"] }, + requirements: { config: { tapeMode: "off" } }, + }, + }, + itsATrap: { + display: "It's a trap!!", + discordRoleId: "744325174668820550", + category: "script", + description: + "Type out the entire Star Wars Episode 6 script with punctuation while watching the movie simultaneously.", + settings: { + type: "script", + parameters: { script: "episode6.txt", funboxes: ["space_balls"] }, + requirements: { config: { tapeMode: "off" } }, + }, + }, + jolly: { + display: "Jolly", + discordRoleId: "768497412548329563", + category: "script", + description: "Type the Jolly script with a minimum of 70 wpm.", + settings: { + autoRole: true, + type: "script", + message: "Minimum 70wpm required.", + parameters: { script: "jolly.txt" }, + requirements: { wpm: { min: 70 } }, + }, + }, + gottaCatchEmAll: { + display: "Gotta catch 'em all", + discordRoleId: "767069340599975998", + category: "script", + description: "Type out the names of all Pokemon.", + settings: { + autoRole: true, + type: "script", + parameters: { script: "pokemon.txt" }, + }, + }, + rapGod: { + display: "Rap God", + discordRoleId: "743844891045396603", + category: "script", + description: + "Type out the lyrics of Eminem's Rap God at a minimum of 85 WPM and 90% accuracy, including punctuation.", + settings: { + autoRole: true, + type: "script", + message: "Minimum 85wpm and 90% accuracy required.", + parameters: { script: "rapgod.txt" }, + requirements: { wpm: { min: 85 }, acc: { min: 90 }, afk: { max: 5 } }, + }, + }, + navySeal: { + display: "Navy Seal", + discordRoleId: "762345535969165342", + category: "script", + description: + "Type out the Navy Seal copy pasta with 100% accuracy and minimum 60 WPM.", + settings: { + autoRole: true, + type: "script", + message: "Minimum 60wpm and 100% accuracy required.", + parameters: { script: "navyseal.txt" }, + requirements: { wpm: { min: 60 }, acc: { exact: 100 }, afk: { max: 5 } }, + }, + }, + littleChef: { + display: "Little Chef", + discordRoleId: "763544714028122153", + category: "script", + description: + "Type out the entire Ratatouille script while watching the movie simultaneously.", + settings: { type: "script", parameters: { script: "littlechef.txt" } }, + }, + crosstalk: { + display: "(CROSSTALK)", + discordRoleId: "761276009664217129", + category: "script", + description: + "Type out the entire transcript of the first 2020 Presidential Debate.", + settings: { type: "script", parameters: { script: "crosstalk.txt" } }, + }, + bees: { + display: "Bees!!!", + discordRoleId: "739636003182084307", + category: "script", + description: + "Type out the entire Bee Movie script while watching the movie simultaneously.", + settings: { type: "script", parameters: { script: "bees.txt" } }, + }, + getOffMySwamp: { + display: "Get off my swamp", + discordRoleId: "757346966987342026", + category: "script", + description: + "Type out the entire Shrek script with punctuation while watching the movie simultaneously.", + settings: { type: "script", parameters: { script: "shrek.txt" } }, + }, + lookAtMeIAmTheDeveloperNow: { + display: "Look at me. I am the developer now.", + discordRoleId: "937358772635074600", + category: "script", + description: + "Type out the entire source code ofMonkeytype, as it was in February 2022.", + settings: { + autoRole: true, + type: "script", + parameters: { script: "sourcecode.txt" }, + }, + }, + beLikeWater: { + display: "Be like water", + discordRoleId: "740568679485276201", + category: "funbox", + description: + "Achieve at least 50 WPM in all three layouts in a 60-second time test using the layoutfluid mode. Layouts must be unique (e.g., QWERTY, Colemak, Dvorak).", + settings: { + type: "funbox", + message: "Remember: You need to achieve at least 50 wpm in each layout.", + parameters: { funbox: "layoutfluid", mode: "time", mode2: 60 }, + }, + }, + rollercoaster: { + display: "Rollercoaster", + discordRoleId: "736032495526740001", + category: "funbox", + description: + "Complete at least a one-hour test using the round round baby mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "round_round_baby", mode: "time", mode2: 3600 }, + requirements: { + time: { min: 3600 }, + funbox: { exact: ["round_round_baby"] }, + }, + }, + }, + oneHourMirror: { + display: "ɿoɿɿim ɿυoʜ ɘno", + discordRoleId: "737385182998429757", + category: "funbox", + description: "Complete at least a one-hour test using the mirror mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "mirror", mode: "time", mode2: 3600 }, + requirements: { time: { min: 3600 }, funbox: { exact: ["mirror"] } }, + }, + }, + chooChoo: { + display: "Choo choo", + discordRoleId: "739306439574683710", + category: "funbox", + description: "Complete at least a one-hour test using choo choo mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "choo_choo", mode: "time", mode2: 3600 }, + requirements: { time: { min: 3600 }, funbox: { exact: ["choo_choo"] } }, + }, + }, + mnemonist: { + display: "Mnemonist", + discordRoleId: "782005606852067328", + category: "funbox", + description: + "Achieve 100+ WPM with 100% accuracy on a 25-word test using the memory funbox.", + settings: { + type: "funbox", + parameters: { + funbox: "memory", + mode: "words", + mode2: 25, + difficulty: "master", + }, + requirements: { config: { tapeMode: "off" } }, + }, + }, + earfquake: { + display: "Earfquake", + discordRoleId: "740730587429601291", + category: "funbox", + description: + "Complete at least a one-hour test using the earthquake funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "earthquake", mode: "time", mode2: 3600 }, + requirements: { time: { min: 3600 }, funbox: { exact: ["earthquake"] } }, + }, + }, + simonSez: { + display: "Simon Sez", + discordRoleId: "742128871825997914", + category: "funbox", + description: + "Complete at least a one-hour test using the simon says funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "simon_says", mode: "time", mode2: 3600 }, + requirements: { time: { min: 3600 }, funbox: { exact: ["simon_says"] } }, + }, + }, + accountant: { + display: "Accountant", + discordRoleId: "743962178821816391", + category: "funbox", + description: + "Complete at least a one-hour test using the 58008 funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "58008", mode: "time", mode2: 3600 }, + requirements: { time: { min: 3600 }, funbox: { exact: ["58008"] } }, + }, + }, + hidden: { + display: "Hidden", + discordRoleId: "782006137742557194", + category: "funbox", + description: + "Achieve 100+ WPM using the read ahead funbox on a 60-second test.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "read_ahead", mode: "time", mode2: 60 }, + requirements: { + wpm: { min: 100 }, + time: { min: 60 }, + funbox: { exact: ["read_ahead"] }, + config: { tapeMode: "off" }, + }, + }, + }, + iCanSeeTheFuture: { + display: "I can see the future", + discordRoleId: "814877508008411226", + category: "funbox", + description: + "Achieve 100+ WPM using the read ahead hard funbox on a 60-second test.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "read_ahead_hard", mode: "time", mode2: 60 }, + requirements: { + wpm: { min: 100 }, + time: { min: 60 }, + funbox: { exact: ["read_ahead_hard"] }, + config: { tapeMode: "off" }, + }, + }, + }, + whatAreWordsAtThisPoint: { + display: "What are words at this point?", + discordRoleId: "744209241396740176", + category: "funbox", + description: + "Complete at least a one-hour test using the gibberish funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "gibberish", mode: "time", mode2: 3600 }, + requirements: { time: { min: 60 }, funbox: { exact: ["gibberish"] } }, + }, + }, + specials: { + display: "Specials", + discordRoleId: "744209452714033162", + category: "funbox", + description: + "Complete at least a one-hour test using the specials funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "specials", mode: "time", mode2: 3600 }, + requirements: { time: { min: 60 }, funbox: { exact: ["specials"] } }, + }, + }, + aeiou: { + display: "Aeiou.", + discordRoleId: "744318102766092362", + category: "funbox", + description: "Complete at least a one-hour test using the tts funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "tts", mode: "time", mode2: 3600 }, + requirements: { time: { min: 60 }, funbox: { exact: ["tts"] } }, + }, + }, + asciiWarrior: { + display: "ASCII warrior", + discordRoleId: "746142791326760980", + category: "funbox", + description: + "Complete at least a one-hour test using the ascii funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "ascii", mode: "time", mode2: 3600 }, + requirements: { time: { min: 60 }, funbox: { exact: ["ascii"] } }, + }, + }, + iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS: { + display: "i KINda LikE HoW inEFFICIeNt QwErtY Is.", + discordRoleId: "760999194525171724", + category: "funbox", + description: + "Complete at least a one-hour test using the randomcase funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "sPoNgEcAsE", mode: "time", mode2: 3600 }, + requirements: { time: { min: 60 }, funbox: { exact: ["sPoNgEcAsE"] } }, + }, + }, + oneNauseousMonkey: { + display: "One Nauseous Monkey", + discordRoleId: "760930262740631633", + category: "funbox", + description: + "Complete at least a one-hour test using the nausea funbox mode.", + settings: { + autoRole: true, + type: "funbox", + parameters: { funbox: "nausea", mode: "time", mode2: 3600 }, + requirements: { time: { min: 60 }, funbox: { exact: ["nausea"] } }, + }, + }, + thumbWarrior: { + display: "Thumb warrior", + discordRoleId: "761794585109200906", + category: "other", + description: "Complete a one-hour test using only your thumbs.", + settings: { type: "customTime", parameters: { time: 3600 } }, + }, + mouseWarrior: { + display: "Mouse warrior", + discordRoleId: "744580294442614790", + category: "other", + description: + "Complete a one-hour test using only the on-screen keyboard. Funbox modes are not allowed.", + settings: { type: "customTime", parameters: { time: 3600 } }, + }, + mobileWarrior: { + display: "Mobile warrior", + discordRoleId: "744723801526370407", + category: "other", + description: "Complete a one-hour test on mobile.", + settings: { type: "customTime", parameters: { time: 3600 } }, + }, + upsideDown: { + display: "uʍop ǝpᴉsdn", + discordRoleId: "782725716114014237", + category: "other", + description: + "Achieve at least 60 WPM on a one-minute test with your keyboard upside down.", + settings: { type: "customTime", parameters: { time: 60 } }, + }, + oneArmedBandit: { + display: "One armed bandit", + discordRoleId: "765919192557682708", + category: "other", + description: + "Complete a one-hour or 10k words test (whichever comes sooner, using an external timer) using a one-handed words list (either left or right) for your layout.", + settings: { type: "customWords", parameters: { words: 10000 } }, + }, + englishMaster: { + display: "English master", + discordRoleId: "751166528824672396", + category: "other", + description: + "Complete a one-hour test using English 10k language with punctuation and numbers enabled.", + settings: { + autoRole: true, + type: "customTime", + parameters: { time: 3600 }, + requirements: { + time: { min: 3600 }, + config: { language: "english_10k", punctuation: true, numbers: true }, + }, + }, + }, + feetWarrior: { + display: "Feet warrior", + discordRoleId: "751953592860147822", + category: "other", + description: "Complete a one-hour test using your feet. Don't ask me why.", + settings: { type: "customTime", parameters: { time: 3600 } }, + }, + wingdings: { + display: "Ten Words of Pain", + discordRoleId: "863192575984140338", + category: "other", + description: + "Complete a 10-word Master mode test using the Wingdings custom font.", + settings: { + type: "other", + message: + "Complete a 10-word Master mode test using the Wingdings custom font. No keymap allowed. Minimum 60 WPM and 100% accuracy required.", + requirements: { acc: { exact: 100 } }, + }, + }, +}; + +const map: Record = Object.fromEntries( + Object.entries(challenges).map(([name, def]) => [name, { ...def, name }]), +) as Record; + +const list: Challenge[] = Object.values(map); +const regular: Challenge[] = list.filter((it) => it.isHidden !== true); + +export function getChallenges(): Challenge[] { + return list; +} + +export function getRegularChallenges(): Challenge[] { + return regular; +} + +export function getChallenge(name: ChallengeName): Challenge { + return map[name]; +} diff --git a/packages/challenges/tsconfig.json b/packages/challenges/tsconfig.json new file mode 100644 index 000000000000..19dc35bfb3ce --- /dev/null +++ b/packages/challenges/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@monkeytype/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "moduleResolution": "Bundler", + "module": "ES6", + "target": "ES2015", + "lib": ["es2019", "dom"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/challenges/tsup.config.js b/packages/challenges/tsup.config.js new file mode 100644 index 000000000000..28181ee3ec44 --- /dev/null +++ b/packages/challenges/tsup.config.js @@ -0,0 +1,3 @@ +import { extendConfig } from "@monkeytype/tsup-config"; + +export default extendConfig(() => ({ entry: ["src/index.ts"] })); diff --git a/packages/challenges/vitest.config.ts b/packages/challenges/vitest.config.ts new file mode 100644 index 000000000000..481ab6a143b8 --- /dev/null +++ b/packages/challenges/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + passWithNoTests: true, + coverage: { + include: ["**/*.ts"], + }, + }, +}); diff --git a/packages/schemas/src/challenges.ts b/packages/schemas/src/challenges.ts index 8f4611ed74f1..005ac9fe71a5 100644 --- a/packages/schemas/src/challenges.ts +++ b/packages/schemas/src/challenges.ts @@ -1,52 +1,70 @@ import { z } from "zod"; -import { FunboxNameSchema, PartialConfigSchema } from "./configs"; +import { customEnumErrorHandler } from "./util"; -const MinRequiredNumber = z.object({ min: z.number() }).strict(); -const MaxRequiredNumber = z.object({ max: z.number() }).strict(); -const ExactRequiredNumber = z.object({ exact: z.number() }).strict(); +export const ChallengeNameSchema = z.enum( + [ + "oneHourWarrior", + "doubleDown", + "tripleTrouble", + "quad", + "8Ball", + "theBig12", + "1Day", + "trueSimp", + "bigramSalad", + "simp", + "simpLord", + "antidiseWhat", + "whatsThisWebsiteCalledAgain", + "developd", + "slowAndSteady", + "speedSpacer", + "iveGotThePower", + "accuracyExpert", + "accuracyMaster", + "accuracyGod", + "inAGalaxyFarFarAway", + "beepBoop", + "whosYourDaddy", + "itsATrap", + "jolly", + "gottaCatchEmAll", + "rapGod", + "navySeal", + "littleChef", + "crosstalk", + "bees", + "getOffMySwamp", + "lookAtMeIAmTheDeveloperNow", + "beLikeWater", + "rollercoaster", + "oneHourMirror", + "chooChoo", + "mnemonist", + "earfquake", + "simonSez", + "accountant", + "hidden", + "iCanSeeTheFuture", + "whatAreWordsAtThisPoint", + "iKiNdAlIkEhOwInEfFiCiEnTqWeRtYiS", + "specials", + "aeiou", + "asciiWarrior", + "oneNauseousMonkey", + "thumbWarrior", + "mouseWarrior", + "mobileWarrior", + "69", + "upsideDown", + "oneArmedBandit", + "englishMaster", + "feetWarrior", + "wingdings", + ], + { + errorMap: customEnumErrorHandler("Must be a known challenge name"), + }, +); -export const ChallengeSchema = z - .object({ - name: z.string(), - display: z.string(), - autoRole: z.boolean().optional(), - type: z.enum([ - "customTime", - "customWords", - "customText", - "script", - "accuracy", - "funbox", - "other", - ]), - message: z.string().optional(), - parameters: z.array( - z - .string() - .or(z.null()) - .or(z.number()) - .or(z.boolean()) - .or(z.array(FunboxNameSchema)), - ), - requirements: z - .object({ - wpm: ExactRequiredNumber.or(MinRequiredNumber), - acc: ExactRequiredNumber.or(MinRequiredNumber), - afk: MaxRequiredNumber, - time: MinRequiredNumber, - funbox: z - .object({ - exact: z.array(FunboxNameSchema), - }) - .partial(), - raw: ExactRequiredNumber, - con: ExactRequiredNumber, - config: PartialConfigSchema, - }) - .partial() - .strict() - .optional(), - }) - .strict(); - -export type Challenge = z.infer; +export type ChallengeName = z.infer; diff --git a/packages/schemas/src/results.ts b/packages/schemas/src/results.ts index fe3b8d594aae..0575644e8dca 100644 --- a/packages/schemas/src/results.ts +++ b/packages/schemas/src/results.ts @@ -10,6 +10,7 @@ import { import { LanguageSchema } from "./languages"; import { Mode, Mode2, Mode2Schema, ModeSchema } from "./shared"; import { DifficultySchema, FunboxSchema } from "./configs"; +import { ChallengeNameSchema } from "./challenges"; export const IncompleteTestSchema = z.object({ acc: PercentageSchema, @@ -136,7 +137,7 @@ export const CompletedEventSchema = ResultBaseSchema.required({ }) .extend({ charTotal: z.number().int().nonnegative(), - challenge: token().max(100).optional(), + challenge: ChallengeNameSchema.optional(), customText: CompletedEventCustomTextSchema.optional(), hash: token().max(100), keyDuration: z.array(z.number().nonnegative()).or(z.literal("toolong")), diff --git a/packages/util/src/objects.ts b/packages/util/src/objects.ts new file mode 100644 index 000000000000..35a295988db7 --- /dev/null +++ b/packages/util/src/objects.ts @@ -0,0 +1,15 @@ +export function typedKeys( + obj: T, +): T extends T ? (keyof T)[] : never { + return Object.keys(obj) as unknown as T extends T ? (keyof T)[] : never; +} + +export function typedEntries( + obj: T, +): { [K in keyof T]: [K, T[K]] }[keyof T][] { + return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][]; +} + +export function typedValues(obj: T): T[keyof T][] { + return Object.values(obj) as T[keyof T][]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e970454ffba..fe94e485d080 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: '@date-fns/utc': specifier: 1.2.0 version: 1.2.0 + '@monkeytype/challenges': + specifier: workspace:* + version: link:../packages/challenges '@monkeytype/contracts': specifier: workspace:* version: link:../packages/contracts @@ -288,6 +291,9 @@ importers: '@leonabcd123/modern-caps-lock': specifier: 3.1.3 version: 3.1.3 + '@monkeytype/challenges': + specifier: workspace:* + version: link:../packages/challenges '@monkeytype/contracts': specifier: workspace:* version: link:../packages/contracts @@ -642,6 +648,43 @@ importers: specifier: ^4.1.0 version: 4.1.0(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) + packages/challenges: + dependencies: + '@monkeytype/schemas': + specifier: workspace:* + version: link:../schemas + '@monkeytype/util': + specifier: workspace:* + version: link:../util + devDependencies: + '@monkeytype/tsup-config': + specifier: workspace:* + version: link:../tsup-config + '@monkeytype/typescript-config': + specifier: workspace:* + version: link:../typescript-config + '@types/node': + specifier: 24.9.1 + version: 24.9.1 + madge: + specifier: 8.0.0 + version: 8.0.0(typescript@6.0.2) + oxlint: + specifier: 1.68.0 + version: 1.68.0(oxlint-tsgolint@0.23.0) + oxlint-tsgolint: + specifier: 0.23.0 + version: 0.23.0 + tsup: + specifier: 8.4.0 + version: 8.4.0(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + typescript: + specifier: 6.0.2 + version: 6.0.2 + vitest: + specifier: 4.1.0 + version: 4.1.0(@types/node@24.9.1)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.25.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) + packages/contracts: dependencies: '@monkeytype/schemas': @@ -680,7 +723,7 @@ importers: version: 6.0.2 vitest: specifier: 4.1.0 - version: 4.1.0(@types/node@24.9.1)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.25.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.0(@types/node@24.9.1)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) packages/funbox: dependencies: @@ -10521,9 +10564,6 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} - std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} @@ -10914,10 +10954,6 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - tinyexec@1.1.2: resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} engines: {node: '>=18'} @@ -22774,8 +22810,6 @@ snapshots: statuses@2.0.2: {} - std-env@4.0.0: {} - std-env@4.1.0: {} stemmer@2.0.1: {} @@ -23344,8 +23378,6 @@ snapshots: tinyexec@0.3.2: {} - tinyexec@1.0.2: {} - tinyexec@1.1.2: {} tinyglobby@0.2.13: @@ -23962,12 +23994,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: @@ -23992,12 +24024,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: @@ -24022,12 +24054,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.25.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: @@ -24051,12 +24083,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.70.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: @@ -24080,12 +24112,12 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 4.0.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: