import type { DistributedVariableTerm, VariableParenthesisProblem, VariableParenthesisTerm, VariableTermChoice, VariableUserAnswer, VariableGradeResult, } from "./variable-types"; const MIN_MULTIPLIER = -9; const MAX_MULTIPLIER = -1; const MIN_COEFFICIENT = 1; const MAX_COEFFICIENT = 9; const MAX_TERM_COUNT = 4; export type VariableParenthesisTermCount = 2 | 3 | 4; interface VariableProblemSeed { multiplier: number; terms: VariableParenthesisTerm[]; operators: ("+" | "-")[]; } function randomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } 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 createConstantTerm(coefficient: number): VariableParenthesisTerm { return { kind: "constant", coefficient, }; } function createVariableTerm(coefficient: number): VariableParenthesisTerm { return { kind: "variable", coefficient, variable: "x", }; } function createUniqueConstantTerms( baseCoefficient: number, count: number, ): VariableParenthesisTerm[] { const used = new Set(); const terms: VariableParenthesisTerm[] = []; let candidate = baseCoefficient; while (terms.length < count) { const coef = ((candidate - 1) % MAX_COEFFICIENT) + 1; if (!used.has(coef)) { used.add(coef); terms.push(createConstantTerm(coef)); } candidate += 1; } return terms; } function termKey(term: VariableParenthesisTerm) { return `${term.kind}:${term.coefficient}`; } export function formatVariableTermLatex( term: Pick, ) { if (term.kind === "constant") { return `${term.coefficient}`; } if (term.coefficient === 1) { return "x"; } if (term.coefficient === -1) { return "-x"; } return `${term.coefficient}x`; } function formatParenthesisTermLatex(term: VariableParenthesisTerm) { return formatVariableTermLatex(term); } export function formatVariableExpressionLatex( terms: DistributedVariableTerm[], ) { return terms .map((term, index) => { if (index === 0) return term.latex; const operator = term.coefficient < 0 ? "-" : "+"; const positiveTerm = { ...term, coefficient: Math.abs(term.coefficient), }; return `${operator} ${formatVariableTermLatex(positiveTerm)}`; }) .join(" "); } function distributeTerm( multiplier: number, term: VariableParenthesisTerm, sign: 1 | -1, ): DistributedVariableTerm { const coefficient = multiplier * sign * term.coefficient; return { kind: term.kind, coefficient, variable: term.variable, latex: formatVariableTermLatex({ ...term, coefficient }), }; } function createChoice( key: string, termIndex: number, term: DistributedVariableTerm, coefficient: number, ): VariableTermChoice { const signKey = coefficient < 0 ? "negative" : "positive"; return { id: `${key}:term-${termIndex}:${signKey}`, coefficient, variable: term.variable, latex: formatVariableTermLatex({ ...term, coefficient }), }; } function createChoices( key: string, termIndex: number, term: DistributedVariableTerm, ) { const absoluteCoefficient = Math.abs(term.coefficient); const choices: VariableTermChoice[] = [ createChoice(key, termIndex, term, -absoluteCoefficient), createChoice(key, termIndex, term, absoluteCoefficient), ]; return shuffle(choices); } function createProblem(seed: VariableProblemSeed): VariableParenthesisProblem { const { multiplier, terms, operators } = seed; const [firstTerm, secondTerm] = terms; const [operator] = operators; const key = [ multiplier, ...terms.flatMap((term, index) => index === 0 ? [termKey(term)] : [operators[index - 1], termKey(term)], ), ].join(":"); const correctTerms = terms.map((term, index) => distributeTerm( multiplier, term, index === 0 || operators[index - 1] === "+" ? 1 : -1, ), ); const choices = correctTerms.map((term, index) => createChoices(key, index, term), ); return { id: `variable-parenthesis:${key}`, key, multiplier, terms, operators, operator, firstTerm, secondTerm, correctTerms, choices, latexBefore: `${multiplier}\\left(${terms .map((term, index) => { if (index === 0) return formatParenthesisTermLatex(term); return `${operators[index - 1]} ${formatParenthesisTermLatex(term)}`; }) .join(" ")}\\right)`, latexAfter: formatVariableExpressionLatex(correctTerms), }; } function createOperatorCombinations(termCount: VariableParenthesisTermCount) { const combinations: ("+" | "-")[][] = []; function addOperators(nextOperators: ("+" | "-")[]) { if (nextOperators.length === termCount - 1) { combinations.push(nextOperators); return; } addOperators([...nextOperators, "+"]); addOperators([...nextOperators, "-"]); } addOperators([]); return combinations; } function createAllProblemSeeds(termCount: VariableParenthesisTermCount) { const seeds: VariableProblemSeed[] = []; const operatorCombinations = createOperatorCombinations(termCount); for ( let multiplier = MIN_MULTIPLIER; multiplier <= MAX_MULTIPLIER; multiplier += 1 ) { for ( let constant = MIN_COEFFICIENT; constant <= MAX_COEFFICIENT; constant += 1 ) { for ( let variableCoefficient = MIN_COEFFICIENT; variableCoefficient <= MAX_COEFFICIENT; variableCoefficient += 1 ) { for (let variableIndex = 0; variableIndex < termCount; variableIndex += 1) { const constantTerms = createUniqueConstantTerms(constant, termCount - 1); let constantTermIndex = 0; const terms = Array.from({ length: termCount }, (_, index) => index === variableIndex ? createVariableTerm(variableCoefficient) : constantTerms[constantTermIndex++], ); for (const operators of operatorCombinations) { seeds.push({ multiplier, terms, operators, }); } } } } } return seeds; } const ALL_PROBLEM_SEEDS_BY_TERM_COUNT: Record< VariableParenthesisTermCount, VariableProblemSeed[] > = { 2: createAllProblemSeeds(2), 3: createAllProblemSeeds(3), 4: createAllProblemSeeds(4), }; function assertTermCount( termCount: number, ): asserts termCount is VariableParenthesisTermCount { if (!Number.isInteger(termCount) || termCount < 2 || termCount > MAX_TERM_COUNT) { throw new Error("termCount must be 2, 3, or 4."); } } export function generateVariableParenthesisProblem( termCount: VariableParenthesisTermCount = 2, ): VariableParenthesisProblem { assertTermCount(termCount); const constant = randomInt(MIN_COEFFICIENT, MAX_COEFFICIENT); const variableCoefficient = randomInt(MIN_COEFFICIENT, MAX_COEFFICIENT); const variableIndex = randomInt(0, termCount - 1); const constantTerms = createUniqueConstantTerms(constant, termCount - 1); let constantTermIndex = 0; return createProblem({ multiplier: randomInt(MIN_MULTIPLIER, MAX_MULTIPLIER), terms: Array.from({ length: termCount }, (_, index) => index === variableIndex ? createVariableTerm(variableCoefficient) : constantTerms[constantTermIndex++], ), operators: Array.from({ length: termCount - 1 }, () => Math.random() < 0.5 ? "+" : "-", ), }); } export function generateVariableParenthesisSession( count = 10, termCount: VariableParenthesisTermCount = 2, ): VariableParenthesisProblem[] { 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); } function findSelectedChoice( problem: VariableParenthesisProblem, termIndex: number, choiceId: string | null, ): DistributedVariableTerm | null { if (!choiceId) return null; const choice = problem.choices[termIndex].find((item) => item.id === choiceId); if (!choice) return null; return { kind: problem.correctTerms[termIndex].kind, coefficient: choice.coefficient, variable: choice.variable, latex: choice.latex, }; } export function gradeVariableParenthesisAnswer( problem: VariableParenthesisProblem, answer: VariableUserAnswer, ): VariableGradeResult { const userAnswer = problem.correctTerms.map((_, index) => findSelectedChoice(problem, index, answer.selectedChoiceIds[index] ?? null), ); const correct = userAnswer.length === problem.correctTerms.length && userAnswer.every( (term, index) => term?.coefficient === problem.correctTerms[index].coefficient, ); return { correct, userAnswer, correctAnswer: problem.correctTerms, }; }