Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Vertical Letter Stacker</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| font-family: 'Arial', sans-serif; | |
| touch-action: none; | |
| } | |
| #gameCanvas { | |
| background-color: #1a202c; | |
| display: block; | |
| } | |
| #startScreen { | |
| background-color: rgba(26, 32, 44, 0.9); | |
| } | |
| #gameOver { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background-color: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| text-align: center; | |
| display: none; | |
| z-index: 100; | |
| } | |
| #wordDisplay { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| color: white; | |
| font-size: 18px; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| padding: 10px; | |
| border-radius: 5px; | |
| z-index: 10; | |
| } | |
| #scoreDisplay { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| color: white; | |
| font-size: 18px; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| padding: 10px; | |
| border-radius: 5px; | |
| z-index: 10; | |
| } | |
| #heightWarning { | |
| position: absolute; | |
| top: 50px; | |
| left: 10px; | |
| color: #f56565; | |
| font-size: 18px; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| padding: 10px; | |
| border-radius: 5px; | |
| z-index: 10; | |
| display: none; | |
| } | |
| #countWarning { | |
| position: absolute; | |
| top: 90px; | |
| left: 10px; | |
| color: #f56565; | |
| font-size: 18px; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| padding: 10px; | |
| border-radius: 5px; | |
| z-index: 10; | |
| display: none; | |
| } | |
| .glow { | |
| animation: glow 0.5s ease-in-out infinite alternate; | |
| } | |
| @keyframes glow { | |
| from { | |
| text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #e60073, 0 0 20px #e60073; | |
| } | |
| to { | |
| text-shadow: 0 0 10px #fff, 0 0 15px #ff4da6, 0 0 20px #ff4da6, 0 0 25px #ff4da6; | |
| } | |
| } | |
| .warning-glow { | |
| animation: warning-glow 0.5s ease-in-out infinite alternate; | |
| } | |
| @keyframes warning-glow { | |
| from { | |
| box-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #f56565, 0 0 20px #f56565; | |
| } | |
| to { | |
| box-shadow: 0 0 10px #fff, 0 0 15px #f56565, 0 0 20px #f56565, 0 0 25px #f56565; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 flex items-center justify-center h-screen"> | |
| <div id="startScreen" class="text-white text-center p-8 rounded-lg shadow-lg max-w-md"> | |
| <h1 class="text-4xl font-bold mb-6 text-blue-400">Vertical Letter Stacker</h1> | |
| <p class="mb-8 text-lg">Catch falling letters to form words. Don't collect more than 11 letters!</p> | |
| <button id="startButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-full text-xl transition-all duration-300 transform hover:scale-105"> | |
| Start Game | |
| </button> | |
| <div class="mt-8 text-gray-400"> | |
| <p>Form words to score points!</p> | |
| <p class="mt-2">Game over if you collect more than 11 letters</p> | |
| <p class="mt-2">Only collect letters when they touch the platform</p> | |
| </div> | |
| </div> | |
| <canvas id="gameCanvas" class="hidden"></canvas> | |
| <div id="wordDisplay"></div> | |
| <div id="scoreDisplay">Score: 0</div> | |
| <div id="heightWarning">Warning: Stack getting too high!</div> | |
| <div id="countWarning">Warning: Too many letters!</div> | |
| <div id="gameOver"> | |
| <h2 class="text-3xl font-bold mb-4 text-red-500">Game Over!</h2> | |
| <p id="finalScore" class="text-xl mb-6">Your score: 0</p> | |
| <p id="gameOverReason" class="text-lg mb-6">You collected too many letters!</p> | |
| <button id="restartButton" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"> | |
| Play Again | |
| </button> | |
| </div> | |
| <script> | |
| // Game variables | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const startScreen = document.getElementById('startScreen'); | |
| const startButton = document.getElementById('startButton'); | |
| const gameOverScreen = document.getElementById('gameOver'); | |
| const restartButton = document.getElementById('restartButton'); | |
| const wordDisplay = document.getElementById('wordDisplay'); | |
| const scoreDisplay = document.getElementById('scoreDisplay'); | |
| const heightWarning = document.getElementById('heightWarning'); | |
| const countWarning = document.getElementById('countWarning'); | |
| const gameOverReason = document.getElementById('gameOverReason'); | |
| let gameRunning = false; | |
| let score = 0; | |
| let platformWidth = 120; | |
| let platformHeight = 20; | |
| let platformX; | |
| let platformY; | |
| let letters = []; | |
| let stackedLetters = []; | |
| let currentWord = ''; | |
| let gameSpeed = 1.5; | |
| let letterSpawnRate = 1000; // ms | |
| let lastLetterSpawn = 0; | |
| let animationId = null; | |
| let mouseX = 0; | |
| const letterSize = 40; | |
| const warningHeight = 150; // Height at which warning appears | |
| const maxLetters = 11; // Maximum allowed letters before game over | |
| // Function to check if a word is valid using Datamuse API | |
| async function isValidWord(word) { | |
| try { | |
| const response = await fetch(`https://api.datamuse.com/words?sp=${word}&max=1`); | |
| const data = await response.json(); | |
| return data.length > 0 && data[0].word.toLowerCase() === word.toLowerCase(); | |
| } catch (error) { | |
| console.error('Error checking word:', error); | |
| return false; | |
| } | |
| } | |
| // Valid words found | |
| let validWords = []; | |
| // Initialize game | |
| function initGame() { | |
| // Clear any existing animation frame | |
| if (animationId) { | |
| cancelAnimationFrame(animationId); | |
| } | |
| // Set canvas size | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| // Position platform | |
| platformX = canvas.width / 2 - platformWidth / 2; | |
| platformY = canvas.height - platformHeight - 20; | |
| // Reset game state | |
| letters = []; | |
| stackedLetters = []; | |
| currentWord = ''; | |
| score = 0; | |
| gameSpeed = 2; | |
| letterSpawnRate = 1000; | |
| // Reset valid words | |
| validWords = []; | |
| // Update UI | |
| updateScore(); | |
| updateWordDisplay(); | |
| heightWarning.style.display = 'none'; | |
| heightWarning.classList.remove('warning-glow'); | |
| countWarning.style.display = 'none'; | |
| countWarning.classList.remove('warning-glow'); | |
| } | |
| // Start game | |
| function startGame() { | |
| startScreen.classList.add('hidden'); | |
| canvas.classList.remove('hidden'); | |
| gameOverScreen.style.display = 'none'; | |
| gameRunning = true; | |
| initGame(); | |
| gameLoop(); | |
| } | |
| // Game loop | |
| function gameLoop(timestamp = 0) { | |
| if (!gameRunning) return; | |
| animationId = requestAnimationFrame(gameLoop); | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw platform | |
| ctx.fillStyle = '#4a5568'; | |
| ctx.fillRect(platformX, platformY, platformWidth, platformHeight); | |
| // Spawn new letters | |
| if (timestamp - lastLetterSpawn > letterSpawnRate) { | |
| spawnLetter(); | |
| lastLetterSpawn = timestamp; | |
| // Increase difficulty over time | |
| if (score > 0 && score % 10 === 0) { | |
| gameSpeed = Math.min(gameSpeed + 0.1, 4); | |
| letterSpawnRate = Math.max(letterSpawnRate - 50, 300); | |
| } | |
| } | |
| // Update and draw falling letters | |
| updateLetters(); | |
| // Update and draw stacked letters | |
| updateStackedLetters(); | |
| // Check for word matches | |
| checkForWords(); | |
| // Check if stack reached top | |
| checkStackHeight(); | |
| // Check if too many letters collected | |
| checkLetterCount(); | |
| } | |
| // Spawn a new random letter | |
| function spawnLetter() { | |
| const lettersPool = 'abcdefghijklmnopqrstuvwxyz'; | |
| const randomLetter = lettersPool[Math.floor(Math.random() * lettersPool.length)]; | |
| const randomX = Math.random() * (canvas.width - letterSize); | |
| letters.push({ | |
| x: randomX, | |
| y: 0, | |
| letter: randomLetter, | |
| width: letterSize, | |
| height: letterSize, | |
| color: getRandomColor(), | |
| bgColor: getRandomDarkColor() | |
| }); | |
| } | |
| // Update falling letters | |
| function updateLetters() { | |
| for (let i = letters.length - 1; i >= 0; i--) { | |
| const letter = letters[i]; | |
| // Move letter down | |
| letter.y += gameSpeed; | |
| // Draw letter box | |
| ctx.fillStyle = letter.bgColor; | |
| ctx.fillRect(letter.x, letter.y, letter.width, letter.height); | |
| ctx.strokeStyle = '#ffffff'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(letter.x, letter.y, letter.width, letter.height); | |
| // Draw letter text | |
| ctx.fillStyle = letter.color; | |
| ctx.font = 'bold 24px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(letter.letter, letter.x + letter.width/2, letter.y + letter.height/2); | |
| // Check collision with platform (only when touching the platform) | |
| if ( | |
| letter.y + letter.height >= platformY && | |
| letter.y + letter.height <= platformY + platformHeight && | |
| letter.x + letter.width > platformX && | |
| letter.x < platformX + platformWidth | |
| ) { | |
| // Calculate position to stack vertically centered on platform | |
| let stackX = platformX + (platformWidth - letter.width) / 2; | |
| // Add to stacked letters (at the end of array for reverse stacking) | |
| stackedLetters.push({ | |
| x: stackX, | |
| y: platformY - letter.height * (stackedLetters.length + 1), // Position above platform | |
| letter: letter.letter, | |
| width: letter.width, | |
| height: letter.height, | |
| color: letter.color, | |
| bgColor: letter.bgColor | |
| }); | |
| // Remove from falling letters | |
| letters.splice(i, 1); | |
| // Add to current word (at the end to maintain capture order) | |
| currentWord = currentWord + letter.letter; | |
| updateWordDisplay(); | |
| } | |
| // Check if letter fell off screen | |
| if (letter.y > canvas.height) { | |
| letters.splice(i, 1); | |
| } | |
| } | |
| } | |
| // Update stacked letters | |
| function updateStackedLetters() { | |
| if (stackedLetters.length === 0) return; | |
| // Draw all stacked letters from bottom to top | |
| for (let i = 0; i < stackedLetters.length; i++) { | |
| const letter = stackedLetters[i]; | |
| // Position letters vertically centered on platform | |
| letter.x = platformX + (platformWidth - letter.width) / 2; | |
| // Position letters stacked above each other (reverse order) | |
| letter.y = platformY - letter.height * (i + 1); | |
| // Draw letter box | |
| ctx.fillStyle = letter.bgColor; | |
| ctx.fillRect(letter.x, letter.y, letter.width, letter.height); | |
| ctx.strokeStyle = '#ffffff'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(letter.x, letter.y, letter.width, letter.height); | |
| // Draw letter text | |
| ctx.fillStyle = letter.color; | |
| ctx.font = 'bold 24px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(letter.letter, letter.x + letter.width/2, letter.y + letter.height/2); | |
| } | |
| } | |
| // Check stack height and show warning | |
| function checkStackHeight() { | |
| if (stackedLetters.length === 0) { | |
| heightWarning.style.display = 'none'; | |
| heightWarning.classList.remove('warning-glow'); | |
| return; | |
| } | |
| const topLetter = stackedLetters[stackedLetters.length - 1]; | |
| // Show warning when stack gets too high | |
| if (topLetter.y < warningHeight) { | |
| heightWarning.style.display = 'block'; | |
| heightWarning.classList.add('warning-glow'); | |
| heightWarning.textContent = `Warning: ${Math.floor(topLetter.y)}px to top!`; | |
| } else { | |
| heightWarning.style.display = 'none'; | |
| heightWarning.classList.remove('warning-glow'); | |
| } | |
| } | |
| // Check letter count and show warning/game over | |
| function checkLetterCount() { | |
| if (stackedLetters.length === 0) { | |
| countWarning.style.display = 'none'; | |
| countWarning.classList.remove('warning-glow'); | |
| return; | |
| } | |
| // Show warning when approaching max letters | |
| if (stackedLetters.length >= maxLetters - 3) { | |
| countWarning.style.display = 'block'; | |
| countWarning.classList.add('warning-glow'); | |
| countWarning.textContent = `Warning: ${stackedLetters.length}/${maxLetters} letters!`; | |
| } else { | |
| countWarning.style.display = 'none'; | |
| countWarning.classList.remove('warning-glow'); | |
| } | |
| // Game over if too many letters collected | |
| if (stackedLetters.length > maxLetters) { | |
| gameOverReason.textContent = "You collected more than 11 letters!"; | |
| gameOver(); | |
| } | |
| } | |
| // Check for valid words | |
| async function checkForWords() { | |
| // Only check words with 3 or more letters | |
| if (currentWord.length >= 3) { | |
| const isValid = await isValidWord(currentWord); | |
| if (isValid && !validWords.includes(currentWord.toLowerCase())) { | |
| // Award points based on word length | |
| const wordScore = currentWord.length * 10; | |
| score += wordScore; | |
| updateScore(); | |
| // Add to valid words list | |
| validWords.push(currentWord.toLowerCase()); | |
| // Visual feedback | |
| wordDisplay.classList.add('glow'); | |
| setTimeout(() => { | |
| wordDisplay.classList.remove('glow'); | |
| }, 500); | |
| // Reset for next word | |
| stackedLetters = []; | |
| currentWord = ''; | |
| updateWordDisplay(); | |
| } | |
| } | |
| } | |
| // Update score display | |
| function updateScore() { | |
| scoreDisplay.textContent = `Score: ${score}`; | |
| } | |
| // Update word display | |
| function updateWordDisplay() { | |
| wordDisplay.textContent = `Valid Words: ${validWords.join(', ')} | Current: ${currentWord}`; | |
| } | |
| // Game over | |
| function gameOver() { | |
| gameRunning = false; | |
| cancelAnimationFrame(animationId); | |
| document.getElementById('finalScore').textContent = `Your score: ${score}`; | |
| gameOverScreen.style.display = 'block'; | |
| } | |
| // Helper function to get random color | |
| function getRandomColor() { | |
| const colors = [ | |
| '#F56565', '#ED8936', '#ECC94B', '#48BB78', '#38B2AC', | |
| '#4299E1', '#667EEA', '#9F7AEA', '#ED64A6', '#F687B3' | |
| ]; | |
| return colors[Math.floor(Math.random() * colors.length)]; | |
| } | |
| // Helper function to get random dark color for boxes | |
| function getRandomDarkColor() { | |
| const colors = [ | |
| '#2D3748', '#4A5568', '#1A365D', '#2C5282', '#2F855A', | |
| '#22543D', '#5F370E', '#744210', '#5F370E', '#652B19' | |
| ]; | |
| return colors[Math.floor(Math.random() * colors.length)]; | |
| } | |
| // Mouse movement | |
| canvas.addEventListener('mousemove', (e) => { | |
| mouseX = e.clientX; | |
| if (gameRunning) { | |
| platformX = Math.min( | |
| Math.max(e.clientX - platformWidth / 2, 0), | |
| canvas.width - platformWidth | |
| ); | |
| } | |
| }); | |
| // Touch movement for mobile | |
| canvas.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| mouseX = touch.clientX; | |
| if (gameRunning) { | |
| platformX = Math.min( | |
| Math.max(touch.clientX - platformWidth / 2, 0), | |
| canvas.width - platformWidth | |
| ); | |
| } | |
| }, { passive: false }); | |
| // Keyboard controls | |
| document.addEventListener('keydown', (e) => { | |
| if (!gameRunning) return; | |
| const moveAmount = 20; | |
| if (e.key === 'ArrowLeft') { | |
| platformX = Math.max(platformX - moveAmount, 0); | |
| mouseX = platformX + platformWidth / 2; | |
| } else if (e.key === 'ArrowRight') { | |
| platformX = Math.min(platformX + moveAmount, canvas.width - platformWidth); | |
| mouseX = platformX + platformWidth / 2; | |
| } | |
| }); | |
| // Window resize | |
| window.addEventListener('resize', () => { | |
| if (gameRunning) { | |
| initGame(); | |
| } | |
| }); | |
| // Start button | |
| startButton.addEventListener('click', startGame); | |
| // Restart button | |
| restartButton.addEventListener('click', startGame); | |
| </script> | |
| </body> | |
| </html> |