From 105e04608777c2978f90ce12c02d57aa780f8661 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Fri, 26 Jun 2026 13:16:37 +0300 Subject: [PATCH] =?UTF-8?q?feat(trainer):=20V4.1=20=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=BF=D0=BF=D0=B0=201=20=E2=80=94=2015=20=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BB=D0=B8=D0=BD=D0=B5=D0=B9=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=20(=D1=83=D1=80=D0=B0=D0=B2=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F/=D0=BD=D0=B5=D1=80=D0=B0=D0=B2=D0=B5=D0=BD=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B0/=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D1=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализована первая волна плана v4 (разнообразие задач по темам). Все «корень-вперёд», ответы чистые целые, подробные пошаговые решения, без правок страницы. Уравнения (linear-eq): lin-both-frac (с делением на a−c≥2), lin-x-denom (a/(x+b)=c), lin-k-over-x (k/x=c), lin-abs (|ax+b|=c, два корня, kind roots), lin-frac-eq-frac (дробь=дробь крест-накрест), lin-nested-paren (вложенные скобки), lin-literal (ax+b=mx, «выразите x»). Неравенства (inequalities): ineq-both-sides (ax+ba), ineq-paren (a(x+b)>c), ineq-count-int (число целых решений lo --- frontend/js/trainer/_trainer_engine.js | 6 +- frontend/js/trainer/generators.js | 274 +++++++++++++++++++++++++ 2 files changed, 278 insertions(+), 2 deletions(-) diff --git a/frontend/js/trainer/_trainer_engine.js b/frontend/js/trainer/_trainer_engine.js index 0de3a82..aecf2a7 100644 --- a/frontend/js/trainer/_trainer_engine.js +++ b/frontend/js/trainer/_trainer_engine.js @@ -354,7 +354,7 @@ latex = exprToLatex(render(gen.srcExpr, env)); } else if (kind === 'inequality') { latex = exprToLatex(lhsExpr + ' ' + (gen.dispOp || '<') + ' ' + rhsExpr); - } else if (kind === 'system' && system) { + } else if (kind === 'system' && system && !gen.display) { var rows = [], okrows = true; for (var si2 = 0; si2 < system.length; si2++) { var l2 = exprToLatex(system[si2].lhs), r2 = exprToLatex(system[si2].rhs); @@ -373,7 +373,9 @@ figurePrompt: gen.figurePrompt || null, // краткое условие для режима «читать с чертежа» lhsExpr: lhsExpr, rhsExpr: rhsExpr, - display: (kind === 'system' && system) + // система: по умолчанию показываем сами уравнения; но если задан gen.display + // (текстовая/сюжетная формулировка) — показываем его (ученик сам составляет систему). + display: (kind === 'system' && system && !gen.display) ? 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, diff --git a/frontend/js/trainer/generators.js b/frontend/js/trainer/generators.js index 26f40ba..f7bfa62 100644 --- a/frontend/js/trainer/generators.js +++ b/frontend/js/trainer/generators.js @@ -1072,6 +1072,275 @@ { note: 'Длина дуги — это часть длины окружности: L = (n/360)·2πr. Здесь n = {n}°, r = {r}, π ≈ 3,14:', tex: 'x = {n}/360*2*3.14*{r}' }, { note: 'Считаем:', tex: 'x = {ans}' } ] + }, + + /* ═══════════════════════════════════════════════════════════════════════ + V4.1 — Группа 1 (линейные): новые формы и формулировки. Все «корень-вперёд». + ═══════════════════════════════════════════════════════════════════════ */ + + /* ── Уравнения ── */ + + /* ax + b = cx + d с гарантированным делением на (a−c)≥2 */ + { + id: 'lin-both-frac', topic: 'linear-eq', order: 8, subject: 'algebra', grade: 7, + title: 'ax + b = cx + d (с делением)', + pick: { a: [5, 9], c: [1, 4], b: [1, 12], root: [-7, 7] }, + constraint: 'a - c >= 2', require: 'root != 0', + derive: { d: '(a - c)*root + b', amc: 'a - c', dmb: '(a - c)*root' }, + lhs: '{a}*x + {b}', rhs: '{c}*x + {d}', display: '{a}x + {b} = {c}x + {d}', + answerVar: 'x', answer: 'root', integerAnswer: true, + solution: [ + { note: 'x есть в обеих частях. Соберём слагаемые с x слева, числа — справа (со сменой знака).', tex: '({a} - {c})x = {d} - {b}' }, + { note: 'Приводим подобные.', tex: '{amc}x = {dmb}' }, + { note: 'Делим обе части на коэффициент при x — на {amc}.', tex: 'x = {dmb} / {amc}' }, + { note: 'Получаем корень.', tex: 'x = {ans}' }, + { note: 'Проверка: подставляем корень — обе части совпадут.', tex: '{a}*({ans}) + {b} = {c}*({ans}) + {d}' } + ] + }, + + /* a/(x + b) = c — x в знаменателе */ + { + id: 'lin-x-denom', topic: 'linear-eq', order: 9, subject: 'algebra', grade: 8, + title: 'a/(x + b) = c', + pick: { c: [2, 6], b: [-5, 8], root: [-7, 9] }, + require: 'root + b != 0 && root != 0', + derive: { a: 'c*(root + b)', rb: 'root + b' }, + lhs: '{a}/(x + {b})', rhs: '{c}', display: '{a}/(x + {b}) = {c}', + answerVar: 'x', answer: 'root', integerAnswer: true, + solution: [ + { note: 'Знаменатель x + {b} не равен нулю. Умножим обе части на него.', tex: '{a} = {c}*(x + {b})' }, + { note: 'Разделим обе части на {c}.', tex: 'x + {b} = {rb}' }, + { note: 'Перенесём {b} вправо со сменой знака.', tex: 'x = {ans}' }, + { note: 'Проверка: подставляем корень в исходную дробь.', tex: '{a}/({ans} + {b}) = {c}' } + ] + }, + + /* k/x = c — обратная пропорция */ + { + id: 'lin-k-over-x', topic: 'linear-eq', order: 10, subject: 'algebra', grade: 8, + title: 'k/x = c', + pick: { c: [2, 9], root: [-9, 9] }, + require: 'root != 0', + derive: { k: 'c*root' }, + lhs: '{k}/x', rhs: '{c}', display: '{k}/x = {c}', + answerVar: 'x', answer: 'root', integerAnswer: true, + solution: [ + { note: 'Умножим обе части на x, чтобы убрать дробь.', tex: '{k} = {c}*x' }, + { note: 'Разделим обе части на {c}.', tex: 'x = {k} / {c}' }, + { note: 'Получаем корень.', tex: 'x = {ans}' }, + { note: 'Проверка: подставляем корень.', tex: '{k}/({ans}) = {c}' } + ] + }, + + /* |ax + b| = c — два корня */ + { + id: 'lin-abs', topic: 'linear-eq', order: 11, subject: 'algebra', grade: 8, kind: 'roots', + title: '|ax + b| = c', + pick: { a: [1, 5], r1: [-7, 3], r2: [-2, 8] }, + constraint: 'r2 > r1', + derive: { b: '-a*(r1 + r2)/2', c: 'a*(r2 - r1)/2' }, + require: 'mod(a*(r1 + r2), 2) == 0 && c > 0', + lhs: 'abs({a}*x + {b})', rhs: '{c}', display: '|{a}x + {b}| = {c}', + answerVar: 'x', answers: ['r1', 'r2'], integerAnswer: true, + solution: [ + { note: 'Модуль равен {c}, значит выражение под модулем равно {c} или −{c}. Рассмотрим два случая.', tex: '' }, + { note: 'Первый случай — выражение равно {c}:', tex: '{a}*x + {b} = {c}' }, + { note: 'Решаем и получаем корень:', tex: 'x = {r2}' }, + { note: 'Второй случай — выражение равно −{c}:', tex: '{a}*x + {b} = -{c}' }, + { note: 'Решаем и получаем второй корень:', tex: 'x = {r1}' } + ] + }, + + /* (ax + b)/c = (dx + g)/f — дробь = дробь (крест-накрест) */ + { + id: 'lin-frac-eq-frac', topic: 'linear-eq', order: 12, subject: 'algebra', grade: 8, + title: '(ax + b)/c = (dx + g)/f', + pick: { a: [2, 5], c: [2, 4], d: [1, 4], f: [2, 4], b: [1, 9], root: [-5, 6] }, + constraint: 'a*f != c*d', + require: 'mod(f*(a*root + b) - c*d*root, c) == 0 && root != 0', + derive: { g: '(f*(a*root + b) - c*d*root)/c', fa: 'f*a', fb: 'f*b', cd: 'c*d', cg: 'c*g', cross: 'f*a - c*d', rc: '(f*a - c*d)*root' }, + lhs: '({a}*x + {b})/{c}', rhs: '({d}*x + {g})/{f}', display: '({a}x + {b})/{c} = ({d}x + {g})/{f}', + answerVar: 'x', answer: 'root', integerAnswer: true, + solution: [ + { note: 'Перед нами равенство двух дробей. Умножаем крест-накрест: числитель левой на знаменатель правой и наоборот.', tex: '{f}*({a}*x + {b}) = {c}*({d}*x + {g})' }, + { note: 'Раскрываем скобки.', tex: '{fa}*x + {fb} = {cd}*x + {cg}' }, + { note: 'Собираем x слева, числа справа.', tex: '{cross}*x = {rc}' }, + { note: 'Делим на коэффициент при x.', tex: 'x = {ans}' }, + { note: 'Проверка: при найденном x обе дроби равны.', tex: '({a}*({ans}) + {b})/{c} = ({d}*({ans}) + {g})/{f}' } + ] + }, + + /* a(b(x + c) + d) = e — вложенные скобки */ + { + id: 'lin-nested-paren', topic: 'linear-eq', order: 13, subject: 'algebra', grade: 8, + title: 'a(b(x + c) + d) = e', + pick: { a: [2, 4], b: [2, 4], c: [-4, 6], d: [1, 8], root: [-5, 6] }, + require: 'root != 0', + derive: { ev: 'a*(b*(root + c) + d)', inner: 'b*(root + c) + d', inner2: 'b*(root + c)', rootc: 'root + c' }, + lhs: '{a}*({b}*(x + {c}) + {d})', rhs: '{ev}', display: '{a}({b}(x + {c}) + {d}) = {ev}', + answerVar: 'x', answer: 'root', integerAnswer: true, + solution: [ + { note: 'Снимаем внешний множитель {a}: делим обе части на {a}.', tex: '{b}*(x + {c}) + {d} = {inner}' }, + { note: 'Переносим {d} вправо.', tex: '{b}*(x + {c}) = {inner2}' }, + { note: 'Делим на {b}.', tex: 'x + {c} = {rootc}' }, + { note: 'Переносим {c} и получаем корень.', tex: 'x = {ans}' }, + { note: 'Проверка: подставляем корень во вложенные скобки.', tex: '{a}*({b}*({ans} + {c}) + {d}) = {ev}' } + ] + }, + + /* ax + b = mx — «выразите x» (буквенный коэффициент как параметр) */ + { + id: 'lin-literal', topic: 'linear-eq', order: 14, subject: 'algebra', grade: 8, + title: 'ax + b = mx (выразите x)', + pick: { a: [2, 5], gap: [2, 6], root: [2, 8] }, + derive: { m: 'a + gap', b: 'gap*root', mma: 'gap' }, + lhs: '{a}*x + {b}', rhs: '{m}*x', display: '{a}x + {b} = {m}x', + answerVar: 'x', answer: 'root', integerAnswer: true, + solution: [ + { note: 'Соберём слагаемые с x в одной части: перенесём {a}x вправо, число оставим слева.', tex: '{b} = ({m} - {a})*x' }, + { note: 'Приводим подобные (коэффициент при x).', tex: '{b} = {mma}*x' }, + { note: 'Делим обе части на {mma}.', tex: 'x = {b} / {mma}' }, + { note: 'Получаем корень.', tex: 'x = {ans}' }, + { note: 'Проверка: подставляем корень.', tex: '{a}*({ans}) + {b} = {m}*({ans})' } + ] + }, + + /* ── Неравенства ── */ + + /* ax + b < cx + d, a>c — знак сохраняется */ + { + id: 'ineq-both-sides', topic: 'inequalities', order: 4, subject: 'algebra', grade: 8, kind: 'inequality', + title: 'ax + b < cx + d', + pick: { a: [3, 7], c: [1, 4], b: [1, 12], root: [-6, 8] }, + constraint: 'a - c >= 2', + derive: { d: '(a - c)*root + b', amc: 'a - c', dmb: '(a - c)*root' }, + lhs: '{a}*x + {b}', rhs: '{c}*x + {d}', dispOp: '<', relOp: '<', bound: 'root', + answerVar: 'x', + solution: [ + { note: 'Собираем x слева, числа справа (со сменой знака при переносе).', tex: '({a} - {c})x < {d} - {b}' }, + { note: 'Приводим подобные.', tex: '{amc}x < {dmb}' }, + { note: 'Делим на {amc} — число положительное, знак неравенства сохраняется.', tex: 'x < {root}' } + ] + }, + + /* ax + b ≥ cx + d, c>a — смена знака */ + { + id: 'ineq-both-flip', topic: 'inequalities', order: 5, subject: 'algebra', grade: 8, kind: 'inequality', + title: 'ax + b ≥ cx + d (смена знака)', + pick: { a: [1, 4], c: [3, 7], b: [1, 12], root: [-6, 8] }, + constraint: 'c - a >= 2', + derive: { d: '(a - c)*root + b', amc: 'a - c', dmb: '(a - c)*root' }, + lhs: '{a}*x + {b}', rhs: '{c}*x + {d}', dispOp: '>=', relOp: '<=', bound: 'root', + answerVar: 'x', + solution: [ + { note: 'Собираем x слева, числа справа.', tex: '({a} - {c})x >= {d} - {b}' }, + { note: 'Приводим подобные — коэффициент при x отрицательный.', tex: '{amc}*x >= {dmb}' }, + { note: 'Делим на отрицательное число {amc} — знак неравенства МЕНЯЕТСЯ.', tex: 'x <= {root}' } + ] + }, + + /* a(x + b) > c — скобка в неравенстве */ + { + id: 'ineq-paren', topic: 'inequalities', order: 6, subject: 'algebra', grade: 8, kind: 'inequality', + title: 'a(x + b) > c', + pick: { a: [2, 6], b: [1, 8], root: [-5, 8] }, + derive: { c: 'a*(root + b)', rootb: 'root + b' }, + lhs: '{a}*(x + {b})', rhs: '{c}', dispOp: '>', relOp: '>', bound: 'root', + answerVar: 'x', + solution: [ + { note: 'Делим обе части на {a} (положительное) — знак сохраняется.', tex: 'x + {b} > {rootb}' }, + { note: 'Переносим {b} вправо.', tex: 'x > {root}' } + ] + }, + + /* Сколько целых решений у lo < x ≤ hi (двойное неравенство через compute) */ + { + id: 'ineq-count-int', topic: 'inequalities', order: 7, subject: 'algebra', grade: 8, kind: 'compute', + title: 'Число целых решений', + pick: { lo: [-6, 3], span: [2, 8] }, + derive: { hi: 'lo + span', val: 'span' }, + lhs: 'x', rhs: '{hi} - {lo}', display: 'Сколько целых чисел x удовлетворяют условию {lo} < x ≤ {hi}?', + answerVar: 'x', answer: 'val', integerAnswer: true, + solution: [ + { note: 'Целые решения — это {lo}+1, {lo}+2, …, {hi}. Их количество равно {hi} − {lo}.', tex: 'x = {hi} - {lo}' }, + { note: 'Считаем.', tex: 'x = {ans}' } + ] + }, + + /* ── Системы ── */ + + /* x + y = S, x − y = D — сумма и разность */ + { + id: 'sys-sum-diff', topic: 'systems', order: 3, subject: 'algebra', grade: 7, kind: 'system', + title: 'Система x + y = S, x − y = D', + pick: { sx: [-7, 8], sy: [-7, 8] }, + derive: { S: 'sx + sy', D: 'sx - sy', twoX: '2*sx', twoY: '2*sy' }, + eqs: [{ lhs: 'x + y', rhs: '{S}' }, { lhs: 'x - y', rhs: '{D}' }], + answers: { x: 'sx', y: 'sy' }, answerVars: ['x', 'y'], integerAnswer: true, + solution: [ + { note: 'Сложим уравнения почленно — переменная y исключается.', tex: '2*x = {twoX}' }, + { note: 'Находим x.', tex: 'x = {sx}' }, + { note: 'Вычтем из первого уравнения второе — исключается x.', tex: '2*y = {twoY}' }, + { note: 'Находим y.', tex: 'y = {sy}' }, + { note: 'Ответ: x = {sx}, y = {sy}.', tex: '' } + ] + }, + + /* y = mx + k, a2·x + b2·y = c2 — метод подстановки */ + { + id: 'sys-subst', topic: 'systems', order: 4, subject: 'algebra', grade: 7, kind: 'system', + title: 'Система (подстановка)', + pick: { sx: [-5, 6], sy: [-6, 7], m: [-3, 4], a2: [1, 5], b2: [1, 4] }, + constraint: 'a2 + b2*m != 0', + derive: { k: 'sy - m*sx', c2: 'a2*sx + b2*sy', detc: 'a2 + b2*m', rhsx: 'c2 - b2*k' }, + eqs: [{ lhs: 'y', rhs: '{m}*x + {k}' }, { lhs: '{a2}*x + {b2}*y', rhs: '{c2}' }], + answers: { x: 'sx', y: 'sy' }, answerVars: ['x', 'y'], integerAnswer: true, + solution: [ + { note: 'Первое уравнение уже выражает y. Подставим его во второе.', tex: '{a2}*x + {b2}*({m}*x + {k}) = {c2}' }, + { note: 'Раскрываем скобки и приводим подобные при x.', tex: '{detc}*x = {rhsx}' }, + { note: 'Находим x.', tex: 'x = {sx}' }, + { note: 'Подставляем x в первое уравнение.', tex: 'y = {m}*({sx}) + {k}' }, + { note: 'Находим y.', tex: 'y = {sy}' }, + { note: 'Ответ: x = {sx}, y = {sy}.', tex: '' } + ] + }, + + /* Текстовая задача на систему (мальчики/девочки) */ + { + id: 'sys-word', topic: 'systems', order: 5, subject: 'algebra', grade: 7, kind: 'system', + title: 'Текстовая задача (система)', + pick: { sx: [5, 16], sy: [6, 18] }, + constraint: 'sy > sx', + derive: { N: 'sx + sy', D: 'sy - sx', twoY: '2*sy' }, + eqs: [{ lhs: 'x + y', rhs: '{N}' }, { lhs: 'y - x', rhs: '{D}' }], + display: 'В классе всего {N} учеников, причём девочек на {D} больше, чем мальчиков. Сколько мальчиков (x) и девочек (y)?', + answers: { x: 'sx', y: 'sy' }, answerVars: ['x', 'y'], integerAnswer: true, + solution: [ + { note: 'Обозначим мальчиков за x, девочек за y. Всего учеников:', tex: 'x + y = {N}' }, + { note: 'Девочек на {D} больше, чем мальчиков:', tex: 'y - x = {D}' }, + { note: 'Сложим уравнения — найдём удвоенное число девочек.', tex: '2*y = {twoY}' }, + { note: 'Находим число девочек y.', tex: 'y = {sy}' }, + { note: 'Тогда мальчиков:', tex: 'x = {N} - {sy}' }, + { note: 'Ответ: мальчиков {sx}, девочек {sy}.', tex: '' } + ] + }, + + /* Система 3×3 (вводный тизер) */ + { + id: 'sys-3x3', topic: 'systems', order: 6, subject: 'algebra', grade: 9, kind: 'system', + title: 'Система 3×3', + pick: { sx: [-4, 5], sy: [-4, 5], sz: [-4, 5] }, + derive: { S: 'sx + sy + sz', Sxy: 'sx + sy', Syz: 'sy + sz' }, + eqs: [{ lhs: 'x + y + z', rhs: '{S}' }, { lhs: 'x + y', rhs: '{Sxy}' }, { lhs: 'y + z', rhs: '{Syz}' }], + answers: { x: 'sx', y: 'sy', z: 'sz' }, answerVars: ['x', 'y', 'z'], integerAnswer: true, + solution: [ + { note: 'Вычтем из первого уравнения второе — найдём z.', tex: 'z = {S} - {Sxy}' }, + { note: 'Получаем z.', tex: 'z = {sz}' }, + { note: 'Вычтем из первого уравнения третье — найдём x.', tex: 'x = {S} - {Syz}' }, + { note: 'Получаем x.', tex: 'x = {sx}' }, + { note: 'Из второго уравнения найдём y.', tex: 'y = {Sxy} - {sx}' }, + { note: 'Ответ: x = {sx}, y = {sy}, z = {sz}.', tex: '' } + ] } ]; @@ -1114,6 +1383,11 @@ 'poly-angles-sum': 1, 'poly-regular-angle': 2, 'sim-side': 1, 'sim-perimeter': 2, // Геометрия — Окружность 'circ-length': 1, 'circ-diam': 1, 'circ-area': 2, 'circ-arc': 3, + // V4.1 — Линейные: новые формы + 'lin-both-frac': 2, 'lin-x-denom': 3, 'lin-k-over-x': 2, 'lin-abs': 3, + 'lin-frac-eq-frac': 3, 'lin-nested-paren': 3, 'lin-literal': 3, + 'ineq-both-sides': 2, 'ineq-both-flip': 3, 'ineq-paren': 2, 'ineq-count-int': 2, + 'sys-sum-diff': 1, 'sys-subst': 2, 'sys-word': 2, 'sys-3x3': 3, // НОД/НОК / Дроби / Десятичные / Отрицательные 'gcd-pair': 1, 'lcm-pair': 2, 'frac-of-number': 1, 'frac-add-same': 2,