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', 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 Rounds', play: 'Go!', timeLeft: 'Time left', 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', 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: 'Начать', 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: '\u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u043F\u043E \u0432\u0440\u0435\u043C\u0435\u043D\u0438', numberOfRounds: '\u041A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0440\u0430\u0443\u043D\u0434\u043E\u0432', play: '\u0412\u043F\u0435\u0440\u0451\u0434!', timeLeft: '\u041E\u0441\u0442\u0430\u043B\u043E\u0441\u044C', 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', 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); }); } 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 MODES = [ { labelKey: 'choose', value: 'choice' }, { labelKey: 'type', value: 'manual' }, ]; const TIMINGS = [ { value: 0 }, { value: 30 }, { value: 15 }, { value: 5 }, ]; const ROUNDS = [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: [], }; function showScreen(id) { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById('screen-' + id).classList.add('active'); } 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 = 0; 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 timingDiv = document.getElementById('setup-timing'); timingDiv.innerHTML = ''; TIMINGS.forEach((tm, i) => { const btn = document.createElement('button'); btn.className = 'option-btn' + (i === defaultTiming ? ' selected' : ''); btn.textContent = tm.value === 0 ? t('noLimit') : tm.value + ' ' + t('sec'); btn.addEventListener('click', () => { timingDiv.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); setupState.timing = i; }); timingDiv.appendChild(btn); }); const roundsDiv = document.getElementById('setup-rounds'); roundsDiv.innerHTML = ''; ROUNDS.forEach((r, i) => { const btn = document.createElement('button'); btn.className = 'option-btn' + (i === defaultRounds ? ' selected' : ''); btn.textContent = String(r); btn.addEventListener('click', () => { roundsDiv.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); setupState.rounds = i; }); roundsDiv.appendChild(btn); }); 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 || RANGES[setupState.range]; 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'; 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'); } 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'; } document.getElementById('btn-submit-answer').classList.remove('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'; 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'); }; } 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; } } } 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'; 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 = `