Models 44

Gemini 3.8 Flash

by GoogleReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
2 min 23 s
Cost
$0.088
Output tokens
23,389
Spent thinking
12,178 tokens
Speed
166.6 tok/s
Console errors
0
First token after
2.2 s
First visible text after
1 min 29 s
Prompt tokens
507
Code size
32.7 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
{"effort":"high"}
Max output
65,536 tokens
Generated
Nov 12, 2026

What it was asked

The prompt
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 1074 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>Cyber Snake</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }

    body {
      background: radial-gradient(circle at 50% 40%, #121826 0%, #080b11 100%);
      color: #e2e8f0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      overflow: hidden;
      padding: 10px;
    }

    /* Top HUD / Score Bar */
    .hud {
      display: flex;
      align-items: center;
      justify-content: space-between;
      width: min(88vw, 84vh, 580px);
      margin-bottom: 12px;
      padding: 10px 18px;
      background: rgba(18, 26, 43, 0.7);
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
      border: 1px solid rgba(0, 245, 160, 0.2);
      border-radius: 14px;
      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.08);
    }

    .brand {
      display: flex;
      align-items: center;
      gap: 8px;
      font-weight: 800;
      font-size: 1.15rem;
      letter-spacing: 2px;
      text-transform: uppercase;
      background: linear-gradient(135deg, #00f5a0 0%, #00d9f5 100%);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
    }

    .brand-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: #00f5a0;
      box-shadow: 0 0 10px #00f5a0;
      display: inline-block;
    }

    .stats {
      display: flex;
      gap: 18px;
    }

    .stat-pill {
      display: flex;
      flex-direction: column;
      align-items: flex-end;
    }

    .stat-label {
      font-size: 0.65rem;
      font-weight: 700;
      letter-spacing: 1.5px;
      color: #94a3b8;
      text-transform: uppercase;
    }

    .stat-val {
      font-size: 1.25rem;
      font-weight: 800;
      font-variant-numeric: tabular-nums;
      color: #f8fafc;
    }

    .stat-val.score {
      color: #00f5a0;
      text-shadow: 0 0 12px rgba(0, 245, 160, 0.4);
    }

    /* Game Container */
    .game-wrapper {
      position: relative;
      width: min(88vw, 72vh, 580px);
      height: min(88vw, 72vh, 580px);
      aspect-ratio: 1 / 1;
      border-radius: 18px;
      padding: 4px;
      background: linear-gradient(135deg, rgba(0, 245, 160, 0.3) 0%, rgba(0, 217, 245, 0.1) 50%, rgba(255, 42, 109, 0.2) 100%);
      box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6), 0 0 30px rgba(0, 245, 160, 0.12);
      display: flex;
      align-items: center;
      justify-content: center;
    }

    canvas {
      width: 100%;
      height: 100%;
      background: #0c111a;
      border-radius: 14px;
      display: block;
      cursor: pointer;
    }

    /* Footer hints & Mobile Controls */
    .controls-hint {
      margin-top: 12px;
      font-size: 0.75rem;
      letter-spacing: 0.8px;
      color: #64748b;
      display: flex;
      gap: 14px;
      text-align: center;
    }

    .controls-hint span {
      display: inline-flex;
      align-items: center;
      gap: 4px;
    }

    .key-badge {
      display: inline-block;
      padding: 2px 6px;
      background: rgba(255, 255, 255, 0.08);
      border: 1px solid rgba(255, 255, 255, 0.15);
      border-radius: 4px;
      font-size: 0.68rem;
      font-weight: 600;
      color: #cbd5e1;
    }

    /* Mobile On-Screen D-Pad (only shows on compact or touch screens) */
    .dpad {
      display: none;
      margin-top: 10px;
      grid-template-columns: repeat(3, 48px);
      grid-template-rows: repeat(2, 42px);
      gap: 6px;
    }

    @media (max-height: 720px), (max-width: 500px) {
      .dpad {
        display: grid;
      }
      .controls-hint {
        display: none;
      }
    }

    .dpad-btn {
      background: rgba(255, 255, 255, 0.06);
      border: 1px solid rgba(255, 255, 255, 0.12);
      border-radius: 8px;
      color: #cbd5e1;
      font-size: 1.1rem;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      touch-action: manipulation;
    }

    .dpad-btn:active {
      background: rgba(0, 245, 160, 0.25);
      border-color: #00f5a0;
      color: #00f5a0;
    }

    .dpad-up { grid-column: 2; grid-row: 1; }
    .dpad-left { grid-column: 1; grid-row: 2; }
    .dpad-down { grid-column: 2; grid-row: 2; }
    .dpad-right { grid-column: 3; grid-row: 2; }
  </style>
