Models 44
Train snakeOne-line

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
22.6 s
Cost
$0.0037
Output tokens
6,771
Spent thinking
0 tokens
Speed
308.1 tok/s
Console errors
0
First token after
580 ms
First visible text after
580 ms
Prompt tokens
362
Code size
22.5 KB
Finish reason
stop
Served by
StepFun
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
“make me a snake game where the snake is a little train with a locomotive and colorful wagons, picking up passengers waiting on the platform”
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 · 678 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Snake Train — Passenger Express</title>
  <style>
    * {
      box-sizing: border-box;
      user-select: none;
    }
    body {
      height: 100vh;
      margin: 0;
      background: linear-gradient(145deg, #1b2a1f 0%, #2c3e2e 100%);
      display: flex;
      justify-content: center;
      align-items: center;
      font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
    }
    .game-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 12px;
      gap: 10px;
      max-width: 100%;
    }
    canvas {
      display: block;
      width: min(85vmin, 640px);
      height: min(85vmin, 640px);
      border-radius: 28px;
      box-shadow: 0 18px 30px rgba(0, 0, 0, 0.7), 0 0 0 4px #3a2e1f, 0 0 0 8px #d9b382;
      background-color: #1f2c1b;
      cursor: pointer;
      transition: filter 0.2s;
    }
    canvas:active {
      filter: brightness(0.95);
    }
    .status-bar {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      align-items: center;
      gap: 20px 32px;
      color: #f7e9c3;
      text-shadow: 0 2px 4px #00000080;
      font-weight: bold;
      font-size: clamp(1rem, 3.5vw, 1.6rem);
      letter-spacing: 0.5px;
      background: #2c3e2e;
      padding: 10px 28px;
      border-radius: 60px;
      box-shadow: inset 0 4px 8px #00000040, 0 8px 12px #00000060;
      border: 1px solid #6b8f6b;
    }
    .status-bar span {
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .badge {
      background: #d9b382;
      color: #1e2b1e;
      padding: 4px 16px;
      border-radius: 30px;
      font-size: 0.9em;
      box-shadow: inset 0 -3px 0 #9c7e58;
      text-shadow: none;
    }
    .overlay-message {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      pointer-events: none;
      z-index: 10;
    }
    .overlay-message div {
      background: #1f2c1bdd;
      backdrop-filter: blur(4px);
      color: #f7e9c3;
      font-size: clamp(1.4rem, 6vw, 2.6rem);
      font-weight: 800;
      padding: 24px 42px;
      border-radius: 60px;
      border: 3px solid #d9b382;
      box-shadow: 0 18px 30px black;
      text-align: center;
      animation: pulse 1.6s infinite ease-in-out;
      pointer-events: none;
      white-space: nowrap;
    }
    @keyframes pulse {
      0% { transform: scale(1); opacity: 0.95; }
      50% { transform: scale(1.04); opacity: 1; }
      100% { transform: scale(1); opacity: 0.95; }
    }
    @media (max-width: 500px) {
      .status-bar { padding: 8px 16px; gap: 12px; }
      .overlay-message div { padding: 16px 24px; white-space: normal; }
    }
  </style>
</head>
<body>
  <div class="game-container">
    <canvas id="gameCanvas" width="640" height="640"></canvas>
    <div class="status-bar">
      <span>🚂 <span class="badge" id="scoreDisplay">0</span></span>
      <span>👥 <span class="badge" id="passengerDisplay">0</span></span>
    </div>
  </div>
  <div class="overlay-message" id="overlay">
    <div>CLICK or PRESS ANY KEY<br>TO START THE TRAIN</div>
  </div>

  <script>
    (function() {
      // ----- CONFIG -----
      const GRID_SIZE = 16;           // 16x16 grid
      const CELL_SIZE = 640 / GRID_SIZE; // 40px

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

      // ----- GAME STATE -----
      let snake = [];          // array of {x, y}, head first
      let direction = { dx: 1, dy: 0 };  // moving right initially
      let nextDirection = { dx: 1, dy: 0 };
      let passengers = [];     // array of {x, y}
      let score = 0;
      let passengersCollected = 0;
      let gameState = 'demo';  // 'demo', 'playing', 'gameover'
      let gameLoopId = null;
      let moveInterval = 180;  // ms per move (demo & human)
      let lastMoveTime = 0;
      let animationFrame = null;

      // ----- DEMO AI -----
      let aiEnabled = true;

      // ----- INIT / RESET -----
      function resetGame() {
        // start with a small train: 3 segments, positioned near center-left
        snake = [
          { x: 5, y: 8 },  // head (locomotive)
          { x: 4, y: 8 },  // wagon 1
          { x: 3, y: 8 }   // wagon 2
        ];
        direction = { dx: 1, dy: 0 };
        nextDirection = { dx: 1, dy: 0 };
        score = 0;
        passengersCollected = 0;
        passengers = [];
        spawnPassenger();
        spawnPassenger(); // two passengers for better visuals
        updateScoreUI();
        // Ensure no immediate game over
        gameState = 'demo';
        aiEnabled = true;
        overlay.style.display = 'flex';
      }

      // spawn passenger at random free cell
      function spawnPassenger() {
        const maxAttempts = 500;
        for (let attempt = 0; attempt < maxAttempts; attempt++) {
          const x = Math.floor(Math.random() * GRID_SIZE);
          const y = Math.floor(Math.random() * GRID_SIZE);
          if (!snake.some(seg => seg.x === x && seg.y === y) &&
              !passengers.some(p => p.x === x && p.y === y)) {
            passengers.push({ x, y });
            return true;
          }
        }
        return false;
      }

      // update score and passenger count
      function updateScoreUI() {
        scoreDisplay.textContent = score;
        passengerDisplay.textContent = passengersCollected;
      }

      // ----- GAME ACTIONS -----
      function moveSnake() {
        if (gameState === 'gameover') return;

        // apply next direction (prevent reversing)
        if ((direction.dx !== -nextDirection.dx || direction.dy !== -nextDirection.dy) &&
            (nextDirection.dx !== 0 || nextDirection.dy !== 0)) {
          direction = { ...nextDirection };
        }

        const head = snake[0];
        const newHead = {
          x: head.x + direction.dx,
          y: head.y + direction.dy
        };

        // wall collision
        if (newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE) {
          gameOver();
          return;
        }

        // self collision (check all body segments)
        if (snake.some(seg => seg.x === newHead.x && seg.y === newHead.y)) {
          gameOver();
          return;
        }

        // move: add new head
        snake.unshift(newHead);

        // check passenger pickup
        const passengerIndex = passengers.findIndex(p => p.x === newHead.x && p.y === newHead.y);
        let pickedUp = false;
        if (passengerIndex !== -1) {
          // remove passenger, add score, increase length (already added)
          passengers.splice(passengerIndex, 1);
          score += 10;
          passengersCollected++;
          updateScoreUI();
          pickedUp = true;
          // spawn a new passenger (so at least one is visible)
          spawnPassenger();
          // also spawn extra passenger occasionally to keep it lively
          if (Math.random() < 0.5 && passengers.length < 4) {
            spawnPassenger();
          }
          // make the train a little faster as it grows
          moveInterval = Math.max(110, 190 - Math.floor(score / 20) * 8);
        }

        // remove tail if no pickup
        if (!pickedUp) {
          snake.pop();
        } else {
          // when picking up, we keep the tail (grow). But we also want to remove tail?
          // Actually we added head, but we need to keep old tail for growth.
          // So we do NOT pop. That's correct.
        }

        // ensure at least one passenger on field
        if (passengers.length === 0) {
          spawnPassenger();
        }
      }

      function gameOver() {
        if (gameState === 'gameover') return;
        gameState = 'gameover';
        overlay.style.display = 'flex';
        overlay.querySelector('div').innerHTML = '💥 TRAIN CRASHED! 💥<br>CLICK or PRESS KEY TO RESTART';
        if (gameLoopId) {
          clearInterval(gameLoopId);
          gameLoopId = null;
        }
      }

      // ----- DEMO AI (simple pathfinding) -----
      function updateDemoAI() {
        if (gameState !== 'demo' || !aiEnabled || passengers.length === 0) return;

        const head = snake[0];
        // choose the closest passenger (manhattan)
        let target = passengers[0];
        let minDist = Infinity;
        for (let p of passengers) {
          const dist = Math.abs(p.x - head.x) + Math.abs(p.y - head.y);
          if (dist < minDist) {
            minDist = dist;
            target = p;
          }
        }

        const dx = target.x - head.x;
        const dy = target.y - head.y;

        // preferred directions: try to move along the axis with larger distance
        let moves = [];
        if (Math.abs(dx) > Math.abs(dy)) {
          if (dx > 0) moves.push({ dx: 1, dy: 0 });
          if (dx < 0) moves.push({ dx: -1, dy: 0 });
          if (dy > 0) moves.push({ dx: 0, dy: 1 });
          if (dy < 0) moves.push({ dx: 0, dy: -1 });
        } else {
          if (dy > 0) moves.push({ dx: 0, dy: 1 });
          if (dy < 0) moves.push({ dx: 0, dy: -1 });
          if (dx > 0) moves.push({ dx: 1, dy: 0 });
          if (dx < 0) moves.push({ dx: -1, dy: 0 });
        }
        // add remaining axes as fallback
        const allDirs = [
          { dx: 1, dy: 0 }, { dx: -1, dy: 0 },
          { dx: 0, dy: 1 }, { dx: 0, dy: -1 }
        ];
        for (let d of allDirs) {
          if (!moves.some(m => m.dx === d.dx && m.dy === d.dy)) {
            moves.push(d);
          }
        }

        // avoid reverse direction
        const safeMoves = moves.filter(m => !(m.dx === -direction.dx && m.dy === -direction.dy));
        if (safeMoves.length === 0) return;

        // pick first safe move that doesn't immediately crash
        for (let m of safeMoves) {
          const nx = head.x + m.dx;
          const ny = head.y + m.dy;
          if (nx < 0 || nx >= GRID_SIZE || ny < 0 || ny >= GRID_SIZE) continue;
          if (snake.some(seg => seg.x === nx && seg.y === ny)) continue;
          if (passengers.some(p => p.x === nx && p.y === ny)) {
            // prioritize passenger move
            nextDirection = m;
            return;
          }
        }
        // if no passenger directly reachable, take any not crashing move
        for (let m of safeMoves) {
          const nx = head.x + m.dx;
          const ny = head.y + m.dy;
          if (nx < 0 || nx >= GRID_SIZE || ny < 0 || ny >= GRID_SIZE) continue;
          if (snake.some(seg => seg.x === nx && seg.y === ny)) continue;
          nextDirection = m;
          return;
        }
        // last resort: any direction not reversing (may crash)
        nextDirection = safeMoves[0] || direction;
      }

      // ----- DRAWING -----
      function draw() {
        ctx.clearRect(0, 0, 640, 640);

        // background grass / ground
        ctx.fillStyle = '#2e4a2a';
        ctx.fillRect(0, 0, 640, 640);

        // subtle grid lines
        ctx.strokeStyle = '#3f613b';
        ctx.lineWidth = 1.5;
        for (let i = 0; i <= GRID_SIZE; i++) {
          const pos = i * CELL_SIZE;
          ctx.beginPath();
          ctx.moveTo(pos, 0);
          ctx.lineTo(pos, 640);
          ctx.strokeStyle = '#3f613b';
          ctx.stroke();
          ctx.beginPath();
          ctx.moveTo(0, pos);
          ctx.lineTo(640, pos);
          ctx.stroke();
        }

        // draw passengers (waiting on platform - represented as tiny people)
        passengers.forEach(p => {
          const cx = p.x * CELL_SIZE + CELL_SIZE / 2;
          const cy = p.y * CELL_SIZE + CELL_SIZE / 2;
          drawPassenger(cx, cy, CELL_SIZE * 0.32);
        });

        // draw snake (train) from tail to head for correct layering
        for (let i = snake.length - 1; i >= 0; i--) {
          const seg = snake[i];
          const isHead = (i === 0);
          drawTrainSegment(seg.x, seg.y, i, isHead);
        }

        // draw headlight glow on locomotive if head exists
        if (snake.length > 0) {
          const head = snake[0];
          const hx = head.x * CELL_SIZE + CELL_SIZE / 2;
          const hy = head.y * CELL_SIZE + CELL_SIZE / 2;
          ctx.shadowColor = '#ffdd88';
          ctx.shadowBlur = 20;
          ctx.beginPath();
          ctx.arc(hx, hy, CELL_SIZE * 0.15, 0, 2 * Math.PI);
          ctx.fillStyle = '#fff9e6';
          ctx.fill();
          ctx.shadowBlur = 0;
          ctx.shadowColor = 'transparent';
        }
      }

      function drawPassenger(cx, cy, radius) {
        // little person with suitcase
        // body
        ctx.fillStyle = '#f7d9b3';
        ctx.beginPath();
        ctx.ellipse(cx - radius * 0.2, cy + radius * 0.2, radius * 0.5, radius * 0.6, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#3b5e8b';
        ctx.beginPath();
        ctx.ellipse(cx - radius * 0.2, cy + radius * 0.55, radius * 0.55, radius * 0.4, 0, 0, Math.PI * 2);
        ctx.fill();
        // head
        ctx.fillStyle = '#f7d9b3';
        ctx.beginPath();
        ctx.arc(cx - radius * 0.2, cy - radius * 0.1, radius * 0.38, 0, 2 * Math.PI);
        ctx.fill();
        // hat
        ctx.fillStyle = '#b84a4a';
        ctx.beginPath();
        ctx.ellipse(cx - radius * 0.2, cy - radius * 0.3, radius * 0.4, radius * 0.18, 0, 0, Math.PI * 2);
        ctx.fill();
        // suitcase
        ctx.fillStyle = '#8b5a2b';
        ctx.fillRect(cx + radius * 0.2, cy + radius * 0.1, radius * 0.5, radius * 0.4);
        ctx.fillStyle = '#c49a6c';
        ctx.fillRect(cx + radius * 0.3, cy + radius * 0.2, radius * 0.1, radius * 0.15);
        // handle
        ctx.strokeStyle = '#3d2b1a';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(cx + radius * 0.3, cy + radius * 0.1);
        ctx.lineTo(cx + radius * 0.3, cy - radius * 0.1);
        ctx.stroke();
      }

      function drawTrainSegment(gridX, gridY, index, isHead) {
        const x = gridX * CELL_SIZE;
        const y = gridY * CELL_SIZE;
        const w = CELL_SIZE;
        const h = CELL_SIZE;
        const padding = 3;
        const radius = 8;

        // colors: locomotive is dark red/black, wagons use cycle of vibrant colors
        const wagonColors = ['#e74c3c', '#f1c40f', '#2ecc71', '#3498db', '#9b59b6', '#e67e22'];
        let baseColor, topColor, windowColor, wheelColor;

        if (isHead) {
          baseColor = '#b33e2b';
          topColor = '#7a2a1c';
          windowColor = '#f9e79f';
          wheelColor = '#2d1e0e';
        } else {
          const colIndex = (index - 1) % wagonColors.length;
          baseColor = wagonColors[colIndex];
          topColor = shadeColor(baseColor, -30);
          windowColor = '#fef9e7';
          wheelColor = '#2d1e0e';
        }

        // draw body with slight rounding
        ctx.shadowColor = '#00000060';
        ctx.shadowBlur = 8;
        ctx.fillStyle = baseColor;
        ctx.beginPath();
        ctx.roundRect(x + padding, y + padding, w - 2 * padding, h - 2 * padding - 4, radius);
        ctx.fill();

        // roof line / accent
        ctx.shadowBlur = 4;
        ctx.fillStyle = topColor;
        ctx.beginPath();
        ctx.roundRect(x + padding, y + padding, w - 2 * padding, 8, radius);
        ctx.fill();

        // windows (two small rectangles)
        ctx.shadowBlur = 0;
        ctx.fillStyle = windowColor;
        // window 1
        ctx.beginPath();
        ctx.roundRect(x + w * 0.2, y + h * 0.35, w * 0.25, h * 0.25, 4);
        ctx.fill();
        // window 2
        ctx.beginPath();
        ctx.roundRect(x + w * 0.55, y + h * 0.35, w * 0.25, h * 0.25, 4);
        ctx.fill();

        // wheels (two circles)
        ctx.fillStyle = wheelColor;
        ctx.beginPath();
        ctx.arc(x + w * 0.3, y + h - 8, 5, 0, 2 * Math.PI);
        ctx.fill();
        ctx.beginPath();
        ctx.arc(x + w * 0.7, y + h - 8, 5, 0, 2 * Math.PI);
        ctx.fill();
        ctx.fillStyle = '#aaa';
        ctx.beginPath();
        ctx.arc(x + w * 0.3, y + h - 8, 2, 0, 2 * Math.PI);
        ctx.fill();
        ctx.beginPath();
        ctx.arc(x + w * 0.7, y + h - 8, 2, 0, 2 * Math.PI);
        ctx.fill();

        // locomotive extra: smokestack and headlight
        if (isHead) {
          ctx.fillStyle = '#3d2b1a';
          ctx.beginPath();
          ctx.roundRect(x + w * 0.15, y + 4, w * 0.2, 10, 3);
          ctx.fill();
          // smoke puff
          ctx.fillStyle = '#ffffffb0';
          ctx.beginPath();
          ctx.arc(x + w * 0.25, y + 2, 6, 0, 2 * Math.PI);
          ctx.fill();
          ctx.fillStyle = '#ffffff60';
          ctx.beginPath();
          ctx.arc(x + w * 0.3, y - 4, 5, 0, 2 * Math.PI);
          ctx.fill();
        }

        // reset shadow
        ctx.shadowBlur = 0;
        ctx.shadowColor = 'transparent';
      }

      // helper to darken/lighten
      function shadeColor(color, percent) {
        const num = parseInt(color.replace('#', ''), 16);
        const r = Math.min(255, Math.max(0, (num >> 16) + percent));
        const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + percent));
        const b = Math.min(255, Math.max(0, (num & 0x0000FF) + percent));
        return `rgb(${r}, ${g}, ${b})`;
      }

      // Canvas roundRect polyfill
      CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
        if (w < 2 * r) r = w / 2;
        if (h < 2 * r) r = h / 2;
        this.moveTo(x + r, y);
        this.lineTo(x + w - r, y);
        this.quadraticCurveTo(x + w, y, x + w, y + r);
        this.lineTo(x + w, y + h - r);
        this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
        this.lineTo(x + r, y + h);
        this.quadraticCurveTo(x, y + h, x, y + h - r);
        this.lineTo(x, y + r);
        this.quadraticCurveTo(x, y, x + r, y);
        return this;
      };

      // ----- GAME LOOP (time-based) -----
      function gameTick(now) {
        if (!lastMoveTime) lastMoveTime = now;
        const elapsed = now - lastMoveTime;

        if (gameState === 'demo' || gameState === 'playing') {
          if (elapsed >= moveInterval) {
            lastMoveTime = now;
            if (gameState === 'demo') {
              updateDemoAI();
            }
            moveSnake();
          }
        }

        draw();

        if (gameState !== 'gameover') {
          animationFrame = requestAnimationFrame(gameTick);
        }
      }

      // ----- START / STOP / RESTART -----
      function startGame() {
        if (gameState === 'gameover') {
          resetGame();
          gameState = 'demo'; // start in demo again for safety
          overlay.querySelector('div').innerHTML = 'CLICK or PRESS ANY KEY<br>TO START THE TRAIN';
        }
        // from demo -> playing
        if (gameState === 'demo') {
          gameState = 'playing';
          aiEnabled = false;
          overlay.style.display = 'none';
        }
        // reset movement timer
        lastMoveTime = performance.now();
        if (gameLoopId) clearInterval(gameLoopId);
        if (animationFrame) cancelAnimationFrame(animationFrame);

        // start loop
        animationFrame = requestAnimationFrame(gameTick);
      }

      // restart from user interaction after game over
      function handleStartInteraction() {
        if (gameState === 'gameover') {
          resetGame();
          gameState = 'demo';
          overlay.querySelector('div').innerHTML = 'CLICK or PRESS ANY KEY<br>TO START THE TRAIN';
          overlay.style.display = 'flex';
          lastMoveTime = 0;
          if (animationFrame) cancelAnimationFrame(animationFrame);
          animationFrame = requestAnimationFrame(gameTick);
        } else if (gameState === 'demo') {
          startGame();
        }
      }

      // ----- EVENT LISTENERS -----
      function handleKeydown(e) {
        // start or restart from overlay
        if (gameState === 'demo' || gameState === 'gameover') {
          handleStartInteraction();
          // prevent default for arrow keys to avoid scroll
          if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
            e.preventDefault();
          }
          return;
        }

        // playing: control direction
        if (gameState === 'playing') {
          const key = e.key;
          let newDx = 0, newDy = 0;
          if (key === 'ArrowUp' || key === 'w' || key === 'W') { newDx = 0; newDy = -1; }
          else if (key === 'ArrowDown' || key === 's' || key === 'S') { newDx = 0; newDy = 1; }
          else if (key === 'ArrowLeft' || key === 'a' || key === 'A') { newDx = -1; newDy = 0; }
          else if (key === 'ArrowRight' || key === 'd' || key === 'D') { newDx = 1; newDy = 0; }

          if (newDx !== 0 || newDy !== 0) {
            e.preventDefault();
            // prevent reversing
            if (!(direction.dx === -newDx && direction.dy === -newDy)) {
              nextDirection = { dx: newDx, dy: newDy };
            }
          }
        }
      }

      function handleCanvasClick() {
        if (gameState === 'demo' || gameState === 'gameover') {
          handleStartInteraction();
        }
      }

      // ----- INITIALIZE -----
      function init() {
        resetGame();
        // start demo loop immediately
        lastMoveTime = performance.now();
        animationFrame = requestAnimationFrame(gameTick);

        // attach listeners
        window.addEventListener('keydown', handleKeydown);
        canvas.addEventListener('click', handleCanvasClick);
        overlay.addEventListener('click', handleStartInteraction);
        // Allow overlay pointer events? It's set to none but on the div? we add click on overlay itself (pointer-events none)
        // So better to listen on document or make overlay pointer-events auto
        // Actually the overlay has pointer-events: none, so we need to rely on canvas click or keypress.
        // But the overlay is on top; we can set pointer-events: auto on the inner div? 
        // Instead, let's attach click on canvas and also on a full-screen listener if needed.
        // We'll add a global click listener that checks if overlay is visible.
        window.addEventListener('click', function(e) {
          if (gameState === 'demo' || gameState === 'gameover') {
            // but avoid double-trigger with canvas click
            if (e.target === canvas || e.target === overlay || overlay.contains(e.target)) {
              // handled by canvas/overlay
            } else {
              handleStartInteraction();
            }
          }
        });
      }

      // start everything
      init();
    })();
  </script>
</body>
</html>