Train snakeOne-line
gpt-oss-120b
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 50.2 s
- Cost
- $0.0074
- Output tokens
- 9,772
- Spent thinking
- 7,958 tokens
- Speed
- 196.7 tok/s
- Console errors
- 0
- First token after
- 524 ms
- First visible text after
- 39.0 s
- Prompt tokens
- 425
- Code size
- 6.2 KB
- Finish reason
- stop
- Served by
- Parasail
- Model id
openai/gpt-oss-120b- Reasoning sent
{"effort":"high"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 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 · 237 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Train Snake Game Demo</title>
<style>
html,body {
margin:0;
padding:0;
height:100%;
overflow:hidden;
background:#111;
display:flex;
align-items:center;
justify-content:center;
font-family:sans-serif;
}
#gameContainer {
position:relative;
display:inline-block;
}
#game {
display:block;
background:#222;
}
#overlay {
position:absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
background:rgba(0,0,0,0.6);
color:#fff;
font-size:1.5rem;
text-align:center;
z-index:10;
cursor:pointer;
user-select:none;
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="game"></canvas>
<div id="overlay">Click or press any key to start controlling the train</div>
</div>
<script>
(() => {
const cellSize = 20; // size of each grid cell
let cols, rows;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const wagonColors = ['orange','yellow','lime','cyan','magenta','lightgreen','pink'];
const train = []; // segments {x,y}
let direction = {dx:1, dy:0}; // initial direction: right
const passengers = []; // {x,y}
const passengerCount = 5;
let userStarted = false;
function init() {
// Determine grid based on viewport size
const maxW = window.innerWidth - 20;
const maxH = window.innerHeight - 20;
cols = Math.max(10, Math.floor(maxW / cellSize));
rows = Math.max(10, Math.floor(maxH / cellSize));
canvas.width = cols * cellSize;
canvas.height = rows * cellSize;
// Initialize the train (locomotive + 3 wagons)
const startX = Math.floor(cols / 2);
const startY = Math.floor(rows / 2);
for (let i = 0; i < 4; i++) {
train.push({x: startX - i, y: startY});
}
// Spawn initial passengers
for (let i = 0; i < passengerCount; i++) spawnPassenger();
// Start the game loop
setInterval(gameLoop, 150);
draw(); // first draw
}
function spawnPassenger() {
let pos;
while (true) {
pos = {
x: Math.floor(Math.random() * cols),
y: Math.floor(Math.random() * rows)
};
const occupiedByTrain = train.some(s => s.x===pos.x && s.y===pos.y);
const occupiedByOther = passengers.some(p => p.x===pos.x && p.y===pos.y);
if (!occupiedByTrain && !occupiedByOther) break;
}
passengers.push(pos);
}
function findNearestPassenger() {
if (passengers.length===0) return null;
const head = train[0];
let nearest = null;
let bestDist = Infinity;
for (const p of passengers) {
const d = Math.abs(p.x-head.x)+Math.abs(p.y-head.y);
if (d < bestDist) {
bestDist = d;
nearest = p;
}
}
return nearest;
}
function aiChooseDirection() {
const target = findNearestPassenger();
if (!target) return;
const head = train[0];
if (head.x !== target.x) {
direction = {dx: head.x < target.x ? 1 : -1, dy:0};
} else if (head.y !== target.y) {
direction = {dx:0, dy: head.y < target.y ? 1 : -1};
}
}
function moveTrain() {
const newHead = {
x: (train[0].x + direction.dx + cols) % cols,
y: (train[0].y + direction.dy + rows) % rows
};
train.unshift(newHead);
// Check for passenger pickup
let ate = false;
for (let i = 0; i < passengers.length; i++) {
if (passengers[i].x===newHead.x && passengers[i].y===newHead.y) {
ate = true;
passengers.splice(i,1);
spawnPassenger();
break;
}
}
if (!ate) train.pop();
}
function drawGrid() {
ctx.strokeStyle = '#333';
ctx.lineWidth = 0.5;
for (let i = 0; i <= cols; i++) {
ctx.beginPath();
ctx.moveTo(i*cellSize,0);
ctx.lineTo(i*cellSize,canvas.height);
ctx.stroke();
}
for (let j = 0; j <= rows; j++) {
ctx.beginPath();
ctx.moveTo(0,j*cellSize);
ctx.lineTo(canvas.width,j*cellSize);
ctx.stroke();
}
}
function drawPassengers() {
ctx.fillStyle = '#fff';
for (const p of passengers) {
const cx = p.x*cellSize + cellSize/2;
const cy = p.y*cellSize + cellSize/2;
const r = cellSize*0.3;
ctx.beginPath();
ctx.arc(cx,cy,r,0,Math.PI*2);
ctx.fill();
}
}
function drawTrain() {
train.forEach((seg, idx) => {
// Locomotive (head) is dark red, wagons use colorful palette
ctx.fillStyle = idx===0 ? '#8B0000' : wagonColors[(idx-1)%wagonColors.length];
ctx.fillRect(seg.x*cellSize, seg.y*cellSize, cellSize, cellSize);
// Simple window on each carriage
ctx.fillStyle = 'rgba(255,255,255,0.3)';
const w = cellSize*0.5, h = cellSize*0.5;
ctx.fillRect(seg.x*cellSize+(cellSize-w)/2, seg.y*cellSize+(cellSize-h)/2, w, h);
});
// Chimney on locomotive for extra flair
const head = train[0];
ctx.fillStyle = '#555';
ctx.fillRect(head.x*cellSize + cellSize*0.6, head.y*cellSize + cellSize*0.1, cellSize*0.2, cellSize*0.4);
}
function draw() {
ctx.fillStyle = '#222';
ctx.fillRect(0,0,canvas.width,canvas.height);
drawGrid();
drawPassengers();
drawTrain();
}
function gameLoop() {
if (!userStarted) aiChooseDirection();
moveTrain();
draw();
}
function startUserControl() {
userStarted = true;
overlay.style.display = 'none';
}
// Input handling
overlay.addEventListener('click', startUserControl);
document.addEventListener('keydown', e => {
if (!userStarted) startUserControl();
switch (e.key) {
case 'ArrowUp':
if (direction.dy !== 1) direction = {dx:0, dy:-1};
break;
case 'ArrowDown':
if (direction.dy !== -1) direction = {dx:0, dy:1};
break;
case 'ArrowLeft':
if (direction.dx !== 1) direction = {dx:-1, dy:0};
break;
case 'ArrowRight':
if (direction.dx !== -1) direction = {dx:1, dy:0};
break;
}
});
// Initialize when ready
if (document.readyState === 'complete' || document.readyState === 'interactive') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
</script>
</body>
</html>Reasoning trace (7,958 tokens)
We need to produce a self-contained HTML file that shows a snake game, but the snake is a train: locomotive and wagons (colored). Passengers waiting on platform. The train moves on a grid, picks up passengers to grow. The demo must show something on first paint, with a "Click or press a key to start" message; a simple AI should play automatically behind that message, so the game can be judged without interaction. The page must adapt to wide and narrow viewports, no scrollbars.
We must comply with output rules: ONE fenced code block containing the complete file with html at top, everything inline.
No external resources except allowed CDNs. It's not required to use any external libs, but we can use pure JS.
Constraints:
- No localStorage etc.
- Not using alert/prompt/confirm.
- No errors.
- Must be able to run in sandbox iframe.
- Must show something on load, event water.
- Game start after user clicks or key; but we must have AI playing automatically behind a "Click or press a key to start" message. The message appears, but AI starts to run in background? Or maybe we could have the AI start automatically after a short delay even before user interaction? The requirement: "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." So we need to show a message overlay instructing to click, but under it the AI runs.
Thus we should have a canvas that draws the game. At start, the game begins with AI controlling the train, but also show overlay with text: "Click or press any key to start". The overlay maybe semi-transparent.
When user clicks or presses a key, the overlay disappears and user can take over? Maybe we can allow user to control with arrow keys after that.
But the injection: The AI should be playing even in the initial state, so the screenshot after 3 seconds shows visible movement. The overlay could be on top but not blocking the game visuals (transparent). That would be okay.
Alternatively, show the overlay before the game starts, then the AI starts after some seconds. But "behind a 'Click or press a key to start' message" implies that the demo should have both a message and AI playing behind it. So use overlay with message (maybe absolute positioned), but underlying game runs.
So how to implement:
We need a grid. Let's decide each cell is 20x20 pixels. The canvas size depends on viewport width and height. Could dynamically set canvas size to fill width/height while maintaining grid alignment.
Set board width = 30 cells; height = 20 cells, total canvas 600x400 (if each cell 20). But for larger screens we can increase, maybe scaling factor with window size.
Better to compute the number of columns = floor(window.innerWidth / cellSize) - 2 for margin, etc. Use a constant grid to simplify.
Simplify: Use canvas width = 600, height = 400, and center it. That's visible across devices. It's safe.
But need to adapt gracefully down to 375px; 600 > 375, but we can set max width. Could use CSS to set width: 100%; max-width: 600px; height: auto; to preserve aspect ratio. Then scale canvas via CSS if needed. Or we can set canvas width attribute physically to eg, 300x200, but then grid size is small. Might be okay.
Alternatively, we can define grid size based on viewport: compute number of columns = floor(viewport width / cellSize), rows = floor(viewport height / cellSize). That ensures scaling.
Simplify: Use 20x20 cells. Set cellSize = 20; compute cols = Math.max(16, Math.floor(window.innerWidth / cellSize) - 2 maybe). Not needed.
Better: Let's have responsive: set canvas width = Math.min(600, window.innerWidth - 20); height = Math.min(400, window.innerHeight - 20). But maintain integer cells: width = cols * cellSize; height = rows * cellSize.
Implementation plan:
- On load, set up canvas with dimensions.
- Game variables: train (array of segments), each segment has x,y. The locomotive is the head; wagons follow.
- Colors: locomotive maybe red, wagons each different colors like orange, yellow, green, blue, purple.
- Passengers: random positions on grid, not overlapping with train, maybe small circles or icons.
- Movement direction: up/down/left/right.
- AI: simple: moves towards nearest passenger by Manhattan distance. If head.x < passenger.x, go right; else if > move left; else if y < passenger.y, down; else up.
- The AI must avoid collisions with walls? Since train can go to any edge, but should not crash into itself (like snake). We'll treat snake collision: if head moves into any segment occupied except the tail (which will vacate), it's game over. AI may get stuck; but we can also ignore self-collision for a demo: just let the train bounce off walls maybe.
Simplify: to keep demo running indefinitely, we can wrap around edges (like Pac-Man). That way AI can always go after passenger.
- When train picks up passenger (head position matches passenger), the train grows: push a new segment at the tail, just maintain the segments like snake: we add a new segment by not removing tail on that tick.
- The passenger then respawns at a random empty cell.
- The AI will constantly move towards nearest passenger. That may produce movement that is simple.
- The overlay: CSS with position: absolute; top:0; left:0; width:100%; height:100%; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,0.5); color: white; font-size: 24px; pointer-events:none; (so click passes through?), but we need pointer-events maybe none so clicks pass through to canvas (the AI runs). But we want user to be able to start controlling after reading the message. We'll show message overlay but pointer-events:auto so click will hide overlay.
Better: Show overlay with a button or just a message "Click or press any key to start". When the user clicks anywhere, hide overlay and let user control via arrow keys. Meanwhile AI runs always anyway; but if user takes over with arrow keys, we need to stop AI controlling. Could implement a flag "userStarted" that disables AI and uses user input.
Implementation: On load, start gameLoop with AI controlling direction. Display overlay with message. When user clicks or keypress, set userStarted = true; hide overlay; arrow key listeners update direction.
The AI will if !userStarted.
Thus we satisfy "self-playing demo mode behind a message".
Edge Cases:
- The game should start with train of length maybe 3 wagons, each colored.
- The train head is locomotive: could draw a rectangle with a locomotive shape? Simple: draw a rectangle for each segment, but the head uses a different color (maybe dark red) and maybe draws a small rectangle. Or we can draw a "train" imagery: a rectangle with a front shape like a chest. But the simplest: draw a rectangle for locomotive with a black rectangle as front.
- Wagons: each a colored rectangle.
- Passengers: small circles colored yellow.
- Platform: maybe a grey background area, but we can just have background grid.
- We should display or animate movement of the train.
- The canvas size: set up responsive resizing on window resize.
- On resizing, we may adjust game board but it could be tricky. But we can ignore resizing after start.
Implement steps:
1. Document ready: set up canvas.
2. Compute cols = floor((window.innerWidth - 20) / cellSize) and rows similar.
Set canvas width = cols * cellSize; height = rows * cellSize.
Add style to center canvas.
Add container with relative position for overlay.
3. Initialize train array: e.g., head at middle of board, direction initially right.
train = [{x: Math.floor(cols/2), y: Math.floor(rows/2)}]; then add two wagons behind: tail segments.
Maybe starting length 4: head + 3 wagons.
Segment colors: predefine colors array: [ 'darkred', 'orange', 'yellow', 'green', 'cyan', 'magenta' ], use accordingly.
Define function drawTrain:
- For each segment at index i, draw rectangle at (seg.x * cellSize, seg.y * cellSize, cellSize, cellSize).
- Use color based on index: use separate colors: head color '#8B0000' (dark red), wagons from colors list.
- Optionally, draw a small black square on head to denote front.
But simplest: each segment colored: head dark red, subsequent wagons each color from an array.
Implement function drawPassengers: for each passenger position, draw a small circle.
Game loop:
- Variables: direction: {dx, dy}.
- AI: compute nearest passenger location. Use Manhattan distance. Determine direction: if head.x < target.x => dx = 1 and dy =0; else if > => dx = -1; else if head.y < target.y => dy=1; else if > => dy=-1. If both x and y differences exist, prefer x or y. Randomly choose to avoid being static.
- This direction will be set to direction.
- Movement: compute newHead = { x: (head.x + dx + cols) % cols, y: (head.y + dy + rows) % rows } to wrap.
- Insert newHead at front of train array.
- Check if passenger at newHead: if match any passenger: remove passenger (or move to new position). Then growth: keep tail (do not pop). Else pop tail.
- After moving, set train positions.
- Ensure segments don't overlap after growth? Might get overlapping self-collision. Since we ignore self-collision, fine.
- The passengers: generate initially 5 random positions.
- The passenger generation: ensure not overlapping train.
Game drawing:
- draw background (grid or plain color). Could also draw subtle grid lines.
- draw passengers.
- draw train.
Game loop using requestAnimationFrame or setInterval. For snake games, typical speed 8 moves per second (125ms). Use setInterval.
Simplify: set update rate of 150ms for visible movement.
Also call draw each tick.
Ensure no errors.
User input: arrow keys or WASD. Set event listeners for keydown for arrow keys.
When userStarted is true, the keydown changes direction. If not, ignore.
If userStarted false, AI sets direction.
Also we might handle pressing any key or click to start: hide overlay and set userStarted = true.
AI may still be moving after user control? When user starts, we disable AI.
Implementation details:
- Variables:
```
const cellSize = 20;
let cols, rows;
let canvas, ctx;
let train = [];
let direction = {dx: 1, dy: 0};
let passengers = [];
let colors = ['orange', 'yellow', 'green', 'cyan', 'magenta', 'lightblue'];
let userStarted = false;
```
- Setup:
```
function init() {
cols = Math.floor((window.innerWidth - 20) / cellSize);
rows = Math.floor((window.innerHeight - 20) / cellSize);
canvas = document.getElementById('game');
canvas.width = cols * cellSize;
canvas.height = rows * cellSize;
ctx = canvas.getContext('2d');
// init train length 4
const startX = Math.floor(cols / 2);
const startY = Math.floor(rows / 2);
for(let i = 0; i < 4; i++) {
train.push({x: startX - i, y: startY});
}
// place passengers
for(let i=0; i<5; i++) spawnPassenger();
// start loop
setInterval(gameLoop, 150);
}
```
- spawnPassenger:
```
function spawnPassenger() {
let pos;
while (true) {
pos = {x: Math.floor(Math.random()*cols), y: Math.floor(Math.random()*rows)};
let occupied = train.some(seg => seg.x===pos.x && seg.y===pos.y);
let exists = passengers.some(p=> p.x===pos.x && p.y===pos.y);
if (!occupied && !exists) break;
}
passengers.push(pos);
}
```
- gameLoop:
```
function gameLoop() {
if (!userStarted) {
// AI choose direction
let target = findNearestPassenger();
if (target) {
let hx = train[0].x;
let hy = train[0].y;
if (hx !== target.x) {
direction.dx = hx < target.x ? 1 : -1;
direction.dy = 0;
} else if (hy !== target.y) {
direction.dy = hy < target.y ? 1 : -1;
direction.dx = 0;
}
}
}
// move train
const newHead = {
x: (train[0].x + direction.dx + cols) % cols,
y: (train[0].y + direction.dy + rows) % rows
};
train.unshift(newHead);
// check passenger consumption
let ate = false;
for(let i=0;i<passengers.length;i++) {
if(passengers[i].x===newHead.x && passengers[i].y===newHead.y) {
ate = true;
passengers.splice(i,1);
spawnPassenger();
break;
}
}
if (!ate) {
train.pop();
}
draw();
}
```
- findNearestPassenger:
```
function findNearestPassenger() {
if (passengers.length===0) return null;
const head = train[0];
let minDist = Infinity, nearest = null;
for (let p of passengers) {
const d = Math.abs(p.x-head.x)+Math.abs(p.y-head.y);
if (d<minDist) {
minDist = d;
nearest = p;
}
}
return nearest;
}
```
- draw:
```
function draw() {
ctx.clearRect(0,0,canvas.width,canvas.height);
// background
ctx.fillStyle = '#222';
ctx.fillRect(0,0,canvas.width,canvas.height);
// grid optional
ctx.strokeStyle = '#444';
ctx.lineWidth = 0.5;
for(let i=0;i<=cols;i++) {
ctx.beginPath();
ctx.moveTo(i*cellSize,0);
ctx.lineTo(i*cellSize,canvas.height);
ctx.stroke();
}
for(let j=0;j<=rows;j++) {
ctx.beginPath();
ctx.moveTo(0,j*cellSize);
ctx.lineTo(canvas.width,j*cellSize);
ctx.stroke();
}
// passengers
for (const p of passengers) {
ctx.fillStyle = 'white';
ctx.beginPath();
const cx = p.x*cellSize + cellSize/2;
const cy = p.y*cellSize + cellSize/2;
const radius = cellSize*0.3;
ctx.arc(cx,cy,radius,0,2*Math.PI);
ctx.fill();
// optional passenger shape: small drawing?
}
// train segments
train.forEach((seg, idx) => {
if (idx===0) {
ctx.fillStyle = '#8B0000'; // locomotive
} else {
const col = colors[(idx-1) % colors.length];
ctx.fillStyle = col;
}
ctx.fillRect(seg.x*cellSize, seg.y*cellSize, cellSize, cellSize);
// optionally draw windows
if (idx===0) {
ctx.fillStyle = '#000';
ctx.fillRect(seg.x*cellSize + cellSize*0.2, seg.y*cellSize + cellSize*0.2, cellSize*0.6, cellSize*0.6);
}
});
}
```
- Input:
```
document.addEventListener('keydown', e => {
if (!userStarted) {
startUser();
}
const key = e.key;
if (key === 'ArrowUp' && direction.dy !== 1) {
direction = {dx:0, dy:-1};
} else if (key === 'ArrowDown' && direction.dy !== -1) {
direction = {dx:0, dy:1};
} else if (key === 'ArrowLeft' && direction.dx !==1) {
direction = {dx:-1, dy:0};
} else if (key === 'ArrowRight' && direction.dx !== -1) {
direction = {dx:1, dy:0};
}
});
```
- Click to start:
```
function startUser() {
userStarted = true;
const overlay = document.getElementById('overlay');
overlay.style.display = 'none';
}
document.addEventListener('click', e => {
if (!userStarted) {
startUser();
}
});
```
But note: we want click to pass through? If overlay is pointer-events:auto, user click will be captured and removed. That's fine.
Better to have overlay covering full screen, with pointer-events: auto. On click, hide overlay, set userStarted = true, which disables AI and prevents further key capture? Actually we still need key capture for user control.
Implementation details: We want the game to get key input only after overlay removed. Since we have keydown listener always active, but when user not started, we ignore key's direction? Actually we set direction to AI each tick irrespective of key. But we can handle early key to start user. In keydown, if !userStarted, we call startUser and then update direction accordingly.
Also for click, we just hide overlay and allow user control.
Now:
- On load, we also call init() after DOM content loaded.
- Additionally maybe we want the overlay to show centered text like "Click or press any key to take control".
- The overlay: a full-screen div with style: position:absolute; top:0; left:0; width:100%; height:100%; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,0.7); color:white; font-size:24px; z-index:10;.
- The canvas container: have a wrapper with position: relative; width: fit-content; margin:auto; All then overlay inside wrapper.
Actually we can have container <div id="gameContainer">, position: relative; holds <canvas id="game"></canvas> and <div id="overlay">Message</div>.
Make container centered using flex in body: body with display:flex; justify-content:center; align-items:center; min-height:100vh; background #111;.
Set canvas to be displayed block.
Potential issues:
- The overlay hides the canvas? It's absolute positioned over container, with higher z-index, but we want to see the moving train behind it. The overlay has semi-transparent background (rgba(0,0,0,0.5)), so background will see game behind but overlay text on top.
- Ensure overlay does not intercept canvas mouse events for controlling? When overlay is visible, we want clicks to hide it, but after it's hidden user can click on canvas but no need to capture.
- If we hide overlay on clicking anywhere (including on canvas) then overlay disappears before UI.
Edge Cases: When the overlay disappears, the user can press keys to control.
But we need to ensure the AI stops after user start; we set userStarted flag.
Potential infinite loop: AI may override direction after user start in next tick if we don't guard. In gameLoop, we check if (!userStarted). Good.
Alternatively we may want AI to not resume when user stops controlling. Good.
Now we need to adapt to viewports: At load for small widths (<375), cellSize*cols must be <= width. The code compute cols based on floor(window.innerWidth-20) / cellSize. At 375, 355/20=17 => canvas width 340. Good.
height: Math.floor((window.innerHeight-20)/20). Eg 800->780/20=39 => canvas height 780, max height 780. That's okay.
list: The canvas height may become large but okay.
If width small, the train may be small but visible.
Where to start? Train will be placed at middle.
Now, ensure we have no errors. Check all used variables defined.
Potential errors:
- In direction assignment inside keydown, use direction = {dx:..., dy:...}. But must not be let direction; direction is declared earlier var possibly const? We'll use let direction = {dx:1, dy:0} initially.
- In keydown we need to reference direction variable.
- Where to handle accidental reverse direction? We check if not reversing (like for snake you cannot move opposite direction if length >1). But for train, we may allow reverse? But to avoid immediate self-collision if we had self-collision constraints. But not needed.
- We may have multiple passengers: initial 5.
- The spawnPassenger ensures not overlapping train or other passengers.
- In drawPassengers, currently using ctx.fillStyle = 'white', but maybe use a distinct color like 'gold'.
- Draw of stationary grid lines may be fine.
- The code uses setInterval for gameLoop but also draws each tick.
- The AI's findNearestPassenger after user start flagged shouldn't be called.
- In keydown event, after userStarted, we want to set direction accordingly. We'll implement condition inside.
- The click event: When clicked, we call startUser() and hide overlay.
Potential issues: Overly small canvas may cause draw loops to be heavy. But fine.
Potential error: The overlay covering canvas may intercept the click events but we call startUser at document click. If overlay is above, click events may not propagate to document? Actually clicking overlay (on top) still triggers document click if using capture? But simpler: add event listener to overlay to hide. Or attach to container. We'll add overlay click to hide.
Implementation:
```
overlay.addEventListener('click', () => {
startUser();
});
```
Also keydown event in document is fine.
Thus the user can click overlay to start.
Now design CSS for overlay:
- width: 100%; height: 100%; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,0.5); color:#fff; font-size: 24px; user-select:none; cursor:pointer; text-align:center;
Now ensure no scrollbars: we set body margin:0; overflow:hidden; position.
Now test mental simulation:
When page loads:
- Body is flex center.
- Game container positioned relative.
- Canvas created with width, height.
- overlay visible.
- AI starts moving train automatically (since userStarted is false), and passes behind overlay.
- After a few seconds screenshot taken, we see moving colored train picking up passengers.
- The overlay is semi-transparent, but we see moving train.
- This meets requirements: "Self-playing demo mode behind a 'Click or press a key to start' message."
Potential enhancements: Use small images for passengers like small circles.
Optional: Add small windows to wagons: Could draw a lighter rectangle for each segment inner.
Simplify: Not needed.
Potential performance issues: drawing grid lines each frame heavy? Use small grid lines for aesthetic.
We can keep grid lines.
Let's implement with no errors.
Better to setup the game loop using setInterval, and also ensure that AI updates direction before moving.
Potential oversight: The AI's logic moves directly towards target; sometimes target may be behind due to wrap-around; but it's fine.
Potential collisions: Since we wrap around edges, train may go over itself eventually and might collide if we detect. We'll ignore collisions. At some point train length may increase to fill board, causing overlapping, but this is okay for demo.
Now write final HTML code.
The code:
```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Train Snake Game Demo</title>
<style>
html,body {
margin:0;
padding:0;
height:100%;
background:#111;
overflow:hidden;
display:flex;
align-items:center;
justify-content:center;
font-family:sans-serif;
}
#gameContainer {
position:relative;
display:inline-block;
}
#game {
display:block;
background:#222;
}
#overlay {
position:absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
background:rgba(0,0,0,0.6);
color:#fff;
font-size:1.5rem;
text-align:center;
z-index:10;
cursor:pointer;
user-select:none;
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="game"></canvas>
<div id="overlay">Click or press any key to start controlling the train</div>
</div>
<script>
(() => {
const cellSize = 20; // size of each grid cell in pixels
let cols, rows;
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const overlay = document.getElementById('overlay');
const trainColors = ['orange','yellow','lime','cyan','magenta','lightgreen','pink']; // for wagons
const train = []; // array of {x,y}
let direction = {dx:1, dy:0}; // initial moving right
const passengers = []; // array of {x,y}
const passengerCount = 5;
let userStarted = false;
function init() {
// compute grid size based on window dimensions
const maxWidth = window.innerWidth - 20;
const maxHeight = window.innerHeight - 20;
cols = Math.max(10, Math.floor(maxWidth / cellSize));
rows = Math.max(10, Math.floor(maxHeight / cellSize));
canvas.width = cols * cellSize;
canvas.height = rows * cellSize;
// create initial train (locomotive + 3 wagons)
const startX = Math.floor(cols / 2);
const startY = Math.floor(rows / 2);
for (let i = 0; i < 4; i++) {
train.push({x: startX - i, y: startY});
}
// spawn passengers
for (let i = 0; i < passengerCount; i++) {
spawnPassenger();
}
// start game loop
setInterval(gameLoop, 150);
draw(); // initial draw
}
function spawnPassenger() {
let pos;
while (true) {
pos = {
x: Math.floor(Math.random() * cols),
y: Math.floor(Math.random() * rows)
};
const occupiedByTrain = train.some(seg => seg.x===pos.x && seg.y===pos.y);
const occupiedByOther = passengers.some(p => p.x===pos.x && p.y===pos.y);
if (!occupiedByTrain && !occupiedByOther) break;
}
passengers.push(pos);
}
function findNearestPassenger() {
if (passengers.length === 0) return null;
const head = train[0];
let best = null;
let bestDist = Infinity;
for (const p of passengers) {
const d = Math.abs(p.x - head.x) + Math.abs(p.y - head.y);
if (d < bestDist) {
bestDist = d;
best = p;
}
}
return best;
}
function aiChooseDirection() {
const target = findNearestPassenger();
if (!target) return;
const head = train[0];
// Prefer horizontal movement if not aligned
if (head.x !== target.x) {
direction.dx = head.x < target.x ? 1 : -1;
direction.dy = 0;
} else if (head.y !== target.y) {
direction.dy = head.y < target.y ? 1 : -1;
direction.dx = 0;
}
}
function moveTrain() {
const newHead = {
x: (train[0].x + direction.dx + cols) % cols,
y: (train[0].y + direction.dy + rows) % rows
};
train.unshift(newHead);
// check passenger consumption
let ate = false;
for (let i = 0; i < passengers.length; i++) {
if (passengers[i].x === newHead.x && passengers[i].y === newHead.y) {
ate = true;
passengers.splice(i, 1);
spawnPassenger();
break;
}
}
if (!ate) {
train.pop(); // remove tail
}
}
function drawGrid() {
ctx.strokeStyle = '#333';
ctx.lineWidth = 0.5;
for (let i = 0; i <= cols; i++) {
ctx.beginPath();
ctx.moveTo(i * cellSize, 0);
ctx.lineTo(i * cellSize, canvas.height);
ctx.stroke();
}
for (let j = 0; j <= rows; j++) {
ctx.beginPath();
ctx.moveTo(0, j * cellSize);
ctx.lineTo(canvas.width, j * cellSize);
ctx.stroke();
}
}
function drawPassengers() {
for (const p of passengers) {
ctx.fillStyle = '#fff';
const radius = cellSize * 0.3;
const cx = p.x * cellSize + cellSize / 2;
const cy = p.y * cellSize + cellSize / 2;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fill();
}
}
function drawTrain() {
train.forEach((seg, idx) => {
if (idx === 0) {
// locomotive
ctx.fillStyle = '#8B0000'; // dark red
} else {
const col = trainColors[(idx - 1) % trainColors.length];
ctx.fillStyle = col;
}
ctx.fillRect(seg.x * cellSize, seg.y * cellSize, cellSize, cellSize);
// draw windows on each carriage for fun
ctx.fillStyle = 'rgba(255,255,255,0.3)';
const w = cellSize * 0.5;
const h = cellSize * 0.5;
ctx.fillRect(seg.x * cellSize + (cellSize - w)/2, seg.y * cellSize + (cellSize - h)/2, w, h);
});
// add a simple chimney on the locomotive
const head = train[0];
ctx.fillStyle = '#555';
ctx.fillRect(head.x * cellSize + cellSize * 0.6, head.y * cellSize + cellSize * 0.1, cellSize * 0.2, cellSize * 0.4);
}
function draw() {
// clear background
ctx.fillStyle = '#222';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawGrid();
drawPassengers();
drawTrain();
}
function gameLoop() {
if (!userStarted) {
aiChooseDirection();
}
moveTrain();
draw();
}
// Input handling
function startUserControl() {
userStarted = true;
overlay.style.display = 'none';
}
overlay.addEventListener('click', startUserControl);
document.addEventListener('keydown', (e) => {
if (!userStarted) startUserControl(); // any key starts
const key = e.key;
// prevent reverse when length > 1; simple check
switch (key) {
case 'ArrowUp':
if (direction.dy !== 1) direction = {dx:0, dy:-1};
break;
case 'ArrowDown':
if (direction.dy !== -1) direction = {dx:0, dy:1};
break;
case 'ArrowLeft':
if (direction.dx !== 1) direction = {dx:-1, dy:0};
break;
case 'ArrowRight':
if (direction.dx !== -1) direction = {dx:1, dy:0};
break;
}
});
// Start on DOM ready
if (document.readyState === 'complete' || document.readyState === 'interactive') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
</script>
</body>
</html>
```
Thus embed this in fenced block with html.
Check for any usage of external network resources: No.
Check for any use of disallowed storage: No.
Check for alert etc: none.
Check for errors:
- In canvas creation: getContext('2d') is okay.
- In drawGrid: lineWidth 0.5, but stroke works.
- In drawTrain, each segment draws a rectangle then draws a window overlay.
- At locomotive, after drawing full rectangle, we add a chimney.
Potential bug: In drawing windows, we used fillRect for windows; but may also hide rectangle overlapping with locomotive? But okay.
Potential bug: The overlay may have pointer-events default auto, so the click will hide overlay; after that the overlay remains hidden; user can click on canvas but not needed.
Potential bug: The keydown event may fire while overlay still visible before click; but we call startUserControl when any key pressed, hide overlay, then direction changes accordingly.
Potential bug: When AI moves automatically, direction may be overwritten each tick based on AI; if user has pressed key but userStarted hasn't been set (but we set on any key). In keydown we call startUserControl first, then set direction accordingly.
Therefore after user presses key, userStarted true => AI disabled, direction set. Good.
Potential bug: When there's a passenger directly aligned horizontally but also vertical distance >0, our AI prefers horizontal orientation first because for x not equal we do horizontal, else vertical. That's fine.
Potential bug: The code for movement uses direction object that may be replaced or mutated (like direction = {dx:..., dy:...}) which is fine.
Potential bug: We use trainColors for wagons. The train may become longer than colors length but we mod index.
Potential bug: If the train length grows long, but we ignore self-collision. The game loops may keep working.
Potential bug: Passengers may spawn on cells that the train will cover soon, but it's fine.
Potential bug: The grid draws each tick; may be heavy but fine.
Potential bug: In userStart control, overlay is hidden; but overlay still exist in DOM with display:none; fine.
Potential bug: We need to ensure that no horizontal scrollbars appear. The body flex and container inline-block; ok.
Potential bug: The canvas may be sized larger than viewport (if innerHeight - 20 results large but width limited). For narrow viewport, height may be large but vertical overflow may produce scrollbars; But we set html,body overflow:hidden; So any overflow is hidden. But we also adjust the canvas may exceed viewport height and cause half hidden. However screenshot of first paint may not see whole canvas but it's okay. But maybe better to limit both width and height to window dimensions minus some margin.
Our code sets canvas width (cols*cellSize) maybe up to, say innerHeight 800, cellSize=20 => 40 rows => 800 height. If window height is less than that e.g., 600, we compute rows = floor(maxHeight/20). Actually maxHeight = window.innerHeight - 20 -> according to code: rows = Math.max(10, floor(maxHeight/20)). So rows won't exceed height; so canvas height will fit.
Similarly width: maxWidth = window.innerWidth - 20, so width will fit.
Thus canvas fits within viewport.
Good.
Potential bug: We use body min-height 100% but body absolute? Already body 100% height.
Potential bug: Visual: grid lines may not be needed but okay.
Let's see the generated code, final answer must be just the code block.
Double-check for any stray punctuation.
Everything within script is inside an IIFE; inside that we have startUserControl defined after overlay and document event hooking.
We added document.addEventListener('keydown',...). In that, on first key press we run startUserControl before checking key code. That's fine: startUserControl hides overlay and set flag.
One more nuance: The keydown handler will call startUserControl potentially before user control update; after that direction may be changed accordingly.
Make sure direction is not undefined; default direction variable exists.
Potential bug: startUserControl sets overlay.style.display = 'none', but overlay remains in DOM but hidden.
All good.
Now ensure only HTML code.
We need to ensure that the fenced code block is exactly with language tag. Use ```html.
Thus final answer is:
```html
... (full code)
```