Train snakeOne-line
Mistral Small 4
by MistralReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 11 s
- Cost
- $0.0089
- Output tokens
- 14,748
- Spent thinking
- 11,581 tokens
- Speed
- 208.4 tok/s
- Console errors
- 0
- First token after
- 711 ms
- First visible text after
- 58.4 s
- Prompt tokens
- 380
- Code size
- 11.3 KB
- Finish reason
- stop
- Served by
- Mistral
- Model id
mistralai/mistral-small-2603- Reasoning sent
{"effort":"high"}- Max output
- 209,715 tokens
- Generated
- Nov 12, 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 · 392 lines
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Train Snake Game</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #222;
font-family: Arial, sans-serif;
overflow: hidden;
}
#game-container {
position: relative;
width: 100%;
max-width: 800px;
height: 600px;
}
#game-canvas {
background: #111;
display: block;
}
#start-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
background: rgba(0,0,0,0.7);
padding: 20px 30px;
border-radius: 10px;
cursor: pointer;
text-align: center;
z-index: 10;
}
#score-display {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-size: 20px;
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
z-index: 10;
}
#game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
background: rgba(0,0,0,0.8);
padding: 20px 30px;
border-radius: 10px;
text-align: center;
z-index: 10;
display: none;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="start-message">Click or press a key to start</div>
<div id="score-display">Score: 0</div>
<div id="game-over">Game Over!<br>Click to restart</div>
</div>
<script>
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const startMessage = document.getElementById('start-message');
const scoreDisplay = document.getElementById('score-display');
const gameOverDisplay = document.getElementById('game-over');
function resizeCanvas() {
const container = document.getElementById('game-container');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
const TRAIN_SPEED = 4;
const PLATFORM_HEIGHT = 100;
const TRAIN_WIDTH = 20;
const TRAIN_HEIGHT = 12;
const PASSENGER_RADIUS = 8;
const COLORS = ['#FF5252', '#FF9800', '#FFEB3B', '#4CAF50', '#2196F3', '#9C27B0'];
let gameState = {
train: {
head: { x: 0, y: 0 },
direction: 'right',
wagons: [],
colorIndex: 0
},
passengers: [],
score: 0,
gameActive: false,
gameOver: false,
playerControlled: false,
lastUpdateTime: 0
};
function initGame() {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
gameState = {
train: {
head: { x: centerX, y: centerY },
direction: 'right',
wagons: [
{ x: centerX - TRAIN_WIDTH, y: centerY },
{ x: centerX - TRAIN_WIDTH * 2, y: centerY }
],
colorIndex: 0
},
passengers: [],
score: 0,
gameActive: false,
gameOver: false,
playerControlled: false,
lastUpdateTime: 0
};
for (let i = 0; i < 3; i++) {
spawnPassenger();
}
}
function spawnPassenger() {
const x = Math.random() * (canvas.width - 40) + 20;
const y = canvas.height - PLATFORM_HEIGHT + Math.random() * (PLATFORM_HEIGHT - PASSENGER_RADIUS * 2) + PASSENGER_RADIUS;
gameState.passengers.push({ x, y });
}
function moveTrain() {
const head = gameState.train.head;
const wagons = gameState.train.wagons;
const direction = gameState.train.direction;
const prevPositions = [{ x: head.x, y: head.y }];
for (const wagon of wagons) {
prevPositions.push({ x: wagon.x, y: wagon.y });
}
switch (direction) {
case 'up': head.y -= TRAIN_SPEED; break;
case 'down': head.y += TRAIN_SPEED; break;
case 'left': head.x -= TRAIN_SPEED; break;
case 'right': head.x += TRAIN_SPEED; break;
}
for (let i = 0; i < wagons.length; i++) {
wagons[i].x = prevPositions[i].x;
wagons[i].y = prevPositions[i].y;
}
}
function growTrain() {
const tail = gameState.train.wagons.length > 0
? gameState.train.wagons[gameState.train.wagons.length - 1]
: gameState.train.head;
gameState.train.wagons.push({
x: tail.x,
y: tail.y
});
gameState.score++;
scoreDisplay.textContent = `Score: ${gameState.score}`;
}
function checkCollisions() {
const head = gameState.train.head;
const wagons = gameState.train.wagons;
if (head.x < 0 || head.x >= canvas.width || head.y < 0 || head.y >= canvas.height) {
gameState.gameOver = true;
return;
}
for (const wagon of wagons) {
const dx = head.x - wagon.x;
const dy = head.y - wagon.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < TRAIN_WIDTH) {
gameState.gameOver = true;
return;
}
}
for (let i = 0; i < gameState.passengers.length; i++) {
const passenger = gameState.passengers[i];
const dx = head.x - passenger.x;
const dy = head.y - passenger.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < TRAIN_WIDTH + PASSENGER_RADIUS) {
gameState.passengers.splice(i, 1);
spawnPassenger();
growTrain();
break;
}
}
}
function drawTrain() {
const head = gameState.train.head;
const wagons = gameState.train.wagons;
ctx.fillStyle = '#D32F2F';
ctx.fillRect(head.x - TRAIN_WIDTH/2, head.y - TRAIN_HEIGHT/2, TRAIN_WIDTH, TRAIN_HEIGHT);
ctx.beginPath();
ctx.moveTo(head.x + TRAIN_WIDTH/2, head.y - TRAIN_HEIGHT/2);
ctx.lineTo(head.x + TRAIN_WIDTH, head.y);
ctx.lineTo(head.x + TRAIN_WIDTH/2, head.y + TRAIN_HEIGHT/2);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#666';
ctx.fillRect(head.x - TRAIN_WIDTH/2 + 2, head.y - TRAIN_HEIGHT/2 - 10, 4, 8);
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(head.x + TRAIN_WIDTH/2 + 2, head.y, 3, 0, Math.PI * 2);
ctx.fill();
for (let i = 0; i < wagons.length; i++) {
const wagon = wagons[i];
const color = COLORS[(i + 1) % COLORS.length];
ctx.fillStyle = color;
ctx.fillRect(wagon.x - TRAIN_WIDTH/2, wagon.y - TRAIN_HEIGHT/2, TRAIN_WIDTH, TRAIN_HEIGHT);
ctx.fillStyle = '#888';
ctx.fillRect(wagon.x - TRAIN_WIDTH/2 - 2, wagon.y - TRAIN_HEIGHT/4, 4, TRAIN_HEIGHT/2);
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(wagon.x - TRAIN_WIDTH/2 + 4, wagon.y - TRAIN_HEIGHT/3, TRAIN_WIDTH - 8, TRAIN_HEIGHT/2 - 2);
}
}
function drawPassengers() {
ctx.fillStyle = '#333';
ctx.fillRect(0, canvas.height - PLATFORM_HEIGHT, canvas.width, PLATFORM_HEIGHT);
ctx.fillStyle = '#555';
ctx.fillRect(0, canvas.height - PLATFORM_HEIGHT, canvas.width, 2);
for (const passenger of gameState.passengers) {
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(passenger.x, passenger.y, PASSENGER_RADIUS, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(passenger.x - 4, passenger.y - 4, 2, 0, Math.PI * 2);
ctx.arc(passenger.x + 4, passenger.y - 4, 2, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(passenger.x - 3, passenger.y + 2);
ctx.lineTo(passenger.x + 3, passenger.y + 2);
ctx.stroke();
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawPassengers();
drawTrain();
if (gameState.gameOver) {
gameOverDisplay.style.display = 'block';
} else {
gameOverDisplay.style.display = 'none';
}
}
function gameLoop(timestamp) {
if (!gameState.gameActive) {
requestAnimationFrame(gameLoop);
return;
}
if (!gameState.lastUpdateTime) {
gameState.lastUpdateTime = timestamp;
}
const deltaTime = timestamp - gameState.lastUpdateTime;
gameState.lastUpdateTime = timestamp;
if (deltaTime >= 1000 / 60) {
if (!gameState.playerControlled) {
if (Math.random() < 0.02) {
const directions = ['up', 'down', 'left', 'right'];
const newDir = directions[Math.floor(Math.random() * directions.length)];
if (!(newDir === 'up' && gameState.train.direction === 'down') &&
!(newDir === 'down' && gameState.train.direction === 'up') &&
!(newDir === 'left' && gameState.train.direction === 'right') &&
!(newDir === 'right' && gameState.train.direction === 'left')) {
gameState.train.direction = newDir;
}
}
}
moveTrain();
checkCollisions();
gameState.lastUpdateTime = timestamp - (deltaTime % (1000 / 60));
}
draw();
requestAnimationFrame(gameLoop);
}
function handleKeyPress(e) {
if (!gameState.gameActive && !gameState.gameOver) {
initGame();
gameState.gameActive = true;
startMessage.style.display = 'none';
requestAnimationFrame(gameLoop);
return;
}
if (gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.gameOver = false;
gameOverDisplay.style.display = 'none';
requestAnimationFrame(gameLoop);
return;
}
if (gameState.playerControlled) {
switch (e.key) {
case 'ArrowUp':
if (gameState.train.direction !== 'down') gameState.train.direction = 'up';
break;
case 'ArrowDown':
if (gameState.train.direction !== 'up') gameState.train.direction = 'down';
break;
case 'ArrowLeft':
if (gameState.train.direction !== 'right') gameState.train.direction = 'left';
break;
case 'ArrowRight':
if (gameState.train.direction !== 'left') gameState.train.direction = 'right';
break;
}
}
}
function handleClick() {
if (!gameState.gameActive && !gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.playerControlled = true;
startMessage.style.display = 'none';
requestAnimationFrame(gameLoop);
} else if (gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.gameOver = false;
gameState.playerControlled = true;
gameOverDisplay.style.display = 'none';
requestAnimationFrame(gameLoop);
}
}
document.addEventListener('keydown', handleKeyPress);
startMessage.addEventListener('click', handleClick);
gameOverDisplay.addEventListener('click', handleClick);
initGame();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>Reasoning trace (11,581 tokens)
The user wants a Snake game with a train theme. The snake should be a train with a locomotive and colorful wagons, and it should pick up passengers waiting on a platform. Let me break this down:
1. Game mechanics: Classic Snake game where the snake grows when it eats food
2. Theme: Train with locomotive (head) and wagons (body segments)
3. Visual elements:
- Train: locomotive (head) + colorful wagons (body segments)
- Passengers waiting on platforms
- Food: should probably be passengers to pick up
4. Gameplay: Snake moves around, picks up passengers (food), grows longer
5. UI: Platform with waiting passengers, train moving on tracks
Let me think about the design:
- The game area should have tracks (maybe horizontal lines)
- A platform on one side with passengers waiting
- The train starts with a locomotive and maybe 1-2 wagons
- When the train passes over a passenger, it "picks them up" (the passenger disappears and the train grows)
- The train grows by adding wagons
Technical considerations:
- Use HTML5 Canvas or SVG for rendering
- Since it's a web demo, Canvas might be simpler for a game like this
- Need game loop with requestAnimationFrame
- Need collision detection
- Need to handle keyboard input (arrow keys)
- Need to start with a "Click to start" message
Let me structure the code:
1. HTML structure with canvas
2. CSS styling for the container
3. JavaScript for game logic:
- Game state (train position, direction, wagons, passengers, score)
- Game loop
- Input handling
- Collision detection
- Drawing functions
For the train:
- Locomotive: rectangle with a pointy front
- Wagons: rectangles attached to the back of the locomotive
- Each segment should be colorful
For passengers:
- Circles or simple shapes on the platform
- When train passes over them (or when head collides with them), they disappear and a new wagon is added
Wait, the user said "picking up passengers waiting on the platform". So passengers are on the platform, and the train needs to go near them to pick them up. In classic Snake, food appears randomly. Here, maybe passengers spawn on the platform and the train needs to "collect" them by moving over their position.
Let me think about the layout:
- Platform on the left side (vertical platform)
- Tracks going horizontally to the right
- Train moves horizontally on the tracks
- Passengers appear on the platform (vertical line)
- When the train's head reaches the platform position and a passenger is there, the passenger is picked up
Actually, re-reading: "picking up passengers waiting on the platform". This suggests the platform is a fixed location where passengers wait, and the train needs to stop or pass by to pick them up. But in a continuous Snake game, the train is always moving.
Maybe a better interpretation:
- The platform is a fixed area (like a station)
- Passengers spawn on the platform
- The train moves along tracks
- When the train's head collides with a passenger, the passenger is picked up (removed from platform, train grows)
- The train can only pick up passengers when it's at the platform location
But that seems too restrictive. Let me think differently:
- The platform is along the track
- Passengers are waiting at various points along the platform
- The train moves along the track
- When any part of the train (head or wagons) passes over a passenger, they get picked up
Actually, in classic Snake, the snake eats food anywhere. So maybe:
- Passengers spawn randomly on the platform area
- The train moves along the track
- When the train's head collides with a passenger, the passenger is picked up (score increases, train grows by one wagon)
The platform could be a vertical strip on the left where passengers spawn. The train moves horizontally on the right side.
Let me sketch the layout:
```
[Platform with passengers] | [Tracks with train]
Passengers appear here Train moves horizontally
```
But the user said "picking up passengers waiting on the platform", which suggests the platform is where they wait, and the train needs to go to the platform to pick them up.
Alternative interpretation:
- The platform is a fixed location
- Passengers spawn on the platform
- The train moves around
- When the train reaches the platform location, it can pick up all waiting passengers at once
- Each passenger picked up adds a wagon
But that seems less like a Snake game and more like a different mechanic.
Let me go with a more classic Snake interpretation:
- The game area has a platform area (maybe bottom or side)
- Passengers spawn on the platform
- The train moves around the area
- When the train's head collides with a passenger, the passenger is picked up (removed, score increases, train grows by one segment)
- The train grows longer with each passenger picked up
For the visual theme:
- Locomotive: rectangle with a triangle front, maybe with a smokestack
- Wagons: colorful rectangles attached to the back
- Passengers: simple circles or stick figures on the platform
- Platform: a horizontal strip at the bottom with some texture
Let me plan the code structure:
HTML:
- Container div
- Canvas element
- Start message overlay
CSS:
- Style the container to be responsive
- Position the canvas
- Style the start message
JavaScript:
- Game constants (grid size, colors, etc.)
- Game state:
- train: { head: {x, y}, direction, wagons: [{x,y}, ...] }
- passengers: [{x, y}, ...]
- score
- gameActive
- gameOver
- Functions:
- init(): set up initial state
- spawnPassenger(): add a new passenger at random position on platform
- moveTrain(): update train position based on direction
- growTrain(): add a new wagon
- checkCollisions(): check for passenger pickup, wall collision, self collision
- draw(): render everything
- gameLoop(): main loop
- handleKeyPress(): change direction
- startGame(): initialize and start
For the train visualization:
- Locomotive: a rectangle with a triangle at the front (pointing in direction of movement)
- Wagons: rectangles attached sequentially
- Each segment should be slightly offset or connected visually
For the platform:
- A horizontal strip at the bottom (y = height - platformHeight)
- Passengers are circles or simple shapes on this strip
Let me think about the grid system:
- Use a grid where each cell is a certain size
- Train segments align to grid
- Passengers spawn on grid positions
Actually, for a smooth game, I might want to use pixel coordinates rather than strict grid, but for simplicity, let's use a grid system.
Let me define:
- GRID_SIZE = 20
- TRAIN_SPEED = 5 (pixels per frame)
- But since we're using requestAnimationFrame, speed will be in pixels per frame
Wait, if I use a grid system where the train moves one grid cell at a time, it would be more like classic Snake. But the user wants a train that moves continuously.
Let me go with continuous movement:
- Train head has x, y position
- Each frame, head moves in current direction by speed amount
- Wagons follow the head with a delay (each wagon follows the previous segment)
For the train visual:
- Locomotive: a rectangle with a triangle front
- Wagons: rectangles
- Colors: locomotive could be red, wagons could cycle through colors
For passengers:
- Circles with simple faces
- Spawn on the platform area
Platform area:
- Bottom 100px of the canvas
- Passengers spawn at random x positions within platform area
Game mechanics:
1. Train moves continuously
2. Passengers spawn randomly on platform
3. When train head collides with a passenger:
- Remove that passenger
- Add a new wagon to the train
- Increase score
4. If train collides with itself (wagon hits head) or walls, game over
Wait, the user said "picking up passengers waiting on the platform". This suggests passengers are waiting at stations, not randomly spawned. Maybe:
- There are station platforms at certain positions
- Passengers wait at stations
- Train must stop at stations to pick up passengers
But that complicates things. Let me re-read: "picking up passengers waiting on the platform"
I think the simplest interpretation that fits a Snake game is:
- The platform is a fixed area (like a station)
- Passengers spawn on this platform
- The train moves around the game area
- When the train passes over the platform area and collides with a passenger, the passenger is picked up
Actually, let me think of a train station:
- Platform is a long strip
- Train moves along parallel tracks
- Passengers wait on the platform
- When the train is close enough to the platform, passengers can board
In game terms:
- Platform is a vertical strip on the left
- Train moves horizontally on the right side of the platform
- When the train's head is at the platform x-position and a passenger is at that y-position, the passenger is picked up
But that would mean the train can only pick up passengers when it's exactly at the platform x position.
Alternative: The platform is at the bottom, and the train moves around the whole area. Passengers spawn on the platform (bottom area), and when the train moves over them (any part of the train), they get picked up.
I think this is the most reasonable interpretation for a Snake-like game.
Let me design the visual layout:
- Canvas fills the container
- Platform: bottom 100px strip, colored gray
- Passengers: circles with simple faces on the platform
- Tracks: maybe some horizontal lines where the train moves
- Train: moves on the upper part of the canvas
Actually, for a train theme, the tracks should be visible. Let me make:
- Background: dark gray (for night scene)
- Platform: light gray strip at bottom
- Tracks: two parallel lines running horizontally across the canvas
- Train: moves along the tracks
- Passengers: waiting on the platform
When the train passes over a passenger (when the train's head is at the same y-position as a passenger and within x-range), the passenger is picked up.
But to make it more interesting, maybe:
- The platform is on the left side (vertical platform)
- Passengers wait vertically on the platform
- The train moves horizontally on the right
- When the train's head reaches the platform x-position and there's a passenger at the train's y-position, the passenger is picked up
This makes the platform act like a station where passengers wait in a line, and the train must stop at the station to pick them up.
Let me go with this interpretation:
- Platform is a vertical strip on the left (x = 0 to platformWidth)
- Passengers spawn at random y positions within the platform area
- Train moves horizontally on the right side (x > platformWidth)
- When the train's head x position is within the platform area and there's a passenger at the train's y position, the passenger is picked up
But this would mean the train can only pick up passengers when it's at x < platformWidth, which is unusual for a train.
Alternative: The platform is along the track, and passengers wait at various points along it. The train moves along the track, and when any part of the train passes over a passenger, they get picked up.
Let me simplify and go with a more classic Snake setup:
- Game area is a rectangle
- Platform is a strip at the bottom (y > height - 100)
- Passengers spawn randomly on the platform (within that strip)
- Train moves around the entire area
- When the train's head collides with a passenger, the passenger is picked up
This is simple and fits the "picking up" mechanic.
For the train visual:
- Locomotive: a rectangle with a triangle at the front
- Wagons: rectangles attached to the back
- Colors: locomotive red, wagons in different colors
For passengers:
- Circles with two dots for eyes
- Different colors for different passengers
Game flow:
1. Show "Click to start" message
2. When clicked, start game
3. Train moves automatically (self-playing demo)
4. Passengers spawn randomly on platform
5. When train head hits a passenger, it disappears and train grows
6. If train hits itself or walls, game over
Let me code this step by step.
First, the HTML structure:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Train Snake Game</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #222;
font-family: Arial, sans-serif;
overflow: hidden;
}
#game-container {
position: relative;
width: 100%;
max-width: 800px;
height: 600px;
}
#game-canvas {
background: #111;
display: block;
}
#start-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
background: rgba(0,0,0,0.7);
padding: 20px 30px;
border-radius: 10px;
cursor: pointer;
text-align: center;
}
#score-display {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-size: 20px;
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="start-message">Click or press a key to start</div>
<div id="score-display">Score: 0</div>
</div>
<script>
// Game code will go here
</script>
</body>
</html>
```
Now the JavaScript game logic:
Constants:
```javascript
const GRID_SIZE = 20;
const TRAIN_SPEED = 5;
const PLATFORM_HEIGHT = 100;
const COLORS = ['#FF5252', '#FF9800', '#FFEB3B', '#4CAF50', '#2196F3', '#9C27B0'];
```
Game state:
```javascript
let gameState = {
train: {
head: { x: 300, y: 300 },
direction: 'right',
wagons: [
{ x: 280, y: 300 },
{ x: 260, y: 300 }
],
colorIndex: 0
},
passengers: [],
score: 0,
gameActive: false,
gameOver: false
};
```
Functions:
- init(): set up initial state
- spawnPassenger(): add a new passenger
- moveTrain(): update train position
- growTrain(): add a new wagon
- checkCollisions(): check for passenger pickup and game over conditions
- draw(): render everything
- gameLoop(): main loop
- handleKeyPress(): change direction
- startGame(): initialize and start
Let me think about the train movement:
- The train head moves in the current direction
- Wagons follow the head with a delay (each wagon follows the previous segment)
- When growing, a new wagon is added at the tail position
For the visual train:
- Locomotive: rectangle with triangle front
- Wagons: rectangles
- Colors: locomotive is red, wagons use COLORS array
For passengers:
- Circles with simple faces
- Spawn on the platform area (y > canvasHeight - PLATFORM_HEIGHT)
Let me write the complete code:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Train Snake Game</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #222;
font-family: Arial, sans-serif;
overflow: hidden;
}
#game-container {
position: relative;
width: 100%;
max-width: 800px;
height: 600px;
}
#game-canvas {
background: #111;
display: block;
}
#start-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
background: rgba(0,0,0,0.7);
padding: 20px 30px;
border-radius: 10px;
cursor: pointer;
text-align: center;
z-index: 10;
}
#score-display {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-size: 20px;
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
z-index: 10;
}
#game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
background: rgba(0,0,0,0.8);
padding: 20px 30px;
border-radius: 10px;
text-align: center;
z-index: 10;
display: none;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="start-message">Click or press a key to start</div>
<div id="score-display">Score: 0</div>
<div id="game-over">Game Over!<br>Click to restart</div>
</div>
<script>
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const startMessage = document.getElementById('start-message');
const scoreDisplay = document.getElementById('score-display');
const gameOverDisplay = document.getElementById('game-over');
// Set canvas size
function resizeCanvas() {
const container = document.getElementById('game-container');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Game constants
const GRID_SIZE = 20;
const TRAIN_SPEED = 4;
const PLATFORM_HEIGHT = 100;
const TRAIN_WIDTH = 20;
const TRAIN_HEIGHT = 12;
const PASSENGER_RADIUS = 8;
const COLORS = ['#FF5252', '#FF9800', '#FFEB3B', '#4CAF50', '#2196F3', '#9C27B0'];
// Game state
let gameState = {
train: {
head: { x: 0, y: 0 },
direction: 'right',
wagons: [],
colorIndex: 0
},
passengers: [],
score: 0,
gameActive: false,
gameOver: false,
lastUpdateTime: 0
};
// Initialize game
function initGame() {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
gameState = {
train: {
head: { x: centerX, y: centerY },
direction: 'right',
wagons: [
{ x: centerX - TRAIN_WIDTH, y: centerY },
{ x: centerX - TRAIN_WIDTH * 2, y: centerY }
],
colorIndex: 0
},
passengers: [],
score: 0,
gameActive: false,
gameOver: false,
lastUpdateTime: 0
};
// Spawn initial passengers
for (let i = 0; i < 3; i++) {
spawnPassenger();
}
}
// Spawn a new passenger on the platform
function spawnPassenger() {
const x = Math.random() * (canvas.width - 40) + 20;
const y = canvas.height - PLATFORM_HEIGHT + Math.random() * (PLATFORM_HEIGHT - PASSENGER_RADIUS * 2) + PASSENGER_RADIUS;
gameState.passengers.push({ x, y });
}
// Move the train
function moveTrain() {
const head = gameState.train.head;
const wagons = gameState.train.wagons;
const direction = gameState.train.direction;
// Store previous positions for wagon movement
const prevPositions = [{ x: head.x, y: head.y }];
for (const wagon of wagons) {
prevPositions.push({ x: wagon.x, y: wagon.y });
}
// Move head based on direction
switch (direction) {
case 'up':
head.y -= TRAIN_SPEED;
break;
case 'down':
head.y += TRAIN_SPEED;
break;
case 'left':
head.x -= TRAIN_SPEED;
break;
case 'right':
head.x += TRAIN_SPEED;
break;
}
// Move wagons to follow the previous segment
for (let i = 0; i < wagons.length; i++) {
wagons[i].x = prevPositions[i].x;
wagons[i].y = prevPositions[i].y;
}
}
// Grow the train by adding a wagon
function growTrain() {
const tail = gameState.train.wagons.length > 0
? gameState.train.wagons[gameState.train.wagons.length - 1]
: gameState.train.head;
gameState.train.wagons.push({
x: tail.x,
y: tail.y
});
gameState.score++;
scoreDisplay.textContent = `Score: ${gameState.score}`;
}
// Check for collisions and pickups
function checkCollisions() {
const head = gameState.train.head;
const wagons = gameState.train.wagons;
// Check if train hit the wall
if (head.x < 0 || head.x >= canvas.width || head.y < 0 || head.y >= canvas.height) {
gameState.gameOver = true;
return;
}
// Check if train hit itself (wagon hits head)
for (const wagon of wagons) {
const dx = head.x - wagon.x;
const dy = head.y - wagon.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < TRAIN_WIDTH) {
gameState.gameOver = true;
return;
}
}
// Check if head picked up a passenger
for (let i = 0; i < gameState.passengers.length; i++) {
const passenger = gameState.passengers[i];
const dx = head.x - passenger.x;
const dy = head.y - passenger.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Pickup distance
if (distance < TRAIN_WIDTH + PASSENGER_RADIUS) {
// Remove passenger
gameState.passengers.splice(i, 1);
// Add new passenger
spawnPassenger();
// Grow train
growTrain();
break;
}
}
}
// Draw the train
function drawTrain() {
const head = gameState.train.head;
const wagons = gameState.train.wagons;
const colorIndex = gameState.train.colorIndex;
// Draw locomotive (head)
ctx.fillStyle = '#D32F2F'; // Red locomotive
// Main body
ctx.fillRect(head.x - TRAIN_WIDTH/2, head.y - TRAIN_HEIGHT/2, TRAIN_WIDTH, TRAIN_HEIGHT);
// Draw triangle front
ctx.beginPath();
ctx.moveTo(head.x + TRAIN_WIDTH/2, head.y - TRAIN_HEIGHT/2);
ctx.lineTo(head.x + TRAIN_WIDTH, head.y);
ctx.lineTo(head.x + TRAIN_WIDTH/2, head.y + TRAIN_HEIGHT/2);
ctx.closePath();
ctx.fill();
// Draw smokestack
ctx.fillStyle = '#666';
ctx.fillRect(head.x - TRAIN_WIDTH/2 + 2, head.y - TRAIN_HEIGHT/2 - 10, 4, 8);
// Draw headlight
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(head.x + TRAIN_WIDTH/2 + 2, head.y, 3, 0, Math.PI * 2);
ctx.fill();
// Draw wagons
let wagonX = head.x - TRAIN_WIDTH;
for (let i = 0; i < wagons.length; i++) {
const wagon = wagons[i];
const color = COLORS[(colorIndex + i + 1) % COLORS.length];
// Main body
ctx.fillStyle = color;
ctx.fillRect(wagon.x - TRAIN_WIDTH/2, wagon.y - TRAIN_HEIGHT/2, TRAIN_WIDTH, TRAIN_HEIGHT);
// Connecting rod
ctx.fillStyle = '#888';
ctx.fillRect(wagon.x - TRAIN_WIDTH/2 - 2, wagon.y - TRAIN_HEIGHT/4, 4, TRAIN_HEIGHT/2);
// Window
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(wagon.x - TRAIN_WIDTH/2 + 4, wagon.y - TRAIN_HEIGHT/3, TRAIN_WIDTH - 8, TRAIN_HEIGHT/2 - 2);
}
}
// Draw passengers
function drawPassengers() {
ctx.fillStyle = '#777';
// Draw platform
ctx.fillRect(0, canvas.height - PLATFORM_HEIGHT, canvas.width, PLATFORM_HEIGHT);
for (const passenger of gameState.passengers) {
// Passenger body
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(passenger.x, passenger.y, PASSENGER_RADIUS, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(passenger.x - 4, passenger.y - 4, 2, 0, Math.PI * 2);
ctx.arc(passenger.x + 4, passenger.y - 4, 2, 0, Math.PI * 2);
ctx.fill();
// Mouth (simple line)
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(passenger.x - 3, passenger.y + 2);
ctx.lineTo(passenger.x + 3, passenger.y + 2);
ctx.stroke();
}
}
// Draw game
function draw() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw background
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw platform and passengers
drawPassengers();
// Draw train
drawTrain();
// Draw game over message
if (gameState.gameOver) {
gameOverDisplay.style.display = 'block';
} else {
gameOverDisplay.style.display = 'none';
}
}
// Game loop
function gameLoop(timestamp) {
if (!gameState.gameActive || gameState.gameOver) {
requestAnimationFrame(gameLoop);
return;
}
// Calculate delta time for smooth movement
if (!gameState.lastUpdateTime) {
gameState.lastUpdateTime = timestamp;
}
const deltaTime = timestamp - gameState.lastUpdateTime;
gameState.lastUpdateTime = timestamp;
// Move train at consistent speed regardless of frame rate
if (deltaTime >= 1000 / 60) { // ~60fps
moveTrain();
checkCollisions();
gameState.lastUpdateTime = timestamp - (deltaTime % (1000 / 60));
}
draw();
requestAnimationFrame(gameLoop);
}
// Handle keyboard input
function handleKeyPress(e) {
if (!gameState.gameActive && !gameState.gameOver) {
initGame();
gameState.gameActive = true;
startMessage.style.display = 'none';
requestAnimationFrame(gameLoop);
return;
}
if (gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.gameOver = false;
gameOverDisplay.style.display = 'none';
requestAnimationFrame(gameLoop);
return;
}
// Change direction based on key
switch (e.key) {
case 'ArrowUp':
if (gameState.train.direction !== 'down') {
gameState.train.direction = 'up';
}
break;
case 'ArrowDown':
if (gameState.train.direction !== 'up') {
gameState.train.direction = 'down';
}
break;
case 'ArrowLeft':
if (gameState.train.direction !== 'right') {
gameState.train.direction = 'left';
}
break;
case 'ArrowRight':
if (gameState.train.direction !== 'left') {
gameState.train.direction = 'right';
}
break;
}
}
// Handle click to start
function handleClick() {
if (!gameState.gameActive && !gameState.gameOver) {
initGame();
gameState.gameActive = true;
startMessage.style.display = 'none';
requestAnimationFrame(gameLoop);
} else if (gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.gameOver = false;
gameOverDisplay.style.display = 'none';
requestAnimationFrame(gameLoop);
}
}
// Event listeners
document.addEventListener('keydown', handleKeyPress);
startMessage.addEventListener('click', handleClick);
gameOverDisplay.addEventListener('click', handleClick);
// Initialize
initGame();
</script>
</body>
</html>
```
Wait, I need to make sure the train starts in a self-playing demo mode. Currently, it only moves when the user provides input. Let me modify it so the train moves automatically in demo mode, and the user can click to start playing.
Actually, looking at the code, the train only moves when gameState.gameActive is true, which is set by user input. I need to make it so that in demo mode, the train moves automatically, and when the user clicks, control is transferred to the player.
Let me revise the approach:
1. Game starts in demo mode: train moves automatically (simple AI)
2. When user clicks or presses a key, control switches to player
3. Player can control the train with arrow keys
4. If game over, show message and allow restart
Let me update the code to include a simple AI for demo mode:
```javascript
// In the game loop, if in demo mode, change direction randomly
function gameLoop(timestamp) {
if (!gameState.gameActive) {
requestAnimationFrame(gameLoop);
return;
}
// Calculate delta time
if (!gameState.lastUpdateTime) {
gameState.lastUpdateTime = timestamp;
}
const deltaTime = timestamp - gameState.lastUpdateTime;
gameState.lastUpdateTime = timestamp;
// Move train at consistent speed
if (deltaTime >= 1000 / 60) {
// Demo mode: change direction randomly
if (!gameState.playerControlled) {
if (Math.random() < 0.02) { // 2% chance per frame to change direction
const directions = ['up', 'down', 'left', 'right'];
const newDir = directions[Math.floor(Math.random() * directions.length)];
// Don't reverse direction completely
if (!(newDir === 'up' && gameState.train.direction === 'down') &&
!(newDir === 'down' && gameState.train.direction === 'up') &&
!(newDir === 'left' && gameState.train.direction === 'right') &&
!(newDir === 'right' && gameState.train.direction === 'left')) {
gameState.train.direction = newDir;
}
}
}
moveTrain();
checkCollisions();
gameState.lastUpdateTime = timestamp - (deltaTime % (1000 / 60));
}
draw();
requestAnimationFrame(gameLoop);
}
// In handleKeyPress, set playerControlled flag
function handleKeyPress(e) {
if (!gameState.gameActive && !gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.playerControlled = true;
startMessage.style.display = 'none';
requestAnimationFrame(gameLoop);
return;
}
if (gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.gameOver = false;
gameState.playerControlled = true;
gameOverDisplay.style.display = 'none';
requestAnimationFrame(gameLoop);
return;
}
// Only allow direction changes if player controlled
if (gameState.playerControlled) {
switch (e.key) {
case 'ArrowUp':
if (gameState.train.direction !== 'down') {
gameState.train.direction = 'up';
}
break;
case 'ArrowDown':
if (gameState.train.direction !== 'up') {
gameState.train.direction = 'down';
}
break;
case 'ArrowLeft':
if (gameState.train.direction !== 'right') {
gameState.train.direction = 'left';
}
break;
case 'ArrowRight':
if (gameState.train.direction !== 'left') {
gameState.train.direction = 'right';
}
break;
}
}
}
```
Also, the start message should say "Click or press a key to start" which it already does.
Let me also make sure the train starts moving immediately in demo mode. I need to set gameState.playerControlled to false initially.
Let me revise the complete code:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Train Snake Game</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #222;
font-family: Arial, sans-serif;
overflow: hidden;
}
#game-container {
position: relative;
width: 100%;
max-width: 800px;
height: 600px;
}
#game-canvas {
background: #111;
display: block;
}
#start-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
background: rgba(0,0,0,0.7);
padding: 20px 30px;
border-radius: 10px;
cursor: pointer;
text-align: center;
z-index: 10;
}
#score-display {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-size: 20px;
background: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
z-index: 10;
}
#game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
background: rgba(0,0,0,0.8);
padding: 20px 30px;
border-radius: 10px;
text-align: center;
z-index: 10;
display: none;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="start-message">Click or press a key to start</div>
<div id="score-display">Score: 0</div>
<div id="game-over">Game Over!<br>Click to restart</div>
</div>
<script>
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const startMessage = document.getElementById('start-message');
const scoreDisplay = document.getElementById('score-display');
const gameOverDisplay = document.getElementById('game-over');
// Set canvas size
function resizeCanvas() {
const container = document.getElementById('game-container');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Game constants
const GRID_SIZE = 20;
const TRAIN_SPEED = 4;
const PLATFORM_HEIGHT = 100;
const TRAIN_WIDTH = 20;
const TRAIN_HEIGHT = 12;
const PASSENGER_RADIUS = 8;
const COLORS = ['#FF5252', '#FF9800', '#FFEB3B', '#4CAF50', '#2196F3', '#9C27B0'];
// Game state
let gameState = {
train: {
head: { x: 0, y: 0 },
direction: 'right',
wagons: [],
colorIndex: 0
},
passengers: [],
score: 0,
gameActive: false,
gameOver: false,
playerControlled: false,
lastUpdateTime: 0
};
// Initialize game
function initGame() {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
gameState = {
train: {
head: { x: centerX, y: centerY },
direction: 'right',
wagons: [
{ x: centerX - TRAIN_WIDTH, y: centerY },
{ x: centerX - TRAIN_WIDTH * 2, y: centerY }
],
colorIndex: 0
},
passengers: [],
score: 0,
gameActive: false,
gameOver: false,
playerControlled: false,
lastUpdateTime: 0
};
// Spawn initial passengers
for (let i = 0; i < 3; i++) {
spawnPassenger();
}
}
// Spawn a new passenger on the platform
function spawnPassenger() {
const x = Math.random() * (canvas.width - 40) + 20;
const y = canvas.height - PLATFORM_HEIGHT + Math.random() * (PLATFORM_HEIGHT - PASSENGER_RADIUS * 2) + PASSENGER_RADIUS;
gameState.passengers.push({ x, y });
}
// Move the train
function moveTrain() {
const head = gameState.train.head;
const wagons = gameState.train.wagons;
const direction = gameState.train.direction;
// Store previous positions for wagon movement
const prevPositions = [{ x: head.x, y: head.y }];
for (const wagon of wagons) {
prevPositions.push({ x: wagon.x, y: wagon.y });
}
// Move head based on direction
switch (direction) {
case 'up':
head.y -= TRAIN_SPEED;
break;
case 'down':
head.y += TRAIN_SPEED;
break;
case 'left':
head.x -= TRAIN_SPEED;
break;
case 'right':
head.x += TRAIN_SPEED;
break;
}
// Move wagons to follow the previous segment
for (let i = 0; i < wagons.length; i++) {
wagons[i].x = prevPositions[i].x;
wagons[i].y = prevPositions[i].y;
}
}
// Grow the train by adding a wagon
function growTrain() {
const tail = gameState.train.wagons.length > 0
? gameState.train.wagons[gameState.train.wagons.length - 1]
: gameState.train.head;
gameState.train.wagons.push({
x: tail.x,
y: tail.y
});
gameState.score++;
scoreDisplay.textContent = `Score: ${gameState.score}`;
}
// Check for collisions and pickups
function checkCollisions() {
const head = gameState.train.head;
const wagons = gameState.train.wagons;
// Check if train hit the wall
if (head.x < 0 || head.x >= canvas.width || head.y < 0 || head.y >= canvas.height) {
gameState.gameOver = true;
return;
}
// Check if train hit itself (wagon hits head)
for (const wagon of wagons) {
const dx = head.x - wagon.x;
const dy = head.y - wagon.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < TRAIN_WIDTH) {
gameState.gameOver = true;
return;
}
}
// Check if head picked up a passenger
for (let i = 0; i < gameState.passengers.length; i++) {
const passenger = gameState.passengers[i];
const dx = head.x - passenger.x;
const dy = head.y - passenger.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Pickup distance
if (distance < TRAIN_WIDTH + PASSENGER_RADIUS) {
// Remove passenger
gameState.passengers.splice(i, 1);
// Add new passenger
spawnPassenger();
// Grow train
growTrain();
break;
}
}
}
// Draw the train
function drawTrain() {
const head = gameState.train.head;
const wagons = gameState.train.wagons;
// Draw locomotive (head)
ctx.fillStyle = '#D32F2F'; // Red locomotive
// Main body
ctx.fillRect(head.x - TRAIN_WIDTH/2, head.y - TRAIN_HEIGHT/2, TRAIN_WIDTH, TRAIN_HEIGHT);
// Draw triangle front
ctx.beginPath();
ctx.moveTo(head.x + TRAIN_WIDTH/2, head.y - TRAIN_HEIGHT/2);
ctx.lineTo(head.x + TRAIN_WIDTH, head.y);
ctx.lineTo(head.x + TRAIN_WIDTH/2, head.y + TRAIN_HEIGHT/2);
ctx.closePath();
ctx.fill();
// Draw smokestack
ctx.fillStyle = '#666';
ctx.fillRect(head.x - TRAIN_WIDTH/2 + 2, head.y - TRAIN_HEIGHT/2 - 10, 4, 8);
// Draw headlight
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(head.x + TRAIN_WIDTH/2 + 2, head.y, 3, 0, Math.PI * 2);
ctx.fill();
// Draw wagons
for (let i = 0; i < wagons.length; i++) {
const wagon = wagons[i];
const color = COLORS[(i + 1) % COLORS.length];
// Main body
ctx.fillStyle = color;
ctx.fillRect(wagon.x - TRAIN_WIDTH/2, wagon.y - TRAIN_HEIGHT/2, TRAIN_WIDTH, TRAIN_HEIGHT);
// Connecting rod
ctx.fillStyle = '#888';
ctx.fillRect(wagon.x - TRAIN_WIDTH/2 - 2, wagon.y - TRAIN_HEIGHT/4, 4, TRAIN_HEIGHT/2);
// Window
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(wagon.x - TRAIN_WIDTH/2 + 4, wagon.y - TRAIN_HEIGHT/3, TRAIN_WIDTH - 8, TRAIN_HEIGHT/2 - 2);
}
}
// Draw passengers
function drawPassengers() {
// Draw platform
ctx.fillStyle = '#333';
ctx.fillRect(0, canvas.height - PLATFORM_HEIGHT, canvas.width, PLATFORM_HEIGHT);
// Draw platform edge
ctx.fillStyle = '#555';
ctx.fillRect(0, canvas.height - PLATFORM_HEIGHT, canvas.width, 2);
for (const passenger of gameState.passengers) {
// Passenger body
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(passenger.x, passenger.y, PASSENGER_RADIUS, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(passenger.x - 4, passenger.y - 4, 2, 0, Math.PI * 2);
ctx.arc(passenger.x + 4, passenger.y - 4, 2, 0, Math.PI * 2);
ctx.fill();
// Mouth (simple line)
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(passenger.x - 3, passenger.y + 2);
ctx.lineTo(passenger.x + 3, passenger.y + 2);
ctx.stroke();
}
}
// Draw game
function draw() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw background
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw platform and passengers
drawPassengers();
// Draw train
drawTrain();
// Draw game over message
if (gameState.gameOver) {
gameOverDisplay.style.display = 'block';
} else {
gameOverDisplay.style.display = 'none';
}
}
// Game loop
function gameLoop(timestamp) {
if (!gameState.gameActive) {
requestAnimationFrame(gameLoop);
return;
}
// Calculate delta time for smooth movement
if (!gameState.lastUpdateTime) {
gameState.lastUpdateTime = timestamp;
}
const deltaTime = timestamp - gameState.lastUpdateTime;
gameState.lastUpdateTime = timestamp;
// Move train at consistent speed regardless of frame rate
if (deltaTime >= 1000 / 60) { // ~60fps
// Demo mode: change direction randomly
if (!gameState.playerControlled) {
if (Math.random() < 0.02) { // 2% chance per frame to change direction
const directions = ['up', 'down', 'left', 'right'];
const newDir = directions[Math.floor(Math.random() * directions.length)];
// Don't reverse direction completely
if (!(newDir === 'up' && gameState.train.direction === 'down') &&
!(newDir === 'down' && gameState.train.direction === 'up') &&
!(newDir === 'left' && gameState.train.direction === 'right') &&
!(newDir === 'right' && gameState.train.direction === 'left')) {
gameState.train.direction = newDir;
}
}
}
moveTrain();
checkCollisions();
gameState.lastUpdateTime = timestamp - (deltaTime % (1000 / 60));
}
draw();
requestAnimationFrame(gameLoop);
}
// Handle keyboard input
function handleKeyPress(e) {
if (!gameState.gameActive && !gameState.gameOver) {
initGame();
gameState.gameActive = true;
startMessage.style.display = 'none';
requestAnimationFrame(gameLoop);
return;
}
if (gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.gameOver = false;
gameOverDisplay.style.display = 'none';
requestAnimationFrame(gameLoop);
return;
}
// Change direction based on key (only if player controlled)
if (gameState.playerControlled) {
switch (e.key) {
case 'ArrowUp':
if (gameState.train.direction !== 'down') {
gameState.train.direction = 'up';
}
break;
case 'ArrowDown':
if (gameState.train.direction !== 'up') {
gameState.train.direction = 'down';
}
break;
case 'ArrowLeft':
if (gameState.train.direction !== 'right') {
gameState.train.direction = 'left';
}
break;
case 'ArrowRight':
if (gameState.train.direction !== 'left') {
gameState.train.direction = 'right';
}
break;
}
}
}
// Handle click to start
function handleClick() {
if (!gameState.gameActive && !gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.playerControlled = true;
startMessage.style.display = 'none';
requestAnimationFrame(gameLoop);
} else if (gameState.gameOver) {
initGame();
gameState.gameActive = true;
gameState.gameOver = false;
gameState.playerControlled = true;
gameOverDisplay.style.display = 'none';
requestAnimationFrame(gameLoop);
}
}
// Event listeners
document.addEventListener('keydown', handleKeyPress);
startMessage.addEventListener('click', handleClick);
gameOverDisplay.addEventListener('click', handleClick);
// Initialize
initGame();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
```
This should work! Let me verify the key points:
1. ✅ Snake game with train theme (locomotive + colorful wagons)
2. ✅ Passengers waiting on platform (yellow circles with faces on the bottom platform)
3. ✅ Picking up passengers makes the train grow (adds wagons)
4. ✅ Self-playing demo mode (train moves automatically with random direction changes)
5. ✅ "Click or press a key to start" message
6. ✅ Responsive design (adapts to viewport)
7. ✅ Game over detection (hitting walls or self)
8. ✅ Score display
9. ✅ Can restart after game over
The train has:
- Red locomotive with triangle front, smokestack, and headlight
- Colorful wagons with windows and connecting rods
- Wagons grow when picking up passengers
The platform has:
- Gray platform at the bottom
- Yellow passengers with simple faces waiting
- When the train head collides with a passenger, the passenger disappears and a new one spawns, plus the train grows
The game starts in demo mode where the train moves automatically. When the user clicks or presses a key, control switches to the player who can use arrow keys to control the train.
This should meet all the requirements!