import { Game, CombinationStats, TypeChartStats, FullTypeChartCell } from './lib/game'; import { getTypeInfo, TypeName } from './lib/types'; import './style.css'; const app = document.getElementById('app')!; let currentView: 'quiz' | 'stats' | 'info' = 'quiz'; let typeChartView: 'attacking' | 'defending' = 'attacking'; const game = new Game(render); const INFO_SEEN_KEY = 'pokemon-type-quiz-info-seen'; // Check if user has seen info screen before const hasSeenInfo = localStorage.getItem(INFO_SEEN_KEY) === 'true'; if (!hasSeenInfo) { currentView = 'info'; } game.nextQuestion(); function renderTypeBadge(name: TypeName, size: 'small' | 'large' = 'small'): string { const info = getTypeInfo(name); const sizeClass = size === 'large' ? 'type-badge-large' : ''; return `${info.name}`; } function renderInfoScreen(): void { app.innerHTML = `

Pokemon Type Quiz

Test your knowledge of type effectiveness!

How to Play

  • A random attacking type will be shown
  • You'll see one or two defending type(s)
  • Select how effective the attack is against the defender

Effectiveness Options

0x No effect (e.g., Ghost vs Normal)
1/4x Not very effective (e.g., Fire vs Rock/Water)
1/2x Not very effective (e.g., Water vs Fire)
1x Normal effectiveness
2x Super effective (e.g., Water vs Fire)
4x Super effective (e.g., Grass vs Water/Ground)

Controls

1-6 to select answer Enter to continue

