feat(trainer): системы 2 уравнений (kind system, пара-ответ) + текстовые задачи
- НОВЫЙ kind system: движок строит \begin{cases}, хранит пару {x,y}, самопроверка подстановкой обоих уравнений; checkStudentAnswer._checkSystem парсит «x=2; y=3» или «2; 3» (метки опциональны), проверяет ОБА уравнения
- тема Системы: sys-2x2 (полож. коэф., ур.2) + sys-2x2-neg (отрицательные, ур.3); приём корень-вперёд (берём решение, выводим правые части, det≠0)
- тема Задачи (compute, текстовые семьи): движение (путь/время/скорость), сплав (%), цена со скидкой
- exprToLatex: единичный коэффициент 1*x->x, -1*x->-x (латентная недоработка)
- 43 генератора, 14 тем; смоук движка 817/817 (T19 системы + T19b текстовые)
- страница (trainer.html) НЕ тронута — её редизайнит параллельная сессия; полировка ввода систем (скрыть «x=», placeholder, фидбэк пары) — после редизайна. Системы уже работают через checkStudentAnswer (ввод «2; 3»)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -200,6 +200,11 @@
|
||||
return base + '^{' + _latex(node.b) + '}';
|
||||
}
|
||||
if (op === '*') {
|
||||
// единичный коэффициент: 1*x -> x, (-1)*x -> -x (только при не-числовом множителе)
|
||||
if (node.a.k === 'num' && Math.abs(node.a.v) === 1 && node.b.k !== 'num')
|
||||
return (node.a.v < 0 ? '-' : '') + _mulOperand(node.b);
|
||||
if (node.b.k === 'num' && Math.abs(node.b.v) === 1 && node.a.k !== 'num')
|
||||
return (node.b.v < 0 ? '-' : '') + _mulOperand(node.a);
|
||||
if (_isNeg(node.a)) return '-' + _latex({ k: 'bin', op: '*', a: _negate(node.a), b: node.b }); // -5*x -> «-5x»
|
||||
var sep = (node.b.k === 'num' && node.b.v >= 0) ? ' \\cdot ' : ''; // знак · между числами; иначе соседство
|
||||
return _mulOperand(node.a) + sep + _mulOperand(node.b);
|
||||
@@ -293,6 +298,19 @@
|
||||
answer = Math.round(answer);
|
||||
}
|
||||
|
||||
// система уравнений (kind system): набор строк + пара-ответ {x,y,...}
|
||||
var system = null, pair = null;
|
||||
if (kind === 'system') {
|
||||
system = (gen.eqs || []).map(function (e) { return { lhs: render(e.lhs, env), rhs: render(e.rhs, env) }; });
|
||||
pair = {};
|
||||
var avs = gen.answerVars || ['x', 'y'];
|
||||
for (var ai = 0; ai < avs.length; ai++) {
|
||||
var pv = evalExpr((gen.answers && gen.answers[avs[ai]]) || '0', env);
|
||||
pair[avs[ai]] = gen.integerAnswer ? Math.round(pv) : pv;
|
||||
}
|
||||
answer = pair[avs[0]]; // запасной одиночный ответ
|
||||
}
|
||||
|
||||
var lhsExpr = render(gen.lhs || 'x', env);
|
||||
var rhsExpr = render(gen.rhs || 'x', env);
|
||||
var sEnv = assign(env, { ans: answer });
|
||||
@@ -308,6 +326,14 @@
|
||||
latex = exprToLatex(render(gen.srcExpr, env));
|
||||
} else if (kind === 'inequality') {
|
||||
latex = exprToLatex(lhsExpr + ' ' + (gen.dispOp || '<') + ' ' + rhsExpr);
|
||||
} else if (kind === 'system' && system) {
|
||||
var rows = [], okrows = true;
|
||||
for (var si2 = 0; si2 < system.length; si2++) {
|
||||
var l2 = exprToLatex(system[si2].lhs), r2 = exprToLatex(system[si2].rhs);
|
||||
if (l2 == null || r2 == null) { okrows = false; break; }
|
||||
rows.push(l2 + ' = ' + r2);
|
||||
}
|
||||
if (okrows) latex = '\\begin{cases} ' + rows.join(' \\\\ ') + ' \\end{cases}';
|
||||
}
|
||||
|
||||
var problem = {
|
||||
@@ -317,13 +343,17 @@
|
||||
kind: kind,
|
||||
lhsExpr: lhsExpr,
|
||||
rhsExpr: rhsExpr,
|
||||
display: prettyMath(render(gen.display || (gen.lhs + (kind === 'inequality' ? (' ' + (gen.dispOp || '<') + ' ') : ' = ') + gen.rhs), env)),
|
||||
display: (kind === 'system' && system)
|
||||
? system.map(function (e) { return prettyMath(e.lhs + ' = ' + e.rhs); }).join('; ')
|
||||
: prettyMath(render(gen.display || (gen.lhs + (kind === 'inequality' ? (' ' + (gen.dispOp || '<') + ' ') : ' = ') + gen.rhs), env)),
|
||||
latex: latex,
|
||||
answerVar: answerVar,
|
||||
answer: answer,
|
||||
answers: answers, // массив корней (kind roots)
|
||||
answerExpr: answerExpr, // канон. выражение (kind simplify)
|
||||
answerRel: answerRel, // { op, bound } (kind inequality)
|
||||
system: system, // [{lhs,rhs},…] (kind system)
|
||||
pair: pair, // эталонная пара {x,y,…} (kind system)
|
||||
answerVars: gen.answerVars || [answerVar],
|
||||
params: env,
|
||||
// шаг решения -> { note(текст), tex(подпись), latex(для KaTeX, null если не разобрался) }
|
||||
@@ -351,6 +381,12 @@
|
||||
okSelf = _origIneqHolds(lhsExpr, rhsExpr, gen.dispOp || '<', answerVar, inside) &&
|
||||
!_origIneqHolds(lhsExpr, rhsExpr, gen.dispOp || '<', answerVar, outside);
|
||||
why = 'неравенство не согласовано с ответом';
|
||||
} else if (kind === 'system') {
|
||||
okSelf = !!(system && system.length) && system.every(function (e) {
|
||||
var L = evalExpr(e.lhs, pair), R = evalExpr(e.rhs, pair);
|
||||
return Math.abs(L - R) <= EPS * Math.max(1, Math.abs(L), Math.abs(R));
|
||||
});
|
||||
why = 'пара не удовлетворяет системе';
|
||||
} else if (answers) {
|
||||
okSelf = answers.every(function (r) { return verifyRoot(problem, r).ok; });
|
||||
why = 'не все корни удовлетворяют уравнению';
|
||||
@@ -395,6 +431,7 @@
|
||||
if (problem.kind === 'simplify') return _checkEquiv(problem, raw);
|
||||
if (problem.kind === 'roots') return _checkMultiRoot(problem, raw);
|
||||
if (problem.kind === 'inequality') return _checkInequality(problem, raw);
|
||||
if (problem.kind === 'system') return _checkSystem(problem, raw);
|
||||
|
||||
var c = SE().compile(raw);
|
||||
if (c.error) {
|
||||
@@ -415,6 +452,32 @@
|
||||
};
|
||||
}
|
||||
|
||||
/* Система: ученик вводит пару «x = 2; y = 3» (или «2; 3»). Проверяем подстановкой в ОБА уравнения.
|
||||
Метки переменных опциональны; без меток — по порядку answerVars. */
|
||||
function _checkSystem(problem, raw) {
|
||||
var vars = problem.answerVars || ['x', 'y'];
|
||||
var parts = raw.split(/[;,]/).map(function (s) { return s.trim(); }).filter(Boolean);
|
||||
if (parts.length < vars.length) return { ok: false, reason: 'incomplete', message: 'Введите обе переменные, напр. x = 2; y = 3.' };
|
||||
var vals = {}, pos = [];
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var m = parts[i].match(/^([a-zA-Z]\w*)\s*=\s*(.+)$/);
|
||||
var c = SE().compile(m ? m[2] : parts[i]);
|
||||
if (c.error) return { ok: false, reason: 'parse', message: 'Не понял запись «' + parts[i] + '».' };
|
||||
var num = c.fn({});
|
||||
if (!isFinite(num)) return { ok: false, reason: 'nan', message: 'Это не число.' };
|
||||
if (m) vals[m[1]] = num; else pos.push(num);
|
||||
}
|
||||
for (var j = 0; j < vars.length; j++) if (vals[vars[j]] === undefined && pos.length) vals[vars[j]] = pos.shift();
|
||||
for (var j2 = 0; j2 < vars.length; j2++) if (vals[vars[j2]] === undefined) return { ok: false, reason: 'incomplete', message: 'Укажите ' + vars[j2] + '.' };
|
||||
var sys = problem.system || [];
|
||||
for (var e = 0; e < sys.length; e++) {
|
||||
var L = evalExpr(sys[e].lhs, vals), R = evalExpr(sys[e].rhs, vals);
|
||||
if (Math.abs(L - R) > EPS * Math.max(1, Math.abs(L), Math.abs(R)))
|
||||
return { ok: false, reason: 'wrong', value: vals, message: 'Пара не подходит под уравнения системы.' };
|
||||
}
|
||||
return { ok: true, reason: null, value: vals, message: 'Верно!' };
|
||||
}
|
||||
|
||||
/* Несколько корней: ученик вводит все через «;»/«,»/пробел; сверяем как мультимножество. */
|
||||
function _checkMultiRoot(problem, raw) {
|
||||
var parts = raw.split(/[;,\s]+/).filter(Boolean);
|
||||
|
||||
@@ -29,11 +29,13 @@
|
||||
{ key: 'powers', label: 'Степени', subject: 'algebra', grade: 7, order: 5 },
|
||||
{ key: 'formulas', label: 'Формулы', subject: 'algebra', grade: 7, order: 6 },
|
||||
{ key: 'inequalities', label: 'Неравенства', subject: 'algebra', grade: 7, order: 7 },
|
||||
{ key: 'quadratic', label: 'Квадратные', subject: 'algebra', grade: 8, order: 8 },
|
||||
{ key: 'progressions', label: 'Прогрессии', subject: 'algebra', grade: 9, order: 9 },
|
||||
{ key: 'g-angles', label: 'Углы', subject: 'geometry', grade: 7, order: 10 },
|
||||
{ key: 'g-pyth', label: 'Пифагор', subject: 'geometry', grade: 8, order: 11 },
|
||||
{ key: 'g-area', label: 'Площади', subject: 'geometry', grade: 8, order: 12 }
|
||||
{ key: 'systems', label: 'Системы', subject: 'algebra', grade: 7, order: 8 },
|
||||
{ key: 'quadratic', label: 'Квадратные', subject: 'algebra', grade: 8, order: 9 },
|
||||
{ key: 'progressions', label: 'Прогрессии', subject: 'algebra', grade: 9, order: 10 },
|
||||
{ key: 'applied', label: 'Задачи', subject: 'algebra', grade: 7, order: 11 },
|
||||
{ key: 'g-angles', label: 'Углы', subject: 'geometry', grade: 7, order: 12 },
|
||||
{ key: 'g-pyth', label: 'Пифагор', subject: 'geometry', grade: 8, order: 13 },
|
||||
{ key: 'g-area', label: 'Площади', subject: 'geometry', grade: 8, order: 14 }
|
||||
];
|
||||
|
||||
var GENERATORS = [
|
||||
@@ -590,6 +592,109 @@
|
||||
{ note: 'Площадь квадрата — сторона, возведённая в квадрат:', tex: 'x = {a}^2' },
|
||||
{ note: 'Считаем:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* ═══ Тема: Системы 2 линейных уравнений (7 класс) ═══
|
||||
kind:'system' — ответ ПАРА (x; y). «Корень-вперёд»: берём решение (sx, sy) и
|
||||
коэффициенты, выводим правые части c1/c2 так, что система имеет ровно это
|
||||
решение (определитель ≠ 0). Движок рисует \begin{cases}, проверяет подстановкой. */
|
||||
|
||||
/* система с положительными коэффициентами */
|
||||
{
|
||||
id: 'sys-2x2', topic: 'systems', order: 1, subject: 'algebra', grade: 7, kind: 'system',
|
||||
title: 'Система 2×2',
|
||||
pick: { a1: [1, 4], b1: [1, 4], a2: [1, 4], b2: [1, 4], sx: [-6, 6], sy: [-6, 6] },
|
||||
constraint: 'a1*b2 - a2*b1 != 0',
|
||||
derive: { c1: 'a1*sx + b1*sy', c2: 'a2*sx + b2*sy' },
|
||||
eqs: [{ lhs: '{a1}*x + {b1}*y', rhs: '{c1}' }, { lhs: '{a2}*x + {b2}*y', rhs: '{c2}' }],
|
||||
answers: { x: 'sx', y: 'sy' }, answerVars: ['x', 'y'], integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Исключите одну переменную: умножьте уравнения так, чтобы коэффициенты при x (или y) совпали, и сложите/вычтите — найдёте одну переменную.', tex: '' },
|
||||
{ note: 'Подставьте найденное в любое уравнение. Решение системы: x = {sx}, y = {sy}.', tex: '' }
|
||||
]
|
||||
},
|
||||
|
||||
/* система с отрицательными коэффициентами (сложнее) */
|
||||
{
|
||||
id: 'sys-2x2-neg', topic: 'systems', order: 2, subject: 'algebra', grade: 8, kind: 'system',
|
||||
title: 'Система (с отрицательными)',
|
||||
pick: { a1: [-4, 4], b1: [-4, 4], a2: [-4, 4], b2: [-4, 4], sx: [-7, 7], sy: [-7, 7] },
|
||||
constraint: 'a1 != 0 && b1 != 0 && a2 != 0 && b2 != 0 && a1*b2 - a2*b1 != 0',
|
||||
derive: { c1: 'a1*sx + b1*sy', c2: 'a2*sx + b2*sy' },
|
||||
eqs: [{ lhs: '{a1}*x + {b1}*y', rhs: '{c1}' }, { lhs: '{a2}*x + {b2}*y', rhs: '{c2}' }],
|
||||
answers: { x: 'sx', y: 'sy' }, answerVars: ['x', 'y'], integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Будьте внимательны со знаками. Исключите переменную методом сложения, найдите одну, подставьте во второе уравнение.', tex: '' },
|
||||
{ note: 'Решение системы: x = {sx}, y = {sy}.', tex: '' }
|
||||
]
|
||||
},
|
||||
|
||||
/* ═══ Тема: Задачи (текстовые, параметрические — 7 класс) ═══
|
||||
kind:'compute' — условие в display, lhs:'x'/rhs:<формула> для проверки. */
|
||||
|
||||
/* путь = скорость × время */
|
||||
{
|
||||
id: 'app-move-dist', topic: 'applied', order: 1, subject: 'algebra', grade: 7, kind: 'compute',
|
||||
title: 'Путь (движение)',
|
||||
pick: { v: [10, 90], t: [2, 9] }, derive: { val: 'v*t' },
|
||||
lhs: 'x', rhs: '{v}*{t}', display: 'Автомобиль ехал {t} ч со скоростью {v} км/ч. Какой путь он проехал (в км)?',
|
||||
answerVar: 'x', answer: 'val', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Путь равен произведению скорости на время:', tex: 'x = {v}*{t}' },
|
||||
{ note: 'Считаем:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* время = путь / скорость */
|
||||
{
|
||||
id: 'app-move-time', topic: 'applied', order: 2, subject: 'algebra', grade: 7, kind: 'compute',
|
||||
title: 'Время (движение)',
|
||||
pick: { v: [10, 90], t: [2, 9] }, derive: { S: 'v*t', val: 't' },
|
||||
lhs: 'x', rhs: '{S}/{v}', display: 'Расстояние между городами {S} км. Автомобиль едет со скоростью {v} км/ч. За сколько часов он доедет?',
|
||||
answerVar: 'x', answer: 'val', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Время равно пути, делённому на скорость:', tex: 'x = {S}/{v}' },
|
||||
{ note: 'Считаем:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* скорость = путь / время */
|
||||
{
|
||||
id: 'app-move-speed', topic: 'applied', order: 3, subject: 'algebra', grade: 7, kind: 'compute',
|
||||
title: 'Скорость (движение)',
|
||||
pick: { v: [10, 90], t: [2, 9] }, derive: { S: 'v*t', val: 'v' },
|
||||
lhs: 'x', rhs: '{S}/{t}', display: 'Поезд прошёл {S} км за {t} ч. Найдите его среднюю скорость (км/ч).',
|
||||
answerVar: 'x', answer: 'val', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Скорость равна пути, делённому на время:', tex: 'x = {S}/{t}' },
|
||||
{ note: 'Считаем:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* содержание вещества в сплаве (проценты) */
|
||||
{
|
||||
id: 'app-alloy', topic: 'applied', order: 4, subject: 'algebra', grade: 7, kind: 'compute',
|
||||
title: 'Сплав (проценты)',
|
||||
pick: { mfac: [1, 9], pidx: [1, 9] }, derive: { m: 'mfac*10', p: 'pidx*10', val: 'mfac*pidx' },
|
||||
lhs: 'x', rhs: '{m}*{p}/100', display: 'Сплав массой {m} кг содержит {p}% меди. Сколько килограммов меди в сплаве?',
|
||||
answerVar: 'x', answer: 'val', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Масса меди = масса сплава × процент ÷ 100:', tex: 'x = {m}*{p}/100' },
|
||||
{ note: 'Считаем:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* цена со скидкой */
|
||||
{
|
||||
id: 'app-discount', topic: 'applied', order: 5, subject: 'algebra', grade: 7, kind: 'compute',
|
||||
title: 'Цена со скидкой',
|
||||
pick: { pbase: [5, 30], didx: [1, 5] }, derive: { price: 'pbase*10', d: 'didx*10', val: 'pbase*(10 - didx)' },
|
||||
lhs: 'x', rhs: '{price}*(100 - {d})/100', display: 'Товар стоил {price} руб. Скидка {d}%. Сколько он стоит после скидки (в рублях)?',
|
||||
answerVar: 'x', answer: 'val', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Новая цена = старая × (100 − скидка) ÷ 100:', tex: 'x = {price}*(100 - {d})/100' },
|
||||
{ note: 'Считаем:', tex: 'x = {ans}' }
|
||||
]
|
||||
}
|
||||
|
||||
];
|
||||
@@ -613,6 +718,10 @@
|
||||
'sq-sum': 2, 'sq-diff': 2, 'diff-sq': 3,
|
||||
// Неравенства (смена знака — сложнее)
|
||||
'ineq-lt': 1, 'ineq-ge': 1, 'ineq-flip': 3,
|
||||
// Системы 2 уравнений
|
||||
'sys-2x2': 2, 'sys-2x2-neg': 3,
|
||||
// Задачи (текстовые)
|
||||
'app-move-dist': 1, 'app-move-speed': 1, 'app-move-time': 2, 'app-alloy': 2, 'app-discount': 2,
|
||||
// Квадратные
|
||||
'quad-diff': 2, 'quad-factored': 3,
|
||||
// Прогрессии
|
||||
|
||||
Reference in New Issue
Block a user