괄호 안의 항을 2, 3, 4 항으로 설정하는 기능 추가

This commit is contained in:
2026-05-31 13:40:45 +09:00
parent e148a957ea
commit ac3c939051
21 changed files with 708 additions and 344 deletions
+150 -69
View File
@@ -11,13 +11,14 @@ 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;
const MAX_TERM_COUNT = 4;
export type VariableParenthesisTermCount = 2 | 3 | 4;
interface VariableProblemSeed {
multiplier: number;
operator: "+" | "-";
firstTerm: VariableParenthesisTerm;
secondTerm: VariableParenthesisTerm;
terms: VariableParenthesisTerm[];
operators: ("+" | "-")[];
}
function randomInt(min: number, max: number) {
@@ -52,6 +53,26 @@ function createVariableTerm(coefficient: number): VariableParenthesisTerm {
};
}
function createUniqueConstantTerms(
baseCoefficient: number,
count: number,
): VariableParenthesisTerm[] {
const used = new Set<number>();
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}`;
}
@@ -79,18 +100,21 @@ function formatParenthesisTermLatex(term: VariableParenthesisTerm) {
}
export function formatVariableExpressionLatex(
terms: [DistributedVariableTerm, DistributedVariableTerm],
terms: DistributedVariableTerm[],
) {
const [firstTerm, secondTerm] = terms;
const operator = secondTerm.coefficient < 0 ? "-" : "+";
const positiveSecondTerm = {
...secondTerm,
coefficient: Math.abs(secondTerm.coefficient),
};
return terms
.map((term, index) => {
if (index === 0) return term.latex;
return `${firstTerm.latex} ${operator} ${formatVariableTermLatex(
positiveSecondTerm,
)}`;
const operator = term.coefficient < 0 ? "-" : "+";
const positiveTerm = {
...term,
coefficient: Math.abs(term.coefficient),
};
return `${operator} ${formatVariableTermLatex(positiveTerm)}`;
})
.join(" ");
}
function distributeTerm(
@@ -110,7 +134,7 @@ function distributeTerm(
function createChoice(
key: string,
termIndex: 0 | 1,
termIndex: number,
term: DistributedVariableTerm,
coefficient: number,
): VariableTermChoice {
@@ -126,7 +150,7 @@ function createChoice(
function createChoices(
key: string,
termIndex: 0 | 1,
termIndex: number,
term: DistributedVariableTerm,
) {
const absoluteCoefficient = Math.abs(term.coefficient);
@@ -139,40 +163,69 @@ function createChoices(
}
function createProblem(seed: VariableProblemSeed): VariableParenthesisProblem {
const { multiplier, operator, firstTerm, secondTerm } = seed;
const { multiplier, terms, operators } = seed;
const [firstTerm, secondTerm] = terms;
const [operator] = operators;
const key = [
multiplier,
termKey(firstTerm),
operator,
termKey(secondTerm),
...terms.flatMap((term, index) =>
index === 0 ? [termKey(term)] : [operators[index - 1], termKey(term)],
),
].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]),
];
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(${formatParenthesisTermLatex(
firstTerm,
)} ${operator} ${formatParenthesisTermLatex(secondTerm)}\\right)`,
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 createAllProblemSeeds() {
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;
@@ -189,19 +242,22 @@ function createAllProblemSeeds() {
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),
});
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,
});
}
}
}
}
@@ -210,43 +266,70 @@ function createAllProblemSeeds() {
return seeds;
}
const ALL_PROBLEM_SEEDS = createAllProblemSeeds();
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);
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 ? "+" : "-";
const variableIndex = randomInt(0, termCount - 1);
const constantTerms = createUniqueConstantTerms(constant, termCount - 1);
let constantTermIndex = 0;
return createProblem({
multiplier: randomInt(MIN_MULTIPLIER, MAX_MULTIPLIER),
operator,
firstTerm: variableFirst
? createVariableTerm(variableCoefficient)
: createConstantTerm(constant),
secondTerm: variableFirst
? createConstantTerm(constant)
: createVariableTerm(variableCoefficient),
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.");
}
if (count > MAX_PROBLEM_COUNT) {
throw new Error(`count cannot exceed ${MAX_PROBLEM_COUNT}.`);
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(ALL_PROBLEM_SEEDS).slice(0, count).map(createProblem);
return shuffle(seeds).slice(0, count).map(createProblem);
}
function findSelectedChoice(
problem: VariableParenthesisProblem,
termIndex: 0 | 1,
termIndex: number,
choiceId: string | null,
): DistributedVariableTerm | null {
if (!choiceId) return null;
@@ -267,16 +350,14 @@ 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 userAnswer = problem.correctTerms.map((_, index) =>
findSelectedChoice(problem, index, answer.selectedChoiceIds[index] ?? null),
);
const correct =
userAnswer[0]?.coefficient === problem.correctTerms[0].coefficient &&
userAnswer[1]?.coefficient === problem.correctTerms[1].coefficient;
userAnswer.length === problem.correctTerms.length &&
userAnswer.every(
(term, index) => term?.coefficient === problem.correctTerms[index].coefficient,
);
return {
correct,