Models 44

gpt-oss-120b

by OpenAIReasoningClean
Open in new window

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

How it went

Total time
3 min 44 s
Cost
$0.011
Output tokens
26,622
Spent thinking
23,044 tokens
Speed
119.6 tok/s
Console errors
0
First token after
898 ms
First visible text after
3 min 15 s
Prompt tokens
557
Code size
14.1 KB
Finish reason
stop
Served by
Sail Research
Model id
openai/gpt-oss-120b
Reasoning sent
{"effort":"high"}
Max output
64,000 tokens
Generated
Nov 11, 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 · 472 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Polished Snake Demo</title>
<style>
    html,body{
        margin:0;
        height:100%;
        overflow:hidden;
        background:#222;
        font-family:Arial,Helvetica,sans-serif;
        color:#fff;
    }
    #gameContainer{
        position:relative;
        width:100%;
        height:100%;
        display:flex;
        align-items:center;
        justify-content:center;
    }
    #gameCanvas{
        background:#111;
        border:4px solid #333;
        border-radius:12px;
        box-shadow:0 0 10px rgba(0,0,0,0.5);
        display:block;
    }
    #overlay{
        position:absolute;
        top:0; left:0;
        width:100%; height:100%;
        background:rgba(0,0,0,0.6);
        display:flex;
        align-items:center;
        justify-content:center;
        text-align:center;
        font-size:1.5rem;
        cursor:pointer;
        user-select:none;
        z-index:10;
    }
    #overlay div{
        padding:1rem 2rem;
        background:rgba(0,0,0,0.3);
        border-radius:8px;
    }
    #scoreboard{
        position:absolute;
        top:10px;
        left:10px;
        font-size:1.1rem;
        z-index:5;
        pointer-events:none;
    }
</style>
</head>
<body>
<div id="gameContainer">
    <canvas id="gameCanvas"></canvas>
    <div id="overlay"></div>
</div>
<div id="scoreboard"><span id="score">Score: 0</span> | <span id="best">Best: 0</span></div>

<script>
(() => {
    // ----- Configuration -----
    const GRID_SIZE = 20;               // 20x20 cells
    const STEP_DURATION = 120;          // ms per cell (speed)
    // -------------------------

    // ----- State variables -----
    let canvas, ctx;
    let canvasSize, cellSize;
    let snake = [], prevSnake = [];
    let direction = {x:1, y:0};
    let directionQueue = [];
    let food = {x:0, y:0};
    let score = 0, bestScore = 0;
    let gameState = 'demo';  // demo | playing | paused | gameover
    let progress = 0;        // 0..1 interpolation progress
    let lastTime = 0;
    let particles = [];
    // ---------------------------

    // ----- Canvas setup -----
    function initCanvas(){
        canvas = document.getElementById('gameCanvas');
        ctx = canvas.getContext('2d');
        resizeCanvas();
        window.addEventListener('resize', () => {
            ctx.setTransform(1,0,0,1,0,0);
            resizeCanvas();
        });
    }
    function resizeCanvas(){
        const dpr = window.devicePixelRatio || 1;
        const size = Math.min(window.innerWidth, window.innerHeight) * 0.9;
        canvasSize = Math.floor(size);
        cellSize = canvasSize / GRID_SIZE;
        canvas.width = canvasSize * dpr;
        canvas.height = canvasSize * dpr;
        canvas.style.width = canvasSize + 'px';
        canvas.style.height = canvasSize + 'px';
        ctx.setTransform(1,0,0,1,0,0);
        ctx.scale(dpr, dpr);
    }
    // ------------------------

    // ----- Game initialization -----
    function initGame(){
        const startX = Math.floor(GRID_SIZE/2);
        const startY = Math.floor(GRID_SIZE/2);
        snake = [
            {x:startX,   y:startY},
            {x:startX-1, y:startY},
            {x:startX-2, y:startY},
            {x:startX-3, y:startY},
            {x:startX-4, y:startY}
        ];
        prevSnake = snake.map(s=>({x:s.x, y:s.y}));
        direction = {x:1, y:0};
        directionQueue = [];
        score = 0;
        updateScore();
        placeFood();
        progress = 0;
        particles = [];
    }

    function placeFood(){
        const occupied = new Set(snake.map(s=>s.x+','+s.y));
        let x, y;
        do{
            x = Math.floor(Math.random()*GRID_SIZE);
            y = Math.floor(Math.random()*GRID_SIZE);
        }while(occupied.has(x+','+y));
        food = {x, y};
    }
    // -------------------------------

    // ----- Input handling -----
    window.addEventListener('keydown', handleKey);
    function handleKey(e){
        if (gameState === 'demo'){
            startGame();
            e.preventDefault();
            return;
        }
        if (gameState === 'gameover'){
            if (e.code === 'Space' || e.code === 'Enter'){
                startGame();
                e.preventDefault();
            }
            return;
        }
        if (gameState === 'paused'){
            if (e.key.toLowerCase() === 'p'){
                resumeGame();
                e.preventDefault();
            }
            return;
        }
        // playing state
        if (e.key.toLowerCase() === 'p'){
            pauseGame();
            e.preventDefault();
            return;
        }
        let nd = null;
        switch(e.key){
            case 'ArrowUp': case 'w': case 'W': nd = {x:0, y:-1}; break;
            case 'ArrowDown': case 's': case 'S': nd = {x:0, y:1}; break;
            case 'ArrowLeft': case 'a': case 'A': nd = {x:-1, y:0}; break;
            case 'ArrowRight': case 'd': case 'D': nd = {x:1, y:0}; break;
        }
        if (nd){
            enqueueDirection(nd);
            e.preventDefault();
        }
    }

    function enqueueDirection(dir){
        const last = directionQueue.length ? directionQueue[directionQueue.length-1] : direction;
        // ignore reverse or duplicate
        if (dir.x===-last.x && dir.y===-last.y) return;
        if (dir.x===last.x && dir.y===last.y) return;
        directionQueue.push(dir);
    }
    // ---------------------------

    // ----- Overlay helpers -----
    const overlay = document.getElementById('overlay');
    overlay.addEventListener('click', () => {
        if (gameState !== 'playing') startGame();
    });

    function showOverlay(html){
        overlay.innerHTML = `<div>${html}</div>`;
        overlay.style.display = 'flex';
    }
    function hideOverlay(){
        overlay.style.display = 'none';
    }

    function startGame(){
        hideOverlay();
        gameState = 'playing';
        initGame();
    }

    function pauseGame(){
        if (gameState === 'playing'){
            gameState = 'paused';
            showOverlay('Paused<br>Press P to resume');
        }
    }
    function resumeGame(){
        if (gameState === 'paused'){
            hideOverlay();
            gameState = 'playing';
        }
    }
    // ---------------------------

    // ----- Score handling -----
    function updateScore(){
        document.getElementById('score').textContent = 'Score: ' + score;
        document.getElementById('best').textContent = 'Best: ' + bestScore;
    }
    // ---------------------------

    // ----- Game over handling -----
    function handleGameOver(){
        if (gameState === 'demo'){
            // Restart demo after a brief pause
            setTimeout(() => initGame(), 200);
        }else{
            if (score > bestScore) bestScore = score;
            updateScore();
            showOverlay(`Game Over<br>Score: ${score}<br>Press Space or Enter to restart`);
            gameState = 'gameover';
        }
    }
    // ------------------------------

    // ----- Simple AI for demo -----
    function demoStep(){
        const head = snake[0];
        const dxFood = Math.sign(food.x - head.x);
        const dyFood = Math.sign(food.y - head.y);
        const dirs = [
            {x:0, y:-1},
            {x:1, y:0},
            {x:0, y:1},
            {x:-1, y:0}
        ];
        const reverse = {x:-direction.x, y:-direction.y};
        const safe = dirs.filter(d => {
            if (d.x===reverse.x && d.y===reverse.y) return false;
            const nx = head.x + d.x;
            const ny = head.y + d.y;
            if (nx<0||nx>=GRID_SIZE||ny<0||ny>=GRID_SIZE) return false;
            if (snake.some(s=>s.x===nx && s.y===ny)) return false;
            return true;
        });
        if (safe.length===0) return; // trapped

        // Prefer moving toward food
        let chosen = null;
        if (Math.abs(food.x - head.x) > Math.abs(food.y - head.y) && dxFood !== 0){
            const cand = {x:dxFood, y:0};
            if (safe.some(d=>d.x===cand.x && d.y===cand.y)) chosen = cand;
        }
        if (!chosen && dyFood !== 0){
            const cand = {x:0, y:dyFood};
            if (safe.some(d=>d.x===cand.x && d.y===cand.y)) chosen = cand;
        }
        if (!chosen){
            chosen = safe[Math.floor(Math.random()*safe.length)];
        }
        direction = chosen;
    }
    // ------------------------------

    // ----- Movement step -----
    function step(){
        // remember previous positions for smooth interpolation
        prevSnake = snake.map(s=>({x:s.x, y:s.y}));

        const head = snake[0];
        const newHead = {x: head.x + direction.x, y: head.y + direction.y};
        const eating = (newHead.x===food.x && newHead.y===food.y);

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

        // body collision (ignore tail if it will move away)
        const collision = snake.some((seg, idx) => {
            if (!eating && idx===snake.length-1) return false;
            return seg.x===newHead.x && seg.y===newHead.y;
        });
        if (collision){
            handleGameOver();
            return;
        }

        // apply movement
        snake.unshift(newHead);
        if (eating){
            score += 10;
            updateScore();
            spawnParticles(newHead);
            placeFood();
            // leave tail to grow
        }else{
            snake.pop(); // move tail forward
        }
    }
    // -------------------------

    // ----- Particle System -----
    function spawnParticles(pos){
        const count = 12;
        const baseX = (pos.x + 0.5) * cellSize;
        const baseY = (pos.y + 0.5) * cellSize;
        for (let i=0;i<count;i++){
            const angle = Math.random()*2*Math.PI;
            // speed ~ 0.015–0.03 * cellSize px per ms (≈0.2–0.4px per frame)
            const speed = cellSize * (0.015 + Math.random()*0.015);
            particles.push({
                x: baseX,
                y: baseY,
                vx: Math.cos(angle)*speed,
                vy: Math.sin(angle)*speed,
                life: 400,
                maxLife: 400,
                size: Math.random()*3 + 2
            });
        }
    }
    function updateParticles(dt){
        for (let i=particles.length-1; i>=0; i--){
            const p = particles[i];
            p.life -= dt;
            if (p.life <= 0){
                particles.splice(i,1);
                continue;
            }
            p.x += p.vx * dt;
            p.y += p.vy * dt;
        }
    }
    // --------------------------

    // ----- Drawing -----
    function draw(){
        // background
        ctx.fillStyle = '#111';
        ctx.fillRect(0,0,canvasSize,canvasSize);

        // food
        ctx.fillStyle = '#e00';
        const fx = (food.x + 0.5) * cellSize;
        const fy = (food.y + 0.5) * cellSize;
        ctx.beginPath();
        ctx.arc(fx, fy, cellSize*0.3, 0, Math.PI*2);
        ctx.fill();

        // snake
        for (let i=0;i<snake.length;i++){
            const cur = snake[i];
            const prev = (prevSnake[i] !== undefined) ? prevSnake[i] : cur;
            const ix = (prev.x + (cur.x - prev.x) * progress) * cellSize;
            const iy = (prev.y + (cur.y - prev.y) * progress) * cellSize;

            if (i===0){
                // head
                ctx.fillStyle = '#4caf50';
                ctx.fillRect(ix, iy, cellSize, cellSize);
                // eyes
                const eyeR = cellSize * 0.12;
                const fwd = cellSize * 0.2;
                const side = cellSize * 0.15;
                const cx = ix + cellSize/2;
                const cy = iy + cellSize/2;
                const dx = direction.x;
                const dy = direction.y;
                const perpX = -dy;
                const perpY = dx;
                const eye1X = cx + dx*fwd + perpX*side;
                const eye1Y = cy + dy*fwd + perpY*side;
                const eye2X = cx + dx*fwd - perpX*side;
                const eye2Y = cy + dy*fwd - perpY*side;
                ctx.fillStyle = '#fff';
                ctx.beginPath();
                ctx.arc(eye1X, eye1Y, eyeR, 0, Math.PI*2);
                ctx.arc(eye2X, eye2Y, eyeR, 0, Math.PI*2);
                ctx.fill();
                ctx.fillStyle = '#000';
                ctx.beginPath();
                ctx.arc(eye1X, eye1Y, eyeR*0.5, 0, Math.PI*2);
                ctx.arc(eye2X, eye2Y, eyeR*0.5, 0, Math.PI*2);
                ctx.fill();
            }else{
                ctx.fillStyle = '#2e7d32';
                ctx.fillRect(ix, iy, cellSize, cellSize);
            }
        }

        // particles
        particles.forEach(p=>{
            const alpha = p.life / p.maxLife;
            ctx.globalAlpha = alpha;
            ctx.fillStyle = '#ffeb3b';
            ctx.beginPath();
            ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
            ctx.fill();
        });
        ctx.globalAlpha = 1;
    }
    // ------------------

    // ----- Main loop -----
    function loop(timestamp){
        if (!lastTime) lastTime = timestamp;
        const dt = timestamp - lastTime;
        lastTime = timestamp;

        if (gameState !== 'paused' && gameState !== 'gameover'){
            progress += dt / STEP_DURATION;
            while (progress >= 1){
                if (gameState === 'demo'){
                    demoStep();
                }else if (gameState === 'playing'){
                    if (directionQueue.length){
                        const nd = directionQueue.shift();
                        if (!(nd.x===-direction.x && nd.y===-direction.y)){
                            direction = nd;
                        }
                    }
                }
                step();
                progress -= 1;
                if (gameState === 'gameover'){
                    progress = 0;
                    break;
                }
            }
        }

        updateParticles(dt);
        draw();
        requestAnimationFrame(loop);
    }
    // --------------------

    // ----- Startup -----
    initCanvas();
    showOverlay('Click or press any key<br>to start');
    gameState = 'demo';
    initGame();
    requestAnimationFrame(loop);
    // -------------------
})();
</script>
</body>
</html>
Reasoning trace (23,044 tokens)
The user asks: Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>.

Requirements:

- A grid of at least 20x20 cells, centered on the page.
- Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but snake cannot reverse onto itself.
- Eating food adds 10 points and grows snake by one cell; new food never spawns on the snake.
- Game over when snake hits wall or itself, with overlay showing the score; Space or Enter restarts, P pauses.
- Visual polish: smooth movement, snake with distinct head and eyes facing direction, small particle burst when eating.
- Display current score and best score of session.

Additional constraints: Must be a single HTML file with everything inline, no external assets except allowed CDNs (like maybe a font but not required). Should be responsive, looks good at 1280x800 down to 375px. The page must display something on first paint. The game should start in a self-playing demo mode behind a "Click or press a key to start" message, because interaction needed for keyboard events. So we need to display a start overlay or "Click to start" message. When the user clicks (or presses a key), then keyboard controls become active. However, the scoring overlay appears after game over. And also need best score of the session - but we cannot use localStorage due to sandbox restrictions. The user says best score of the session: we can store best score in a global variable for the current session (i.e., until page reload). So we can maintain bestScore and update it in runtime.

Additionally need to run self-playing demo when page loads. In the requirement of sandbox: game must start in a self-playing demo mode behind a "Click or press a key to start" message. So we need an AI (simple movement) for snake to play automatically in a demo, perhaps using a simple algorithm: follow direction and attempt to avoid walls by turning left or right randomly or a simple deterministic approach like always turn right when nearing wall. That AI should be running in the background when the game hasn't started. Once user initiates, the AI stops, and player can control.

But we have to also consider the initial screenshot taken after 3 seconds automatically. So the "Click or press a key to start" overlay should be visible for that screenshot. It says "The page must display something on first paint". The screenshot will show the overlay and the game state (probably a snake moving). That is fine.

Implementation details: Use <canvas> centred on page. Use CSS Flexbox for centering. Set canvas size relative to viewport to fit the grid. 20x20 cells grid at unit size maybe 20px each = 400px. But we need scaling for high DPI. We can compute tile size based on canvas width/height so that grid fits.

To have smooth movement: snake moves one cell per step. But polish: we can animate movement across cells with smooth interpolation using requestAnimationFrame. To keep snake movement smooth, we can maintain a per-frame progress from 0 to 1 over a step duration (e.g., 100ms per cell). The snake model: array of segment positions (grid coordinates). For rendering, we can compute intermediate positions for each segment based on progress; the head moves to next cell, tail moves following. Smooth animation size: at each frame, we compute interpolation between each segment's previous position and current target position.

Or we can use simple approach: draw grid at cell resolution, moving per tick, but smoothing not required; but spec says "smooth movement". We'll implement a simple interpolation technique.

Snake head: distinct head shape, perhaps a rectangle with round corners and eyes (two small circles) that points direction: we need to rotate eyes positions based on direction. Or we can render head as a rectangle with head color differ from body. And we can draw eyes as small white circles.

Particle burst when eating: small radial particles that expand and fade.

Input handling: arrow keys and WASD. We need to ensure quick successive key presses are honored: we need to record direction changes that are valid and apply each tick. Usually pressing quickly may cause direction changes within the same frame. The snake cannot reverse onto itself: ensure new direction is not opposite of current direction.

Game loop: using requestAnimationFrame to animate, with a per-step timer that is based on speed (e.g., 150ms per step). When paused, stop updating.

Game over overlay with score, restart with Space or Enter. P for pause.

Display of current score and best score: probably as top-left overlay.

Start overlay: "Click or press any key to start" and maybe "Demo playing..." behind.

We must not use localStorage. So bestScore just in memory.

We also need responsive design: canvas size responsive. Could set canvas width = 80vw or limited by max dimension. E.g., we can compute size = Math.min(window.innerWidth, window.innerHeight) * 0.8. Keep the aspect ratio same as grid (square). Set appropriate CSS.

We need all code in one HTML file with inline <style> <script>. Avoid external requests. However we can import maybe a CDN for a font like "Press Start 2P" for retro vibe; but not required. The task doesn't require any external fonts. So we can use system fonts.

Potential tricky parts: the uniform movement with interpolation. We'll need to store the positions of segments and the previous positions to interpolate. For each segment we need previous grid coordinate (xPrev, yPrev) and new coordinate after step (xNew, yNew). At each frame we compute t = progress / stepDuration, and segment position = lerp(prev, new). For the head, same. The tail's previous and new are also stored; after each step, we shift the positions.

Implementation approach:

Define variables:

gridSize = 20 (cells per side)
cellSize = computed based on canvas size

Define snake = array of {x: number, y: number} representing each segment's current cell location (grid coords). Also store prevPositions: array of same length, storing last cell positions before moving.

Define direction = {dx, dy}, initially (1,0) (right). We can map directions as 0: up, 1: right, 2: down, 3: left.

Define pendingDirection = null; process input to set pendingDirection if it's not opposite. Also add to a directionQueue to handle multiple quick presses; but given the movement speed is 150ms per step, we can queue any number of valid direction changes, but must ensure no reversal; we can store them in an array; each movement step we apply one direction change if any from queue.

Simplify: We'll keep a directionQueue (max length maybe 2). When key pressed, if it's not opposite of current direction and not opposite of the last queued direction (or if queue empty, not opposite of current), then push to queue.

During each step (when progress reaches 1), we set current direction = queue.shift() if exists; else keep direction.

Movement: newHead = {x: head.x + direction.dx, y: head.y + direction.dy}. Add to front of snake; remove tail if not eating.

Food: generate random cell not on snake; store in variable.

Growth: when head reaches food cell (i.e., after step completed), we increment score, add 10 points, grow by leaving tail (i.e., skip removal of tail). Also spawn particle burst.

We need to track score, bestScore.

Game states: "demo", "running", "paused", "gameover", "startOverlay"? We'll have states: 'waiting' (show start overlay, no game), 'demo' (ai playing), 'playing', 'paused', 'gameover'. When page loads, we can start demo automatically, and show overlay "Click or press any key to start". When click or any key, we transition to playing (player control) and hide overlay. If player dies, go to gameover overlay where they can press Space/Enter to restart.

During demo, we need AI to decide direction. Simple AI: always try to move towards food; if moving directly would cause collision (wall or self), then choose a random safe direction that's not opposite. Actually simpler: we can do a basic algorithm: head tries to turn right when hitting wall else go straight. Or we can use BFS to find path to food and follow; but that'll be heavy. Simpler approach: prioritize staying within bounds: if next move leads to collision, choose a direction that avoids colliding (e.g., rotate direction clockwise until safe). That works.

Implementation details for handling keyboard events and audio (not required). No audio.

Rendering:

- Clear canvas each frame.
- Draw background (maybe dark).
- Draw food: a red circle or square within cell.
- Draw snake: for each segment, draw rectangle with color (head color distinct). Use rounded corners for head. Draw eyes on head: small white circles offset based on direction.

- For each segment besides head, simple filled rectangle.

- Particle burst: maintain list of particles: each particle has position (x, y) in pixel coordinates, velocity (dx, dy), life, radius, etc. When food eaten, generate N particles (e.g., 10) with random direction, speed, max life. Each frame, update positions, fade, and draw as small circles.

Also show score text: e.g., top left "Score: X" and "Best: Y". Use CSS overlay or draw in canvas.

We might have overlay for Game over: show large rectangle semi-transparent over canvas, with text.

We must ensure everything is visible on first paint; the demo must be playing automatically behind an overlay: we can have overlay div with text "Click or press any key to start" in the center. The demo runs in background; but not visible to user unless they can see through overlay (maybe overlay partially transparent). But the requirement: "Game 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." So we can have that overlay placed on top but allow the demo to be visible behind. Could set overlay with semi-transparent dark background, with text; canvas visible behind shows snake moving. So screenshot will show that overlay + moving snake. Then after click or key, hide overlay and start player control.

Implement the overlay as a full-screen absolute positioned div with background rgba(0,0,0,0.5) and text. Also show "Press any key or click to start". Use pointer events: everything is clickable.

Keyboard start: handle keydown events even when overlay present? The overlay might capture key events; we attach to window.

One detail: On first load, need a requestAnimationFrame loop always running, regardless of game state, to animate demo. When paused, skip updating.

We must ensure horizontal scroll no. Use CSS: body { margin:0; display:flex; align-items:center; justify-content:center; height:100vh; overflow:hidden; background:#111; } Canvas element: width= size, height= size, style to display block.

Initialize canvas dimensions: use devicePixelRatio for crispness: set canvas.width = size * dpr; canvas.height = size * dpr; then scale context.

Compute cellSize = size / gridSize; actual pixel coordinates per cell = cellSize.

Implement coordinate conversion: to draw a cell at (x,y) in grid, call ctx.fillRect(x*cellSize, y*cellSize, cellSize, cellSize) (after scaling context for dpr).

Now code.

Edge cases: snake collides with wall if head.x < 0 or >= gridSize or y similarly. Or collides with itself: check array of positions (including new head) if any duplicates.

Don't allow reverse: direction cannot be opposite to current direction (dx,dy). So allowed new direction if newdx+currdx !=0 or newdy+currdy !=0? Actually if newdx=== -dx && newdy === -dy, then reverse.

Implementation details for quick successive keypress: Use directionQueue as above. At each movement step (when progress resets), we apply at most one queued direction (the first). If multiple queued, they will be applied after subsequent steps.

Also ensure that start of demo uses directionQueue or the AI sets direction directly each step.

Game speed: define stepDuration = 100ms? Depends. To give smooth movement, we do a short step. But must be fast enough. Maybe stepDuration = 120ms initially, can accelerate over time? Not required. We'll keep constant.

Now create structure:

<html>
<head>
<title>Snake</title>
<style> ... </style>
</head>
<body>
<div id="overlay">Click or press any key to start</div>
<canvas id="gameCanvas"></canvas>
<div id="scoreboard"><span id="score">Score: 0</span> | <span id="best">Best: 0</span></div>
</body>
<script>

// constants
const GRID_SIZE = 20;
const CANVAS_SIZE = Math.min(window.innerWidth, window.innerHeight) * 0.8; // compute after load

// variables
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let dpr = window.devicePixelRatio || 1;

// set sizes
function resizeCanvas(){ ... }
resizeCanvas();
window.addEventListener('resize', resizeCanvas);

// Game state variables
let snake = [];
let direction = {x: 1, y: 0};
let directionQueue = [];
let food = {x: 0, y: 0};
let score = 0;
let bestScore = 0;
let gameState = 'demo'; // 'waiting' for overlay? Actually overlay shown when state 'waiting' but demo runs anyway
let progress = 0; // 0 to 1 interpolation progress
let stepDuration = 120; // ms per cell
let lastTime = 0;
let particles = [];

// functions: initGame, resetGame (when starting new), placeFood, update, draw, etc.

function initGame(){ // sets snake initial length 5, direction right, etc.
  snake = [{x: 5, y: 10}, {x:4, y:10}, {x:3,y:10}, {x:2,y:10}, {x:1,y:10}]; // head first
  direction = {x:1, y:0};
  directionQueue = [];
  score = 0; updateScore();
  placeFood();
  progress = 0;
  particles = [];
}

function placeFood(){ // find random cell not occupied by snake
  let free = false;
  while (!free){
    let x = Math.floor(Math.random()*GRID_SIZE);
    let y = Math.floor(Math.random()*GRID_SIZE);
    free = !snake.some(seg => seg.x===x && seg.y===y);
    if (free){ food = {x, y}; }
  }
}

// Input
window.addEventListener('keydown', handleKey);
function handleKey(e){
   if (gameState === 'waiting'){
      // start playing
      startGame();
      e.preventDefault();
      return;
   }
   if (gameState === 'gameover'){
      if (e.code==='Space' || e.code==='Enter'){
        startGame();
        e.preventDefault();
      }
      return;
   }
   if (gameState === 'paused'){
      if (e.code==='KeyP'){
         resumeGame();
      }
      return;
   }
   // playing state
   const key = e.key;
   let newDir = null;
   if (key==='ArrowUp' || key==='w' || key==='W') newDir = {x:0, y:-1};
   else if (key==='ArrowDown' || key==='s' || key==='S') newDir = {x:0, y:1};
   else if (key==='ArrowLeft' || key==='a' || key==='A') newDir = {x:-1, y:0};
   else if (key==='ArrowRight' || key==='d' || key==='D') newDir = {x:1, y:0};
   else if (key==='p' || key==='P') { pauseGame(); e.preventDefault(); return;}
   if (newDir) {
      enqueueDirection(newDir);
      e.preventDefault();
   }
}

// direction queue handling
function enqueueDirection(newDir){
   // disallow opposite relative to last queued direction or current direction if queue empty
   const lastDir = directionQueue.length ? directionQueue[directionQueue.length-1] : direction;
   // check opposite
   if (newDir.x === -lastDir.x && newDir.y === -lastDir.y) return; // ignore
   // also ignore if same direction as last
   if (newDir.x === lastDir.x && newDir.y === lastDir.y) return;
   directionQueue.push(newDir);
}

// startGame
function startGame(){
   // hide overlay
   document.getElementById('overlay').style.display='none';
   gameState='playing';
   initGame();
}

// pause/resume
function pauseGame(){ if (gameState==='playing'){ gameState='paused'; } else if (gameState==='paused') { resumeGame(); } }
function resumeGame(){ if (gameState==='paused'){ gameState='playing'; } }

// game over
function gameOver(){ gameState='gameover'; if (score > bestScore){ bestScore = score; }
// show overlay with score
  const overlay = document.getElementById('overlay');
  overlay.innerHTML = `<div>Game Over<br>Score: ${score}<br>Press Space or Enter to restart</div>`;
  overlay.style.display='flex';
}

// demo AI: simple function returning direction
function demoStep(){
   // Already have direction; if about to hit wall or snake, rotate
   const head = snake[0];
   const nextX = head.x + direction.x;
   const nextY = head.y + direction.y;
   const willCollide = nextX < 0 || nextX >= GRID_SIZE || nextY < 0 || nextY >= GRID_SIZE || snake.some(seg=> seg.x===nextX && seg.y===nextY);
   if (willCollide){
       // try turning right (clockwise) until safe
       const dirs = [{x:0,y:-1},{x:1,y:0},{x:0,y:1},{x:-1,y:0}]; // up,right,down,left
       // find index of current direction
       let idx = dirs.findIndex(d => d.x===direction.x && d.y===direction.y);
       // try next directions
       for(let i=1;i<4;i++){
          const nd = dirs[(idx+i)%4];
          const nx = head.x + nd.x;
          const ny = head.y + nd.y;
          const safe = nx>=0 && nx<GRID_SIZE && ny>=0 && ny<GRID_SIZE && !snake.some(seg=> seg.x===nx && seg.y===ny);
          if (safe){
             direction = nd; break;
          }
       }
   }
   // occasionally try turn towards food
   // probability 0.3
   if (Math.random()<0.2){
      // Prefer direction towards food if safe
      const dx = Math.sign(food.x - head.x);
      const dy = Math.sign(food.y - head.y);
      // prioritize axis with larger distance
      if (Math.abs(food.x - head.x) > Math.abs(food.y - head.y)){
         if (dx!==0){
            const nd = {x:dx, y:0};
            if (dx!==0 && !(dx===-direction.x && dy===0)){
               // check safety
               const nx=head.x+nd.x;
               const ny=head.y+nd.y;
               if (nx>=0&&nx<GRID_SIZE&&ny>=0&&ny<GRID_SIZE && !snake.some(seg=>seg.x===nx&&seg.y===ny)){
                  direction = nd;
               }
            }
         }
      } else {
         if (dy!==0){
            const nd={x:0,y:dy};
            if (dy!==0 && !(nd.x===-direction.x && nd.y===-direction.y)){
               const nx=head.x+nd.x;
               const ny=head.y+nd.y;
               if (nx>=0&&nx<GRID_SIZE&&ny>=0&&ny<GRID_SIZE && !snake.some(seg=>seg.x===nx&&seg.y===ny)){
                  direction = nd;
               }
            }
         }
      }
   }
}

// main loop
function gameLoop(timestamp){
    const dt = timestamp - lastTime;
    lastTime = timestamp;
    if (gameState === 'demo'){
        // run AI update at each step start (when progress===0)
        // We'll update direction at each step start maybe
        // Actually we can update direction before moving; here's the logic:
    }
    // update state based on dt and stepDuration
    if (gameState !== 'paused' && gameState !== 'gameover'){
        // increment progress: dt/stepDuration
        progress += dt/stepDuration;
        if (progress >= 1){
            // step completed
            // apply queued direction if any
            if (directionQueue.length > 0){
               const dir = directionQueue.shift();
               if (!(dir.x===-direction.x && dir.y===-direction.y)){
                  direction = dir;
               }
            }
            // if demo, update direction via AI
            if (gameState === 'demo'){
                demoStep();
            }

            // Move snake
            moveSnake();
            progress -= 1; // keep overflow
        }
    }

    // update particles
    updateParticles(dt);

    // draw
    draw();

    requestAnimationFrame(gameLoop);
}

// moveSnake function
function moveSnake(){
    const head = snake[0];
    const newHead = {x: head.x + direction.x, y: head.y + direction.y};

    // detect collisions
    if (newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE){
        gameOver();
        return;
    }
    if (snake.some(seg=> seg.x===newHead.x && seg.y===newHead.y)){
        gameOver();
        return;
    }

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

    // check eating
    if (newHead.x===food.x && newHead.y===food.y){
        // eat
        score+=10;
        updateScore();
        // generate particles
        spawnParticles(newHead);
        placeFood();
        // do not pop tail (grow)
    } else {
        // remove tail
        snake.pop();
    }

    // record previous positions for smooth movement: we can store a copy of snake positions before moving & after moving
    // We'll manage a separate array prevSnake to keep positions before step for interpolation.
    // But we need ability to get previous positions. Let's maintain prevSnake variable.
}

// We'll need to keep prevSnake: clone before moving each step. So define prevSnake globally.

let prevSnake = [];
function moveSnake(){
    // store previous snake positions before moving
    prevSnake = snake.map(seg=> ({x: seg.x, y: seg.y}));
    const head = snake[0];
    const newHead = {x: head.x + direction.x, y: head.y + direction.y};

    // check collisions
    if (newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE){
        gameOver();
        return;
    }
    for (let i=0;i<snake.length;i++){
        const seg=snake[i];
        if (seg.x===newHead.x && seg.y===newHead.y){
            gameOver();
            return;
        }
    }

    // add new head
    snake.unshift(newHead);
    // update prevSnake: need to shift previous positions appropriately for interpolation.
    // Actually after moving, snake has new head; prevSnake currently holds old positions.
    // We also need prev positions for each segment for animation between frames.
    // Let's create arrays: prevPositions = previous snake positions (including previous head position). current snake positions = snake.
    // For each segment, its previous position is either its own previous index+1 (the one it occupied). For now we will treat prevSnake as positions before moving, tail's previous position will be the one that is popped (or original tail). So we need to handle differences when snake grows: the new tail will have no previous position? We can treat previous tail = same as new tail (so no animation for new segment).
    // To handle this we may need to compute interpolation for each segment based on its index and previous snake. Implementation: In draw function, for segment i, we know its current position snake[i]; its previous position is prevSnake[i] if i < prevSnake.length, else same as snake[i] (for new segment). This works.

    // handle growth/eating
    let ate = false;
    if (newHead.x===food.x && newHead.y===food.y){
        ate = true;
        score+=10;
        updateScore();
        spawnParticles(newHead);
        placeFood();
        // do not pop tail - snake grows
    }
    if (!ate){
        // remove tail
        snake.pop();
    }

    // after moving, prevSnake will be used for interpolation in next frame
}

// updateScore function
function updateScore(){
   document.getElementById('score').textContent = `Score: ${score}`;
   document.getElementById('best').textContent = `Best: ${bestScore}`;
}

// particles
function spawnParticles(position){
   const count = 12;
   for(let i=0;i<count;i++){
       const angle = Math.random()*2*Math.PI;
       const speed = Math.random()*0.5 + 0.2; // cell per second? but we use per frame scaled
       particles.push({
           x: (position.x + 0.5) * cellSize,
           y: (position.y + 0.5) * cellSize,
           vx: Math.cos(angle) * speed,
           vy: Math.sin(angle) * speed,
           life: 300, // ms
           maxLife: 300,
           size: Math.random()*4+2
       });
   }
}
function updateParticles(dt){
   for(let i=particles.length-1;i>=0;i--){
     const p=particles[i];
     p.life-=dt;
     if(p.life<=0){
        particles.splice(i,1);
        continue;
     }
     p.x+=p.vx*dt/16; // adjust speed: pixels per frame? We'll convert speed to px per ms: appropriate scale = cellSize per 200ms? using dt works: Speed per ms so we adjust accordingly. For simplicity, set vx,vy in px per ms: define speed = cellSize * (0.03+ random);... Let's compute speed in px per ms.

   }
}

// For simplicity, adjust particle speed: we set speed, say ~cellSize / 200ms = cellSize*0.005 per ms. Let's compute each particle speed as random from 0.1..0.3 cell per ms? Actually cellSize is maybe ~30px, speed in px per ms maybe 0.2-0.4? Good.

function spawnParticles(position){
   const count = 12;
   for(let i=0;i<count;i++){
       const angle = Math.random()*2*Math.PI;
       const speed = (cellSize * (0.05 + Math.random()*0.1)) / 16; // per frame approx
       particles.push({
           x: (position.x + 0.5) * cellSize,
           y: (position.y + 0.5) * cellSize,
           vx: Math.cos(angle) * speed,
           vy: Math.sin(angle) * speed,
           life: 400,
           maxLife: 400,
           size: Math.random()*3 + 2
       });
   }
}
function updateParticles(dt){
   // dt in ms
   for(let i=particles.length-1;i>=0;i--){
     const p=particles[i];
     p.life-=dt;
     if(p.life<=0){
        particles.splice(i,1);
        continue;
     }
     // move per ms: convert speed from per frame to per ms by dividing by 16 as we used? Actually speed already is per frame approx (assuming 60fps). To avoid confusion, we can compute speed in px per ms directly: speed = cellSize * (0.002 + random*0.008) => approx 0.2–0.8 px per ms (12–48 px per 60ms). Let's recalc.
   }
}

// Let's recalc: We'll define speed = (cellSize * (0.2 + Math.random()*0.3)) / 1000; // px per ms? Actually cellSize maybe 30, so speed ~ 0.006 to 0.015 px per ms => tiny. Let's use bigger: speed = (cellSize * 0.5) / 200 = 0.075*cellSize per ms? Hmm.

Better: approximate particle speed: 0.1 to 0.3 cell per step. Since stepDuration is maybe 120ms, moving by 0.2* cellSize per ms is unrealistic. Let's compute speed in px per ms: we want a particle to travel maybe 2*cellSize in about 300 ms; speed per ms = (2*cellSize)/300 ≈0.0067*cellSize. For cellSize=30, speed ~0.2 px/ms. That's fine. So speed^ = cellSize * (0.2~0.5)/1000? Actually cellSize*2/300 = cellSize * 0.0067. So set speed = cellSize * (0.015 + Math.random()*0.015). That's approx 0.015*30=0.45 px/ms to 0.03*30=0.9 px/ms. For dt (ms) ~16ms per frame, movement per frame = speed*dt ~ 7-14px, which is okay.

So spawnParticles:

function spawnParticles(position){
  const count=12;
  const baseX=(position.x+0.5)*cellSize;
  const baseY=(position.y+0.5)*cellSize;
  for(let i=0;i<count;i++){
    const angle=Math.random()*2*Math.PI;
    const speed = cellSize * (0.015 + Math.random()*0.015); // px per ms
    particles.push({x:baseX, y:baseY, vx:Math.cos(angle)*speed, vy:Math.sin(angle)*speed, life:400, maxLife:400, size:Math.random()*3+2});
  }
}

In updateParticles(dt): p.x += p.vx * dt; p.y += p.vy * dt;

Now drawing: Use ctx.save(); ctx.scale(dpr, dpr); So that drawing coordinates are in CSS pixels.

Define draw function:

function draw(){
   // clear
   ctx.clearRect(0,0,canvas.width,canvas.height);
   // draw background (dark)
   ctx.fillStyle='#111';
   ctx.fillRect(0,0,canvas.width,canvas.height);
   // draw food
   ctx.fillStyle='red';
   ctx.beginPath();
   const fx = (food.x + 0.5) * cellSize;
   const fy = (food.y + 0.5) * cellSize;
   const radius = cellSize*0.3;
   ctx.arc(fx,fy,radius,0,2*Math.PI);
   ctx.fill();

   // draw snake with interpolation
   // compute interpolated positions
   for(let i = 0; i < snake.length; i++){
       const cur = snake[i];
       const prev = (prevSnake[i] !== undefined) ? prevSnake[i] : cur;
       const ix = (prev.x + (cur.x - prev.x) * progress) * cellSize;
       const iy = (prev.y + (cur.y - prev.y) * progress) * cellSize;
       // draw segment
       ctx.fillStyle = i===0 ? '#6C3' : '#2C2'; // head different
       // For head, we draw a rounded rect or rectangle
       // use radius for corners
       const segSize = cellSize;
       const radiusSeg = cellSize*0.2;
       ctx.beginPath();
       if (i===0){
          // draw head rectangle with rounded corners
          ctx.moveTo(ix + radiusSeg, iy);
          ctx.lineTo(ix + segSize - radiusSeg, iy);
          ctx.quadraticCurveTo(ix + segSize, iy, ix + segSize, iy + radiusSeg);
          ctx.lineTo(ix + segSize, iy + segSize - radiusSeg);
          ctx.quadraticCurveTo(ix + segSize, iy + segSize, ix + segSize - radiusSeg, iy + segSize);
          ctx.lineTo(ix + radiusSeg, iy + segSize);
          ctx.quadraticCurveTo(ix, iy + segSize, ix, iy + segSize - radiusSeg);
          ctx.lineTo(ix, iy + radiusSeg);
          ctx.quadraticCurveTo(ix, iy, ix + radiusSeg, iy);
          ctx.closePath();
          ctx.fill();
          // draw eyes
          const eyeSize = cellSize * 0.12;
          const eyeOffset = cellSize * 0.2;
          // eye positions based on direction
          const eyeXOffset = direction.x * eyeOffset;
          const eyeYOffset = direction.y * eyeOffset;
          // left eye and right eye positions relative to head center
          // Determine perpendicular vector to direction for eye separation
          let perpX = -direction.y;
          let perpY = direction.x;
          const centerX = ix + segSize/2;
          const centerY = iy + segSize/2;
          const eye1X = centerX + perpX * eyeOffset - eyeXOffset;
          const eye1Y = centerY + perpY * eyeOffset - eyeYOffset;
          const eye2X = centerX - perpX * eyeOffset - eyeXOffset;
          const eye2Y = centerY - perpY * eyeOffset - eyeYOffset;
          ctx.fillStyle = 'white';
          ctx.beginPath();
          ctx.arc(eye1X, eye1Y, eyeSize, 0, Math.PI*2);
          ctx.arc(eye2X, eye2Y, eyeSize, 0, Math.PI*2);
          ctx.fill();
       } else {
          // body segment rectangle
          ctx.fillRect(ix, iy, segSize, segSize);
       }
   }

   // draw particles
   particles.forEach(p=>{
      const alpha = p.life / p.maxLife;
      ctx.fillStyle = `rgba(255,255,0,${alpha})`;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
      ctx.fill();
   });

   // draw score overlay perhaps somewhere (but already shown in html)
}

// So that ensures smooth movement.

Now we need to initialize prevSnake, perhaps before first draw we set prevSnake = snake.map(...). That's okay.

Now handling game start: We have overlay initially visible with "Click or press any key to start". GameState initially should be 'demo' to allow demo play. However, requirement: The game must start in a self-playing demo mode behind an overlay. So initial state is demo and we have overlay visible.

Thus we can set gameState='demo', overlay visible. On click anywhere or keydown, startGame => gameState='playing', hide overlay.

Need overlay click event: document.getElementById('overlay').addEventListener('click', startGame); Also keydown will just call startGame if waiting.

But the state 'demo' and 'waiting'? If overlay visible, we can indicate gameState = 'demo', and hide overlay to start game. But when overlay is visible, we might still accept keydown/ click to start. We should also have two states: 'demo' and 'playing' both considered running for AI and updates, except that directions from player are used only in playing state. The overlay messages can be displayed based on state: in 'gameover' we display game over overlay with score. In 'demo', overlay says "Click or press any key to start". When user starts, we hide overlay.

The demo uses AI for direction and not accept user input. However need to ensure that during demo, we don't process input in handleKey except to start game. So in handleKey, if gameState === 'demo' then treat as start, unless maybe pressing P to pause etc., but likely treat any key as start. So that initial press will start.

Implement overlay HTML: <div id="overlay"><div>Click or press any key to start</div></div>

CSS: #overlay { position: absolute; top:0; left:0; width:100%; height:100%; display:flex; align-items:center; justify-content:center; background: rgba(0,0,0,0.6); color: white; font-size: 1.5rem; text-align:center; cursor:pointer; } hide overlay via display:none.

Now ensure overlay covers whole screen; its parent container must be position:relative to make overlay cover canvas. We'll wrap canvas and overlay within a container <div id="gameContainer"> with position:relative. So HTML: <div id="gameContainer"><canvas id="gameCanvas"></canvas><div id="overlay"><div>Click or press any key to start</div></div></div> Also scoreboard maybe positioned absolute top left.

We'll need to adjust CSS accordingly. Let's plan final DOM:

<body>
<div id="gameContainer">
   <canvas id="gameCanvas"></canvas>
   <div id="overlay"><div>Click or press any key to start</div></div>
</div>
<div id="scoreboard"><span id="score">Score: 0</span> | <span id="best">Best: 0</span></div>
</body>

CSS:

html,body {margin:0;height:100%;background:#222;font-family:Arial,Helvetica,sans-serif;}
#gameContainer{position:relative;display:flex;align-items:center;justify-content:center;}
#gameCanvas{display:block;background:#111;border:2px solid #333; border-radius:10px;}
#overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.5rem;cursor:pointer;z-index:10;user-select:none;}
#scoreboard{position:absolute;top:10px;left:10px;color:#fff;font-size:1.2rem;z-index:5;pointer-events:none;}

Additionally, we implement overlay click event to start. Also keydown on overlay may not capture if overlay covers whole area. So we attach keydown to window.

Now the main loop runs even if overlay is shown. The GameState 'demo' means snake used AI, direction changes handled by demo code. We'll set directionQueue empty. In demo, we ignore directionQueue, always move using AI.

Thus modify movement: At each step if gameState is 'demo', we apply AI to set direction before moving snake (maybe inside moveSnake). Or we can have separate in loop:

if (gameState !== 'paused' && gameState !== 'gameover'){
    // update progress
    // When progress crosses >= 1:
        // For 'demo': we call demoStep to adjust direction (the direction may have changed)
        // For 'playing': apply directionQueue
        // Then move snake
}

We need to incorporate both: for demo, ignore directionQueue; for playing, apply directionQueue.

Modify GameLoop: At step start (when progress >= 1), before moving snake, we decide direction:

if (gameState === 'demo'){
    demoStep(); // AI updates direction
}
if (gameState === 'playing'){
   // apply queued directions
   if (directionQueue.length > 0){
       const nd = directionQueue.shift();
       if(!(nd.x===-direction.x && nd.y===-direction.y)){
           direction = nd;
       }
   }
}
moveSnake();

We need to store previous snake positions before moving (prevSnake). So firstly, before move we set prevSnake = snake.map(...). Then we compute new positions.

Thus let's adjust moveSnake: It will be called after direction possibly updated. Inside moveSnake, we need to store prevSnake as current before updating. Let's separate:

function step(){
   // store previous positions
   prevSnake = snake.map(seg=> ({x: seg.x, y: seg.y}));
   // direction already set
   // compute new head and handle collisions/eating/growth
   let newHead = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
   // collisions...
   // etc
}

Thus in game loop, when progress >= 1, we call step().

But wait: When snake grows, we need to handle prevSnake length diff: If snake grew, prevSnake has length oldSnake (maybe n) and new snake length is n+1. We'll handle interpolation by using prevSnake for index < prevSnake.length, else use new snake segment's position as both previous and current for new segment, effectively having it appear at its position without movement (like it appears out of thin air). But visual we want tail to appear from previous location? That's okay: when snake grows, the new segment appears at end (i.e., tail doesn't move). So with prevSnake[prevSnake.length] undefined, interpolation will set prev = cur; interpolation yields same coordinates. That's fine.

Now step functions:

function step(){
    // set new direction before calling step if playing
    // store prevSnake already saved in step before update?
    // Actually just do it here:
    const curPrev = snake.map(seg=> ({x: seg.x, y: seg.y})); // copy previous positions
    // compute new head
    const newHead = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
    // collisions detection
    if (newHead.x < 0 || newHead.x >= GRID_SIZE || newHead.y < 0 || newHead.y >= GRID_SIZE){
        gameOver();
        return;
    }
    if (snake.some(seg => seg.x===newHead.x && seg.y===newHead.y)){
        gameOver();
        return;
    }
    // add new head to front
    snake.unshift(newHead);
    // determine if eating
    let ate = false;
    if (newHead.x===food.x && newHead.y===food.y){
        ate = true;
        score+=10;
        updateScore();
        spawnParticles(newHead);
        placeFood();
    }
    if (!ate){
        // remove tail
        snake.pop();
    }
    // set prevSnake for interpolation for next frame
    prevSnake = curPrev;
}

But careful: Need prevSnake before moving for interpolation after movement, as we will draw interpolation using previous positions (old positions). Indeed, after step we want to start interpolation from old positions (prevSnake) to new snake positions (snake). So we should store prevSnake before movement. So correct approach:

function step(){
    // store previous positions before changing snake
    prevSnake = snake.map(seg=> ({x: seg.x, y: seg.y}));
    // compute new head and update snake (as above)
}

Yes.

Now animate: For each frame we need to compute progress between 0 and 1. At start of step (when progress reset to 0) we have just stored prevSnake = previous positions, direction set, and moved snake. So at progress=0, snake positions are new positions; prevSnake is old positions. We then interpolate from prev to new over the duration.

Thus need to ensure that step called when progress >=1 (i.e., after previous step completed). But progress increments each frame. When progress exceeds 1, call step, then subtract 1. So after step, progress is maybe less than 1 (some leftover fraction). That's okay.

Now for demo AI: before step we need to decide direction. Currently we update direction in game loop at step boundary (when progress >=1, before calling step). So demoStep function will adjust direction based on snake and food, also ensure not reverse.

Edge Cases: If AI direction is opposite to current direction, we must avoid for fairness. DemoStep can check that, but we can also incorporate same opposite check as we do for directionQueue.

Alternatively, we can let demoStep assign direction directly; if it tries to reverse onto itself, that'll cause game over. So need to avoid that. We'll design demoStep to never reverse.

Goal: Simple AI: try to go towards food: decide if horizontally aligned; if moving horizontally blocked, go vertical, else continue. Also if not safe, rotate.

Algorithm: For snake head (x,y) and food (fx,fy). Compute dx = sign(fx - x), dy = sign(fy - y). If dx !=0 and direction.x != -dx (i.e., not reverse) and moving in that direction is safe (no collision), set direction {dx,0}. Else if dy !=0 and direction.y != -dy etc... else rotate.

Simplify: Use prioritized axis: prefer larger distance axis.

Implementation:

function demoStep(){
   const head = snake[0];
   // Determine safe directions set
   const possible = [
       {x:0,y:-1},
       {x:1,y:0},
       {x:0,y:1},
       {x:-1,y:0}
   ];
   // exclude reverse
   const reverse = {x:-direction.x,y:-direction.y};
   // filter safe directions - not collide
   const safe = possible.filter(d=>{
       if (d.x===reverse.x && d.y===reverse.y) return false;
       const nx=head.x+d.x;
       const ny=head.y+d.y;
       if (nx<0||nx>=GRID_SIZE||ny<0||ny>=GRID_SIZE) return false;
       if (snake.some(seg=>seg.x===nx&&seg.y===ny)) return false;
       return true;
   });
   // if no safe? should not happen unless dead.
   if (safe.length===0) return;
   // choose direction towards food if safe
   const dx = Math.sign(food.x - head.x);
   const dy = Math.sign(food.y - head.y);
   // try horizontal if distance bigger
   const prioritize = Math.abs(food.x - head.x) > Math.abs(food.y - head.y) ? 'h' : 'v';
   let chosen=null;
   if (prioritize==='h' && dx!==0){
       const dir = {x:dx,y:0};
       if (safe.some(d=>d.x===dir.x && d.y===dir.y)) chosen=dir;
   }
   if (!chosen && prioritize==='v' && dy!==0){
       const dir = {x:0,y:dy};
       if (safe.some(d=>d.x===dir.x && d.y===dir.y)) chosen=dir;
   }
   // fallback: if not chosen, try other axis
   if (!chosen){
       if (dx!==0){
           const dir={x:dx,y:0};
           if (safe.some(d=>d.x===dir.x && d.y===dir.y)) chosen=dir;
       }
   }
   if (!chosen && dy!==0){
       const dir={x:0,y:dy};
       if (safe.some(d=>d.x===dir.x && d.y===dir.y)) chosen=dir;
   }
   // fallback to random safe
   if (!chosen){
       chosen=safe[Math.floor(Math.random()*safe.length)];
   }
   direction = chosen;
}

This simple AI will move towards food.

But need to ensure direction is not reversed; safe list already excludes reverse.

Thus when gameState='demo', each step we set direction via demoStep.

Need to reinitialize snake and food for demo as well. At page load, before first step we can call initGame() to initialize snake and food. Then set gameState='demo' and overlay visible.

Now handle gameOver: When snake dies in demo, we want to restart automatically? Possibly show overlay but for demo we could immediately restart after a brief delay. But requirement didn't mention resetting demo; but we can restart automatically after a second. Maybe better: after gameOver we set overlay to "Demo ended. Click to start" but we can restart automatically after maybe 2 seconds to keep visible dynamics. However they want demo playing behind overlay; if the snake dies, the demo would stop; but screenshot may show gameover overlay. Might be okay. But perhaps better to reset automatically to keep demo alive: After a game over, if gameState==='demo', we can schedule a restart after a few seconds. For simplicity, we can call startDemo() which resets the game and continues to show overlay.

Implementation approach: At initialization, call startDemo(), which sets gameState='demo', overlay visible, hide overlay after start? Actually overlay stays visible but we hide text behind? Actually overlay is visible, but we want it to show "Click or press any key to start". However during demo we want overlay still visible but not hide it; we only hide when player starts. However after game over during demo, we may want to restart demo automatically. But we may also keep overlay visible as game over message; but then the demo won't be visible. So for better demonstration, we can reset demo automatically and keep overlay visible with same "Click or press any key to start" text, but that will not show game because overlay is covering. Actually we want demo visible behind overlay; if overlay covers entire canvas and is transparent black with text, that's okay; demo remains visible behind. So after game restarts, the overlay stays visible, showing same text. That is acceptable.

Thus after game over while in demo mode, we should reset the game automatically after a short delay, but not hide overlay. So define:

function handleGameOver(){
    if (gameState === 'demo'){
        // just restart demo after brief
        setTimeout(()=>{
            initGame(); // resets snake, ... but keep gameState='demo'
        }, 500);
    } else {
        // player mode: show overlay
        gameOver();
    }
}

But we also need to differentiate for gameState. Let's rename gameOverOverlay function to show overlay.

Therefore implement:

function endGame(){
   if (gameState === 'demo'){
       // restart demo after delay
       initGame();
   } else {
       // player lost
       gameOver();
   }
}

However we need to ensure bestScore updated only during player mode? Probably only track in player mode; but we can also track in demo, not needed. Let's implement bestScore only for player.

Thus in endGame after collision detection, set game Over and then either reset or show overlay.

Now create functions accordingly.

Now wiring key events:

In handleKey, for gameState=== 'demo' perhaps any key should start playing. So if key is any key, call startGame(); and also stop demo. We'll call startGame which sets gameState='playing', hide overlay, and resets game? Should we keep current snake? Typically start new game fresh, else the demo snake will continue under player control. Usually when you start playing, you start a fresh game. We'll implement startGame as resetting game. So in startGame: hide overlay, set gameState='playing', initGame().

Edge Cases: The keydown will also capture arrow keys etc after transition. We'll start game but also execute key for direction after resetting. Might be okay.

Now we need to hide overlay and set its display, but also update innerHTML accordingly for start. Initially overlay shows "Click or press any key to start". After game over in playing mode, overlay shows "Game Over" plus score.

So we need to have overlay text that changes based on state. We'll have overlay element with innerHTML that we can modify.

Define functions:

function showOverlay(messageHTML){
   const overlay = document.getElementById('overlay');
   overlay.innerHTML = `<div>${messageHTML}</div>`;
   overlay.style.display='flex';
}
function hideOverlay(){
   const overlay=document.getElementById('overlay');
   overlay.style.display='none';
}

Initialize overlay with start message:

showOverlay('Click or press any key to start');

When player triggers startGame, hideOverlay.

When game over player, showOverlay('Game Over<br>Score: X<br>Press Space or Enter to restart');

When player pauses: maybe show overlay with "Paused (Press P to resume)", but we can just show scoreboard indicating pause. For simplicity, we can display a simple overlay with "Paused". But not required.

Simplify: implement pause overlay or just hide the game and overlay? Let's implement pause overlay.

When pressing P while playing, we set gameState='paused', and show overlay "Paused (Press P to resume)".

When pressing P again when paused, we hide overlay and resume.

Now the scoreboard: #scoreboard is a fixed overlay top left, showing current score and best score. This is visible always.

Now we need to make snake head distinct with eyes.

We can also add subtle gradient or color variations.

Now ensure CSS includes fonts for readability.

Now fill the code.

We also need to handle devicePixelRatio for crisp canvas. We'll calculate: let dpr = window.devicePixelRatio||1; canvas.width = canvasSize * dpr; canvas.height = canvasSize * dpr; canvas.style.width = canvasSize + 'px'; canvas.style.height = canvasSize + 'px'; ctx.scale(dpr,dpr).

Thus cellSize = canvasSize / GRID_SIZE.

Now handling resize: recalc canvasSize and cellSize, maintain game state; when resized, we can recalc canvas size but preserve snake positions. For simplicity, on resize reload the page (but we want nice). We'll just recalc and keep snake positions; cellSize changes but positions remain scaled.

Now timeline: On page load:

- Determine canvasSize (maybe 80% of min(viewport width, height), but at least some minimal? We'll compute as: const canvasSize = Math.min(window.innerWidth, window.innerHeight) * 0.9; Still fits.

- Set canvas size.

- initGame()

- set gameState='demo'

- Show overlay start message.

- Add event listeners for click, keydown.

- Start requestAnimationFrame loop.

Now let’s deep dive to code specifics with proper handling.

Implementation details of curses and quick key input:

Our directionQueue should contain at most 2 entries? Or we can push unlimited but limited by step time. We'll allow a small length; it's fine.

Now interfering with AI while in demo: directionQueue not used.

Now add constants for colors: snakeBodyColor = '#2cbe4e', snakeHeadColor = '#4cf', etc.

Now design scoreboard: #scoreboard is positioned absolute top left, with small margin and color.

Now tests: Let's simulate a basic run.

Potential bugs: Need to ensure prevSnake is defined initially before first draw to avoid errors. Initially after initGame, we set prevSnake = snake.map(...). Since there has been no previous position, we can set prevSnake as copy of snake as same positions. This will cause interpolation no movement at start.

Now hitting food: spawnParticles needed to use cellSize variable set after canvas size resolved; which is available globally.

Now collision detection: snake.some may include head itself after insertion? Actually we check before adding head? No, we compute new head and then check if any segment matches that position already (including current head). Since snake[0] is old head. The new head could match any segment of existing snake (including tail), so detection works. However, after adding head, we then run ate detection and tail removal. So collision detection should be done before adding head to snake because after adding head, the check would always find new head not in the old snake (except if new head collides with body). We should check before adding head: check if new head collides with any segment of snake (except possibly tail when it will be removed). However typical snake game allows head colliding with the tail cell that will be removed in this move only if not eating. But many implementations treat that as collision. Simpler: we check against snake (including tail), but after moving head, if not eating, we will pop the tail. If new head equals tail's previous position, it would be considered a non-collision in typical snake (since tail moves away). But we can ignore that complexity; It doesn't affect short demonstration.

Let's keep it simple: check collision with any segment. When not eating, we pop tail after adding head, but still if head position equals tail position before removal, detection may think collision and kill snake; nevertheless not a huge issue.

We could do more correct detection: If not eating, exclude tail segment from collision check because it will move away. Implementation: Determine if not eating, the tail = snake[snake.length-1]; then during collision check we ignore this tail cell. Let's incorporate.

function willCollide(newHead){
    if (newHead.x <0||newHead.x>=GRID_SIZE||newHead.y<0||newHead.y>=GRID_SIZE) return true;
    // body collision: check all segments except tail if not eating.
    // But at detection we don't know if eating or not because we haven't checked if newHead equals food. So compute ate = (newHead.x===food.x && newHead.y===food.y)
    // then ignore tail only if !ate
    // Let's implement in step():
}

Thus step():

function step(){
    // store previous positions
    prevSnake = snake.map(seg=> ({x: seg.x, y: seg.y}));
    const head = snake[0];
    const newHead = {x: head.x + direction.x, y: head.y + direction.y};

    // Eating?
    const eating = (newHead.x === food.x && newHead.y === food.y);

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

    // check for body collision
    // If not eating, tail will move, so skip tail segment for collision check
    const collision = snake.some((seg, index) => {
        // skip checking tail if not eating because tail will vanish
        if (!eating && index === snake.length - 1) return false;
        return seg.x === newHead.x && seg.y === newHead.y;
    });
    if (collision){
        gameOverHandler();
        return;
    }

    // add new head
    snake.unshift(newHead);
    if (eating){
        score+=10;
        updateScore();
        spawnParticles(newHead);
        placeFood();
        // keep tail (grow)
    } else {
        // remove tail
        snake.pop();
    }
}

Define gameOverHandler: if gameState === 'demo', restart demo; else show overlay.

Now note: we need to ensure current direction is stored globally; needs update to direction before step if playing and there is directionQueue; for demo, we update direction before step via demoStep.

Thus game loop:

function gameLoop(timestamp){
    if (!lastTime) lastTime = timestamp;
    const dt = timestamp - lastTime;
    lastTime = timestamp;

    if (gameState !== 'paused' && gameState !== 'gameover'){
        progress += dt / stepDuration;
        if (progress >= 1){
            // multiple steps may have passed if dt large; we can handle overshoot: while loop
            while (progress >= 1){
                if (gameState === 'demo'){
                    demoStep();
                } else if (gameState === 'playing'){
                    if (directionQueue.length){
                        const nd = directionQueue.shift();
                        // don't allow reverse (just in case)
                        if (!(nd.x===-direction.x && nd.y===-direction.y)){
                            direction=nd;
                        }
                    }
                }
                step();
                progress -= 1;
                // if game over occurs during step, break out of loop
                if (gameState === 'gameover') {
                    // break while loop
                    progress = 0;
                    break;
                }
            }
        }
    }
    // update particles
    updateParticles(dt);
    // draw
    draw();
    requestAnimationFrame(gameLoop);
}

Note: When gameState='gameover', we still want to draw over overlay; but overlay draws on top via #overlay.

Now overlay: It should appear on top using absolute position: #overlay with z-index high.

Make sure when gameOver from demo, we restart demo automatically. Implementation:

function handleGameOver(){
    if (gameState === 'demo'){
        // restart demo after short delay
        setTimeout(()=>{
            initGame();
        }, 200);
    } else {
        // update bestScore
        if (score > bestScore){
            bestScore = score;
        }
        updateScore();
        showOverlay(`Game Over<br>Score: ${score}<br>Press Space or Enter to restart`);
        gameState='gameover';
    }
}

Thus step() calls handleGameOver() if collisions.

Now click overlay to start playing: overlay click event triggers startGame if gameState not playing.

Add:

document.getElementById('overlay').addEventListener('click', () => {
    if (gameState !== 'playing'){
        startGame();
    }
});

Also keydown: as soon as key pressed, if gameState is 'demo' or not playing, startGame.

But some keys like 'p' or other may be pressed; but we treat any key in that state as start. So in handleKey:

if (gameState !== 'playing' && gameState !== 'paused'){
   // not playing yet: start game
   startGame();
   return;
}

But need to make sure we also handle pause when playing.

Thus rearranged:

function handleKey(e){
    if (gameState === 'demo'){
        startGame();
        e.preventDefault();
        return;
    }
    if (gameState === 'gameover'){
        if (e.code === 'Space' || e.code === 'Enter'){
            startGame();
            e.preventDefault();
        }
        return;
    }
    if (gameState === 'paused'){
        if (e.key.toLowerCase() === 'p'){
            resumeGame();
            e.preventDefault();
        }
        return;
    }
    // playing
    const key = e.key;
    if (key === 'p' || key === 'P'){
        pauseGame();
        e.preventDefault();
        return;
    }
    let dir = null;
    if (key === 'ArrowUp' || key === 'w' || key === 'W'){
        dir = {x:0, y:-1};
    } else if (key === 'ArrowDown' || key === 's' || key === 'S'){
        dir = {x:0, y:1};
    } else if (key === 'ArrowLeft' || key === 'a' || key === 'A'){
        dir = {x:-1, y:0};
    } else if (key === 'ArrowRight' || key === 'd' || key === 'D'){
        dir = {x:1, y:0};
    }
    if (dir){
        enqueueDirection(dir);
        e.preventDefault();
    }
}

Now implement startGame:

function startGame(){
    hideOverlay();
    gameState='playing';
    initGame();
    // ensures input processing uses directionQueue.
}

Now implement pauseGame and resumeGame:

function pauseGame(){
    if (gameState === 'playing'){
        gameState='paused';
        showOverlay('Paused<br>Press P to resume');
    }
}
function resumeGame(){
    if (gameState === 'paused'){
        hideOverlay();
        gameState='playing';
    }
}

Now score updates: as above.

Now we need to handle bestScore display: updateScore should reflect bestScore.

function updateScore(){
   document.getElementById('score').textContent = `Score: ${score}`;
   document.getElementById('best').textContent = `Best: ${bestScore}`;
}

Now particles: Already defined spawnParticles and updateParticles.

Implementation details for particle movement: p.x, p.y are pixel coordinates; p.vx, p.vy are pixel per ms; dt is ms.

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

In draw particles to apply fading:

ctx.globalAlpha = p.life / p.maxLife; // alpha
ctx.fillStyle = 'rgb(255,255,0)'; // yellow
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
ctx.globalAlpha = 1.0;

But ensure we restore globalAlpha after each.

Now drawing snake head with eyes:

calc eye placement:

- Base center of head: cx = ix + cellSize/2; cy = iy + cellSize/2;
- Direction vector (dx, dy) indicates orientation; we need to offset eyes forward a bit and offset laterally.

Simplify: Place eyes at positions offset outward slightly along direction. We can compute eye offset forward offset (fwd = 0.2*cellSize) and lateral offset (lat = 0.15*cellSize). Use direction for forward; compute perpendicular direction: perp = {x: -dy, y: dx}.

Then eye1 = {x: cx + direction.x * fwd + perp.x * lat, y: cy + direction.y * fwd + perp.y * lat}
and eye2 = {x: cx + direction.x * fwd - perp.x * lat, y: cy + direction.y * fwd - perp.y * lat}

Eye radius maybe 0.07*cellSize.

Now shape of head: maybe use fillRect with same color, but draw eyes on top.

Or we could draw head as rounded rectangle.

Simplify: head as a filled rect color distinct.

Now we need also to render grid lines? Not required but optional. Add subtle grid lines.

We can draw background rectangle for each cell. Or skip.

Now we need to maintain high performance.

Now about customizing CSS to hide overflow.

Check for console errors: ensure functions defined before usage.

Now check everything.

Let's refine functions:

- Global variables:

const GRID_SIZE = 20;
let canvasSize;
let cellSize;
let canvas, ctx;
let snake, prevSnake;
let direction;
let directionQueue;
let food;
let score, bestScore;
let gameState;
let stepDuration = 120; // ms per cell
let progress = 0;
let lastTime = 0;
let particles = [];

Add more definitions.

Now function resizeCanvas:

function resizeCanvas(){
    const size = Math.min(window.innerWidth, window.innerHeight) * 0.9;
    canvasSize = Math.floor(size);
    cellSize = canvasSize / GRID_SIZE;
    canvas.width = canvasSize * dpr;
    canvas.height = canvasSize * dpr;
    canvas.style.width = canvasSize + 'px';
    canvas.style.height = canvasSize + 'px';
    ctx.scale(dpr, dpr);
}

Now after resizing, need to recalc cellSize.

Now initialization:

function init(){
    canvas = document.getElementById('gameCanvas');
    ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    resizeCanvas();
    // set up event listeners
    window.addEventListener('keydown', handleKey);
    document.getElementById('overlay').addEventListener('click', () => {
        if (gameState !== 'playing'){
            startGame();
        }
    });
    window.addEventListener('resize', ()=> {
        // need to adjust canvas, but we also need to re-scale context - can't just call ctx.scale again after scaling multiple times
        // approach: reset transform: ctx.setTransform(1,0,0,1,0,0); then call resizeCanvas, then ctx.scale(dpr,dpr)
    });
    // configure overlay start message
    showOverlay('Click or press any key<br>to start');
    // set gameState to demo
    gameState='demo';
    initGame();
    requestAnimationFrame(gameLoop);
}
window.onload = init;

But careful: In resizeCanvas need to apply proper scaling. Use ctx.setTransform(dpr,dpr?). Actually .scale(dpr, dpr) after setting width/height. Let's incorporate:

function resizeCanvas(){
    const dpr = window.devicePixelRatio || 1;
    const size = Math.min(window.innerWidth, window.innerHeight) * 0.9;
    canvasSize = Math.floor(size);
    cellSize = canvasSize / GRID_SIZE;
    canvas.width = canvasSize * dpr;
    canvas.height = canvasSize * dpr;
    canvas.style.width = canvasSize + 'px';
    canvas.style.height = canvasSize + 'px';
    ctx.setTransform(1,0,0,1,0,0);
    ctx.scale(dpr,dpr);
}

Now also call ctx.setTransform before drawing each frame? Not needed if we maintain.

Now on resize, need to recalc cellSize but snake positions remain grid coordinates, thereby they will be drawn correctly.

Now initGame sets snake and direction.

Implement initGame:

function initGame(){
    // start snake with length 5 horizontally centered left side
    const startX = Math.floor(GRID_SIZE/2);
    const startY = Math.floor(GRID_SIZE/2);
    snake = [];
    for (let i = 0; i < 5; i++){
        snake.push({x: startX - i, y: startY});
    }
    // snake[0] is head? Actually we want head at front. For loops, push from tail to head? We can push tail then later unshift head? Let's set snake = [{x:startX, y:startY}, {x:startX-1,y:startY}, {x:startX-2,y:startY}, {x:startX-3,y:startY}, {x:startX-4,y:startY}]; That gives head at index0.
    // Let's use:
    snake = [
        {x:startX, y:startY},
        {x:startX-1, y:startY},
        {x:startX-2, y:startY},
        {x:startX-3, y:startY},
        {x:startX-4, y:startY}
    ];
    // previous positions set equal initially
    prevSnake = snake.map(seg=> ({x: seg.x, y: seg.y}));
    direction = {x:1,y:0}; // moving right
    directionQueue = [];
    score = 0;
    updateScore();
    placeFood();
    progress = 0;
    particles = [];
}

Now placeFood:

function placeFood(){
    const occupied = new Set(snake.map(seg => seg.x + ',' + seg.y));
    let x, y;
    do {
        x = Math.floor(Math.random()*GRID_SIZE);
        y = Math.floor(Math.random()*GRID_SIZE);
    } while (occupied.has(x + ',' + y));
    food = {x, y};
}

Now step (movement) as earlier but with proper usage of variables.

Define handleGameOver:

function handleGameOver(){
    if (gameState === 'demo'){
        // simply restart demo after short pause
        setTimeout(() => {
            initGame();
        }, 200);
    } else {
        // player lost
        if (score > bestScore) bestScore = score;
        updateScore();
        showOverlay(`Game Over<br>Score: ${score}<br>Press Space or Enter to restart`);
        gameState='gameover';
    }
}

Now step function:

function step(){
    // store previous positions for interpolation
    prevSnake = snake.map(seg => ({x: seg.x, y: seg.y}));

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

    const eating = (newHead.x===food.x && newHead.y===food.y);

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

    // body collision
    const collision = snake.some((seg, idx) => {
        if (!eating && idx === snake.length-1) return false; // tail will move
        return seg.x===newHead.x && seg.y===newHead.y;
    });
    if (collision){
        handleGameOver();
        return;
    }

    // move
    snake.unshift(newHead);
    if (eating){
        score+=10;
        updateScore();
        spawnParticles(newHead);
        placeFood(); // new food
        // do not pop tail => grow
    } else {
        // pop tail
        snake.pop();
    }
}

Now demoStep: as previously defined; but also we need to set direction variable.

Implementation done.

Now draw function:

function draw(){
    // clear background
    ctx.fillStyle = '#111';
    ctx.fillRect(0,0,canvasSize,canvasSize);
    // draw grid lines (optional)
    // Draw food:
    const fx = (food.x + 0.5) * cellSize;
    const fy = (food.y + 0.5) * cellSize;
    ctx.fillStyle = '#e00';
    ctx.beginPath();
    ctx.arc(fx, fy, cellSize*0.3, 0, 2*Math.PI);
    ctx.fill();

    // draw snake segments
    for (let i=0;i<snake.length;i++){
        const cur = snake[i];
        const prev = (prevSnake[i] !== undefined) ? prevSnake[i] : cur;
        const ix = (prev.x + (cur.x - prev.x) * progress) * cellSize;
        const iy = (prev.y + (cur.y - prev.y) * progress) * cellSize;
        const size = cellSize;
        if (i===0){
            // head
            ctx.fillStyle = '#4caf50';
            ctx.fillRect(ix, iy, size, size);
            // eyes
            const eyeR = size * 0.12;
            const eyeOffsetForward = size * 0.2;
            const eyeOffsetSide = size * 0.15;
            const cx = ix + size/2;
            const cy = iy + size/2;
            const dx = direction.x;
            const dy = direction.y;
            const perpX = -dy;
            const perpY = dx;
            const eye1X = cx + dx*eyeOffsetForward + perpX*eyeOffsetSide;
            const eye1Y = cy + dy*eyeOffsetForward + perpY*eyeOffsetSide;
            const eye2X = cx + dx*eyeOffsetForward - perpX*eyeOffsetSide;
            const eye2Y = cy + dy*eyeOffsetForward - perpY*eyeOffsetSide;
            ctx.fillStyle = '#fff';
            ctx.beginPath();
            ctx.arc(eye1X, eye1Y, eyeR, 0, Math.PI*2);
            ctx.arc(eye2X, eye2Y, eyeR, 0, Math.PI*2);
            ctx.fill();

            // pupil optional: black circle within each eye
            const pupilR = eyeR * 0.5;
            ctx.fillStyle = '#000';
            ctx.beginPath();
            ctx.arc(eye1X, eye1Y, pupilR, 0, Math.PI*2);
            ctx.arc(eye2X, eye2Y, pupilR, 0, Math.PI*2);
            ctx.fill();
        } else {
            // body
            ctx.fillStyle = '#2e7d32';
            ctx.fillRect(ix, iy, size, size);
        }
    }

    // draw particles
    particles.forEach(p => {
        const lifeRatio = p.life / p.maxLife;
        ctx.globalAlpha = lifeRatio;
        ctx.fillStyle = '#ffeb3b';
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
        ctx.fill();
    });
    ctx.globalAlpha = 1;
}

Now updateScore, showOverlay, hideOverlay:

function showOverlay(html){
    const overlay = document.getElementById('overlay');
    overlay.innerHTML = `<div>${html}</div>`;
    overlay.style.display='flex';
}
function hideOverlay(){
    const overlay = document.getElementById('overlay');
    overlay.style.display='none';
}

Now handling pause overlay: In pauseGame function we already show Overlay.

Now we need to initialize overlay with start message using showOverlay.

Now final step: start requestAnimationFrame.

Let's write complete code structure.

Edge Cases and tricks:

- Use of global variable 'direction' defined as object with x,y.

- Ensure direction is updated before step for both demo and playing.

- When directionQueue is used, we always apply at step boundary.

- For quick successive key press: The directionQueue pushes each valid direction; as progress increments they get applied one per step. This fulfills requirement.

- To guarantee quick successive key presses are both honored: If user presses two arrow keys quickly before the next movement, both will be in queue; the snake will change direction each step accordingly, not ignoring second.

- Ensure not reverse onto itself: In enqueueDirection we check against reverse of last direction of queue or current direction.

Now interactive demonstration:

- On load, overlay puzzle displayed; demo runs behind.

- The overlay is clickable; clicking or any key starts the game (player control).

- When playing, score displayed; best displayed too.

- On pause (P), overlay shows "Paused".

- On game over, overlay shows score and prompt to restart.

- When restarted, new game starts fresh.

Now test for issues: Ensure that after game over in player mode, pressing Space or Enter restarts; need to ensure startGame resets scoreboard and snake.

In startGame we hide overlay and set gameState='playing'; but need to ensure bestScore is updated accordingly; updateScore is called via initGame which sets score to 0 and bestScore unchanged.

Now consider scenario: If player restarts quickly, the overlay is still there and pressing Space may both restart and treat as direction, but startGame will hide overlay and initGame; then directionQueue remains empty.

Now need to make sure startGame resets directionQueue.

Now final check: When overlay is visible, keyboard events might go to overlay or to window; we added keydown listener on window, which should work even if overlay covers. However, when user presses a key to start, both overlay's click and keydown will call startGame. That's fine.

Now need to handle clear of interval? Not needed.

Now need to ensure no external resources; all code is inline.

Now incorporate all into a single HTML file with style and script.

Potential improvements: Add subtle CSS for overlay text center, etc.

Now code skeleton:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Snake Demo</title>
<style>
    html,body{
        margin:0;
        height:100%;
        background:#222;
        overflow:hidden;
        font-family:Arial,Helvetica,sans-serif;
        color:#fff;
    }
    #gameContainer{
        position:relative;
        width:100%;
        height:100%;
        display:flex;
        align-items:center;
        justify-content:center;
    }
    #gameCanvas{
        background:#111;
        border:4px solid #333;
        border-radius:12px;
        box-shadow:0 0 10px rgba(0,0,0,0.5);
        display:block;
    }
    #overlay{
        position:absolute;
        top:0;
        left:0;
        width:100%;
        height:100%;
        background:rgba(0,0,0,0.6);
        display:flex;
        align-items:center;
        justify-content:center;
        text-align:center;
        font-size:1.5rem;
        cursor:pointer;
        user-select:none;
        z-index:10;
    }
    #overlay div{
        padding:1rem 2rem;
        background:rgba(0,0,0,0.3);
        border-radius:8px;
    }
    #scoreboard{
        position:absolute;
        top:10px;
        left:10px;
        font-size:1.1rem;
        z-index:5;
        pointer-events:none;
    }
