Files
2026-09-03 18:02:36 +07:00

395 lines
11 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>2048</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
overflow: hidden;
background: #faf8ef;
font-family: 'Clear Sans', 'Helvetica Neue', Arial, sans-serif;
color: #776e65;
touch-action: manipulation;
}
.game-wrapper {
width: 100%;
max-width: 450px;
margin: auto;
padding: 15px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 15px;
}
.header h1 {
margin: 0;
font-size: 36px;
}
.score-container {
background: #bbada0;
padding: 8px 15px;
border-radius: 5px;
color: white;
font-weight: bold;
font-size: 18px;
text-align: center;
}
.btn-reset {
background: #8f7a66;
color: white;
border: none;
border-radius: 5px;
padding: 8px 15px;
font-weight: bold;
cursor: pointer;
font-size: 16px;
margin-top: 5px;
}
.btn-reset:active {
background: #776e65;
}
#game-container {
background: #bbada0;
border-radius: 10px;
position: relative;
width: 100%;
aspect-ratio: 1;
overflow: hidden;
box-sizing: border-box;
padding: 1%;
}
.cell-bg, .tile {
width: 25%;
height: 25%;
position: absolute;
padding: 1.5%;
box-sizing: border-box;
top: 0;
left: 0;
}
.cell-bg-inner {
width: 100%;
height: 100%;
background: rgba(238, 228, 218, 0.35);
border-radius: 5px;
}
.tile {
z-index: 10;
}
.tile.new .tile-inner {
animation: appear 0.2s;
}
.tile.merged .tile-inner {
animation: pop 0.2s;
}
@keyframes appear {
0% { opacity: 0; transform: scale(0); }
100% { opacity: 1; transform: scale(1); }
}
@keyframes pop {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.tile-inner {
width: 100%;
height: 100%;
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
font-size: 28px;
font-weight: bold;
}
.tile-2 .tile-inner { background: #eee4da; color: #776e65; }
.tile-4 .tile-inner { background: #ede0c8; color: #776e65; }
.tile-8 .tile-inner { background: #f2b179; color: #f9f6f2; }
.tile-16 .tile-inner { background: #f59563; color: #f9f6f2; }
.tile-32 .tile-inner { background: #f67c5f; color: #f9f6f2; }
.tile-64 .tile-inner { background: #f65e3b; color: #f9f6f2; }
.tile-128 .tile-inner { background: #edcf72; color: #f9f6f2; font-size: 24px; }
.tile-256 .tile-inner { background: #edcc61; color: #f9f6f2; font-size: 24px; }
.tile-512 .tile-inner { background: #edc850; color: #f9f6f2; font-size: 24px; }
.tile-1024 .tile-inner { background: #edc53f; color: #f9f6f2; font-size: 20px; }
.tile-2048 .tile-inner { background: #edc22e; color: #f9f6f2; font-size: 20px; }
#game-over {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(238, 228, 218, 0.73);
border-radius: 10px;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
}
#game-over button {
margin-top: 20px;
padding: 10px 20px;
background: #8f7a66;
color: white;
border: none;
border-radius: 5px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
}
.controls {
width: 100%;
max-width: 250px;
margin: 30px auto 20px auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 60px 60px;
gap: 15px;
flex-grow: 1;
align-content: end;
}
.controls button {
background: #8f7a66;
color: white;
border: none;
border-radius: 10px;
font-size: 24px;
font-weight: bold;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}
.controls button:active {
background: #776e65;
}
.btn-up { grid-column: 2; grid-row: 1; }
.btn-left { grid-column: 1; grid-row: 2; }
.btn-down { grid-column: 2; grid-row: 2; }
.btn-right { grid-column: 3; grid-row: 2; }
</style>
</head>
<body>
<div class="game-wrapper">
<div class="header">
<div>
<h1>2048</h1>
<button class="btn-reset" onclick="confirmReset()">Reset</button>
</div>
<div class="score-container">
<div>Score</div>
<div id="score">0</div>
</div>
</div>
<div id="game-container">
<div id="grid-bg"></div>
<div id="tile-container"></div>
<div id="game-over">
<h2 style="margin:0; font-size: 36px;">Game Over!</h2>
<button onclick="startGame()">Try Again</button>
</div>
</div>
<div class="controls">
<button class="btn-up" onclick="move('Up')">↑</button>
<button class="btn-left" onclick="move('Left')">←</button>
<button class="btn-down" onclick="move('Down')">↓</button>
<button class="btn-right" onclick="move('Right')">→</button>
</div>
</div>
<script>
const gridBg = document.getElementById('grid-bg');
const tileContainer = document.getElementById('tile-container');
const scoreEl = document.getElementById('score');
const gameOverEl = document.getElementById('game-over');
let board = [];
let score = 0;
let tiles = {}; // id -> { value, el, r, c }
let tileIdCounter = 0;
function initGrid() {
gridBg.innerHTML = '';
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
const bg = document.createElement('div');
bg.className = 'cell-bg';
bg.style.transform = `translate(${c * 100}%, ${r * 100}%)`;
bg.innerHTML = '<div class="cell-bg-inner"></div>';
gridBg.appendChild(bg);
}
}
}
function confirmReset() {
if (confirm("Are you sure you want to reset the game?")) {
startGame();
}
}
function startGame() {
board = Array(4).fill(null).map(() => Array(4).fill(null));
tiles = {};
score = 0;
scoreEl.innerText = score;
gameOverEl.style.display = 'none';
tileContainer.innerHTML = '';
addRandomTile();
addRandomTile();
}
function addRandomTile() {
let empty = [];
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
if (!board[r][c]) empty.push({r, c});
}
}
if (empty.length > 0) {
let {r, c} = empty[Math.floor(Math.random() * empty.length)];
let value = Math.random() < 0.9 ? 2 : 4;
createTile(r, c, value);
}
}
function createTile(r, c, value) {
let id = ++tileIdCounter;
let el = document.createElement('div');
el.className = `tile tile-${value} new`;
el.style.transition = 'none';
el.style.transform = `translate(${c * 100}%, ${r * 100}%)`;
el.innerHTML = `<div class="tile-inner">${value}</div>`;
tileContainer.appendChild(el);
let tile = { id, value, el, r, c };
tiles[id] = tile;
board[r][c] = tile;
// Remove 'new' class after animation
setTimeout(() => {
el.classList.remove('new');
}, 200);
}
function updateTilePos(tile, r, c) {
tile.r = r;
tile.c = c;
tile.el.style.transition = 'transform 0.15s ease-in-out';
tile.el.style.transform = `translate(${c * 100}%, ${r * 100}%)`;
}
function move(direction) {
let moved = false;
let scoreInc = 0;
// Remove merged class from all tiles
Object.values(tiles).forEach(t => t.el.classList.remove('merged'));
const traverse = (r, c, dr, dc) => {
let curr = board[r][c];
if (!curr) return;
let nr = r + dr, nc = c + dc;
let lastEmpty = null;
while (nr >= 0 && nr < 4 && nc >= 0 && nc < 4) {
if (board[nr][nc]) {
if (board[nr][nc].value === curr.value && !board[nr][nc].mergedThisTurn) {
// Merge
let target = board[nr][nc];
board[r][c] = null;
updateTilePos(curr, nr, nc);
// Animate removal of old tile and update target
setTimeout(() => {
curr.el.remove();
delete tiles[curr.id];
target.value *= 2;
scoreInc += target.value;
target.el.className = `tile tile-${target.value} merged`;
target.el.style.transform = `translate(${target.c * 100}%, ${target.r * 100}%)`;
target.el.innerHTML = `<div class="tile-inner">${target.value}</div>`;
scoreEl.innerText = score + scoreInc;
}, 150);
target.mergedThisTurn = true;
moved = true;
return;
}
break;
}
lastEmpty = {r: nr, c: nc};
nr += dr; nc += dc;
}
if (lastEmpty) {
board[r][c] = null;
board[lastEmpty.r][lastEmpty.c] = curr;
updateTilePos(curr, lastEmpty.r, lastEmpty.c);
moved = true;
}
};
if (direction === 'Left') {
for(let r=0; r<4; r++) for(let c=1; c<4; c++) traverse(r, c, 0, -1);
} else if (direction === 'Right') {
for(let r=0; r<4; r++) for(let c=2; c>=0; c--) traverse(r, c, 0, 1);
} else if (direction === 'Up') {
for(let c=0; c<4; c++) for(let r=1; r<4; r++) traverse(r, c, -1, 0);
} else if (direction === 'Down') {
for(let c=0; c<4; c++) for(let r=2; r>=0; r--) traverse(r, c, 1, 0);
}
if (moved) {
// Clear merge flags
for(let r=0; r<4; r++) for(let c=0; c<4; c++) {
if (board[r][c]) board[r][c].mergedThisTurn = false;
}
score += scoreInc;
setTimeout(() => {
addRandomTile();
if (checkGameOver()) gameOverEl.style.display = 'flex';
}, 150);
}
}
function checkGameOver() {
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
if (!board[r][c]) return false;
if (r !== 3 && board[r+1][c] && board[r][c].value === board[r+1][c].value) return false;
if (c !== 3 && board[r][c+1] && board[r][c].value === board[r][c+1].value) return false;
}
}
return true;
}
document.addEventListener('keydown', (e) => {
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
e.preventDefault();
move(e.key.replace('Arrow', ''));
}
});
initGrid();
startGame();
</script>
</body>
</html>