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:
Maxim Dolgolyov
2026-06-25 16:44:19 +03:00
parent 8df7d1713c
commit 5226deb975
2 changed files with 178 additions and 6 deletions
+64 -1
View File
@@ -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);