</head>
<body>

  <div class="hud">
    <div class="brand">
      <span class="brand-dot"></span>
      <span>Cyber Snake</span>
    </div>
    <div class="stats">
      <div class="stat-pill">
        <span class="stat-label">Score</span>
        <span class="stat-val score" id="scoreDisplay">0</span>
      </div>
      <div class="stat-pill">
        <span class="stat-label">Best</span>
        <span class="stat-val" id="bestDisplay">0</span>
      </div>
    </div>
  </div>

  <div class="game-wrapper">
    <canvas id="gameCanvas" width="600" height="600"></canvas>
  </div>

  <div class="controls-hint">
    <span><span class="key-badge">W A S D</span> / <span class="key-badge">Arrows</span> Move</span>
    <span><span class="key-badge">P</span> Pause</span>
    <span><span class="key-badge">Space</span> Restart</span>
  </div>

  <div class="dpad" id="dpad">
    <button class="dpad-btn dpad-up" data-dir="UP">▲</button>
    <button class="dpad-btn dpad-left" data-dir="LEFT">◀</button>
    <button class="dpad-btn dpad-down" data-dir="DOWN">▼</button>
    <button class="dpad-btn dpad-right" data-dir="RIGHT">▶</button>
  </div>

  <script>
    (() => {
      // --- CONSTANTS & CONFIG ---
      const GRID_SIZE = 22; // 22x22 cells
      const BASE_TICK_MS = 118; // Speed of step in ms
      const MIN_TICK_MS = 75;  // Max speed when score rises

      const canvas = document.getElementById('gameCanvas');
      const ctx = canvas.getContext('2d');
      const scoreDisplay = document.getElementById('scoreDisplay');
      const bestDisplay = document.getElementById('bestDisplay');

      // Visual Resolution
      const CANVAS_RES = 616; // 22 * 28px
      canvas.width = CANVAS_RES;
      canvas.height = CANVAS_RES;
      const CELL_SIZE = CANVAS_RES / GRID_SIZE; // 28px

      // Direction Vectors
      const DIRS = {
        UP:    { x: 0, y: -1 },
        DOWN:  { x: 0, y: 1 },
        LEFT:  { x: -1, y: 0 },
        RIGHT: { x: 1, y: 0 }
      };

      // Game States: 'demo', 'playing', 'paused', 'gameover'
      let gameState = 'demo';
      let score = 0;
      let bestScore = 0;

      // Snake Data
      // Each body node is {x, y}
      let body = [];
      let currentDir = DIRS.RIGHT;
      let nextDir = DIRS.RIGHT;
      let inputQueue = [];

      // Step & Interpolation State
      let lastStepTime = 0;
      let isGrowing = false;
      let oldTail = { x: 0, y: 0 };
      let stepProgress = 0; // 0 to 1 between steps

      // Food
      let food = { x: 15, y: 11 };

      // Particles & Popups
      let particles = [];
      let floatingTexts = [];

      // Screen Shake
      let shakeTime = 0;
      let shakeMagnitude = 0;

      // --- INITIALIZATION ---
      function initSnake() {
        const startX = 6;
        const startY = 11;
        body = [
          { x: startX, y: startY },
          { x: startX - 1, y: startY },
          { x: startX - 2, y: startY },
          { x: startX - 3, y: startY }
        ];
        currentDir = DIRS.RIGHT;
        nextDir = DIRS.RIGHT;
        inputQueue = [];
        isGrowing = false;
        oldTail = { ...body[body.length - 1] };
      }

      function spawnFood() {
        const occupied = new Set();
        for (const seg of body) {
          occupied.add(`${seg.x},${seg.y}`);
        }

        const freeCells = [];
        for (let x = 0; x < GRID_SIZE; x++) {
          for (let y = 0; y < GRID_SIZE; y++) {
            if (!occupied.has(`${x},${y}`)) {
              freeCells.push({ x, y });
            }
          }
        }

        if (freeCells.length > 0) {
          const rand = freeCells[Math.floor(Math.random() * freeCells.length)];
          food = { x: rand.x, y: rand.y };
        }
      }

      function startNewGame() {
        score = 0;
        scoreDisplay.textContent = '0';
        initSnake();
        spawnFood();
        particles = [];
        floatingTexts = [];
        gameState = 'playing';
        lastStepTime = performance.now();
      }

      function enterDemoMode() {
        score = 0;
        scoreDisplay.textContent = '0';
        initSnake();
        spawnFood();
        gameState = 'demo';
        lastStepTime = performance.now();
      }

      // --- INPUT HANDLING ---
      function queueDirection(newDir) {
        if (gameState === 'demo') {
          startNewGame();
          return;
        }

        if (gameState === 'gameover') {
          startNewGame();
          return;
        }

        if (gameState === 'paused') {
          gameState = 'playing';
          lastStepTime = performance.now();
          return;
        }

        // Determine reference direction: latest queued or current moving direction
        const refDir = inputQueue.length > 0 ? inputQueue[inputQueue.length - 1] : currentDir;

        // Disallow 180° reversal
        if (newDir.x + refDir.x === 0 && newDir.y + refDir.y === 0) return;
        // Disallow identical direction spam
        if (newDir.x === refDir.x && newDir.y === refDir.y) return;

        // Allow up to 2 buffered turns for hyper-responsive play
        if (inputQueue.length < 2) {
          inputQueue.push(newDir);
        }
      }

      window.addEventListener('keydown', (e) => {
        const key = e.key.toLowerCase();

        if (key === 'p' || key === 'escape') {
          if (gameState === 'playing') {
            gameState = 'paused';
          } else if (gameState === 'paused') {
            gameState = 'playing';
            lastStepTime = performance.now();
          }
          return;
        }

        if (key === ' ' || key === 'enter') {
          if (gameState === 'gameover' || gameState === 'demo') {
            startNewGame();
          } else if (gameState === 'paused') {
            gameState = 'playing';
            lastStepTime = performance.now();
          }
          return;
        }

        let dir = null;
        if (key === 'arrowup' || key === 'w') dir = DIRS.UP;
        else if (key === 'arrowdown' || key === 's') dir = DIRS.DOWN;
        else if (key === 'arrowleft' || key === 'a') dir = DIRS.LEFT;
        else if (key === 'arrowright' || key === 'd') dir = DIRS.RIGHT;

        if (dir) {
          e.preventDefault();
          queueDirection(dir);
        }
      });

      // Pointer / Touch click on canvas
      canvas.addEventListener('pointerdown', () => {
        if (gameState === 'demo' || gameState === 'gameover') {
          startNewGame();
        } else if (gameState === 'paused') {
          gameState = 'playing';
          lastStepTime = performance.now();
        }
      });

      // Swipe Controls for Mobile
      let touchStartX = 0;
      let touchStartY = 0;
      canvas.addEventListener('touchstart', (e) => {
        const t = e.touches[0];
        touchStartX = t.clientX;
        touchStartY = t.clientY;
      }, { passive: true });

      canvas.addEventListener('touchend', (e) => {
        const t = e.changedTouches[0];
        const dx = t.clientX - touchStartX;
        const dy = t.clientY - touchStartY;
        const absX = Math.abs(dx);
        const absY = Math.abs(dy);

        if (Math.max(absX, absY) > 20) {
          if (absX > absY) {
            queueDirection(dx > 0 ? DIRS.RIGHT : DIRS.LEFT);
          } else {
            queueDirection(dy > 0 ? DIRS.DOWN : DIRS.UP);
          }
        }
      }, { passive: true });

      // On-Screen D-pad listeners
      document.getElementById('dpad').addEventListener('pointerdown', (e) => {
        const btn = e.target.closest('.dpad-btn');
        if (!btn) return;
        const d = btn.getAttribute('data-dir');
        if (DIRS[d]) queueDirection(DIRS[d]);
      });

      // --- AI AUTO-PLAY CONTROLLER (DEMO MODE) ---
      function getAiDirection() {
        const head = body[0];
        const target = food;

        // Build obstacle set of snake body
        const obstacles = new Set();
        // Exclude the tail segment if not growing because it will vacate
        const limit = isGrowing ? body.length : body.length - 1;
        for (let i = 0; i < limit; i++) {
          obstacles.add(`${body[i].x},${body[i].y}`);
        }

        const dirs = [DIRS.UP, DIRS.RIGHT, DIRS.DOWN, DIRS.LEFT];

        // BFS to target
        const queue = [{ x: head.x, y: head.y, path: [] }];
        const visited = new Set([`${head.x},${head.y}`]);
        let foundPath = null;

        while (queue.length > 0) {
          const curr = queue.shift();
          if (curr.x === target.x && curr.y === target.y) {
            foundPath = curr.path;
            break;
          }
          for (const d of dirs) {
            const nx = curr.x + d.x;
            const ny = curr.y + d.y;
            const key = `${nx},${ny}`;
            if (nx >= 0 && nx < GRID_SIZE && ny >= 0 && ny < GRID_SIZE && !obstacles.has(key) && !visited.has(key)) {
              visited.add(key);
              queue.push({ x: nx, y: ny, path: curr.path.length === 0 ? [d] : curr.path });
            }
          }
        }

        // Helper: evaluate available free space (flood fill)
        function floodFillCount(start) {
          const q = [start];
          const seen = new Set([`${start.x},${start.y}`]);
          let count = 0;
          while (q.length > 0 && count < 60) {
            const c = q.shift();
            count++;
            for (const d of dirs) {
              const nx = c.x + d.x;
              const ny = c.y + d.y;
              const k = `${nx},${ny}`;
              if (nx >= 0 && nx < GRID_SIZE && ny >= 0 && ny < GRID_SIZE && !obstacles.has(k) && !seen.has(k)) {
                seen.add(k);
                q.push({ x: nx, y: ny });
              }
            }
          }
          return count;
        }

        // Assess candidate legal moves
        const legalMoves = [];
        for (const d of dirs) {
          // Can't reverse
          if (d.x + currentDir.x === 0 && d.y + currentDir.y === 0) continue;
          const nx = head.x + d.x;
          const ny = head.y + d.y;
          if (nx >= 0 && nx < GRID_SIZE && ny >= 0 && ny < GRID_SIZE && !obstacles.has(`${nx},${ny}`)) {
            legalMoves.push({ dir: d, space: floodFillCount({ x: nx, y: ny }) });
          }
        }

        if (legalMoves.length === 0) return currentDir;

        // If BFS path found, verify it doesn't lead to trap
        if (foundPath && foundPath.length > 0) {
          const wantedDir = foundPath[0];
          const move = legalMoves.find(m => m.dir.x === wantedDir.x && m.dir.y === wantedDir.y);
          if (move && move.space >= Math.min(body.length + 2, 24)) {
            return wantedDir;
          }
        }

        // Otherwise pick move with maximum space
        legalMoves.sort((a, b) => b.space - a.space);
        return legalMoves[0].dir;
      }

      // --- LOGICAL TICK STEP ---
      function triggerGameOver() {
        gameState = 'gameover';
        shakeTime = 0.45;
        shakeMagnitude = 10;

        // Explode snake body into sparks
        for (const seg of body) {
          createBurst(
            (seg.x + 0.5) * CELL_SIZE,
            (seg.y + 0.5) * CELL_SIZE,
            ['#00f5a0', '#00d9f5', '#ff2a6d', '#ffffff'],
            6
          );
        }
      }

      function step() {
        if (gameState === 'demo') {
          nextDir = getAiDirection();
        } else if (inputQueue.length > 0) {
          nextDir = inputQueue.shift();
        }

        currentDir = nextDir;
        const head = body[0];
        const newHead = { x: head.x + currentDir.x, y: head.y + currentDir.y };

        // Wall collision check
        if (newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE) {
          if (gameState === 'demo') {
            enterDemoMode();
          } else {
            triggerGameOver();
          }
          return;
        }

        // Self collision check
        // If not growing, the tail will move away, so hitting tail is valid unless growing
        const checkLimit = isGrowing ? body.length : body.length - 1;
        for (let i = 0; i < checkLimit; i++) {
          if (body[i].x === newHead.x && body[i].y === newHead.y) {
            if (gameState === 'demo') {
              enterDemoMode();
            } else {
              triggerGameOver();
            }
            return;
          }
        }

        // Check Food
        oldTail = { ...body[body.length - 1] };
        if (newHead.x === food.x && newHead.y === food.y) {
          // Ate food
          body.unshift(newHead);
          isGrowing = true;

          score += 10;
          if (score > bestScore) {
            bestScore = score;
            bestDisplay.textContent = bestScore;
          }
          scoreDisplay.textContent = score;

          // Particle burst & float text
          const fx = (food.x + 0.5) * CELL_SIZE;
          const fy = (food.y + 0.5) * CELL_SIZE;
          createBurst(fx, fy, ['#ff2a6d', '#ff7a00', '#ffd200', '#00f5a0', '#ffffff'], 24);
          floatingTexts.push({
            text: '+10',
            x: fx,
            y: fy - 6,
            life: 1.0,
            vy: -1.2
          });

          spawnFood();
        } else {
          // Standard move
          body.unshift(newHead);
          body.pop();
          isGrowing = false;
        }
      }

      // --- VISUAL BURSTS & PARTICLES ---
      function createBurst(x, y, colors, count) {
        for (let i = 0; i < count; i++) {
          const angle = Math.random() * Math.PI * 2;
          const speed = 1.2 + Math.random() * 4.8;
          particles.push({
            x,
            y,
            vx: Math.cos(angle) * speed,
            vy: Math.sin(angle) * speed,
            color: colors[Math.floor(Math.random() * colors.length)],
            radius: 2 + Math.random() * 3.5,
            life: 1.0,
            decay: 1.2 + Math.random() * 1.6
          });
        }
      }

      function updateParticles(dt) {
        for (let i = particles.length - 1; i >= 0; i--) {
          const p = particles[i];
          p.x += p.vx;
          p.y += p.vy;
          p.vx *= 0.95;
          p.vy *= 0.95;
          p.life -= p.decay * dt;
          if (p.life <= 0) particles.splice(i, 1);
        }

        for (let i = floatingTexts.length - 1; i >= 0; i--) {
          const ft = floatingTexts[i];
          ft.y += ft.vy;
          ft.life -= 1.4 * dt;
          if (ft.life <= 0) floatingTexts.splice(i, 1);
        }
      }

      // --- RENDERING PIPELINE ---
      function drawGrid() {
        ctx.fillStyle = '#0a0f19';
        ctx.fillRect(0, 0, CANVAS_RES, CANVAS_RES);

        // Fine grid lines
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'rgba(0, 245, 160, 0.035)';
        ctx.beginPath();
        for (let i = 0; i <= GRID_SIZE; i++) {
          const pos = i * CELL_SIZE;
          ctx.moveTo(pos, 0);
          ctx.lineTo(pos, CANVAS_RES);
          ctx.moveTo(0, pos);
          ctx.lineTo(CANVAS_RES, pos);
        }
        ctx.stroke();

        // Subtle ambient radial glow in center
        const grad = ctx.createRadialGradient(
          CANVAS_RES / 2, CANVAS_RES / 2, 40,
          CANVAS_RES / 2, CANVAS_RES / 2, CANVAS_RES * 0.7
        );
        grad.addColorStop(0, 'rgba(0, 245, 160, 0.03)');
        grad.addColorStop(1, 'transparent');
        ctx.fillStyle = grad;
        ctx.fillRect(0, 0, CANVAS_RES, CANVAS_RES);
      }

      function drawFood(time) {
        const cx = (food.x + 0.5) * CELL_SIZE;
        const cy = (food.y + 0.5) * CELL_SIZE;

        const pulse = Math.sin(time * 0.007) * 1.5;
        const radius = CELL_SIZE * 0.38 + pulse * 0.8;

        // Outer pulsing aura
        ctx.save();
        ctx.shadowColor = '#ff2a6d';
        ctx.shadowBlur = 18;

        // Food Body Gradient
        const foodGrad = ctx.createRadialGradient(cx - radius * 0.3, cy - radius * 0.3, 1, cx, cy, radius);
        foodGrad.addColorStop(0, '#ff7a00');
        foodGrad.addColorStop(0.6, '#ff2a6d');
        foodGrad.addColorStop(1, '#9b0038');

        ctx.fillStyle = foodGrad;
        ctx.beginPath();
        ctx.arc(cx, cy, radius, 0, Math.PI * 2);
        ctx.fill();

        // Specular highlight
        ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
        ctx.beginPath();
        ctx.arc(cx - radius * 0.35, cy - radius * 0.35, radius * 0.22, 0, Math.PI * 2);
        ctx.fill();

        // Cute Leaf
        ctx.fillStyle = '#00f5a0';
        ctx.shadowColor = '#00f5a0';
        ctx.shadowBlur = 8;
        ctx.beginPath();
        ctx.ellipse(cx + 2, cy - radius * 0.95, radius * 0.32, radius * 0.18, Math.PI / 4, 0, Math.PI * 2);
        ctx.fill();

        ctx.restore();
      }

      function drawSnake(t) {
        if (body.length === 0) return;

        // Spine interpolation:
        // We construct a smooth continuous spine of points:
        // P_head(t) = lerp(body[1], body[0], t)
        // Joint points: body[1], body[2], ..., body[n-2]
        // P_tail(t) = lerp(oldTail, body[n-1], t)
        // This guarantees fixed constant length and zero corner cutting!

        const headPos = {
          x: (body[0].x * (1 - t) + (body[0].x) * t + 0.5) * CELL_SIZE,
          y: (body[0].y * (1 - t) + (body[0].y) * t + 0.5) * CELL_SIZE
        };

        // Current head and segment 1
        const curHead = body[0];
        const prevHead = body[1] || curHead;

        const pHeadX = (prevHead.x + (curHead.x - prevHead.x) * t + 0.5) * CELL_SIZE;
        const pHeadY = (prevHead.y + (curHead.y - prevHead.y) * t + 0.5) * CELL_SIZE;

        const spine = [];
        spine.push({ x: pHeadX, y: pHeadY });

        // Add settled joints
        for (let i = 1; i < body.length - 1; i++) {
          spine.push({
            x: (body[i].x + 0.5) * CELL_SIZE,
            y: (body[i].y + 0.5) * CELL_SIZE
          });
        }

        // Tail point
        if (body.length > 1) {
          const lastIdx = body.length - 1;
          const curTail = body[lastIdx];
          const prevT = isGrowing ? curTail : oldTail;
          const pTailX = (prevT.x + (curTail.x - prevT.x) * t + 0.5) * CELL_SIZE;
          const pTailY = (prevT.y + (curTail.y - prevT.y) * t + 0.5) * CELL_SIZE;
          spine.push({ x: pTailX, y: pTailY });
        }

        // Draw body glow & stroke
        ctx.save();
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';

        // Outer Neon Glow
        ctx.shadowColor = '#00f5a0';
        ctx.shadowBlur = 14;

        // Gradient from Head (Emerald) to Tail (Cyan/Blue)
        const strokeGrad = ctx.createLinearGradient(spine[0].x, spine[0].y, spine[spine.length - 1].x, spine[spine.length - 1].y);
        strokeGrad.addColorStop(0, '#00f5a0');
        strokeGrad.addColorStop(0.5, '#00d9f5');
        strokeGrad.addColorStop(1, '#0062ff');

        ctx.strokeStyle = strokeGrad;
        ctx.lineWidth = CELL_SIZE * 0.74;

        ctx.beginPath();
        ctx.moveTo(spine[0].x, spine[0].y);
        for (let i = 1; i < spine.length; i++) {
          ctx.lineTo(spine[i].x, spine[i].y);
        }
        ctx.stroke();

        // Inner Core Tube Highlight (3D visual pop)
        ctx.shadowBlur = 0;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.38)';
        ctx.lineWidth = CELL_SIZE * 0.24;
        ctx.beginPath();
        ctx.moveTo(spine[0].x, spine[0].y);
        for (let i = 1; i < spine.length; i++) {
          ctx.lineTo(spine[i].x, spine[i].y);
        }
        ctx.stroke();

        // --- DRAW HEAD & FACE ---
        const hx = spine[0].x;
        const hy = spine[0].y;
        const dir = currentDir;

        // Head Base Cap
        ctx.fillStyle = '#00f5a0';
        ctx.shadowColor = '#00f5a0';
        ctx.shadowBlur = 12;
        ctx.beginPath();
        ctx.arc(hx, hy, CELL_SIZE * 0.44, 0, Math.PI * 2);
        ctx.fill();

        // Perpendicular vector for eyes
        const perpX = -dir.y;
        const perpY = dir.x;

        const eyeForward = CELL_SIZE * 0.14;
        const eyeOffset = CELL_SIZE * 0.22;
        const eyeRadius = CELL_SIZE * 0.14;
        const pupilRadius = CELL_SIZE * 0.075;

        // Eye Positions
        const leftEyeX = hx + dir.x * eyeForward + perpX * eyeOffset;
        const leftEyeY = hy + dir.y * eyeForward + perpY * eyeOffset;
        const rightEyeX = hx + dir.x * eyeForward - perpX * eyeOffset;
        const rightEyeY = hy + dir.y * eyeForward - perpY * eyeOffset;

        // Draw Eyeballs (White)
        ctx.shadowBlur = 0;
        ctx.fillStyle = '#ffffff';
        ctx.beginPath();
        ctx.arc(leftEyeX, leftEyeY, eyeRadius, 0, Math.PI * 2);
        ctx.arc(rightEyeX, rightEyeY, eyeRadius, 0, Math.PI * 2);
        ctx.fill();

        // Draw Pupils (Tracking forward direction)
        const pupilShift = CELL_SIZE * 0.045;
        ctx.fillStyle = '#0b1329';
        ctx.beginPath();
        ctx.arc(leftEyeX + dir.x * pupilShift, leftEyeY + dir.y * pupilShift, pupilRadius, 0, Math.PI * 2);
        ctx.arc(rightEyeX + dir.x * pupilShift, rightEyeY + dir.y * pupilShift, pupilRadius, 0, Math.PI * 2);
        ctx.fill();

        // Tiny Specular Catchlights in Pupils
        ctx.fillStyle = '#ffffff';
        ctx.beginPath();
        ctx.arc(leftEyeX + dir.x * pupilShift - 1, leftEyeY + dir.y * pupilShift - 1, pupilRadius * 0.4, 0, Math.PI * 2);
        ctx.arc(rightEyeX + dir.x * pupilShift - 1, rightEyeY + dir.y * pupilShift - 1, pupilRadius * 0.4, 0, Math.PI * 2);
        ctx.fill();

        // Forked Tongue (flicks periodically)
        const tongueCycle = Math.sin(performance.now() * 0.012);
        if (tongueCycle > 0.45) {
          const tLength = CELL_SIZE * (0.35 + tongueCycle * 0.15);
          const rootX = hx + dir.x * (CELL_SIZE * 0.4);
          const rootY = hy + dir.y * (CELL_SIZE * 0.4);
          const tipX = rootX + dir.x * tLength;
          const tipY = rootY + dir.y * tLength;

          ctx.strokeStyle = '#ff2a6d';
          ctx.lineWidth = 2.2;
          ctx.beginPath();
          ctx.moveTo(rootX, rootY);
          ctx.lineTo(tipX, tipY);
          // Fork tips
          ctx.lineTo(tipX + (dir.x - perpX) * 4, tipY + (dir.y - perpY) * 4);
          ctx.moveTo(tipX, tipY);
          ctx.lineTo(tipX + (dir.x + perpX) * 4, tipY + (dir.y + perpY) * 4);
          ctx.stroke();
        }

        ctx.restore();
      }

      function drawParticles() {
        ctx.save();
        ctx.globalCompositeOperation = 'lighter';
        for (const p of particles) {
          ctx.fillStyle = p.color;
          ctx.globalAlpha = Math.max(0, p.life);
          ctx.beginPath();
          ctx.arc(p.x, p.y, p.radius * p.life, 0, Math.PI * 2);
          ctx.fill();
        }

        // Draw Floating Score Indicators
        ctx.font = '800 16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        for (const ft of floatingTexts) {
          ctx.fillStyle = `rgba(0, 245, 160, ${Math.max(0, ft.life)})`;
          ctx.shadowColor = '#00f5a0';
          ctx.shadowBlur = 8;
          ctx.fillText(ft.text, ft.x, ft.y);
        }
        ctx.restore();
      }

      function drawOverlays() {
        // --- DEMO MODE OVERLAY ---
        if (gameState === 'demo') {
          ctx.save();
          // Top Demo Badge
          ctx.fillStyle = 'rgba(18, 26, 43, 0.85)';
          ctx.strokeStyle = 'rgba(0, 245, 160, 0.4)';
          ctx.lineWidth = 1;

          const badgeW = 150;
          const badgeH = 28;
          const badgeX = (CANVAS_RES - badgeW) / 2;
          const badgeY = 16;

          ctx.beginPath();
          ctx.roundRect(badgeX, badgeY, badgeW, badgeH, 14);
          ctx.fill();
          ctx.stroke();

          ctx.fillStyle = '#00f5a0';
          ctx.font = '700 11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.letterSpacing = '1.5px';
          ctx.fillText('AUTOPLAY DEMO', CANVAS_RES / 2, badgeY + badgeH / 2);

          // Center Call-To-Action Banner
          const pulse = Math.sin(performance.now() * 0.005) * 0.08 + 1;
          ctx.translate(CANVAS_RES / 2, CANVAS_RES - 70);
          ctx.scale(pulse, pulse);

          ctx.fillStyle = 'rgba(11, 16, 26, 0.88)';
          ctx.strokeStyle = '#00f5a0';
          ctx.lineWidth = 1.5;
          ctx.shadowColor = 'rgba(0, 245, 160, 0.5)';
          ctx.shadowBlur = 18;

          const ctaW = 320;
          const ctaH = 46;
          ctx.beginPath();
          ctx.roundRect(-ctaW / 2, -ctaH / 2, ctaW, ctaH, 23);
          ctx.fill();
          ctx.stroke();

          ctx.shadowBlur = 0;
          ctx.fillStyle = '#ffffff';
          ctx.font = '800 13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
          ctx.fillText('CLICK OR PRESS ANY KEY TO PLAY', 0, 1);

          ctx.restore();
        }

        // --- PAUSED OVERLAY ---
        if (gameState === 'paused') {
          ctx.save();
          ctx.fillStyle = 'rgba(8, 11, 17, 0.75)';
          ctx.fillRect(0, 0, CANVAS_RES, CANVAS_RES);

          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';

          ctx.fillStyle = '#00d9f5';
          ctx.font = '800 32px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
          ctx.shadowColor = '#00d9f5';
          ctx.shadowBlur = 16;
          ctx.fillText('PAUSED', CANVAS_RES / 2, CANVAS_RES / 2 - 14);

          ctx.shadowBlur = 0;
          ctx.fillStyle = '#94a3b8';
          ctx.font = '600 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
          ctx.fillText('Press P or Space to Resume', CANVAS_RES / 2, CANVAS_RES / 2 + 24);

          ctx.restore();
        }

        // --- GAME OVER OVERLAY ---
        if (gameState === 'gameover') {
          ctx.save();
          ctx.fillStyle = 'rgba(8, 11, 17, 0.82)';
          ctx.fillRect(0, 0, CANVAS_RES, CANVAS_RES);

          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';

          // "GAME OVER"
          ctx.fillStyle = '#ff2a6d';
          ctx.font = '900 36px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
          ctx.shadowColor = '#ff2a6d';
          ctx.shadowBlur = 20;
          ctx.fillText('GAME OVER', CANVAS_RES / 2, CANVAS_RES / 2 - 50);

          // Scores Card
          ctx.shadowBlur = 0;
          ctx.fillStyle = '#e2e8f0';
          ctx.font = '700 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
          ctx.fillText(`Final Score: ${score}`, CANVAS_RES / 2, CANVAS_RES / 2);

          ctx.fillStyle = '#94a3b8';
          ctx.font = '600 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
          ctx.fillText(`Session Best: ${bestScore}`, CANVAS_RES / 2, CANVAS_RES / 2 + 28);

          // Restart Prompt
          const pulse = Math.sin(performance.now() * 0.006) * 0.15 + 0.85;
          ctx.fillStyle = `rgba(0, 245, 160, ${pulse})`;
          ctx.font = '800 13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
          ctx.fillText('PRESS SPACE OR CLICK TO RESTART', CANVAS_RES / 2, CANVAS_RES / 2 + 76);

          ctx.restore();
        }
      }

      // --- MAIN ENGINE LOOP ---
      let prevTimestamp = performance.now();

      function gameLoop(now) {
        requestAnimationFrame(gameLoop);

        const dt = Math.min((now - prevTimestamp) / 1000, 0.1);
        prevTimestamp = now;

        // Calculate dynamic tick interval (speeds up as score increases)
        const tickInterval = Math.max(MIN_TICK_MS, BASE_TICK_MS - Math.floor(score / 50) * 4);

        // Update logical tick
        if (gameState === 'playing' || gameState === 'demo') {
          const elapsed = now - lastStepTime;
          if (elapsed >= tickInterval) {
            step();
            lastStepTime = now;
            stepProgress = 0;
          } else {
            stepProgress = elapsed / tickInterval;
          }
        } else {
          stepProgress = 1;
        }

        updateParticles(dt);

        // Handle Screen Shake
        ctx.save();
        if (shakeTime > 0) {
          shakeTime -= dt;
          const damping = Math.max(0, shakeTime / 0.45);
          const ox = (Math.random() - 0.5) * shakeMagnitude * damping;
          const oy = (Math.random() - 0.5) * shakeMagnitude * damping;
          ctx.translate(ox, oy);
        }

        // Render Layers
        drawGrid();
        drawFood(now);
        drawSnake(stepProgress);
        drawParticles();
        ctx.restore();

        drawOverlays();
      }

      // Start in auto-playing demo mode
      enterDemoMode();
      requestAnimationFrame(gameLoop);
    })();
  </script>
</body>
</html>
Reasoning trace (12,178 tokens)
**Developing Game Logic**

I'm currently focused on implementing the core game mechanics for the Snake game. This involves setting up the canvas, drawing the grid, and handling the snake's movement and growth based on player input. I'm also working on the food generation and collision detection.

**Refining Movement Logic**

I'm fine-tuning the snake's input handling to ensure rapid key presses are registered correctly, preventing illegal 180-degree turns. Simultaneously, I'm refining the visual polish, aiming for a sleek, modern aesthetic with glowing elements and distinct snake features.

**Implementing State Management**

I'm exploring options for storing the best score, considering `sessionStorage`, `cookies`, or even a simple JavaScript variable for session persistence. I'm also defining the responsive layout, targeting 1280x800 down to 375px without horizontal scrolling.

**Simplifying Movement Interpolation**

I'm exploring a simpler interpolation method for the snake's segments. Each segment will hold its previous and target positions. During a step, segments shift to the previous segment's location, and I'll use linear interpolation for smooth movement along grid lines. The tail's behavior during growth and shrinkage is also being refined for a cleaner visual.

**Updating Snake Movement**

I'm now working on a synchronized sliding motion for each snake segment, interpolating between old and new positions for a smooth slither. I've also implemented an input queue to handle rapid key presses and a basic AI for demo mode, including pathfinding and safety checks.

