import { SYSTEMS, generateChoices } from './numerals.js'; let LANG = localStorage.getItem('numnum-lang') || (navigator.language.startsWith('ru') ? 'ru' : 'en'); const STRINGS = { en: { subtitle: 'Numeral systems trainer', startGame: 'Start', theory: 'Theory', chooseSystem: 'Choose numeral system', positional: 'Positional', nonPositional: 'Non-Positional', back: 'Back', numberRange: 'Number Range', difficulty: 'Difficulty level', answerMode: 'Answer Mode', timeLimit: 'Time Limit', numberOfRounds: 'Number of Questions', play: 'Go!', timeLeft: '\u23F1', submit: 'Submit', next: 'Next', results: 'Results', mistakes: 'Mistakes', playAgain: 'Start again', menu: 'Menu', choose: 'Choose', type: 'Type', noLimit: 'No limit', correct: 'Correct!', incorrect: 'Incorrect \u2014 answer: {n}', timesUp: "Time's up! Answer: {n}", pctCorrect: '{pct}% correct', congratulations: 'Congratulations!', pleaseSelect: 'Please select all options', installApp: 'Install App', theoryUnavailable: 'Content unavailable', sec: 'sec', chooseAnswer: 'Choose answer', enterEquivalent: 'Enter equivalent', systemNames: { binary: 'Binary', base3: 'Ternary', octal: 'Octal', hex: 'Hexadecimal', ternary: 'Balanced Ternary', braille: 'Braille Decimal', roman: 'Roman', slavonic: 'Church Slavonic', greek: 'Greek', hebrew: 'Hebrew', }, }, ru: { subtitle: '\u0422\u0440\u0435\u043D\u0430\u0436\u0435\u0440 \u0441\u0438\u0441\u0442\u0435\u043C \u0441\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F', startGame: 'Начать', theory: '\u0422\u0435\u043E\u0440\u0438\u044F', chooseSystem: '\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043C\u0443 \u0441\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F', positional: '\u041F\u043E\u0437\u0438\u0446\u0438\u043E\u043D\u043D\u044B\u0435', nonPositional: '\u041D\u0435\u043F\u043E\u0437\u0438\u0446\u0438\u043E\u043D\u043D\u044B\u0435', back: '\u041D\u0430\u0437\u0430\u0434', numberRange: '\u0414\u0438\u0430\u043F\u0430\u0437\u043E\u043D \u0447\u0438\u0441\u0435\u043B', difficulty: '\u0423\u0440\u043E\u0432\u0435\u043D\u044C \u0441\u043B\u043E\u0436\u043D\u043E\u0441\u0442\u0438', answerMode: '\u0420\u0435\u0436\u0438\u043C \u043E\u0442\u0432\u0435\u0442\u0430', timeLimit: '\u0412\u0440\u0435\u043C\u044F \u043D\u0430 \u043E\u0442\u0432\u0435\u0442', numberOfRounds: '\u041A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432', play: '\u0412\u043F\u0435\u0440\u0451\u0434!', timeLeft: '\u23F1', submit: '\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C', next: '\u0414\u0430\u043B\u0435\u0435', results: '\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B', mistakes: '\u041E\u0448\u0438\u0431\u043A\u0438', playAgain: 'Начать заново', menu: '\u041C\u0435\u043D\u044E', choose: '\u0412\u044B\u0431\u043E\u0440', type: '\u0412\u0432\u043E\u0434', noLimit: '\u0411\u0435\u0437 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0439', correct: '\u041F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E!', incorrect: '\u041D\u0435\u0432\u0435\u0440\u043D\u043E \u2014 \u043E\u0442\u0432\u0435\u0442: {n}', timesUp: '\u0412\u0440\u0435\u043C\u044F \u0432\u044B\u0448\u043B\u043E! \u041E\u0442\u0432\u0435\u0442: {n}', pctCorrect: '{pct}% \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u0445', congratulations: '\u041F\u043E\u0437\u0434\u0440\u0430\u0432\u043B\u044F\u0435\u043C!', pleaseSelect: '\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0441\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B', installApp: '\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435', theoryUnavailable: '\u041C\u0430\u0442\u0435\u0440\u0438\u0430\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D', sec: '\u0441\u0435\u043A', chooseAnswer: '\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043E\u0442\u0432\u0435\u0442', enterEquivalent: '\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043B\u043E', systemNames: { binary: '\u0414\u0432\u043E\u0438\u0447\u043D\u0430\u044F', base3: '\u0422\u0440\u043E\u0438\u0447\u043D\u0430\u044F', octal: '\u0412\u043E\u0441\u044C\u043C\u0435\u0440\u0438\u0447\u043D\u0430\u044F', hex: '\u0428\u0435\u0441\u0442\u043D\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043D\u0430\u044F', ternary: '\u0421\u0438\u043C\u043C\u0435\u0442\u0440\u0438\u0447\u043D\u0430\u044F \u0442\u0440\u043E\u0438\u0447\u043D\u0430\u044F', braille: '\u0414\u0435\u0441\u044F\u0442\u0438\u0447\u043D\u0430\u044F \u0411\u0440\u0430\u0439\u043B\u044F', roman: '\u0420\u0438\u043C\u0441\u043A\u0430\u044F', slavonic: '\u0426\u0435\u0440\u043A\u043E\u0432\u043D\u043E\u0441\u043B\u0430\u0432\u044F\u043D\u0441\u043A\u0430\u044F', greek: '\u0413\u0440\u0435\u0447\u0435\u0441\u043A\u0430\u044F', hebrew: '\u0415\u0432\u0440\u0435\u0439\u0441\u043A\u0430\u044F', }, }, }; function t(key, params) { let s = STRINGS[LANG][key] || STRINGS.en[key] || key; if (params) { for (const [k, v] of Object.entries(params)) { s = s.replace('{' + k + '}', v); } } return s; } function systemName(key) { return STRINGS[LANG].systemNames[key] || STRINGS.en.systemNames[key] || SYSTEMS[key].name; } function applyTranslations() { document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); }); } function refreshDynamicText() { const active = document.querySelector('.screen.active'); if (!active) return; switch (active.id) { case 'screen-setup-system': renderSetupSystem(); break; case 'screen-setup-range': renderSetupRange(); break; case 'screen-setup-options': document.querySelectorAll('#setup-modes .option-btn').forEach((btn, i) => { btn.textContent = t(MODES[i].labelKey); }); [...document.getElementById('setup-timing').options].forEach((opt, i) => { opt.textContent = TIMINGS[i].value === 0 ? t('noLimit') : TIMINGS[i].value + ' ' + t('sec'); }); break; case 'screen-theory-system': renderTheorySystem(); break; case 'screen-theory-content': document.getElementById('theory-title').textContent = systemName(lastTheoryKey); break; case 'screen-game': if (game.system && game.range) { document.getElementById('score-system-range').textContent = systemName(game.system) + ': ' + game.range.label; } break; case 'screen-results': { const completed = game.currentRound >= game.rounds; const total = completed ? game.rounds : game.currentRound; const pct = total > 0 ? Math.round((game.correctCount / total) * 100) : 0; document.getElementById('results-summary').innerHTML = '
' + game.correctCount + '/' + total + '
' + '
' + t('pctCorrect', {pct: pct}) + '
'; const labelEl = document.getElementById('results-mistakes-label'); if (game.mistakes.length > 0 || labelEl.style.display !== 'none') { labelEl.textContent = t('mistakes'); } const congratsEl = document.getElementById('results-congrats'); if (congratsEl.textContent) { congratsEl.textContent = t('congratulations'); } break; } } } const SYSTEM_RANGES = { binary: [ { label: '2\u2070 \u2013 2\u00B3', min: 1, max: 8 }, { label: '2\u00B3 \u2013 2\u2077', min: 8, max: 128 }, { label: '2\u2070 \u2013 2\u2077', min: 1, max: 128 }, { label: '2\u2070 \u2013 2\u00B9\u2075', min: 1, max: 32768 }, ], base3: [ { label: '3\u2070 \u2013 3\u00B9', min: 1, max: 3 }, { label: '3\u00B9 \u2013 3\u00B2', min: 3, max: 9 }, { label: '3\u00B2 \u2013 3\u00B3', min: 9, max: 27 }, { label: '3\u2070 \u2013 3\u00B3', min: 1, max: 27 }, ], ternary: [ { label: '3\u2070 \u2013 3\u00B9', min: 1, max: 3 }, { label: '\u20133\u00B9 \u2013 3\u00B9', min: -3, max: 3 }, { label: '\u20133\u00B2 \u2013 3\u00B2', min: -9, max: 9 }, { label: '\u20133\u00B3 \u2013 3\u00B3', min: -27, max: 27 }, ], octal: [ { label: '8\u00B9 \u2013 8\u00B2', min: 8, max: 64 }, { label: '8\u00B2 \u2013 8\u00B3', min: 64, max: 512 }, { label: '8\u2070 \u2013 8\u00B3', min: 1, max: 512 }, { label: '8\u2070 \u2013 8\u2074', min: 1, max: 4096 }, ], hex: [ { label: 'F\u2070 \u2013 F\u00B9', min: 1, max: 16 }, { label: 'F\u2070 \u2013 F\u00B2', min: 1, max: 256 }, { label: 'F\u00B2 \u2013 F\u2074', min: 256, max: 65536 }, { label: 'F\u2070 \u2013 F\u2074', min: 1, max: 65536 }, ], braille: [ { label: '0 \u2013 9', min: 0, max: 9 }, { label: '10 \u2013 99', min: 10, max: 99 }, { label: '100 \u2013 999', min: 100, max: 999 }, { label: '0 \u2013 9999', min: 0, max: 9999 }, ], roman: [ { label: '1 \u2013 9', min: 1, max: 9 }, { label: '10 \u2013 99', min: 10, max: 99 }, { label: '100 \u2013 999', min: 100, max: 999 }, { label: '1000 \u2013 3999', min: 1000, max: 3999 }, ], greek: [ { label: '1 \u2013 9', min: 1, max: 9 }, { label: '10 \u2013 19', min: 10, max: 19 }, { label: '20 \u2013 99', min: 20, max: 99 }, { label: '1 \u2013 99', min: 1, max: 99 }, { label: '100 \u2013 999', min: 100, max: 999 }, { label: '1000 \u2013 9999', min: 1000, max: 9999 }, ], slavonic: [ { label: '1 \u2013 9', min: 1, max: 9 }, { label: '10 \u2013 19', min: 10, max: 19 }, { label: '20 \u2013 99', min: 20, max: 99 }, { label: '1 \u2013 99', min: 1, max: 99 }, { label: '100 \u2013 999', min: 100, max: 999 }, { label: '1000 \u2013 9999', min: 1000, max: 9999 }, ], hebrew: [ { label: '1 \u2013 9', min: 1, max: 9 }, { label: '10 \u2013 19', min: 10, max: 19 }, { label: '20 \u2013 99', min: 20, max: 99 }, { label: '1 \u2013 99', min: 1, max: 99 }, { label: '100 \u2013 999', min: 100, max: 999 }, { label: '1000 \u2013 9999', min: 1000, max: 9999 }, ], }; const SYSTEM_GROUPS = { pos: ['binary', 'octal', 'hex', 'braille', 'base3', 'ternary'], nonpos: ['roman', 'greek', 'slavonic', 'hebrew'], }; const theoryCache = {}; let lastTheoryKey = null; const MODES = [ { labelKey: 'choose', value: 'choice' }, { labelKey: 'type', value: 'manual' }, ]; const TIMINGS = [ { value: 0 }, { value: 30 }, { value: 10 }, { value: 5 }, { value: 3 }, { value: 2 }, { value: 1 }, ]; const ROUNDS = [5, 10, 25, 50, 100]; let game = { system: null, range: null, mode: null, timing: null, rounds: null, currentRound: 0, correctCount: 0, correctAnswer: null, answered: false, timerInterval: null, timeLeft: 0, mistakes: [], }; let popstateHandled = false; let navDepth = 0; function showScreen(id) { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById('screen-' + id).classList.add('active'); if (id === 'menu') { if (navDepth > 0) { const n = navDepth; navDepth = 0; history.go(-n); } } else if (!popstateHandled) { navDepth++; history.pushState({ screen: id }, ''); } popstateHandled = false; } function shuffle(arr) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function randInt(min, max) { return min + Math.floor(Math.random() * (max - min + 1)); } // ---- SETUP ---- let setupState = {}; function renderSetupSystem() { setupState = {}; const posDiv = document.getElementById('setup-systems-pos'); const nonposDiv = document.getElementById('setup-systems-nonpos'); posDiv.innerHTML = ''; nonposDiv.innerHTML = ''; for (const [group, keys] of Object.entries(SYSTEM_GROUPS)) { const target = group === 'pos' ? posDiv : nonposDiv; keys.forEach(key => { const btn = document.createElement('button'); btn.className = 'option-btn'; btn.textContent = systemName(key); btn.addEventListener('click', () => { posDiv.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected')); nonposDiv.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); setupState.system = key; renderSetupRange(); }); target.appendChild(btn); }); } showScreen('setup-system'); } function renderSetupRange() { const ranges = SYSTEM_RANGES[setupState.system]; document.getElementById('range-title').textContent = t('numberRange') + ' \u2014 ' + systemName(setupState.system); const rangesDiv = document.getElementById('setup-ranges'); rangesDiv.innerHTML = ''; delete setupState.range; delete setupState.rangeData; ranges.forEach((r, i) => { const btn = document.createElement('button'); btn.className = 'option-btn'; btn.textContent = r.label; btn.addEventListener('click', () => { rangesDiv.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); setupState.range = i; setupState.rangeData = r; renderSetupOptions(); }); rangesDiv.appendChild(btn); }); showScreen('setup-range'); } function renderSetupOptions() { const defaultMode = 0; const defaultTiming = 0; const defaultRounds = 1; setupState.mode = defaultMode; setupState.timing = defaultTiming; setupState.rounds = defaultRounds; const modesDiv = document.getElementById('setup-modes'); modesDiv.innerHTML = ''; MODES.forEach((m, i) => { const btn = document.createElement('button'); btn.className = 'option-btn' + (i === defaultMode ? ' selected' : ''); btn.textContent = t(m.labelKey); btn.addEventListener('click', () => { modesDiv.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); setupState.mode = i; }); modesDiv.appendChild(btn); }); const timingSelect = document.getElementById('setup-timing'); timingSelect.innerHTML = ''; TIMINGS.forEach((tm, i) => { const opt = document.createElement('option'); opt.value = i; opt.textContent = tm.value === 0 ? t('noLimit') : tm.value + ' ' + t('sec'); timingSelect.appendChild(opt); }); timingSelect.value = defaultTiming; timingSelect.addEventListener('change', () => { setupState.timing = parseInt(timingSelect.value, 10); }); const roundsSelect = document.getElementById('setup-rounds'); roundsSelect.value = defaultRounds; roundsSelect.addEventListener('change', () => { setupState.rounds = parseInt(roundsSelect.value, 10); }); showScreen('setup-options'); } // ---- GAME ---- let lastSetupState = null; const MARK_CORRECT = '\u2713 '; const MARK_INCORRECT = '\u2717 '; function startGame() { lastSetupState = { ...setupState }; game.system = setupState.system; game.range = setupState.rangeData; game.mode = MODES[setupState.mode]?.value ?? setupState.mode; game.timing = TIMINGS[setupState.timing]?.value ?? setupState.timing; game.rounds = ROUNDS[setupState.rounds]; game.currentRound = 0; game.correctCount = 0; game.mistakes = []; game.lastAnswer = null; document.getElementById('score-system-range').textContent = systemName(game.system) + ': ' + game.range.label; document.getElementById('score-round').textContent = '0/' + game.rounds; document.getElementById('score-correct').textContent = MARK_CORRECT + '0'; document.getElementById('score-incorrect').textContent = MARK_INCORRECT + '0'; showScreen('game'); nextRound(); } function nextRound() { if (game.currentRound >= game.rounds) { endGame(); return; } game.currentRound++; game.answered = false; let num; do { num = randInt(game.range.min, game.range.max); } while (num === game.lastAnswer && game.range.min !== game.range.max); game.correctAnswer = num; game.lastAnswer = num; document.getElementById('score-round').textContent = game.currentRound + '/' + game.rounds; document.getElementById('score-correct').textContent = MARK_CORRECT + game.correctCount; const display = SYSTEMS[game.system].toDisplay(game.correctAnswer); const numEl = document.getElementById('number-display'); numEl.textContent = display; numEl.classList.toggle('small-text', display.length > 12); numEl.classList.toggle('slavonic-font', game.system === 'slavonic'); numEl.classList.toggle('mono', SYSTEM_GROUPS.pos.includes(game.system)); const feedback = document.getElementById('answer-feedback'); feedback.textContent = game.mode === 'choice' ? t('chooseAnswer') : t('enterEquivalent'); feedback.className = 'answer-feedback'; document.getElementById('btn-next').classList.add('hidden'); if (game.mode === 'choice') { renderChoices(); } else { renderNumpad(); } startTimer(); } function renderChoices() { const choiceArea = document.getElementById('choice-area'); const numpadArea = document.getElementById('numpad-area'); choiceArea.classList.remove('hidden'); numpadArea.classList.add('hidden'); const nonpos = ['roman', 'greek', 'slavonic', 'hebrew']; const choices = generateChoices(game.correctAnswer, [game.range.min, game.range.max], 3, nonpos.includes(game.system)); choices.forEach((val, i) => { const btn = document.getElementById('choice-' + i); btn.textContent = val; btn.className = 'choice-btn'; btn.disabled = false; btn.onclick = () => handleChoice(i, val, choices); }); } function handleChoice(idx, val, choices) { if (game.answered) return; game.answered = true; stopTimer(); const feedback = document.getElementById('answer-feedback'); const allBtns = choices.map((_, i) => document.getElementById('choice-' + i)); allBtns.forEach(b => b.disabled = true); if (val === game.correctAnswer) { game.correctCount++; document.getElementById('score-correct').textContent = MARK_CORRECT + game.correctCount; allBtns[idx].classList.add('correct-choice'); feedback.textContent = t('correct'); feedback.className = 'answer-feedback correct'; const rect = allBtns[idx].getBoundingClientRect(); spawnAnswerConfetti(rect.left + rect.width / 2, rect.top + rect.height / 2); } else { allBtns[idx].classList.add('incorrect-choice'); const correctIdx = choices.indexOf(game.correctAnswer); allBtns[correctIdx].classList.add('correct-choice'); feedback.textContent = t('incorrect', {n: game.correctAnswer}); feedback.className = 'answer-feedback incorrect'; if (!game.mistakes.some(m => m.decimal === game.correctAnswer)) { game.mistakes.push({ key: game.system, system: SYSTEMS[game.system].toDisplay(game.correctAnswer), decimal: game.correctAnswer, systemName: systemName(game.system), wrong: val, }); } } document.getElementById('score-incorrect').textContent = MARK_INCORRECT + (game.currentRound - game.correctCount); document.getElementById('btn-next').classList.remove('hidden'); } function renderNumpad() { const choiceArea = document.getElementById('choice-area'); const numpadArea = document.getElementById('numpad-area'); choiceArea.classList.add('hidden'); numpadArea.classList.remove('hidden'); const inputEl = document.getElementById('numpad-input'); inputEl.textContent = ''; const clearBtn = numpadArea.querySelector('[data-key="clear"]'); if (game.system === 'ternary') { clearBtn.textContent = '±'; clearBtn.dataset.key = 'sign'; } else { clearBtn.textContent = 'C'; clearBtn.dataset.key = 'clear'; } const submitBtn = document.getElementById('btn-submit-answer'); submitBtn.classList.add('hidden'); numpadArea.querySelectorAll('.numpad-key').forEach(key => { key.onclick = () => handleNumpadKey(key.dataset.key); }); document.getElementById('btn-submit-answer').onclick = () => { if (game.answered) return; const val = parseInt(inputEl.textContent, 10); if (isNaN(val)) return; game.answered = true; stopTimer(); document.getElementById('btn-submit-answer').classList.add('hidden'); const feedback = document.getElementById('answer-feedback'); if (val === game.correctAnswer) { game.correctCount++; document.getElementById('score-correct').textContent = MARK_CORRECT + game.correctCount; feedback.textContent = t('correct'); feedback.className = 'answer-feedback correct'; const rect = inputEl.getBoundingClientRect(); spawnAnswerConfetti(rect.left + rect.width / 2, rect.top + rect.height / 2); } else { feedback.textContent = t('incorrect', {n: game.correctAnswer}); feedback.className = 'answer-feedback incorrect'; if (!game.mistakes.some(m => m.decimal === game.correctAnswer)) { game.mistakes.push({ key: game.system, system: SYSTEMS[game.system].toDisplay(game.correctAnswer), decimal: game.correctAnswer, systemName: systemName(game.system), wrong: val, }); } } document.getElementById('score-incorrect').textContent = MARK_INCORRECT + (game.currentRound - game.correctCount); document.getElementById('btn-next').classList.remove('hidden'); }; } function handleNumpadKey(key) { const inputEl = document.getElementById('numpad-input'); if (game.answered) return; if (key === 'clear') { inputEl.textContent = ''; } else if (key === 'backspace') { inputEl.textContent = inputEl.textContent.slice(0, -1); } else if (key === 'sign') { if (inputEl.textContent.startsWith('-')) { inputEl.textContent = inputEl.textContent.slice(1); } else { inputEl.textContent = '-' + inputEl.textContent; } } else { if (inputEl.textContent.length < 12) { inputEl.textContent += key; } } const submitBtn = document.getElementById('btn-submit-answer'); submitBtn.classList.toggle('hidden', isNaN(parseInt(inputEl.textContent, 10))); } function startTimer() { stopTimer(); const timerEl = document.getElementById('timer-display'); const timerArea = document.getElementById('timer-area'); if (game.timing === 0) { timerEl.textContent = ''; timerEl.classList.remove('urgent'); timerArea.classList.remove('visible'); return; } timerArea.classList.add('visible'); game.timeLeft = game.timing; timerEl.textContent = game.timeLeft + ' ' + t('sec'); timerEl.classList.remove('urgent'); game.timerInterval = setInterval(() => { game.timeLeft--; timerEl.textContent = game.timeLeft + ' ' + t('sec'); if (game.timeLeft <= 10) { timerEl.classList.add('urgent'); } if (game.timeLeft <= 0) { stopTimer(); if (!game.answered) { game.answered = true; const feedback = document.getElementById('answer-feedback'); feedback.textContent = t('timesUp', {n: game.correctAnswer}); feedback.className = 'answer-feedback incorrect'; if (!game.mistakes.some(m => m.decimal === game.correctAnswer)) { game.mistakes.push({ key: game.system, system: SYSTEMS[game.system].toDisplay(game.correctAnswer), decimal: game.correctAnswer, systemName: systemName(game.system), }); } document.getElementById('score-incorrect').textContent = MARK_INCORRECT + (game.currentRound - game.correctCount); document.getElementById('btn-next').classList.remove('hidden'); if (game.mode === 'choice') { document.querySelectorAll('.choice-btn').forEach(b => b.disabled = true); } } } }, 1000); } function stopTimer() { if (game.timerInterval) { clearInterval(game.timerInterval); game.timerInterval = null; } document.getElementById('timer-display').classList.remove('urgent'); } function endGame() { stopTimer(); showScreen('results'); const completed = game.currentRound >= game.rounds; const total = completed ? game.rounds : game.currentRound; const correct = game.correctCount; const pct = total > 0 ? Math.round((correct / total) * 100) : 0; const summary = document.getElementById('results-summary'); summary.innerHTML = `
${correct}/${total}
${t('pctCorrect', {pct: pct})}
`; const congratsEl = document.getElementById('results-congrats'); const labelEl = document.getElementById('results-mistakes-label'); const listEl = document.getElementById('results-list'); congratsEl.innerHTML = ''; labelEl.textContent = t('mistakes'); labelEl.style.display = ''; listEl.innerHTML = ''; if (game.mistakes.length > 0) { game.mistakes.forEach(m => { const item = document.createElement('div'); item.className = 'result-item'; const sysClass = m.key === 'slavonic' ? ' slavonic-font' : ''; item.innerHTML = `
${m.system}
\u2B95
${m.decimal}
${m.wrong ? '
\u2718 ' + m.wrong + '
' : '
\u2718 \u23F1
' } `; listEl.appendChild(item); }); } else if (completed) { labelEl.style.display = 'none'; congratsEl.textContent = t('congratulations'); spawnConfetti(); } else { labelEl.style.display = 'none'; } } function spawnAnswerConfetti(x, y) { const chars = ['🎉', '🎊', '✨', '🎈', '🥳', '💫']; const burst = document.createElement('div'); burst.className = 'answer-confetti-burst'; burst.style.left = x + 'px'; burst.style.top = y + 'px'; for (let j = 0; j < 8; j++) { const particle = document.createElement('span'); particle.className = 'confetti-particle'; particle.textContent = chars[Math.floor(Math.random() * chars.length)]; particle.style.setProperty('--dx', (Math.random() - 0.5) * 80 + 'px'); particle.style.setProperty('--dy', -(30 + Math.random() * 60) + 'px'); particle.style.setProperty('--rot', Math.random() * 360 + 'deg'); particle.style.animationDuration = (0.5 + Math.random() * 0.3) + 's'; burst.appendChild(particle); } document.body.appendChild(burst); setTimeout(() => burst.remove(), 1000); } function spawnConfetti() { const chars = ['🎉', '🎊', '✨', '🎈', '🥳', '💫']; const container = document.getElementById('results-congrats'); const bursts = Math.round(game.rounds / 3); for (let i = 0; i < bursts; i++) { setTimeout(() => { const burst = document.createElement('div'); burst.className = 'confetti-burst'; burst.style.left = 15 + Math.random() * 70 + '%'; burst.style.top = 20 + Math.random() * 40 + '%'; for (let j = 0; j < 12; j++) { const particle = document.createElement('span'); particle.className = 'confetti-particle'; particle.textContent = chars[Math.floor(Math.random() * chars.length)]; particle.style.setProperty('--dx', (Math.random() - 0.5) * 120 + 'px'); particle.style.setProperty('--dy', -(40 + Math.random() * 80) + 'px'); particle.style.setProperty('--rot', Math.random() * 360 + 'deg'); particle.style.animationDuration = (0.6 + Math.random() * 0.4) + 's'; burst.appendChild(particle); } container.appendChild(burst); setTimeout(() => burst.remove(), 1200); }, i * 500); } } // ---- THEORY ---- async function loadTheory(key) { if (theoryCache[key]) return theoryCache[key]; try { const resp = await fetch(`theory/${key}.${LANG}.html`); if (!resp.ok) throw new Error(resp.status); const html = await resp.text(); theoryCache[key] = html; return html; } catch { if (LANG !== 'en') { try { const resp = await fetch(`theory/${key}.en.html`); const html = await resp.text(); theoryCache[key] = html; return html; } catch { return null; } } return null; } } function renderTheorySystem() { const posDiv = document.getElementById('theory-systems-pos'); const nonposDiv = document.getElementById('theory-systems-nonpos'); posDiv.innerHTML = ''; nonposDiv.innerHTML = ''; for (const [group, keys] of Object.entries(SYSTEM_GROUPS)) { const target = group === 'pos' ? posDiv : nonposDiv; keys.forEach(key => { const btn = document.createElement('button'); btn.className = 'option-btn'; btn.textContent = systemName(key); btn.addEventListener('click', () => showTheoryContent(key)); target.appendChild(btn); }); } showScreen('theory-system'); } async function showTheoryContent(key) { lastTheoryKey = key; const html = await loadTheory(key); document.getElementById('theory-title').textContent = systemName(key); document.getElementById('theory-scroll').innerHTML = html || '
' + t('theoryUnavailable') + '
'; showScreen('theory-content'); } // ---- INIT ---- const SUBSCRIPTS = { '0':'\u2080', '1':'\u2081', '2':'\u2082', '3':'\u2083', '4':'\u2084', '5':'\u2085', '6':'\u2086', '7':'\u2087', '8':'\u2088', '9':'\u2089' }; function toSubscript(n) { return String(n).split('').map(d => SUBSCRIPTS[d]).join(''); } const TICKER_COLORS_DARK = [ '#7ec8e3', '#a8d5a2', '#f0c674', '#d4a0d4', '#e8a87c', '#87ceeb', '#98d4bb', '#deb887', '#c9b1ff', '#ff9a9e' ]; const TICKER_COLORS_LIGHT = [ '#2980b9', '#27ae60', '#d4a017', '#9b59b6', '#d35400', '#3498db', '#1abc9c', '#b8860b', '#8e44ad', '#e74c3c' ]; const THEME_KEY = 'numnum-theme'; function getPreferredTheme() { const saved = localStorage.getItem(THEME_KEY); if (saved === 'light' || saved === 'dark') return saved; return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; } const TICKER_SYSTEMS = [ { key: 'binary', min: 1, max: 255, suffix: toSubscript(2) }, { key: 'base3', min: 1, max: 81, suffix: toSubscript(3) }, { key: 'octal', min: 1, max: 511, suffix: toSubscript(8) }, { key: 'hex', min: 1, max: 255, prefix: '0x' }, { key: 'ternary', min: 1, max: 27, suffix: toSubscript(3) }, { key: 'braille', min: 1, max: 99, prefix: null }, { key: 'roman', min: 1, max: 3999, prefix: null }, { key: 'greek', min: 1, max: 999, prefix: null }, { key: 'slavonic', min: 1, max: 999, prefix: null }, ]; function generateTickerText() { const palette = document.documentElement.dataset.theme === 'light' ? TICKER_COLORS_LIGHT : TICKER_COLORS_DARK; const pairs = []; let lastKey = null; let lastColor = null; for (let i = 0; i < 20; i++) { let sys; do { sys = TICKER_SYSTEMS[Math.floor(Math.random() * TICKER_SYSTEMS.length)]; } while (sys.key === lastKey); lastKey = sys.key; let color; do { color = palette[Math.floor(Math.random() * palette.length)]; } while (color === lastColor); lastColor = color; const num = sys.min + Math.floor(Math.random() * (sys.max - sys.min + 1)); const display = SYSTEMS[sys.key].toDisplay(num); let labelled = display; if (sys.prefix) labelled = sys.prefix + display; if (sys.suffix) labelled = display + sys.suffix; const wrapped = sys.key === 'slavonic' ? '' + labelled + '' : labelled; pairs.push('' + wrapped + ' \u2B95 ' + num + ''); } return pairs.join('\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0'); } function refreshTicker() { const tickerText = generateTickerText(); document.getElementById('ticker').innerHTML = tickerText + '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0' + tickerText; } const langSelect = document.getElementById('lang-select'); langSelect.value = LANG; langSelect.addEventListener('change', () => { LANG = langSelect.value; localStorage.setItem('numnum-lang', LANG); applyTranslations(); refreshDynamicText(); }); applyTranslations(); function applyTheme(theme) { document.documentElement.dataset.theme = theme; document.getElementById('theme-toggle').textContent = theme === 'dark' ? '\u2600' : '\u263E'; refreshTicker(); } applyTheme(getPreferredTheme()); document.getElementById('theme-toggle').addEventListener('click', () => { const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; localStorage.setItem(THEME_KEY, next); applyTheme(next); }); let deferredPrompt = null; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; document.getElementById('btn-install').classList.remove('hidden'); }); document.getElementById('btn-install').addEventListener('click', async () => { if (!deferredPrompt) return; deferredPrompt.prompt(); await deferredPrompt.userChoice; deferredPrompt = null; document.getElementById('btn-install').classList.add('hidden'); }); window.addEventListener('appinstalled', () => { deferredPrompt = null; document.getElementById('btn-install').classList.add('hidden'); }); if (window.matchMedia('(display-mode: standalone)').matches || navigator.standalone) { document.getElementById('btn-install').classList.add('hidden'); } function showSetupMessage(msg) { const containers = document.querySelectorAll('.setup-container'); let el = document.getElementById('setup-message'); if (!el) { el = document.createElement('div'); el.id = 'setup-message'; el.className = 'setup-message'; } containers.forEach(c => { if (!c.contains(el)) c.prepend(el); }); el.textContent = msg; el.classList.add('visible'); setTimeout(() => el.classList.remove('visible'), 2500); } document.getElementById('btn-start').addEventListener('click', () => { renderSetupSystem(); }); document.getElementById('btn-system-back').addEventListener('click', () => { showScreen('menu'); }); document.getElementById('btn-range-back').addEventListener('click', () => { renderSetupSystem(); }); document.getElementById('btn-options-back').addEventListener('click', () => { renderSetupRange(); }); document.getElementById('btn-play').addEventListener('click', () => { if (!setupState.system || !setupState.rangeData || setupState.mode === undefined || setupState.timing === undefined || setupState.rounds === undefined) { showSetupMessage(t('pleaseSelect')); return; } startGame(); }); document.getElementById('btn-next').addEventListener('click', () => { nextRound(); }); document.getElementById('btn-play-again').addEventListener('click', () => { setupState = { ...lastSetupState }; startGame(); }); document.getElementById('btn-results-menu').addEventListener('click', () => { showScreen('menu'); }); document.getElementById('btn-theory').addEventListener('click', () => { renderTheorySystem(); }); document.getElementById('btn-theory-back').addEventListener('click', () => { showScreen('menu'); }); document.getElementById('btn-theory-content-back').addEventListener('click', () => { renderTheorySystem(); }); document.getElementById('btn-stop').addEventListener('click', () => { stopTimer(); endGame(); }); // Keyboard support for numpad document.addEventListener('keydown', (e) => { if (!document.getElementById('screen-game').classList.contains('active')) return; if (game.answered) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); nextRound(); } return; } if (game.mode === 'manual') { if (e.key >= '0' && e.key <= '9') { handleNumpadKey(e.key); } else if (e.key === 'Backspace') { handleNumpadKey('backspace'); } else if (e.key === 'Escape') { handleNumpadKey('clear'); } else if (e.key === 'Enter') { document.getElementById('btn-submit-answer').click(); } } else if (game.mode === 'choice') { if (e.key === '1' || e.key === '2' || e.key === '3') { const idx = parseInt(e.key) - 1; const btn = document.getElementById('choice-' + idx); if (btn && !btn.disabled) btn.click(); } } }); // Handle mobile back/cancel button const BACK_MAP = { 'screen-setup-system': 'btn-system-back', 'screen-setup-range': 'btn-range-back', 'screen-setup-options': 'btn-options-back', 'screen-game': 'btn-stop', 'screen-results': 'btn-results-menu', 'screen-theory-system': 'btn-theory-back', 'screen-theory-content': 'btn-theory-content-back', }; window.addEventListener('popstate', () => { navDepth = Math.max(0, navDepth - 1); const active = document.querySelector('.screen.active'); if (!active) return; const btnId = BACK_MAP[active.id]; if (btnId) { popstateHandled = true; document.getElementById(btnId).click(); } });