<!
DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Snake JS — Duver</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
--bg: #0f1226;
--grid: #1c2140;
--snake: #34d399; /* Verde menta */
--snake-head: #10b981;
--food: #f59e0b; /* Ámbar */
--text: #e5e7eb;
--accent: #60a5fa;
}
* { box-sizing: border-box; }
body {
margin: 0; min-height: 100vh; display: grid; place-items: center;
background: radial-gradient(1200px 600px at 30% -10%, #1a1f3b, transparent),
var(--bg);
color: var(--text); font-family: ui-sans-serif, system-ui, -apple-system,
Segoe UI, Roboto, Arial;
}
.wrap { display: grid; gap: 14px; justify-items: center; }
h1 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: .3px; color:
var(--accent); }
canvas {
background: repeating-linear-gradient(0deg, var(--grid), var(--grid) 20px,
transparent 20px, transparent 40px),
repeating-linear-gradient(90deg, var(--grid), var(--grid) 20px,
transparent 20px, transparent 40px),
#0b0f24;
box-shadow: 0 8px 30px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.05)
inset;
border-radius: 10px;
}
.hud { display: flex; gap: 16px; align-items: center; font-size: 14px; }
.pill { padding: 6px 10px; border-radius: 999px; background:
rgba(96,165,250,.12); border: 1px solid rgba(96,165,250,.25); }
.keys { opacity: .8; font-size: 12px; }
button {
background: #1f2937; color: var(--text); border: 1px solid #374151; border-
radius: 8px;
padding: 8px 12px; cursor: pointer;
}
button:hover { background: #2b3650; }
@media (max-width: 520px) {
canvas { width: 300px; height: 300px; }
}
</style>
</head>
<body>
<div class="wrap">
<h1>Snake JS</h1>
<canvas id="game" width="400" height="400" aria-label="Tablero del juego"
role="img"></canvas>
<div class="hud">
<div class="pill">Puntos: <span id="score">0</span></div>
<div class="pill">Récord: <span id="best">0</span></div>
<div class="keys">Controles: Flechas/WASD · P = Pausa · R = Reiniciar</div>
<button id="btn">Pausa</button>
</div>
</div>
<script>
// Configuración del tablero
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const SIZE = 20; // tamaño de cada celda (px)
const CELLS = canvas.width / SIZE; // 400/20 = 20 celdas por lado
// HUD
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const btn = document.getElementById('btn');
// Estado del juego
let snake, dir, nextDir, food, score, best, speed, tickId, paused, gameOver;
function init() {
snake = [{ x: 10, y: 10 }]; // cabeza en el centro
dir = { x: 1, y: 0 }; // moviendo a la derecha
nextDir = { ...dir };
score = 0;
speed = 120; // ms por tick (menor = más rápido)
paused = false;
gameOver = false;
scoreEl.textContent = '0';
best = Number(localStorage.getItem('snake_best') || 0);
bestEl.textContent = best;
placeFood();
clearInterval(tickId);
tickId = setInterval(loop, speed);
draw();
}
function placeFood() {
// Coloca la comida en una celda libre
while (true) {
const f = { x: rand(0, CELLS - 1), y: rand(0, CELLS - 1) };
if (!snake.some(s => s.x === f.x && s.y === f.y)) { food = f; break; }
}
}
function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function loop() {
if (paused || gameOver) return;
// Actualiza dirección sin permitir giro de 180°
if ((nextDir.x !== -dir.x || nextDir.y !== -dir.y)) dir = nextDir;
const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
// Colisiones con muro
if (head.x < 0 || head.y < 0 || head.x >= CELLS || head.y >= CELLS) {
return end();
}
// Colisión consigo misma
if (snake.some(s => s.x === head.x && s.y === head.y)) {
return end();
}
snake.unshift(head); // mueve la cabeza
// Comer
if (head.x === food.x && head.y === food.y) {
score++;
scoreEl.textContent = score;
if (score % 5 === 0 && speed > 60) { // aumenta dificultad
speed -= 5;
clearInterval(tickId);
tickId = setInterval(loop, speed);
}
placeFood();
} else {
snake.pop(); // avanza (quita cola) si no come
}
draw();
}
function end() {
gameOver = true;
if (score > best) {
best = score;
localStorage.setItem('snake_best', best);
}
bestEl.textContent = best;
// Mensaje de fin de juego
draw();
ctx.fillStyle = 'rgba(0,0,0,.6)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px Segoe UI, Arial';
ctx.textAlign = 'center';
ctx.fillText('¡Juego terminado!', canvas.width / 2, canvas.height / 2 - 10);
ctx.font = '16px Segoe UI, Arial';
ctx.fillText('Pulsa R para reiniciar', canvas.width / 2, canvas.height / 2 +
18);
}
function draw() {
// Fondo (el grid está en el CSS)
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Comida
drawCell(food.x, food.y, '#00000022'); // sombra sutil
drawRoundedCell(food.x, food.y, 6, getCss('--food'));
// Serpiente
snake.forEach((seg, i) => {
const color = i === 0 ? getCss('--snake-head') : getCss('--snake');
drawRoundedCell(seg.x, seg.y, 4, color);
});
}
function drawCell(x, y, color) {
ctx.fillStyle = color;
ctx.fillRect(x * SIZE, y * SIZE, SIZE, SIZE);
}
function drawRoundedCell(x, y, r, color) {
const px = x * SIZE, py = y * SIZE, w = SIZE, h = SIZE;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(px + r, py);
ctx.arcTo(px + w, py, px + w, py + h, r);
ctx.arcTo(px + w, py + h, px, py + h, r);
ctx.arcTo(px, py + h, px, py, r);
ctx.arcTo(px, py, px + w, py, r);
ctx.closePath();
ctx.fill();
}
function getCss(varName) {
return
getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
}
// Controles
window.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
const map = {
arrowup: { x: 0, y: -1 }, w: { x: 0, y: -1 },
arrowdown: { x: 0, y: 1 }, s: { x: 0, y: 1 },
arrowleft: { x: -1, y: 0 }, a: { x: -1, y: 0 },
arrowright:{ x: 1, y: 0 }, d: { x: 1, y: 0 }
};
if (map[k]) nextDir = map[k];
if (k === 'p') togglePause();
if (k === 'r') init();
});
btn.addEventListener('click', togglePause);
function togglePause() {
if (gameOver) return;
paused = !paused;
btn.textContent = paused ? 'Reanudar' : 'Pausa';
// Pintar overlay en pausa
if (paused) {
ctx.fillStyle = 'rgba(0,0,0,.45)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px Segoe UI, Arial';
ctx.textAlign = 'center';
ctx.fillText('Pausa', canvas.width / 2, canvas.height / 2);
} else {
draw();
}
}
// Iniciar
init();
</script>
</body>
</html>