Make it 3d: <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta
    name="viewport"
    content="width=device-width,initial-scale=1,user-scalable=no,viewport-fit=cover"
  />
  <title>Cyber‑Neon Snake</title>
  <style>
    /* Reset & Base */
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    html, body {
      height: 100%;
      overflow: hidden;
      user-select: none;
      background: linear-gradient(45deg, #0f2027, #203a43, #2c5364);
      background-size: 600% 600%;
      animation: bgShift 20s ease infinite;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      color: #0f9;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    @keyframes bgShift {
      0% { background-position: 0% 50%; }
      50% { background-position: 100% 50%; }
      100% { background-position: 0% 50%; }
    }

    /* Game Frame */
    #game {
      position: relative;
      width: 90vmin;
      height: 90vmin;
      background: #111;
      border: 4px solid #0f9;
      border-radius: 20px;
      box-shadow: inset 0 0 20px #0ff9;
      touch-action: none;
      user-select: none;
      overflow: hidden;
    }
    canvas {
      width: 100%;
      height: 100%;
      display: block;
      background: #111;
      border-radius: 20px;
      image-rendering: pixelated;
    }

    /* Neon Scoreboard */
    @keyframes neonFlicker {
      0%, 18%, 22%, 25%, 53%, 57%, 100% {
        text-shadow:
          0 0 5px #0f9,
          0 0 10px #0f9,
          0 0 20px #0f9,
          0 0 40px #0ff,
          0 0 80px #0ff;
      }
      20%, 24%, 55% {
        text-shadow: none;
      }
    }
    #scoreboard {
      margin-top: 12px;
      text-align: center;
      font-size: clamp(1.2rem, 1.5vw, 1.8rem);
      font-weight: 700;
      animation: neonFlicker 3s infinite;
      user-select: none;
      letter-spacing: 0.05em;
      display: flex;
      justify-content: center;
      gap: 1.5rem;
    }
    #scoreboard div {
      min-width: 6rem;
    }
    #scoreboard.flash {
      color: #fff;
      text-shadow:
        0 0 25px #0ff,
        0 0 50px #0ff,
        0 0 75px #0ff;
      transition: text-shadow 0.2s, color 0.2s;
    }

    /* Restart button */
    #restartBtn {
      margin-top: 16px;
      background: #0ff;
      color: #000;
      border-radius: 12px;
      padding: 12px 0;
      font-size: 1.3rem;
      font-weight: 700;
      box-shadow: 0 0 25px #0ff;
      cursor: pointer;
      user-select: none;
      width: 90vmin;
      max-width: 400px;
      display: none;
      transition: background 0.3s, box-shadow 0.3s, transform 0.2s;
    }
    #restartBtn:hover,
    #restartBtn:focus-visible {
      background: #0cc;
      box-shadow: 0 0 40px #0cc;
      outline: none;
      transform: scale(1.05);
    }
    #restartBtn:active {
      background: #099;
      box-shadow: inset 0 0 30px #099;
      transform: scale(0.95);
    }

    /* Game Over Overlay */
    #overlay {
      position: absolute;
      top: 0; left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0,0,0,0.8);
      display: none;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      border-radius: 20px;
      z-index: 5;
      user-select: none;
      color: #0ff;
      font-weight: 700;
      font-size: clamp(1.8rem, 3vw, 2.8rem);
      text-align: center;
      text-shadow:
        0 0 10px #0ff,
        0 0 20px #0ff,
        0 0 40px #0ff;
      animation: fadeIn 0.4s ease forwards;
    }
    #overlay p {
      margin-bottom: 1rem;
    }
    @keyframes fadeIn {
      from {opacity:0}
      to {opacity:1}
    }

    /* Countdown */
    #countdown {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      pointer-events: none;
      user-select: none;
      color: #0ff;
      font-weight: 900;
      text-shadow:
        0 0 15px #0ff,
        0 0 25px #0ff,
        0 0 40px #0ff;
      font-size: clamp(5rem, 20vw, 10rem);
      opacity: 0;
      transition: opacity 0.3s ease;
      z-index: 10;
    }

    /* Accessibility instructions */
    #instructions {
      margin-top: 12px;
      max-width: 380px;
      font-size: 0.9rem;
      color: #0a5;
      user-select: none;
      text-align: center;
      line-height: 1.4;
      text-shadow:
        0 0 5px #0a5;
    }

    /* FPS Counter */
    #fpsCounter {
      position: fixed;
      top: 8px;
      left: 8px;
      color: #0ff;
      font-weight: 700;
      font-size: 1rem;
      text-shadow: 0 0 5px #0ff;
      user-select: none;
      font-family: monospace;
      display: none;
      z-index: 20;
      pointer-events: none;
    }
  </style>
</head>
<body>
  <div id="game" role="region" aria-label="Snake game area">
    <canvas id="canvas" tabindex="0" aria-live="polite" aria-label="Game canvas"></canvas>
    <div id="overlay" role="alert" aria-live="assertive"><p id="gameOverText"></p></div>
    <div id="countdown" aria-hidden="true"></div>
  </div>

  <div id="scoreboard" aria-live="polite" aria-atomic="true" role="status" aria-label="Current and high score">
    <div>Score: <span id="currentScore">0</span></div>
    <div>High: <span id="highScore">0</span></div>
  </div>

  <button id="restartBtn" type="button" aria-label="Restart game">Restart Game</button>

  <div id="instructions" aria-hidden="false">
    Use arrow keys or WASD to move<br>
    Swipe on touch devices<br>
    Press <kbd>F</kbd> to toggle FPS display
  </div>

  <div id="fpsCounter" aria-hidden="true">FPS: 0</div>

  <!-- Sound effect file must be in same folder -->
  <audio id="eatSfx" src="eat.mp3" preload="auto" aria-hidden="true"></audio>
  <audio id="gameOverSfx" src="gameover.mp3" preload="auto" aria-hidden="true"></audio>
  <audio id="turnSfx" src="turn.mp3" preload="auto" aria-hidden="true"></audio>

  <script>
  (function(){
    // DOM & audio refs
    const game       = document.getElementById('game');
    const canvas     = document.getElementById('canvas');
    const ctx        = canvas.getContext('2d');
    const scoreEl    = document.getElementById('currentScore');
    const highEl     = document.getElementById('highScore');
    const sb         = document.getElementById('scoreboard');
    const eatSfx     = document.getElementById('eatSfx');
    const restartBtn = document.getElementById('restartBtn');
    const overlay    = document.getElementById('overlay');
    const gameOverText = document.getElementById('gameOverText');
    const countdownEl = document.getElementById('countdown');
    const gameOverSfx = document.getElementById('gameOverSfx');
    const turnSfx = document.getElementById('turnSfx');
    const fpsCounter = document.getElementById('fpsCounter');

    // Config & state
    const tileCount    = 24;
    let gridSize;
    const baseFps      = 9;
    const maxApples    = 3;
    let snake, velocity, nextVel, apples, score, gameOver;
    let comboCounter, lastAppleTime, hue;
    let effects;
    let speedMultiplier = 1;
    let currentInterval;
    let gameStarted = false;
    let animFrameId;
    let showFPS = false;
    let fps = 0;

    let highScore     = parseInt(localStorage.getItem('snakeHighScore'),10) || 0;
    highEl.textContent = highScore;

    // Timing variables for fixed FPS using delta accumulator
    let lastTime = 0;
    let delta = 0;
    let interval;

    // For smooth animation of snake movement between tiles
    const smoothSpeed = 0.12; // smaller is slower (between 0 and 1)
    let smoothPos = []; // will store intermediate positions of snake parts (x and y floats)

    // Initialize or reset game state
    function initGame(){
      snake = [{x:12,y:12},{x:11,y:12},{x:10,y:12}];
      velocity = {x:1,y:0};
      nextVel = {x:1,y:0};
      apples = [];
      score = 0;
      comboCounter = 0;
      lastAppleTime = 0;
      hue = 0;
      effects = { score: [], particles: [], trail: [] };
      speedMultiplier = 1;
      gameOver = false;
      interval = 1000 / baseFps;
      scoreEl.textContent = score;
      highEl.textContent = highScore;
      restartBtn.style.display = 'none';
      overlay.style.display = 'none';
      countdownEl.style.opacity = 0;
      smoothPos = snake.map(p => ({x:p.x, y:p.y}));

      spawnApples();

      // Start with countdown
      runCountdown(() => {
        gameStarted = true;
        lastTime = performance.now();
        delta = 0;
        loop(lastTime);
      });
    }

    // Countdown animation 3..2..1.. GO!
    function runCountdown(onComplete){
      countdownEl.style.opacity = '1';
      let count = 3;
      countdownEl.textContent = count;
      const countdownInterval = setInterval(() => {
        count--;
        if(count > 0){
          countdownEl.textContent = count;
        } else if(count === 0){
          countdownEl.textContent = 'GO!';
        } else {
          countdownEl.style.opacity = '0';
          clearInterval(countdownInterval);
          if(onComplete) onComplete();
        }
      }, 900);
    }

    // Dynamic canvas sizing and scale
    function resizeCanvas(){
      const size = game.clientWidth;
      canvas.width  = size;
      canvas.height = size;
      gridSize      = size / tileCount;
    }
    resizeCanvas();
    window.addEventListener('resize', resizeCanvas);

    // Helpers (color gradients, rounded rect, empty‑cell check)
    function colorGradient(isSnake){
      // Cycle hue slower for smooth effect
      hue = (hue + 0.8) % 360;
      if(isSnake){
        const c1 = `hsl(${hue}, 85%, 57%)`;
        const c2 = `hsl(${(hue+40)%360}, 100%, 65%)`;
        let g = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        g.addColorStop(0, c1);
        g.addColorStop(1, c2);
        return g;
      } else {
        let g = ctx.createRadialGradient(0,0,gridSize*0.1, 0,0, gridSize*0.5);
        g.addColorStop(0, `hsl(${hue},90%,70%)`);
        g.addColorStop(1, `hsl(${(hue+60)%360}, 85%, 40%)`);
        return g;
      }
    }
    function roundedRect(x,y,w,h,r){
      ctx.beginPath();
      ctx.moveTo(x+r,y);
      ctx.lineTo(x+w-r,y);
      ctx.quadraticCurveTo(x+w,y,x+w,y+r);
      ctx.lineTo(x+w,y+h-r);
      ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
      ctx.lineTo(x+r,y+h);
      ctx.quadraticCurveTo(x,y+h,x,y+h-r);
      ctx.lineTo(x,y+r);
      ctx.quadraticCurveTo(x,y,x+r,y);
      ctx.closePath();
      ctx.fill();
    }
    function isEmpty(x,y){
      return !snake.some(s=>s.x===x&&s.y===y)
          && !apples.some(a=>a.x===x&&a.y===y);
    }

    // Apple spawning (up to maxApples apples)
    function spawnApples(){
      while(apples.length < maxApples){
        let p = {
          x: Math.floor(Math.random()*tileCount),
          y: Math.floor(Math.random()*tileCount)
        };
        if(isEmpty(p.x,p.y)){
          apples.push(p);
        }
      }
    }

    // Effects: score pop-up, particles, trail for snake head
    function addScoreFx(x,y,amt){
      effects.score.push({
        x: x*gridSize + gridSize/2,
        y: y*gridSize + gridSize/2,
        alpha: 1, text: '+'+amt, size: 24
      });
    }
    function addParticles(x,y){
      for(let i=0; i<14; i++){
        effects.particles.push({
          x: x*gridSize + gridSize/2,
          y: y*gridSize + gridSize/2,
          dx: (Math.random()-0.5)*8,
          dy: (Math.random()-0.5)*8,
          alpha: 1,
          size: Math.random()*3+1.5,
          colorHue: (hue + i*28) % 360,
        });
      }
    }
    // Particle Trail for snake head, always has few particles trailing the head
    function addTrail(){
      const head = snake[0];
      effects.trail.push({
        x: head.x*gridSize + gridSize/2 + (Math.random()-0.5)*4,
        y: head.y*gridSize + gridSize/2 + (Math.random()-0.5)*4,
        dx: (Math.random()-0.5)*0.8,
        dy: (Math.random()-0.5)*0.8,
        size: Math.random()*1.1+0.8,
        alpha: 0.6 + Math.random()*0.4,
        colorHue: (hue + 50) % 360,
      });
      // Limit trail length
      if(effects.trail.length > 30) effects.trail.shift();
    }
    function updateEffects(){
      // Score floats
      for(let i=effects.score.length-1; i >= 0; i--){
        let e = effects.score[i];
        e.y -= 1.4;
        e.alpha -= 0.02;
        e.size += 0.26;
        if(e.alpha <= 0) effects.score.splice(i,1);
      }

      // Particles explode outwards on apple eat
      for(let i=effects.particles.length - 1; i >= 0; i--){
        let p = effects.particles[i];
        p.x += p.dx;
        p.y += p.dy;
        p.alpha -= 0.03;
        if(p.alpha <= 0) effects.particles.splice(i,1);
      }

      // Trail fades & moves gently
      for(let i=effects.trail.length - 1; i >= 0; i--){
        let t = effects.trail[i];
        t.x += t.dx;
        t.y += t.dy;
        t.alpha -= 0.018;
        if(t.alpha <= 0) effects.trail.splice(i,1);
      }
    }
    function flashScore(){
      sb.classList.add('flash');
      setTimeout(() => sb.classList.remove('flash'), 180);
      try {
        eatSfx.currentTime = 0;
        eatSfx.play();
      } catch(e){ }
    }

    // Progressively increase speed every 40 points, capped
    function updateSpeed(){
      const increaseEvery = 40;
      speedMultiplier = Math.min(2.6, 1 + Math.floor(score / increaseEvery) * 0.22);
      interval = 1000 / (baseFps * speedMultiplier);
    }

    // Main game logic update (at fixed FPS)
    function update(){
      if(gameOver || !gameStarted) return;

      // Only change velocity if not reversing on axis
      if((nextVel.x !== -velocity.x || nextVel.y !== -velocity.y)
       && (nextVel.x !== velocity.x || nextVel.y !== velocity.y)) {
        velocity = nextVel;
      }

      // Calculate new head position with wraparound
      let head = {
        x: (snake[0].x + velocity.x + tileCount) % tileCount,
        y: (snake[0].y + velocity.y + tileCount) % tileCount
      };

      // Check collision with self: game over
      if(snake.some(p => p.x === head.x && p.y === head.y)) {
        onGameOver();
        return;
      }

      // Add new head to snake
      snake.unshift(head);
      smoothPos.unshift({x: head.x, y: head.y}); // add position for smooth animation

      // Eating apple?
      let ate = false;
      for(let i = 0; i < apples.length; i++){
        if(apples[i].x === head.x && apples[i].y === head.y){
          let now = Date.now();
          let timeBonus = (now - lastAppleTime < 3000) ? 5 : 0;
          let comboBonus = comboCounter * 2;
          let gain = 10 + timeBonus + comboBonus;
          score += gain;
          comboCounter++;
          lastAppleTime = now;
          scoreEl.textContent = score;
          if(score > highScore){
            highScore = score;
            localStorage.setItem('snakeHighScore', highScore);
            highEl.textContent = highScore;
          }
          addScoreFx(head.x, head.y, gain);
          addParticles(head.x, head.y);
          flashScore();
          apples.splice(i,1);
          spawnApples();
          ate = true;
          updateSpeed();
          break;
        }
      }

      if(!ate){
        comboCounter = 0;
        snake.pop();
        smoothPos.pop();
      }

      // Add particle trail behind head
      addTrail();
    }

    // Draw everything: called every animation frame
    function draw(){
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      // grid - subtle lines
      ctx.strokeStyle = '#222';
      ctx.lineWidth = 1;
      let tileSize = gridSize;
      for(let i = 0; i <= tileCount; i++){
        const pos = i * tileSize;
        ctx.beginPath();
        ctx.moveTo(pos, 0);
        ctx.lineTo(pos, canvas.height);
        ctx.moveTo(0, pos);
        ctx.lineTo(canvas.width, pos);
        ctx.stroke();
      }

      // Draw apples with a pulsing, rotating effect
      let pulse = 0.12 * Math.sin(performance.now() / 400) + 1;
      apples.forEach(a => {
        const cx = a.x * tileSize + tileSize/2;
        const cy = a.y * tileSize + tileSize/2;
        ctx.save();
        ctx.translate(cx, cy);
        ctx.rotate((performance.now()/1000 + a.x + a.y) % (2*Math.PI));
        ctx.scale(pulse, pulse);

        // Outer Glow ellipse
        ctx.shadowColor = '#f44';
        ctx.shadowBlur = 20;
        ctx.fillStyle = colorGradient(false);
        ctx.beginPath();
        ctx.ellipse(0, 0, tileSize*0.42, tileSize*0.58, Math.PI/6, 0, 2*Math.PI);
        ctx.fill();

        // Leaf shape
        ctx.fillStyle = '#282';
        ctx.beginPath();
        ctx.ellipse(0, -tileSize*0.32, tileSize*0.18, tileSize*0.35, Math.PI/4, 0, 2*Math.PI);
        ctx.fill();

        ctx.restore();
      });

      // Draw snake trail particles
      effects.trail.forEach(t => {
        ctx.fillStyle = `hsla(${t.colorHue}, 100%, 65%, ${t.alpha.toFixed(2)})`;
        ctx.beginPath();
        ctx.arc(t.x, t.y, t.size, 0, 2*Math.PI);
        ctx.fill();
      });

      // Draw snake with smooth animation between tiles
      // Smoothly interpolate positions towards target grid cells for animation

      for(let i=0; i < snake.length; i++){
        let target = snake[i];
        let pos = smoothPos[i];

        // Lerp smoothPos gradually towards actual grid position
        pos.x += (target.x - pos.x) * smoothSpeed;
        pos.y += (target.y - pos.y) * smoothSpeed;

        let px = pos.x * tileSize;
        let py = pos.y * tileSize;

        ctx.save();

        let grad = colorGradient(true);
        ctx.fillStyle = grad;

        // Head glows stronger
        ctx.shadowColor = (i === 0 ? '#0ff' : '#0ffaa');
        ctx.shadowBlur = (i === 0 ? 28 : 16);

        roundedRect(px + 3, py + 3, tileSize - 6, tileSize - 6, 9);

        // Draw eyes for head only
        if(i === 0){
          ctx.fillStyle = '#0ff';
          const eyeSize = tileSize * 0.13;
          const eyeY = py + tileSize*0.38;
          // Eyes slightly rotated based on direction
          let eyeOffsetX = velocity.x * tileSize * 0.18;
          let eyeOffsetY = velocity.y * tileSize * 0.18;

          ctx.beginPath();
          ctx.ellipse(px + tileSize*0.35 + eyeOffsetX, eyeY + eyeOffsetY, eyeSize, eyeSize*1.3, 0, 0, 2*Math.PI);
          ctx.ellipse(px + tileSize*0.65 + eyeOffsetX, eyeY + eyeOffsetY, eyeSize, eyeSize*1.3, 0, 0, 2*Math.PI);
          ctx.fill();

          ctx.fillStyle = '#002b2b';
          const pupilSize = eyeSize * 0.6;
          ctx.beginPath();
          ctx.ellipse(px + tileSize*0.35 + eyeOffsetX*1.1, eyeY + eyeOffsetY, pupilSize, pupilSize*1.1, 0, 0, 2*Math.PI);
          ctx.ellipse(px + tileSize*0.65 + eyeOffsetX*1.1, eyeY + eyeOffsetY, pupilSize, pupilSize*1.1, 0, 0, 2*Math.PI);
          ctx.fill();
        }
        ctx.restore();
      }

      // Effects: particles exploded and score pops
      effects.particles.forEach(p => {
        ctx.fillStyle = `hsla(${p.colorHue}, 100%, 60%, ${p.alpha.toFixed(2)})`;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size, 0, 2*Math.PI);
        ctx.fill();
      });
      effects.score.forEach(e=>{
        ctx.fillStyle = `rgba(0,255,150,${e.alpha.toFixed(2)})`;
        ctx.font = `${e.size}px Arial`;
        ctx.textAlign = 'center';
        ctx.shadowColor = '#0f9';
        ctx.shadowBlur = 10;
        ctx.fillText(e.text, e.x, e.y);
      });
    }

    // Input handling and direction change sounds + no reversing
    let touchStart = null;
    const minSwipe = 30;

    canvas.addEventListener('touchstart', e => {
      if(e.touches.length === 1) {
        touchStart = { x:e.touches[0].clientX, y:e.touches[0].clientY };
      }
    }, {passive:true});

    canvas.addEventListener('touchmove', e => {
      if(!touchStart) return;
      const dx = e.touches[0].clientX - touchStart.x;
      const dy = e.touches[0].clientY - touchStart.y;
      if(Math.abs(dx) > minSwipe || Math.abs(dy) > minSwipe){
        const dir = Math.abs(dx) > Math.abs(dy)
                  ? (dx > 0 ? 'right' : 'left')
                  : (dy > 0 ? 'down' : 'up');
        changeDir(dir);
        touchStart = null;
      }
    }, {passive:true});

    canvas.addEventListener('touchend', () => { touchStart = null; });

    // Keyboard support: arrows + WASD (case-insensitive letters, exact arrows)
    window.addEventListener('keydown', e => {
      if(!gameStarted || gameOver) return;  // ignore input if not started or game over
      const keyMap = {
        ArrowUp: 'up', ArrowDown: 'down',
        ArrowLeft: 'left', ArrowRight: 'right',
        w: 'up', s: 'down', a: 'left', d: 'right'
      };
      let k = e.key;
      if(k.length === 1) k = k.toLowerCase();
      if(k === 'f'){
        // Toggle FPS display
        showFPS = !showFPS;
        fpsCounter.style.display = showFPS ? 'block' : 'none';
        return;
      }
      let dir = keyMap[k];
      if(dir){
        e.preventDefault();
        changeDir(dir);
      }
    });

    function changeDir(direction){
      const map = {
        up: {x:0, y:-1},
        down: {x:0, y:1},
        left: {x:-1, y:0},
        right: {x:1, y:0}
      };
      const nv = map[direction];
      if(nv && (nv.x !== -velocity.x || nv.y !== -velocity.y)){
        if(nv.x !== velocity.x || nv.y !== velocity.y){ // only on actual change
          nextVel = nv;
          // Play turn sound
          try {
            turnSfx.currentTime = 0;
            turnSfx.play();
          } catch(e){}
        }
      }
    }

    // On Game Over, show overlay with score & restart
    function onGameOver(){
      gameOver = true;
      gameStarted = false;
      // Play game over sound
      try{
        gameOverSfx.currentTime = 0;
        gameOverSfx.play();
      }catch(e){}

      // Show overlay after delay for animation
      setTimeout(()=>{
        gameOverText.textContent = `Game Over! Your Score: ${score}`;
        overlay.style.display = 'flex';
        restartBtn.style.display = 'block';
        restartBtn.focus();
      }, 300);
    }

    restartBtn.addEventListener('click', () => {
      initGame();
      restartBtn.style.display = 'none';
    });

    // Main loop with fixed FPS using delta accumulator and smooth animation
    function loop(ts=0){
      animFrameId = requestAnimationFrame(loop);
      if(!lastTime) lastTime = ts;
      delta += ts - lastTime;
      lastTime = ts;

      // Update at fixed interval related to progressive speed multiplier
      while(delta >= interval){
        update();
        updateEffects();
        delta -= interval;
      }

      draw();

      // Calculate FPS if enabled
      if(showFPS){
        let frameTime = delta ? delta : interval;
        fps = Math.round(1000/frameTime);
        fpsCounter.textContent = `FPS: ${fps}`;
      }
    }

    // Start game first time:
    initGame();
  })();
  </script>
</body>
</html>