MiMo-V2.6-Pro
by Xiaomireasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 50 s
- Cost
- $0.012
- Output tokens
- 15,136
- Spent thinking
- 7,051 tokens
- Speed
- 138.4 tok/s
- Console errors
- 0
- First token after
- 631 ms
- First visible text after
- 1 min 6 s
- Prompt tokens
- 497
- Code size
- 24.6 KB
- Finish reason
- stop
- Served by
- DeepInfra
- Model id
xiaomi/mimo-v2.6-pro- Reasoning sent
model default- Max output
- 131,072 tokens
- Generated
- Nov 13, 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 · 1011 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Snake</title>
<style>
:root {
--bg: #070b14;
--panel: #0d1526;
--line: rgba(148, 197, 255, 0.13);
--mint: #34d399;
--cyan: #22d3ee;
--rose: #fb7185;
--amber: #fbbf24;
--text: #eaf2ff;
--muted: #8aa4c0;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
min-height: 100%;
overflow-x: hidden;
}
body {
background:
radial-gradient(1100px 680px at 50% -12%, #16294180 0%, #070b14 62%),
linear-gradient(180deg, #070b14 0%, #05080f 100%);
color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
display: flex;
justify-content: center;
align-items: flex-start;
}
.app {
width: 100%;
max-width: 1180px;
min-height: 100vh;
padding: 20px clamp(12px, 3vw, 34px) 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: clamp(12px, 2.2vh, 20px);
}
/* ---------- header ---------- */
.topbar {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
}
.brand {
display: flex;
align-items: center;
gap: 13px;
min-width: 0;
}
.logo {
position: relative;
width: 44px;
height: 44px;
flex: 0 0 auto;
border-radius: 14px;
background: linear-gradient(150deg, #22d3ee 0%, #34d399 52%, #0ea5e9 100%);
box-shadow: 0 12px 30px rgba(34, 211, 238, 0.28);
overflow: hidden;
}
.logo::before,
.logo::after {
content: "";
position: absolute;
border-radius: 50%;
background: #06121f;
}
.logo::before {
width: 9px;
height: 9px;
left: 10px;
top: 11px;
box-shadow: 12px 12px 0 #06121f;
}
.logo::after {
width: 5px;
height: 5px;
right: 10px;
top: 12px;
background: #04222c;
opacity: 0.65;
}
.brand h1 {
margin: 0;
font-size: clamp(19px, 2.5vw, 27px);
font-weight: 800;
letter-spacing: 0.22em;
text-transform: uppercase;
white-space: nowrap;
}
.brand p {
margin: 5px 0 0;
color: var(--muted);
font-size: 11px;
letter-spacing: 0.19em;
text-transform: uppercase;
white-space: nowrap;
}
.stats {
display: flex;
gap: 10px;
}
.stat {
min-width: 94px;
padding: 9px 16px 11px;
border: 1px solid var(--line);
border-radius: 15px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.015));
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.22);
}
.stat span {
display: block;
font-size: 9.5px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--muted);
}
.stat strong {
display: block;
margin-top: 3px;
font-size: 25px;
line-height: 1.15;
font-weight: 800;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
.stat.score strong { color: var(--mint); }
.stat.best strong { color: var(--amber); }
.stat.length strong { color: var(--cyan); }
/* ---------- board ---------- */
.stage {
width: 100%;
display: flex;
justify-content: center;
}
.board-wrap {
position: relative;
width: min(620px, calc(100vw - 26px), calc(100vh - 236px));
aspect-ratio: 1 / 1;
border-radius: 22px;
overflow: hidden;
cursor: pointer;
background: #0a1322;
box-shadow:
0 34px 80px rgba(0, 0, 0, 0.55),
0 0 0 1px rgba(126, 200, 255, 0.16),
0 0 70px rgba(34, 211, 238, 0.11);
-webkit-tap-highlight-color: transparent;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
/* ---------- overlay ---------- */
.overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 9px;
padding: 24px;
text-align: center;
background:
radial-gradient(120% 92% at 50% 42%, rgba(7, 11, 20, 0.5) 0%, rgba(7, 11, 20, 0.88) 100%);
-webkit-backdrop-filter: blur(3px);
backdrop-filter: blur(3px);
transition: opacity 0.24s ease;
pointer-events: none;
}
.overlay.hidden {
opacity: 0;
}
.ov-kicker {
font-size: 10px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--cyan);
font-weight: 700;
}
.ov-title {
margin: 0;
font-size: clamp(21px, 3.6vw, 34px);
font-weight: 800;
letter-spacing: 0.015em;
line-height: 1.16;
text-wrap: balance;
}
.ov-text {
margin: 2px 0 0;
max-width: 330px;
color: var(--muted);
font-size: 13.5px;
line-height: 1.65;
}
.ov-text b {
color: var(--text);
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.ov-btn {
margin-top: 11px;
padding: 11px 21px;
border-radius: 999px;
background: linear-gradient(135deg, #22d3ee 0%, #34d399 100%);
color: #04202b;
font-size: 10.5px;
font-weight: 800;
letter-spacing: 0.19em;
text-transform: uppercase;
box-shadow: 0 14px 34px rgba(34, 211, 238, 0.28);
}
/* ---------- footer ---------- */
.hints {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.hint {
padding: 7px 13px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(255, 255, 255, 0.028);
color: var(--muted);
font-size: 10.5px;
letter-spacing: 0.15em;
text-transform: uppercase;
white-space: nowrap;
}
.hint b {
color: var(--text);
font-weight: 700;
letter-spacing: 0.12em;
}
@media (max-width: 620px) {
.app { padding-top: 16px; gap: 13px; }
.topbar {
flex-direction: column;
align-items: stretch;
gap: 13px;
}
.brand { padding-left: 2px; }
.stats {
width: 100%;
gap: 8px;
}
.stat {
flex: 1 1 0;
min-width: 0;
padding: 8px 11px 10px;
}
.stat strong { font-size: 21px; }
.stat span { font-size: 8.5px; letter-spacing: 0.18em; }
.board-wrap {
width: min(calc(100vw - 22px), calc(100vh - 268px));
border-radius: 18px;
}
.hint {
font-size: 9px;
letter-spacing: 0.11em;
padding: 6px 10px;
}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<div class="logo" aria-hidden="true"></div>
<div>
<h1>Neon Snake</h1>
<p>Arcade grid runner</p>
</div>
</div>
<div class="stats">
<div class="stat score">
<span>Score</span>
<strong id="score">0</strong>
</div>
<div class="stat best">
<span>Best</span>
<strong id="best">0</strong>
</div>
<div class="stat length">
<span>Length</span>
<strong id="length">5</strong>
</div>
</div>
</header>
<main class="stage">
<div class="board-wrap" id="boardWrap">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<div class="ov-kicker" id="ovKicker">Demo mode · AI playing</div>
<h2 class="ov-title" id="ovTitle">Click or press a key to start</h2>
<p class="ov-text" id="ovText">Arrow keys or WASD to steer · <b>P</b> to pause</p>
<div class="ov-btn" id="ovBtn">Start game</div>
</div>
</div>
</main>
<footer class="hints">
<div class="hint"><b>↑ ↓ ← →</b> Move</div>
<div class="hint"><b>W A S D</b> Move</div>
<div class="hint"><b>P</b> Pause</div>
<div class="hint"><b>Space / Enter</b> Restart</div>
<div class="hint">Food = <b>+10</b></div>
</footer>
</div>
<script>
(() => {
"use strict";
/* ============================================================
Setup
============================================================ */
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const boardWrap = document.getElementById("boardWrap");
const scoreEl = document.getElementById("score");
const bestEl = document.getElementById("best");
const lengthEl = document.getElementById("length");
const overlay = document.getElementById("overlay");
const ovKicker = document.getElementById("ovKicker");
const ovTitle = document.getElementById("ovTitle");
const ovText = document.getElementById("ovText");
const ovBtn = document.getElementById("ovBtn");
const COLS = 24;
const ROWS = 24;
const STEP_MS = 112; // one grid move per step
const MAX_QUEUE = 3;
const DIRS = {
up: { x: 0, y: -1, name: "up" },
right: { x: 1, y: 0, name: "right" },
down: { x: 0, y: 1, name: "down" },
left: { x: -1, y: 0, name: "left" }
};
const KEY_DIRS = {
arrowup: DIRS.up, w: DIRS.up,
arrowright: DIRS.right, d: DIRS.right,
arrowdown: DIRS.down, s: DIRS.down,
arrowleft: DIRS.left, a: DIRS.left
};
let size = 0; // css pixel size of the square canvas
let cell = 0; // css pixel size of one grid cell
let dpr = 1;
let state = "demo"; // demo | playing | paused | gameover
let snake = [];
let prevRender = [];
let direction = DIRS.right;
let queue = [];
let food = { x: 16, y: 12 };
let particles = [];
let score = 0;
let best = 0;
let accumulator = 0;
let lastTime = performance.now();
/* ============================================================
Utilities
============================================================ */
const keyOf = (x, y) => x + "," + y;
const opposite = (a, b) => a.x === -b.x && a.y === -b.y;
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const lerp = (a, b, t) => a + (b - a) * t;
function resize() {
const rect = boardWrap.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio || 1, 2);
size = Math.max(1, rect.width);
canvas.width = Math.round(size * dpr);
canvas.height = Math.round(size * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
cell = size / COLS;
}
function updateHUD() {
scoreEl.textContent = String(score);
bestEl.textContent = String(best);
lengthEl.textContent = String(snake.length);
}
function updateOverlay() {
if (state === "playing") {
overlay.classList.add("hidden");
return;
}
overlay.classList.remove("hidden");
if (state === "demo") {
ovKicker.textContent = "Demo mode · AI playing";
ovTitle.textContent = "Click or press a key to start";
ovText.innerHTML = "Arrow keys or WASD to steer · <b>P</b> to pause";
ovBtn.textContent = "Start game";
} else if (state === "paused") {
ovKicker.textContent = "Paused";
ovTitle.textContent = "Take a breath";
ovText.innerHTML = "Press <b>P</b> or <b>Space</b> to keep going";
ovBtn.textContent = "Resume";
} else if (state === "gameover") {
ovKicker.textContent = "Game over";
ovTitle.textContent = score >= best && score > 0 ? "New session best!" : "Run ended";
ovText.innerHTML =
"Score <b>" + score + "</b> · Best <b>" + best + "</b> · Length <b>" + snake.length + "</b>";
ovBtn.textContent = "Play again";
}
}
/* ============================================================
Game state
============================================================ */
function spawnFood() {
const occupied = new Set(snake.map(s => keyOf(s.x, s.y)));
let x, y, guard = 0;
do {
x = Math.floor(Math.random() * COLS);
y = Math.floor(Math.random() * ROWS);
guard++;
} while (occupied.has(keyOf(x, y)) && guard < 2000);
food = { x, y };
}
function resetSnake() {
const cy = Math.floor(ROWS / 2);
const cx = Math.floor(COLS / 2) - 2;
snake = [];
for (let i = 0; i < 5; i++) {
snake.push({ x: cx - i, y: cy });
}
direction = DIRS.right;
queue = [];
prevRender = snake.map(s => ({ x: s.x, y: s.y }));
}
function startGame() {
score = 0;
particles = [];
resetSnake();
spawnFood();
accumulator = 0;
state = "playing";
updateHUD();
updateOverlay();
}
function startDemo() {
score = 0;
particles = [];
resetSnake();
spawnFood();
accumulator = 0;
state = "demo";
updateHUD();
updateOverlay();
}
function endGame() {
if (score > best) best = score;
state = "gameover";
updateHUD();
updateOverlay();
}
/* ============================================================
Input
============================================================ */
function queueDirection(dir) {
const last = queue.length ? queue[queue.length - 1] : direction;
if (opposite(dir, last)) return; // never reverse into yourself
if (dir.x === last.x && dir.y === last.y) return;
if (queue.length < MAX_QUEUE) queue.push(dir); // honour quick successive presses
}
window.addEventListener("keydown", (e) => {
const k = e.key.toLowerCase();
const isDir = Object.prototype.hasOwnProperty.call(KEY_DIRS, k);
if (isDir || k === " " || k === "enter" || k === "p") {
e.preventDefault();
}
// Pause toggle only while a real run is live
if (k === "p" && (state === "playing" || state === "paused")) {
state = state === "playing" ? "paused" : "playing";
accumulator = 0;
updateOverlay();
return;
}
if (state === "demo") {
startGame();
if (isDir) queueDirection(KEY_DIRS[k]);
return;
}
if (state === "gameover") {
if (k === " " || k === "enter") startGame();
return;
}
if (state === "paused") {
if (k === " " || k === "enter") {
state = "playing";
accumulator = 0;
updateOverlay();
}
return;
}
if (isDir) queueDirection(KEY_DIRS[k]);
}, { passive: false });
boardWrap.addEventListener("click", () => {
if (state === "demo" || state === "gameover") {
startGame();
} else if (state === "paused") {
state = "playing";
accumulator = 0;
updateOverlay();
}
});
window.addEventListener("resize", resize);
/* ============================================================
Demo AI — greedy move choice with a flood-fill safety check
============================================================ */
function floodCount(sx, sy, blocked) {
const seen = new Set([keyOf(sx, sy)]);
const stack = [[sx, sy]];
let count = 0;
while (stack.length && count < 260) {
const [x, y] = stack.pop();
count++;
for (const d of [DIRS.up, DIRS.right, DIRS.down, DIRS.left]) {
const nx = x + d.x;
const ny = y + d.y;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const k = keyOf(nx, ny);
if (seen.has(k) || blocked.has(k)) continue;
seen.add(k);
stack.push([nx, ny]);
}
}
return count;
}
function chooseDemoDirection() {
const head = snake[0];
const tailFree = new Set(
snake.slice(0, Math.max(0, snake.length - 1)).map(s => keyOf(s.x, s.y))
);
let bestDir = direction;
let bestScore = -Infinity;
for (const d of [DIRS.up, DIRS.right, DIRS.down, DIRS.left]) {
if (opposite(d, direction)) continue;
const nx = head.x + d.x;
const ny = head.y + d.y;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const willEat = nx === food.x && ny === food.y;
const bodyBlocked = willEat
? new Set(snake.map(s => keyOf(s.x, s.y)))
: tailFree;
if (bodyBlocked.has(keyOf(nx, ny))) continue;
const space = floodCount(nx, ny, tailFree);
const dist = Math.abs(nx - food.x) + Math.abs(ny - food.y);
let s = space * 1.6 - dist * 2.4;
if (d === direction) s += 0.9;
if (willEat) s += 14;
if (s > bestScore) {
bestScore = s;
bestDir = d;
}
}
return bestDir;
}
/* ============================================================
Simulation step
============================================================ */
function stepGame() {
if (state === "demo") {
queue.length = 0;
direction = chooseDemoDirection();
} else if (queue.length) {
const next = queue.shift();
if (!opposite(next, direction)) direction = next;
}
const head = snake[0];
const nx = head.x + direction.x;
const ny = head.y + direction.y;
const willEat = nx === food.x && ny === food.y;
// Walls
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) {
if (state === "demo") {
startDemo();
return;
}
endGame();
return;
}
// Body (the tail cell vacates this step unless we are growing)
const bodyCheck = willEat ? snake : snake.slice(0, Math.max(0, snake.length - 1));
for (let i = 0; i < bodyCheck.length; i++) {
if (bodyCheck[i].x === nx && bodyCheck[i].y === ny) {
if (state === "demo") {
startDemo();
return;
}
endGame();
return;
}
}
// Keep a render source of the same length for smooth interpolation
prevRender = snake.map(s => ({ x: s.x, y: s.y }));
snake.unshift({ x: nx, y: ny });
if (willEat) {
score += 10;
if (score > best) best = score;
spawnFood();
burst(nx, ny);
} else {
snake.pop();
}
// Pad the interpolation source for a freshly grown tail
if (prevRender.length < snake.length) {
prevRender.push({ x: snake[snake.length - 1].x, y: snake[snake.length - 1].y });
}
updateHUD();
}
/* ============================================================
Particles
============================================================ */
const PARTICLE_COLORS = ["#fb7185", "#fbbf24", "#34d399", "#22d3ee", "#f472b6"];
function burst(gx, gy) {
const cx = gx + 0.5;
const cy = gy + 0.5;
for (let i = 0; i < 22; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1.8 + Math.random() * 5.2;
particles.push({
x: cx,
y: cy,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 0.42 + Math.random() * 0.5,
maxLife: 0.42 + Math.random() * 0.5,
size: 0.07 + Math.random() * 0.11,
color: PARTICLE_COLORS[(Math.random() * PARTICLE_COLORS.length) | 0]
});
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= 0.965;
p.vy = p.vy * 0.965 + 3.4 * dt;
}
}
/* ============================================================
Rendering
============================================================ */
function drawBoard() {
// Base
ctx.fillStyle = "#0a1322";
ctx.fillRect(0, 0, size, size);
// Subtle checkerboard
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if ((x + y) % 2 === 0) {
ctx.fillStyle = "rgba(120, 190, 255, 0.028)";
ctx.fillRect(x * cell, y * cell, cell, cell);
}
}
}
// Grid lines
ctx.lineWidth = 1;
ctx.strokeStyle = "rgba(126, 190, 255, 0.07)";
ctx.beginPath();
for (let i = 1; i < COLS; i++) {
const p = Math.round(i * cell) + 0.5;
ctx.moveTo(p, 0);
ctx.lineTo(p, size);
}
for (let i = 1; i < ROWS; i++) {
const p = Math.round(i * cell) + 0.5;
ctx.moveTo(0, p);
ctx.lineTo(size, p);
}
ctx.stroke();
}
function drawFood(now) {
const pulse = 1 + Math.sin(now / 240) * 0.07;
const cx = (food.x + 0.5) * cell;
const cy = (food.y + 0.5) * cell;
const r = cell * 0.33 * pulse;
ctx.save();
ctx.shadowColor = "rgba(251, 113, 133, 0.75)";
ctx.shadowBlur = cell * 0.95;
const g = ctx.createRadialGradient(
cx - r * 0.35, cy - r * 0.4, r * 0.12,
cx, cy, r * 1.25
);
g.addColorStop(0, "#ffd2d9");
g.addColorStop(0.35, "#fb7185");
g.addColorStop(1, "#e11d48");
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
// Leaf / stem accent
ctx.shadowBlur = 0;
ctx.strokeStyle = "rgba(52, 211, 153, 0.95)";
ctx.lineWidth = cell * 0.11;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(cx + r * 0.1, cy - r * 0.85);
ctx.quadraticCurveTo(cx + r * 0.55, cy - r * 1.35, cx + r * 1.05, cy - r * 1.05);
ctx.stroke();
ctx.restore();
}
function drawSnake(t) {
const pts = [];
for (let i = 0; i < snake.length; i++) {
const from = prevRender[i] || prevRender[prevRender.length - 1] || snake[i];
const to = snake[i];
pts.push({
x: (lerp(from.x, to.x, t) + 0.5) * cell,
y: (lerp(from.y, to.y, t) + 0.5) * cell
});
}
if (!pts.length) return;
const grad = ctx.createLinearGradient(0, 0, size, size);
grad.addColorStop(0, "#22d3ee");
grad.addColorStop(0.45, "#34d399");
grad.addColorStop(1, "#14b8a6");
// Soft glow under the body
ctx.save();
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.strokeStyle = "rgba(34, 211, 238, 0.20)";
ctx.lineWidth = cell * 0.95;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.stroke();
// Main body
ctx.strokeStyle = grad;
ctx.lineWidth = cell * 0.62;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.stroke();
// Inner highlight
ctx.strokeStyle = "rgba(255, 255, 255, 0.13)";
ctx.lineWidth = cell * 0.2;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y - cell * 0.1);
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y - cell * 0.1);
ctx.stroke();
// Tail cap
const tail = pts[pts.length - 1];
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(tail.x, tail.y, cell * 0.31, 0, Math.PI * 2);
ctx.fill();
// ---- Head ----
const head = pts[0];
const d = direction;
const perp = { x: -d.y, y: d.x };
ctx.shadowColor = "rgba(52, 211, 153, 0.55)";
ctx.shadowBlur = cell * 0.65;
const headGrad = ctx.createRadialGradient(
head.x - cell * 0.14, head.y - cell * 0.16, cell * 0.05,
head.x, head.y, cell * 0.58
);
headGrad.addColorStop(0, "#b9ffe4");
headGrad.addColorStop(0.42, "#4be3a8");
headGrad.addColorStop(1, "#12b3a2");
ctx.fillStyle = headGrad;
ctx.beginPath();
ctx.arc(head.x, head.y, cell * 0.44, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// Eyes facing the movement direction
const fwd = cell * 0.115;
const side = cell * 0.175;
for (const s of [1, -1]) {
const ex = head.x + d.x * fwd + perp.x * side * s;
const ey = head.y + d.y * fwd + perp.y * side * s;
ctx.fillStyle = "#f4fbff";
ctx.beginPath();
ctx.arc(ex, ey, cell * 0.118, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#07131f";
ctx.beginPath();
ctx.arc(
ex + d.x * cell * 0.055,
ey + d.y * cell * 0.055,
cell * 0.062,
0,
Math.PI * 2
);
ctx.fill();
}
ctx.restore();
}
function drawParticles() {
for (const p of particles) {
const a = clamp(p.life / p.maxLife, 0, 1);
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x * cell, p.y * cell, p.size * cell * (0.5 + a * 0.8), 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawFrame(now, t) {
drawBoard();
drawFood(now);
drawSnake(t);
drawParticles();
// Board edge
ctx.strokeStyle = "rgba(126, 200, 255, 0.18)";
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, size - 2, size - 2);
}
/* ============================================================
Main loop
============================================================ */
function frame(now) {
const dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
if (state === "playing" || state === "demo") {
accumulator += dt * 1000;
let guard = 0;
while (accumulator >= STEP_MS && guard < 8) {
accumulator -= STEP_MS;
stepGame();
guard++;
}
}
updateParticles(dt);
drawFrame(now, accumulator / STEP_MS);
requestAnimationFrame(frame);
}
/* ============================================================
Boot
============================================================ */
resize();
startDemo();
requestAnimationFrame((t) => {
lastTime = t;
frame(t);
});
})();
</script>
</body>
</html>Reasoning trace (7,051 tokens)
We need output single html code block. Need build polished. Need self-playing demo mode with "Click or press a key to start". But game must show meaningful first paint. Let's design.
Canvas responsive square, grid 24x24 maybe. Smooth movement: render interpolated positions between grid ticks. Input queue honoring quick presses, prevent reverse relative to last queued direction. Food never on snake.
Demo mode: AI plays automatically behind overlay "Click or press a key to start". Actually requirement: games must start self-playing demo mode behind message. So state 'demo' where AI snake plays, overlay message with score maybe. On click/any key, restart to playing. Keyboard listener needs click? window keydown gets after focus/iframe click. Fine.
Session best score via in-memory variable (sessionStorage not allowed? "session" best — use variable; localStorage prohibited).
Visual: dark background? Snake game. Use CSS gradients fine. Canvas drawing: board with subtle checker, rounded cells, glow. Head distinct eyes. Smooth: interpolation with tick accumulator. Particle burst.
Careful: no external fonts. Use system font. Layout at 1280x800: header score cards, canvas size min( calc(100vh - stuff), 620px). Board centered. Controls hint.
Implement fixed timestep with rAF and accumulator. Movement tick = 118ms. Smooth interpolation: store prevSnake array and snake. Render each segment at lerp(prev, current) but wrapping not used (walls kill), so fine. Snake body positions: when moving, snake array new head + body; prevSnake is old array. For rendering, need align: segment i of new snake corresponds to prev segment i-1? Actually old snake positions: head at old[0], body old[1..]. New snake: head = old[0]+dir, new[1]=old[0], new[i]=old[i-1]. So new segment i moves from old[i-1] to new[i] (=old[i-1]) for i>=1 — no movement. That gives static body with head moving, which is actually correct-ish but looks like the whole snake follows smoothly? Visually typical smooth snake interpolates each segment from its previous position to new position, where new[i] = old[i-1], so body segments stay put and shift at tick... Hmm the whole snake geometry with head interpolating toward next cell looks like snake lengthening. Better approach: interpolate along path positions: render body segment i at lerp(old[i], new[i], t) but new[i]=old[i-1], so it moves forward one cell over the tick. That gives each segment moving one cell, including tail retracting — but when growing, tail doesn't move. Actually correct rendering: each snake piece occupies a cell during the tick; the visual body should travel. Standard "smooth movement" interpolation: draw head interpolated from old head to new head, and draw body from old positions with each segment interpolated toward the position of segment ahead. Using lerp(old[i], new[i], t) where new[i]=old[i-1] works: segment i moves from old[i] toward old[i-1]. Segment 0 moves old[0]→new head. Yes that's right and it's what I said: new[i] for i>=1 = old[i-1]. So interpolation from old[i] to new[i] = old[i-1], moving forward. Good. But if grown, snake length increases by 1; the new last segment new[n-1] = old[n-2], and there is no old[n-1]; set prev for it = old[n-2] (stationary) or just handle. Simpler: keep renderFrom array of same length as new snake: renderFrom[0] = old[0], renderFrom[i] = old[i] ?? old[i-1]. Fine.
But drawing connected rounded snake via circles per segment plus thick line path: use interpolated positions and stroke path with round line joins/caps. That gives nice smooth snake. Then head as larger rounded circle at pos 0, eyes facing dir. Need distinct head.
Snake tick state: eat -> grow (don't pop tail). Implementation: unshift new head; if food eaten, keep tail; else pop.
Game over overlay with score + best, Space/Enter restart.
Particles: array with x,y (canvas pixel coords in grid units maybe), vx, vy, life, color. Update with dt in rAF.
Food pulse animation.
Grid 24x24, cell size computed from canvas width / COLS. Use devicePixelRatio.
Canvas resize: set width = clientWidth*dpr, height same. Board CSS aspect-ratio 1/1.
Layout: page flex column, header top with title left and stats right; board; footer hints. At 375 width: header stacks, board width calc(100vw - 24px). Keep total fits.
Demo AI: simple pathing. Compute a safe move: candidates avoiding walls/self, prefer direction toward food with BFS-ish greedy + safety (flood fill area after move > snake length maybe). Keep it reasonably simple but competent. Use BFS to food? Could implement greedy with safety check: for each candidate direction not reverse, simulate move, count reachable free space (flood fill limited), score = distance to food negative + space. Choose best. Demo plays indefinitely-ish; when dies, auto restart demo after short pause.
Demo overlay message: "Click or press a key to start" plus small "Demo mode · AI is playing". Should not block canvas visibility — use semi-transparent gradient overlay panel.
Important: overlay only visible in demo; on start it hides. Requirement game start demo mode behind message — yes.
Also "P pauses" — pause overlay with "Paused — press P to resume". Space/Enter restart on gameover; on pause P resumes. Clicking canvas starts / resumes.
Input queue: queue max 2-3, on keydown compute direction; reject if same as last effective or reverse of last queued/current. Then applied at each tick: if queue length, pop and set direction.
Prevent default for arrows/space to avoid scroll.
Best score session: update when eating / game over.
Score display: score, best. Maybe also length. "SCORE" and "BEST".
Let's write code carefully.
Canvas drawing details:
- background: fill '#0b1220'; draw grid cells subtle alternating with alpha, plus board border.
- Actually CSS canvas has border radius; draw full board.
Colors: deep navy backdrop with teal/lime snake? Let's pick palette: bg #070b14, board #0d1626, grid lines #16243a, snake body gradient from #34d399 to #22d3ee, head #a7f3d0? Food coral #fb7185 with glow. Accent amber for scores.
Page background: radial gradient dark with subtle. Fine.
Rounded stroke path: strokeStyle gradient? Use ctx.createLinearGradient across board for snake. Set lineWidth = cell*0.62, lineJoin round, lineCap round. Draw path through interpolated points. Then draw highlight stroke thinner with rgba(255,255,255,0.12) offset? Could draw a second path with lineWidth cell*0.22 and rgba white .10 slightly offset via translate. Keep simple.
Head: circle radius cell*0.42, fill head color, then eyes: compute perpendicular offsets. Direction vector (dx,dy). Eye positions = head + dir*0.13*cell ± perp*0.17*cell. Pupil black radius 0.07 cell offset toward direction 0.035 cell. Draw white eyes radius 0.105 cell.
Food: draw glowing circle with pulsing radius, plus small leaf/stem maybe. Draw outer glow via shadowBlur.
Particles in board coordinate (cell units) to handle resize: store x,y in cell units, vx,vy cells/sec. Draw at x*cell.
Game loop: let lastTime, accumulator. On each frame dt = min((now-last)/1000, 0.1). If state playing or demo: accumulator += dt*1000; while accumulator >= STEP: stepGame(); accumulator -= STEP. render interpolation t = accumulator/STEP. Particles update always (even paused? pause freeze maybe update particles fine). Food pulse uses time.
State: 'demo','playing','paused','gameover'. Demo step uses AI to set direction queue.
Game over: on collision in stepGame: state='gameover'; overlay; update best.
Demo death: set state='demo' but reset game and demo pause timer 700ms maybe. Just reset immediately and keep playing behind overlay; that's fine but slightly jarring. Could set demoResetAt and freeze briefly. Keep simple: on demo death, resetGame(true) and continue.
Input handling:
window.addEventListener('keydown', e => {
const k=e.key.toLowerCase();
if (['arrowup',...,' ','w','a','s','d','p','enter'].includes(k)) e.preventDefault();
if (k==='p') { if(state==='playing') state='paused'; else if(state==='paused') state='playing'; return; }
if (state==='gameover' && (k===' '||k==='enter')) { startGame(); return; }
if (state==='demo') { if (k===' '||k==='enter'||direction key) startGame(); ... }
if (state==='paused') { if (k===' '||k==='enter') state='playing'; maybe }
directions -> queueDirection
});
Careful: in demo, pressing arrow should start game AND apply the direction. Order: startGame() then queue.
Click on canvas: if demo -> startGame(); if gameover -> startGame(); if paused -> resume.
Also "Click or press a key to start" message. Add subtle "P pause · Arrows / WASD move" hints outside overlay.
Demo AI logic: function chooseDemoDirection() {
const head = snake[0]; const dirs = [up,right,down,left] filtered not reverse of direction and not colliding immediately;
For each candidate: simulate new head; if collision skip; compute dist = |nx-food.x|+|ny-food.y|; space = floodCount(nx,ny, snake body excluding tail) capped at e.g. 120; score = -dist*2 + space*1.2 + (dir===currentDirection ? 0.5 : 0); plus avoid adjacency to own body? flood handles.
pick max. If none, keep current (will die).
}
floodCount BFS with set of occupied (snake minus tail since tail moves).
Note demo direction must not reverse onto itself: candidates exclude opposite of current direction.
Input queue handling in demo AI: set direction directly at tick.
Rendering interpolation for demo too.
Score display update in DOM each change.
Layout HTML:
<div class="app">
<header class="topbar"><div class="brand">...<h1>NEON SNAKE</h1><p>...</p></div><div class="stats"><div class="stat"><span>Score</span><strong id="score">0</strong></div>... </div></header>
<main class="stage"><div class="board-wrap"><canvas id="game"></canvas><div class="overlay" id="overlay">...</div></div></main>
<footer class="hints">...</footer>
</div>
Need board size responsive and centered; .board-wrap width: min(620px, calc(100vw - 32px), calc(100vh - 220px)) — but using vh with min could be odd. Use width: min(620px, calc(100vw - 28px), calc(100vh - 210px)); aspect-ratio 1. At 800 height: 800-210=590 -> 590 wide, good. At 375 width: 347. Good.
Total height: header ~90 + board 590 + footer 60 + gaps = ~770 fits 800. Fine.
On small screens header stacks, maybe height larger; use media query reduce sizes and board min with vh. Fine — no horizontal scrollbar: set body overflow-x hidden, use min() with 100vw.
Canvas crispness: on resize set canvas.width = rect.width*dpr etc, ctx.setTransform(dpr,0,0,dpr,0,0) and use CSS pixel coords. Store size = rect.width (square).
Render function uses cell = size / COLS.
Overlay CSS: absolute inset 0, display flex, pointer-events none maybe but click handled on board-wrap. Overlay background: radial-gradient plus backdrop blur. Keep text legible but board visible: rgba(7,11,20,0.72).
Overlay content states via JS set overlay innerHTML? Simpler: overlay has elements: .ov-kicker, .ov-title, .ov-text, .ov-btn, .ov-score. Update text per state.
Demo overlay: title "Click or press a key to start", kicker "DEMO MODE · AI PLAYING", text "Arrow keys or WASD to move · P to pause". Button-ish "START GAME".
Game over: kicker "GAME OVER", title "Run ended", text "Score 120 · Best 240", hint "Press Space or Enter to restart".
Paused: kicker "PAUSED", title "Take a breath", text "Press P to resume".
Ensure overlay doesn't intercept clicks: pointer-events:none, board-wrap handles click.
Now write code.
Potential issue: key ' ' keydown -> e.key is ' '. lowercase fine.
Session best: let best = 0.
Food spawn: while true pick random cell not in snake set.
stepGame():
- apply input queue: if queue.length: const nd = queue.shift(); if not opposite of direction -> direction = nd.
- prevSnake = snake.map(...)
- newHead = ...
- collision: walls or snake body (excluding tail if not growing? For self collision, if the snake isn't growing, the tail cell will move away, so colliding with tail is allowed). Standard: check against snake.slice(0, snake.length-1) when not eating. But we don't know eating until after computing new head. Compute willEat = newHead equals food. body to check = willEat ? snake : snake.slice(0,-1). Fine.
- if collision -> gameOver.
- snake.unshift(newHead); if willEat: score+=10, spawn food, particles, best update; else snake.pop().
Render interpolation: need renderFrom array length snake.length. Compute after step: prevRender = old snake. In render: for i in snake: from = prevRender[i] || prevRender[prevRender.length-1] ... Actually when grown, prevRender length = old length = new length -1; new last index = old length. from = prevRender[i] || prevRender[i-1] || snake[i]. Use that.
But when i indexes beyond prevRender length-1: use prevRender[prevRender.length-1].
Also after reset (startGame), prevRender = snake copy so t interpolation no movement.
Demo mode reset at start: snake initial length 5 moving right, food random.
Initial demo snake should be positioned mid-grid. score 0.
Let me also handle "Quick successive key presses must both be honored" — queue length 3 and applied one per tick.
Now, particles: on eat, spawn 18 particles at food center, colors from palette ['#fb7185','#fbbf24','#34d399','#22d3ee'], velocity random outward 2-7 cells/s, life 0.5-0.9s, size 0.07-0.16 cell.
Render particles after snake/food, before overlay (overlay is DOM anyway).
Food drawing: shadowBlur maybe expensive but fine at 60fps with one food.
Grid: draw lines each cell? 24 lines fine. Or checkerboard rects 576 — fine.
Let me write the CSS with nice typography: font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial. Letter-spacing.
Header brand: little logo square with SVG? Could use CSS square with snake-ish dot. Inline SVG allowed (it's in HTML). Keep simple: a rounded square with gradient and a small dot pattern.
Now produce the final code, checking syntax mentally.
JS structure:
const canvas = ..., ctx = canvas.getContext('2d');
const COLS = 24, ROWS = 24, STEP = 110;
let size = 0, cell = 0, dpr = 1;
function resize() { const rect = canvas.getBoundingClientRect(); dpr = Math.min(window.devicePixelRatio||1, 2); size = rect.width; canvas.width = Math.round(size*dpr); canvas.height = Math.round(size*dpr); ctx.setTransform(dpr,0,0,dpr,0,0); cell = size/COLS; }
Since canvas CSS width/height 100%, rect.width==rect.height.
Direction object: const DIRS = {up:{x:0,y:-1}, ...}. direction = DIRS.right.
opposite(a,b) => a.x===-b.x && a.y===-b.y.
queueDirection(d): const last = queue.length ? queue[queue.length-1] : direction; if (d.x===-last.x && d.y===-last.y) return; if (d.x===last.x&&d.y===last.y) return; if(queue.length<3) queue.push(d).
Also if snake length is 1 no reverse restriction but fine.
startGame(): state='playing'; score=0; snake=..., direction=DIRS.right, queue=[], particles=[], spawnFood(), prevRender=snake.map(...); accumulator=0; updateOverlay(); updateHUD();
resetDemo(): same but state='demo'.
Demo AI in stepGame: if (state==='demo') direction = chooseDemoDirection(); Actually apply queue first then AI override. Do: if state demo { direction = ai; queue.length=0; } else apply queue.
Let's handle render loop:
function frame(now) { const dt = Math.min((now - last)/1000, 0.1); last=now; if (state==='playing'||state==='demo'){ acc += dt*1000; while(acc>=STEP){ stepGame(); acc-=STEP; } } updateParticles(dt); draw(acc/STEP); requestAnimationFrame(frame); }
If state paused/gameover, acc stays; t computed but no step. Fine — draw(t).
updateParticles: p.x += p.vx*dt; p.y += p.vy*dt; p.vy += 4*dt (gravity in cell units?) slight. p.life -= dt.
Now the AI flood fill:
function floodCount(sx,sy, blocked) {
const seen = new Set([sx+','+sy]); const q=[[sx,sy]]; let n=0;
while(q.length && n<200){ const [x,y]=q.pop(); n++; for(const d of Object.values(DIRS)){...if in bounds && !blocked.has(key) && !seen.has(key)) {seen.add(key);q.push([nx,ny]);}}
return n;
}
blocked set built from snake minus tail.
chooseDemoDirection():
const head=snake[0]; let bestD=direction, bestScore=-Infinity;
for(const d of [DIRS.up,DIRS.right,DIRS.down,DIRS.left]) {
if (opposite(d,direction)) continue;
const nx=head.x+d.x, ny=head.y+d.y;
if(nx<0||...) continue;
const willEat = nx===food.x&&ny===food.y;
const bodySet = new Set((willEat?snake:snake.slice(0,-1)).map(s=>s.key));
if(bodySet.has(nx+','+ny)) continue;
const blocked = new Set(snake.slice(0,-1).map(...)); blocked.add? For flood, the new head is occupied too. Compute flood from new head with blocked = snake minus tail (the tail moves). Actually flood from new head; blocked includes snake body except tail.
const space = floodCount(nx,ny,blocked);
const dist = Math.abs(nx-food.x)+Math.abs(ny-food.y);
let sc = space*1.5 - dist*2.4 + (d===direction?0.8:0) + (willEat?12:0);
// penalty for moving near own neck? skip
if(sc>bestScore){bestScore=sc;bestD=d;}
}
return bestD;
Need snake segments have x,y; I'll store plain objects {x,y}.
Food spawn uses Set of keys.
Now draw:
function draw(t) {
ctx.clearRect(0,0,size,size);
// board bg
ctx.fillStyle='#0a1322'; roundRect? canvas fills whole. Use fillRect.
// checker
for y,x: fillStyle = ((x+y)%2? 'rgba(255,255,255,0.018)':'rgba(255,255,255,0.032)') — hmm make subtle.
// grid lines
ctx.strokeStyle='rgba(120,180,220,0.07)'; lineWidth=1; for i...
// vignette maybe skip.
drawFood();
drawSnake(t);
drawParticles();
// border
ctx.strokeStyle='rgba(120,200,255,0.18)'; lineWidth=2; strokeRect(1,1,size-2,size-2);
}
drawSnake: build points array: for i, from = prevRender[i] || prevRender[prevRender.length-1] || snake[i]; p = lerp(from, snake[i], t) + 0.5 cell center. Path stroke with gradient.
Gradient: createLinearGradient(0,0,size,size) with '#2dd4bf','#22d3ee','#34d399'.
Then body highlight: stroke same path with lineWidth cell*0.2, strokeStyle 'rgba(255,255,255,0.13)'.
Head: compute hp = points[0]. radius cell*0.44. Shadow glow: ctx.shadowColor 'rgba(52,211,153,0.55)', shadowBlur cell*0.5.
Eyes facing direction: direction vector uses current direction. perp = {-dy, dx}. eye offset forward 0.12*cell, side 0.17*cell. White ellipse/circle radius 0.115*cell; pupil radius 0.06*cell offset forward 0.045*cell.
Also draw a small tail taper? The stroke with round caps is fine.
Food draw: center = (food.x+0.5)*cell etc. pulse = 1 + Math.sin(time/220)*0.06. glow via shadowBlur cell*0.8, shadowColor '#fb7185'. circle radius cell*0.34*pulse. Inner highlight small white circle offset.
Time variable: performance.now().
OK. Also handle resize with ResizeObserver or window resize.
One thing: canvas rect width computed before layout? Use requestAnimationFrame(init) or resize after DOM load (script at end of body). Add window.addEventListener('resize', resize); resize() immediately.
Now HUD update: scoreEl.textContent = score; bestEl.textContent = best; maybe length stat too — requirement score & best. Add "Length" for polish.
Overlay update function:
function updateOverlay(){
if(state==='demo'){ kicker 'DEMO MODE · AI PLAYING'; title 'Click or press a key to start'; text 'Arrow keys or WASD to move · P to pause'; action 'START GAME'; overlay.classList.remove('hidden') }
else if state playing: hidden
...
}
Overlay visible in game over: show score.
Also maybe "Press P to pause" hints in footer.
Now write the actual final HTML. Ensure no errors: careful with object key naming and functions defined before use (function declarations hoisted).
Let me write it out.
Also "in-page text describing how to use" restriction — but the task explicitly requires the message "Click or press a key to start", so hints are fine.
Edge: pressing Space in demo: startGame() and don't queue direction. Fine.
Key handler code:
window.addEventListener('keydown', (e)=>{
const k = e.key.toLowerCase();
const isDir = k in KEYDIRS;
if (isDir || k===' ' || k==='enter' || k==='p') e.preventDefault();
if (k==='p'){ if(state==='playing'){state='paused';} else if(state==='paused'){state='playing';} updateOverlay(); return; }
if (state==='gameover'){ if(k===' '||k==='enter'){ startGame(); } return; }
if (state==='paused'){ if(k===' '||k==='enter'){state='playing';updateOverlay();} return; }
if (state==='demo'){ startGame(); if(isDir) queueDirection(KEYDIRS[k]); return; }
if (isDir) queueDirection(KEYDIRS[k]);
});
KEYDIRS = {arrowup:DIRS.up, w:DIRS.up, ...}.
Note 'p' key when state demo: handled before start — pressing P in demo just... would not start. Requirement "Click or press a key to start" — any key should start. Move demo check earlier? Let's: if state demo and k!=='p' -> startGame... Hmm "press a key to start" includes P. Let's handle: if (state==='demo'){ startGame(); if(isDir) queue...; if(k==='p'){ /* started, then pause? */ } return; } — simpler: in demo, any key starts game; if it's 'p', also toggle pause? Not needed. Just start.
So reorder: if (k==='p' && (state==='playing'||state==='paused')) toggle; else if state demo start; etc.
Fine.
Click handler on boardWrap.
Also add `tabindex="0"` maybe. Not needed since window listener.
Now writing final answer with only code block.
One more detail: `roundRect` support in Chrome — fine but I'll avoid it.
CSS specifics:
:root { --bg:#070b14; --panel:#0d1526; --line: rgba(148,197,255,.12); --mint:#34d399; --cyan:#22d3ee; --rose:#fb7185; --amber:#fbbf24; --text:#e8f1ff; --muted:#8aa2bd; }
body { min-height:100vh; display:flex; justify-content:center; background: radial-gradient(1200px 700px at 50% -10%, #12233c 0%, #070b14 60%); color:var(--text); font: ... ; overflow-x:hidden; }
.app { width:100%; max-width:1180px; padding: 22px clamp(14px,3vw,34px) 18px; display:flex; flex-direction:column; align-items:center; gap: clamp(12px,2vh,20px); }
.topbar { width:100%; display:flex; align-items:flex-end; justify-content:space-between; gap:18px; flex-wrap:wrap; }
.brand { display:flex; align-items:center; gap:12px; }
.logo { width:42px;height:42px;border-radius:13px; background:linear-gradient(150deg,#22d3ee,#34d399 55%, #0ea5e9); box-shadow: 0 10px 28px rgba(34,211,238,.28); position:relative;}
.logo::after { content:''; position:absolute; inset:12px; border-radius:50%; background:#06111f; box-shadow: 9px 0 0 -3px #06111f? } hmm. Simpler: logo has ::before dot.
h1 font-size clamp(20px,2.4vw,28px); letter-spacing .18em; font-weight 800.
.stats { display:flex; gap:10px; }
.stat { min-width:92px; padding:10px 16px 11px; border:1px solid var(--line); border-radius:14px; background: linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.015)); }
.stat span { display:block; font-size:10px; letter-spacing:.22em; color:var(--muted); text-transform:uppercase;}
.stat strong { font-size:26px; font-variant-numeric: tabular-nums; }
.stat.best strong { color: var(--amber) } .stat.score strong { color: var(--mint) }
.stage { width:100%; display:flex; justify-content:center; }
.board-wrap { position:relative; width:min(620px, calc(100vw - 28px), calc(100vh - 232px)); aspect-ratio:1/1; border-radius:22px; overflow:hidden; box-shadow: 0 30px 70px rgba(0,0,0,.55), 0 0 0 1px rgba(126,200,255,.14), 0 0 60px rgba(34,211,238,.10); cursor:pointer; }
Hmm at 800px height: 800-232=568 board. Topbar ~70 + gaps 30 + footer 40 = 140 -> 568+140 = 708 fits. Good.
canvas { display:block; width:100%; height:100%; }
.overlay { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; text-align:center; padding:22px; background: radial-gradient(120% 90% at 50% 40%, rgba(7,11,20,.55), rgba(7,11,20,.86)); backdrop-filter: blur(3px); transition: opacity .25s ease; }
.overlay.hidden { opacity:0; pointer-events:none; }
.ov-kicker letter-spacing .28em; font-size 10px; color var(--cyan);
.ov-title font-size clamp(20px, 3.4vw, 32px); font-weight 800; letter-spacing .01em;
.ov-text color muted font-size 14px max-width 320px line-height 1.6;
.ov-btn margin-top 8px; padding 11px 20px; border-radius 999px; background linear-gradient(135deg,#22d3ee,#34d399); color:#04202b; font-weight 800; letter-spacing .12em; font-size 11px;
.hints { display:flex; gap:8px; flex-wrap:wrap; justify-content:center; }
.hint { padding:7px 13px; border:1px solid var(--line); border-radius:999px; color:var(--muted); font-size:11px; letter-spacing:.12em; text-transform:uppercase; background:rgba(255,255,255,.025);}
.hint b { color:var(--text); font-weight:700; }
Media query max-width 560: topbar column align flex-start? Keep stats full width with 3 columns equal; board width calc(100vw - 24px). Reduce hints font.
Fine. Write the code now. Also ensure `updateOverlay` sets button text.
For game over overlay text: `Score ${score} · Best ${best}`.
Let me finalize the JS carefully including initial call: resize(); resetDemo(); requestAnimationFrame(frame). Need last = performance.now().
Careful with `frame` first call: set last inside.
Write out now.