211 lines
8.2 KiB
TypeScript
211 lines
8.2 KiB
TypeScript
import type { MathProblem, Difficulty } from "../types";
|
||
import { randomInt, randomChoice } from "@/lib/utils";
|
||
|
||
let counter = 0;
|
||
function nextId() {
|
||
return `int-${++counter}-${Date.now()}`;
|
||
}
|
||
|
||
/** Wrap negatives in parentheses when they follow an operator, e.g. 5 + (-3). */
|
||
function signed(n: number): string {
|
||
return n < 0 ? `(${n})` : `${n}`;
|
||
}
|
||
|
||
export function generateIntegerAddSubtract(difficulty: Difficulty): MathProblem {
|
||
if (difficulty === 3) {
|
||
const a = randomInt(-12, 12);
|
||
const b = randomInt(-12, 12);
|
||
const c = randomInt(-12, 12);
|
||
const op1 = randomChoice(["+", "-"] as const);
|
||
const op2 = randomChoice(["+", "-"] as const);
|
||
const partial = op1 === "+" ? a + b : a - b;
|
||
const value = op2 === "+" ? partial + c : partial - c;
|
||
return {
|
||
id: nextId(),
|
||
prompt: `${a} ${op1} ${signed(b)} ${op2} ${signed(c)}`,
|
||
answer: { kind: "integer", value },
|
||
hints: [
|
||
"Work from left to right, one operation at a time.",
|
||
"Subtracting a negative is the same as adding a positive.",
|
||
`First: ${a} ${op1} ${signed(b)} = ${partial}`,
|
||
],
|
||
steps: [
|
||
{ explanation: `Work left to right. First: ${a} ${op1} ${signed(b)} = ${partial}` },
|
||
{ explanation: `Then: ${partial} ${op2} ${signed(c)} = ${value}` },
|
||
],
|
||
};
|
||
}
|
||
|
||
const range = difficulty === 1 ? 10 : 20;
|
||
const a = randomInt(-range, range);
|
||
const b = randomInt(-range, range);
|
||
const op = randomChoice(["+", "-"] as const);
|
||
const value = op === "+" ? a + b : a - b;
|
||
|
||
const rule =
|
||
op === "+"
|
||
? b < 0
|
||
? "Adding a negative means moving left — the same as subtracting."
|
||
: "Adding a positive means moving right."
|
||
: b < 0
|
||
? "Subtracting a negative is the same as adding a positive (move right)."
|
||
: "Subtracting a positive means moving left.";
|
||
|
||
return {
|
||
id: nextId(),
|
||
prompt: `${a} ${op} ${signed(b)}`,
|
||
answer: { kind: "integer", value },
|
||
hints: [
|
||
"Picture the first number on a number line.",
|
||
rule,
|
||
op === "-" && b < 0 ? "Two minus signs make a plus." : "Watch the sign of the second number.",
|
||
],
|
||
steps: [
|
||
{ explanation: `Start at ${a} on the number line.` },
|
||
{ explanation: rule },
|
||
{ explanation: `${a} ${op} ${signed(b)} = ${value}` },
|
||
],
|
||
};
|
||
}
|
||
|
||
export function generateIntegerMultiplyDivide(difficulty: Difficulty): MathProblem {
|
||
if (difficulty === 3) {
|
||
// triple product, e.g. (-2) × (-3) × (-4)
|
||
const a = randomInt(-5, 5) || 2;
|
||
const b = randomInt(-5, 5) || 3;
|
||
const c = randomInt(-5, 5) || 4;
|
||
const value = a * b * c;
|
||
const negatives = [a, b, c].filter((n) => n < 0).length;
|
||
return {
|
||
id: nextId(),
|
||
prompt: `${signed(a)} \\times ${signed(b)} \\times ${signed(c)}`,
|
||
answer: { kind: "integer", value },
|
||
hints: [
|
||
"Multiply two numbers at a time.",
|
||
"Count the negative signs: an even count gives a positive result, an odd count gives a negative result.",
|
||
`There are ${negatives} negative sign${negatives === 1 ? "" : "s"} here.`,
|
||
],
|
||
steps: [
|
||
{ explanation: `Multiply the first two: ${signed(a)} × ${signed(b)} = ${a * b}` },
|
||
{ explanation: `Then multiply by the last: ${signed(a * b)} × ${signed(c)} = ${value}` },
|
||
{ explanation: `${negatives} negative signs → ${negatives % 2 === 0 ? "positive" : "negative"} answer.` },
|
||
],
|
||
};
|
||
}
|
||
|
||
const op = randomChoice(["\\times", "\\div"] as const);
|
||
const factorRange = difficulty === 1 ? 9 : 12;
|
||
|
||
if (op === "\\times") {
|
||
const a = randomInt(-factorRange, factorRange) || 3;
|
||
const b = randomInt(-factorRange, factorRange) || 4;
|
||
const value = a * b;
|
||
const sameSign = a < 0 === b < 0;
|
||
return {
|
||
id: nextId(),
|
||
prompt: `${signed(a)} \\times ${signed(b)}`,
|
||
answer: { kind: "integer", value },
|
||
hints: [
|
||
"Multiply the numbers as normal, then decide the sign.",
|
||
"Same signs → positive. Different signs → negative.",
|
||
`These signs are ${sameSign ? "the same" : "different"}.`,
|
||
],
|
||
steps: [
|
||
{ explanation: `Multiply the sizes: ${Math.abs(a)} × ${Math.abs(b)} = ${Math.abs(value)}` },
|
||
{ explanation: `${sameSign ? "Same signs give a positive" : "Different signs give a negative"} answer.` },
|
||
{ explanation: `${signed(a)} × ${signed(b)} = ${value}` },
|
||
],
|
||
};
|
||
}
|
||
|
||
// division — build from an exact product so the answer is a whole number
|
||
const divisor = (randomInt(-factorRange, factorRange) || 3);
|
||
const quotient = (randomInt(-factorRange, factorRange) || 2);
|
||
const dividend = divisor * quotient;
|
||
const sameSign = divisor < 0 === dividend < 0;
|
||
return {
|
||
id: nextId(),
|
||
prompt: `${signed(dividend)} \\div ${signed(divisor)}`,
|
||
answer: { kind: "integer", value: quotient },
|
||
hints: [
|
||
"Divide the numbers as normal, then decide the sign.",
|
||
"Same signs → positive. Different signs → negative.",
|
||
`These signs are ${sameSign ? "the same" : "different"}.`,
|
||
],
|
||
steps: [
|
||
{ explanation: `Divide the sizes: ${Math.abs(dividend)} ÷ ${Math.abs(divisor)} = ${Math.abs(quotient)}` },
|
||
{ explanation: `${sameSign ? "Same signs give a positive" : "Different signs give a negative"} answer.` },
|
||
{ explanation: `${signed(dividend)} ÷ ${signed(divisor)} = ${quotient}` },
|
||
],
|
||
};
|
||
}
|
||
|
||
export function generateIntegerOrderCompare(difficulty: Difficulty): MathProblem {
|
||
const range = difficulty === 1 ? 10 : difficulty === 2 ? 20 : 30;
|
||
|
||
if (difficulty === 3) {
|
||
const nums: number[] = [];
|
||
while (nums.length < 3) {
|
||
const v = randomInt(-range, range);
|
||
if (!nums.includes(v)) nums.push(v);
|
||
}
|
||
const wantLargest = Math.random() < 0.5;
|
||
const value = wantLargest ? Math.max(...nums) : Math.min(...nums);
|
||
return {
|
||
id: nextId(),
|
||
prompt: `\\text{Which is the ${wantLargest ? "largest" : "smallest"}: } ${nums.join(",\\ ")}\\text{ ?}`,
|
||
answer: { kind: "integer", value },
|
||
hints: ["Picture them on a number line.", "Further right is larger; further left is smaller."],
|
||
steps: [{ explanation: `The ${wantLargest ? "largest" : "smallest"} is ${value}.` }],
|
||
};
|
||
}
|
||
|
||
const a = randomInt(-range, range);
|
||
let b = randomInt(-range, range);
|
||
if (a === b) b = a + 1;
|
||
const wantGreater = Math.random() < 0.5;
|
||
const value = wantGreater ? Math.max(a, b) : Math.min(a, b);
|
||
const dir = wantGreater ? "right" : "left";
|
||
|
||
return {
|
||
id: nextId(),
|
||
prompt: `\\text{Which is ${wantGreater ? "greater" : "smaller"}, } ${a} \\text{ or } ${b}\\text{? Enter it.}`,
|
||
answer: { kind: "integer", value },
|
||
hints: [`On a number line, the number further ${dir} wins.`, "Negatives get smaller the further left they are."],
|
||
steps: [{ explanation: `${value} is ${wantGreater ? "greater" : "smaller"}.` }],
|
||
};
|
||
}
|
||
|
||
/** Add/subtract integers, occasionally as a real-world temperature word problem. */
|
||
export function generateIntegerAddSubtractMixed(difficulty: Difficulty): MathProblem {
|
||
return Math.random() < 0.3 ? generateTemperatureProblem(difficulty) : generateIntegerAddSubtract(difficulty);
|
||
}
|
||
|
||
export function generateTemperatureProblem(difficulty: Difficulty): MathProblem {
|
||
const cities = ["London", "Moscow", "Toronto", "Oslo", "Helsinki"];
|
||
const days = ["Monday", "Tuesday", "Wednesday", "Thursday"];
|
||
const city = randomChoice(cities);
|
||
const day = randomChoice(days);
|
||
const start = randomInt(-8, 3);
|
||
const rose = Math.random() < 0.5;
|
||
const change = randomInt(2, difficulty === 1 ? 6 : 12);
|
||
const value = rose ? start + change : start - change;
|
||
const verb = rose ? "rose" : "fell";
|
||
|
||
return {
|
||
id: nextId(),
|
||
prompt: `\\begin{array}{l}\\text{In ${city} the temperature was ${start}°C on ${day}.}\\\\\\text{The next day it ${verb} by ${change}°C.}\\\\\\text{What was the temperature then?}\\end{array}`,
|
||
answer: { kind: "integer", value },
|
||
hints: [
|
||
`Start at ${start}°C.`,
|
||
`${rose ? "Rising" : "Falling"} means ${rose ? "add" : "subtract"} ${change}.`,
|
||
`${start} ${rose ? "+" : "-"} ${change} = ${value}`,
|
||
],
|
||
steps: [
|
||
{ explanation: `Start at ${start}°C.` },
|
||
{ explanation: `It ${verb}, so ${rose ? "add" : "subtract"} ${change}: ${start} ${rose ? "+" : "-"} ${change} = ${value}` },
|
||
{ explanation: `The temperature was ${value}°C.` },
|
||
],
|
||
};
|
||
}
|