Files
cabrits-math/lib/problems/generators/integer-problems.ts
2026-07-05 11:12:45 -04:00

211 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.` },
],
};
}