Files
Learn_System/backend/scripts/inject_p8_ch1_tasks.cjs
Maxim Dolgolyov 75165d900b feat(p8 ch1): IV-5 расчётные задачи для §1-5, §8, §10 (тепловые явления)
В Физике 8 ch1 §6, §7, §9, §11 уже имели IV-4 'Тренажёр N расчётных задач'.
У §1-5, §8, §10 IV-4 был только MCQ — числовых задач не было.

inject_p8_ch1_tasks.cjs добавляет IV-5 виджет после IV-4 в build_pN:
- §1 Внутр. энергия: 5 задач (T-конверсия, U vs масса/высота)
- §2 Способы изменения U: 5 (Q=ΔU+A, кин. энергия молота → тепло)
- §3 Теплопроводность: 5 (тепловой поток P=Q/t, зависимости от d, S, λ)
- §4 Конвекция: 5 (плотности тёплого/холодного, нагрев радиатором)
- §5 Излучение: 5 (солнечный поток, Стефан-Больцман упрощённо)
- §8 Плавление: 5 (Q=λm)
- §10 Испарение: 5 (Q=rm, испарение пота, лужи)

Всего 35 новых задач с автопроверкой числового ответа (±tol),
подсказкой-решением (KaTeX) и +20 XP при прохождении всей серии.
Используется существующий design system (.wg, .tinp, .feedback,
.score-display) — уже подключён через phys-textbook-widgets.css.
2026-05-30 09:30:45 +03:00

