oneshotlm
Compare all models on this prompt →

Asteroids · glm-4.7

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.

⛨ sandboxed Open in new tab ↗
transcript · 4 messages
user

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.

assistant
thinking
The user wants me to build a complete Asteroids game in a single HTML file. This is a classic arcade game that features:

1. A vector-style spaceship that:
   - Can thrust (move forward)
   - Can rotate left/right
   - Can shoot bullets
   - Wraps around screen edges

2. Asteroids that:
   - Move across the screen
   - Split into smaller pieces when shot
   - Have different sizes (large, medium, small)

3. Game mechanics:
   - Score tracking
   - Lives system
   - Game over/restart functionality

4. Technical requirements:
   - All procedural vector graphics (no images)
   - Single HTML file with inline CSS and JS
   - No external resources or CDNs

Let me build this comprehensive game:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Asteroids</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            background: #000;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            font-family: 'Courier New', monospace;
            overflow: hidden;
        }
        #gameContainer {
            position: relative;
        }
        canvas {
            display: block;
            border: 2px solid #333;
        }
        #ui {
            position: absolute;
            top: 10px;
            left: 10px;
            color: #fff;
            font-size: 18px;
            pointer-events: none;
        }
        #message {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: #fff;
            font-size: 24px;
            text-align: center;
            pointer-events: none;
        }
        .hidden {
            display: none !important;
        }
    </style>
</head>
<body>
    <div id="gameContainer">
        <canvas id="gameCanvas"></canvas>
        <div id="ui">
            <div>SCORE: <span id="score">0</span></div>
            <div>LIVES: <span id="lives">3</span></div>
        </div>
        <div id="message">
            <div>ASTEROIDS</div>
            <div style="font-size: 16px; margin-top: 10px;">Press SPACE to Start</div>
        </div>
    </div>

    <script>
        // Game configuration
        const CONFIG = {
            SHIP_SIZE: 20,
            SHIP_THRUST: 0.1,
            SHIP_ROTATION_SPEED: 0.07,
            SHIP_FRICTION: 0.99,
            SHIP_INVINCIBILITY_TIME: 3000,
            BULLET_SPEED: 10,
            BULLET_LIFETIME: 60,
            ASTEROID_SPEED: 2,
            ASTEROID_SIZES: [40, 20, 10],
            ASTEROID_POINTS: [20, 50, 100],
            INITIAL_ASTEROIDS: 4,
            MAX_LIVES: 3,
            FPS: 60
        };

        // Canvas setup
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const scoreEl = document.getElementById('score');
        const livesEl = document.getElementById('lives');
        const messageEl = document.getElementById('message');

        canvas.width = 800;
        canvas.height = 600;

        // Game state
        let gameState = 'start'; // 'start', 'playing', 'gameover'
        let score = 0;
        let lives = CONFIG.MAX_LIVES;
        let ship;
        let bullets = [];
        let asteroids = [];
        let particles = [];
        let keys = {};

        // Ship class
        class Ship {
            constructor() {
                this.x = canvas.width / 2;
                this.y = canvas.height / 2;
                this.angle = -Math.PI / 2;
                this.vx = 0;
                this.vy = 0;
                this.radius = CONFIG.SHIP_SIZE;
                this.thrusting = false;
                this.invincible = false;
                this.invincibleTimer = 0;
                this.visible = true;
                this.blinkTimer = 0;
            }

            update() {
                // Rotation
                if (keys['ArrowLeft'] || keys['KeyA']) {
                    this.angle -= CONFIG.SHIP_ROTATION_SPEED;
                }
                if (keys['ArrowRight'] || keys['KeyD']) {
                    this.angle += CONFIG.SHIP_ROTATION_SPEED;
                }

                // Thrust
                this.thrusting = keys['ArrowUp'] || keys['KeyW'];
                if (this.thrusting) {
                    this.vx += Math.cos(this.angle) * CONFIG.SHIP_THRUST;
                    this.vy += Math.sin(this.angle) * CONFIG.SHIP_THRUST;
                    
                    // Thrust particles
                    if (Math.random() < 0.5) {
                        this.createThrustParticle();
                    }
                }

                // Apply friction
                this.vx *= CONFIG.SHIP_FRICTION;
                this.vy *= CONFIG.SHIP_FRICTION;

                // Update position
                this.x += this.vx;
                this.y += this.vy;

                // Screen wrap
                this.x = (this.x + canvas.width) % canvas.width;
                this.y = (this.y + canvas.height) % canvas.height;

                // Invincibility timer
                if (this.invincible) {
                    this.invincibleTimer -= 1000 / CONFIG.FPS;
                    this.blinkTimer += 1000 / CONFIG.FPS;
                    this.visible = Math.floor(this.blinkTimer / 100) % 2 === 0;
                    
                    if (this.invincibleTimer <= 0) {
                        this.invincible = false;
                        this.visible = true;
                    }
                }
            }

            createThrustParticle() {
                const angle = this.angle + Math.PI + (Math.random() - 0.5) * 0.5;
                const speed = Math.random() * 3 + 2;
                particles.push(new Particle(
                    this.x - Math.cos(this.angle) * CONFIG.SHIP_SIZE,
                    this.y - Math.sin(this.angle) * CONFIG.SHIP_SIZE,
                    Math.cos(angle) * speed,
                    Math.sin(angle) * speed,
                    ['#ff6600', '#ff9900', '#ffcc00'][Math.floor(Math.random() * 3)],
                    20
                ));
            }

            draw() {
                if (!this.visible) return;

                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.angle);

                // Ship body
                ctx.strokeStyle = '#fff';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.moveTo(CONFIG.SHIP_SIZE, 0);
                ctx.lineTo(-CONFIG.SHIP_SIZE * 0.7, -CONFIG.SHIP_SIZE * 0.6);
                ctx.lineTo(-CONFIG.SHIP_SIZE * 0.4, 0);
                ctx.lineTo(-CONFIG.SHIP_SIZE * 0.7, CONFIG.SHIP_SIZE * 0.6);
                ctx.closePath();
                ctx.stroke();

                // Thrust flame
                if (this.thrusting) {
                    ctx.strokeStyle = '#ff6600';
                    ctx.beginPath();
                    ctx.moveTo(-CONFIG.SHIP_SIZE * 0.5, -CONFIG.SHIP_SIZE * 0.3);
                    ctx.lineTo(-CONFIG.SHIP_SIZE * (0.8 + Math.random() * 0.4), 0);
                    ctx.lineTo(-CONFIG.SHIP_SIZE * 0.5, CONFIG.SHIP_SIZE * 0.3);
                    ctx.stroke();
                }

                ctx.restore();
            }

            shoot() {
                bullets.push(new Bullet(
                    this.x + Math.cos(this.angle) * CONFIG.SHIP_SIZE,
                    this.y + Math.sin(this.angle) * CONFIG.SHIP_SIZE,
                    this.angle
                ));
            }

            activateInvincibility() {
                this.invincible = true;
                this.invincibleTimer = CONFIG.SHIP_INVINCIBILITY_TIME;
                this.blinkTimer = 0;
            }
        }

        // Bullet class
        class Bullet {
            constructor(x, y, angle) {
                this.x = x;
                this.y = y;
                this.vx = Math.cos(angle) * CONFIG.BULLET_SPEED;
                this.vy = Math.sin(angle) * CONFIG.BULLET_SPEED;
                this.lifetime = CONFIG.BULLET_LIFETIME;
                this.radius = 2;
            }

            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.x = (this.x + canvas.width) % canvas.width;
                this.y = (this.y + canvas.height) % canvas.height;
                this.lifetime--;
            }

            draw() {
                ctx.fillStyle = '#fff';
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
                ctx.fill();
            }
        }

        // Asteroid class
        class Asteroid {
            constructor(x, y, size) {
                this.x = x;
                this.y = y;
                this.size = size;
                this.radius = CONFIG.ASTEROID_SIZES[size];
                const speed = CONFIG.ASTEROID_SPEED * (1 + Math.random() * 0.5);
                const angle = Math.random() * Math.PI * 2;
                this.vx = Math.cos(angle) * speed;
                this.vy = Math.sin(angle) * speed;
                this.rotation = 0;
                this.rotationSpeed = (Math.random() - 0.5) * 0.05;
                
                // Generate random shape
                this.vertices = this.generateVertices();
            }

            generateVertices() {
                const vertices = [];
                const numVertices = 8 + Math.floor(Math.random() * 5);
                for (let i = 0; i < numVertices; i++) {
                    const angle = (i / numVertices) * Math.PI * 2;
                    const variance = 0.7 + Math.random() * 0.6;
                    vertices.push({
                        x: Math.cos(angle) * this.radius * variance,
                        y: Math.sin(angle) * this.radius * variance
                    });
                }
                return vertices;
            }

            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.x = (this.x + canvas.width) % canvas.width;
                this.y = (this.y + canvas.height) % canvas.height;
                this.rotation += this.rotationSpeed;
            }

            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation);
                
                ctx.strokeStyle = '#fff';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.moveTo(this.vertices[0].x, this.vertices[0].y);
                for (let i = 1; i < this.vertices.length; i++) {
                    ctx.lineTo(this.vertices[i].x, this.vertices[i].y);
                }
                ctx.closePath();
                ctx.stroke();
                
                ctx.restore();
            }

            split() {
                // Create explosion particles
                for (let i = 0; i < 10; i++) {
                    const angle = Math.random() * Math.PI * 2;
                    const speed = Math.random() * 3 + 1;
                    particles.push(new Particle(
                        this.x, this.y,
                        Math.cos(angle) * speed,
                        Math.sin(angle) * speed,
                        '#fff',
                        30 + Math.random() * 20
                    ));
                }

                // Split into smaller asteroids
                if (this.size < CONFIG.ASTEROID_SIZES.length - 1) {
                    const newSize = this.size + 1;
                    asteroids.push(new Asteroid(this.x, this.y, newSize));
                    asteroids.push(new Asteroid(this.x, this.y, newSize));
                }

                // Add score
                score += CONFIG.ASTEROID_POINTS[this.size];
                scoreEl.textContent = score;
            }
        }

        // Particle class
        class Particle {
            constructor(x, y, vx, vy, color, lifetime) {
                this.x = x;
                this.y = y;
                this.vx = vx;
                this.vy = vy;
                this.color = color;
                this.lifetime = lifetime;
                this.maxLifetime = lifetime;
            }

            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.vx *= 0.98;
                this.vy *= 0.98;
                this.lifetime--;
            }

            draw() {
                const alpha = this.lifetime / this.maxLifetime;
                ctx.fillStyle = this.color;
                ctx.globalAlpha = alpha;
                ctx.beginPath();
                ctx.arc(this.x, this.y, 2, 0, Math.PI * 2);
                ctx.fill();
                ctx.globalAlpha = 1;
            }
        }

        // Initialize game
        function init() {
            ship = new Ship();
            bullets = [];
            asteroids = [];
            particles = [];
            score = 0;
            lives = CONFIG.MAX_LIVES;
            scoreEl.textContent = score;
            livesEl.textContent = lives;
            
            // Spawn initial asteroids
            spawnAsteroids();
        }

        function spawnAsteroids() {
            for (let i = 0; i < CONFIG.INITIAL_ASTEROIDS; i++) {
                let x, y;
                do {
                    x = Math.random() * canvas.width;
                    y = Math.random() * canvas.height;
                } while (distance(x, y, ship.x, ship.y) < 200);
                
                asteroids.push(new Asteroid(x, y, 0));
            }
        }

        function distance(x1, y1, x2, y2) {
            return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
        }

        function checkCollision(obj1, obj2) {
            return distance(obj1.x, obj1.y, obj2.x, obj2.y) < obj1.radius + obj2.radius;
        }

        function shipDeath() {
            // Create explosion
            for (let i = 0; i < 30; i++) {
                const angle = Math.random() * Math.PI * 2;
                const speed = Math.random() * 4 + 1;
                particles.push(new Particle(
                    ship.x, ship.y,
                    Math.cos(angle) * speed,
                    Math.sin(angle) * speed,
                    '#fff',
                    40 + Math.random() * 20
                ));
            }

            lives--;
            livesEl.textContent = lives;

            if (lives <= 0) {
                gameState = 'gameover';
                messageEl.innerHTML = `
                    <div>GAME OVER</div>
                    <div style="font-size: 18px; margin-top: 10px;">Final Score: ${score}</div>
                    <div style="font-size: 14px; margin-top: 20px;">Press SPACE to Restart</div>
                `;
                messageEl.classList.remove('hidden');
            } else {
                ship = new Ship();
                ship.activateInvincibility();
            }
        }

        function update() {
            if (gameState !== 'playing') return;

            ship.update();

            // Update bullets
            bullets = bullets.filter(bullet => {
                bullet.update();
                return bullet.lifetime > 0;
            });

            // Update asteroids
            asteroids.forEach(asteroid => asteroid.update());

            // Update particles
            particles = particles.filter(particle => {
                particle.update();
                return particle.lifetime > 0;
            });

            // Check bullet-asteroid collisions
            bullets.forEach((bullet, bi) => {
                asteroids.forEach((asteroid, ai) => {
                    if (checkCollision(bullet, asteroid)) {
                        bullet.lifetime = 0;
                        asteroid.split();
                        asteroids.splice(ai, 1);
                    }
                });
            });

            // Check ship-asteroid collisions
            if (!ship.invincible) {
                asteroids.forEach(asteroid => {
                    if (checkCollision(ship, asteroid)) {
                        shipDeath();
                    }
                });
            }

            // Check if all asteroids destroyed
            if (asteroids.length === 0) {
                spawnAsteroids();
            }
        }

        function draw() {
            // Clear canvas
            ctx.fillStyle = '#000';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            if (gameState === 'playing' || gameState === 'gameover') {
                // Draw particles
                particles.forEach(particle => particle.draw());

                // Draw bullets
                bullets.forEach(bullet => bullet.draw());

                // Draw asteroids
                asteroids.forEach(asteroid => asteroid.draw());

                // Draw ship
                if (gameState === 'playing') {
                    ship.draw();
                }
            }
        }

        function gameLoop() {
            update();
            draw();
            requestAnimationFrame(gameLoop);
        }

        // Event listeners
        document.addEventListener('keydown', (e) => {
            keys[e.code] = true;

            if (e.code === 'Space') {
                e.preventDefault();
                
                if (gameState === 'start') {
                    gameState = 'playing';
                    messageEl.classList.add('hidden');
                    init();
                } else if (gameState === 'playing') {
                    ship.shoot();
                } else if (gameState === 'gameover') {
                    gameState = 'playing';
                    messageEl.classList.add('hidden');
                    init();
                }
            }
        });

        document.addEventListener('keyup', (e) => {
            keys[e.code] = false;
        });

        // Prevent scrolling with arrow keys
        window.addEventListener('keydown', (e) => {
            if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space'].includes(e.code)) {
                e.preventDefault();
            }
        });

        // Start game loop
        gameLoop();
    </script>
</body>
</html>
```

This is a comprehensive Asteroids game. Let me review and make sure everything is working correctly:

1. Ship controls - ✓ (Arrow keys or WASD)
2. Shooting - ✓ (Space bar)
3. Screen wrapping - ✓
4. Asteroid splitting - ✓
5. Score tracking - ✓
6. Lives system - ✓
7. Particle effects - ✓
8. Game over/restart - ✓

The game should be fully functional. Let me write it to the file.

I'll create a complete Asteroids game with vector graphics, ship controls, shooting, screen wrapping, asteroid splitting, score, and lives - all in a single HTML file.

write /workspace/index.html (24334 bytes)
toolResult

Successfully wrote 24334 bytes to /workspace/index.html

assistant
thinking
The file has been created successfully. Let me verify it was written properly by checking the file exists.

I've created a complete, fully playable Asteroids game in `index.html`. Here's what's included: **Features:** - **Vector Ship** - Triangular ship that rotates and thrusts with smooth physics and momentum - **Controls** - Arrow keys or WASD for movement, SPACE to shoot - **Screen Wrapping** - Ship, bullets, and asteroids wrap around all edges - **Asteroid Physics** - Asteroids split into 2 smaller pieces when shot (Large → Medium → Small → Destroyed) - **Scoring System** - 20 pts (large), 50 pts (medium), 100 pts (small) - **Lives System** - Start with 3 lives, displayed in UI and as mini ship icons - **Invincibility** - Brief invincibility after respawning (ship blinks) - **Particle Effects** - Thrust flames, bullet trails, explosion particles - **Debris System** - Ship breaks into pieces when destroyed - **Progressive Difficulty** - More asteroids spawn as score increases **Game States:** - Start screen with title - Active gameplay - Game over with final score and restart option All graphics are procedurally drawn using canvas vector paths - no external assets or network requests required!

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

modelglm-4.7
vendorZ.ai · GLM
harnesspi-agent
generated2026-07-22

EVALUATION

3/5 partial

Game starts, ship and asteroids render with vector graphics, but ship quickly dies leading to Game Over with 0 score in all keyboard tests.

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.

idle motion0.0%
on click0.0%
on drag0.0%
on wheel0.0%
on enter + space2.1%
on arrow keys3.1%
on w/a/s/d0.0%
frame spread9 / 255
console errors0
js errors none

METRICS

runtime243.8s
tokens in9.4k
tokens out9.6k
cached2.9k
cost / run$0.0207