feat(algebra-8 ch3): Wave 2 — §15 (промежутки + линейные) + §16 (системы)

Новый общий хелпер drawNumLine(opts) — SVG числовая прямая с делениями,
стрелкой и подписями, поддерживает несколько интервалов разных цветов.

§ 15 «Числовые промежутки. Линейные неравенства»:
- Теория: таблица 5 видов промежутков, алгоритм решения линейного
- INTERACT 1: Конструктор промежутка (a, b + 6 типов) → SVG-визуализация
- INTERACT 2: Конвертация записи (8 multiple-choice)
- INTERACT 3: Пошаговый решатель (вводишь a,b,c,d → 4 шага решения
  ax+b ≥ cx+d с обработкой смены знака)
- INTERACT 4: Тренажёр линейных (8 случайных, выбор знака + ввод k)
- INTERACT 5: Drag-сопоставление неравенство ↔ запись промежутка

§ 16 «Системы и совокупности»:
- Теория: система (И, пересечение) и совокупность (ИЛИ, объединение)
- INTERACT 1: Пересечение двух промежутков на SVG-прямой
  (4 слайдера → пересечение зелёным, оригиналы индиго/янтарный)
- INTERACT 2: Пошаговый решатель системы (3 шага + ответ)
- INTERACT 3: Drag «система или совокупность» (8 примеров)
- INTERACT 4: Тренажёр систем (6 случайных)
- INTERACT 5: Совокупность визуально — (-∞;a) ∪ (b;+∞) с слайдерами
This commit is contained in:
Maxim Dolgolyov
2026-05-27 16:18:05 +03:00
parent dc201f28ff
commit a508b6a4da
+586 -2
View File
@@ -1152,8 +1152,592 @@ function init(){
document.addEventListener('DOMContentLoaded', init);
/* STUBS */
function buildP15stub(){ document.getElementById('p15-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>§ 15 — Числовые промежутки. Линейные неравенства</b><br><br>Будет в Wave 2.</p></div></div>${secNav('p14','p16')}`; }
function buildP16stub(){ document.getElementById('p16-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>§ 16 — Системы неравенств</b><br><br>Будет в Wave 2.</p></div></div>${secNav('p15','p17')}`; }
function buildP15stub(){ buildP15(); }
function buildP16stub(){ buildP16(); }
/* Helper: SVG-рисовалка интервала / луча / системы */
function drawNumLine(opts){
const W = 520, H = 80, M = 30;
const min = opts.min != null ? opts.min : -10;
const max = opts.max != null ? opts.max : 10;
const x = v => M + (v - min) * (W - 2*M) / (max - min);
let s = '<svg viewBox="0 0 ' + W + ' ' + H + '">';
// ось
s += '<line x1="' + M + '" y1="40" x2="' + (W - M) + '" y2="40" stroke="#94a3b8" stroke-width="1.5"/>';
// стрелка вправо
s += '<polygon points="' + (W - M) + ',35 ' + (W - M + 10) + ',40 ' + (W - M) + ',45" fill="#94a3b8"/>';
// деления и подписи
for(let v = Math.ceil(min); v <= Math.floor(max); v++){
const px = x(v);
s += '<line x1="' + px + '" y1="36" x2="' + px + '" y2="44" stroke="#94a3b8"/>';
if(v % Math.max(1, Math.floor((max - min) / 10)) === 0 || max - min <= 12) s += '<text x="' + px + '" y="62" text-anchor="middle" font-size="11" fill="#64748b">' + v + '</text>';
}
// интервалы
(opts.intervals || []).forEach(it => {
const color = it.color || 'var(--sec-acc)';
const a = it.a != null ? Math.max(min, it.a) : min;
const b = it.b != null ? Math.min(max, it.b) : max;
const xa = x(a), xb = x(b);
// полоска
s += '<line x1="' + xa + '" y1="40" x2="' + xb + '" y2="40" stroke="' + color + '" stroke-width="6" opacity="0.55"/>';
// концы
if(it.a != null){
if(it.openA) s += '<circle cx="' + xa + '" cy="40" r="6" fill="white" stroke="' + color + '" stroke-width="2.5"/>';
else s += '<circle cx="' + xa + '" cy="40" r="6" fill="' + color + '"/>';
} else {
s += '<polygon points="' + (M-2) + ',32 ' + (M-12) + ',40 ' + (M-2) + ',48" fill="' + color + '"/>';
}
if(it.b != null){
if(it.openB) s += '<circle cx="' + xb + '" cy="40" r="6" fill="white" stroke="' + color + '" stroke-width="2.5"/>';
else s += '<circle cx="' + xb + '" cy="40" r="6" fill="' + color + '"/>';
} else {
s += '<polygon points="' + (W-M+2) + ',32 ' + (W-M+12) + ',40 ' + (W-M+2) + ',48" fill="' + color + '"/>';
}
});
s += '</svg>';
return s;
}
/* ============================================================
§ 15 — ЧИСЛОВЫЕ ПРОМЕЖУТКИ. ЛИНЕЙНЫЕ НЕРАВЕНСТВА
============================================================ */
function buildP15(){
const box = document.getElementById('p15-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Свойства неравенств (§13): при делении на отрицательное знак меняется.</li>
<li>Линейное уравнение $ax + b = 0$ имеет корень $x = -b/a$.</li>
<li>Числовая прямая: точки слева меньше, справа — больше.</li>
</ul>`);
html += makeCard('theory','5 видов промежутков','15.1',`
<table class="tbl" style="margin:8px 0">
<thead><tr><th>Название</th><th>Неравенство</th><th>Запись</th><th>Изображение</th></tr></thead>
<tbody>
<tr><td>Отрезок</td><td>$a \\leq x \\leq b$</td><td>$[a;\\,b]$</td><td>● ━━━ ●</td></tr>
<tr><td>Интервал</td><td>$a < x < b$</td><td>$(a;\\,b)$</td><td>○ ━━━ ○</td></tr>
<tr><td>Полуинтервал</td><td>$a \\leq x < b$</td><td>$[a;\\,b)$</td><td>● ━━━ ○</td></tr>
<tr><td>Луч</td><td>$x \\geq a$</td><td>$[a;\\,+\\infty)$</td><td>● ━━━ →</td></tr>
<tr><td>Открытый луч</td><td>$x < a$</td><td>$(-\\infty;\\,a)$</td><td>← ━━━ ○</td></tr>
</tbody>
</table>
<p style="font-size:.88rem;color:var(--muted)">Запомните: <b>квадратная</b> скобка $[\\,]$ — точка <b>входит</b>. <b>Круглая</b> $(\\,)$ — <b>выколота</b>. У бесконечности всегда круглая.</p>`);
html += makeCard('algo','Решение линейного неравенства','15.2',`
<ol style="margin-left:18px;line-height:1.9">
<li>Раскрыть скобки, если есть.</li>
<li>Перенести члены с $x$ в одну часть, числа — в другую (со сменой знака при переносе).</li>
<li>Привести подобные.</li>
<li>Разделить на коэффициент при $x$. Если он <b>отрицательный — знак неравенства меняется</b>.</li>
<li>Записать ответ как промежуток.</li>
</ol>`);
html += makeCard('example','Пример',null,`
<p><b>Решим:</b> $3x - 7 > 2x + 1$.</p>
<p>1) $3x - 2x > 1 + 7$. 2) $x > 8$. 3) Ответ: $x \\in (8;\\,+\\infty)$.</p>
<p style="margin-top:6px"><b>С отрицательным коэффициентом:</b> $-2x \\geq 6$ &rarr; делим на $-2$: $x \\leq -3$. Ответ: $x \\in (-\\infty;\\,-3]$.</p>`);
/* INT 1 — Конструктор промежутка */
html += widget('Конструктор промежутка','INTERACT 1','Выбери концы $a$, $b$ и тип. Промежуток нарисуется на числовой прямой.',`
<div class="sliders">
<label>$a$ = <b id="p15c-a-val">-2</b><input type="range" min="-9" max="9" step="1" value="-2" id="p15c-a"></label>
<label>$b$ = <b id="p15c-b-val">5</b><input type="range" min="-9" max="9" step="1" value="5" id="p15c-b"></label>
</div>
<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin:8px 0">
<button class="btn small p15c-type active" data-t="cc">$[a;b]$</button>
<button class="btn small p15c-type" data-t="oo">$(a;b)$</button>
<button class="btn small p15c-type" data-t="co">$[a;b)$</button>
<button class="btn small p15c-type" data-t="oc">$(a;b]$</button>
<button class="btn small p15c-type" data-t="rcr">$[a;+\\infty)$</button>
<button class="btn small p15c-type" data-t="rol">$(-\\infty;b)$</button>
</div>
<div class="numline" id="p15c-line"></div>
<div id="p15c-out" style="padding:12px;background:var(--sec-acc-soft);border-radius:10px;text-align:center;font-size:1.1rem"></div>`);
/* INT 2 — Конвертация: запись → неравенство */
html += widget('Конвертация записи','INTERACT 2','Найди соответствие между записью промежутка и неравенством.',`
<div class="score-display"><span>Раунд <b id="p15v-i">1</b> / 8</span><span>Очки: <b id="p15v-score">0</b></span></div>
<div id="p15v-task" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>
<div id="p15v-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p15v-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p15v-start" style="margin-top:10px">Начать</button>`);
/* INT 3 — Пошаговый решатель линейного */
html += widget('Пошаговый решатель','INTERACT 3','Введите $a, b, c, d$ для $ax + b \\geq cx + d$ и нажимайте «Дальше».',`
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:center;margin-bottom:10px;font-size:1.02rem">
<input type="number" id="p15s-a" value="3" class="tinp" style="width:55px">$x +$
<input type="number" id="p15s-b" value="-7" class="tinp" style="width:55px">$\\geq$
<input type="number" id="p15s-c" value="2" class="tinp" style="width:55px">$x +$
<input type="number" id="p15s-d" value="1" class="tinp" style="width:55px">
<button class="btn primary" id="p15s-go">Старт</button>
<button class="btn" id="p15s-next" style="display:none">Дальше</button>
<button class="btn" id="p15s-reset" style="display:none">Сначала</button>
</div>
<div id="p15s-stage" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:80px"></div>`);
/* INT 4 — Тренажёр линейных */
html += widget('Тренажёр линейных','INTERACT 4','Решите неравенство и введите ответ в формате $(a; +\\infty)$ или $[-\\infty; b)$. Используйте `inf` для бесконечности.',`
<div class="score-display"><span>Задача <b id="p15t-i">1</b> / 8</span><span>Очки: <b id="p15t-score">0</b></span></div>
<div id="p15t-task" style="font-size:1.3rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap">
<button class="btn" data-cmp="gt" id="p15t-gt">$x > k$</button>
<button class="btn" data-cmp="ge" id="p15t-ge">$x \\geq k$</button>
<button class="btn" data-cmp="lt" id="p15t-lt">$x < k$</button>
<button class="btn" data-cmp="le" id="p15t-le">$x \\leq k$</button>
</div>
<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin-top:6px">
<input type="number" id="p15t-k" placeholder="k =" class="tinp" style="width:90px">
<button class="btn primary" id="p15t-go">Ответ</button>
</div>
<div class="feedback" id="p15t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p15t-start" style="margin-top:10px">Начать</button>`);
/* INT 5 — Drag: какой промежуток */
html += widget('Сопоставь неравенство и промежуток','INTERACT 5','Отнеси каждое неравенство к правильной записи промежутка.',`
${DND_HINT_HTML}
<div id="p15d-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px">
<div class="drop-box"><h5>$[a;b]$</h5><div class="drop-items" data-cat="cc"></div></div>
<div class="drop-box"><h5>$(a;b)$</h5><div class="drop-items" data-cat="oo"></div></div>
<div class="drop-box"><h5>$(a;+\\infty)$</h5><div class="drop-items" data-cat="rop"></div></div>
<div class="drop-box"><h5>$(-\\infty;b]$</h5><div class="drop-items" data-cat="lcr"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p15d-check">Проверить</button><button class="btn" id="p15d-reset">Сначала</button></div>
<div class="feedback" id="p15d-fb" style="display:none"></div>`);
html += makeCard('oral','Устно',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Запишите промежуток для $x > 5$.</li>
<li>Что означает скобка $[$?</li>
<li>Решите устно: $2x > 6$.</li>
</ol>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$5x - 3 < 12$</li>
<li>$-3x + 1 \\geq 7$</li>
<li>$2(x - 1) > 3(x + 2)$</li>
<li>Запишите $[-2; 5)$ через неравенство.</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$7x + 2 \\leq 4x - 7$</li>
<li>$-\\dfrac{x}{3} > 2$</li>
<li>$3 - 2x \\leq x + 9$</li>
<li>Изобразите $(-\\infty; -1) \\cup [3; +\\infty)$.</li>
</ol>`);
html += secNav('p14', 'p16');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Конструктор */
(function(){
const aE = document.getElementById('p15c-a'), bE = document.getElementById('p15c-b');
const lineE = document.getElementById('p15c-line'), out = document.getElementById('p15c-out');
let type = 'cc', done = false;
document.querySelectorAll('.p15c-type').forEach(btn => btn.addEventListener('click', ()=>{
document.querySelectorAll('.p15c-type').forEach(x => x.classList.remove('active'));
btn.classList.add('active'); type = btn.dataset.t; refresh();
}));
function refresh(){
const a = +aE.value, b = +bE.value;
document.getElementById('p15c-a-val').textContent = a;
document.getElementById('p15c-b-val').textContent = b;
let interval = null, label = '', ineq = '';
if(type === 'cc'){ if(b < a){ out.innerHTML = 'Нужно $a \\leq b$'; renderMath(out); return; } interval = { a, b, openA:false, openB:false }; label = '$[' + a + ';\\,' + b + ']$'; ineq = a + ' \\leq x \\leq ' + b; }
else if(type === 'oo'){ if(b <= a){ out.innerHTML = 'Нужно $a < b$'; renderMath(out); return; } interval = { a, b, openA:true, openB:true }; label = '$(' + a + ';\\,' + b + ')$'; ineq = a + ' < x < ' + b; }
else if(type === 'co'){ if(b <= a){ out.innerHTML = 'Нужно $a < b$'; renderMath(out); return; } interval = { a, b, openA:false, openB:true }; label = '$[' + a + ';\\,' + b + ')$'; ineq = a + ' \\leq x < ' + b; }
else if(type === 'oc'){ if(b <= a){ out.innerHTML = 'Нужно $a < b$'; renderMath(out); return; } interval = { a, b, openA:true, openB:false }; label = '$(' + a + ';\\,' + b + ']$'; ineq = a + ' < x \\leq ' + b; }
else if(type === 'rcr'){ interval = { a, b:null, openA:false, openB:false }; label = '$[' + a + ';\\,+\\infty)$'; ineq = 'x \\geq ' + a; }
else if(type === 'rol'){ interval = { a:null, b, openA:false, openB:true }; label = '$(-\\infty;\\,' + b + ')$'; ineq = 'x < ' + b; }
lineE.innerHTML = drawNumLine({ intervals:[interval] });
out.innerHTML = '<div><b>Запись:</b> ' + label + '</div><div><b>Неравенство:</b> $' + ineq + '$</div>';
renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p15_line'); bumpProgress('p15', 14); }, 300); }
}
[aE,bE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
/* INIT 2 — Конвертация */
(function(){
const tasks = [
{ q:'Запись: $[3;\\,7]$. Какое неравенство?', opts:['$3 \\leq x \\leq 7$','$3 < x < 7$','$x \\geq 3$','$3 \\leq x < 7$'], ok:0 },
{ q:'Неравенство: $-2 < x \\leq 5$. Какая запись?', opts:['$(-2;\\,5]$','$[-2;\\,5)$','$[-2;\\,5]$','$(-2;\\,5)$'], ok:0 },
{ q:'Запись: $(-\\infty;\\,4)$. Что значит?', opts:['$x < 4$','$x \\leq 4$','$x > 4$','$-\\infty < x < 4$ строго'], ok:0 },
{ q:'Неравенство: $x \\geq 6$. Какая запись?', opts:['$[6;\\,+\\infty)$','$(6;\\,+\\infty)$','$(-\\infty;\\,6]$','$[-6;\\,+\\infty)$'], ok:0 },
{ q:'Запись: $(0;\\,1)$. Содержит ли точку $0$?', opts:['Нет, выколота','Да, входит','Только если $0 > 0$','Зависит от контекста'], ok:0 },
{ q:'Запись с двумя круглыми скобками означает:', opts:['Оба конца выколоты','Оба конца входят','Один выколот, один входит','Только справа выколот'], ok:0 },
{ q:'Какой записью обозначить «все числа меньше или равны $-3$»?', opts:['$(-\\infty;\\,-3]$','$(-\\infty;\\,-3)$','$[-3;\\,+\\infty)$','$(-3;\\,+\\infty)$'], ok:0 },
{ q:'Промежуток $[2;\\,2]$ содержит:', opts:['Только число $2$','Все числа от $0$ до $2$','Пуст','Все числа $\\geq 2$'], ok:0 },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p15v-i').textContent = i;
document.getElementById('p15v-task').innerHTML = cur.q;
renderMath(document.getElementById('p15v-task'));
const opts = document.getElementById('p15v-opts'); opts.innerHTML = '';
cur.opts.forEach((o, k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>{
const fb = document.getElementById('p15v-fb'); fb.style.display = 'block';
if(k === cur.ok){ score++; b.classList.add('ok'); feedback(fb, true, '&#10003;'); }
else { b.classList.add('fail'); feedback(fb, false, 'Не то.'); }
document.getElementById('p15v-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 6, 'Итог: ' + score + '/' + shuffled.length); if(score >= 6){ achievement('p15_convert'); bumpProgress('p15', 14); confetti(); } }, 600); }
else { i++; setTimeout(show, 800); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p15v-fb').style.display = 'none';
}
document.getElementById('p15v-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p15v-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
/* INIT 3 — Шаговый решатель */
(function(){
const stage = document.getElementById('p15s-stage');
const goBtn = document.getElementById('p15s-go'), nextBtn = document.getElementById('p15s-next'), resetBtn = document.getElementById('p15s-reset');
let steps = [], idx = 0, awarded = false;
function build(a, b, c, d){
// ax + b >= cx + d -> (a-c)x >= d-b
const k = a - c, r = d - b;
const arr = [];
arr.push('<b>Дано:</b> $' + a + 'x ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + ' \\geq ' + c + 'x ' + (d >= 0 ? '+ ' + d : '- ' + Math.abs(d)) + '$');
arr.push('<b>Шаг 1.</b> Переносим $x$ влево, числа — вправо: $' + a + 'x - ' + c + 'x \\geq ' + d + ' - (' + b + ') \\Rightarrow ' + k + 'x \\geq ' + r + '$');
if(k === 0){
arr.push('<b>Шаг 2.</b> $0 \\cdot x \\geq ' + r + '$. ' + (r <= 0 ? 'Верно для любого $x$ — решение $x \\in \\mathbb{R}$.' : 'Невозможно — решений нет.'));
arr.push('<b>Ответ:</b> ' + (r <= 0 ? '$(-\\infty;\\,+\\infty)$' : 'нет решений'));
} else if(k > 0){
arr.push('<b>Шаг 2.</b> Делим на $' + k + ' > 0$: $x \\geq ' + r + '/' + k + ' = ' + fmt(r/k) + '$');
arr.push('<b>Ответ:</b> $x \\in [' + fmt(r/k) + ';\\,+\\infty)$');
} else {
arr.push('<b>Шаг 2.</b> Делим на $' + k + ' < 0$ — <span style="color:var(--bad);font-weight:700">знак меняется!</span> $x \\leq ' + r + '/' + k + ' = ' + fmt(r/k) + '$');
arr.push('<b>Ответ:</b> $x \\in (-\\infty;\\,' + fmt(r/k) + ']$');
}
return arr;
}
function render(){
stage.innerHTML = steps.slice(0, idx + 1).map(s => `<div style="margin:6px 0;padding:9px 12px;background:var(--card);border-left:3px solid var(--sec-acc);border-radius:7px;animation:fadeIn .3s ease">${s}</div>`).join('');
renderMath(stage);
if(idx >= steps.length - 1){
nextBtn.disabled = true; nextBtn.textContent = 'Готово';
if(!awarded){ awarded = true; achievement('p15_solver'); bumpProgress('p15', 14); confetti(); }
} else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
}
goBtn.addEventListener('click', ()=>{
const a = +document.getElementById('p15s-a').value, b = +document.getElementById('p15s-b').value;
const c = +document.getElementById('p15s-c').value, d = +document.getElementById('p15s-d').value;
steps = build(a, b, c, d); idx = 0; awarded = false;
goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = '';
render();
});
nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
resetBtn.addEventListener('click', ()=>{ idx = 0; stage.innerHTML = ''; goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none'; });
})();
/* INIT 4 — Тренажёр линейных */
(function(){
function gen(){
const a = (Math.random() < 0.5 ? -1 : 1) * (1 + Math.floor(Math.random()*4));
const b = -5 + Math.floor(Math.random()*11);
const c = -5 + Math.floor(Math.random()*11);
// ax + b > c
const v = (c - b) / a;
// знак ответа зависит от знака a
const baseCmp = ['gt','ge','lt','le'][Math.floor(Math.random()*4)];
const flipped = a < 0 ? { gt:'lt', ge:'le', lt:'gt', le:'ge' }[baseCmp] : baseCmp;
return { a, b, c, baseCmp, ansCmp: flipped, ansK: v, txt: a + 'x ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + ' ' + ({gt:'>',ge:'\\geq',lt:'<',le:'\\leq'})[baseCmp] + ' ' + c };
}
let cur = null, i = 1, score = 0, chosen = null;
function show(){
cur = gen();
document.getElementById('p15t-i').textContent = i;
document.getElementById('p15t-task').innerHTML = '$' + cur.txt + '$';
renderMath(document.getElementById('p15t-task'));
document.getElementById('p15t-k').value = '';
chosen = null;
document.querySelectorAll('#p15t-gt,#p15t-ge,#p15t-lt,#p15t-le').forEach(b => b.classList.remove('primary'));
document.getElementById('p15t-fb').style.display = 'none';
}
function check(){
const fb = document.getElementById('p15t-fb'); fb.style.display = 'block';
const u = +document.getElementById('p15t-k').value;
const ok = chosen === cur.ansCmp && Math.abs(u - cur.ansK) < 1e-6;
if(ok){ score++; feedback(fb, true, '&#10003; $x ' + ({gt:'>',ge:'\\geq',lt:'<',le:'\\leq'})[cur.ansCmp] + ' ' + fmt(cur.ansK) + '$'); renderMath(fb); }
else feedback(fb, false, 'Правильно: $x ' + ({gt:'>',ge:'\\geq',lt:'<',le:'\\leq'})[cur.ansCmp] + ' ' + fmt(cur.ansK) + '$');
renderMath(fb);
document.getElementById('p15t-score').textContent = score;
if(i >= 8){ setTimeout(()=>{ feedback(fb, score >= 5, 'Итог: ' + score + '/8'); if(score >= 5){ achievement('p15_train'); bumpProgress('p15', 16); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
}
document.getElementById('p15t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p15t-score').textContent = 0; show(); });
['gt','ge','lt','le'].forEach(c => document.getElementById('p15t-' + c).addEventListener('click', ()=>{
chosen = c;
document.querySelectorAll('#p15t-gt,#p15t-ge,#p15t-lt,#p15t-le').forEach(b => b.classList.remove('primary'));
document.getElementById('p15t-' + c).classList.add('primary');
}));
document.getElementById('p15t-go').addEventListener('click', ()=>{ if(!chosen){ const fb = document.getElementById('p15t-fb'); fb.style.display='block'; feedback(fb, false, 'Выберите знак.'); return; } check(); });
})();
/* INIT 5 — Drag */
(function(){
const items = [
{ id:1, html:'$2 \\leq x \\leq 7$', cat:'cc' },
{ id:2, html:'$-3 < x < 4$', cat:'oo' },
{ id:3, html:'$x > 5$', cat:'rop' },
{ id:4, html:'$x \\leq 0$', cat:'lcr' },
{ id:5, html:'$-1 \\leq x \\leq 6$', cat:'cc' },
{ id:6, html:'$0 < x < 10$', cat:'oo' },
{ id:7, html:'$x > 8$', cat:'rop' },
{ id:8, html:'$x \\leq -2$', cat:'lcr' },
];
const sorter = setupSorter({ poolId:'p15d-pool', cats:['cc','oo','rop','lcr'], items, scopeSelector:'#p15-body' });
document.getElementById('p15d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p15d-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все верно!'); achievement('p15_drag'); bumpProgress('p15', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p15d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p15d-fb').style.display='none'; });
})();
}
/* ============================================================
§ 16 — СИСТЕМЫ И СОВОКУПНОСТИ НЕРАВЕНСТВ
============================================================ */
function buildP16(){
const box = document.getElementById('p16-body');
let html = '';
html += makeCard('repeat','Повторение § 15',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Промежутки: $[a;b]$, $(a;b)$, лучи и др.</li>
<li>Решение линейного: изоляция $x$, не забывать про знак при делении на отрицательное.</li>
</ul>`);
html += makeCard('theory','Система и совокупность','16.1',`
<p><b>Система</b> неравенств — несколько неравенств, выполненных <b>одновременно</b> («И»). Решение — <b>пересечение</b> решений каждого. Записывается фигурной скобкой:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:10px;margin:8px 0;text-align:center;font-size:1.05rem">$$\\begin{cases} x \\geq 1 \\\\ x < 5 \\end{cases} \\Rightarrow x \\in [1;\\,5)$$</div>
<p><b>Совокупность</b> — неравенства, из которых выполняется <b>хотя бы одно</b> («ИЛИ»). Решение — <b>объединение</b>. Записывается квадратной скобкой:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:10px;margin:8px 0;text-align:center;font-size:1.05rem">$$\\left[\\begin{array}{l} x < -2 \\\\ x \\geq 3 \\end{array}\\right. \\Rightarrow x \\in (-\\infty;\\,-2) \\cup [3;\\,+\\infty)$$</div>`);
html += makeCard('algo','Алгоритм решения системы',null,`
<ol style="margin-left:18px;line-height:1.9">
<li>Решить каждое неравенство отдельно.</li>
<li>Изобразить решения на одной числовой прямой.</li>
<li>Найти пересечение (общую часть для системы) или объединение (для совокупности).</li>
<li>Записать ответ как промежуток.</li>
</ol>`);
html += makeCard('example','Пример',null,`
<p><b>Решим систему:</b> $\\begin{cases} 2x - 1 > 3 \\\\ x + 4 \\leq 10 \\end{cases}$</p>
<p>Первое: $2x > 4 \\Rightarrow x > 2$, т.е. $(2;\\,+\\infty)$.</p>
<p>Второе: $x \\leq 6$, т.е. $(-\\infty;\\,6]$.</p>
<p>Пересечение: $x \\in (2;\\,6]$.</p>`);
/* INT 1 — Пересечение промежутков */
html += widget('Пересечение двух промежутков','INTERACT 1','Сдвигай границы каждого промежутка и наблюдай пересечение.',`
<div class="sliders">
<label>$a_1$ = <b id="p16i-a1-val">-3</b><input type="range" min="-9" max="9" step="1" value="-3" id="p16i-a1"></label>
<label>$b_1$ = <b id="p16i-b1-val">4</b><input type="range" min="-9" max="9" step="1" value="4" id="p16i-b1"></label>
<label>$a_2$ = <b id="p16i-a2-val">1</b><input type="range" min="-9" max="9" step="1" value="1" id="p16i-a2"></label>
<label>$b_2$ = <b id="p16i-b2-val">7</b><input type="range" min="-9" max="9" step="1" value="7" id="p16i-b2"></label>
</div>
<div class="numline" id="p16i-line"></div>
<div id="p16i-out" style="padding:12px;background:var(--sec-acc-soft);border-radius:10px;text-align:center;font-size:1.05rem;line-height:1.8"></div>`);
/* INT 2 — Пошаговый решатель */
html += widget('Шаговый решатель системы','INTERACT 2','Решаем систему пошагово: каждое неравенство отдельно, затем пересечение.',`
<p style="margin-bottom:10px"><b>Система:</b> $\\begin{cases} 3x - 5 \\geq 1 \\\\ -2x + 4 > -6 \\end{cases}$</p>
<div id="p16s-stage" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;line-height:1.8;min-height:80px"></div>
<div class="actions" style="margin-top:10px"><button class="btn primary" id="p16s-go">Старт</button><button class="btn" id="p16s-next" style="display:none">Дальше</button><button class="btn" id="p16s-reset" style="display:none">Сначала</button></div>`);
/* INT 3 — Drag: система или совокупность */
html += widget('Что это: система или совокупность?','INTERACT 3','По логической связке («И» / «ИЛИ») определи тип записи.',`
${DND_HINT_HTML}
<div id="p16d-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="drop-box"><h5>Система (И, пересечение)</h5><div class="drop-items" data-cat="sys"></div></div>
<div class="drop-box"><h5>Совокупность (ИЛИ, объединение)</h5><div class="drop-items" data-cat="sov"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p16d-check">Проверить</button><button class="btn" id="p16d-reset">Сначала</button></div>
<div class="feedback" id="p16d-fb" style="display:none"></div>`);
/* INT 4 — Тренажёр систем */
html += widget('Тренажёр систем','INTERACT 4','Решите систему и введите ответ как промежуток.',`
<div class="score-display"><span>Задача <b id="p16t-i">1</b> / 6</span><span>Очки: <b id="p16t-score">0</b></span></div>
<div id="p16t-task" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>
<div id="p16t-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p16t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p16t-start" style="margin-top:10px">Начать</button>`);
/* INT 5 — Совокупность визуально */
html += widget('Совокупность: объединение','INTERACT 5','Двигай $a$ и $b$, смотри, как меняется $(-\\infty;a) \\cup (b;+\\infty)$.',`
<div class="sliders">
<label>$a$ = <b id="p16u-a-val">-2</b><input type="range" min="-9" max="9" step="1" value="-2" id="p16u-a"></label>
<label>$b$ = <b id="p16u-b-val">3</b><input type="range" min="-9" max="9" step="1" value="3" id="p16u-b"></label>
</div>
<div class="numline" id="p16u-line"></div>
<div id="p16u-out" style="padding:12px;background:var(--sec-acc-soft);border-radius:10px;text-align:center;font-size:1.05rem"></div>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$\\begin{cases} x > 2 \\\\ x < 8 \\end{cases}$</li>
<li>$\\begin{cases} 2x - 1 \\leq 5 \\\\ x + 3 > -2 \\end{cases}$</li>
<li>$\\left[\\begin{array}{l} x < -1 \\\\ x \\geq 4 \\end{array}\\right.$</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$\\begin{cases} 3x + 1 > 7 \\\\ x - 2 \\leq 3 \\end{cases}$</li>
<li>$\\begin{cases} 5 - 2x \\geq 1 \\\\ x + 1 > -3 \\end{cases}$</li>
<li>$\\left[\\begin{array}{l} 2x > 8 \\\\ -x \\geq 2 \\end{array}\\right.$</li>
</ol>`);
html += secNav('p15', 'p17');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Пересечение промежутков */
(function(){
const ids = ['p16i-a1','p16i-b1','p16i-a2','p16i-b2'];
const lineE = document.getElementById('p16i-line'), out = document.getElementById('p16i-out');
let done = false;
function refresh(){
const a1 = +document.getElementById('p16i-a1').value, b1 = +document.getElementById('p16i-b1').value;
const a2 = +document.getElementById('p16i-a2').value, b2 = +document.getElementById('p16i-b2').value;
document.getElementById('p16i-a1-val').textContent = a1;
document.getElementById('p16i-b1-val').textContent = b1;
document.getElementById('p16i-a2-val').textContent = a2;
document.getElementById('p16i-b2-val').textContent = b2;
const intervals = [];
if(a1 <= b1) intervals.push({ a:a1, b:b1, openA:false, openB:false, color:'#6366f1' });
if(a2 <= b2) intervals.push({ a:a2, b:b2, openA:false, openB:false, color:'#f59e0b' });
const lo = Math.max(a1, a2), hi = Math.min(b1, b2);
if(lo <= hi) intervals.push({ a:lo, b:hi, openA:false, openB:false, color:'#10b981' });
lineE.innerHTML = drawNumLine({ intervals });
let s = '<div><b style="color:#6366f1">[' + a1 + ';' + b1 + ']</b> ∩ <b style="color:#f59e0b">[' + a2 + ';' + b2 + ']</b> = ';
if(lo > hi) s += '<span style="color:var(--bad)">∅ (пусто)</span></div>';
else s += '<b style="color:#10b981">[' + lo + ';' + hi + ']</b></div>';
out.innerHTML = s;
if(!done){ done = true; setTimeout(()=>{ achievement('p16_intersect'); bumpProgress('p16', 14); }, 300); }
}
ids.forEach(id => document.getElementById(id).addEventListener('input', refresh));
refresh();
})();
/* INIT 2 — Шаговый решатель */
(function(){
const stage = document.getElementById('p16s-stage');
const goBtn = document.getElementById('p16s-go'), nextBtn = document.getElementById('p16s-next'), resetBtn = document.getElementById('p16s-reset');
const steps = [
'<b>Шаг 1.</b> Решим первое: $3x - 5 \\geq 1 \\Rightarrow 3x \\geq 6 \\Rightarrow x \\geq 2$, т.е. $[2;\\,+\\infty)$.',
'<b>Шаг 2.</b> Решим второе: $-2x + 4 > -6 \\Rightarrow -2x > -10 \\Rightarrow x < 5$ (знак сменился!), т.е. $(-\\infty;\\,5)$.',
'<b>Шаг 3.</b> Пересечение: $[2;\\,+\\infty) \\cap (-\\infty;\\,5) = [2;\\,5)$.',
'<b>Ответ:</b> $x \\in [2;\\,5)$.',
];
let idx = 0, awarded = false;
function render(){
stage.innerHTML = steps.slice(0, idx + 1).map(s => `<div style="margin:6px 0;padding:9px 12px;background:var(--card);border-left:3px solid var(--sec-acc);border-radius:7px;animation:fadeIn .3s ease">${s}</div>`).join('');
renderMath(stage);
if(idx >= steps.length - 1){
nextBtn.disabled = true; nextBtn.textContent = 'Готово';
if(!awarded){ awarded = true; achievement('p16_solver'); bumpProgress('p16', 14); confetti(); }
} else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
}
goBtn.addEventListener('click', ()=>{ idx = 0; awarded = false; goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = ''; render(); });
nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
resetBtn.addEventListener('click', ()=>{ idx = 0; stage.innerHTML = ''; goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none'; });
})();
/* INIT 3 — Drag система/совокупность */
(function(){
const items = [
{ id:1, html:'$\\begin{cases} x > 2 \\\\ x \\leq 5 \\end{cases}$', cat:'sys' },
{ id:2, html:'$\\left[\\begin{array}{l} x < -3 \\\\ x \\geq 4 \\end{array}\\right.$', cat:'sov' },
{ id:3, html:'Оба условия одновременно', cat:'sys' },
{ id:4, html:'Хотя бы одно условие', cat:'sov' },
{ id:5, html:'$\\begin{cases} x \\geq -1 \\\\ x < 7 \\end{cases}$', cat:'sys' },
{ id:6, html:'$\\left[\\begin{array}{l} x \\leq 0 \\\\ x > 5 \\end{array}\\right.$', cat:'sov' },
{ id:7, html:'Пересечение решений', cat:'sys' },
{ id:8, html:'Объединение решений', cat:'sov' },
];
const sorter = setupSorter({ poolId:'p16d-pool', cats:['sys','sov'], items, scopeSelector:'#p16-body' });
document.getElementById('p16d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p16d-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все верно!'); achievement('p16_union'); bumpProgress('p16', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p16d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p16d-fb').style.display='none'; });
})();
/* INIT 4 — Тренажёр систем */
(function(){
const tasks = [
{ q:'$\\begin{cases} x > 3 \\\\ x \\leq 7 \\end{cases}$', opts:['$(3;\\,7]$','$[3;\\,7)$','$(3;\\,7)$','$\\emptyset$'], ok:0 },
{ q:'$\\begin{cases} x \\geq -2 \\\\ x < 4 \\end{cases}$', opts:['$[-2;\\,4)$','$(-2;\\,4)$','$[-2;\\,4]$','$\\emptyset$'], ok:0 },
{ q:'$\\begin{cases} x > 5 \\\\ x < 2 \\end{cases}$', opts:['$\\emptyset$ (нет решений)','$(2;\\,5)$','$(5;\\,2)$','любое $x$'], ok:0 },
{ q:'$\\left[\\begin{array}{l} x < -1 \\\\ x \\geq 3 \\end{array}\\right.$', opts:['$(-\\infty;\\,-1) \\cup [3;\\,+\\infty)$','$(-1;\\,3)$','$[-1;\\,3)$','$\\emptyset$'], ok:0 },
{ q:'$\\begin{cases} 2x \\geq 6 \\\\ x \\leq 10 \\end{cases}$', opts:['$[3;\\,10]$','$[3;\\,10)$','$(3;\\,10]$','$[6;\\,10]$'], ok:0 },
{ q:'$\\begin{cases} x > 0 \\\\ x < 0 \\end{cases}$', opts:['$\\emptyset$','$\\{0\\}$','$\\mathbb{R}$','$(-\\infty;\\,0)$'], ok:0 },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p16t-i').textContent = i;
document.getElementById('p16t-task').innerHTML = cur.q;
renderMath(document.getElementById('p16t-task'));
const opts = document.getElementById('p16t-opts'); opts.innerHTML = '';
cur.opts.forEach((o, k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>{
const fb = document.getElementById('p16t-fb'); fb.style.display = 'block';
if(k === cur.ok){ score++; b.classList.add('ok'); feedback(fb, true, '&#10003;'); }
else { b.classList.add('fail'); feedback(fb, false, 'Не то.'); }
document.getElementById('p16t-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p16_train'); bumpProgress('p16', 16); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p16t-fb').style.display = 'none';
}
document.getElementById('p16t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p16t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
/* INIT 5 — Совокупность визуально */
(function(){
const aE = document.getElementById('p16u-a'), bE = document.getElementById('p16u-b');
const lineE = document.getElementById('p16u-line'), out = document.getElementById('p16u-out');
function refresh(){
const a = +aE.value, b = +bE.value;
document.getElementById('p16u-a-val').textContent = a;
document.getElementById('p16u-b-val').textContent = b;
const intervals = [
{ a:null, b:a, openA:false, openB:true, color:'#8b5cf6' },
{ a:b, b:null, openA:true, openB:false, color:'#8b5cf6' },
];
lineE.innerHTML = drawNumLine({ intervals });
out.innerHTML = '<b>Совокупность:</b> $x < ' + a + '$ или $x > ' + b + '$. Решение: $(-\\infty;\\,' + a + ') \\cup (' + b + ';\\,+\\infty)$.';
renderMath(out);
}
[aE,bE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
}
function buildP17stub(){ document.getElementById('p17-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>§ 17 — Квадратные неравенства. Метод интервалов</b><br><br>Будет в Wave 3.</p></div></div>${secNav('p16','p18')}`; }
function buildP18stub(){ document.getElementById('p18-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>§ 18 — Дробно-рациональные неравенства</b><br><br>Будет в Wave 3.</p></div></div>${secNav('p17','final3')}`; }
function buildFinal3stub(){ document.getElementById('final3-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>Финал главы</b><br><br>Будет в Wave 4 — 7 боссов, увлекательная математика, практика.</p></div></div>${secNav('p18',null)}`; }