feat(trainer): НОД/НОК с нормальными числами; универсальная шапка; НОК теперь появляется
НОД/НОК (числа больше + без степеней):
- движок: фича factorize ({name, of}) кладёт в шаги решения СТРОКУ разложения на простые множители без степеней (36 -> «2*2*3*3»); helper primeFactorString
- генераторы: a=g·m, b=g·n (g,m,n из 2..9) -> нормальные числа (14, 35, 16, 112…), общий множитель гарантирован; решение показывает разложение обоих + НОД/НОК = произведение множителей
- пример: 16 = 2·2·2·2, НОК = 2·2·2·2·7 = 112
НОК теперь появляется (раньше показывался только НОД):
- причина: smart-подбор брал первый неосвоенный навык ГЛОБАЛЬНО -> из НОД прыгал на lin-basic, НОК не доходил
- фикс: умная тренировка теперь адаптируется В ПРЕДЕЛАХ выбранной темы (pickNext scope = skillsOf(curTopic)) -> в теме «НОД и НОК» ведёт по обоим навыкам; тему выбирает ученик в рейле
Шапка: пилюля стала универсальной и динамической (updateSubjectPill: «Алгебра · 5–9 класс» / «Геометрия · 7–8 класс» по текущему предмету), вместо статичной «Алгебра · 7–8 класс».
Смоук движка 1154/1154, страница 42/42; эмодзи 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,17 @@
|
||||
}
|
||||
function randInt(rng, lo, hi) { return lo + Math.floor(rng() * (hi - lo + 1)); }
|
||||
|
||||
/* Разложение числа на простые множители как строка «2*2*3» (без степеней, по
|
||||
возрастанию). Для отображения в шагах решения (НОД/НОК и т.п.). */
|
||||
function primeFactorString(n) {
|
||||
n = Math.abs(Math.round(n));
|
||||
if (!isFinite(n) || n < 2) return String(n || 0);
|
||||
var fs = [], d = 2;
|
||||
while (d * d <= n) { while (n % d === 0) { fs.push(d); n = n / d; } d++; }
|
||||
if (n > 1) fs.push(n);
|
||||
return fs.join('*');
|
||||
}
|
||||
|
||||
/* ── Уровни сложности: масштабирование диапазона pick ──
|
||||
level 2 — базовый (как задано); 1 — легче (меньше магнитуды, меньше
|
||||
отрицательных); 3 — сложнее (шире магнитуды). Универсально для всех
|
||||
@@ -322,6 +333,15 @@
|
||||
var lhsExpr = render(gen.lhs || 'x', env);
|
||||
var rhsExpr = render(gen.rhs || 'x', env);
|
||||
var sEnv = assign(env, { ans: answer });
|
||||
// factorize: добавляет в шаги решения СТРОКУ разложения на простые множители
|
||||
// (повторяющиеся простые, без степеней: «36» -> «2*2*3*3»). gen.factorize —
|
||||
// массив { name, of }: name — ключ для {name} в шагах, of — выражение-число.
|
||||
if (gen.factorize) {
|
||||
for (var fzi = 0; fzi < gen.factorize.length; fzi++) {
|
||||
var fz = gen.factorize[fzi];
|
||||
sEnv[fz.name] = primeFactorString(evalExpr(fz.of, env));
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -847,20 +847,17 @@
|
||||
{
|
||||
id: 'gcd-pair', topic: 'gcd-lcm', order: 1, subject: 'algebra', grade: 5, kind: 'compute',
|
||||
title: 'НОД двух чисел',
|
||||
pick: { ip: [0, 3], iq: [0, 3], ir: [0, 3] },
|
||||
constraint: 'ip != iq && ip != ir && iq != ir',
|
||||
derive: {
|
||||
p: '(ip==0)?2:((ip==1)?3:((ip==2)?5:7))',
|
||||
q: '(iq==0)?2:((iq==1)?3:((iq==2)?5:7))',
|
||||
r: '(ir==0)?2:((ir==1)?3:((ir==2)?5:7))',
|
||||
a: 'p*q', b: 'p*r', val: 'gcd(a, b)'
|
||||
},
|
||||
pick: { g: [2, 9], m: [2, 8], n: [2, 8] }, constraint: 'm != n',
|
||||
derive: { a: 'g*m', b: 'g*n', val: 'gcd(a, b)' },
|
||||
require: 'a <= 90 && b <= 90',
|
||||
factorize: [{ name: 'aFac', of: 'a' }, { name: 'bFac', of: 'b' }, { name: 'dFac', of: 'gcd(a, b)' }],
|
||||
lhs: 'x', rhs: 'gcd({a}, {b})', display: 'Найдите наибольший общий делитель (НОД) чисел {a} и {b}.',
|
||||
answerVar: 'x', answer: 'val', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Разложим {a} на простые множители:', tex: '{a} = {p} * {q}' },
|
||||
{ note: 'Разложим {b} на простые множители:', tex: '{b} = {p} * {r}' },
|
||||
{ note: 'Общий простой множитель у обоих чисел — {p}. Значит, НОД равен {p}:', tex: 'x = {p}' }
|
||||
{ note: 'Разложим {a} на простые множители:', tex: '{a} = {aFac}' },
|
||||
{ note: 'Разложим {b} на простые множители:', tex: '{b} = {bFac}' },
|
||||
{ note: 'НОД — произведение ОБЩИХ множителей (повторяющиеся берём в наименьшем количестве):', tex: 'x = {dFac}' },
|
||||
{ note: 'Перемножаем:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -868,20 +865,16 @@
|
||||
{
|
||||
id: 'lcm-pair', topic: 'gcd-lcm', order: 2, subject: 'algebra', grade: 6, kind: 'compute',
|
||||
title: 'НОК двух чисел',
|
||||
pick: { ip: [0, 3], iq: [0, 3], ir: [0, 3] },
|
||||
constraint: 'ip != iq && ip != ir && iq != ir',
|
||||
derive: {
|
||||
p: '(ip==0)?2:((ip==1)?3:((ip==2)?5:7))',
|
||||
q: '(iq==0)?2:((iq==1)?3:((iq==2)?5:7))',
|
||||
r: '(ir==0)?2:((ir==1)?3:((ir==2)?5:7))',
|
||||
a: 'p*q', b: 'p*r', val: 'lcm(a, b)'
|
||||
},
|
||||
pick: { g: [2, 9], m: [2, 8], n: [2, 8] }, constraint: 'm != n',
|
||||
derive: { a: 'g*m', b: 'g*n', val: 'lcm(a, b)' },
|
||||
require: 'a <= 60 && b <= 60 && lcm(a, b) <= 240',
|
||||
factorize: [{ name: 'aFac', of: 'a' }, { name: 'bFac', of: 'b' }, { name: 'kFac', of: 'lcm(a, b)' }],
|
||||
lhs: 'x', rhs: 'lcm({a}, {b})', display: 'Найдите наименьшее общее кратное (НОК) чисел {a} и {b}.',
|
||||
answerVar: 'x', answer: 'val', integerAnswer: true,
|
||||
solution: [
|
||||
{ note: 'Разложим {a} на простые множители:', tex: '{a} = {p} * {q}' },
|
||||
{ note: 'Разложим {b} на простые множители:', tex: '{b} = {p} * {r}' },
|
||||
{ note: 'НОК — произведение всех множителей, общий ({p}) берём один раз:', tex: 'x = {p} * {q} * {r}' },
|
||||
{ note: 'Разложим {a} на простые множители:', tex: '{a} = {aFac}' },
|
||||
{ note: 'Разложим {b} на простые множители:', tex: '{b} = {bFac}' },
|
||||
{ note: 'НОК — произведение ВСЕХ множителей (общие берём в наибольшем количестве):', tex: 'x = {kFac}' },
|
||||
{ note: 'Перемножаем:', tex: 'x = {ans}' }
|
||||
]
|
||||
},
|
||||
|
||||
+18
-4
@@ -361,7 +361,7 @@
|
||||
<span class="tr-brand-mark"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.6L18.5 9l-4.7 1.4L12 15l-1.8-4.6L5.5 9l4.7-1.4z"/><path d="M19 14l.7 1.8L21.5 16.5l-1.8.7L19 19l-.7-1.8L16.5 16.5l1.8-.7z"/></svg></span>
|
||||
<div>
|
||||
<h1 class="tr-h1">Тренажёр</h1>
|
||||
<div class="tr-brand-sub"><span class="tr-pill" id="tr-subject">Алгебра · 7–8 класс</span></div>
|
||||
<div class="tr-brand-sub"><span class="tr-pill" id="tr-subject">Математика · 5–9 класс</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tr-modes">
|
||||
@@ -685,13 +685,24 @@
|
||||
topics.forEach(function (t) { if (t.subject && !seen[t.subject]) { seen[t.subject] = 1; out.push(t.subject); } });
|
||||
return out;
|
||||
}
|
||||
var SUBJ_LBL = { algebra: 'Алгебра', geometry: 'Геометрия' };
|
||||
// подпись в шапке: предмет + диапазон классов текущего предмета (универсально 5–9)
|
||||
function updateSubjectPill() {
|
||||
var pill = $('tr-subject'); if (!pill) return;
|
||||
var gr = topics.filter(function (t) { return (t.subject || 'algebra') === curSubject && t.grade; })
|
||||
.map(function (t) { return t.grade; });
|
||||
var name = SUBJ_LBL[curSubject] || 'Математика';
|
||||
if (!gr.length) { pill.textContent = name; return; }
|
||||
var lo = Math.min.apply(null, gr), hi = Math.max.apply(null, gr);
|
||||
pill.textContent = name + ' · ' + (lo === hi ? (lo + ' класс') : (lo + '–' + hi + ' класс'));
|
||||
}
|
||||
function renderSubjects() {
|
||||
updateSubjectPill();
|
||||
var el = $('tr-subjects'); if (!el) return;
|
||||
var subs = presentSubjects();
|
||||
if (subs.length <= 1) { el.innerHTML = ''; return; }
|
||||
var LBL = { algebra: 'Алгебра', geometry: 'Геометрия' };
|
||||
el.innerHTML = subs.map(function (s) {
|
||||
return '<button class="tr-subbtn' + (s === curSubject ? ' on' : '') + '" type="button" data-sub="' + s + '">' + esc(LBL[s] || s) + '</button>';
|
||||
return '<button class="tr-subbtn' + (s === curSubject ? ' on' : '') + '" type="button" data-sub="' + s + '">' + esc(SUBJ_LBL[s] || s) + '</button>';
|
||||
}).join('');
|
||||
}
|
||||
function skillPanelHeader() {
|
||||
@@ -943,7 +954,10 @@
|
||||
function pickNext(lastSkill) {
|
||||
if (!TA) return;
|
||||
var last = (lastSkill !== undefined) ? lastSkill : (curGen ? skillKey(curGen) : null);
|
||||
var id = TA.nextSkill({ ordered: ordered, progress: prog, queue: reviewQ, answered: sessAnswered, last: last });
|
||||
// адаптив в пределах ВЫБРАННОЙ темы: ведёт по её навыкам (простое→сложное) и
|
||||
// возвращает ошибки, не перепрыгивая в другие темы (тему выбирает ученик в рейле).
|
||||
var scope = skillsOf(curTopic); if (!scope || !scope.length) scope = ordered;
|
||||
var id = TA.nextSkill({ ordered: scope, progress: prog, queue: reviewQ, answered: sessAnswered, last: last });
|
||||
var g = id ? gens.filter(function (x) { return skillKey(x) === id; })[0] : null;
|
||||
if (g) { curGen = g; curTopic = g.topic; if (g.subject) curSubject = g.subject; renderSubjects(); renderTopics(); renderSkills(); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user