Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hyphen Hunter: Interactive Exercise</title> | |
| <!-- Importing FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #6366f1; | |
| --primary-dark: #4f46e5; | |
| --secondary: #ec4899; | |
| --bg-dark: #0f172a; | |
| --bg-card: #1e293b; | |
| --text-main: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --success: #22c55e; | |
| --error: #ef4444; | |
| --hyphen-color: #fbbf24; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| } | |
| body { | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow-x: hidden; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| background: rgba(30, 41, 59, 0.8); | |
| backdrop-filter: blur(10px); | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| background: linear-gradient(to right, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .built-with { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| } | |
| .built-with:hover { | |
| color: var(--primary); | |
| } | |
| /* --- Main Content --- */ | |
| main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 2rem; | |
| position: relative; | |
| } | |
| /* --- Game Container --- */ | |
| .game-container { | |
| width: 100%; | |
| max-width: 900px; | |
| background: var(--bg-card); | |
| border-radius: 20px; | |
| padding: 3rem; | |
| box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| text-align: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| /* --- Start Screen --- */ | |
| .screen { | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 2rem; | |
| animation: fadeIn 0.5s ease-out; | |
| } | |
| .screen.active { | |
| display: flex; | |
| } | |
| h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| .instruction { | |
| color: var(--text-muted); | |
| font-size: 1.1rem; | |
| max-width: 600px; | |
| line-height: 1.6; | |
| } | |
| .highlight { | |
| color: var(--hyphen-color); | |
| font-weight: bold; | |
| font-size: 1.2em; | |
| } | |
| /* --- Word Display --- */ | |
| .word-display { | |
| font-size: 4rem; | |
| font-weight: 800; | |
| letter-spacing: 2px; | |
| margin: 2rem 0; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 120px; | |
| position: relative; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| user-select: none; | |
| } | |
| .word-display:active { | |
| transform: scale(0.98); | |
| } | |
| .letter { | |
| display: inline-block; | |
| transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| .letter.correct { | |
| color: var(--success); | |
| text-shadow: 0 0 15px rgba(34, 197, 94, 0.5); | |
| } | |
| .letter.wrong { | |
| color: var(--error); | |
| text-decoration: line-through; | |
| opacity: 0.5; | |
| } | |
| /* --- Controls --- */ | |
| .controls { | |
| display: flex; | |
| gap: 1rem; | |
| margin-top: 1rem; | |
| } | |
| .btn { | |
| padding: 1rem 2rem; | |
| border: none; | |
| border-radius: 50px; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(99, 102, 241, 0.6); | |
| } | |
| .btn-primary:active { | |
| transform: translateY(1px); | |
| } | |
| .btn-outline { | |
| background: transparent; | |
| border: 2px solid var(--text-muted); | |
| color: var(--text-muted); | |
| } | |
| .btn-outline:hover { | |
| border-color: var(--text-main); | |
| color: var(--text-main); | |
| } | |
| /* --- Feedback Area --- */ | |
| .feedback-area { | |
| height: 40px; | |
| margin-top: 1rem; | |
| font-weight: 600; | |
| font-size: 1.2rem; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .feedback-area.visible { | |
| opacity: 1; | |
| } | |
| .feedback-area.success { color: var(--success); } | |
| .feedback-area.error { color: var(--error); } | |
| /* --- Stats --- */ | |
| .stats-bar { | |
| display: flex; | |
| justify-content: space-between; | |
| width: 100%; | |
| padding: 1rem; | |
| background: rgba(0,0,0,0.2); | |
| border-radius: 12px; | |
| margin-bottom: 2rem; | |
| font-size: 1.1rem; | |
| } | |
| .stat-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .stat-value { | |
| font-weight: bold; | |
| color: var(--primary); | |
| } | |
| /* --- Confetti Canvas --- */ | |
| #confetti-canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| /* --- Animations --- */ | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 25% { transform: translateX(-10px); } | |
| 75% { transform: translateX(10px); } | |
| } | |
| .shake { | |
| animation: shake 0.4s ease-in-out; | |
| } | |
| /* --- Responsive --- */ | |
| @media (max-width: 600px) { | |
| .game-container { | |
| padding: 1.5rem; | |
| } | |
| .word-display { | |
| font-size: 2.5rem; | |
| } | |
| h1 { | |
| font-size: 1.8rem; | |
| } | |
| .controls { | |
| flex-direction: column; | |
| width: 100%; | |
| } | |
| .btn { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-solid fa-asterisk"></i> Hyphen Hunter | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with"> | |
| Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square"></i> | |
| </a> | |
| </header> | |
| <main> | |
| <div class="game-container"> | |
| <canvas id="confetti-canvas"></canvas> | |
| <!-- Stats Bar --> | |
| <div class="stats-bar"> | |
| <div class="stat-item"> | |
| <i class="fa-solid fa-heart" style="color: var(--error)"></i> | |
| <span>Score: <span id="score" class="stat-value">0</span></span> | |
| </div> | |
| <div class="stat-item"> | |
| <i class="fa-solid fa-trophy" style="color: var(--hyphen-color)"></i> | |
| <span>Streak: <span id="streak" class="stat-value">0</span></span> | |
| </div> | |
| </div> | |
| <!-- Start Screen --> | |
| <div id="start-screen" class="screen active"> | |
| <div style="font-size: 4rem; color: var(--hyphen-color); margin-bottom: 1rem;"> | |
| <i class="fa-solid fa-scissors"></i> | |
| </div> | |
| <h1>Identify the Hyphen</h1> | |
| <p class="instruction"> | |
| Your goal is to spot the <span class="highlight">hyphen (-)</span> inside the word.<br> | |
| Tap the letter that contains the hyphen to score points. | |
| </p> | |
| <button class="btn btn-primary" onclick="startGame()"> | |
| <i class="fa-solid fa-play"></i> Start Game | |
| </button> | |
| </div> | |
| <!-- Game Screen --> | |
| <div id="game-screen" class="screen"> | |
| <div class="word-display" id="word-display"> | |
| <!-- Letters injected here --> | |
| </div> | |
| <div id="feedback" class="feedback-area"></div> | |
| <div class="controls"> | |
| <button class="btn btn-outline" onclick="skipWord()" id="skip-btn"> | |
| <i class="fa-solid fa-forward"></i> Skip | |
| </button> | |
| <button class="btn btn-primary" onclick="checkAnswer()" id="check-btn"> | |
| <i class="fa-solid fa-check"></i> Confirm | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Game Over Screen --> | |
| <div id="game-over-screen" class="screen"> | |
| <div style="font-size: 4rem; color: var(--primary); margin-bottom: 1rem;"> | |
| <i class="fa-solid fa-flag-checkered"></i> | |
| </div> | |
| <h1>Game Over</h1> | |
| <p class="instruction"> | |
| Great effort! You finished the set. | |
| </p> | |
| <div style="font-size: 2rem; margin: 1rem 0;"> | |
| Final Score: <span id="final-score" class="stat-value">0</span> | |
| </div> | |
| <div class="controls"> | |
| <button class="btn btn-primary" onclick="startGame()"> | |
| <i class="fa-solid fa-rotate-right"></i> Play Again | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| // --- Game Data --- | |
| const words = [ | |
| { text: "mother-in-law", index: 3 }, | |
| { text: "well-being", index: 4 }, | |
| { text: "state-of-the-art", index: 7 }, | |
| { text: "decision-making", index: 5 }, | |
| { text: "full-time", index: 3 }, | |
| { text: "twenty-one", index: 5 }, | |
| { text: "high-level", index: 4 }, | |
| { text: "copy-editing", index: 3 }, | |
| { text: "fourth-of-july", index: 6 }, | |
| { text: "decision-making", index: 5 }, | |
| { text: "co-operate", index: 2 }, | |
| { text: "eco-friendly", index: 2 }, | |
| { text: "self-control", index: 3 }, | |
| { text: "high-tech", index: 3 }, | |
| { text: "long-term", index: 3 }, | |
| { text: "decision-making", index: 5 } | |
| ]; | |
| // --- State --- | |
| let currentWord = ""; | |
| let currentHyphenIndex = -1; | |
| let score = 0; | |
| let streak = 0; | |
| let isProcessing = false; | |
| let audioCtx = null; | |
| // --- Elements --- | |
| const screens = { | |
| start: document.getElementById('start-screen'), | |
| game: document.getElementById('game-screen'), | |
| over: document.getElementById('game-over-screen') | |
| }; | |
| const wordDisplay = document.getElementById('word-display'); | |
| const scoreEl = document.getElementById('score'); | |
| const streakEl = document.getElementById('streak'); | |
| const feedbackEl = document.getElementById('feedback'); | |
| const finalScoreEl = document.getElementById('final-score'); | |
| // --- Audio System (Web Audio API) --- | |
| function initAudio() { | |
| if (!audioCtx) { | |
| audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| } | |
| function playSound(type) { | |
| if (!audioCtx) return; | |
| const osc = audioCtx.createOscillator(); | |
| const gain = audioCtx.createGain(); | |
| osc.connect(gain); | |
| gain.connect(audioCtx.destination); | |
| const now = audioCtx.currentTime; | |
| if (type === 'success') { | |
| osc.type = 'sine'; | |
| osc.frequency.setValueAtTime(500, now); | |
| osc.frequency.exponentialRampToValueAtTime(1000, now + 0.1); | |
| gain.gain.setValueAtTime(0.3, now); | |
| gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3); | |
| osc.start(now); | |
| osc.stop(now + 0.3); | |
| } else if (type === 'error') { | |
| osc.type = 'sawtooth'; | |
| osc.frequency.setValueAtTime(200, now); | |
| osc.frequency.linearRampToValueAtTime(100, now + 0.2); | |
| gain.gain.setValueAtTime(0.3, now); | |
| gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3); | |
| osc.start(now); | |
| osc.stop(now + 0.3); | |
| } else if (type === 'hover') { | |
| osc.type = 'triangle'; | |
| osc.frequency.setValueAtTime(800, now); | |
| gain.gain.setValueAtTime(0.05, now); | |
| gain.gain.exponentialRampToValueAtTime(0.01, now + 0.05); | |
| osc.start(now); | |
| osc.stop(now + 0.05); | |
| } | |
| } | |
| // --- Game Logic --- | |
| function switchScreen(screenName) { | |
| Object.values(screens).forEach(s => s.classList.remove('active')); | |
| screens[screenName].classList.add('active'); | |
| } | |
| function startGame() { | |
| score = 0; | |
| streak = 0; | |
| updateStats(); | |
| switchScreen('game'); | |
| loadNewWord(); | |
| } | |
| function loadNewWord() { | |
| isProcessing = false; | |
| feedbackEl.className = 'feedback-area'; | |
| feedbackEl.innerText = ''; | |
| // Pick random word | |
| const randomIndex = Math.floor(Math.random() * words.length); | |
| currentWord = words[randomIndex].text; | |
| currentHyphenIndex = words[randomIndex].index; | |
| renderWord(); | |
| } | |
| function renderWord() { | |
| wordDisplay.innerHTML = ''; | |
| currentWord.split('').forEach((char, index) => { | |
| const span = document.createElement('span'); | |
| span.className = 'letter'; | |
| span.innerText = char; | |
| span.dataset.index = index; | |
| // Interaction | |
| span.addEventListener('click', () => handleLetterClick(span)); | |
| span.addEventListener('mouseenter', () => playSound('hover')); | |
| wordDisplay.appendChild(span); | |
| }); | |
| } | |
| function handleLetterClick(element) { | |
| if (isProcessing) return; | |
| const index = parseInt(element.dataset.index); | |
| const letter = currentWord[index]; | |
| // Visual feedback immediately on click | |
| if (letter === '-') { | |
| element.classList.add('correct'); | |
| // Don't change color to green yet, wait for confirm | |
| } else { | |
| element.classList.add('wrong'); | |
| playSound('error'); | |
| streak = 0; | |
| updateStats(); | |
| triggerShake(); | |
| } | |
| } | |
| function checkAnswer() { | |
| if (isProcessing) return; | |
| // Find the selected hyphen | |
| const selected = wordDisplay.querySelector('.letter.correct'); | |
| if (selected) { | |
| const clickedIndex = parseInt(selected.dataset.index); | |
| if (clickedIndex === currentHyphenIndex) { | |
| handleSuccess(); | |
| } else { | |
| handleFailure(selected); | |
| } | |
| } else { | |
| // No selection made | |
| feedbackEl.innerText = "Please select a letter first!"; | |
| feedbackEl.classList.add('visible', 'error'); | |
| } | |
| } | |
| function handleSuccess() { | |
| isProcessing = true; | |
| playSound('success'); | |
| // Highlight the correct letter green permanently | |
| const correctSpan = wordDisplay.querySelectorAll('.letter')[currentHyphenIndex]; | |
| correctSpan.classList.remove('correct'); // remove temp color | |
| correctSpan.style.color = 'var(--success)'; | |
| score += 10 + (streak * 2); | |
| streak++; | |
| updateStats(); | |
| feedbackEl.innerText = "Correct! +Points"; | |
| feedbackEl.className = 'feedback-area visible success'; | |
| // Confetti effect | |
| fireConfetti(); | |
| setTimeout(() => { | |
| loadNewWord(); | |
| }, 1500); | |
| } | |
| function handleFailure(wrongSpan) { | |
| isProcessing = true; | |
| playSound('error'); | |
| streak = 0; | |
| updateStats(); | |
| wrongSpan.classList.remove('wrong'); | |
| wrongSpan.style.color = 'var(--error)'; | |
| feedbackEl.innerText = "Wrong! That wasn't the hyphen."; | |
| feedbackEl.className = 'feedback-area visible error'; | |
| triggerShake(); | |
| setTimeout(() => { | |
| // Reset visual state | |
| wrongSpan.style.color = ''; | |
| loadNewWord(); | |
| }, 1000); | |
| } | |
| function skipWord() { | |
| if (isProcessing) return; | |
| streak = 0; | |
| updateStats(); | |
| loadNewWord(); | |
| } | |
| function triggerShake() { | |
| wordDisplay.classList.add('shake'); | |
| setTimeout(() => wordDisplay.classList.remove('shake'), 400); | |
| } | |
| function updateStats() { | |
| scoreEl.innerText = score; | |
| streakEl.innerText = streak; | |
| } | |
| // --- Confetti System (Canvas) --- | |
| const canvas = document.getElementById('confetti-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let particles = []; | |
| function resizeCanvas() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| resizeCanvas(); | |
| function fireConfetti() { | |
| const colors = ['#6366f1', '#ec4899', '#22c55e', '#fbbf24']; | |
| for (let i = 0; i < 100; i++) { | |
| particles.push({ | |
| x: canvas.width / 2, | |
| y: canvas.height / 2, | |
| vx: (Math.random() - 0.5) * 15, | |
| vy: (Math.random() - 0.5) * 15 - 5, | |
| life: 1, | |
| color: colors[Math.floor(Math.random() * colors.length)], | |
| size: Math.random() * 5 + 2 | |
| }); | |
| } | |
| animateConfetti(); | |
| } | |
| function animateConfetti() { | |
| if (particles.length === 0) return; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| for (let i = 0; i < particles.length; i++) { | |
| const p = particles[i]; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.vy += 0.2; // Gravity | |
| p.life -= 0.02; | |
| ctx.fillStyle = p.color; | |
| ctx.globalAlpha = p.life; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| particles = particles.filter(p => p.life > 0); | |
| if (particles.length > 0) { | |
| requestAnimationFrame(animateConfetti); | |
| } else { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |