Skip to content

Commit

Permalink
refactor: move funboxes to a shared package (@Miodec) (#6063)
Browse files Browse the repository at this point in the history
  • Loading branch information
Miodec authored Dec 4, 2024
1 parent a75f0d3 commit fdadb4a
Show file tree
Hide file tree
Showing 43 changed files with 1,605 additions and 2,281 deletions.
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"@date-fns/utc": "1.2.0",
"@monkeytype/contracts": "workspace:*",
"@monkeytype/funbox": "workspace:*",
"@monkeytype/util": "workspace:*",
"@ts-rest/core": "3.51.0",
"@ts-rest/express": "3.51.0",
Expand Down
29 changes: 19 additions & 10 deletions backend/src/api/controllers/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Logger from "../../utils/logger";
import "dotenv/config";
import { MonkeyResponse } from "../../utils/monkey-response";
import MonkeyError from "../../utils/error";
import { areFunboxesCompatible, isTestTooShort } from "../../utils/validation";
import { isTestTooShort } from "../../utils/validation";
import {
implemented as anticheatImplemented,
validateResult,
Expand All @@ -22,7 +22,6 @@ import { getDailyLeaderboard } from "../../utils/daily-leaderboards";
import AutoRoleList from "../../constants/auto-roles";
import * as UserDAL from "../../dal/user";
import { buildMonkeyMail } from "../../utils/monkey-mail";
import FunboxList from "../../constants/funbox-list";
import _, { omit } from "lodash";
import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard";
import { UAParser } from "ua-parser-js";
Expand Down Expand Up @@ -57,6 +56,11 @@ import {
getStartOfDayTimestamp,
} from "@monkeytype/util/date-and-time";
import { MonkeyRequest } from "../types";
import {
getFunbox,
checkCompatibility,
stringToFunboxNames,
} from "@monkeytype/funbox";

try {
if (!anticheatImplemented()) throw new Error("undefined");
Expand Down Expand Up @@ -232,7 +236,9 @@ export async function addResult(
}
}

if (!areFunboxesCompatible(completedEvent.funbox ?? "")) {
const funboxNames = stringToFunboxNames(completedEvent.funbox ?? "");

if (!checkCompatibility(funboxNames)) {
throw new MonkeyError(400, "Impossible funbox combination");
}

Expand Down Expand Up @@ -660,7 +666,7 @@ async function calculateXp(
charStats,
punctuation,
numbers,
funbox,
funbox: resultFunboxes,
} = result;

const {
Expand Down Expand Up @@ -713,12 +719,15 @@ async function calculateXp(
}
}

if (funboxBonusConfiguration > 0) {
const funboxModifier = _.sumBy(funbox.split("#"), (funboxName) => {
const funbox = FunboxList.find((f) => f.name === funboxName);
const difficultyLevel = funbox?.difficultyLevel ?? 0;
return Math.max(difficultyLevel * funboxBonusConfiguration, 0);
});
if (funboxBonusConfiguration > 0 && resultFunboxes !== "none") {
const funboxModifier = _.sumBy(
stringToFunboxNames(resultFunboxes),
(funboxName) => {
const funbox = getFunbox(funboxName);
const difficultyLevel = funbox?.difficultyLevel ?? 0;
return Math.max(difficultyLevel * funboxBonusConfiguration, 0);
}
);
if (funboxModifier > 0) {
modifier += funboxModifier;
breakdown.funbox = Math.round(baseXp * funboxModifier);
Expand Down
23 changes: 9 additions & 14 deletions backend/src/utils/pb.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import _ from "lodash";
import FunboxList from "../constants/funbox-list";

import {
Mode,
PersonalBest,
PersonalBests,
} from "@monkeytype/contracts/schemas/shared";
import { Result as ResultType } from "@monkeytype/contracts/schemas/results";
import { getFunboxesFromString } from "@monkeytype/funbox";

export type LbPersonalBests = {
time: Record<number, Record<string, PersonalBest>>;
Expand All @@ -21,20 +20,16 @@ type CheckAndUpdatePbResult = {
type Result = Omit<ResultType<Mode>, "_id" | "name">;

export function canFunboxGetPb(result: Result): boolean {
const funbox = result.funbox;
if (funbox === undefined || funbox === "" || funbox === "none") return true;

let ret = true;
const resultFunboxes = funbox.split("#");
for (const funbox of FunboxList) {
if (resultFunboxes.includes(funbox.name)) {
if (!funbox.canGetPb) {
ret = false;
}
}
const funboxString = result.funbox;
if (
funboxString === undefined ||
funboxString === "" ||
funboxString === "none"
) {
return true;
}

return ret;
return getFunboxesFromString(funboxString).every((f) => f.canGetPb);
}

export function checkAndUpdatePb(
Expand Down
137 changes: 0 additions & 137 deletions backend/src/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import _ from "lodash";
import { default as FunboxList } from "../constants/funbox-list";
import { CompletedEvent } from "@monkeytype/contracts/schemas/results";
import { intersect } from "@monkeytype/util/arrays";

export function isTestTooShort(result: CompletedEvent): boolean {
const { mode, mode2, customText, testDuration, bailedOut } = result;
Expand Down Expand Up @@ -48,138 +46,3 @@ export function isTestTooShort(result: CompletedEvent): boolean {

return false;
}

export function areFunboxesCompatible(funboxesString: string): boolean {
const funboxes = funboxesString.split("#").filter((f) => f !== "none");

const funboxesToCheck = FunboxList.filter((f) => funboxes.includes(f.name));

const allFunboxesAreValid = funboxesToCheck.length === funboxes.length;
const oneWordModifierMax =
funboxesToCheck.filter(
(f) =>
f.frontendFunctions?.includes("getWord") ??
f.frontendFunctions?.includes("pullSection") ??
f.frontendFunctions?.includes("withWords")
).length <= 1;
const layoutUsability =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesLayout")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "ignoresLayout" || fp === "usesLayout")
).length === 0;
const oneNospaceOrToPushMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "nospace" || fp.startsWith("toPush"))
).length <= 1;
const oneWordOrderMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp.startsWith("wordOrder"))
).length <= 1;
const oneChangesWordsVisibilityMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesWordsVisibility")
).length <= 1;
const oneFrequencyChangesMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesWordsFrequency")
).length <= 1;
const noFrequencyChangesConflicts =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesWordsFrequency")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "ignoresLanguage")
).length === 0;
const capitalisationChangePosibility =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "noLetters")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesCapitalisation")
).length === 0;
const noConflictsWithSymmetricChars =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "conflictsWithSymmetricChars")
).length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "symmetricChars")
).length === 0;
const canSpeak =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "speaks" || fp === "unspeakable")
).length <= 1;
const hasLanguageToSpeak =
funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
.length === 0 ||
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "ignoresLanguage")
).length === 0;
const oneToPushOrPullSectionMax =
funboxesToCheck.filter(
(f) =>
f.properties?.some((fp) => fp.startsWith("toPush:")) ??
f.frontendFunctions?.includes("pullSection")
).length <= 1;
// const oneApplyCSSMax =
// funboxesToCheck.filter((f) => f.frontendFunctions?.includes("applyCSS"))
// .length <= 1; //todo: move all funbox stuff to the shared package, this is ok to remove for now
const onePunctuateWordMax =
funboxesToCheck.filter((f) =>
f.frontendFunctions?.includes("punctuateWord")
).length <= 1;
const oneCharCheckerMax =
funboxesToCheck.filter((f) =>
f.frontendFunctions?.includes("isCharCorrect")
).length <= 1;
const oneCharReplacerMax =
funboxesToCheck.filter((f) => f.frontendFunctions?.includes("getWordHtml"))
.length <= 1;
const oneChangesCapitalisationMax =
funboxesToCheck.filter((f) =>
f.properties?.find((fp) => fp === "changesCapitalisation")
).length <= 1;
const allowedConfig = {} as Record<string, string[] | boolean[]>;
let noConfigConflicts = true;
for (const f of funboxesToCheck) {
if (!f.frontendForcedConfig) continue;
for (const key in f.frontendForcedConfig) {
const allowedConfigValue = allowedConfig[key];
const funboxValue = f.frontendForcedConfig[key];
if (allowedConfigValue !== undefined && funboxValue !== undefined) {
if (
intersect<string | boolean>(allowedConfigValue, funboxValue, true)
.length === 0
) {
noConfigConflicts = false;
break;
}
} else if (funboxValue !== undefined) {
allowedConfig[key] = funboxValue;
}
}
}

