From dd0d63d25ace6f295eb637cb2d18bebf1e2f12ee Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 2 Jun 2026 14:58:44 +0300 Subject: [PATCH] =?UTF-8?q?feat(math6):=20=D0=93=D0=BB=D0=B0=D0=B2=D0=B0?= =?UTF-8?q?=201,=20=D0=B2=D0=BE=D0=BB=D0=BD=D0=B0=202=20=E2=80=94=20=C2=A7?= =?UTF-8?q?4=E2=80=93=C2=A76=20(=D1=81=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5/=D0=B2=D1=8B=D1=87=D0=B8=D1=82=D0=B0=D0=BD=D0=B8=D0=B5,?= =?UTF-8?q?=20=D1=81=D0=B4=D0=B2=D0=B8=D0=B3=20=D0=B7=D0=B0=D0=BF=D1=8F?= =?UTF-8?q?=D1=82=D0=BE=D0=B9,=20=D1=83=D0=BC=D0=BD=D0=BE=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §4 столбик «запятая под запятой» + ловушка выравнивания; §5 демонстратор сдвига запятой ×/÷10,100,1000 + тренажёр; §6 подсчёт знаков после запятой (ползунки) + тренажёр умножения. Целочисленные мантиссы вместо float. Шпаргалки/типсы/глоссарий. Тесты 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/tests/math6-page.test.js | 15 ++ frontend/textbooks/math_6_ch1.html | 217 ++++++++++++++++++++++++++++- 2 files changed, 228 insertions(+), 4 deletions(-) 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
Столбик: посчитай сам
' + +'
Запятая под запятой — выполни действие и введи ответ (запятая или точка).
' + +'
Пример 1 / 6Очки: 0 / 6
' + +'
' + +'
' + +'
'; + h+='
Интерактив 2
Найди ошибку выравнивания
' + +'
Частая ошибка — сложить «по правому краю», не выровняв запятую. Выбери верный ответ.
' + +'
Вопрос 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 ''; }).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+='
Интерактив 1
Куда сдвинуть запятую?
' + +'
Выбери число и действие — посмотри, как и куда переносится запятая.
' + +'
' + +'
' + +'' + +'
' + +'
'; + h+='
Интерактив 2
Вычисли в уме
' + +'
Перенеси запятую и введи ответ.
' + +'
Пример 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
Считаем знаки после запятой
' + +'
Двигай множители — смотри, как число знаков после запятой определяет ответ.
' + +'
' + +'
' + +'
'; + h+='
Интерактив 2
Тренажёр умножения
' + +'
Перемножь десятичные дроби и введи ответ.
' + +'
Пример 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 });