From 5226deb975beaedb2c93498b3582f6d060b8236e Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Thu, 25 Jun 2026 16:44:19 +0300 Subject: [PATCH] =?UTF-8?q?feat(trainer):=20=D1=81=D0=B8=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=D1=8B=202=20=D1=83=D1=80=D0=B0=D0=B2=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20(kind=20system,=20=D0=BF=D0=B0=D1=80=D0=B0-?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=82)=20+=20=D1=82=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - НОВЫЙ 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) --- frontend/js/trainer/_trainer_engine.js | 65 +++++++++++++- frontend/js/trainer/generators.js | 119 +++++++++++++++++++++++-- 2 files changed, 178 insertions(+), 6 deletions(-) diff --git a/frontend/js/trainer/_trainer_engine.js b/frontend/js/trainer/_trainer_engine.js index 87b039c..6c6475f 100644 --- a/frontend/js/trainer/_trainer_engine.js +++ b/frontend/js/trainer/_trainer_engine.js @@ -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); diff --git a/frontend/js/trainer/generators.js b/frontend/js/trainer/generators.js index a03a57d..d0496d8 100644 --- a/frontend/js/trainer/generators.js +++ b/frontend/js/trainer/generators.js @@ -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, // Прогрессии