Asteroids · deepseek-r1
Build a playable Asteroids game on a canvas: a thrust-and-rotate vector ship that shoots and wraps around the screen edges, splitting asteroids into smaller pieces, with score and lives. Procedural vector graphics only, no external assets.
This one-shot run produced no runnable artifact.
agent produced no index.html
The transcript below shows what the model did — read it to see why the one shot fell short.
transcript · 2 messages
Build a playable Asteroids game on a canvas: a thrust-and-rotate vector ship that shoots and wraps around the screen edges, splitting asteroids into smaller pieces, with score and lives. Procedural vector graphics only, no external assets.
thinking
We are building a single-page Asteroids game in one HTML file (index.html). The game will have:
- A ship that can rotate and thrust, and wrap around screen edges.
- The ship can shoot bullets that destroy asteroids.
- Asteroids split into smaller pieces when hit, and we track score and lives.
- Procedural vector graphics (using canvas) and no external assets.
We'll structure the HTML file with:
- A <canvas> element for drawing.
- A <script> tag containing the entire game logic in JavaScript.
- A <style> tag for minimal styling (to make the canvas fullscreen and center).
Steps for the game:
1. Set up the canvas and context.
2. Define the ship (position, velocity, rotation, etc.).
3. Handle keyboard input for rotation, thrust, shooting.
4. Implement asteroids (random positions, sizes, and velocities).
5. Implement bullets that the ship shoots.
6. Collision detection between bullets and asteroids, and between ship and asteroids.
7. When an asteroid is hit, split it into smaller ones (or remove if too small) and add score.
8. Implement lives: when the ship collides with an asteroid, lose a life and reset the ship.
9. Wrap around the edges for all moving objects.
10. Draw everything: ship, asteroids, bullets, and UI (score, lives).
We'll use a game loop (requestAnimationFrame) to update and draw.
Important: We must write the entire game in one file (index.html) with inline CSS and JS.
Let's create the HTML structure and then write the JavaScript inside.
Note: We'll use ES6 syntax and assume modern browser support.
Implementation plan:
HTML:
<!DOCTYPE html>
<html>
<head>
<title>Asteroids</title>
<style>
/* Minimal styles to make canvas fullscreen */
body { margin: 0; overflow: hidden; }
canvas { display: block; background: black; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// Entire game code here
</script>
</body>
</html>
JavaScript:
We'll define:
- Constants (canvas size, ship size, asteroid sizes, etc.)
- Game state (score, lives, level, etc.)
- Arrays for game objects: asteroids, bullets, particles (if we want explosions)
Classes (or constructor functions) for:
- Ship
- Asteroid
- Bullet
Functions:
- init(): to set up the game
- update(): update game state (positions, collisions, etc.)
- draw(): draw everything
- gameLoop(): call update and draw, then requestAnimationFrame
Event listeners for keyboard input.
We'll use keydown and keyup to set flags for ship controls.
Let's outline the classes:
class Ship {
constructor() {
this.position = { x: canvas.width/2, y: canvas.height/2 };
this.velocity = { x: 0, y: 0 };
this.rotation = 0; // in radians
this.rotationSpeed = 0.05; // radians per frame
this.thrustPower = 0.1;
this.radius = 10; // for collision
this.invincible = false; // after being hit
this.invincibleTime = 0;
}
update() {
// Apply rotation if turning
// Apply thrust if thrusting (add acceleration in the direction of rotation)
// Apply velocity to position
// Wrap around edges
// If invincible, decrement invincibleTime
}
draw(ctx) {
// Draw the ship as a triangle
// If invincible, draw with blinking effect?
}
}
class Asteroid {
constructor(x, y, size) {
// size: 0 (large), 1 (medium), 2 (small)
this.position = { x, y };
this.velocity = { x: (Math.random()-0.5)*2, y: (Math.random()-0.5)*2 };
this.size = size;
this.radius = size * 10 + 10; // large: 30, medium: 20, small: 10
}
update() {
// Move by velocity and wrap around
}
draw(ctx) {
// Draw a polygon (random points?) at position
}
}
class Bullet {
constructor(x, y, rotation) {
this.position = { x, y };
this.velocity = { x: Math.cos(rotation)*10, y: Math.sin(rotation)*10 };
this.lifetime = 60; // frames
}
update() {
// Move and decrement lifetime
}
draw(ctx) {
// Draw a small rectangle or circle
}
}
Collision detection: use circle-circle for simplicity.
We'll also need:
- A function to split an asteroid: when hit, create two smaller ones (if not already smallest) and remove the hit one.
Game flow:
- Start with a number of large asteroids (e.g., 4).
- When all asteroids are destroyed, spawn a new level with more asteroids.
We'll also handle:
- When the ship is hit, reset the ship (with invincibility for a short time) and decrement lives.
Let's code step by step in the script tag.
Note: We must be cautious of performance and garbage collection (avoid creating objects in the game loop if possible, but for simplicity we'll do it).
We'll write the entire code in one go.
Due to the complexity, we'll break the code into sections:
1. Canvas setup and constants.
2. Game state variables.
3. Class definitions.
4. Helper functions (like wrapAround, collision, etc.).
5. Input handling.
6. init(), update(), draw(), gameLoop().
7. Event listeners.
We'll also add a restart mechanism when lives run out.
Let's write the code accordingly.
Note: We are writing directly to index.html, so we'll use write function to create the file.
We'll write the entire HTML content as a string and then write it to index.html.
Since the code is long, we'll write it carefully.
We'll use a template string for the HTML content.
Important: We must escape backticks in the JavaScript code? Actually, we are using a template string for the entire HTML, so we'll use a function to generate the string without nested template literals (or we'll use string concatenation). Alternatively, we can write the JavaScript as a string without template literals (using single quotes).
We'll write the JavaScript code as a string and then embed it.
Alternatively, we can write the entire HTML as a string without using a template literal for the whole thing? Actually, we'll write it as a single string.
Steps:
1. Define the HTML string.
2. Inside the script tag, write the JavaScript code.
We'll write the JavaScript code step by step.
Due to the length, we'll break the JavaScript into parts and concatenate.
We'll write:
let htmlContent = `<!DOCTYPE html> ... <script> ... </script> ...`;
But note: the JavaScript code might contain backticks. We'll use a function to escape them? Actually, we can use ${} only for the entire script content? Or we can write the script content as a separate string and then embed.
Alternatively, we can avoid using template literals in the JavaScript code? We'll write the JavaScript without template literals.
Actually, we can write:
let jsCode = ` ... `; // This is the JavaScript code as a string, but we have to escape backticks inside by using \` or by using a different quote.
But we are already in a template literal for the whole HTML. So we'll use:
let htmlContent = `<!DOCTYPE html>
...
<script>
${jsCode}
</script>
...`;
And we'll build jsCode as a string.
However, to avoid complexity, we'll write the entire HTML in one go without nested template literals. We'll use single quotes inside the JavaScript.
We'll write the JavaScript code without using template literals (for strings) and use single quotes for strings.
So, in the JavaScript code, we'll use single quotes for strings.
Let's write the JavaScript code:
// Canvas setup
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Game state
let ship;
let asteroids = [];
let bullets = [];
let score = 0;
let lives = 3;
let level = 1;
let gameOver = false;
// Input state
const keys = {};
// Then define classes...
We'll write the classes and functions.
Due to the length, we'll write the code and then output it via write.
We'll write the entire HTML content as a string and then write it to index.html.
Let's generate the content.
Note: We must be cautious of the string length. We'll write it step by step.
We'll write the HTML structure first, then the CSS, then the JavaScript.
We'll write the JavaScript code as a string without any backticks (using single quotes for strings) to avoid escaping.
Alternatively, we can escape backticks by using \` if we need them, but we'll avoid them.
We'll write the JavaScript code without template literals.
Let's start building the JavaScript code:
// Set up canvas
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Game state
let ship;
let asteroids = [];
let bullets = [];
let score = 0;
let lives = 3;
let level = 1;
let gameOver = false;
// Input state
const keys = {};
// Ship class
class Ship {
constructor() {
this.position = { x: canvas.width / 2, y: canvas.height / 2 };
this.velocity = { x: 0, y: 0 };
this.rotation = 0; // radians
this.rotationSpeed = 0.05;
this.thrustPower = 0.1;
this.radius = 10; // collision radius
this.invincible = false;
this.invincibleTime = 0;
this.thrusting = false;
}
update() {
// Rotation
if (keys['ArrowLeft']) {
this.rotation -= this.rotationSpeed;
}
if (keys['ArrowRight']) {
this.rotation += this.rotationSpeed;
}
// Thrust
if (keys['ArrowUp']) {
this.thrusting = true;
this.velocity.x += Math.cos(this.rotation) * this.thrustPower;
this.velocity.y += Math.sin(this.rotation) * this.thrustPower;
} else {
this.thrusting = false;
}
// Apply velocity
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
// Wrap around edges
if (this.position.x < 0) this.position.x = canvas.width;
if (this.position.x > canvas.width) this.position.x = 0;
if (this.position.y < 0) this.position.y = canvas.height;
if (this.position.y > canvas.height) this.position.y = 0;
// Friction (optional) - to slow down over time
this.velocity.x *= 0.98;
this.velocity.y *= 0.98;
// Invincibility
if (this.invincible) {
this.invincibleTime--;
if (this.invincibleTime <= 0) {
this.invincible = false;
}
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.rotate(this.rotation);
ctx.strokeStyle = this.invincible ? 'rgba(255,255,255,0.5)' : 'white';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(10, 0);
ctx.lineTo(-10, -8);
ctx.lineTo(-10, 8);
ctx.closePath();
ctx.stroke();
// Draw thrust if thrusting
if (this.thrusting) {
ctx.beginPath();
ctx.moveTo(-10, 0);
ctx.lineTo(-15, 0);
ctx.strokeStyle = 'red';
ctx.stroke();
}
ctx.restore();
}
shoot() {
// Create a bullet at the ship's position, going in the direction of rotation
const bullet = new Bullet(
this.position.x + Math.cos(this.rotation) * 15,
this.position.y + Math.sin(this.rotation) * 15,
this.rotation
);
bullets.push(bullet);
}
}
// Asteroid class
class Asteroid {
constructor(x, y, size) {
this.position = { x, y };
this.velocity = {
x: (Math.random() - 0.5) * 2,
y: (Math.random() - 0.5) * 2
};
this.size = size; // 0: large, 1: medium, 2: small
this.radius = size * 10 + 10; // large: 30, medium: 20, small: 10
this.vertices = Math.floor(Math.random() * 5) + 5; // Random number of vertices (5 to 9)
this.offsets = [];
for (let i = 0; i < this.vertices; i++) {
this.offsets.push(Math.random() * 0.4 + 0.8); // between 0.8 and 1.2
}
}
update() {
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
// Wrap around edges
if (this.position.x < 0) this.position.x = canvas.width;
if (this.position.x > canvas.width) this.position.x = 0;
if (this.position.y < 0) this.position.y = canvas.height;
if (this.position.y > canvas.height) this.position.y = 0;
}
draw(ctx) {
ctx.save();
ctx.translate(this.position.x, this.position.y);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < this.vertices; i++) {
const angle = (i / this.vertices) * Math.PI * 2;
const radius = this.radius * this.offsets[i];
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.stroke();
ctx.restore();
}
}
// Bullet class
class Bullet {
constructor(x, y, rotation) {
this.position = { x, y };
this.velocity = {
x: Math.cos(rotation) * 10,
y: Math.sin(rotation) * 10
};
this.lifetime = 60; // frames
}
update() {
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
this.lifetime--;
// Wrap around edges
if (this.position.x < 0) this.position.x = canvas.width;
if (this.position.x > canvas.width) this.position.x = 0;
if (this.position.y < 0) this.position.y = canvas.height;
if (this.position.y > canvas.height) this.position.y = 0;
}
draw(ctx) {
ctx.save();
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(this.position.x, this.position.y, 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
// Initialize the game
function init() {
ship = new Ship();
asteroids = [];
bullets = [];
score = 0;
lives = 3;
level = 1;
gameOver = false;
// Create initial asteroids
for (let i = 0; i < 4; i++) {
let x, y;
// Place away from the center
do {
x = Math.random() * canvas.width;
y = Math.random() * canvas.height;
} while (Math.hypot(x - canvas.width/2, y - canvas.height/2) < 100);
asteroids.push(new Asteroid(x, y, 0)); // size 0: large
}
}
// Check collision between two circles
function circleCollision(obj1, obj2) {
const dx = obj1.position.x - obj2.position.x;
const dy = obj1.position.y - obj2.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < obj1.radius + obj2.radius;
}
// Split an asteroid into two smaller ones
function splitAsteroid(asteroid) {
if (asteroid.size < 2) {
// Create two smaller asteroids
for (let i = 0; i < 2; i++) {
asteroids.push(new Asteroid(
asteroid.position.x,
asteroid.position.y,
asteroid.size + 1
));
}
}
}
// Update game state
function update() {
if (gameOver) return;
ship.update();
// Update asteroids
asteroids.forEach(asteroid => asteroid.update());
// Update bullets
bullets.forEach((bullet, index) => {
bullet.update();
// Remove bullets that have expired
if (bullet.lifetime <= 0) {
bullets.splice(index, 1);
}
});
// Check bullet-asteroid collisions
for (let i = bullets.length - 1; i >= 0; i--) {
for (let j = asteroids.length - 1; j >= 0; j--) {
if (circleCollision(bullets[i], asteroids[j])) {
// Remove bullet and asteroid
bullets.splice(i, 1);
// Split asteroid
splitAsteroid(asteroids[j]);
asteroids.splice(j, 1);
// Add score: large=100, medium=50, small=20
const points = [100, 50, 20][asteroids[j]?.size] || 0;
score += points;
break; // break inner loop, then break outer? Actually, break inner and continue with next bullet
// But note: we removed the bullet, so break inner and then break outer for this bullet
// Since we are removing the bullet, we break inner and then break outer to avoid using the same bullet again
break;
}
}
}
// Check ship-asteroid collisions (if not invincible)
if (!ship.invincible) {
for (let j = asteroids.length - 1; j >= 0; j--) {
if (circleCollision(ship, asteroids[j])) {
// Lose a life
lives--;
if (lives <= 0) {
gameOver = true;
} else {
// Reset ship with invincibility
ship.position = { x: canvas.width/2, y: canvas.height/2 };
ship.velocity = { x: 0, y: 0 };
ship.invincible = true;
ship.invincibleTime = 120; // 2 seconds at 60fps
}
// Remove the asteroid? Or leave it? Typically, we remove and split?
// But in original Asteroids, the ship is destroyed and the asteroid remains.
// So we don't remove the asteroid.
break;
}
}
}
// Check if level is cleared
if (asteroids.length === 0) {
level++;
// Spawn new asteroids: 3 + level (but let's do 4 + level)
for (let i = 0; i < 4 + level; i++) {
let x, y;
// Place away from the ship
do {
x = Math.random() * canvas.width;
y = Math.random() * canvas.height;
} while (Math.hypot(x - ship.position.x, y - ship.position.y) < 100);
asteroids.push(new Asteroid(x, y, 0)); // large
}
}
}
// Draw everything
function draw() {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw ship
ship.draw(ctx);
// Draw asteroids
asteroids.forEach(asteroid => asteroid.draw(ctx));
// Draw bullets
bullets.forEach(bullet => bullet.draw(ctx));
// Draw UI: score, lives, level
ctx.fillStyle = 'white';
ctx.font = '16px Arial';
ctx.fillText('Score: ' + score, 10, 20);
ctx.fillText('Lives: ' + lives, 10, 40);
ctx.fillText('Level: ' + level, 10, 60);
if (gameOver) {
ctx.font = '48px Arial';
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.fillText('GAME OVER', canvas.width/2, canvas.height/2);
ctx.font = '24px Arial';
ctx.fillText('Press R to restart', canvas.width/2, canvas.height/2 + 40);
ctx.textAlign = 'left';
}
}
// Game loop
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// Event listeners
window.addEventListener('keydown', (e) => {
keys[e.key] = true;
// Space to shoot
if (e.key === ' ' && !gameOver) {
ship.shoot();
}
// R to restart
if (e.key === 'r' && gameOver) {
init();
}
});
window.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
// Handle window resize
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
// Start the game
init();
gameLoop();
This is the basic structure.
We'll write the entire HTML file as a string and write it to index.html.
Let's write the HTML string:
const htmlContent = `<!DOCTYPE html>
<html>
<head>
<title>Asteroids</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; background: black; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// The entire JavaScript code here
${jsCode}
</script>
</body>
</html>`;
But note: we have to escape backticks in jsCode? Actually, we are building jsCode as a string that might contain backticks? We'll avoid them by using single quotes.
We'll write the JavaScript code using single quotes for strings.
Alternatively, we can write the JavaScript code without any backticks (except for the template string we are building?).
Actually, we are writing the JavaScript code as a string that we are going to embed. We'll write it as a string and then assign to jsCode.
But note: we are already in a template string for the entire HTML. So we'll build the JavaScript code as a string and then embed it.
However, to avoid complexity, we'll write the entire HTML in one write call without breaking the JavaScript into a variable.
We'll write:
let content = `<!DOCTYPE html>
<html>
<head>
<title>Asteroids</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; background: black; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// JavaScript code goes here, written with single quotes for strings.
...
</script>
</body>
</html>`;
And we'll put the JavaScript code inside.
But the JavaScript code is long and contains single quotes. We can escape them by using \' or use a mix of single and double quotes? Actually, we can use double quotes for the entire JavaScript string? Or we can escape.
We'll write the JavaScript code using double quotes for strings? Or we'll escape single quotes.
Alternatively, we can use a function to generate the string without any quotes? Not really.
We'll write the JavaScript code and escape single quotes by replacing them with \'.
But note: the JavaScript code might contain both. We'll write the code and then escape.
Actually, we can write the JavaScript code without any single quotes? We'll use double quotes for strings.
Let's rewrite the JavaScript code using double quotes for strings.
We'll go through the code and change every single quote to double quotes.
Alternatively, we can use a here-document equivalent? Not in JavaScript.
We'll write the JavaScript code as a string and replace single quotes with double quotes.
Actually, we can use double quotes for the entire JavaScript code and then embed without escaping.
We'll write the JavaScript code using double quotes.
Steps:
Replace every ' with " in the JavaScript code.
But note: we have apostrophes in comments? We'll avoid them or use double quotes.
Actually, we can use double quotes for all string literals.
We'll change:
ctx.strokeStyle = 'white'; -> ctx.strokeStyle = "white";
And so on.
We'll do a global replace.
Alternatively, we can use a mix: if we have a string containing a single quote, we use double quotes, and vice versa? But we'll stick to double quotes.
We'll write the entire JavaScript code using double quotes.
Then, we'll put it inside the <script> tag.
Now, we'll write the entire HTML content.
Due to the length, we'll write the JavaScript code first (with double quotes) and then embed.
We'll write the JavaScript code as a string and then write the HTML.
But note: the JavaScript code is long. We'll write it step by step.
Alternatively, we can write the entire HTML content as a string and then write it to index.html.
We'll do:
write({ path: "index.html", content: theHTMLString });
Let's generate theHTMLString.
We'll write the JavaScript code using double quotes and without backticks (except for the template string we are building? no, we avoid template literals).
We'll avoid template literals in the JavaScript code by using string concatenation with +.
Actually, we don't use any template literals in the JavaScript code? We use Math.PI and other expressions, but not in strings. So we are safe.
We'll write the JavaScript code as a string with double quotes.
Now, we write the entire HTML content.
Due to the length, we'll break the JavaScript code into sections and concatenate.
We'll write the JavaScript code as a string and then embed in the HTML template.
But note: the JavaScript code is long and might have newlines. We'll write it as a multi-line string.
We'll write:
const jsCode = `...`; // This is the JavaScript code as a string, but we are already in a template literal for the HTML? Actually, we are writing the entire HTML as a template literal.
We are going to write the HTML content via write, so we'll build the string.
We'll do:
let htmlContent = `<!DOCTYPE html>
<html>
<head>
<title>Asteroids</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; background: black; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// Start of JavaScript code
const canvas = document.getElementById("gameCanvas");
... // rest of the code
</script>
</body>
</html>`;
And then write it.
We'll write the entire JavaScript code inside the <script> tag.
Due to the length, we'll write it without breaking.
We'll write the entire HTML content as a string and then call write.
But note: the message might be too long? We'll do it.
Alternatively, we can write the JavaScript code separately and then embed, but we are in one file.
We'll write the entire content.
We'll write the JavaScript code with double quotes.
We'll write the entire HTML string.
Let's write the entire HTML content.
We'll write the JavaScript code exactly as above but with double quotes.
We'll write:
// Canvas setup
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// ... and so on
We'll write the entire JavaScript code.
Due to the character limit, we'll write the entire HTML content and then call write.
We'll write the HTML content as a string.
We'll write the entire Asteroids game in one file.
Let's write the file.
I'll create a self-contained Asteroids game in a single HTML file with canvas rendering. Here's the implementation: ```html <!DOCTYPE html> <html> <head> <title>Asteroids</title> <style> body { margin: 0; overflow: hidden; background: #000; font-family: monospace; color: white; } canvas { display: block; } #ui { position: absolute; top: 10px; left: 10px; } </style> </head> <body> <canvas id="gameCanvas"></canvas> <div id="ui"> <div>SCORE: <span id="score">0</span></div> <div>LIVES: <span id="lives">3</span></div> </div> <script> // Canvas setup const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Game state const state = { score: 0, lives: 3, level: 1, gameOver: false, keys: {}, ship: { x: canvas.width / 2, y: canvas.height / 2, rotation: 0, velocityX: 0, velocityY: 0, thrusting: false, radius: 10, invincible: false, invincibilityTimer: 0 }, asteroids: [], bullets: [], particles: [] }; // Initialize game function init() { // Reset state state.score = 0; state.lives = 3; state.level = 1; state.gameOver = false; state.ship = { x: canvas.width / 2, y: canvas.height / 2, rotation: 0, velocityX: 0, velocityY: 0, thrusting: false, radius: 10, invincible: false, invincibilityTimer: 0 }; state.asteroids = []; state.bullets = []; state.particles = []; // Create initial asteroids for (let i = 0; i < 6; i++) { createAsteroid(3); } // Update UI document.getElementById('score').textContent = state.score; document.getElementById('lives').textContent = state.lives; } // Create asteroid function createAsteroid(size, x, y) { const asteroid = { x: x || Math.random() * canvas.width, y: y || Math.random() * canvas.height, size: size, radius: size * 10, rotation: Math.random() * Math.PI * 2, rotationSpeed: (Math.random() - 0.5) * 0.05, velocityX: (Math.random() - 0.5) * 2, velocityY: (Math.random() - 0.5) * 2, vertices: Math.floor(Math.random() * 5) + 5, offsets: [] }; // Create irregular shape for (let i = 0; i < asteroid.vertices; i++) { asteroid.offsets.push(Math.random() * 0.4 + 0.8); } state.asteroids.push(asteroid); return asteroid; } // Create bullet function createBullet() { state.bullets.push({ x: state.ship.x + Math.cos(state.ship.rotation) * 15, y: state.ship.y + Math.sin(state.ship.rotation) * 15, rotation: state.ship.rotation, velocityX: Math.cos(state.ship.rotation) * 8, velocityY: Math.sin(state.ship.rotation) * 8, lifetime: 60 }); } // Create explosion particles function createExplosion(x, y, count) { for (let i = 0; i < count; i++) { state.particles.push({ x: x, y: y, velocityX: (Math.random() - 0.5) * 4, velocityY: (Math.random() - 0.5) * 4, size: Math.random() * 2 + 1, lifetime: Math.random() * 30 + 20 }); } } // Update game state function update() { if (state.gameOver) return; // Update ship if (state.keys['ArrowLeft']) state.ship.rotation -= 0.05; if (state.keys['ArrowRight']) state.ship.rotation += 0.05; state.ship.thrusting = state.keys['ArrowUp']; if (state.ship.thrusting) { state.ship.velocityX += Math.cos(state.ship.rotation) * 0.1; state.ship.velocityY += Math.sin(state.ship.rotation) * 0.1; } // Apply friction state.ship.velocityX *= 0.98; state.ship.velocityY *= 0.98; // Update position with wrapping state.ship.x += state.ship.velocityX; state.ship.y += state.ship.velocityY; if (state.ship.x < 0) state.ship.x = canvas.width; if (state.ship.x > canvas.width) state.ship.x = 0; if (state.ship.y < 0) state.ship.y = canvas.height; if (state.ship.y > canvas.height) state.ship.y = 0; // Update invincibility if (state.ship.invincible) { state.ship.invincibilityTimer--; if (state.ship.invincibilityTimer <= 0) { state.ship.invincible = false; } } // Update asteroids state.asteroids.forEach(asteroid => { asteroid.x += asteroid.velocityX; asteroid.y += asteroid.velocityY; asteroid.rotation += asteroid.rotationSpeed; // Wrap around screen if (asteroid.x < 0) asteroid.x = canvas.width; if (asteroid.x > canvas.width) asteroid.x = 0; if (asteroid.y < 0) asteroid.y = canvas.height; if (asteroid.y > canvas.height) asteroid.y = 0; }); // Update bullets for (let i = state.bullets.length - 1; i >= 0; i--) { const bullet = state.bullets[i]; bullet.x += bullet.velocityX; bullet.y += bullet.velocityY; bullet.lifetime--; // Wrap around screen if (bullet.x < 0) bullet.x = canvas.width; if (bullet.x > canvas.width) bullet.x = 0; if (bullet.y < 0) bullet.y = canvas.height; if (bullet.y > canvas.height) bullet.y = 0; // Remove expired bullets if (bullet.lifetime <= 0) { state.bullets.splice(i, 1); } } // Update particles for (let i = state.particles.length - 1; i >= 0; i--) { const particle = state.particles[i]; particle.x += particle.velocityX; particle.y += particle.velocityY; particle.lifetime--; if (particle.lifetime <= 0) { state.particles.splice(i, 1); } } // Collision detection: bullets vs asteroids for (let i = state.bullets.length - 1; i >= 0; i--) { for (let j = state.asteroids.length - 1; j >= 0; j--) { const bullet = state.bullets[i]; const asteroid = state.asteroids[j]; const dx = bullet.x - asteroid.x; const dy = bullet.y - asteroid.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < asteroid.radius) { // Create explosion createExplosion(asteroid.x, asteroid.y, 20); // Split asteroid or remove if (asteroid.size > 1) { for (let k = 0; k < 2; k++) { createAsteroid(asteroid.size - 1, asteroid.x, asteroid.y); } } // Update score state.score += (4 - asteroid.size) * 100; document.getElementById('score').textContent = state.score; // Remove bullet and asteroid state.bullets.splice(i, 1); state.asteroids.splice(j, 1); break; } } } // Collision detection: ship vs asteroids if (!state.ship.invincible) { for (let i = state.asteroids.length - 1; i >= 0; i--) { const asteroid = state.asteroids[i]; const dx = state.ship.x - asteroid.x; const dy = state.ship.y - asteroid.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < state.ship.radius + asteroid.radius) { // Create explosion createExplosion(state.ship.x, state.ship.y, 30); // Lose life state.lives--; document.getElementById('lives').textContent = state.lives; if (state.lives <= 0) { state.gameOver = true; } else { // Reset ship with invincibility state.ship.x = canvas.width / 2; state.ship.y = canvas.height / 2; state.ship.velocityX = 0; state.ship.velocityY = 0; state.ship.invincible = true; state.ship.invincibilityTimer = 120; } // Remove asteroid state.asteroids.splice(i, 1); break; } } } // Level progression if (state.asteroids.length === 0) { state.level++; for (let i = 0; i < state.level + 5; i++) { createAsteroid(3); } } } // Draw game objects function draw() { // Clear canvas ctx.fillStyle = 'black'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw particles state.particles.forEach(particle => { ctx.fillStyle = `rgba(255, 255, 255, ${particle.lifetime / 50})`; ctx.beginPath(); ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); ctx.fill(); }); // Draw bullets state.bullets.forEach(bullet => { ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(bullet.x, bullet.y, 2, 0, Math.PI * 2); ctx.fill(); }); // Draw asteroids state.asteroids.forEach(asteroid => { ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.beginPath(); for (let i = 0; i < asteroid.vertices; i++) { const angle = asteroid.rotation + (i / asteroid.vertices) * Math.PI * 2; const radius = asteroid.radius * asteroid.offsets[i]; const x = asteroid.x + Math.cos(angle) * radius; const y = asteroid.y + Math.sin(angle) * radius; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.closePath(); ctx.stroke(); }); // Draw ship if (!state.gameOver && (!state.ship.invincible || Math.floor(state.ship.invincibilityTimer / 5) % 2 === 0)) { ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.beginPath(); // Ship shape const noseX = state.ship.x + Math.cos(state.ship.rotation) * 12; const noseY = state.ship.y + Math.sin(state.ship.rotation) * 12; const leftWingX = state.ship.x + Math.cos(state.ship.rotation + Math.PI * 0.8) * 10; const leftWingY = state.ship.y + Math.sin(state.ship.rotation + Math.PI * 0.8) * 10; const rightWingX = state.ship.x + Math.cos(state.ship.rotation + Math.PI * 1.2) * 10; const rightWingY = state.ship.y + Math.sin(state.ship.rotation + Math.PI * 1.2) * 10; ctx.moveTo(noseX, noseY); ctx.lineTo(leftWingX, leftWingY); ctx.lineTo(rightWingX, rightWingY); ctx.closePath(); ctx.stroke(); // Thruster if (state.ship.thrusting) { ctx.strokeStyle = 'red'; ctx.beginPath(); const backX = state.ship.x - Math.cos(state.ship.rotation) * 8; const backY = state.ship.y - Math.sin(state.ship.rotation) * 8; const thrustLeftX = backX + Math.cos(state.ship.rotation + Math.PI * 0.9) * 5; const thrustLeftY = backY + Math.sin(state.ship.rotation + Math.PI * 0.9) * 5; const thrustRightX = backX + Math.cos(state.ship.rotation + Math.PI * 1.1) * 5; const thrustRightY = backY + Math.sin(state.ship.rotation + Math.PI * 1.1) * 5; ctx.moveTo(thrustLeftX, thrustLeftY); ctx.lineTo(backX, backY); ctx.lineTo(thrustRightX, thrustRightY); ctx.stroke(); } } // Draw game over if (state.gameOver) { ctx.fillStyle = 'white'; ctx.font = '48px monospace'; ctx.textAlign = 'center'; ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2); ctx.font = '24px monospace'; ctx.fillText('Press R to restart', canvas.width / 2, canvas.height / 2 + 50); } } // Input handling window.addEventListener('keydown', e => { if (e.key === 'r' && state.gameOver) { init(); } state.keys[e.key] = true; if (e.key === ' ' && !state.gameOver) { createBullet(); } }); window.addEventListener('keyup', e => { state.keys[e.key] = false; }); // Window resize handling window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }); // Game loop function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); } // Start the game init(); gameLoop(); </script> </body> </html> ``` This implementation includes all the requested features: 1. Vector-based ship with rotation and thrust mechanics 2. Asteroid splitting system (large → medium → small) 3. Screen wrapping for all objects 4. Shooting mechanics with bullet lifetime 5. Score tracking and lives system 6. Explosion particle effects 7. Level progression 8. Game over and restart functionality 9. Responsive canvas sizing 10. Procedural vector graphics for asteroids and ship The game is entirely self-contained in a single HTML file with no external dependencies. Controls: - Arrow keys: Rotate left/right and thrust - Spacebar: Shoot - R: Restart after game over The ship has temporary invincibility after being hit, shown by blinking effect. Asteroids have randomized shapes and movements for variety.
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.