mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-17 19:15:59 +08:00
Add logic to prevent repeated random quotes (#2693)
* Add logic to prevent repeated random quotes
This commit is contained in:
parent
4213b6576c
commit
cb6ddc5fc7
6 changed files with 207 additions and 101 deletions
156
frontend/src/scripts/controllers/quotes-controller.ts
Normal file
156
frontend/src/scripts/controllers/quotes-controller.ts
Normal file
|
@ -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<QuoteCollection> {
|
||||
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;
|
|
@ -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<void> {
|
|||
|
||||
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;
|
||||
});
|
||||
|
|
|
@ -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<void> {
|
||||
const { quotes } = await Misc.getQuotes(Config.language);
|
||||
const { quotes } = await QuotesController.getQuotes(Config.language);
|
||||
|
||||
const quoteSearchService = getSearchService<MonkeyTypes.Quote>(
|
||||
Config.language,
|
||||
|
|
|
@ -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<void> {
|
|||
}
|
||||
}
|
||||
}
|
||||
} 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<void> {
|
|||
|
||||
let rq: MonkeyTypes.Quote | undefined = undefined;
|
||||
if (Config.quoteLength.includes(-2) && Config.quoteLength.length == 1) {
|
||||
quotes.groups.forEach((group) => {
|
||||
const filtered = (<MonkeyTypes.Quote[]>group).filter(
|
||||
(quote) => quote.id == QuoteSearchPopup.selectedId
|
||||
);
|
||||
if (filtered.length > 0) {
|
||||
rq = filtered[0];
|
||||
}
|
||||
});
|
||||
if (rq === undefined) {
|
||||
rq = <MonkeyTypes.Quote>quotes.groups[0][0];
|
||||
const targetQuote = QuotesController.getQuoteById(
|
||||
QuoteSearchPopup.selectedId
|
||||
);
|
||||
if (targetQuote === undefined) {
|
||||
rq = <MonkeyTypes.Quote>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;
|
||||
|
|
|
@ -57,6 +57,7 @@ class Words {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const words = new Words();
|
||||
export let hasTab = false;
|
||||
export let randomQuote = null as unknown as MonkeyTypes.Quote;
|
||||
|
|
|
@ -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<QuoteCollection> {
|
||||
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<MonkeyTypes.Layouts> {
|
||||
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<T>(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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue