import type { GradeResult, ParenthesisSignProblem, UserAnswer, } from "./types"; const MIN_MULTIPLIER = -9; const MAX_MULTIPLIER = -1; const MIN_TERM = 1; const MAX_TERM = 9; const MAX_TERM_COUNT = 4; export type ParenthesisTermCount = 2 | 3 | 4; interface ProblemSeed { multiplier: number; terms: number[]; operators: ("+" | "-")[]; } function randomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } function formatLatexTerms(terms: number[]) { return terms .map((term, index) => { if (index === 0) return `${term}`; const operator = term < 0 ? "-" : "+"; return `${operator} ${Math.abs(term)}`; }) .join(" "); } function formatParenthesisTerms( terms: number[], operators: ("+" | "-")[], ) { return terms .map((term, index) => { if (index === 0) return `${term}`; return `${operators[index - 1]} ${term}`; }) .join(" "); } function createProblem({ multiplier, terms, operators, }: ProblemSeed): ParenthesisSignProblem { const signedTerms = terms.map((term, index) => { if (index === 0) return term; return operators[index - 1] === "-" ? -term : term; }); const distributedTerms = signedTerms.map((term) => multiplier * term); const [a, b] = terms; const [operator] = operators; const [distributedA, distributedB] = distributedTerms; const key = [ multiplier, ...terms.flatMap((term, index) => index === 0 ? [`${term}`] : [operators[index - 1], `${term}`], ), ].join(":"); return { id: `parenthesis-sign:${key}`, key, multiplier, terms, operators, a, operator, b, distributedTerms, distributedA, distributedB, latexBefore: `${multiplier}\\left(${formatParenthesisTerms( terms, operators, )}\\right)`, latexAfter: formatLatexTerms(distributedTerms), }; } function createTermCombinations(termCount: ParenthesisTermCount) { const combinations: number[][] = []; function addTerms(nextTerms: number[]) { if (nextTerms.length === termCount) { combinations.push(nextTerms); return; } for (let term = MIN_TERM; term <= MAX_TERM; term += 1) { if (nextTerms.includes(term)) continue; addTerms([...nextTerms, term]); } } addTerms([]); return combinations; } function createOperatorCombinations(termCount: ParenthesisTermCount) { const combinations: ("+" | "-")[][] = []; const operatorCount = termCount - 1; function addOperators(nextOperators: ("+" | "-")[]) { if (nextOperators.length === operatorCount) { combinations.push(nextOperators); return; } addOperators([...nextOperators, "+"]); addOperators([...nextOperators, "-"]); } addOperators([]); return combinations; } function createAllProblemSeeds(termCount: ParenthesisTermCount) { const seeds: ProblemSeed[] = []; const termCombinations = createTermCombinations(termCount); const operatorCombinations = createOperatorCombinations(termCount); for ( let multiplier = MIN_MULTIPLIER; multiplier <= MAX_MULTIPLIER; multiplier += 1 ) { for (const terms of termCombinations) { for (const operators of operatorCombinations) { seeds.push({ multiplier, terms, operators }); } } } return seeds; } const ALL_PROBLEM_SEEDS_BY_TERM_COUNT: Record< ParenthesisTermCount, ProblemSeed[] > = { 2: createAllProblemSeeds(2), 3: createAllProblemSeeds(3), 4: createAllProblemSeeds(4), }; function shuffle(items: T[]) { const shuffled = [...items]; for (let index = shuffled.length - 1; index > 0; index -= 1) { const targetIndex = randomInt(0, index); const current = shuffled[index]; shuffled[index] = shuffled[targetIndex]; shuffled[targetIndex] = current; } return shuffled; } function assertTermCount(termCount: number): asserts termCount is ParenthesisTermCount { if (!Number.isInteger(termCount) || termCount < 2 || termCount > MAX_TERM_COUNT) { throw new Error("termCount must be 2, 3, or 4."); } } export function generateProblem( termCount: ParenthesisTermCount = 2, ): ParenthesisSignProblem { assertTermCount(termCount); const terms: number[] = []; while (terms.length < termCount) { const term = randomInt(MIN_TERM, MAX_TERM); if (!terms.includes(term)) { terms.push(term); } } return createProblem({ multiplier: randomInt(MIN_MULTIPLIER, MAX_MULTIPLIER), terms, operators: Array.from({ length: termCount - 1 }, () => Math.random() < 0.5 ? "+" : "-", ), }); } export function generateSession( count = 10, termCount: ParenthesisTermCount = 2, ): ParenthesisSignProblem[] { if (!Number.isInteger(count) || count < 1) { throw new Error("count must be a positive integer."); } assertTermCount(termCount); const seeds = ALL_PROBLEM_SEEDS_BY_TERM_COUNT[termCount]; if (count > seeds.length) { throw new Error(`count cannot exceed ${seeds.length}.`); } return shuffle(seeds).slice(0, count).map(createProblem); } export function gradeAnswer( problem: ParenthesisSignProblem, answer: UserAnswer, ): GradeResult { const userAnswer = answer.terms ?? (answer.a !== undefined && answer.b !== undefined ? [answer.a, answer.b] : []); return { correct: userAnswer.length === problem.distributedTerms.length && userAnswer.every((value, index) => value === problem.distributedTerms[index]), userAnswer, correctAnswer: problem.distributedTerms, }; }