**Enhancing Visuals and Demo**

I'm refining the demo mode to appear impressively smooth and engaging, incorporating an "AUTO-PLAY DEMO" badge and a clear call to action. My focus is shifting towards a sleek, modern arcade aesthetic with a Neo-Emerald theme, detailed snake gradients, expressive head features, and a subtly animated food item.

**Designing Visual Effects**

I'm focusing on enhancing the visual feedback for eating and death with particle effects. Ambient background particles and a sleek UI header with essential game stats are also in development. Mobile touch controls are a priority for smooth gameplay on smaller viewports.

**Refining Interpolation Logic**

I'm solidifying the interpolation mechanism, managing `currentSegments` and `prevSegments` to ensure fluid movement. Key logic for handling growth, tail behavior, and particle effects on eating is being refined. I'm also working on intelligent food spawning logic.

**Smooth Segment Rendering**

I'm refining the interpolation for each snake segment, using `lerp` to smoothly transition between previous and current positions. I'm also addressing how to handle growth by ensuring new segments appear gracefully at the tail. Corner rounding is being considered for visual continuity.

**Drawing Snake Path**

I'm exploring how to render the snake as a continuous path for smoother visuals. Specifically, I'm testing the effect of `ctx.lineJoin = 'round'` on curved segments and considering how to layer individual segment details for depth.

