diff --git a/backend/src/constants/funbox.ts b/backend/src/constants/funbox.ts index 99326daf6..376f7e068 100644 --- a/backend/src/constants/funbox.ts +++ b/backend/src/constants/funbox.ts @@ -123,6 +123,10 @@ const Funboxes: Record = { canGetPb: false, difficultyLevel: 1, }, + zipf: { + canGetPb: false, + difficultyLevel: 1, + }, }; export default Funboxes; diff --git a/frontend/src/ts/test/funbox/funbox-list.ts b/frontend/src/ts/test/funbox/funbox-list.ts index b3d89dbf7..5356fe308 100644 --- a/frontend/src/ts/test/funbox/funbox-list.ts +++ b/frontend/src/ts/test/funbox/funbox-list.ts @@ -214,6 +214,12 @@ const list: MonkeyTypes.FunboxMetadata[] = [ punctuation: [false], }, }, + { + name: "zipf", + alias: "frequency", + info: "Words are generated according to Zipf's law. (not all languages will produce Zipfy results, use with caution)", + properties: ["changesWordsFrequency"], + }, ]; export function getAll(): MonkeyTypes.FunboxMetadata[] { diff --git a/frontend/src/ts/test/funbox/funbox-validation.ts b/frontend/src/ts/test/funbox/funbox-validation.ts index 4d33efea8..f7ae11a6b 100644 --- a/frontend/src/ts/test/funbox/funbox-validation.ts +++ b/frontend/src/ts/test/funbox/funbox-validation.ts @@ -98,7 +98,8 @@ export function canSetConfigWithCurrentFunboxes( f.properties?.find((fp) => fp.startsWith("toPush:")) || f.properties?.includes("changesWordsVisibility") || f.properties?.includes("speaks") || - f.properties?.includes("changesLayout") + f.properties?.includes("changesLayout") || + f.properties?.includes("changesWordsFrequency") ) ); } @@ -108,7 +109,8 @@ export function canSetConfigWithCurrentFunboxes( (f) => f.functions?.getWord || f.functions?.pullSection || - f.functions?.withWords + f.functions?.withWords || + f.properties?.includes("changesWordsFrequency") ) ); } @@ -222,6 +224,17 @@ export function areFunboxesCompatible( 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 || @@ -284,6 +297,8 @@ export function areFunboxesCompatible( layoutUsability && oneNospaceOrToPushMax && oneChangesWordsVisibilityMax && + oneFrequencyChangesMax && + noFrequencyChangesConflicts && capitalisationChangePosibility && noConflictsWithSymmetricChars && canSpeak && diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts index 38e6a2432..b87568d81 100644 --- a/frontend/src/ts/test/funbox/funbox.ts +++ b/frontend/src/ts/test/funbox/funbox.ts @@ -526,6 +526,12 @@ FunboxList.setFunboxFunctions("binary", { }, }); +FunboxList.setFunboxFunctions("zipf", { + getWordsFrequencyMode(): MonkeyTypes.FunboxWordsFrequency { + return "zipf"; + }, +}); + export function toggleScript(...params: string[]): void { FunboxList.get(Config.funbox).forEach((funbox) => { if (funbox.functions?.toggleScript) funbox.functions.toggleScript(params); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 800cabcae..601995934 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -677,6 +677,18 @@ export function restart(options = {} as RestartOptions): void { ); } +function getFunboxWordsFrequency(): + | MonkeyTypes.FunboxWordsFrequency + | undefined { + const wordFunbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.getWordsFrequencyMode + ); + if (wordFunbox?.functions?.getWordsFrequencyMode) { + return wordFunbox.functions.getWordsFrequencyMode(); + } + return undefined; +} + function getFunboxWord(word: string, wordset?: Misc.Wordset): string { const wordFunbox = FunboxList.get(Config.funbox).find( (f) => f.functions?.getWord @@ -718,7 +730,9 @@ async function getNextWord( language: MonkeyTypes.LanguageObject, wordsBound: number ): Promise { - let randomWord = wordset.randomWord(); + const funboxFrequency = getFunboxWordsFrequency() ?? "normal"; + + let randomWord = wordset.randomWord(funboxFrequency); const previousWord = TestWords.words.get(TestWords.words.length - 1, true); const previousWord2 = TestWords.words.get(TestWords.words.length - 2, true); if (Config.mode === "quote") { @@ -735,7 +749,7 @@ async function getNextWord( (CustomText.isWordRandom || CustomText.isTimeRandom) && (wordset.length < 4 || PractiseWords.before.mode !== null) ) { - randomWord = wordset.randomWord(); + randomWord = wordset.randomWord(funboxFrequency); } else { let regenarationCount = 0; //infinite loop emergency stop button while ( @@ -754,12 +768,12 @@ async function getNextWord( /[0-9]/i.test(randomWord))) ) { regenarationCount++; - randomWord = wordset.randomWord(); + randomWord = wordset.randomWord(funboxFrequency); } } if (randomWord === undefined) { - randomWord = wordset.randomWord(); + randomWord = wordset.randomWord(funboxFrequency); } if ( diff --git a/frontend/src/ts/test/weak-spot.ts b/frontend/src/ts/test/weak-spot.ts index 31532b129..bb4ee1ed7 100644 --- a/frontend/src/ts/test/weak-spot.ts +++ b/frontend/src/ts/test/weak-spot.ts @@ -63,7 +63,7 @@ export function getWord(wordset: Wordset): string { let highScore; let randomWord = ""; for (let i = 0; i < wordSamples; i++) { - const newWord = wordset.randomWord(); + const newWord = wordset.randomWord("normal"); const newScore = score(newWord); if (i == 0 || highScore === undefined || newScore > highScore) { randomWord = newWord; diff --git a/frontend/src/ts/types/types.d.ts b/frontend/src/ts/types/types.d.ts index ab32a5c02..73c6ef9db 100644 --- a/frontend/src/ts/types/types.d.ts +++ b/frontend/src/ts/types/types.d.ts @@ -187,6 +187,8 @@ declare namespace MonkeyTypes { display?: string; } + type FunboxWordsFrequency = "normal" | "zipf"; + type FunboxProperty = | "symmetricChars" | "conflictsWithSymmetricChars" @@ -202,7 +204,8 @@ declare namespace MonkeyTypes { | "changesCapitalisation" | "nospace" | `toPush:${number}` - | "noInfiniteDuration"; + | "noInfiniteDuration" + | "changesWordsFrequency"; interface FunboxFunctions { getWord?: (wordset?: Misc.Wordset) => string; @@ -227,6 +230,7 @@ declare namespace MonkeyTypes { start?: () => void; restart?: () => void; getWordHtml?: (char: string, letterTag?: boolean) => string; + getWordsFrequencyMode?: () => FunboxWordsFrequency; } interface FunboxForcedConfig { diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 596f5859a..c6252c1be 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -1309,8 +1309,12 @@ export class Wordset { this.length = this.words.length; } - public randomWord(): string { - return randomElementFromArray(this.words); + public randomWord(mode: MonkeyTypes.FunboxWordsFrequency): string { + if (mode === "zipf") { + return this.words[dreymarIndex(this.words.length)]; + } else { + return randomElementFromArray(this.words); + } } } @@ -1388,3 +1392,13 @@ export function getBinary(): string { const ret = Math.floor(Math.random() * 256).toString(2); return ret.padStart(8, "0"); } + +export function dreymarIndex(arrayLength: number): number { + const n = arrayLength; + const g = 0.5772156649; + const M = Math.log(n) + g; + const r = Math.random(); + const h = Math.exp(r * M - g); + const W = Math.ceil(h); + return W - 1; +} diff --git a/frontend/static/funbox/_list.json b/frontend/static/funbox/_list.json index bb6abb17f..6b5e01c4f 100644 --- a/frontend/static/funbox/_list.json +++ b/frontend/static/funbox/_list.json @@ -155,5 +155,10 @@ "name": "binary", "info": "01000010 01100101 01100101 01110000 00100000 01100010 01101111 01101111 01110000 00101110", "canGetPb": false + }, + { + "name": "zipf", + "info": "Words are generated according to Zipf's law. (not all languages will produce Zipfy results, use with caution)", + "canGetPb": false } ]