Files
Learn_System/frontend/js/trainer/generators.js
T
Maxim Dolgolyov 7cc2a9d526 feat(trainer): P5 — несколько корней, эквивалентность выражений, новые темы
- движок: gen.answers → несколько корней (_checkMultiRoot, ввод через «;», сверка мультимножеством)
- kind simplify: эквивалентность выражений численным сэмплингом (_sampleEquiv, _checkEquiv), фикс. точки без Math.random
- exprToLatex: знаковые коэффициенты — -5x, x²−5x+6, a−(−b)→a+b (вынос ведущего минуса, схлопывание)
- темы: Упрощение (подобные, скобки) + Квадратные (Виета x²+bx+c=0, разность квадратов) → 17 генераторов, 5 тем
- страница: префикс «x=»/подсказка ввода и ответ-лейбл по типу задачи
- смоук движка 291/291 (T11 roots, T12 simplify, T13 latex), страница 26/26, adaptive 12/12; план P5 → DONE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:15:21 +03:00

338 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* ════════════════════════════════════════════════════════════════════════
Генераторы задач тренажёра — ДАННЫЕ, не код. Таксономия: тема → навык.
Приём «корень-вперёд»: выбираем целый корень/множители и коэффициенты, затем
ВЫВОДИМ остальное так, чтобы ответ был целым, а задача — решаемой. Поэтому
самопроверка движка (verifyRoot) всегда проходит. Шаг решения —
{ note(подробный текст), tex(одно равенство) }; tex рендерится в KaTeX
(exprToLatex). Последний шаг «Проверка» подставляет корень — это и педагогика,
и наглядная демонстрация того, как движок проверяет ответ.
Виды задач:
• solve (деф.) — уравнение lhs = rhs, ученик находит x. Показывается уравнение.
• compute (kind:'compute') — вычислительная задача (проценты): на сцене —
текстовый prompt из display, а lhs:'x' / rhs:<значение> служат ТОЛЬКО для
проверки ответа подстановкой (latex уравнения не показывается).
Темы (7 класс, алгебра): Уравнения → Пропорции → Проценты. Дальше (Уровень 1):
текстовые задачи через LLM с той же подстановочной верификацией.
════════════════════════════════════════════════════════════════════════ */
(function (global) {
var TOPICS = [
{ key: 'linear-eq', label: 'Уравнения', subject: 'algebra', grade: 7, order: 1 },
{ 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 }
];
var GENERATORS = [
/* ═══ Тема: Уравнения ═══ */
/* ax + b = c */
{
id: 'lin-basic', topic: 'linear-eq', order: 1, subject: 'algebra', grade: 7,
title: 'ax + b = c',
pick: { a: [2, 9], b: [1, 20], root: [-9, 9] },
require: 'root != 0',
derive: { c: 'a*root + b', cmb: 'a*root' },
lhs: '{a}*x + {b}', rhs: '{c}', display: '{a}x + {b} = {c}',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Перед нами линейное уравнение. Наша цель — оставить x одного в левой части. Сначала уберём свободное число {b}: перенесём его вправо, поменяв знак.', tex: '{a}x = {c} - {b}' },
{ note: 'Выполняем вычитание в правой части.', tex: '{a}x = {cmb}' },
{ note: 'Осталось избавиться от множителя при x — делим обе части уравнения на {a}.', tex: 'x = {cmb} / {a}' },
{ note: 'Получаем корень уравнения.', tex: 'x = {ans}' },
{ note: 'Проверка: подставим найденное значение в исходное уравнение — левая часть должна совпасть с правой.', tex: '{a}*({ans}) + {b} = {c}' }
]
},
/* a(x + b) = c */
{
id: 'lin-paren', topic: 'linear-eq', order: 2, subject: 'algebra', grade: 7,
title: 'a(x + b) = c',
pick: { a: [2, 8], b: [1, 12], root: [-9, 9] },
require: 'root != 0',
derive: { c: 'a*(root + b)', ca: 'root + b' },
lhs: '{a}*(x + {b})', rhs: '{c}', display: '{a}(x + {b}) = {c}',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Слева число {a} умножается на скобку. Самый короткий путь — разделить обе части уравнения на {a}, чтобы убрать этот множитель.', tex: 'x + {b} = {ca}' },
{ note: 'Справа получилось целое число. Теперь переносим {b} вправо со сменой знака.', tex: 'x = {ca} - {b}' },
{ note: 'Получаем корень уравнения.', tex: 'x = {ans}' },
{ note: 'Проверка: подставляем корень в скобку исходного уравнения.', tex: '{a}*({ans} + {b}) = {c}' }
]
},
/* ax + b = cx + d */
{
id: 'lin-both-sides', topic: 'linear-eq', order: 3, subject: 'algebra', grade: 7,
title: 'ax + b = cx + d',
pick: { a: [3, 9], c: [1, 8], b: [1, 20], root: [-9, 9] },
constraint: 'c < a', require: 'root != 0',
derive: { d: '(a - c)*root + b', amc: 'a - c', dmb: '(a - c)*root' },
lhs: '{a}*x + {b}', rhs: '{c}*x + {d}', display: '{a}x + {b} = {c}x + {d}',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Здесь x есть в обеих частях. Соберём все слагаемые с x слева, а числа — справа. Переносимые слагаемые меняют знак.', tex: '({a} - {c})x = {d} - {b}' },
{ note: 'Приводим подобные: вычитаем коэффициенты при x и отдельно числа.', tex: '{amc}x = {dmb}' },
{ note: 'Делим обе части на коэффициент при x, то есть на {amc}.', tex: 'x = {dmb} / {amc}' },
{ note: 'Получаем корень уравнения.', tex: 'x = {ans}' },
{ note: 'Проверка: подставим корень — обе части дадут одно и то же число.', tex: '{a}*({ans}) + {b} = {c}*({ans}) + {d}' }
]
},
/* a(x + b) = c(x + d) */
{
id: 'lin-paren-both', topic: 'linear-eq', order: 4, subject: 'algebra', grade: 7,
title: 'a(x+b) = c(x+d)',
pick: { a: [2, 6], c: [2, 6], b: [1, 10], root: [-6, 6] },
constraint: 'a != c',
derive: { V: 'a*(root + b)', d: 'V/c - root', ab: 'a*b', cd: 'c*(V/c - root)', amc: 'a - c', diff: 'c*(V/c - root) - a*b' },
require: 'mod(V, c) == 0 && root != 0',
lhs: '{a}*(x + {b})', rhs: '{c}*(x + {d})', display: '{a}(x + {b}) = {c}(x + {d})',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Скобки с двух сторон. Раскрываем их: умножаем множитель перед скобкой на каждое слагаемое внутри.', tex: '{a}x + {ab} = {c}x + {cd}' },
{ note: 'Переносим слагаемые с x влево, числа — вправо, и приводим подобные.', tex: '{amc}x = {diff}' },
{ note: 'Делим обе части на {amc}.', tex: 'x = {diff} / {amc}' },
{ note: 'Получаем корень уравнения.', tex: 'x = {ans}' },
{ note: 'Проверка: подставляем корень в обе скобки.', tex: '{a}*({ans} + {b}) = {c}*({ans} + {d})' }
]
},
/* x/a + b = c */
{
id: 'lin-frac-denom', topic: 'linear-eq', order: 5, subject: 'algebra', grade: 7,
title: 'x/a + b = c',
pick: { a: [2, 6], k: [-6, 6], b: [1, 12] },
require: 'k != 0',
derive: { root: 'a*k', c: 'k + b', cmb: 'k' },
lhs: 'x/{a} + {b}', rhs: '{c}', display: 'x/{a} + {b} = {c}',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Слева — дробь x/{a} и число {b}. Сначала уберём свободное число: вычтем {b} из обеих частей.', tex: 'x/{a} = {cmb}' },
{ note: 'Чтобы избавиться от знаменателя {a}, умножаем обе части уравнения на {a}.', tex: 'x = {cmb} * {a}' },
{ note: 'Получаем корень уравнения.', tex: 'x = {ans}' },
{ note: 'Проверка: подставляем корень в исходное уравнение.', tex: '{ans}/{a} + {b} = {c}' }
]
},
/* (a·x)/b = c */
{
id: 'lin-coef-frac', topic: 'linear-eq', order: 6, subject: 'algebra', grade: 7,
title: 'ax/b = c',
pick: { a: [2, 5], b: [2, 5], m: [-5, 5] },
require: 'm != 0',
derive: { root: 'b*m', c: 'a*m', cb: 'a*m*b' },
lhs: '{a}*x/{b}', rhs: '{c}', display: '{a}x/{b} = {c}',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Слева дробь, в числителе — {a}x. Умножаем обе части уравнения на знаменатель {b}, чтобы избавиться от дроби.', tex: '{a}x = {cb}' },
{ note: 'Теперь делим обе части на коэффициент {a}.', tex: 'x = {cb} / {a}' },
{ note: 'Получаем корень уравнения.', tex: 'x = {ans}' },
{ note: 'Проверка: подставляем корень в исходную дробь.', tex: '{a}*({ans})/{b} = {c}' }
]
},
/* (ax + b)/c = d */
{
id: 'lin-frac-eq', topic: 'linear-eq', order: 7, subject: 'algebra', grade: 7,
title: '(ax + b)/c = d',
pick: { a: [2, 6], b: [1, 12], c: [2, 6], root: [-6, 6] },
derive: { prod: 'a*root + b', d: '(a*root + b)/c', cd: 'a*root + b', cdmb: 'a*root' },
require: 'mod(a*root + b, c) == 0 && root != 0',
lhs: '({a}*x + {b})/{c}', rhs: '{d}', display: '({a}x + {b})/{c} = {d}',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Вся левая часть делится на {c}. Умножаем обе части уравнения на {c}, чтобы убрать знаменатель.', tex: '{a}x + {b} = {cd}' },
{ note: 'Переносим число {b} в правую часть со сменой знака.', tex: '{a}x = {cdmb}' },
{ note: 'Делим обе части на {a}.', tex: 'x = {cdmb} / {a}' },
{ note: 'Получаем корень уравнения.', tex: 'x = {ans}' },
{ note: 'Проверка: подставляем корень в исходную дробь.', tex: '({a}*({ans}) + {b})/{c} = {d}' }
]
},
/* ═══ Тема: Пропорции ═══ */
/* a/b = c/x */
{
id: 'prop-x-right', topic: 'proportions', order: 1, subject: 'algebra', grade: 7,
title: 'a/b = c/x',
pick: { a: [2, 9], b: [2, 9], t: [2, 9] },
derive: { c: 'a*t', root: 'b*t', bc: 'b*a*t' },
lhs: '{a}/{b}', rhs: '{c}/x', display: '{a}/{b} = {c}/x',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Это пропорция — равенство двух отношений. По основному свойству пропорции произведение крайних членов равно произведению средних (умножаем «крест-накрест»).', tex: '{a}*x = {b} * {c}' },
{ note: 'Считаем произведение в правой части.', tex: '{a}x = {bc}' },
{ note: 'Делим обе части на {a}, чтобы найти x.', tex: 'x = {bc} / {a}' },
{ note: 'Получаем корень.', tex: 'x = {ans}' },
{ note: 'Проверка: при найденном x обе дроби равны.', tex: '{a}/{b} = {c}/{ans}' }
]
},
/* x/a = b/c */
{
id: 'prop-x-left', topic: 'proportions', order: 2, subject: 'algebra', grade: 7,
title: 'x/a = b/c',
pick: { a: [2, 9], c: [2, 9], s: [2, 9] },
derive: { b: 'c*s', root: 'a*s', ab: 'a*c*s' },
lhs: 'x/{a}', rhs: '{b}/{c}', display: 'x/{a} = {b}/{c}',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Перед нами пропорция. Перемножаем её члены крест-накрест: числитель левой дроби на знаменатель правой и наоборот.', tex: '{c}*x = {a} * {b}' },
{ note: 'Считаем произведение в правой части.', tex: '{c}x = {ab}' },
{ note: 'Делим обе части на {c}.', tex: 'x = {ab} / {c}' },
{ note: 'Получаем корень.', tex: 'x = {ans}' },
{ note: 'Проверка: обе дроби равны.', tex: '{ans}/{a} = {b}/{c}' }
]
},
/* a/x = b/c */
{
id: 'prop-x-denom', topic: 'proportions', order: 3, subject: 'algebra', grade: 7,
title: 'a/x = b/c',
pick: { b: [2, 9], c: [2, 9], s: [2, 9] },
derive: { a: 'b*s', root: 'c*s', ac: 'b*s*c' },
lhs: '{a}/x', rhs: '{b}/{c}', display: '{a}/x = {b}/{c}',
answerVar: 'x', answer: 'root', integerAnswer: true,
solution: [
{ note: 'Пропорция, где неизвестное стоит в знаменателе. Перемножаем крест-накрест.', tex: '{b}*x = {a} * {c}' },
{ note: 'Считаем произведение в правой части.', tex: '{b}x = {ac}' },
{ note: 'Делим обе части на {b}.', tex: 'x = {ac} / {b}' },
{ note: 'Получаем корень.', tex: 'x = {ans}' },
{ note: 'Проверка: обе дроби равны.', tex: '{a}/{ans} = {b}/{c}' }
]
},
/* ═══ Тема: Проценты (вычислительные задачи) ═══ */
/* p% от числа a */
{
id: 'pct-of', topic: 'percents', order: 1, subject: 'algebra', grade: 7, kind: 'compute',
title: 'p% от числа',
pick: { pidx: [2, 10], abase: [1, 15] },
derive: { p: 'pidx*5', a: 'abase*20', val: 'pidx*abase' },
lhs: 'x', rhs: '{p}*{a}/100', display: 'Найдите {p}% от числа {a}',
answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [
{ note: 'Процент — это сотая доля числа. Чтобы найти {p}% от {a}, нужно умножить число на {p} и разделить на 100.', tex: 'x = {a}*{p}/100' },
{ note: 'Выполняем умножение и деление — получаем ответ.', tex: 'x = {ans}' }
]
},
/* сколько % составляет a от b */
{
id: 'pct-what', topic: 'percents', order: 2, subject: 'algebra', grade: 7, kind: 'compute',
title: 'Сколько процентов',
pick: { pidx: [2, 10], bbase: [1, 8] },
derive: { p: 'pidx*5', b: 'bbase*20', a: 'pidx*bbase' },
lhs: 'x', rhs: '100*{a}/{b}', display: 'Сколько процентов составляет {a} от {b}?',
answerVar: 'x', answer: 'p', integerAnswer: true,
solution: [
{ note: 'Чтобы узнать, какую часть {a} составляет от {b}, делим {a} на {b}. А чтобы перевести эту часть в проценты — умножаем результат на 100.', tex: 'x = {a}/{b}*100' },
{ note: 'Считаем — ответ получается в процентах.', tex: 'x = {ans}' }
]
},
/* p% числа равны a — найти число */
{
id: 'pct-whole', topic: 'percents', order: 3, subject: 'algebra', grade: 7, kind: 'compute',
title: 'Число по проценту',
pick: { pidx: [2, 10], wbase: [1, 12] },
derive: { p: 'pidx*5', whole: 'wbase*20', a: 'pidx*wbase' },
lhs: 'x', rhs: '100*{a}/{p}', display: '{p}% числа равны {a}. Найдите это число.',
answerVar: 'x', answer: 'whole', integerAnswer: true,
solution: [
{ note: 'Известно, что {p}% некоторого числа равны {a}. Значит само число во столько раз больше: умножаем {a} на 100 и делим на {p}.', tex: 'x = {a}*100/{p}' },
{ note: 'Считаем — получаем искомое число.', tex: 'x = {ans}' }
]
},
/* ═══ Тема: Упрощение выражений (проверка эквивалентностью) ═══ */
/* a·x + b·x → (a+b)x */
{
id: 'simp-like', topic: 'simplify', order: 1, subject: 'algebra', grade: 7, kind: 'simplify',
title: 'Привести подобные',
pick: { a: [2, 9], b: [2, 9] },
derive: { s: 'a + b' },
srcExpr: '{a}*x + {b}*x', answerExpr: '{s}*x', answerVars: ['x'],
display: 'Упростите: {a}x + {b}x',
solution: [
{ note: 'Оба слагаемых содержат x — это подобные слагаемые. Складываем их коэффициенты: {a} + {b} = {s}.', tex: '{a}x + {b}x = {s}x' }
]
},
/* a(x + b) → ax + ab */
{
id: 'simp-expand', topic: 'simplify', order: 2, subject: 'algebra', grade: 7, kind: 'simplify',
title: 'Раскрыть скобки',
pick: { a: [2, 9], b: [1, 9] },
derive: { ab: 'a*b' },
srcExpr: '{a}*(x + {b})', answerExpr: '{a}*x + {ab}', answerVars: ['x'],
display: 'Раскройте скобки: {a}(x + {b})',
solution: [
{ note: 'Умножаем множитель {a} на каждое слагаемое внутри скобки.', tex: '{a}(x + {b}) = {a}x + {ab}' }
]
},
/* ═══ Тема: Квадратные уравнения (несколько корней) ═══ */
/* x² + bx + c = 0 — разложение по Виета (два корня r1, r2) */
{
id: 'quad-factored', topic: 'quadratic', order: 1, subject: 'algebra', grade: 8, kind: 'roots',
title: 'x² + bx + c = 0',
pick: { r1: [-7, 7], r2: [-7, 7] },
constraint: 'r1 != r2',
derive: { b: '-(r1 + r2)', c: 'r1*r2' },
lhs: 'x^2 + {b}*x + {c}', rhs: '0',
answerVar: 'x', answers: ['r1', 'r2'], integerAnswer: true,
solution: [
{ note: 'Квадратное уравнение приравнено к нулю. По теореме Виета ищем два числа: их сумма равна {r1}+{r2}, произведение — {c}. Это и есть корни. Раскладываем на множители:', tex: '(x - {r1})(x - {r2}) = 0' },
{ note: 'Произведение равно нулю, когда обнуляется множитель. Первый корень:', tex: 'x = {r1}' },
{ note: 'Второй корень:', tex: 'x = {r2}' }
]
},
/* x² − a² = 0 — разность квадратов (корни ±a) */
{
id: 'quad-diff', topic: 'quadratic', order: 2, subject: 'algebra', grade: 8, kind: 'roots',
title: 'x² a² = 0',
pick: { a: [2, 9] },
derive: { a2: 'a*a' },
lhs: 'x^2 - {a2}', rhs: '0',
answerVar: 'x', answers: ['a', '-a'], integerAnswer: true,
solution: [
{ note: 'Слева — разность квадратов: x² − {a2} = (x {a})(x + {a}). Раскладываем:', tex: '(x - {a})(x + {a}) = 0' },
{ note: 'Первый корень:', tex: 'x = {a}' },
{ note: 'Второй корень:', tex: 'x = -{a}' }
]
}
];
function get(id) {
for (var i = 0; i < GENERATORS.length; i++) if (GENERATORS[i].id === id) return GENERATORS[i];
return null;
}
function byTopic(key) {
return GENERATORS.filter(function (g) { return g.topic === key; })
.sort(function (a, b) { return (a.order || 0) - (b.order || 0); });
}
global.TrainerGenerators = {
list: function () { return GENERATORS.slice(); },
topics: function () { return TOPICS.slice(); },
byTopic: byTopic,
get: get,
GENERATORS: GENERATORS,
TOPICS: TOPICS
};
})(typeof window !== 'undefined' ? window : globalThis);