178 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Inject IV-5 «Расчётные задачи» widget into build_pN of physics_8_ch1.html
// for paragraphs where IV-4 is только MCQ (§1, §2, §3, §4, §5, §8, §10).
// §6, §7, §9, §11 already have numeric task trainers.
'use strict';
const fs = require('fs');
const path = require('path');
const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_8_ch1.html');
let h = fs.readFileSync(DST, 'utf8');
// === Numeric tasks per paragraph (§1..§11 thermal) ===
// Каждая задача: q (вопрос с KaTeX в $..$), ans (число), tol (допуск), why (пошаговое решение)
const TASKS = {
p1: [ // Внутренняя энергия
{ q: 'Переведите температуру $t = 27\\,^\\circ$C в кельвины. ($T = t + 273$)', ans: 300, tol: 1, why: '$T = 27 + 273 = 300$ К.' },
{ q: 'Температура воды $T = 373$ К. Чему равно $t$ в градусах Цельсия?', ans: 100, tol: 1, why: '$t = T - 273 = 373 - 273 = 100\\,^\\circ$C — кипение воды.' },
{ q: 'У стакана воды массой $m_1 = 0{,}5$ кг и у бочки воды массой $m_2 = 50$ кг одинаковая температура. У кого внутренняя энергия больше во сколько раз?', ans: 100, tol: 1, why: '$U \\propto m$ при одинаковой $T$. $U_2/U_1 = m_2/m_1 = 50/0{,}5 = 100$.' },
{ q: 'Тело нагрели на $\\Delta T = 30$ К. На сколько градусов Цельсия изменилась его температура?', ans: 30, tol: 0.5, why: 'Шкалы Кельвина и Цельсия отличаются только сдвигом — разность температур одинакова.' },
{ q: 'При какой температуре по шкале Цельсия средняя кинетическая энергия молекул равна нулю (абсолютный ноль)?', ans: -273, tol: 1, why: 'Абсолютный ноль $T = 0$ К соответствует $t = 0 - 273 = -273\\,^\\circ$C.' },
],
p2: [ // Способы изменения U
{ q: 'Газу передали $Q = 200$ Дж теплоты, и он совершил работу $A = 60$ Дж. На сколько увеличилась его внутренняя энергия? ($\\Delta U = Q - A$)', ans: 140, tol: 2, why: '$\\Delta U = Q - A = 200 - 60 = 140$ Дж (первое начало термодинамики).' },
{ q: 'Над газом совершили работу $A_{внеш} = 150$ Дж, газ отдал $Q = 50$ Дж тепла. На сколько изменилась $U$?', ans: 100, tol: 2, why: '$\\Delta U = A_{внеш} - Q_{отд} = 150 - 50 = 100$ Дж.' },
{ q: 'Газ адиабатно (без теплообмена, $Q = 0$) расширился, совершив $A = 80$ Дж. Найдите $|\\Delta U|$.', ans: 80, tol: 2, why: 'При $Q = 0$: $\\Delta U = -A = -80$ Дж. Модуль изменения $|\\Delta U| = 80$ Дж.' },
{ q: 'Молотом массой $0{,}5$ кг, движущимся со скоростью $v = 4$ м/с, ударили по гвоздю. Вся кинетическая энергия перешла в тепло. На сколько Джоулей увеличилась $U$ гвоздя?', ans: 4, tol: 0.1, why: '$E_к = \\dfrac{mv^2}{2} = \\dfrac{0{,}5 \\cdot 16}{2} = 4$ Дж $= \\Delta U$.' },
{ q: 'Газу сообщили $Q = 500$ Дж, при этом $\\Delta U = 350$ Дж. Какую работу совершил газ?', ans: 150, tol: 3, why: '$A = Q - \\Delta U = 500 - 350 = 150$ Дж.' },
],
p3: [ // Теплопроводность
{ q: 'Через стенку площадью $S = 2$ м² с разностью температур $\\Delta T = 20$ К за $t = 100$ с прошло $Q = 200$ Дж. Найдите тепловой поток (Вт): $P = Q/t$.', ans: 2, tol: 0.05, why: '$P = Q/t = 200 / 100 = 2$ Вт.' },
{ q: 'Тепловой поток через стенку $P = 50$ Вт. Сколько джоулей теплоты пройдёт через неё за $t = 1$ час?', ans: 180000, tol: 1000, why: '$Q = P \\cdot t = 50 \\cdot 3600 = 180\\,000$ Дж.' },
{ q: 'У какого материала теплопроводность больше при прочих равных: у $\\lambda_1 = 400$ Вт/(м·К) (медь) или $\\lambda_2 = 0{,}5$ Вт/(м·К) (вода)? Введите $\\lambda_1/\\lambda_2$.', ans: 800, tol: 5, why: '$\\lambda_1 / \\lambda_2 = 400 / 0{,}5 = 800$ — металлы намного лучше проводят тепло.' },
{ q: 'Стенка толщиной $d_1 = 0{,}1$ м заменена на стенку толщиной $d_2 = 0{,}05$ м из того же материала. Во сколько раз вырастет тепловой поток?', ans: 2, tol: 0.05, why: 'Поток $P \\propto 1/d$, поэтому $P_2/P_1 = d_1/d_2 = 0{,}1/0{,}05 = 2$.' },
{ q: 'Площадь стенки увеличили в 3 раза. Во сколько раз вырастет тепловой поток (при той же толщине и $\\Delta T$)?', ans: 3, tol: 0.05, why: 'Поток $P \\propto S$, поэтому увеличивается в 3 раза.' },
],
p4: [ // Конвекция
{ q: 'Плотность тёплого воздуха в $\\rho_1 = 1{,}1$ кг/м³, холодного $\\rho_2 = 1{,}3$ кг/м³. На сколько % холодный плотнее? $((\\rho_2 - \\rho_1)/\\rho_1) \\cdot 100$.', ans: 18, tol: 1, why: '$(1{,}3 - 1{,}1)/1{,}1 \\cdot 100 \\approx 18\\,\\%$.' },
{ q: 'Радиатор отдаёт мощность $P = 1500$ Вт, нагревая воздух массой $m = 50$ кг за $t = 60$ с. На сколько $\\Delta T$ нагрелся воздух? ($c_{возд} = 1000$ Дж/(кг·К))', ans: 1.8, tol: 0.1, why: '$\\Delta T = Q/(cm) = (P\\cdot t)/(cm) = (1500 \\cdot 60)/(1000 \\cdot 50) = 1{,}8$ К.' },
{ q: 'Вода нагревается снизу. Где будет тёплая вода: $a)$ снизу, $b)$ сверху? Введите 2, если сверху, 1, если снизу.', ans: 2, tol: 0.1, why: 'Тёплая вода легче — поднимается вверх. Это и есть конвекция.' },
{ q: 'Холодильник остужает $m = 2$ кг воздуха с $T_1 = 25$ до $T_2 = 5\\,^\\circ$C. Какое тепло (в кДж) он унёс? ($c = 1000$)', ans: 40, tol: 1, why: '$Q = cm\\Delta T = 1000 \\cdot 2 \\cdot 20 = 40\\,000$ Дж $= 40$ кДж.' },
{ q: 'Ветер охлаждает кожу. Если без ветра тело отдаёт $P_0 = 50$ Вт, а с ветром $P = 200$ Вт, во сколько раз быстрее идёт теплоотдача?', ans: 4, tol: 0.1, why: '$P/P_0 = 200/50 = 4$ раза.' },
],
p5: [ // Излучение
{ q: 'Солнце нагревает квадратный метр земной поверхности с мощностью $P = 1000$ Вт. Сколько теплоты получит $S = 5$ м² за $t = 60$ с?', ans: 300000, tol: 5000, why: '$Q = P \\cdot S \\cdot t = 1000 \\cdot 5 \\cdot 60 = 300\\,000$ Дж = $300$ кДж.' },
{ q: 'Черное тело излучает в 2 раза эффективнее белого. Если белое тело отдаёт $P_1 = 100$ Вт, сколько отдаст чёрное при той же $T$?', ans: 200, tol: 5, why: '$P_{черн} = 2 \\cdot P_{белого} = 2 \\cdot 100 = 200$ Вт.' },
{ q: 'Какая температура (в К) горячей плиты, если её излучение в 16 раз сильнее излучения тела при $T_0 = 300$ К? ($P \\propto T^4$)', ans: 600, tol: 10, why: '$P/P_0 = (T/T_0)^4 = 16$, откуда $T/T_0 = 2$, $T = 600$ К.' },
{ q: 'Какой цвет одежды летом холоднее: белый или чёрный? Введите 1, если чёрный, 2, если белый.', ans: 2, tol: 0.1, why: 'Белая отражает солнечное излучение лучше — в ней прохладнее.' },
{ q: 'Тело площадью $S = 0{,}5$ м² излучает $P = 200$ Вт. Найдите интенсивность излучения $I = P/S$ (Вт/м²).', ans: 400, tol: 10, why: '$I = P/S = 200/0{,}5 = 400$ Вт/м².' },
],
p8: [ // Плавление (Q = λm)
{ q: 'Сколько теплоты (в кДж) нужно для плавления $m = 2$ кг льда при $0\\,^\\circ$C? ($\\lambda_{льда} = 330$ кДж/кг)', ans: 660, tol: 5, why: '$Q = \\lambda m = 330 \\cdot 2 = 660$ кДж.' },
{ q: 'Какая масса (в кг) свинца расплавится, получив $Q = 50$ кДж? ($\\lambda_{св} = 25$ кДж/кг)', ans: 2, tol: 0.1, why: '$m = Q/\\lambda = 50/25 = 2$ кг.' },
{ q: 'Найдите удельную теплоту плавления вещества (кДж/кг), если на плавление $m = 0{,}5$ кг затрачено $Q = 100$ кДж.', ans: 200, tol: 5, why: '$\\lambda = Q/m = 100/0{,}5 = 200$ кДж/кг.' },
{ q: 'Сколько теплоты (кДж) нужно, чтобы расплавить $m = 5$ кг алюминия при $T_{пл}$? ($\\lambda_{Al} = 380$ кДж/кг)', ans: 1900, tol: 20, why: '$Q = \\lambda m = 380 \\cdot 5 = 1900$ кДж.' },
{ q: 'Лёд массой $m = 1$ кг при $0\\,^\\circ$C сначала нагрели до $t = 0\\,^\\circ$C (не нужно тепла), затем расплавили. Сколько кДж потратили? ($\\lambda = 330$)', ans: 330, tol: 3, why: '$Q = \\lambda m = 330 \\cdot 1 = 330$ кДж — только на плавление.' },
],
p10: [ // Испарение (Q = rm)
{ q: 'Сколько теплоты (в кДж) нужно, чтобы испарить $m = 0{,}2$ кг воды при $100\\,^\\circ$C? ($r_{воды} = 2300$ кДж/кг)', ans: 460, tol: 5, why: '$Q = rm = 2300 \\cdot 0{,}2 = 460$ кДж.' },
{ q: 'При испарении $m = 5$ кг этилового спирта поглощено $Q = 4500$ кДж. Найдите $r$ (кДж/кг).', ans: 900, tol: 10, why: '$r = Q/m = 4500/5 = 900$ кДж/кг.' },
{ q: 'Какая масса (в кг) воды испарится, если ей сообщили $Q = 1150$ кДж при $100\\,^\\circ$C? ($r = 2300$)', ans: 0.5, tol: 0.02, why: '$m = Q/r = 1150/2300 = 0{,}5$ кг.' },
{ q: 'Лужа площадью $S = 0{,}5$ м² и толщиной $d = 1$ мм испаряется. Сколько кДж нужно? ($\\rho_{воды} = 1000$ кг/м³, $r = 2300$ кДж/кг)', ans: 1.15, tol: 0.05, why: '$V = Sd = 0{,}5 \\cdot 0{,}001 = 5 \\cdot 10^{-4}$ м³, $m = \\rho V = 0{,}5$ кг $\\cdot 10^{-3} = 0{,}0005$ кг, $Q = rm = 2300 \\cdot 0{,}0005 = 1{,}15$ кДж.' },
{ q: 'Почему пот холодит кожу? При испарении пота поглощается теплота. Если испарилось $m = 100$ г пота ($r \\approx 2400$ кДж/кг), сколько кДж унесено с кожи?', ans: 240, tol: 5, why: '$Q = rm = 2400 \\cdot 0{,}1 = 240$ кДж.' },
],
};
// === Generate iv5 widget HTML + initializer function per pid ===
function makeIv5Widget(pid) {
const n = pid.slice(1);
return `
/* IV5 — Расчётные задачи (auto-injected) */
h += '<div class="wg">'
+'<div class="wg-header"><span class="wg-badge">IV-5</span><div class="wg-title">Тренажёр: ${TASKS[pid].length} расчётных задач</div></div>'
+'<div class="wg-help">Введи числовой ответ (можно с точкой как разделителем). Решено все верно — +20 XP.</div>'
+'<div id="${pid}-tasks5"></div>'
+'<div class="score-display" style="margin-top:10px"><span>Задача: <b id="${pid}-tasks5-i">1</b> / ${TASKS[pid].length}</span><span>Правильно: <b id="${pid}-tasks5-ok">0</b></span></div>'
+'</div>';
`;
}
function makeIv5Init(pid) {
const n = pid.slice(1);
const tasksLit = JSON.stringify(TASKS[pid]);
return `
function _init${pid}_iv5(){
const TASKS = ${tasksLit};
let i = 0, ok = 0, awarded = false;
function render(){
const t = TASKS[i]; const wrap = document.getElementById('${pid}-tasks5'); if(!wrap) return;
wrap.innerHTML =
'<div style="padding:10px 14px;background:rgba(15,23,42,.04);border-radius:9px;margin-bottom:10px;font-size:.95rem;line-height:1.55"><b>Задача '+(i+1)+'.</b> '+t.q+'</div>'
+'<div class="actions"><input type="number" step="0.001" class="tinp" id="${pid}-iv5-inp" placeholder="число" style="width:140px">'
+'<button class="btn primary" id="${pid}-iv5-go">Ответ</button>'
+'<button class="btn" id="${pid}-iv5-hint">Подсказка</button>'
+'<button class="btn" id="${pid}-iv5-next">Следующая</button></div>'
+'<details class="spoiler" id="${pid}-iv5-why-wrap" style="margin-top:8px;display:none"><summary>Решение</summary><div class="spoiler-body">'+t.why+'</div></details>'
+'<div class="feedback" id="${pid}-iv5-fb"></div>';
if (window.renderMathInElement) try { renderMathInElement(wrap, {delimiters:[{left:'$',right:'$',display:false}],throwOnError:false}); } catch(e){}
document.getElementById('${pid}-iv5-go').onclick = () => {
const v = parseFloat(document.getElementById('${pid}-iv5-inp').value.replace(',','.'));
const fb = document.getElementById('${pid}-iv5-fb');
const wh = document.getElementById('${pid}-iv5-why-wrap');
if (Math.abs(v - t.ans) <= t.tol) {
fb.className = 'feedback ok'; fb.innerHTML = 'Верно!'; ok++;
document.getElementById('${pid}-tasks5-ok').textContent = ok;
wh.style.display = 'block';
} else {
fb.className = 'feedback fail'; fb.innerHTML = 'Не совсем. Ожидался $' + t.ans + '$. Загляни в подсказку.';
if (window.renderMathInElement) try { renderMathInElement(fb, {delimiters:[{left:'$',right:'$',display:false}],throwOnError:false}); } catch(e){}
}
};
document.getElementById('${pid}-iv5-hint').onclick = () => {
const wh = document.getElementById('${pid}-iv5-why-wrap');
wh.style.display = wh.style.display === 'block' ? 'none' : 'block';
};
document.getElementById('${pid}-iv5-next').onclick = () => {
i = (i + 1) % TASKS.length;
document.getElementById('${pid}-tasks5-i').textContent = i + 1;
render();
if (ok === TASKS.length && !awarded) { awarded = true; if (typeof addXp === 'function') addXp(20, '${pid}-iv5'); }
};
}
render();
}
`;
}
// === Patch ch1 file ===
let patchedCount = 0;
for (const pid of Object.keys(TASKS)) {
// 1. Append IV-5 widget HTML inside build_pN before `box.innerHTML = h + secNavFor`
// 2. Append init call after wireReadBtn line
// 3. Append _initPN_iv5 function after the build_pN function block
const widget = makeIv5Widget(pid);
const init = makeIv5Init(pid);
// Marker to insert widget: find `box.innerHTML = h + secNavFor('pN') + readButton('pN');`
const insertWidgetBefore = `box.innerHTML = h + secNavFor('${pid}') + readButton('${pid}');`;
if (!h.includes(insertWidgetBefore)) {
console.warn(`${pid}: insert marker not found`);
continue;
}
h = h.replace(insertWidgetBefore, widget.trim() + '\n\n ' + insertWidgetBefore);
// Marker to insert init call: after `wireReadBtn('pN');` add `_initPN_iv5();`
const wireMarker = `wireReadBtn('${pid}');`;
h = h.replace(wireMarker, wireMarker + `\n _init${pid}_iv5();`);
// Append the _initPN_iv5 function — insert before the closing }\n of build_pN
// Search end of build_pN
const fnStart = h.indexOf(`function build_${pid}()`);
// Find closing brace of the function (look for `\n}\n` after fnStart, going past the new init call)
const fnEnd = h.indexOf('\n}\n', fnStart);
// Insert init function AFTER build_pN closing brace
const insertPos = fnEnd + 3; // skip "\n}\n"
h = h.slice(0, insertPos) + '\n' + init.trim() + '\n' + h.slice(insertPos);
patchedCount++;
console.log(` ${pid}: patched (${TASKS[pid].length} tasks)`);
}
fs.writeFileSync(DST, h);
console.log('Patched', patchedCount, '/', Object.keys(TASKS).length, 'paragraphs');
console.log('File size:', h.length);
// Sanity parse
const scripts = [...h.matchAll(/<script>([\s\S]*?)<\/script>/g)];
for (const m of scripts) {
try { new Function(m[1]); }
catch(e) { console.error('JS PARSE FAIL:', e.message); process.exit(1); }
}
console.log('inline JS parses OK');