return (
allFunboxesAreValid &&
oneWordModifierMax &&
layoutUsability &&
oneNospaceOrToPushMax &&
oneChangesWordsVisibilityMax &&
oneFrequencyChangesMax &&
noFrequencyChangesConflicts &&
capitalisationChangePosibility &&
noConflictsWithSymmetricChars &&
canSpeak &&
hasLanguageToSpeak &&
oneToPushOrPullSectionMax &&
// oneApplyCSSMax &&
onePunctuateWordMax &&
oneCharCheckerMax &&
oneCharReplacerMax &&
oneChangesCapitalisationMax &&
noConfigConflicts &&
oneWordOrderMax
);
}
6 changes: 2 additions & 4 deletions frontend/__tests__/root/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,8 @@ describe("Config", () => {
expect(Config.setFavThemes([stringOfLength(51)])).toBe(false);
});
it("setFunbox", () => {
expect(Config.setFunbox("one")).toBe(true);
expect(Config.setFunbox("one#two")).toBe(true);
expect(Config.setFunbox("one#two#")).toBe(true);
expect(Config.setFunbox(stringOfLength(100))).toBe(true);
expect(Config.setFunbox("mirror")).toBe(true);
expect(Config.setFunbox("mirror#58008")).toBe(true);

expect(Config.setFunbox(stringOfLength(101))).toBe(false);
});
Expand Down
24 changes: 24 additions & 0 deletions frontend/__tests__/test/funbox.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getAllFunboxes } from "../../src/ts/test/funbox/list";