**Verifying Segment Spacing**

I'm re-evaluating the Euclidean distance between snake segments during interpolation. It appears the assumed unit distance between segments might not hold true with smooth interpolation, potentially causing overlap issues that I need to address.

**Fixing Path Interpolation**

I'm encountering an issue where direct line connections between interpolated snake segments are cutting corners incorrectly. I'm investigating a "True Path" method, treating the snake's body as a continuous ribbon of historical positions to correctly render corners.

**Revising Path Rendering**

I'm confirming that the "True Path" approach correctly maintains corner fidelity. By treating the snake's body as a series of historical positions rather than just individual segments, I can ensure that diagonal shortcuts are avoided and the path renders smoothly, preserving sharp turns at grid points.

**Confirming Path Accuracy**

I've confirmed that my "True Path" rendering method maintains perfect corner fidelity. By treating the snake's body as a continuous ribbon of historical positions, I'm ensuring that diagonal shortcuts are avoided and the path renders smoothly, preserving sharp turns at grid points and maintaining constant length.

**Updating Segment Logic**

I'm now confirming how segment interpolation behaves during growth. The tail position correctly extends smoothly, and I'm verifying that drawing directly between interpolated points maintains linearity. The transition from a discrete number of segments to a continuously growing length is proving robust.

**Rendering Smooth Snake Body**

