columns / index.html
Michaelx1987's picture
Add 1 files
ec9be92 verified
<!DOCTYPE html>
<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>