`; document.getElementById('startBtn')?.addEventListener('click', () => { localStorage.setItem(INFO_SEEN_KEY, 'true'); currentView = 'quiz'; render(); }); } function render(): void { if (currentView === 'info') { renderInfoScreen(); return; } if (currentView === 'stats') { renderStatsPage(); return; } const state = game.getState(); const { options, startKey } = game.getOptions(); const stats = game.getStats(); if (!state.currentQuestion) { app.innerHTML = '
Loading...
'; return; } const q = state.currentQuestion; const defenderHtml = q.defenderTypes.length === 1 ? renderTypeBadge(q.defenderTypes[0], 'large') : `${renderTypeBadge(q.defenderTypes[0], 'large')} ${renderTypeBadge(q.defenderTypes[1], 'large')}`; const optionsHtml = state.showResult ? '' : options.map((opt, i) => { let cls = 'option-btn'; if (i === state.selectedIndex) { cls += ' selected'; } const shortcut = `${startKey + i}`; return ` `; }).join(''); const resultHtml = state.showResult ? `
${state.isCorrect ? '
✓ Correct!
' : `
✗ Wrong
${game.getExplanation()}
` } ${!state.isCorrect ? '' : ''}
` : ''; app.innerHTML = `

Pokemon Type Quiz

Streak: ${state.streak} Accuracy: ${stats.accuracy.toFixed(1)}% Total: ${stats.total}
Attacking Type
${renderTypeBadge(q.attackType, 'large')}
against
Defending Type${q.defenderTypes.length > 1 ? 's' : ''}
${defenderHtml}
${optionsHtml} ${resultHtml}
`; attachEventListeners(); } function renderCombinationItem(combo: CombinationStats): string { const defenderHtml = combo.defenderTypes.length === 1 ? renderTypeBadge(combo.defenderTypes[0], 'small') : `${renderTypeBadge(combo.defenderTypes[0], 'small')} ${renderTypeBadge(combo.defenderTypes[1], 'small')}`; const accuracyClass = combo.accuracy >= 80 ? 'high' : combo.accuracy >= 50 ? 'medium' : 'low'; return `
${renderTypeBadge(combo.attackType, 'small')} vs ${defenderHtml}
${combo.accuracy.toFixed(0)}% (${combo.correct}/${combo.total})
`; } function renderTypeChartCell(typeStat: TypeChartStats): string { const info = getTypeInfo(typeStat.attackType); const accuracyClass = typeStat.total === 0 ? 'no-data' : typeStat.accuracy >= 80 ? 'high' : typeStat.accuracy >= 50 ? 'medium' : 'low'; const display = typeStat.total === 0 ? '-' : `${typeStat.accuracy.toFixed(0)}%`; return `
${typeStat.attackType.slice(0, 3)} ${display}
`; } function renderFullTypeChart(cells: FullTypeChartCell[]): string { const defenderTypes = ['Normal', 'Fire', 'Water', 'Electric', 'Grass', 'Ice', 'Fighting', 'Poison', 'Ground', 'Flying', 'Psychic', 'Bug', 'Rock', 'Ghost', 'Dragon', 'Dark', 'Steel', 'Fairy']; // Create a map for quick cell lookup const cellMap = new Map(); for (const cell of cells) { const defenderKey = cell.defenderTypes.join('/'); const key = `${cell.attackType}:${defenderKey}`; cellMap.set(key, cell); } // Generate header row const headerCells = defenderTypes.map(def => { const info = getTypeInfo(def as TypeName); return `
${def.slice(0, 3)}
`; }).join(''); // Generate rows const rows = defenderTypes.map(rowType => { const rowInfo = getTypeInfo(rowType as TypeName); const dataCells = defenderTypes.map(colType => { const cell = cellMap.get(`${rowType}:${colType}`); const total = cell?.total || 0; const accuracy = cell?.accuracy || 0; let bgColor = 'var(--bg-accent)'; let textColor = 'var(--text-secondary)'; let display = '-'; if (total > 0) { display = `${accuracy.toFixed(0)}`; if (accuracy >= 80) { bgColor = 'rgba(74, 222, 128, 0.3)'; textColor = 'var(--success)'; } else if (accuracy >= 50) { bgColor = 'rgba(251, 191, 36, 0.3)'; textColor = '#fbbf24'; } else { bgColor = 'rgba(248, 113, 113, 0.3)'; textColor = 'var(--error)'; } } return `
${display}
`; }).join(''); return `
${rowType.slice(0, 3)}
${dataCells}
`; }).join(''); return `
${headerCells}
${rows}
`; } function renderStatsPage(): void { const stats = game.getStats(); const bestCombos = game.getBestCombinations(5); const worstCombos = game.getWorstCombinations(5); const typeChartStats = typeChartView === 'attacking' ? game.getTypeChartStats() : game.getDefendingTypeStats(); const fullTypeChartCells = game.getFullTypeChartStats(); const hasData = stats.total > 0; const bestCombosHtml = bestCombos.length > 0 ? bestCombos.map(renderCombinationItem).join('') : '

Answer at least 3 questions to see stats

'; const worstCombosHtml = worstCombos.length > 0 ? worstCombos.map(renderCombinationItem).join('') : '

Answer at least 3 questions to see stats

'; const typeChartHtml = typeChartStats.map(renderTypeChartCell).join(''); app.innerHTML = `

Your Statistics

${stats.total}
Total Questions
${stats.accuracy.toFixed(1)}%
Accuracy
${stats.correct}
Correct
${hasData ? `

Best Combinations

Highest accuracy (min. 3 attempts)

${bestCombosHtml}

Needs Practice

Lowest accuracy (min. 3 attempts)

${worstCombosHtml}

Type Chart Performance

Accuracy by ${typeChartView} type

${typeChartHtml}

Full Type Chart

Accuracy for each type combination

${renderFullTypeChart(fullTypeChartCells)}
` : `

No data yet. Start playing to track your statistics!

`}
`; document.getElementById('backBtn')?.addEventListener('click', () => { currentView = 'quiz'; render(); }); document.querySelectorAll('.chart-toggle').forEach(btn => { btn.addEventListener('click', () => { const view = btn.getAttribute('data-view') as 'attacking' | 'defending'; typeChartView = view; render(); }); }); } function attachEventListeners(): void { document.querySelectorAll('.option-btn').forEach(btn => { btn.addEventListener('click', () => { const index = parseInt(btn.getAttribute('data-index')!); game.selectAnswer(index); }); }); const dualToggle = document.getElementById('dualToggle') as HTMLInputElement; if (dualToggle) { dualToggle.addEventListener('change', () => { game.setAllowDualTypes(dualToggle.checked); }); } const nextBtn = document.querySelector('.next-btn'); if (nextBtn) { nextBtn.addEventListener('click', () => { game.nextQuestion(); }); } const statsBtn = document.getElementById('statsBtn'); if (statsBtn) { statsBtn.addEventListener('click', () => { currentView = 'stats'; render(); }); } const infoBtn = document.getElementById('infoBtn'); if (infoBtn) { infoBtn.addEventListener('click', () => { currentView = 'info'; render(); }); } } document.addEventListener('keydown', (e) => { const state = game.getState(); const { options, startKey } = game.getOptions(); if (state.showResult) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); game.nextQuestion(); } return; } const num = parseInt(e.key); if (num >= startKey && num < startKey + options.length) { e.preventDefault(); game.selectAnswer(num - startKey); } if (e.key === 'Enter') { if (state.selectedAnswer !== null) { e.preventDefault(); } } }); render();