I'm refining the drawing logic to create a visually stunning snake. I'm focusing on drawing the spine as a continuous path, applying a gradient from head to tail, and adding inner highlight strokes for a 3D, neon effect. This approach ensures seamless movement and sharp corner fidelity.

**Styling Snake Head**

I'm finalizing the snake's head design with a vector-based direction, periscopic eyes that track movement, and subtle tongue flicks. The food is now a pulsing neon fruit with a gradient, stem, and a gentle bobbing animation. Particle effects for eating are also being refined.

**Simulating Particle Effects**

I'm implementing detailed particle effects for when food is eaten, each with random velocities, colors, and decay rates. Score popups are also being added to float upwards and fade. My current focus is on the AI's pathfinding, ensuring it can safely navigate the grid and avoid its own body.

**Updating AI Pathfinding**

I'm refining the AI's pathfinding logic, prioritizing safe steps that don't lead to inescapable situations. If a direct safe path isn't available, it'll choose the next best option that maximizes free space, ensuring it avoids coiling into dead ends.

**Finalizing Demo Mode**

I'm implementing a stylish "AUTOPLAY DEMO" banner and a clear "CLICK OR PRESS ANY KEY TO PLAY" overlay. Interacting with these elements will immediately reset the game and transition to player control, initiating gameplay swiftly.

