|
|
|
|
@ -0,0 +1,441 @@
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
class MathQuestionGenerator {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.currentSessionQuestions = new Set();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成指定年级和数量的题目
|
|
|
|
|
generateQuestions(grade, count) {
|
|
|
|
|
console.log(`正在生成${grade} ${count}道题目...`);
|
|
|
|
|
const questions = [];
|
|
|
|
|
this.currentSessionQuestions.clear();
|
|
|
|
|
|
|
|
|
|
const maxAttempts = count * 20;
|
|
|
|
|
let attempts = 0;
|
|
|
|
|
|
|
|
|
|
while (questions.length < count && attempts < maxAttempts) {
|
|
|
|
|
attempts++;
|
|
|
|
|
const question = this.generateQuestion(grade);
|
|
|
|
|
if (!question) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const questionKey = `${grade}-${question.stem}`;
|
|
|
|
|
|
|
|
|
|
if (!this.currentSessionQuestions.has(questionKey)) {
|
|
|
|
|
questions.push(question);
|
|
|
|
|
this.currentSessionQuestions.add(questionKey);
|
|
|
|
|
console.log(`✅ 生成第${questions.length}题: ${question.stem}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (questions.length < count) {
|
|
|
|
|
console.warn(`⚠️ 只生成了${questions.length}道题目,未能达到要求的${count}道`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return questions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 根据年级生成单个题目
|
|
|
|
|
generateQuestion(grade) {
|
|
|
|
|
try {
|
|
|
|
|
switch (grade) {
|
|
|
|
|
case '小学':
|
|
|
|
|
return this.generatePrimaryQuestion();
|
|
|
|
|
case '初中':
|
|
|
|
|
return this.generateMiddleSchoolQuestion();
|
|
|
|
|
case '高中':
|
|
|
|
|
return this.generateHighSchoolQuestion();
|
|
|
|
|
default:
|
|
|
|
|
return this.generatePrimaryQuestion();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`生成${grade}题目时出错:`, error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成小学题目(2-5个操作数,基础四则运算)
|
|
|
|
|
generatePrimaryQuestion() {
|
|
|
|
|
const numOperands = Math.floor(Math.random() * 4) + 2;
|
|
|
|
|
const operations = ['+', '-', '×', '÷'];
|
|
|
|
|
let expression = '';
|
|
|
|
|
let correctAnswer = 0;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < numOperands; i++) {
|
|
|
|
|
const num = Math.floor(Math.random() * 100) + 1;
|
|
|
|
|
if (i === 0) {
|
|
|
|
|
expression = num.toString();
|
|
|
|
|
correctAnswer = num;
|
|
|
|
|
} else {
|
|
|
|
|
const op = operations[Math.floor(Math.random() * operations.length)];
|
|
|
|
|
expression += ` ${op} ${num}`;
|
|
|
|
|
correctAnswer = this.applyOperation(correctAnswer, num, op);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
correctAnswer = this.calculateWithPriority(expression);
|
|
|
|
|
if (numOperands >= 3 && Math.random() < 0.3) {
|
|
|
|
|
const result = this.addParentheses(expression, correctAnswer);
|
|
|
|
|
if (result) {
|
|
|
|
|
expression = result.expression;
|
|
|
|
|
correctAnswer = result.answer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const options = this.generateOptions(correctAnswer, 4);
|
|
|
|
|
return {
|
|
|
|
|
id: `primary-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
|
|
|
stem: `计算:${expression} = ?`,
|
|
|
|
|
options: options,
|
|
|
|
|
answer: options.find(opt => opt.isCorrect).key
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成初中题目(1-5个操作数,包含平方和开方)
|
|
|
|
|
generateMiddleSchoolQuestion() {
|
|
|
|
|
const numOperands = Math.floor(Math.random() * 5) + 1;
|
|
|
|
|
const operations = ['+', '-', '×', '÷'];
|
|
|
|
|
const specialPosition = Math.floor(Math.random() * numOperands);
|
|
|
|
|
const isSquare = Math.random() < 0.5;
|
|
|
|
|
let expression = '';
|
|
|
|
|
let correctAnswer = 0;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < numOperands; i++) {
|
|
|
|
|
const {term, value} = i === specialPosition ?
|
|
|
|
|
this.generateSpecialTerm(isSquare) : this.generateRandomTerm();
|
|
|
|
|
if (i === 0) {
|
|
|
|
|
expression = term;
|
|
|
|
|
correctAnswer = value;
|
|
|
|
|
} else {
|
|
|
|
|
const op = operations[Math.floor(Math.random() * operations.length)];
|
|
|
|
|
expression += ` ${op} ${term}`;
|
|
|
|
|
correctAnswer = this.applyOperation(correctAnswer, value, op);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
correctAnswer = this.calculateWithPriority(expression);
|
|
|
|
|
if (numOperands >= 3 && Math.random() < 0.4) {
|
|
|
|
|
const result = this.addParentheses(expression, correctAnswer);
|
|
|
|
|
if (result) {expression = result.expression; correctAnswer = result.answer;}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const options = this.generateOptions(correctAnswer, 4);
|
|
|
|
|
return {
|
|
|
|
|
id: `middle-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
|
|
|
stem: `计算:${expression} = ?`,
|
|
|
|
|
options: options,
|
|
|
|
|
answer: options.find(opt => opt.isCorrect).key
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成特殊项(平方或开方)
|
|
|
|
|
generateSpecialTerm(isSquare) {
|
|
|
|
|
if (isSquare) {
|
|
|
|
|
const base = Math.floor(Math.random() * 15) + 1;
|
|
|
|
|
return { term: `${base}²`, value: base * base };
|
|
|
|
|
} else {
|
|
|
|
|
const perfectSquares = [4, 9, 16, 25, 36, 49, 64, 81, 100];
|
|
|
|
|
const num = perfectSquares[Math.floor(Math.random() * perfectSquares.length)];
|
|
|
|
|
return { term: `√${num}`, value: Math.sqrt(num) };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成随机项(数字、平方或开方)
|
|
|
|
|
generateRandomTerm() {
|
|
|
|
|
const termType = Math.random();
|
|
|
|
|
if (termType < 0.3) {
|
|
|
|
|
const base = Math.floor(Math.random() * 15) + 1;
|
|
|
|
|
return { term: `${base}²`, value: base * base };
|
|
|
|
|
} else if (termType < 0.6) {
|
|
|
|
|
const perfectSquares = [4, 9, 16, 25, 36, 49, 64, 81, 100];
|
|
|
|
|
const num = perfectSquares[Math.floor(Math.random() * perfectSquares.length)];
|
|
|
|
|
return { term: `√${num}`, value: Math.sqrt(num) };
|
|
|
|
|
} else {
|
|
|
|
|
const num = Math.floor(Math.random() * 100) + 1;
|
|
|
|
|
return { term: num.toString(), value: num };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成高中题目(1-5个操作数,包含三角函数)
|
|
|
|
|
generateHighSchoolQuestion() {
|
|
|
|
|
const numOperands = Math.floor(Math.random() * 5) + 1;
|
|
|
|
|
const operations = ['+', '-', '×', '÷'];
|
|
|
|
|
const specialPosition = Math.floor(Math.random() * numOperands);
|
|
|
|
|
let expression = '', correctAnswer = 0;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < numOperands; i++) {
|
|
|
|
|
const {term, value} = i === specialPosition ?
|
|
|
|
|
this.generateTrigTerm() : (Math.random() < 0.4 ? this.generateTrigTerm() : this.generateNumberTerm());
|
|
|
|
|
|
|
|
|
|
if (i === 0) { expression = term; correctAnswer = value; }
|
|
|
|
|
else {
|
|
|
|
|
const op = operations[Math.floor(Math.random() * operations.length)];
|
|
|
|
|
expression += ` ${op} ${term}`;
|
|
|
|
|
correctAnswer = this.applyOperation(correctAnswer, value, op);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
correctAnswer = this.calculateWithPriority(expression);
|
|
|
|
|
if (numOperands >= 3 && Math.random() < 0.4) {
|
|
|
|
|
const result = this.addParentheses(expression, correctAnswer);
|
|
|
|
|
if (result) { expression = result.expression; correctAnswer = result.answer; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const options = this.generateOptions(correctAnswer, 4);
|
|
|
|
|
return {
|
|
|
|
|
id: `high-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
|
|
|
stem: `计算:${expression} = ?`,
|
|
|
|
|
options: options,
|
|
|
|
|
answer: options.find(opt => opt.isCorrect).key
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成三角函数项
|
|
|
|
|
generateTrigTerm() {
|
|
|
|
|
const functions = ['sin', 'cos', 'tan'];
|
|
|
|
|
const func = functions[Math.floor(Math.random() * functions.length)];
|
|
|
|
|
const angle = Math.floor(Math.random() * 100) + 1;
|
|
|
|
|
const value = Math.round(Math[func](angle * Math.PI / 180) * 100) / 100;
|
|
|
|
|
return { term: `${func}${angle}`, value };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成数字项
|
|
|
|
|
generateNumberTerm() {
|
|
|
|
|
const num = Math.floor(Math.random() * 100) + 1;
|
|
|
|
|
return { term: num.toString(), value: num };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成选择题选项
|
|
|
|
|
generateOptions(correctAnswer, count) {
|
|
|
|
|
const options = [];
|
|
|
|
|
const keys = ['A', 'B', 'C', 'D'];
|
|
|
|
|
const correctIndex = Math.floor(Math.random() * count);
|
|
|
|
|
const isInteger = typeof correctAnswer === 'number' && Number.isInteger(correctAnswer);
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
|
|
|
const value = i === correctIndex ? correctAnswer : this.generateWrongOption(correctAnswer, isInteger, i);
|
|
|
|
|
options.push({key: keys[i], text: value.toString(), isCorrect: i === correctIndex});
|
|
|
|
|
}
|
|
|
|
|
return options;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成错误选项
|
|
|
|
|
generateWrongOption(correctAnswer, isInteger, index) {
|
|
|
|
|
if (typeof correctAnswer !== 'number') {
|
|
|
|
|
return Math.random().toString(36).substring(2, 6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let attempts = 0;
|
|
|
|
|
let value;
|
|
|
|
|
do {
|
|
|
|
|
const deviation = (Math.random() - 0.5) * 4;
|
|
|
|
|
value = isInteger ?
|
|
|
|
|
correctAnswer + Math.floor(Math.random() * 10) - 5 :
|
|
|
|
|
Math.round((correctAnswer + deviation) * 100) / 100;
|
|
|
|
|
|
|
|
|
|
if (++attempts > 10) {
|
|
|
|
|
value = correctAnswer + (index + 1);
|
|
|
|
|
if (isInteger) {
|
|
|
|
|
value = Math.round(value);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} while (value === correctAnswer);
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取数字的所有因数
|
|
|
|
|
getDivisors(n) {
|
|
|
|
|
const divisors = [];
|
|
|
|
|
for (let i = 2; i <= Math.min(n, 100); i++) { // 除数也限制在1-100范围内
|
|
|
|
|
if (n % i === 0) divisors.push(i);
|
|
|
|
|
}
|
|
|
|
|
return divisors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 小学题目专用的括号添加函数,确保括号内不会产生负数
|
|
|
|
|
addParenthesesForPrimary(expression, originalAnswer) {
|
|
|
|
|
const parts = expression.split(' ');
|
|
|
|
|
|
|
|
|
|
// 如果表达式太短,不需要加括号
|
|
|
|
|
if (parts.length < 5) return null;
|
|
|
|
|
|
|
|
|
|
// 找到所有可以加括号的位置(运算符位置)
|
|
|
|
|
const operatorPositions = [];
|
|
|
|
|
for (let i = 1; i < parts.length - 1; i += 2) {
|
|
|
|
|
// 只考虑加法和乘法,避免减法导致负数
|
|
|
|
|
if (parts[i] === '+' || parts[i] === '×') {
|
|
|
|
|
operatorPositions.push(i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (operatorPositions.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
// 随机选择一个运算符位置
|
|
|
|
|
const operatorIndex = operatorPositions[Math.floor(Math.random() * operatorPositions.length)];
|
|
|
|
|
|
|
|
|
|
// 确定括号的范围(从运算符前一个操作数到运算符后一个操作数)
|
|
|
|
|
const startPos = operatorIndex - 1;
|
|
|
|
|
const endPos = operatorIndex + 1;
|
|
|
|
|
|
|
|
|
|
// 构建带括号的表达式
|
|
|
|
|
let result = '';
|
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
|
|
|
if (i === startPos) {
|
|
|
|
|
result += '(';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result += parts[i];
|
|
|
|
|
|
|
|
|
|
if (i === endPos) {
|
|
|
|
|
result += ')';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i < parts.length - 1) {
|
|
|
|
|
result += ' ';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算带括号的答案
|
|
|
|
|
let newAnswer = this.calculateWithPriority(result);
|
|
|
|
|
|
|
|
|
|
// 确保答案是非负整数
|
|
|
|
|
if (newAnswer < 0 || !Number.isInteger(newAnswer)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
answer: newAnswer,
|
|
|
|
|
expression: result,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 通用的括号添加函数
|
|
|
|
|
// 为表达式添加括号
|
|
|
|
|
addParentheses(expression, originalAnswer) {
|
|
|
|
|
const parts = expression.split(' ');
|
|
|
|
|
|
|
|
|
|
// 如果表达式太短,不需要加括号
|
|
|
|
|
if (parts.length < 5) return null;
|
|
|
|
|
|
|
|
|
|
// 找到所有可以加括号的位置(运算符位置)
|
|
|
|
|
const operatorPositions = [];
|
|
|
|
|
for (let i = 1; i < parts.length - 1; i += 2) {
|
|
|
|
|
operatorPositions.push(i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (operatorPositions.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
// 随机选择一个运算符位置
|
|
|
|
|
const operatorIndex = operatorPositions[Math.floor(Math.random() * operatorPositions.length)];
|
|
|
|
|
|
|
|
|
|
// 确定括号的范围(从运算符前一个操作数到运算符后一个操作数)
|
|
|
|
|
const startPos = operatorIndex - 1;
|
|
|
|
|
const endPos = operatorIndex + 1;
|
|
|
|
|
|
|
|
|
|
// 构建带括号的表达式
|
|
|
|
|
let result = '';
|
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
|
|
|
if (i === startPos) {
|
|
|
|
|
result += '(';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result += parts[i];
|
|
|
|
|
|
|
|
|
|
if (i === endPos) {
|
|
|
|
|
result += ')';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i < parts.length - 1) {
|
|
|
|
|
result += ' ';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算带括号的答案
|
|
|
|
|
let newAnswer = this.calculateWithPriority(result);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
answer: newAnswer,
|
|
|
|
|
expression: result,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 执行四则运算操作
|
|
|
|
|
applyOperation(current, value, op) {
|
|
|
|
|
switch (op) {
|
|
|
|
|
case '+': return current + value;
|
|
|
|
|
case '-': return current - value;
|
|
|
|
|
case '×': return current * value;
|
|
|
|
|
case '÷': return value !== 0 ? current / value : current;
|
|
|
|
|
default: return current;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用正确优先级计算表达式的答案
|
|
|
|
|
calculateWithPriority(expression) {
|
|
|
|
|
// 替换运算符为JavaScript可识别的
|
|
|
|
|
let jsExpression = expression
|
|
|
|
|
.replace(/×/g, '*')
|
|
|
|
|
.replace(/÷/g, '/')
|
|
|
|
|
.replace(/²/g, '**2')
|
|
|
|
|
.replace(/√(\d+)/g, 'Math.sqrt($1)')
|
|
|
|
|
.replace(/sin(\d+)/g, 'Math.sin($1 * Math.PI / 180)')
|
|
|
|
|
.replace(/cos(\d+)/g, 'Math.cos($1 * Math.PI / 180)')
|
|
|
|
|
.replace(/tan(\d+)/g, 'Math.tan($1 * Math.PI / 180)');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 使用eval计算表达式
|
|
|
|
|
let result = eval(jsExpression);
|
|
|
|
|
|
|
|
|
|
// 处理特殊情况
|
|
|
|
|
if (typeof result === 'number') {
|
|
|
|
|
// 如果是整数,返回整数
|
|
|
|
|
if (Number.isInteger(result)) {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
// 否则保留两位小数
|
|
|
|
|
return Math.round(result * 100) / 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('计算表达式时出错:', expression, error);
|
|
|
|
|
// 如果计算失败,返回原始表达式的估算值
|
|
|
|
|
return this.estimateExpression(expression);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 估算表达式的值(当eval失败时使用)
|
|
|
|
|
estimateExpression(expression) {
|
|
|
|
|
// 简单的估算逻辑,按顺序计算
|
|
|
|
|
const parts = expression.split(' ');
|
|
|
|
|
let result = parseFloat(parts[0]);
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i < parts.length; i += 2) {
|
|
|
|
|
const operator = parts[i];
|
|
|
|
|
const num = parseFloat(parts[i + 1]);
|
|
|
|
|
|
|
|
|
|
switch (operator) {
|
|
|
|
|
case '+':
|
|
|
|
|
result += num;
|
|
|
|
|
break;
|
|
|
|
|
case '-':
|
|
|
|
|
result -= num;
|
|
|
|
|
break;
|
|
|
|
|
case '×':
|
|
|
|
|
result *= num;
|
|
|
|
|
break;
|
|
|
|
|
case '÷':
|
|
|
|
|
if (num !== 0) result /= num;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Math.round(result * 100) / 100;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = MathQuestionGenerator;
|