diff --git a/frontend/js/trainer/_trainer_engine.js b/frontend/js/trainer/_trainer_engine.js index 1b205d6..f362901 100644 --- a/frontend/js/trainer/_trainer_engine.js +++ b/frontend/js/trainer/_trainer_engine.js @@ -330,6 +330,30 @@ answer = pair[avs[0]]; // запасной одиночный ответ } + // ── новые форматы условий (Ф C1) ── + // choice — выбор из вариантов; verify — верно/неверно; estimate — ответ в допуске + var choices = null, tol = null; + if (kind === 'choice') { + var corr = answer; + var pool = [corr]; + (gen.distractors || []).forEach(function (d) { + var v = evalExpr(d, env); + if (gen.integerAnswer) v = Math.round(v); + if (isFinite(v) && pool.every(function (x) { return Math.abs(x - v) > 1e-9; })) pool.push(v); + }); + var opts = pool.slice(0, 4); // правильный + до 3 дистракторов + for (var oi = opts.length - 1; oi > 0; oi--) { // детерминированное перемешивание (rng) + var oj = Math.floor(rng() * (oi + 1)), tmp = opts[oi]; opts[oi] = opts[oj]; opts[oj] = tmp; + } + choices = opts.map(function (v) { return { label: fmtNum(v), correct: Math.abs(v - corr) < 1e-9 }; }); + } else if (kind === 'verify') { + var claimTrue = truthy(evalExpr(gen.claim, env)); + answer = claimTrue ? 1 : 0; + choices = [{ label: 'Верно', correct: claimTrue }, { label: 'Неверно', correct: !claimTrue }]; + } else if (kind === 'estimate') { + tol = (gen.tol != null) ? Math.abs(evalExpr(String(gen.tol), env)) : Math.max(1, Math.abs(answer) * 0.05); + } + var lhsExpr = render(gen.lhs || 'x', env); var rhsExpr = render(gen.rhs || 'x', env); var sEnv = assign(env, { ans: answer }); @@ -372,6 +396,8 @@ figure: gen.figure || null, // спека чертежа (данные) — рисует TrainerFigures по params figurePrompt: gen.figurePrompt || null, // краткое условие для режима «читать с чертежа» answerSym: gen.answerSym || null, // обозначение искомой величины (P/S/C/d/…) — только для показа + choices: choices, // [{label, correct}] для kind choice/verify + tol: tol, // допуск для kind estimate lhsExpr: lhsExpr, rhsExpr: rhsExpr, // система: по умолчанию показываем сами уравнения; но если задан gen.display @@ -418,6 +444,12 @@ okSelf = _origIneqHolds(lhsExpr, rhsExpr, gen.dispOp || '<', answerVar, inside) && !_origIneqHolds(lhsExpr, rhsExpr, gen.dispOp || '<', answerVar, outside); why = 'неравенство не согласовано с ответом'; + } else if (kind === 'choice') { + okSelf = !!choices && choices.length >= 2 && choices.filter(function (c) { return c.correct; }).length === 1; + why = 'некорректный набор вариантов выбора'; + } else if (kind === 'verify') { + okSelf = !!choices && choices.length === 2; + why = 'verify без вариантов'; } else if (kind === 'system') { okSelf = !!(system && system.length) && system.every(function (e) { var L = evalExpr(e.lhs, pair), R = evalExpr(e.rhs, pair); @@ -469,6 +501,7 @@ if (problem.kind === 'roots') return _checkMultiRoot(problem, raw); if (problem.kind === 'inequality') return _checkInequality(problem, raw); if (problem.kind === 'system') return _checkSystem(problem, raw); + if (problem.kind === 'estimate') return _checkEstimate(problem, raw); var c = SE().compile(raw); if (c.error) { @@ -582,6 +615,17 @@ return { ok: true, reason: null, message: 'Верно!' }; } + /* Оценка/прикидка: ответ верен, если попадает в допуск tol вокруг истинного значения. */ + function _checkEstimate(problem, raw) { + var c = SE().compile(raw); + if (c.error) return { ok: false, reason: 'parse', value: null, message: 'Не понял ответ: ' + c.error }; + var val = c.fn({}); + if (!isFinite(val)) return { ok: false, reason: 'nan', value: val, message: 'Это не число.' }; + var tol = (typeof problem.tol === 'number' && isFinite(problem.tol)) ? problem.tol : 0; + var ok = Math.abs(val - problem.answer) <= tol + 1e-9; + return { ok: ok, reason: ok ? null : 'wrong', value: val, message: ok ? 'Верно (в допуске)!' : 'Пока неверно.' }; + } + /* Упрощение: ответ-выражение проверяем на эквивалентность сэмплингом. */ function _checkEquiv(problem, raw) { var c = SE().compile(raw); diff --git a/frontend/js/trainer/generators.js b/frontend/js/trainer/generators.js index 0e5551f..cbe1b4f 100644 --- a/frontend/js/trainer/generators.js +++ b/frontend/js/trainer/generators.js @@ -547,7 +547,7 @@ /* гипотенуза по катетам (пифагорова тройка m,n) */ { id: 'pyth-hyp', topic: 'g-pyth', order: 1, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Гипотенуза (Пифагор)', + title: 'Гипотенуза (Пифагор)', answerSym: 'c', figure: { type: 'right-triangle', a: 'a', b: 'b', c: 'c', unknown: 'c' }, figurePrompt: 'Найдите гипотенузу прямоугольного треугольника (отмечена «?»).', pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n', @@ -563,7 +563,7 @@ /* катет по гипотенузе и катету */ { id: 'pyth-leg', topic: 'g-pyth', order: 2, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Катет (Пифагор)', + title: 'Катет (Пифагор)', answerSym: 'b', figure: { type: 'right-triangle', a: 'a', b: 'b', c: 'c', unknown: 'b' }, figurePrompt: 'Найдите неизвестный катет (отмечен «?»).', pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n', @@ -2088,7 +2088,7 @@ /* сумма n членов арифметической прогрессии */ { id: 'prog-arith-sum', topic: 'progressions', order: 3, subject: 'algebra', grade: 9, kind: 'compute', - title: 'Сумма арифм. прогрессии', + title: 'Сумма арифм. прогрессии', answerSym: 'S', pick: { a: [-8, 12], d: [-6, 6], n: [4, 12] }, require: 'd != 0', derive: { an: 'a + (n - 1)*d', sum: 'n*(a + an)/2' }, lhs: 'x', rhs: '{n}*({a} + {an})/2', display: 'Арифметическая прогрессия: a₁ = {a}, d = {d}. Найдите сумму первых {n} членов.', @@ -2173,7 +2173,7 @@ /* сумма n членов геометрической прогрессии */ { id: 'prog-geom-sum', topic: 'progressions', order: 9, subject: 'algebra', grade: 9, kind: 'compute', - title: 'Сумма геом. прогрессии', + title: 'Сумма геом. прогрессии', answerSym: 'S', pick: { b: [1, 4], q: [2, 3], n: [2, 5] }, derive: { qn: 'q^n', sum: 'b*(q^n - 1)/(q - 1)' }, lhs: 'x', rhs: '{b}*({q}^{n} - 1)/({q} - 1)', display: 'Геометрическая прогрессия: b₁ = {b}, q = {q}. Найдите сумму первых {n} членов.', @@ -2187,7 +2187,7 @@ /* текстовая задача (ряды кресел) */ { id: 'prog-arith-word', topic: 'progressions', order: 10, subject: 'algebra', grade: 9, kind: 'compute', - title: 'Задача (ряды кресел)', + title: 'Задача (ряды кресел)', answerSym: 'S', pick: { a: [10, 20], d: [2, 5], n: [5, 12] }, derive: { an: 'a + (n - 1)*d', sum: 'n*(a + an)/2' }, lhs: 'x', rhs: '{n}*({a} + {an})/2', display: 'В первом ряду зала {a} кресел, в каждом следующем на {d} больше. Сколько всего кресел в {n} рядах?', @@ -2659,7 +2659,7 @@ /* найти сторону по площади (обратная) */ { id: 'area-rect-inverse', topic: 'g-area', order: 7, subject: 'geometry', grade: 8, kind: 'compute', - title: 'Сторона по площади', + title: 'Сторона по площади', answerSym: 'b', figure: { type: 'rectangle', w: 'a', h: 'b', unknown: 'h', area: 'S' }, figurePrompt: 'Найдите неизвестную сторону прямоугольника.', pick: { a: [2, 16], b: [2, 16] }, derive: { S: 'a*b', val: 'b' }, @@ -3036,6 +3036,153 @@ { note: 'Циферблат — 360°, каждый час — 30°. В {H}:00 между стрелками {ang0}°.', tex: '' }, { note: 'Берём меньший угол (не больше 180°).', tex: 'x = {ans}' } ] + }, + + /* ═══════════════════════════════════════════════════════════════════════ + V4 C1 — новые форматы условий: choice (выбор), verify (верно/неверно), + estimate (прикидка в допуске). Разнообразие подачи во всех темах. + ═══════════════════════════════════════════════════════════════════════ */ + + /* ── Выбор ответа ── */ + + /* площадь прямоугольника — выбор */ + { + id: 'ch-area-rect', topic: 'g-area', order: 10, subject: 'geometry', grade: 8, kind: 'choice', + title: 'Площадь — выбор ответа', + figure: { type: 'rectangle', w: 'a', h: 'b' }, + pick: { a: [3, 16], b: [3, 16] }, constraint: 'a != b', + derive: { val: 'a*b' }, answer: 'val', distractors: ['2*(a + b)', 'a + b', '2*a*b'], integerAnswer: true, + display: 'Чему равна площадь прямоугольника со сторонами {a} и {b}? Выберите ответ.', + solution: [ + { note: 'Площадь прямоугольника — произведение сторон (а не периметр!).', tex: 'S = {a} * {b}' }, + { note: 'Считаем.', tex: 'S = {val}' } + ] + }, + + /* корень линейного уравнения — выбор */ + { + id: 'ch-lin-basic', topic: 'linear-eq', order: 15, subject: 'algebra', grade: 7, kind: 'choice', + title: 'Корень уравнения — выбор', + pick: { a: [2, 9], b: [1, 20], root: [-9, 9] }, require: 'root != 0', + derive: { c: 'a*root + b' }, answer: 'root', distractors: ['-root', 'c - b', 'root + a'], integerAnswer: true, + display: 'Чему равен корень уравнения {a}x + {b} = {c}? Выберите ответ.', + solution: [ + { note: 'Переносим {b} вправо и делим на {a}.', tex: 'x = ({c} - {b}) / {a}' }, + { note: 'Получаем корень.', tex: 'x = {root}' } + ] + }, + + /* процент от числа — выбор */ + { + id: 'ch-pct-of', topic: 'percents', order: 10, subject: 'algebra', grade: 6, kind: 'choice', + title: 'Проценты — выбор ответа', + pick: { pidx: [2, 10], abase: [1, 15] }, + derive: { p: 'pidx*5', a: 'abase*20', val: 'pidx*abase' }, answer: 'val', + distractors: ['p', 'val*2', 'a - val'], integerAnswer: true, + display: 'Сколько будет {p}% от числа {a}? Выберите ответ.', + solution: [ + { note: 'Процент — сотая доля: умножаем число на {p} и делим на 100.', tex: 'x = {a} * {p} / 100' }, + { note: 'Считаем.', tex: 'x = {val}' } + ] + }, + + /* гипотенуза — выбор */ + { + id: 'ch-pyth-hyp', topic: 'g-pyth', order: 7, subject: 'geometry', grade: 8, kind: 'choice', + title: 'Гипотенуза — выбор ответа', + figure: { type: 'right-triangle', a: 'a', b: 'b', c: 'c', unknown: 'c' }, + pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n', + derive: { a: 'm*m - n*n', b: '2*m*n', c: 'm*m + n*n' }, answer: 'c', + distractors: ['a + b', 'c - 1', 'c + 2'], integerAnswer: true, + display: 'Чему равна гипотенуза прямоугольного треугольника с катетами {a} и {b}? Выберите ответ.', + solution: [ + { note: 'По теореме Пифагора c = √(a² + b²) (не сумма катетов!).', tex: 'c = sqrt({a}^2 + {b}^2)' }, + { note: 'Считаем.', tex: 'c = {c}' } + ] + }, + + /* ── Верно / неверно ── */ + + /* сравнение дробей — верно/неверно */ + { + id: 'vf-frac-compare', topic: 'fractions', order: 9, subject: 'algebra', grade: 6, kind: 'verify', + title: 'Сравнение дробей (верно?)', + pick: { a: [1, 7], b: [2, 9], c: [1, 7], d: [2, 9] }, constraint: 'a < b && c < d && a*d != c*b', + claim: 'a*d > c*b', + display: 'Верно ли, что {a}/{b} > {c}/{d}?', + solution: [ + { note: 'Сравниваем перекрёстные произведения: {a}·{d} и {c}·{b}.', tex: '' }, + { note: 'Первая дробь больше, если {a}·{d} больше {c}·{b}.', tex: '' } + ] + }, + + /* делимость — верно/неверно */ + { + id: 'vf-divisible', topic: 'gcd-lcm', order: 7, subject: 'algebra', grade: 5, kind: 'verify', + title: 'Делимость (верно?)', + pick: { N: [10, 99], k: [2, 9] }, derive: { rem: 'mod(N, k)' }, + claim: 'mod(N, k) == 0', + display: 'Верно ли, что {N} делится нацело на {k}?', + solution: [ + { note: 'Остаток от деления {N} на {k} равен {rem}.', tex: '' }, + { note: 'Делится нацело только если остаток равен нулю.', tex: '' } + ] + }, + + /* прямоугольный ли треугольник — верно/неверно (обратная т. Пифагора) */ + { + id: 'vf-pyth', topic: 'g-pyth', order: 8, subject: 'geometry', grade: 8, kind: 'verify', + title: 'Прямоугольный ли? (верно?)', + pick: { m: [2, 5], n: [1, 4], yes: [0, 1] }, constraint: 'm > n', + derive: { a: 'm*m - n*n', b: '2*m*n', c: 'yes*(m*m + n*n) + (1 - yes)*(m*m + n*n + 1)', sumSq: '(m*m - n*n)^2 + (2*m*n)^2', cSq: '(yes*(m*m + n*n) + (1 - yes)*(m*m + n*n + 1))^2' }, + claim: 'a*a + b*b == c*c', + display: 'Верно ли, что треугольник со сторонами {a}, {b} и {c} прямоугольный?', + solution: [ + { note: 'Проверяем обратную теорему Пифагора: {a}² + {b}² должно равняться {c}².', tex: '' }, + { note: '{a}² + {b}² = {sumSq}, а {c}² = {cSq}.', tex: '' } + ] + }, + + /* проверка корня — верно/неверно */ + { + id: 'vf-eq-root', topic: 'linear-eq', order: 16, subject: 'algebra', grade: 7, kind: 'verify', + title: 'Корень ли это? (верно?)', + pick: { a: [2, 6], b: [1, 12], r: [-6, 6], yes: [0, 1] }, require: 'r != 0', + derive: { c: 'yes*(a*r + b) + (1 - yes)*(a*r + b + 1)', lhsval: 'a*r + b' }, + claim: 'a*r + b == c', + display: 'Верно ли, что x = {r} — корень уравнения {a}x + {b} = {c}?', + solution: [ + { note: 'Подставим x = {r}: {a}·({r}) + {b} = {lhsval}.', tex: '' }, + { note: 'Сравниваем с правой частью {c}.', tex: '' } + ] + }, + + /* ── Прикидка (ответ в допуске) ── */ + + /* прикидка произведения */ + { + id: 'est-product', topic: 'applied', order: 12, subject: 'algebra', grade: 6, kind: 'estimate', + title: 'Прикидка произведения', + pick: { a: [12, 89], b: [12, 89] }, derive: { val: 'a*b' }, tol: 'a*b*0.12', + lhs: 'x', rhs: '{a}*{b}', display: 'Оцените (приближённо) произведение {a} · {b}. Допускается близкий ответ.', + answerVar: 'x', answer: 'val', + solution: [ + { note: 'Округлим множители до десятков и перемножим. Точное значение:', tex: 'x = {a} * {b}' }, + { note: 'Получаем.', tex: 'x = {val}' } + ] + }, + + /* прикидка процента */ + { + id: 'est-percent', topic: 'percents', order: 11, subject: 'algebra', grade: 6, kind: 'estimate', + title: 'Прикидка процента', + pick: { p: [5, 95], a: [20, 400] }, derive: { val: 'p*a/100' }, tol: '(p*a/100)*0.12 + 1', + lhs: 'x', rhs: '{p}*{a}/100', display: 'Оцените (приближённо) {p}% от {a}. Допускается близкий ответ.', + answerVar: 'x', answer: 'val', + solution: [ + { note: 'Точное значение {p}% от {a}:', tex: 'x = {p} * {a} / 100' }, + { note: 'Подойдёт близкая прикидка.', tex: 'x = {val}' } + ] } ]; @@ -3110,6 +3257,10 @@ 'ang-alt-exterior': 2, 'ang-coint-exterior': 2, 'ang-parallel-twostep': 3, 'ang-alt-solve': 3, 'ang-bisector': 1, 'ang-complementary': 1, 'ang-right-acute': 1, 'ang-parallelogram': 2, 'ang-polygon-missing': 3, 'ang-triangle-ratio': 3, 'ang-clock': 2, + // V4 C1 — новые форматы (выбор/верно-неверно/прикидка) + 'ch-area-rect': 1, 'ch-lin-basic': 1, 'ch-pct-of': 2, 'ch-pyth-hyp': 2, + 'vf-frac-compare': 2, 'vf-divisible': 1, 'vf-pyth': 3, 'vf-eq-root': 2, + 'est-product': 2, 'est-percent': 2, 'pyth-perimeter': 3, 'pyth-distance': 3, 'pyth-rect-diagonal': 2, 'pyth-space-diagonal': 3, 'area-rect-inverse': 2, 'area-l-shape': 3, 'area-sector': 3, 'poly-diagonals': 2, 'poly-find-n': 3, 'poly-exterior-sum': 2, diff --git a/frontend/trainer.html b/frontend/trainer.html index c0573c4..327b6f7 100644 --- a/frontend/trainer.html +++ b/frontend/trainer.html @@ -237,6 +237,15 @@ .tr-fm-btn:hover { border-color: var(--g1); color: var(--accent-ink); } .tr-fm-btn.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 8px 16px -6px rgba(99,102,241,.5); } + /* ── варианты выбора (kind choice / verify) ── */ + .tr-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; max-width: 460px; margin: 0 auto 4px; } + .tr-choice-btn { font: inherit; font-size: 1.15rem; font-weight: 700; cursor: pointer; padding: 14px 16px; border-radius: 15px; border: 2px solid rgba(99,102,241,.22); background: #fbfbff; color: var(--ink); transition: .14s var(--ease); } + .tr-choice-btn:hover:not(:disabled) { border-color: var(--g1); background: #fff; transform: translateY(-1px); } + .tr-choice-btn:disabled { cursor: default; } + .tr-choice-btn.right { border-color: transparent; color: #fff; background: linear-gradient(135deg, #059669, #10b981); } + .tr-choice-btn.wrongpick { border-color: transparent; color: #fff; background: linear-gradient(135deg, #dc2626, #ef4444); } + #tr-choice-next { display: block; margin: 0 auto 4px; } + /* строка ответа */ .tr-inrow { display: flex; gap: 10px; align-items: stretch; max-width: 460px; margin: 0 auto; } #tr-eqx { font-family: 'Cambria Math', serif; font-size: 1.55rem; font-weight: 600; color: var(--accent-ink); align-self: center; padding-left: 4px; } @@ -459,12 +468,14 @@