feat(trainer): ИИ-тренажёр — генераторы задач + SimExpr-верификатор, прогресс, фича-флаг
- движок _trainer_engine.js: instantiate/generateBatch/verifyRoot/checkStudentAnswer/exprToLatex - 5 генераторов уравнений 7 класса (generators.js), приём «корень-вперёд» → целые ответы - страница /trainer: KaTeX-рендер, чипы-темы, мгновенная проверка, подсказка/решение, авто-выбор навыка - прогресс practice_progress (мигр.081) + /api/practice/progress|attempt + LS.practiceProgressList/Submit - фича-флаг trainer: тумблер в админке (Модули), requireFeature, FEATURE_HREFS (скрытие сайдбара+редирект), MODULE_CATALOG - fix: подключён Lucide CDN на странице (иначе иконки сайдбара пустые) - тесты practice.test.js (10/10); план развития plans/ai-trainer/PLAN.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
'use strict';
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
TrainerEngine — ядро ИИ-тренажёра (Фаза 0, прототип).
|
||||
|
||||
Идея (гибрид): задачи рождаются из ДАННЫХ — «генераторов», а математика
|
||||
считается ДЕТЕРМИНИРОВАННО через SimExpr (тот же безопасный вычислитель, что
|
||||
у конструктора симуляций; ⛔ без eval/new Function). LLM в этом ядре НЕ
|
||||
участвует: его роль — один раз сочинить генераторы (Уровень 0) либо позже
|
||||
отдавать текстовые задачи, которые ЭТОТ ЖЕ слой верифицирует подстановкой
|
||||
(Уровень 1). Любой источник задачи проходит один и тот же verifyRoot.
|
||||
|
||||
Генератор (данные):
|
||||
{
|
||||
id, skill, title,
|
||||
pick: { a:[lo,hi], ... }, // целые параметры из диапазонов
|
||||
constraint?: "c < a", // булево над pick (SimExpr) — иначе пересэмпл
|
||||
derive?: { c: "a*root + b" }, // доп. параметры последовательно (SimExpr)
|
||||
require?: "...", // булево после derive — иначе пересэмпл
|
||||
lhs, rhs, // СТОРОНЫ уравнения как выражения с {param} и x
|
||||
display?, // как показать (по умолч. "lhs = rhs")
|
||||
answerVar?: "x", // имя неизвестной (деф. x)
|
||||
answer: "root", // корень как формула над параметрами
|
||||
integerAnswer?: true, // требовать целый корень
|
||||
solution?: ["шаг … {ans}", …] // шаблоны шагов (доступен {ans})
|
||||
}
|
||||
|
||||
Гарантия КОРРЕКТНОСТИ: после материализации движок ПОДСТАВЛЯЕТ заявленный
|
||||
корень в уравнение (verifyRoot). Не сходится — экземпляр отбрасывается (в
|
||||
strict-режиме — исключение). Та же подстановка проверяет ответ ученика
|
||||
(checkStudentAnswer) и автоматически принимает эквивалентные формы
|
||||
(5, 5.0, 10/2, "x=15/3", "2+3").
|
||||
|
||||
API (window.TrainerEngine):
|
||||
instantiate(gen, opts) -> problem | null
|
||||
generateBatch(gen, n, opts) -> problem[]
|
||||
verifyRoot(problem, value) -> { ok, residual, lhs, rhs }
|
||||
checkStudentAnswer(problem, input)-> { ok, value, residual, message, reason? }
|
||||
makeRng(seed) -> () => [0,1) (детерминизм для тестов/пула)
|
||||
|
||||
problem:
|
||||
{ genId, skill, title, lhsExpr, rhsExpr, display, answerVar, answer,
|
||||
params, solution }
|
||||
════════════════════════════════════════════════════════════════════════ */
|
||||
(function (global) {
|
||||
|
||||
function SE() {
|
||||
var s = global.SimExpr;
|
||||
if (!s) throw new Error('TrainerEngine требует SimExpr (подключите _sim_expr.js раньше).');
|
||||
return s;
|
||||
}
|
||||
|
||||
// Допуск подстановки: масштабируется величиной сторон, чтобы крупные
|
||||
// коэффициенты не давали ложного «не сходится» из-за плавающей арифметики.
|
||||
var EPS = 1e-7;
|
||||
|
||||
/* ── Детерминированный ГПСЧ (mulberry32) — тот же, что в game/map.js ──
|
||||
Нужен, чтобы предгенерация пула и тесты были воспроизводимы. В рантайме
|
||||
можно не передавать seed (тогда берётся внутренний инкремент от Date нельзя —
|
||||
поэтому дефолт фиксирован, а вариативность даёт сам диапазон pick). */
|
||||
function makeRng(seed) {
|
||||
var s = (seed >>> 0) || 1;
|
||||
return function () {
|
||||
s |= 0; s = (s + 0x6D2B79F5) | 0;
|
||||
var t = Math.imul(s ^ (s >>> 15), 1 | s);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
function randInt(rng, lo, hi) { return lo + Math.floor(rng() * (hi - lo + 1)); }
|
||||
|
||||
/* ── Кэш компиляции выражений (рендеренные строки часто повторяются) ── */
|
||||
var _cache = Object.create(null);
|
||||
function compileExpr(src) {
|
||||
var key = String(src);
|
||||
var c = _cache[key];
|
||||
if (!c) { c = SE().compile(key); _cache[key] = c; }
|
||||
return c;
|
||||
}
|
||||
function evalExpr(src, env) { return compileExpr(src).fn(env); }
|
||||
|
||||
function truthy(v) { return typeof v === 'number' && isFinite(v) && v !== 0; }
|
||||
function isIntApprox(v) { return isFinite(v) && Math.abs(v - Math.round(v)) < 1e-9; }
|
||||
|
||||
function fmtNum(v) {
|
||||
if (typeof v !== 'number') return String(v);
|
||||
if (isIntApprox(v)) return String(Math.round(v));
|
||||
return String(Math.round(v * 1e6) / 1e6);
|
||||
}
|
||||
|
||||
/* Подстановка {name} -> значение (для выражений и подписей). */
|
||||
function render(tpl, vals) {
|
||||
return String(tpl).replace(/\{(\w+)\}/g, function (m, k) {
|
||||
return Object.prototype.hasOwnProperty.call(vals, k) ? fmtNum(vals[k]) : m;
|
||||
});
|
||||
}
|
||||
|
||||
/* Лёгкая косметика ТОЛЬКО для показа (не для вычислений):
|
||||
5*x -> 5x, «+ -» -> «− », ведущий коэффициент 1 у x убираем. */
|
||||
function prettyMath(s) {
|
||||
return String(s)
|
||||
.replace(/(\d)\s*\*\s*(\d)/g, '$1·$2') // 4*5 -> 4·5 (число·число)
|
||||
.replace(/\s*\*\s*/g, '') // 7*x -> 7x (неявное умножение)
|
||||
.replace(/\+\s*-\s*/g, '− ') // + -3 -> − 3
|
||||
.replace(/-\s*-\s*/g, '+ ')
|
||||
.replace(/(^|[(=+\-\s])1(?=x)/g, '$1'); // ведущий 1·x -> x
|
||||
}
|
||||
|
||||
function assign(base, extra) {
|
||||
var o = {}, k;
|
||||
for (k in base) if (Object.prototype.hasOwnProperty.call(base, k)) o[k] = base[k];
|
||||
for (k in extra) if (Object.prototype.hasOwnProperty.call(extra, k)) o[k] = extra[k];
|
||||
return o;
|
||||
}
|
||||
|
||||
/* ── Выражение -> LaTeX (через AST SimExpr) для KaTeX-рендера ──
|
||||
Возвращает строку LaTeX или null, если выражение не разобралось. Покрывает
|
||||
наши нужды: дроби (\frac), степени, неявное умножение, скобки по приоритету,
|
||||
сравнения (= ≠ ≤ ≥), sqrt/abs/тригонометрию. Один проход AST, без eval.
|
||||
Reusable: тем же конвертером можно рендерить и задачи Уровня-1 (LLM). */
|
||||
function _prec(n) {
|
||||
if (!n) return 9;
|
||||
if (n.k === 'cmp' || n.k === 'logic') return 0;
|
||||
if (n.k === 'bin') {
|
||||
if (n.op === '+' || n.op === '-') return 1;
|
||||
if (n.op === '*' || n.op === '/' || n.op === '%') return 2;
|
||||
if (n.op === '^') return 4;
|
||||
}
|
||||
if (n.k === 'un' || n.k === 'not') return 3;
|
||||
return 5;
|
||||
}
|
||||
function _isNeg(n) { return (n.k === 'num' && n.v < 0) || (n.k === 'un' && n.op === '-'); }
|
||||
function _negate(n) { return n.k === 'num' ? { k: 'num', v: -n.v } : n.a; }
|
||||
function _wrapL(node, minPrec) {
|
||||
var s = _latex(node);
|
||||
return _prec(node) < minPrec ? '\\left(' + s + '\\right)' : s;
|
||||
}
|
||||
function _latex(node) {
|
||||
switch (node.k) {
|
||||
case 'num': return fmtNum(node.v);
|
||||
case 'const':
|
||||
if (node.v === Math.PI) return '\\pi';
|
||||
if (node.v === Math.PI * 2) return '\\tau';
|
||||
if (node.v === Math.E) return 'e';
|
||||
return fmtNum(node.v);
|
||||
case 'var': return node.name;
|
||||
case 'un': return '-' + _wrapL(node.a, 3);
|
||||
case 'not': return '\\lnot ' + _wrapL(node.a, 3);
|
||||
case 'cmp': {
|
||||
var m = { '==': '=', '!=': '\\ne', '<': '<', '<=': '\\le', '>': '>', '>=': '\\ge' };
|
||||
return _latex(node.a) + ' ' + (m[node.op] || node.op) + ' ' + _latex(node.b);
|
||||
}
|
||||
case 'logic':
|
||||
return _latex(node.a) + (node.op === '&&' ? ' \\land ' : ' \\lor ') + _latex(node.b);
|
||||
case 'cond':
|
||||
return _wrapL(node.c, 1) + ' \\,?\\, ' + _latex(node.a) + ' : ' + _latex(node.b);
|
||||
case 'call': {
|
||||
if (node.name === 'sqrt') return '\\sqrt{' + _latex(node.args[0]) + '}';
|
||||
if (node.name === 'abs') return '\\left|' + _latex(node.args[0]) + '\\right|';
|
||||
var TRIG = { sin: '\\sin', cos: '\\cos', tan: '\\tan', tg: '\\tan', ln: '\\ln', log: '\\log', exp: '\\exp' };
|
||||
var fn = TRIG[node.name] || ('\\operatorname{' + node.name + '}');
|
||||
return fn + '\\left(' + node.args.map(_latex).join(',\\, ') + '\\right)';
|
||||
}
|
||||
case 'bin': {
|
||||
var op = node.op;
|
||||
if (op === '/') return '\\frac{' + _latex(node.a) + '}{' + _latex(node.b) + '}';
|
||||
if (op === '^') {
|
||||
var base = _prec(node.a) < 5 ? '\\left(' + _latex(node.a) + '\\right)' : _latex(node.a);
|
||||
return base + '^{' + _latex(node.b) + '}';
|
||||
}
|
||||
if (op === '*') {
|
||||
var sep = (node.b.k === 'num') ? ' \\cdot ' : ''; // ·число; иначе соседство (7x, 6(x+1))
|
||||
return _wrapL(node.a, 2) + sep + _wrapL(node.b, 2);
|
||||
}
|
||||
if (op === '%') return _wrapL(node.a, 2) + ' \\bmod ' + _wrapL(node.b, 3);
|
||||
// + или - (схлопываем a + (-b) -> a - b)
|
||||
var right = node.b, rop = op;
|
||||
if (op === '+' && _isNeg(right)) { rop = '-'; right = _negate(right); }
|
||||
return _wrapL(node.a, 1) + ' ' + rop + ' ' + _wrapL(right, rop === '-' ? 2 : 1);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function exprToLatex(src) {
|
||||
var ast;
|
||||
try { ast = SE().parse(String(src)); } catch (e) { return null; }
|
||||
try { return _latex(ast); } catch (e2) { return null; }
|
||||
}
|
||||
|
||||
/* ── Подстановочная верификация корня ──
|
||||
Истинно, если левая и правая части совпадают при answerVar = value. */
|
||||
function verifyRoot(problem, value) {
|
||||
var env = {};
|
||||
env[problem.answerVar || 'x'] = value;
|
||||
var L = evalExpr(problem.lhsExpr, env);
|
||||
var R = evalExpr(problem.rhsExpr, env);
|
||||
var residual = Math.abs(L - R);
|
||||
var scale = Math.max(1, Math.abs(L), Math.abs(R));
|
||||
return { ok: residual <= EPS * scale, residual: residual, lhs: L, rhs: R };
|
||||
}
|
||||
|
||||
/* ── Материализация одного экземпляра ──
|
||||
Возвращает problem или null, если за maxTries не удалось выполнить
|
||||
ограничения / целочисленность / самопроверку. */
|
||||
function instantiate(gen, opts) {
|
||||
opts = opts || {};
|
||||
var rng = opts.rng || makeRng(opts.seed != null ? opts.seed : 1);
|
||||
var maxTries = opts.maxTries || 300;
|
||||
var answerVar = gen.answerVar || 'x';
|
||||
|
||||
for (var attempt = 0; attempt < maxTries; attempt++) {
|
||||
var env = {};
|
||||
var pk = gen.pick || {}, k;
|
||||
for (k in pk) if (Object.prototype.hasOwnProperty.call(pk, k)) {
|
||||
env[k] = randInt(rng, pk[k][0], pk[k][1]);
|
||||
}
|
||||
|
||||
if (gen.constraint && !truthy(evalExpr(gen.constraint, env))) continue;
|
||||
|
||||
if (gen.derive) {
|
||||
for (k in gen.derive) if (Object.prototype.hasOwnProperty.call(gen.derive, k)) {
|
||||
env[k] = evalExpr(gen.derive[k], env);
|
||||
}
|
||||
}
|
||||
|
||||
if (gen.require && !truthy(evalExpr(gen.require, env))) continue;
|
||||
|
||||
var answer = evalExpr(gen.answer, env);
|
||||
if (gen.integerAnswer) {
|
||||
if (!isIntApprox(answer)) continue;
|
||||
answer = Math.round(answer);
|
||||
}
|
||||
|
||||
var lhsExpr = render(gen.lhs, env);
|
||||
var rhsExpr = render(gen.rhs, env);
|
||||
var sEnv = assign(env, { ans: answer });
|
||||
var ll = exprToLatex(lhsExpr), rl = exprToLatex(rhsExpr);
|
||||
|
||||
var problem = {
|
||||
genId: gen.id,
|
||||
skill: gen.skill,
|
||||
title: gen.title,
|
||||
lhsExpr: lhsExpr,
|
||||
rhsExpr: rhsExpr,
|
||||
display: prettyMath(render(gen.display || (gen.lhs + ' = ' + gen.rhs), env)),
|
||||
latex: (ll != null && rl != null) ? (ll + ' = ' + rl) : null,
|
||||
answerVar: answerVar,
|
||||
answer: answer,
|
||||
params: env,
|
||||
// шаг решения -> { note(текст), tex(подпись), latex(для KaTeX, null если не разобрался) }
|
||||
// строковый шаг (легаси) трактуется как чистая заметка без формулы.
|
||||
solution: (gen.solution || []).map(function (st) {
|
||||
if (typeof st === 'string') return { note: render(st, sEnv), tex: '', latex: null };
|
||||
var texSrc = st.tex ? render(st.tex, sEnv) : '';
|
||||
return {
|
||||
note: st.note ? render(st.note, sEnv) : '',
|
||||
tex: texSrc ? prettyMath(texSrc) : '',
|
||||
latex: texSrc ? exprToLatex(texSrc) : null
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
// Самопроверка: эталонный корень ОБЯЗАН удовлетворять уравнению.
|
||||
var v = verifyRoot(problem, answer);
|
||||
if (!v.ok) {
|
||||
if (opts.strict) {
|
||||
throw new Error('Генератор «' + gen.id + '»: корень ' + fmtNum(answer) +
|
||||
' не удовлетворяет уравнению (невязка ' + v.residual + ').');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return problem;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ── Пакет из n различных по виду задач ── */
|
||||
function generateBatch(gen, n, opts) {
|
||||
opts = opts || {};
|
||||
var rng = opts.rng || makeRng(opts.seed != null ? opts.seed : 1);
|
||||
var out = [], seen = Object.create(null);
|
||||
var guard = n * 20 + 50;
|
||||
while (out.length < n && guard-- > 0) {
|
||||
var p = instantiate(gen, { rng: rng, strict: opts.strict, maxTries: opts.maxTries });
|
||||
if (!p) break;
|
||||
if (seen[p.display]) continue;
|
||||
seen[p.display] = 1;
|
||||
out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ── Проверка ответа ученика ──
|
||||
Принимает строку/число. SimExpr.compile сам срезает ведущее «x=», поэтому
|
||||
"x = 5", "5", "10/2", "2+3" нормализуются к числу. Верно, если значение
|
||||
удовлетворяет уравнению (эквивалентные формы проходят) ИЛИ совпадает с
|
||||
эталонным корнем (страховка единственности для будущих многокорневых типов). */
|
||||
function checkStudentAnswer(problem, input) {
|
||||
var raw = String(input == null ? '' : input).trim();
|
||||
if (!raw) return { ok: false, reason: 'empty', value: null, residual: null, message: 'Введите ответ.' };
|
||||
|
||||
var c = SE().compile(raw);
|
||||
if (c.error) {
|
||||
return { ok: false, reason: 'parse', value: null, residual: null,
|
||||
message: 'Не понял ответ: ' + c.error };
|
||||
}
|
||||
var val = c.fn({});
|
||||
if (!isFinite(val)) {
|
||||
return { ok: false, reason: 'nan', value: val, residual: null, message: 'Это не число.' };
|
||||
}
|
||||
|
||||
var v = verifyRoot(problem, val);
|
||||
var nearCanonical = Math.abs(val - problem.answer) <= 1e-6 * Math.max(1, Math.abs(problem.answer));
|
||||
var ok = v.ok || nearCanonical;
|
||||
return {
|
||||
ok: ok, reason: ok ? null : 'wrong', value: val, residual: v.residual,
|
||||
message: ok ? 'Верно!' : 'Пока неверно.'
|
||||
};
|
||||
}
|
||||
|
||||
global.TrainerEngine = {
|
||||
instantiate: instantiate,
|
||||
generateBatch: generateBatch,
|
||||
verifyRoot: verifyRoot,
|
||||
checkStudentAnswer: checkStudentAnswer,
|
||||
makeRng: makeRng,
|
||||
// мелочи наружу для билдера/тестов
|
||||
render: render,
|
||||
prettyMath: prettyMath,
|
||||
exprToLatex: exprToLatex
|
||||
};
|
||||
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
@@ -0,0 +1,129 @@
|
||||
'use strict';
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
Генераторы уравнений — 7 класс (прототип). Это ДАННЫЕ, не код.
|
||||
|
||||
Приём «корень-вперёд»: выбираем целый корень (или множитель) и коэффициенты,
|
||||
затем ВЫВОДИМ свободный член так, чтобы ответ гарантированно был целым, а
|
||||
уравнение — решаемым. Поэтому самопроверка движка (verifyRoot) всегда
|
||||
проходит. Шаг решения — { note(текст), tex(формула) }; tex рендерится в KaTeX
|
||||
через TrainerEngine.exprToLatex (одно равенство на шаг, без цепочек a=b=c).
|
||||
|
||||
Прогрессия 7 класса: простое линейное → скобки → переменная в обеих частях →
|
||||
уравнение с дробью в знаменателе → дробный коэффициент. Дальше (Уровень 1):
|
||||
текстовые задачи через LLM с той же подстановочной верификацией.
|
||||
════════════════════════════════════════════════════════════════════════ */
|
||||
(function (global) {
|
||||
|
||||
var GENERATORS = [
|
||||
|
||||
/* 1. ax + b = c */
|
||||
{
|
||||
id: 'lin-basic',
|
||||
skill: 'linear-basic',
|
||||
title: 'Линейное: ax + b = c',
|
||||
grade: 7,
|
||||
pick: { a: [2, 9], b: [1, 20], root: [-9, 9] },
|
||||
require: 'root != 0',
|
||||
derive: { c: 'a*root + b', cmb: 'a*root' }, // cmb = c - b
|
||||
lhs: '{a}*x + {b}', rhs: '{c}',
|
||||
display: '{a}x + {b} = {c}',
|
||||
answerVar: 'x', answer: 'root', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Переносим число вправо:', tex: '{a}x = {cmb}' },
|
||||
{ note: 'Делим обе части на {a}:', tex: 'x = {cmb} / {a}' },
|
||||
{ note: 'Ответ:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* 2. a(x + b) = c */
|
||||
{
|
||||
id: 'lin-paren',
|
||||
skill: 'linear-parentheses',
|
||||
title: 'Со скобками: a(x + b) = c',
|
||||
grade: 7,
|
||||
pick: { a: [2, 8], b: [1, 12], root: [-9, 9] },
|
||||
require: 'root != 0',
|
||||
derive: { c: 'a*(root + b)', ca: 'root + b' }, // ca = c / a
|
||||
lhs: '{a}*(x + {b})', rhs: '{c}',
|
||||
display: '{a}(x + {b}) = {c}',
|
||||
answerVar: 'x', answer: 'root', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Делим обе части на {a}:', tex: 'x + {b} = {ca}' },
|
||||
{ note: 'Переносим {b} влево:', tex: 'x = {ca} - {b}' },
|
||||
{ note: 'Ответ:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* 3. ax + b = cx + d */
|
||||
{
|
||||
id: 'lin-both-sides',
|
||||
skill: 'linear-both-sides',
|
||||
title: 'Переменная с двух сторон: ax + b = cx + d',
|
||||
grade: 7,
|
||||
pick: { a: [3, 9], c: [1, 8], b: [1, 20], root: [-9, 9] },
|
||||
constraint: 'c < a', // гарантируем a - c > 0
|
||||
require: 'root != 0',
|
||||
derive: { d: '(a - c)*root + b', amc: 'a - c', dmb: '(a - c)*root' }, // dmb = d - b
|
||||
lhs: '{a}*x + {b}', rhs: '{c}*x + {d}',
|
||||
display: '{a}x + {b} = {c}x + {d}',
|
||||
answerVar: 'x', answer: 'root', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Собираем x слева, числа справа:', tex: '({a} - {c})x = {d} - {b}' },
|
||||
{ note: 'Приводим подобные:', tex: '{amc}x = {dmb}' },
|
||||
{ note: 'Делим на {amc}:', tex: 'x = {dmb} / {amc}' },
|
||||
{ note: 'Ответ:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* 4. x/a + b = c (дробь в знаменателе) */
|
||||
{
|
||||
id: 'lin-frac-denom',
|
||||
skill: 'linear-fraction-denom',
|
||||
title: 'Дробь: x/a + b = c',
|
||||
grade: 7,
|
||||
pick: { a: [2, 6], k: [-6, 6], b: [1, 12] },
|
||||
require: 'k != 0',
|
||||
derive: { root: 'a*k', c: 'k + b', cmb: 'k' }, // root = a·k, cmb = c - b = k
|
||||
lhs: 'x/{a} + {b}', rhs: '{c}',
|
||||
display: 'x/{a} + {b} = {c}',
|
||||
answerVar: 'x', answer: 'root', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Вычитаем {b}:', tex: 'x/{a} = {cmb}' },
|
||||
{ note: 'Умножаем обе части на {a}:', tex: 'x = {cmb} * {a}' },
|
||||
{ note: 'Ответ:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
/* 5. (a·x)/b = c (дробный коэффициент) */
|
||||
{
|
||||
id: 'lin-coef-frac',
|
||||
skill: 'linear-coef-frac',
|
||||
title: 'Дробный коэффициент: ax/b = c',
|
||||
grade: 7,
|
||||
pick: { a: [2, 5], b: [2, 5], m: [-5, 5] },
|
||||
require: 'm != 0',
|
||||
derive: { root: 'b*m', c: 'a*m', cb: 'a*m*b' }, // root = b·m, c = a·m, cb = c·b
|
||||
lhs: '{a}*x/{b}', rhs: '{c}',
|
||||
display: '{a}x/{b} = {c}',
|
||||
answerVar: 'x', answer: 'root', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Умножаем обе части на {b}:', tex: '{a}x = {cb}' },
|
||||
{ note: 'Делим на {a}:', tex: 'x = {cb} / {a}' },
|
||||
{ note: 'Ответ:', tex: 'x = {ans}' }
|
||||
]
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
function get(id) {
|
||||
for (var i = 0; i < GENERATORS.length; i++) if (GENERATORS[i].id === id) return GENERATORS[i];
|
||||
return null;
|
||||
}
|
||||
|
||||
global.TrainerGenerators = {
|
||||
list: function () { return GENERATORS.slice(); },
|
||||
get: get,
|
||||
GENERATORS: GENERATORS
|
||||
};
|
||||
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
Reference in New Issue
Block a user