math4 / math-engine.js
matt-ritchey's picture
Upload 6 files
fd586d8 verified
Raw History Blame Contribute Delete
11.1 kB
const MathEngine = {
_rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min },
_pick(arr) { return arr[Math.floor(Math.random() * arr.length)] },
generateProblem(topic, difficulty) {
const level = Math.min(3, Math.max(1, difficulty || 1));
const ops = ['addition', 'subtraction', 'multiplication', 'division', 'word-problems'];
const t = topic === 'mixed' ? this._pick(ops) : topic;
switch (t) {
case 'addition': return this._genAddition(level);
case 'subtraction': return this._genSubtraction(level);
case 'multiplication': return this._genMultiplication(level);
case 'division': return this._genDivision(level);
case 'word-problems': return this._genWordProblem(level);
default: return this._genAddition(level);
}
},
generateQuizQuestions(topic, difficulty, count) {
const questions = [];
for (let i = 0; i < count; i++) {
questions.push(this.generateProblem(topic, difficulty));
}
return questions;
},
_genAddition(level) {
let a, b;
if (level === 1) { a = this._rand(10, 99); b = this._rand(10, 99); }
else if (level === 2) { a = this._rand(100, 999); b = this._rand(100, 999); }
else { a = this._rand(1000, 9999); b = this._rand(1000, 9999); }
return {
type: 'addition',
display: `${a} + ${b}`,
answer: a + b,
steps: this._additionSteps(a, b)
};
},
_additionSteps(a, b) {
const result = a + b;
const s = [];
s.push('Line up the numbers by place value:');
s.push(`${a} + ${b}`);
const sa = String(a), sb = String(b);
const maxLen = Math.max(sa.length, sb.length);
const pa = sa.padStart(maxLen, '0');
const pb = sb.padStart(maxLen, '0');
const places = ['ones', 'tens', 'hundreds', 'thousands', 'ten-thousands'];
let carry = 0;
for (let i = maxLen - 1; i >= 0; i--) {
const idx = maxLen - 1 - i;
const da = parseInt(pa[i]);
const db = parseInt(pb[i]);
const sum = da + db + carry;
const digit = sum % 10;
const newCarry = Math.floor(sum / 10);
const pname = places[idx] || 'next';
if (carry > 0) {
s.push(`${pname}: ${da} + ${db} + carried ${carry} = ${sum}. Write ${digit}.`);
} else {
s.push(`${pname}: ${da} + ${db} = ${sum}. Write ${digit}.`);
}
carry = newCarry;
}
if (carry > 0) s.push(`Write the carried ${carry} at the front.`);
s.push(`Answer: ${result}`);
return s;
},
_genSubtraction(level) {
let a, b;
if (level === 1) { a = this._rand(10, 99); b = this._rand(10, a); }
else if (level === 2) { a = this._rand(100, 999); b = this._rand(100, a); }
else { a = this._rand(1000, 9999); b = this._rand(1000, a); }
return {
type: 'subtraction',
display: `${a} - ${b}`,
answer: a - b,
steps: this._subtractionSteps(a, b)
};
},
_subtractionSteps(a, b) {
const result = a - b;
const s = [];
s.push('Line up the numbers by place value:');
s.push(`${a} - ${b}`);
const sa = String(a), sb = String(b);
const maxLen = Math.max(sa.length, sb.length);
const pa = sa.padStart(maxLen, '0');
const pb = sb.padStart(maxLen, '0');
const places = ['ones', 'tens', 'hundreds', 'thousands', 'ten-thousands'];
let digits = pa.split('').map(Number);
for (let i = maxLen - 1; i >= 0; i--) {
const idx = maxLen - 1 - i;
let da = digits[i];
const db = parseInt(pb[i]);
const pname = places[idx] || 'next';
if (da < db) {
digits[i - 1]--;
digits[i] += 10;
da = digits[i];
const borrowFrom = places[idx + 1] || 'next';
s.push(`Borrow 1 from ${borrowFrom}: ${da - 10} becomes ${da}.`);
}
s.push(`${pname}: ${da} - ${db} = ${da - db}. Write ${da - db}.`);
}
s.push(`Answer: ${result}`);
return s;
},
_genMultiplication(level) {
let a, b;
if (level === 1) { a = this._rand(2, 9); b = this._rand(10, 99); }
else if (level === 2) {
if (Math.random() < 0.5) { a = this._rand(2, 9); b = this._rand(100, 999); }
else { a = this._rand(11, 99); b = this._rand(11, 99); }
} else { a = this._rand(11, 99); b = this._rand(100, 999); }
return {
type: 'multiplication',
display: `${a} × ${b}`,
answer: a * b,
steps: this._multiplicationSteps(a, b)
};
},
_multiplicationSteps(a, b) {
const result = a * b;
const s = [];
s.push(`Multiply ${a} by ${b}:`);
const sb = String(b);
if (sb.length === 1) {
let carry = 0;
const sa = String(a);
for (let i = sa.length - 1; i >= 0; i--) {
const digit = parseInt(sa[i]);
const product = digit * b + carry;
const write = product % 10;
carry = Math.floor(product / 10);
s.push(`${digit} × ${b} + carry = ${product}. Write ${write}.`);
}
if (carry > 0) s.push(`Write the carry ${carry}.`);
} else {
const bStr = String(b);
let partials = [];
for (let i = bStr.length - 1; i >= 0; i--) {
const d = parseInt(bStr[i]);
const p = a * d * Math.pow(10, bStr.length - 1 - i);
partials.push(p);
s.push(`Multiply ${a} by ${d}${d !== 0 ? ' in the ' + (bStr.length - 1 - i === 0 ? 'ones' : bStr.length - 1 - i === 1 ? 'tens' : 'hundreds') + ' place' : ''}: ${a} × ${d} = ${a * d}${d !== 0 ? ' (add ' + (bStr.length - 1 - i === 0 ? '0' : bStr.length - 1 - i === 1 ? '1 zero' : '2 zeros') + ')' : ''}.`);
}
const sum = partials.reduce((x, y) => x + y, 0);
s.push(`Add the partial products: ${partials.filter(p => p !== 0).join(' + ')} = ${sum}.`);
}
s.push(`Answer: ${result}`);
return s;
},
_genDivision(level) {
let divisor, dividend;
if (level === 1) {
divisor = this._rand(2, 9);
const quotient = this._rand(10, 99);
dividend = divisor * quotient;
} else if (level === 2) {
divisor = this._rand(2, 9);
const quotient = this._rand(10, 99);
dividend = divisor * quotient + this._rand(0, divisor - 1);
} else {
divisor = this._rand(2, 12);
const quotient = this._rand(100, 999);
dividend = divisor * quotient + this._rand(0, divisor - 1);
}
const quotient = Math.floor(dividend / divisor);
const remainder = dividend % divisor;
return {
type: 'division',
display: `${dividend} ÷ ${divisor}`,
answer: quotient,
answerDisplay: remainder > 0 ? `${quotient} R${remainder}` : String(quotient),
steps: this._divisionSteps(dividend, divisor)
};
},
_divisionSteps(dividend, divisor) {
const s = [];
const quotient = Math.floor(dividend / divisor);
const remainder = dividend % divisor;
s.push(`Divide ${dividend} by ${divisor}:`);
s.push(`${divisor} goes into ${dividend}...`);
s.push(`${divisor} × ${quotient} = ${divisor * quotient}`);
if (remainder > 0) {
s.push(`${dividend} - ${divisor * quotient} = ${remainder} (remainder)`);
s.push(`Answer: ${quotient} R${remainder}`);
} else {
s.push(`${dividend} - ${divisor * quotient} = 0 (no remainder)`);
s.push(`Answer: ${quotient}`);
}
return s;
},
_genWordProblem(level) {
const templates = [
{ fn: () => {
const a = this._rand(10, 99); const b = this._rand(1, a);
const names = ['Sarah', 'Tom', 'Emma', 'Jack', 'Lily', 'Sam'];
const items = ['stickers', 'marbles', 'crayons', 'books', 'coins', 'pencils'];
const n = this._pick(names); const f = this._pick(names.filter(x => x !== n)); const i = this._pick(items);
return { display: `${n} has ${a} ${i}. ${f} gives ${n} ${b} more ${i}. How many ${i} does ${n} have now?`, answer: a + b, op: 'addition' };
}},
{ fn: () => {
const a = this._rand(20, 100); const b = this._rand(1, a - 1);
const items = ['cookies', 'apples', 'toys', 'balloons', 'candies', 'pennies'];
const n = this._pick(['Mia', 'Lucas', 'Ava', 'Noah', 'Sophia', 'Ethan']); const i = this._pick(items);
return { display: `${n} had ${a} ${i}. ${n} gave away ${b} ${i}. How many ${i} does ${n} have left?`, answer: a - b, op: 'subtraction' };
}},
{ fn: () => {
const a = this._rand(2, 12); const b = this._rand(2, 12);
const items1 = this._pick(['bags', 'boxes', 'packs', 'baskets', 'trays']);
const items2 = this._pick(['apples', 'cookies', 'eggs', 'marbles', 'crayons', 'pennies']);
return { display: `There are ${a} ${items1}. Each ${items1[0]}${items1[0] === 'boxes' ? '' : ''} has ${b} ${items2}. How many ${items2} are there total?`, answer: a * b, op: 'multiplication' };
}},
{ fn: () => {
const b = this._rand(2, 9); const q = this._rand(10, 20); const a = b * q;
const items = this._pick(['cookies', 'candies', 'books', 'toys', 'pencils', 'stickers']);
const n = this._pick(['Aiden', 'Olivia', 'James', 'Isabella', 'Benjamin', 'Charlotte']);
return { display: `${n} has ${a} ${items}. ${n} shares them equally among ${b} friends. How many ${items} does each friend get?`, answer: q, op: 'division' };
}},
{ fn: () => {
const b = this._rand(2, 12); const q = this._rand(5, 15); const a = b * q;
const items = this._pick(['seats', 'chairs', 'rows', 'crates', 'shelves', 'bags']);
return { display: `There are ${a} ${items}. They are arranged into ${b} equal groups. How many ${items} are in each group?`, answer: q, op: 'division' };
}},
{ fn: () => {
const a = this._rand(100, 999); const b = this._rand(100, 999);
const items = this._pick(['tickets sold', 'miles driven', 'pages read', 'points scored', 'dollars raised']);
return { display: `School A ${this._pick(['raised', 'sold', 'collected', 'earned'])} ${a} ${items}. School B ${this._pick(['raised', 'sold', 'collected', 'earned'])} ${b} ${items}. How many ${items} in total?`, answer: a + b, op: 'addition' };
}},
{ fn: () => {
const b = this._rand(2, 9); const q = this._rand(10, 99); const a = b * q;
const items = this._pick(['pencils', 'rulers', 'notebooks', 'erasers', 'markers']);
return { display: `${a} ${items} are packed into boxes. Each box holds ${b} ${items}. How many boxes are needed?`, answer: q, op: 'division' };
}},
{ fn: () => {
const names = ['Zoe', 'Max', 'Chloe', 'Liam', 'Grace', 'Owen'];
const items = this._pick(['bracelets', 'drawings', 'cards', 'origami', 'paintings', 'poems']);
const n = this._pick(names); const a = this._rand(10, 99); const b = this._rand(10, 99);
return { display: `${n} made ${a} ${items} on Monday and ${b} ${items} on Tuesday. How many more ${items} did ${n} make on Monday than Tuesday?`, answer: Math.abs(a - b), op: 'subtraction' };
}},
];
const t = this._pick(templates)();
return {
type: 'word-problem',
display: t.display,
answer: t.answer,
steps: [t.display, `The answer is ${t.answer}. (${t.op})`]
};
}
};