**Updating Input Handling**

I'm refining the input queue to manage directional inputs. It will store up to two distinct moves and intelligently ignore illegal 180-degree reversals or redundant directional inputs. On each tick, it will process the next queued direction, ensuring smooth and responsive control.

**Implementing Mobile Controls**

I'm integrating touch swipe support and on-screen arrow buttons for mobile users. Simultaneously, I'm defining game over and pause overlay requirements, including score display, restart prompts, and screen shake for impact.

**Designing Layout and Styling**

I'm defining the overall layout and visual styling. This involves centering the canvas, setting its dimensions with device pixel ratio scaling for sharpness, and applying rounded corners and a neon border. I'm also determining the styling for score cards and control hints.

**Checking AI Pathfinding Logic**

I'm currently reviewing the Breadth-First Search and Flood Fill logic for the AI. The goal is to accurately construct obstacle sets and explore potential paths to the food, ensuring safe navigation and avoiding dead ends.

**Evaluating Pathfinding Options**

I'm assessing the current Breadth-First Search (BFS) implementation for the AI. While it finds paths, I'm questioning its efficiency for prioritizing safe moves and ensuring the snake doesn't corner itself. I'm also looking at heuristics for evaluating path viability.

**Improving Pathfinding Safety**

I'm currently evaluating my AI's pathfinding logic, specifically how it counts free spaces and handles potential dead ends. I'm refining the candidate move selection to ensure it prioritizes paths that leave maximum open space, even if a slightly longer route, to prevent self-entrapment.

