GPT-5 Nano
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 17 s
- Cost
- $0.0044
- Output tokens
- 10,952
- Spent thinking
- 5,504 tokens
- Speed
- 149.4 tok/s
- Console errors
- 0
- First token after
- 3.6 s
- First visible text after
- 41.9 s
- Prompt tokens
- 493
- Code size
- 18.1 KB
- Finish reason
- stop
- Served by
- OpenAI
- Model id
openai/gpt-5-nano- Reasoning sent
{"effort":"medium"}- Max output
- 128,000 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 635 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Snake – Vanilla JS Canvas (AI Demo)</title>
<style>
:root {
--bg: #0f1220;
--panel: rgba(20, 20, 40, 0.95);
--snake: #5bd46e;
--snakeHead: #3a9e3e;
--food: #e74c3c;
--grid: rgba(255,255,255,0.08);
--overlay: rgba(0,0,0,0.65);
--text: #e8eaf6;
--shadow: rgba(0,0,0,0.25);
}
html, body {
height: 100%;
margin: 0;
padding: 0;
background: radial-gradient(circle at 50% 0%, rgba(80,0,60,0.25), rgba(0,0,0,0)) no-repeat,
radial-gradient(circle at 10% 10%, rgba(0,120,180,0.15), rgba(0,0,0,0)) no-repeat,
var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial;
overflow: hidden;
}
.wrapper {
height: 100%;
width: 100%;
display: grid;
place-items: center;
padding: 20px;
box-sizing: border-box;
}
.game-area {
position: relative;
display: inline-block;
/* Canvas will size itself by JS to fit grid */
border-radius: 14px;
overflow: hidden;
background: linear-gradient(135deg, rgba(30,30,60,0.9), rgba(10,10,25,0.9));
box-shadow: 0 20px 40px rgba(0,0,0,0.25);
padding: 16px;
}
canvas {
display: block;
/* size set by JS to gridSize * cell
translate to crisp pixels on all DPRs via integer cell size */
image-rendering: pixelated;
border-radius: 8px;
background: #0a0a12;
}
.hud {
position: absolute;
top: 8px;
left: 8px;
padding: 6px 10px;
border-radius: 8px;
background: rgba(0,0,0,0.4);
color: #fff;
font-size: 14px;
letter-spacing: 0.3px;
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
user-select: none;
}
.overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: var(--overlay);
backdrop-filter: saturate(1.2) blur(1px);
text-align: center;
padding: 20px;
color: #fff;
font-family: inherit;
cursor: default;
}
.overlay-inner {
max-width: 520px;
padding: 24px 28px;
border-radius: 12px;
background: linear-gradient(180deg, rgba(10,10,40,0.95), rgba(10,10,40,0.75));
border: 1px solid rgba(255,255,255,0.15);
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
}
h1 {
margin: 0 0 8px;
font-size: 28px;
letter-spacing: 0.6px;
}
p { margin: 6px 0; opacity: 0.95; }
.btnhint {
display: inline-block;
margin-top: 12px;
padding: 10px 14px;
border-radius: 8px;
background: #2d8cff;
color: white;
font-weight: 600;
font-size: 14px;
box-shadow: 0 6px 14px rgba(0,0,0,0.25);
user-select: none;
}
@media (max-width: 900px) {
.hud { font-size: 13px; padding: 5px 8px; }
h1 { font-size: 26px; }
}
@media (max-width: 520px) {
canvas { width: 92vw; height: auto; }
.overlay-inner { padding: 20px; }
}
</style>
</head>
<body>
<div class="wrapper">
<div class="game-area" id="gameArea" aria-label="Snake game area">
<div class="hud" id="hud">
Score: <span id="score">0</span> • Best: <span id="best">0</span>
</div>
<canvas id="game" tabindex="0" width="600" height="600"></canvas>
<div class="overlay" id="overlay" aria-hidden="true">
<div class="overlay-inner" id="overlayContent">
<h1>Snake</h1>
<p>Click or press any key to start</p>
<p>AI demo mode: the snake moves automatically until you interact.</p>
<div class="btnhint" id="startHint">Click to Start</div>
</div>
</div>
</div>
</div>
<script>
(function() {
// Grid and canvas setup
const GRID = 22; // at least 20x20
const CELL_PADDING = 0; // not used; kept for clarity
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const overlayContent = document.getElementById('overlayContent');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const hud = document.getElementById('hud');
const startHint = document.getElementById('startHint');
// Game state
let gridSize = GRID;
let cellSize = 20; // will be recalculated on resize
let snake = [];
let dir = {x: 1, y: 0}; // current direction
let nextDirs = []; // queue of pending directions (to honor quick presses)
let food = {x: 0, y: 0};
let score = 0;
let bestScore = 0;
let running = false;
let paused = false;
let started = false; // user has interacted to start
let mode = 'demo'; // 'demo' or 'manual'
let lastTime = 0;
let speed = 140; // ms per tick
let particles = [];
// Helpers
function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }
function idx(x,y) { return x + y*gridSize; }
function randInt(a,b) { return Math.floor(Math.random()*(b-a+1))+a; }
function onSnake(x,y) {
for (let i = 0; i < snake.length; i++) {
if (snake[i].x === x && snake[i].y === y) return true;
}
return false;
}
function spawnFood() {
// choose random cell not on the snake
let x, y, tries = 0;
do {
x = randInt(0, gridSize-1);
y = randInt(0, gridSize-1);
tries++;
} while (onSnake(x,y) && tries < 1000);
food = {x, y};
}
function resetGame() {
// center start
const cx = Math.floor(gridSize/2);
const cy = Math.floor(gridSize/2);
snake = [
{x: cx, y: cy},
{x: cx-1, y: cy},
{x: cx-2, y: cy}
];
dir = {x: 1, y: 0};
nextDirs = [];
score = 0;
spawnFood();
running = true;
paused = false;
lastTime = 0;
}
function resize() {
// Fit grid nicely into available viewport
const maxW = window.innerWidth * 0.92;
const maxH = window.innerHeight * 0.92;
const best = Math.floor(Math.min(maxW, maxH) / gridSize);
cellSize = Math.max(14, Math.min(best, 42)); // cap size for aesthetics
canvas.width = gridSize * cellSize;
canvas.height = gridSize * cellSize;
// redraw
draw();
}
// AI path decision (very simple but effective for demo)
function aiChooseDirection() {
// If there are queued directions, try to honor them first (handled in step)
// Build candidate directions in simple priority: horizontal toward food, then vertical
const head = snake[0];
const dx = food.x - head.x;
const dy = food.y - head.y;
const candidates = [];
if (dx !== 0) candidates.push({x: Math.sign(dx), y: 0});
if (dy !== 0) candidates.push({x: 0, y: Math.sign(dy)});
// add alternatives to avoid dead-ends
candidates.push({x: 1, y:0});
candidates.push({x:-1,y:0});
candidates.push({x:0,y:1});
candidates.push({x:0,y:-1});
// Try to pick first safe candidate that is not reversing
for (const c of candidates) {
const ndx = c.x;
const ndy = c.y;
if (ndx === -dir.x && ndy === -dir.y) continue; // avoid immediate reversal
const nx = head.x + ndx;
const ny = head.y + ndy;
if (nx < 0 || ny < 0 || nx >= gridSize || ny >= gridSize) continue;
// collision with body (excluding tail)
let collision = false;
for (let i = 0; i < snake.length - 1; i++) { // exclude tail
if (snake[i].x === nx && snake[i].y === ny) { collision = true; break; }
}
if (!collision) {
return {x: ndx, y: ndy};
}
}
// If none safe, try any non-reverse manually
const fallback = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
for (const f of fallback) {
if (f.x === -dir.x && f.y === -dir.y) continue;
const nx = head.x + f.x, ny = head.y + f.y;
if (nx < 0 || ny < 0 || nx >= gridSize || ny >= gridSize) continue;
return {x: f.x, y: f.y};
}
// If completely trapped, keep current direction
return {x: dir.x, y: dir.y};
}
// Main loop step
function step() {
// If paused or not running, skip
if (!running || paused) return;
// Progress input queue (honor quick successive keys)
if (nextDirs.length > 0) {
const cand = nextDirs.shift();
// prevent reversal
if (!(cand.x === -dir.x && cand.y === -dir.y)) {
dir = cand;
}
} else if (mode === 'demo') {
// AI decides direction
const aiMove = aiChooseDirection();
if (!(aiMove.x === -dir.x && aiMove.y === -dir.y)) {
dir = aiMove;
}
}
// compute next head
const head = snake[0];
const nx = head.x + dir.x;
const ny = head.y + dir.y;
// wall collision
if (nx < 0 || ny < 0 || nx >= gridSize || ny >= gridSize) {
endGame();
return;
}
// self collision (ignore tail which will move unless eating)
let collision = false;
for (let i = 0; i < snake.length - 1; i++) {
if (snake[i].x === nx && snake[i].y === ny) {
collision = true; break;
}
}
if (collision) {
endGame();
return;
}
// move
snake.unshift({x: nx, y: ny});
// eating?
if (nx === food.x && ny === food.y) {
score += 10;
if (score > bestScore) bestScore = score;
spawnFood();
// particle burst at the food location
burstParticles(nx, ny);
} else {
// normal move: remove tail
snake.pop();
}
// update HUD
scoreEl.textContent = String(score);
bestEl.textContent = String(bestScore);
}
// Particle burst
function burstParticles(x, y) {
const cx = x * cellSize + cellSize/2;
const cy = y * cellSize + cellSize/2;
const count = 8;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speedP = Math.random() * 1.5 + 0.5;
particles.push({
x: cx, y: cy,
vx: Math.cos(angle) * speedP,
vy: Math.sin(angle) * speedP,
life: randInt(14, 28),
maxLife: 28,
r: randInt(180, 255),
g: randInt(100, 230),
b: randInt(50, 180),
size: randInt(1, 3)
});
}
}
function endGame() {
running = false;
paused = false;
overlay.style.pointerEvents = 'auto';
overlay.style.display = 'grid';
overlayContent.innerHTML = `
<h1>Game Over</h1>
<p>Your score: ${score}</p>
<p>Best: ${bestScore}</p>
<p>Press Space or Enter to restart, P to pause</p>
<div class="btnhint" id="restartHint" tabindex="0">Restart</div>
`;
// Focus overlay to capture key events
overlay.focus?.();
}
function restartGameFromOverlay() {
// reset and start again in demo mode
score = 0;
scoreEl.textContent = String(score);
overlay.style.display = 'none';
overlay.style.pointerEvents = 'none';
started = true;
mode = 'demo';
resetGame();
canvas.focus();
}
function drawGrid() {
// optional subtle grid
ctx.save();
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
for (let i = 0; i <= gridSize; i++) {
// vertical
const x = i * cellSize + 0.5;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, gridSize * cellSize);
ctx.stroke();
// horizontal
const y = i * cellSize + 0.5;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(gridSize * cellSize, y);
ctx.stroke();
}
ctx.restore();
}
function draw() {
// clear
ctx.clearRect(0,0, canvas.width, canvas.height);
// background fill
ctx.fillStyle = '#0a0a12';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// grid (soft)
drawGrid();
// food
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(food.x * cellSize + 2, food.y * cellSize + 2, cellSize - 4, cellSize - 4);
// snake
for (let i = 0; i < snake.length; i++) {
const s = snake[i];
const px = s.x * cellSize;
const py = s.y * cellSize;
if (i === 0) {
// head
const headColor = '#2ecc71';
ctx.fillStyle = headColor;
roundRect(ctx, px + 1, py + 1, cellSize - 2, cellSize - 2, 6);
ctx.fill();
// eyes based on direction
const eyeSize = Math.max(2, Math.floor(cellSize * 0.15));
const ex = px;
const ey = py;
// eye positions inside head cell
// head center
const cx = px + cellSize/2;
const cy = py + cellSize/2;
if (dir.x > 0) {
// facing right
drawEye(ctx, px + cellSize*0.25, py + cellSize*0.35, eyeSize);
drawEye(ctx, px + cellSize*0.25, py + cellSize*0.75, eyeSize);
} else if (dir.x < 0) {
// left
drawEye(ctx + 0, 0,0,0); // placeholder to satisfy linter
drawEye(ctx, px + cellSize*0.75, py + cellSize*0.35, eyeSize);
drawEye(ctx, px + cellSize*0.75, py + cellSize*0.75, eyeSize);
} else if (dir.y > 0) {
// down
drawEye(ctx, px + cellSize*0.35, py + cellSize*0.25, eyeSize);
drawEye(ctx, px + cellSize*0.65, py + cellSize*0.25, eyeSize);
} else {
// up
drawEye(ctx, px + cellSize*0.35, py + cellSize*0.75, eyeSize);
drawEye(ctx, px + cellSize*0.65, py + cellSize*0.75, eyeSize);
}
} else {
// body
ctx.fillStyle = '#4caf50';
roundRect(ctx, px + 1, py + 1, cellSize - 2, cellSize - 2, 4);
ctx.fill();
}
// body shading
if (i > 0) {
ctx.fillStyle = '#4caf50';
ctx.fillRect(px + 2, py + 2, cellSize - 4, cellSize - 4);
}
}
// particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 1;
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
const alpha = p.life / p.maxLife;
ctx.fillStyle = `rgba(${p.r},${p.g},${p.b},${alpha})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
}
}
function drawEye(ctx, x, y, r) {
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(x, y, Math.max(1.6, r*0.9), 0, Math.PI*2);
ctx.fill();
}
// Rounded rect helper
function roundRect(ctx, x, y, w, h, r) {
const rr = Math.min(r, w/2, h/2);
ctx.beginPath();
ctx.moveTo(x+rr, y);
ctx.arcTo(x+w, y, x+w, y+h, rr);
ctx.arcTo(x+w, y+h, x, y+h, rr);
ctx.arcTo(x, y+h, x, y, rr);
ctx.arcTo(x, y, x+w, y, rr);
ctx.closePath();
}
// Input handling
function onKeyDown(e) {
if (!started) return;
const key = e.key;
// Map keys to directions
let newDir = null;
if (key === 'ArrowUp' || key === 'w' || key === 'W') newDir = {x:0, y:-1};
if (key === 'ArrowDown' || key === 's' || key === 'S') newDir = {x:0, y:1};
if (key === 'ArrowLeft' || key === 'a' || key === 'A') newDir = {x:-1, y:0};
if (key === 'ArrowRight' || key === 'd' || key === 'D') newDir = {x:1, y:0};
if (newDir) {
// queue it
nextDirs.push(newDir);
// if in switches to manual mode, we could set mode = 'manual'
mode = 'manual';
}
if (e.code === 'Space' || key === 'Enter') {
// restart if game over
if (!running) {
restartGameFromOverlay();
} else {
// toggle pause
paused = !paused;
}
}
if (key === 'p' || key === 'P') {
paused = !paused;
}
}
// Start/restore flow
function ensureStart() {
if (!started) {
// start in demo mode after click
started = true;
overlay.style.display = 'none';
overlay.style.pointerEvents = 'none';
mode = 'demo';
resetGame();
canvas.focus();
}
}
// Init
function init() {
// Attach events
window.addEventListener('resize', resize);
window.addEventListener('keydown', onKeyDown);
// Start overlay interaction
overlay.addEventListener('click', ensureStart, { once: true });
startHint.addEventListener('click', ensureStart, { once: true });
// Initialize dims and start in paused state with demo
resize();
// Pre-paint
draw();
}
// Animation loop
function loop(t) {
if (!lastTime) lastTime = t;
const dt = t - lastTime;
if (running && !paused) {
if (dt >= speed) {
step();
lastTime = t;
}
} else {
lastTime = t;
}
draw();
requestAnimationFrame(loop);
}
// Public function: restart after game over
function restartGameFromOverlayInternal() {
score = 0;
scoreEl.textContent = String(score);
if (score > bestScore) bestScore = score;
bestEl.textContent = String(bestScore);
overlay.style.display = 'none';
overlay.style.pointerEvents = 'none';
started = true;
mode = 'demo';
resetGame();
canvas.focus();
}
// Expose a tiny API for the restart when user hits restart button
overlay.addEventListener('click', function(e) {
if (!started) {
ensureStart();
} else if (!running) {
restartGameFromOverlayInternal();
}
}, { once: false });
// Start
init();
// Start the render loop
requestAnimationFrame(loop);
// Accessibility nudges
// If user presses any key before starting, trigger start to satisfy "start on click or key"
window.addEventListener('keydown', function(e) {
if (!started) {
ensureStart();
e.preventDefault();
}
}, { once: false });
})();
</script>
</body>
</html>Reasoning trace (5,504 tokens)
**Considering game interaction modes** I’m thinking about an AI demonstration where the keyboard can override the AI's route if the user interacts. The plan is to have AI as the default driver, but if the user presses a key, it switches to manual mode. I realize we need a "start" overlay, where the game begins after interaction. I also need to ensure controls like "Space" or "Enter" restart the game and that scores are stored correctly, either in memory or localStorage.**Evaluating grid and score constraints** I’m realizing that the constraint prevents using localStorage, so I won’t be able to store the best score persistently across reloads. However, I can track the "best score of the session" with a variable called bestScore. There’s a requirement for a grid of at least 20x20, so I’m thinking of implementing a 22x22 grid for safety. I’ll center a canvas to fit the viewport and decide on a fixed cell size or calculate it dynamically to fit. I also want smooth movement, possibly using rounded corners for segments.**Designing the Snake game elements** For the Snake game, I’m focusing on making movement discrete to avoid jagged leaps. Rendering will involve a pixel-art grid with vibrant colors and gradient shading for the snake's body, along with eyes that face the direction of movement. I plan to add a simple particle system that spawns small particles when the snake eats. Eating will earn 10 points and grow the snake by one cell, ensuring food doesn’t spawn on the snake. I also need a score overlay and responsive design for different viewport sizes. Finally, I'll remember that keyboard events only register after the iframe is clicked.**Setting up user events and layout** I’m planning to implement an overlay that captures events, and I’ll require the user to click to start the game. After this click, I can add an event listener for keydown, but the sandbox specifies that keyboard events only work after clicking the iframe. I’ll set "started" to true and focus on the canvas upon clicking. The page needs to look good in a 1280×800 viewport and be responsive down to 375px wide, while maintaining overflow hidden. I’ll keep all CSS inline for organization and ensure proper data structures, like gridSize being 22 or greater.**Implementing the Snake game mechanics** I’m planning to create a 22x22 grid for the snake game. The snake will be represented as an array of positions, with the head at index 0 for easier manipulation. I’ll define direction mappings and create a function to prevent 180-degree reversals. Players can set a pending direction with key presses, and I’ll implement a queue to manage quick successive direction changes while respecting reversals. Movement speed will be set with a tick interval, and food will spawn randomly on the grid. For visual elements, I’ll distinguish the head and eyes and implement particle effects. Game states need to be managed, including starting, paused, and running conditions. I'll also add AI logic to control the snake in demo mode, ensuring it navigates towards food while avoiding collisions.**Simplifying collision and gameplay mechanics** For collision detection in the snake game, I’ll simplify the logic: if the newHead equals any body cell except the last one—if the snake is moving—then it will count as a collision. The only time the last cell is allowed as a newHead is if the snake is eating, meaning we safely treat it as valid. Next, I need to set up canvas rendering with a coordinate system, ensuring a crisp display with gradients and grid lines. I’ll implement various elements like the score and best score displayed on the overlay, as well as a pause indicator. The HTML structure will include a canvas and overlays, with CSS to center everything properly and prevent scrollbars. The main game logic will also focus on initializing the canvas, spawning the snake and food, and drawing the game elements, including implementing simple eyes based on the snake's direction.**Implementing game graphics and mechanics** I'm planning to keep the graphics simple using `fillRect` for the snake's head with color `#2b8bd6` and the body with `#4ab449`. I'll add eyes as small black circles based on the head's direction, calculating their positions accordingly. For food, a red square with an orange center will be used. Particles will be drawn with alpha gradients to create a visual effect, and I need to establish a HUD overlay to display the score and best score. There will be separate overlays for starting the game and showing "Game Over," with instructions for restarting and pausing. I'll ensure efficient event handling for key presses and manage direction queues, checking for collisions and updating the snake's position as needed. The game will run with zero errors while providing a smooth experience!**Setting up game mechanics and visuals** I'm creating the `initGame` function to set up the grid and initialize the snake's starting position. The direction will start to the right, and I'll manage the score and time for gameplay. Next, I’ll ensure there are no horizontal scrollbars by adjusting margins with CSS. To achieve the right sizing for the game canvas, I'll calculate `cellSize` to fit the viewport. Adding food will require it to be drawn as a colored block, and I’ll implement particle effects as well. Overlays for starting instructions and score will be formatted so the game begins in demo mode with AI control. Finally, event listeners will be established to handle game starts effectively.