(() => { const sourceLine = document.getElementById('source-line'); const codeDisplay = document.getElementById('code-display'); const input = document.getElementById('typing-input'); const statWpm = document.getElementById('stat-wpm'); const statAcc = document.getElementById('stat-acc'); const statKps = document.getElementById('stat-kps'); const statTime = document.getElementById('stat-time'); const restartBtn = document.getElementById('restart-btn'); const gameCard = document.getElementById('game-card'); const helpBtn = document.getElementById('help-btn'); const closeHelp = document.getElementById('close-help'); const helpModal = document.getElementById('help-modal'); let snippets = []; let target = ''; let startTime = null; let finished = false; let totalKeystrokes = 0; let correctKeystrokes = 0; let tickHandle = null; let resultBanner = null; function escapeHtml(s) { return s.replace(/&/g, '&').replace(//g, '>'); } function pickSnippet() { return snippets[Math.floor(Math.random() * snippets.length)]; } function loadSnippet() { const s = pickSnippet(); // Tabs shown as two spaces so the player never needs to press Tab -- // a plain textarea's default Tab behavior is "move focus," and // preventDefault-ing that just to insert a literal tab is more // fragile (IME/mobile edge cases) than sidestepping it entirely. target = s.code.replace(/\t/g, ' '); sourceLine.innerHTML = `typing: ${escapeHtml(s.repo)}/${escapeHtml(s.file)}`; startTime = null; finished = false; totalKeystrokes = 0; correctKeystrokes = 0; input.value = ''; if (resultBanner) { resultBanner.remove(); resultBanner = null; } render(); input.disabled = false; input.focus(); stopTick(); startTick(); } function render() { const typed = input.value; let html = ''; for (let i = 0; i < target.length; i++) { const targetChar = target[i] === '\n' ? '\n' : target[i]; let cls = 'char-pending'; if (i < typed.length) { cls = typed[i] === target[i] ? 'char-correct' : 'char-incorrect'; } else if (i === typed.length) { cls = 'char-pending char-cursor'; } html += `${escapeHtml(targetChar)}`; } codeDisplay.innerHTML = html; updateStats(); } function elapsedSeconds() { if (startTime === null) return 0; return (performance.now() - startTime) / 1000; } function updateStats() { const typed = input.value; let correctSoFar = 0; for (let i = 0; i < typed.length && i < target.length; i++) { if (typed[i] === target[i]) correctSoFar++; } const secs = elapsedSeconds(); const mins = secs / 60; const wpm = mins > 0 ? Math.round((correctSoFar / 5) / mins) : 0; const acc = totalKeystrokes > 0 ? Math.round((correctKeystrokes / totalKeystrokes) * 100) : 100; const kps = secs > 0 ? (totalKeystrokes / secs) : 0; statWpm.textContent = wpm; statAcc.textContent = `${acc}%`; statKps.textContent = kps.toFixed(1); statTime.textContent = `${secs.toFixed(1)}s`; } function startTick() { tickHandle = setInterval(() => { if (!finished) updateStats(); }, 200); } function stopTick() { if (tickHandle) clearInterval(tickHandle); tickHandle = null; } function finish() { finished = true; input.disabled = true; stopTick(); updateStats(); resultBanner = document.createElement('div'); resultBanner.className = 'result-banner'; resultBanner.textContent = `Done — ${statWpm.textContent} wpm, ${statAcc.textContent} accuracy, ${statTime.textContent}. Nice.`; gameCard.insertBefore(resultBanner, gameCard.querySelector('.actions')); } input.addEventListener('keydown', (e) => { if (finished) return; const isBackspace = e.key === 'Backspace'; const isNewline = e.key === 'Enter'; const isChar = e.key.length === 1; if (!isBackspace && !isNewline && !isChar) return; // ignore Shift/Control/Arrow/Tab/etc if (startTime === null && !isBackspace) startTime = performance.now(); totalKeystrokes++; if (!isBackspace) { const expected = target[input.value.length]; const pressed = isNewline ? '\n' : e.key; if (expected !== undefined && pressed === expected) correctKeystrokes++; } }); input.addEventListener('input', () => { render(); if (input.value === target) finish(); }); // mousedown + preventDefault, not click -- a click handler runs after the // browser's own default-focus behavior has already fired, which was // stealing focus away from the hidden textarea whenever this had a // native tabindex. preventDefault on mousedown stops that entirely, so // our explicit focus() call is the only thing that happens. codeDisplay.addEventListener('mousedown', (e) => { e.preventDefault(); if (!finished) input.focus(); }); restartBtn.addEventListener('click', loadSnippet); helpBtn.addEventListener('click', () => { helpModal.hidden = false; }); closeHelp.addEventListener('click', () => { helpModal.hidden = true; }); helpModal.addEventListener('click', (e) => { if (e.target === helpModal) helpModal.hidden = true; }); fetch('snippets.json') .then(r => r.json()) .then(data => { snippets = data; loadSnippet(); }) .catch(() => { sourceLine.textContent = 'Could not load snippets.'; }); })();