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 равносильно исходному ⟺ выполняется во ВСЕХ корнях
|
||||
|
||||
Reference in New Issue
Block a user