From 3b095a9fd385a53bf6081d56d21ebeac7780eef3 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Mon, 29 Jun 2026 16:16:41 +0300 Subject: [PATCH] =?UTF-8?q?feat(trainer):=20=D0=BE=D0=B1=D0=BE=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D1=81=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BE=D0=B9=20=D0=B2=D0=B5=D0=BB=D0=B8=D1=87=D0=B8=D0=BD?= =?UTF-8?q?=D1=8B=20(P/S/C/L/d/k/n/q/b)=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=20=C2=ABx=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Поле answerSym у генератора задаёт, как подписывать искомую величину: периметр — P, площадь — S, длина окружности — C, длина (дуга/хорда/касательная) — L, диагональ/ расстояние — d, плюс коэффициент k, разность d, номер n, знаменатель q, b (по корню). answerVar/lhs остаются 'x' (проверка не меняется) — answerSym только для показа. - Движок: проблема несёт answerSym; в шагах решения переменная ответа автоматически подменяется на символ (\b x \b → P/S/…), так что и ввод, и решение согласованы. - Страница: префикс ввода (#tr-eqx) и подпись ответа используют answerSym (например «P =», «S =») вместо жёсткого «x =». - 26 генераторов размечены (площади/периметры/длины/диагонали + находящие коэффициент/разность/номер/знаменатель — где раньше ввод «x» не совпадал с решением). Смоук v41 81712 проверок (answerSym проходит сквозь; в шагах решения нет остаточного x; числовой ответ по-прежнему принимается). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/trainer/_trainer_engine.js | 7 +++- frontend/js/trainer/generators.js | 52 +++++++++++++------------- frontend/trainer.html | 8 +++- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/frontend/js/trainer/_trainer_engine.js b/frontend/js/trainer/_trainer_engine.js index aecf2a7..1b205d6 100644 --- a/frontend/js/trainer/_trainer_engine.js +++ b/frontend/js/trainer/_trainer_engine.js @@ -371,6 +371,7 @@ kind: kind, figure: gen.figure || null, // спека чертежа (данные) — рисует TrainerFigures по params figurePrompt: gen.figurePrompt || null, // краткое условие для режима «читать с чертежа» + answerSym: gen.answerSym || null, // обозначение искомой величины (P/S/C/d/…) — только для показа lhsExpr: lhsExpr, rhsExpr: rhsExpr, // система: по умолчанию показываем сами уравнения; но если задан gen.display @@ -391,8 +392,12 @@ // шаг решения -> { note(текст), tex(подпись), latex(для KaTeX, null если не разобрался) } // строковый шаг (легаси) трактуется как чистая заметка без формулы. solution: (gen.solution || []).map(function (st) { + // обозначение искомой: подменяем переменную answerVar на answerSym (P/S/…) в формулах шагов + var aSym = (gen.answerSym && gen.answerSym !== answerVar) ? gen.answerSym : null; + var symRe = aSym ? new RegExp('\\b' + answerVar + '\\b', 'g') : null; + function subSym(s) { return symRe ? s.replace(symRe, aSym) : s; } if (typeof st === 'string') return { note: render(st, sEnv), tex: '', latex: null }; - var texSrc = st.tex ? render(st.tex, sEnv) : ''; + var texSrc = st.tex ? subSym(render(st.tex, sEnv)) : ''; return { note: st.note ? render(st.note, sEnv) : '', tex: texSrc ? prettyMath(texSrc) : '', diff --git a/frontend/js/trainer/generators.js b/frontend/js/trainer/generators.js index ce1954f..d8654f2 100644 --- a/frontend/js/trainer/generators.js +++ b/frontend/js/trainer/generators.js @@ -581,7 +581,7 @@ /* площадь прямоугольника */ { id: 'area-rect', topic: 'g-area', order: 1, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Площадь прямоугольника', + title: 'Площадь прямоугольника', answerSym: 'S', figure: { type: 'rectangle', w: 'a', h: 'b' }, figurePrompt: 'Найдите площадь прямоугольника.', pick: { a: [2, 16], b: [2, 16] }, derive: { val: 'a*b' }, @@ -596,7 +596,7 @@ /* площадь треугольника */ { id: 'area-triangle', topic: 'g-area', order: 2, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Площадь треугольника', + title: 'Площадь треугольника', answerSym: 'S', figure: { type: 'triangle-base-height', base: 'a', height: 'h' }, figurePrompt: 'Найдите площадь треугольника.', pick: { a: [2, 16], h: [2, 16] }, require: 'mod(a*h, 2) == 0', @@ -612,7 +612,7 @@ /* площадь квадрата */ { id: 'area-square', topic: 'g-area', order: 3, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Площадь квадрата', + title: 'Площадь квадрата', answerSym: 'S', figure: { type: 'square', a: 'a' }, figurePrompt: 'Найдите площадь квадрата.', pick: { a: [2, 20] }, derive: { val: 'a*a' }, @@ -740,7 +740,7 @@ /* площадь трапеции */ { id: 'area-trapezoid', topic: 'g-area', order: 4, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Площадь трапеции', + title: 'Площадь трапеции', answerSym: 'S', figure: { type: 'trapezoid', bottom: 'a', top: 'b', height: 'h' }, figurePrompt: 'Найдите площадь трапеции.', pick: { a: [2, 14], b: [2, 14], h: [2, 12] }, require: 'mod((a + b)*h, 2) == 0', @@ -756,7 +756,7 @@ /* площадь параллелограмма */ { id: 'area-parallelogram', topic: 'g-area', order: 5, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Площадь параллелограмма', + title: 'Площадь параллелограмма', answerSym: 'S', figure: { type: 'parallelogram', base: 'a', height: 'h' }, figurePrompt: 'Найдите площадь параллелограмма.', pick: { a: [2, 16], h: [2, 14] }, derive: { val: 'a*h' }, @@ -771,7 +771,7 @@ /* площадь ромба по диагоналям */ { id: 'area-rhombus', topic: 'g-area', order: 6, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Площадь ромба', + title: 'Площадь ромба', answerSym: 'S', figure: { type: 'rhombus', d1: 'd1', d2: 'd2' }, figurePrompt: 'Найдите площадь ромба.', pick: { d1: [2, 16], d2: [2, 16] }, require: 'mod(d1*d2, 2) == 0', @@ -837,7 +837,7 @@ /* периметр подобной фигуры */ { id: 'sim-perimeter', topic: 'g-sim', order: 2, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Периметр подобной фигуры', + title: 'Периметр подобной фигуры', answerSym: 'P', figure: { type: 'two-similar', perim: 'p', k: 'k', mode: 'perimeter' }, figurePrompt: 'Найдите периметр подобной фигуры (отмечена «?»).', pick: { p: [5, 30], k: [2, 5] }, derive: { val: 'p*k' }, @@ -1016,7 +1016,7 @@ /* длина окружности по радиусу: C = 2πr */ { id: 'circ-length', topic: 'g-circle', order: 1, subject: 'geometry', grade: 9, kind: 'compute', - title: 'Длина окружности', + title: 'Длина окружности', answerSym: 'C', figure: { type: 'circle', r: 'r', show: 'radius' }, figurePrompt: 'Найдите длину окружности (π ≈ 3,14).', pick: { r: [1, 20] }, derive: { val: '2*3.14*r' }, @@ -1031,7 +1031,7 @@ /* длина окружности по диаметру: C = πd */ { id: 'circ-diam', topic: 'g-circle', order: 2, subject: 'geometry', grade: 9, kind: 'compute', - title: 'Длина по диаметру', + title: 'Длина по диаметру', answerSym: 'C', figure: { type: 'circle', d: 'd', show: 'diameter' }, figurePrompt: 'Найдите длину окружности (π ≈ 3,14).', pick: { d: [2, 30] }, derive: { val: '3.14*d' }, @@ -1046,7 +1046,7 @@ /* площадь круга: S = πr² */ { id: 'circ-area', topic: 'g-circle', order: 3, subject: 'geometry', grade: 9, kind: 'compute', - title: 'Площадь круга', + title: 'Площадь круга', answerSym: 'S', figure: { type: 'circle', r: 'r', show: 'area' }, figurePrompt: 'Найдите площадь круга (π ≈ 3,14).', pick: { r: [1, 15] }, derive: { val: '3.14*r^2' }, @@ -1061,7 +1061,7 @@ /* длина дуги: L = (n/360)·2πr; n = 45·k, требуем конечную дробь (r·k чётно) */ { id: 'circ-arc', topic: 'g-circle', order: 4, subject: 'geometry', grade: 9, kind: 'compute', - title: 'Длина дуги', + title: 'Длина дуги', answerSym: 'L', figure: { type: 'circle-arc', r: 'r', angle: 'n' }, figurePrompt: 'Найдите длину дуги (π ≈ 3,14).', pick: { r: [3, 12], k: [1, 7] }, require: 'mod(r*k, 2) == 0', @@ -1972,7 +1972,7 @@ /* найти b по одному корню */ { id: 'quad-find-b', topic: 'quadratic', order: 7, subject: 'algebra', grade: 8, kind: 'compute', - title: 'Найти b по корню', + title: 'Найти b по корню', answerSym: 'b', pick: { r: [-6, 6], k: [-6, 6] }, constraint: 'r != 0 && k != 0 && k != r', derive: { c: 'r*k', b: '-(r + k)' }, lhs: 'x', rhs: '{b}', display: 'Один из корней уравнения x² + bx + {c} = 0 равен {r}. Найдите b.', @@ -2103,7 +2103,7 @@ /* найти разность d по двум членам */ { id: 'prog-arith-find-d', topic: 'progressions', order: 4, subject: 'algebra', grade: 9, kind: 'compute', - title: 'Найти разность d', + title: 'Найти разность d', answerSym: 'd', pick: { a: [-6, 10], d: [-5, 5], n: [4, 10] }, require: 'd != 0', derive: { an: 'a + (n - 1)*d' }, lhs: 'x', rhs: '({an} - {a})/({n} - 1)', display: 'В арифметической прогрессии a₁ = {a}, а член с номером {n} равен {an}. Найдите разность d.', @@ -2117,7 +2117,7 @@ /* найти номер члена n */ { id: 'prog-arith-find-n', topic: 'progressions', order: 5, subject: 'algebra', grade: 9, kind: 'compute', - title: 'Номер члена прогрессии', + title: 'Номер члена прогрессии', answerSym: 'n', pick: { a: [-6, 10], d: [-5, 5], n: [4, 12] }, require: 'd != 0', derive: { an: 'a + (n - 1)*d' }, lhs: 'x', rhs: '({an} - {a})/{d} + 1', display: 'В арифметической прогрессии a₁ = {a}, d = {d}. Каким по счёту является член, равный {an}?', @@ -2145,7 +2145,7 @@ /* найти знаменатель q */ { id: 'prog-geom-find-q', topic: 'progressions', order: 7, subject: 'algebra', grade: 9, kind: 'compute', - title: 'Найти знаменатель q', + title: 'Найти знаменатель q', answerSym: 'q', pick: { b: [1, 5], q: [2, 4] }, derive: { b2: 'b*q' }, lhs: 'x', rhs: '{b2}/{b}', display: 'Геометрическая прогрессия: b₁ = {b}, b₂ = {b2}. Найдите знаменатель q.', @@ -2593,7 +2593,7 @@ /* периметр прямоугольного треугольника */ { id: 'pyth-perimeter', topic: 'g-pyth', order: 3, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Периметр прям. треугольника', + title: 'Периметр прям. треугольника', answerSym: 'P', figure: { type: 'right-triangle', a: 'a', b: 'b', c: 'c' }, figurePrompt: 'Найдите периметр прямоугольного треугольника.', pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n', @@ -2610,7 +2610,7 @@ /* расстояние между точками */ { id: 'pyth-distance', topic: 'g-pyth', order: 4, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Расстояние между точками', + title: 'Расстояние между точками', answerSym: 'd', figure: { type: 'points-distance', x1: 'x1', y1: 'y1', x2: 'x2', y2: 'y2' }, figurePrompt: 'Найдите расстояние между точками A и B.', pick: { m: [2, 4], n: [1, 3], x1: [-4, 4], y1: [-4, 4] }, constraint: 'm > n', @@ -2626,7 +2626,7 @@ /* диагональ прямоугольника */ { id: 'pyth-rect-diagonal', topic: 'g-pyth', order: 5, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Диагональ прямоугольника', + title: 'Диагональ прямоугольника', answerSym: 'd', figure: { type: 'rectangle', w: 'a', h: 'b', diagonal: true }, figurePrompt: 'Найдите диагональ прямоугольника.', pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n', @@ -2642,7 +2642,7 @@ /* диагональ параллелепипеда (3D) */ { id: 'pyth-space-diagonal', topic: 'g-pyth', order: 6, subject: 'geometry', grade: 9, kind: 'compute', - title: 'Диагональ параллелепипеда', + title: 'Диагональ параллелепипеда', answerSym: 'd', figure: { type: 'space-diagonal-box', a: 'a', b: 'b', c: 'c' }, figurePrompt: 'Найдите диагональ параллелепипеда.', pick: { s: [1, 5] }, derive: { a: '1*s', b: '2*s', c: '2*s', val: '3*s' }, @@ -2674,7 +2674,7 @@ /* площадь L-образной фигуры */ { id: 'area-l-shape', topic: 'g-area', order: 8, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Площадь L-фигуры', + title: 'Площадь L-фигуры', answerSym: 'S', figure: { type: 'l-shape', W: 'W', H: 'H', cw: 'cw', ch: 'ch' }, figurePrompt: 'Найдите площадь фигуры (все углы прямые).', pick: { W: [6, 14], H: [5, 12], cw: [1, 10], ch: [1, 10] }, constraint: 'cw < W - 1 && ch < H - 1', @@ -2690,7 +2690,7 @@ /* площадь сектора */ { id: 'area-sector', topic: 'g-area', order: 9, subject: 'geometry', grade: 9, kind: 'compute', - title: 'Площадь сектора', + title: 'Площадь сектора', answerSym: 'S', figure: { type: 'circle-arc', r: 'r', angle: 'n' }, figurePrompt: 'Найдите площадь сектора (π ≈ 3,14).', pick: { r: [2, 12], k2: [1, 3] }, require: 'mod(k2*r*r, 4) == 0', @@ -2723,7 +2723,7 @@ /* число сторон по углу (обратная) */ { id: 'poly-find-n', topic: 'g-poly', order: 4, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Число сторон по углу', + title: 'Число сторон по углу', answerSym: 'n', figure: { type: 'regular-polygon', n: 'n', markAngle: true }, figurePrompt: 'Сколько сторон у многоугольника?', pick: { n: [3, 12] }, require: 'mod(180*(n - 2), n) == 0', @@ -2757,7 +2757,7 @@ /* коэффициент подобия по сторонам */ { id: 'sim-scale-factor', topic: 'g-sim', order: 3, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Коэффициент по сторонам', + title: 'Коэффициент по сторонам', answerSym: 'k', figure: { type: 'two-similar', side: 'a', side2: 'b', k: 'k', mode: 'side', hideK: true }, figurePrompt: 'Найдите коэффициент подобия (большего к меньшему).', pick: { a: [2, 12], k: [2, 5] }, derive: { b: 'a*k', val: 'k' }, @@ -2772,7 +2772,7 @@ /* отношение площадей подобных = k² */ { id: 'sim-area-ratio', topic: 'g-sim', order: 4, subject: 'geometry', grade: 9, kind: 'compute', - title: 'Отношение площадей (k²)', + title: 'Отношение площадей (k²)', answerSym: 'S', figure: { type: 'two-similar', area: 'S', k: 'k', mode: 'area' }, figurePrompt: 'Найдите площадь второй фигуры.', pick: { k: [2, 4], S: [2, 20] }, derive: { val: 'S*k*k' }, @@ -2832,7 +2832,7 @@ /* длина хорды через радиус */ { id: 'circ-chord-pyth', topic: 'g-circle', order: 6, subject: 'geometry', grade: 9, kind: 'compute', - title: 'Длина хорды', + title: 'Длина хорды', answerSym: 'L', figure: { type: 'chord-circle', r: 'r', dd: 'dd' }, figurePrompt: 'Найдите длину хорды.', pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n', @@ -2849,7 +2849,7 @@ /* длина касательной */ { id: 'circ-tangent-len', topic: 'g-circle', order: 7, subject: 'geometry', grade: 9, kind: 'compute', - title: 'Длина касательной', + title: 'Длина касательной', answerSym: 'L', figure: { type: 'tangent-circle', r: 'r', op: 'op' }, figurePrompt: 'Найдите длину касательной.', pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n', diff --git a/frontend/trainer.html b/frontend/trainer.html index e28c8b2..c0573c4 100644 --- a/frontend/trainer.html +++ b/frontend/trainer.html @@ -924,7 +924,11 @@ function applyInputMode() { var k = cur && cur.kind; var multi = (k === 'roots' || k === 'simplify' || k === 'inequality' || k === 'system'); - var eqx = $('tr-eqx'); if (eqx) eqx.style.display = multi ? 'none' : ''; + var eqx = $('tr-eqx'); + if (eqx) { + eqx.style.display = multi ? 'none' : ''; + eqx.textContent = ((cur && (cur.answerSym || cur.answerVar)) || 'x') + ' ='; // P =/S =/x = по смыслу задачи + } $('tr-input').placeholder = (k === 'roots') ? 'корни через ;' : (k === 'simplify') ? 'упрощённое выражение' : (k === 'inequality') ? ('напр. ' + (cur.answerVar || 'x') + ' < 3') @@ -941,7 +945,7 @@ if (cur.kind === 'simplify') return '= ' + (cur.answerExpr ? fmt(cur.answerExpr) : ''); if (cur.kind === 'inequality' && cur.answerRel) return (cur.answerVar || 'x') + ' ' + (REL_SYM[cur.answerRel.op] || cur.answerRel.op) + ' ' + fmt(cur.answerRel.bound); if (cur.kind === 'system' && cur.pair) return (cur.answerVars || ['x', 'y']).map(function (v) { return v + ' = ' + fmt(cur.pair[v]); }).join(', '); - return 'x = ' + fmt(cur.answer); + return ((cur.answerSym || cur.answerVar) || 'x') + ' = ' + fmt(cur.answer); } function isLabelKind() { return cur.kind === 'roots' || cur.kind === 'simplify' || cur.kind === 'inequality' || cur.kind === 'system'; } function updateStats() { $('tr-solved').textContent = solved; $('tr-streak').textContent = streak; }