letterfall / index.html
gh0stgl1tch's picture
Update index.html
a0d5f93 verified
<!DOCTYPE html>
<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>