feat(trainer): P10 — контент 8 класса (степени, формулы, неравенства)
- новый тип kind:inequality: answerRel{op,bound}, парсер отношения (_parseRel/_checkInequality) — нормализация «x op c», приём обратной записи, сверка op+границы; self-check внутри/снаружи решения
- темы: Степени (aⁿ, xᵃ·xᵇ, (xᵃ)ᵇ), Формулы сокр. умножения (квадрат суммы/разности, разность квадратов), Неравенства (вкл. смену знака при делении на отрицательное) → 26 генераторов, 8 тем
- движок: simplify рендерит выражение в KaTeX (exprToLatex(srcExpr)); неравенство — в KaTeX с отношением; fallback-display учитывает op
- страница: ввод/лейбл для неравенств, isLabelKind
- смоук движка 397/397 (T15 неравенства, T16 степени/формулы; T3 ≥10 для малых пространств), страница 33/33; ROADMAP_V2 P10 → DONE
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -276,12 +276,19 @@
|
||||
var lhsExpr = render(gen.lhs || 'x', env);
|
||||
var rhsExpr = render(gen.rhs || 'x', env);
|
||||
var sEnv = assign(env, { ans: answer });
|
||||
// latex уравнения строим только для уравнений (solve/roots); compute/simplify —
|
||||
// текстовый prompt из display.
|
||||
var showEq = (kind === 'solve' || kind === 'roots');
|
||||
var ll = showEq ? exprToLatex(lhsExpr) : null;
|
||||
var rl = showEq ? exprToLatex(rhsExpr) : null;
|
||||
var answerExpr = gen.answerExpr ? render(gen.answerExpr, env) : null;
|
||||
var answerRel = (kind === 'inequality') ? { op: gen.relOp || '<', bound: evalExpr(gen.bound, env) } : null;
|
||||
// latex: уравнение (solve/roots) | выражение (simplify) | неравенство (inequality)
|
||||
// | null (compute → текстовый prompt из display).
|
||||
var latex = null;
|
||||
if (kind === 'solve' || kind === 'roots') {
|
||||
var ll = exprToLatex(lhsExpr), rl = exprToLatex(rhsExpr);
|
||||
if (ll != null && rl != null) latex = ll + ' = ' + rl;
|
||||
} else if (kind === 'simplify' && gen.srcExpr) {
|
||||
latex = exprToLatex(render(gen.srcExpr, env));
|
||||
} else if (kind === 'inequality') {
|
||||
latex = exprToLatex(lhsExpr + ' ' + (gen.dispOp || '<') + ' ' + rhsExpr);
|
||||
}
|
||||
|
||||
var problem = {
|
||||
genId: gen.id,
|
||||
@@ -290,12 +297,13 @@
|
||||
kind: kind,
|
||||
lhsExpr: lhsExpr,
|
||||
rhsExpr: rhsExpr,
|
||||
display: prettyMath(render(gen.display || (gen.lhs + ' = ' + gen.rhs), env)),
|
||||
latex: (ll != null && rl != null) ? (ll + ' = ' + rl) : null,
|
||||
display: 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)
|
||||
answerVars: gen.answerVars || [answerVar],
|
||||
params: env,
|
||||
// шаг решения -> { note(текст), tex(подпись), latex(для KaTeX, null если не разобрался) }
|
||||
@@ -316,6 +324,13 @@
|
||||
if (kind === 'simplify') {
|
||||
okSelf = _sampleEquiv(render(gen.srcExpr || gen.lhs || 'x', env), answerExpr, problem.answerVars).ok;
|
||||
why = 'упрощение не эквивалентно ответу';
|
||||
} else if (kind === 'inequality') {
|
||||
var bnd = answerRel.bound, iop = answerRel.op;
|
||||
var inside = (iop === '<' || iop === '<=') ? bnd - 1 : bnd + 1;
|
||||
var outside = (iop === '<' || iop === '<=') ? bnd + 1 : bnd - 1;
|
||||
okSelf = _origIneqHolds(lhsExpr, rhsExpr, gen.dispOp || '<', answerVar, inside) &&
|
||||
!_origIneqHolds(lhsExpr, rhsExpr, gen.dispOp || '<', answerVar, outside);
|
||||
why = 'неравенство не согласовано с ответом';
|
||||
} else if (answers) {
|
||||
okSelf = answers.every(function (r) { return verifyRoot(problem, r).ok; });
|
||||
why = 'не все корни удовлетворяют уравнению';
|
||||
@@ -357,8 +372,9 @@
|
||||
var raw = String(input == null ? '' : input).trim();
|
||||
if (!raw) return { ok: false, reason: 'empty', value: null, residual: null, message: 'Введите ответ.' };
|
||||
|
||||
if (problem.kind === 'simplify') return _checkEquiv(problem, raw);
|
||||
if (problem.kind === 'roots') return _checkMultiRoot(problem, raw);
|
||||
if (problem.kind === 'simplify') return _checkEquiv(problem, raw);
|
||||
if (problem.kind === 'roots') return _checkMultiRoot(problem, raw);
|
||||
if (problem.kind === 'inequality') return _checkInequality(problem, raw);
|
||||
|
||||
var c = SE().compile(raw);
|
||||
if (c.error) {
|
||||
@@ -413,6 +429,40 @@
|
||||
return { ok: se.ok, reason: se.ok ? null : (se.reason || 'wrong'), value: raw, message: se.ok ? 'Верно!' : 'Пока неверно.' };
|
||||
}
|
||||
|
||||
/* ── Неравенства: проверка ответа-отношения «x < c» ──
|
||||
Парсим отношение ученика, нормализуем к виду «x op c» (переменная слева;
|
||||
если справа — отношение переворачивается), сравниваем op и границу. */
|
||||
function _origIneqHolds(lhsExpr, rhsExpr, op, v, xv) {
|
||||
var env = {}; env[v] = xv;
|
||||
var L = evalExpr(lhsExpr, env), R = evalExpr(rhsExpr, env);
|
||||
switch (op) {
|
||||
case '<': return L < R; case '>': return L > R;
|
||||
case '<=': return L <= R; case '>=': return L >= R;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function _parseRel(raw, v) {
|
||||
var s = String(raw).replace(/≤/g, '<=').replace(/≥/g, '>=').replace(/\s+/g, '');
|
||||
var m = s.match(/<=|>=|<|>/);
|
||||
if (!m) return null;
|
||||
var op = m[0], left = s.slice(0, m.index), right = s.slice(m.index + op.length);
|
||||
if (!left || !right) return null;
|
||||
var cl = SE().compile(left), cr = SE().compile(right);
|
||||
if (cl.error || cr.error) return null;
|
||||
var flip = { '<': '>', '>': '<', '<=': '>=', '>=': '<=' };
|
||||
if (left === v && right !== v && _isConst(cr, v)) { var b = cr.fn({}); return isFinite(b) ? { op: op, bound: b } : null; }
|
||||
if (right === v && left !== v && _isConst(cl, v)) { var b2 = cl.fn({}); return isFinite(b2) ? { op: flip[op], bound: b2 } : null; }
|
||||
return null;
|
||||
}
|
||||
function _checkInequality(problem, raw) {
|
||||
var v = problem.answerVar || 'x';
|
||||
var rel = _parseRel(raw, v);
|
||||
if (!rel) return { ok: false, reason: 'parse', message: 'Ответ — неравенство, напр. ' + v + ' < 3.' };
|
||||
var want = problem.answerRel || {};
|
||||
var ok = rel.op === want.op && Math.abs(rel.bound - want.bound) <= 1e-6 * Math.max(1, Math.abs(want.bound));
|
||||
return { ok: ok, reason: ok ? null : 'wrong', value: raw, message: ok ? 'Верно!' : 'Пока неверно.' };
|
||||
}
|
||||
|
||||
/* ── Пошаговое решение (репетитор): проверка одного шага-равенства ──
|
||||
Шаг = равносильное уравнение (то же множество корней). Идея без решения
|
||||
уравнений: уравнение L=R равносильно исходному ⟺ выполняется во ВСЕХ корнях
|
||||
|
||||
@@ -25,7 +25,10 @@
|
||||
{ key: 'proportions', label: 'Пропорции', subject: 'algebra', grade: 7, order: 2 },
|
||||
{ key: 'percents', label: 'Проценты', subject: 'algebra', grade: 7, order: 3 },
|
||||
{ key: 'simplify', label: 'Упрощение', subject: 'algebra', grade: 7, order: 4 },
|
||||
{ key: 'quadratic', label: 'Квадратные', subject: 'algebra', grade: 8, order: 5 }
|
||||
{ key: 'quadratic', label: 'Квадратные', subject: 'algebra', grade: 8, order: 5 },
|
||||
{ key: 'powers', label: 'Степени', subject: 'algebra', grade: 8, order: 6 },
|
||||
{ key: 'formulas', label: 'Формулы', subject: 'algebra', grade: 8, order: 7 },
|
||||
{ key: 'inequalities', label: 'Неравенства', subject: 'algebra', grade: 8, order: 8 }
|
||||
];
|
||||
|
||||
var GENERATORS = [
|
||||
@@ -312,6 +315,133 @@
|
||||
{ note: 'Первый корень:', tex: 'x = {a}' },
|
||||
{ note: 'Второй корень:', tex: 'x = -{a}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* ═══ Тема: Степени ═══ */
|
||||
|
||||
/* вычислить aⁿ */
|
||||
{
|
||||
id: 'pow-eval', topic: 'powers', order: 1, subject: 'algebra', grade: 8, kind: 'compute',
|
||||
title: 'Вычислить степень',
|
||||
pick: { a: [2, 6], n: [2, 3] },
|
||||
derive: { val: 'a^n' },
|
||||
lhs: 'x', rhs: '{a}^{n}', display: 'Вычислите {a} в степени {n}',
|
||||
answerVar: 'x', answer: 'val', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Степень — это повторное умножение основания на себя.', tex: 'x = {a}^{n}' },
|
||||
{ note: 'Считаем:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* xᵃ · xᵇ = xᵃ⁺ᵇ */
|
||||
{
|
||||
id: 'pow-mult', topic: 'powers', order: 2, subject: 'algebra', grade: 8, kind: 'simplify',
|
||||
title: 'Произведение степеней',
|
||||
pick: { a: [2, 5], b: [2, 5] },
|
||||
derive: { s: 'a + b' },
|
||||
srcExpr: 'x^{a}*x^{b}', answerExpr: 'x^{s}', answerVars: ['x'],
|
||||
display: 'Упростите: x^{a}·x^{b}',
|
||||
solution: [
|
||||
{ note: 'При умножении степеней с одинаковым основанием показатели складываются: {a} + {b} = {s}.', tex: 'x^{a}*x^{b} = x^{s}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* (xᵃ)ᵇ = xᵃᵇ */
|
||||
{
|
||||
id: 'pow-pow', topic: 'powers', order: 3, subject: 'algebra', grade: 8, kind: 'simplify',
|
||||
title: 'Степень степени',
|
||||
pick: { a: [2, 4], b: [2, 3] },
|
||||
derive: { ab: 'a*b' },
|
||||
srcExpr: '(x^{a})^{b}', answerExpr: 'x^{ab}', answerVars: ['x'],
|
||||
display: 'Упростите: (x^{a})^{b}',
|
||||
solution: [
|
||||
{ note: 'При возведении степени в степень показатели перемножаются: {a}·{b} = {ab}.', tex: '(x^{a})^{b} = x^{ab}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* ═══ Тема: Формулы сокращённого умножения ═══ */
|
||||
|
||||
/* (x + a)² */
|
||||
{
|
||||
id: 'sq-sum', topic: 'formulas', order: 1, subject: 'algebra', grade: 8, kind: 'simplify',
|
||||
title: 'Квадрат суммы',
|
||||
pick: { a: [1, 9] },
|
||||
derive: { a2: 'a*a', a2x: '2*a' },
|
||||
srcExpr: '(x + {a})^2', answerExpr: 'x^2 + {a2x}*x + {a2}', answerVars: ['x'],
|
||||
display: 'Раскройте: (x + {a})²',
|
||||
solution: [
|
||||
{ note: 'Квадрат суммы: (x + {a})² = x² + 2·{a}·x + {a}².', tex: '(x + {a})^2 = x^2 + {a2x}*x + {a2}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* (x − a)² */
|
||||
{
|
||||
id: 'sq-diff', topic: 'formulas', order: 2, subject: 'algebra', grade: 8, kind: 'simplify',
|
||||
title: 'Квадрат разности',
|
||||
pick: { a: [1, 9] },
|
||||
derive: { a2: 'a*a', a2x: '2*a' },
|
||||
srcExpr: '(x - {a})^2', answerExpr: 'x^2 - {a2x}*x + {a2}', answerVars: ['x'],
|
||||
display: 'Раскройте: (x − {a})²',
|
||||
solution: [
|
||||
{ note: 'Квадрат разности: (x − {a})² = x² − 2·{a}·x + {a}².', tex: '(x - {a})^2 = x^2 - {a2x}*x + {a2}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* (x − a)(x + a) = x² − a² */
|
||||
{
|
||||
id: 'diff-sq', topic: 'formulas', order: 3, subject: 'algebra', grade: 8, kind: 'simplify',
|
||||
title: 'Разность квадратов',
|
||||
pick: { a: [2, 9] },
|
||||
derive: { a2: 'a*a' },
|
||||
srcExpr: '(x - {a})*(x + {a})', answerExpr: 'x^2 - {a2}', answerVars: ['x'],
|
||||
display: 'Раскройте: (x − {a})(x + {a})',
|
||||
solution: [
|
||||
{ note: 'Произведение разности и суммы даёт разность квадратов: (x − {a})(x + {a}) = x² − {a}².', tex: '(x - {a})*(x + {a}) = x^2 - {a2}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* ═══ Тема: Линейные неравенства ═══ */
|
||||
|
||||
/* ax + b < c (a>0, знак сохраняется) */
|
||||
{
|
||||
id: 'ineq-lt', topic: 'inequalities', order: 1, subject: 'algebra', grade: 8, kind: 'inequality',
|
||||
title: 'ax + b < c',
|
||||
pick: { a: [2, 6], b: [1, 15], root: [-8, 8] },
|
||||
derive: { c: 'a*root + b', cmb: 'a*root' },
|
||||
lhs: '{a}*x + {b}', rhs: '{c}', dispOp: '<', relOp: '<', bound: 'root',
|
||||
answerVar: 'x',
|
||||
solution: [
|
||||
{ note: 'Переносим свободный член {b} вправо:', tex: '{a}x < {cmb}' },
|
||||
{ note: 'Делим обе части на {a} — число положительное, знак неравенства не меняется:', tex: 'x < {root}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* ax + b ≥ c (a>0) */
|
||||
{
|
||||
id: 'ineq-ge', topic: 'inequalities', order: 2, subject: 'algebra', grade: 8, kind: 'inequality',
|
||||
title: 'ax + b ≥ c',
|
||||
pick: { a: [2, 6], b: [1, 15], root: [-8, 8] },
|
||||
derive: { c: 'a*root + b', cmb: 'a*root' },
|
||||
lhs: '{a}*x + {b}', rhs: '{c}', dispOp: '>=', relOp: '>=', bound: 'root',
|
||||
answerVar: 'x',
|
||||
solution: [
|
||||
{ note: 'Переносим {b} вправо:', tex: '{a}x >= {cmb}' },
|
||||
{ note: 'Делим на {a} (положительное) — знак сохраняется:', tex: 'x >= {root}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* −ax + b < c (коэффициент отрицательный → знак МЕНЯЕТСЯ) */
|
||||
{
|
||||
id: 'ineq-flip', topic: 'inequalities', order: 3, subject: 'algebra', grade: 8, kind: 'inequality',
|
||||
title: '−ax + b < c (смена знака)',
|
||||
pick: { a: [2, 6], b: [1, 15], root: [-8, 8] },
|
||||
derive: { c: 'b - a*root', cmb: '-a*root' },
|
||||
lhs: '-{a}*x + {b}', rhs: '{c}', dispOp: '<', relOp: '>', bound: 'root',
|
||||
answerVar: 'x',
|
||||
solution: [
|
||||
{ note: 'Переносим {b} вправо:', tex: '-{a}x < {cmb}' },
|
||||
{ note: 'Делим на отрицательное число (−{a}) — знак неравенства МЕНЯЕТСЯ на противоположный:', tex: 'x > {root}' }
|
||||
]
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
+10
-4
@@ -643,17 +643,23 @@
|
||||
// Префикс «x =» и подсказка ввода зависят от типа задачи.
|
||||
function applyInputMode() {
|
||||
var k = cur && cur.kind;
|
||||
var multi = (k === 'roots' || k === 'simplify');
|
||||
var multi = (k === 'roots' || k === 'simplify' || k === 'inequality');
|
||||
var eqx = $('tr-eqx'); if (eqx) eqx.style.display = multi ? 'none' : '';
|
||||
$('tr-input').placeholder = (k === 'roots') ? 'корни через ;' : (k === 'simplify') ? 'упрощённое выражение' : 'ответ';
|
||||
$('tr-input').placeholder = (k === 'roots') ? 'корни через ;'
|
||||
: (k === 'simplify') ? 'упрощённое выражение'
|
||||
: (k === 'inequality') ? ('напр. ' + (cur.answerVar || 'x') + ' < 3')
|
||||
: 'ответ';
|
||||
var tog = $('tr-step-toggle'); if (tog) tog.style.display = canStep() ? '' : 'none';
|
||||
}
|
||||
// Текст ответа в фидбеке/раскрытии — по типу задачи.
|
||||
var REL_SYM = { '<': '<', '>': '>', '<=': '≤', '>=': '≥' };
|
||||
function answerLabel() {
|
||||
if (cur.kind === 'roots' && cur.answers) return 'Корни: ' + cur.answers.map(fmt).join('; ');
|
||||
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);
|
||||
return 'x = ' + fmt(cur.answer);
|
||||
}
|
||||
function isLabelKind() { return cur.kind === 'roots' || cur.kind === 'simplify' || cur.kind === 'inequality'; }
|
||||
function updateStats() { $('tr-solved').textContent = solved; $('tr-streak').textContent = streak; }
|
||||
|
||||
function stepHtml(st, n) {
|
||||
@@ -764,7 +770,7 @@
|
||||
$('tr-input').disabled = true;
|
||||
var si = $('tr-stepin'); if (si) si.disabled = true;
|
||||
var fb = $('tr-feedback'); fb.className = 'tr-feedback';
|
||||
if (cur.kind === 'roots' || cur.kind === 'simplify') fb.textContent = 'Ответ: ' + answerLabel();
|
||||
if (isLabelKind()) fb.textContent = 'Ответ: ' + answerLabel();
|
||||
else setMath(fb, 'x = ' + cur.answer, 'Ответ: x = ' + fmt(cur.answer), false);
|
||||
setMode(true);
|
||||
recordAnswer(false); submitAttempt(false);
|
||||
@@ -783,7 +789,7 @@
|
||||
setMode(true);
|
||||
if (r.ok) {
|
||||
fb.className = 'tr-feedback ok';
|
||||
var lbl = (cur.kind === 'roots' || cur.kind === 'simplify') ? esc(answerLabel())
|
||||
var lbl = isLabelKind() ? esc(answerLabel())
|
||||
: (kat('x = ' + cur.answer, false) || esc('x = ' + fmt(cur.answer)));
|
||||
fb.innerHTML = ICON.ok + ' <span>Верно!</span> ' + lbl;
|
||||
$('tr-input').disabled = true;
|
||||
|
||||
@@ -50,7 +50,18 @@ solved-форме `x=c` → общий `onSolved` (засчитывается к
|
||||
(`callLLMFailover`) — только ОБЪЯСНЕНИЯ (безопасно, не генерация задач).
|
||||
- 3 уровня подсказок (намёк → шаг → решение).
|
||||
|
||||
## P10 — Контент 5–9 классов + ЦТ
|
||||
## P10 — Контент 5–9 классов + ЦТ — DONE (частично)
|
||||
**Сделано:** +3 темы (8 всего, 26 генераторов): **Степени** (вычислить aⁿ; xᵃ·xᵇ; (xᵃ)ᵇ),
|
||||
**Формулы сокр. умножения** (квадрат суммы/разности, разность квадратов), **Линейные
|
||||
неравенства** — НОВЫЙ тип `kind:'inequality'` (`answerRel:{op,bound}`; парсер отношения
|
||||
`_parseRel`/`_checkInequality` — нормализация «x op c», приём обратной записи «c op x»,
|
||||
сверка op+границы; ineq-flip учит смене знака при делении на отрицательное). Движок:
|
||||
**simplify теперь рендерит выражение в KaTeX** (`latex = exprToLatex(srcExpr)`, eyebrow =
|
||||
действие), неравенство — в KaTeX с отношением; self-check неравенства (внутри/снаружи
|
||||
решения). Страница: ввод/лейбл для неравенств (`x < 3`), `isLabelKind`. Смоук 397/397
|
||||
(T15 неравенства, T16 степени/формулы). **Осталось (стретч):** системы 2 ур-ний
|
||||
(пара-ответ), дроби 5–6, явная привязка к таксономии ЦТ.
|
||||
|
||||
Расширить охват и связать с подготовкой к ЦТ/ЦЭ.
|
||||
- Новые темы: арифметика/дроби/десятичные (5–6), степени, формулы сокр. умножения,
|
||||
разложение на множители, **линейные неравенства** (новый тип ответа: парсинг и
|
||||
|
||||
Reference in New Issue
Block a user