Spaces:
Running
Running
<html lang="ko"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | |
<title>Mango Coconut Rhythm Game</title> | |
<style> | |
body { | |
font-family: 'Arial', sans-serif; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
height: 100vh; | |
margin: 0; | |
background: linear-gradient(135deg, #FFD700, #FFA500); /* Mango/Orange gradient */ | |
color: #fff; | |
text-align: center; | |
overflow: hidden; | |
touch-action: manipulation; /* Prevents zoom on double tap, better for games */ | |
} | |
#game-container { | |
background-color: rgba(255, 255, 255, 0.1); | |
padding: 30px; | |
border-radius: 20px; | |
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); | |
width: 90%; | |
max-width: 400px; | |
} | |
#target-fruit { | |
font-size: 3em; | |
font-weight: bold; | |
margin-bottom: 20px; | |
color: #FFF; | |
text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
height: 60px; /* Fixed height to prevent layout shifts */ | |
} | |
#input-area { | |
margin-bottom: 20px; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
flex-wrap: wrap; /* Allow dots to wrap */ | |
} | |
.tap-indicator { | |
width: 20px; | |
height: 20px; | |
background-color: rgba(255,255,255,0.3); | |
border-radius: 50%; | |
margin: 0 5px; | |
transition: background-color 0.1s ease; | |
} | |
.tap-indicator.active { | |
background-color: #FFEB3B; /* Bright yellow for active tap */ | |
} | |
#tap-button { | |
padding: 20px 40px; | |
font-size: 1.5em; | |
background-color: #FFC107; /* Mango yellow */ | |
color: #8C5B00; /* Darker text for contrast */ | |
border: none; | |
border-radius: 10px; | |
cursor: pointer; | |
box-shadow: 0 5px 15px rgba(0,0,0,0.15); | |
transition: background-color 0.2s, transform 0.1s; | |
user-select: none; /* Prevent text selection on rapid clicks */ | |
-webkit-tap-highlight-color: transparent; /* Remove tap highlight on mobile */ | |
} | |
#tap-button:active { | |
background-color: #FFA000; /* Darker when pressed */ | |
transform: scale(0.95); | |
} | |
#feedback { | |
font-size: 1.5em; | |
margin-top: 20px; | |
height: 30px; /* Fixed height */ | |
font-weight: bold; | |
} | |
.feedback-correct { | |
color: #A5D6A7; /* Light green */ | |
} | |
.feedback-incorrect { | |
color: #EF9A9A; /* Light red */ | |
} | |
#score-display { | |
margin-top: 15px; | |
font-size: 1.2em; | |
} | |
#start-button { | |
padding: 15px 30px; | |
font-size: 1.2em; | |
background-color: #4CAF50; /* Green */ | |
color: white; | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
margin-top: 20px; | |
} | |
#instructions { | |
margin-top: 20px; | |
font-size: 0.9em; | |
color: rgba(255,255,255,0.8); | |
} | |
</style> | |
</head> | |
<body> | |
<div id="game-container"> | |
<div id="target-fruit">Press Start!</div> | |
<div id="input-area"> | |
<!-- Tap indicators will be generated here --> | |
</div> | |
<button id="tap-button" disabled>TAP</button> | |
<div id="feedback"></div> | |
<div id="score-display">Score: 0</div> | |
<button id="start-button">Start Game</button> | |
<div id="instructions"> | |
<p>Tap the rhythm for the displayed fruit!<br> | |
MANGO: 2 taps (tap-tap)<br> | |
COCONUT: 3 taps (tap-tap-tap)<br> | |
Press SPACE or TAP button.</p> | |
</div> | |
</div> | |
<script> | |
const targetFruitDisplay = document.getElementById('target-fruit'); | |
const inputArea = document.getElementById('input-area'); | |
const tapButton = document.getElementById('tap-button'); | |
const feedbackDisplay = document.getElementById('feedback'); | |
const scoreDisplay = document.getElementById('score-display'); | |
const startButton = document.getElementById('start-button'); | |
const fruits = [ | |
{ name: 'MANGO', rhythm: [1, 1], beats: 2 }, // 2 taps | |
{ name: 'COCONUT', rhythm: [1, 1, 1], beats: 3 } // 3 taps | |
]; | |
let currentFruit = null; | |
let currentTapCount = 0; | |
let expectedTaps = 0; | |
let score = 0; | |
let gameActive = false; | |
let inputTimeout; | |
// Web Audio API setup | |
let audioContext; | |
const tapFrequency = 440; // A4 note for tap | |
const successFrequency = 660; // E5 note for success | |
const failFrequency = 220; // A3 note for fail | |
function initAudioContext() { | |
if (!audioContext) { | |
audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
} | |
} | |
function playTone(frequency, duration = 0.1, type = 'sine') { | |
if (!audioContext) return; | |
const oscillator = audioContext.createOscillator(); | |
const gainNode = audioContext.createGain(); | |
oscillator.type = type; | |
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); | |
gainNode.gain.setValueAtTime(0.5, audioContext.currentTime); // Volume | |
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration); | |
oscillator.connect(gainNode); | |
gainNode.connect(audioContext.destination); | |
oscillator.start(); | |
oscillator.stop(audioContext.currentTime + duration); | |
} | |
function setupNextFruit() { | |
currentTapCount = 0; | |
currentFruit = fruits[Math.floor(Math.random() * fruits.length)]; | |
expectedTaps = currentFruit.beats; | |
targetFruitDisplay.textContent = currentFruit.name; | |
feedbackDisplay.textContent = ''; | |
feedbackDisplay.className = ''; // Clear feedback color | |
renderTapIndicators(); | |
clearTimeout(inputTimeout); // Clear previous timeout | |
// Start a timeout for the current fruit - player must complete within X seconds | |
inputTimeout = setTimeout(() => { | |
if (gameActive && currentTapCount < expectedTaps && currentTapCount > 0) { // Partial input | |
handleResult(false, "Too slow!"); | |
} else if (gameActive && currentTapCount === 0) { // No input | |
// Optional: penalty or just move on | |
// console.log("No input for", currentFruit.name); | |
// setupNextFruit(); // Silently move to next | |
} | |
}, 3000 + expectedTaps * 500); // Generous timeout based on beats | |
} | |
function renderTapIndicators() { | |
inputArea.innerHTML = ''; | |
for (let i = 0; i < expectedTaps; i++) { | |
const indicator = document.createElement('div'); | |
indicator.classList.add('tap-indicator'); | |
if (i < currentTapCount) { | |
indicator.classList.add('active'); | |
} | |
inputArea.appendChild(indicator); | |
} | |
} | |
function handleTap() { | |
if (!gameActive || !currentFruit) return; | |
initAudioContext(); // Ensure audio context is started on user interaction | |
playTone(tapFrequency, 0.05); // Short tap sound | |
currentTapCount++; | |
renderTapIndicators(); | |
clearTimeout(inputTimeout); // Reset timeout on each tap | |
if (currentTapCount === expectedTaps) { | |
handleResult(true); | |
} else if (currentTapCount > expectedTaps) { | |
// This case should ideally not be reached if logic is tight | |
handleResult(false, "Too many taps!"); | |
} else { | |
// Set a short timeout for the next tap in the sequence | |
inputTimeout = setTimeout(() => { | |
if (gameActive && currentTapCount < expectedTaps) { | |
handleResult(false, "Rhythm incomplete!"); | |
} | |
}, 800); // Time window for next tap in sequence | |
} | |
} | |
function handleResult(isCorrect, message = "") { | |
if (!gameActive) return; | |
if (isCorrect) { | |
feedbackDisplay.textContent = 'GREAT!'; | |
feedbackDisplay.className = 'feedback-correct'; | |
playTone(successFrequency, 0.2, 'triangle'); | |
score++; | |
scoreDisplay.textContent = `Score: ${score}`; | |
setTimeout(setupNextFruit, 700); // Wait a bit then show next fruit | |
} else { | |
feedbackDisplay.textContent = message || 'MISSED!'; | |
feedbackDisplay.className = 'feedback-incorrect'; | |
playTone(failFrequency, 0.3, 'sawtooth'); | |
// Optional: Implement lives or game over condition | |
// For now, just move to the next fruit after a delay | |
setTimeout(setupNextFruit, 1200); | |
} | |
currentTapCount = 0; // Reset for safety, though setupNextFruit also does this | |
} | |
function startGame() { | |
initAudioContext(); | |
gameActive = true; | |
score = 0; | |
scoreDisplay.textContent = `Score: ${score}`; | |
tapButton.disabled = false; | |
startButton.style.display = 'none'; // Hide start button | |
document.getElementById('instructions').style.display = 'none'; | |
setupNextFruit(); | |
} | |
startButton.addEventListener('click', startGame); | |
tapButton.addEventListener('click', handleTap); | |
// Keyboard support (Spacebar) | |
document.addEventListener('keydown', (event) => { | |
if (event.code === 'Space') { | |
if (!gameActive && startButton.style.display !== 'none') { | |
startGame(); | |
} else if (gameActive) { | |
tapButton.click(); // Simulate button click for visual feedback and logic | |
} | |
event.preventDefault(); // Prevent page scroll | |
} | |
}); | |
</script> | |
</body> | |
</html> |