🎮KidHubb

Neon Snake

by NovaFox99
592 lines19.1 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));

    // Defaults requested:
    const difficulty = 'normal';        // medium
    const walls = true;                 // classic walls
    let soundOn = true;                 // sound on

    const BASE_SPEED = { easy: 140, normal: 115, hard: 92 };

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

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

    // ===== Audio (WebAudio, no external files) =====
    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);
    }

    // ===== Game init/reset =====
    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; }
        }
      }
    }

    // ===== Core game step =====
    function step(){
      if (paused || gameOver) return;

      // Commit buffered direction (disallow instant reversal)
      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;

      // Walls: edge collision ends game
      if (nx < 0 || nx >= GRID || ny < 0 || ny >= GRID){
        endGame();
        return;
      }

      // Collision with self
      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 });

      // Eat
      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;
        }

        // Level up every 5 points, speed increases slightly
        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";
    }

    // ===== Rendering =====
    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();
    }

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

      // Background gradient
      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);

      // Grid lines
      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;

      // Food glow
      const fx = food.x * CELL;
      const fy = food.y * CELL;
      const pulse = 0.45 + 0.15*Math.sin(food.pulse || 0);
      const pad = CELL*0.16;
      const r = CELL*0.28;

      ctx.save();
      ctx.shadowColor = 'rgba(124,247,255,0.95)';
      ctx.shadowBlur = 18;
      ctx.fillStyle = `rgba(124,247,255,${0.75*pulse})`;
      roundRect(fx+pad, fy+pad, CELL-2*pad, CELL-2*pad, r);
      ctx.fill();
      ctx.restore();

      // Snake
      for (let i=snake.length-1; i>=0; i--){
        const s = snake[i];
        const x = s.x * CELL;
        const y = s.y * CELL;
        const inset = CELL*0.10;
        const rr = CELL*0.30;

        const t = i / Math.max(1, snake.length-1);
        const alpha = i === 0 ? 0.95 : (0.70 - 0.30*t);

        ctx.save();
        ctx.shadowColor = i === 0 ? 'rgba(139,123,255,0.70)' : 'rgba(139,123,255,0.25)';
        ctx.shadowBlur = i === 0 ? 22 : 10;
        ctx.fillStyle = i === 0
          ? `rgba(139,123,255,${alpha})`
          : `rgba(124,247,255,${alpha})`;
        roundRect(x+inset, y+inset, CELL-2*inset, CELL-2*inset, rr);
        ctx.fill();
        ctx.restore();
      }

      // Walls border (visual)
      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();

      // Small pause toast (only when not overlay)
      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();
    }

    // ===== Overlay =====
    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;
    }

    // ===== Input handling =====
    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"; });

    // Touch D-pad
    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();
    });

    // Swipe on canvas
    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 dx = e.clientX - touchStart.x;
      const dy = e.clientY - touchStart.y;
      const adx = Math.abs(dx), ady = Math.abs(dy);
      const dist = Math.hypot(dx,dy);
      const dt = performance.now() - touchStart.t;
      touchStart = null;

      // tap toggles pause
      if (dist < 14 && dt < 300){ togglePause(); return; }

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

    btnStart.addEventListener('click', ()=>{
      ensureAudio(); // required user gesture for audio
      hideOverlay();
      btnPause.textContent = "Pause";
      resetGame();
    });

    // Start in overlay (paused)
    showOverlay("Neon Snake", "Eat the glowing food, grow longer, and don’t crash into yourself.");
  </script>
</body>
</html>

Game Source: Neon Snake

Creator: NovaFox99

Libraries: none

Complexity: complex (592 lines, 19.1 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: snake-novafox99" to link back to the original. Then publish at kidhubb.com/publish.