**Choosing Safe Moves**

I'm refining the AI's pathfinding to prioritize moves that maximize available free space. This is crucial for preventing the snake from cornering itself, even if it means taking a slightly longer route to the food.

**Calculating Next Steps**

I'm refining the core game loop logic. This involves precisely calculating the snake's next position based on input or AI, handling wall and self-collision checks, and determining food consumption to manage growth and scoring. The moment `t` reaches 1.0 is the critical juncture for these actions.

**Processing Movement Logic**

I'm analyzing the core game loop and how it synchronizes with movement interpolation. My current focus is on precisely defining when the snake's "next position" is calculated and applied relative to the interpolation timing. I need to ensure the head's movement from `oldHead` to `newHead` is correctly framed within the interpolation interval.

**Updating Collision Detection**

I'm refining collision detection to allow the snake's head to slide into the point of impact before crashing. This ensures a more visually satisfying death animation rather than an abrupt stop. I'm also streamlining data structures to maintain clarity and efficiency.

**Clarifying Interpolation Logic**

I'm reviewing the interpolation algorithm again, specifically how segments should track their predecessors. The current `currentSegments` and `previousSegments` approach seems sound, but I'm re-evaluating the exact movement of each segment relative to its neighbor to ensure perfect linearity and avoid any corner-cutting issues.

**Drawing Snake's Continuous Path**

I'm confirming the "True Path" rendering approach. By treating the snake's body as a continuous ribbon of historical positions, I can draw a smooth, unbroken line. This ensures that the snake's path correctly renders sharp turns without any diagonal shortcuts and maintains its length.

**Verifying Path Length**

I've confirmed the snake's total path length remains constant at `n-1` grid units during interpolation. This occurs because the head's movement from `oldHead` to `newHead` is precisely balanced by the tail's interpolated movement from `oldTail` to `body[n-2]`, ensuring consistent visual length.

**Confirming Elegant Logic**

I've confirmed that my interpolation logic is elegantly sound. When time $t$ reaches 1, the head reaches its new position and the tail interpolates to the second-to-last segment's previous position. This allows for a seamless discrete update and reset of $t$ for the next step, gracefully handling growth and maintaining path length.

**Confirming Seamless Growth**

My interpolation logic is mathematically sound. When time $t$ reaches 1, the head reaches its new position, and the tail smoothly interpolates to the second-to-last segment's previous position. This gracefully handles growth, maintaining a consistent path length and producing flawless fluid curves.

**Designing Visual Flourishes**

I'm finalizing the snake's character with periscopic eyes and a darting forked tongue. The food now pulses with a neon glow. I'm also refining the canvas background with a gradient and faint glowing grid lines, adding a neon border to the playfield for a complete aesthetic.

**Implementing Visual Details**

I'm integrating advanced particle effects for food consumption and game over states, along with floating score indicators. I'm also defining various game states, including 'demo', 'playing', and others to manage the game flow effectively.

**Refining Game State Logic**

I'm consolidating the game states to manage transitions between demo, playing, paused, and game over more robustly. This includes defining precise triggers for starting a new game, whether from demo mode via user input or from game over, ensuring immediate responsiveness.

**Implementing Mobile Controls**

I'm integrating touch swipe detection and on-screen D-pad buttons for mobile. These buttons are designed to be sleek, semi-transparent, and occupy minimal space, ensuring effortless mobile play. They are activated via `pointerdown` for immediate response.

**Implementing Layout Elements**

I'm defining the structural layout, including a header for scores, a canvas container with advanced styling, and responsive mobile controls. The script section is being organized to hold constants, game variables, AI functions, and rendering logic.

**Verifying Input Logic**

I've confirmed that my input queue correctly handles rapid directional inputs, prioritizing valid moves and preventing immediate reversals. This ensures responsive controls, even with quick key presses, while the game logic consistently prevents the snake from turning back on itself.