238 lines
5.4 KiB
TypeScript
238 lines
5.4 KiB
TypeScript
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<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 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,
|
|
};
|
|
}
|