Gemini 4 Argon
by Googlereasoning defaultClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 17 s
- Cost
- $0.113
- Output tokens
- 11,156
- Spent thinking
- 0 tokens
- Speed
- 85 tok/s
- Console errors
- 0
- First token after
- 6.2 s
- First visible text after
- 6.2 s
- Prompt tokens
- 546
- Code size
- 32.8 KB
- Finish reason
- stop
- Served by
- Model id
google/gemini-4-argon- Reasoning sent
model default- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 1109 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Polished Neon Snake</title>
<style>
:root {
--bg-dark: #0b0f19;
--bg-panel: #131b2e;
--accent-green: #10b981;
--accent-lime: #34d399;
--accent-food: #f43f5e;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--border-glow: rgba(52, 211, 153, 0.25);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
body {
background: radial-gradient(circle at 50% 20%, #17233b 0%, var(--bg-dark) 80%);
color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px;
overflow-x: hidden;
}
.game-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 560px;
gap: 14px;
}
/* Header & Scoreboard */
.hud {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
background: rgba(19, 27, 46, 0.85);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
border-radius: 14px;
padding: 12px 20px;
backdrop-filter: blur(8px);
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-icon {
width: 14px;
height: 14px;
border-radius: 4px;
background: var(--accent-lime);
box-shadow: 0 0 12px var(--accent-lime);
position: relative;
}
.brand-title {
font-weight: 800;
font-size: 1.25rem;
letter-spacing: 0.5px;
background: linear-gradient(90deg, #fff, #a7f3d0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.scores {
display: flex;
gap: 20px;
}
.score-box {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.score-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted);
font-weight: 600;
}
.score-value {
font-size: 1.35rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: #fff;
text-shadow: 0 0 10px rgba(52, 211, 153, 0.3);
transition: transform 0.15s ease;
}
.score-value.bump {
transform: scale(1.25);
color: var(--accent-lime);
}
/* Canvas Container & Overlays */
.board-container {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 16px;
padding: 6px;
background: linear-gradient(135deg, rgba(52, 211, 153, 0.25), rgba(15, 23, 42, 0.9));
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), 0 0 25px var(--border-glow);
}
canvas {
width: 100%;
height: 100%;
display: block;
border-radius: 11px;
background: #0d1322;
cursor: pointer;
}
.overlay {
position: absolute;
inset: 6px;
border-radius: 11px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 24px;
transition: opacity 0.25s ease, visibility 0.25s ease;
pointer-events: none;
}
/* Demo Mode Banner */
.overlay.demo-mode {
justify-content: flex-end;
padding-bottom: 28px;
background: radial-gradient(circle at center, transparent 40%, rgba(11, 15, 25, 0.55) 100%);
}
.demo-banner {
background: rgba(15, 23, 42, 0.88);
border: 1px solid rgba(52, 211, 153, 0.45);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6), 0 0 20px rgba(16, 185, 129, 0.25);
padding: 14px 26px;
border-radius: 999px;
backdrop-filter: blur(8px);
pointer-events: auto;
cursor: pointer;
animation: pulseBanner 2s infinite ease-in-out;
}
@keyframes pulseBanner {
0%, 100% { transform: translateY(0) scale(1); border-color: rgba(52, 211, 153, 0.45); }
50% { transform: translateY(-3px) scale(1.02); border-color: rgba(52, 211, 153, 0.9); }
}
.demo-badge {
display: inline-block;
font-size: 0.68rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--accent-lime);
margin-bottom: 3px;
}
.demo-cta {
font-size: 1.05rem;
font-weight: 700;
color: #fff;
}
/* Fullscreen Modal Overlays (Pause / Game Over) */
.overlay.modal {
background: rgba(11, 15, 25, 0.78);
backdrop-filter: blur(6px);
pointer-events: auto;
}
.overlay.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.modal-card {
background: rgba(19, 27, 46, 0.95);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
padding: 28px 32px;
max-width: 340px;
width: 100%;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
transform: translateY(0);
transition: transform 0.25s ease;
}
.overlay.hidden .modal-card {
transform: translateY(12px);
}
.modal-title {
font-size: 1.75rem;
font-weight: 800;
margin-bottom: 6px;
color: #fff;
}
.modal-subtitle {
font-size: 0.95rem;
color: var(--text-muted);
margin-bottom: 20px;
}
.modal-score-summary {
display: flex;
justify-content: space-around;
background: rgba(11, 15, 25, 0.6);
border-radius: 10px;
padding: 12px;
margin-bottom: 22px;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-lime), var(--accent-green));
color: #06281e;
font-weight: 800;
font-size: 0.95rem;
border: none;
border-radius: 10px;
padding: 12px 24px;
width: 100%;
cursor: pointer;
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.35);
transition: transform 0.12s ease, filter 0.12s ease;
}
.btn-primary:hover {
filter: brightness(1.08);
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(1px);
}
/* Footer Controls */
.controls-footer {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
font-size: 0.8rem;
color: var(--text-muted);
}
.key-hint {
display: flex;
align-items: center;
gap: 6px;
}
kbd {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-bottom-width: 2px;
border-radius: 5px;
padding: 2px 6px;
font-family: inherit;
font-size: 0.72rem;
font-weight: 700;
color: #e2e8f0;
}
/* Touch D-Pad for smaller viewports */
.dpad {
display: none;
grid-template-columns: repeat(3, 52px);
grid-template-rows: repeat(2, 46px);
gap: 8px;
margin-top: 4px;
}
.dpad-btn {
background: rgba(19, 27, 46, 0.9);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
color: var(--text-main);
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
active-background: var(--accent-green);
}
.dpad-btn:active {
background: rgba(52, 211, 153, 0.25);
border-color: var(--accent-lime);
}
@media (max-width: 600px) {
.game-wrapper {
max-width: 420px;
gap: 10px;
}
.hud {
padding: 10px 14px;
}
.brand-title {
font-size: 1.05rem;
}
.score-value {
font-size: 1.15rem;
}
.dpad {
display: grid;
}
}
</style>
</head>
<body>
<div class="game-wrapper">
<!-- Top HUD -->
<header class="hud">
<div class="brand">
<div class="brand-icon"></div>
<div class="brand-title">NEON SNAKE</div>
</div>
<div class="scores">
<div class="score-box">
<span class="score-label">Score</span>
<span class="score-value" id="scoreDisplay">0</span>
</div>
<div class="score-box">
<span class="score-label">Best</span>
<span class="score-value" id="bestDisplay">0</span>
</div>
</div>
</header>
<!-- Game Board -->
<div class="board-container">
<canvas id="gameCanvas" width="660" height="660"></canvas>
<!-- Demo Mode Overlay -->
<div id="demoOverlay" class="overlay demo-mode">
<div class="demo-banner" id="startBanner">
<div class="demo-badge">Auto-Pilot Demo</div>
<div class="demo-cta">Click or press a key to start</div>
</div>
</div>
<!-- Pause Overlay -->
<div id="pauseOverlay" class="overlay modal hidden">
<div class="modal-card">
<div class="modal-title">Game Paused</div>
<div class="modal-subtitle">Press <kbd>P</kbd> or click below to continue</div>
<button class="btn-primary" id="resumeBtn">Resume Game</button>
</div>
</div>
<!-- Game Over Overlay -->
<div id="gameOverOverlay" class="overlay modal hidden">
<div class="modal-card">
<div class="modal-title">Game Over</div>
<div class="modal-subtitle" id="deathReason">You crashed!</div>
<div class="modal-score-summary">
<div class="score-box" style="align-items: center;">
<span class="score-label">Final Score</span>
<span class="score-value" id="finalScore">0</span>
</div>
<div class="score-box" style="align-items: center;">
<span class="score-label">Session Best</span>
<span class="score-value" id="finalBest">0</span>
</div>
</div>
<button class="btn-primary" id="restartBtn">Play Again (Space / Enter)</button>
</div>
</div>
</div>
<!-- Mobile / Touch D-Pad -->
<div class="dpad" aria-label="Directional Controls">
<div></div>
<button class="dpad-btn" data-dir="UP" aria-label="Up">▲</button>
<div></div>
<button class="dpad-btn" data-dir="LEFT" aria-label="Left">◀</button>
<button class="dpad-btn" data-dir="DOWN" aria-label="Down">▼</button>
<button class="dpad-btn" data-dir="RIGHT" aria-label="Right">▶</button>
</div>
<!-- Keyboard Hints -->
<footer class="controls-footer">
<div class="key-hint"><kbd>▲</kbd><kbd>▼</kbd><kbd>◀</kbd><kbd>▶</kbd> or <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> Move</div>
<div class="key-hint"><kbd>P</kbd> Pause</div>
<div class="key-hint"><kbd>Space</kbd> / <kbd>Enter</kbd> Restart</div>
</footer>
</div>
<script>
(() => {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// UI Elements
const scoreDisplay = document.getElementById('scoreDisplay');
const bestDisplay = document.getElementById('bestDisplay');
const finalScore = document.getElementById('finalScore');
const finalBest = document.getElementById('finalBest');
const deathReason = document.getElementById('deathReason');
const demoOverlay = document.getElementById('demoOverlay');
const pauseOverlay = document.getElementById('pauseOverlay');
const gameOverOverlay = document.getElementById('gameOverOverlay');
const resumeBtn = document.getElementById('resumeBtn');
const restartBtn = document.getElementById('restartBtn');
// Grid & Speed Constants (22x22 grid -> 30px cells on 660x660 canvas)
const GRID_SIZE = 22;
const CELL_SIZE = canvas.width / GRID_SIZE;
const BASE_STEP_MS = 105;
const DEMO_STEP_MS = 85;
// Game State
let mode = 'DEMO'; // 'DEMO' | 'PLAYING' | 'PAUSED' | 'GAMEOVER'
let score = 0;
let bestScore = 0;
let snake = [];
let prevSnake = [];
let direction = { x: 1, y: 0 };
let inputQueue = [];
let grewLastTick = false;
let food = { x: 15, y: 10 };
let particles = [];
let floatingTexts = [];
let lastFrameTime = performance.now();
let stepAccumulator = 0;
let shakeTime = 0;
// Initialize / Reset Game
function initGame(targetMode) {
mode = targetMode;
score = 0;
updateScoreUI(false);
const startX = 7;
const startY = Math.floor(GRID_SIZE / 2);
snake = [
{ x: startX, y: startY },
{ x: startX - 1, y: startY },
{ x: startX - 2, y: startY },
{ x: startX - 3, y: startY },
{ x: startX - 4, y: startY }
];
prevSnake = snake.map(seg => ({ ...seg }));
direction = { x: 1, y: 0 };
inputQueue = [];
grewLastTick = false;
stepAccumulator = 0;
if (mode === 'DEMO') {
// Place first food right ahead of the demo snake so action happens within seconds
food = { x: startX + 4, y: startY };
// Spawn a cheerful initial burst so first paint is lively
spawnEatParticles((startX + 4) * CELL_SIZE + CELL_SIZE / 2, startY * CELL_SIZE + CELL_SIZE / 2, 12);
} else {
spawnFood();
}
updateOverlays();
}
function updateOverlays() {
demoOverlay.classList.toggle('hidden', mode !== 'DEMO');
pauseOverlay.classList.toggle('hidden', mode !== 'PAUSED');
gameOverOverlay.classList.toggle('hidden', mode !== 'GAMEOVER');
}
function updateScoreUI(animate = true) {
scoreDisplay.textContent = score;
bestDisplay.textContent = bestScore;
if (animate) {
scoreDisplay.classList.remove('bump');
void scoreDisplay.offsetWidth; // trigger reflow
scoreDisplay.classList.add('bump');
setTimeout(() => scoreDisplay.classList.remove('bump'), 160);
}
}
// Spawn food guaranteed not on the snake
function spawnFood() {
const occupied = new Set(snake.map(s => `${s.x},${s.y}`));
const available = [];
for (let x = 0; x < GRID_SIZE; x++) {
for (let y = 0; y < GRID_SIZE; y++) {
if (!occupied.has(`${x},${y}`)) {
available.push({ x, y });
}
}
}
if (available.length > 0) {
const pick = available[Math.floor(Math.random() * available.length)];
food = pick;
}
}
// Queue player directional input preventing self-reversal & honoring rapid keys
function queueDirection(newDir) {
const lastScheduledDir = inputQueue.length > 0 ? inputQueue[inputQueue.length - 1] : direction;
// Ignore same direction or 180-degree reversal
if (newDir.x === lastScheduledDir.x && newDir.y === lastScheduledDir.y) return;
if (newDir.x === -lastScheduledDir.x && newDir.y === -lastScheduledDir.y) return;
// Buffer up to 3 rapid inputs
if (inputQueue.length < 3) {
inputQueue.push(newDir);
}
}
// Simple smart AI for Demo Mode (BFS shortest path + safe fallback)
function computeAIDirection() {
const head = snake[0];
const dirs = [
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: -1 }
];
// Treat all segments except the very tip of the tail as blocked
const blocked = new Set();
for (let i = 0; i < snake.length - 1; i++) {
blocked.add(`${snake[i].x},${snake[i].y}`);
}
// BFS to find shortest path to food
const queue = [{ x: head.x, y: head.y, firstMove: null }];
const visited = new Set([`${head.x},${head.y}`]);
while (queue.length > 0) {
const curr = queue.shift();
if (curr.x === food.x && curr.y === food.y && curr.firstMove) {
return curr.firstMove;
}
for (const d of dirs) {
// Cannot reverse immediately on first step
if (!curr.firstMove && d.x === -direction.x && d.y === -direction.y) continue;
const nx = curr.x + d.x;
const ny = curr.y + d.y;
const key = `${nx},${ny}`;
if (nx >= 0 && nx < GRID_SIZE && ny >= 0 && ny < GRID_SIZE && !blocked.has(key) && !visited.has(key)) {
visited.add(key);
queue.push({ x: nx, y: ny, firstMove: curr.firstMove || d });
}
}
}
// Fallback: pick valid move with the most open neighbors
let bestDir = direction;
let maxSpace = -1;
for (const d of dirs) {
if (d.x === -direction.x && d.y === -direction.y) continue;
const nx = head.x + d.x;
const ny = head.y + d.y;
if (nx >= 0 && nx < GRID_SIZE && ny >= 0 && ny < GRID_SIZE && !blocked.has(`${nx},${ny}`)) {
// Count free adjacent cells
let freeCount = 0;
for (const sub of dirs) {
const sx = nx + sub.x;
const sy = ny + sub.y;
if (sx >= 0 && sx < GRID_SIZE && sy >= 0 && sy < GRID_SIZE && !blocked.has(`${sx},${sy}`)) {
freeCount++;
}
}
if (freeCount > maxSpace) {
maxSpace = freeCount;
bestDir = d;
}
}
}
return bestDir;
}
// Particle Burst Effect
function spawnEatParticles(px, py, count = 22) {
const palette = ['#34d399', '#10b981', '#fb7185', '#fde047', '#ffffff'];
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.4;
const speed = 60 + Math.random() * 150;
particles.push({
x: px,
y: py,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
radius: 2.5 + Math.random() * 3.5,
color: palette[Math.floor(Math.random() * palette.length)],
alpha: 1,
life: 0,
maxLife: 0.35 + Math.random() * 0.25
});
}
}
function spawnFloatingText(px, py, text) {
floatingTexts.push({
x: px,
y: py,
text,
alpha: 1,
life: 0,
maxLife: 0.65
});
}
// Advance one logical grid step
function stepGame() {
prevSnake = snake.map(seg => ({ ...seg }));
if (mode === 'DEMO') {
direction = computeAIDirection();
} else if (inputQueue.length > 0) {
direction = inputQueue.shift();
}
const head = snake[0];
const nextHead = {
x: head.x + direction.x,
y: head.y + direction.y
};
// Check wall collision
if (nextHead.x < 0 || nextHead.x >= GRID_SIZE || nextHead.y < 0 || nextHead.y >= GRID_SIZE) {
handleCrash('You hit the wall!');
return;
}
// Check self collision (note: if not growing, tail tip vacates its cell this tick)
const willEat = (nextHead.x === food.x && nextHead.y === food.y);
const checkLimit = willEat ? snake.length : snake.length - 1;
for (let i = 0; i < checkLimit; i++) {
if (snake[i].x === nextHead.x && snake[i].y === nextHead.y) {
handleCrash('You ran into yourself!');
return;
}
}
// Move snake
snake.unshift(nextHead);
if (willEat) {
grewLastTick = true;
score += 10;
if (score > bestScore) {
bestScore = score;
}
updateScoreUI(true);
const fx = food.x * CELL_SIZE + CELL_SIZE / 2;
const fy = food.y * CELL_SIZE + CELL_SIZE / 2;
spawnEatParticles(fx, fy, 24);
spawnFloatingText(fx, fy, '+10');
spawnFood();
} else {
grewLastTick = false;
snake.pop();
}
}
function handleCrash(reasonText) {
const head = snake[0];
const hx = head.x * CELL_SIZE + CELL_SIZE / 2;
const hy = head.y * CELL_SIZE + CELL_SIZE / 2;
spawnEatParticles(hx, hy, 30);
shakeTime = 0.25;
if (mode === 'DEMO') {
// Seamlessly restart demo if AI ever traps itself
initGame('DEMO');
return;
}
mode = 'GAMEOVER';
deathReason.textContent = reasonText;
finalScore.textContent = score;
finalBest.textContent = bestScore;
updateOverlays();
}
// Smoothly interpolate snake segments between discrete grid steps
function getInterpolatedSnake(alpha) {
const pts = [];
for (let i = 0; i < snake.length; i++) {
const curr = snake[i];
let prev;
if (i === 0) {
prev = prevSnake[0] || curr;
} else if (grewLastTick && i === snake.length - 1) {
// Newly grown tail segment stays anchored at the old tail tip
prev = curr;
} else {
prev = prevSnake[i - 1] || curr;
}
pts.push({
x: (prev.x + (curr.x - prev.x) * alpha) * CELL_SIZE + CELL_SIZE / 2,
y: (prev.y + (curr.y - prev.y) * alpha) * CELL_SIZE + CELL_SIZE / 2
});
}
return pts;
}
// Render Board, Food, Snake, Head/Eyes, and Particles
function render(alpha, timestamp) {
ctx.save();
// Screen shake on collision
if (shakeTime > 0) {
const mag = shakeTime * 22;
ctx.translate((Math.random() - 0.5) * mag, (Math.random() - 0.5) * mag);
}
// 1. Checkerboard Grid Background
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
ctx.fillStyle = (x + y) % 2 === 0 ? '#0f172a' : '#131d35';
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
}
}
// Subtle grid intersections
ctx.fillStyle = 'rgba(255, 255, 255, 0.03)';
for (let y = 1; y < GRID_SIZE; y++) {
for (let x = 1; x < GRID_SIZE; x++) {
ctx.fillRect(x * CELL_SIZE - 1, y * CELL_SIZE - 1, 2, 2);
}
}
// 2. Draw Food (Glowing Pulsing Apple)
const pulse = 1 + Math.sin(timestamp * 0.008) * 0.1;
const fx = food.x * CELL_SIZE + CELL_SIZE / 2;
const fy = food.y * CELL_SIZE + CELL_SIZE / 2;
const foodRadius = (CELL_SIZE * 0.38) * pulse;
ctx.save();
ctx.shadowColor = '#f43f5e';
ctx.shadowBlur = 18;
const foodGrad = ctx.createRadialGradient(
fx - foodRadius * 0.3,
fy - foodRadius * 0.3,
foodRadius * 0.1,
fx,
fy,
foodRadius
);
foodGrad.addColorStop(0, '#fda4af');
foodGrad.addColorStop(0.4, '#f43f5e');
foodGrad.addColorStop(1, '#be123c');
ctx.fillStyle = foodGrad;
ctx.beginPath();
ctx.arc(fx, fy, foodRadius, 0, Math.PI * 2);
ctx.fill();
// Little green leaf on top of the apple
ctx.shadowBlur = 0;
ctx.fillStyle = '#4ade80';
ctx.beginPath();
ctx.ellipse(fx + 2, fy - foodRadius - 1, 4.5, 2.2, -Math.PI / 5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 3. Draw Smooth Snake Body & Distinct Head
const pts = getInterpolatedSnake(alpha);
if (pts.length > 0) {
ctx.save();
ctx.shadowColor = 'rgba(16, 185, 129, 0.35)';
ctx.shadowBlur = 12;
// Draw connected body spine for seamless corners
ctx.strokeStyle = '#10b981';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = CELL_SIZE * 0.72;
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();
// Draw individual tapered segments from tail to neck for organic scales feel
for (let i = pts.length - 1; i >= 1; i--) {
const p = pts[i];
const t = i / pts.length; // 0 near head, 1 at tail
const radius = (CELL_SIZE * 0.41) * (1 - t * 0.38);
ctx.fillStyle = i % 2 === 0 ? '#10b981' : '#059669';
ctx.beginPath();
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
ctx.fill();
}
// 4. Draw Distinct Snake Head with Eyes & Tongue
const headPt = pts[0];
const neckPt = pts[1] || { x: headPt.x - direction.x, y: headPt.y - direction.y };
const headAngle = Math.atan2(headPt.y - neckPt.y, headPt.x - neckPt.x);
ctx.translate(headPt.x, headPt.y);
ctx.rotate(headAngle);
// Flickering forked tongue
const tongueWave = Math.sin(timestamp * 0.025);
if (tongueWave > -0.2) {
const tongueLen = CELL_SIZE * 0.55 + tongueWave * 3;
ctx.strokeStyle = '#fb7185';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(CELL_SIZE * 0.3, 0);
ctx.lineTo(tongueLen, 0);
ctx.lineTo(tongueLen + 4, -3.5);
ctx.moveTo(tongueLen, 0);
ctx.lineTo(tongueLen + 4, 3.5);
ctx.stroke();
}
// Distinct sculpted head shape
const headGrad = ctx.createRadialGradient(-2, -3, 2, 0, 0, CELL_SIZE * 0.55);
headGrad.addColorStop(0, '#6ee7b7');
headGrad.addColorStop(0.55, '#34d399');
headGrad.addColorStop(1, '#059669');
ctx.fillStyle = headGrad;
ctx.beginPath();
ctx.ellipse(2, 0, CELL_SIZE * 0.48, CELL_SIZE * 0.43, 0, 0, Math.PI * 2);
ctx.fill();
// Subtle nostrils near snout
ctx.fillStyle = '#064e3b';
ctx.beginPath();
ctx.arc(CELL_SIZE * 0.3, -3, 1.2, 0, Math.PI * 2);
ctx.arc(CELL_SIZE * 0.3, 3, 1.2, 0, Math.PI * 2);
ctx.fill();
// Eyes facing movement direction
const eyeForward = CELL_SIZE * 0.12;
const eyeSpread = CELL_SIZE * 0.24;
const eyeRadius = CELL_SIZE * 0.145;
// Sclera (white of the eyes)
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(eyeForward, -eyeSpread, eyeRadius, 0, Math.PI * 2);
ctx.arc(eyeForward, eyeSpread, eyeRadius, 0, Math.PI * 2);
ctx.fill();
// Pupils looking slightly ahead towards food
const foodAngleWorld = Math.atan2(fy - headPt.y, fx - headPt.x);
const relFoodAngle = foodAngleWorld - headAngle;
const pupilShiftX = Math.cos(relFoodAngle) * 1.4;
const pupilShiftY = Math.sin(relFoodAngle) * 1.4;
ctx.fillStyle = '#0f172a';
ctx.beginPath();
ctx.arc(eyeForward + 1.2 + pupilShiftX, -eyeSpread + pupilShiftY, eyeRadius * 0.56, 0, Math.PI * 2);
ctx.arc(eyeForward + 1.2 + pupilShiftX, eyeSpread + pupilShiftY, eyeRadius * 0.56, 0, Math.PI * 2);
ctx.fill();
// Specular eye highlight
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(eyeForward + 0.5, -eyeSpread - 1.2, eyeRadius * 0.22, 0, Math.PI * 2);
ctx.arc(eyeForward + 0.5, eyeSpread - 1.2, eyeRadius * 0.22, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// 5. Render Particle Bursts
for (const p of particles) {
ctx.save();
ctx.globalAlpha = Math.max(0, p.alpha);
ctx.fillStyle = p.color;
ctx.shadowColor = p.color;
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// 6. Render Floating "+10" Score Popups
for (const ft of floatingTexts) {
ctx.save();
ctx.globalAlpha = Math.max(0, ft.alpha);
ctx.font = '800 15px -apple-system, BlinkMacSystemFont, sans-serif';
ctx.fillStyle = '#fde047';
ctx.textAlign = 'center';
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 4;
ctx.fillText(ft.text, ft.x, ft.y);
ctx.restore();
}
ctx.restore();
}
// Main Animation Loop
function loop(timestamp) {
const dt = Math.min((timestamp - lastFrameTime) / 1000, 0.1);
lastFrameTime = timestamp;
if (shakeTime > 0) {
shakeTime = Math.max(0, shakeTime - dt);
}
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life += dt;
if (p.life >= p.maxLife) {
particles.splice(i, 1);
continue;
}
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= 0.92;
p.vy *= 0.92;
p.alpha = 1 - p.life / p.maxLife;
}
// Update floating text
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const ft = floatingTexts[i];
ft.life += dt;
if (ft.life >= ft.maxLife) {
floatingTexts.splice(i, 1);
continue;
}
ft.y -= 28 * dt;
ft.alpha = 1 - ft.life / ft.maxLife;
}
// Step logic if playing or in demo
const stepDuration = mode === 'DEMO' ? DEMO_STEP_MS : BASE_STEP_MS;
if (mode === 'PLAYING' || mode === 'DEMO') {
stepAccumulator += dt * 1000;
while (stepAccumulator >= stepDuration) {
stepAccumulator -= stepDuration;
stepGame();
if (mode === 'GAMEOVER') {
stepAccumulator = 0;
break;
}
}
}
const alpha = (mode === 'PLAYING' || mode === 'DEMO')
? Math.min(1, stepAccumulator / stepDuration)
: 1;
render(alpha, timestamp);
requestAnimationFrame(loop);
}
// Input Handling
const KEY_DIR_MAP = {
ArrowUp: { x: 0, y: -1 },
KeyW: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
KeyS: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
KeyA: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
KeyD: { x: 1, y: 0 }
};
function startFromDemo(initialDir = null) {
initGame('PLAYING');
if (initialDir) {
queueDirection(initialDir);
}
}
window.addEventListener('keydown', (e) => {
const mappedDir = KEY_DIR_MAP[e.code];
// Prevent page scrolling on arrow keys or space
if (mappedDir || e.code === 'Space') {
e.preventDefault();
}
// If in Demo Mode, any key starts the player game immediately
if (mode === 'DEMO') {
startFromDemo(mappedDir);
return;
}
// Pause toggle (P)
if (e.code === 'KeyP') {
if (mode === 'PLAYING') {
mode = 'PAUSED';
updateOverlays();
} else if (mode === 'PAUSED') {
mode = 'PLAYING';
lastFrameTime = performance.now();
updateOverlays();
}
return;
}
// Restart on Space or Enter when Game Over
if (mode === 'GAMEOVER' && (e.code === 'Space' || e.code === 'Enter')) {
initGame('PLAYING');
return;
}
// Directional controls during active play
if (mode === 'PLAYING' && mappedDir) {
queueDirection(mappedDir);
}
});
// Mouse / Touch interactions
canvas.addEventListener('pointerdown', () => {
if (mode === 'DEMO') {
startFromDemo();
}
});
demoOverlay.addEventListener('pointerdown', () => {
if (mode === 'DEMO') {
startFromDemo();
}
});
resumeBtn.addEventListener('click', () => {
if (mode === 'PAUSED') {
mode = 'PLAYING';
lastFrameTime = performance.now();
updateOverlays();
}
});
restartBtn.addEventListener('click', () => {
initGame('PLAYING');
});
// On-screen D-Pad buttons
const DPAD_DIRS = {
UP: { x: 0, y: -1 },
DOWN: { x: 0, y: 1 },
LEFT: { x: -1, y: 0 },
RIGHT: { x: 1, y: 0 }
};
document.querySelectorAll('.dpad-btn').forEach(btn => {
btn.addEventListener('pointerdown', (e) => {
e.preventDefault();
const dir = DPAD_DIRS[btn.dataset.dir];
if (!dir) return;
if (mode === 'DEMO') {
startFromDemo(dir);
} else if (mode === 'PLAYING') {
queueDirection(dir);
}
});
});
// Start immediately in self-playing Demo Mode
initGame('DEMO');
requestAnimationFrame(loop);
})();
</script>
</body>
</html>