DeepSeek V4.1 Flash 1017
by DeepSeekReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 23.8 s
- Cost
- $0.0058
- Output tokens
- 7,895
- Spent thinking
- 0 tokens
- Speed
- 340.5 tok/s
- Console errors
- 0
- First token after
- 604 ms
- First visible text after
- 604 ms
- Prompt tokens
- 493
- Code size
- 23.4 KB
- Finish reason
- stop
- Served by
- DigitalOcean
- Model id
deepseek/deepseek-v4.1-flash-1017- Reasoning sent
{"effort":"none"}- Max output
- 64,000 tokens
- Generated
- Nov 10, 2026
What it was asked
The prompt
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 851 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Polished Snake</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0e1a;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
color: #e0e6f0;
overflow: hidden;
}
.game-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 40px;
}
.score-box {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(30, 40, 65, 0.7);
padding: 10px 24px;
border-radius: 12px;
border: 1px solid rgba(80, 120, 200, 0.3);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 100px;
}
.score-label {
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: #6a80a8;
margin-bottom: 2px;
}
.score-value {
font-size: 26px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
#score { color: #5eff9e; text-shadow: 0 0 12px rgba(94, 255, 158, 0.4); }
#best { color: #ffcf5e; text-shadow: 0 0 12px rgba(255, 207, 94, 0.4); }
.canvas-wrap {
position: relative;
border-radius: 16px;
padding: 3px;
background: linear-gradient(135deg, rgba(80,120,200,0.5), rgba(94,255,158,0.2), rgba(80,120,200,0.3));
box-shadow: 0 0 40px rgba(40, 80, 160, 0.3), 0 8px 40px rgba(0,0,0,0.5);
}
canvas {
display: block;
border-radius: 14px;
background: #0d1424;
}
.overlay {
position: absolute;
inset: 3px;
border-radius: 14px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
background: rgba(8, 12, 22, 0.82);
backdrop-filter: blur(4px);
text-align: center;
padding: 20px;
transition: opacity 0.25s ease;
}
.overlay.hidden { opacity: 0; pointer-events: none; }
.overlay h1 {
font-size: 38px;
font-weight: 800;
letter-spacing: 1px;
background: linear-gradient(135deg, #5eff9e, #4ab8ff);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.overlay h2 {
font-size: 32px;
font-weight: 800;
color: #ff5e7e;
text-shadow: 0 0 20px rgba(255, 94, 126, 0.5);
}
.overlay p {
font-size: 15px;
color: #9fb0cc;
line-height: 1.6;
}
.overlay .final-score {
font-size: 22px;
font-weight: 700;
color: #5eff9e;
}
.overlay .key-hint {
display: inline-block;
background: rgba(80, 120, 200, 0.25);
border: 1px solid rgba(100, 150, 230, 0.4);
border-radius: 6px;
padding: 2px 8px;
font-size: 13px;
color: #cfe0ff;
margin: 0 2px;
}
.controls-hint {
font-size: 12px;
color: #556890;
letter-spacing: 1px;
}
@media (max-width: 600px) {
.overlay h1 { font-size: 28px; }
.overlay h2 { font-size: 24px; }
.header { gap: 16px; }
.score-box { padding: 8px 16px; min-width: 80px; }
.score-value { font-size: 20px; }
.controls-hint { display: none; }
}
</style>
</head>
<body>
<div class="game-container">
<div class="header">
<div class="score-box">
<span class="score-label">Score</span>
<span class="score-value" id="score">0</span>
</div>
<div class="score-box">
<span class="score-label">Best</span>
<span class="score-value" id="best">0</span>
</div>
</div>
<div class="canvas-wrap">
<canvas id="game"></canvas>
<div class="overlay" id="overlay">
<h1>🐍 SNAKE</h1>
<p>Watch the AI play, or jump in yourself!</p>
<p><span class="key-hint">Arrow Keys</span> / <span class="key-hint">WASD</span> to move</p>
<p style="color:#5eff9e;font-weight:600;">Click or press a key to start</p>
</div>
</div>
<div class="controls-hint">SPACE / ENTER restart · P pause</div>
</div>
<script>
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const COLS = 24, ROWS = 24;
const CELL = 26;
const W = COLS * CELL, H = ROWS * CELL;
canvas.width = W;
canvas.height = H;
// Responsive sizing
function fitCanvas() {
const maxW = Math.min(window.innerWidth - 60, 720);
const maxH = window.innerHeight - 220;
const size = Math.min(maxW, maxH, W);
canvas.style.width = size + 'px';
canvas.style.height = size + 'px';
}
window.addEventListener('resize', fitCanvas);
fitCanvas();
const DIRS = {
up: { x: 0, y: -1 },
down: { x: 0, y: 1 },
left: { x: -1, y: 0 },
right: { x: 1, y: 0 }
};
const state = {
snake: [],
dir: { x: 1, y: 0 },
queue: [],
food: { x: 0, y: 0 },
score: 0,
best: 0,
grow: 0,
running: false,
paused: false,
gameOver: false,
started: false,
particles: [],
particlesTime: 0,
moveInterval: 130,
acc: 0,
prevTime: 0,
aiMode: true,
smooth: [],
eatFlash: 0,
deathFlash: 0
};
function reset(keepBest = true) {
const cx = Math.floor(COLS / 2), cy = Math.floor(ROWS / 2);
state.snake = [
{ x: cx, y: cy },
{ x: cx - 1, y: cy },
{ x: cx - 2, y: cy }
];
state.dir = { x: 1, y: 0 };
state.queue = [];
state.score = 0;
state.grow = 0;
state.gameOver = false;
state.paused = false;
state.running = true;
state.particles = [];
state.eatFlash = 0;
state.deathFlash = 0;
state.acc = 0;
state.smooth = state.snake.map(s => ({ x: s.x, y: s.y }));
if (!keepBest) state.best = 0;
scoreEl.textContent = '0';
bestEl.textContent = state.best;
spawnFood();
}
function spawnFood() {
const occupied = new Set(state.snake.map(s => s.x + ',' + s.y));
const free = [];
for (let y = 0; y < ROWS; y++)
for (let x = 0; x < COLS; x++)
if (!occupied.has(x + ',' + y)) free.push({ x, y });
if (free.length === 0) return;
state.food = free[Math.floor(Math.random() * free.length)];
}
function queueDir(name) {
const d = DIRS[name];
if (!d) return;
const last = state.queue.length ? state.queue[state.queue.length - 1] : state.dir;
// prevent reversing
if (d.x === -last.x && d.y === -last.y) return;
if (d.x === last.x && d.y === last.y) return;
if (state.queue.length >= 3) return;
state.queue.push(d);
}
function step() {
if (state.queue.length) {
state.dir = state.queue.shift();
}
const head = state.snake[0];
const nx = head.x + state.dir.x;
const ny = head.y + state.dir.y;
// Wall collision
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) {
return die();
}
// Self collision (ignore tail cell if not growing, since it will move)
const willGrow = state.grow > 0;
for (let i = 0; i < state.snake.length; i++) {
if (i === state.snake.length - 1 && !willGrow) continue;
if (state.snake[i].x === nx && state.snake[i].y === ny) {
return die();
}
}
state.snake.unshift({ x: nx, y: ny });
if (nx === state.food.x && ny === state.food.y) {
state.score += 10;
scoreEl.textContent = state.score;
if (state.score > state.best) {
state.best = state.score;
bestEl.textContent = state.best;
}
state.grow += 1;
burstFood(state.food.x, state.food.y);
state.eatFlash = 1;
spawnFood();
}
if (state.grow > 0) {
state.grow--;
} else {
state.snake.pop();
}
}
function die() {
state.gameOver = true;
state.running = false;
state.deathFlash = 1;
if (state.best < state.score) {
state.best = state.score;
bestEl.textContent = state.best;
}
showOverlay('gameover');
}
function burstFood(gx, gy) {
const cx = (gx + 0.5) * CELL;
const cy = (gy + 0.5) * CELL;
const colors = ['#ff5e7e', '#ffcf5e', '#5eff9e', '#4ab8ff', '#ffffff'];
for (let i = 0; i < 22; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 40 + Math.random() * 160;
state.particles.push({
x: cx, y: cy,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp,
life: 1,
decay: 1.5 + Math.random() * 1.5,
size: 2 + Math.random() * 3,
color: colors[Math.floor(Math.random() * colors.length)]
});
}
}
// ---- AI ----
function aiDecide() {
const head = state.snake[0];
const occupied = new Set(state.snake.map(s => s.x + ',' + s.y));
// remove tail
const tail = state.snake[state.snake.length - 1];
occupied.delete(tail.x + ',' + tail.y);
// BFS to food avoiding occupied
const dirs = [DIRS.up, DIRS.down, DIRS.left, DIRS.right];
let bestPath = null;
const queue = [{ x: head.x, y: head.y, path: [] }];
const seen = new Set([head.x + ',' + head.y]);
while (queue.length) {
const cur = queue.shift();
if (cur.x === state.food.x && cur.y === state.food.y) {
bestPath = cur.path;
break;
}
if (cur.path.length > 200) continue;
for (const d of dirs) {
const nx = cur.x + d.x, ny = cur.y + d.y;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const key = nx + ',' + ny;
if (seen.has(key)) continue;
if (occupied.has(key)) continue;
seen.add(key);
queue.push({ x: nx, y: ny, path: [...cur.path, d] });
}
}
if (bestPath && bestPath.length) {
queueDirFrom(bestPath[0]);
return;
}
// No path: pick a safe direction with most free space (flood fill)
let bestDir = null, bestSpace = -1;
for (const d of dirs) {
const nx = head.x + d.x, ny = head.y + d.y;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const key = nx + ',' + ny;
if (occupied.has(key)) continue;
// don't reverse
if (d.x === -state.dir.x && d.y === -state.dir.y) continue;
const space = floodFill(nx, ny, occupied);
if (space > bestSpace) {
bestSpace = space;
bestDir = d;
}
}
if (bestDir) {
queueDirFrom(bestDir);
}
}
function queueDirFrom(d) {
const last = state.queue.length ? state.queue[state.queue.length - 1] : state.dir;
if (d.x === -last.x && d.y === -last.y) return;
if (d.x === last.x && d.y === last.y) return;
if (state.queue.length >= 3) return;
state.queue.push(d);
}
function floodFill(sx, sy, occupied) {
const seen = new Set([sx + ',' + sy]);
const stack = [{ x: sx, y: sy }];
let count = 0;
while (stack.length && count < 400) {
const c = stack.pop();
count++;
const dirs = [DIRS.up, DIRS.down, DIRS.left, DIRS.right];
for (const d of dirs) {
const nx = c.x + d.x, ny = c.y + d.y;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const key = nx + ',' + ny;
if (seen.has(key)) continue;
if (occupied.has(key)) continue;
seen.add(key);
stack.push({ x: nx, y: ny });
}
}
return count;
}
// ---- Rendering ----
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function drawGrid() {
ctx.fillStyle = '#0d1424';
ctx.fillRect(0, 0, W, H);
// subtle checker
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if ((x + y) % 2 === 0) {
ctx.fillStyle = 'rgba(30,45,75,0.25)';
ctx.fillRect(x * CELL, y * CELL, CELL, CELL);
}
}
}
// grid lines
ctx.strokeStyle = 'rgba(100,140,200,0.06)';
ctx.lineWidth = 1;
for (let x = 0; x <= COLS; x++) {
ctx.beginPath();
ctx.moveTo(x * CELL + 0.5, 0);
ctx.lineTo(x * CELL + 0.5, H);
ctx.stroke();
}
for (let y = 0; y <= ROWS; y++) {
ctx.beginPath();
ctx.moveTo(0, y * CELL + 0.5);
ctx.lineTo(W, y * CELL + 0.5);
ctx.stroke();
}
}
function drawFood(time) {
const cx = (state.food.x + 0.5) * CELL;
const cy = (state.food.y + 0.5) * CELL;
const pulse = 1 + Math.sin(time * 0.006) * 0.12;
const r = CELL * 0.32 * pulse;
// glow
const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, CELL * 1.1);
grad.addColorStop(0, 'rgba(255,94,126,0.35)');
grad.addColorStop(1, 'rgba(255,94,126,0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(cx, cy, CELL * 1.1, 0, Math.PI * 2);
ctx.fill();
// body
const bg = ctx.createRadialGradient(cx - r * 0.3, cy - r * 0.3, 0, cx, cy, r);
bg.addColorStop(0, '#ffb0c0');
bg.addColorStop(0.5, '#ff5e7e');
bg.addColorStop(1, '#c2264a');
ctx.fillStyle = bg;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
// highlight
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.beginPath();
ctx.arc(cx - r * 0.3, cy - r * 0.35, r * 0.28, 0, Math.PI * 2);
ctx.fill();
// stem
ctx.strokeStyle = '#5eff9e';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(cx, cy - r);
ctx.quadraticCurveTo(cx + 3, cy - r - 6, cx + 7, cy - r - 7);
ctx.stroke();
}
function drawSnake(t) {
const n = state.snake.length;
const segPx = CELL;
// Interpolate positions between grid cells using accumulator progress
const progress = state.running && !state.gameOver ? Math.min(state.acc / state.moveInterval, 1) : 0;
// Build a smooth polyline of centers
const points = [];
for (let i = 0; i < n; i++) {
const s = state.snake[i];
let px = s.x, py = s.y;
if (i === 0 && state.running && !state.gameOver) {
// head moves toward next
const nx = s.x + state.dir.x;
const ny = s.y + state.dir.y;
if (nx >= 0 && ny >= 0 && nx < COLS && ny < ROWS) {
px = s.x + state.dir.x * progress;
py = s.y + state.dir.y * progress;
}
} else if (i > 0) {
// tail follows: previous cell's movement — for smoothness, interpolate body
// Body cells just snap to their grid positions; head interpolation is the main visual.
}
points.push({ x: (px + 0.5) * segPx, y: (py + 0.5) * segPx });
}
// Draw body as thick line with rounded caps
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// Outer glow
ctx.strokeStyle = 'rgba(94,255,158,0.12)';
ctx.lineWidth = CELL * 0.95;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
ctx.stroke();
// Body gradient
const headPt = points[0];
const tailPt = points[points.length - 1];
const g = ctx.createLinearGradient(headPt.x, headPt.y, tailPt.x, tailPt.y);
g.addColorStop(0, '#7dffb8');
g.addColorStop(0.5, '#3ddc84');
g.addColorStop(1, '#1f8f55');
ctx.strokeStyle = g;
ctx.lineWidth = CELL * 0.78;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
ctx.stroke();
// Inner highlight
ctx.strokeStyle = 'rgba(200,255,220,0.25)';
ctx.lineWidth = CELL * 0.28;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
ctx.stroke();
// Draw scales dots along body
for (let i = 1; i < points.length; i += 1) {
ctx.fillStyle = 'rgba(10,60,35,0.25)';
ctx.beginPath();
ctx.arc(points[i].x, points[i].y, CELL * 0.08, 0, Math.PI * 2);
ctx.fill();
}
// Head
if (points.length) {
const hx = points[0].x, hy = points[0].y;
const hr = CELL * 0.46;
// head glow
const hg = ctx.createRadialGradient(hx, hy, 0, hx, hy, hr * 1.6);
hg.addColorStop(0, 'rgba(125,255,184,0.35)');
hg.addColorStop(1, 'rgba(125,255,184,0)');
ctx.fillStyle = hg;
ctx.beginPath();
ctx.arc(hx, hy, hr * 1.6, 0, Math.PI * 2);
ctx.fill();
// head shape
const hgrad = ctx.createRadialGradient(hx - hr*0.3, hy - hr*0.3, 0, hx, hy, hr);
hgrad.addColorStop(0, '#b8ffd6');
hgrad.addColorStop(0.6, '#5eff9e');
hgrad.addColorStop(1, '#2bb56a');
ctx.fillStyle = hgrad;
ctx.beginPath();
ctx.arc(hx, hy, hr, 0, Math.PI * 2);
ctx.fill();
// Eyes
const d = state.dir;
const perp = { x: -d.y, y: d.x };
const eyeDist = hr * 0.42;
const eyeOff = hr * 0.46;
const eyeR = hr * 0.26;
for (const side of [-1, 1]) {
const ex = hx + d.x * eyeOff + perp.x * eyeDist * side;
const ey = hy + d.y * eyeOff + perp.y * eyeDist * side;
// white
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(ex, ey, eyeR, 0, Math.PI * 2);
ctx.fill();
// pupil
ctx.fillStyle = '#0a1420';
ctx.beginPath();
ctx.arc(ex + d.x * eyeR * 0.45, ey + d.y * eyeR * 0.45, eyeR * 0.5, 0, Math.PI * 2);
ctx.fill();
// glint
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.beginPath();
ctx.arc(ex - eyeR*0.25 + d.x*eyeR*0.45, ey - eyeR*0.25 + d.y*eyeR*0.45, eyeR*0.18, 0, Math.PI*2);
ctx.fill();
}
// tongue
if (state.running && Math.sin(t * 0.004) > 0.5) {
const tongueBase = { x: hx + d.x * hr, y: hy + d.y * hr };
ctx.strokeStyle = '#ff5e7e';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(tongueBase.x, tongueBase.y);
const tipX = tongueBase.x + d.x * hr * 0.7;
const tipY = tongueBase.y + d.y * hr * 0.7;
ctx.lineTo(tipX, tipY);
ctx.stroke();
// fork
ctx.beginPath();
ctx.moveTo(tipX, tipY);
ctx.lineTo(tipX + perp.x * 4 + d.x * 4, tipY + perp.y * 4 + d.y * 4);
ctx.moveTo(tipX, tipY);
ctx.lineTo(tipX - perp.x * 4 + d.x * 4, tipY - perp.y * 4 + d.y * 4);
ctx.stroke();
}
}
}
function drawParticles(dt) {
for (let i = state.particles.length - 1; i >= 0; i--) {
const p = state.particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= 0.96;
p.vy *= 0.96;
p.life -= p.decay * dt;
if (p.life <= 0) {
state.particles.splice(i, 1);
continue;
}
const a = Math.max(0, p.life);
ctx.globalAlpha = a;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * a, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawFlash() {
if (state.eatFlash > 0) {
ctx.fillStyle = `rgba(94,255,158,${state.eatFlash * 0.06})`;
ctx.fillRect(0, 0, W, H);
}
if (state.deathFlash > 0) {
ctx.fillStyle = `rgba(255,94,126,${state.deathFlash * 0.25})`;
ctx.fillRect(0, 0, W, H);
}
}
// ---- Overlay ----
function showOverlay(type) {
overlay.classList.remove('hidden');
if (type === 'start') {
overlay.innerHTML = `
<h1>🐍 SNAKE</h1>
<p>Watch the AI play, or jump in yourself!</p>
<p><span class="key-hint">Arrow Keys</span> / <span class="key-hint">WASD</span> to move</p>
<p style="color:#5eff9e;font-weight:600;">Click or press a key to start</p>
`;
} else if (type === 'gameover') {
overlay.innerHTML = `
<h2>GAME OVER</h2>
<p class="final-score">Score: ${state.score}</p>
<p>Best: ${state.best}</p>
<p>Press <span class="key-hint">SPACE</span> or <span class="key-hint">ENTER</span> to restart</p>
<p style="color:#5eff9e;font-weight:600;">Click or press a key to play</p>
`;
} else if (type === 'paused') {
overlay.innerHTML = `
<h2 style="color:#4ab8ff;">PAUSED</h2>
<p>Press <span class="key-hint">P</span> to resume</p>
`;
}
}
function hideOverlay() {
overlay.classList.add('hidden');
}
// ---- Input ----
const keyToDir = {
'ArrowUp': 'up', 'ArrowDown': 'down', 'ArrowLeft': 'left', 'ArrowRight': 'right',
'w': 'up', 'W': 'up', 's': 'down', 'S': 'down', 'a': 'left', 'A': 'left', 'd': 'right', 'D': 'right'
};
function handleStart() {
if (!state.started) {
state.started = true;
state.aiMode = false;
reset();
hideOverlay();
return;
}
}
window.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
if (!state.started) {
state.started = true;
state.aiMode = false;
reset();
hideOverlay();
e.preventDefault();
return;
}
if (state.gameOver) {
state.aiMode = false;
reset();
hideOverlay();
e.preventDefault();
return;
}
e.preventDefault();
return;
}
if (e.key === 'p' || e.key === 'P') {
if (state.running && !state.gameOver) {
state.paused = !state.paused;
if (state.paused) showOverlay('paused'); else hideOverlay();
}
return;
}
if (keyToDir[e.key]) {
e.preventDefault();
if (!state.started) {
state.started = true;
state.aiMode = false;
reset();
hideOverlay();
queueDir(keyToDir[e.key]);
return;
}
if (state.gameOver) {
state.aiMode = false;
reset();
hideOverlay();
queueDir(keyToDir[e.key]);
return;
}
if (state.paused) return;
state.aiMode = false;
queueDir(keyToDir[e.key]);
}
});
canvas.addEventListener('click', () => {
handleStart();
});
overlay.addEventListener('click', () => {
handleStart();
});
// Start in AI demo mode immediately
state.started = true;
state.aiMode = true;
reset();
// Keep overlay visible on start actually? Requirement says demo mode behind message.
// We'll keep overlay hidden for demo, but show a small "press key" hint — actually
// requirement: demo mode behind a message. Let's keep overlay shown briefly, then fade.
// Better: show the overlay but semi-transparent so AI demo visible behind it.
// We'll make overlay backdrop lighter in demo.
// Show start overlay for demo mode (it says click or press key to start)
showOverlay('start');
overlay.style.background = 'rgba(8,12,22,0.45)';
// ---- Main Loop ----
let lastTime = performance.now();
function loop(now) {
const dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
// Update flashes
if (state.eatFlash > 0) state.eatFlash = Math.max(0, state.eatFlash - dt * 4);
if (state.deathFlash > 0) state.deathFlash = Math.max(0, state.deathFlash - dt * 1.5);
if (state.running && !state.paused && !state.gameOver) {
state.acc += dt * 1000 * (state.aiMode ? 1 : 1);
while (state.acc >= state.moveInterval) {
state.acc -= state.moveInterval;
if (state.aiMode) aiDecide();
step();
if (state.gameOver) break;
}
} else {
// keep acc modest
state.acc = Math.min(state.acc, state.moveInterval);
}
// Render
drawGrid();
drawFood(now);
drawSnake(now);
drawParticles(dt);
drawFlash();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>