From cb6ddc5fc7a19f4c9a3fc2da682cb3a056a16dc1 Mon Sep 17 00:00:00 2001 From: Bruce Berrios <58147810+Bruception@users.noreply.github.com> Date: Sun, 13 Mar 2022 14:30:07 -0400 Subject: [PATCH] Add logic to prevent repeated random quotes (#2693) * Add logic to prevent repeated random quotes --- .../scripts/controllers/quotes-controller.ts | 156 ++++++++++++++++++ .../src/scripts/popups/quote-report-popup.ts | 4 +- .../src/scripts/popups/quote-search-popup.ts | 4 +- frontend/src/scripts/test/test-logic.ts | 63 +++---- frontend/src/scripts/test/test-words.ts | 1 + frontend/src/scripts/utils/misc.ts | 80 +++------ 6 files changed, 207 insertions(+), 101 deletions(-) create mode 100644 frontend/src/scripts/controllers/quotes-controller.ts diff --git a/frontend/src/scripts/controllers/quotes-controller.ts b/frontend/src/scripts/controllers/quotes-controller.ts new file mode 100644 index 000000000..8b80b86ae --- /dev/null +++ b/frontend/src/scripts/controllers/quotes-controller.ts @@ -0,0 +1,156 @@ +import { shuffle } from "../utils/misc"; +import { subscribe } from "../observables/config-event"; + +interface Quote { + text: string; + source: string; + length: number; + id: number; +} + +interface QuoteData { + language: string; + quotes: Quote[]; + groups: number[][]; +} + +interface QuoteCollection { + quotes: MonkeyTypes.Quote[]; + length: number; + language: string | null; + groups: MonkeyTypes.Quote[][]; +} + +const defaultQuoteCollection: QuoteCollection = { + quotes: [], + length: 0, + language: null, + groups: [], +}; + +class QuotesController { + private quoteCollection: QuoteCollection = defaultQuoteCollection; + + private quoteQueue: MonkeyTypes.Quote[] = []; + private queueIndex = 0; + + async getQuotes( + language: string, + quoteLengths?: number[] + ): Promise { + const normalizedLanguage = language.replace(/_\d*k$/g, ""); + + if (this.quoteCollection.language !== normalizedLanguage) { + try { + const data: QuoteData = await $.getJSON(`quotes/${language}.json`); + + if (data.quotes === undefined || data.quotes.length === 0) { + return defaultQuoteCollection; + } + + this.quoteCollection = { + quotes: [], + length: data.quotes.length, + groups: [], + language: data.language, + }; + + // Transform JSON Quote schema to MonkeyTypes Quote schema + data.quotes.forEach((quote: Quote) => { + const monkeyTypeQuote: MonkeyTypes.Quote = { + text: quote.text, + source: quote.source, + length: quote.length, + id: quote.id, + language: language, + }; + + this.quoteCollection.quotes.push(monkeyTypeQuote); + }); + + data.groups.forEach((quoteGroup, groupIndex) => { + const lower = quoteGroup[0]; + const upper = quoteGroup[1]; + + this.quoteCollection.groups[groupIndex] = + this.quoteCollection.quotes.filter((quote) => { + if (quote.length >= lower && quote.length <= upper) { + quote.group = groupIndex; + return true; + } + return false; + }); + }); + + if (quoteLengths !== undefined) { + this.updateQuoteQueue(quoteLengths); + } + } catch { + return defaultQuoteCollection; + } + } + + return this.quoteCollection; + } + + getQuoteById(id: number): MonkeyTypes.Quote | undefined { + const targetQuote = this.quoteCollection.quotes.find( + (quote: MonkeyTypes.Quote) => { + return quote.id === id; + } + ); + + return targetQuote; + } + + updateQuoteQueue(quoteGroups: number[]): void { + this.quoteQueue = []; + + quoteGroups.forEach((group) => { + if (group < 0) { + return; + } + this.quoteCollection.groups[group]?.forEach((quote) => { + this.quoteQueue.push(quote); + }); + }); + + shuffle(this.quoteQueue); + this.queueIndex = 0; + } + + getRandomQuote(): MonkeyTypes.Quote | null { + if (this.quoteQueue.length === 0) { + return null; + } + + if (this.queueIndex >= this.quoteQueue.length) { + this.queueIndex = 0; + shuffle(this.quoteQueue); + } + + const randomQuote = this.quoteQueue[this.queueIndex]; + + this.queueIndex += 1; + + return randomQuote; + } + + getCurrentQuote(): MonkeyTypes.Quote | null { + if (this.quoteQueue.length === 0) { + return null; + } + + return this.quoteQueue[this.queueIndex]; + } +} + +const quoteController = new QuotesController(); + +subscribe((key, newValue) => { + if (key === "quoteLength") { + quoteController.updateQuoteQueue(newValue as number[]); + } +}); + +export default quoteController; diff --git a/frontend/src/scripts/popups/quote-report-popup.ts b/frontend/src/scripts/popups/quote-report-popup.ts index 7ae683237..8df9f704f 100644 --- a/frontend/src/scripts/popups/quote-report-popup.ts +++ b/frontend/src/scripts/popups/quote-report-popup.ts @@ -3,7 +3,7 @@ import Config from "../config"; import * as TestWords from "../test/test-words"; import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; -import * as Misc from "../utils/misc"; +import QuotesController from "../controllers/quotes-controller"; const CAPTCHA_ID = 1; @@ -37,7 +37,7 @@ export async function show(options = defaultOptions): Promise { state.previousPopupShowCallback = previousPopupShowCallback; - const { quotes } = await Misc.getQuotes(Config.language); + const { quotes } = await QuotesController.getQuotes(Config.language); state.quoteToReport = quotes.find((quote) => { return quote.id === quoteId; }); diff --git a/frontend/src/scripts/popups/quote-search-popup.ts b/frontend/src/scripts/popups/quote-search-popup.ts index 29b1b5c1c..c2471e855 100644 --- a/frontend/src/scripts/popups/quote-search-popup.ts +++ b/frontend/src/scripts/popups/quote-search-popup.ts @@ -6,7 +6,6 @@ import * as Notifications from "../elements/notifications"; import * as QuoteSubmitPopup from "./quote-submit-popup"; import * as QuoteApprovePopup from "./quote-approve-popup"; import * as QuoteReportPopup from "./quote-report-popup"; -import * as Misc from "../utils/misc"; import { buildSearchService, SearchService, @@ -14,6 +13,7 @@ import { } from "../utils/search-service"; import { debounce } from "../utils/debounce"; import { splitByAndKeep } from "../utils/strings"; +import QuotesController from "../controllers/quotes-controller"; export let selectedId = 1; @@ -55,7 +55,7 @@ function highlightMatches(text: string, matchedText: string[]): string { } async function updateResults(searchText: string): Promise { - const { quotes } = await Misc.getQuotes(Config.language); + const { quotes } = await QuotesController.getQuotes(Config.language); const quoteSearchService = getSearchService( Config.language, diff --git a/frontend/src/scripts/test/test-logic.ts b/frontend/src/scripts/test/test-logic.ts index 4717bca96..a47fb4cbc 100644 --- a/frontend/src/scripts/test/test-logic.ts +++ b/frontend/src/scripts/test/test-logic.ts @@ -3,6 +3,7 @@ import * as TestUI from "./test-ui"; import * as ManualRestart from "./manual-restart-tracker"; import Config, * as UpdateConfig from "../config"; import * as Misc from "../utils/misc"; +import QuotesController from "../controllers/quotes-controller"; import * as Notifications from "../elements/notifications"; import * as CustomText from "./custom-text"; import * as TestStats from "./test-stats"; @@ -881,12 +882,13 @@ export async function init(): Promise { } } } - } else if (Config.mode == "quote") { - // setLanguage(Config.language.replace(/_\d*k$/g, ""), true); + } else if (Config.mode === "quote") { + const quotesCollection = await QuotesController.getQuotes( + Config.language, + Config.quoteLength + ); - const quotes = await Misc.getQuotes(Config.language.replace(/_\d*k$/g, "")); - - if (quotes.length === 0) { + if (quotesCollection.length === 0) { TestUI.setTestRestarting(false); Notifications.add( `No ${Config.language.replace(/_\d*k$/g, "")} quotes found`, @@ -902,49 +904,24 @@ export async function init(): Promise { let rq: MonkeyTypes.Quote | undefined = undefined; if (Config.quoteLength.includes(-2) && Config.quoteLength.length == 1) { - quotes.groups.forEach((group) => { - const filtered = (group).filter( - (quote) => quote.id == QuoteSearchPopup.selectedId - ); - if (filtered.length > 0) { - rq = filtered[0]; - } - }); - if (rq === undefined) { - rq = quotes.groups[0][0]; + const targetQuote = QuotesController.getQuoteById( + QuoteSearchPopup.selectedId + ); + if (targetQuote === undefined) { + rq = quotesCollection.groups[0][0]; Notifications.add("Quote Id Does Not Exist", 0); + } else { + rq = targetQuote; } } else { - const quoteLengths = Config.quoteLength; - let groupIndex; - if (quoteLengths.length > 1) { - groupIndex = - quoteLengths[Math.floor(Math.random() * quoteLengths.length)]; - while (quotes.groups[groupIndex].length === 0) { - groupIndex = - quoteLengths[Math.floor(Math.random() * quoteLengths.length)]; - } - } else { - groupIndex = quoteLengths[0]; - if (quotes.groups[groupIndex].length === 0) { - Notifications.add("No quotes found for selected quote length", 0); - TestUI.setTestRestarting(false); - return; - } + const randomQuote = QuotesController.getRandomQuote(); + if (randomQuote === null) { + Notifications.add("No quotes found for selected quote length", 0); + TestUI.setTestRestarting(false); + return; } - rq = quotes.groups[groupIndex][ - Math.floor(Math.random() * quotes.groups[groupIndex].length) - ] as MonkeyTypes.Quote; - if ( - TestWords.randomQuote != null && - typeof rq !== "number" && - rq.id === TestWords.randomQuote.id - ) { - rq = quotes.groups[groupIndex][ - Math.floor(Math.random() * quotes.groups[groupIndex].length) - ] as MonkeyTypes.Quote; - } + rq = randomQuote; } if (rq === undefined) return; diff --git a/frontend/src/scripts/test/test-words.ts b/frontend/src/scripts/test/test-words.ts index aab430771..367a01b7d 100644 --- a/frontend/src/scripts/test/test-words.ts +++ b/frontend/src/scripts/test/test-words.ts @@ -57,6 +57,7 @@ class Words { } } } + export const words = new Words(); export let hasTab = false; export let randomQuote = null as unknown as MonkeyTypes.Quote; diff --git a/frontend/src/scripts/utils/misc.ts b/frontend/src/scripts/utils/misc.ts index 1d591fb29..cfaee6b6c 100644 --- a/frontend/src/scripts/utils/misc.ts +++ b/frontend/src/scripts/utils/misc.ts @@ -132,60 +132,6 @@ export async function getFunbox( }); } -type QuoteCollection = { - quotes: MonkeyTypes.Quote[]; - length?: number; - language?: string; - groups: number[][] | MonkeyTypes.Quote[][]; -}; - -let quotes: QuoteCollection; -export async function getQuotes(language: string): Promise { - if ( - quotes === undefined || - quotes.language !== language.replace(/_\d*k$/g, "") - ) { - Loader.show(); - try { - const data: QuoteCollection = await $.getJSON(`quotes/${language}.json`); - Loader.hide(); - if (data.quotes === undefined || data.quotes.length === 0) { - quotes = { - quotes: [], - length: 0, - groups: [], - }; - return quotes; - } - quotes = data; - quotes.length = data.quotes.length; - quotes.groups?.forEach((qg, i) => { - const lower = qg[0]; - const upper = qg[1]; - quotes.groups[i] = quotes.quotes.filter((q) => { - if (q.length >= lower && q.length <= upper) { - q.group = i; - return true; - } else { - return false; - } - }); - }); - return quotes; - } catch { - Loader.hide(); - quotes = { - quotes: [], - length: 0, - groups: [], - }; - return quotes; - } - } else { - return quotes; - } -} - let layoutsList: MonkeyTypes.Layouts = {}; export async function getLayoutsList(): Promise { if (Object.keys(layoutsList).length === 0) { @@ -1025,3 +971,29 @@ export async function downloadResultsCSV( link.remove(); Loader.hide(); } + +/** + * Gets an integer between min and max, both are inclusive. + * @param min + * @param max + * @returns Random integer betwen min and max. + */ +export function randomIntFromRange(min: number, max: number): number { + const minNorm = Math.ceil(min); + const maxNorm = Math.floor(max); + return Math.floor(Math.random() * (maxNorm - minNorm + 1) + minNorm); +} + +/** + * Shuffle an array of elements using the Fisher–Yates algorithm. + * This function mutates the input array. + * @param elements + */ +export function shuffle(elements: T[]): void { + for (let i = elements.length - 1; i > 0; --i) { + const j = randomIntFromRange(0, i); + const temp = elements[j]; + elements[j] = elements[i]; + elements[i] = temp; + } +}