'use strict'; /* ═══════════════════════════════════════════════════════════════ Интерактив «Весы и системы счисления» — 8 класс ═══════════════════════════════════════════════════════════════ */ const NS_COLORS = [ '#6aa9ff', // 0 — единицы '#4ade80', // 1 '#fde68a', // 2 '#fca5a5', // 3 '#c792ea', // 4 '#7ee787', // 5 '#93c5fd', // 6 '#fbbf24', // 6 ]; const state = { base: 3, target: 17, weights: [], // { id, mass, power, location: 'pile' | 'pan' } }; /* ── Утилиты ─────────────────────────────────────────────── */ function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); } // Разложение числа по основанию: digits[0] — младший разряд function toBase(n, base) { if (n === 0) return [0]; const digits = []; while (n > 0) { digits.push(n % base); n = Math.floor(n / base); } return digits; } function plural(n, forms) { // forms = [ед.ч., род.пад. ед. (2-4), мн.ч.] const m10 = n % 10; const m100 = n % 100; if (m10 === 1 && m100 !== 11) return forms[0]; if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) return forms[1]; return forms[2]; } function chipColor(power) { return NS_COLORS[power % NS_COLORS.length]; } /* ── Генерация набора гирек ──────────────────────────────── */ function generateWeights(base, target) { const weights = []; const maxExp = Math.max(0, Math.floor(Math.log(target) / Math.log(base))); const copies = Math.max(base - 1, 3); // минимум 3 для экспериментов for (let e = 0; e <= maxExp; e++) { const mass = Math.pow(base, e); for (let i = 0; i < copies; i++) { weights.push({ id: `w-${e}-${i}`, mass, power: e, location: 'pile', }); } } return weights; } /* ── Рендер ──────────────────────────────────────────────── */ function makeChip(w) { const el = document.createElement('div'); el.className = 'ns-chip'; el.draggable = true; el.dataset.id = w.id; el.dataset.mass = w.mass; el.style.setProperty('--chip-color', chipColor(w.power)); el.textContent = w.mass; el.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', w.id); e.dataTransfer.effectAllowed = 'move'; el.classList.add('ns-chip-dragging'); }); el.addEventListener('dragend', () => { el.classList.remove('ns-chip-dragging'); }); el.addEventListener('click', () => { // Клик как fallback: переключить локацию w.location = w.location === 'pile' ? 'pan' : 'pile'; render(); }); return el; } function render() { // Левая чаша const panGrid = document.getElementById('nsPanGrid'); panGrid.innerHTML = ''; const onPan = state.weights.filter(w => w.location === 'pan'); for (const w of onPan) panGrid.appendChild(makeChip(w)); // Куча const pileRows = document.getElementById('nsPileRows'); pileRows.innerHTML = ''; // Группируем по степеням в порядке убывания const powers = [...new Set(state.weights.map(w => w.power))].sort((a, b) => b - a); for (const p of powers) { const mass = Math.pow(state.base, p); const onPile = state.weights.filter(w => w.location === 'pile' && w.power === p); const row = document.createElement('div'); row.className = 'ns-pile-row'; const head = document.createElement('div'); head.className = 'ns-pile-head'; head.innerHTML = ` ${mass} кг ${state.base}${p} `; const chips = document.createElement('div'); chips.className = 'ns-pile-chips'; chips.dataset.drop = 'pile'; for (const w of onPile) chips.appendChild(makeChip(w)); row.appendChild(head); row.appendChild(chips); pileRows.appendChild(row); } // Итоги и статус const panTotal = onPan.reduce((s, w) => s + w.mass, 0); document.getElementById('nsPanTotal').textContent = panTotal; updateStatus(panTotal); } function updateStatus(panTotal) { const statusEl = document.getElementById('nsScaleStatus'); const iconEl = document.getElementById('nsScaleIcon'); const panLeft = document.getElementById('nsPanLeft'); const target = state.target; statusEl.classList.remove('ok', 'over', 'under'); panLeft.classList.remove('ns-balanced'); if (panTotal === target) { statusEl.classList.add('ok'); statusEl.textContent = '⚖️ Равновесие! Нажмите «Взвесить»'; panLeft.classList.add('ns-balanced'); iconEl.style.transform = 'scale(1.1)'; } else if (panTotal < target) { statusEl.classList.add('under'); statusEl.textContent = `На чаше ${panTotal} кг — товар тяжелее на ${target - panTotal} кг`; iconEl.style.transform = ''; } else { statusEl.classList.add('over'); statusEl.textContent = `На чаше ${panTotal} кг — гирьки тяжелее на ${panTotal - target} кг`; iconEl.style.transform = ''; } } /* ── Drag-n-drop (глобальный делегированный обработчик) ──── */ document.addEventListener('dragover', e => { const zone = e.target.closest('[data-drop]'); if (zone) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } }); document.addEventListener('drop', e => { const zone = e.target.closest('[data-drop]'); if (!zone) return; e.preventDefault(); const id = e.dataTransfer.getData('text/plain'); const w = state.weights.find(x => x.id === id); if (!w) return; w.location = zone.dataset.drop; render(); }); /* ── Действия ────────────────────────────────────────────── */ function startTask() { const baseInput = document.getElementById('nsBase'); const targetInput = document.getElementById('nsTarget'); const base = parseInt(baseInput.value, 10); const target = parseInt(targetInput.value, 10); if (!(base >= 2 && base <= 9)) { alert('Основание системы должно быть от 2 до 9.'); return; } if (!(target >= 1 && target <= 9999)) { alert('Вес товара должен быть от 1 до 9999 кг.'); return; } state.base = base; state.target = target; state.weights = generateWeights(base, target); document.getElementById('nsGoodsMass').textContent = target; document.getElementById('nsStage').classList.remove('ns-hidden'); document.getElementById('nsResult').classList.add('ns-hidden'); render(); document.getElementById('nsStage').scrollIntoView({ behavior: 'smooth', block: 'start' }); } function resetWeights() { for (const w of state.weights) w.location = 'pile'; document.getElementById('nsResult').classList.add('ns-hidden'); render(); } function clearTask() { document.getElementById('nsStage').classList.add('ns-hidden'); document.getElementById('nsResult').classList.add('ns-hidden'); } function weigh() { const onPan = state.weights.filter(w => w.location === 'pan'); const panTotal = onPan.reduce((s, w) => s + w.mass, 0); const target = state.target; const base = state.base; const resultEl = document.getElementById('nsResult'); resultEl.classList.remove('ns-hidden'); // Если не в равновесии if (panTotal !== target) { resultEl.innerHTML = `
✗ Весы не в равновесии

На чаше ${panTotal} кг, товар — ${target} кг. Разница: ${Math.abs(panTotal - target)} кг.

Добавьте ещё гирьки или уберите лишние, чтобы уравновесить чаши.

`; resultEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); return; } // Разложение const digits = toBase(target, base); // digits[0] — младший const minCount = digits.reduce((s, d) => s + d, 0); const userCount = onPan.length; const isOptimal = userCount === minCount; // Разложение от старшего к младшему const breakdown = digits.map((count, power) => ({ count, power, mass: Math.pow(base, power) })).filter(x => x.count > 0).reverse(); const digitStr = digits.slice().reverse().join(''); const formulaParts = breakdown .map(b => `${b.count} · ${base}${b.power}`) .join(' + '); resultEl.innerHTML = `
${isOptimal ? '✓ Идеально! Минимум гирек' : '✓ Взвешено!'}

Вы положили ${userCount} ${plural(userCount, ['гирьку', 'гирьки', 'гирек'])} общей массой ${target} кг.

${isOptimal ? `

Это минимально возможное число гирек для такого веса.

` : `

Можно обойтись ${minCount} ${plural(minCount, ['гирькой', 'гирьками', 'гирьками'])} — попробуйте найти такой набор.

` }
Перевод из десятичной в ${base}-ричную систему
${target}10 = ${formulaParts} = ${digitStr}${base}
${breakdown.map(b => `
${b.count} ${b.mass} кг ${base}${b.power}
`).join('')}
`; resultEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } /* ── Инициализация ───────────────────────────────────────── */ function init() { const startBtn = document.getElementById('nsStart'); if (!startBtn) return; startBtn.addEventListener('click', startTask); document.getElementById('nsWeigh').addEventListener('click', weigh); document.getElementById('nsReset').addEventListener('click', resetWeights); document.getElementById('nsClear').addEventListener('click', clearTask); // Enter в полях ввода → старт document.getElementById('nsBase').addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); startTask(); } }); document.getElementById('nsTarget').addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); startTask(); } }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }