// Minimal app logic for math training (addition & subtraction) const state = { ops: new Set(), mode: null, // 'timed' or 'training' score: 0, currentProblem: null, answerBuffer: '', timerId: null, timeLeft: 60, settings: { timedSeconds: 60, maxResult: 40, maxOperand: 20, sessionProblems: 20, allowNegative: false, allowFraction: false, } } // elements const menuScreen = document.getElementById('menu-screen') const playScreen = document.getElementById('play-screen') const opsContainer = document.getElementById('ops') const modeButtons = document.querySelectorAll('.mode-btn') const opButtons = document.querySelectorAll('.op-btn') const backBtn = document.getElementById('back-btn') const problemEl = document.getElementById('problem') const answerEl = document.getElementById('answer') const feedbackEl = document.getElementById('feedback') const timerEl = document.getElementById('timer') const scoreEl = document.getElementById('score') const keypad = document.querySelectorAll('.key') const submitBtn = document.getElementById('submit') const clearBtn = document.getElementById('clear') const backspaceBtn = document.getElementById('backspace') const historyBtn = document.getElementById('history-btn') const historyScreen = document.getElementById('history-screen') const historyBack = document.getElementById('history-back') const historyList = document.getElementById('history-list') const clearHistoryBtn = document.getElementById('clear-history') const historyPanel = document.getElementById('history-panel') const historyToggle = document.getElementById('history-toggle') const settingTimed = document.getElementById('setting-timed') const settingMaxResult = document.getElementById('setting-max-result') const settingMaxOperand = document.getElementById('setting-max-operand') const settingAllowNegative = document.getElementById('setting-allow-negative') const settingAllowFraction = document.getElementById('setting-allow-fraction') const saveSettingsBtn = document.getElementById('save-settings') const resetSettingsBtn = document.getElementById('reset-settings') const settingsToggle = document.getElementById('settings-toggle') const settingsPanel = document.getElementById('settings-panel') const progressInner = document.getElementById('progress-inner') const dotBtn = document.getElementById('dot') const negateBtn = document.getElementById('negate') const summaryPanel = document.getElementById('summary-panel') const summaryText = document.getElementById('summary-text') const summaryBack = document.getElementById('summary-back') // load settings from localStorage function loadSettings(){ try{ const raw = localStorage.getItem('matma:settings') if (raw) { const s = JSON.parse(raw) state.settings = Object.assign(state.settings, s) } }catch(e){ console.warn('settings load failed', e) } // reflect to inputs settingTimed.value = state.settings.timedSeconds settingMaxResult.value = state.settings.maxResult settingMaxOperand.value = state.settings.maxOperand const sess = state.settings.sessionProblems || 20 const settingSessionProblems = document.getElementById('setting-session-problems') if (settingSessionProblems) settingSessionProblems.value = sess if (settingAllowNegative) settingAllowNegative.checked = !!state.settings.allowNegative if (settingAllowFraction) settingAllowFraction.checked = !!state.settings.allowFraction } function saveSettings(){ const timed = parseInt(settingTimed.value,10) || state.settings.timedSeconds const maxResult = parseInt(settingMaxResult.value,10) || state.settings.maxResult const maxOperand = parseInt(settingMaxOperand.value,10) || state.settings.maxOperand state.settings.timedSeconds = Math.max(5, Math.min(600, timed)) state.settings.maxResult = Math.max(1, Math.min(999, maxResult)) state.settings.maxOperand = Math.max(1, Math.min(999, maxOperand)) const settingSessionProblems = document.getElementById('setting-session-problems') if (settingSessionProblems) state.settings.sessionProblems = Math.max(1, Math.min(500, parseInt(settingSessionProblems.value,10) || state.settings.sessionProblems)) if (settingAllowNegative) state.settings.allowNegative = !!settingAllowNegative.checked if (settingAllowFraction) state.settings.allowFraction = !!settingAllowFraction.checked try{ localStorage.setItem('matma:settings', JSON.stringify(state.settings)) }catch(e){console.warn('save failed',e)} // visual feedback feedbackEl.textContent = 'Ustawienia zapisane' setTimeout(()=>feedbackEl.textContent = '',1500) } function resetSettings(){ state.settings = {timedSeconds:60, maxResult:40, maxOperand:20, sessionProblems:20} saveSettings() loadSettings() } saveSettingsBtn.addEventListener('click', saveSettings) resetSettingsBtn.addEventListener('click', resetSettings) // settings toggle settingsToggle.addEventListener('click', ()=>{ const collapsed = settingsPanel.classList.toggle('collapsed') settingsToggle.textContent = collapsed ? 'Pokaż' : 'Ukryj' }) loadSettings() // initial wiring opButtons.forEach(b => { if (b.classList.contains('disabled')) return b.addEventListener('click', () => { const op = b.dataset.op if (state.ops.has(op)) { state.ops.delete(op); b.classList.remove('active') } else { state.ops.add(op); b.classList.add('active') } }) }) modeButtons.forEach(b => b.addEventListener('click', () => { modeButtons.forEach(x=>x.classList.remove('active')) b.classList.add('active') state.mode = b.dataset.mode // require at least one op selected to start if (state.ops.size === 0) { feedbackEl.textContent = 'Wybierz co najmniej jedno działanie.' return } startPlay() })) backBtn.addEventListener('click', ()=>{ stopTimer() playScreen.classList.add('hidden') menuScreen.classList.remove('hidden') feedbackEl.textContent = '' }) function startPlay(){ menuScreen.classList.add('hidden') playScreen.classList.remove('hidden') state.score = 0 scoreEl.textContent = state.score state.answerBuffer = '' answerEl.textContent = '' feedbackEl.textContent = '' state.currentProblem = generateProblem() renderProblem() // make problem larger for mobile clarity problemEl.classList.add('big') // init progress if (state.mode === 'timed'){ progressInner.style.width = '0%' startTimer(state.settings.timedSeconds) } else { progressInner.style.width = '0%' state.sessionSolved = 0 state.sessionTarget = Math.max(1, state.settings.sessionProblems || 20) updateProgress() hideTimer() } } function generateProblem(){ // choose an operation from selected ops const ops = Array.from(state.ops) const op = ops[Math.floor(Math.random()*ops.length)] // generate operands within maxOperand and ensure result within maxResult const maxOp = Math.max(1, state.settings.maxOperand) const maxRes = Math.max(1, state.settings.maxResult) let a = 0, b = 0 // try a few times to satisfy constraints for (let i=0;i<200;i++){ if (op === 'div'){ // divisor b should be non-zero b = randInt(1, maxOp) if (!state.settings.allowFraction){ // produce integer result q such that a = q * b and a <= maxOp and q <= maxRes const maxQByOp = Math.floor(maxOp / b) const maxQ = Math.min(maxRes, maxQByOp) if (maxQ < 0) continue const q = randInt(0, maxQ) a = q * b // optional negative results if (state.settings.allowNegative && Math.random() < 0.2) a = -a return {a,b,op} } else { // allow fractional results: generate tenths (one decimal place) const maxResultNByOp = Math.floor((10 * maxOp) / b) const maxResultN = Math.min(maxRes * 10, maxResultNByOp) if (maxResultN < 0) continue const result_n = randInt(0, maxResultN) // ensure dividend is integer: (result_n * b) % 10 === 0 if ((result_n * b) % 10 !== 0) continue a = (result_n * b) / 10 if (state.settings.allowNegative && Math.random() < 0.2) a = -a return {a,b,op} } } else if (op === 'mul'){ a = randInt(0, maxOp) b = randInt(0, maxOp) if (a * b <= maxRes) return {a,b,op} } else if (op === 'add'){ a = randInt(0, maxOp) b = randInt(0, maxOp) if (a + b <= maxRes) return {a,b,op} } else if (op === 'sub'){ a = randInt(0, maxOp) b = randInt(0, maxOp) if (!state.settings.allowNegative){ if (a < b) [a,b] = [b,a] if (a - b <= maxRes) return {a,b,op} } else { // negative allowed: result absolute should be <= maxRes if (Math.abs(a - b) <= maxRes) return {a,b,op} } } } // fallback: return simple small numbers a = Math.min(maxOp, Math.floor(maxRes/2)) b = Math.min(maxOp, 1) return {a,b,op} } function randInt(min,max){ return Math.floor(Math.random()*(max-min+1))+min } function renderProblem(){ const p = state.currentProblem const map = {add: '+', sub: '−', mul: '×', div: '÷'} const symbol = map[p.op] || '?' problemEl.textContent = `${p.a} ${symbol} ${p.b} = ?` } function submitAnswer(){ const buf = state.answerBuffer.trim() if (buf.length === 0) return const given = parseFloat(buf) const p = state.currentProblem let correct = null if (p.op === 'add') correct = p.a + p.b else if (p.op === 'sub') correct = p.a - p.b else if (p.op === 'mul') correct = p.a * p.b else if (p.op === 'div') correct = (p.b === 0) ? null : (p.a / p.b) const eps = state.settings.allowFraction ? 1e-6 : 1e-9 const isCorrect = (correct !== null) && (Math.abs(given - correct) < eps) if (isCorrect) { state.score += 1 feedbackEl.textContent = '✔ dobrze' } else { feedbackEl.textContent = `✖ poprawne: ${Number.isFinite(correct) ? correct : '—'}` } scoreEl.textContent = state.score // next problem state.answerBuffer = '' answerEl.textContent = '' // increment solved count and handle training limit if (state.mode === 'training'){ state.sessionSolved = (state.sessionSolved || 0) + 1 updateProgress() if (state.sessionSolved >= state.sessionTarget){ // reached limit -> show summary instead of next problem saveSessionToHistory() showSummary() return } } // otherwise generate next problem state.currentProblem = generateProblem() renderProblem() } submitBtn.addEventListener('click', submitAnswer) keypad.forEach(k => { k.addEventListener('click', ()=>{ const v = k.textContent.trim() if (!/^[0-9]$/.test(v)) return if (state.answerBuffer.length >= 12) return state.answerBuffer += v answerEl.textContent = state.answerBuffer }) }) if (dotBtn){ dotBtn.addEventListener('click', ()=>{ if (!state.settings.allowFraction) return if (state.answerBuffer.includes('.')) return if (state.answerBuffer.length === 0) state.answerBuffer = '0' state.answerBuffer += '.' answerEl.textContent = state.answerBuffer }) } if (negateBtn){ negateBtn.addEventListener('click', ()=>{ if (!state.settings.allowNegative) return if (state.answerBuffer.startsWith('-')) state.answerBuffer = state.answerBuffer.slice(1) else state.answerBuffer = '-' + state.answerBuffer answerEl.textContent = state.answerBuffer }) } clearBtn.addEventListener('click', ()=>{ state.answerBuffer = '' answerEl.textContent = '' }) backspaceBtn.addEventListener('click', ()=>{ state.answerBuffer = state.answerBuffer.slice(0,-1) answerEl.textContent = state.answerBuffer }) function startTimer(seconds){ state.timeLeft = seconds timerEl.textContent = state.timeLeft timerEl.classList.remove('hidden') state.timerId = setInterval(()=>{ state.timeLeft -= 1 timerEl.textContent = state.timeLeft // update progress bar for timed mode const pct = Math.max(0, ((seconds - state.timeLeft) / seconds) * 100) progressInner.style.width = pct + '%' if (state.timeLeft <= 0) { stopTimer() endSession() } },1000) } function stopTimer(){ if (state.timerId) clearInterval(state.timerId) state.timerId = null } function hideTimer(){ timerEl.classList.add('hidden') } function endSession(){ feedbackEl.textContent = `Koniec! Wynik: ${state.score}` saveSessionToHistory() // fill progress progressInner.style.width = '100%' // return to menu after short pause (timed mode) setTimeout(()=>{ playScreen.classList.add('hidden') menuScreen.classList.remove('hidden') },2500) } function showSummary(){ // hide problem area and keypad, show summary panel const summaryTextEl = summaryText if (summaryTextEl) summaryTextEl.textContent = `Poprawne odpowiedzi: ${state.score}` // hide play UI sections const problemArea = document.querySelector('.problem-area') const answerArea = document.querySelector('.answer-area') if (problemArea) problemArea.classList.add('hidden') if (answerArea) answerArea.classList.add('hidden') if (summaryPanel) summaryPanel.classList.remove('hidden') } function hideSummary(){ const problemArea = document.querySelector('.problem-area') const answerArea = document.querySelector('.answer-area') if (problemArea) problemArea.classList.remove('hidden') if (answerArea) answerArea.classList.remove('hidden') if (summaryPanel) summaryPanel.classList.add('hidden') } if (summaryBack) summaryBack.addEventListener('click', ()=>{ // close summary and go back to menu hideSummary() playScreen.classList.add('hidden') menuScreen.classList.remove('hidden') }) function updateProgress(){ if (state.mode === 'training'){ const solved = state.sessionSolved || 0 const target = state.sessionTarget || 1 const pct = Math.min(100, Math.round((solved/target)*100)) progressInner.style.width = pct + '%' // show count in status const status = document.getElementById('status') if (status) status.textContent = `${solved}/${target}` } } // history persistence function loadHistory(){ try{ const raw = localStorage.getItem('matma:history') return raw ? JSON.parse(raw) : [] }catch(e){ return [] } } function saveSessionToHistory(){ const h = loadHistory() h.unshift({ts: Date.now(), mode: state.mode, score: state.score, settings: {...state.settings}}) // keep only last 100 while(h.length>100) h.pop() try{ localStorage.setItem('matma:history', JSON.stringify(h)) }catch(e){ } } function renderHistory(){ const h = loadHistory() historyList.innerHTML = '' if (h.length === 0) { historyList.innerHTML = '
Brak zapisanych sesji
' if (historyList) historyList.innerHTML = historyList.innerHTML return } h.forEach(item => { const d = new Date(item.ts).toLocaleString() const el = document.createElement('div') el.className = 'history-item' el.innerHTML = `
${item.mode === 'timed' ? 'Na czas' : 'Trening'} — Wynik: ${item.score}
${d} • czas:${item.settings.timedSeconds}s maxRes:${item.settings.maxResult} maxOp:${item.settings.maxOperand}
` historyList.appendChild(el) }) if (historyList) historyList.innerHTML = historyList.innerHTML } // Toggle in-menu history panel (behaves like settings) function toggleHistory(){ if (!historyPanel) return // toggle collapsed class (collapsed => hidden) const nowCollapsed = historyPanel.classList.toggle('collapsed') // nowCollapsed is true when panel is hidden if (!nowCollapsed) renderHistory() // keep main menu button label constant; header toggle shows state historyBtn.textContent = 'Historia' if (historyToggle) historyToggle.textContent = nowCollapsed ? 'Pokaż' : 'Ukryj' } // history toggle historyToggle.addEventListener('click', ()=>{ const collapsed = historyPanel.classList.toggle('collapsed') historyToggle.textContent = collapsed ? 'Pokaż' : 'Ukryj' if (!collapsed) renderHistory() else historyList.innerHTML = '' }) // historyBtn.addEventListener('click', toggleHistory) // if (historyToggle) historyToggle.addEventListener('click', toggleHistory) historyBack.addEventListener('click', ()=>{ historyScreen.classList.add('hidden') menuScreen.classList.remove('hidden') }) clearHistoryBtn.addEventListener('click', ()=>{ try{ localStorage.removeItem('matma:history') }catch(e){} renderHistory() }) // keyboard support: allow Enter/Backspace/0-9 window.addEventListener('keydown', (e)=>{ if (playScreen.classList.contains('hidden')) return if (/^[0-9]$/.test(e.key)){ if (state.answerBuffer.length < 6) { state.answerBuffer += e.key answerEl.textContent = state.answerBuffer } } else if (e.key === 'Backspace'){ state.answerBuffer = state.answerBuffer.slice(0,-1) answerEl.textContent = state.answerBuffer } else if (e.key === 'Enter'){ submitAnswer() } }) // small helper to set initial focus - mobile browsers will not open keyboard for divs document.addEventListener('touchstart', ()=>{}, {passive:true})