<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2048</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #faf8ef;
font-family: 'Arial Rounded MT Bold', Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 20px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 480px;
margin-bottom: 16px;
}
h1 { font-size: 3rem; color: #776e65; }
.scores {
display: flex;
gap: 8px;
}
.score-box {
background: #bbada0;
color: #fff;
border-radius: 6px;
padding: 6px 14px;
text-align: center;
min-width: 80px;
}
.score-box .label { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 1px; }
.score-box .value { font-size: 1.3rem; font-weight: bold; }
.btn-new {
background: #8f7a66;
color: #fff;
border: none;
border-radius: 6px;
padding: 10px 18px;
font-size: 0.9rem;
font-weight: bold;
cursor: pointer;
font-family: inherit;
}
.btn-new:hover { background: #7a6758; }
/* ?? Tablero ?? */
#board-wrap { position: relative; }
#board {
background: #bbada0;
border-radius: 10px;
padding: 10px;
display: grid;
grid-template: repeat(4, 110px) / repeat(4, 110px);
gap: 10px;
width: 480px;
height: 480px;
}
.cell-bg {
background: rgba(238,228,218,0.35);
border-radius: 6px;
}
.tile {
position: absolute;
width: 110px;
height: 110px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.2rem;
font-weight: bold;
color: #776e65;
transition: top 0.1s ease, left 0.1s ease;
z-index: 2;
}
.tile.new { animation: appear 0.15s ease both; }
.tile.merge { animation: pop 0.15s ease both; }
@keyframes appear { from{transform:scale(0);opacity:0} to{transform:scale(1);opacity:1} }
@keyframes pop { 0%{transform:scale(1)} 50%{transform:scale(1.2)} 100%{transform:scale(1)} }
/* Colores clásicos */
.t2 { background:#eee4da; }
.t4 { background:#ede0c8; }
.t8 { background:#f2b179; color:#f9f6f2; }
.t16 { background:#f59563; color:#f9f6f2; }
.t32 { background:#f67c5f; color:#f9f6f2; }
.t64 { background:#f65e3b; color:#f9f6f2; }
.t128 { background:#edcf72; color:#f9f6f2; font-size:1.8rem; }
.t256 { background:#edcc61; color:#f9f6f2; font-size:1.8rem; }
.t512 { background:#edc850; color:#f9f6f2; font-size:1.8rem; }
.t1024 { background:#edc53f; color:#f9f6f2; font-size:1.5rem; }
.t2048 { background:#edc22e; color:#f9f6f2; font-size:1.5rem; }
.thigh { background:#3c3a32; color:#f9f6f2; font-size:1.2rem; }
/* Overlay game-over / victoria */
#overlay {
display: none;
position: absolute;
inset: 0;
background: rgba(238,228,218,0.85);
border-radius: 10px;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
z-index: 10;
}
#overlay h2 { font-size: 2.5rem; color: #776e65; }
#overlay p { color: #776e65; }
@media (max-width: 520px) {
#board { width: calc(100vw - 20px); height: calc(100vw - 20px); grid-template: repeat(4, 1fr) / repeat(4, 1fr); }
.tile { width: calc((100vw - 60px) / 4); height: calc((100vw - 60px) / 4); font-size: 1.4rem; }
h1 { font-size: 2rem; }
}
</style>
</head>
<body>
<header>
<h1>2048</h1>
<div class="scores">
<div class="score-box"><div class="label">Score</div><div class="value" id="score">0</div></div>
<div class="score-box"><div class="label">Mejor</div><div class="value" id="best">0</div></div>
</div>
<button class="btn-new" id="btn-new">Nuevo</button>
</header>
<div id="board-wrap">
<div id="board"></div>
<div id="overlay">
<h2 id="overlay-title"></h2>
<p id="overlay-msg"></p>
<button class="btn-new" id="btn-retry">Reintentar</button>
</div>
</div>
<script>
const GRID = 4;
const GAP = 10;
const board = document.getElementById('board');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const overlay = document.getElementById('overlay');
let grid, score, best, won;
// Calcular posición CSS de una celda (row,col) dentro del board
function cellPx(row, col) {
const bRect = board.getBoundingClientRect();
const cellSize = (bRect.width - (GRID + 1) * GAP) / GRID;
return { top: GAP + row * (cellSize + GAP), left: GAP + col * (cellSize + GAP), size: cellSize };
}
function init() {
grid = Array.from({length: GRID}, () => Array(GRID).fill(0));
score = 0;
won = false;
updateScore(0);
board.innerHTML = '';
overlay.style.display = 'none';
// Celdas de fondo (slot vacíos)
for (let i = 0; i < GRID * GRID; i++) {
const d = document.createElement('div');
d.className = 'cell-bg';
board.appendChild(d);
}
addRandom();
addRandom();
render();
}
function addRandom() {
const empty = [];
for (let r = 0; r < GRID; r++)
for (let c = 0; c < GRID; c++)
if (grid[r][c] === 0) empty.push([r, c]);
if (!empty.length) return false;
const [r, c] = empty[Math.floor(Math.random() * empty.length)];
grid[r][c] = Math.random() < 0.9 ? 2 : 4;
return [r, c];
}
function tileClass(v) {
if (v >= 2048) return v > 2048 ? 'thigh' : 't2048';
return 't' + v;
}
function render() {
// Eliminar tiles anteriores
board.querySelectorAll('.tile').forEach(t => t.remove());
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
const v = grid[r][c];
if (!v) continue;
const {top, left, size} = cellPx(r, c);
const tile = document.createElement('div');
tile.className = 'tile ' + tileClass(v);
tile.textContent = v;
tile.style.cssText = `top:${top}px;left:${left}px;width:${size}px;height:${size}px`;
board.appendChild(tile);
}
}
}
function updateScore(delta) {
score += delta;
scoreEl.textContent = score;
if (score > best) {
best = score;
bestEl.textContent = best;
localStorage.setItem('2048best', best);
}
}
// ?? Lógica de movimiento ??????????????????????????????????????
function slideRow(row) {
// Compactar: quitar ceros
let arr = row.filter(x => x);
let merged = false;
let gained = 0;
for (let i = 0; i < arr.length - 1; i++) {
if (!merged && arr[i] === arr[i + 1]) {
arr[i] *= 2;
gained += arr[i];
arr.splice(i + 1, 1);
merged = true;
} else {
merged = false;
}
}
while (arr.length < GRID) arr.push(0);
return { row: arr, gained };
}
function move(dir) {
let moved = false;
let gained = 0;
const rotate = (g) => g[0].map((_, c) => g.map(r => r[c]).reverse());
// Normalizar: todos los movimientos como "slide left"
let times = {left:0, right:2, up:1, down:3}[dir];
for (let i = 0; i < times; i++) grid = rotate(grid);
for (let r = 0; r < GRID; r++) {
const {row, gained: g} = slideRow(grid[r]);
if (row.some((v, i) => v !== grid[r][i])) moved = true;
grid[r] = row;
gained += g;
}
// Desrotar
for (let i = 0; i < (4 - times) % 4; i++) grid = rotate(grid);
if (moved) {
updateScore(gained);
const pos = addRandom();
render();
if (pos) {
const {top, left} = cellPx(pos[0], pos[1]);
const newTile = board.querySelector(`.tile[style*="top:${top}px"][style*="left:${left}px"]`);
if (newTile) newTile.classList.add('new');
}
checkEnd();
}
}
function checkEnd() {
// Victoria
if (!won && grid.some(r => r.some(v => v === 2048))) {
won = true;
showOverlay('¡2048!', 'Puedes seguir o reiniciar.');
return;
}
// Derrota: sin celdas vacías y sin movimientos posibles
if (grid.every(r => r.every(v => v !== 0))) {
const canMove = ['left','right','up','down'].some(dir => {
const clone = grid.map(r => [...r]);
const orig = JSON.stringify(grid);
const times = {left:0,right:2,up:1,down:3}[dir];
const rotate = g => g[0].map((_, c) => g.map(r => r[c]).reverse());
let g2 = clone;
for (let i=0;i<times;i++) g2=rotate(g2);
for (let r=0;r<GRID;r++) {
const {row} = slideRow([...g2[r]]);
if (row.some((v,i)=>v!==g2[r][i])) return true;
}
return false;
});
if (!canMove) showOverlay('Game Over', `Puntuación: ${score}`);
}
}
function showOverlay(title, msg) {
document.getElementById('overlay-title').textContent = title;
document.getElementById('overlay-msg').textContent = msg;
overlay.style.display = 'flex';
}
// ?? Controles ?????????????????????????????????????????????????
document.addEventListener('keydown', e => {
const map = {ArrowLeft:'left',ArrowRight:'right',ArrowUp:'up',ArrowDown:'down'};
if (map[e.key]) { e.preventDefault(); move(map[e.key]); }
});
let tx, ty;
board.addEventListener('touchstart', e => { tx=e.touches[0].clientX; ty=e.touches[0].clientY; e.preventDefault(); },{passive:false});
board.addEventListener('touchend', e => {
const dx=e.changedTouches[0].clientX-tx, dy=e.changedTouches[0].clientY-ty;
if (Math.max(Math.abs(dx),Math.abs(dy)) < 20) return;
if (Math.abs(dx) > Math.abs(dy)) move(dx>0?'right':'left');
else move(dy>0?'down':'up');
e.preventDefault();
},{passive:false});
document.getElementById('btn-new').addEventListener('click', init);
document.getElementById('btn-retry').addEventListener('click', init);
best = parseInt(localStorage.getItem('2048best') || '0', 10);
bestEl.textContent = best;
init();
</script>
</body>
</html>
Juego 2048 en JavaScript
Implementación completa del juego 2048 en JavaScript puro. Grid 4×4, controles de teclado y swipe táctil, animación de aparición de tiles, detección de victoria y game over, score y mejor puntuación guardada en localStorage. Sin dependencias externas.
Descargar adjuntos
COMPARTE ESTE TUTORIAL
COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP