feat(algebra-8 ch3): Wave 3 — §17 (метод интервалов) + §18 (дробно-рац.)

§ 17 «Квадратные неравенства. Метод интервалов»:
- Теория: парабола, метод интервалов, правило знаков
- INTERACT 1: SVG-парабола + слайдеры a, b, c с цветовой раскраской
  на оси: зелёные зоны = выражение > 0, красные = < 0. Корни как
  точки. Внизу — текстовый анализ (D, корни, решение для >0 и <0).
- INTERACT 2: Пошаговый решатель — D, корни, знаки, ответ
  (4-5 шагов с обработкой D<0 и D=0)
- INTERACT 3: Тренажёр 6 квадратных (multiple-choice)
- INTERACT 4: Drag-сопоставление (a, D, направление неравенства) →
  тип ответа: вне корней / между / R / пусто
- INTERACT 5: «Где плюс, где минус?» — кликаем по 3 интервалам
  параболы x²−4x+3, ставим знаки. Победа = +, -, +.

§ 18 «Дробно-рациональные»:
- Теория: f/g ≷ 0, выколотые точки знаменателя, алгоритм
- INTERACT 1: Пошаговый решатель (x-a)/(x-b) ≥ 0 с учётом a vs b
  (включая случай a == b)
- INTERACT 2: Тренажёр 6 неравенств (multiple-choice)
- INTERACT 3: Найди ОДЗ — 5 выражений, вводим запрещённые точки
- INTERACT 4: Drag «закрашена/выколота» — 8 ситуаций
This commit is contained in:
Maxim Dolgolyov
2026-05-27 16:21:49 +03:00
parent a508b6a4da
commit b540c1b3a0
+545 -2
View File
@@ -1738,8 +1738,551 @@ function buildP16(){
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 buildP17stub(){ buildP17(); }
function buildP18stub(){ buildP18(); }
/* ============================================================
§ 17 — КВАДРАТНЫЕ НЕРАВЕНСТВА. МЕТОД ИНТЕРВАЛОВ
============================================================ */
function buildP17(){
const box = document.getElementById('p17-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Квадратное уравнение $ax^2 + bx + c = 0$ (Глава 2).</li>
<li>Дискриминант $D = b^2 - 4ac$, корни $x_{1,2} = \\dfrac{-b \\pm \\sqrt{D}}{2a}$.</li>
<li>Парабола: при $a > 0$ ветви вверх, при $a < 0$ — вниз.</li>
</ul>`);
html += makeCard('theory','Что такое квадратное неравенство','17.1',`
<p><b>Определение.</b> Неравенство вида $ax^2 + bx + c \\gtrless 0$, $a \\neq 0$.</p>
<p style="margin-top:6px"><b>Геометрический смысл:</b> найти, где парабола $y = ax^2 + bx + c$ <i>выше</i> или <i>ниже</i> оси OX (в зависимости от знака неравенства).</p>`);
html += makeCard('algo','Метод интервалов','17.2',`
<ol style="margin-left:18px;line-height:1.9">
<li>Найти корни уравнения $ax^2 + bx + c = 0$.</li>
<li>Если $D < 0$ — знак выражения постоянен (совпадает со знаком $a$).</li>
<li>Если $D \\geq 0$ — отметить корни на числовой прямой.</li>
<li>Определить знак выражения на каждом интервале (можно подставить пробную точку).</li>
<li>Выбрать интервалы, удовлетворяющие неравенству.</li>
</ol>
<p style="margin-top:8px"><b>Правило знаков для квадратного:</b> при $a > 0$ — между корнями знак минус, вне — плюс. При $a < 0$ — наоборот.</p>`);
html += makeCard('example','Пример',null,`
<p><b>Решить</b> $x^2 - 5x + 6 > 0$.</p>
<p>Корни: $x_1 = 2$, $x_2 = 3$. $a = 1 > 0$ — парабола вверх.</p>
<p>Знаки: $+$ при $x<2$, $-$ при $2<x<3$, $+$ при $x>3$.</p>
<p><b>Ответ:</b> $x \\in (-\\infty;\\,2) \\cup (3;\\,+\\infty)$.</p>`);
/* INT 1 — Парабола + закраска */
html += widget('Парабола и знак','INTERACT 1','Двигай $a, b, c$. Парабола показывает, где выражение положительно (зелёное) и отрицательно (красное).',`
<div class="sliders">
<label>$a$ = <b id="p17p-a-val">1</b><input type="range" min="-3" max="3" step="0.5" value="1" id="p17p-a"></label>
<label>$b$ = <b id="p17p-b-val">-1</b><input type="range" min="-6" max="6" step="0.5" value="-1" id="p17p-b"></label>
<label>$c$ = <b id="p17p-c-val">-6</b><input type="range" min="-8" max="8" step="0.5" value="-6" id="p17p-c"></label>
</div>
<svg id="p17p-svg" viewBox="-10 -10 240 180" style="width:100%;max-width:520px;display:block;margin:10px auto;background:#fafafa;border:1px solid var(--border);border-radius:10px"></svg>
<div id="p17p-out" style="padding:12px;background:var(--sec-acc-soft);border-radius:10px;line-height:1.7"></div>`);
/* INT 2 — Шаговый решатель */
html += widget('Метод интервалов: шаг за шагом','INTERACT 2','Введите $a, b, c$ для $ax^2 + bx + c \\geq 0$ и нажимайте «Дальше».',`
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:center;margin-bottom:10px">
<label>$a$ = <input type="number" id="p17s-a" value="1" class="tinp" style="width:60px"></label>
<label>$b$ = <input type="number" id="p17s-b" value="-5" class="tinp" style="width:60px"></label>
<label>$c$ = <input type="number" id="p17s-c" value="6" class="tinp" style="width:60px"></label>
<button class="btn primary" id="p17s-go">Старт</button>
<button class="btn" id="p17s-next" style="display:none">Дальше</button>
<button class="btn" id="p17s-reset" style="display:none">Сначала</button>
</div>
<div id="p17s-stage" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:80px"></div>`);
/* INT 3 — Тренажёр */
html += widget('Тренажёр квадратных неравенств','INTERACT 3','Решите неравенство и выберите правильный промежуток-ответ.',`
<div class="score-display"><span>Задача <b id="p17t-i">1</b> / 6</span><span>Очки: <b id="p17t-score">0</b></span></div>
<div id="p17t-task" style="font-size:1.2rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div id="p17t-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p17t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p17t-start" style="margin-top:10px">Начать</button>`);
/* INT 4 — Drag: парабола ↔ ответ */
html += widget('Сопоставь параболу и неравенство','INTERACT 4','По описанию ситуации (направление ветвей + расположение корней) подбери решение неравенства $\\geq 0$.',`
${DND_HINT_HTML}
<div id="p17d-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px">
<div class="drop-box"><h5>$(-\\infty; x_1] \\cup [x_2; +\\infty)$</h5><div class="drop-items" data-cat="out"></div></div>
<div class="drop-box"><h5>$[x_1; x_2]$</h5><div class="drop-items" data-cat="in"></div></div>
<div class="drop-box"><h5>$\\mathbb{R}$ (все числа)</h5><div class="drop-items" data-cat="all"></div></div>
<div class="drop-box"><h5>$\\emptyset$ (нет решений)</h5><div class="drop-items" data-cat="none"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p17d-check">Проверить</button><button class="btn" id="p17d-reset">Сначала</button></div>
<div class="feedback" id="p17d-fb" style="display:none"></div>`);
/* INT 5 — Знак между / вне корней */
html += widget('Где плюс, где минус?','INTERACT 5','Дана парабола. Кликни на интервал — раскрась знак.',`
<p style="margin-bottom:10px">Пусть $y = x^2 - 4x + 3$. Корни: $x_1 = 1$, $x_2 = 3$. Кликни по каждому интервалу и поставь знак.</p>
<div id="p17z-line" style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px"></div>
<div class="feedback" id="p17z-fb" style="display:none;margin-top:10px"></div>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 - 6x + 5 \\leq 0$</li>
<li>$-x^2 + 4x - 3 > 0$</li>
<li>$x^2 + 2x + 5 > 0$ (без корней — особый случай)</li>
<li>$4x^2 - 9 \\geq 0$</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 - 7x + 10 < 0$</li>
<li>$3x^2 + 2x - 1 \\geq 0$</li>
<li>$x^2 + 1 \\leq 0$ (особый случай)</li>
<li>При каких $m$ неравенство $x^2 + 6x + m > 0$ верно для всех $x$?</li>
</ol>`);
html += secNav('p16', 'p18');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Парабола */
(function(){
const aE = document.getElementById('p17p-a'), bE = document.getElementById('p17p-b'), cE = document.getElementById('p17p-c');
const svg = document.getElementById('p17p-svg'), out = document.getElementById('p17p-out');
let done = false;
const W = 220, H = 160, x0 = W/2, y0 = H/2, sx = 14, sy = 14;
function refresh(){
const a = +aE.value, b = +bE.value, c = +cE.value;
document.getElementById('p17p-a-val').textContent = a;
document.getElementById('p17p-b-val').textContent = b;
document.getElementById('p17p-c-val').textContent = c;
let path = '';
for(let i = 0; i <= 240; i++){
const x = -W/(2*sx) + i * (W/sx) / 240;
const y = a*x*x + b*x + c;
const cx = x0 + x*sx, cy = y0 - y*sy;
path += (i === 0 ? 'M' : 'L') + cx.toFixed(2) + ',' + cy.toFixed(2) + ' ';
}
const D = b*b - 4*a*c;
const xs = D >= 0 ? [(-b - Math.sqrt(D))/(2*a), (-b + Math.sqrt(D))/(2*a)].sort((p,q)=>p-q) : [];
// фон зон
let s = '';
if(a !== 0){
if(D > 0){
// полосы зелёный/красный на оси
const x1 = x0 + xs[0]*sx, x2 = x0 + xs[1]*sx;
if(a > 0){
s += '<rect x="0" y="' + (y0-3) + '" width="' + x1 + '" height="6" fill="#10b981" opacity="0.35"/>';
s += '<rect x="' + x1 + '" y="' + (y0-3) + '" width="' + (x2-x1) + '" height="6" fill="#ef4444" opacity="0.35"/>';
s += '<rect x="' + x2 + '" y="' + (y0-3) + '" width="' + (W-x2) + '" height="6" fill="#10b981" opacity="0.35"/>';
} else {
s += '<rect x="0" y="' + (y0-3) + '" width="' + x1 + '" height="6" fill="#ef4444" opacity="0.35"/>';
s += '<rect x="' + x1 + '" y="' + (y0-3) + '" width="' + (x2-x1) + '" height="6" fill="#10b981" opacity="0.35"/>';
s += '<rect x="' + x2 + '" y="' + (y0-3) + '" width="' + (W-x2) + '" height="6" fill="#ef4444" opacity="0.35"/>';
}
} else {
s += '<rect x="0" y="' + (y0-3) + '" width="' + W + '" height="6" fill="' + (a*c >= 0 ? '#10b981' : '#ef4444') + '" opacity="0.35"/>';
}
}
s += '<line x1="0" y1="' + y0 + '" x2="' + W + '" y2="' + y0 + '" stroke="#94a3b8" stroke-width="0.8"/>';
s += '<line x1="' + x0 + '" y1="0" x2="' + x0 + '" y2="' + H + '" stroke="#94a3b8" stroke-width="0.8"/>';
s += '<path d="' + path + '" fill="none" stroke="#6366f1" stroke-width="2"/>';
xs.forEach(r => { s += '<circle cx="' + (x0 + r*sx).toFixed(2) + '" cy="' + y0 + '" r="4" fill="#6366f1"/>'; });
svg.innerHTML = s;
let info = '<div><b>$D$ = ' + D.toFixed(2) + '</b></div>';
if(D > 0){
info += '<div>Корни: $x_1 = ' + fmt(xs[0]) + ',\\ x_2 = ' + fmt(xs[1]) + '$</div>';
if(a > 0) info += '<div><b>$ax^2+bx+c > 0$:</b> $x < ' + fmt(xs[0]) + '$ или $x > ' + fmt(xs[1]) + '$</div><div><b>$ax^2+bx+c < 0$:</b> $' + fmt(xs[0]) + ' < x < ' + fmt(xs[1]) + '$</div>';
else info += '<div><b>$ax^2+bx+c > 0$:</b> $' + fmt(xs[0]) + ' < x < ' + fmt(xs[1]) + '$</div><div><b>$ax^2+bx+c < 0$:</b> $x < ' + fmt(xs[0]) + '$ или $x > ' + fmt(xs[1]) + '$</div>';
} else if(D === 0){
const r = -b/(2*a);
info += '<div>Один корень: $x = ' + fmt(r) + '$</div>';
info += '<div>Парабола касается оси. Знак — везде ' + (a > 0 ? 'положителен (кроме точки $x = ' + fmt(r) + '$)' : 'отрицателен') + '.</div>';
} else {
info += '<div>Корней нет. Парабола ' + (a > 0 ? 'выше' : 'ниже') + ' оси.</div>';
info += '<div>Знак выражения везде ' + (a > 0 ? 'положителен' : 'отрицателен') + '.</div>';
}
out.innerHTML = info; renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p17_parab'); bumpProgress('p17', 14); }, 300); }
}
[aE,bE,cE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
/* INIT 2 — Метод интервалов шаговый */
(function(){
const stage = document.getElementById('p17s-stage');
const goBtn = document.getElementById('p17s-go'), nextBtn = document.getElementById('p17s-next'), resetBtn = document.getElementById('p17s-reset');
let steps = [], idx = 0, awarded = false;
function build(a, b, c){
const arr = [];
arr.push('<b>Дано:</b> $' + a + 'x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' \\geq 0$');
const D = b*b - 4*a*c;
arr.push('<b>Шаг 1.</b> $D = b^2 - 4ac = ' + (b*b) + ' - ' + (4*a*c) + ' = ' + D + '$');
if(D < 0){
arr.push('<b>Шаг 2.</b> $D < 0$ — корней нет. Знак совпадает со знаком $a = ' + a + '$.');
if(a > 0) arr.push('<b>Шаг 3.</b> $a > 0$ — выражение всегда $> 0$, тем более $\\geq 0$. Ответ: $\\mathbb{R}$.');
else arr.push('<b>Шаг 3.</b> $a < 0$ — выражение всегда $< 0$, ни одна точка не удовлетворяет $\\geq 0$. Ответ: $\\emptyset$.');
} else if(D === 0){
const r = -b/(2*a);
arr.push('<b>Шаг 2.</b> $D = 0$ — один корень: $x = ' + fmt(r) + '$.');
if(a > 0) arr.push('<b>Шаг 3.</b> $a > 0$ — выражение всюду $\\geq 0$, равно нулю только в $x = ' + fmt(r) + '$. Ответ: $\\mathbb{R}$.');
else arr.push('<b>Шаг 3.</b> $a < 0$ — выражение всюду $\\leq 0$, равно нулю только в $x = ' + fmt(r) + '$. Ответ: $\\{' + fmt(r) + '\\}$.');
} else {
const x1 = (-b - Math.sqrt(D))/(2*a), x2 = (-b + Math.sqrt(D))/(2*a);
const lo = Math.min(x1, x2), hi = Math.max(x1, x2);
arr.push('<b>Шаг 2.</b> Корни: $x_1 = ' + fmt(lo) + ',\\ x_2 = ' + fmt(hi) + '$');
if(a > 0){
arr.push('<b>Шаг 3.</b> $a > 0$ — парабола вверх. Знак: $+$ при $x < x_1$, $-$ между корнями, $+$ при $x > x_2$.');
arr.push('<b>Ответ:</b> $x \\in (-\\infty;\\,' + fmt(lo) + '] \\cup [' + fmt(hi) + ';\\,+\\infty)$');
} else {
arr.push('<b>Шаг 3.</b> $a < 0$ — парабола вниз. Знак: $-$ вне, $+$ между корнями.');
arr.push('<b>Ответ:</b> $x \\in [' + fmt(lo) + ';\\,' + fmt(hi) + ']$');
}
}
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('p17_solver'); bumpProgress('p17', 16); confetti(); }
} else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
}
goBtn.addEventListener('click', ()=>{
const a = +document.getElementById('p17s-a').value, b = +document.getElementById('p17s-b').value, c = +document.getElementById('p17s-c').value;
if(!a){ stage.innerHTML = '<p>$a \\neq 0$</p>'; renderMath(stage); return; }
steps = build(a, b, c); 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 — Тренажёр */
(function(){
const tasks = [
{ q:'$x^2 - 5x + 6 > 0$', opts:['$(-\\infty;\\,2) \\cup (3;\\,+\\infty)$','$[2;\\,3]$','$(2;\\,3)$','$\\mathbb{R}$'], ok:0 },
{ q:'$x^2 - 4x + 3 \\leq 0$', opts:['$[1;\\,3]$','$(-\\infty;\\,1] \\cup [3;\\,+\\infty)$','$(1;\\,3)$','$\\emptyset$'], ok:0 },
{ q:'$x^2 + 1 > 0$', opts:['$\\mathbb{R}$','$\\emptyset$','$x \\neq 0$','$x > 0$'], ok:0 },
{ q:'$x^2 + 2x + 5 < 0$', opts:['$\\emptyset$','$\\mathbb{R}$','$(-1;\\,1)$','$x < -1$'], ok:0 },
{ q:'$-x^2 + 4 \\geq 0$', opts:['$[-2;\\,2]$','$(-\\infty;\\,-2] \\cup [2;\\,+\\infty)$','$(-2;\\,2)$','$\\mathbb{R}$'], ok:0 },
{ q:'$x^2 - 9 < 0$', opts:['$(-3;\\,3)$','$[-3;\\,3]$','$(-\\infty;\\,-3) \\cup (3;\\,+\\infty)$','$\\emptyset$'], ok:0 },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p17t-i').textContent = i;
document.getElementById('p17t-task').innerHTML = '$' + cur.q + '$';
renderMath(document.getElementById('p17t-task'));
const opts = document.getElementById('p17t-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('p17t-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('p17t-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p17_train'); bumpProgress('p17', 16); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p17t-fb').style.display = 'none';
}
document.getElementById('p17t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p17t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
/* INIT 4 — Drag парабола ↔ ответ */
(function(){
const items = [
{ id:1, html:'$a > 0$, есть 2 корня, неравенство $\\geq 0$', cat:'out' },
{ id:2, html:'$a > 0$, есть 2 корня, неравенство $\\leq 0$', cat:'in' },
{ id:3, html:'$a < 0$, есть 2 корня, неравенство $\\geq 0$', cat:'in' },
{ id:4, html:'$a > 0$, $D < 0$, неравенство $\\geq 0$', cat:'all' },
{ id:5, html:'$a > 0$, $D < 0$, неравенство $\\leq 0$', cat:'none' },
{ id:6, html:'$a < 0$, $D < 0$, неравенство $\\geq 0$', cat:'none' },
{ id:7, html:'$a < 0$, $D < 0$, неравенство $\\leq 0$', cat:'all' },
{ id:8, html:'$a < 0$, есть 2 корня, неравенство $\\leq 0$', cat:'out' },
];
const sorter = setupSorter({ poolId:'p17d-pool', cats:['out','in','all','none'], items, scopeSelector:'#p17-body', columnLayout:true });
document.getElementById('p17d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p17d-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('p17_drag'); bumpProgress('p17', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p17d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p17d-fb').style.display='none'; });
})();
/* INIT 5 — Знаки на интервалах (клик) */
(function(){
// 3 интервала: (-inf, 1), (1, 3), (3, inf). Должны быть: +, -, +.
const correct = ['+','-','+'];
const labels = ['$x < 1$', '$1 < x < 3$', '$x > 3$'];
const lineE = document.getElementById('p17z-line');
function build(){
let s = '<div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center;align-items:center;font-size:1.1rem">';
s += '<span>$-\\infty$</span>';
[0, 1, 2].forEach(i => {
s += '<button class="btn p17z-int" data-i="' + i + '" data-sign="" style="min-width:90px">' + labels[i] + '<br><span class="sg">?</span></button>';
if(i < 2) s += '<span style="font-family:JetBrains Mono,monospace;font-size:1.2rem;color:var(--sec-acc-d)">●' + (i === 0 ? ' 1 ●' : ' 3 ●').replace('●','') + '</span>';
});
s += '<span>$+\\infty$</span></div>';
lineE.innerHTML = s;
renderMath(lineE);
document.querySelectorAll('.p17z-int').forEach(btn => {
btn.addEventListener('click', ()=>{
const cur = btn.dataset.sign;
const next = cur === '' ? '+' : cur === '+' ? '-' : '';
btn.dataset.sign = next;
btn.querySelector('.sg').textContent = next || '?';
btn.style.background = next === '+' ? 'rgba(16,185,129,.2)' : next === '-' ? 'rgba(239,68,68,.2)' : '';
checkAll();
});
});
}
function checkAll(){
const all = [...document.querySelectorAll('.p17z-int')];
const got = all.map(b => b.dataset.sign);
if(got.every((s, i) => s === correct[i])){
const fb = document.getElementById('p17z-fb'); fb.style.display = 'block';
feedback(fb, true, '&#10003; Точно! Между корнями знак минус (так как $a > 0$).');
achievement('p17_intervals'); bumpProgress('p17', 14); confetti();
}
}
build();
})();
}
/* ============================================================
§ 18 — ДРОБНО-РАЦИОНАЛЬНЫЕ НЕРАВЕНСТВА
============================================================ */
function buildP18(){
const box = document.getElementById('p18-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Метод интервалов из § 17 — корни, знаки на интервалах.</li>
<li>ОДЗ (Глава 2 § 12): знаменатель $\\neq 0$.</li>
<li>$\\dfrac{a}{b}$ имеет тот же знак, что и $a \\cdot b$.</li>
</ul>`);
html += makeCard('theory','Что такое дробно-рациональное неравенство','18.1',`
<p><b>Дробно-рациональное</b> — неравенство, в котором есть дроби с переменной в знаменателе:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:10px;margin:8px 0;text-align:center;font-size:1.1rem">$$\\dfrac{f(x)}{g(x)} \\gtrless 0$$</div>
<p><b>Ключевая идея:</b> знак дроби определяется произведением знаков числителя и знаменателя. Метод интервалов работает, но точки, где знаменатель $= 0$, всегда <b>выколотые</b> (не входят в ОДЗ).</p>`);
html += makeCard('algo','Алгоритм','18.2',`
<ol style="margin-left:18px;line-height:1.9">
<li>Привести к виду $\\dfrac{f(x)}{g(x)} \\gtrless 0$ (всё в одну часть, общий знаменатель).</li>
<li>Найти нули числителя $f(x) = 0$ и знаменателя $g(x) = 0$.</li>
<li>Отметить точки на прямой: нули $f$ — закрашены (если знак $\\geq, \\leq$), нули $g$ — всегда выколотые.</li>
<li>Определить знак выражения на каждом интервале.</li>
<li>Выбрать интервалы, удовлетворяющие неравенству.</li>
</ol>`);
html += makeCard('example','Пример',null,`
<p><b>Решим:</b> $\\dfrac{x - 1}{x + 2} \\geq 0$.</p>
<p>Нули: числитель $x = 1$ (входит), знаменатель $x = -2$ (выколот).</p>
<p>Знаки на интервалах: $(-\\infty;\\,-2)$$+$ (минус на минус), $(-2;\\,1)$$-$ (плюс на минус), $(1;\\,+\\infty)$$+$.</p>
<p><b>Ответ:</b> $x \\in (-\\infty;\\,-2) \\cup [1;\\,+\\infty)$.</p>`);
/* INT 1 — Пошаговый решатель */
html += widget('Пошаговый решатель дроби','INTERACT 1','Решаем $\\dfrac{x-a}{x-b} \\geq 0$ пошагово.',`
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:center;margin-bottom:10px;font-size:1.05rem">
<span>$\\dfrac{x -$</span><input type="number" id="p18s-a" value="1" class="tinp" style="width:55px"><span>$}{x -$</span><input type="number" id="p18s-b" value="-2" class="tinp" style="width:55px"><span>$} \\geq 0$</span>
<button class="btn primary" id="p18s-go">Старт</button>
<button class="btn" id="p18s-next" style="display:none">Дальше</button>
<button class="btn" id="p18s-reset" style="display:none">Сначала</button>
</div>
<div id="p18s-stage" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:80px"></div>`);
/* INT 2 — Тренажёр */
html += widget('Тренажёр дробно-рациональных','INTERACT 2','Выбери правильный ответ.',`
<div class="score-display"><span>Задача <b id="p18t-i">1</b> / 6</span><span>Очки: <b id="p18t-score">0</b></span></div>
<div id="p18t-task" style="font-size:1.2rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div id="p18t-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p18t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p18t-start" style="margin-top:10px">Начать</button>`);
/* INT 3 — Найди ОДЗ */
html += widget('Найди ОДЗ','INTERACT 3','По выражению определи запрещённые точки (где знаменатель = 0).',`
<div class="score-display"><span>Раунд <b id="p18o-i">1</b> / 5</span><span>Очки: <b id="p18o-score">0</b></span></div>
<div id="p18o-task" style="font-size:1.15rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<input type="text" id="p18o-inp" placeholder="x = ... ; x = ..." class="tinp" style="width:220px">
<button class="btn primary" id="p18o-go">Ответ</button>
</div>
<div class="feedback" id="p18o-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p18o-start" style="margin-top:10px">Начать</button>`);
/* INT 4 — Drag: закрашена/выколота */
html += widget('Закрашена или выколота?','INTERACT 4','Отнеси каждую точку к нужной категории.',`
${DND_HINT_HTML}
<div id="p18d-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="full"></div></div>
<div class="drop-box"><h5>Выколота (не входит)</h5><div class="drop-items" data-cat="open"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p18d-check">Проверить</button><button class="btn" id="p18d-reset">Сначала</button></div>
<div class="feedback" id="p18d-fb" style="display:none"></div>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$\\dfrac{x - 2}{x + 3} > 0$</li>
<li>$\\dfrac{x + 1}{x - 4} \\leq 0$</li>
<li>$\\dfrac{x^2 - 4}{x - 1} \\geq 0$</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$\\dfrac{x - 3}{x + 1} \\geq 0$</li>
<li>$\\dfrac{x + 5}{x^2 - 4} > 0$</li>
<li>$\\dfrac{x^2 - 9}{x^2 + 2x - 8} \\leq 0$</li>
</ol>`);
html += secNav('p17', 'final3');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Пошаговый */
(function(){
const stage = document.getElementById('p18s-stage');
const goBtn = document.getElementById('p18s-go'), nextBtn = document.getElementById('p18s-next'), resetBtn = document.getElementById('p18s-reset');
let steps = [], idx = 0, awarded = false;
function build(a, b){
const arr = [];
arr.push('<b>Дано:</b> $\\dfrac{x - (' + a + ')}{x - (' + b + ')} \\geq 0$');
arr.push('<b>Шаг 1.</b> Нули числителя: $x = ' + a + '$ (входит, $\\geq$). Нули знаменателя: $x = ' + b + '$ (всегда выколот).');
const lo = Math.min(a, b), hi = Math.max(a, b);
arr.push('<b>Шаг 2.</b> Отметим на прямой: ' + (a < b ? ('$' + a + '$ (закрашена), $' + b + '$ (выколота)') : ('$' + b + '$ (выколота), $' + a + '$ (закрашена)')) + '.');
arr.push('<b>Шаг 3.</b> Знаки: подставим $x = ' + (hi + 1) + '$ — числитель $' + (hi + 1 - a) + ' > 0$, знаменатель $' + (hi + 1 - b) + ' > 0$, дробь $> 0$. Чередуем знаки от правого края: $+,\\ -,\\ +$.');
if(a < b){
arr.push('<b>Шаг 4.</b> $\\geq 0$ — берём $+$. Это $(-\\infty;\\,' + a + ']$ и $(' + b + ';\\,+\\infty)$.');
arr.push('<b>Ответ:</b> $x \\in (-\\infty;\\,' + a + '] \\cup (' + b + ';\\,+\\infty)$');
} else if(a > b){
arr.push('<b>Шаг 4.</b> $\\geq 0$ — берём $+$. Это $(-\\infty;\\,' + b + ')$ и $[' + a + ';\\,+\\infty)$.');
arr.push('<b>Ответ:</b> $x \\in (-\\infty;\\,' + b + ') \\cup [' + a + ';\\,+\\infty)$');
} else {
arr.push('<b>Шаг 4.</b> $a = b$ — дробь равна 1 везде, кроме $x = ' + a + '$ (выколота). $1 \\geq 0$. Ответ: $x \\neq ' + a + '$.');
}
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('p18_solver'); bumpProgress('p18', 16); confetti(); }
} else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
}
goBtn.addEventListener('click', ()=>{
const a = +document.getElementById('p18s-a').value, b = +document.getElementById('p18s-b').value;
steps = build(a, b); 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 2 — Тренажёр */
(function(){
const tasks = [
{ q:'$\\dfrac{x - 3}{x + 1} > 0$', opts:['$(-\\infty;\\,-1) \\cup (3;\\,+\\infty)$','$(-1;\\,3)$','$[-1;\\,3]$','$\\emptyset$'], ok:0 },
{ q:'$\\dfrac{x + 2}{x - 5} \\leq 0$', opts:['$[-2;\\,5)$','$(-2;\\,5)$','$[-2;\\,5]$','$(-\\infty;\\,-2] \\cup (5;\\,+\\infty)$'], ok:0 },
{ q:'$\\dfrac{1}{x - 4} > 0$', opts:['$(4;\\,+\\infty)$','$(-\\infty;\\,4)$','$\\mathbb{R} \\setminus \\{4\\}$','$[4;\\,+\\infty)$'], ok:0 },
{ q:'$\\dfrac{x - 1}{x + 3} \\geq 0$', opts:['$(-\\infty;\\,-3) \\cup [1;\\,+\\infty)$','$[-3;\\,1]$','$(-3;\\,1)$','$\\mathbb{R}$'], ok:0 },
{ q:'$\\dfrac{x + 6}{x} < 0$', opts:['$(-6;\\,0)$','$[-6;\\,0)$','$(-\\infty;\\,-6)$','$(0;\\,+\\infty)$'], ok:0 },
{ q:'$\\dfrac{x - 2}{x - 2} > 0$', opts:['$\\mathbb{R} \\setminus \\{2\\}$','$\\mathbb{R}$','$\\{2\\}$','$\\emptyset$'], ok:0 },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p18t-i').textContent = i;
document.getElementById('p18t-task').innerHTML = '$' + cur.q.replace(/^\$|\$$/g,'') + '$';
renderMath(document.getElementById('p18t-task'));
const opts = document.getElementById('p18t-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('p18t-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('p18t-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p18_intervals'); bumpProgress('p18', 16); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p18t-fb').style.display = 'none';
}
document.getElementById('p18t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p18t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
/* INIT 3 — ОДЗ */
(function(){
const tasks = [
{ q:'$\\dfrac{x + 1}{x - 3}$', ans:[3] },
{ q:'$\\dfrac{x^2 + 1}{x(x - 5)}$', ans:[0, 5] },
{ q:'$\\dfrac{1}{x^2 - 4}$', ans:[-2, 2] },
{ q:'$\\dfrac{x + 3}{x^2 - 6x + 9}$', ans:[3] },
{ q:'$\\dfrac{1}{x} + \\dfrac{1}{x + 1}$', ans:[-1, 0] },
];
let cur = null, i = 1, score = 0;
function show(){
cur = tasks[i-1];
document.getElementById('p18o-i').textContent = i;
document.getElementById('p18o-task').innerHTML = 'Запрещённые точки для ' + cur.q;
renderMath(document.getElementById('p18o-task'));
document.getElementById('p18o-inp').value = '';
document.getElementById('p18o-fb').style.display = 'none';
}
document.getElementById('p18o-go').addEventListener('click', ()=>{
const fb = document.getElementById('p18o-fb'); fb.style.display = 'block';
const u = document.getElementById('p18o-inp').value.replace(/[xX\s=]/g, '').split(/[;,]+/).filter(Boolean).map(Number).sort((a,b)=>a-b);
const a = [...cur.ans].sort((p,q)=>p-q);
const ok = u.length === a.length && a.every((v, k) => v === u[k]);
if(ok){ score++; feedback(fb, true, '&#10003;'); }
else feedback(fb, false, 'Правильно: ' + a.join(', '));
document.getElementById('p18o-score').textContent = score;
if(i >= tasks.length){ setTimeout(()=>{ feedback(fb, score >= 3, 'Итог: ' + score + '/' + tasks.length); if(score >= 3){ achievement('p18_odz'); bumpProgress('p18', 14); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
});
document.getElementById('p18o-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p18o-score').textContent = 0; show(); });
})();
/* INIT 4 — Drag закрашена/выколота */
(function(){
const items = [
{ id:1, html:'нуль числителя при $\\geq 0$', cat:'full' },
{ id:2, html:'нуль знаменателя', cat:'open' },
{ id:3, html:'нуль числителя при $> 0$ строго', cat:'open' },
{ id:4, html:'нуль числителя при $\\leq 0$', cat:'full' },
{ id:5, html:'точка вне ОДЗ', cat:'open' },
{ id:6, html:'граница в системе с $\\leq$', cat:'full' },
{ id:7, html:'граница в системе с $<$', cat:'open' },
{ id:8, html:'нуль знаменателя — всегда', cat:'open' },
];
const sorter = setupSorter({ poolId:'p18d-pool', cats:['full','open'], items, scopeSelector:'#p18-body', columnLayout:true });
document.getElementById('p18d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p18d-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('p18_odz'); bumpProgress('p18', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p18d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p18d-fb').style.display='none'; });
})();
}
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)}`; }
</script>