diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js
index 1084c5d..d2fcb6f 100644
--- a/backend/tests/math6-page.test.js
+++ b/backend/tests/math6-page.test.js
@@ -89,6 +89,21 @@ test('ch1 Волна 1: интерактивы §1–§3 монтируются
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});
+test('ch1 Волна 2: интерактивы §4–§6 монтируются без ошибок', async () => {
+ const { doc, errors } = await loadDom('math_6_ch1.html');
+ const win = doc.defaultView;
+ win.goTo('p4'); await wait(80);
+ assert.ok(doc.querySelector('#p4-fig'), 'столбик §4');
+ assert.ok(doc.querySelectorAll('#p4-eopts').length === 1, 'варианты §4');
+ win.goTo('p5'); await wait(80);
+ assert.ok(doc.querySelector('#p5-iv1 [data-op]'), 'кнопки сдвига запятой §5');
+ assert.ok(doc.querySelector('#p5-out').textContent.length > 0 || doc.querySelector('#p5-out'), 'демонстратор §5');
+ win.goTo('p6'); await wait(80);
+ assert.ok(doc.querySelector('#p6-asl') && doc.querySelector('#p6-bsl'), 'ползунки множителей §6');
+ assert.ok(doc.querySelector('#p6-q'), 'тренажёр умножения §6');
+ assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
+});
+
test('навигация и прогресс: переход на § и отметка прочтения', async () => {
const { doc, errors } = await loadDom('math_6_ch1.html');
const win = doc.defaultView;
diff --git a/frontend/textbooks/math_6_ch1.html b/frontend/textbooks/math_6_ch1.html
index 03f9e11..f230504 100644
--- a/frontend/textbooks/math_6_ch1.html
+++ b/frontend/textbooks/math_6_ch1.html
@@ -304,6 +304,196 @@ function buildP3(){
})();
}
+/* ===================== § 4. СЛОЖЕНИЕ И ВЫЧИТАНИЕ ===================== */
+function _dec(x){ var s=String(x), i=s.indexOf('.'); return i<0?0:s.length-i-1; }
+function _mant(x,d){ return Math.round(x*Math.pow(10,d)); }
+function _splitNum(x){ var s=String(x).replace(',','.').split('.'); return {i:s[0], f:s[1]||''}; }
+function _rnum(maxd){ var d=_ri(0,maxd==null?2:maxd); var m=_ri(1, d===0?99:(d===1?499:4999)); return m/Math.pow(10,d); }
+function _colText(a,b,op){
+ var A=_splitNum(a),B=_splitNum(b), iw=Math.max(A.i.length,B.i.length), fw=Math.max(A.f.length,B.f.length);
+ function line(sign,N){ var f=fw?(','+N.f.padEnd(fw,'0')):''; return sign+' '+N.i.padStart(iw)+f; }
+ var w=2+iw+(fw?fw+1:0);
+ return '
'+line(' ',A)+'\n'+line(op,B)+'\n'+'─'.repeat(w)+'
';
+}
+function buildP4(){
+ var box=document.getElementById('p4-body'); var h='';
+ h+=makeCard('rule','Сложение и вычитание «в столбик»','4.1',
+ '1) Записывают числа так, чтобы запятая стояла под запятой , одинаковые разряды — друг под другом.
'
+ +'2) Уравнивают число знаков после запятой нулями .
'
+ +'3) Складывают или вычитают как натуральные числа, не глядя на запятую.
'
+ +'4) В ответе ставят запятую под запятыми .
');
+ h+=makeCard('example','Примеры','4.2',
+ '$3{,}4 + 12{,}65 = 3{,}40 + 12{,}65 = 16{,}05$.
'
+ +'$7 - 2{,}3 = 7{,}0 - 2{,}3 = 4{,}7$.
');
+ h+=''
+ +'
Запятая под запятой — выполни действие и введи ответ (запятая или точка).
'
+ +'
Пример 1 / 6 Очки: 0 / 6
'
+ +'
'
+ +'
Проверить
'
+ +'
';
+ h+=''
+ +'
Частая ошибка — сложить «по правому краю», не выровняв запятую. Выбери верный ответ.
'
+ +'
Вопрос 1 / 5 Очки: 0 / 5
'
+ +'
'
+ +'
';
+ h+=secNav('p3','p5')+readBtn('p4');
+ box.innerHTML=h; renderMath(box);
+
+ (function(){
+ var i=0,score=0,cur=null;
+ function gen(){ var op=_pick(['+','−']), a=_rnum(2), b=_rnum(2); if(op==='−'&&b>a){ var t=a;a=b;b=t; }
+ var D=Math.max(_dec(a),_dec(b)), A=_mant(a,D),B=_mant(b,D), r=(op==='+'?A+B:A-B)/Math.pow(10,D); cur={a:a,b:b,op:op,r:r}; }
+ function show(){
+ if(i>=6){ document.getElementById('p4-fig').innerHTML='Готово! Результат: '+score+' / 6'; if(score>=5){addXp(15,'p4-iv1');bumpProgress('p4',30);}else if(score>=3){addXp(8,'p4-iv1');bumpProgress('p4',16);} return; }
+ gen(); document.getElementById('p4-i').textContent=i+1;
+ document.getElementById('p4-fig').innerHTML=_colText(cur.a,cur.b,cur.op);
+ document.getElementById('p4-a').value=''; document.getElementById('p4-fb').style.display='none';
+ }
+ function go(){ if(i>=6)return; var fb=document.getElementById('p4-fb'), v=parseFloat(document.getElementById('p4-a').value.replace(',','.').trim());
+ if(isNaN(v)){ feedback(fb,false,'Введи число.'); return; }
+ if(Math.abs(v-cur.r)<1e-9){ score++; feedback(fb,true,'✓ Верно: $'+_kf(cur.r)+'$.'); } else feedback(fb,false,'✗ Нет. Правильно: $'+_kf(cur.r)+'$.');
+ document.getElementById('p4-s').textContent=score; i++; setTimeout(show,1200); }
+ document.getElementById('p4-go').addEventListener('click',go);
+ document.getElementById('p4-a').addEventListener('keydown',function(e){ if(e.key==='Enter')go(); });
+ show();
+ })();
+
+ (function(){
+ var i=0,score=0,cur=null;
+ function gen(){ var a=_rnum(2), b=_rnum(2); var D=Math.max(_dec(a),_dec(b)), r=(_mant(a,D)+_mant(b,D))/Math.pow(10,D);
+ // ловушка: выравнивание по правому краю (складываем мантиссы как целые)
+ var trapD=Math.max(_dec(a),_dec(b)); var trap=(_mant(a,_dec(a))+_mant(b,_dec(b)))/Math.pow(10,trapD);
+ var opts=[r]; if(Math.abs(trap-r)>1e-9)opts.push(trap); opts.push(_round(r+_pick([0.1,-0.1,1,-1]),2));
+ opts=opts.filter(function(v,k,arr){return arr.indexOf(v)===k;}); while(opts.length<3)opts.push(_round(r+_ri(1,9)/10,2));
+ // перемешать
+ for(var j=opts.length-1;j>0;j--){ var k=_ri(0,j); var t=opts[j];opts[j]=opts[k];opts[k]=t; }
+ cur={a:a,b:b,r:r,opts:opts.slice(0,3)}; }
+ function show(){
+ if(i>=5){ document.getElementById('p4-eq').innerHTML='Готово! Результат: '+score+' / 5'; document.getElementById('p4-eopts').innerHTML=''; if(score>=4){addXp(15,'p4-iv2');bumpProgress('p4',30);}else if(score>=2){addXp(8,'p4-iv2');bumpProgress('p4',16);} return; }
+ gen(); document.getElementById('p4-ei').textContent=i+1;
+ document.getElementById('p4-eq').innerHTML='Чему равно $'+_kf(cur.a)+' + '+_kf(cur.b)+'$?'; renderMath(document.getElementById('p4-eq'));
+ document.getElementById('p4-eopts').innerHTML=cur.opts.map(function(o){ return '$'+_kf(o)+'$ '; }).join('');
+ document.querySelectorAll('#p4-eopts [data-v]').forEach(function(b){ b.addEventListener('click',function(){ ans(parseFloat(b.getAttribute('data-v'))); }); });
+ renderMath(document.getElementById('p4-eopts'));
+ document.getElementById('p4-efb').style.display='none';
+ }
+ function ans(v){ if(i>=5)return; var fb=document.getElementById('p4-efb');
+ if(Math.abs(v-cur.r)<1e-9){ score++; feedback(fb,true,'✓ Верно: $'+_kf(cur.r)+'$. Запятая под запятой!'); } else feedback(fb,false,'✗ Нет. Правильно $'+_kf(cur.r)+'$ — выравнивай по запятой.');
+ document.getElementById('p4-es').textContent=score; i++; setTimeout(show,1300); }
+ show();
+ })();
+}
+
+/* ===================== § 5. УМНОЖЕНИЕ И ДЕЛЕНИЕ НА 10, 100, 1000 ===================== */
+function buildP5(){
+ var box=document.getElementById('p5-body'); var h='';
+ h+=makeCard('rule','Сдвиг запятой','5.1',
+ 'Чтобы умножить десятичную дробь на $10$, $100$, $1000$, запятую переносят вправо на $1$, $2$, $3$ знака.
'
+ +'Чтобы разделить на $10$, $100$, $1000$, запятую переносят влево на $1$, $2$, $3$ знака.
'
+ +'Если цифр не хватает — дописывают нули : $3{,}5\\cdot 100 = 350$, $4\\div 100 = 0{,}04$.
');
+ h+=makeCard('example','Примеры','5.2',
+ '$2{,}71\\cdot 10 = 27{,}1$ · $0{,}6\\cdot 1000 = 600$ · $58{,}3\\div 100 = 0{,}583$.
');
+ h+=''
+ +'
Выбери число и действие — посмотри, как и куда переносится запятая.
'
+ +'
Число = 3,45
'
+ +'
'
+ +'÷1000 ÷100 ÷10 '
+ +'×10 ×100 ×1000
'
+ +'
';
+ h+=''
+ +'
Перенеси запятую и введи ответ.
'
+ +'
Пример 1 / 6 Очки: 0 / 6
'
+ +'
'
+ +'
Проверить
'
+ +'
';
+ h+=secNav('p4','p6')+readBtn('p5');
+ box.innerHTML=h; renderMath(box);
+
+ (function(){
+ var NUMS=[0.6,2.71,3.45,58.3,0.04,12.5]; var sl=document.getElementById('p5-n'), out=document.getElementById('p5-out');
+ function shift(n,f){ var d=_dec(n); if(f>=1){ var k=Math.round(Math.log10(f)); return {res:_round(n*f, Math.max(0,d-k)), dir:'вправо', k:k}; } var k2=Math.round(-Math.log10(f)); return {res:_round(n/Math.pow(10,k2), d+k2), dir:'влево', k:k2}; }
+ var lastF=10;
+ function render(){ var n=NUMS[+sl.value]; document.getElementById('p5-nv').textContent=_kf(n).replace('{,}',','); var r=shift(n,lastF);
+ var sign=lastF>=1?'\\cdot '+lastF:'\\div '+Math.round(1/lastF);
+ out.innerHTML='$'+_kf(n)+' '+sign+' = '+_kf(r.res)+'$
'
+ +'Запятая сдвигается '+r.dir+' на '+r.k+' '+(r.k===1?'знак':'знака')+'.
'; renderMath(out); }
+ sl.oninput=render;
+ document.querySelectorAll('#p5-iv1 [data-op]').forEach(function(b){ b.addEventListener('click',function(){ lastF=parseFloat(b.getAttribute('data-op')); render(); }); });
+ render();
+ })();
+
+ (function(){
+ var i=0,score=0,cur=null, fs=[10,100,1000,0.1,0.01,0.001];
+ function gen(){ var n=_rnum(2), f=_pick(fs); var d=_dec(n), r; if(f>=1){ var k=Math.round(Math.log10(f)); r=_round(n*f,Math.max(0,d-k)); } else { var k2=Math.round(-Math.log10(f)); r=_round(n/Math.pow(10,k2), d+k2); } cur={n:n,f:f,r:r}; }
+ function show(){
+ if(i>=6){ document.getElementById('p5-q').innerHTML='Готово! Результат: '+score+' / 6'; if(score>=5){addXp(15,'p5-iv2');bumpProgress('p5',30);}else if(score>=3){addXp(8,'p5-iv2');bumpProgress('p5',16);} return; }
+ gen(); document.getElementById('p5-i').textContent=i+1;
+ var sign=cur.f>=1?'\\cdot '+cur.f:'\\div '+Math.round(1/cur.f);
+ document.getElementById('p5-q').innerHTML='Вычисли $'+_kf(cur.n)+' '+sign+'$'; renderMath(document.getElementById('p5-q'));
+ document.getElementById('p5-a').value=''; document.getElementById('p5-fb').style.display='none';
+ }
+ function go(){ if(i>=6)return; var fb=document.getElementById('p5-fb'), v=parseFloat(document.getElementById('p5-a').value.replace(',','.').trim());
+ if(isNaN(v)){ feedback(fb,false,'Введи число.'); return; }
+ if(Math.abs(v-cur.r)<1e-9){ score++; feedback(fb,true,'✓ Верно: $'+_kf(cur.r)+'$.'); } else feedback(fb,false,'✗ Нет. Правильно: $'+_kf(cur.r)+'$.');
+ document.getElementById('p5-s').textContent=score; i++; setTimeout(show,1200); }
+ document.getElementById('p5-go').addEventListener('click',go);
+ document.getElementById('p5-a').addEventListener('keydown',function(e){ if(e.key==='Enter')go(); });
+ show();
+ })();
+}
+
+/* ===================== § 6. УМНОЖЕНИЕ ДЕСЯТИЧНЫХ ДРОБЕЙ ===================== */
+function buildP6(){
+ var box=document.getElementById('p6-body'); var h='';
+ h+=makeCard('rule','Как умножать десятичные дроби','6.1',
+ '1) Умножают числа, не обращая внимания на запятые (как натуральные).
'
+ +'2) В произведении отделяют запятой столько цифр справа, сколько их после запятой у обоих множителей вместе.
');
+ h+=makeCard('example','Пример','6.2',
+ '$1{,}2\\cdot 0{,}3$: умножаем $12\\cdot 3 = 36$; всего знаков после запятой $1+1=2$, значит $1{,}2\\cdot 0{,}3 = 0{,}36$.
'
+ +'$0{,}25\\cdot 4 = 1$ (знаков $2+0=2$: $100\\to 1{,}00 = 1$).
');
+ h+=''
+ +'
Двигай множители — смотри, как число знаков после запятой определяет ответ.
'
+ +'
Первый = 1,2 '
+ +'Второй = 0,3
'
+ +'
';
+ h+=''
+ +'
Перемножь десятичные дроби и введи ответ.
'
+ +'
Пример 1 / 6 Очки: 0 / 6
'
+ +'
'
+ +'
Проверить
'
+ +'
';
+ h+=secNav('p5','p7')+readBtn('p6');
+ box.innerHTML=h; renderMath(box);
+
+ (function(){
+ var AS=[0.4,1.2,0.25,3.5,0.06,2.5], BS=[0.3,4,0.2,1.5,0.7,0.08];
+ var asl=document.getElementById('p6-asl'), bsl=document.getElementById('p6-bsl'), out=document.getElementById('p6-out');
+ function render(){ var a=AS[+asl.value], b=BS[+bsl.value]; document.getElementById('p6-av').textContent=_kf(a).replace('{,}',','); document.getElementById('p6-bv').textContent=_kf(b).replace('{,}',',');
+ var da=_dec(a),db=_dec(b), ma=_mant(a,da),mb=_mant(b,db), p=ma*mb, r=p/Math.pow(10,da+db);
+ out.innerHTML='$'+ma+'\\cdot '+mb+' = '+p+'$, знаков после запятой: $'+da+'+'+db+'='+(da+db)+'$
'
+ +'$'+_kf(a)+'\\cdot '+_kf(b)+' = '+_kf(r)+'$
'; renderMath(out); }
+ asl.oninput=render; bsl.oninput=render; render();
+ })();
+
+ (function(){
+ var i=0,score=0,cur=null;
+ function gen(){ var a=_rnum(_pick([1,2])), b=_rnum(_pick([0,1])); var da=_dec(a),db=_dec(b), r=_mant(a,da)*_mant(b,db)/Math.pow(10,da+db); cur={a:a,b:b,r:r}; }
+ function show(){
+ if(i>=6){ document.getElementById('p6-q').innerHTML='Готово! Результат: '+score+' / 6'; if(score>=5){addXp(15,'p6-iv2');bumpProgress('p6',30);}else if(score>=3){addXp(8,'p6-iv2');bumpProgress('p6',16);} return; }
+ gen(); document.getElementById('p6-i').textContent=i+1;
+ document.getElementById('p6-q').innerHTML='Вычисли $'+_kf(cur.a)+'\\cdot '+_kf(cur.b)+'$'; renderMath(document.getElementById('p6-q'));
+ document.getElementById('p6-a').value=''; document.getElementById('p6-fb').style.display='none';
+ }
+ function go(){ if(i>=6)return; var fb=document.getElementById('p6-fb'), v=parseFloat(document.getElementById('p6-a').value.replace(',','.').trim());
+ if(isNaN(v)){ feedback(fb,false,'Введи число.'); return; }
+ if(Math.abs(v-cur.r)<1e-9){ score++; feedback(fb,true,'✓ Верно: $'+_kf(cur.r)+'$.'); } else feedback(fb,false,'✗ Нет. Правильно: $'+_kf(cur.r)+'$.');
+ document.getElementById('p6-s').textContent=score; i++; setTimeout(show,1200); }
+ document.getElementById('p6-go').addEventListener('click',go);
+ document.getElementById('p6-a').addEventListener('keydown',function(e){ if(e.key==='Enter')go(); });
+ show();
+ })();
+}
+
/* ===================== ДАННЫЕ САЙДБАРА / ГЛОССАРИЯ ===================== */
var SIDEBARS = {
p1:{ title:'Шпаргалка § 1', rows:[
@@ -321,21 +511,40 @@ var SIDEBARS = {
['Координатный луч','начало $O$, $0$, единичный отрезок'],
['Координата','расстояние от $0$ в единичных отрезках'],
['Десятые','единичный отрезок делим на 10'],
- ['$2{,}5$','посередине между 2 и 3'] ]}
+ ['$2{,}5$','посередине между 2 и 3'] ]},
+ p4:{ title:'Шпаргалка § 4', rows:[
+ ['Запятая','под запятой'],
+ ['Уравнять','нулями справа'],
+ ['Считаем','как натуральные'],
+ ['$7-2{,}3$','$=7{,}0-2{,}3=4{,}7$'] ]},
+ p5:{ title:'Шпаргалка § 5', rows:[
+ ['× 10, 100, 1000','запятая вправо на 1, 2, 3'],
+ ['÷ 10, 100, 1000','запятая влево на 1, 2, 3'],
+ ['Не хватает цифр','дописываем нули'],
+ ['$0{,}6\\cdot 1000$','$=600$'] ]},
+ p6:{ title:'Шпаргалка § 6', rows:[
+ ['Умножение','как натуральные числа'],
+ ['Запятая','знаков = сумма знаков множителей'],
+ ['$1{,}2\\cdot 0{,}3$','$12\\cdot3=36$, 2 знака → $0{,}36$'] ]}
};
var TIPS = [
{ sec:'p1', html:'Число цифр после запятой = числу нулей в знаменателе. У $0{,}305$ три цифры → знаменатель $1000$.' },
{ sec:'p2', html:'Перед сравнением мысленно уравняй число знаков нулями: $0{,}5$ это $0{,}50$, и сразу видно, что $0{,}50>0{,}48$.' },
- { sec:'p3', html:'Чтобы отметить десятые, дели единичный отрезок на 10. $1{,}7$ — это $1$ и ещё $7$ маленьких делений.' }
+ { sec:'p3', html:'Чтобы отметить десятые, дели единичный отрезок на 10. $1{,}7$ — это $1$ и ещё $7$ маленьких делений.' },
+ { sec:'p4', html:'Перед сложением «лесенкой» допиши нули: $7$ это $7{,}0$, тогда $7{,}0-2{,}3$ считается легко.' },
+ { sec:'p5', html:'Считай нули множителя: у $1000$ их три → запятая прыгает на 3 знака. Умножаем — вправо, делим — влево.' },
+ { sec:'p6', html:'Сначала перемножь без запятых. Потом отсчитай справа столько знаков, сколько их после запятой у обоих множителей вместе.' }
];
var GLOSSARY = [
{ term:'десятичная дробь', def:'Дробь со знаменателем $10,100,1000,\\ldots$, записанная через запятую.', sec:'p1', aliases:['десятичная дробь','десятичной дроби','десятичные дроби','десятичных дробей','десятичную дробь'] },
{ term:'разряд', def:'Место цифры в записи числа: десятые, сотые, тысячные и т. д.', sec:'p1', aliases:['разряд','разряда','разряде','разряды','разрядов'] },
{ term:'координатный луч', def:'Луч с началом $O$ (число $0$), единичным отрезком и направлением.', sec:'p3', aliases:['координатный луч','координатном луче','координатного луча'] },
{ term:'координата', def:'Число, показывающее расстояние точки от начала в единичных отрезках.', sec:'p3', aliases:['координата','координату','координаты','координатой'] },
- { term:'округление', def:'Замена числа близким с меньшим числом разрядов по правилу: следующая цифра $\\ge5$ — разряд увеличивают.', sec:'p2', aliases:['округление','округления','округлить','округлении'] }
+ { term:'округление', def:'Замена числа близким с меньшим числом разрядов по правилу: следующая цифра $\\ge5$ — разряд увеличивают.', sec:'p2', aliases:['округление','округления','округлить','округлении'] },
+ { term:'множитель', def:'Число, которое умножают. В произведении число знаков после запятой равно сумме знаков множителей.', sec:'p6', aliases:['множитель','множителя','множители','множителей'] },
+ { term:'произведение', def:'Результат умножения. $1{,}2\\cdot 0{,}3 = 0{,}36$.', sec:'p6', aliases:['произведение','произведения','произведении'] }
];
-var BUILDERS = { p1:buildP1, p2:buildP2, p3:buildP3 };
+var BUILDERS = { p1:buildP1, p2:buildP2, p3:buildP3, p4:buildP4, p5:buildP5, p6:buildP6 };
Object.assign(window.M6, { sidebars:SIDEBARS, tips:TIPS, glossary:GLOSSARY, builders:BUILDERS });