Models 44

DeepSeek V4.1 Flash 1017

by DeepSeekReasoningClean
Open in new window

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

How it went

Total time
23.8 s
Cost
$0.0058
Output tokens
7,895
Spent thinking
0 tokens
Speed
340.5 tok/s
Console errors
0
First token after
604 ms
First visible text after
604 ms
Prompt tokens
493
Code size
23.4 KB
Finish reason
stop
Served by
DigitalOcean
Model id
deepseek/deepseek-v4.1-flash-1017
Reasoning sent
{"effort":"none"}
Max output
64,000 tokens
Generated
Nov 10, 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 · 851 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Polished Snake</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  
  body {
    background: #0a0e1a;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
    color: #e0e6f0;
    overflow: hidden;
  }
  
  .game-container {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    padding: 20px;
  }
  
  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    gap: 40px;
  }
  
  .score-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: rgba(30, 40, 65, 0.7);
    padding: 10px 24px;
    border-radius: 12px;
    border: 1px solid rgba(80, 120, 200, 0.3);
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
    min-width: 100px;
  }
  
  .score-label {
    font-size: 11px;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: #6a80a8;
    margin-bottom: 2px;
  }
  
  .score-value {
    font-size: 26px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }
  
  #score { color: #5eff9e; text-shadow: 0 0 12px rgba(94, 255, 158, 0.4); }
  #best { color: #ffcf5e; text-shadow: 0 0 12px rgba(255, 207, 94, 0.4); }
  
  .canvas-wrap {
    position: relative;
    border-radius: 16px;
    padding: 3px;
    background: linear-gradient(135deg, rgba(80,120,200,0.5), rgba(94,255,158,0.2), rgba(80,120,200,0.3));
    box-shadow: 0 0 40px rgba(40, 80, 160, 0.3), 0 8px 40px rgba(0,0,0,0.5);
  }
  
  canvas {
    display: block;
    border-radius: 14px;
    background: #0d1424;
  }
  
  .overlay {
    position: absolute;
    inset: 3px;
    border-radius: 14px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 12px;
    background: rgba(8, 12, 22, 0.82);
    backdrop-filter: blur(4px);
    text-align: center;
    padding: 20px;
    transition: opacity 0.25s ease;
  }
  
  .overlay.hidden { opacity: 0; pointer-events: none; }
  
  .overlay h1 {
    font-size: 38px;
    font-weight: 800;
    letter-spacing: 1px;
    background: linear-gradient(135deg, #5eff9e, #4ab8ff);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
  }
  
  .overlay h2 {
    font-size: 32px;
    font-weight: 800;
    color: #ff5e7e;
    text-shadow: 0 0 20px rgba(255, 94, 126, 0.5);
  }
  
  .overlay p {
    font-size: 15px;
    color: #9fb0cc;
    line-height: 1.6;
  }
  
  .overlay .final-score {
    font-size: 22px;
    font-weight: 700;
    color: #5eff9e;
  }
  
  .overlay .key-hint {
    display: inline-block;
    background: rgba(80, 120, 200, 0.25);
    border: 1px solid rgba(100, 150, 230, 0.4);
    border-radius: 6px;
    padding: 2px 8px;
    font-size: 13px;
    color: #cfe0ff;
    margin: 0 2px;
  }
  
  .controls-hint {
    font-size: 12px;
    color: #556890;
    letter-spacing: 1px;
  }
  
  @media (max-width: 600px) {
    .overlay h1 { font-size: 28px; }
    .overlay h2 { font-size: 24px; }
    .header { gap: 16px; }
    .score-box { padding: 8px 16px; min-width: 80px; }
    .score-value { font-size: 20px; }
    .controls-hint { display: none; }
  }
</style>
</head>
<body>
<div class="game-container">
  <div class="header">
    <div class="score-box">
      <span class="score-label">Score</span>
      <span class="score-value" id="score">0</span>
    </div>
    <div class="score-box">
      <span class="score-label">Best</span>
      <span class="score-value" id="best">0</span>
    </div>
  </div>
  <div class="canvas-wrap">
    <canvas id="game"></canvas>
    <div class="overlay" id="overlay">
      <h1>🐍 SNAKE</h1>
      <p>Watch the AI play, or jump in yourself!</p>
      <p><span class="key-hint">Arrow Keys</span> / <span class="key-hint">WASD</span> to move</p>
      <p style="color:#5eff9e;font-weight:600;">Click or press a key to start</p>
    </div>
  </div>
  <div class="controls-hint">SPACE / ENTER restart &nbsp;·&nbsp; P pause</div>
</div>

<script>
(() => {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const overlay = document.getElementById('overlay');
  const scoreEl = document.getElementById('score');
  const bestEl = document.getElementById('best');

  const COLS = 24, ROWS = 24;
  const CELL = 26;
  const W = COLS * CELL, H = ROWS * CELL;
  canvas.width = W;
  canvas.height = H;

  // Responsive sizing
  function fitCanvas() {
    const maxW = Math.min(window.innerWidth - 60, 720);
    const maxH = window.innerHeight - 220;
    const size = Math.min(maxW, maxH, W);
    canvas.style.width = size + 'px';
    canvas.style.height = size + 'px';
  }
  window.addEventListener('resize', fitCanvas);
  fitCanvas();

  const DIRS = {
    up: { x: 0, y: -1 },
    down: { x: 0, y: 1 },
    left: { x: -1, y: 0 },
    right: { x: 1, y: 0 }
  };

  const state = {
    snake: [],
    dir: { x: 1, y: 0 },
    queue: [],
    food: { x: 0, y: 0 },
    score: 0,
    best: 0,
    grow: 0,
    running: false,
    paused: false,
    gameOver: false,
    started: false,
    particles: [],
    particlesTime: 0,
    moveInterval: 130,
    acc: 0,
    prevTime: 0,
    aiMode: true,
    smooth: [],
    eatFlash: 0,
    deathFlash: 0
  };

  function reset(keepBest = true) {
    const cx = Math.floor(COLS / 2), cy = Math.floor(ROWS / 2);
    state.snake = [
      { x: cx, y: cy },
      { x: cx - 1, y: cy },
      { x: cx - 2, y: cy }
    ];
    state.dir = { x: 1, y: 0 };
    state.queue = [];
    state.score = 0;
    state.grow = 0;
    state.gameOver = false;
    state.paused = false;
    state.running = true;
    state.particles = [];
    state.eatFlash = 0;
    state.deathFlash = 0;
    state.acc = 0;
    state.smooth = state.snake.map(s => ({ x: s.x, y: s.y }));
    if (!keepBest) state.best = 0;
    scoreEl.textContent = '0';
    bestEl.textContent = state.best;
    spawnFood();
  }

  function spawnFood() {
    const occupied = new Set(state.snake.map(s => s.x + ',' + s.y));
    const free = [];
    for (let y = 0; y < ROWS; y++)
      for (let x = 0; x < COLS; x++)
        if (!occupied.has(x + ',' + y)) free.push({ x, y });
    if (free.length === 0) return;
    state.food = free[Math.floor(Math.random() * free.length)];
  }

  function queueDir(name) {
    const d = DIRS[name];
    if (!d) return;
    const last = state.queue.length ? state.queue[state.queue.length - 1] : state.dir;
    // prevent reversing
    if (d.x === -last.x && d.y === -last.y) return;
    if (d.x === last.x && d.y === last.y) return;
    if (state.queue.length >= 3) return;
    state.queue.push(d);
  }

  function step() {
    if (state.queue.length) {
      state.dir = state.queue.shift();
    }
    const head = state.snake[0];
    const nx = head.x + state.dir.x;
    const ny = head.y + state.dir.y;

    // Wall collision
    if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) {
      return die();
    }
    // Self collision (ignore tail cell if not growing, since it will move)
    const willGrow = state.grow > 0;
    for (let i = 0; i < state.snake.length; i++) {
      if (i === state.snake.length - 1 && !willGrow) continue;
      if (state.snake[i].x === nx && state.snake[i].y === ny) {
        return die();
      }
    }

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

    if (nx === state.food.x && ny === state.food.y) {
      state.score += 10;
      scoreEl.textContent = state.score;
      if (state.score > state.best) {
        state.best = state.score;
        bestEl.textContent = state.best;
      }
      state.grow += 1;
      burstFood(state.food.x, state.food.y);
      state.eatFlash = 1;
      spawnFood();
    }

    if (state.grow > 0) {
      state.grow--;
    } else {
      state.snake.pop();
    }
  }

  function die() {
    state.gameOver = true;
    state.running = false;
    state.deathFlash = 1;
    if (state.best < state.score) {
      state.best = state.score;
      bestEl.textContent = state.best;
    }
    showOverlay('gameover');
  }

  function burstFood(gx, gy) {
    const cx = (gx + 0.5) * CELL;
    const cy = (gy + 0.5) * CELL;
    const colors = ['#ff5e7e', '#ffcf5e', '#5eff9e', '#4ab8ff', '#ffffff'];
    for (let i = 0; i < 22; i++) {
      const a = Math.random() * Math.PI * 2;
      const sp = 40 + Math.random() * 160;
      state.particles.push({
        x: cx, y: cy,
        vx: Math.cos(a) * sp,
        vy: Math.sin(a) * sp,
        life: 1,
        decay: 1.5 + Math.random() * 1.5,
        size: 2 + Math.random() * 3,
        color: colors[Math.floor(Math.random() * colors.length)]
      });
    }
  }

  // ---- AI ----
  function aiDecide() {
    const head = state.snake[0];
    const occupied = new Set(state.snake.map(s => s.x + ',' + s.y));
    // remove tail
    const tail = state.snake[state.snake.length - 1];
    occupied.delete(tail.x + ',' + tail.y);

    // BFS to food avoiding occupied
    const dirs = [DIRS.up, DIRS.down, DIRS.left, DIRS.right];
    let bestPath = null;

    const queue = [{ x: head.x, y: head.y, path: [] }];
    const seen = new Set([head.x + ',' + head.y]);
    while (queue.length) {
      const cur = queue.shift();
      if (cur.x === state.food.x && cur.y === state.food.y) {
        bestPath = cur.path;
        break;
      }
      if (cur.path.length > 200) continue;
      for (const d of dirs) {
        const nx = cur.x + d.x, ny = cur.y + d.y;
        if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
        const key = nx + ',' + ny;
        if (seen.has(key)) continue;
        if (occupied.has(key)) continue;
        seen.add(key);
        queue.push({ x: nx, y: ny, path: [...cur.path, d] });
      }
    }

    if (bestPath && bestPath.length) {
      queueDirFrom(bestPath[0]);
      return;
    }

    // No path: pick a safe direction with most free space (flood fill)
    let bestDir = null, bestSpace = -1;
    for (const d of dirs) {
      const nx = head.x + d.x, ny = head.y + d.y;
      if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
      const key = nx + ',' + ny;
      if (occupied.has(key)) continue;
      // don't reverse
      if (d.x === -state.dir.x && d.y === -state.dir.y) continue;
      const space = floodFill(nx, ny, occupied);
      if (space > bestSpace) {
        bestSpace = space;
        bestDir = d;
      }
    }
    if (bestDir) {
      queueDirFrom(bestDir);
    }
  }

  function queueDirFrom(d) {
    const last = state.queue.length ? state.queue[state.queue.length - 1] : state.dir;
    if (d.x === -last.x && d.y === -last.y) return;
    if (d.x === last.x && d.y === last.y) return;
    if (state.queue.length >= 3) return;
    state.queue.push(d);
  }

  function floodFill(sx, sy, occupied) {
    const seen = new Set([sx + ',' + sy]);
    const stack = [{ x: sx, y: sy }];
    let count = 0;
    while (stack.length && count < 400) {
      const c = stack.pop();
      count++;
      const dirs = [DIRS.up, DIRS.down, DIRS.left, DIRS.right];
      for (const d of dirs) {
        const nx = c.x + d.x, ny = c.y + d.y;
        if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
        const key = nx + ',' + ny;
        if (seen.has(key)) continue;
        if (occupied.has(key)) continue;
        seen.add(key);
        stack.push({ x: nx, y: ny });
      }
    }
    return count;
  }

  // ---- Rendering ----
  function roundRect(x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y, x + w, y + h, r);
    ctx.arcTo(x + w, y + h, x, y + h, r);
    ctx.arcTo(x, y + h, x, y, r);
    ctx.arcTo(x, y, x + w, y, r);
    ctx.closePath();
  }

  function drawGrid() {
    ctx.fillStyle = '#0d1424';
    ctx.fillRect(0, 0, W, H);
    // subtle checker
    for (let y = 0; y < ROWS; y++) {
      for (let x = 0; x < COLS; x++) {
        if ((x + y) % 2 === 0) {
          ctx.fillStyle = 'rgba(30,45,75,0.25)';
          ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
        }
      }
    }
    // grid lines
    ctx.strokeStyle = 'rgba(100,140,200,0.06)';
    ctx.lineWidth = 1;
    for (let x = 0; x <= COLS; x++) {
      ctx.beginPath();
      ctx.moveTo(x * CELL + 0.5, 0);
      ctx.lineTo(x * CELL + 0.5, H);
      ctx.stroke();
    }
    for (let y = 0; y <= ROWS; y++) {
      ctx.beginPath();
      ctx.moveTo(0, y * CELL + 0.5);
      ctx.lineTo(W, y * CELL + 0.5);
      ctx.stroke();
    }
  }

  function drawFood(time) {
    const cx = (state.food.x + 0.5) * CELL;
    const cy = (state.food.y + 0.5) * CELL;
    const pulse = 1 + Math.sin(time * 0.006) * 0.12;
    const r = CELL * 0.32 * pulse;

    // glow
    const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, CELL * 1.1);
    grad.addColorStop(0, 'rgba(255,94,126,0.35)');
    grad.addColorStop(1, 'rgba(255,94,126,0)');
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.arc(cx, cy, CELL * 1.1, 0, Math.PI * 2);
    ctx.fill();

    // body
    const bg = ctx.createRadialGradient(cx - r * 0.3, cy - r * 0.3, 0, cx, cy, r);
    bg.addColorStop(0, '#ffb0c0');
    bg.addColorStop(0.5, '#ff5e7e');
    bg.addColorStop(1, '#c2264a');
    ctx.fillStyle = bg;
    ctx.beginPath();
    ctx.arc(cx, cy, r, 0, Math.PI * 2);
    ctx.fill();

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

    // stem
    ctx.strokeStyle = '#5eff9e';
    ctx.lineWidth = 2;
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(cx, cy - r);
    ctx.quadraticCurveTo(cx + 3, cy - r - 6, cx + 7, cy - r - 7);
    ctx.stroke();
  }

  function drawSnake(t) {
    const n = state.snake.length;
    const segPx = CELL;

    // Interpolate positions between grid cells using accumulator progress
    const progress = state.running && !state.gameOver ? Math.min(state.acc / state.moveInterval, 1) : 0;

    // Build a smooth polyline of centers
    const points = [];
    for (let i = 0; i < n; i++) {
      const s = state.snake[i];
      let px = s.x, py = s.y;
      if (i === 0 && state.running && !state.gameOver) {
        // head moves toward next
        const nx = s.x + state.dir.x;
        const ny = s.y + state.dir.y;
        if (nx >= 0 && ny >= 0 && nx < COLS && ny < ROWS) {
          px = s.x + state.dir.x * progress;
          py = s.y + state.dir.y * progress;
        }
      } else if (i > 0) {
        // tail follows: previous cell's movement — for smoothness, interpolate body
        // Body cells just snap to their grid positions; head interpolation is the main visual.
      }
      points.push({ x: (px + 0.5) * segPx, y: (py + 0.5) * segPx });
    }

    // Draw body as thick line with rounded caps
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';

    // Outer glow
    ctx.strokeStyle = 'rgba(94,255,158,0.12)';
    ctx.lineWidth = CELL * 0.95;
    ctx.beginPath();
    ctx.moveTo(points[0].x, points[0].y);
    for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
    ctx.stroke();

    // Body gradient
    const headPt = points[0];
    const tailPt = points[points.length - 1];
    const g = ctx.createLinearGradient(headPt.x, headPt.y, tailPt.x, tailPt.y);
    g.addColorStop(0, '#7dffb8');
    g.addColorStop(0.5, '#3ddc84');
    g.addColorStop(1, '#1f8f55');

    ctx.strokeStyle = g;
    ctx.lineWidth = CELL * 0.78;
    ctx.beginPath();
    ctx.moveTo(points[0].x, points[0].y);
    for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
    ctx.stroke();

    // Inner highlight
    ctx.strokeStyle = 'rgba(200,255,220,0.25)';
    ctx.lineWidth = CELL * 0.28;
    ctx.beginPath();
    ctx.moveTo(points[0].x, points[0].y);
    for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
    ctx.stroke();

    // Draw scales dots along body
    for (let i = 1; i < points.length; i += 1) {
      ctx.fillStyle = 'rgba(10,60,35,0.25)';
      ctx.beginPath();
      ctx.arc(points[i].x, points[i].y, CELL * 0.08, 0, Math.PI * 2);
      ctx.fill();
    }

    // Head
    if (points.length) {
      const hx = points[0].x, hy = points[0].y;
      const hr = CELL * 0.46;

      // head glow
      const hg = ctx.createRadialGradient(hx, hy, 0, hx, hy, hr * 1.6);
      hg.addColorStop(0, 'rgba(125,255,184,0.35)');
      hg.addColorStop(1, 'rgba(125,255,184,0)');
      ctx.fillStyle = hg;
      ctx.beginPath();
      ctx.arc(hx, hy, hr * 1.6, 0, Math.PI * 2);
      ctx.fill();

      // head shape
      const hgrad = ctx.createRadialGradient(hx - hr*0.3, hy - hr*0.3, 0, hx, hy, hr);
      hgrad.addColorStop(0, '#b8ffd6');
      hgrad.addColorStop(0.6, '#5eff9e');
      hgrad.addColorStop(1, '#2bb56a');
      ctx.fillStyle = hgrad;
      ctx.beginPath();
      ctx.arc(hx, hy, hr, 0, Math.PI * 2);
      ctx.fill();

      // Eyes
      const d = state.dir;
      const perp = { x: -d.y, y: d.x };
      const eyeDist = hr * 0.42;
      const eyeOff = hr * 0.46;
      const eyeR = hr * 0.26;

      for (const side of [-1, 1]) {
        const ex = hx + d.x * eyeOff + perp.x * eyeDist * side;
        const ey = hy + d.y * eyeOff + perp.y * eyeDist * side;
        // white
        ctx.fillStyle = '#ffffff';
        ctx.beginPath();
        ctx.arc(ex, ey, eyeR, 0, Math.PI * 2);
        ctx.fill();
        // pupil
        ctx.fillStyle = '#0a1420';
        ctx.beginPath();
        ctx.arc(ex + d.x * eyeR * 0.45, ey + d.y * eyeR * 0.45, eyeR * 0.5, 0, Math.PI * 2);
        ctx.fill();
        // glint
        ctx.fillStyle = 'rgba(255,255,255,0.9)';
        ctx.beginPath();
        ctx.arc(ex - eyeR*0.25 + d.x*eyeR*0.45, ey - eyeR*0.25 + d.y*eyeR*0.45, eyeR*0.18, 0, Math.PI*2);
        ctx.fill();
      }

      // tongue
      if (state.running && Math.sin(t * 0.004) > 0.5) {
        const tongueBase = { x: hx + d.x * hr, y: hy + d.y * hr };
        ctx.strokeStyle = '#ff5e7e';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(tongueBase.x, tongueBase.y);
        const tipX = tongueBase.x + d.x * hr * 0.7;
        const tipY = tongueBase.y + d.y * hr * 0.7;
        ctx.lineTo(tipX, tipY);
        ctx.stroke();
        // fork
        ctx.beginPath();
        ctx.moveTo(tipX, tipY);
        ctx.lineTo(tipX + perp.x * 4 + d.x * 4, tipY + perp.y * 4 + d.y * 4);
        ctx.moveTo(tipX, tipY);
        ctx.lineTo(tipX - perp.x * 4 + d.x * 4, tipY - perp.y * 4 + d.y * 4);
        ctx.stroke();
      }
    }
  }

  function drawParticles(dt) {
    for (let i = state.particles.length - 1; i >= 0; i--) {
      const p = state.particles[i];
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.vx *= 0.96;
      p.vy *= 0.96;
      p.life -= p.decay * dt;
      if (p.life <= 0) {
        state.particles.splice(i, 1);
        continue;
      }
      const a = Math.max(0, p.life);
      ctx.globalAlpha = a;
      ctx.fillStyle = p.color;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * a, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }

  function drawFlash() {
    if (state.eatFlash > 0) {
      ctx.fillStyle = `rgba(94,255,158,${state.eatFlash * 0.06})`;
      ctx.fillRect(0, 0, W, H);
    }
    if (state.deathFlash > 0) {
      ctx.fillStyle = `rgba(255,94,126,${state.deathFlash * 0.25})`;
      ctx.fillRect(0, 0, W, H);
    }
  }

  // ---- Overlay ----
  function showOverlay(type) {
    overlay.classList.remove('hidden');
    if (type === 'start') {
      overlay.innerHTML = `
        <h1>🐍 SNAKE</h1>
        <p>Watch the AI play, or jump in yourself!</p>
        <p><span class="key-hint">Arrow Keys</span> / <span class="key-hint">WASD</span> to move</p>
        <p style="color:#5eff9e;font-weight:600;">Click or press a key to start</p>
      `;
    } else if (type === 'gameover') {
      overlay.innerHTML = `
        <h2>GAME OVER</h2>
        <p class="final-score">Score: ${state.score}</p>
        <p>Best: ${state.best}</p>
        <p>Press <span class="key-hint">SPACE</span> or <span class="key-hint">ENTER</span> to restart</p>
        <p style="color:#5eff9e;font-weight:600;">Click or press a key to play</p>
      `;
    } else if (type === 'paused') {
      overlay.innerHTML = `
        <h2 style="color:#4ab8ff;">PAUSED</h2>
        <p>Press <span class="key-hint">P</span> to resume</p>
      `;
    }
  }

  function hideOverlay() {
    overlay.classList.add('hidden');
  }

  // ---- Input ----
  const keyToDir = {
    'ArrowUp': 'up', 'ArrowDown': 'down', 'ArrowLeft': 'left', 'ArrowRight': 'right',
    'w': 'up', 'W': 'up', 's': 'down', 'S': 'down', 'a': 'left', 'A': 'left', 'd': 'right', 'D': 'right'
  };

  function handleStart() {
    if (!state.started) {
      state.started = true;
      state.aiMode = false;
      reset();
      hideOverlay();
      return;
    }
  }

  window.addEventListener('keydown', (e) => {
    if (e.key === ' ' || e.key === 'Enter') {
      if (!state.started) {
        state.started = true;
        state.aiMode = false;
        reset();
        hideOverlay();
        e.preventDefault();
        return;
      }
      if (state.gameOver) {
        state.aiMode = false;
        reset();
        hideOverlay();
        e.preventDefault();
        return;
      }
      e.preventDefault();
      return;
    }
    if (e.key === 'p' || e.key === 'P') {
      if (state.running && !state.gameOver) {
        state.paused = !state.paused;
        if (state.paused) showOverlay('paused'); else hideOverlay();
      }
      return;
    }
    if (keyToDir[e.key]) {
      e.preventDefault();
      if (!state.started) {
        state.started = true;
        state.aiMode = false;
        reset();
        hideOverlay();
        queueDir(keyToDir[e.key]);
        return;
      }
      if (state.gameOver) {
        state.aiMode = false;
        reset();
        hideOverlay();
        queueDir(keyToDir[e.key]);
        return;
      }
      if (state.paused) return;
      state.aiMode = false;
      queueDir(keyToDir[e.key]);
    }
  });

  canvas.addEventListener('click', () => {
    handleStart();
  });
  overlay.addEventListener('click', () => {
    handleStart();
  });

  // Start in AI demo mode immediately
  state.started = true;
  state.aiMode = true;
  reset();
  // Keep overlay visible on start actually? Requirement says demo mode behind message.
  // We'll keep overlay hidden for demo, but show a small "press key" hint — actually
  // requirement: demo mode behind a message. Let's keep overlay shown briefly, then fade.
  // Better: show the overlay but semi-transparent so AI demo visible behind it.
  // We'll make overlay backdrop lighter in demo.

  // Show start overlay for demo mode (it says click or press key to start)
  showOverlay('start');
  overlay.style.background = 'rgba(8,12,22,0.45)';

  // ---- Main Loop ----
  let lastTime = performance.now();

  function loop(now) {
    const dt = Math.min((now - lastTime) / 1000, 0.1);
    lastTime = now;

    // Update flashes
    if (state.eatFlash > 0) state.eatFlash = Math.max(0, state.eatFlash - dt * 4);
    if (state.deathFlash > 0) state.deathFlash = Math.max(0, state.deathFlash - dt * 1.5);

    if (state.running && !state.paused && !state.gameOver) {
      state.acc += dt * 1000 * (state.aiMode ? 1 : 1);
      while (state.acc >= state.moveInterval) {
        state.acc -= state.moveInterval;
        if (state.aiMode) aiDecide();
        step();
        if (state.gameOver) break;
      }
    } else {
      // keep acc modest
      state.acc = Math.min(state.acc, state.moveInterval);
    }

    // Render
    drawGrid();
    drawFood(now);
    drawSnake(now);
    drawParticles(dt);
    drawFlash();

    requestAnimationFrame(loop);
  }

  requestAnimationFrame(loop);
})();
</script>
</body>
</html>