Train snakeOne-line
Claude Haiku 5.5
by AnthropicReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 50 s
- Cost
- $0.012
- Output tokens
- 24,583
- Spent thinking
- 14,494 tokens
- Speed
- 225.9 tok/s
- Console errors
- 0
- First token after
- 1.6 s
- First visible text after
- 1 min 20 s
- Prompt tokens
- 548
- Code size
- 19.2 KB
- Finish reason
- stop
- Served by
- Claude Platform on AWS
- Model id
anthropic/claude-haiku-5.5- Reasoning sent
{"effort":"low"}- Max output
- 32,000 tokens
- Generated
- Nov 10, 2026
What it was asked
The prompt
“make me a snake game where the snake is a little train with a locomotive and colorful wagons, picking up passengers waiting on the platform”
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 · 610 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Choo-Choo Snake</title>
<style>
* { box-sizing: border-box; }
html, body {
margin: 0; height: 100%; overflow: hidden;
background: linear-gradient(180deg, #1d3b4f 0%, #2f5d62 100%);
font-family: "Trebuchet MS", "Segoe UI", system-ui, sans-serif;
color: #fff;
user-select: none; -webkit-user-select: none;
}
body { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 8px; }
header {
width: min(960px, 100%); display: flex; justify-content: space-between; align-items: center;
flex-wrap: wrap; gap: 6px 12px;
}
h1 { margin: 0; font-size: clamp(18px, 3.2vw, 28px); letter-spacing: 1px; color: #ffd166; text-shadow: 0 2px 0 #7a4b00; }
.stats { display: flex; gap: 8px; flex-wrap: wrap; font-size: 14px; }
.stat { background: rgba(0,0,0,.28); padding: 4px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,.12); white-space: nowrap; }
.stat b { color: #ffd166; }
#wrap { position: relative; }
canvas {
display: block; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.45);
border: 4px solid #5a4632; touch-action: none; cursor: pointer;
}
#msg {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
background: rgba(15, 25, 35, .82); padding: 14px 22px; border-radius: 12px;
text-align: center; pointer-events: none; border: 2px solid #ffd166;
max-width: 90%; transition: opacity .4s;
}
#msg .big { font-size: clamp(16px, 2.6vw, 22px); font-weight: bold; color: #ffd166; }
#msg .small { font-size: 13px; opacity: .8; margin-top: 6px; }
#msg.hide { opacity: 0; }
footer { font-size: 12px; opacity: .65; text-align: center; }
@media (max-width: 480px) {
.stats { font-size: 12px; }
footer { font-size: 11px; }
}
</style>
</head>
<body>
<header>
<h1>Choo-Choo Snake</h1>
<div class="stats">
<span class="stat">Passengers <b id="score">0</b></span>
<span class="stat">Best <b id="best">0</b></span>
<span class="stat">Mode <b id="mode">Demo</b></span>
</div>
</header>
<div id="wrap">
<canvas id="c"></canvas>
<div id="msg">
<div class="big">Click or press a key to start</div>
<div class="small">Arrow keys / WASD or swipe to steer the train</div>
</div>
</div>
<footer>Pick up waiting passengers on the platforms and couple them as new wagons. Don't crash into your own train!</footer>
<script>
(() => {
const COLS = 24, ROWS = 16;
const PALETTE = ['#e63946', '#f4a261', '#2a9d8f', '#8ecae6', '#ffb703', '#9b5de5', '#06d6a0', '#ef476f', '#118ab2'];
const SKIN = ['#f1c27d', '#e0ac69', '#c68642', '#8d5524', '#ffdbac'];
const cv = document.getElementById('c');
const g = cv.getContext('2d');
const msgEl = document.getElementById('msg');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const modeEl = document.getElementById('mode');
let dpr = 1, cell = 30, W = 0, H = 0;
let segs, cols, dir, queue, passengers, particles, sparks;
let mode = 'ai', started = false, score = 0, best = 0;
let derailT = 0, paletteIdx = 0, tickAcc = 0, lastT = performance.now();
const TICK = 0.125;
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
const avail = Math.min(window.innerWidth - 20, 960);
cell = Math.max(12, Math.floor(avail / COLS));
W = cell * COLS; H = cell * ROWS;
cv.style.width = W + 'px';
cv.style.height = H + 'px';
cv.width = Math.round(W * dpr);
cv.height = Math.round(H * dpr);
g.setTransform(dpr, 0, 0, dpr, 0, 0);
}
const key = (x, y) => x + ',' + y;
const onSnake = (x, y) => segs.some(s => s.x === x && s.y === y);
const onPass = (x, y) => passengers.some(p => p.x === x && p.y === y);
function nextColor() {
paletteIdx = (paletteIdx + 1) % PALETTE.length;
return PALETTE[paletteIdx];
}
function spawnPassenger() {
let x, y, tries = 0;
do {
x = Math.floor(Math.random() * COLS);
y = Math.floor(Math.random() * ROWS);
tries++;
} while ((onSnake(x, y) || onPass(x, y)) && tries < 500);
passengers.push({
x, y,
color: PALETTE[Math.floor(Math.random() * PALETTE.length)],
skin: SKIN[Math.floor(Math.random() * SKIN.length)],
ph: Math.random() * 6.28
});
}
function targetCount() {
return Math.min(5, 3 + Math.floor(segs.length / 12));
}
function reset() {
const sx = 5, sy = 8;
segs = [
{ x: sx, y: sy, dx: sx, dy: sy },
{ x: sx - 1, y: sy, dx: sx - 1, dy: sy },
{ x: sx - 2, y: sy, dx: sx - 2, dy: sy }
];
cols = [null, PALETTE[0], PALETTE[2]];
dir = { x: 1, y: 0 };
queue = [];
passengers = [];
particles = [];
sparks = [];
derailT = 0;
while (passengers.length < 3) spawnPassenger();
updateHud();
}
function updateHud() {
scoreEl.textContent = score;
bestEl.textContent = best;
modeEl.textContent = mode === 'ai' ? 'Demo' : 'You';
}
function crash() {
derailT = 1.1;
for (let i = 0; i < 30; i++) {
sparks.push({
x: (segs[0].dx + 0.5) * cell, y: (segs[0].dy + 0.5) * cell,
vx: (Math.random() - 0.5) * 420, vy: (Math.random() - 0.5) * 420,
life: 1, color: Math.random() < .5 ? '#ffd166' : '#ef476f'
});
}
score = 0;
updateHud();
}
// ---------- AI: BFS towards nearest passenger, with safety fallback ----------
function chooseAI() {
const blocked = new Set(segs.slice(0, -1).map(s => key(s.x, s.y)));
const head = segs[0];
const dirs = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
const targets = new Set(passengers.map(p => key(p.x, p.y)));
const first = new Map();
const visited = new Set([key(head.x, head.y)]);
const q = [];
for (const d of dirs) {
if (d.x === -dir.x && d.y === -dir.y) continue;
const nx = head.x + d.x, ny = head.y + d.y;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const k = key(nx, ny);
if (blocked.has(k) || visited.has(k)) continue;
visited.add(k);
first.set(k, d);
q.push([nx, ny, d]);
}
let found = null;
for (let i = 0; i < q.length; i++) {
const [x, y, d] = q[i];
if (targets.has(key(x, y))) { found = d; break; }
for (const n of dirs) {
const nx = x + n.x, ny = y + n.y;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const k = key(nx, ny);
if (blocked.has(k) || visited.has(k)) continue;
visited.add(k);
q.push([nx, ny, d]);
}
}
if (found) { dir = found; return; }
// Fallback: pick the safe move with the most open space
let best = null, bestScore = -1;
for (const d of dirs) {
if (d.x === -dir.x && d.y === -dir.y) continue;
const nx = head.x + d.x, ny = head.y + d.y;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
if (blocked.has(key(nx, ny))) continue;
const sc = flood(nx, ny, blocked);
if (sc > bestScore) { bestScore = sc; best = d; }
}
if (best) dir = best;
}
function flood(sx, sy, blocked) {
const seen = new Set([key(sx, sy)]);
const q = [[sx, sy]];
while (q.length && seen.size < 150) {
const [x, y] = q.shift();
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
const nx = x + dx, ny = y + dy;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
const k = key(nx, ny);
if (blocked.has(k) || seen.has(k)) continue;
seen.add(k);
q.push([nx, ny]);
}
}
return seen.size;
}
// ---------- Game step ----------
function step() {
if (mode === 'ai') {
chooseAI();
} else if (queue.length) {
const d = queue.shift();
if (!(d.x === -dir.x && d.y === -dir.y)) dir = d;
}
const h = segs[0];
const nx = h.x + dir.x, ny = h.y + dir.y;
const pIdx = passengers.findIndex(p => p.x === nx && p.y === ny);
const eat = pIdx >= 0;
const bodyCheckEnd = eat ? segs.length : segs.length - 1;
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS ||
segs.slice(0, bodyCheckEnd).some(s => s.x === nx && s.y === ny)) {
crash();
return;
}
// Move: new locomotive head; old head becomes first wagon
segs.unshift({ x: nx, y: ny, dx: h.dx, dy: h.dy });
cols.unshift(null);
cols[1] = nextColor();
if (eat) {
const p = passengers.splice(pIdx, 1)[0];
cols[1] = p.color; // passenger couples on as a new wagon
score++;
if (score > best) best = score;
for (let i = 0; i < 16; i++) {
const a = Math.random() * 6.28, sp = 60 + Math.random() * 120;
sparks.push({
x: (p.x + 0.5) * cell, y: (p.y + 0.5) * cell,
vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - 40,
life: 1, color: p.color
});
}
while (passengers.length < targetCount()) spawnPassenger();
updateHud();
} else {
segs.pop();
cols.pop();
}
}
// ---------- Drawing ----------
function rr(x, y, w, h, r) {
g.beginPath();
g.moveTo(x + r, y);
g.arcTo(x + w, y, x + w, y + h, r);
g.arcTo(x + w, y + h, x, y + h, r);
g.arcTo(x, y + h, x, y, r);
g.arcTo(x, y, x + w, y, r);
g.closePath();
}
function drawBackground() {
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
g.fillStyle = (x + y) % 2 ? '#cfe8b5' : '#c4e0a4';
g.fillRect(x * cell, y * cell, cell, cell);
}
}
// little grass tufts
g.strokeStyle = 'rgba(60,120,50,.35)';
g.lineWidth = Math.max(1, cell * 0.05);
for (let y = 0; y < ROWS; y++) for (let x = 0; x < COLS; x++) {
if (((x * 7 + y * 13) % 5) !== 0) continue;
const cx = (x + 0.3) * cell, cy = (y + 0.75) * cell;
g.beginPath();
g.moveTo(cx, cy); g.lineTo(cx - cell * .08, cy - cell * .2);
g.moveTo(cx, cy); g.lineTo(cx + cell * .02, cy - cell * .24);
g.stroke();
}
}
function drawPassenger(p, t) {
const c = cell;
const x0 = p.x * c, y0 = p.y * c;
// platform tile
g.fillStyle = '#d8d2c4';
g.fillRect(x0 + c * .04, y0 + c * .04, c * .92, c * .92);
g.fillStyle = '#f2c14e';
g.fillRect(x0 + c * .04, y0 + c * .04, c * .92, c * .08);
g.strokeStyle = '#9a9384';
g.lineWidth = Math.max(1, c * .03);
g.strokeRect(x0 + c * .04, y0 + c * .04, c * .92, c * .92);
const cx = x0 + c * .5, cy = y0 + c * .58;
const wave = Math.sin(t * 5 + p.ph);
const lw = Math.max(1.5, c * .07);
g.lineCap = 'round';
// legs
g.strokeStyle = '#333';
g.lineWidth = lw;
g.beginPath();
g.moveTo(cx - c * .06, cy + c * .22); g.lineTo(cx - c * .08, cy + c * .36);
g.moveTo(cx + c * .06, cy + c * .22); g.lineTo(cx + c * .08, cy + c * .36);
g.stroke();
// body
g.fillStyle = p.color;
rr(cx - c * .14, cy - c * .02, c * .28, c * .26, c * .08);
g.fill();
// waving arm
g.strokeStyle = p.color;
g.lineWidth = lw;
g.beginPath();
g.moveTo(cx + c * .13, cy + c * .02);
g.lineTo(cx + c * .24, cy - c * .12 - wave * c * .08);
g.stroke();
// other arm
g.beginPath();
g.moveTo(cx - c * .13, cy + c * .02);
g.lineTo(cx - c * .2, cy + c * .12);
g.stroke();
// head
g.fillStyle = p.skin;
g.beginPath();
g.arc(cx, cy - c * .16, c * .12, 0, 6.283);
g.fill();
g.fillStyle = '#3b2a1a';
g.beginPath();
g.arc(cx, cy - c * .21, c * .13, Math.PI * 1.1, Math.PI * 1.9);
g.fill();
// sign-like suitcase
g.fillStyle = '#7a4b2a';
rr(cx - c * .32, cy + c * .14, c * .14, c * .1, c * .02);
g.fill();
}
function drawWagon(s, prev, color, t) {
const sx = (s.dx + 0.5) * cell, sy = (s.dy + 0.5) * cell;
const px = (prev.dx + 0.5) * cell, py = (prev.dy + 0.5) * cell;
const a = Math.atan2(py - sy, px - sx);
const c = cell;
g.save();
g.translate(sx, sy);
g.rotate(a);
// wheels
g.fillStyle = '#1b1b1b';
for (const wx of [-0.28, 0.28]) {
g.beginPath(); g.arc(wx * c, 0.31 * c, 0.09 * c, 0, 6.283); g.fill();
}
// body
g.fillStyle = color;
rr(-0.45 * c, -0.31 * c, 0.9 * c, 0.62 * c, 0.1 * c);
g.fill();
// roof stripe
g.fillStyle = 'rgba(0,0,0,.18)';
g.fillRect(-0.42 * c, -0.31 * c, 0.84 * c, 0.1 * c);
// windows
g.fillStyle = '#bde7f5';
g.fillRect(-0.32 * c, -0.18 * c, 0.2 * c, 0.2 * c);
g.fillRect(0.12 * c, -0.18 * c, 0.2 * c, 0.2 * c);
// peeking passenger in one window
const bob = Math.sin(t * 3 + s.dx * 2) * 0.01 * c;
g.fillStyle = '#f1c27d';
g.beginPath(); g.arc(0.22 * c, -0.08 * c + bob, 0.055 * c, 0, 6.283); g.fill();
// door line
g.strokeStyle = 'rgba(0,0,0,.35)';
g.lineWidth = Math.max(1, c * .03);
g.beginPath(); g.moveTo(-0.02 * c, -0.31 * c); g.lineTo(-0.02 * c, 0.31 * c); g.stroke();
g.restore();
}
function drawLoco(s, t) {
const sx = (s.dx + 0.5) * cell, sy = (s.dy + 0.5) * cell;
const a = Math.atan2(dir.y, dir.x);
const c = cell;
g.save();
g.translate(sx, sy);
g.rotate(a);
// wheels
g.fillStyle = '#111';
for (const wx of [-0.28, 0, 0.22]) {
g.beginPath(); g.arc(wx * c, 0.31 * c, 0.1 * c, 0, 6.283); g.fill();
}
// boiler body
g.fillStyle = '#d62828';
rr(-0.42 * c, -0.28 * c, 0.74 * c, 0.58 * c, 0.12 * c); g.fill();
// cab
g.fillStyle = '#9d0208';
rr(-0.44 * c, -0.38 * c, 0.3 * c, 0.74 * c, 0.07 * c); g.fill();
g.fillStyle = '#a8dadc';
rr(-0.36 * c, -0.22 * c, 0.14 * c, 0.14 * c, 0.03 * c); g.fill();
// gold stripe
g.fillStyle = '#ffb703';
g.fillRect(-0.12 * c, -0.03 * c, 0.4 * c, 0.07 * c);
// dome & chimney
g.fillStyle = '#2b2b2b';
g.beginPath(); g.arc(-0.02 * c, -0.28 * c, 0.08 * c, Math.PI, 0); g.fill();
rr(0.14 * c, -0.5 * c, 0.1 * c, 0.24 * c, 0.03 * c); g.fill();
// cow catcher
g.fillStyle = '#555';
g.beginPath();
g.moveTo(0.32 * c, 0.3 * c); g.lineTo(0.44 * c, 0.1 * c); g.lineTo(0.32 * c, 0.1 * c);
g.closePath(); g.fill();
// headlight
const glow = g.createRadialGradient(0.36 * c, 0, 0, 0.36 * c, 0, 0.5 * c);
glow.addColorStop(0, 'rgba(255,240,170,.6)');
glow.addColorStop(1, 'rgba(255,240,170,0)');
g.fillStyle = glow;
g.beginPath(); g.arc(0.36 * c, 0, 0.5 * c, 0, 6.283); g.fill();
g.fillStyle = '#fff6c2';
g.beginPath(); g.arc(0.36 * c, 0, 0.08 * c, 0, 6.283); g.fill();
g.restore();
}
function drawTrain(t) {
// couplings first
g.strokeStyle = '#2b2b2b';
g.lineWidth = cell * .1;
g.lineCap = 'round';
for (let i = 1; i < segs.length; i++) {
const a = segs[i], b = segs[i - 1];
g.beginPath();
g.moveTo((a.dx + .5) * cell, (a.dy + .5) * cell);
g.lineTo((b.dx + .5) * cell, (b.dy + .5) * cell);
g.stroke();
}
for (let i = segs.length - 1; i >= 1; i--) {
drawWagon(segs[i], segs[i - 1], cols[i] || PALETTE[0], t);
}
drawLoco(segs[0], t);
}
function chimneyWorld() {
const s = segs[0];
const a = Math.atan2(dir.y, dir.x);
const lx = 0.19 * cell, ly = -0.5 * cell;
const cx = (s.dx + .5) * cell, cy = (s.dy + .5) * cell;
return {
x: cx + lx * Math.cos(a) - ly * Math.sin(a),
y: cy + lx * Math.sin(a) + ly * Math.cos(a)
};
}
function updateFx(dt) {
// smoke puffs
if (Math.random() < dt * 9) {
const p = chimneyWorld();
particles.push({ x: p.x, y: p.y, vx: 8 + Math.random() * 10, vy: -25 - Math.random() * 15, life: 1, r: cell * (.14 + Math.random() * .08) });
}
for (const p of particles) {
p.x += p.vx * dt; p.y += p.vy * dt; p.r += cell * .35 * dt; p.life -= dt * .7;
}
particles = particles.filter(p => p.life > 0);
for (const s of sparks) {
s.x += s.vx * dt; s.y += s.vy * dt; s.vy += 500 * dt; s.life -= dt * 1.6;
}
sparks = sparks.filter(s => s.life > 0);
}
function draw(t) {
g.clearRect(0, 0, W, H);
drawBackground();
for (const p of passengers) drawPassenger(p, t);
for (const p of particles) {
g.globalAlpha = Math.max(0, p.life) * .55;
g.fillStyle = '#f5f5f5';
g.beginPath(); g.arc(p.x, p.y, p.r, 0, 6.283); g.fill();
}
g.globalAlpha = 1;
drawTrain(t);
for (const s of sparks) {
g.globalAlpha = Math.max(0, s.life);
g.fillStyle = s.color;
g.beginPath(); g.arc(s.x, s.y, cell * .09, 0, 6.283); g.fill();
}
g.globalAlpha = 1;
if (derailT > 0) {
g.fillStyle = 'rgba(200,30,30,.18)';
g.fillRect(0, 0, W, H);
g.fillStyle = '#fff';
g.font = `bold ${Math.round(cell * 1.6)}px system-ui, sans-serif`;
g.textAlign = 'center';
g.textBaseline = 'middle';
g.fillStyle = '#ffd166';
g.fillText('DERAILED!', W / 2, H / 2);
}
}
// ---------- Loop ----------
function frame(now) {
let dt = Math.min(0.1, (now - lastT) / 1000);
lastT = now;
const t = now / 1000;
if (derailT > 0) {
derailT -= dt;
if (derailT <= 0) reset();
} else {
tickAcc += dt;
while (tickAcc >= TICK) {
tickAcc -= TICK;
if (derailT <= 0) step();
if (derailT > 0) { tickAcc = 0; break; }
}
}
// ease drawn positions toward logical positions for smooth motion
for (const s of segs) {
s.dx += (s.x - s.dx) * Math.min(1, dt * 22);
s.dy += (s.y - s.dy) * Math.min(1, dt * 22);
}
updateFx(dt);
draw(t);
requestAnimationFrame(frame);
}
// ---------- Input ----------
const DIRS = {
ArrowUp: {x:0,y:-1}, ArrowDown: {x:0,y:1}, ArrowLeft: {x:-1,y:0}, ArrowRight: {x:1,y:0},
w: {x:0,y:-1}, s: {x:0,y:1}, a: {x:-1,y:0}, d: {x:1,y:0},
W: {x:0,y:-1}, S: {x:0,y:1}, A: {x:-1,y:0}, D: {x:1,y:0}
};
function startPlayer() {
if (started) return;
started = true;
mode = 'player';
score = 0;
msgEl.classList.add('hide');
reset();
updateHud();
}
window.addEventListener('keydown', e => {
if (e.ctrlKey || e.metaKey || e.altKey) return;
if (DIRS[e.key]) {
e.preventDefault();
if (!started) startPlayer();
if (mode === 'player') {
const d = DIRS[e.key];
const last = queue.length ? queue[queue.length - 1] : dir;
if (!(d.x === last.x && d.y === last.y) && !(d.x === -last.x && d.y === -last.y) && queue.length < 3) {
queue.push(d);
}
}
} else if (!started) {
startPlayer();
}
});
cv.addEventListener('click', () => { if (!started) startPlayer(); });
let touchStart = null;
cv.addEventListener('touchstart', e => {
const t = e.touches[0];
touchStart = { x: t.clientX, y: t.clientY };
if (!started) startPlayer();
}, { passive: true });
cv.addEventListener('touchend', e => {
if (!touchStart) return;
const t = e.changedTouches[0];
const dx = t.clientX - touchStart.x, dy = t.clientY - touchStart.y;
touchStart = null;
if (Math.max(Math.abs(dx), Math.abs(dy)) < 18) return;
const d = Math.abs(dx) > Math.abs(dy)
? (dx > 0 ? {x:1,y:0} : {x:-1,y:0})
: (dy > 0 ? {x:0,y:1} : {x:0,y:-1});
if (mode === 'player') queue.push(d);
}, { passive: true });
window.addEventListener('resize', resize);
// ---------- Boot ----------
resize();
reset();
updateHud();
requestAnimationFrame(t => { lastT = t; frame(t); });
})();
</script>
</body>
</html>Reasoning trace (14,494 tokens)
I'm planning a single HTML file: a snake styled as a train, with a locomotive head and colored wagons that grow as it collects passengers waiting on the platform. There's also a self-playing demo mode behind a "Click or press a key to start" prompt, all drawn on a canvas, and I'm weighing a 24x18 grid with a responsive canvas size. Now I'm working out the layout for the platform and passengers: they'll spawn on random free cells, capped at four at a time, drawn as small waving figures over yellow tiles. The wagons will cycle through a palette, with a locomotive head, chimney puffs, and a fixed 24x16 grid whose cell size scales to fit the window. Now I'm sorting out the game state: the snake as a list of segments with the locomotive at the front, each wagon carrying the color of the passenger it picked up. On each tick it moves one cell, and I still need to decide how the crash, pickup and growth logic fit together around that. Still working out the state handling: the wagon colors will be queued as passengers are picked up and attached to the tail as the snake grows. Still working out how the growth should work: a pending counter holds the grow steps, and the tail is kept on the tick that eats so the new wagon can take the passenger's color. Still working out the tail handling: when a passenger is eaten, I'll keep the tail in place and append a new wagon at that spot with the passenger's color, shifting the pending count down each tick until the growth is used up. I'm checking whether the duplicated tail segment actually works: after the next move the old tail is still there one cell behind, so the segments would sit on the same cell for a tick. I need to confirm the collision check and the growth step handle that overlap cleanly. I've settled that the tail should stay in place on the eating tick, with a copy of it added as the new wagon carrying the passenger's color. Checking the positions, the old tail and the new one don't collide, since each segment just takes the cell of the one before it on each move. Still working out the collision and growth rules: on an eating tick the tail stays put and a new wagon gets the passenger's color, and otherwise I'll check the new head against the body while leaving the tail out. Then the AI will run a BFS toward the nearest passenger through free cells, avoiding reversals. Now I'm turning to the passengers and the look of the scene: they spawn on random empty cells off the snake, three at a time from the start, and each is drawn on a yellow platform tile as a small figure waving its arms. The background will be a muted track-and-ballast pattern, kept fairly simple. The "Click or press a key to start" overlay stays up while the demo runs behind it, and once the player starts, the AI switches off. Now I'm settling how the game flows: once the player starts, they control the train, and a crash resets the snake while they stay in player mode. The start overlay only appears before the first input. I'm also adding simple swipe support on the canvas, and then I'll begin writing the code, sizing the canvas from the window width with a 24 by 16 grid. Still working on the rendering: the background is a light ground with a faint checker, the passengers sit on grey platform tiles with yellow edges and wave their arms, and the locomotive gets drawn rotated to face its direction of travel, with a red body, darker cab, and chimney. Still writing the drawing code: the locomotive is a red body with a darker cab, a chimney, and a yellow headlight, and each wagon is a rounded rectangle with windows and wheels, oriented toward the segment ahead of it. I'm also planning chimney steam puffs that rise and fade, and a score display above the canvas that counts delivered passengers. Still writing the setup code: I'm sizing the canvas from the window width with a 24-column grid, so cells stay small on phones, and scaling it for the device pixel ratio. Still writing the game loop: I'm handling the tick where the snake advances, checking for walls and its own body, and working out whether a passenger is eaten on that step. Still working through the eating step: rather than popping the tail, I'm planning to keep it and push a copy of it onto the end with the passenger's color, so the new wagon appears at the back. Still sorting out the growth rule: on an eating tick I'll skip removing the tail, so the old tail becomes the new wagon, and I just need to give it the passenger's color rather than adding an extra segment. I'm settling the eating case: the tail stays where it is, and I need to give the new wagon the passenger's color without recoloring the wagon that's already there. Still working out how eating a passenger changes the tail: the snake would grow by one, but the new position and the color it carries are still tangled up together. Still settling the eating case: I'll keep the tail where it is rather than popping it, so the snake grows by one, and each segment keeps its own color so the new wagon takes the passenger's color. I'm going around in circles on the growth case: duplicating the tail just gives the copy the passenger's color, and then the next pop removes that copy and leaves the original tail with its old color, so the extra wagon disappears. I'm settling the growth bookkeeping: instead of attaching colors to segment objects, which keeps breaking as positions shift, I'll store each wagon's color in a separate list indexed by wagon. On eating, I just append the passenger's color to that list, so the snake grows by one and the colors stay put. Still settling the colour bookkeeping: the wagon colours will be stored by position in the train, so when the head is added I'll shift the colour list too, and when the player eats, the new wagon's colour gets appended at the end. Still working through the bookkeeping: colours shift along with the segments, eating pushes the passenger's colour onto the tail, and the collision check skips the tail cell when the snake isn't growing. Now I'm settling the passenger spawning and drawing: I'll keep three to five passengers on the field, adding one more once the train gets long, and draw each on a yellow-edged platform tile with a figure whose arms wave. The background stays modest, a soft green ground with a faint checker, and when the snake crashes I'll show a short spark flash for about 0.8 seconds before resetting, or reset right away in demo mode. Still writing the game loop: I'm driving the updates through requestAnimationFrame with a time accumulator, so the snake ticks every 120ms (the AI a little faster) while the rendering runs every frame. Player input goes through the arrow keys and WASD with default prevented, and any key or a click starts player mode and hides the start prompt, after which crashes just reset the snake. I'm also planning a brief "Derailed!" flash, and rotating the locomotive to face its direction of travel using the heading angle. Still writing the drawing code: the locomotive is a rounded red body with a darker cab, a chimney, a yellow headlight, and three wheels, and I'm about to start on the wagon shape. Still writing the drawing code: each wagon is a rounded rectangle with two light-blue windows, a passenger peeking out of one, and wheels underneath, with a dark coupling drawn between neighbors first. Still writing the game loop: the step function does nothing while the snake is derailed, lets the AI pick a direction in demo mode, and otherwise applies the next queued player turn unless it would reverse straight into the body. Still writing the eating logic: the head moves forward, and the game checks whether the next cell hits a wall or the body, leaving the tail out of that check when the snake isn't growing. Still working out the growth step: on an eating tick the tail stays put, and I'm checking how the color list lines up with the segments once the new head is added. Still working out the growth case: on an eating tick the tail stays where it is, and the question is which colour the new last wagon should carry once the segments are shifted. Here I'm working through the growth case: on eating, the snake gains one cell, so the old tail stays and the head is added in front, but then the question is which wagon ends up carrying the passenger's colour, since the old tail's colour would otherwise be reused. I'm stuck on how eating changes the colours: keeping the tail in place makes the snake one cell longer, but the colour list is indexed by position, so the old tail's colour ends up being reused. The simplest outcome is that the tail wagon takes on the passenger's colour, so the train visibly gains a coloured wagon at the back. I'm still stuck on the growth case, since keeping the tail in place means the wagon colours no longer line up with the segments. Overwriting the tail's colour with the passenger's looks like the simplest way to make the train visibly gain a coloured wagon at the back. Working out the colour bookkeeping on eat: if the snake simply grows by one cell with the old tail kept in place, the list of wagon colours has to line up with the segments again. I'm checking how many wagons that leaves once the loco is counted separately. I'm realizing the old locomotive cell turns into a wagon once the snake moves, so it has to be given a color, which is the bug I've been hunting. I'm settling on giving each segment its own colour field, with the locomotive always at the front and the wagons after it. The old locomotive cell will need a colour once it becomes a wagon, so I'll handle that when the head moves. I'm settling how eating changes the colours: the old locomotive cell becomes the first wagon once the snake moves, so on an eating tick that wagon takes the passenger's colour and sits right behind the engine. The other wagons keep their colours, and when nothing is eaten the tail gets dropped as before. I'm still on the colour bookkeeping: if the segments stay index-aligned, the old locomotive cell has to become a wagon and so needs a colour of its own. I'm going to keep colours tied to positions so that normal moves preserve each wagon's colour, and only the eating case changes anything. Still sorting out the color bookkeeping: the old locomotive cell turns into a wagon on each move, so it needs a color, and when a passenger is eaten that new wagon takes the passenger's color while the others keep theirs. I'm thinking the colour list should stay in step with the segments: each move gives the old locomotive cell a palette colour as it turns into the first wagon, and on an eating tick that wagon takes the passenger's colour instead. The tail gets dropped on normal moves, so the train keeps a steady length and its wagons keep their colours as they travel. Now I'm settling the colour handling: each move gives the old locomotive cell a palette colour as it becomes the first wagon, and on an eating tick that wagon takes the passenger's colour instead. Non-eating moves drop the tail and keep the rest of the colours intact. Still writing the code: I'm setting up the canvas and a resize function that sizes the grid cells to fit the window, capping the device pixel ratio and keeping a minimum cell size. Still writing the game code: I'm choosing the AI's move by finding the nearest passenger with a breadth-first search that ignores the tail, and if none is reachable, falling back to the safe direction that leaves the most open space. Still writing the draw code: I'm filling the grass background cell by cell in two alternating shades, then drawing each platform with a tan base and yellow stripe, and placing a passenger figure in the middle with a head, body in their shirt colour, and arms. Still writing the render code: I'm drawing the passengers on their platforms with a waving arm, then the train, with a dark coupling line between each pair of cars. Still writing the wagon drawing: each wagon gets rotated to face the car ahead, with windows, a roof stripe, a passenger peeking out, and wheels underneath. Still working on the smoothing: each segment keeps a drawn position that eases toward its grid cell, so the train glides instead of snapping, and I need the wagon angles to come from those eased positions too. Now I'm adding the smoothing: the new head segment starts from the old head's drawn position so it glides rather than jumping, and the other segments keep their drawn positions too. The tail is still dropped as before, and the colours stay tied to their indices. Still writing the game code: I'm adding particle effects, a puff of smoke from the locomotive's chimney, and a burst of colored confetti when a passenger is picked up. If the train derails, I'll show a red "DERAILED!" flash for a second before resetting, and I'm putting a centered overlay on top of the canvas for the start prompt. Still writing the game loop: I'm stepping the simulation on a fixed tick, with a slightly faster tick in player mode, and wiring up input so the first key press or click starts the game. Still writing the game loop: I'm stepping the simulation on a fixed tick, and I'm making the crash timer count down in real time so the derail flash lasts a second whatever the tick rate. I'll also ease each segment's drawn position toward its grid cell so the train glides rather than snapping. Still writing the game code: I'm adding a screen-shake and a derail flash, and I'm working out the rounded-rectangle helper for drawing the wagon bodies. Still writing the locomotive drawing: I'm translating to its center, rotating it to match the direction of travel, and laying down a red body with a darker cab on top. Still writing the locomotive drawing: I'm laying out the cab, boiler stripe, and chimney, and placing the dome on top. Still writing the draw code: I'm adding the locomotive's wheels, headlight, and nameplate-free front, then planning the background as alternating cells with a rail pattern. Still writing the background: I'm filling the field with a grass checker pattern and a gray gravel border, and I've decided a dark overlay card will hold the start prompt.