Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Columns of Old China</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> | |
| @keyframes fall { | |
| 0% { transform: translateY(-100%); } | |
| 100% { transform: translateY(0); } | |
| } | |
| @keyframes match { | |
| 0% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.2); opacity: 0.8; } | |
| 100% { transform: scale(0); opacity: 0; } | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| 100% { transform: scale(1); } | |
| } | |
| .falling { | |
| animation: fall 0.3s ease-out; | |
| } | |
| .matching { | |
| animation: match 0.5s ease-out forwards; | |
| } | |
| .fade-in { | |
| animation: fadeIn 1s ease-out; | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| .game-container { | |
| touch-action: manipulation; | |
| } | |
| .gem { | |
| transition: all 0.1s ease; | |
| } | |
| .controls button:active { | |
| transform: scale(0.95); | |
| } | |
| #music-toggle { | |
| transition: all 0.3s ease; | |
| } | |
| .title-screen { | |
| background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), | |
| url('https://images.unsplash.com/photo-1508804185872-d7badad00f7d?ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80'); | |
| background-size: cover; | |
| background-position: center; | |
| } | |
| .chinese-pattern { | |
| background-image: url('https://www.transparenttextures.com/patterns/chinese-pattern.png'); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4 chinese-pattern"> | |
| <!-- Audio Elements --> | |
| <audio id="bgMusic" loop> | |
| <source src="https://assets.mixkit.co/music/preview/mixkit-chinese-traditional-market-1170.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="moveSound"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-plastic-knock-1124.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="rotateSound"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-bell-notification-933.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="matchSound"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-winning-chimes-2015.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="gameOverSound"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-retro-arcade-lose-2027.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="clickSound"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-select-click-1109.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="titleMusic" loop> | |
| <source src="https://assets.mixkit.co/music/preview/mixkit-chinese-traditional-flute-1171.mp3" type="audio/mpeg"> | |
| </audio> | |
| <!-- Title Screen --> | |
| <div id="title-screen" class="absolute inset-0 flex flex-col items-center justify-center title-screen fade-in z-50"> | |
| <div class="text-center max-w-2xl px-4"> | |
| <h1 class="text-6xl md:text-8xl font-bold text-yellow-400 mb-6 font-serif tracking-wider pulse"> | |
| <span class="text-red-500">C</span> | |
| <span class="text-orange-400">o</span> | |
| <span class="text-yellow-300">l</span> | |
| <span class="text-green-400">u</span> | |
| <span class="text-blue-400">m</span> | |
| <span class="text-indigo-400">n</span> | |
| <span class="text-purple-400">s</span> | |
| <br> | |
| <span class="text-xl md:text-2xl text-white">of Old China</span> | |
| </h1> | |
| <div class="mt-16 space-y-4"> | |
| <button id="start-btn" class="bg-red-600 hover:bg-red-500 text-white px-8 py-3 rounded-lg text-xl font-bold tracking-wider transform transition hover:scale-105"> | |
| START GAME | |
| </button> | |
| <button id="how-to-play-btn" class="bg-yellow-600 hover:bg-yellow-500 text-white px-8 py-3 rounded-lg text-xl font-bold tracking-wider transform transition hover:scale-105"> | |
| HOW TO PLAY | |
| </button> | |
| <button id="credits-btn" class="bg-blue-600 hover:bg-blue-500 text-white px-8 py-3 rounded-lg text-xl font-bold tracking-wider transform transition hover:scale-105"> | |
| CREDITS | |
| </button> | |
| </div> | |
| <div class="mt-12 text-white text-opacity-70 text-sm"> | |
| <p>Match 3 or more gems to score points!</p> | |
| <p class="mt-1">Use arrow keys, space bar, or touch controls</p> | |
| </div> | |
| <!-- Difficulty Selection --> | |
| <div id="difficulty-selection" class="mt-8 hidden"> | |
| <h3 class="text-lg font-semibold mb-2">Select Difficulty:</h3> | |
| <div class="flex justify-center space-x-2"> | |
| <button data-difficulty="easy" class="bg-green-600 hover:bg-green-500 text-white px-4 py-2 rounded-lg">Easy</button> | |
| <button data-difficulty="medium" class="bg-yellow-600 hover:bg-yellow-500 text-white px-4 py-2 rounded-lg">Medium</button> | |
| <button data-difficulty="hard" class="bg-red-600 hover:bg-red-500 text-white px-4 py-2 rounded-lg">Hard</button> | |
| </div> | |
| </div> | |
| <!-- Audio permission button (hidden after first interaction) --> | |
| <button id="audio-permission" class="mt-8 bg-green-600 hover:bg-green-500 text-white px-6 py-2 rounded-lg text-lg"> | |
| <i class="fas fa-volume-up mr-2"></i> Enable Audio | |
| </button> | |
| </div> | |
| </div> | |
| <!-- How to Play Screen --> | |
| <div id="how-to-play-screen" class="absolute inset-0 bg-black bg-opacity-90 flex flex-col items-center justify-center hidden z-50 p-4"> | |
| <div class="max-w-md bg-gray-800 rounded-lg p-6"> | |
| <h2 class="text-3xl font-bold text-yellow-400 mb-6 text-center">HOW TO PLAY</h2> | |
| <div class="space-y-4"> | |
| <div class="flex items-start"> | |
| <div class="bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center mr-3 mt-1 flex-shrink-0"> | |
| <i class="fas fa-arrow-left"></i> | |
| </div> | |
| <p>Move left</p> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center mr-3 mt-1 flex-shrink-0"> | |
| <i class="fas fa-arrow-right"></i> | |
| </div> | |
| <p>Move right</p> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center mr-3 mt-1 flex-shrink-0"> | |
| <i class="fas fa-arrow-down"></i> | |
| </div> | |
| <p>Drop faster</p> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center mr-3 mt-1 flex-shrink-0"> | |
| <i class="fas fa-arrow-up"></i> | |
| </div> | |
| <p>Rotate piece (Up Arrow)</p> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center mr-3 mt-1 flex-shrink-0"> | |
| <i class="fas fa-space-shuttle"></i> | |
| </div> | |
| <p>Rotate piece (Space Bar)</p> | |
| </div> | |
| <div class="flex items-start"> | |
| <div class="bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center mr-3 mt-1 flex-shrink-0"> | |
| <i class="fas fa-mouse-pointer"></i> | |
| </div> | |
| <p>Click/Tap to rotate</p> | |
| </div> | |
| <div class="pt-4 border-t border-gray-700"> | |
| <p>Match 3 or more gems of the same color in any direction to score points.</p> | |
| <p class="mt-2">Longer chains give more points!</p> | |
| </div> | |
| </div> | |
| <button id="back-from-how-to-play" class="mt-6 w-full bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg"> | |
| BACK | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Credits Screen --> | |
| <div id="credits-screen" class="absolute inset-0 bg-black bg-opacity-90 flex flex-col items-center justify-center hidden z-50 p-4"> | |
| <div class="max-w-md bg-gray-800 rounded-lg p-6"> | |
| <h2 class="text-3xl font-bold text-yellow-400 mb-6 text-center">CREDITS</h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <h3 class="text-xl font-semibold text-white">Game Design</h3> | |
| <p>Columns of Old China</p> | |
| </div> | |
| <div> | |
| <h3 class="text-xl font-semibold text-white">Music & Sounds</h3> | |
| <p>Traditional Chinese melodies</p> | |
| <p>Mixkit Sound Effects</p> | |
| </div> | |
| <div> | |
| <h3 class="text-xl font-semibold text-white">Special Thanks</h3> | |
| <p>To all puzzle game lovers!</p> | |
| </div> | |
| </div> | |
| <button id="back-from-credits" class="mt-6 w-full bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg"> | |
| BACK | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Game Screen (hidden initially) --> | |
| <div id="game-screen" class="max-w-md w-full hidden"> | |
| <header class="flex justify-between items-center mb-4"> | |
| <h1 class="text-2xl font-bold text-yellow-400 flex items-center"> | |
| <i class="fas fa-gem mr-2"></i> Columns of Old China | |
| </h1> | |
| <div class="flex items-center space-x-4"> | |
| <button id="music-toggle" class="bg-red-600 hover:bg-red-500 text-white px-3 py-1 rounded-lg"> | |
| <i class="fas fa-music"></i> | |
| </button> | |
| <div class="bg-gray-800 px-3 py-1 rounded-lg"> | |
| <span class="text-yellow-300 font-mono text-xl" id="score">0</span> | |
| </div> | |
| <div class="bg-gray-800 px-3 py-1 rounded-lg hidden" id="high-score-container"> | |
| <span class="text-green-300 font-mono text-sm">HI: </span> | |
| <span class="text-green-300 font-mono text-sm" id="high-score">0</span> | |
| </div> | |
| <button id="pause-btn" class="bg-gray-700 hover:bg-gray-600 text-white px-3 py-1 rounded-lg"> | |
| <i class="fas fa-pause"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <div class="game-container bg-gray-800 rounded-lg overflow-hidden relative mb-4 border-2 border-yellow-700"> | |
| <canvas id="gameCanvas" class="w-full block"></canvas> | |
| <div id="game-over" class="absolute inset-0 bg-black bg-opacity-80 flex flex-col items-center justify-center hidden"> | |
| <h2 class="text-3xl font-bold text-red-500 mb-4">Game Over!</h2> | |
| <p class="text-xl mb-2">Your score: <span id="final-score" class="text-yellow-300">0</span></p> | |
| <p class="text-lg mb-6">High score: <span id="final-high-score" class="text-green-300">0</span></p> | |
| <div class="flex space-x-4"> | |
| <button id="restart-btn" class="bg-red-600 hover:bg-red-500 text-white px-6 py-2 rounded-lg text-lg"> | |
| Play Again | |
| </button> | |
| <button id="quit-btn" class="bg-gray-700 hover:bg-gray-600 text-white px-6 py-2 rounded-lg text-lg"> | |
| Quit | |
| </button> | |
| </div> | |
| </div> | |
| <div id="pause-screen" class="absolute inset-0 bg-black bg-opacity-80 flex flex-col items-center justify-center hidden"> | |
| <h2 class="text-3xl font-bold text-yellow-400 mb-4">Paused</h2> | |
| <button id="resume-btn" class="bg-red-600 hover:bg-red-500 text-white px-6 py-2 rounded-lg text-lg mb-4"> | |
| Resume | |
| </button> | |
| <button id="quit-to-menu-btn" class="bg-gray-700 hover:bg-gray-600 text-white px-6 py-2 rounded-lg text-lg"> | |
| Quit to Menu | |
| </button> | |
| </div> | |
| </div> | |
| <div class="controls flex justify-between mb-4"> | |
| <button id="left-btn" class="bg-red-600 hover:bg-red-500 text-white w-16 h-16 rounded-full flex items-center justify-center"> | |
| <i class="fas fa-arrow-left text-2xl"></i> | |
| </button> | |
| <button id="rotate-btn" class="bg-yellow-600 hover:bg-yellow-500 text-white w-16 h-16 rounded-full flex items-center justify-center"> | |
| <i class="fas fa-sync-alt text-2xl"></i> | |
| </button> | |
| <button id="down-btn" class="bg-red-600 hover:bg-red-500 text-white w-16 h-16 rounded-full flex items-center justify-center"> | |
| <i class="fas fa-arrow-down text-2xl"></i> | |
| </button> | |
| <button id="right-btn" class="bg-red-600 hover:bg-red-500 text-white w-16 h-16 rounded-full flex items-center justify-center"> | |
| <i class="fas fa-arrow-right text-2xl"></i> | |
| </button> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <div class="bg-gray-800 px-4 py-2 rounded-lg"> | |
| <span class="text-gray-400">Level:</span> | |
| <span class="text-green-400 font-mono ml-2" id="level">1</span> | |
| </div> | |
| <div class="bg-gray-800 px-4 py-2 rounded-lg"> | |
| <span class="text-gray-400">Next:</span> | |
| <div id="next-piece" class="inline-block ml-2"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Game constants | |
| const COLS = 6; | |
| const ROWS = 12; | |
| const GEM_SIZE = 40; | |
| const GEM_TYPES = 5; | |
| const COLORS = [ | |
| '#FF5252', // Red | |
| '#4CAF50', // Green | |
| '#2196F3', // Blue | |
| '#FFC107', // Yellow | |
| '#9C27B0', // Purple | |
| ]; | |
| const GEM_SYMBOLS = ['福', '禄', '寿', '喜', '财']; // Chinese prosperity symbols | |
| // Game state | |
| let canvas, ctx; | |
| let grid = []; | |
| let currentPiece = null; | |
| let nextPiece = null; | |
| let score = 0; | |
| let highScore = localStorage.getItem('columnsHighScore') || 0; | |
| let level = 1; | |
| let gameSpeed = 800; // ms per fall | |
| let gameInterval; | |
| let isGameOver = false; | |
| let isPaused = false; | |
| let touchStartX = 0; | |
| let touchStartY = 0; | |
| let musicEnabled = false; // Start with music off until user enables | |
| let lastRotateTime = 0; | |
| const rotateCooldown = 200; // ms | |
| let fallProgress = 0; // For smooth falling | |
| let lastFallTime = 0; | |
| let difficulty = 'medium'; // Default difficulty | |
| let audioContext; // For better audio handling | |
| // Audio elements | |
| const bgMusic = document.getElementById('bgMusic'); | |
| const moveSound = document.getElementById('moveSound'); | |
| const rotateSound = document.getElementById('rotateSound'); | |
| const matchSound = document.getElementById('matchSound'); | |
| const gameOverSound = document.getElementById('gameOverSound'); | |
| const clickSound = document.getElementById('clickSound'); | |
| const titleMusic = document.getElementById('titleMusic'); | |
| // UI elements | |
| const titleScreen = document.getElementById('title-screen'); | |
| const howToPlayScreen = document.getElementById('how-to-play-screen'); | |
| const creditsScreen = document.getElementById('credits-screen'); | |
| const gameScreen = document.getElementById('game-screen'); | |
| const audioPermissionBtn = document.getElementById('audio-permission'); | |
| const difficultySelection = document.getElementById('difficulty-selection'); | |
| // Initialize the game | |
| function init() { | |
| // Try to initialize audio context | |
| try { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| } catch (e) { | |
| console.log("Web Audio API not supported"); | |
| } | |
| canvas = document.getElementById('gameCanvas'); | |
| ctx = canvas.getContext('2d'); | |
| // Set canvas size based on game dimensions | |
| canvas.width = COLS * GEM_SIZE; | |
| canvas.height = ROWS * GEM_SIZE; | |
| // Initialize empty grid | |
| grid = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
| // Create first pieces | |
| currentPiece = createPiece(); | |
| nextPiece = createPiece(); | |
| updateNextPieceDisplay(); | |
| // Update high score display | |
| updateHighScoreDisplay(); | |
| // Start game loop | |
| gameInterval = setInterval(gameTick, 16); // 60fps for smooth animation | |
| // Event listeners | |
| document.getElementById('left-btn').addEventListener('click', () => movePiece(-1)); | |
| document.getElementById('right-btn').addEventListener('click', () => movePiece(1)); | |
| document.getElementById('down-btn').addEventListener('click', () => dropPiece()); | |
| document.getElementById('rotate-btn').addEventListener('click', rotatePiece); | |
| document.getElementById('pause-btn').addEventListener('click', togglePause); | |
| document.getElementById('restart-btn').addEventListener('click', restartGame); | |
| document.getElementById('resume-btn').addEventListener('click', togglePause); | |
| document.getElementById('music-toggle').addEventListener('click', toggleMusic); | |
| document.getElementById('quit-btn').addEventListener('click', returnToTitle); | |
| document.getElementById('quit-to-menu-btn').addEventListener('click', returnToTitle); | |
| // Title screen buttons | |
| document.getElementById('start-btn').addEventListener('click', showDifficultySelection); | |
| document.getElementById('how-to-play-btn').addEventListener('click', showHowToPlay); | |
| document.getElementById('credits-btn').addEventListener('click', showCredits); | |
| document.getElementById('back-from-how-to-play').addEventListener('click', hideHowToPlay); | |
| document.getElementById('back-from-credits').addEventListener('click', hideCredits); | |
| // Difficulty buttons | |
| document.querySelectorAll('[data-difficulty]').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| difficulty = btn.dataset.difficulty; | |
| startGame(); | |
| }); | |
| }); | |
| // Audio permission button | |
| audioPermissionBtn.addEventListener('click', enableAudio); | |
| // Touch controls | |
| canvas.addEventListener('touchstart', handleTouchStart, false); | |
| canvas.addEventListener('touchmove', handleTouchMove, false); | |
| // Click to rotate | |
| canvas.addEventListener('click', handleCanvasClick); | |
| // Keyboard controls | |
| document.addEventListener('keydown', handleKeyDown); | |
| // Draw initial state | |
| draw(); | |
| } | |
| // Show difficulty selection | |
| function showDifficultySelection() { | |
| playSound(clickSound); | |
| document.getElementById('start-btn').classList.add('hidden'); | |
| document.getElementById('how-to-play-btn').classList.add('hidden'); | |
| document.getElementById('credits-btn').classList.add('hidden'); | |
| difficultySelection.classList.remove('hidden'); | |
| } | |
| // Enable audio after user interaction (required for autoplay) | |
| function enableAudio() { | |
| playSound(clickSound); | |
| musicEnabled = true; | |
| audioPermissionBtn.classList.add('hidden'); | |
| // Update music toggle button | |
| const musicBtn = document.getElementById('music-toggle'); | |
| musicBtn.classList.remove('bg-gray-700'); | |
| musicBtn.classList.add('bg-red-600'); | |
| // Start title music | |
| titleMusic.volume = 0.5; | |
| titleMusic.play().catch(e => console.log("Audio play error:", e)); | |
| } | |
| // Start game from title screen | |
| function startGame() { | |
| playSound(clickSound); | |
| titleScreen.classList.add('hidden'); | |
| titleMusic.pause(); | |
| gameScreen.classList.remove('hidden'); | |
| // Set initial speed based on difficulty | |
| switch(difficulty) { | |
| case 'easy': | |
| gameSpeed = 1000; | |
| break; | |
| case 'medium': | |
| gameSpeed = 800; | |
| break; | |
| case 'hard': | |
| gameSpeed = 600; | |
| break; | |
| } | |
| if (musicEnabled) { | |
| bgMusic.currentTime = 0; | |
| bgMusic.play().catch(e => console.log("Audio play error:", e)); | |
| } | |
| // Reset game state | |
| isGameOver = false; | |
| score = 0; | |
| level = 1; | |
| fallProgress = 0; | |
| document.getElementById('score').textContent = '0'; | |
| document.getElementById('level').textContent = '1'; | |
| document.getElementById('game-over').classList.add('hidden'); | |
| // Clear grid | |
| grid = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
| // Create new pieces | |
| currentPiece = createPiece(); | |
| nextPiece = createPiece(); | |
| updateNextPieceDisplay(); | |
| // Start game loop | |
| clearInterval(gameInterval); | |
| gameInterval = setInterval(gameTick, 16); | |
| lastFallTime = Date.now(); | |
| draw(); | |
| } | |
| // Return to title screen | |
| function returnToTitle() { | |
| playSound(clickSound); | |
| gameScreen.classList.add('hidden'); | |
| document.getElementById('game-over').classList.add('hidden'); | |
| document.getElementById('pause-screen').classList.add('hidden'); | |
| bgMusic.pause(); | |
| titleScreen.classList.remove('hidden'); | |
| titleMusic.currentTime = 0; | |
| // Show buttons again | |
| document.getElementById('start-btn').classList.remove('hidden'); | |
| document.getElementById('how-to-play-btn').classList.remove('hidden'); | |
| document.getElementById('credits-btn').classList.remove('hidden'); | |
| difficultySelection.classList.add('hidden'); | |
| if (musicEnabled) { | |
| titleMusic.play().catch(e => console.log("Audio play error:", e)); | |
| } | |
| } | |
| // Show how to play screen | |
| function showHowToPlay() { | |
| playSound(clickSound); | |
| titleScreen.classList.add('hidden'); | |
| howToPlayScreen.classList.remove('hidden'); | |
| } | |
| // Hide how to play screen | |
| function hideHowToPlay() { | |
| playSound(clickSound); | |
| howToPlayScreen.classList.add('hidden'); | |
| titleScreen.classList.remove('hidden'); | |
| } | |
| // Show credits screen | |
| function showCredits() { | |
| playSound(clickSound); | |
| titleScreen.classList.add('hidden'); | |
| creditsScreen.classList.remove('hidden'); | |
| } | |
| // Hide credits screen | |
| function hideCredits() { | |
| playSound(clickSound); | |
| creditsScreen.classList.add('hidden'); | |
| titleScreen.classList.remove('hidden'); | |
| } | |
| // Create a new piece (3 gems) | |
| function createPiece() { | |
| const types = []; | |
| for (let i = 0; i < 3; i++) { | |
| types.push(Math.floor(Math.random() * GEM_TYPES) + 1); | |
| } | |
| return { | |
| x: Math.floor(COLS / 2) - 1, | |
| y: 0, | |
| types: types, | |
| rotation: 0 | |
| }; | |
| } | |
| // Game tick - handles smooth falling and game timing | |
| function gameTick() { | |
| if (isGameOver || isPaused) return; | |
| const now = Date.now(); | |
| const deltaTime = now - lastFallTime; | |
| // Check if it's time to move the piece down | |
| if (deltaTime >= gameSpeed) { | |
| lastFallTime = now; | |
| if (canMove(0, 1)) { | |
| currentPiece.y++; | |
| fallProgress = 0; | |
| } else { | |
| lockPiece(); | |
| checkMatches(); | |
| currentPiece = nextPiece; | |
| nextPiece = createPiece(); | |
| updateNextPieceDisplay(); | |
| if (!canMove(0, 0)) { | |
| gameOver(); | |
| } | |
| } | |
| } else { | |
| // Calculate falling progress for smooth animation | |
| fallProgress = deltaTime / gameSpeed; | |
| } | |
| draw(); | |
| } | |
| // Move piece left or right | |
| function movePiece(dx) { | |
| if (isGameOver || isPaused) return; | |
| if (canMove(dx, 0)) { | |
| currentPiece.x += dx; | |
| playSound(moveSound); | |
| draw(); | |
| } | |
| } | |
| // Rotate piece | |
| function rotatePiece() { | |
| if (isGameOver || isPaused) return; | |
| const now = Date.now(); | |
| if (now - lastRotateTime < rotateCooldown) return; | |
| lastRotateTime = now; | |
| const rotatedTypes = [...currentPiece.types]; | |
| rotatedTypes.unshift(rotatedTypes.pop()); | |
| // Check if rotation is possible | |
| const oldTypes = currentPiece.types; | |
| currentPiece.types = rotatedTypes; | |
| if (!canMove(0, 0)) { | |
| currentPiece.types = oldTypes; | |
| } else { | |
| playSound(rotateSound); | |
| } | |
| draw(); | |
| } | |
| // Drop piece to the bottom | |
| function dropPiece() { | |
| if (isGameOver || isPaused) return; | |
| while (canMove(0, 1)) { | |
| currentPiece.y++; | |
| } | |
| playSound(moveSound); | |
| lockPiece(); | |
| checkMatches(); | |
| currentPiece = nextPiece; | |
| nextPiece = createPiece(); | |
| updateNextPieceDisplay(); | |
| if (!canMove(0, 0)) { | |
| gameOver(); | |
| } | |
| draw(); | |
| } | |
| // Check if piece can move to (dx, dy) | |
| function canMove(dx, dy) { | |
| for (let i = 0; i < 3; i++) { | |
| const x = currentPiece.x + dx; | |
| const y = currentPiece.y + dy + i; | |
| // Check boundaries | |
| if (x < 0 || x >= COLS || y >= ROWS) { | |
| return false; | |
| } | |
| // Check if cell is occupied (only when moving down) | |
| if (y >= 0 && grid[y][x] !== 0) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| // Lock piece in place | |
| function lockPiece() { | |
| for (let i = 0; i < 3; i++) { | |
| const y = currentPiece.y + i; | |
| const x = currentPiece.x; | |
| if (y >= 0) { | |
| grid[y][x] = currentPiece.types[i]; | |
| } | |
| } | |
| } | |
| // Check for matches in all directions | |
| function checkMatches() { | |
| let matchesFound = false; | |
| let matchedCells = Array(ROWS).fill().map(() => Array(COLS).fill(false)); | |
| // Check horizontal matches | |
| for (let y = 0; y < ROWS; y++) { | |
| for (let x = 0; x < COLS - 2; x++) { | |
| if (grid[y][x] !== 0 && | |
| grid[y][x] === grid[y][x+1] && | |
| grid[y][x] === grid[y][x+2]) { | |
| // Check for longer matches | |
| let length = 3; | |
| while (x + length < COLS && grid[y][x] === grid[y][x+length]) { | |
| length++; | |
| } | |
| // Mark all matching cells | |
| for (let i = 0; i < length; i++) { | |
| matchedCells[y][x+i] = true; | |
| } | |
| matchesFound = true; | |
| } | |
| } | |
| } | |
| // Check vertical matches | |
| for (let x = 0; x < COLS; x++) { | |
| for (let y = 0; y < ROWS - 2; y++) { | |
| if (grid[y][x] !== 0 && | |
| grid[y][x] === grid[y+1][x] && | |
| grid[y][x] === grid[y+2][x]) { | |
| // Check for longer matches | |
| let length = 3; | |
| while (y + length < ROWS && grid[y][x] === grid[y+length][x]) { | |
| length++; | |
| } | |
| // Mark all matching cells | |
| for (let i = 0; i < length; i++) { | |
| matchedCells[y+i][x] = true; | |
| } | |
| matchesFound = true; | |
| } | |
| } | |
| } | |
| // Check diagonal (top-left to bottom-right) matches | |
| for (let y = 0; y < ROWS - 2; y++) { | |
| for (let x = 0; x < COLS - 2; x++) { | |
| if (grid[y][x] !== 0 && | |
| grid[y][x] === grid[y+1][x+1] && | |
| grid[y][x] === grid[y+2][x+2]) { | |
| // Check for longer matches | |
| let length = 3; | |
| while (y + length < ROWS && x + length < COLS && | |
| grid[y][x] === grid[y+length][x+length]) { | |
| length++; | |
| } | |
| // Mark all matching cells | |
| for (let i = 0; i < length; i++) { | |
| matchedCells[y+i][x+i] = true; | |
| } | |
| matchesFound = true; | |
| } | |
| } | |
| } | |
| // Check diagonal (top-right to bottom-left) matches | |
| for (let y = 0; y < ROWS - 2; y++) { | |
| for (let x = 2; x < COLS; x++) { | |
| if (grid[y][x] !== 0 && | |
| grid[y][x] === grid[y+1][x-1] && | |
| grid[y][x] === grid[y+2][x-2]) { | |
| // Check for longer matches | |
| let length = 3; | |
| while (y + length < ROWS && x - length >= 0 && | |
| grid[y][x] === grid[y+length][x-length]) { | |
| length++; | |
| } | |
| // Mark all matching cells | |
| for (let i = 0; i < length; i++) { | |
| matchedCells[y+i][x-i] = true; | |
| } | |
| matchesFound = true; | |
| } | |
| } | |
| } | |
| // If matches found, remove them and add score | |
| if (matchesFound) { | |
| // Count matches first for scoring | |
| let matchCount = 0; | |
| for (let y = 0; y < ROWS; y++) { | |
| for (let x = 0; x < COLS; x++) { | |
| if (matchedCells[y][x]) matchCount++; | |
| } | |
| } | |
| // Add score (more points for longer matches) | |
| const points = matchCount * matchCount * 10 * level; | |
| score += points; | |
| updateScore(); | |
| // Play match sound | |
| playSound(matchSound); | |
| // Remove matched gems with animation | |
| animateMatches(matchedCells); | |
| // Drop remaining gems after a delay | |
| setTimeout(() => { | |
| dropGemsAfterMatch(); | |
| checkMatches(); // Check for chain reactions | |
| }, 500); | |
| } | |
| return matchesFound; | |
| } | |
| // Animate matched gems | |
| function animateMatches(matchedCells) { | |
| for (let y = 0; y < ROWS; y++) { | |
| for (let x = 0; x < COLS; x++) { | |
| if (matchedCells[y][x]) { | |
| const gem = document.createElement('div'); | |
| gem.className = 'gem absolute rounded-full flex items-center justify-center text-white font-bold'; | |
| gem.style.width = `${GEM_SIZE - 4}px`; | |
| gem.style.height = `${GEM_SIZE - 4}px`; | |
| gem.style.left = `${x * GEM_SIZE + 2}px`; | |
| gem.style.top = `${y * GEM_SIZE + 2}px`; | |
| gem.style.backgroundColor = COLORS[grid[y][x] - 1]; | |
| gem.textContent = GEM_SYMBOLS[grid[y][x] - 1]; | |
| gem.style.zIndex = '10'; | |
| gem.classList.add('matching'); | |
| canvas.parentNode.appendChild(gem); | |
| // Remove after animation | |
| setTimeout(() => { | |
| gem.remove(); | |
| }, 500); | |
| } | |
| } | |
| } | |
| // Clear matched cells from grid | |
| for (let y = 0; y < ROWS; y++) { | |
| for (let x = 0; x < COLS; x++) { | |
| if (matchedCells[y][x]) { | |
| grid[y][x] = 0; | |
| } | |
| } | |
| } | |
| } | |
| // Drop gems after matches are cleared | |
| function dropGemsAfterMatch() { | |
| for (let x = 0; x < COLS; x++) { | |
| let emptyRow = ROWS - 1; | |
| for (let y = ROWS - 1; y >= 0; y--) { | |
| if (grid[y][x] !== 0) { | |
| if (y !== emptyRow) { | |
| grid[emptyRow][x] = grid[y][x]; | |
| grid[y][x] = 0; | |
| } | |
| emptyRow--; | |
| } | |
| } | |
| } | |
| draw(); | |
| } | |
| // Update score display | |
| function updateScore() { | |
| // Format score with commas | |
| document.getElementById('score').textContent = score.toLocaleString(); | |
| // Update high score if needed | |
| if (score > highScore) { | |
| highScore = score; | |
| localStorage.setItem('columnsHighScore', highScore); | |
| updateHighScoreDisplay(); | |
| } | |
| // Level up every 5000 points | |
| const newLevel = Math.floor(score / 5000) + 1; | |
| if (newLevel > level) { | |
| level = newLevel; | |
| document.getElementById('level').textContent = level; | |
| // Increase game speed (but not too fast) | |
| switch(difficulty) { | |
| case 'easy': | |
| gameSpeed = Math.max(400, 1000 - (level - 1) * 100); | |
| break; | |
| case 'medium': | |
| gameSpeed = Math.max(300, 800 - (level - 1) * 100); | |
| break; | |
| case 'hard': | |
| gameSpeed = Math.max(200, 600 - (level - 1) * 100); | |
| break; | |
| } | |
| // Show level up message | |
| showMessage(`Level ${level}!`, 'text-yellow-400'); | |
| } | |
| } | |
| // Update high score display | |
| function updateHighScoreDisplay() { | |
| if (highScore > 0) { | |
| document.getElementById('high-score-container').classList.remove('hidden'); | |
| document.getElementById('high-score').textContent = highScore.toLocaleString(); | |
| document.getElementById('final-high-score').textContent = highScore.toLocaleString(); | |
| } | |
| } | |
| // Show temporary message | |
| function showMessage(text, colorClass) { | |
| const msg = document.createElement('div'); | |
| msg.className = `absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-2xl font-bold ${colorClass} opacity-0 transition-opacity duration-300`; | |
| msg.textContent = text; | |
| canvas.parentNode.appendChild(msg); | |
| setTimeout(() => { | |
| msg.classList.remove('opacity-0'); | |
| msg.classList.add('opacity-100'); | |
| }, 10); | |
| setTimeout(() => { | |
| msg.classList.remove('opacity-100'); | |
| msg.classList.add('opacity-0'); | |
| setTimeout(() => msg.remove(), 300); | |
| }, 1000); | |
| } | |
| // Update next piece display | |
| function updateNextPieceDisplay() { | |
| const container = document.getElementById('next-piece'); | |
| container.innerHTML = ''; | |
| for (let i = 0; i < 3; i++) { | |
| const gem = document.createElement('div'); | |
| gem.className = 'gem inline-block rounded-full flex items-center justify-center text-white font-bold mx-1'; | |
| gem.style.width = '24px'; | |
| gem.style.height = '24px'; | |
| gem.style.backgroundColor = COLORS[nextPiece.types[i] - 1]; | |
| gem.textContent = GEM_SYMBOLS[nextPiece.types[i] - 1]; | |
| container.appendChild(gem); | |
| } | |
| } | |
| // Draw the game state with smooth falling | |
| function draw() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw grid background | |
| ctx.fillStyle = '#1F2937'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw grid lines | |
| ctx.strokeStyle = '#374151'; | |
| ctx.lineWidth = 1; | |
| for (let x = 0; x <= COLS; x++) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x * GEM_SIZE, 0); | |
| ctx.lineTo(x * GEM_SIZE, ROWS * GEM_SIZE); | |
| ctx.stroke(); | |
| } | |
| for (let y = 0; y <= ROWS; y++) { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, y * GEM_SIZE); | |
| ctx.lineTo(COLS * GEM_SIZE, y * GEM_SIZE); | |
| ctx.stroke(); | |
| } | |
| // Draw locked gems | |
| for (let y = 0; y < ROWS; y++) { | |
| for (let x = 0; x < COLS; x++) { | |
| if (grid[y][x] !== 0) { | |
| drawGem(x, y, grid[y][x]); | |
| } | |
| } | |
| } | |
| // Draw current piece with smooth falling | |
| if (currentPiece) { | |
| for (let i = 0; i < 3; i++) { | |
| const y = currentPiece.y + i + fallProgress; | |
| if (y >= 0) { | |
| drawGem(currentPiece.x, y, currentPiece.types[i]); | |
| } | |
| } | |
| } | |
| } | |
| // Draw a single gem | |
| function drawGem(x, y, type) { | |
| const size = GEM_SIZE - 4; | |
| const centerX = x * GEM_SIZE + GEM_SIZE / 2; | |
| const centerY = Math.floor(y) * GEM_SIZE + GEM_SIZE / 2 + (y % 1) * GEM_SIZE; | |
| // Gem base | |
| ctx.fillStyle = COLORS[type - 1]; | |
| ctx.beginPath(); | |
| ctx.arc(centerX, centerY, size / 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Gem highlight | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; | |
| ctx.beginPath(); | |
| ctx.arc(centerX - size/6, centerY - size/6, size/4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Gem symbol | |
| ctx.fillStyle = 'white'; | |
| ctx.font = `bold ${size/2}px Arial`; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(GEM_SYMBOLS[type - 1], centerX, centerY); | |
| } | |
| // Game over | |
| function gameOver() { | |
| isGameOver = true; | |
| clearInterval(gameInterval); | |
| document.getElementById('final-score').textContent = score.toLocaleString(); | |
| document.getElementById('game-over').classList.remove('hidden'); | |
| playSound(gameOverSound); | |
| } | |
| // Restart game | |
| function restartGame() { | |
| playSound(clickSound); | |
| isGameOver = false; | |
| score = 0; | |
| level = 1; | |
| // Reset speed based on difficulty | |
| switch(difficulty) { | |
| case 'easy': | |
| gameSpeed = 1000; | |
| break; | |
| case 'medium': | |
| gameSpeed = 800; | |
| break; | |
| case 'hard': | |
| gameSpeed = 600; | |
| break; | |
| } | |
| fallProgress = 0; | |
| document.getElementById('score').textContent = '0'; | |
| document.getElementById('level').textContent = '1'; | |
| document.getElementById('game-over').classList.add('hidden'); | |
| // Clear grid | |
| grid = Array(ROWS).fill().map(() => Array(COLS).fill(0)); | |
| // Create new pieces | |
| currentPiece = createPiece(); | |
| nextPiece = createPiece(); | |
| updateNextPieceDisplay(); | |
| // Start game loop | |
| clearInterval(gameInterval); | |
| gameInterval = setInterval(gameTick, 16); | |
| lastFallTime = Date.now(); | |
| draw(); | |
| } | |
| // Toggle pause | |
| function togglePause() { | |
| playSound(clickSound); | |
| isPaused = !isPaused; | |
| if (isPaused) { | |
| document.getElementById('pause-screen').classList.remove('hidden'); | |
| if (musicEnabled) bgMusic.pause(); | |
| } else { | |
| document.getElementById('pause-screen').classList.add('hidden'); | |
| lastFallTime = Date.now(); // Reset timer to avoid instant drop | |
| if (musicEnabled) bgMusic.play(); | |
| } | |
| } | |
| // Toggle music | |
| function toggleMusic() { | |
| playSound(clickSound); | |
| musicEnabled = !musicEnabled; | |
| const musicBtn = document.getElementById('music-toggle'); | |
| if (musicEnabled) { | |
| musicBtn.classList.remove('bg-gray-700'); | |
| musicBtn.classList.add('bg-red-600'); | |
| bgMusic.play().catch(e => console.log("Audio play error:", e)); | |
| } else { | |
| musicBtn.classList.remove('bg-red-600'); | |
| musicBtn.classList.add('bg-gray-700'); | |
| bgMusic.pause(); | |
| } | |
| } | |
| // Play sound effect | |
| function playSound(sound) { | |
| if (!musicEnabled) return; | |
| // Try to use Web Audio API if available for better reliability | |
| if (audioContext) { | |
| const source = audioContext.createBufferSource(); | |
| fetch(sound.src) | |
| .then(response => response.arrayBuffer()) | |
| .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer)) | |
| .then(audioBuffer => { | |
| source.buffer = audioBuffer; | |
| source.connect(audioContext.destination); | |
| source.start(0); | |
| }) | |
| .catch(e => { | |
| console.log("Web Audio API error:", e); | |
| // Fallback to HTML5 Audio | |
| sound.currentTime = 0; | |
| sound.play().catch(e => console.log("Sound play error:", e)); | |
| }); | |
| } else { | |
| // Fallback to HTML5 Audio | |
| sound.currentTime = 0; | |
| sound.play().catch(e => console.log("Sound play error:", e)); | |
| } | |
| } | |
| // Handle canvas click (for rotation) | |
| function handleCanvasClick(e) { | |
| if (isGameOver || isPaused) return; | |
| // Only rotate if click is on the game area (not UI elements) | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| if (x >= 0 && x <= canvas.width && y >= 0 && y <= canvas.height) { | |
| rotatePiece(); | |
| } | |
| } | |
| // Handle keyboard input | |
| function handleKeyDown(e) { | |
| if (isGameOver) return; | |
| switch (e.key) { | |
| case 'ArrowLeft': | |
| movePiece(-1); | |
| e.preventDefault(); | |
| break; | |
| case 'ArrowRight': | |
| movePiece(1); | |
| e.preventDefault(); | |
| break; | |
| case 'ArrowDown': | |
| dropPiece(); | |
| e.preventDefault(); | |
| break; | |
| case 'ArrowUp': | |
| rotatePiece(); | |
| e.preventDefault(); | |
| break; | |
| case ' ': | |
| rotatePiece(); | |
| e.preventDefault(); | |
| break; | |
| case 'p': | |
| case 'P': | |
| togglePause(); | |
| e.preventDefault(); | |
| break; | |
| case 'm': | |
| case 'M': | |
| toggleMusic(); | |
| e.preventDefault(); | |
| break; | |
| case 'Escape': | |
| if (!titleScreen.classList.contains('hidden')) break; | |
| if (howToPlayScreen.classList.contains('hidden') && | |
| creditsScreen.classList.contains('hidden')) { | |
| togglePause(); | |
| } | |
| e.preventDefault(); | |
| break; | |
| } | |
| } | |
| // Handle touch start for swipe controls | |
| function handleTouchStart(e) { | |
| touchStartX = e.touches[0].clientX; | |
| touchStartY = e.touches[0].clientY; | |
| } | |
| // Handle touch move for swipe controls | |
| function handleTouchMove(e) { | |
| if (!touchStartX || !touchStartY || isGameOver || isPaused) return; | |
| const touchEndX = e.touches[0].clientX; | |
| const touchEndY = e.touches[0].clientY; | |
| const dx = touchEndX - touchStartX; | |
| const dy = touchEndY - touchStartY; | |
| // Determine if it's a horizontal or vertical swipe | |
| if (Math.abs(dx) > Math.abs(dy)) { | |
| // Horizontal swipe | |
| if (dx > 50) { | |
| movePiece(1); | |
| touchStartX = 0; | |
| touchStartY = 0; | |
| } else if (dx < -50) { | |
| movePiece(-1); | |
| touchStartX = 0; | |
| touchStartY = 0; | |
| } | |
| } else { | |
| // Vertical swipe | |
| if (dy > 50) { | |
| dropPiece(); | |
| touchStartX = 0; | |
| touchStartY = 0; | |
| } else if (dy < -50) { | |
| rotatePiece(); | |
| touchStartX = 0; | |
| touchStartY = 0; | |
| } | |
| } | |
| e.preventDefault(); | |
| } | |
| // Start the game | |
| 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=Michaelx1987/columns" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |