Files
math-quiz/lib/engine/variable-parenthesis.ts
T

287 lines
7.2 KiB
TypeScript

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_PROBLEM_COUNT = 9 * 9 * 9 * 2 * 2;
interface VariableProblemSeed {
multiplier: number;
operator: "+" | "-";
firstTerm: VariableParenthesisTerm;
secondTerm: VariableParenthesisTerm;
}
function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function shuffle<T>(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 termKey(term: VariableParenthesisTerm) {
return `${term.kind}:${term.coefficient}`;
}
export function formatVariableTermLatex(
term: Pick<VariableParenthesisTerm, "kind" | "coefficient" | "variable">,
) {
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, DistributedVariableTerm],
) {
const [firstTerm, secondTerm] = terms;
const operator = secondTerm.coefficient < 0 ? "-" : "+";
const positiveSecondTerm = {
...secondTerm,
coefficient: Math.abs(secondTerm.coefficient),
};
return `${firstTerm.latex} ${operator} ${formatVariableTermLatex(
positiveSecondTerm,
)}`;
}
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: 0 | 1,
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: 0 | 1,
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, operator, firstTerm, secondTerm } = seed;
const key = [
multiplier,
termKey(firstTerm),
operator,
termKey(secondTerm),
].join(":");
const correctTerms: [DistributedVariableTerm, DistributedVariableTerm] = [
distributeTerm(multiplier, firstTerm, 1),
distributeTerm(multiplier, secondTerm, operator === "-" ? -1 : 1),
];
const choices: [VariableTermChoice[], VariableTermChoice[]] = [
createChoices(key, 0, correctTerms[0]),
createChoices(key, 1, correctTerms[1]),
];
return {
id: `variable-parenthesis:${key}`,
key,
multiplier,
operator,
firstTerm,
secondTerm,
correctTerms,
choices,
latexBefore: `${multiplier}\\left(${formatParenthesisTermLatex(
firstTerm,
)} ${operator} ${formatParenthesisTermLatex(secondTerm)}\\right)`,
latexAfter: formatVariableExpressionLatex(correctTerms),
};
}
function createAllProblemSeeds() {
const seeds: VariableProblemSeed[] = [];
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 (const operator of ["+", "-"] as const) {
seeds.push({
multiplier,
operator,
firstTerm: createVariableTerm(variableCoefficient),
secondTerm: createConstantTerm(constant),
});
seeds.push({
multiplier,
operator,
firstTerm: createConstantTerm(constant),
secondTerm: createVariableTerm(variableCoefficient),
});
}
}
}
}
return seeds;
}
const ALL_PROBLEM_SEEDS = createAllProblemSeeds();
export function generateVariableParenthesisProblem(): VariableParenthesisProblem {
const constant = randomInt(MIN_COEFFICIENT, MAX_COEFFICIENT);
const variableCoefficient = randomInt(MIN_COEFFICIENT, MAX_COEFFICIENT);
const variableFirst = Math.random() < 0.5;
const operator = Math.random() < 0.5 ? "+" : "-";
return createProblem({
multiplier: randomInt(MIN_MULTIPLIER, MAX_MULTIPLIER),
operator,
firstTerm: variableFirst
? createVariableTerm(variableCoefficient)
: createConstantTerm(constant),
secondTerm: variableFirst
? createConstantTerm(constant)
: createVariableTerm(variableCoefficient),
});
}
export function generateVariableParenthesisSession(
count = 10,
): VariableParenthesisProblem[] {
if (!Number.isInteger(count) || count < 1) {
throw new Error("count must be a positive integer.");
}
if (count > MAX_PROBLEM_COUNT) {
throw new Error(`count cannot exceed ${MAX_PROBLEM_COUNT}.`);
}
return shuffle(ALL_PROBLEM_SEEDS).slice(0, count).map(createProblem);
}
function findSelectedChoice(
problem: VariableParenthesisProblem,
termIndex: 0 | 1,
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: [
DistributedVariableTerm | null,
DistributedVariableTerm | null,
] = [
findSelectedChoice(problem, 0, answer.selectedChoiceIds[0]),
findSelectedChoice(problem, 1, answer.selectedChoiceIds[1]),
];
const correct =
userAnswer[0]?.coefficient === problem.correctTerms[0].coefficient &&
userAnswer[1]?.coefficient === problem.correctTerms[1].coefficient;
return {
correct,
userAnswer,
correctAnswer: problem.correctTerms,
};
}