🎮KidHubb

Neon Snake

by SolarScout64
773 lines23.7 KB
▶ Play
<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Neon Snake</title>
  <style>
    :root{
      --bg:#0b0f1a;
      --text:#e8eefc;
      --muted:#9fb0d6;
      --accent:#7cf7ff;
      --accent2:#8b7bff;
      --danger:#ff4d6d;
    }
    html,body{height:100%;margin:0;background:radial-gradient(1200px 800px at 50% 30%, #121b33 0%, var(--bg) 55%, #070a12 100%); color:var(--text); font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;}
    body{display:grid; place-items:center; padding:16px;}

```
#app{width:min(900px, 100%); display:grid; gap:14px; grid-template-columns: 1fr; align-items:start;}
@media (min-width: 860px){ #app{grid-template-columns: 1fr 290px;} }

.card{
  background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
  border:1px solid rgba(255,255,255,0.10);
  border-radius:18px;
  box-shadow: 0 12px 40px rgba(0,0,0,0.35);
  overflow:hidden;
}
.topbar{
  display:flex; gap:10px; align-items:center; justify-content:space-between;
  padding:12px 14px; background:rgba(0,0,0,0.20); border-bottom:1px solid rgba(255,255,255,0.08);
}
.brand{display:flex; gap:10px; align-items:center;}
.dot{width:10px;height:10px;border-radius:999px;background:var(--accent); box-shadow:0 0 14px rgba(124,247,255,0.9);}
.title{font-weight:700; letter-spacing:0.2px;}
.stats{display:flex; gap:14px; font-size:13px; color:var(--muted);}
.stats b{color:var(--text); font-weight:700;}
.main{padding:14px; display:grid; gap:12px; justify-items:center;}

canvas{
  width: min(640px, 100%);
  aspect-ratio: 1 / 1;
  border-radius:16px;
  background: linear-gradient(180deg, rgba(10,15,28,0.95), rgba(9,12,20,0.95));
  border:1px solid rgba(255,255,255,0.10);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.35), 0 18px 60px rgba(0,0,0,0.40);
  touch-action: none;
}

.side{padding:14px; display:grid; gap:12px;}
.section{background:rgba(0,0,0,0.16); border:1px solid rgba(255,255,255,0.08); border-radius:16px; padding:12px;}
.section h3{margin:0 0 8px 0; font-size:13px; color:var(--muted); font-weight:700; letter-spacing:0.3px; text-transform:uppercase;}
.row{display:flex; gap:10px; align-items:center; flex-wrap:wrap; justify-content:center;}

button{
  font: inherit;
  border-radius:12px;
  border:1px solid rgba(255,255,255,0.12);
  color: var(--text);
  padding:9px 10px;
  outline:none;
  cursor:pointer;
  background: linear-gradient(180deg, rgba(124,247,255,0.22), rgba(124,247,255,0.06));
  border-color: rgba(124,247,255,0.28);
}
button.secondary{
  background: rgba(0,0,0,0.22);
  border-color: rgba(255,255,255,0.12);
}
button:active{transform: translateY(1px);}

.pill{
  display:inline-flex; gap:8px; align-items:center;
  border-radius:999px; padding:8px 10px;
  border:1px solid rgba(255,255,255,0.10);
  background: rgba(0,0,0,0.20);
  font-size:13px; color:var(--muted);
}
.pill b{color:var(--text);}
.hint{font-size:12.5px; color:var(--muted); line-height:1.35;}
.kbd{display:inline-block; padding:2px 6px; border-radius:8px; border:1px solid rgba(255,255,255,0.18); background:rgba(0,0,0,0.20); color:var(--text); font-size:12px;}

#dpad{
  display:grid; grid-template-columns: 58px 58px 58px; grid-template-rows: 58px 58px 58px;
  gap:10px; justify-content:center; align-content:center; margin-top:6px;
  user-select:none;
}
.padbtn{
  border-radius:16px;
  border:1px solid rgba(255,255,255,0.12);
  background: rgba(0,0,0,0.22);
  display:grid; place-items:center;
  font-size:18px;
  cursor:pointer;
  touch-action: manipulation;
}
.padbtn:active{transform: translateY(1px); border-color: rgba(124,247,255,0.35);}
.padbtn.blank{opacity:0; pointer-events:none;}

#overlay{
  position:absolute; inset:0;
  display:grid; place-items:center;
  background: radial-gradient(900px 600px at 50% 40%, rgba(17,26,46,0.78), rgba(5,7,12,0.88));
  backdrop-filter: blur(6px);
}
#overlay.hidden{display:none;}
.modal{
  width:min(520px, calc(100% - 28px));
  border-radius:20px;
  background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
  border:1px solid rgba(255,255,255,0.14);
  box-shadow: 0 22px 70px rgba(0,0,0,0.55);
  padding:16px;
  display:grid; gap:12px;
}
.modal h2{margin:0; font-size:20px;}
.modal p{margin:0; color:var(--muted); line-height:1.35;}
.modal .actions{display:flex; gap:10px; flex-wrap:wrap; justify-content:flex-end;}
.tag{
  display:inline-flex; align-items:center; gap:8px;
  border-radius:999px;
  padding:6px 10px;
  border:1px solid rgba(255,255,255,0.10);
  background: rgba(0,0,0,0.18);
  color: var(--muted);
  font-size:12px;
}
.tag .warn{color:#ffd1d8;}
.tag .dot2{
  width:8px;height:8px;border-radius:999px;background:var(--danger);
  box-shadow:0 0 14px rgba(255,77,109,0.7);
}
```

  </style>
</head>
<body>
  <div id="app">
    <div class="card" style="position:relative;">
      <div class="topbar">
        <div class="brand">
          <div class="dot"></div>
          <div class="title">Neon Snake</div>
        </div>
        <div class="stats">
          <div><b>Score</b> <span id="score">0</span></div>
          <div><b>Best</b> <span id="best">0</span></div>
          <div><b>Level</b> <span id="level">1</span></div>
        </div>
      </div>

```
  <div class="main">
    <canvas id="c" width="600" height="600"></canvas>
    <div class="row">
      <span class="pill"><b>Controls:</b> <span class="kbd">Arrows</span> / <span class="kbd">WASD</span> • <span class="kbd">Space</span> pause</span>
      <button id="btnPause" class="secondary">Pause</button>
      <button id="btnRestart">Restart</button>
    </div>
  </div>

  <div id="overlay">
    <div class="modal">
      <h2 id="overlayTitle">Neon Snake</h2>
      <p id="overlayText">Eat the glowing food, grow longer, and don't crash into yourself.</p>
      <div class="row" style="justify-content:flex-start;">
        <span class="tag"><span class="dot2"></span><span class="warn"><b>Classic walls:</b> hitting the edge ends the game</span></span>
        <span class="tag"><b>Sound:</b> on</span>
        <span class="tag"><b>Difficulty:</b> medium</span>
      </div>
      <div class="actions">
        <button id="btnStart">Start</button>
      </div>
      <p class="hint">On phones/tablets: swipe on the board or use the D-pad on the right.</p>
    </div>
  </div>
</div>

<div class="card">
  <div class="side">
    <div class="section">
      <h3>Touch controls</h3>
      <div class="hint">Use the D-pad below, or swipe on the board.</div>
      <div id="dpad" aria-label="Touch controls">
        <div class="padbtn blank"></div>
        <div class="padbtn" data-dir="up">▲</div>
        <div class="padbtn blank"></div>

        <div class="padbtn" data-dir="left">◀</div>
        <div class="padbtn" data-dir="pause">⏸</div>
        <div class="padbtn" data-dir="right">▶</div>

        <div class="padbtn blank"></div>
        <div class="padbtn" data-dir="down">▼</div>
        <div class="padbtn blank"></div>
      </div>
    </div>

    <div class="section">
      <h3>Shortcuts</h3>
      <div class="hint">
        <span class="kbd">R</span> restart • <span class="kbd">Space</span> pause
      </div>
    </div>
  </div>
</div>
```

  </div>

  <script>
    const canvas = document.getElementById('c');
    const ctx = canvas.getContext('2d');

    const scoreEl = document.getElementById('score');
    const bestEl = document.getElementById('best');
    const levelEl = document.getElementById('level');

    const btnPause = document.getElementById('btnPause');
    const btnRestart = document.getElementById('btnRestart');

    const overlay = document.getElementById('overlay');
    const overlayTitle = document.getElementById('overlayTitle');
    const overlayText = document.getElementById('overlayText');
    const btnStart = document.getElementById('btnStart');

    const GRID = 24;
    const W = canvas.width;
    const CELL = W / GRID;

    const key = (x,y)=> `${x},${y}`;
    const clamp = (n,min,max)=> Math.max(min, Math.min(max,n));

    const difficulty = 'normal';
    const walls = true;
    let soundOn = true;

    const BASE_SPEED = { easy: 200, normal: 165, hard: 130 };

    let best = Number(localStorage.getItem('neon_snake_best') || 0);
    bestEl.textContent = best;

    let snake, dir, nextDir, food, score, level, tickMs, timer;
    let paused = true;
    let gameOver = false;

    let audioCtx = null;
    function ensureAudio() {
      if (!soundOn) return;
      if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      if (audioCtx.state === 'suspended') audioCtx.resume().catch(()=>{});
    }
    function beep({freq=440, dur=0.08, type='sine', gain=0.035}={}) {
      if (!soundOn) return;
      ensureAudio();
      if (!audioCtx) return;
      const t0 = audioCtx.currentTime;
      const o = audioCtx.createOscillator();
      const g = audioCtx.createGain();
      o.type = type;
      o.frequency.setValueAtTime(freq, t0);
      g.gain.setValueAtTime(0, t0);
      g.gain.linearRampToValueAtTime(gain, t0 + 0.01);
      g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
      o.connect(g).connect(audioCtx.destination);
      o.start(t0);
      o.stop(t0 + dur + 0.02);
    }
    function sfxEat(){ beep({freq: 640, dur:0.07, type:'triangle', gain:0.04}); }
    function sfxTurn(){ beep({freq: 300, dur:0.03, type:'sine', gain:0.02}); }
    function sfxLose(){
      if (!soundOn) return;
      ensureAudio();
      beep({freq: 210, dur:0.10, type:'sawtooth', gain:0.05});
      setTimeout(()=> beep({freq: 160, dur:0.14, type:'sawtooth', gain:0.05}), 90);
    }
    function sfxLevel(){
      beep({freq: 780, dur:0.06, type:'square', gain:0.03});
      setTimeout(()=> beep({freq: 980, dur:0.06, type:'square', gain:0.03}), 70);
    }

    function resetGame() {
      const cx = Math.floor(GRID/2);
      const cy = Math.floor(GRID/2);

      snake = [
        { x: cx, y: cy+2 },
        { x: cx, y: cy+3 },
        { x: cx, y: cy+4 }
      ];
      dir = { x: 0, y: -1 };
      nextDir = { ...dir };

      score = 0;
      level = 1;
      gameOver = false;

      tickMs = BASE_SPEED[difficulty];
      scoreEl.textContent = score;
      levelEl.textContent = level;

      spawnFood();
      startLoop();
      draw();
    }

    function startLoop(){
      if (timer) clearInterval(timer);
      timer = setInterval(step, tickMs);
    }
    function setSpeed(ms){
      tickMs = clamp(ms, 55, 220);
      startLoop();
    }

    function spawnFood(){
      const occupied = new Set(snake.map(s => key(s.x,s.y)));
      const head = snake[0];
      for (let tries=0; tries<999; tries++){
        const x = Math.floor(Math.random()*GRID);
        const y = Math.floor(Math.random()*GRID);
        if (occupied.has(key(x,y))) continue;
        const d = Math.abs(x-head.x) + Math.abs(y-head.y);
        if (d < 4 && tries < 80) continue;
        food = { x, y, pulse: 0 };
        return;
      }
      for (let y=0; y<GRID; y++){
        for (let x=0; x<GRID; x++){
          if (!occupied.has(key(x,y))) { food = { x,y,pulse:0 }; return; }
        }
      }
    }

    function step(){
      if (paused || gameOver) return;

      if (!(nextDir.x === -dir.x && nextDir.y === -dir.y)){
        if (nextDir.x !== dir.x || nextDir.y !== dir.y) sfxTurn();
        dir = nextDir;
      }

      const head = snake[0];
      const nx = head.x + dir.x;
      const ny = head.y + dir.y;

      if (nx < 0 || nx >= GRID || ny < 0 || ny >= GRID){
        endGame();
        return;
      }

      for (let i=0; i<snake.length; i++){
        if (snake[i].x === nx && snake[i].y === ny){
          endGame();
          return;
        }
      }

      snake.unshift({ x:nx, y:ny });

      if (nx === food.x && ny === food.y){
        score++;
        scoreEl.textContent = score;
        sfxEat();

        if (score > best){
          best = score;
          localStorage.setItem('neon_snake_best', String(best));
          bestEl.textContent = best;
        }

        const newLevel = 1 + Math.floor(score / 5);
        if (newLevel !== level){
          level = newLevel;
          levelEl.textContent = level;
          sfxLevel();
          setSpeed(tickMs - 8);
        }

        spawnFood();
      } else {
        snake.pop();
      }

      food.pulse += 0.22;
      draw();
    }

    function endGame(){
      gameOver = true;
      paused = true;
      sfxLose();
      draw();
      showOverlay("Game Over", `Score ${score}. Press Start to play again.`);
      btnStart.textContent = "Play again";
    }

    function roundRect(x, y, w, h, r){
      const rr = Math.min(r, w/2, h/2);
      ctx.beginPath();
      ctx.moveTo(x+rr, y);
      ctx.arcTo(x+w, y, x+w, y+h, rr);
      ctx.arcTo(x+w, y+h, x, y+h, rr);
      ctx.arcTo(x, y+h, x, y, rr);
      ctx.arcTo(x, y, x+w, y, rr);
      ctx.closePath();
    }

    // Tongue flicker timer
    let tongueOut = true;
    setInterval(()=>{ tongueOut = !tongueOut; }, 300);

    function drawSnake(){
      if (!snake || snake.length === 0) return;
      const R = CELL * 0.40;
      ctx.save();

      // --- Body tube: draw from tail to just behind head ---
      for (let i = snake.length - 1; i >= 1; i--) {
        const curr = snake[i];
        const prev = snake[i - 1];
        const cx = (curr.x + 0.5) * CELL;
        const cy = (curr.y + 0.5) * CELL;
        const px = (prev.x + 0.5) * CELL;
        const py = (prev.y + 0.5) * CELL;
        const t  = i / Math.max(1, snake.length - 1);

        // Taper tail
        const segR = i === snake.length - 1 ? R * 0.55 : R;

        // Outer green body
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(px, py);
        ctx.lineWidth  = segR * 2;
        ctx.lineCap    = 'round';
        const g = Math.round(130 + 70 * (1 - t));
        ctx.strokeStyle = `rgb(10, ${g}, 15)`;
        ctx.shadowColor = 'rgba(30,160,30,0.25)';
        ctx.shadowBlur  = 6;
        ctx.stroke();
        ctx.shadowBlur  = 0;

        // Belly stripe – lighter centre line
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(px, py);
        ctx.lineWidth  = segR * 0.55;
        ctx.strokeStyle = `rgba(160, 220, 100, ${0.28 - 0.15 * t})`;
        ctx.stroke();
      }

      // --- Scales: small arc on each body segment ---
      for (let i = snake.length - 1; i >= 1; i--) {
        const s = snake[i];
        const cx = (s.x + 0.5) * CELL;
        const cy = (s.y + 0.5) * CELL;
        // Angle perpendicular to movement direction for this segment
        const nx = snake[i - 1];
        const angle = Math.atan2(nx.y - s.y, nx.x - s.x) + Math.PI / 2;

        ctx.beginPath();
        ctx.arc(cx, cy, R * 0.62, angle - 0.9, angle + 0.9);
        ctx.strokeStyle = 'rgba(0, 60, 0, 0.55)';
        ctx.lineWidth = 1.8;
        ctx.lineCap = 'round';
        ctx.stroke();
      }

      // --- Head ---
      const head = snake[0];
      const hx = (head.x + 0.5) * CELL;
      const hy = (head.y + 0.5) * CELL;

      // Head blob – slightly wider than body
      ctx.shadowColor = 'rgba(60,220,60,0.55)';
      ctx.shadowBlur  = 18;
      ctx.beginPath();
      ctx.arc(hx, hy, R * 1.15, 0, Math.PI * 2);
      const hg = ctx.createRadialGradient(hx - R*0.3, hy - R*0.3, R*0.05, hx, hy, R*1.15);
      hg.addColorStop(0, '#5ef55e');
      hg.addColorStop(0.45, '#1daa1d');
      hg.addColorStop(1, '#085508');
      ctx.fillStyle = hg;
      ctx.fill();
      ctx.shadowBlur = 0;

      // Head outline
      ctx.beginPath();
      ctx.arc(hx, hy, R * 1.15, 0, Math.PI * 2);
      ctx.strokeStyle = 'rgba(0,80,0,0.6)';
      ctx.lineWidth = 1.5;
      ctx.stroke();

      // Snout highlight
      let snoutX = hx + dir.x * R * 0.6;
      let snoutY = hy + dir.y * R * 0.6;
      ctx.beginPath();
      ctx.arc(snoutX, snoutY, R * 0.38, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(120,255,120,0.18)';
      ctx.fill();

      // Eyes – offset to sides of direction
      const eyeDist = R * 0.52;
      // perpendicular to direction
      const px = -dir.y, py = dir.x;
      const eyeFwd = R * 0.28;
      const eyeR   = R * 0.24;

      [1, -1].forEach(side => {
        const ex = hx + dir.x * eyeFwd + px * side * eyeDist;
        const ey = hy + dir.y * eyeFwd + py * side * eyeDist;

        // Iris
        ctx.beginPath();
        ctx.arc(ex, ey, eyeR, 0, Math.PI * 2);
        ctx.fillStyle = '#f5e500';
        ctx.fill();

        // Slit pupil – rotated to match direction
        const pupilAngle = Math.atan2(dir.y, dir.x) + Math.PI / 2;
        ctx.save();
        ctx.translate(ex, ey);
        ctx.rotate(pupilAngle);
        ctx.beginPath();
        ctx.ellipse(0, 0, eyeR * 0.28, eyeR * 0.82, 0, 0, Math.PI * 2);
        ctx.fillStyle = '#111';
        ctx.fill();
        ctx.restore();

        // Eye shine
        ctx.beginPath();
        ctx.arc(ex - eyeR * 0.28, ey - eyeR * 0.28, eyeR * 0.22, 0, Math.PI * 2);
        ctx.fillStyle = 'rgba(255,255,255,0.75)';
        ctx.fill();
      });

      // Tongue
      if (tongueOut) {
        const tBaseX = hx + dir.x * R * 1.05;
        const tBaseY = hy + dir.y * R * 1.05;
        const tTipX  = hx + dir.x * R * 1.80;
        const tTipY  = hy + dir.y * R * 1.80;
        const forkSize = R * 0.40;

        ctx.strokeStyle = '#ff2222';
        ctx.lineWidth   = 1.6;
        ctx.lineCap     = 'round';

        // Stem
        ctx.beginPath();
        ctx.moveTo(tBaseX, tBaseY);
        ctx.lineTo(tTipX, tTipY);
        ctx.stroke();

        // Fork A
        ctx.beginPath();
        ctx.moveTo(tTipX, tTipY);
        ctx.lineTo(tTipX + dir.x * forkSize * 0.6 + px * forkSize * 0.55,
                   tTipY + dir.y * forkSize * 0.6 + py * forkSize * 0.55);
        ctx.stroke();

        // Fork B
        ctx.beginPath();
        ctx.moveTo(tTipX, tTipY);
        ctx.lineTo(tTipX + dir.x * forkSize * 0.6 - px * forkSize * 0.55,
                   tTipY + dir.y * forkSize * 0.6 - py * forkSize * 0.55);
        ctx.stroke();
      }

      ctx.restore();
    }

    function draw(){
      ctx.clearRect(0,0,W,W);

      const g = ctx.createLinearGradient(0,0,0,W);
      g.addColorStop(0, 'rgba(18,26,46,0.55)');
      g.addColorStop(1, 'rgba(6,8,14,0.65)');
      ctx.fillStyle = g;
      ctx.fillRect(0,0,W,W);

      ctx.globalAlpha = 0.10;
      ctx.strokeStyle = '#ffffff';
      ctx.beginPath();
      for (let i=1;i<GRID;i++){
        ctx.moveTo(i*CELL, 0); ctx.lineTo(i*CELL, W);
        ctx.moveTo(0, i*CELL); ctx.lineTo(W, i*CELL);
      }
      ctx.stroke();
      ctx.globalAlpha = 1;

      if (!food || !snake) return;

      const fx = food.x * CELL + CELL / 2;
      const fy = food.y * CELL + CELL / 2;
      const pulse = 0.9 + 0.1 * Math.sin(food.pulse || 0);
      const ar = CELL * 0.36 * pulse;

      ctx.save();
      ctx.shadowColor = 'rgba(255,80,80,0.85)';
      ctx.shadowBlur = 18;

      // Apple body
      ctx.beginPath();
      ctx.arc(fx - ar*0.08, fy + ar*0.08, ar, 0, Math.PI*2);
      ctx.closePath();
      const appleGrad = ctx.createRadialGradient(fx - ar*0.25, fy - ar*0.25, ar*0.05, fx, fy, ar);
      appleGrad.addColorStop(0, '#ff6e6e');
      appleGrad.addColorStop(0.5, '#e8001c');
      appleGrad.addColorStop(1, '#8b0010');
      ctx.fillStyle = appleGrad;
      ctx.fill();

      // Indent at top
      ctx.beginPath();
      ctx.arc(fx - ar*0.08, fy - ar*0.82, ar*0.28, 0, Math.PI*2);
      ctx.fillStyle = 'rgba(0,0,0,0.35)';
      ctx.fill();

      // Shine highlight
      ctx.beginPath();
      ctx.ellipse(fx - ar*0.32, fy - ar*0.30, ar*0.18, ar*0.11, -0.6, 0, Math.PI*2);
      ctx.fillStyle = 'rgba(255,255,255,0.55)';
      ctx.fill();

      // Stem
      ctx.beginPath();
      ctx.moveTo(fx - ar*0.05, fy - ar*0.78);
      ctx.bezierCurveTo(fx - ar*0.05, fy - ar*1.18, fx + ar*0.38, fy - ar*1.22, fx + ar*0.30, fy - ar*1.05);
      ctx.strokeStyle = '#5a3010';
      ctx.lineWidth = CELL * 0.07;
      ctx.lineCap = 'round';
      ctx.stroke();

      // Leaf
      ctx.beginPath();
      ctx.ellipse(fx + ar*0.18, fy - ar*1.10, ar*0.22, ar*0.11, -0.8, 0, Math.PI*2);
      ctx.fillStyle = '#3ecf3e';
      ctx.fill();

      ctx.restore();

      drawSnake();

      ctx.save();
      ctx.globalAlpha = 0.45;
      ctx.strokeStyle = 'rgba(255,77,109,0.55)';
      ctx.lineWidth = 6;
      ctx.strokeRect(3,3,W-6,W-6);
      ctx.restore();

      if (paused && overlay.classList.contains('hidden')){
        drawCenterToast("Paused");
      }
    }

    function drawCenterToast(text){
      ctx.save();
      ctx.fillStyle = 'rgba(0,0,0,0.45)';
      const w = 260, h = 56;
      const x = (W-w)/2, y = (W-h)/2;
      roundRect(x,y,w,h,16);
      ctx.fill();
      ctx.fillStyle = 'rgba(255,255,255,0.90)';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.font = '700 18px system-ui, -apple-system, Segoe UI, Roboto, Arial';
      ctx.fillText(text, W/2, W/2);
      ctx.restore();
    }

    function showOverlay(title, text) {
      overlayTitle.textContent = title;
      overlayText.textContent = text;
      overlay.classList.remove('hidden');
      paused = true;
      draw();
    }
    function hideOverlay() {
      overlay.classList.add('hidden');
      paused = false;
    }

    function setDir(dx,dy){ nextDir = { x:dx, y:dy }; }

    function togglePause(){
      if (!overlay.classList.contains('hidden')) return;
      paused = !paused;
      btnPause.textContent = paused ? "Resume" : "Pause";
      draw();
    }

    function onKey(e){
      const k = e.key.toLowerCase();
      if (k === ' ' || k === 'spacebar'){ togglePause(); e.preventDefault(); return; }
      if (k === 'r'){ ensureAudio(); hideOverlay(); resetGame(); return; }

      if (k === 'arrowup' || k === 'w') setDir(0,-1);
      else if (k === 'arrowdown' || k === 's') setDir(0,1);
      else if (k === 'arrowleft' || k === 'a') setDir(-1,0);
      else if (k === 'arrowright' || k === 'd') setDir(1,0);
    }
    window.addEventListener('keydown', onKey, { passive:false });

    btnPause.addEventListener('click', ()=>{ ensureAudio(); togglePause(); });
    btnRestart.addEventListener('click', ()=>{ ensureAudio(); hideOverlay(); resetGame(); btnStart.textContent="Start"; btnPause.textContent="Pause"; });

    document.getElementById('dpad').addEventListener('click', (e)=>{
      const btn = e.target.closest('.padbtn');
      if (!btn || btn.classList.contains('blank')) return;
      ensureAudio();
      const d = btn.dataset.dir;
      if (d === 'up') setDir(0,-1);
      else if (d === 'down') setDir(0,1);
      else if (d === 'left') setDir(-1,0);
      else if (d === 'right') setDir(1,0);
      else if (d === 'pause') togglePause();
    });

    let touchStart = null;
    canvas.addEventListener('pointerdown', (e)=>{
      ensureAudio();
      canvas.setPointerCapture(e.pointerId);
      touchStart = { x:e.clientX, y:e.clientY, t:performance.now() };
    });
    canvas.addEventListener('pointerup', (e)=>{
      if (!touchStart) return;
      const tapX = e.clientX;
      const tapY = e.clientY;
      touchStart = null;

      // Convert tap to canvas coordinates
      const rect = canvas.getBoundingClientRect();
      const scaleX = W / rect.width;
      const scaleY = W / rect.height;
      const canvasTapX = (tapX - rect.left) * scaleX;
      const canvasTapY = (tapY - rect.top) * scaleY;

      // Head center in canvas pixels
      const head = snake[0];
      const headPx = (head.x + 0.5) * CELL;
      const headPy = (head.y + 0.5) * CELL;

      const dx = canvasTapX - headPx;
      const dy = canvasTapY - headPy;
      const adx = Math.abs(dx);
      const ady = Math.abs(dy);

      // Tiny tap near head = pause
      if (adx < CELL && ady < CELL){ togglePause(); return; }

      if (adx > ady){
        setDir(dx > 0 ? 1 : -1, 0);
      } else {
        setDir(0, dy > 0 ? 1 : -1);
      }
    });

    btnStart.addEventListener('click', ()=>{
      ensureAudio();
      hideOverlay();
      btnPause.textContent = "Pause";
      resetGame();
    });

    showOverlay("Neon Snake", "Eat the glowing food, grow longer, and don't crash into yourself.");
  </script>

</body>
</html>

Game Source: Neon Snake

Creator: SolarScout64

Libraries: none

Complexity: complex (773 lines, 23.7 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a KIDHUBB header at the top with "remix_of: neon-snake-solarscout64" to link back to the original. Then publish at kidhubb.com/publish.