kids-math-generator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/generators/add.d.ts +7 -0
- package/dist/generators/add.js +27 -0
- package/dist/generators/div.d.ts +7 -0
- package/dist/generators/div.js +37 -0
- package/dist/generators/missing.d.ts +3 -0
- package/dist/generators/missing.js +47 -0
- package/dist/generators/mixed.d.ts +7 -0
- package/dist/generators/mixed.js +54 -0
- package/dist/generators/mul.d.ts +7 -0
- package/dist/generators/mul.js +33 -0
- package/dist/generators/sub.d.ts +7 -0
- package/dist/generators/sub.js +24 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +55 -0
- package/dist/rules/difficultyRules.d.ts +9 -0
- package/dist/rules/difficultyRules.js +14 -0
- package/dist/rules/gradeRules.d.ts +5 -0
- package/dist/rules/gradeRules.js +18 -0
- package/dist/test/index.d.ts +1 -0
- package/dist/test/index.js +38 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/utils/random.d.ts +5 -0
- package/dist/utils/random.js +12 -0
- package/dist/utils/seed.d.ts +5 -0
- package/dist/utils/seed.js +16 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @kids/math-generator
|
|
2
|
+
|
|
3
|
+
A small TypeScript package to generate math questions for grades 1–6.
|
|
4
|
+
|
|
5
|
+
- **Zero runtime dependencies**
|
|
6
|
+
- **Deterministic option** via `seed`
|
|
7
|
+
|
|
8
|
+
Key features
|
|
9
|
+
- Generate grade-appropriate math questions (grades 1–6)
|
|
10
|
+
- Difficulty levels: `easy`, `medium`, `hard`, `veryHard`
|
|
11
|
+
- Mixed-operation questions (e.g., `(6 × 4) - 5 = ?`) for higher grades
|
|
12
|
+
- Missing-operand (reverse) questions (e.g., `? × 12 = 144`) for advanced practice
|
|
13
|
+
|
|
14
|
+
Quick usage (ESM):
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { generateQuestion } from '@kids/math-generator';
|
|
18
|
+
|
|
19
|
+
// Explicit multiplication question
|
|
20
|
+
const q1 = generateQuestion({ grade: 3, operation: 'mul', difficulty: 'easy', seed: 42 });
|
|
21
|
+
console.log(q1.text, '→', q1.answer);
|
|
22
|
+
|
|
23
|
+
// Explicit mixed question (available for grade >= 5)
|
|
24
|
+
const mixed = generateQuestion({ grade: 5, operation: 'mixed', difficulty: 'hard', seed: 123 });
|
|
25
|
+
console.log(mixed.text, '→', mixed.answer);
|
|
26
|
+
|
|
27
|
+
// Missing-operand (reverse) question example
|
|
28
|
+
// For deterministic transformation, import the helper and a RNG
|
|
29
|
+
import { makeMissingOperandQuestion, makeRng } from '@kids/math-generator';
|
|
30
|
+
|
|
31
|
+
const base = generateQuestion({ grade: 6, operation: 'add', difficulty: 'hard', seed: 7 });
|
|
32
|
+
const rng = makeRng(1); // deterministic helper RNG
|
|
33
|
+
const missing = makeMissingOperandQuestion(base, rng);
|
|
34
|
+
console.log(missing.text, '→', missing.answer);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Notes
|
|
38
|
+
- If `operation` is omitted, the generator will pick an operation appropriate for the grade and difficulty. For grades >= 5 and harder difficulties, it may produce mixed problems.
|
|
39
|
+
- Missing-operand reverse questions are automatically produced sometimes for grade >= 6 when `difficulty` allows it; the `makeMissingOperandQuestion` helper is provided for explicit transformations.
|
|
40
|
+
|
|
41
|
+
Run a quick sample/tests locally:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run build
|
|
45
|
+
npm test
|
|
46
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getNumberRangeForGrade } from '../rules/gradeRules.js';
|
|
2
|
+
import { getDifficultyModifiers } from '../rules/difficultyRules.js';
|
|
3
|
+
export function generateAddQuestion(opts) {
|
|
4
|
+
const { grade, difficulty, rng } = opts;
|
|
5
|
+
const range = getNumberRangeForGrade(grade);
|
|
6
|
+
const modifiers = getDifficultyModifiers(difficulty);
|
|
7
|
+
// scale the max by difficulty modifiers
|
|
8
|
+
const maxVal = Math.max(range.min, Math.floor(range.max * modifiers.scale));
|
|
9
|
+
let a = rng.randInt(range.min, Math.max(range.min, maxVal));
|
|
10
|
+
let b = rng.randInt(range.min, Math.max(range.min, maxVal));
|
|
11
|
+
if (!modifiers.allowCarry) {
|
|
12
|
+
// ensure no carry in ones place
|
|
13
|
+
while (((a % 10) + (b % 10)) >= 10) {
|
|
14
|
+
b = rng.randInt(range.min, Math.max(range.min, maxVal));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const text = `${a} + ${b} = ?`;
|
|
18
|
+
const answer = a + b;
|
|
19
|
+
return {
|
|
20
|
+
text,
|
|
21
|
+
answer,
|
|
22
|
+
operation: 'add',
|
|
23
|
+
operands: [a, b],
|
|
24
|
+
grade,
|
|
25
|
+
difficulty,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getNumberRangeForGrade } from '../rules/gradeRules.js';
|
|
2
|
+
import { getDifficultyModifiers } from '../rules/difficultyRules.js';
|
|
3
|
+
export function generateDivQuestion(opts) {
|
|
4
|
+
const { grade, difficulty, rng } = opts;
|
|
5
|
+
const range = getNumberRangeForGrade(grade);
|
|
6
|
+
const modifiers = getDifficultyModifiers(difficulty);
|
|
7
|
+
// For grade 3, pick multiplication table reverses (no remainder)
|
|
8
|
+
if (grade === 3) {
|
|
9
|
+
const b = rng.randInt(1, 10);
|
|
10
|
+
const answer = rng.randInt(1, 10);
|
|
11
|
+
const a = b * answer;
|
|
12
|
+
return {
|
|
13
|
+
text: `${a} ÷ ${b} = ?`,
|
|
14
|
+
answer,
|
|
15
|
+
operation: 'div',
|
|
16
|
+
operands: [a, b],
|
|
17
|
+
grade,
|
|
18
|
+
difficulty,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const maxDivisor = Math.max(1, Math.floor(range.max * modifiers.scale * 0.01));
|
|
22
|
+
const divisor = rng.randInt(1, maxDivisor);
|
|
23
|
+
let quotient = rng.randInt(0, Math.max(0, Math.floor(range.max * modifiers.scale * 0.01)));
|
|
24
|
+
let dividend = divisor * quotient;
|
|
25
|
+
if (modifiers.allowRemainder) {
|
|
26
|
+
// add a remainder smaller than divisor
|
|
27
|
+
dividend += rng.randInt(0, Math.max(0, divisor - 1));
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
text: `${dividend} ÷ ${divisor} = ?`,
|
|
31
|
+
answer: Math.floor(dividend / divisor),
|
|
32
|
+
operation: 'div',
|
|
33
|
+
operands: [dividend, divisor],
|
|
34
|
+
grade,
|
|
35
|
+
difficulty,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Make a missing-operand (reverse) version of a question. It returns a new Question
|
|
2
|
+
// where one operand is hidden with `?` and the answer is the hidden operand's value.
|
|
3
|
+
export function makeMissingOperandQuestion(q, rng) {
|
|
4
|
+
// Only support textual replacement for typical forms.
|
|
5
|
+
const op = q.operation;
|
|
6
|
+
// Helper for binary operations (add, sub, mul, div)
|
|
7
|
+
if (op === 'add' || op === 'sub' || op === 'mul' || op === 'div') {
|
|
8
|
+
const [a, b] = q.operands;
|
|
9
|
+
const hideFirst = rng.randInt(0, 1) === 0;
|
|
10
|
+
let text = '';
|
|
11
|
+
let answer = 0;
|
|
12
|
+
const symbol = op === 'add' ? '+' : op === 'sub' ? '-' : op === 'mul' ? '×' : '÷';
|
|
13
|
+
if (hideFirst) {
|
|
14
|
+
// ? op b = result
|
|
15
|
+
text = `? ${symbol} ${b} = ${q.answer}`;
|
|
16
|
+
answer = a;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// a op ? = result
|
|
20
|
+
text = `${a} ${symbol} ? = ${q.answer}`;
|
|
21
|
+
answer = b;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
...q,
|
|
25
|
+
text,
|
|
26
|
+
answer,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// For mixed expressions, try replacing one of the operand numbers in the textual expression
|
|
30
|
+
const operandStrs = q.operands.map((n) => String(n));
|
|
31
|
+
const idx = rng.randInt(0, Math.max(0, operandStrs.length - 1));
|
|
32
|
+
const target = operandStrs[idx];
|
|
33
|
+
// Replace first full-token occurrence of target in text with '?'
|
|
34
|
+
// Use a regex with word boundaries to avoid partial matches
|
|
35
|
+
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
36
|
+
const re = new RegExp('\\b' + escaped + '\\b');
|
|
37
|
+
let newText = q.text.replace(re, '?');
|
|
38
|
+
// If replacement didn't change text (unlikely), fallback to a simple replacement
|
|
39
|
+
if (newText === q.text) {
|
|
40
|
+
newText = q.text.replace(target, '?');
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
...q,
|
|
44
|
+
text: newText,
|
|
45
|
+
answer: Number(target),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getNumberRangeForGrade } from '../rules/gradeRules.js';
|
|
2
|
+
import { getDifficultyModifiers } from '../rules/difficultyRules.js';
|
|
3
|
+
export function generateMixedQuestion(opts) {
|
|
4
|
+
const { grade, difficulty, rng } = opts;
|
|
5
|
+
const range = getNumberRangeForGrade(grade);
|
|
6
|
+
const modifiers = getDifficultyModifiers(difficulty);
|
|
7
|
+
const max = Math.max(1, Math.floor(range.max * modifiers.scale));
|
|
8
|
+
// Choose a pattern suitable for the grade/difficulty
|
|
9
|
+
// Patterns:
|
|
10
|
+
// 0: (a × b) - c
|
|
11
|
+
// 1: (a × b) + c
|
|
12
|
+
// 2: (a + b) × c
|
|
13
|
+
// 3: (a - b) × c (ensure a-b >=0)
|
|
14
|
+
const pattern = rng.randInt(0, 3);
|
|
15
|
+
// Choose numbers with reasonable sizes
|
|
16
|
+
const a = rng.randInt(1, Math.max(1, Math.floor(Math.sqrt(max))));
|
|
17
|
+
const b = rng.randInt(1, Math.max(1, Math.floor(Math.sqrt(max))));
|
|
18
|
+
const c = rng.randInt(1, Math.max(1, Math.floor(Math.sqrt(max))));
|
|
19
|
+
let text = '';
|
|
20
|
+
let answer = 0;
|
|
21
|
+
switch (pattern) {
|
|
22
|
+
case 0:
|
|
23
|
+
// (a × b) - c
|
|
24
|
+
answer = a * b - c;
|
|
25
|
+
text = `(${a} × ${b}) - ${c} = ?`;
|
|
26
|
+
break;
|
|
27
|
+
case 1:
|
|
28
|
+
// (a × b) + c
|
|
29
|
+
answer = a * b + c;
|
|
30
|
+
text = `(${a} × ${b}) + ${c} = ?`;
|
|
31
|
+
break;
|
|
32
|
+
case 2:
|
|
33
|
+
// (a + b) × c
|
|
34
|
+
answer = (a + b) * c;
|
|
35
|
+
text = `(${a} + ${b}) × ${c} = ?`;
|
|
36
|
+
break;
|
|
37
|
+
case 3:
|
|
38
|
+
default:
|
|
39
|
+
// (a - b) × c ensure non-negative
|
|
40
|
+
const aa = Math.max(a, b);
|
|
41
|
+
const bb = Math.min(a, b);
|
|
42
|
+
answer = (aa - bb) * c;
|
|
43
|
+
text = `(${aa} - ${bb}) × ${c} = ?`;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
text,
|
|
48
|
+
answer,
|
|
49
|
+
operation: 'mixed',
|
|
50
|
+
operands: [a, b, c],
|
|
51
|
+
grade,
|
|
52
|
+
difficulty,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getNumberRangeForGrade } from '../rules/gradeRules.js';
|
|
2
|
+
import { getDifficultyModifiers } from '../rules/difficultyRules.js';
|
|
3
|
+
export function generateMulQuestion(opts) {
|
|
4
|
+
const { grade, difficulty, rng } = opts;
|
|
5
|
+
const range = getNumberRangeForGrade(grade);
|
|
6
|
+
const modifiers = getDifficultyModifiers(difficulty);
|
|
7
|
+
// For grade 3, use 1-10 tables; for higher grades increase digits scaled by difficulty
|
|
8
|
+
let a;
|
|
9
|
+
let b;
|
|
10
|
+
if (grade === 3) {
|
|
11
|
+
a = rng.randInt(1, 10);
|
|
12
|
+
b = rng.randInt(1, 10);
|
|
13
|
+
}
|
|
14
|
+
else if (grade === 4) {
|
|
15
|
+
a = rng.randInt(10, 99);
|
|
16
|
+
b = rng.randInt(2, 9);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const maxA = Math.max(1, Math.floor(range.max * modifiers.scale * 0.1));
|
|
20
|
+
a = rng.randInt(1, Math.max(1, maxA));
|
|
21
|
+
b = rng.randInt(1, Math.max(1, Math.floor(12 * modifiers.scale)));
|
|
22
|
+
}
|
|
23
|
+
const text = `${a} × ${b} = ?`;
|
|
24
|
+
const answer = a * b;
|
|
25
|
+
return {
|
|
26
|
+
text,
|
|
27
|
+
answer,
|
|
28
|
+
operation: 'mul',
|
|
29
|
+
operands: [a, b],
|
|
30
|
+
grade,
|
|
31
|
+
difficulty,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getNumberRangeForGrade } from '../rules/gradeRules.js';
|
|
2
|
+
import { getDifficultyModifiers } from '../rules/difficultyRules.js';
|
|
3
|
+
export function generateSubQuestion(opts) {
|
|
4
|
+
const { grade, difficulty, rng } = opts;
|
|
5
|
+
const range = getNumberRangeForGrade(grade);
|
|
6
|
+
const modifiers = getDifficultyModifiers(difficulty);
|
|
7
|
+
const maxVal = Math.max(range.min, Math.floor(range.max * modifiers.scale));
|
|
8
|
+
let a = rng.randInt(range.min, Math.max(range.min, maxVal));
|
|
9
|
+
let b = rng.randInt(range.min, Math.max(range.min, maxVal));
|
|
10
|
+
if (grade < 5) {
|
|
11
|
+
if (a < b)
|
|
12
|
+
[a, b] = [b, a];
|
|
13
|
+
}
|
|
14
|
+
const text = `${a} - ${b} = ?`;
|
|
15
|
+
const answer = a - b;
|
|
16
|
+
return {
|
|
17
|
+
text,
|
|
18
|
+
answer,
|
|
19
|
+
operation: 'sub',
|
|
20
|
+
operands: [a, b],
|
|
21
|
+
grade,
|
|
22
|
+
difficulty,
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Grade, Difficulty, Operation, Question } from './types.js';
|
|
2
|
+
export declare function generateQuestion(options: {
|
|
3
|
+
grade: Grade;
|
|
4
|
+
operation?: Operation;
|
|
5
|
+
difficulty?: Difficulty;
|
|
6
|
+
seed?: number;
|
|
7
|
+
}): Question;
|
|
8
|
+
export { makeMissingOperandQuestion } from './generators/missing.js';
|
|
9
|
+
export { makeRng } from './utils/random.js';
|
|
10
|
+
export type { Grade, Difficulty, Operation, Question };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { makeRng } from './utils/random.js';
|
|
2
|
+
import { generateAddQuestion } from './generators/add.js';
|
|
3
|
+
import { generateSubQuestion } from './generators/sub.js';
|
|
4
|
+
import { generateMulQuestion } from './generators/mul.js';
|
|
5
|
+
import { generateDivQuestion } from './generators/div.js';
|
|
6
|
+
import { generateMixedQuestion } from './generators/mixed.js';
|
|
7
|
+
import { makeMissingOperandQuestion } from './generators/missing.js';
|
|
8
|
+
import { getDifficultyModifiers } from './rules/difficultyRules.js';
|
|
9
|
+
export function generateQuestion(options) {
|
|
10
|
+
const { grade, operation, difficulty = 'easy', seed } = options;
|
|
11
|
+
const rng = makeRng(seed);
|
|
12
|
+
const modifiers = getDifficultyModifiers(difficulty);
|
|
13
|
+
// If operation is not specified, pick one. If mixed questions are allowed for the grade/difficulty,
|
|
14
|
+
// sometimes produce a mixed question.
|
|
15
|
+
let op;
|
|
16
|
+
if (operation) {
|
|
17
|
+
op = operation;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
if (grade >= 5 && modifiers.allowMixed && rng.rand() < 0.25) {
|
|
21
|
+
op = 'mixed';
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
op = ['add', 'sub', 'mul', 'div'][Math.floor(rng.rand() * 4)];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
let q;
|
|
28
|
+
switch (op) {
|
|
29
|
+
case 'add':
|
|
30
|
+
q = generateAddQuestion({ grade, difficulty, rng });
|
|
31
|
+
break;
|
|
32
|
+
case 'sub':
|
|
33
|
+
q = generateSubQuestion({ grade, difficulty, rng });
|
|
34
|
+
break;
|
|
35
|
+
case 'mul':
|
|
36
|
+
q = generateMulQuestion({ grade, difficulty, rng });
|
|
37
|
+
break;
|
|
38
|
+
case 'div':
|
|
39
|
+
q = generateDivQuestion({ grade, difficulty, rng });
|
|
40
|
+
break;
|
|
41
|
+
case 'mixed':
|
|
42
|
+
q = generateMixedQuestion({ grade, difficulty, rng });
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
throw new Error('Unsupported operation');
|
|
46
|
+
}
|
|
47
|
+
// Optionally transform into a missing-operand reverse question for higher grades/difficulties
|
|
48
|
+
if (grade >= 6 && modifiers.allowMissingOperand && rng.randInt(1, 100) <= 20) {
|
|
49
|
+
q = makeMissingOperandQuestion(q, rng);
|
|
50
|
+
}
|
|
51
|
+
return q;
|
|
52
|
+
}
|
|
53
|
+
// Convenience exports for advanced usage
|
|
54
|
+
export { makeMissingOperandQuestion } from './generators/missing.js';
|
|
55
|
+
export { makeRng } from './utils/random.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Difficulty } from '../types.js';
|
|
2
|
+
export interface DifficultyModifiers {
|
|
3
|
+
scale: number;
|
|
4
|
+
allowCarry: boolean;
|
|
5
|
+
allowRemainder: boolean;
|
|
6
|
+
allowMissingOperand: boolean;
|
|
7
|
+
allowMixed: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function getDifficultyModifiers(difficulty: Difficulty): DifficultyModifiers;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function getDifficultyModifiers(difficulty) {
|
|
2
|
+
switch (difficulty) {
|
|
3
|
+
case 'easy':
|
|
4
|
+
return { scale: 0.2, allowCarry: false, allowRemainder: false, allowMissingOperand: false, allowMixed: false };
|
|
5
|
+
case 'medium':
|
|
6
|
+
return { scale: 0.5, allowCarry: true, allowRemainder: false, allowMissingOperand: false, allowMixed: false };
|
|
7
|
+
case 'hard':
|
|
8
|
+
return { scale: 0.85, allowCarry: true, allowRemainder: true, allowMissingOperand: true, allowMixed: true };
|
|
9
|
+
case 'veryHard':
|
|
10
|
+
return { scale: 1.0, allowCarry: true, allowRemainder: true, allowMissingOperand: true, allowMixed: true };
|
|
11
|
+
default:
|
|
12
|
+
return { scale: 0.5, allowCarry: true, allowRemainder: false, allowMissingOperand: false, allowMixed: false };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function getNumberRangeForGrade(grade) {
|
|
2
|
+
switch (grade) {
|
|
3
|
+
case 1:
|
|
4
|
+
return { min: 0, max: 20 };
|
|
5
|
+
case 2:
|
|
6
|
+
return { min: 0, max: 100 };
|
|
7
|
+
case 3:
|
|
8
|
+
return { min: 0, max: 100 };
|
|
9
|
+
case 4:
|
|
10
|
+
return { min: 0, max: 1000 };
|
|
11
|
+
case 5:
|
|
12
|
+
return { min: 0, max: 10000 };
|
|
13
|
+
case 6:
|
|
14
|
+
return { min: 0, max: 100000 };
|
|
15
|
+
default:
|
|
16
|
+
return { min: 0, max: 100 };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as assert from 'assert';
|
|
2
|
+
import { generateQuestion } from '../index.js';
|
|
3
|
+
import { makeMissingOperandQuestion } from '../generators/missing.js';
|
|
4
|
+
import { makeRng } from '../utils/random.js';
|
|
5
|
+
function runTests() {
|
|
6
|
+
console.log('Running basic tests...');
|
|
7
|
+
// Deterministic results with seed
|
|
8
|
+
const q1 = generateQuestion({ grade: 1, operation: 'add', difficulty: 'easy', seed: 42 });
|
|
9
|
+
assert.strictEqual(q1.operation, 'add');
|
|
10
|
+
assert.strictEqual(q1.grade, 1);
|
|
11
|
+
const q2 = generateQuestion({ grade: 3, operation: 'mul', difficulty: 'easy', seed: 42 });
|
|
12
|
+
assert.strictEqual(q2.operation, 'mul');
|
|
13
|
+
assert.ok(q2.answer >= 1);
|
|
14
|
+
// Subtraction for grade <5 should not be negative
|
|
15
|
+
const q3 = generateQuestion({ grade: 2, operation: 'sub', difficulty: 'medium', seed: 7 });
|
|
16
|
+
assert.ok(q3.answer >= 0, 'Subtraction produced negative answer for grade 2');
|
|
17
|
+
// Division for grade 3 should have integer answer (no remainder)
|
|
18
|
+
const q4 = generateQuestion({ grade: 3, operation: 'div', difficulty: 'easy', seed: 123 });
|
|
19
|
+
assert.strictEqual(q4.answer, Math.floor(q4.operands[0] / q4.operands[1]));
|
|
20
|
+
// Mixed operation test (explicit)
|
|
21
|
+
const q5 = generateQuestion({ grade: 5, operation: 'mixed', difficulty: 'hard', seed: 42 });
|
|
22
|
+
assert.strictEqual(q5.operation, 'mixed');
|
|
23
|
+
// Compute left-hand expression by parsing q5.text and evaluating (replace × and ÷)
|
|
24
|
+
const lh = q5.text.split('=')[0].replace(/×/g, '*').replace(/÷/g, '/');
|
|
25
|
+
// Evaluate safely by creating a Function
|
|
26
|
+
// eslint-disable-next-line no-new-func
|
|
27
|
+
const evaluated = Function(`return ${lh}`)();
|
|
28
|
+
assert.strictEqual(evaluated, q5.answer);
|
|
29
|
+
// Missing operand transformer test
|
|
30
|
+
const base = generateQuestion({ grade: 6, operation: 'add', difficulty: 'hard', seed: 7 });
|
|
31
|
+
const rng = makeRng(1);
|
|
32
|
+
const missing = makeMissingOperandQuestion(base, rng);
|
|
33
|
+
assert.ok(missing.text.includes('?'));
|
|
34
|
+
// missing.answer should equal one of the original operands
|
|
35
|
+
assert.ok(base.operands.includes(missing.answer));
|
|
36
|
+
console.log('All tests passed ✅');
|
|
37
|
+
}
|
|
38
|
+
runTests();
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Grade = 1 | 2 | 3 | 4 | 5 | 6;
|
|
2
|
+
export type Operation = 'add' | 'sub' | 'mul' | 'div' | 'mixed';
|
|
3
|
+
export type Difficulty = 'easy' | 'medium' | 'hard' | 'veryHard';
|
|
4
|
+
export interface Question {
|
|
5
|
+
text: string;
|
|
6
|
+
answer: number;
|
|
7
|
+
operation: Operation;
|
|
8
|
+
operands: number[];
|
|
9
|
+
grade: Grade;
|
|
10
|
+
difficulty: Difficulty;
|
|
11
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createSeededPRNG } from './seed.js';
|
|
2
|
+
export function makeRng(seed) {
|
|
3
|
+
const base = createSeededPRNG(seed);
|
|
4
|
+
return {
|
|
5
|
+
rand: base,
|
|
6
|
+
randInt(min, max) {
|
|
7
|
+
if (min > max)
|
|
8
|
+
throw new Error('min > max');
|
|
9
|
+
return Math.floor(base() * (max - min + 1)) + min;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny seeded PRNG using a simple LCG. Returns a function that yields [0,1).
|
|
3
|
+
* Not cryptographically secure but deterministic and dependency-free.
|
|
4
|
+
*/
|
|
5
|
+
export function createSeededPRNG(seed) {
|
|
6
|
+
if (seed === undefined || seed === null) {
|
|
7
|
+
return Math.random;
|
|
8
|
+
}
|
|
9
|
+
// Ensure a 32-bit unsigned seed
|
|
10
|
+
let state = seed >>> 0;
|
|
11
|
+
return function () {
|
|
12
|
+
// LCG parameters (Numerical Recipes)
|
|
13
|
+
state = (state * 1664525 + 1013904223) >>> 0;
|
|
14
|
+
return state / 0x100000000; // 2^32
|
|
15
|
+
};
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kids-math-generator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Math question generator for grades 1-6",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"math",
|
|
7
|
+
"generator",
|
|
8
|
+
"kids",
|
|
9
|
+
"math",
|
|
10
|
+
"additin",
|
|
11
|
+
"division",
|
|
12
|
+
"multiplication",
|
|
13
|
+
"division"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/serkanalgur/kids-math-generator#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/serkanalgur/kids-math-generator/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/serkanalgur/kids-math-generator.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Serkan Algur <kaisercrazy@gmail.com> (https://github.com/serkanalgur)",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"main": "dist/index.js",
|
|
33
|
+
"types": "dist/index.d.ts",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json",
|
|
39
|
+
"test": "node dist/test/index.js",
|
|
40
|
+
"prepare": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"undici-types": "^7.16.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"typescript": "^5.0.0",
|
|
50
|
+
"@types/node": "^25.0.3"
|
|
51
|
+
}
|
|
52
|
+
}
|