Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Ultimate Speed Math Trainer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .timer-bar { | |
| transition: width linear; | |
| height: 4px; | |
| } | |
| .feedback-message { | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| height: 24px; | |
| } | |
| .feedback-message.show { | |
| opacity: 1; | |
| } | |
| .settings-panel { | |
| transform: translateX(-100%); | |
| transition: transform 0.3s ease; | |
| } | |
| .settings-panel.open { | |
| transform: translateX(0); | |
| } | |
| @media (max-width: 640px) { | |
| .settings-panel { | |
| width: 100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100 min-h-screen flex flex-col"> | |
| <div class="container mx-auto px-4 py-8 pb-16 sm:pb-8 flex-1 flex flex-col"> | |
| <!-- Header with stats --> | |
| <header class="flex justify-between items-center mb-8"> | |
| <button id="settings-btn" class="text-gray-400 hover:text-white p-2"> | |
| <i class="fas fa-cog text-2xl transition-opacity duration-300"></i> | |
| </button> | |
| <div class="flex space-x-4 text-sm"> | |
| <div class="bg-gray-800 px-3 py-1 rounded"> | |
| <span class="text-green-400">✓</span> <span id="correct-count">0</span> | |
| </div> | |
| <div class="bg-gray-800 px-3 py-1 rounded"> | |
| <span class="text-red-400">✗</span> <span id="incorrect-count">0</span> | |
| </div> | |
| <div class="bg-gray-800 px-3 py-1 rounded"> | |
| <span class="text-yellow-400">%</span> <span id="accuracy">0</span> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main training area --> | |
| <main class="flex-1 flex flex-col items-center justify-center"> | |
| <!-- Timer bar --> | |
| <div class="w-full bg-gray-700 rounded-full mb-6"> | |
| <div id="timer-bar" class="timer-bar bg-blue-500 rounded-full" style="width: 100%"></div> | |
| </div> | |
| <!-- Question display --> | |
| <div id="question" class="text-5xl sm:text-6xl md:text-8xl font-bold mb-8 sm:mb-12 text-center min-h-[80px] sm:min-h-[120px] flex items-center justify-center"> | |
| Ready? | |
| </div> | |
| <!-- Answer input --> | |
| <div class="w-full max-w-md mb-6 sm:mb-8 px-2 sm:px-0"> | |
| <input | |
| type="number" | |
| id="answer-input" | |
| class="w-full bg-gray-800 text-2xl sm:text-3xl text-center py-3 sm:py-4 px-4 sm:px-6 rounded-lg border-2 border-gray-700 focus:border-blue-500 focus:outline-none" | |
| placeholder="Your answer" | |
| autofocus | |
| > | |
| </div> | |
| <!-- Feedback message --> | |
| <div id="feedback" class="feedback-message text-xl font-semibold mb-4"></div> | |
| </main> | |
| </div> | |
| <!-- Settings backdrop (hidden by default) --> | |
| <div id="settings-backdrop" class="fixed inset-0 bg-black bg-opacity-50 z-0 opacity-0 pointer-events-none transition-opacity duration-300"></div> | |
| <!-- Settings panel (hidden by default) --> | |
| <div id="settings-panel" class="settings-panel fixed inset-y-0 left-0 w-full sm:w-96 bg-gray-800 shadow-xl p-4 sm:p-6 overflow-y-auto z-10 border-r border-gray-700"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-bold text-blue-400">Configuration</h2> | |
| <button id="close-settings" class="text-gray-400 hover:text-white"> | |
| <i class="fas fa-times text-2xl"></i> | |
| </button> | |
| </div> | |
| <!-- Mode selection --> | |
| <div class="mb-8 pb-6 border-b border-gray-700"> | |
| <h3 class="text-lg font-semibold mb-3">Mode</h3> | |
| <div class="space-y-2"> | |
| <label class="flex items-center space-x-3"> | |
| <input type="radio" name="mode" value="multiplication" class="form-radio text-blue-500" checked> | |
| <span>Multiplication (A × B = ?)</span> | |
| </label> | |
| <label class="flex items-center space-x-3"> | |
| <input type="radio" name="mode" value="factoring" class="form-radio text-blue-500"> | |
| <span>Factoring (C ÷ A = ?)</span> | |
| </label> | |
| </div> | |
| </div> | |
| <!-- Number ranges --> | |
| <div class="mb-8 pb-6 border-b border-gray-700"> | |
| <h3 class="text-lg font-semibold mb-3">Number Ranges</h3> | |
| <div class="grid grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Multiplier Min (A)</label> | |
| <input type="number" id="multiplier-min" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 focus:border-blue-500 focus:outline-none" value="2" min="1"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Multiplier Max (A)</label> | |
| <input type="number" id="multiplier-max" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2" value="12" min="1"> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Multiplicand Min (B)</label> | |
| <input type="number" id="multiplicand-min" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2" value="2" min="1"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Multiplicand Max (B)</label> | |
| <input type="number" id="multiplicand-max" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2" value="12" min="1"> | |
| </div> | |
| </div> | |
| <div id="product-range-container" class="grid grid-cols-2 gap-4 hidden"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Product Min (C)</label> | |
| <input type="number" id="product-min" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2" value="10" min="1"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Product Max (C)</label> | |
| <input type="number" id="product-max" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2" value="100" min="1"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Exclusions --> | |
| <div class="mb-8 pb-6 border-b border-gray-700"> | |
| <h3 class="text-lg font-semibold mb-3">Exclusions</h3> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Exclude these numbers (comma separated)</label> | |
| <input type="text" id="exclude-list" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2" placeholder="e.g., 5, 10, 11"> | |
| <p class="text-xs text-gray-400 mt-1">Numbers to exclude from appearing in problems</p> | |
| </div> | |
| </div> | |
| <!-- Time limit --> | |
| <div class="mb-8"> | |
| <h3 class="text-lg font-semibold mb-3">Time Limit (seconds)</h3> | |
| <input type="number" id="time-limit" class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2" value="5" min="1"> | |
| </div> | |
| <!-- Action buttons --> | |
| <div class="flex space-x-3"> | |
| <button id="save-settings" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded"> | |
| Save & Restart | |
| </button> | |
| <button id="reset-stats" class="bg-gray-700 hover:bg-gray-600 text-white font-medium py-3 px-4 rounded"> | |
| Reset Stats | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| // Configuration object with default values | |
| let config = { | |
| mode: 'multiplication', | |
| multiplierMin: 2, | |
| multiplierMax: 12, | |
| multiplicandMin: 2, | |
| multiplicandMax: 12, | |
| productMin: 10, | |
| productMax: 100, | |
| excludeList: [], | |
| timeLimit: 5 | |
| }; | |
| // Game state | |
| let state = { | |
| currentQuestion: null, | |
| correctAnswer: null, | |
| timer: null, | |
| timeLeft: 0, | |
| correctCount: 0, | |
| incorrectCount: 0, | |
| isRunning: false | |
| }; | |
| // DOM elements | |
| const elements = { | |
| question: document.getElementById('question'), | |
| answerInput: document.getElementById('answer-input'), | |
| feedback: document.getElementById('feedback'), | |
| timerBar: document.getElementById('timer-bar'), | |
| correctCount: document.getElementById('correct-count'), | |
| incorrectCount: document.getElementById('incorrect-count'), | |
| accuracy: document.getElementById('accuracy'), | |
| settingsBtn: document.getElementById('settings-btn'), | |
| settingsPanel: document.getElementById('settings-panel'), | |
| closeSettings: document.getElementById('close-settings'), | |
| saveSettings: document.getElementById('save-settings'), | |
| resetStats: document.getElementById('reset-stats'), | |
| modeRadios: document.querySelectorAll('input[name="mode"]'), | |
| multiplierMin: document.getElementById('multiplier-min'), | |
| multiplierMax: document.getElementById('multiplier-max'), | |
| multiplicandMin: document.getElementById('multiplicand-min'), | |
| multiplicandMax: document.getElementById('multiplicand-max'), | |
| productMin: document.getElementById('product-min'), | |
| productMax: document.getElementById('product-max'), | |
| productRangeContainer: document.getElementById('product-range-container'), | |
| excludeList: document.getElementById('exclude-list'), | |
| timeLimit: document.getElementById('time-limit') | |
| }; | |
| // Initialize the game | |
| function init() { | |
| loadConfig(); | |
| updateStats(); | |
| setupEventListeners(); | |
| updateModeUI(); | |
| generateQuestion(); | |
| } | |
| // Load config from localStorage | |
| function loadConfig() { | |
| const savedConfig = localStorage.getItem('mathTrainerConfig'); | |
| if (savedConfig) { | |
| Object.assign(config, JSON.parse(savedConfig)); | |
| updateSettingsForm(); | |
| } | |
| } | |
| // Save config to localStorage | |
| function saveConfig() { | |
| localStorage.setItem('mathTrainerConfig', JSON.stringify(config)); | |
| } | |
| // Update the settings form with current config values | |
| function updateSettingsForm() { | |
| // Set radio button | |
| document.querySelector(`input[name="mode"][value="${config.mode}"]`).checked = true; | |
| // Set number inputs | |
| elements.multiplierMin.value = config.multiplierMin; | |
| elements.multiplierMax.value = config.multiplierMax; | |
| elements.multiplicandMin.value = config.multiplicandMin; | |
| elements.multiplicandMax.value = config.multiplicandMax; | |
| elements.productMin.value = config.productMin; | |
| elements.productMax.value = config.productMax; | |
| elements.excludeList.value = config.excludeList.join(', '); | |
| elements.timeLimit.value = config.timeLimit; | |
| updateModeUI(); | |
| } | |
| // Update UI based on selected mode | |
| function updateModeUI() { | |
| if (config.mode === 'factoring') { | |
| elements.productRangeContainer.classList.remove('hidden'); | |
| } else { | |
| elements.productRangeContainer.classList.add('hidden'); | |
| } | |
| } | |
| // Set up event listeners | |
| function setupEventListeners() { | |
| // Answer submission | |
| elements.answerInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| checkAnswer(); | |
| } | |
| }); | |
| // Settings button | |
| elements.settingsBtn.addEventListener('click', () => { | |
| elements.settingsPanel.classList.add('open'); | |
| document.getElementById('settings-backdrop').classList.add('opacity-100', 'pointer-events-auto'); | |
| elements.settingsBtn.querySelector('i').classList.add('opacity-0'); | |
| elements.answerInput.blur(); | |
| }); | |
| // Close settings | |
| elements.closeSettings.addEventListener('click', () => { | |
| elements.settingsPanel.classList.remove('open'); | |
| document.getElementById('settings-backdrop').classList.remove('opacity-100', 'pointer-events-auto'); | |
| elements.settingsBtn.querySelector('i').classList.remove('opacity-0'); | |
| elements.answerInput.focus(); | |
| }); | |
| // Close settings when clicking backdrop | |
| document.getElementById('settings-backdrop').addEventListener('click', () => { | |
| elements.settingsPanel.classList.remove('open'); | |
| document.getElementById('settings-backdrop').classList.remove('opacity-100', 'pointer-events-auto'); | |
| elements.settingsBtn.querySelector('i').classList.remove('opacity-0'); | |
| elements.answerInput.focus(); | |
| }); | |
| // Save settings | |
| elements.saveSettings.addEventListener('click', saveSettings); | |
| // Reset stats | |
| elements.resetStats.addEventListener('click', resetStats); | |
| // Mode change | |
| elements.modeRadios.forEach(radio => { | |
| radio.addEventListener('change', (e) => { | |
| if (e.target.value === 'factoring') { | |
| elements.productRangeContainer.classList.remove('hidden'); | |
| } else { | |
| elements.productRangeContainer.classList.add('hidden'); | |
| } | |
| }); | |
| }); | |
| } | |
| // Save settings from form to config | |
| function saveSettings() { | |
| // Get mode | |
| config.mode = document.querySelector('input[name="mode"]:checked').value; | |
| // Get number ranges | |
| config.multiplierMin = parseInt(elements.multiplierMin.value) || 1; | |
| config.multiplierMax = parseInt(elements.multiplierMax.value) || 12; | |
| config.multiplicandMin = parseInt(elements.multiplicandMin.value) || 1; | |
| config.multiplicandMax = parseInt(elements.multiplicandMax.value) || 12; | |
| config.productMin = parseInt(elements.productMin.value) || 10; | |
| config.productMax = parseInt(elements.productMax.value) || 100; | |
| // Get exclude list | |
| const excludeStr = elements.excludeList.value.trim(); | |
| config.excludeList = excludeStr ? excludeStr.split(',').map(num => parseInt(num.trim())).filter(num => !isNaN(num)) : []; | |
| // Get time limit | |
| config.timeLimit = parseInt(elements.timeLimit.value) || 5; | |
| // Save to localStorage | |
| saveConfig(); | |
| // Close settings panel | |
| elements.settingsPanel.classList.remove('open'); | |
| document.getElementById('settings-backdrop').classList.remove('opacity-100', 'pointer-events-auto'); | |
| elements.settingsBtn.querySelector('i').classList.remove('opacity-0'); | |
| // Restart game with new settings | |
| resetGame(); | |
| setTimeout(() => { | |
| elements.answerInput.focus(); | |
| elements.answerInput.select(); | |
| }, 100); | |
| } | |
| // Reset game stats | |
| function resetStats() { | |
| state.correctCount = 0; | |
| state.incorrectCount = 0; | |
| updateStats(); | |
| } | |
| // Reset the game state | |
| function resetGame() { | |
| clearTimeout(state.timer); | |
| state.isRunning = false; | |
| generateQuestion(); | |
| } | |
| // Update stats display | |
| function updateStats() { | |
| elements.correctCount.textContent = state.correctCount; | |
| elements.incorrectCount.textContent = state.incorrectCount; | |
| const total = state.correctCount + state.incorrectCount; | |
| const accuracy = total > 0 ? Math.round((state.correctCount / total) * 100) : 0; | |
| elements.accuracy.textContent = accuracy; | |
| } | |
| // Generate a new question | |
| function generateQuestion() { | |
| clearTimeout(state.timer); | |
| let num1, num2, question, answer; | |
| if (config.mode === 'multiplication') { | |
| // Generate multiplication question | |
| [num1, num2] = generateValidNumbers( | |
| config.multiplierMin, | |
| config.multiplierMax, | |
| config.multiplicandMin, | |
| config.multiplicandMax | |
| ); | |
| question = `${num1} × ${num2} =`; | |
| answer = num1 * num2; | |
| } else { | |
| // Generate factoring question | |
| let attempts = 0; | |
| const maxAttempts = 100; | |
| while (attempts < maxAttempts) { | |
| num1 = getRandomInt(config.multiplierMin, config.multiplierMax); | |
| num2 = getRandomInt(config.multiplicandMin, config.multiplicandMax); | |
| // Check if numbers are excluded | |
| if (config.excludeList.includes(num1) || config.excludeList.includes(num2)) { | |
| attempts++; | |
| continue; | |
| } | |
| const product = num1 * num2; | |
| // Check if product is within range | |
| if (product >= config.productMin && product <= config.productMax) { | |
| question = `${product} ÷ ${num1} =`; | |
| answer = num2; | |
| break; | |
| } | |
| attempts++; | |
| } | |
| // If we couldn't find a valid question after max attempts | |
| if (attempts >= maxAttempts) { | |
| question = "Couldn't generate question with current settings"; | |
| answer = null; | |
| } | |
| } | |
| state.currentQuestion = question; | |
| state.correctAnswer = answer; | |
| elements.question.textContent = question; | |
| elements.answerInput.value = ''; | |
| elements.feedback.textContent = ''; | |
| elements.feedback.classList.remove('show', 'text-green-400', 'text-red-400'); | |
| if (answer !== null) { | |
| startTimer(); | |
| } | |
| } | |
| // Generate valid numbers that aren't in the exclude list | |
| function generateValidNumbers(min1, max1, min2, max2) { | |
| let num1, num2; | |
| let attempts = 0; | |
| const maxAttempts = 100; | |
| do { | |
| num1 = getRandomInt(min1, max1); | |
| num2 = getRandomInt(min2, max2); | |
| attempts++; | |
| if (attempts >= maxAttempts) { | |
| // If we can't find valid numbers after many attempts, just use any | |
| break; | |
| } | |
| } while (config.excludeList.includes(num1) || config.excludeList.includes(num2)); | |
| return [num1, num2]; | |
| } | |
| // Get random integer between min and max (inclusive) | |
| function getRandomInt(min, max) { | |
| min = Math.ceil(min); | |
| max = Math.floor(max); | |
| return Math.floor(Math.random() * (max - min + 1)) + min; | |
| } | |
| // Start the timer for the current question | |
| function startTimer() { | |
| clearTimeout(state.timer); | |
| state.timeLeft = config.timeLimit; | |
| state.isRunning = true; | |
| // Update timer bar immediately | |
| updateTimerBar(); | |
| // Start countdown | |
| state.timer = setInterval(() => { | |
| state.timeLeft -= 0.1; | |
| updateTimerBar(); | |
| if (state.timeLeft <= 0) { | |
| clearInterval(state.timer); | |
| timeUp(); | |
| } | |
| }, 100); | |
| } | |
| // Update the timer bar display | |
| function updateTimerBar() { | |
| const percentage = Math.max(0, (state.timeLeft / config.timeLimit) * 100); | |
| elements.timerBar.style.width = `${percentage}%`; | |
| // Change color based on time left | |
| if (percentage > 50) { | |
| elements.timerBar.className = 'timer-bar bg-blue-500 rounded-full'; | |
| } else if (percentage > 25) { | |
| elements.timerBar.className = 'timer-bar bg-yellow-500 rounded-full'; | |
| } else { | |
| elements.timerBar.className = 'timer-bar bg-red-500 rounded-full'; | |
| } | |
| } | |
| // Handle when time is up | |
| function timeUp() { | |
| state.isRunning = false; | |
| state.incorrectCount++; | |
| updateStats(); | |
| showFeedback(`Time's up! Answer: ${state.correctAnswer}`, false); | |
| // Auto-advance after delay | |
| setTimeout(() => { | |
| generateQuestion(); | |
| }, 2000); | |
| } | |
| // Check the user's answer | |
| function checkAnswer() { | |
| if (!state.isRunning || state.correctAnswer === null) return; | |
| clearTimeout(state.timer); | |
| state.isRunning = false; | |
| const userAnswer = parseInt(elements.answerInput.value.trim()); | |
| if (isNaN(userAnswer)) { | |
| showFeedback('Please enter a number', false); | |
| return; | |
| } | |
| if (userAnswer === state.correctAnswer) { | |
| state.correctCount++; | |
| showFeedback('Correct!', true); | |
| } else { | |
| state.incorrectCount++; | |
| showFeedback(`Incorrect! Answer: ${state.correctAnswer}`, false); | |
| } | |
| updateStats(); | |
| // Auto-advance after delay | |
| setTimeout(() => { | |
| generateQuestion(); | |
| }, 1500); | |
| } | |
| // Show feedback message | |
| function showFeedback(message, isCorrect) { | |
| elements.feedback.textContent = message; | |
| elements.feedback.classList.add('show'); | |
| if (isCorrect) { | |
| elements.feedback.classList.add('text-green-400'); | |
| elements.feedback.classList.remove('text-red-400'); | |
| } else { | |
| elements.feedback.classList.add('text-red-400'); | |
| elements.feedback.classList.remove('text-green-400'); | |
| } | |
| } | |
| // Initialize the game when the page loads | |
| window.addEventListener('DOMContentLoaded', init); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LAshi/calculation" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |