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.
				<!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>

			
Descargar adjuntos
COMPARTE ESTE TUTORIAL

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP