Snake en JavaScript con HTML5 Canvas

Implementación completa del clásico juego Snake con HTML5 Canvas. Controles de teclado y swipe táctil, velocidad creciente según la puntuación y diseño dark responsivo. Todo en un único fichero HTML sin dependencias.
				<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snake JS - HTML5 Canvas</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #1a1a2e;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    font-family: 'Courier New', monospace;
    color: #eee;
  }
  h1 { font-size: 1.6rem; margin-bottom: 10px; color: #4ecca3; letter-spacing: 3px; }
  #score-panel { font-size: 1rem; margin-bottom: 12px; color: #ccc; }
  #score-panel span { color: #4ecca3; font-weight: bold; }
  canvas {
    border: 2px solid #4ecca3;
    background: #16213e;
    display: block;
  }
  #overlay {
    position: absolute;
    background: rgba(26, 26, 46, 0.92);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 16px;
    width: 400px;
    height: 400px;
    border: 2px solid #4ecca3;
  }
  #overlay h2 { font-size: 2rem; color: #e94560; }
  #overlay p { color: #aaa; font-size: 0.9rem; }
  #overlay button {
    background: #4ecca3;
    color: #1a1a2e;
    border: none;
    padding: 10px 28px;
    font-size: 1rem;
    font-family: inherit;
    font-weight: bold;
    cursor: pointer;
    border-radius: 4px;
  }
  #overlay button:hover { background: #38b58e; }
  #wrapper { position: relative; }
</style>
</head>
<body>
<h1>SNAKE</h1>
<div id="score-panel">Puntuación: <span id="score">0</span> &nbsp;&nbsp; Máximo: <span id="best">0</span></div>
<div id="wrapper">
  <canvas id="canvas" width="400" height="400"></canvas>
  <div id="overlay">
    <h2>SNAKE</h2>
    <p>Usa las flechas del teclado o desliza en móvil</p>
    <button id="btn-start">Empezar</button>
  </div>
</div>

<script>
const COLS = 20, ROWS = 20, CELL = 20;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const btn = document.getElementById('btn-start');

// Estado del juego
let snake, dir, nextDir, food, score, best, running, loop;

function init() {
  // Serpiente inicial: 3 celdas en el centro
  snake = [
    {x: 12, y: 10},
    {x: 11, y: 10},
    {x: 10, y: 10}
  ];
  dir = {x: 1, y: 0};
  nextDir = {x: 1, y: 0};
  score = 0;
  scoreEl.textContent = '0';
  spawnFood();
}

function spawnFood() {
  // Colocar comida en una celda libre
  let pos;
  do {
    pos = {
      x: Math.floor(Math.random() * COLS),
      y: Math.floor(Math.random() * ROWS)
    };
  } while (snake.some(s => s.x === pos.x && s.y === pos.y));
  food = pos;
}

function step() {
  dir = nextDir;
  const head = {x: snake[0].x + dir.x, y: snake[0].y + dir.y};

  // Colisión con paredes
  if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) {
    gameOver(); return;
  }
  // Colisión consigo misma (excepto la última celda que se va a mover)
  if (snake.slice(0, -1).some(s => s.x === head.x && s.y === head.y)) {
    gameOver(); return;
  }

  snake.unshift(head);

  if (head.x === food.x && head.y === food.y) {
    // Comer: no quitar cola, subir puntuación
    score += 10;
    scoreEl.textContent = score;
    if (score > best) {
      best = score;
      bestEl.textContent = best;
    }
    spawnFood();
  } else {
    snake.pop();
  }

  draw();
}

function draw() {
  // Fondo
  ctx.fillStyle = '#16213e';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Grid (sutil)
  ctx.strokeStyle = 'rgba(255,255,255,0.03)';
  ctx.lineWidth = 0.5;
  for (let x = 0; x < COLS; x++) {
    ctx.beginPath(); ctx.moveTo(x * CELL, 0); ctx.lineTo(x * CELL, canvas.height); ctx.stroke();
  }
  for (let y = 0; y < ROWS; y++) {
    ctx.beginPath(); ctx.moveTo(0, y * CELL); ctx.lineTo(canvas.width, y * CELL); ctx.stroke();
  }

  // Comida
  ctx.fillStyle = '#e94560';
  ctx.fillRect(food.x * CELL + 3, food.y * CELL + 3, CELL - 6, CELL - 6);
  // Brillo comida
  ctx.fillStyle = 'rgba(255,255,255,0.3)';
  ctx.fillRect(food.x * CELL + 5, food.y * CELL + 5, 5, 5);

  // Serpiente
  snake.forEach((seg, i) => {
    if (i === 0) {
      // Cabeza
      ctx.fillStyle = '#4ecca3';
    } else {
      // Cuerpo con degradado hacia más oscuro
      const alpha = Math.max(0.4, 1 - i / snake.length * 0.6);
      ctx.fillStyle = `rgba(78, 204, 163, ${alpha})`;
    }
    ctx.fillRect(seg.x * CELL + 1, seg.y * CELL + 1, CELL - 2, CELL - 2);
  });
}

function gameOver() {
  clearInterval(loop);
  running = false;
  // Mostrar overlay de game over
  overlay.innerHTML = `
    <h2>GAME OVER</h2>
    <p>Puntuación: <strong style="color:#4ecca3">${score}</strong></p>
    <p>Máximo: <strong style="color:#4ecca3">${best}</strong></p>
    <button id="btn-start">Reintentar</button>
  `;
  overlay.style.display = 'flex';
  document.getElementById('btn-start').addEventListener('click', startGame);
}

function startGame() {
  overlay.style.display = 'none';
  init();
  running = true;
  // Velocidad base: 150ms; aumenta 5ms cada 50 puntos (mínimo 60ms)
  clearInterval(loop);
  function tick() {
    if (!running) return;
    step();
    const speed = Math.max(60, 150 - Math.floor(score / 50) * 5);
    clearTimeout(loop);
    loop = setTimeout(tick, speed);
  }
  tick();
}

// Teclado
document.addEventListener('keydown', e => {
  if (!running) return;
  switch(e.key) {
    case 'ArrowUp':    if (dir.y !== 1)  nextDir = {x:0, y:-1}; e.preventDefault(); break;
    case 'ArrowDown':  if (dir.y !== -1) nextDir = {x:0, y:1};  e.preventDefault(); break;
    case 'ArrowLeft':  if (dir.x !== 1)  nextDir = {x:-1,y:0};  e.preventDefault(); break;
    case 'ArrowRight': if (dir.x !== -1) nextDir = {x:1, y:0};  e.preventDefault(); break;
  }
});

// Touch / swipe
let touchX, touchY;
canvas.addEventListener('touchstart', e => {
  touchX = e.touches[0].clientX;
  touchY = e.touches[0].clientY;
  e.preventDefault();
}, {passive: false});
canvas.addEventListener('touchend', e => {
  if (!running) return;
  const dx = e.changedTouches[0].clientX - touchX;
  const dy = e.changedTouches[0].clientY - touchY;
  if (Math.abs(dx) > Math.abs(dy)) {
    if (dx > 20 && dir.x !== -1) nextDir = {x:1, y:0};
    if (dx < -20 && dir.x !== 1)  nextDir = {x:-1,y:0};
  } else {
    if (dy > 20 && dir.y !== -1)  nextDir = {x:0, y:1};
    if (dy < -20 && dir.y !== 1)  nextDir = {x:0,y:-1};
  }
  e.preventDefault();
}, {passive: false});

// Arranque inicial
btn.addEventListener('click', startGame);
</script>
</body>
</html>

			
Descargar adjuntos
COMPARTE ESTE TUTORIAL

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
SIGUIENTE TUTORIAL