describe("funbox", () => {
describe("list", () => {
it("should have every frontendFunctions function defined", () => {
for (const funbox of getAllFunboxes()) {
const packageFunctions = (funbox.frontendFunctions ?? []).sort();
const implementations = Object.keys(funbox.functions ?? {}).sort();

let message = "has mismatched functions";

if (packageFunctions.length > implementations.length) {
message = `missing function implementation in frontend`;
} else if (implementations.length > packageFunctions.length) {
message = `missing properties in frontendFunctions in the package`;
}

expect(packageFunctions, `Funbox ${funbox.name} ${message}`).toEqual(
implementations
);
}
});
});
});
2 changes: 1 addition & 1 deletion frontend/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"ts-node": {
"files": true
},
"files": ["../src/ts/types/types.d.ts", "vitest.d.ts"],
"files": ["vitest.d.ts"],
"include": ["./**/*.spec.ts", "./setup-tests.ts"]
}
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"dependencies": {
"@date-fns/utc": "1.2.0",
"@monkeytype/contracts": "workspace:*",
"@monkeytype/funbox": "workspace:*",
"@monkeytype/util": "workspace:*",
"@ts-rest/core": "3.51.0",
"canvas-confetti": "1.5.1",
Expand Down
28 changes: 0 additions & 28 deletions frontend/scripts/json-validation.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,34 +46,6 @@ function validateOthers() {
return reject(new Error(fontsValidator.errors[0].message));
}

//funbox
const funboxData = JSON.parse(
fs.readFileSync("./static/funbox/_list.json", {
encoding: "utf8",
flag: "r",
})
);
const funboxSchema = {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
info: { type: "string" },
canGetPb: { type: "boolean" },
alias: { type: "string" },
},
required: ["name", "info", "canGetPb"],
},
};
const funboxValidator = ajv.compile(funboxSchema);
if (funboxValidator(funboxData)) {
console.log("Funbox list JSON schema is \u001b[32mvalid\u001b[0m");
} else {
console.log("Funbox list JSON schema is \u001b[31minvalid\u001b[0m");
return reject(new Error(funboxValidator.errors[0].message));
}

//themes
const themesData = JSON.parse(
fs.readFileSync("./static/themes/_list.json", {
Expand Down
Loading

0 comments on commit fdadb4a

Please sign in to comment.