GPT-6 Luna
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 9 s
- Cost
- $0.0082
- Output tokens
- 16,320
- Spent thinking
- 7,077 tokens
- Speed
- 137.3 tok/s
- Console errors
- 0
- First token after
- 10.5 s
- First visible text after
- 1 min 14 s
- Prompt tokens
- 493
- Code size
- 28.2 KB
- Finish reason
- stop
- Served by
- OpenAI
- Model id
openai/gpt-6-luna- Reasoning sent
{"effort":"high"}- 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 · 1027 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Snake — Arcade Edition</title>
<style>
:root {
color-scheme: dark;
--bg: #07110f;
--panel: #0d1a17;
--line: rgba(173, 226, 196, .12);
--muted: #849992;
--text: #eef8f1;
--lime: #b9f86a;
--mint: #70e6a1;
}
* { box-sizing: border-box; }
html, body { min-height: 100%; }
body {
margin: 0;
color: var(--text);
background:
radial-gradient(ellipse at 50% -22%, rgba(66, 136, 91, .22), transparent 58%),
radial-gradient(circle at 8% 70%, rgba(23, 88, 63, .12), transparent 28%),
var(--bg);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
overflow-x: hidden;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: .16;
background-image:
linear-gradient(rgba(180, 255, 212, .035) 1px, transparent 1px),
linear-gradient(90deg, rgba(180, 255, 212, .035) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: linear-gradient(to bottom, black, transparent 85%);
}
.app {
width: min(100% - 32px, 740px);
margin: 0 auto;
padding: 17px 0 18px;
position: relative;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.brand {
display: flex;
align-items: center;
gap: 11px;
min-width: 0;
}
.brand-mark {
width: 39px;
height: 39px;
flex: 0 0 auto;
display: grid;
place-items: center;
border: 1px solid rgba(185, 248, 106, .25);
border-radius: 13px;
background: linear-gradient(145deg, rgba(185, 248, 106, .15), rgba(112, 230, 161, .045));
box-shadow: inset 0 1px rgba(255,255,255,.07), 0 5px 22px rgba(115, 227, 128, .07);
}
.brand-mark svg { width: 23px; height: 23px; }
.brand-copy { min-width: 0; }
h1 {
margin: 0;
font-size: 23px;
line-height: 1.06;
font-weight: 780;
letter-spacing: -.045em;
}
.subtitle {
margin-top: 4px;
color: #80958b;
font-size: 9px;
font-weight: 750;
letter-spacing: .16em;
text-transform: uppercase;
}
.top-actions {
display: flex;
align-items: center;
gap: 10px;
}
.run-status {
display: inline-flex;
align-items: center;
gap: 7px;
color: #b7c8bf;
font-size: 10px;
font-weight: 750;
letter-spacing: .09em;
text-transform: uppercase;
white-space: nowrap;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--mint);
box-shadow: 0 0 10px rgba(112, 230, 161, .8);
animation: blink 1.5s ease-in-out infinite;
}
.run-status.paused .status-dot { background: #ffd16a; box-shadow: 0 0 10px #ffd16a; }
.run-status.ended .status-dot { background: #ff806c; box-shadow: 0 0 10px #ff806c; }
@keyframes blink { 50% { opacity: .45; } }
button {
font: inherit;
color: inherit;
}
.new-game {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
padding: 9px 12px;
border: 1px solid rgba(200, 237, 214, .15);
border-radius: 10px;
background: rgba(255,255,255,.035);
color: #dcebe1;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: .18s ease;
}
.new-game:hover {
background: rgba(185, 248, 106, .1);
border-color: rgba(185, 248, 106, .35);
transform: translateY(-1px);
}
.new-game svg { width: 13px; height: 13px; color: var(--lime); }
.scoreboard {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 12px;
}
.score-card {
min-height: 58px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 15px;
overflow: hidden;
position: relative;
border: 1px solid var(--line);
border-radius: 13px;
background: linear-gradient(135deg, rgba(18, 35, 29, .95), rgba(11, 24, 20, .93));
box-shadow: inset 0 1px rgba(255,255,255,.025);
}
.score-card::after {
content: "";
width: 74px;
height: 74px;
position: absolute;
right: 22px;
top: -44px;
border-radius: 50%;
background: rgba(185, 248, 106, .07);
filter: blur(17px);
pointer-events: none;
}
.score-info { display: grid; gap: 4px; }
.score-label {
color: #879d91;
font-size: 9px;
font-weight: 800;
letter-spacing: .14em;
text-transform: uppercase;
}
.score-value {
font-size: 22px;
line-height: 1;
font-weight: 780;
font-variant-numeric: tabular-nums;
letter-spacing: -.04em;
}
.score-card.current .score-value { color: var(--lime); }
.score-card.best .score-value { color: #eaf5ed; }
.score-icon {
z-index: 1;
width: 31px;
height: 31px;
display: grid;
place-items: center;
border-radius: 10px;
color: var(--lime);
background: rgba(185, 248, 106, .09);
}
.score-card.best .score-icon {
color: #f5c96c;
background: rgba(245, 201, 108, .09);
}
.score-icon svg { width: 16px; height: 16px; }
.board-frame {
position: relative;
width: 100%;
overflow: hidden;
border: 1px solid rgba(156, 219, 178, .19);
border-radius: 16px;
background: #0a1713;
box-shadow:
0 18px 55px rgba(0,0,0,.25),
0 0 0 5px rgba(151, 225, 174, .018),
inset 0 1px rgba(255,255,255,.05);
}
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 26 / 20;
touch-action: none;
}
.game-overlay {
position: absolute;
inset: 0;
z-index: 2;
display: grid;
place-items: center;
padding: 18px;
background: radial-gradient(ellipse at center, rgba(5, 15, 11, .39), rgba(5, 15, 11, .05) 72%);
pointer-events: none;
transition: opacity .22s ease, visibility .22s ease;
}
.game-overlay.hidden { opacity: 0; visibility: hidden; }
.overlay-card {
width: min(100%, 340px);
padding: 20px 23px 18px;
text-align: center;
border: 1px solid rgba(183, 231, 196, .21);
border-radius: 16px;
background: linear-gradient(150deg, rgba(17, 34, 27, .94), rgba(8, 20, 15, .94));
box-shadow: 0 18px 50px rgba(0,0,0,.4), inset 0 1px rgba(255,255,255,.055);
backdrop-filter: blur(8px);
animation: card-in .28s ease-out both;
}
@keyframes card-in {
from { transform: translateY(7px) scale(.98); opacity: 0; }
to { transform: translateY(0) scale(1); opacity: 1; }
}
.overlay-kicker {
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--lime);
font-size: 9px;
font-weight: 850;
letter-spacing: .16em;
text-transform: uppercase;
}
.overlay-kicker .status-dot { width: 6px; height: 6px; }
.overlay-card h2 {
margin: 9px 0 5px;
font-size: 22px;
letter-spacing: -.04em;
}
.overlay-card p {
margin: 0;
color: #9aada2;
font-size: 12px;
line-height: 1.5;
}
.start-prompt {
margin-top: 14px;
padding: 9px 10px;
border-radius: 9px;
background: rgba(185, 248, 106, .075);
color: #dff3df;
font-size: 11px;
font-weight: 730;
}
.start-prompt strong { color: var(--lime); }
.overlay-score {
margin: 9px 0 3px;
color: var(--lime);
font-size: 35px;
line-height: 1;
font-weight: 820;
font-variant-numeric: tabular-nums;
letter-spacing: -.05em;
}
.overlay-note {
margin-top: 13px;
color: #b6c8bc;
font-size: 10px;
font-weight: 750;
letter-spacing: .08em;
text-transform: uppercase;
}
.help-row {
min-height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 2px;
color: #8b9e93;
font-size: 10px;
}
.controls, .key-group {
display: flex;
align-items: center;
gap: 6px;
}
.keycap {
min-width: 22px;
height: 21px;
padding: 0 5px;
display: inline-grid;
place-items: center;
border: 1px solid rgba(197, 229, 207, .16);
border-bottom-color: rgba(197, 229, 207, .26);
border-radius: 6px;
background: rgba(255,255,255,.035);
color: #d5e3d9;
font-size: 9px;
font-weight: 750;
line-height: 1;
}
.keycap.wide { min-width: 35px; }
.control-label { color: #82958a; margin-left: 3px; }
.help-divider { height: 16px; width: 1px; background: rgba(196, 222, 205, .13); }
@media (max-width: 520px) {
.app { width: min(100% - 24px, 740px); padding-top: 12px; }
.topbar { margin-bottom: 11px; }
.brand-mark { width: 35px; height: 35px; border-radius: 11px; }
h1 { font-size: 20px; }
.subtitle { font-size: 8px; }
.run-status { font-size: 8px; gap: 5px; }
.new-game { padding: 8px 9px; font-size: 10px; }
.scoreboard { gap: 8px; margin-bottom: 9px; }
.score-card { min-height: 54px; padding: 8px 11px; }
.score-value { font-size: 20px; }
.score-icon { width: 28px; height: 28px; }
.board-frame { border-radius: 13px; }
.help-row { font-size: 9px; }
.keycap { min-width: 20px; }
}
@media (max-width: 390px) {
.run-status { display: none; }
.help-row { gap: 7px; }
.controls, .key-group { gap: 4px; }
.keycap { min-width: 18px; height: 20px; padding: 0 4px; }
.keycap.wide { min-width: 31px; }
.overlay-card { padding: 17px 15px 15px; }
}
</style>
</head>
<body>
<main class="app">
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 7.5h8.4a3.1 3.1 0 1 1 0 6.2H9.4a3.1 3.1 0 1 0 0 6.2H19" stroke="#b9f86a" stroke-width="2.8" stroke-linecap="round"/>
<circle cx="16.5" cy="10.6" r="1.1" fill="#07110f"/>
</svg>
</div>
<div class="brand-copy">
<h1>Snake</h1>
<div class="subtitle">Arcade edition · 01</div>
</div>
</div>
<div class="top-actions">
<div class="run-status" id="runStatus"><span class="status-dot"></span><span id="statusText">Demo autopilot</span></div>
<button class="new-game" id="newGame" type="button" aria-label="Start a new game">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M13 5.1V2.8m0 0h-2.3M13 2.8 10.9 5M12.6 8a4.7 4.7 0 1 1-1.1-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
New game
</button>
</div>
</header>
<section class="scoreboard" aria-label="Scores">
<div class="score-card current">
<div class="score-info">
<span class="score-label">Current score</span>
<span class="score-value" id="score">0</span>
</div>
<div class="score-icon" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none"><path d="M10 2.2v15.6M14.4 5.2H8a2.5 2.5 0 1 0 0 5h4a2.5 2.5 0 1 1 0 5H5.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
</div>
</div>
<div class="score-card best">
<div class="score-info">
<span class="score-label">Session best</span>
<span class="score-value" id="best">0</span>
</div>
<div class="score-icon" aria-hidden="true">
<svg viewBox="0 0 20 20" fill="none"><path d="m10 2 2.3 4.7 5.2.8-3.8 3.7.9 5.2-4.6-2.5-4.6 2.5.9-5.2L2.5 7.5l5.2-.8L10 2Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
</div>
</div>
</section>
<section class="board-frame" aria-label="Snake game board">
<canvas id="game" aria-label="Snake game on a 26 by 20 grid"></canvas>
<div class="game-overlay" id="overlay"></div>
</section>
<footer class="help-row">
<div class="controls">
<div class="key-group"><span class="keycap">↑</span><span class="keycap">←</span><span class="keycap">↓</span><span class="keycap">→</span></div>
<span class="control-label">or WASD to steer</span>
</div>
<span class="help-divider"></span>
<div class="controls">
<span class="keycap">P</span><span class="control-label">pause</span>
<span class="keycap wide">SPACE</span><span class="control-label">restart</span>
</div>
</footer>
</main>
<script>
(() => {
"use strict";
const COLS = 26;
const ROWS = 20;
const STEP_MS = 135;
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 statusEl = document.getElementById("runStatus");
const statusText = document.getElementById("statusText");
let state = "demo";
let snake = [];
let previousSnake = [];
let direction = { x: 1, y: 0 };
let directionQueue = [];
let food = { x: 0, y: 0 };
let score = 0;
let best = 0;
let accumulator = 0;
let lastFrame = 0;
let viewWidth = 0;
let viewHeight = 0;
let pixelRatio = 1;
let demoPath = [];
let demoIndex = 0;
let particles = [];
let elapsed = 0;
let touchStart = null;
const copyPoint = p => ({ x: p.x, y: p.y });
const samePoint = (a, b) => a.x === b.x && a.y === b.y;
function makeCycle() {
const path = [];
for (let x = 0; x < COLS; x++) path.push({ x, y: 0 });
for (let y = 1; y < ROWS; y++) {
if (y % 2 === 1) {
for (let x = COLS - 1; x >= 1; x--) path.push({ x, y });
} else {
for (let x = 1; x < COLS; x++) path.push({ x, y });
}
}
path.push({ x: 0, y: ROWS - 1 });
for (let y = ROWS - 2; y >= 1; y--) path.push({ x: 0, y });
return path;
}
function setStatus(label, mode = "") {
statusText.textContent = label;
statusEl.className = "run-status" + (mode ? " " + mode : "");
}
function updateScore() {
scoreEl.textContent = String(score);
bestEl.textContent = String(best);
}
function showOverlay(kind) {
overlay.classList.remove("hidden");
if (kind === "demo") {
overlay.innerHTML = `
<div class="overlay-card">
<div class="overlay-kicker"><span class="status-dot"></span>Demo in motion</div>
<h2>Ready when you are.</h2>
<p>Take the wheel and see how long you can grow.</p>
<div class="start-prompt"><strong>Click or press a key to start</strong></div>
</div>`;
} else if (kind === "paused") {
overlay.innerHTML = `
<div class="overlay-card">
<div class="overlay-kicker">Run paused</div>
<h2>Take a breath.</h2>
<p>Your run is right where you left it.</p>
<div class="start-prompt"><strong>Press P</strong> to continue</div>
</div>`;
} else if (kind === "over") {
overlay.innerHTML = `
<div class="overlay-card">
<div class="overlay-kicker">Run complete</div>
<h2>Nice run.</h2>
<div class="overlay-score">${score}</div>
<p>points scored</p>
<div class="overlay-note">Space or Enter to play again</div>
</div>`;
}
}
function hideOverlay() {
overlay.classList.add("hidden");
}
function randomFreeCell() {
const occupied = new Set(snake.map(p => `${p.x},${p.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 });
}
}
return free.length ? free[Math.floor(Math.random() * free.length)] : null;
}
function spawnDemoFood() {
for (let offset = 8; offset < demoPath.length; offset++) {
const candidate = demoPath[(demoIndex + offset) % demoPath.length];
if (!snake.some(part => samePoint(part, candidate))) {
food = copyPoint(candidate);
return;
}
}
food = randomFreeCell() || { x: 1, y: 1 };
}
function startDemo() {
state = "demo";
score = 0;
directionQueue = [];
particles = [];
demoPath = makeCycle();
demoIndex = 6;
snake = [];
for (let i = 0; i < 5; i++) {
snake.push(copyPoint(demoPath[(demoIndex - i + demoPath.length) % demoPath.length]));
}
previousSnake = snake.map(copyPoint);
direction = {
x: demoPath[demoIndex + 1].x - demoPath[demoIndex].x,
y: demoPath[demoIndex + 1].y - demoPath[demoIndex].y
};
spawnDemoFood();
accumulator = 0;
updateScore();
setStatus("Demo autopilot");
showOverlay("demo");
}
function startGame() {
state = "playing";
score = 0;
direction = { x: 1, y: 0 };
directionQueue = [];
particles = [];
snake = [
{ x: 12, y: 10 },
{ x: 11, y: 10 },
{ x: 10, y: 10 },
{ x: 9, y: 10 }
];
previousSnake = snake.map(copyPoint);
food = randomFreeCell() || { x: 18, y: 10 };
accumulator = 0;
updateScore();
setStatus("Your run");
hideOverlay();
}
function enqueueDirection(next) {
const reference = directionQueue.length ? directionQueue[directionQueue.length - 1] : direction;
if (next.x === reference.x && next.y === reference.y) return;
if (next.x === -reference.x && next.y === -reference.y) return;
directionQueue.push(next);
}
function getKeyDirection(key) {
switch (key.toLowerCase()) {
case "arrowup": case "w": return { x: 0, y: -1 };
case "arrowdown": case "s": return { x: 0, y: 1 };
case "arrowleft": case "a": return { x: -1, y: 0 };
case "arrowright": case "d": return { x: 1, y: 0 };
default: return null;
}
}
function addEatParticles(cell) {
for (let i = 0; i < 19; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1.1 + Math.random() * 3.2;
particles.push({
x: cell.x + .5,
y: cell.y + .5,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: .42 + Math.random() * .3,
maxLife: .7,
size: .045 + Math.random() * .055,
color: Math.random() > .38 ? "#c8ff78" : "#ffbd6d"
});
}
}
function finishGame() {
state = "over";
setStatus("Game over", "ended");
showOverlay("over");
}
function advance() {
if (state === "demo") {
const nextIndex = (demoIndex + 1) % demoPath.length;
const next = demoPath[nextIndex];
previousSnake = snake.map(copyPoint);
demoIndex = nextIndex;
direction = {
x: next.x - snake[0].x,
y: next.y - snake[0].y
};
moveTo(next);
return;
}
if (state !== "playing") return;
if (directionQueue.length) direction = directionQueue.shift();
const head = snake[0];
const next = { x: head.x + direction.x, y: head.y + direction.y };
previousSnake = snake.map(copyPoint);
moveTo(next);
}
function moveTo(next) {
const eats = samePoint(next, food);
if (next.x < 0 || next.x >= COLS || next.y < 0 || next.y >= ROWS) {
if (state === "playing") finishGame();
return;
}
const collisionLength = eats ? snake.length : snake.length - 1;
for (let i = 0; i < collisionLength; i++) {
if (samePoint(snake[i], next)) {
if (state === "playing") finishGame();
return;
}
}
snake.unshift(next);
if (eats) {
score += 10;
if (state === "playing") best = Math.max(best, score);
addEatParticles(next);
if (state === "demo") spawnDemoFood();
else food = randomFreeCell() || food;
updateScore();
} else {
snake.pop();
}
}
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
if (!rect.width || !rect.height) return;
pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
viewWidth = rect.width;
viewHeight = rect.height;
const targetWidth = Math.round(viewWidth * pixelRatio);
const targetHeight = Math.round(viewHeight * pixelRatio);
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
canvas.width = targetWidth;
canvas.height = targetHeight;
}
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
}
function roundedRect(x, y, w, h, r) {
ctx.beginPath();
ctx.roundRect(x, y, w, h, r);
}
function drawBoard(time) {
const cw = viewWidth / COLS;
const ch = viewHeight / ROWS;
const bg = ctx.createLinearGradient(0, 0, viewWidth, viewHeight);
bg.addColorStop(0, "#0b1914");
bg.addColorStop(.55, "#091610");
bg.addColorStop(1, "#0b1813");
ctx.fillStyle = bg;
ctx.fillRect(0, 0, viewWidth, viewHeight);
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if ((x + y) % 2 === 0) {
ctx.fillStyle = "rgba(178, 241, 194, .018)";
ctx.fillRect(x * cw, y * ch, cw, ch);
}
}
}
ctx.beginPath();
for (let x = 0; x <= COLS; x++) {
const px = x * cw + .5;
ctx.moveTo(px, 0);
ctx.lineTo(px, viewHeight);
}
for (let y = 0; y <= ROWS; y++) {
const py = y * ch + .5;
ctx.moveTo(0, py);
ctx.lineTo(viewWidth, py);
}
ctx.strokeStyle = "rgba(177, 230, 192, .055)";
ctx.lineWidth = 1;
ctx.stroke();
drawFood(time, cw, ch);
drawParticles(cw, ch);
drawSnake(cw, ch);
}
function drawFood(time, cw, ch) {
if (!food) return;
const pulse = 1 + Math.sin(time * .006) * .09;
const cx = (food.x + .5) * cw;
const cy = (food.y + .5) * ch;
const radius = Math.min(cw, ch) * .27 * pulse;
ctx.save();
ctx.shadowColor = "rgba(255, 151, 76, .7)";
ctx.shadowBlur = Math.min(cw, ch) * .6;
ctx.fillStyle = "rgba(255, 122, 75, .18)";
ctx.beginPath();
ctx.arc(cx, cy, radius * 1.7, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
const fruit = ctx.createRadialGradient(cx - radius * .35, cy - radius * .45, radius * .08, cx, cy, radius * 1.4);
fruit.addColorStop(0, "#ffe3a1");
fruit.addColorStop(.34, "#ffb25f");
fruit.addColorStop(1, "#f26b4e");
ctx.fillStyle = fruit;
ctx.beginPath();
ctx.arc(cx, cy + radius * .07, radius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#b9f86a";
ctx.beginPath();
ctx.ellipse(cx + radius * .32, cy - radius * .83, radius * .48, radius * .23, -.55, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "rgba(255,255,255,.68)";
ctx.beginPath();
ctx.arc(cx - radius * .31, cy - radius * .31, radius * .18, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawSnake(cw, ch) {
if (!snake.length) return;
const alpha = (state === "demo" || state === "playing")
? Math.max(0, Math.min(1, accumulator / STEP_MS))
: 1;
const unit = Math.min(cw, ch);
const inset = unit * .075;
for (let i = snake.length - 1; i >= 1; i--) {
const current = snake[i];
const previous = previousSnake[i] || previousSnake[previousSnake.length - 1] || current;
const x = (previous.x + (current.x - previous.x) * alpha + .5) * cw;
const y = (previous.y + (current.y - previous.y) * alpha + .5) * ch;
const t = i / Math.max(1, snake.length - 1);
const w = cw - inset * 2;
const h = ch - inset * 2;
const gradient = ctx.createLinearGradient(x - w * .5, y - h * .5, x + w * .5, y + h * .5);
gradient.addColorStop(0, `rgb(${Math.round(108 - t * 24)}, ${Math.round(226 - t * 30)}, ${Math.round(131 - t * 22)})`);
gradient.addColorStop(1, `rgb(${Math.round(65 - t * 12)}, ${Math.round(173 - t * 32)}, ${Math.round(103 - t * 17)})`);
ctx.save();
ctx.shadowColor = i < 4 ? "rgba(102, 239, 137, .18)" : "transparent";
ctx.shadowBlur = unit * .18;
ctx.fillStyle = gradient;
roundedRect(x - w / 2, y - h / 2, w, h, unit * .27);
ctx.fill();
ctx.restore();
}
const current = snake[0];
const previous = previousSnake[0] || current;
const hx = (previous.x + (current.x - previous.x) * alpha + .5) * cw;
const hy = (previous.y + (current.y - previous.y) * alpha + .5) * ch;
const headW = cw - inset * 1.2;
const headH = ch - inset * 1.2;
ctx.save();
ctx.shadowColor = "rgba(155, 255, 123, .53)";
ctx.shadowBlur = unit * .38;
const headGradient = ctx.createLinearGradient(hx - headW / 2, hy - headH / 2, hx + headW / 2, hy + headH / 2);
headGradient.addColorStop(0, "#d0ff83");
headGradient.addColorStop(1, "#83ed7d");
ctx.fillStyle = headGradient;
roundedRect(hx - headW / 2, hy - headH / 2, headW, headH, unit * .31);
ctx.fill();
ctx.restore();
const dx = direction.x;
const dy = direction.y;
const perpX = -dy;
const perpY = dx;
const eyeForward = unit * .11;
const eyeSide = unit * .19;
const eyeR = unit * .055;
for (const side of [-1, 1]) {
const ex = hx + dx * eyeForward + perpX * eyeSide * side;
const ey = hy + dy * eyeForward + perpY * eyeSide * side;
ctx.fillStyle = "#183621";
ctx.beginPath();
ctx.arc(ex, ey, eyeR, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "rgba(255,255,255,.72)";
ctx.beginPath();
ctx.arc(ex - dx * eyeR * .2, ey - dy * eyeR * .2, eyeR * .28, 0, Math.PI * 2);
ctx.fill();
}
}
function drawParticles(cw, ch) {
ctx.save();
for (const p of particles) {
const alpha = Math.max(0, p.life / p.maxLife);
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x * cw, p.y * ch, Math.min(cw, ch) * p.size * (.5 + alpha * .5), 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function frame(now) {
if (!lastFrame) lastFrame = now;
const dt = Math.min(50, now - lastFrame);
lastFrame = now;
elapsed += dt;
accumulator += dt;
if (state === "playing" || state === "demo") {
while (accumulator >= STEP_MS) {
accumulator -= STEP_MS;
advance();
if (state !== "playing" && state !== "demo") break;
}
} else {
accumulator = Math.min(accumulator, STEP_MS);
}
const seconds = dt / 1000;
for (const p of particles) {
p.x += p.vx * seconds;
p.y += p.vy * seconds;
p.vx *= .975;
p.vy *= .975;
p.life -= seconds;
}
particles = particles.filter(p => p.life > 0);
if (viewWidth && viewHeight) {
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
drawBoard(now);
}
requestAnimationFrame(frame);
}
document.addEventListener("keydown", event => {
const key = event.key;
const directionForKey = getKeyDirection(key);
if (directionForKey || key === " " || key === "Enter") event.preventDefault();
if (state === "demo") {
startGame();
if (directionForKey) enqueueDirection(directionForKey);
return;
}
if (state === "over") {
if (key === " " || key === "Enter") startGame();
return;
}
if (key.toLowerCase() === "p") {
if (state === "playing") {
state = "paused";
setStatus("Paused", "paused");
showOverlay("paused");
} else if (state === "paused") {
state = "playing";
setStatus("Your run");
hideOverlay();
}
return;
}
if (state === "paused") {
if (key === " " || key === "Enter") {
state = "playing";
setStatus("Your run");
hideOverlay();
}
return;
}
if (state === "playing" && directionForKey) enqueueDirection(directionForKey);
});
canvas.addEventListener("click", () => {
if (state === "demo" || state === "over") startGame();
else if (state === "paused") {
state = "playing";
setStatus("Your run");
hideOverlay();
}
});
document.getElementById("newGame").addEventListener("click", startGame);
canvas.addEventListener("touchstart", event => {
const touch = event.changedTouches[0];
touchStart = { x: touch.clientX, y: touch.clientY };
}, { passive: true });
canvas.addEventListener("touchend", event => {
if (!touchStart) return;
const touch = event.changedTouches[0];
const dx = touch.clientX - touchStart.x;
const dy = touch.clientY - touchStart.y;
touchStart = null;
if (state === "demo") startGame();
if (state !== "playing" || Math.max(Math.abs(dx), Math.abs(dy)) < 18) return;
if (Math.abs(dx) > Math.abs(dy)) enqueueDirection({ x: dx > 0 ? 1 : -1, y: 0 });
else enqueueDirection({ x: 0, y: dy > 0 ? 1 : -1 });
}, { passive: true });
if ("ResizeObserver" in window) {
new ResizeObserver(resizeCanvas).observe(canvas);
} else {
window.addEventListener("resize", resizeCanvas);
}
startDemo();
resizeCanvas();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>Reasoning trace (7,077 tokens)
**Designing a game board** I’m thinking about how the game works: the cells wrap around, and the body follows the path. The head moves forward, but it can only collide with its own tail if it gets too long. Starting with an initial length of 4 seems good. I will reset to the center and design the canvas with fixed dimensions, maybe 26x20. I'll make sure the canvas scales properly using CSS for actual width and max width, aiming for a dark navy theme with glowing green accents.**Sizing the game layout** I’m figuring out the dimensions for the game board. If I use a 26x20 grid with cell size 32, that gives a total size of 832x640. A container width of around 900 seems good. I'd set the app’s maximum width to 1040. The height at 1280 looks reasonable, with a header of 100, a board of 600, and space for a footer. It all fits nicely within an 800 height, which is great!**Implementing movement rules** I’m focusing on the user's requirement that quick successive key presses must be honored, while ensuring the snake cannot reverse onto itself. A common approach would be to implement a buffered direction queue with a limit of 2. I’ll check that any new direction is not the same or opposite of the last one. At each step, I'll consume one direction from the queue and adjust accordingly, making sure to maintain the queue properly for quick input acceptance.**Evaluating queue mechanics** I’m figuring out the effective queue length. Is it the last queue or direction? I wonder if a maximum of 2 is enough for key presses since both must be honored. Retaining the first two seems fine, but maybe I could allow three. Each new move should consume one, while avoiding straight duplicates. I also need to think about the game states: demo, playing, paused, and game over, and how the demo should clearly prompt for control.**Organizing game restart mechanics** I’m planning how to handle restarts after game over, using the space bar or Enter to restart, and maybe space resumes from pause. During the demo, clicking the canvas will start it, and I need to ensure all key events are recognized. I think I should add a "New Game" button up top and ensure it functions properly. For the demo, I’ll set up a cycle to generate food at random locations while also ensuring it spawns ahead of the cycle for players to eat quickly.**Planning food spawn mechanics** I’m considering how to spawn food in relation to the cycle's movement. It makes sense to place the food ahead by about 8 steps so it’s visible and not directly behind the head of the cycle. I want to ensure the food is free and that I'm selecting the cycle index correctly. After each eating event, I’ll increment the cycle's length while keeping it manageable. The food should still be part of the path, ensuring it grows naturally as the demo plays out.**Evaluating grid setup** I’m observing some demo ticks at 145 ms, which seems to be a timing detail. The grid I’m working with is set up as 26 by 20, and I’m keeping in mind how this relates to the canvas coordinates. It looks like I'm considering placing food in the center as a potential starting point. That might work well! I'll need to see how this all comes together during implementation.**Developing game mechanics** I'm working on a food-based game with movement functions that have a step function to choose directions. The demo needs to manage direction queues and handle collision checks against walls and self. The tail removal depends on the state of the food target. I also want to devise an overlay using HTML and canvas to display scores and game states, ensuring it's interactive and slightly covers the game for better visibility. So, I’m exploring how to visually present this effectively!**Designing the game interface** I'm envisioning a demo screen with a small centered glass overlay displaying "SNAKE / AUTOPILOT" and a prompt to start with a click or key press. The overlay should be around 340x120, and I'd like it to show the game over with the score and a "SPACE / ENTER TO RETRY" message. I'll need event listeners for clicks on the canvas and the overlay. Importantly, I want to ensure that keyboard events only register when the game is active, and any interaction should be clear and user-friendly!**Creating the game visuals** I'm settling on a color palette featuring dark forest tones with lime accents, yellow for food, and a coral touch, keeping it subtle. For animations, I’ll use a continuous requestAnimationFrame (RAF) approach while ensuring that particle bursts have random velocities. The rendering process involves clearing the canvas, filling the background, and drawing a dark checkerboard grid. I also need to consider the outer canvas background and whether to include a border. Food will glow and pulse, while snake segments need to interpolate their positions based on their previous state.**Organizing design specifications** I'm mapping out some design specs, like for a 375-screen layout with main padding, size, and canvas dimensions. I’m considering a top header, needing vertical scrolling, but no horizontal. The header should feature a brand font, along with score stats having small labels. The page should have a viewport minimum height of 100svh and a centered board. I'll also ensure everything is responsive, with inline SVGs for the brand icon allowed, but no external resources.**Designing game overlays** I’m thinking about the logo, which uses a simple square icon, and I want to keep things visually sophisticated. The game canvas will have a grid of 26x20 cells. I need to create overlays for the text DOM, making sure the overlay covers the canvas absolutely and lets the background show through. There should be a demo overlay with instructions and an icon, and it won’t interfere with pointer events. The game-over overlay will dynamically display the score. I’ll manage this with a container class for the overlay.**Updating game mechanics** I’m thinking about the online game status. I don’t need to display that status, but I do need to update the UI when scores change. I’ll use the context to draw food particles as small, random circles. In the AI demo, it will select food based on cycle offsets. When food is eaten, I’ll update the headIndex and then spawn food at headIndex + 11. The cycle advances every 145ms, and I want the demo to start automatically at page load. The score can reach around 20 after three seconds.**Resetting game states** When the game actually starts, I need to reset the score and state. I should reset scores while retaining the best score from the session. If a player restarts after a game over, the score resets, but I want to keep the best score intact. I'll implement this feature so that it remains throughout the session. For the demo, I might show the score starting at 0 and then going to 10, but I’m not sure if the demo score should count towards the session.**Evaluating demo and scoring mechanics** I'm feeling a bit confused about the scores. The demo score is visible now, but not updating the best score until the player starts scoring. I think I need to reset the game score to 0 on start. The UI might need a default indicator too. There are also some details about handling direction inputs while completing a queue — I need to manage the direction effectively to ensure valid inputs while accounting for sequences and potential conflicts.**Reviewing queue and direction mechanics** I need to think about the queue capacity. If I set it to 4, I can eliminate unwanted moves. Maybe I could remove the cap altogether for accepted actions but limit it to 8 to avoid complications. I want to honor quick successive key presses while skipping duplicates and preventing inverse directions. After each tick, the direction updates should occur, and I have to ensure the game checks for updates on collision and when paused. Timing is crucial!**Designing overlay and canvas elements** I'm working on overlay styles that include a click message, possibly large to grab attention. It seems that the canvas shouldn’t have an alt attribute, but I wonder if touch controls would be useful even though they weren't requested. Implementing swipe controls might be tricky since a click starts the game. I also plan to use a rounded rectangle function for drawing shapes and adding gradients, along with ensuring a nice background fill. The design will include a border radius and a slight vignette effect to direct the eyes.**Crafting graphical code** I'm thinking about creating a background with a subtle gradient and grid cells, maybe adding some tiny decorative dots. I also want to incorporate a food-styled star with an 8-point design, a circle, and a little green leaf. The outer glow effect will be orange or coral. I need to ensure that the canvas board starts empty for the demo and that animations are smooth, but I’ll likely not reduce motion. Lastly, I’ll write the code in one block, making sure to handle the canvas size correctly.