initial commit
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,643 @@
|
|||||||
|
import { SYSTEMS, generateChoices } from './numerals.js';
|
||||||
|
|
||||||
|
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 },
|
||||||
|
],
|
||||||
|
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\u2070 \u2013 8\u00B9', min: 1, max: 8 },
|
||||||
|
{ 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 99', min: 10, 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 99', min: 10, 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 99', min: 10, max: 99 },
|
||||||
|
{ label: '100 \u2013 499', min: 100, max: 499 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const SYSTEM_GROUPS = {
|
||||||
|
pos: ['binary', 'octal', 'hex', 'ternary', 'braille'],
|
||||||
|
nonpos: ['roman', 'greek', 'slavonic', 'hebrew'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const MODES = [
|
||||||
|
{ label: 'Choose', value: 'choice' },
|
||||||
|
{ label: 'Type', value: 'manual' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TIMINGS = [
|
||||||
|
{ label: '60 sec', value: 60 },
|
||||||
|
{ label: '40 sec', value: 40 },
|
||||||
|
{ label: '10 sec', value: 10 },
|
||||||
|
{ label: 'No limit', value: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
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 = SYSTEMS[key].name;
|
||||||
|
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 = 'Number Range — ' + SYSTEMS[setupState.system].name;
|
||||||
|
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 = 3;
|
||||||
|
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 = m.label;
|
||||||
|
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((t, i) => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'option-btn' + (i === defaultTiming ? ' selected' : '');
|
||||||
|
btn.textContent = t.label;
|
||||||
|
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 ----
|
||||||
|
function startGame() {
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
document.getElementById('score-correct').textContent = '0';
|
||||||
|
document.getElementById('score-total').textContent = '0';
|
||||||
|
document.getElementById('score-system').textContent = SYSTEMS[game.system].name;
|
||||||
|
|
||||||
|
showScreen('game');
|
||||||
|
nextRound();
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextRound() {
|
||||||
|
if (game.currentRound >= game.rounds) {
|
||||||
|
endGame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
game.currentRound++;
|
||||||
|
game.answered = false;
|
||||||
|
game.correctAnswer = randInt(game.range.min, game.range.max);
|
||||||
|
|
||||||
|
document.getElementById('score-total').textContent = game.currentRound;
|
||||||
|
document.getElementById('score-correct').textContent = 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');
|
||||||
|
|
||||||
|
document.getElementById('answer-feedback').textContent = '';
|
||||||
|
document.getElementById('answer-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 choices = generateChoices(game.correctAnswer, [game.range.min, game.range.max], 3);
|
||||||
|
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 = game.correctCount;
|
||||||
|
allBtns[idx].classList.add('correct-choice');
|
||||||
|
feedback.textContent = 'Correct!';
|
||||||
|
feedback.className = 'answer-feedback correct';
|
||||||
|
} else {
|
||||||
|
allBtns[idx].classList.add('incorrect-choice');
|
||||||
|
const correctIdx = choices.indexOf(game.correctAnswer);
|
||||||
|
allBtns[correctIdx].classList.add('correct-choice');
|
||||||
|
feedback.textContent = `Incorrect — answer: ${game.correctAnswer}`;
|
||||||
|
feedback.className = 'answer-feedback incorrect';
|
||||||
|
game.mistakes.push({
|
||||||
|
system: SYSTEMS[game.system].toDisplay(game.correctAnswer),
|
||||||
|
decimal: game.correctAnswer,
|
||||||
|
systemName: SYSTEMS[game.system].name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = game.correctCount;
|
||||||
|
feedback.textContent = 'Correct!';
|
||||||
|
feedback.className = 'answer-feedback correct';
|
||||||
|
} else {
|
||||||
|
feedback.textContent = `Incorrect — answer: ${game.correctAnswer}`;
|
||||||
|
feedback.className = 'answer-feedback incorrect';
|
||||||
|
game.mistakes.push({
|
||||||
|
system: SYSTEMS[game.system].toDisplay(game.correctAnswer),
|
||||||
|
decimal: game.correctAnswer,
|
||||||
|
systemName: SYSTEMS[game.system].name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + 's';
|
||||||
|
timerEl.classList.remove('urgent');
|
||||||
|
|
||||||
|
game.timerInterval = setInterval(() => {
|
||||||
|
game.timeLeft--;
|
||||||
|
timerEl.textContent = game.timeLeft + 's';
|
||||||
|
|
||||||
|
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 = `Time's up! Answer: ${game.correctAnswer}`;
|
||||||
|
feedback.className = 'answer-feedback incorrect';
|
||||||
|
game.mistakes.push({
|
||||||
|
system: SYSTEMS[game.system].toDisplay(game.correctAnswer),
|
||||||
|
decimal: game.correctAnswer,
|
||||||
|
systemName: SYSTEMS[game.system].name,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="big-number">${correct}/${total}</div>
|
||||||
|
<div>${pct}% correct</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const congratsEl = document.getElementById('results-congrats');
|
||||||
|
const labelEl = document.getElementById('results-mistakes-label');
|
||||||
|
const listEl = document.getElementById('results-list');
|
||||||
|
congratsEl.innerHTML = '';
|
||||||
|
labelEl.textContent = 'Mistakes';
|
||||||
|
labelEl.style.display = '';
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
|
||||||
|
if (game.mistakes.length > 0) {
|
||||||
|
game.mistakes.forEach(m => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'result-item';
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="result-system">${m.system}</div>
|
||||||
|
<div class="result-arrow">\u2192</div>
|
||||||
|
<div class="result-decimal">${m.decimal}</div>
|
||||||
|
`;
|
||||||
|
listEl.appendChild(item);
|
||||||
|
});
|
||||||
|
} else if (completed) {
|
||||||
|
labelEl.style.display = 'none';
|
||||||
|
congratsEl.textContent = 'Congratulations!';
|
||||||
|
spawnConfetti();
|
||||||
|
} else {
|
||||||
|
labelEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnConfetti() {
|
||||||
|
const chars = ['🎉', '🎊', '✨', '🎈', '🥳', '💫'];
|
||||||
|
const container = document.getElementById('results-congrats');
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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_SYSTEMS = [
|
||||||
|
{ key: 'binary', min: 1, max: 255, prefix: toSubscript(2) },
|
||||||
|
{ key: 'octal', min: 1, max: 511, prefix: toSubscript(8) },
|
||||||
|
{ key: 'hex', min: 1, max: 255, prefix: '0x' },
|
||||||
|
{ key: 'ternary', min: 1, max: 27, prefix: 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 pairs = [];
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const sys = TICKER_SYSTEMS[Math.floor(Math.random() * TICKER_SYSTEMS.length)];
|
||||||
|
const num = sys.min + Math.floor(Math.random() * (sys.max - sys.min + 1));
|
||||||
|
const display = SYSTEMS[sys.key].toDisplay(num);
|
||||||
|
const prefixed = sys.prefix ? sys.prefix + display : display;
|
||||||
|
const wrapped = sys.key === 'slavonic'
|
||||||
|
? '<span class="slavonic-font">' + prefixed + '</span>'
|
||||||
|
: prefixed;
|
||||||
|
pairs.push(wrapped + ' = ' + num);
|
||||||
|
}
|
||||||
|
return pairs.join('\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tickerText = generateTickerText();
|
||||||
|
document.getElementById('ticker').innerHTML = tickerText + '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0' + tickerText;
|
||||||
|
|
||||||
|
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.mode === undefined || setupState.timing === undefined || setupState.rounds === undefined) {
|
||||||
|
showSetupMessage('Please select all options');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startGame();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-next').addEventListener('click', () => {
|
||||||
|
nextRound();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-play-again').addEventListener('click', () => {
|
||||||
|
renderSetupSystem();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-results-menu').addEventListener('click', () => {
|
||||||
|
showScreen('menu');
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" rx="96" fill="#1a1a2e"/>
|
||||||
|
<text x="256" y="200" text-anchor="middle" font-family="Arial,sans-serif" font-size="120" font-weight="800" fill="#e94560">Num</text>
|
||||||
|
<text x="256" y="340" text-anchor="middle" font-family="Arial,sans-serif" font-size="120" font-weight="800" fill="#ff9a56">Num</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
+160
@@ -0,0 +1,160 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
<meta name="theme-color" content="#1a1a2e">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<title>NumNum</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="icon.svg">
|
||||||
|
<link rel="apple-touch-icon" href="icon-192.png">
|
||||||
|
<link rel="stylesheet" href="styles.css?v=30">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
|
||||||
|
<!-- MENU SCREEN -->
|
||||||
|
<div id="screen-menu" class="screen active">
|
||||||
|
<div class="menu-container">
|
||||||
|
<h1 class="logo">NumNum</h1>
|
||||||
|
<p class="subtitle">Numeral systems trainer</p>
|
||||||
|
<div class="ticker-wrap">
|
||||||
|
<div class="ticker" id="ticker"></div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-large" id="btn-start">Start Game</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SETUP: SYSTEM SELECTION -->
|
||||||
|
<div id="screen-setup-system" class="screen">
|
||||||
|
<div class="setup-container">
|
||||||
|
<h2>Choose numeral system</h2>
|
||||||
|
|
||||||
|
<div class="system-group">
|
||||||
|
<h3>Positional</h3>
|
||||||
|
<div id="setup-systems-pos" class="option-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="system-group">
|
||||||
|
<h3>Non-Positional</h3>
|
||||||
|
<div id="setup-systems-nonpos" class="option-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary" id="btn-system-back">Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SETUP: RANGE SELECTION -->
|
||||||
|
<div id="screen-setup-range" class="screen">
|
||||||
|
<div class="setup-container">
|
||||||
|
<h2 id="range-title">Number Range</h2>
|
||||||
|
|
||||||
|
<div class="setup-section">
|
||||||
|
<div id="setup-ranges" class="option-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary" id="btn-range-back">Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SETUP: OPTIONS (mode, timing, rounds) -->
|
||||||
|
<div id="screen-setup-options" class="screen">
|
||||||
|
<div class="setup-container">
|
||||||
|
<h2>Difficulty level</h2>
|
||||||
|
|
||||||
|
<div class="setup-section">
|
||||||
|
<h3>Answer Mode</h3>
|
||||||
|
<div id="setup-modes" class="option-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setup-section">
|
||||||
|
<h3>Time Limit</h3>
|
||||||
|
<div id="setup-timing" class="option-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setup-section">
|
||||||
|
<h3>Number of Rounds</h3>
|
||||||
|
<div id="setup-rounds" class="option-grid"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary btn-large" id="btn-play">Play!</button>
|
||||||
|
<button class="btn btn-secondary" id="btn-options-back">Back</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GAME SCREEN -->
|
||||||
|
<div id="screen-game" class="screen">
|
||||||
|
<div class="game-header">
|
||||||
|
<div class="score-display">
|
||||||
|
<span id="score-correct">0</span>/<span id="score-total">0</span>
|
||||||
|
<span id="score-system"></span>
|
||||||
|
</div>
|
||||||
|
<div class="timer-area" id="timer-area">
|
||||||
|
<span class="timer-label">Time left</span>
|
||||||
|
<div class="timer-display" id="timer-display"></div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<button class="btn-stop" id="btn-stop">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-body">
|
||||||
|
<div class="number-display" id="number-display"></div>
|
||||||
|
<div class="answer-feedback" id="answer-feedback"></div>
|
||||||
|
|
||||||
|
<!-- Choice mode -->
|
||||||
|
<div id="choice-area" class="choice-area hidden">
|
||||||
|
<button class="choice-btn" id="choice-0"></button>
|
||||||
|
<button class="choice-btn" id="choice-1"></button>
|
||||||
|
<button class="choice-btn" id="choice-2"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual entry mode -->
|
||||||
|
<div id="numpad-area" class="numpad-area hidden">
|
||||||
|
<div class="numpad-input" id="numpad-input"></div>
|
||||||
|
<div class="numpad">
|
||||||
|
<button class="numpad-key" data-key="1">1</button>
|
||||||
|
<button class="numpad-key" data-key="2">2</button>
|
||||||
|
<button class="numpad-key" data-key="3">3</button>
|
||||||
|
<button class="numpad-key" data-key="4">4</button>
|
||||||
|
<button class="numpad-key" data-key="5">5</button>
|
||||||
|
<button class="numpad-key" data-key="6">6</button>
|
||||||
|
<button class="numpad-key" data-key="7">7</button>
|
||||||
|
<button class="numpad-key" data-key="8">8</button>
|
||||||
|
<button class="numpad-key" data-key="9">9</button>
|
||||||
|
<button class="numpad-key numpad-action" data-key="clear">C</button>
|
||||||
|
<button class="numpad-key" data-key="0">0</button>
|
||||||
|
<button class="numpad-key numpad-action" data-key="backspace">⌫</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary hidden" id="btn-submit-answer">Submit</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary hidden" id="btn-next">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RESULTS SCREEN -->
|
||||||
|
<div id="screen-results" class="screen">
|
||||||
|
<div class="results-container">
|
||||||
|
<h2>Results</h2>
|
||||||
|
<div class="results-summary" id="results-summary"></div>
|
||||||
|
<div class="results-congrats" id="results-congrats"></div>
|
||||||
|
<h3 class="results-mistakes-label" id="results-mistakes-label">Mistakes</h3>
|
||||||
|
<div class="results-list" id="results-list"></div>
|
||||||
|
<button class="btn btn-primary btn-large" id="btn-play-again">Play Again</button>
|
||||||
|
<button class="btn btn-secondary btn-large" id="btn-results-menu">Menu</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="app.js?v=30"></script>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/sw.js?v=30').catch(() => {});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "NumNum",
|
||||||
|
"short_name": "NumNum",
|
||||||
|
"description": "Train your numeric systems",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"background_color": "#1a1a2e",
|
||||||
|
"theme_color": "#1a1a2e",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icon.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+221
@@ -0,0 +1,221 @@
|
|||||||
|
export const SYSTEMS = {
|
||||||
|
binary: {
|
||||||
|
name: 'Binary',
|
||||||
|
short: 'BIN',
|
||||||
|
toDisplay: (n) => n.toString(2),
|
||||||
|
},
|
||||||
|
octal: {
|
||||||
|
name: 'Octal',
|
||||||
|
short: 'OCT',
|
||||||
|
toDisplay: (n) => n.toString(8),
|
||||||
|
},
|
||||||
|
hex: {
|
||||||
|
name: 'Hexadecimal',
|
||||||
|
short: 'HEX',
|
||||||
|
toDisplay: (n) => n.toString(16).toUpperCase(),
|
||||||
|
},
|
||||||
|
ternary: {
|
||||||
|
name: 'Balanced Ternary',
|
||||||
|
short: 'BT3',
|
||||||
|
toDisplay: toBalTernary,
|
||||||
|
},
|
||||||
|
braille: {
|
||||||
|
name: 'Braille Decimal',
|
||||||
|
short: 'BRL',
|
||||||
|
toDisplay: toBraille,
|
||||||
|
},
|
||||||
|
roman: {
|
||||||
|
name: 'Roman',
|
||||||
|
short: 'ROM',
|
||||||
|
toDisplay: toRoman,
|
||||||
|
},
|
||||||
|
slavonic: {
|
||||||
|
name: 'Church Slavonic',
|
||||||
|
short: 'SLV',
|
||||||
|
toDisplay: toSlavonic,
|
||||||
|
},
|
||||||
|
greek: {
|
||||||
|
name: 'Greek',
|
||||||
|
short: 'GRK',
|
||||||
|
toDisplay: toGreek,
|
||||||
|
},
|
||||||
|
hebrew: {
|
||||||
|
name: 'Hebrew',
|
||||||
|
short: 'HEB',
|
||||||
|
toDisplay: toHebrew,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function toRoman(num) {
|
||||||
|
if (num <= 0 || num > 3999) return String(num);
|
||||||
|
const vals = [1000,900,500,400,100,90,50,40,10,9,5,4,1];
|
||||||
|
const syms = ['M','CM','D','CD','C','XC','L','XL','X','IX','V','IV','I'];
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < vals.length; i++) {
|
||||||
|
while (num >= vals[i]) {
|
||||||
|
result += syms[i];
|
||||||
|
num -= vals[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBalTernary(num) {
|
||||||
|
if (num === 0) return '0';
|
||||||
|
|
||||||
|
let sign = 1;
|
||||||
|
let n = num;
|
||||||
|
if (n < 0) {
|
||||||
|
sign = -1;
|
||||||
|
n = -n;
|
||||||
|
}
|
||||||
|
|
||||||
|
const digits = [];
|
||||||
|
while (n > 0) {
|
||||||
|
let rem = n % 3;
|
||||||
|
n = Math.floor(n / 3);
|
||||||
|
if (rem === 2) {
|
||||||
|
digits.unshift('\u2296');
|
||||||
|
n += 1;
|
||||||
|
} else if (rem === 1) {
|
||||||
|
digits.unshift('\u2295');
|
||||||
|
} else {
|
||||||
|
digits.unshift('0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sign === -1) {
|
||||||
|
return digits.map(d => {
|
||||||
|
if (d === '\u2295') return '\u2296';
|
||||||
|
if (d === '\u2296') return '\u2295';
|
||||||
|
return d;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return digits.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const BRAILLE_NUMS = ['\u281A','\u2801','\u2803','\u2809','\u2819','\u2811','\u280B','\u281B','\u2813','\u280A'];
|
||||||
|
const BRAILLE_HASH = '\u283C';
|
||||||
|
|
||||||
|
function toBraille(num) {
|
||||||
|
if (num < 0) return String(num);
|
||||||
|
if (num === 0) return BRAILLE_HASH + BRAILLE_NUMS[0];
|
||||||
|
return BRAILLE_HASH + String(num).split('').map(d => BRAILLE_NUMS[parseInt(d)]).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const GRK_UNITS = ['\u03B1','\u03B2','\u03B3','\u03B4','\u03B5','\u03DB','\u03B6','\u03B7','\u03B8'];
|
||||||
|
const GRK_TENS = ['\u03B9','\u03BA','\u03BB','\u03BC','\u03BD','\u03BE','\u03BF','\u03C0','\u03DF'];
|
||||||
|
const GRK_HUNDREDS = ['\u03C1','\u03C3','\u03C4','\u03C5','\u03C6','\u03C7','\u03C8','\u03C9','\u03E1'];
|
||||||
|
const GRK_THOU = '\u0375';
|
||||||
|
const GRK_KER = '\u1FFD';
|
||||||
|
|
||||||
|
function toGreek(num) {
|
||||||
|
if (num <= 0 || num > 9999) return String(num);
|
||||||
|
let result = '';
|
||||||
|
const thou = Math.floor(num / 1000);
|
||||||
|
num %= 1000;
|
||||||
|
const h = Math.floor(num / 100);
|
||||||
|
num %= 100;
|
||||||
|
const t = Math.floor(num / 10);
|
||||||
|
const u = num % 10;
|
||||||
|
if (thou > 0) result += GRK_THOU + GRK_UNITS[thou - 1];
|
||||||
|
if (h > 0) result += GRK_HUNDREDS[h - 1];
|
||||||
|
if (t > 0) result += GRK_TENS[t - 1];
|
||||||
|
if (u > 0) result += GRK_UNITS[u - 1];
|
||||||
|
return result ? result + GRK_KER : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
const SLV_UNITS = ['а','в','г','д','є','ѕ','з','и','ѳ'];
|
||||||
|
const SLV_TENS = ['і','к','л','м','н','ѯ','ѻ','п','ч'];
|
||||||
|
const SLV_HUNDREDS = ['р','с','т','у','ф','х','ѱ','ѿ','ц'];
|
||||||
|
const SLV_THOU = '\u0482';
|
||||||
|
const SLV_TITLO = '\u0483';
|
||||||
|
|
||||||
|
function toSlavonic(num) {
|
||||||
|
if (num <= 0 || num > 9999) return String(num);
|
||||||
|
|
||||||
|
const groups = [];
|
||||||
|
const thou = Math.floor(num / 1000);
|
||||||
|
num %= 1000;
|
||||||
|
const h = Math.floor(num / 100);
|
||||||
|
num %= 100;
|
||||||
|
const t = Math.floor(num / 10);
|
||||||
|
const u = num % 10;
|
||||||
|
|
||||||
|
if (thou > 0) groups.push({ p: SLV_THOU, l: SLV_UNITS[thou - 1] });
|
||||||
|
|
||||||
|
if (h > 0) groups.push({ p: '', l: SLV_HUNDREDS[h - 1] });
|
||||||
|
|
||||||
|
const twoDigit = t * 10 + u;
|
||||||
|
if (twoDigit >= 11 && twoDigit <= 19) {
|
||||||
|
if (u > 0) groups.push({ p: '', l: SLV_UNITS[u - 1] });
|
||||||
|
if (t > 0) groups.push({ p: '', l: SLV_TENS[t - 1] });
|
||||||
|
} else {
|
||||||
|
if (t > 0) groups.push({ p: '', l: SLV_TENS[t - 1] });
|
||||||
|
if (u > 0) groups.push({ p: '', l: SLV_UNITS[u - 1] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groups.length === 0) return '0';
|
||||||
|
|
||||||
|
const titloIdx = groups.length >= 2 ? groups.length - 2 : 0;
|
||||||
|
groups[titloIdx].l += SLV_TITLO;
|
||||||
|
|
||||||
|
return groups.map(g => g.p + g.l).join('') + '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEB_UNITS = ['\u05D0','\u05D1','\u05D2','\u05D3','\u05D4','\u05D5','\u05D6','\u05D7','\u05D8'];
|
||||||
|
const HEB_TENS = ['\u05D9','\u05DB','\u05DC','\u05DE','\u05E0','\u05E1','\u05E2','\u05E4','\u05E6'];
|
||||||
|
const HEB_HUNDREDS = ['\u05E7','\u05E8','\u05E9','\u05EA'];
|
||||||
|
const HEB_GERESH = '\u05F3';
|
||||||
|
const HEB_GERSHAYIM = '\u05F4';
|
||||||
|
|
||||||
|
function toHebrew(num) {
|
||||||
|
if (num <= 0 || num > 499) return String(num);
|
||||||
|
|
||||||
|
if (num === 15) return '\u05D8' + HEB_GERSHAYIM + '\u05D5';
|
||||||
|
if (num === 16) return '\u05D8' + HEB_GERSHAYIM + '\u05D6';
|
||||||
|
|
||||||
|
const letters = [];
|
||||||
|
const h = Math.floor(num / 100);
|
||||||
|
num %= 100;
|
||||||
|
const t = Math.floor(num / 10);
|
||||||
|
const u = num % 10;
|
||||||
|
|
||||||
|
if (h > 0) letters.push(HEB_HUNDREDS[h - 1]);
|
||||||
|
if (t > 0) letters.push(HEB_TENS[t - 1]);
|
||||||
|
if (u > 0) letters.push(HEB_UNITS[u - 1]);
|
||||||
|
|
||||||
|
if (letters.length === 0) return '0';
|
||||||
|
if (letters.length === 1) return letters[0] + HEB_GERESH;
|
||||||
|
return letters.slice(0, -1).join('') + HEB_GERSHAYIM + letters[letters.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateChoices(correctAnswer, allRanges, count = 3) {
|
||||||
|
const choices = new Set([correctAnswer]);
|
||||||
|
const [min, max] = allRanges;
|
||||||
|
let attempts = 0;
|
||||||
|
while (choices.size < count && attempts < 100) {
|
||||||
|
let wrong;
|
||||||
|
const offset = Math.floor(Math.random() * 20) + 1;
|
||||||
|
if (Math.random() < 0.5) {
|
||||||
|
wrong = correctAnswer + offset;
|
||||||
|
} else {
|
||||||
|
wrong = correctAnswer - offset;
|
||||||
|
}
|
||||||
|
if (wrong >= min && wrong <= max && wrong !== correctAnswer) {
|
||||||
|
choices.add(wrong);
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
while (choices.size < count) {
|
||||||
|
const r = min + Math.floor(Math.random() * (max - min + 1));
|
||||||
|
choices.add(r);
|
||||||
|
}
|
||||||
|
const arr = Array.from(choices);
|
||||||
|
for (let i = arr.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
+616
@@ -0,0 +1,616 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #1a1a2e;
|
||||||
|
--bg-light: #16213e;
|
||||||
|
--bg-card: #0f3460;
|
||||||
|
--accent: #e94560;
|
||||||
|
--accent-hover: #ff6b81;
|
||||||
|
--text: #eaeaea;
|
||||||
|
--text-dim: #a0a0b0;
|
||||||
|
--correct: #2ecc71;
|
||||||
|
--incorrect: #e74c3c;
|
||||||
|
--border-radius: 12px;
|
||||||
|
--safe-top: env(safe-area-inset-top);
|
||||||
|
--safe-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ponomar';
|
||||||
|
src: url('Ponomar-Regular.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
padding-top: var(--safe-top);
|
||||||
|
padding-bottom: var(--safe-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen {
|
||||||
|
display: none;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen.active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MENU */
|
||||||
|
.menu-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(135deg, var(--accent), #ff9a56);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticker-wrap {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticker {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-family: monospace;
|
||||||
|
animation: ticker-scroll 40s linear infinite;
|
||||||
|
padding-left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ticker-scroll {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
100% { transform: translateX(-50%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BUTTONS */
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s, opacity 0.1s;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
padding: 18px 32px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SETUP */
|
||||||
|
.setup-container {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-container h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-section {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-section h3 {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-group {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-group h3 {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn {
|
||||||
|
padding: 14px 12px;
|
||||||
|
background: var(--bg-light);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn.selected {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: rgba(233, 69, 96, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-play {
|
||||||
|
margin-top: 16px;
|
||||||
|
max-width: 320px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-options-back {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-setup-back, #btn-config-back {
|
||||||
|
margin-top: 12px;
|
||||||
|
max-width: 320px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GAME */
|
||||||
|
.game-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: var(--bg-light);
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-display {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#score-system {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-area {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-area.visible {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-label {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-display {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop:active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-display.urgent {
|
||||||
|
color: var(--incorrect);
|
||||||
|
animation: pulse 0.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-display {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 20px;
|
||||||
|
margin: 16px 0;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.3;
|
||||||
|
min-height: 100px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-display.small-text {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slavonic-font {
|
||||||
|
font-family: 'Ponomar', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-display.slavonic-font {
|
||||||
|
font-size: 3.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-feedback {
|
||||||
|
min-height: 40px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
margin: 8px 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-feedback.correct {
|
||||||
|
color: var(--correct);
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-feedback.incorrect {
|
||||||
|
color: var(--incorrect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CHOICES */
|
||||||
|
.choice-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-btn {
|
||||||
|
padding: 18px 20px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-btn.correct-choice {
|
||||||
|
border-color: var(--correct);
|
||||||
|
background: rgba(46, 204, 113, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-btn.incorrect-choice {
|
||||||
|
border-color: var(--incorrect);
|
||||||
|
background: rgba(231, 76, 60, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-btn:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NUMPAD */
|
||||||
|
.numpad-area {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.numpad-input {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 60px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.numpad {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.numpad-key {
|
||||||
|
padding: 18px;
|
||||||
|
background: var(--bg-light);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.numpad-key:active {
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.numpad-action {
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-submit-answer {
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-next {
|
||||||
|
margin-top: 16px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESULTS */
|
||||||
|
.results-container {
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 100px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container h2 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-summary {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-summary .big-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-congrats {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--correct);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-mistakes-label {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: var(--bg-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid var(--incorrect);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-system {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 80px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-arrow {
|
||||||
|
color: var(--text-dim);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-decimal {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--correct);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-mistakes {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--correct);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-play-again {
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-results-menu {
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-message {
|
||||||
|
display: none;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
animation: fadeIn 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-message.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti-burst {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti-particle {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
animation: confetti-fly 0.8s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes confetti-fly {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0) rotate(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(var(--dx), var(--dy)) rotate(var(--rot));
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const CACHE_NAME = 'numnum-v30';
|
||||||
|
const ASSETS = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/styles.css',
|
||||||
|
'/app.js',
|
||||||
|
'/numerals.js',
|
||||||
|
'/manifest.json',
|
||||||
|
'/icon.svg',
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', (e) => {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (e) => {
|
||||||
|
e.waitUntil(
|
||||||
|
caches.keys().then(keys =>
|
||||||
|
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (e) => {
|
||||||
|
e.respondWith(
|
||||||
|
caches.match(e.request).then(r => r || fetch(e.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user