Skip to content

Commit fdadb4a

Browse files
authored
refactor: move funboxes to a shared package (@Miodec) (monkeytypegame#6063)
1 parent a75f0d3 commit fdadb4a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1605
-2281
lines changed

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"dependencies": {
2525
"@date-fns/utc": "1.2.0",
2626
"@monkeytype/contracts": "workspace:*",
27+
"@monkeytype/funbox": "workspace:*",
2728
"@monkeytype/util": "workspace:*",
2829
"@ts-rest/core": "3.51.0",
2930
"@ts-rest/express": "3.51.0",

backend/src/api/controllers/result.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Logger from "../../utils/logger";
66
import "dotenv/config";
77
import { MonkeyResponse } from "../../utils/monkey-response";
88
import MonkeyError from "../../utils/error";
9-
import { areFunboxesCompatible, isTestTooShort } from "../../utils/validation";
9+
import { isTestTooShort } from "../../utils/validation";
1010
import {
1111
implemented as anticheatImplemented,
1212
validateResult,
@@ -22,7 +22,6 @@ import { getDailyLeaderboard } from "../../utils/daily-leaderboards";
2222
import AutoRoleList from "../../constants/auto-roles";
2323
import * as UserDAL from "../../dal/user";
2424
import { buildMonkeyMail } from "../../utils/monkey-mail";
25-
import FunboxList from "../../constants/funbox-list";
2625
import _, { omit } from "lodash";
2726
import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard";
2827
import { UAParser } from "ua-parser-js";
@@ -57,6 +56,11 @@ import {
5756
getStartOfDayTimestamp,
5857
} from "@monkeytype/util/date-and-time";
5958
import { MonkeyRequest } from "../types";
59+
import {
60+
getFunbox,
61+
checkCompatibility,
62+
stringToFunboxNames,
63+
} from "@monkeytype/funbox";
6064

6165
try {
6266
if (!anticheatImplemented()) throw new Error("undefined");
@@ -232,7 +236,9 @@ export async function addResult(
232236
}
233237
}
234238

235-
if (!areFunboxesCompatible(completedEvent.funbox ?? "")) {
239+
const funboxNames = stringToFunboxNames(completedEvent.funbox ?? "");
240+
241+
if (!checkCompatibility(funboxNames)) {
236242
throw new MonkeyError(400, "Impossible funbox combination");
237243
}
238244

@@ -660,7 +666,7 @@ async function calculateXp(
660666
charStats,
661667
punctuation,
662668
numbers,
663-
funbox,
669+
funbox: resultFunboxes,
664670
} = result;
665671

666672
const {
@@ -713,12 +719,15 @@ async function calculateXp(
713719
}
714720
}
715721

716-
if (funboxBonusConfiguration > 0) {
717-
const funboxModifier = _.sumBy(funbox.split("#"), (funboxName) => {
718-
const funbox = FunboxList.find((f) => f.name === funboxName);
719-
const difficultyLevel = funbox?.difficultyLevel ?? 0;
720-
return Math.max(difficultyLevel * funboxBonusConfiguration, 0);
721-
});
722+
if (funboxBonusConfiguration > 0 && resultFunboxes !== "none") {
723+
const funboxModifier = _.sumBy(
724+
stringToFunboxNames(resultFunboxes),
725+
(funboxName) => {
726+
const funbox = getFunbox(funboxName);
727+
const difficultyLevel = funbox?.difficultyLevel ?? 0;
728+
return Math.max(difficultyLevel * funboxBonusConfiguration, 0);
729+
}
730+
);
722731
if (funboxModifier > 0) {
723732
modifier += funboxModifier;
724733
breakdown.funbox = Math.round(baseXp * funboxModifier);

backend/src/utils/pb.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import _ from "lodash";
2-
import FunboxList from "../constants/funbox-list";
3-
42
import {
53
Mode,
64
PersonalBest,
75
PersonalBests,
86
} from "@monkeytype/contracts/schemas/shared";
97
import { Result as ResultType } from "@monkeytype/contracts/schemas/results";
8+
import { getFunboxesFromString } from "@monkeytype/funbox";
109

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

2322
export function canFunboxGetPb(result: Result): boolean {
24-
const funbox = result.funbox;
25-
if (funbox === undefined || funbox === "" || funbox === "none") return true;
26-
27-
let ret = true;
28-
const resultFunboxes = funbox.split("#");
29-
for (const funbox of FunboxList) {
30-
if (resultFunboxes.includes(funbox.name)) {
31-
if (!funbox.canGetPb) {
32-
ret = false;
33-
}
34-
}
23+
const funboxString = result.funbox;
24+
if (
25+
funboxString === undefined ||
26+
funboxString === "" ||
27+
funboxString === "none"
28+
) {
29+
return true;
3530
}
3631

37-
return ret;
32+
return getFunboxesFromString(funboxString).every((f) => f.canGetPb);
3833
}
3934

4035
export function checkAndUpdatePb(

backend/src/utils/validation.ts

Lines changed: 0 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import _ from "lodash";
2-
import { default as FunboxList } from "../constants/funbox-list";
32
import { CompletedEvent } from "@monkeytype/contracts/schemas/results";
4-
import { intersect } from "@monkeytype/util/arrays";
53

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

4947
return false;
5048
}
51-
52-
export function areFunboxesCompatible(funboxesString: string): boolean {
53-
const funboxes = funboxesString.split("#").filter((f) => f !== "none");
54-
55-
const funboxesToCheck = FunboxList.filter((f) => funboxes.includes(f.name));
56-
57-
const allFunboxesAreValid = funboxesToCheck.length === funboxes.length;
58-
const oneWordModifierMax =
59-
funboxesToCheck.filter(
60-
(f) =>
61-
f.frontendFunctions?.includes("getWord") ??
62-
f.frontendFunctions?.includes("pullSection") ??
63-
f.frontendFunctions?.includes("withWords")
64-
).length <= 1;
65-
const layoutUsability =
66-
funboxesToCheck.filter((f) =>
67-
f.properties?.find((fp) => fp === "changesLayout")
68-
).length === 0 ||
69-
funboxesToCheck.filter((f) =>
70-
f.properties?.find((fp) => fp === "ignoresLayout" || fp === "usesLayout")
71-
).length === 0;
72-
const oneNospaceOrToPushMax =
73-
funboxesToCheck.filter((f) =>
74-
f.properties?.find((fp) => fp === "nospace" || fp.startsWith("toPush"))
75-
).length <= 1;
76-
const oneWordOrderMax =
77-
funboxesToCheck.filter((f) =>
78-
f.properties?.find((fp) => fp.startsWith("wordOrder"))
79-
).length <= 1;
80-
const oneChangesWordsVisibilityMax =
81-
funboxesToCheck.filter((f) =>
82-
f.properties?.find((fp) => fp === "changesWordsVisibility")
83-
).length <= 1;
84-
const oneFrequencyChangesMax =
85-
funboxesToCheck.filter((f) =>
86-
f.properties?.find((fp) => fp === "changesWordsFrequency")
87-
).length <= 1;
88-
const noFrequencyChangesConflicts =
89-
funboxesToCheck.filter((f) =>
90-
f.properties?.find((fp) => fp === "changesWordsFrequency")
91-
).length === 0 ||
92-
funboxesToCheck.filter((f) =>
93-
f.properties?.find((fp) => fp === "ignoresLanguage")
94-
).length === 0;
95-
const capitalisationChangePosibility =
96-
funboxesToCheck.filter((f) =>
97-
f.properties?.find((fp) => fp === "noLetters")
98-
).length === 0 ||
99-
funboxesToCheck.filter((f) =>
100-
f.properties?.find((fp) => fp === "changesCapitalisation")
101-
).length === 0;
102-
const noConflictsWithSymmetricChars =
103-
funboxesToCheck.filter((f) =>
104-
f.properties?.find((fp) => fp === "conflictsWithSymmetricChars")
105-
).length === 0 ||
106-
funboxesToCheck.filter((f) =>
107-
f.properties?.find((fp) => fp === "symmetricChars")
108-
).length === 0;
109-
const canSpeak =
110-
funboxesToCheck.filter((f) =>
111-
f.properties?.find((fp) => fp === "speaks" || fp === "unspeakable")
112-
).length <= 1;
113-
const hasLanguageToSpeak =
114-
funboxesToCheck.filter((f) => f.properties?.find((fp) => fp === "speaks"))
115-
.length === 0 ||
116-
funboxesToCheck.filter((f) =>
117-
f.properties?.find((fp) => fp === "ignoresLanguage")
118-
).length === 0;
119-
const oneToPushOrPullSectionMax =
120-
funboxesToCheck.filter(
121-
(f) =>
122-
f.properties?.some((fp) => fp.startsWith("toPush:")) ??
123-
f.frontendFunctions?.includes("pullSection")
124-
).length <= 1;
125-
// const oneApplyCSSMax =
126-
// funboxesToCheck.filter((f) => f.frontendFunctions?.includes("applyCSS"))
127-
// .length <= 1; //todo: move all funbox stuff to the shared package, this is ok to remove for now
128-
const onePunctuateWordMax =
129-
funboxesToCheck.filter((f) =>
130-
f.frontendFunctions?.includes("punctuateWord")
131-
).length <= 1;
132-
const oneCharCheckerMax =
133-
funboxesToCheck.filter((f) =>
134-
f.frontendFunctions?.includes("isCharCorrect")
135-
).length <= 1;
136-
const oneCharReplacerMax =
137-
funboxesToCheck.filter((f) => f.frontendFunctions?.includes("getWordHtml"))
138-
.length <= 1;
139-
const oneChangesCapitalisationMax =
140-
funboxesToCheck.filter((f) =>
141-
f.properties?.find((fp) => fp === "changesCapitalisation")
142-
).length <= 1;
143-
const allowedConfig = {} as Record<string, string[] | boolean[]>;
144-
let noConfigConflicts = true;
145-
for (const f of funboxesToCheck) {
146-
if (!f.frontendForcedConfig) continue;
147-
for (const key in f.frontendForcedConfig) {
148-
const allowedConfigValue = allowedConfig[key];
149-
const funboxValue = f.frontendForcedConfig[key];
150-
if (allowedConfigValue !== undefined && funboxValue !== undefined) {
151-
if (
152-
intersect<string | boolean>(allowedConfigValue, funboxValue, true)
153-
.length === 0
154-
) {
155-
noConfigConflicts = false;
156-
break;
157-
}
158-
} else if (funboxValue !== undefined) {
159-
allowedConfig[key] = funboxValue;
160-
}
161-
}
162-
}
163-
164-
return (
165-
allFunboxesAreValid &&
166-
oneWordModifierMax &&
167-
layoutUsability &&
168-
oneNospaceOrToPushMax &&
169-
oneChangesWordsVisibilityMax &&
170-
oneFrequencyChangesMax &&
171-
noFrequencyChangesConflicts &&
172-
capitalisationChangePosibility &&
173-
noConflictsWithSymmetricChars &&
174-
canSpeak &&
175-
hasLanguageToSpeak &&
176-
oneToPushOrPullSectionMax &&
177-
// oneApplyCSSMax &&
178-
onePunctuateWordMax &&
179-
oneCharCheckerMax &&
180-
oneCharReplacerMax &&
181-
oneChangesCapitalisationMax &&
182-
noConfigConflicts &&
183-
oneWordOrderMax
184-
);
185-
}

frontend/__tests__/root/config.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,8 @@ describe("Config", () => {
332332
expect(Config.setFavThemes([stringOfLength(51)])).toBe(false);
333333
});
334334
it("setFunbox", () => {
335-
expect(Config.setFunbox("one")).toBe(true);
336-
expect(Config.setFunbox("one#two")).toBe(true);
337-
expect(Config.setFunbox("one#two#")).toBe(true);
338-
expect(Config.setFunbox(stringOfLength(100))).toBe(true);
335+
expect(Config.setFunbox("mirror")).toBe(true);
336+
expect(Config.setFunbox("mirror#58008")).toBe(true);
339337

340338
expect(Config.setFunbox(stringOfLength(101))).toBe(false);
341339
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { getAllFunboxes } from "../../src/ts/test/funbox/list";
2+
3+
describe("funbox", () => {
4+
describe("list", () => {
5+
it("should have every frontendFunctions function defined", () => {
6+
for (const funbox of getAllFunboxes()) {
7+
const packageFunctions = (funbox.frontendFunctions ?? []).sort();
8+
const implementations = Object.keys(funbox.functions ?? {}).sort();
9+
10+
let message = "has mismatched functions";
11+
12+
if (packageFunctions.length > implementations.length) {
13+
message = `missing function implementation in frontend`;
14+
} else if (implementations.length > packageFunctions.length) {
15+
message = `missing properties in frontendFunctions in the package`;
16+
}
17+
18+
expect(packageFunctions, `Funbox ${funbox.name} ${message}`).toEqual(
19+
implementations
20+
);
21+
}
22+
});
23+
});
24+
});

frontend/__tests__/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
"ts-node": {
1010
"files": true
1111
},
12-
"files": ["../src/ts/types/types.d.ts", "vitest.d.ts"],
12+
"files": ["vitest.d.ts"],
1313
"include": ["./**/*.spec.ts", "./setup-tests.ts"]
1414
}

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"dependencies": {
7171
"@date-fns/utc": "1.2.0",
7272
"@monkeytype/contracts": "workspace:*",
73+
"@monkeytype/funbox": "workspace:*",
7374
"@monkeytype/util": "workspace:*",
7475
"@ts-rest/core": "3.51.0",
7576
"canvas-confetti": "1.5.1",

frontend/scripts/json-validation.cjs

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,34 +46,6 @@ function validateOthers() {
4646
return reject(new Error(fontsValidator.errors[0].message));
4747
}
4848

49-
//funbox
50-
const funboxData = JSON.parse(
51-
fs.readFileSync("./static/funbox/_list.json", {
52-
encoding: "utf8",
53-
flag: "r",
54-
})
55-
);
56-
const funboxSchema = {
57-
type: "array",
58-
items: {
59-
type: "object",
60-
properties: {
61-
name: { type: "string" },
62-
info: { type: "string" },
63-
canGetPb: { type: "boolean" },
64-
alias: { type: "string" },
65-
},
66-
required: ["name", "info", "canGetPb"],
67-
},
68-
};
69-
const funboxValidator = ajv.compile(funboxSchema);
70-
if (funboxValidator(funboxData)) {
71-
console.log("Funbox list JSON schema is \u001b[32mvalid\u001b[0m");
72-
} else {
73-
console.log("Funbox list JSON schema is \u001b[31minvalid\u001b[0m");
74-
return reject(new Error(funboxValidator.errors[0].message));
75-
}
76-
7749
//themes
7850
const themesData = JSON.parse(
7951
fs.readFileSync("./static/themes/_list.json", {

0 commit comments

Comments
 (0)