Kimi K3.1 Pro
by MoonshotAIreasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 4 min 19 s
- Cost
- $0.267
- Output tokens
- 29,507
- Spent thinking
- 18,258 tokens
- Speed
- 114.2 tok/s
- Console errors
- 0
- First token after
- 346 ms
- First visible text after
- 3 min 2 s
- Prompt tokens
- 579
- Code size
- 37.6 KB
- Finish reason
- stop
- Served by
- InferenceNet
- Model id
moonshotai/kimi-k3.1-pro- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 1305 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snake — Midnight Garden</title>
<style>
:root {
color-scheme: dark;
--ink: #eefdf4;
--muted: #8da9a2;
--green: #9dff75;
--teal: #45e4ad;
--gold: #ffd36a;
--panel: rgba(12, 31, 31, 0.82);
--line: rgba(181, 255, 213, 0.11);
}
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body {
margin: 0;
min-height: 100%;
overflow-x: hidden;
}
body {
min-height: 100vh;
min-height: 100svh;
background:
radial-gradient(circle at 18% 14%, rgba(32, 117, 88, 0.17), transparent 34%),
radial-gradient(circle at 84% 82%, rgba(29, 99, 104, 0.16), transparent 35%),
#071112;
color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.23;
background-image:
linear-gradient(rgba(167, 255, 202, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(167, 255, 202, 0.025) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: radial-gradient(circle at center, black, transparent 78%);
}
.app {
position: relative;
min-height: 100vh;
min-height: 100svh;
padding: 20px 14px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.topbar {
width: min(720px, 100%);
margin-bottom: 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.brand {
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
}
.brand-mark {
width: 46px;
height: 46px;
flex: 0 0 auto;
display: grid;
place-items: center;
border: 1px solid rgba(159, 255, 122, 0.17);
border-radius: 14px;
background: linear-gradient(145deg, rgba(139, 255, 132, 0.1), rgba(49, 192, 151, 0.035));
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), 0 8px 30px rgba(0,0,0,0.2);
}
.brand-mark svg { width: 38px; height: 38px; }
.brand h1 {
margin: 0;
font-size: 25px;
line-height: 1;
letter-spacing: 0.18em;
font-weight: 800;
}
.brand h1 span { color: var(--green); }
.brand p {
margin: 6px 0 0;
color: var(--muted);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
white-space: nowrap;
}
.stats {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 20px;
padding: 10px 16px;
border: 1px solid var(--line);
border-radius: 15px;
background: rgba(12, 29, 29, 0.64);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.025);
backdrop-filter: blur(12px);
}
.stat { min-width: 60px; }
.stat + .stat {
padding-left: 20px;
border-left: 1px solid var(--line);
}
.stat span {
display: block;
margin-bottom: 2px;
color: var(--muted);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
white-space: nowrap;
}
.stat strong {
display: block;
font-size: 25px;
line-height: 1.05;
font-weight: 750;
letter-spacing: 0.04em;
font-variant-numeric: tabular-nums;
transform-origin: left center;
}
.stat:first-child strong { color: var(--green); }
.stat:last-child strong { color: var(--gold); }
.bump { animation: score-bump 240ms ease-out; }
.stage {
width: min(64vh, 580px, calc(100vw - 28px));
display: flex;
flex-direction: column;
gap: 10px;
}
.game-meta {
min-height: 22px;
padding: 0 3px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--muted);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.status {
display: inline-flex;
align-items: center;
gap: 8px;
color: #a5cbbd;
white-space: nowrap;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 11px rgba(157,255,117,0.75);
animation: status-pulse 1.8s ease-in-out infinite;
}
.status[data-state="paused"] .status-dot {
background: var(--gold);
box-shadow: 0 0 10px rgba(255,211,106,0.55);
animation: none;
}
.status[data-state="over"] .status-dot {
background: #ff887b;
box-shadow: 0 0 10px rgba(255,110,99,0.55);
animation: none;
}
.food-rule {
display: inline-flex;
align-items: center;
gap: 7px;
white-space: nowrap;
}
.food-rule::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: #ffbd62;
box-shadow: 0 0 9px rgba(255,174,82,0.65);
}
.board-frame {
position: relative;
width: 100%;
aspect-ratio: 1;
padding: 8px;
overflow: hidden;
border: 1px solid rgba(167, 255, 202, 0.14);
border-radius: 24px;
background: linear-gradient(145deg, rgba(20, 47, 44, 0.94), rgba(7, 22, 24, 0.97));
box-shadow:
0 30px 80px rgba(0,0,0,0.42),
0 0 70px rgba(68, 213, 152, 0.055),
inset 0 1px 0 rgba(255,255,255,0.055);
}
.board-frame:focus-within {
border-color: rgba(157, 255, 117, 0.24);
}
.board-frame.hit { animation: board-hit 320ms ease-out; }
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 1;
border-radius: 16px;
outline: none;
touch-action: none;
}
.overlay {
position: absolute;
inset: 8px;
z-index: 4;
padding: 20px;
display: flex;
border-radius: 16px;
cursor: pointer;
user-select: none;
}
.overlay.demo {
align-items: flex-end;
justify-content: center;
background: linear-gradient(to top, rgba(3, 13, 15, 0.48), transparent 52%);
}
.overlay.modal {
align-items: center;
justify-content: center;
background: rgba(3, 12, 14, 0.69);
backdrop-filter: blur(4px);
}
.overlay-card {
width: min(340px, 100%);
padding: 18px 22px;
border: 1px solid rgba(188, 255, 211, 0.14);
border-radius: 18px;
background: rgba(8, 25, 26, 0.91);
box-shadow: 0 18px 45px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.045);
text-align: center;
backdrop-filter: blur(14px);
animation: card-in 280ms ease-out;
}
.overlay.demo .overlay-card { padding: 15px 20px; }
.overlay-kicker {
display: inline-flex;
align-items: center;
gap: 7px;
margin-bottom: 8px;
color: var(--green);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.17em;
text-transform: uppercase;
}
.overlay-kicker::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 9px currentColor;
}
.overlay.danger .overlay-kicker { color: #ff9a89; }
.overlay.pause .overlay-kicker { color: var(--gold); }
.overlay h2 {
margin: 0;
font-size: clamp(21px, 4.5vw, 27px);
line-height: 1.15;
letter-spacing: -0.025em;
}
.overlay p {
margin: 9px 0 0;
color: #c0d8d0;
font-size: 13px;
line-height: 1.45;
}
.overlay.demo p {
color: #f2fff7;
font-weight: 650;
}
.overlay-hint {
margin-top: 12px;
color: #78988e;
font-size: 9px;
font-weight: 750;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.final-score {
margin: 14px auto 2px;
display: flex;
flex-direction: column;
align-items: center;
}
.final-score strong {
color: var(--green);
font-size: 52px;
line-height: 1;
font-weight: 800;
letter-spacing: -0.04em;
font-variant-numeric: tabular-nums;
text-shadow: 0 0 28px rgba(157,255,117,0.18);
}
.final-score span {
margin-top: 5px;
color: var(--muted);
font-size: 9px;
font-weight: 750;
letter-spacing: 0.2em;
}
.controls {
width: min(720px, 100%);
margin-top: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 10px 20px;
color: #809d94;
font-size: 11px;
}
.control {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
kbd {
min-width: 23px;
height: 23px;
padding: 0 6px;
display: inline-grid;
place-items: center;
border: 1px solid rgba(186, 255, 213, 0.14);
border-bottom-color: rgba(186, 255, 213, 0.23);
border-radius: 6px;
background: rgba(16, 37, 36, 0.82);
box-shadow: 0 2px 0 rgba(0,0,0,0.28);
color: #bad2ca;
font: 700 10px/1 ui-sans-serif, sans-serif;
}
.arrow-keys { display: inline-flex; gap: 3px; }
@keyframes status-pulse {
0%, 100% { opacity: 0.65; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.12); }
}
@keyframes score-bump {
0% { transform: scale(1); }
45% { transform: scale(1.22); }
100% { transform: scale(1); }
}
@keyframes card-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes board-hit {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
50% { transform: translateX(4px); }
75% { transform: translateX(-2px); }
}
@media (max-width: 480px) {
.app { padding: 14px 12px; }
.topbar { gap: 10px; margin-bottom: 11px; }
.brand { gap: 8px; }
.brand-mark { width: 38px; height: 38px; border-radius: 12px; }
.brand-mark svg { width: 32px; height: 32px; }
.brand h1 { font-size: 20px; }
.brand p { display: none; }
.stats { gap: 11px; padding: 8px 11px; border-radius: 12px; }
.stat { min-width: 48px; }
.stat + .stat { padding-left: 11px; }
.stat span { font-size: 8px; letter-spacing: 0.08em; }
.stat strong { font-size: 21px; }
.game-meta { font-size: 8px; letter-spacing: 0.09em; }
.board-frame { padding: 6px; border-radius: 20px; }
.overlay { inset: 6px; padding: 13px; }
.overlay-card { padding: 15px 16px; }
.overlay.demo .overlay-card { padding: 13px 15px; }
.overlay p { font-size: 12px; }
.overlay-hint { margin-top: 9px; font-size: 8px; }
.controls { margin-top: 11px; gap: 9px 13px; font-size: 10px; }
kbd { min-width: 21px; height: 21px; padding: 0 5px; }
}
@media (prefers-reduced-motion: reduce) {
.status-dot, .overlay-card, .bump, .board-frame.hit { animation: none; }
}
</style>
</head>
<body>
<main class="app">
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 44 44" fill="none">
<path d="M11 12h14a6 6 0 0 1 0 12h-7a5 5 0 0 0 0 10h13" stroke="url(#logoGradient)" stroke-width="6" stroke-linecap="round"/>
<circle cx="32" cy="34" r="4.5" fill="#a7ff79"/>
<circle cx="33.5" cy="32.8" r="1" fill="#09201b"/>
<defs>
<linearGradient id="logoGradient" x1="9" y1="10" x2="34" y2="36" gradientUnits="userSpaceOnUse">
<stop stop-color="#48ddb0"/>
<stop offset="1" stop-color="#b2ff72"/>
</linearGradient>
</defs>
</svg>
</div>
<div>
<h1>SNAKE<span>.</span></h1>
<p>Midnight garden</p>
</div>
</div>
<div class="stats" aria-label="Scoreboard">
<div class="stat">
<span>Score</span>
<strong id="scoreValue">000</strong>
</div>
<div class="stat">
<span>Session best</span>
<strong id="bestValue">000</strong>
</div>
</div>
</header>
<section class="stage" aria-label="Snake game">
<div class="game-meta">
<div id="statusPill" class="status" data-state="demo">
<span class="status-dot"></span>
<span id="statusText">Autopilot demo</span>
</div>
<div class="food-rule">Eat +10 · Grow +1</div>
</div>
<div id="boardFrame" class="board-frame">
<canvas id="gameCanvas" width="640" height="640" tabindex="0" aria-label="A 22 by 22 Snake game board">
Your browser needs canvas support to play Snake.
</canvas>
<div id="overlay" class="overlay demo">
<div class="overlay-card">
<div id="overlayKicker" class="overlay-kicker">Autopilot demo</div>
<h2 id="overlayTitle">Ready to take over?</h2>
<div id="finalScore" class="final-score" hidden>
<strong id="finalScoreValue">0</strong>
<span>POINTS</span>
</div>
<p id="overlayText">Click or press a key to start</p>
<div id="overlayHint" class="overlay-hint">Arrow keys / WASD · P to pause</div>
</div>
</div>
</div>
</section>
<footer class="controls" aria-label="Controls">
<div class="control">
<span class="arrow-keys" aria-hidden="true">
<kbd>←</kbd><kbd>↑</kbd><kbd>↓</kbd><kbd>→</kbd>
</span>
<span>/</span><kbd>WASD</kbd><span>Move</span>
</div>
<div class="control"><kbd>P</kbd><span>Pause</span></div>
<div class="control"><kbd>Space</kbd><span>/</span><kbd>Enter</kbd><span>Restart after crash</span></div>
</footer>
</main>
<script>
(() => {
"use strict";
const GRID = 22;
const ALL_DIRS = [
{ x: 1, y: 0 },
{ x: 0, y: 1 },
{ x: -1, y: 0 },
{ x: 0, y: -1 }
];
const KEY_DIRS = {
ArrowUp: { x: 0, y: -1 },
KeyW: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
KeyS: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
KeyA: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
KeyD: { x: 1, y: 0 }
};
const FOOD_COLORS = ["#ffd76f", "#ff9e62", "#fff2ad", "#a6ff79"];
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const boardFrame = document.getElementById("boardFrame");
const scoreValue = document.getElementById("scoreValue");
const bestValue = document.getElementById("bestValue");
const statusPill = document.getElementById("statusPill");
const statusText = document.getElementById("statusText");
const overlay = document.getElementById("overlay");
const overlayKicker = document.getElementById("overlayKicker");
const overlayTitle = document.getElementById("overlayTitle");
const overlayText = document.getElementById("overlayText");
const overlayHint = document.getElementById("overlayHint");
const finalScore = document.getElementById("finalScore");
const finalScoreValue = document.getElementById("finalScoreValue");
let mode = "demo";
let snake = [];
let prevSnake = [];
let direction = { x: 1, y: 0 };
let inputQueue = [];
let food = null;
let score = 0;
let best = 0;
let didWin = false;
let particles = [];
let floaters = [];
let rings = [];
let lastTick = performance.now();
let lastFrame = performance.now();
let frozenAlpha = 0;
let viewSize = 640;
let pixelRatio = 1;
const cellKey = p => `${p.x},${p.y}`;
const sameCell = (a, b) => a.x === b.x && a.y === b.y;
const inside = (x, y) => x >= 0 && x < GRID && y >= 0 && y < GRID;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const isOpposite = (a, b) => a.x === -b.x && a.y === -b.y;
const copySnake = value => value.map(p => ({ x: p.x, y: p.y }));
function stepDuration() {
if (mode === "demo") return 105;
return Math.max(76, 118 - Math.floor(score / 50) * 5);
}
function updateHUD() {
scoreValue.textContent = String(score).padStart(3, "0");
bestValue.textContent = String(best).padStart(3, "0");
statusPill.dataset.state = mode;
const labels = {
demo: "Autopilot demo",
playing: "Live run · 22 × 22",
paused: "Game paused",
over: didWin ? "Garden conquered" : "Run complete"
};
statusText.textContent = labels[mode] || "Snake";
}
function updateOverlay() {
finalScore.hidden = true;
if (mode === "playing") {
overlay.hidden = true;
return;
}
overlay.hidden = false;
if (mode === "demo") {
overlay.className = "overlay demo";
overlayKicker.textContent = "Autopilot demo";
overlayTitle.textContent = "Ready to take over?";
overlayText.textContent = "Click or press a key to start";
overlayHint.textContent = "Arrow keys / WASD · P to pause";
} else if (mode === "paused") {
overlay.className = "overlay modal pause";
overlayKicker.textContent = "Take a breath";
overlayTitle.textContent = "Paused";
overlayText.textContent = "Press P to jump back in.";
overlayHint.textContent = "P to resume";
} else {
overlay.className = `overlay modal ${didWin ? "win" : "danger"}`;
overlayKicker.textContent = didWin ? "Board complete" : "Run over";
overlayTitle.textContent = didWin ? "Perfect run!" : "Game over";
finalScore.hidden = false;
finalScoreValue.textContent = score;
overlayText.textContent = didWin
? "Every cell conquered. Your session best is " + best + "."
: "Your session best is " + best + ".";
overlayHint.textContent = "Space / Enter to restart";
}
}
function resetWorld(asDemo) {
mode = asDemo ? "demo" : "playing";
didWin = false;
score = 0;
snake = [
{ x: 6, y: 11 },
{ x: 5, y: 11 },
{ x: 4, y: 11 },
{ x: 3, y: 11 },
{ x: 2, y: 11 }
];
prevSnake = copySnake(snake);
direction = { x: 1, y: 0 };
inputQueue = [];
food = { x: 13, y: 11 };
particles = [];
floaters = [];
rings = [];
frozenAlpha = 0;
lastTick = performance.now();
updateHUD();
updateOverlay();
}
function startGame() {
resetWorld(false);
canvas.focus({ preventScroll: true });
}
function queueDirection(next) {
const last = inputQueue.length ? inputQueue[inputQueue.length - 1] : direction;
if (isOpposite(next, last) || sameCell(next, last)) return;
if (inputQueue.length < 4) inputQueue.push(next);
}
function togglePause() {
if (mode === "playing") {
frozenAlpha = clamp((performance.now() - lastTick) / stepDuration(), 0, 1);
mode = "paused";
updateHUD();
updateOverlay();
} else if (mode === "paused") {
mode = "playing";
lastTick = performance.now() - frozenAlpha * stepDuration();
updateHUD();
updateOverlay();
canvas.focus({ preventScroll: true });
}
}
function spawnFood() {
const occupied = new Set(snake.map(cellKey));
const empty = [];
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
if (!occupied.has(`${x},${y}`)) empty.push({ x, y });
}
}
if (!empty.length) {
food = null;
return false;
}
food = empty[Math.floor(Math.random() * empty.length)];
return true;
}
function bump(element) {
element.classList.remove("bump");
void element.offsetWidth;
element.classList.add("bump");
}
function emitBurst(x, y, colors = FOOD_COLORS, count = 19) {
rings.push({ x, y, life: 0.46, maxLife: 0.46, color: colors[0] });
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1.6 + Math.random() * 3.5;
const life = 0.42 + Math.random() * 0.38;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 0.35,
life,
maxLife: life,
size: 0.04 + Math.random() * 0.055,
color: colors[Math.floor(Math.random() * colors.length)]
});
}
}
function finishGame(won, impact) {
didWin = won;
mode = "over";
inputQueue = [];
best = Math.max(best, score);
if (impact) {
emitBurst(
clamp(impact.x + 0.5, 0.2, GRID - 0.2),
clamp(impact.y + 0.5, 0.2, GRID - 0.2),
["#ff8b75", "#ffd09a", "#ff5f69"],
24
);
}
updateHUD();
updateOverlay();
boardFrame.classList.remove("hit");
void boardFrame.offsetWidth;
boardFrame.classList.add("hit");
}
function orderedDirections() {
return [
direction,
{ x: direction.y, y: -direction.x },
{ x: -direction.y, y: direction.x },
{ x: -direction.x, y: -direction.y }
];
}
function findFoodDirection() {
if (!food) return null;
const head = snake[0];
const blocked = new Set(snake.slice(0, -1).map(cellKey));
const seen = new Set([cellKey(head)]);
const queue = [{ x: head.x, y: head.y, first: null }];
for (let i = 0; i < queue.length; i++) {
const node = queue[i];
const choices = node.first ? ALL_DIRS : orderedDirections();
for (const d of choices) {
if (!node.first && isOpposite(d, direction)) continue;
const nx = node.x + d.x;
const ny = node.y + d.y;
const key = `${nx},${ny}`;
if (!inside(nx, ny) || seen.has(key) || blocked.has(key)) continue;
const first = node.first || d;
if (nx === food.x && ny === food.y) return first;
seen.add(key);
queue.push({ x: nx, y: ny, first });
}
}
return null;
}
function evaluateMove(d) {
const head = snake[0];
const next = { x: head.x + d.x, y: head.y + d.y };
if (!inside(next.x, next.y)) return null;
const ate = food && sameCell(next, food);
const bodyCount = snake.length - (ate ? 0 : 1);
for (let i = 0; i < bodyCount; i++) {
if (sameCell(next, snake[i])) return null;
}
const future = [next, ...snake.slice(0, bodyCount)];
const blocked = new Set(future.slice(1).map(cellKey));
const seen = new Set([cellKey(next)]);
const stack = [next];
let area = 0;
while (stack.length) {
const current = stack.pop();
area++;
for (const nd of ALL_DIRS) {
const nx = current.x + nd.x;
const ny = current.y + nd.y;
const key = `${nx},${ny}`;
if (!inside(nx, ny) || seen.has(key) || blocked.has(key)) continue;
seen.add(key);
stack.push({ x: nx, y: ny });
}
}
const distance = food
? Math.abs(next.x - food.x) + Math.abs(next.y - food.y)
: 0;
return { d, area, distance };
}
function chooseDemoDirection() {
const candidates = [];
for (const d of orderedDirections()) {
if (isOpposite(d, direction)) continue;
const evaluation = evaluateMove(d);
if (evaluation) candidates.push(evaluation);
}
if (!candidates.length) return direction;
const pathDirection = findFoodDirection();
const spaceNeeded = Math.min(snake.length + 2, GRID * GRID - snake.length);
if (pathDirection) {
const pathMove = candidates.find(c => sameCell(c.d, pathDirection));
if (pathMove && pathMove.area >= spaceNeeded) return pathMove.d;
}
candidates.sort((a, b) => {
const aScore = a.area * 8 - a.distance * 1.5 + (sameCell(a.d, direction) ? 1 : 0);
const bScore = b.area * 8 - b.distance * 1.5 + (sameCell(b.d, direction) ? 1 : 0);
return bScore - aScore;
});
return candidates[0].d;
}
function takeStep() {
prevSnake = copySnake(snake);
if (mode === "demo") {
direction = chooseDemoDirection();
inputQueue = [];
} else if (inputQueue.length) {
direction = inputQueue.shift();
}
const head = snake[0];
const next = { x: head.x + direction.x, y: head.y + direction.y };
const ate = food && sameCell(next, food);
let collision = !inside(next.x, next.y);
if (!collision) {
const bodyCount = snake.length - (ate ? 0 : 1);
for (let i = 0; i < bodyCount; i++) {
if (sameCell(next, snake[i])) {
collision = true;
break;
}
}
}
if (collision) {
if (mode === "demo") {
resetWorld(true);
} else {
finishGame(false, next);
}
return;
}
snake.unshift(next);
if (!ate) {
snake.pop();
return;
}
const eatenFood = { x: food.x, y: food.y };
const previousBest = best;
score += 10;
if (mode === "playing") best = Math.max(best, score);
emitBurst(eatenFood.x + 0.5, eatenFood.y + 0.5);
floaters.push({
x: eatenFood.x + 0.5,
y: eatenFood.y + 0.15,
life: 0.9,
maxLife: 0.9,
text: "+10"
});
const hasFood = spawnFood();
updateHUD();
bump(scoreValue);
if (best > previousBest) bump(bestValue);
if (!hasFood) {
if (mode === "demo") resetWorld(true);
else finishGame(true, null);
}
}
function updateEffects(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;
p.vx *= Math.exp(-2.1 * dt);
p.vy = p.vy * Math.exp(-1.4 * dt) + 1.7 * dt;
}
for (let i = floaters.length - 1; i >= 0; i--) {
const f = floaters[i];
f.life -= dt;
f.y -= 0.7 * dt;
if (f.life <= 0) floaters.splice(i, 1);
}
for (let i = rings.length - 1; i >= 0; i--) {
rings[i].life -= dt;
if (rings[i].life <= 0) rings.splice(i, 1);
}
}
function drawBoard() {
const c = viewSize / GRID;
const background = ctx.createLinearGradient(0, 0, viewSize, viewSize);
background.addColorStop(0, "#0b2423");
background.addColorStop(1, "#081a1d");
ctx.fillStyle = background;
ctx.fillRect(0, 0, viewSize, viewSize);
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
if ((x + y) % 2 === 0) {
ctx.fillStyle = "rgba(176,255,206,0.014)";
ctx.fillRect(x * c, y * c, c, c);
}
}
}
ctx.beginPath();
for (let i = 1; i < GRID; i++) {
const p = i * c;
ctx.moveTo(p, 0);
ctx.lineTo(p, viewSize);
ctx.moveTo(0, p);
ctx.lineTo(viewSize, p);
}
ctx.strokeStyle = "rgba(176,255,211,0.045)";
ctx.lineWidth = 1;
ctx.stroke();
const ambient = ctx.createRadialGradient(
viewSize * 0.5, viewSize * 0.42, viewSize * 0.08,
viewSize * 0.5, viewSize * 0.5, viewSize * 0.72
);
ambient.addColorStop(0, "rgba(55,153,116,0.035)");
ambient.addColorStop(1, "rgba(0,5,9,0.26)");
ctx.fillStyle = ambient;
ctx.fillRect(0, 0, viewSize, viewSize);
}
function drawFood(now) {
if (!food) return;
const c = viewSize / GRID;
const x = (food.x + 0.5) * c;
const y = (food.y + 0.5) * c;
const pulse = 1 + Math.sin(now * 0.006) * 0.065;
const r = c * 0.255 * pulse;
ctx.save();
ctx.translate(x, y);
const halo = ctx.createRadialGradient(0, 0, 0, 0, 0, c * 0.8);
halo.addColorStop(0, "rgba(255,190,85,0.20)");
halo.addColorStop(1, "rgba(255,170,70,0)");
ctx.fillStyle = halo;
ctx.beginPath();
ctx.arc(0, 0, c * 0.8, 0, Math.PI * 2);
ctx.fill();
ctx.shadowColor = "rgba(255,174,75,0.72)";
ctx.shadowBlur = c * 0.62;
const fruit = ctx.createRadialGradient(-r * 0.35, -r * 0.42, r * 0.08, 0, 0, r);
fruit.addColorStop(0, "#fff3b0");
fruit.addColorStop(0.38, "#ffd166");
fruit.addColorStop(1, "#f47b50");
ctx.fillStyle = fruit;
ctx.beginPath();
ctx.arc(0, c * 0.035, r, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = "#8d6841";
ctx.lineWidth = Math.max(1.3, c * 0.065);
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(0, -r * 0.72);
ctx.quadraticCurveTo(c * 0.01, -r * 1.15, c * 0.09, -r * 1.28);
ctx.stroke();
ctx.fillStyle = "#83ef8b";
ctx.beginPath();
ctx.ellipse(c * 0.16, -r * 1.03, c * 0.145, c * 0.072, -0.55, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.75)";
ctx.beginPath();
ctx.arc(-r * 0.32, -r * 0.23, Math.max(1, r * 0.16), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function interpolatedPoints(alpha) {
const c = viewSize / GRID;
return snake.map((current, i) => {
const previous = prevSnake[Math.min(i, prevSnake.length - 1)] || current;
return {
x: (previous.x + (current.x - previous.x) * alpha + 0.5) * c,
y: (previous.y + (current.y - previous.y) * alpha + 0.5) * c
};
});
}
function drawSnake(alpha) {
if (!snake.length) return;
const c = viewSize / GRID;
const points = interpolatedPoints(alpha);
ctx.save();
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(points[points.length - 1].x, points[points.length - 1].y);
for (let i = points.length - 2; i >= 0; i--) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.strokeStyle = "rgba(1,12,14,0.66)";
ctx.lineWidth = c * 0.78;
ctx.stroke();
ctx.shadowColor = "rgba(92,255,157,0.28)";
ctx.shadowBlur = c * 0.62;
ctx.strokeStyle = "#38bd8b";
ctx.lineWidth = c * 0.68;
ctx.stroke();
const bodyGradient = ctx.createLinearGradient(0, viewSize, viewSize, 0);
bodyGradient.addColorStop(0, "#32c99b");
bodyGradient.addColorStop(0.55, "#65e99a");
bodyGradient.addColorStop(1, "#a9f876");
ctx.shadowBlur = 0;
ctx.strokeStyle = bodyGradient;
ctx.lineWidth = c * 0.56;
ctx.stroke();
ctx.strokeStyle = "rgba(231,255,202,0.13)";
ctx.lineWidth = c * 0.12;
ctx.stroke();
for (let i = 2; i < points.length - 1; i += 2) {
ctx.fillStyle = "rgba(7,77,59,0.17)";
ctx.beginPath();
ctx.arc(points[i].x, points[i].y, c * 0.075, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
const head = points[0];
const angle = Math.atan2(direction.y, direction.x);
ctx.save();
ctx.translate(head.x, head.y);
ctx.rotate(angle);
ctx.shadowColor = "rgba(155,255,117,0.42)";
ctx.shadowBlur = c * 0.55;
const headGradient = ctx.createLinearGradient(-c * 0.45, -c * 0.35, c * 0.45, c * 0.35);
headGradient.addColorStop(0, "#c4ff82");
headGradient.addColorStop(1, "#77ed8a");
ctx.fillStyle = headGradient;
ctx.beginPath();
ctx.roundRect(-c * 0.46, -c * 0.42, c * 0.96, c * 0.84, c * 0.28);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(234,255,198,0.32)";
ctx.lineWidth = Math.max(1, c * 0.035);
ctx.stroke();
for (const side of [-1, 1]) {
const eyeX = c * 0.15;
const eyeY = side * c * 0.205;
ctx.fillStyle = "#f5fff0";
ctx.beginPath();
ctx.arc(eyeX, eyeY, c * 0.112, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#09231e";
ctx.beginPath();
ctx.arc(eyeX + c * 0.045, eyeY, c * 0.058, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.85)";
ctx.beginPath();
ctx.arc(eyeX + c * 0.063, eyeY - c * 0.018, c * 0.018, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawEffects() {
const c = viewSize / GRID;
for (const ring of rings) {
const t = 1 - ring.life / ring.maxLife;
ctx.save();
ctx.globalAlpha = (1 - t) * 0.65;
ctx.strokeStyle = ring.color;
ctx.lineWidth = Math.max(1, c * 0.055 * (1 - t));
ctx.beginPath();
ctx.arc(ring.x * c, ring.y * c, c * (0.2 + t * 0.8), 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
for (const p of particles) {
ctx.save();
ctx.globalAlpha = clamp(p.life / p.maxLife, 0, 1);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x * c, p.y * c, Math.max(1, p.size * c), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
for (const f of floaters) {
const alpha = clamp(f.life / f.maxLife, 0, 1);
ctx.save();
ctx.globalAlpha = Math.min(1, alpha * 1.8);
ctx.fillStyle = "#fff2ad";
ctx.font = `800 ${Math.max(12, c * 0.55)}px ui-sans-serif, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowColor = "rgba(0,0,0,0.5)";
ctx.shadowBlur = 5;
ctx.fillText(f.text, f.x * c, f.y * c);
ctx.restore();
}
}
function draw(now) {
const c = viewSize / GRID;
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
ctx.clearRect(0, 0, viewSize, viewSize);
drawBoard();
drawFood(now);
let alpha = mode === "paused"
? frozenAlpha
: clamp((now - lastTick) / stepDuration(), 0, 1);
if (mode === "over") alpha = 1;
drawSnake(alpha);
drawEffects();
const vignette = ctx.createRadialGradient(
viewSize / 2, viewSize / 2, viewSize * 0.32,
viewSize / 2, viewSize / 2, viewSize * 0.74
);
vignette.addColorStop(0, "rgba(0,0,0,0)");
vignette.addColorStop(1, "rgba(0,5,8,0.25)");
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, viewSize, viewSize);
ctx.strokeStyle = "rgba(190,255,215,0.055)";
ctx.lineWidth = Math.max(1, c * 0.035);
ctx.strokeRect(1, 1, viewSize - 2, viewSize - 2);
}
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
viewSize = Math.max(1, rect.width);
pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
const width = Math.round(viewSize * pixelRatio);
const height = Math.round(viewSize * pixelRatio);
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
}
function loop(now) {
const dt = Math.min(0.05, Math.max(0, (now - lastFrame) / 1000));
lastFrame = now;
if ((mode === "playing" || mode === "demo") && now - lastTick >= stepDuration()) {
lastTick = now;
takeStep();
}
if (mode !== "paused") updateEffects(dt);
draw(now);
requestAnimationFrame(loop);
}
window.addEventListener("keydown", event => {
if (event.ctrlKey || event.metaKey || event.altKey) return;
const controlled = KEY_DIRS[event.code] || event.code === "Space";
if (controlled) event.preventDefault();
if (event.repeat && ["KeyP", "Space", "Enter", "NumpadEnter"].includes(event.code)) return;
if (mode === "demo") {
startGame();
if (KEY_DIRS[event.code]) queueDirection(KEY_DIRS[event.code]);
return;
}
if (mode === "over") {
if (["Space", "Enter", "NumpadEnter"].includes(event.code)) startGame();
return;
}
if (event.code === "KeyP") {
togglePause();
return;
}
if (mode === "playing" && KEY_DIRS[event.code]) {
queueDirection(KEY_DIRS[event.code]);
}
}, { passive: false });
overlay.addEventListener("pointerdown", () => {
if (mode === "demo" || mode === "over") startGame();
else if (mode === "paused") togglePause();
});
window.addEventListener("resize", resizeCanvas);
resetWorld(true);
resizeCanvas();
draw(performance.now());
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Reasoning trace (18,258 tokens)
I need output one HTML block complete single file polished snake vanilla canvas. Should reason implementation robust quick successive presses both honored no reversal relative latest queued? Should self-playing demo mode behind Click or press key start, meaningful first seconds. Requirements games start self-playing demo AI behind message. Should have visual polished, 20x20 centered, smooth movement, eyes direction particles. Score best session variable (not storage). P pause, Space/Enter restart game over. Click or press key to start overlay in demo. Should have canvas adaptable 1280x800 to 375 no horiz. Zero errors. No alert etc.
I should create elegant layout perhaps dark sleek premium neon glass, top header, score cards, canvas square within responsive. Should have everything visible initial. Demo runs autopilot, controls user can start via any key/click. Overlay "Click or press a key to start", likely center panel but allow game visible. AI self-playing enough not die quickly and demonstrates food particle. Should food spawn, score demo? Real scores upon user. Best session updates on gameplay (perhaps demo? Usually no). Make sure game demo continues without actual user starts, AI pathfinding avoids traps reasonably. I can implement robust AI Hamiltonian path? Grid 22x22 or 24x24. Autoplay via BFS to food avoiding snake then fallback safe moves, with self collision avoidance. Demo can die then auto reset after delay; overlay remains start message and tag demo mode. If AI uses direct BFS might trap eventually but demo resets. Initial board curated. Must make interesting visible quickly, food near path, particles in first secs. AI snake movement tick ~110ms. Board 22x22. Smooth interpolation between previous and current segments. Should render correctly around wrap? no wrap. Grid cells. Should snake path interpolation. Food pulse, glow. Particle burst on food. Start screen transparent dim enough. Demo score visible perhaps "DEMO" and scoring? Best session 0 initially. Could display "AUTOPILOT DEMO" in status. Overlay panel center not huge. "SNAKE" title perhaps header with logo. "Click or press a key to start" prominent bottom chip? Requirement behind a "Click or press a key to start" message. Overlay centered canvas with card and controls. Shouldn't obscure too much. Could initial demo overlay at bottom? "behind a ... message" implies message over. I can have center start card translucent with snake visible around. Canvas 560x560 at desktop, panel small.
Should satisfy quick successive pressesboth honored but cannot reverse. Input queue. Classic: each simulation step shift one direction, validate against last queued direction or current if no queued. Suppose moving right, quick up then left before tick: both accepted sequentially, next tick up, following tick left, no reverse. On keydown if queue length max? Quick successive both honored. Use queue up to 3 or 4; if more ignore. I should compare candidate with last queued/current and disallow opposites and duplicates? Duplicates perhaps can ignore to avoid queue clutter. Both quick successive turns accepted. I should not apply immediately. At tick direction = queue.shift(); because validated against previous queued logical direction. Self-collision: when moving into tail that's going to move allowed if not growing. Compute newHead, willEat, bodyToCheck excludes tail if !willEat. Wall. Then previousSnake = snake.map copy; unshift head, if food grow don't pop, else pop. Score +10; spawn food never on snake using empty cells list; particles at food center perhaps old food location. Best update score immediately. Game over overlay shows score, restart Space/Enter. P pauses only during playing; perhaps pause overlay. Keys event listener. Prevent default arrows/space to avoid page scroll. Start on any key? "Click or press a key to start": if demo state and keydown any key, start then process game controls perhaps arrows. I can have startGame() and if direction key apply to initial snake; but if Space starts. Game over only Space/Enter restarts per req; click perhaps also restart button? Good. P toggles pause. Demo AI must not trigger start via internally calling key handler. Render loop.
State values: mode = 'demo' | 'playing' | 'paused' | 'over'. User click canvas or document? Add pointerdown on start overlay or canvas; if demo startGame. Since iframe receives key after click, clicking overlay pointerdown calls start. Click anywhere start button. P pause. If paused Space/Enter? requirement Space or Enter restarts game over only, perhaps Space pause? no. P resume. Click pause overlay resume perhaps okay. On game over clicking restart button start. Avoid global click auto restarting accidentally from same event? overlay replaced dynamic DOM or one overlay. Simpler DOM overlay with card, eyebrow, title, text, key hint. updateOverlay changes based mode. Canvas draw not text overlays except perhaps pause? DOM card.
Could structure:body gradient, subtle grid background via CSS radial/linear.
main.app max-width 1100 display flex column.
header brand top left icon CSS snake dots and title SNAKE / "NEON GARDEN" perhaps polished but avoid gimmick excessive.
score top right panel scores.
Game shell center with canvas and side panel? Requirement centered grid. At 1280x800 can have game card centered with top status and bottom controls. Canvas 22*24 =528 plus pad. Header 70, game 600 = total 760 fits. I can set board-wrap width min(72vh, 620px, calc(100vw - 32px)), aspect-ratio 1. At 1280x800 height: header 70 + main margins + 620+foot = perhaps 790. Better canvas 540 based min(66vh, 580px). Use .board {width:min(62vh, 560px, calc(100vw - 32px));} At 800 =>496. 22 cells 22.5 px, nice. Header 88, board plus header/footer 100 =684. Good. But visual at 1280 game perhaps 560. Use --board-size:min(64vh, 580px, calc(100vw - 32px)); 512 at 800. Main container with scorebar above same width. Side absolute decorative info? Could use layout .game-layout display grid columns 220px board 220px perhaps board centered page. Requirement centered page grid exact centered if symmetric sidebars. Could have scoreboard top to avoid. Center board.
I can have header max-width 960, brand centered? Title and stats top. Board centered. Score panel above board 2 cards horizontally, current score best score, speed/status. Board canvas itself. At 1280 board 536. App height. Nice.
Perhaps use top header .topbar width min(920px,100%), brand left and stats right; board centered at page (canvas center). Then top stats not symmetric but board centered. Below control hints. A right floating panel "HOW TO PLAY" etc could distract. Simplicity polished.
Canvas high DPI: canvas CSS width 100%, height auto aspect ratio. backing resolution set based rect * dpr; draw coordinates in CSS pixels via ctx.setTransform(dpr,0...). Should ResizeObserver (allowed) or resize event. Canvas width fixed internal logical perhaps 660 and scaleCSS? For crisp use resize. Let logical S = canvas.clientWidth, cell=S/COLS; no borders? Canvas within padded shell? canvas itself includes background and grid. board-frame padding 10, actual canvas width fills. aspect ratio. draw rounded board inside? canvas full square. CSS border radius 22, background. The grid exactly 22x22. Use coordinates.
Smooth movement: fixed timestep and interpolate positions. Keep prevSnake. On each tick set prevSnake before update. render alpha=(now-lastStep)/STEP, cap 0..1. For each segment i current snake[i], previous position prevSnake[Math.min(i, prevSnake.length-1)] perhaps interpolation issue growth: when grow length increases, head from prev head to new head, body each from prevSnake[i] to snake[i], new tail at prev tail stays. Correct. At moment movement, snake array current new head + previous positions sans tail. prevSnake old. Each segment current snake[i], previous prevSnake[i] if exists else prevSnake[last]. At alpha=0, renders old arrangement. At alpha=1 current. Good. On reset prev=snake copy. Demo auto resets perhaps render.
Draw snake as continuous rounded tube for polish. Segment circles/rounded rect interpolated with gradient. Should distinct head and eyes direction. Approach draw body from tail to head using rounded rectangles per segment, connected with thick line through interpolated centers. Use ctx.lineJoin/lineCap round and stroke path through positions with width cell*0.68, gradient from head color to tail? createLinearGradient overall board. But self overlap? Fine. Draw black glow via shadow. Stroke path in order tail->head to connect; positions centers. Body as path line through centers. At corners during interpolation, individual segment interpolation can cut corner smoothly but connecting gives smooth. Add inner stroke thinner with gradient, perhaps scales. Should avoid line connection between weird order at initial movement? snake current order head to tail, positions array head->tail; draw reverse to head. Good.
Gradient head #b7ff6a to tail #37d996 perhaps. Outer shadow with rgba. Stroke underlay darker width .78 cell, inner width .62. Add scales / segment dots? Could draw small circles at body segment centers with alpha low; not necessary. Draw head separately rounded circle or rounded rect oriented direction. Distinct head larger than body, with rounded rectusing transform rotate based direction. Since current interpolated head center and direction current. Head angle = atan2(dir.y,dir.x). draw save translate rotate. Head shape perhaps capsule oriented x with roundRect(-.48c,-.43c,.96c,.86c,.28c); nose extends to .52. Eye positions in local coordinates at x=.17c,y=±.22c (if facing right); white eyes and pupils shifted +.04x. Since rotate handles. Draw head fill gradient? local linear grad. Add eyes 0.13c white dark pupils. Tongue occasionally? Could add tiny tongue at food proximity? Not needed.
Body connecting line extends into head; draw body first then head. Distinct head. Head previous direction interpolation? Just current direction snaps at tick start while head slides, acceptable. Smooth rotation perhaps direction angle lerp shortest for turns. Could store prevDirection, headAngle interpolated. Not necessary but visual polish. Use current dir; at tick after turn, snake head rotates at beginning movement. Fine.
Food: draw apple/berry orb at grid cell center, pulse sin now. Outer glow with radial gradient. Use leaf green two ellipses, highlight. It shouldn't spawn on snake. Food initial perhaps {x:15,y:11} snake at x 7 etc.
Particles: array objects x,y,vx,vy,life,maxLife,color,size in cell coordinate or pixels? On resize tricky. Use grid coords, update dt seconds. On eat, position at food.x+.5 etc, velocities in cells/s 2-5. life .45-.85. draw circle at x*cell, etc. Add perhaps score float +10 as particles type text? Requirement small particle burst; implement 18 sparks. Floating +10 improves. Keep floaters array with grid positions life. Update in animation frame even paused? freeze if paused perhaps render now. Particle update dt if mode not paused. Demo continues. No issues.
Grid: dark navy #081a1b / #0b2322. Draw checker alternating very subtle, lines. Border inner. Canvas background gradient via createLinearGradient each draw; could cache? 60fps small fine. Use background radial gradient perhaps. Add vignette. Food and snake glow. Canvas CSS box shadow.
Input:
const DIRS = {ArrowUp:{x:0,y:-1}, KeyW...}; use event.codeor key. keydown:
if (["Arrow...", "Space"].includes(e.code)) e.preventDefault().
if mode demo:
if e.code perhaps any key incl modifiers? avoid start if meta? Start on any key except perhaps Shift? Requirement press a key. startGame(); if dir enqueueDirection; return. But pressing P in demo would start and pause? I can start then if code P toggle pause, perhaps okay.
if over:
if Space/Enter startGame; else directions perhaps should not start per spec only Space Enter restart. Show "Press Space or Enter". Return.
if playing:
P pause -> mode paused, update overlay. directions queue.
if paused:
P resumes; perhaps Escape resume? no; directions ignore. Space/Enter no effect.
Potential repeated keydown: if e.repeat return for directions? Quick repeated same not needed. For P repeat avoid toggling: if e.repeat return global? Should directional held repeat not needed. Use if(e.repeat && (P/Space/Enter)) return. Queue duplicate candidate same as last ignore. Queue max 3. "Quick successive key presses must both be honored" up to 3; perhaps max 4. I can do 4. User could press up,left,down in <step: initial right -> up -> left -> down accepted (down relative left not reverse) will execute across ticks, could collide if short but that's game. Compare no reverse to last queued direction. At tick direction shifts; if next direction now opposite actual current? By validation chain no. Example current right queue up,left; after up current up; left okay. Good.
Start snake initial moving right at row 11 with segments [{x:6,y:11},{5,11},{4,11},{3,11},{2,11}] perhaps length 5. At initial demo, overlay center obscures snake if centered. Put snake horizontally around y=16 perhaps food y=16? Overlay centered y around mid, snake at row 15 near bottom visible. But AI moves around. Use snake initial at center row 11 typical. Overlay panel width 330 height 180 covers center, snake behind. Still background AI visible around. I could make start message compact bottom center within board (not blocking center action): .overlay align-items:flex-end; padding-bottom:28px; background linear gradient transparent etc. Card small "Click or press a key to start" pluskeys. Requirement overlay game over showing score; game over center. Demo start card perhaps positioned bottom 22 with translucent background and no full dim? Canvas overlay pointer events all. The snake at row 8 could visible. To ensure first seconds meaningful, use .overlay.demo {background:linear-gradient(...); align-items:flex-end;} card margin-bottom. Text "AUTOPILOT DEMO" top status perhaps. Center snake and food visible. Good.
Game layout and overlay: .board-frame position relative; canvas display block width 100%; .overlay absolute inset 0 display flex center; .overlay.demo justify? CSS classes. Demo overlay with pointer events auto cursor pointer; card; on click start. Pause overlay can pointer-events:none except resume hint. Game over click overlay restart? Can support.
Start card: pill "● AUTOPILOT DEMO" then h2 "Ready to slither?" p "Click or press a key to start" and key icons? This is more text than needed, board width 512 card perhaps 340. At bottom card 130 height. Perhaps enough.
Scoreboard:
<header class=topbar>
<div brand> logo svg? Everything inline, can use inline SVG but task single canvas doesn't prohibit other svg. Better CSS mark composed 4 rounded dots.
<div title><h1>SNAKE<span>· NEON</span></h1><p>Arcade classic, refined.</p></div>
<div class=stats>
<div stat><span>SCORE</span><strong id=score>000</strong></div>
divider
<div stat><span>BEST</span><strong id=best>000</strong></div>
</div>
</header>
At mobile stack perhaps topbar scores same row, title small. Board top .game-meta above with status dot + "Demo running" and speed chip "22 × 22" perhaps. Could place between header and board .stage width board. Status bar display "AUTOPILOT" and controls pause hint. bottom .controls "← ↑ ↓ → / WASD to move • P pause • Space restart". Use kbd. At 375 wrap. Should not horizontal scroll.
Could have decorative background floating glows. Avoid external fonts, use system (Inter fallback) perhaps no network. Use font-family ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI". Numeric use font-variant-numeric tabular-nums. Colors dark teal lime.
Timingand loop:
const STEP=115 perhaps demo 105 and play speed gradually with score? Requirements not ask speed. Keep base 120, speed up every 50 points min 75. stepDuration() mode demo 110 else Math.max(72, 118 - Math.floor(score/50)*6). lastStep. requestAnimationFrame loop(now):
dt = Math.min((now-lastFrame)/1000,.05); lastFrame=now
if (mode !== 'paused' && mode !== 'over') {
accumulator? let elapsed=now-lastTick; while elapsed>=step ms max perhaps 3 step. smoother and background not huge. Use if now-lastTick >= currentStep: lastTick=now (or +=). One step per frame enough. When paused lastTick resets on resume.
updateParticles(dt)
} else particles perhaps update on game over (particle visual should continue) perhaps particles update always except paused. During over particles decay okay.
render(now, alpha)
request next.
At first load initDemo and lastTick=performance.now, loop. Overlay visible first paint? DOM/CSS yes canvas initial rendered on first RAF before screenshot. Could call render(performance.now(),0) after resize. resize sets canvas dims and render. Use ResizeObserver on canvas? Changing canvas width in observer can cause loops? Observe board frame or window resize. function resize uses rect=canvas.getBoundingClientRect and dpr. canvas CSS aspect-ratio ensures. canvas.width=Math.round(rect.width*dpr), height likewise. render uses viewSize=rect.width. setTransform. If called by ResizeObserver after first render fine. No zero errors if ResizeObserver exists modern Chrome. Just window addEventListener resize and call initially after DOM loaded script at end. CSS canvas width 100%; aspect-ratio:1; HTML width=640 height=640 attributes. getBoundingClientRect. max devicePixelRatio 2 to avoid huge.
State and overlay updates:
const overlay=document.getElementById('overlay'), etc.
function setMode(next) { mode=next; updateOverlay; updateHud;}
function updateOverlay:
if demo:
overlay.className='overlay demo'; innerHTML static from HTML? Better overlay contains elements:
<div class="overlay-card">
<span id=overlayKicker ...>
<h2 id=overlayTitle>
<p id=overlayText>
<div id=overlayKeys>...
</div>
Set hidden via class .is-hidden. Use textContent only, nosanitize issues.
Demo: kicker "AUTOPILOT • DEMO"; title "Watch. Then take over."; text "Click or press a key to start"; extra perhaps `<div class=key-row>` static but dynamic label. Could have button? button "Start game" clickable within overlay. Requirement message exact. Use card with a .play-icon and text. Since pointerdown anywhere overlay.
Playing: overlay.hidden=true. CSS [hidden]{display:none!important}
Paused: kicker "TAKE A BREATH"; title "Paused"; text "Press P to resume"; overlay class pause.
Over: kicker "RUN COMPLETE"; title "Game over"; text perhaps `Score ${score} · Best ${best}` and score big. Requirement overlay showing score. Add .final-score area only game over. Could set title "Game over" and text `You scored ${score} points`; .overlay-score strong. Better dynamic dedicated `<div id=finalScore class=final-score><strong>120</strong><span>POINTS</span></div>`. In demo hidden. Keys "SPACE / ENTER TO RESTART".
Start demo text exact.
Status:
statusPill label and dot. In demo "DEMO · AUTOPILOT" cyan/lime; playing "LIVE · 22 × 22" perhaps status "SNAKE IN MOTION"; paused "PAUSED"; over "GAME OVER". Right in meta perhaps "Eat +10 · Grow +1". At mobile hide detail.
score update padStart(3,'0') but if >999 still fine. best initial 0. Session best update on eating in playing. If demo score shown? Score HUD should current score. In demo perhaps reset demoScore separately and HUD displays "—" or score current demo? Display score = demoScore but best unaffected. Requirement display current score; demo scoring can show. On user start resets. Use score variable for both demo and playing; best update only if mode playing. HUD score demo increments (fun). Over best updates. Demo game over should auto reset after 800 ms and no overlay game over (since must remain self playing). In endGame if demo set mode remains demo? set demoResetAt=now+900; freeze snake and flash? Then resetDemo. Simpler tick() if collision and mode==='demo' => initDemo() immediately perhaps jarring every so often. Use demoResetTimer counts via setTimeout; avoid stale timers when user start. In collision: ifdemo { crashFlash=1; resetSnakeDemo after 500 via setTimeout if mode==='demo' }. But game loop continues calling tick repeatedly unless mode = 'demoOver'. Could support mode 'demoOver', overlay demo remains but render dead; after timeout initDemo. More complexity. Better endDemo immediately with particles burst red, then reset state preserving overlay, but next frame new snake. Fine. AI should rarely die quickly. Could just resetDemo(false) immediate. No console.
AI:
Should self-playing demo behind message and food eaten. Use algorithm safe direction with BFS shortest path. I control input queue perhaps each tick demo determines dir. Current snake positions. For each candidate not reverse, evaluate:
- inside
- collision with body (tail exclusion if not eating)
- avoid traps via flood fill free space >= snake.length perhaps.
Use BFS path to food avoiding snake body excluding tail (tail will move), get first dir. Then test safe with area. If shortest path leads area too small, choose candidate maximizing flood area and distance to food. This is enough. But AI may reverse? filter. Deterministic and some randomness initial? Should have screenshot 3 sec action. Place initial food reachable within ~8 ticks. Initial snake from x2..6 row 12, dir right, food at 13,12 =7 steps ~0.8sec, eat. New food random could be anywhere. First particle visible at <1 sec and perhaps another by 3. Good. Overlay bottom may cover row 12? Center board and bottom card at bottom 18 height 110 covers rows 17+, snake row 11 visible. Great.
BFS implementation:
function demoDirection() {
const head=snake[0];
const blocked=new Set(snake.slice(0,-1).map(key)); // exclude tail
blocked perhaps not exclude tail if food growth? For path planning okay.
BFS queue [{x:head.x,y:head.y,first:null}], visited head. For neighbors directions ordered perhaps [current, ...]. Can't reverse first step. Should store first.
For each neighbor inside and not blocked (except food), first = starting direction; skip opposite. If reaches food, reconstruct first only but BFS first path can be returned first upon discovery food. BFS with queue and visited enough.
Path ignoring future movement could still collide, but next ticks recalc. On return candidate dir, simulate next and flood check; if unsafe fallback.
Safety function candidate:
next = head+d, if wall return -1
willEat = next equals food
bodyLimit = snake.length - (willEat?0:1)
collision for i< bodyLimit? Body array current head..tail; new head can't be current head because no reverse etc. check i=1..<bodyLimit perhaps including current head impossible except reversals. Set obstacles snake.slice(0, snake.length-(willEat?0:1)). If newHead equals tail and not eat allowed.
After move construct nextSnake=[next,...snake.slice(0, snake.length-(willEat?0:1))]? If grow length+1 uses full old snake, else length-1 old? For flood fill area just blocked future = [next,...snake.slice(0, snake.length-(willEat?0:1))] perhaps if no eat and next was old tail included duplicate? Example next=tail, future snake newHead tail + old snake excluding tail good.
flood count from next with blocked future excluding next, count available cells. Should have >= Math.min(snake.length+? , free). Strict area>=snake.length ensures not trapped, but all candidates early game area huge. BFS path candidate safe.
Fallback candidate score = area*10 - manhattan distance + (straight bias) perhaps. Could choose random among safe to look organic, but focus food. Use BFS if safe, else safe max area. If none choose any nonreverse collision even if death. Could compute BFS return first without path safety and simulate. Good.
At tick demo before movement if queue empty queue.push(demoDirection()) then usual shift. Ensure initDemo dir right and no input.
Food spawning:
function spawnFood() {
const occupied = new Set(snake.map(...));
const empties=[]; for cells if not occupied push
if no empties: game win? Snake fills board, mode over perhaps title "Board cleared!" but not needed. Could handle food=null and endGame(true). Requirement food never spawns on snake satisfied.
food=empties[Math.floor(Math.random()*empties.length)]
}
Random is okay. Demo initial score resets to 0. New food always no snake.
Collision/end:
function step():
if mode not demo/playing return
prevSnake = snake.map copy
if mode demo queue = [choose]
if inputQueue.length direction=inputQueue.shift()
const head...
if collision:
if demo { burst at headred? initDemo perhaps; return;}
mode='over'; crash particles; play? no audio requirement. update overlay/hud; render
return
snake.unshift(next)
const ate=food && equals
if !ate snake.pop()
else score+=10; if playing best=Math.max; makeParticles(food); spawnFood(); updateHUD()
last direction etc.
Should if food null.
InputQueue stale after demo? reset on start.
Game over overlay immediately at collision while prevSnake old and snake current unchanged. To show impact, set deadHead perhaps next and render head? Could leave snake before wall; burst at clamped impact. Set shake=1, canvas frame class bump and remove after animation? Could add CSS .board-frame.shake animation. Use class remove/reflow? not necessary. Draw red glow at next head perhaps. Particles burst at clamped grid coords. Game over overlay covers anyway. Distinct.
Pause:
function togglePause:
if playing mode='paused', overlay, lastTick; if paused mode='playing', lastTick=performance.now(), prevSnake=snake.map copy (avoid interpolating stale) and render.
Input queue retained during pause okay. Could allow direction while paused? ignore.
Game over score best perhaps best already update on eat. if score>best at game over set.
Session best no localStorage.
Demo to user:
startGame() resets snake score etc, mode playing, overlay hidden. lastTick=performance.now. Use initial snake length 5. Score 0. best persists. food at perhaps fixed? For fairness random? use initial food {x:13,y:11}; new games can random after init. Set initial food to (14,11) each game? Fine. Requirement new food not on snake. Food initial known not on snake. Could random spawn for variation but first eat perhaps after a while user anyway. Demo needs visible first seconds. initWorld(isDemo):
snake = [{x:6,y:11},{5,11},{4,11},{3,11},{2,11}]
dir={1,0}; prevDir...
food = demo ? {x:13,y:11} : randomFood? {x:14,y:11}. Keep same.
particles=[] floaters=[]
Score=0.
At demo resets after collision, score zero. status label remains. If start click after demo score e.g. 20, resets 0.
AI queue: in tick ifdemo set direction directly chosen not inputQueue to avoid any queued (none). dir = chooseDemoDirection(); else if queue shift.
Smooth motion alpha: lastTick set at step, alpha = (now-lastTick)/stepMs. If mode paused/over render alpha=1? At pause during mid-step if last render alpha e.g .5 then snake is in intermediate pos. When pause should freeze intermediate rather than snap? Could maintain pausedAlpha variable. Simpler renderAlpha=mode paused ? Math.min(1,(now-lastTick)/stepMs) would continue to 1 while paused, not strictly freeze but snake settles; game logic paused. Better pauseAlpha = current alpha captured; use variable frozenAlpha. In togglePause set frozenAlpha=getAlpha(performance.now()); render uses frozenAlpha. On game over set alpha=1? Snake unchanged but prevSnake from before attempted tick, current snake same as prev? At step I set prevSnake=snake before attempting, so current==prev -> no issue. Pause current snake after last tick with prev old. frozenAlpha.
Demo overlay no impact.
Rendering positions:
function interpolatedSnake(alpha) returns list:
for i=0; i<snake.length; i++:
a=prevSnake[Math.min(i,prevSnake.length-1)] || snake[i]
b=snake[i]
x=(a.x+(b.x-a.x)*ease(alpha)+.5)*cell
Should use smoothing easing? Linear interpolation matches constant speed, eyes. Could use alpha directly; smoothstep causes subtle ease each cell (stutter). linear good.
At turns body segments each move along axis. Rounded connectors line path may cut diagonal between body centers at mid transition, organic.
drawBoard:
ctx.clearRect; background fill;
cell = size/COLS
for y,x fill if (x+y)%2 with rgba
grid lines stroke rgba(151,255,195,.04), lineWidth=1, positions crisp not important.
Could add random decorative dots? no.
Draw faint radial around food with gradient.
drawFood before snake.
particles behind/above snake? burst above.
drawSnake.
floating text.
Vignette fill radial gradient.
Perhaps border inside with roundRect.
Canvas CSS background no need.
Canvas sizing: const rect = canvas.getBoundingClientRect(); size=rect.width; canvas.height etc; board frame ratio. Should have dpr and ctx setTransform in render each time to ensure after resize. `ctx.setTransform(dpr,0,0,dpr,0,0)` and draw in size CSS. canvas CSS height auto attributes perhaps aspect ratio. Set `canvas {width:100%; height:auto; aspect-ratio:1;}` actual height from attributes consistent. In .board-frame width var. On mobile viewport height could be 667 and board width 343, header 70, scoreboard etc total <640. At 375 no horiz. body min-height:100svh, overflow-x:hidden. topbar padding 20px 16px 12. .stage width: min(64vh, 580px, calc(100vw - 28px)); At mobile height 667 64vh=427 vs width347 so 347. board frame padding 8 -> canvas 331, grid 15px okay. Stats topbar may squeeze brand+stats. Brand h1 23, subtitle hidden mobile; stats each 54. topbar width 100%, max 740. Use .topbar width:min(760px, calc(100% - 32px)); margin auto. Board size min(65vh,580px,calc(100vw - 32px)). At desktop 520; topbar width 680. Meta stage width same. On mobile topbar .brand-text p display none; h1 20; stats padding 8; gap 12. Board frame border radius 20.
Footer controls max width 680, centered, font 12. At 375, kbd groups wrap. Height fits.
Perhaps score above board instead of topbar stats? Topbar has scores current best. Session best visible. Current demo score changes.
A small `NEW BEST` badge appears if current equals best >0? Could include #bestLabel. Not necessary.
Accessibility: canvas role img aria-label, overlay button semantics? `<div class=overlay ... role=button tabindex=0>` but keyboard global. Could have actual `<button id=startButton>` style within card, but clicking button event bubbles. Buttons need no default focus issues. I can have overlay `<div>` and card with `<span class=mouse-icon>`. No requirement accessibility. Add `aria-live=polite` score? Could be too chatty demo. no.
No external network. CSS no fonts import. JS in script.
Make sure output exactly code block. I should write complete polished HTML ~800 lines? Respond concise. Aim 500 lines enough.
I'll deeply examine key handling quicksuccessive:
let direction = {x:1,y:0}; let inputQueue=[];
function queueDirection(nd) {
const last = inputQueue.length ? inputQueue[inputQueue.length-1] : direction;
if ((nd.x===-last.x&&nd.y===-last.y)||(nd.x===last.x&&nd.y===last.y)) return;
if (inputQueue.length<4) inputQueue.push(nd);
}
Same key duplicates ignored. Quick up,left both queued. At tick shift up; direction up. Next tick left. Correct. If queue full quick press not honored but reasonable cap. Requirement "Quick successive key presses must both be honored" two always because cap4. Good.
Potential scenario new input after tick but during render queue uses direction updated. Fine.
Pause key P in demo: global starts then subsequent logic if code P toggles. The message says press key to start, so P starts paused might weird. Could on demo call start and if direction then queue; if P perhaps start and pause perhaps accepted. Space start. Enter start. Any key including Arrow will start and enqueue perhaps if arrow left opposite ignored. If user presses left initially moving right, start moving right (no reverse), correct. Best to start on any non modifier; `if (e.ctrlKey||e.metaKey||e.altKey) return;` avoids interfering browser. prevent default for recognized.
Order:
window.addEventListener('keydown', e=>{
const isControl = ...
if (isControl) e.preventDefault();
if (e.repeat && ['KeyP','Space','Enter'].includes(e.code)) return;
if (mode==='demo') { startGame(); if(keyDirections[e.code]) queueDirection(...); return; }
if (mode==='over') {if space enter startGame(); return;}
if (e.code==='KeyP') {togglePause();return;}
if(mode==='playing'&&keyDirections[e.code]) queueDirection...
});
If paused pressing arrow no start, okay.
Click:
overlay.addEventListener('pointerdown', e=>{
e.preventDefault();
if(mode==='demo'||mode==='over') startGame();
else if(mode==='paused') resume;
});
Overlay hidden playing so no click. This means game over click restarts though text Space Enter. fine. The overlay message in demo click start. Canvas itself underneath overlay, overlay full catches. Could add boardFrame click for paused? overlay catches.
`overlay` hidden true and pointer events no.
Should focusiframe keyboard after click: clicking calls start, then keyboard works automatically as window focused. Fine.
Self playing demo: `mode='demo'`; overlay visible; step executes. user key starts. AI chooses food. On demo collision `resetDemo()` sets snake etc and food initial. Could avoid same score best. UpdateHUD.
Status update only when mode transitions. Score update on eat. Use status element classes. `statusDot` CSS animation pulse; paused grey, over red.
`paceLabel` perhaps "CLASSIC PACE" static.
Game speed changes currentStep used to alpha. function stepMs() { return mode==='demo'?105:Math.max(76,118-Math.floor(score/50)*5);}. When score crosses and step duration decreases, alpha perhaps >1 clamped. okay.
lastTick initial performance.now. In loop use if(now-lastTick >= stepMs()) { lastTick=now; tick(); } One tick per frame. If tab resumes after long only one tick, no runaway. Good.
Particles update dt each frame even paused? define `if(mode!=='paused') updateParticles(dt);` Demo overlay no effect. On game over update. Floaters update.
On reset `lastFrame` not needed.
Particle coordinates and food event:
Before spawn store atePos={...food}; after snake update. emitBurst(atePos.x+.5, atePos.y+.5). Score floater at same x,y perhaps. On game over burst at clamp new head or old head color coral. `emitBurst(clamp(nx+.5,.2,COLS-.2),...)`.
Particles object {x,y,vx,vy,life:...,maxLife, size, color}; update: life-=dt; x+=vx*dt; y+=vy*dt; vx*=Math.pow(.08,dt) perhaps heavy damping; vy=vy*Math.pow(.2,dt)+1.8*dt; use 60fps independent. `p.vx *= Math.exp(-2*dt)` etc. Draw alpha=Math.max(life/maxLife). radial circle shadowBlur perhaps per particle expensive but 18 only. Use ctx.globalAlpha and fillStyle.
`floaters` {x,y,life:1,maxLife:1,text:'+10'} update y-=0.8dt. draw font 700 `${Math.max(12,c*.55)}px ...`; white lime; globalalpha. In demo +10 floats visible. Text may be hidden by overlay only if bottom.
Food pulse render now even paused pulse continues; pause visual background still animates but okay. Could use visualTime now/1000. Food leaf rotate gentle. Draw:
const pulse=1+Math.sin(now*.006)*.06; radius=c*.25*pulse;
glow radial centered r 0->c*.75 with rgba...
ctx.save translate; shadowColor '#ffdf6b'; shadowBlur=18; fill radial gradient from -r*.35,-r*.4 etc colors #fff4ad to #ffbe3c to #f46b45; circle.
Leaf fill #76ed89; ellipse(c*.12,-c*.27,c*.14,c*.07,-.6). Stem line. highlight small.
Food distinct from green snake (orange golden). Good.
Snake gradient from lime to turquoise. Food coral.
Body shadow outer. Body path if snake length1? starts at least 5. ctx.beginPath move points last then lineTo all reverse. `ctx.strokeStyle='rgba(3,11,14,.75)'; lineWidth=c*.76;` shadow? Then second outer stroke green width .68 shadowBlur 14, inner gradient width .56. If draw three strokes may look bold. Add scale highlights circles every other body segment radius .07c alpha .18. Head separate c*.90.
Head gradient `createLinearGradient(-c*.45,-c*.35,c*.45,c*.35)` after transforms gradient coordinates local current transform? Canvas gradients use current transform at creation? It's okay. roundRect supported latest Chrome. Use helper roundedRect path perhaps ctx.roundRect available. Latest Chrome yes. Could avoid dependency but allowed. Use ctx.beginPath(); ctx.roundRect. Fine.
Head position interpolation from prev head to current. Body path includes head center under head. Draw head shape after.
Eyes:
rotate angle from direction. `const eyeX=c*.14, eyeY=c*.20`;
fill white circles r=c*.105 at (c*.12, ±c*.20); pupils at (c*.16, ±c*.20) r=c*.052 dark. If facing left rotate π so okay; up rotate -π/2. Eyes facing direction by positioned towards front x positive and pupil shifted front. Additional small shine. `direction` may be queued not applied until tick, head faces actual.
Could show shorttongue occasionally if now cycles? unnecessary and can be distracting.
Head outline, small darker jaw line.
Draw tail rounded via lineCap round. distinct.
Background can include ambient circles CSS. Body `::before` fixed radial gradients.
A small badge inside meta status with green dot and text. Right `#foodHint` orange dot "FOOD +10" perhaps.
Overlay design:
HTML:
<div id="overlay" class="overlay demo">
<div class="overlay-card">
<div id=overlayKicker class=overlay-kicker><span></span>AUTOPILOT DEMO</div>
<h2 id=overlayTitle>Ready when you are.</h2>
<div id=finalScore ...>
<p id=overlayText>Click or press a key to start</p>
<div id=overlayHint class=overlay-hint><kbd>↑</kbd><kbd>↓</kbd>... </div>
</div>
</div>
In demo card bottom: kicker, h2 perhaps 22, text, hint "ARROWS / WASD TO MOVE". Board card height ~140. On 331 canvas, card width calc100%-36=295, height 130, bottom 16. Good. It may cover bottom ~40%, but snake starts center. AI after eating perhaps near bottom after 3 sec but still action partly visible. Could make overlay card background .86 and backdrop-filter blur; CSS backdrop supported. Overlay full background transparent for demo; paused/over rgba(3,10,14,.68) backdrop-filter blur(3px). Demo card box shadow.
`overlay.demo {align-items:flex-end; padding:18px; background:linear-gradient(to top, rgba(3,12,15,.36), transparent 48%);}`
`.overlay-card {width:min(340px,100%); ...}`
Demo h2 perhaps "You’re up next." exact. Text bright.
For over card final score display grid and score huge. `finalScore.hidden`.
Demo hint perhaps `Arrow keys / WASD · P to pause`.
Paused hint `P TO RESUME`.
Over hint `SPACE / ENTER TO RESTART`.
Use textContent.
Should updateOverlay demo on reset; no changes.
`setOverlay` set finalScore etc.
For hidden attribute finalScore and overlay CSS `[hidden]{display:none !important;}` global.
`overlayText` gameover `You scored ${score} point${score===1?'':'s'}` score increments 10 so "points". final score shows score, text "Space or Enter to restart" perhaps hint duplicates. Requirements overlay score yes.
Demo overlay title "Ready to take over?" p exact.
Paused no final score.
Game over collision if mode playing:
direction remains old, prevSnake clone old. `mode='over'; inputQueue=[]; updateHud(); updateOverlay();`
best score update just in case.
Should update status call in updateHud using mode.
Perhaps mode variable is demoOver? no.
Demo collision resets immediately. Add emit burst before reset gets cleared if reset clears particles so invisible. Could preserve particles in resetDemo? Not important. Better `resetWorld(true)` clears. AI robust enough rare.
AI implementation detail BFS path:
```
function findFoodDirection() {
const head=snake[0], tail=snake[snake.length-1];
const blocked=new Set(snake.slice(0,-1).map(key));
const q=[{x:head.x,y:head.y,first:null}];
const seen=new Set([key(head)]);
for(let qi=0; qi<q.length; qi++){
const node=q[qi];
for(const d of directionPriority(head?)){
const nx=..., first=node.first||d;
if(node===head? explicit first)...
if(node.first===null && isOpposite(d,direction)) continue;
if out/seen/blocked continue;
if(nx food) return first;
seen.add; q.push({...,first});
}
}
return null;
}
```
When neighbor is food I should mark not needed. blocked includes head and body except tail, neighbors cannot head unless reversal but filter. Food cannot be on snake. BFS returns first but path may require immediate reversal later? BFS has no direction state and can include paths that reverse from one step to next theoretically shortest simple path won't immediate reverse because seen prevents previous node. So okay.
Order directions based current and others rotated:
`const DEMO_DIRS=[{1,0},{0,1},{-1,0},{0,-1}]` sorted with current first, then others perhaps deterministic. To make movement natural, array [direction, left, right, opposite] filtered. Could create:
```
const turnChoices = [
direction,
{x:-direction.y,y:direction.x},
{x:direction.y,y:-direction.x},
{x:-direction.x,y:-direction.y}
];
```
BFS path neighbor order influences first path but BFS global order not strictly straightforward. Fine.
After find candidate, check safe. `safeCandidates = all nonreverse valid`. Candidate may be in safe. `evaluateMove(d)`:
```
const n...
if outside return null
const ate=...
const bodyCount = snake.length - (ate?0:1)
for(let i=0;i<bodyCount;i++) if snake[i] equals n return null;
const future=[n,...snake.slice(0,bodyCount)] // Wait if no eat bodyCount = len-1, future length 1+len-1=len correct. if eat bodyCount=len, future len+1 correct.
const blocked=new Set(future.slice(1).map(key)) // allow start n
// flood from n neighbors? If blocked includes n? future.slice1 might include n if collision tail allowed? if next=old tail and no eat, old tail excluded from slice, no duplicate. Good.
let count=0; stack=[n]; seen [n]; while... count++, for neighbors if !out !seen !blocked...
const dist=Math.abs(n-food)
const spaceNeeded=Math.min(COLS*ROWS - future.length, snake.length+4)
return {d,n,area:count, score:... , safe:count>=Math.min(snake.length+2,30?)}
```
Flood count includes n, free connected area. Require area >= snake.length+3 perhaps too strict near end but fallback max area. For early all good. BFS candidate safe if area >= Math.min(snake.length+2, COLS*ROWS-snake.length). If not, fallback safe candidates sorted area primary. But if candidate shortest safe I take.
Fallback sort `(b.area*4 - manhattan*0.4) - ...` area primary may cause AI wandering in open space instead of food when BFS unsafe, desirable survival. Straight bias .4.
If BFS null choose move with highest `area*2 - dist` etc.
If no valid moves return direction; tick collision resets.
`validCandidates` calls flood for max 3 each tick, 484 cells fine.
BFS candidate could collide next? find uses blocked current except tail so valid. evaluate assures.
When path candidate unsafe but safe candidates exist, choose fallback score `area*6 - distance*1.5 +(straight?1:0)`. This may ignore food if small area but all open. Fine.
Perhaps AI gets stuck circling never eats due area priority only if path deemed unsafe rare.
At initial snake length5, BFS dir right food 7 away. all safe area 479. Eat ~0.8sec.
After eating food random possibly in isolated enclosed? no walls internal so path.
Particle first eat within ~0.8 sec, at 3 sec food random. Demo score at least 10, often 20-30. Great.
Potential random food spawn far and game score. Fine.
Collision self rules tick:
`const bodyToCheck = snake.length - (ate?0:1); for (let i=0;i<bodyToCheck;i++)` includes head i0. Moving into current head impossible except zero direction no. For not eating tail excluded. Eating on tail impossible food never snake. Good.
Wall if newHead<0 etc.
When input direction queued after growth works.
Food full: after eating last cell, snake length N*N, spawnFood sees empties zero, set food=null, end game as victory. Could handle in `spawnFood` returns false and if playing mode over with special `won=true`. Requirements no need but robust. `spawnFood` called initialization? initial fixed. If empties.length===0: food=null; if playing endGame(true). But endGame function emits etc perhaps. Simpler in step after grow:
```
if(!spawnFood() && mode==='playing'){ finishGame(true); }
```
finishGame sets mode over and overlay title "You win!" based won variable. demo full reset.
Best score update before.
No likely full but code correct.
`spawnFood()` initial after? no.
End game score final.
`finishGame(won=false, impact)` if won no impact.
Variable didWin.
Overlay over text `Score ...` plus final. use didWin ? "Every cell conquered. Beautiful run." : "The garden got the better of you." But requirement overlay score shown. final.
Status best of session: best=0 persists start, demo does not update. If user score 0 best remains 0. Fine.
First paint: CSS overlay and canvas perhaps blank for a frame before JS but screenshot 3 sec. Zero errors.
No localStorage etc.
Resize: `resizeCanvas` obtains `const rect=canvas.getBoundingClientRect(); size=Math.max(1,rect.width); dpr=Math.min(window.devicePixelRatio||1,2); const w=Math.round(size*dpr); if(canvas.width!==w) ...`. Attributes initial 640 perhaps CSS height determined aspect ratio. Use `canvas.style` no.
Call `resizeCanvas()` after `resetDemo` perhaps render. Window resize event. Could use `ResizeObserver(resizeCanvas).observe(canvas);` but when canvas backing changes no CSS rect change so no loop. Add if typeof. Not needed latest Chrome but can use. Simpler window resize and request loop draws each frame with current `viewSize` initial. On responsive changes only viewport triggers. CSS layout due overlay no. Fine.
dpr changes moving screens window resize perhaps triggered.
`ctx.setTransform(pixelRatio,0,0,pixelRatio,0,0)`.
Draw CSS size and border: canvas actual drawing all square. .board-frame padding 8, background #0b1b1c; border. `overflow:hidden`? If frame overflow hidden and overlay border radius. canvas border-radius 14. board frame box shadow. `overlay border-radius:22`.
`board-frame` no horizontal overflow. width stage.
Perhaps page at 1280x800 board size 65vh=520, plus topbar height76, meta42, board536, controls50 and gaps ~18 =722. Good.
App:
body min-height 100vh display flex? `.app{min-height:100svh; display:flex; flex-direction:column; align-items:center; padding:18px 16px 14px; box-sizing:border-box;}`
topbar flex none width min(740,100%); margin-bottom 16.
stage width min(...). .board-frame.
controls margin-top 14.
At mobile height small body scroll vertically allowed, no horizontal. Canvas center horizontally. Requirement centered page yes horizontally; vertically perhaps game in central.
At desktop topbar scores width etc.
Background decorative `body::before` and `body::after` fixed pointer-events none, no external.
No horizontal scrollbar from glows? pseudo with transform might extend and body overflow-x hidden.
Title "SNAKE" and small "MIDNIGHT EDITION".
Brand logo perhaps inline `<div class=brand-mark><i...>`. Four squares snake pattern. CSS .brand-mark width 44 height44 border etc; ::before etc. Could use inline SVG to draw snake logo, but "using canvas" game yes. CSS simpler:
```
<div class=brand-mark aria-hidden=true>
<span></span><span></span><span></span><span></span><b></b>
</div>
```
position absolute. But visual not crucial. Use `<svg viewBox...>` inline shapes polished, no external. Output single file can contain SVG in HTML, task output html. Brand mark lime snake line using path with stroke and eyes. Fine.
`<svg width=40 height=40 ...><rect .../><path d...>` perhaps. CSS .brand-mark background etc. Inline svg no errors.
Perhaps avoid duplicate canvas requirement interpretation no issue.
Top stats card backgrounds. Current score label with tiny green dot? best amber. Score values `000`. Score on first paint demo starts 000 then 010.
Controls hints desktop:
`<div class=control-group><span class=key-pair><kbd>←</kbd><kbd>↑</kbd><kbd>↓</kbd><kbd>→</kbd></span><span>or</span><kbd>WASD</kbd><span>Move</span></div>`
Arrows 4 keys wide ~100 plus, at mobile wraps. Better show `<kbd>↑↓←→</kbd>` single key weird. Use group.
`<span class=control><kbd>P</kbd> Pause</span>`
`<span class=control><kbd>Space</kbd> Restart</span>`
Space only restart game over; control label "Restart" could imply anytime, but implement Space only over. Perhaps requirement only Space/Enter restart game over. Fine.
At demo any key start, once playing Space doesn't restart, label "Restart" without "after crash" potentially mismatch but not fatal. Could label `<kbd>Space</kbd> Restart after crash`. At mobile shorter via CSS.
Meta "EAT +10 · GROW +1" communicates requirement.
Current score and best score of session label "SESSION BEST" explicit.
No audio (not required) avoid browser issues.
Game overlay score final `finalScoreValue.textContent=score` no padding perhaps.
Best update UI highlight when new best? Add class `pop` score animation on eat:
```
scoreEl.classList.remove('bump'); void scoreEl.offsetWidth; scoreEl.classList.add('bump');
```
CSS keyframe. DOM reflow okay on eat. No console. Best same if changed. Could implement `bump(scoreEl)`.
Board shake on game over:
```
boardFrame.classList.remove('hit'); void boardFrame.offsetWidth; boardFrame.classList.add('hit');
setTimeout(()=>boardFrame.classList.remove('hit'),350);
```
No stale error. CSS animation. But overlay also shakes inside frame if frame class, okay. Not needed but polish. Add subtle.
`@media(prefers-reduced-motion:reduce)` disable CSS, but JS smooth movement still requirement. Don't disable snake.
One concern: DOM overlay `hidden` attribute, when unhide update `.hidden=false`. CSS `.overlay` display flex. fine.
`finalScore.hidden` proper.
Status class colors using data-mode perhaps `statusPill.dataset.state=mode`. CSS `[data-state=over]`.
UpdateHUD:
```
scoreEl.textContent=String(score).padStart(3,'0')
bestEl...
const labels =...
statusText.textContent=...
document.getElementById('modeLabel'). ...
```
Meta right fixed.
Demo after start status text "LIVE RUN". paused etc.
Overlay hint perhaps update with `innerHTML`? avoid dynamic, textContent "ARROWS / WASD · P PAUSE". Use kbd styling unavailable if text only. Could have `<div id=overlayHint class=overlay-hint>` and text. Fine.
Should explicit Space or Enter restarts game over. Text and keydown correct.
P paused while game over no effect. P paused while demo currently start game and return no pause if code handling order. I can if demo `startGame(); if(dir) queue; else if code KeyP togglePause(); return;` pressing P starts paused, but message "any key" technically. Better just start playing on any key and only enqueue direction. P doesn't pause immediately. Fine.
If over Space/Enter start. Direction keys do not start. Good.
`Enter` code is "Enter", numpad "NumpadEnter" optional support. Requirement Enter include NumpadEnter. Add.
Space `e.code==='Space'`.
Demo pressing Enter starts. over NumpadEnter.
Prevent default Space arrows. P no default.
Click/press key start message is p. Good.
Particle burst when eating regardless demo. Update score float. Use random colors gold orange lime.
Could draw slight rings on eat: particles includes ring array? Not required but nice. Add `ripples` array {x,y,life,maxLife}; draw ring expanding in particles update. Could integrate particle type ring but unnecessary. Small burst 18 circles + floater enough.
Perhaps implement ripples for visual polish ~3 arrays? Not needed.
Background grid of at least20x20: 22 x22. Clearly visible. JS const GRID=22. Canvas central.
Food spawn no snake: empties. Initial fixed not snake. After each eaten uses snake updated. Good.
Self playing demo behind message and meaningful first seconds. Yes.
Check AI `chooseDemoDirection` uses food perhaps null. If null return current direction.
`findPath` BFS visited:
```
const start={x:head.x,y:head.y,first:null}
while...
const isStart=node.first===null;
for d ...
if (isStart&&opposite) continue;
const first=node.first||d;
...
if food return first
```
Direction object references constant safe (not mutated). dir always assigned object no mutation.
`directionPriority` includes opposite but BFS deeper uses all dirs including opposite; seen prevents immediate backtracking. Fine.
Evaluation `future.slice(1)` etc.
Flood `blocked` excludes start but contains body; neighbors check. It can expand into head's old tile depending future includes old head yes body future includes old head at index1, blocked. Good.
Count area includes start (new head), expected.
Candidate safety:
```
const needed=Math.min(snake.length+2, GRID*GRID - snake.length);
if (path && pathEval.area >= needed) return path;
```
At initial needed7, area huge. At near full needed total free perhaps small. okay.
Fallback sorts area first *8. Manhattan if food.
No random AI turns besides food. Looks intentional.
Game over collision:
At beginning tick `prevSnake=copy(snake)`, choose dir. If collision direction has changed but snake positions unchanged; render head rotates into collision because direction assigned before check. Could only assign after collision? If queued direction leads self collision, game over and head facing attempted direction is okay. prevSnake current so interpolation static. impact burst at outside may draw clipped, okay.
Call `finishGame(false,newHead)`.
`finishGame` best update, inputQueue=[], mode over, emitBurst using clamped. update overlay/hud. board hit CSS. `lastTick=performance.now`.
Demo collision `resetWorld(true)` sets overlay update (demo) and HUD. No flash, fine.
At game over, particles update and render but overlay background blur/dim may hide, still polished.
Start game from overlay pointer event and subsequent pointerup perhaps no effects. `e.preventDefault` avoids focus? Clicking overlay with preventDefault pointerdown may prevent iframe focus? In sandbox, pointerdown default focusing document perhaps still click focuses. Don't preventDefault? To ensure keyboard events after click, not preventing might be better. `overlay.addEventListener('pointerdown', ()=>{...})` no prevent. Button not actual. Browser focuses body on click. Fine. Could call `canvas.focus()` with tabindex=0, but keyboard listener window and iframe receives. Set canvas tabindex=0 and on pointerdown `canvas.focus({preventScroll:true})` but overlay hides after start, focus remains. `focus` options supported. However clicking overlay and hidden during handler then canvas focus can ensure. Add `canvas.focus({preventScroll:true});` inside startGame called by keyboard too, no issue? Focus on keydown perhaps. startGame always calls canvas.focus? At page load demo not start so no auto focus. On user start yes. For click restart yes. Use try? no need.
Canvas `tabindex="0"` outline none but accessibility focus visible perhaps board frame focus-within border. Fine.
Click start event window gets key after.
Perhaps keyboard global starts demo when user presses Tab to navigate; okay any key. `if(e.ctrlKey... ) return`, Tab starts. Fine.
Best score of session in variable not persistent due no localStorage requirement. correct.
I should decide whether include a visible "Demo" overlay and live score demo affects best? Not. The user might see score 010 and best 000 which seems odd but demo label explains. On start score resets and best remains 000. Session best logically should perhaps include demo? Usually best score of actual player. Label "SESSION BEST" could be 000 while demo score 010. Accept. Could avoid demo score scoreboard weird by display score but best update in demo too? Requirement best score of session could include all runs including demo, but then AI might set best and player compares. Better best only player. Add meta or small `best` label "YOUR BEST" perhaps. "SESSION BEST" expected. On demo HUD current score is demo's, best 000; not a functional issue.
Alternatively maintain demoScore separately and HUD display 0 during demo, while AI eats score variable but hidden? Current score of demo not important. Display 000 until user starts reduces confusion, but score doesn't increase during self play perhaps less interesting. Keep demo score.
Potential overlay hides food at initial? Food at x13,y11 center; overlay bottom only. Snake y11 center. Great.
At initial tick AI direct horizontal, food burst center visible.
Page first 3 sec demo may move elsewhere random food but enough.
I'll approximate code length. Should have no explanatory text in response. I'll deliver one code block.
Before responding, perhaps test mentally JS syntax and DOM IDs. Plan code exact.
HTML skeleton:
```
<!DOCTYPE html>
<html lang=en>
<head>...
<style>
:root...
*...
[hidden]...
...
</style>
</head>
<body>
<div class=ambient ...> perhaps body pseudo enough
<main class=app>
<header class=topbar>...
</header>
<section class=stage aria-label="Snake game">
<div class=game-meta>...
<div id=boardFrame class=board-frame>
<canvas id=gameCanvas width=640 height=640 tabindex=0 aria-label=...></canvas>
<div id=overlay class="overlay demo">
...
</div>
</div>
</section>
<footer class=controls>...
</footer>
</main>
<script>
(()=>{'use strict';
...
})();
</script>
</body>
</html>
```
Canvas fallback text perhaps "Your browser..." not needed but okay.
Stats:
```
<div class=stat><span>Score</span><strong id=score>000</strong></div>
...
```
CSS topbar height.
Meta width stage. .stage perhaps display flex column gap10.
Board frame line.
Overlay .demo bottom card. .overlay.center etc class assigned update. Use classes `overlay demo`, `overlay modal`, hidden.
`.overlay.modal {justify-content:center; align-items:center;background:rgba(...); backdrop-filter:blur(4px); padding:20px;}`
`.overlay.demo .overlay-card {...}`
Could use same card. Game over card max width 300.
Canvas focus outline none; `.board-frame:focus-within{border-color:rgba...}`.
CSS keyframes score pop, status pulse, overlay in, shake.
Reduced motion CSS only animations none, fine.
At 375:
topbar brand mark 38, h1 20, tagline display none.
stats gap12 padding 8 12; strong font22.
topbar width 100%.
controls gap8 font11.
`.stage{width:min(64vh,580px,calc(100vw - 28px));}` CSS min mixed valid. At desktop `64vh=512`. `.topbar{width:min(720px,100%);}` app inner width.
At 375 app padding 12 each => content351, stage calc100vw-28=347 fits content351. Good.
At 320 stage292. no horizontal.
Topbar stats+brand width perhaps brand 150 + stats 170 + gap =320; at375 content351 okay. At320 content296 overflow; media max 420 brand-tag hidden, brand h1 19, mark36 -> brand104; stats each ~60 plus gaps/pad=154; total258 fits. Good.
Controls individual arrow keys total 4*25+gaps=112, plus label WASD etc. Wrap no horizontal. Footer max width 100%.
At desktop vertical total: app padding 18+14=32; topbar 58 + margin-bottom16=74; stage meta 24 +gap10 + board frame width stage 512 includes canvas? .board-frame width 100%, padding8 border => box-sizing border-box so height512. stage total546; footer height30 +margin16=46; total 32+74+546+46=698. Nice centered with leftover ~102? App flex no justify center, starts top 18 leaving bottom 100. Could set `.app{justify-content:center}` with min-height; then content ~666 centered in800 (top67) and topbar etc. Requirement centered page more balanced. Use `justify-content:center; gap:0`. If content height > viewport, min-height and center can cause clipping top? Flex container min-height with content bigger grows height to fit, center fine. `.app` min-height 100svh, justify-content:center. At desktop content ~666, top67. Great. At mobile 667 content topbar56+12 + stage347+34 + footer50 + pads28=527 centered top70. Good. At short heights content may grow body. Fine.
Topbar margin-bottom14. Controls margin-top14.
Body pseudo fixed.
Board 64vh max580. At 1280x800 512. Good.
Rendering dimensions canvas inner size = frame content width 494 due pad/border. grid cell 22.45. Fine.
JS initial:
```
const canvas=..., ctx=canvas.getContext('2d');
if(!ctx) return; // would blank but Chrome supports. no errors.
...
let viewSize=640, pixelRatio=1;
...
function resetWorld(demoMode) { ... updateHUD(); if(demoMode) updateOverlay(); }
resetWorld(true);
updateOverlay();
resizeCanvas();
requestAnimationFrame(loop);
```
Call resize before reset? reset uses no draw. `resizeCanvas(); resetWorld(true); updateOverlay(); request...`.
ResizeCanvas calls render? Could call `draw(performance.now())` but alpha uses state. Initial snake defined before? define state snake=[]; reset first then resize then draw. Order resetWorld(true); updateOverlay(); resizeCanvas(); draw(performance.now()); request.
resetWorld calls updateHUD elements available. lastTick=performance.now. fine.
`resizeCanvas` sets viewSize based rect, then canvas dims. If CSS rect height perhaps width. use rect.width.
Window resize calls resizeCanvas, no immediate draw needed RAF.
`document.addEventListener('visibilitychange',...)` to reset lastTick when visible to avoid jump? Loop one tick only anyway. Particles dt capped. Not needed.
`lastFrame=performance.now` global initial. loop first dt.
`getAlpha(now)`:
```
if(mode==='paused') return frozenAlpha;
if(mode==='over') return 1;
return Math.min(1,Math.max(0,(now-lastTick)/currentStep()));
```
At over prevSnake=snake so alpha irrelevant.
At start `prevSnake` clone snake and alpha begins 0; static first step then moves.
After each tick lastTick set before tick. tick sets prev old then update. render alpha near0 -> old positions, ideal.
When demo reset in tick, resetWorld sets lastTick=performance.now and prev clone. Loop after tick draw alpha based new lastTick, static. Good.
When finish game lastTick perhaps now, prev==snake.
Loop:
```
function loop(now){
const dt=Math.min(.05,(now-lastFrame)/1000||0); lastFrame=now;
if((mode==='playing'||mode==='demo') && now-lastTick>=stepDuration()){
lastTick=now;
takeStep();
}
if(mode!=='paused') updateEffects(dt);
draw(now);
requestAnimationFrame(loop);
}
```
If resetWorld inside takeStep updates lastTick real now.
When pause at now between ticks frozenAlpha. Loop stops step. updateEffects stops. draw food pulses though.
Resume lastTick=performance.now()-frozenAlpha*stepDuration()? To continue from same progress, could set lastTick=now - frozenAlpha*stepDuration(), so snake continues from pause point. prev remains. Use:
```
lastTick=performance.now()-frozenAlpha*stepDuration();
```
Then smooth. But stepDuration based score same. Good. If pause long doesn't advance. Set mode playing then lastTick. lastFrame = performance.now to avoid dt? loop updates dt capped. Fine.
Start game sets frozenAlpha=0.
P pause update overlay calls status.
`togglePause` only playing/paused.
In keydown demo start then if direction queue; not toggle pause.
Space at playing prevent default but no action. Requirement only game over.
Could support `KeyR` restart? not needed.
Effects update:
```
for (let i=particles.length-1;i>=0;i--){... if life<=0 splice}
...
```
Emit ring? Add `rings` perhaps. I can add one ring per eat:
`rings.push({x,y,life:.45,maxLife:.45})`, update. draw before particles. Small. Adds polish. On reset rings=[].
draw ring stroke `rgba(255,209,92,alpha)` lineWidth etc radius `(1-alpha)*cell*.8 + cell*.2`.
Particle colors static from array.
At eating after snake growth spawnFood changes food, emit before/after doesn't matter using ate pos.
`emitBurst(x,y,palette=true)` color arrays optional for collision. For eat colors gold. game over red. Function `emitBurst(x,y,colors=BURST_COLORS,count=18)`.
`addFloater` only eat.
`scoreEl bump` in updateScore not every HUD reset. In eat:
```
updateHud();
pulse(scoreValue);
if(score===best && score>0) pulse(bestValue) only if new best
```
track `const wasBest=best;` before update.
`finishGame(won, impact)`:
```
mode='over'; didWin=won; inputQueue=[];
if(score>best) best=score;
if(impact) emitBurst(...);
updateHUD(); showOverlay();
boardFrame.classList.remove('hit'); void...; add;
```
If full spawnFood calls finish after emit and updateHUD, finish again okay.
`spawnFood` returns bool.
takeStep after eating:
```
const spawnOk=spawnFood();
updateHUD();
...
if(!spawnOk){
if(mode==='demo'){resetWorld(true);} else finishGame(true);
}
return;
```
Mode before finish still playing. Good.
If demo full resets, particles cleared but impossible.
Score floater should show +10.
`drawFood` if null skip.
`drawSnake` uses alpha and points. If snake empty no.
Interpolate tail growth correctly check:
old snake length5 positions indices0..4. after eat new length6: new current index0 new,1 old0,...5 old4. prevSnake[5] nonexistent => prevSnake[4]=old4. segment5 stays old4 while segment4 moves from old4? Wait current index4 = old3, prev index4=old4, so old tail segment index4 moves to old3; current new tail index5=old4, prev fallback old4 static. At alpha0 rendered points: indices0 old0,1 old1,...4 old4,5 old4 duplicate tail. Body path duplicate tail okay. At alpha1 correct. Growth by one cell visual tail emerges. Fine.
After non-eat new length5 current 0 new,1 old0,...4 old3. prev same indices old0..old4. smooth.
At initial prev clone current static.
Body path points duplicates okay. `ctx.lineTo` same no error.
Gradient for body computed based head to tail? `ctx.createLinearGradient(0,0,viewSize,viewSize)` universal diagonal not follow snake but colors. Better create from tail point to head point; if same position create okay even x0=x1,y0=y1? Canvas gradient with identical points is allowed paints transparent? spec not throw. Use board diagonal safe. Colors head vs tail perhaps with diagonal not strictly but okay. Use gradient across board #62ef9a to #b9ff61. Body all green. Outer and inner.
Could draw tail taper segments individually to create taper but connecting stroke constant width. Distinct head sufficient. Add tail circle smaller? Current line cap full width tail. Snake polished enough.
To taper tail, draw tail last 2 circles darker smaller over? not necessary.
Draw scale highlights only body excluding head:
```
for (let i=2;i<points.length-1;i+=2){ctx... arc(...)}
```
Should be above head? draw before head.
Eyes white, pupils dark. Add tiny tongue? no.
Direction head uses `direction`; at game over attempted direction as noted.
Draw board grid loops using fillRect each cell 484, fine. Use path grid lines 46. background radial every frame perhaps fine. dpr 2 1000x1000, 60fps okay. 484 fillRects + shadow snake ~ okay.
Use `ctx.shadowBlur` resets via save restore. After draw body restore.
Vignette radial fill. Food shadow.
Particles `ctx.shadowBlur=0` to avoid inherited because draw functions save restore.
At start draw sets globalAlpha 1 and line defaults. Ensure each save restore.
`draw()`:
```
ctx.setTransform(pixelRatio,...)
ctx.clearRect(0,0,viewSize,viewSize)
drawBoard...
...
ctx.globalAlpha=1
```
Canvas width backing and CSS.
Rounded inner border `ctx.strokeRect(.5,.5,size-1,size-1)` square while CSS rounds clipping. Fine.
Food spawn colors.
HUD update called reset before mode? resetWorld sets mode = demo? function `resetWorld(isDemo){ mode=isDemo?'demo':'playing'...}`. startGame calls resetWorld(false); overlay.hidden=true; updateOverlay handles playing; focus. In resetWorld updateOverlay too? Combine:
```
function resetWorld(asDemo) {
mode=...
...
updateHUD();
updateOverlay();
}
function startGame(){ resetWorld(false); canvas.focus...}
```
Good. On resetDemo collision update overlay demo.
Initial resetWorld(true).
Overlay update playing sets hidden true first and return.
Demo finalScore hidden. overlay class. `overlayKicker.innerHTML`? dot separate CSS via ::before, so textContent. `overlayKicker` class kicker. Kicker pseudo dot.
Set title etc.
For over:
```
overlay.hidden=false;
overlay.className='overlay modal';
kicker.textContent=didWin?'BOARD COMPLETE':'RUN OVER';
title.textContent=didWin?'Perfect run!':'Game over';
finalScore.hidden=false;
finalScoreValue.textContent=score;
text.textContent=didWin?'...':'You scored ...';
hint.textContent='SPACE / ENTER TO RESTART';
```
Score duplicate in p perhaps okay; make p didWin ? ... : 'Press Space or Enter for another run.' Then overlay has score. But exact restart visible hint. Set text `Your session best is ${best}.` Then final score. Good.
Demo hint `'ARROW KEYS / WASD · P TO PAUSE'`
Paused finalScore hidden; text `'Press P to jump back in.'`; hint `'P TO RESUME'`.
Kicker dot colors via `.overlay.modal .overlay-kicker` perhaps game over red? Add class `danger` on overlay if over and !won? `overlay.className='overlay modal'+(didWin?' win':' danger')`. CSS `.overlay.danger .overlay-kicker{color:#ff9b8d}` and pseudo. paused class `'overlay modal pause'`.
Demo overlay card bottom. Kicker dot green.
Overlay card entrance animation re-trigger on update? class changes, okay.
Game over .final-score strong font56.
Status data mode:
`statusPill.dataset.state=mode;`
CSS `.status[data-state="paused"]` etc.
`statusText` labels.
Meta `difficultyLabel` perhaps `22 × 22 GRID` static.
Score best numbers no local storage.
No horizontal scrollbar: body overflow-x:hidden; topbar flex could overflow at tiny but media. At 375 okay.
Use `min-width:0` brand and stats.
One potential requirement "Space or Enter restarts" could imply at any time? It says "Game over ...; Space or Enter restarts, P pauses." I implement game over. Good.
P pauses only active. Paused overlay P resumes.
Space on game over starts and hides overlay. Enter too.
Food +10 exactly, grows one (not pop on eat).
Score increments before spawn. Good.
At least20x20 constant 22.
I should ensure when new food generated it never spawns on new snake including new head and tail growth. yes occupied after unshift and no pop.
Quick successive keys handled via queue validated sequentially. yes.
Perhaps user presses W and A quick from right; queue W,A. At first step W may collide self if snake shape but that's legitimate. Both honored. Great.
I'll think self-playing demo AI's initial food and score. Initial snake head x6,y11, tail x2. Direction right. BFS blocked snake except tail. food x13,y11. BFS could find path first candidate right. tick 7 at x13? Steps: start6, after1=7 ... after7=13 -> eat at 7*105=735ms. particles at 0.7s. At 3 sec after eaten snake head moving to subsequent random food. Score 10 perhaps 20. Good.
Overlay bottom message remains. Fine.
No user interaction screenshot sees self playing.
Could use AI score best not updated so best 000. Fine.
Now possible JavaScript error from `canvas.focus({preventScroll:true})` in sandbox? Allowed. Yes.
No `window.open`, storage etc.
Inline SVG logo path valid:
```
<svg viewBox="0 0 44 44"><path d="M12 12h14a6 6... " .../>
...
</svg>
```
Not critical. Use four circles in CSS perhaps less code. Inline svg nice:
`<path d="M12 13h14a6 6 0 0 1 0 12H18a5 5 0 0 0 0 10h14" ...>` and circle at32 35? Head should at path end 32 35 with eye. But line ends. Add circle lime at32,35 and eye. Looks snake.
Background logo border.
Score panel perhaps title "SESSION BEST" too long at mobile. CSS font9 letter spacing1.2, fits 74 width. Stats 2*78+gap+bg=190. Brand120 total310 at375 content351. Fine. At 320 could fit barely. media stats label font8.
No text before code in response.
I can now draft the response. Make sure no bugs due helper key function object accepts `{x,y}`.
Code details exact:
```
function isOpposite(a,b){return a.x===-b.x&&a.y===-b.y;}
```
food equality `same`.
`inside(x,y)`.
`bfsFirstDirection`:
```
const queue = [{x:head.x,y:head.y,first:null}];
const seen = new Set([cellKey(head)]);
for (let i=0;i<queue.length;i++){
const node=queue[i];
const dirs=node.first ? ALL_DIRS : orderedDirections();
for(const d of dirs){
if(!node.first && isOpposite(d,direction)) continue;
...
const first=node.first || d;
if(food && nx===food.x...) return first;
seen.add...
queue.push...
}
}
```
For non-start dirs ALL includes reverse, seen blocks immediate parent. Fine.
Blocked set `snake.slice(0,-1)` excludes tail but includes head. Food could equal tail? never. good.
When candidate next outside skip.
Return first object from ordered dirs stable.
`evaluateMove` collision loop i<bodyCount includes head. For non-reverse candidates new head can't head. okay.
Flood blocked future.slice(1); if `future.slice(1)` includes duplicates from next moving into tail? no as explained. If moving into body collision returns null.
`spaceNeeded` perhaps zero near full; path safe.
```
const ranked = candidates.sort((a,b)=> (b.area*8 - dist(b)*1.4 + ...) - ...);
return ranked[0]?.d || direction;
```
Optional chaining supported. Avoid mutation issue candidates. fine.
`distanceToFood(eval)` uses eval.n and food. if null0.
Path eval find via candidates.find(c=>same direction).
If path evaluation safe return path else fallback. If path null fallback.
This means AI always chooses BFS path even if area safe but may head into future trap beyond one step, recalculates each tick. fine.
`orderedDirections` returns `[direction, {x:direction.y,y:-direction.x}, {x:-direction.y,y:direction.x}, opposite]` left/right whichever. Initial current right, first right, second down? {x:0,y:-1} actually right dir (1,0) -> (0,-1) up, other down. fine.
No randomness direction, stable.
Effects:
`emitBurst` receives grid coords. Collision newHead perhaps -1; clamp x+.5 between .2 and GRID-.2. Helper clamp.
For eat `emitBurst(ate.x+.5, ate.y+.5, FOOD_COLORS,20)`.
Rings optional color argument? rings always gold, game over red ring weird if emit collision adds ring. Add `emitBurst(x,y,colors,count=18,withRing=true)` ring stroke style based colors[0]. ring object color. Good.
`rings.push({x,y,life:.45,max:.45,color:colors[0]})`.
Update max property consistent.
draw ring globalAlpha life/max. radius `(1-t)*cell*.75 + cell*.18`.
Particle life max random. `p.maxLife`.
Floater draw `context.font` etc.
`updateEffects(dt)` even mode over; if dt=0 initial no.
Velocity random angle radial, speed 1.7+random3.6 cells/sec. gravity 2.2. Size c? store size fraction `.045+Math.random()*.055` times cell. Draw max2? at mobile cell15 size ~1-1.5 fine.
Draw p color and arc.
Add glow only rings/food; particles no shadow.
`drawFloaters` text "+10" in demo and play.
Score bump:
```
function bump(el){el.classList.remove('bump'); void el.offsetWidth; el.classList.add('bump');}
```
CSS `.bump{animation:scoreBump .25s ease}` re-adding class after reflow works. class remains after animation; next remove.
Best update in playing only. If demo no best bump.
In takeStep:
```
const oldBest=best;
score +=10;
if(mode==='playing') best=Math.max...
emit...
const spawned=spawnFood();
updateHUD();
bump(scoreValue);
if(best>oldBest)bump(bestValue);
if(!spawned)...
```
Note if spawnFood full and mode demo then reset after bump; okay.
Food var `ateFood` copy before spawn.
Step collision check needs determine ate before wall? `ate = food && nx...`; if outside food can't. fine.
Set direction before collision. bodyCount.
When no food, ate false.
Finish hit CSS:
```
boardFrame.classList.remove('hit');
requestAnimationFrame(()=>boardFrame.classList.add('hit'));
```
If request after overlay perhaps animation. Simpler reflow. Allowed.
`window.setTimeout(()=>boardFrame.classList.remove('hit'),360)` stale start game might shake new game if old timeout only removes class, harmless.
No timers besides RAF.
Pause overlay when pausing with particles freeze. Food pulse not freeze but okay. update status.
Key handling `e.code` WASD only uses KeyW etc independent keyboard layout as required.
Direction mapping constant.
Prevent default for arrows and space even demo; Enter not prevent perhaps no button default. Add if Space etc.
Repeated direction e.repeat could queue turns based held? Same directions ignored. If holding up then left etc key presses unaffected. Don't ignore repeat directions except P etc.
When mode over and direction key pressed, return no restart. good.
When mode demo and e.code KeyP start game but no pause. good.
`startGame` calls canvas.focus but if triggered by keydown, focus canvas may cause subsequent key event still window. yes.
Click paused overlay resume; click game over restart. `pointerdown` uses startGame and focus.
Any click outside overlay in demo? overlay full board only, message says click likely anywhere perhaps "Click or press a key". User click header doesn't start. Usually click game. Could add `window.addEventListener('pointerdown',...)` if demo start from anywhere, but then clicking perhaps scores triggers. Requirement click to start, best support clicking anywhere: overlay is main target and obvious. Enough. Could add canvas board only. Fine.
Initial overlay text exact case "Click or press a key to start". yes.
CSS body font and numeric.
Perhaps viewport at 1280x800 screenshot title and game board centered with start card at bottom. yes.
I'll outline the response code now in response. Ensure one fenced block only.