blockshift-chronicles / index.html
Batfly's picture
BlockShift Chronicles – A Futuristic Shape-Stacking Puzzle Game Objective: Create a fully responsive HTML5, CSS, and JavaScript puzzle game inspired by Tetris but featuring unique gameplay mechanics and original visuals to ensure no copyright infringement. The game must work seamlessly across desktop, tablet, and mobile browsers with smooth controls and animations. Core Concept Instead of standard falling blocks, players control energy shards that shift and rotate through gravitational zones. The board is hexagonal or circular, unlike the standard rectangular Tetris grid. Players must align matching colors or energy symbols rather than simply filling lines. Matching 3 or more similar shards in connected clusters causes them to detonate in spectacular energy bursts, scoring points and freeing space. Combo chains reward higher multipliers. Difficulty increases by introducing rotating obstacles and gravity reversals after certain levels. Unique Features Gravity Zones: Blocks can fall in multiple directions depending on the zone’s gravitational pull. Color Fusion Mechanic: Merging different color shards creates special power shards with unique effects (e.g., clear nearby blocks, slow time, reverse gravity). Dynamic Board Shapes: Levels shift between hex grids, circular arenas, and spiraling vortex shapes. Combo Scoring: Chain reactions reward skillful planning. Power-Ups: Temporary shields, double-score boosts, and “magnet mode” to attract pieces to certain areas. Art Style & Theme Futuristic neon aesthetic with glowing shards, pulsating energy lines, and smooth particle effects. Background: animated galaxy or energy core that subtly shifts color as levels progress. Minimalistic UI with holographic panels for score, level, and power-up indicators. Technical Requirements Technologies: Pure HTML5 Canvas, CSS3 for responsive layout, vanilla JavaScript (no external game engines). Responsiveness: Auto-scale canvas to fit any screen resolution (mobile, tablet, desktop). Controls: Desktop: Arrow keys (move), Space (rotate), Down (quick drop). Mobile: On-screen swipe controls and tap-to-rotate. Animations: Smooth transitions (60fps), particle effects for explosions, glowing color transitions for combos. Audio: Original retro-futuristic soundtrack + satisfying block placement and explosion sounds. Scoring System: Points for matches, combo multipliers, and time bonuses. Additional Features Level progression with escalating difficulty (speed, new shard types, rotating hazards). Local high score leaderboard stored in browser (IndexedDB/localStorage). Pause, resume, and restart buttons with smooth menu transitions. Optional colorblind mode (patterns instead of colors). Offline play support via service worker (PWA-ready). Final Deliverable A single-page game with clean, modular code separated into index.html, style.css, and game.js. Fully documented code with clear comments for future updates. All assets (shards, effects) must be original vector shapes (no copyrighted textures). Game should feel fast, modern, and addictive, appealing to both casual and hardcore puzzle fans. - Initial Deployment
15bdce5 verified
Raw
History Blame Contribute Delete
53.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BlockShift Chronicles</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes pulse {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.glow {
filter: drop-shadow(0 0 8px currentColor);
}
.particle {
position: absolute;
border-radius: 50%;
pointer-events: none;
animation: fadeOut 1s forwards;
}
@keyframes fadeOut {
to { opacity: 0; transform: translate(var(--tx), var(--ty)) scale(0.5); }
}
.hexagon {
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
.swipe-area {
touch-action: none;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>
</head>
<body class="bg-black text-white font-mono overflow-hidden select-none">
<div id="game-container" class="relative w-full h-screen flex flex-col items-center justify-center">
<!-- Main Menu -->
<div id="main-menu" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-50 transition-opacity duration-500">
<h1 class="text-6xl md:text-8xl font-bold mb-8 text-transparent bg-clip-text bg-gradient-to-r from-purple-500 to-blue-500 glow">
BlockShift Chronicles
</h1>
<div class="flex flex-col space-y-4 w-64">
<button id="start-game" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105">
New Game
</button>
<button id="continue-game" class="px-6 py-3 bg-purple-600 hover:bg-purple-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105 opacity-50" disabled>
Continue
</button>
<button id="high-scores" class="px-6 py-3 bg-green-600 hover:bg-green-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105">
High Scores
</button>
<button id="settings" class="px-6 py-3 bg-yellow-600 hover:bg-yellow-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105">
Settings
</button>
</div>
<div class="mt-12 text-gray-400 text-sm">
<p>Use arrow keys or swipe to move</p>
<p>Space/Click to rotate</p>
<p>Down arrow/Swipe down to drop</p>
</div>
</div>
<!-- High Scores -->
<div id="high-scores-menu" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-50 hidden">
<h2 class="text-4xl mb-8 text-blue-400 glow">High Scores</h2>
<div id="scores-list" class="bg-gray-900 bg-opacity-70 rounded-lg p-4 w-64 max-h-96 overflow-y-auto">
<!-- Scores will be populated here -->
</div>
<button id="back-to-menu" class="mt-8 px-6 py-3 bg-red-600 hover:bg-red-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105">
Back
</button>
</div>
<!-- Settings -->
<div id="settings-menu" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-50 hidden">
<h2 class="text-4xl mb-8 text-yellow-400 glow">Settings</h2>
<div class="bg-gray-900 bg-opacity-70 rounded-lg p-6 w-80 max-w-full">
<div class="mb-6">
<label class="block text-lg mb-2">Controls</label>
<select id="control-type" class="w-full bg-gray-800 text-white p-2 rounded">
<option value="keyboard">Keyboard</option>
<option value="touch">Touch Controls</option>
</select>
</div>
<div class="mb-6">
<label class="block text-lg mb-2">Difficulty</label>
<select id="difficulty" class="w-full bg-gray-800 text-white p-2 rounded">
<option value="easy">Easy</option>
<option value="medium" selected>Medium</option>
<option value="hard">Hard</option>
</select>
</div>
<div class="mb-6">
<label class="flex items-center space-x-3">
<input type="checkbox" id="colorblind-mode" class="form-checkbox h-5 w-5 text-blue-600">
<span class="text-lg">Colorblind Mode</span>
</label>
</div>
<div class="mb-6">
<label class="block text-lg mb-2">Sound Volume</label>
<input type="range" id="volume" min="0" max="100" value="70" class="w-full">
</div>
</div>
<button id="save-settings" class="mt-8 px-6 py-3 bg-green-600 hover:bg-green-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105">
Save Settings
</button>
</div>
<!-- Game UI -->
<div id="game-ui" class="absolute inset-0 hidden">
<!-- Score and Level Display -->
<div class="absolute top-4 left-4 bg-gray-900 bg-opacity-70 rounded-lg p-3">
<div class="text-lg">Score: <span id="score" class="font-bold">0</span></div>
<div class="text-lg">Level: <span id="level" class="font-bold">1</span></div>
<div class="text-lg">Lines: <span id="lines" class="font-bold">0</span></div>
</div>
<!-- Next Piece Preview -->
<div class="absolute top-4 right-4 bg-gray-900 bg-opacity-70 rounded-lg p-3 flex flex-col items-center">
<div class="text-lg mb-2">Next:</div>
<canvas id="next-piece" width="100" height="100" class="bg-gray-800 rounded"></canvas>
</div>
<!-- Power-ups -->
<div class="absolute bottom-4 left-4 bg-gray-900 bg-opacity-70 rounded-lg p-3">
<div class="text-lg mb-2">Power-ups:</div>
<div id="power-ups" class="flex space-x-2">
<!-- Power-up icons will appear here -->
</div>
</div>
<!-- Game Controls -->
<div id="mobile-controls" class="absolute bottom-4 right-4 hidden">
<div class="grid grid-cols-3 gap-2">
<button id="rotate-btn" class="bg-blue-600 hover:bg-blue-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold">
</button>
<button id="up-btn" class="bg-purple-600 hover:bg-purple-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold col-start-2">
</button>
<button id="left-btn" class="bg-green-600 hover:bg-green-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold row-start-2">
</button>
<button id="down-btn" class="bg-yellow-600 hover:bg-yellow-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold row-start-2 col-start-2">
</button>
<button id="right-btn" class="bg-red-600 hover:bg-red-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold row-start-2 col-start-3">
</button>
</div>
</div>
<!-- Pause Button -->
<button id="pause-btn" class="absolute top-4 right-1/2 transform translate-x-1/2 bg-gray-900 bg-opacity-70 hover:bg-opacity-90 rounded-lg px-4 py-2 text-lg font-bold">
Pause
</button>
<!-- Game Canvas -->
<canvas id="game-canvas" class="border border-gray-800 rounded-lg"></canvas>
<!-- Game Over Screen -->
<div id="game-over" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-40 hidden">
<h2 class="text-5xl mb-6 text-red-500 glow">Game Over!</h2>
<div class="text-2xl mb-8">Final Score: <span id="final-score" class="font-bold">0</span></div>
<div class="flex space-x-4">
<button id="restart-btn" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105">
Play Again
</button>
<button id="menu-btn" class="px-6 py-3 bg-purple-600 hover:bg-purple-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105">
Main Menu
</button>
</div>
</div>
<!-- Pause Screen -->
<div id="pause-screen" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-40 hidden">
<h2 class="text-5xl mb-6 text-yellow-400 glow">Paused</h2>
<div class="flex space-x-4">
<button id="resume-btn" class="px-6 py-3 bg-green-600 hover:bg-green-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105">
Resume
</button>
<button id="quit-btn" class="px-6 py-3 bg-red-600 hover:bg-red-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105">
Quit
</button>
</div>
</div>
</div>
<!-- Background Elements -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div id="particles-container" class="absolute inset-0"></div>
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-black opacity-30"></div>
</div>
</div>
<script>
// Game Constants
const COLORS = [
'#FF5252', // Red
'#4CAF50', // Green
'#2196F3', // Blue
'#FFC107', // Yellow
'#9C27B0', // Purple
'#00BCD4', // Cyan
'#FF9800', // Orange
'#E91E63' // Pink
];
const SHAPES = [
[[1, 1, 1, 1]], // I
[[1, 1], [1, 1]], // O
[[1, 1, 1], [0, 1, 0]], // T
[[1, 1, 1], [1, 0, 0]], // L
[[1, 1, 1], [0, 0, 1]], // J
[[0, 1, 1], [1, 1, 0]], // S
[[1, 1, 0], [0, 1, 1]] // Z
];
const SPECIAL_SHAPES = [
[[1, 1, 1, 1, 1]], // Long I
[[1, 1, 1], [1, 1, 1], [1, 1, 1]], // Big O
[[1, 0, 1], [1, 1, 1], [1, 0, 1]], // Plus
[[1, 1, 1, 1], [0, 0, 1, 0], [0, 0, 1, 0]], // L with tail
[[1, 1, 1, 1], [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0]] // Big L
];
const POWER_UPS = {
bomb: { name: 'Bomb', color: '#FF5252', effect: 'Clear a 3x3 area' },
gravity: { name: 'Gravity Shift', color: '#2196F3', effect: 'Reverse gravity direction' },
slow: { name: 'Time Warp', color: '#9C27B0', effect: 'Slow down pieces for 10 seconds' },
clear: { name: 'Line Clear', color: '#4CAF50', effect: 'Clear one random line' },
score: { name: 'Multiplier', color: '#FFC107', effect: '2x score for 15 seconds' }
};
// Game Variables
let canvas, ctx;
let nextCanvas, nextCtx;
let grid = [];
let currentPiece, nextPiece;
let score = 0;
let level = 1;
let lines = 0;
let gameSpeed = 1000;
let gameInterval;
let isGameOver = false;
let isPaused = false;
let gravityDirection = 'down'; // Can be 'down', 'left', 'right', 'up'
let colorblindMode = false;
let volume = 0.7;
let activePowerUps = [];
let lastUpdateTime = 0;
let touchStartX = 0;
let touchStartY = 0;
let touchEndX = 0;
let touchEndY = 0;
let boardWidth = 10;
let boardHeight = 20;
let cellSize = 30;
let boardOffsetX = 0;
let boardOffsetY = 0;
let gameStarted = false;
let highScores = [];
let settings = {
controls: 'keyboard',
difficulty: 'medium',
colorblind: false,
volume: 70
};
// Initialize the game
function init() {
// Load saved data
loadHighScores();
loadSettings();
// Set up event listeners
document.getElementById('start-game').addEventListener('click', startGame);
document.getElementById('high-scores').addEventListener('click', showHighScores);
document.getElementById('settings').addEventListener('click', showSettings);
document.getElementById('back-to-menu').addEventListener('click', showMainMenu);
document.getElementById('save-settings').addEventListener('click', saveSettings);
document.getElementById('restart-btn').addEventListener('click', startGame);
document.getElementById('menu-btn').addEventListener('click', showMainMenu);
document.getElementById('resume-btn').addEventListener('click', resumeGame);
document.getElementById('quit-btn').addEventListener('click', quitGame);
document.getElementById('pause-btn').addEventListener('click', togglePause);
// Mobile controls
document.getElementById('rotate-btn').addEventListener('click', () => rotatePiece());
document.getElementById('up-btn').addEventListener('click', () => movePiece(0, -1));
document.getElementById('down-btn').addEventListener('click', () => movePiece(0, 1));
document.getElementById('left-btn').addEventListener('click', () => movePiece(-1, 0));
document.getElementById('right-btn').addEventListener('click', () => movePiece(1, 0));
// Touch controls
document.addEventListener('touchstart', handleTouchStart, false);
document.addEventListener('touchmove', handleTouchMove, false);
document.addEventListener('touchend', handleTouchEnd, false);
// Keyboard controls
document.addEventListener('keydown', handleKeyDown);
// Set up canvas
setupCanvas();
// Create background particles
createBackgroundParticles();
// Update UI based on settings
updateUIFromSettings();
}
// Set up the game canvas
function setupCanvas() {
canvas = document.getElementById('game-canvas');
ctx = canvas.getContext('2d');
nextCanvas = document.getElementById('next-piece');
nextCtx = nextCanvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
}
// Resize canvas to fit the screen
function resizeCanvas() {
const container = document.getElementById('game-container');
const width = Math.min(container.clientWidth - 40, 500);
const height = Math.min(container.clientHeight - 40, 800);
// Calculate cell size based on board dimensions
cellSize = Math.min(
Math.floor(width / boardWidth),
Math.floor(height / boardHeight)
);
// Adjust board dimensions if needed
boardWidth = Math.floor(width / cellSize);
boardHeight = Math.floor(height / cellSize);
// Set canvas dimensions
canvas.width = boardWidth * cellSize;
canvas.height = boardHeight * cellSize;
// Calculate offset to center the board
boardOffsetX = (container.clientWidth - canvas.width) / 2;
boardOffsetY = (container.clientHeight - canvas.height) / 2;
canvas.style.width = `${canvas.width}px`;
canvas.style.height = `${canvas.height}px`;
canvas.style.position = 'absolute';
canvas.style.left = `${boardOffsetX}px`;
canvas.style.top = `${boardOffsetY}px`;
// Redraw if game is in progress
if (gameStarted && !isGameOver && !isPaused) {
draw();
}
}
// Start a new game
function startGame() {
// Hide menus
document.getElementById('main-menu').classList.add('hidden');
document.getElementById('high-scores-menu').classList.add('hidden');
document.getElementById('settings-menu').classList.add('hidden');
document.getElementById('game-over').classList.add('hidden');
// Show game UI
document.getElementById('game-ui').classList.remove('hidden');
// Reset game state
resetGame();
// Initialize grid
initGrid();
// Create first pieces
currentPiece = createPiece();
nextPiece = createPiece();
// Start game loop
gameStarted = true;
isGameOver = false;
lastUpdateTime = Date.now();
gameInterval = setInterval(update, gameSpeed);
// Draw initial state
draw();
// Show mobile controls if touch device
if (settings.controls === 'touch') {
document.getElementById('mobile-controls').classList.remove('hidden');
} else {
document.getElementById('mobile-controls').classList.add('hidden');
}
}
// Reset game variables
function resetGame() {
score = 0;
level = 1;
lines = 0;
gameSpeed = 1000;
gravityDirection = 'down';
activePowerUps = [];
// Update UI
document.getElementById('score').textContent = score;
document.getElementById('level').textContent = level;
document.getElementById('lines').textContent = lines;
updatePowerUpsDisplay();
}
// Initialize the game grid
function initGrid() {
grid = [];
for (let y = 0; y < boardHeight; y++) {
grid[y] = [];
for (let x = 0; x < boardWidth; x++) {
grid[y][x] = 0;
}
}
}
// Create a new piece
function createPiece() {
// Random shape and color
const shapeIndex = Math.floor(Math.random() * SHAPES.length);
const colorIndex = Math.floor(Math.random() * COLORS.length);
// 5% chance for a special shape
const isSpecial = Math.random() < 0.05;
const shape = isSpecial ?
SPECIAL_SHAPES[Math.floor(Math.random() * SPECIAL_SHAPES.length)] :
SHAPES[shapeIndex];
// Starting position (centered at top)
const x = Math.floor(boardWidth / 2) - Math.floor(shape[0].length / 2);
const y = 0;
return {
shape: shape,
color: COLORS[colorIndex],
x: x,
y: y,
rotation: 0
};
}
// Game update loop
function update() {
if (isGameOver || isPaused) return;
// Move piece down (or in current gravity direction)
if (gravityDirection === 'down') {
if (!movePiece(0, 1)) {
// Piece couldn't move down, lock it in place
lockPiece();
// Check for completed lines
const linesCleared = checkLines();
if (linesCleared > 0) {
updateScore(linesCleared);
// Check for power-up chance (20% per line cleared)
if (Math.random() < 0.2 * linesCleared) {
addRandomPowerUp();
}
}
// Create new piece
currentPiece = nextPiece;
nextPiece = createPiece();
// Check if game over (new piece collides immediately)
if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) {
gameOver();
}
}
} else if (gravityDirection === 'left') {
movePiece(-1, 0);
} else if (gravityDirection === 'right') {
movePiece(1, 0);
} else if (gravityDirection === 'up') {
movePiece(0, -1);
}
// Update power-ups
updatePowerUps();
// Draw updated game state
draw();
}
// Move the current piece
function movePiece(dx, dy) {
const newX = currentPiece.x + dx;
const newY = currentPiece.y + dy;
if (!checkCollision(newX, newY, currentPiece.shape)) {
currentPiece.x = newX;
currentPiece.y = newY;
return true;
}
return false;
}
// Rotate the current piece
function rotatePiece() {
if (isGameOver || isPaused) return;
const rotated = [];
for (let i = 0; i < currentPiece.shape[0].length; i++) {
rotated[i] = [];
for (let j = currentPiece.shape.length - 1; j >= 0; j--) {
rotated[i][currentPiece.shape.length - 1 - j] = currentPiece.shape[j][i];
}
}
if (!checkCollision(currentPiece.x, currentPiece.y, rotated)) {
currentPiece.shape = rotated;
currentPiece.rotation = (currentPiece.rotation + 90) % 360;
draw();
}
}
// Check for collisions
function checkCollision(x, y, shape) {
for (let row = 0; row < shape.length; row++) {
for (let col = 0; col < shape[row].length; col++) {
if (shape[row][col] !== 0) {
const newX = x + col;
const newY = y + row;
// Check boundaries
if (newX < 0 || newX >= boardWidth || newY >= boardHeight) {
return true;
}
// Don't check above the board
if (newY < 0) {
continue;
}
// Check for existing blocks
if (grid[newY] && grid[newY][newX] !== 0) {
return true;
}
}
}
}
return false;
}
// Lock the current piece in place
function lockPiece() {
for (let row = 0; row < currentPiece.shape.length; row++) {
for (let col = 0; col < currentPiece.shape[row].length; col++) {
if (currentPiece.shape[row][col] !== 0) {
const y = currentPiece.y + row;
const x = currentPiece.x + col;
// Only lock if within bounds
if (y >= 0 && x >= 0 && x < boardWidth && y < boardHeight) {
grid[y][x] = currentPiece.color;
}
}
}
}
// Create explosion effect
createExplosion(currentPiece.x, currentPiece.y, currentPiece.color);
}
// Check for completed lines
function checkLines() {
let linesCleared = 0;
for (let y = boardHeight - 1; y >= 0; y--) {
let lineComplete = true;
for (let x = 0; x < boardWidth; x++) {
if (grid[y][x] === 0) {
lineComplete = false;
break;
}
}
if (lineComplete) {
// Remove the line
for (let yy = y; yy > 0; yy--) {
grid[yy] = [...grid[yy - 1]];
}
// Add empty line at top
grid[0] = Array(boardWidth).fill(0);
linesCleared++;
y++; // Check the same row again (now with the line above)
}
}
if (linesCleared > 0) {
lines += linesCleared;
// Check for level up (every 10 lines)
const newLevel = Math.floor(lines / 10) + 1;
if (newLevel > level) {
level = newLevel;
gameSpeed = Math.max(100, 1000 - (level - 1) * 100);
// Change gravity direction every 3 levels
if (level % 3 === 0) {
const directions = ['down', 'left', 'right', 'up'];
gravityDirection = directions[(level / 3 - 1) % directions.length];
}
clearInterval(gameInterval);
gameInterval = setInterval(update, gameSpeed);
}
}
return linesCleared;
}
// Update the score
function updateScore(linesCleared) {
const points = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines
let multiplier = 1;
// Check for score multiplier power-up
const scoreBoost = activePowerUps.find(p => p.type === 'score');
if (scoreBoost) {
multiplier = 2;
}
score += points[Math.min(linesCleared, 4)] * level * multiplier;
// Update UI
document.getElementById('score').textContent = score;
document.getElementById('level').textContent = level;
document.getElementById('lines').textContent = lines;
}
// Game over
function gameOver() {
isGameOver = true;
clearInterval(gameInterval);
// Show game over screen
document.getElementById('game-over').classList.remove('hidden');
document.getElementById('final-score').textContent = score;
// Add to high scores if score is high enough
addHighScore(score);
}
// Draw the game state
function draw() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw grid background
ctx.fillStyle = 'rgba(30, 30, 30, 0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw grid lines
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 0.5;
for (let x = 0; x <= boardWidth; x++) {
ctx.beginPath();
ctx.moveTo(x * cellSize, 0);
ctx.lineTo(x * cellSize, boardHeight * cellSize);
ctx.stroke();
}
for (let y = 0; y <= boardHeight; y++) {
ctx.beginPath();
ctx.moveTo(0, y * cellSize);
ctx.lineTo(boardWidth * cellSize, y * cellSize);
ctx.stroke();
}
// Draw locked pieces
for (let y = 0; y < boardHeight; y++) {
for (let x = 0; x < boardWidth; x++) {
if (grid[y][x] !== 0) {
drawBlock(x, y, grid[y][x]);
}
}
}
// Draw current piece
if (currentPiece) {
for (let row = 0; row < currentPiece.shape.length; row++) {
for (let col = 0; col < currentPiece.shape[row].length; col++) {
if (currentPiece.shape[row][col] !== 0) {
const x = currentPiece.x + col;
const y = currentPiece.y + row;
// Only draw if within visible area
if (y >= 0) {
drawBlock(x, y, currentPiece.color, true);
}
}
}
}
}
// Draw next piece preview
drawNextPiece();
}
// Draw a single block
function drawBlock(x, y, color, isCurrent = false) {
const padding = isCurrent ? 0 : 2;
const blockX = x * cellSize + padding;
const blockY = y * cellSize + padding;
const blockSize = cellSize - padding * 2;
// Draw block
ctx.fillStyle = color;
ctx.fillRect(blockX, blockY, blockSize, blockSize);
// Add highlight effect
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
ctx.fillRect(blockX, blockY, blockSize, blockSize / 3);
// Add shadow effect
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(blockX, blockY + blockSize * 2/3, blockSize, blockSize / 3);
// Add border
ctx.strokeStyle = isCurrent ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1;
ctx.strokeRect(blockX, blockY, blockSize, blockSize);
// Add pattern if colorblind mode
if (colorblindMode) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
// Different patterns based on color
if (color === COLORS[0] || color === COLORS[7]) { // Red or Pink
// Diagonal lines
ctx.beginPath();
ctx.moveTo(blockX, blockY);
ctx.lineTo(blockX + blockSize, blockY + blockSize);
ctx.moveTo(blockX + blockSize, blockY);
ctx.lineTo(blockX, blockY + blockSize);
ctx.stroke();
} else if (color === COLORS[1] || color === COLORS[4]) { // Green or Purple
// Dots
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
ctx.beginPath();
ctx.arc(
blockX + blockSize / 4 + i * blockSize / 3,
blockY + blockSize / 4 + j * blockSize / 3,
blockSize / 10,
0,
Math.PI * 2
);
ctx.fill();
}
}
} else if (color === COLORS[2] || color === COLORS[5]) { // Blue or Cyan
// Horizontal lines
for (let i = 0; i < 3; i++) {
ctx.fillRect(
blockX,
blockY + i * blockSize / 3,
blockSize,
blockSize / 10
);
}
} else { // Yellow or Orange
// Vertical lines
for (let i = 0; i < 3; i++) {
ctx.fillRect(
blockX + i * blockSize / 3,
blockY,
blockSize / 10,
blockSize
);
}
}
}
}
// Draw the next piece preview
function drawNextPiece() {
// Clear canvas
nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
// Draw background
nextCtx.fillStyle = 'rgba(30, 30, 30, 0.5)';
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
if (!nextPiece) return;
// Calculate center position
const centerX = nextCanvas.width / 2 - (nextPiece.shape[0].length * cellSize / 2) / 2;
const centerY = nextCanvas.height / 2 - (nextPiece.shape.length * cellSize / 2) / 2;
// Draw each block of the next piece
for (let row = 0; row < nextPiece.shape.length; row++) {
for (let col = 0; col < nextPiece.shape[row].length; col++) {
if (nextPiece.shape[row][col] !== 0) {
const x = centerX + col * cellSize / 2;
const y = centerY + row * cellSize / 2;
nextCtx.fillStyle = nextPiece.color;
nextCtx.fillRect(x, y, cellSize / 2, cellSize / 2);
// Add highlight effect
nextCtx.fillStyle = 'rgba(255, 255, 255, 0.2)';
nextCtx.fillRect(x, y, cellSize / 2, cellSize / 6);
// Add shadow effect
nextCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
nextCtx.fillRect(x, y + cellSize / 3, cellSize / 2, cellSize / 6);
// Add border
nextCtx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
nextCtx.lineWidth = 1;
nextCtx.strokeRect(x, y, cellSize / 2, cellSize / 2);
}
}
}
}
// Create explosion effect
function createExplosion(x, y, color) {
const particles = [];
const particleCount = 20;
for (let i = 0; i < particleCount; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 3;
particles.push({
x: (x + 0.5) * cellSize,
y: (y + 0.5) * cellSize,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: 2 + Math.random() * 4,
color: color,
life: 30 + Math.random() * 30
});
}
// Animate particles
const animateParticles = () => {
const container = document.getElementById('particles-container');
container.innerHTML = '';
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life--;
if (p.life > 0) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.width = `${p.size}px`;
particle.style.height = `${p.size}px`;
particle.style.backgroundColor = p.color;
particle.style.left = `${p.x}px`;
particle.style.top = `${p.y}px`;
particle.style.opacity = p.life / 60;
particle.style.setProperty('--tx', `${p.vx * 10}px`);
particle.style.setProperty('--ty', `${p.vy * 10}px`);
container.appendChild(particle);
}
}
// Remove dead particles
for (let i = particles.length - 1; i >= 0; i--) {
if (particles[i].life <= 0) {
particles.splice(i, 1);
}
}
// Continue animation if particles remain
if (particles.length > 0) {
requestAnimationFrame(animateParticles);
}
};
animateParticles();
}
// Create background particles
function createBackgroundParticles() {
const container = document.getElementById('particles-container');
const particleCount = 30;
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'particle absolute rounded-full';
// Random properties
const size = 1 + Math.random() * 3;
const x = Math.random() * 100;
const y = Math.random() * 100;
const duration = 10 + Math.random() * 20;
const delay = Math.random() * 20;
const color = COLORS[Math.floor(Math.random() * COLORS.length)];
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
particle.style.left = `${x}%`;
particle.style.top = `${y}%`;
particle.style.backgroundColor = color;
particle.style.opacity = 0.3 + Math.random() * 0.3;
particle.style.animation = `pulse ${duration}s ${delay}s infinite`;
container.appendChild(particle);
}
}
// Add a random power-up
function addRandomPowerUp() {
const powerUpTypes = Object.keys(POWER_UPS);
const randomType = powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)];
activePowerUps.push({
type: randomType,
duration: 15000, // 15 seconds
startTime: Date.now()
});
updatePowerUpsDisplay();
// Special effects for certain power-ups
if (randomType === 'gravity') {
gravityDirection = ['down', 'left', 'right', 'up'][Math.floor(Math.random() * 4)];
} else if (randomType === 'slow') {
clearInterval(gameInterval);
gameInterval = setInterval(update, gameSpeed * 2);
} else if (randomType === 'score') {
// Multiplier already handled in score calculation
}
}
// Update active power-ups
function updatePowerUps() {
const now = Date.now();
for (let i = activePowerUps.length - 1; i >= 0; i--) {
const powerUp = activePowerUps[i];
if (now - powerUp.startTime >= powerUp.duration) {
// Power-up expired
if (powerUp.type === 'slow') {
clearInterval(gameInterval);
gameInterval = setInterval(update, gameSpeed);
} else if (powerUp.type === 'gravity') {
gravityDirection = 'down';
}
activePowerUps.splice(i, 1);
}
}
if (activePowerUps.length > 0) {
updatePowerUpsDisplay();
}
}
// Update power-ups display
function updatePowerUpsDisplay() {
const container = document.getElementById('power-ups');
container.innerHTML = '';
activePowerUps.forEach(powerUp => {
const powerUpEl = document.createElement('div');
powerUpEl.className = 'flex flex-col items-center';
powerUpEl.title = `${POWER_UPS[powerUp.type].name}: ${POWER_UPS[powerUp.type].effect}`;
const icon = document.createElement('div');
icon.className = 'w-8 h-8 rounded-full flex items-center justify-center text-white font-bold';
icon.style.backgroundColor = POWER_UPS[powerUp.type].color;
icon.textContent = powerUp.type[0].toUpperCase();
const progress = document.createElement('div');
progress.className = 'w-full h-1 mt-1 bg-gray-700 rounded-full overflow-hidden';
const progressBar = document.createElement('div');
progressBar.className = 'h-full';
progressBar.style.backgroundColor = POWER_UPS[powerUp.type].color;
progressBar.style.width = `${100 - ((Date.now() - powerUp.startTime) / powerUp.duration * 100)}%`;
progress.appendChild(progressBar);
powerUpEl.appendChild(icon);
powerUpEl.appendChild(progress);
container.appendChild(powerUpEl);
});
}
// Handle keyboard input
function handleKeyDown(e) {
if (isGameOver) return;
switch (e.key) {
case 'ArrowLeft':
movePiece(-1, 0);
break;
case 'ArrowRight':
movePiece(1, 0);
break;
case 'ArrowDown':
movePiece(0, 1);
break;
case 'ArrowUp':
rotatePiece();
break;
case ' ':
// Hard drop
while (movePiece(0, 1)) {}
lockPiece();
const linesCleared = checkLines();
if (linesCleared > 0) {
updateScore(linesCleared);
}
currentPiece = nextPiece;
nextPiece = createPiece();
if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) {
gameOver();
}
break;
case 'p':
case 'P':
togglePause();
break;
case 'Escape':
if (isPaused) {
resumeGame();
} else {
pauseGame();
}
break;
}
// Prevent default for game controls
if (['ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp', ' '].includes(e.key)) {
e.preventDefault();
}
}
// Handle touch start
function handleTouchStart(e) {
if (settings.controls !== 'touch' || isGameOver || isPaused) return;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
// Handle touch move
function handleTouchMove(e) {
if (settings.controls !== 'touch' || isGameOver || isPaused) return;
e.preventDefault();
touchEndX = e.touches[0].clientX;
touchEndY = e.touches[0].clientY;
}
// Handle touch end
function handleTouchEnd(e) {
if (settings.controls !== 'touch' || isGameOver || isPaused) return;
const dx = touchEndX - touchStartX;
const dy = touchEndY - touchStartY;
// Determine swipe direction
if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal swipe
if (dx > 30) {
movePiece(1, 0); // Right
} else if (dx < -30) {
movePiece(-1, 0); // Left
}
} else {
// Vertical swipe
if (dy > 30) {
movePiece(0, 1); // Down
} else if (dy < -30) {
rotatePiece(); // Up for rotate
}
}
// Reset touch positions
touchStartX = touchStartY = touchEndX = touchEndY = 0;
}
// Toggle pause
function togglePause() {
if (isPaused) {
resumeGame();
} else {
pauseGame();
}
}
// Pause the game
function pauseGame() {
if (isGameOver) return;
isPaused = true;
clearInterval(gameInterval);
document.getElementById('pause-screen').classList.remove('hidden');
}
// Resume the game
function resumeGame() {
if (isGameOver) return;
isPaused = false;
lastUpdateTime = Date.now();
gameInterval = setInterval(update, gameSpeed);
document.getElementById('pause-screen').classList.add('hidden');
}
// Quit game and return to menu
function quitGame() {
clearInterval(gameInterval);
isPaused = false;
gameStarted = false;
document.getElementById('pause-screen').classList.add('hidden');
document.getElementById('game-ui').classList.add('hidden');
showMainMenu();
}
// Show main menu
function showMainMenu() {
document.getElementById('main-menu').classList.remove('hidden');
document.getElementById('high-scores-menu').classList.add('hidden');
document.getElementById('settings-menu').classList.add('hidden');
document.getElementById('game-ui').classList.add('hidden');
}
// Show high scores
function showHighScores() {
document.getElementById('main-menu').classList.add('hidden');
document.getElementById('high-scores-menu').classList.remove('hidden');
// Populate scores
const scoresList = document.getElementById('scores-list');
scoresList.innerHTML = '';
if (highScores.length === 0) {
scoresList.innerHTML = '<p class="text-center text-gray-400">No scores yet</p>';
return;
}
highScores.sort((a, b) => b - a).slice(0, 10).forEach((score, index) => {
const scoreEl = document.createElement('div');
scoreEl.className = 'flex justify-between items-center py-2 border-b border-gray-800';
const rankEl = document.createElement('span');
rankEl.className = 'font-bold text-yellow-400';
rankEl.textContent = `${index + 1}.`;
const scoreValueEl = document.createElement('span');
scoreValueEl.className = 'font-mono';
scoreValueEl.textContent = score.toLocaleString();
scoreEl.appendChild(rankEl);
scoreEl.appendChild(scoreValueEl);
scoresList.appendChild(scoreEl);
});
}
// Show settings
function showSettings() {
document.getElementById('main-menu').classList.add('hidden');
document.getElementById('settings-menu').classList.remove('hidden');
// Set current settings in form
document.getElementById('control-type').value = settings.controls;
document.getElementById('difficulty').value = settings.difficulty;
document.getElementById('colorblind-mode').checked = settings.colorblind;
document.getElementById('volume').value = settings.volume;
}
// Save settings
function saveSettings() {
settings.controls = document.getElementById('control-type').value;
settings.difficulty = document.getElementById('difficulty').value;
settings.colorblind = document.getElementById('colorblind-mode').checked;
settings.volume = parseInt(document.getElementById('volume').value);
// Save to localStorage
localStorage.setItem('blockShiftSettings', JSON.stringify(settings));
// Update game state
colorblindMode = settings.colorblind;
volume = settings.volume / 100;
// Update UI
updateUIFromSettings();
// Return to menu
showMainMenu();
}
// Update UI based on settings
function updateUIFromSettings() {
// Enable/disable continue button based on saved game
const savedGame = localStorage.getItem('blockShiftGame');
document.getElementById('continue-game').disabled = !savedGame;
// Update colorblind mode
colorblindMode = settings.colorblind;
}
// Add high score
function addHighScore(newScore) {
highScores.push(newScore);
// Keep only top 10 scores
highScores.sort((a, b) => b - a);
if (highScores.length > 10) {
highScores = highScores.slice(0, 10);
}
// Save to localStorage
localStorage.setItem('blockShiftHighScores', JSON.stringify(highScores));
}
// Load high scores
function loadHighScores() {
const savedScores = localStorage.getItem('blockShiftHighScores');
if (savedScores) {
highScores = JSON.parse(savedScores);
} else {
highScores = [];
}
}
// Load settings
function loadSettings() {
const savedSettings = localStorage.getItem('blockShiftSettings');
if (savedSettings) {
settings = JSON.parse(savedSettings);
}
// Apply settings
colorblindMode = settings.colorblind || false;
volume = (settings.volume || 70) / 100;
}
// Initialize the game when the page loads
window.addEventListener('load', 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=Batfly/blockshift-chronicles" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>