Snake · qwen3-235b-a22b-thinking-2507
Build a playable Snake game on a canvas with arrow-key controls, a growing tail, food, score, and a game-over and restart state.
transcript · 4 messages
Build a playable Snake game on a canvas with arrow-key controls, a growing tail, food, score, and a game-over and restart state.
thinking
Okay, I need to create a single-page Snake game in an HTML file. Let me think about the requirements.
First, it has to be all in one index.html file. So, HTML structure with <style> for CSS and <script> for JavaScript. No external resources allowed, so all code must be inline.
The game needs a canvas element. I'll start by setting up the HTML structure. The canvas should take up the screen, maybe centered. Then, CSS to style the canvas and maybe some game-over message.
For the JavaScript part: Snake game mechanics. The snake moves with arrow keys. It should have a direction, position, tail that grows when eating food. Food is randomly placed on the canvas. Score increments when food is eaten. Game over when hitting the wall or itself.
Let me outline the steps:
1. HTML: Create a canvas element with id "gameCanvas".
2. CSS: Style canvas, maybe set background color, center it.
3. JS:
- Get canvas context.
- Set up game variables: snake position, direction, food position, score.
- Handle key presses for direction changes (arrow keys).
- Game loop: update snake position, check collisions, draw everything.
- Collision detection with walls and self.
- When eating food, grow the snake and generate new food.
- Display score.
- Game over state with restart option.
Wait, the user mentioned a restart state. So when game over, show a message and allow restarting by pressing a key or clicking a button.
Let me think about the code structure.
In the HTML body:
<canvas id="gameCanvas" width="400" height="400"></canvas>
<div id="score">Score: 0</div>
<div id="gameOver" style="display:none;">Game Over! Press Enter to restart.</div>
But the user said it's a single file, so all in one. Maybe the game over message is part of the canvas or as a div.
Wait, the instructions say CSS in <style> and JS in <script>. So the HTML will have the canvas, and maybe some divs for score and game over.
But the user said "self-contained", so maybe the game over is drawn on the canvas. Alternatively, use DOM elements. Let's go with DOM elements for simplicity.
Wait, but the problem says "NO external resources", but DOM elements are okay as long as they're inline.
So, HTML structure:
<!DOCTYPE html>
<html>
<head>
<title>Snake Game</title>
<style>
body { ... }
canvas { ... }
#score, #gameOver { ... }
</style>
</head>
<body>
<canvas id="gameCanvas" width="400" height="400"></canvas>
<div id="score">Score: 0</div>
<div id="gameOver" style="display:none;">Game Over! Press Enter to restart.</div>
<script>
// Game logic here
</script>
</body>
</html>
Now, the JS part.
Variables:
- canvas, ctx
- snake = [{x: 10, y:10}] (array of segments)
- food = {x: 5, y:5}
- direction = {x: 1, y:0} (right initially)
- nextDirection (to prevent 180 turns)
- score = 0
- game over flag
Key events: listen for keydown, update direction. Only allow changing direction to left/right/up/down, but not opposite (e.g., can't go left if currently right).
Game loop: setInterval or requestAnimationFrame. Each step:
- Update direction based on nextDirection.
- Move snake: new head = current head + direction.
- Check if new head hits food: if yes, grow (don't remove tail), generate new food. Else, remove tail.
- Check collisions: with walls (if head x or y <0 or >= canvas size) or with self (check if new head is in snake body).
- If collision, game over.
- Redraw everything.
Drawing:
- Clear canvas.
- Draw snake: each segment as a rectangle.
- Draw food as a rectangle.
- Update score display.
Game over: display message, stop loop, allow restart.
Restart: reset snake, direction, score, generate new food, hide game over message.
Let me sketch the code.
First, in the script:
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const gridSize = 20;
const gridWidth = canvas.width / gridSize;
const gridHeight = canvas.height / gridSize;
let snake = [{x: 10, y: 10}];
let food = {x: 5, y: 5};
let direction = {x: 1, y: 0};
let nextDirection = {x: 1, y: 0};
let score = 0;
let gameActive = true;
function drawSnake() {
ctx.fillStyle = 'green';
snake.forEach(segment => {
ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize - 1, gridSize - 1);
});
}
function drawFood() {
ctx.fillStyle = 'red';
ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize - 1, gridSize - 1);
}
function generateFood() {
food = {
x: Math.floor(Math.random() * gridWidth),
y: Math.floor(Math.random() * gridHeight)
};
// Ensure food isn't on snake
// Maybe loop until it's not on any snake segment
}
function checkCollision() {
const head = snake[0];
// Check wall collision
if (head.x < 0 || head.x >= gridWidth || head.y < 0 || head.y >= gridHeight) {
return true;
}
// Check self collision
for (let i = 1; i < snake.length; i++) {
if (head.x === snake[i].x && head.y === snake[i].y) {
return true;
}
}
return false;
}
function update() {
if (!gameActive) return;
direction = nextDirection;
const head = {
x: snake[0].x + direction.x,
y: snake[0].y + direction.y
};
if (head.x === food.x && head.y === food.y) {
// Eat food
score++;
document.getElementById('score').textContent = `Score: ${score}`;
generateFood();
} else {
// Move snake: pop tail
snake.pop();
}
snake.unshift(head);
if (checkCollision()) {
gameActive = false;
document.getElementById('gameOver').style.display = 'block';
return;
}
draw();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSnake();
drawFood();
}
function handleKeydown(e) {
switch(e.key) {
case 'ArrowUp':
if (direction.y !== 1) nextDirection = {x: 0, y: -1};
break;
case 'ArrowDown':
if (direction.y !== -1) nextDirection = {x: 0, y: 1};
break;
case 'ArrowLeft':
if (direction.x !== 1) nextDirection = {x: -1, y: 0};
break;
case 'ArrowRight':
if (direction.x !== -1) nextDirection = {x: 1, y: 0};
break;
case 'Enter':
if (!gameActive) restartGame();
break;
}
}
function restartGame() {
snake = [{x: 10, y: 10}];
direction = {x: 1, y: 0};
nextDirection = {x: 1, y: 0};
score = 0;
document.getElementById('score').textContent = 'Score: 0';
document.getElementById('gameOver').style.display = 'none';
generateFood();
gameActive = true;
gameLoop();
}
function gameLoop() {
update();
if (gameActive) {
setTimeout(gameLoop, 150); // Adjust speed here
}
}
// Initialize
document.addEventListener('keydown', handleKeydown);
generateFood();
gameLoop();
Wait, but the gameLoop is using setTimeout. Maybe better to use setInterval, but setTimeout in a recursive way is okay.
Also, need to make sure that the initial direction is set correctly.
Wait, in the key handling, when changing direction, we check that it's not the opposite direction. For example, if current direction is right (x:1), can't go left (x:-1). So in the keydown handler, for each arrow, check if it's opposite.
In the code above, for ArrowUp, direction.y shouldn't be 1 (which is down), so if current direction is down, can't go up. Wait, no: if direction is down (y:1), then up would be y:-1, which is opposite. So the check is correct.
Now, the generateFood function needs to ensure that the new food isn't on the snake. So in generateFood:
function generateFood() {
let newFood;
do {
newFood = {
x: Math.floor(Math.random() * gridWidth),
y: Math.floor(Math.random() * gridHeight)
};
} while (snake.some(segment => segment.x === newFood.x && segment.y === newFood.y));
food = newFood;
}
But initially, when the snake is at [10,10], the food might be generated there. So this loop ensures it's not on the snake.
Now, putting all together.
CSS: style the canvas, maybe center it. The score and game over divs.
In the style tag:
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background-color: #000;
color: white;
font-family: Arial, sans-serif;
}
canvas {
border: 2px solid #fff;
}
#score, #gameOver {
margin-top: 10px;
font-size: 24px;
}
#gameOver {
color: red;
}
This should style the elements.
Now, putting it all into index.html.
Wait, the user said "write it ALL into one self-contained file named index.html".
So the structure is:
<!DOCTYPE html>
<html>
<head>
<title>Snake Game</title>
<style>
... CSS here ...
</style>
</head>
<body>
<canvas id="gameCanvas" width="400" height="400"></canvas>
<div id="score">Score: 0</div>
<div id="gameOver" style="display:none;">Game Over! Press Enter to restart.</div>
<script>
... JS code here ...
</script>
</body>
</html>
Let me check if all parts are covered.
- Canvas with id gameCanvas.
- Score and game over divs.
- CSS to style them.
- JS with game logic.
Testing the JS code for errors:
In generateFood, using snake.some() which is correct.
In the keydown handler, checking direction.y !== 1 for ArrowUp. Wait, direction.y is -1 for up, 1 for down. So if current direction is down (y:1), pressing up is allowed. Wait, no: the check is to prevent going opposite. For example, if direction is down (y:1), you can't go up (y:-1) immediately, but the check in the code is:
case 'ArrowUp':
if (direction.y !== 1) nextDirection = {x: 0, y: -1};
So if direction.y is 1 (down), then direction.y !== 1 is false, so can't set nextDirection to up. That's correct because you can't reverse direction.
Wait, no: if the current direction is down (y:1), pressing up would be a 180 turn, which is not allowed. So the check is correct: if direction.y is 1 (down), then you can't go up (y:-1), so the condition direction.y !== 1 must be true to allow changing direction. Wait, no: the code says if direction.y !== 1, then set nextDirection. So if direction.y is 1 (current direction is down), then direction.y !== 1 is false, so the code doesn't set nextDirection. That's correct.
Yes, so the key handling prevents 180 turns.
Another thing: the initial direction is right (x:1, y:0). So pressing left would be x:-1, which is opposite. The check for ArrowLeft is direction.x !== 1. Wait, current direction.x is 1 (right), so direction.x !== 1 is false, so can't go left. Correct.
So the key handling is correct.
In the gameLoop, using setTimeout with 150ms interval. Maybe adjust the speed as needed.
Now, putting all together into the index.html file.
Wait, the user said to write it ALL into one file, so the write function should create index.html with all the code.
So the content for write is the entire HTML file as a string.
Let me structure the content:
<!DOCTYPE html>
<html>
<head>
<title>Snake Game</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background-color: #000;
color: white;
font-family: Arial, sans-serif;
}
canvas {
border: 2px solid #fff;
}
#score, #gameOver {
margin-top: 10px;
font-size: 24px;
}
#gameOver {
color: red;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="400" height="400"></canvas>
<div id="score">Score: 0</div>
<div id="gameOver" style="display:none;">Game Over! Press Enter to restart.</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const gridSize = 20;
const gridWidth = canvas.width / gridSize;
const gridHeight = canvas.height / gridSize;
let snake = [{x: 10, y: 10}];
let food = {x: 5, y: 5};
let direction = {x: 1, y: 0};
let nextDirection = {x: 1, y: 0};
let score = 0;
let gameActive = true;
function drawSnake() {
ctx.fillStyle = 'green';
snake.forEach(segment => {
ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize - 1, gridSize - 1);
});
}
function drawFood() {
ctx.fillStyle = 'red';
ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize - 1, gridSize - 1);
}
function generateFood() {
let newFood;
do {
newFood = {
x: Math.floor(Math.random() * gridWidth),
y: Math.floor(Math.random() * gridHeight)
};
} while (snake.some(segment => segment.x === newFood.x && segment.y === newFood.y));
food = newFood;
}
function checkCollision() {
const head = snake[0];
if (head.x < 0 || head.x >= gridWidth || head.y < 0 || head.y >= gridHeight) {
return true;
}
for (let i = 1; i < snake.length; i++) {
if (head.x === snake[i].x && head.y === snake[i].y) {
return true;
}
}
return false;
}
function update() {
if (!gameActive) return;
direction = nextDirection;
const head = {
x: snake[0].x + direction.x,
y: snake[0].y + direction.y
};
if (head.x === food.x && head.y === food.y) {
score++;
document.getElementById('score').textContent = `Score: ${score}`;
generateFood();
} else {
snake.pop();
}
snake.unshift(head);
if (checkCollision()) {
gameActive = false;
document.getElementById('gameOver').style.display = 'block';
return;
}
draw();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSnake();
drawFood();
}
function handleKeydown(e) {
switch(e.key) {
case 'ArrowUp':
if (direction.y !== 1) nextDirection = {x: 0, y: -1};
break;
case 'ArrowDown':
if (direction.y !== -1) nextDirection = {x: 0, y: 1};
break;
case 'ArrowLeft':
if (direction.x !== 1) nextDirection = {x: -1, y: 0};
break;
case 'ArrowRight':
if (direction.x !== -1) nextDirection = {x: 1, y: 0};
break;
case 'Enter':
if (!gameActive) restartGame();
break;
}
}
function restartGame() {
snake = [{x: 10, y: 10}];
direction = {x: 1, y: 0};
nextDirection = {x: 1, y: 0};
score = 0;
document.getElementById('score').textContent = 'Score: 0';
document.getElementById('gameOver').style.display = 'none';
generateFood();
gameActive = true;
gameLoop();
}
function gameLoop() {
update();
if (gameActive) {
setTimeout(gameLoop, 150);
}
}
document.addEventListener('keydown', handleKeydown);
generateFood();
gameLoop();
</script>
</body>
</html>
Let me check for any possible issues.
- The canvas size is 400x400, gridSize 20, so gridWidth and gridHeight are 20 each.
- Initial snake position at (10,10), which is center.
- Food is generated not on the snake.
- Key handling prevents 180 turns.
- Game over when hitting walls or self.
- Restart by pressing Enter.
This should work. Now, the write function will create index.html with this content.index.html (4026 bytes)Successfully wrote 4026 bytes to index.html
thinking
Okay, let me check what the user asked for. They wanted a playable Snake game on a canvas with arrow-key controls, growing tail, food, score, game-over, and restart state. All in a single index.html file with no external resources. I used the write function to create the index.html file. The response says it successfully wrote 4026 bytes. Now I need to confirm if the file meets all requirements. First, check if it's a single HTML file with inline CSS and JS. The code I generated has <style> and <script> tags inside the HTML, so that's good. No external links or CDNs, which is required. Looking at the HTML structure: canvas element, score div, game over message. CSS styles the body, canvas, and the divs. The JS handles game logic—snake movement, collision detection, food generation, scoring, and restart. Key features: - Arrow keys control direction, with checks to prevent 180-degree turns. - Snake grows when eating food (score increases, food respawns). - Game over when hitting walls or self, with a message and restart via Enter. - All code is inline, no external files. Possible issues to verify: 1. Canvas size: 400x400, which is reasonable. 2. Grid size set to 20, so 20x20 grid. Initial snake at (10,10), which is center. 3. Food generation checks for collision with snake, so it shouldn't spawn on the snake. 4. Collision detection for walls and self. 5. Restart function resets all variables and restarts the game loop. The code seems solid. The timeout in gameLoop is 150ms, which is a common speed for Snake games. The keydown event listener correctly updates direction. No external resources are used, so it's fully sandboxed. The user should be able to open index.html in a browser and play immediately. Since the write was successful, the next step is to inform the user that the file is ready and confirm it meets all requirements.
The Snake game has been successfully written to `index.html` in the current directory. It's a fully self-contained single-page application with: - Canvas-based gameplay (400x400) - Arrow key controls with direction validation - Growing snake tail when eating food - Score tracking - Game-over detection (wall/self collision) - Restart functionality (press Enter) - No external dependencies or network requests You can open `index.html` directly in any modern browser to play. The game starts automatically - use arrow keys to control the snake and press Enter to restart after game over.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Game starts already in Game Over state at Score 0; Enter restart never actually resets the game, snake never moves or grows.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.