Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>2048 Game</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 tile-appear { | |
0% { | |
transform: scale(0.5); | |
opacity: 0; | |
} | |
100% { | |
transform: scale(1); | |
opacity: 1; | |
} | |
} | |
@keyframes tile-merge { | |
0% { | |
transform: scale(1); | |
} | |
50% { | |
transform: scale(1.2); | |
} | |
100% { | |
transform: scale(1); | |
} | |
} | |
.tile-new { | |
animation: tile-appear 0.2s ease-out; | |
} | |
.tile-merged { | |
animation: tile-merge 0.2s ease-out; | |
} | |
.game-container { | |
perspective: 1000px; | |
} | |
.board { | |
transform-style: preserve-3d; | |
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); | |
} | |
.tile { | |
transition: all 0.1s ease-in-out; | |
font-weight: 700; | |
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center font-sans"> | |
<div class="game-container w-full max-w-md px-4"> | |
<div class="flex justify-between items-center mb-6"> | |
<h1 class="text-4xl font-bold text-gray-800">2048</h1> | |
<div class="flex items-center space-x-4"> | |
<div class="bg-gray-800 text-white px-4 py-2 rounded-lg text-center"> | |
<div class="text-xs text-gray-300">SCORE</div> | |
<div id="score" class="text-xl font-bold">0</div> | |
</div> | |
<button id="restart" class="bg-amber-500 hover:bg-amber-600 text-white px-4 py-2 rounded-lg transition"> | |
<i class="fas fa-redo mr-1"></i> New Game | |
</button> | |
</div> | |
</div> | |
<div class="mb-4 flex justify-between items-center"> | |
<p class="text-gray-600">Join the numbers and get to the <span class="font-bold">2048 tile!</span></p> | |
<div id="best-score" class="bg-gray-800 text-white px-3 py-1 rounded text-sm"> | |
Best: 0 | |
</div> | |
</div> | |
<div class="board bg-gray-300 p-3 rounded-xl relative mb-6"> | |
<div class="grid grid-cols-4 gap-3"> | |
<!-- Empty cells for the board background --> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
<div class="bg-gray-200 rounded-lg aspect-square"></div> | |
</div> | |
<!-- Tiles will be dynamically added here --> | |
<div id="tiles-container" class="absolute inset-0 grid grid-cols-4 gap-3 p-3"></div> | |
</div> | |
<div class="text-center text-gray-500 text-sm"> | |
<p>Use arrow keys or swipe to move the tiles. When two tiles with the same number touch, they merge into one!</p> | |
</div> | |
</div> | |
<!-- Game Over Modal --> | |
<div id="game-over-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50"> | |
<div class="bg-white rounded-xl p-6 max-w-sm w-full mx-4 text-center"> | |
<h2 class="text-2xl font-bold text-gray-800 mb-2">Game Over!</h2> | |
<p class="text-gray-600 mb-4">Your score: <span id="final-score" class="font-bold">0</span></p> | |
<div class="flex space-x-3 justify-center"> | |
<button id="try-again" class="bg-amber-500 hover:bg-amber-600 text-white px-4 py-2 rounded-lg transition"> | |
Try Again | |
</button> | |
<button id="close-modal" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-lg transition"> | |
Close | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Win Modal --> | |
<div id="win-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50"> | |
<div class="bg-white rounded-xl p-6 max-w-sm w-full mx-4 text-center"> | |
<h2 class="text-2xl font-bold text-gray-800 mb-2">You Win! 🎉</h2> | |
<p class="text-gray-600 mb-4">You reached 2048 with a score of <span id="win-score" class="font-bold">0</span></p> | |
<div class="flex space-x-3 justify-center"> | |
<button id="continue-playing" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg transition"> | |
Keep Going | |
</button> | |
<button id="new-game-win" class="bg-amber-500 hover:bg-amber-600 text-white px-4 py-2 rounded-lg transition"> | |
New Game | |
</button> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// Game state | |
let board = Array(16).fill(0); | |
let score = 0; | |
let bestScore = localStorage.getItem('2048-best-score') || 0; | |
let gameOver = false; | |
let won = false; | |
let touchStartX = 0; | |
let touchStartY = 0; | |
// DOM elements | |
const tilesContainer = document.getElementById('tiles-container'); | |
const scoreElement = document.getElementById('score'); | |
const bestScoreElement = document.getElementById('best-score'); | |
const restartButton = document.getElementById('restart'); | |
const gameOverModal = document.getElementById('game-over-modal'); | |
const winModal = document.getElementById('win-modal'); | |
const finalScoreElement = document.getElementById('final-score'); | |
const winScoreElement = document.getElementById('win-score'); | |
const tryAgainButton = document.getElementById('try-again'); | |
const closeModalButton = document.getElementById('close-modal'); | |
const continuePlayingButton = document.getElementById('continue-playing'); | |
const newGameWinButton = document.getElementById('new-game-win'); | |
// Tile colors based on value | |
const tileColors = { | |
0: 'bg-gray-200', | |
2: 'bg-amber-50 text-gray-800', | |
4: 'bg-amber-100 text-gray-800', | |
8: 'bg-amber-300 text-white', | |
16: 'bg-amber-400 text-white', | |
32: 'bg-orange-400 text-white', | |
64: 'bg-orange-500 text-white', | |
128: 'bg-yellow-300 text-white', | |
256: 'bg-yellow-400 text-white', | |
512: 'bg-yellow-500 text-white', | |
1024: 'bg-yellow-600 text-white', | |
2048: 'bg-yellow-700 text-white', | |
4096: 'bg-red-500 text-white', | |
8192: 'bg-red-600 text-white' | |
}; | |
// Initialize the game | |
function initGame() { | |
board = Array(16).fill(0); | |
score = 0; | |
gameOver = false; | |
won = false; | |
updateScore(); | |
addRandomTile(); | |
addRandomTile(); | |
renderBoard(); | |
// Hide modals if they're open | |
gameOverModal.classList.add('hidden'); | |
winModal.classList.add('hidden'); | |
} | |
// Add a random tile (2 or 4) to an empty cell | |
function addRandomTile() { | |
const emptyCells = board.reduce((acc, val, index) => { | |
if (val === 0) acc.push(index); | |
return acc; | |
}, []); | |
if (emptyCells.length > 0) { | |
const randomIndex = emptyCells[Math.floor(Math.random() * emptyCells.length)]; | |
board[randomIndex] = Math.random() < 0.9 ? 2 : 4; | |
return true; | |
} | |
return false; | |
} | |
// Render the board | |
function renderBoard() { | |
tilesContainer.innerHTML = ''; | |
board.forEach((value, index) => { | |
if (value !== 0) { | |
const tile = document.createElement('div'); | |
tile.className = `tile flex items-center justify-center rounded-lg aspect-square ${tileColors[value] || 'bg-red-700 text-white'}`; | |
tile.textContent = value; | |
// Calculate position based on index | |
const row = Math.floor(index / 4); | |
const col = index % 4; | |
tile.style.gridRow = row + 1; | |
tile.style.gridColumn = col + 1; | |
// Add animation class for new tiles (handled in game logic) | |
tilesContainer.appendChild(tile); | |
} | |
}); | |
} | |
// Update the score display | |
function updateScore() { | |
scoreElement.textContent = score; | |
bestScoreElement.textContent = `Best: ${bestScore}`; | |
// Update best score if current score is higher | |
if (score > bestScore) { | |
bestScore = score; | |
localStorage.setItem('2048-best-score', bestScore); | |
} | |
} | |
// Check if the game is over | |
function checkGameOver() { | |
// If there are empty cells, game is not over | |
if (board.some(cell => cell === 0)) return false; | |
// Check for possible merges in rows | |
for (let row = 0; row < 4; row++) { | |
for (let col = 0; col < 3; col++) { | |
const index = row * 4 + col; | |
if (board[index] === board[index + 1]) { | |
return false; | |
} | |
} | |
} | |
// Check for possible merges in columns | |
for (let col = 0; col < 4; col++) { | |
for (let row = 0; row < 3; row++) { | |
const index = row * 4 + col; | |
if (board[index] === board[index + 4]) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
// Check if the player has won (reached 2048) | |
function checkWin() { | |
return board.some(cell => cell === 2048); | |
} | |
// Move tiles in a direction | |
function move(direction) { | |
if (gameOver) return false; | |
let moved = false; | |
const newBoard = [...board]; | |
// Helper function to process a line (row or column) | |
const processLine = (line) => { | |
// Remove zeros | |
let filtered = line.filter(val => val !== 0); | |
let result = []; | |
let merged = false; | |
// Merge adjacent equal values | |
for (let i = 0; i < filtered.length; i++) { | |
if (i < filtered.length - 1 && filtered[i] === filtered[i + 1] && !merged) { | |
result.push(filtered[i] * 2); | |
score += filtered[i] * 2; | |
merged = true; | |
i++; // Skip next element | |
} else { | |
result.push(filtered[i]); | |
merged = false; | |
} | |
} | |
// Pad with zeros | |
while (result.length < 4) { | |
result.push(0); | |
} | |
// Check if the line changed | |
if (line.some((val, i) => val !== result[i])) { | |
moved = true; | |
} | |
return result; | |
}; | |
// Process based on direction | |
if (direction === 'left') { | |
for (let row = 0; row < 4; row++) { | |
const line = newBoard.slice(row * 4, row * 4 + 4); | |
const processed = processLine(line); | |
for (let col = 0; col < 4; col++) { | |
newBoard[row * 4 + col] = processed[col]; | |
} | |
} | |
} else if (direction === 'right') { | |
for (let row = 0; row < 4; row++) { | |
const line = newBoard.slice(row * 4, row * 4 + 4).reverse(); | |
const processed = processLine(line).reverse(); | |
for (let col = 0; col < 4; col++) { | |
newBoard[row * 4 + col] = processed[col]; | |
} | |
} | |
} else if (direction === 'up') { | |
for (let col = 0; col < 4; col++) { | |
const line = [ | |
newBoard[col], | |
newBoard[col + 4], | |
newBoard[col + 8], | |
newBoard[col + 12] | |
]; | |
const processed = processLine(line); | |
for (let row = 0; row < 4; row++) { | |
newBoard[row * 4 + col] = processed[row]; | |
} | |
} | |
} else if (direction === 'down') { | |
for (let col = 0; col < 4; col++) { | |
const line = [ | |
newBoard[col + 12], | |
newBoard[col + 8], | |
newBoard[col + 4], | |
newBoard[col] | |
]; | |
const processed = processLine(line).reverse(); | |
for (let row = 0; row < 4; row++) { | |
newBoard[row * 4 + col] = processed[row]; | |
} | |
} | |
} | |
// If any movement occurred, update the board | |
if (moved) { | |
board = newBoard; | |
updateScore(); | |
addRandomTile(); | |
renderBoard(); | |
// Check for win condition (only once) | |
if (!won && checkWin()) { | |
won = true; | |
winScoreElement.textContent = score; | |
winModal.classList.remove('hidden'); | |
} | |
// Check for game over | |
if (checkGameOver()) { | |
gameOver = true; | |
finalScoreElement.textContent = score; | |
gameOverModal.classList.remove('hidden'); | |
} | |
} | |
return moved; | |
} | |
// Handle keyboard events | |
function handleKeyDown(e) { | |
if (e.key === 'ArrowUp') { | |
move('up'); | |
} else if (e.key === 'ArrowDown') { | |
move('down'); | |
} else if (e.key === 'ArrowLeft') { | |
move('left'); | |
} else if (e.key === 'ArrowRight') { | |
move('right'); | |
} | |
} | |
// Handle touch events for mobile | |
function handleTouchStart(e) { | |
touchStartX = e.touches[0].clientX; | |
touchStartY = e.touches[0].clientY; | |
} | |
function handleTouchEnd(e) { | |
if (!touchStartX || !touchStartY) return; | |
const touchEndX = e.changedTouches[0].clientX; | |
const touchEndY = e.changedTouches[0].clientY; | |
const dx = touchEndX - touchStartX; | |
const dy = touchEndY - touchStartY; | |
// Determine the direction based on the greatest change | |
if (Math.abs(dx) > Math.abs(dy)) { | |
if (dx > 0) { | |
move('right'); | |
} else { | |
move('left'); | |
} | |
} else { | |
if (dy > 0) { | |
move('down'); | |
} else { | |
move('up'); | |
} | |
} | |
// Reset touch coordinates | |
touchStartX = 0; | |
touchStartY = 0; | |
} | |
// Event listeners | |
document.addEventListener('keydown', handleKeyDown); | |
restartButton.addEventListener('click', initGame); | |
tryAgainButton.addEventListener('click', initGame); | |
closeModalButton.addEventListener('click', () => gameOverModal.classList.add('hidden')); | |
continuePlayingButton.addEventListener('click', () => winModal.classList.add('hidden')); | |
newGameWinButton.addEventListener('click', initGame); | |
// Touch events for mobile | |
document.addEventListener('touchstart', handleTouchStart, false); | |
document.addEventListener('touchend', handleTouchEnd, false); | |
// Prevent scrolling on touch move | |
document.addEventListener('touchmove', (e) => { | |
if (Math.abs(e.touches[0].clientX - touchStartX) > 10 || | |
Math.abs(e.touches[0].clientY - touchStartY) > 10) { | |
e.preventDefault(); | |
} | |
}, { passive: false }); | |
// Start the game | |
initGame(); | |
}); | |
</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=thinkall/2048-by-deepsite" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
</html> |