</style>
</head>
<body>
<div id="gameContainer">
    <canvas id="gameCanvas"></canvas>
    <div id="overlay"></div>
</div>
<div id="scoreboard"><span id="score">Score: 0</span> | <span id="best">Best: 0</span></div>
<script>
(() => {
    // Constants
    const GRID_SIZE = 20;
    let canvasSize, cellSize;
    const stepDuration = 120; // ms per cell

    // State variables
    let canvas, ctx;
    let snake = [], prevSnake = [];
    let direction = {x:1, y:0};
    let directionQueue = [];
    let food = {x:0, y:0};
    let score = 0, bestScore = 0;
    let gameState = 'demo'; // 'demo','playing','paused','gameover'
    let progress = 0;
    let lastTime = 0;
    let particles = [];

    // Helper: set up canvas
    function initCanvas(){
        canvas = document.getElementById('gameCanvas');
        ctx = canvas.getContext('2d');
        resizeCanvas();
        window.addEventListener('resize', () => {
            // on resize, reset transform before scaling again
            ctx.setTransform(1,0,0,1,0,0);
            resizeCanvas();
        });
    }

    function resizeCanvas(){
        const dpr = window.devicePixelRatio || 1;
        const size = Math.min(window.innerWidth, window.innerHeight) * 0.9;
        canvasSize = Math.floor(size);
        cellSize = canvasSize / GRID_SIZE;
        canvas.width = canvasSize * dpr;
        canvas.height = canvasSize * dpr;
        canvas.style.width = canvasSize + 'px';
        canvas.style.height = canvasSize + 'px';
        ctx.setTransform(1,0,0,1,0,0);
        ctx.scale(dpr, dpr);
    }

    // Game init
    function initGame(){
        const startX = Math.floor(GRID_SIZE/2);
        const startY = Math.floor(GRID_SIZE/2);
        snake = [
            {x:startX, y:startY},
            {x:startX-1, y:startY},
            {x:startX-2, y:startY},
            {x:startX-3, y:startY},
            {x:startX-4, y:startY}
        ];
        // copy to prev for first interpolation
        prevSnake = snake.map(seg => ({x: seg.x, y: seg.y}));
        direction = {x:1, y:0};
        directionQueue = [];
        score = 0;
        updateScore();
        placeFood();
        progress = 0;
        particles = [];
    }

    function placeFood(){
        const occupied = new Set(snake.map(s => s.x + ',' + s.y));
        let x, y;
        do {
            x = Math.floor(Math.random()*GRID_SIZE);
            y = Math.floor(Math.random()*GRID_SIZE);
        } while (occupied.has(x+','+y));
        food = {x, y};
    }

    // Input handling
    window.addEventListener('keydown', handleKey);
    function handleKey(e){
        if (gameState === 'demo'){
            startGame();
            e.preventDefault();
            return;
        }
        if (gameState === 'gameover'){
            if (e.code === 'Space' || e.code === 'Enter'){
                startGame();
                e.preventDefault();
            }
            return;
        }
        if (gameState === 'paused'){
            if (e.key.toLowerCase() === 'p'){
                resumeGame();
                e.preventDefault();
            }
            return;
        }

        // Playing state
        if (e.key.toLowerCase() === 'p'){
            pauseGame();
            e.preventDefault();
            return;
        }
        let newDir = null;
        switch(e.key){
            case 'ArrowUp': case 'w': case 'W': newDir = {x:0, y:-1}; break;
            case 'ArrowDown': case 's': case 'S': newDir = {x:0, y:1}; break;
            case 'ArrowLeft': case 'a': case 'A': newDir = {x:-1, y:0}; break;
            case 'ArrowRight': case 'd': case 'D': newDir = {x:1, y:0}; break;
        }
        if (newDir){
            enqueueDirection(newDir);
            e.preventDefault();
        }
    }

    function enqueueDirection(dir){
        const last = directionQueue.length ? directionQueue[directionQueue.length-1] : direction;
        // ignore reverse or same direction
        if (dir.x === -last.x && dir.y === -last.y) return;
        if (dir.x === last.x && dir.y === last.y) return;
        directionQueue.push(dir);
    }

    function pauseGame(){
        if (gameState === 'playing'){
            gameState = 'paused';
            showOverlay('Paused<br>Press P to resume');
        }
    }

    function resumeGame(){
        if (gameState === 'paused'){
            hideOverlay();
            gameState = 'playing';
        }
    }

    // Overlay helpers
    const overlay = document.getElementById('overlay');
    overlay.addEventListener('click', () => {
        if (gameState !== 'playing'){
            startGame();
        }
    });
    function showOverlay(html){
        overlay.innerHTML = `<div>${html}</div>`;
        overlay.style.display = 'flex';
    }
    function hideOverlay(){
        overlay.style.display = 'none';
    }

    function startGame(){
        hideOverlay();
        gameState = 'playing';
        initGame();
    }

    // Score display
    function updateScore(){
        document.getElementById('score').textContent = 'Score: ' + score;
        document.getElementById('best').textContent = 'Best: ' + bestScore;
    }

    // Game over handler
    function handleGameOver(){
        if (gameState === 'demo'){
            // Simple demo loop restart
            setTimeout(() => {
                initGame();
            }, 200);
        } else {
            if (score > bestScore) bestScore = score;
            updateScore();
            showOverlay(`Game Over<br>Score: ${score}<br>Press Space or Enter to restart`);
            gameState = 'gameover';
        }
    }

    // AI for demo mode
    function demoStep(){
        const head = snake[0];
        const dxToFood = Math.sign(food.x - head.x);
        const dyToFood = Math.sign(food.y - head.y);

        // Determine safe directions
        const dirs = [
            {x:0, y:-1},
            {x:1, y:0},
            {x:0, y:1},
            {x:-1, y:0}
        ];
        const reverse = {x:-direction.x, y:-direction.y};
        const safe = dirs.filter(d => {
            if (d.x===reverse.x && d.y===reverse.y) return false;
            const nx = head.x + d.x;
            const ny = head.y + d.y;
            if (nx<0 || nx>=GRID_SIZE || ny<0 || ny>=GRID_SIZE) return false;
            if (snake.some(seg=> seg.x===nx && seg.y===ny)) return false;
            return true;
        });
        if (safe.length===0) return; // trapped

        // Prefer moving towards food
        let chosen = null;
        if (Math.abs(food.x - head.x) > Math.abs(food.y - head.y) && dxToFood !== 0){
            const cand = {x:dxToFood, y:0};
            if (safe.some(d=> d.x===cand.x && d.y===cand.y)) chosen = cand;
        }
        if (!chosen && dyToFood !== 0){
            const cand = {x:0, y:dyToFood};
            if (safe.some(d=> d.x===cand.x && d.y===cand.y)) chosen = cand;
        }
        if (!chosen){
            // fallback: random safe direction
            chosen = safe[Math.floor(Math.random()*safe.length)];
        }
        direction = chosen;
    }

    // Movement step
    function step(){
        // store previous positions for interpolation
        prevSnake = snake.map(seg => ({x: seg.x, y: seg.y}));

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

        const eating = (newHead.x===food.x && newHead.y===food.y);

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

        // body collision
        const collision = snake.some((seg, idx) => {
            // ignore tail if it will move away (no growth)
            if (!eating && idx === snake.length-1) return false;
            return seg.x===newHead.x && seg.y===newHead.y;
        });
        if (collision){
            handleGameOver();
            return;
        }

        // move snake
        snake.unshift(newHead);
        if (eating){
            score += 10;
            updateScore();
            spawnParticles(newHead);
            placeFood();
            // keep tail (grow)
        } else {
            // remove tail
            snake.pop();
        }
    }

    // Particle system
    function spawnParticles(pos){
        const count = 12;
        const baseX = (pos.x + 0.5) * cellSize;
        const baseY = (pos.y + 0.5) * cellSize;
        for (let i=0;i<count;i++){
            const angle = Math.random()*2*Math.PI;
            const speed = cellSize * (0.015 + Math.random()*0.015); // px per ms
            particles.push({
                x: baseX,
                y: baseY,
                vx: Math.cos(angle)*speed,
                vy: Math.sin(angle)*speed,
                life: 400,
                maxLife: 400,
                size: Math.random()*3 + 2
            });
        }
    }

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

    // Rendering
    function draw(){
        // background
        ctx.fillStyle = '#111';
        ctx.fillRect(0,0,canvasSize,canvasSize);

        // Food
        ctx.fillStyle = '#e00';
        const fx = (food.x + 0.5) * cellSize;
        const fy = (food.y + 0.5) * cellSize;
        ctx.beginPath();
        ctx.arc(fx, fy, cellSize*0.3, 0, Math.PI*2);
        ctx.fill();

        // Snake
        for (let i=0;i<snake.length;i++){
            const cur = snake[i];
            const prev = (prevSnake[i] !== undefined) ? prevSnake[i] : cur;
            const ix = (prev.x + (cur.x - prev.x) * progress) * cellSize;
            const iy = (prev.y + (cur.y - prev.y) * progress) * cellSize;
            if (i===0){
                // head
                ctx.fillStyle = '#4caf50';
                ctx.fillRect(ix, iy, cellSize, cellSize);
                // eyes
                const eyeR = cellSize * 0.12;
                const fwd = cellSize * 0.2;
                const side = cellSize * 0.15;
                const cx = ix + cellSize/2;
                const cy = iy + cellSize/2;
                const dx = direction.x;
                const dy = direction.y;
                const perpX = -dy;
                const perpY = dx;
                const eye1X = cx + dx*fwd + perpX*side;
                const eye1Y = cy + dy*fwd + perpY*side;
                const eye2X = cx + dx*fwd - perpX*side;
                const eye2Y = cy + dy*fwd - perpY*side;
                ctx.fillStyle = '#fff';
                ctx.beginPath();
                ctx.arc(eye1X, eye1Y, eyeR, 0, Math.PI*2);
                ctx.arc(eye2X, eye2Y, eyeR, 0, Math.PI*2);
                ctx.fill();
                // pupils
                ctx.fillStyle = '#000';
                ctx.beginPath();
                ctx.arc(eye1X, eye1Y, eyeR*0.5, 0, Math.PI*2);
                ctx.arc(eye2X, eye2Y, eyeR*0.5, 0, Math.PI*2);
                ctx.fill();
            } else {
                ctx.fillStyle = '#2e7d32';
                ctx.fillRect(ix, iy, cellSize, cellSize);
            }
        }

        // Particles
        particles.forEach(p => {
            const alpha = p.life / p.maxLife;
            ctx.globalAlpha = alpha;
            ctx.fillStyle = '#ffeb3b';
            ctx.beginPath();
            ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
            ctx.fill();
        });
        ctx.globalAlpha = 1;
    }

    // Game loop
    function loop(timestamp){
        if (!lastTime) lastTime = timestamp;
        const dt = timestamp - lastTime;
        lastTime = timestamp;

        if (gameState !== 'paused' && gameState !== 'gameover'){
            progress += dt / stepDuration;
            while (progress >= 1){
                if (gameState === 'demo'){
                    demoStep();
                } else if (gameState === 'playing'){
                    if (directionQueue.length){
                        const nd = directionQueue.shift();
                        if (!(nd.x === -direction.x && nd.y === -direction.y)){
                            direction = nd;
                        }
                    }
                }
                step();
                progress -= 1;
                // If game ended during step, break loop
                if (gameState === 'gameover') {
                    progress = 0;
                    break;
                }
            }
        }

        updateParticles(dt);
        draw();
        requestAnimationFrame(loop);
    }

    // Startup
    initCanvas();
    showOverlay('Click or press any key<br>to start');
    gameState = 'demo';
    initGame();
    requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Now double-check the behavior:

- At first paint, overlay visible with start message, behind which demo is playing (we had initGame called so snake and food placed). The demo will start moving automatically because gameState='demo' and loop runs, demoStep updating direction each step.

- However, we need to ensure demoStep applies direction each step BEFORE the movement step, which we do in the loop while when gameState === 'demo'.

- At start, direction = {1,0} so snake moves right.

- demoStep might also adjust direction to follow the food.

- So the snake will move and eat food.

- Score updates while gameState='demo', but scoreboard shows score (score reset to 0 each init). In demo mode, scoreboard will update as snake eats. Because we call updateScore during eating. Might be interesting.

- bestScore updates only after player game over.

- Overlay still visible covering canvas, so scoreboard may be hidden behind overlay? overlay has higher z-index (10) and scoreboard has z-index 5, so overlay covers scoreboard; but scoreboard is not visible. The requirement wants the current score displayed, but not required to be visible behind overlay when overlay is shown. Dropdown. In gameplay overlay is hidden, scoreboard appears.

- When playing, overlay hidden and scoreboard visible.

- On pause, overlay shows paused message; scoreboard hidden.

- On game over, overlay appears with game over message. Scoreboard hidden.

- On demo restart after crash, overlay still visible, scoreboard hidden.

- Ensure that demonstration runs smoothly.

Potential issue: In demo mode, after a collision, handleGameOver restarts game after 200ms, but we didn't clear particles or progress? We call initGame which resets progress. However the overlay stays visible; it will still be covering; but we still have timeouts.

Potential issue: After calling initGame inside setTimeout for demo restart, direction may be still whatever, we need to set direction to {1,0} as part of initGame; which we do. So demo resets.

Now about quick successive key presses: The queue mechanism ensures valid directions are stored. After applying a direction at each step, we shift one direction.

But scenario: Player presses arrow keys quickly: They may press "up", "right" quickly before any movement. The queue will have those two directions. At step boundary, we apply first "up" (if not reverse) and then shift to the new direction next tick "right". However after applying "up", direction = up; then on next tick before moving again, we apply second direction "right". This ensures both directions honored.

Now negative scenario: if player tries to reverse direction (like moving right and pressing left), queue will ignore due to opposite check. So snake cannot reverse onto itself.

Now about the requirement "Quick successive key presses must both be honored, but the snake can never reverse onto itself." This meets.

Now not using storage; bestScore only stored per session.

One more requirement: "Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating." We have smooth movement via interpolation, distinct head with eyes, particle burst.

Now confirm the code has no external dependencies. It uses only HTML, CSS, JS; no images. It's all inline.

Now ensure no console errors. Let's run mental lint:

- In initCanvas we call resizeCanvas after setting canvas variable; inside resizeCanvas use ctx - defined after canvas get context. OK.

- In window resize event, we call ctx.setTransform before resizing; but we reassign transform inside resizeCanvas anyway, but it's okay to call setTransform prematurely. However we are using ctx variable defined globally after initCanvas. So after canvas =..., ctx = canvas.getContext...; then we attach resize event to window. In the event callback, we use ctx which is still defined because it's global. Good.

- In initGame we set snake and prevSnake; we also call placeFood which uses snake. All good.

- In placeFood, set occupied = new Set(snake.map(...)). Works if snake is defined.

- In updateScore we reference document.getElementById('score') which exists.

- In handleGameOver for demo we call initGame after setTimeout. Good.

- In demoStep we reference direction, snake, food and GRID_SIZE; works.

Potential bug: In demoStep, if direction leads to snake colliding, we rotate to safe direction; but we might also sometimes set direction to a direction leading to immediate collision if not careful. However safe list ensures no collision. So fine.

- In step, we store prevSnake before movement, then compute newHead. Then check collision; use snake in collision check but includes old tail. If eating or not we use conditional to exclude tail if not eating (i.e., the tail will move). Good.

- For moving, we unshift newHead, then if not eating pop tail. If eating, we keep tail and generate particles.

- In spawnParticles we compute speed in px per ms; updateParticles uses p.x += p.vx * dt; given speed per ms, this increments correctly.

- In draw, we use cellSize variable for drawing positions. Ok.

- In draw snake head, we use direction variable for eye orientation; direction is updated each step accordingly.

- In draw, for each segment, we compute prev and cur positions. For i>=snake.length maybe prevSnake has less length (if snake grew) - but we check prevSnake[i] !== undefined else use cur as previous. That means new segments appear with no interpolation. Good.

- In draw particles, we use ctx.globalAlpha property; at each particle we set and later reset to 1. This works.

- In loop, we maintain lastTime and dt in ms (timestamp is in ms). progress accumulates dt / stepDuration.

- "stepDuration = 120" ms per movement step, making speed ~ 8.33 cells per second. That seems fine.

- At each iteration we call updateParticles(dt) even when paused? Should we pause particles when game paused? Probably fine; we could not update. But optional.

- In pauseGame overlay we show "Paused". That overlay also blocks visibility of game/resumes.

- When paused and resumed, direction, snake positions unaffected.

Potential repeated definitions: we defined "const overlay = document.getElementById('overlay');" after overlay element exists because script at bottom of body after overlay? Yes the script is placed after the DOM elements (script at end of body). overlay variable defined after: we call overlay.addEventListener in script after element exists. Good.

Potential interference: We call initCanvas which does resizeCanvas; canvas referenced before it's defined? In initCanvas, we set canvas = document.getElementById('gameCanvas'), then ctx = canvas.getContext('2d'); then we call resizeCanvas which uses canvas variable and ctx. Works.

Now ensure correct ordering: In script at end, we call initCanvas (which registers resize event). Then showOverlay with message. Then set gameState='demo', initGame(); Then requestAnimationFrame(loop);

Potential race: In loop, on first call progress is 0 and lastTime = timestamp when first called. Then dt = timestamp - lastTime computed incorrectly? Actually we set if (!lastTime) lastTime = timestamp; then dt = timestamp - lastTime; but after setting lastTime in that block we need to compute dt after? Wait: after if (!lastTime) lastTime = timestamp; then we compute dt = timestamp - lastTime; Since we set lastTime to timestamp on first call, dt = 0. Then lastTime = timestamp set again later? Let's see:

function loop(timestamp){
   if (!lastTime) lastTime = timestamp;
   const dt = timestamp - lastTime;
   lastTime = timestamp;
   // ...
}

If lastTime is set to timestamp in if statement, then dt = timestamp - timestamp = 0, then lastTime gets overwritten to timestamp (no change). So dt = 0 on first frame, not problematic.

Now time for overlay start: After initGame we also call initGame which resets snake and score (score 0). Good.

Now confirm that overlay is initially visible. In init we call showOverlay('Click or press any key<br>to start'); Then gameState set to 'demo'. So overlay visible but demo runs.

Now when user clicks or presses any key, startGame is called. startGame hides overlay, sets gameState='playing', and initGame resets snake. Good.

Now after game over (player), bestScore updates, overlay shows game over with restart instructions. Press space or Enter triggers startGame again.

Now press P to pause: overlay appear with paused message; resume with P again.

Now ensure the scoreboard is not hidden when overlay is hidden? scoreboard is positioned absolute top left with pointer-events:none, will be visible even under overlay if overlay is hidden.

Potential issue: scoreboard may be overlapped by overlay when overlay is hidden? No.

Now ensure colors are visible against dark background.

Potential performance issues: With requestAnimationFrame at 60fps, we run draw each frame; we compute interpolation using progress variable; fine.

Now responsiveness: The canvas width/height are set to 90% of smaller of viewport width/height; at 1280x800 minimum, canvas may be ~720px (90% of 800). At 375px wide viewport, canvas maybe 337.5 (90% of 375) which is okay. The container uses flexbox to centre. So game fits.

Now final code must be entire file inside a single fenced code block with language "html". The user expects that.

Thus final message: a code block with the entire HTML file.

Before final, double-check for any missing parentheses or variable scoping.

Now rewrite with minimal coding errors.

Potential mistakes:

- In overlay variable we used const overlay = document.getElementById('overlay'); But that requires overlay element to exist before script runs. Since script is placed after the HTML elements, it's fine.

- In functions like showOverlay we reference overlay with .innerHTML; but overlay's innerHTML is overwritten each time. That is okay.

- In pauseGame we call showOverlay('Paused<br>Press P to resume'); That's fine.

- In startGame we hide overlay and set gameState='playing', initGame. However if gameState was 'demo', then we still have directionQueue cleared.

- In handleKey when gameState is 'demo' we startGame and e.preventDefault().

- In initGame we called updateScore, but updateScore also references bestScore which may not be defined yet? It's defined globally as 0. Ok.

- At gameState change from 'demo' to 'playing', the progress variable should be reset maybe? initGame sets progress=0. Good.

Potential oversight: In demo mode on game over, we call initGame after short delay. The overlay is still shown but we keep gameState 'demo' unchanged. However the overlay currently shows "Click or press any key to start"? Actually after startGame it's hidden. In demo mode start after page load, we call showOverlay('Click or press any key...'); Then set gameState='demo', but overlay is visible. After demo event, when snake dies, handleGameOver resets snake (initGame) but does not hide overlay, overlay remains visible. Good.

BUT we might need to ensure that on demo restart, the overlay text stays the same "Click or press any key to start". Since we never changed overlay text, it's still that. So fine.

Potential bug: In handleGameOver for demo we call setTimeout with initGame only. However, we also need to resume progress variable and particles etc. Since initGame resets progress and particles. Also after resetting we want the snake moving again; game loop will continue with progress increments. That should work.

One more oversight: The overlay being visible blocks pointer events when the demo is playing, but we have overlay covering the canvas. Might block pointer events for clicking to start. But we have overlay click event to start game; after that hide overlay, subsequent game input uses keyboard only; the overlay not blocking.

Potential need for moving snake positions after resume: progress might be something non-zero after a step; after initGame resets progress to 0, fine.

Now confirm draw of background we fill with #111; scoreboard text color white.

Now about aligning the scoreboard: It's positioned absolute top left; but since #gameContainer is relative? Actually #scoreboard is absolute with respect to body; we may want relative container for scoreboard reposition (but not critical). It has z-index 5, overlay has z-index 10, so scoreboard behind overlay; fine.

Now let's see final code.

Make sure code does not use any disallowed functions: No localStorage, sessionStorage, cookies, etc. Use of setTimeout is okay (allowed). No alert/prompt.

Now finalize.