oneshotlm
Compare all models on this prompt →

Asteroids · solar-pro4

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 playable Asteroids game as a single HTML file. Let me plan the features:

1. **Ship**: Vector-based triangle ship that rotates with left/right arrows, thrusts with up arrow
2. **Controls**: 
   - Left/Right arrow or A/D to rotate
   - Up arrow or W to thrust
   - Space to shoot
3. **Physics**: 
   - Ship wraps around screen edges
   - Inertia-based movement (ship has velocity, drift)
   - Asteroids move in straight lines
4. **Asteroids**: 
   - Random sizes (large, medium, small)
   - Split into smaller pieces when shot
   - Wrap around screen edges
5. **Shooting**: 
   - Bullets with limited lifetime
   - Bullets wrap around screen edges too
6. **Collision detection**: 
   - Ship vs asteroid (lose a life)
   - Bullet vs asteroid (split asteroid)
7. **Score**: 
   - Large asteroid: 20 points
   - Medium: 50 points
   - Small: 100 points
8. **Lives**: 3 lives, game over when all lost
9. **Procedural vector graphics**: All drawn with canvas paths, no images

Let me build this out fully.

Key design decisions:
- Classic Asteroids physics: ship has thrust that applies acceleration, friction/damping
- Ship wraps around screen
- Asteroids have random rotational velocity and linear velocity
- Bullets travel in the direction the ship is facing
- Collision uses point-in-polygon or circle-based detection

For collision, I'll use circle-based collision (asteroids and ship as circles) for simplicity, or I can use more precise polygon collision. Let me use circle-based for speed.

Actually, for a retro feel, I'll use the ship as a triangle and check collision with asteroid circles. Or I can use circle-circle collision for everything.

Let me design:
- Ship: triangle, radius ~12 for collision
- Asteroids: irregular polygons, radius based on size
- Bullets: small circles

For asteroid shapes, I'll generate random polygon vertices procedurally.

Let me write the full game:

```html
<!DOCTYPE html>
<html>
<head>
<style>
  /* full page canvas, dark background */
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
  // Full game code
</script>
</body>
</html>
```

Let me think about the game state:

```
GAME_STATE = 'playing' | 'gameover' | 'start'
```

- Start screen: "Press SPACE to start" or similar
- Playing: normal gameplay
- Game over: show final score, restart option

Ship physics:
- position (x, y)
- velocity (vx, vy)
- rotation angle
- thrust acceleration
- damping/friction (e.g., 0.99 per frame)

Asteroids:
- position
- velocity
- rotation
- rotation speed
- size (large: 3, medium: 2, small: 1)
- radius (large: ~40, medium: ~25, small: ~12)
- vertices (for drawing)

Bullets:
- position
- velocity
- lifetime

Stars background: procedural starfield

Let me code this up comprehensively.

Window resize handling: I'll make the canvas fill the window and use the current dimensions for wrapping.

Let me write the complete code now.

For the asteroid shapes, I'll generate them as follows:
- For each asteroid, generate 8-12 random points on a circle with some radius variation
- Store these as local coordinates relative to center
- Draw by rotating them

Actually, for vector look, I'll draw asteroids as irregular polygons with lines connecting vertices, maybe with some inner lines for detail.

Let me write the code:

```javascript
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');

function resize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();

// Constants
const SHIP_SIZE = 15;
const SHIP_THRUST = 0.12;
const SHIP_FRICTION = 0.99;
const SHIP_TURN_SPEED = 0.07;
const BULLET_SPEED = 7;
const BULLET_LIFE = 60; // frames
const MAX_BULLETS = 10;
const SHOOT_COOLDOWN = 8;

// Game state
let state = 'start'; // 'start', 'playing', 'gameover'
let score = 0;
let lives = 3;
let ship = null;
let asteroids = [];
let bullets = [];
let stars = [];
let shootCooldown = 0;
let invincible = 0;
let frameCount = 0;

// Initialize stars
function initStars() {
  stars = [];
  for (let i = 0; i < 200; i++) {
    stars.push({
      x: Math.random() * canvas.width,
      y: Math.random() * canvas.height,
      size: Math.random() * 1.5 + 0.5,
      brightness: Math.random()
    });
  }
}

// Ship
function createShip() {
  return {
    x: canvas.width / 2,
    y: canvas.height / 2,
    vx: 0,
    vy: 0,
    angle: -Math.PI / 2, // pointing up
    thrusting: false
  };
}

// Asteroid
function createAsteroid(x, y, size, vx, vy) {
  const sizes = { 3: 40, 2: 22, 1: 12 };
  const radius = sizes[size];
  const vertices = [];
  const numVerts = 8 + Math.floor(Math.random() * 5);
  for (let i = 0; i < numVerts; i++) {
    const angle = (i / numVerts) * Math.PI * 2;
    const r = radius * (0.7 + Math.random() * 0.3);
    vertices.push({ angle, r });
  }
  return {
    x, y, vx, vy,
    radius,
    size,
    rotation: Math.random() * Math.PI * 2,
    rotSpeed: (Math.random() - 0.5) * 0.02,
    vertices
  };
}

// Spawn initial asteroids
function spawnAsteroids() {
  asteroids = [];
  const count = 4 + Math.floor(Math.random() * 2);
  for (let i = 0; i < count; i++) {
    let x, y;
    do {
      x = Math.random() * canvas.width;
      y = Math.random() * canvas.height;
    } while (Math.hypot(x - canvas.width/2, y - canvas.height/2) < 150);
    const vx = (Math.random() - 0.5) * 1.5;
    const vy = (Math.random() - 0.5) * 1.5;
    asteroids.push(createAsteroid(x, y, 3, vx, vy));
  }
}

function wrap(obj) {
  if (obj.x < 0) obj.x += canvas.width;
  if (obj.x > canvas.width) obj.x -= canvas.width;
  if (obj.y < 0) obj.y += canvas.height;
  if (obj.y > canvas.height) obj.y -= canvas.height;
}

function updateShip() {
  if (keys['ArrowLeft'] || keys['KeyA']) {
    ship.angle -= SHIP_TURN_SPEED;
  }
  if (keys['ArrowRight'] || keys['KeyD']) {
    ship.angle += SHIP_TURN_SPEED;
  }
  ship.thrusting = !!(keys['ArrowUp'] || keys['KeyW']);
  
  if (ship.thrusting) {
    ship.vx += Math.cos(ship.angle) * SHIP_THRUST;
    ship.vy += Math.sin(ship.angle) * SHIP_THRUST;
  }
  
  ship.vx *= SHIP_FRICTION;
  ship.vy *= SHIP_FRICTION;
  
  ship.x += ship.vx;
  ship.y += ship.vy;
  
  wrap(ship);
  
  if (invincible > 0) invincible--;
}

function updateBullets() {
  for (let i = bullets.length - 1; i >= 0; i--) {
    const b = bullets[i];
    b.x += b.vx;
    b.y += b.vy;
    b.life--;
    wrap(b);
    if (b.life <= 0) {
      bullets.splice(i, 1);
    }
  }
}

function updateAsteroids() {
  for (const a of asteroids) {
    a.x += a.vx;
    a.y += a.vy;
    a.rotation += a.rotSpeed;
    wrap(a);
  }
}

function shoot() {
  if (shootCooldown > 0) return;
  if (bullets.length >= MAX_BULLETS) return;
  const bx = ship.x + Math.cos(ship.angle) * SHIP_SIZE;
  const by = ship.y + Math.sin(ship.angle) * SHIP_SIZE;
  bullets.push({
    x: bx, y: by,
    vx: Math.cos(ship.angle) * BULLET_SPEED + ship.vx * 0.3,
    vy: Math.sin(ship.angle) * BULLET_SPEED + ship.vy * 0.3,
    life: BULLET_LIFE
  });
  shootCooldown = SHOOT_COOLDOWN;
}

function checkCollisions() {
  // Bullets vs asteroids
  for (let i = bullets.length - 1; i >= 0; i--) {
    const b = bullets[i];
    for (let j = asteroids.length - 1; j >= 0; j--) {
      const a = asteroids[j];
      if (Math.hypot(b.x - a.x, b.y - a.y) < a.radius) {
        // Hit!
        bullets.splice(i, 1);
        score += a.size === 3 ? 20 : a.size === 2 ? 50 : 100;
        // Split asteroid
        if (a.size > 1) {
          for (let k = 0; k < 2; k++) {
            const nx = a.x + (Math.random() - 0.5) * 10;
            const ny = a.y + (Math.random() - 0.5) * 10;
            const nvx = a.vx + (Math.random() - 0.5) * 1;
            const nvy = a.vy + (Math.random() - 0.5) * 1;
            asteroids.push(createAsteroid(nx, ny, a.size - 1, nvx, nvy));
          }
        }
        asteroids.splice(j, 1);
        break;
      }
    }
  }
  
  // Ship vs asteroids
  if (invincible <= 0 && ship) {
    for (const a of asteroids) {
      if (Math.hypot(ship.x - a.x, ship.y - a.y) < a.radius + SHIP_SIZE * 0.6) {
        lives--;
        invincible = 120; // 2 seconds at 60fps
        // Reset ship position
        ship.x = canvas.width / 2;
        ship.y = canvas.height / 2;
        ship.vx = 0;
        ship.vy = 0;
        ship.angle = -Math.PI / 2;
        if (lives <= 0) {
          state = 'gameover';
          ship = null;
        }
        break;
      }
    }
  }
}

function update() {
  frameCount++;
  if (state === 'playing') {
    shootCooldown--;
    updateShip();
    updateBullets();
    updateAsteroids();
    checkCollisions();
    
    if (asteroids.length === 0 && ship) {
      // Level complete, spawn more
      spawnAsteroids();
    }
  }
}

function drawShip() {
  if (!ship) return;
  ctx.save();
  ctx.translate(ship.x, ship.y);
  ctx.rotate(ship.angle);
  
  const blinking = invincible > 0 && Math.floor(frameCount / 4) % 2 === 0;
  if (blinking) {
    ctx.restore();
    return;
  }
  
  // Ship body - classic triangle
  ctx.beginPath();
  ctx.moveTo(SHIP_SIZE, 0);
  ctx.lineTo(-SHIP_SIZE * 0.7, -SHIP_SIZE * 0.6);
  ctx.lineTo(-SHIP_SIZE * 0.4, 0);
  ctx.lineTo(-SHIP_SIZE * 0.7, SHIP_SIZE * 0.6);
  ctx.closePath();
  
  if (ship.thrusting) {
    // Thrust flame
    ctx.beginPath();
    ctx.moveTo(-SHIP_SIZE * 0.4, -SHIP_SIZE * 0.25);
    ctx.lineTo(-SHIP_SIZE * (1.2 + Math.random() * 0.5), 0);
    ctx.lineTo(-SHIP_SIZE * 0.4, SHIP_SIZE * 0.25);
    ctx.closePath();
  }
  
  ctx.strokeStyle = '#0ff';
  ctx.lineWidth = 1.5;
  ctx.stroke();
  ctx.restore();
}

function drawAsteroid(a) {
  ctx.save();
  ctx.translate(a.x, a.y);
  ctx.rotate(a.rotation);
  ctx.beginPath();
  for (let i = 0; i < a.vertices.length; i++) {
    const v = a.vertices[i];
    const px = Math.cos(v.angle) * v.r;
    const py = Math.sin(v.angle) * v.r;
    if (i === 0) ctx.moveTo(px, py);
    else ctx.lineTo(px, py);
  }
  ctx.closePath();
  ctx.strokeStyle = '#aaa';
  ctx.lineWidth = 1.5;
  ctx.stroke();
  ctx.restore();
}

function drawBullet(b) {
  ctx.beginPath();
  ctx.arc(b.x, b.y, 2, 0, Math.PI * 2);
  ctx.fillStyle = '#fff';
  ctx.fill();
}

function drawStars() {
  for (const s of stars) {
    const flicker = 0.5 + 0.5 * Math.sin(frameCount * 0.02 + s.brightness * 100);
    ctx.globalAlpha = flicker * s.brightness;
    ctx.fillStyle = '#fff';
    ctx.fillRect(s.x, s.y, s.size, s.size);
  }
  ctx.globalAlpha = 1;
}

function drawUI() {
  ctx.fillStyle = '#0ff';
  ctx.font = '14px monospace';
  ctx.textAlign = 'left';
  ctx.fillText('Score: ' + score, 16, 24);
  
  ctx.textAlign = 'right';
  let livesStr = '';
  for (let i = 0; i < lives; i++) {
    livesStr += '♥ ';
  }
  ctx.fillText(livesStr, canvas.width - 16, 24);
}

function drawStartScreen() {
  ctx.fillStyle = '#0ff';
  ctx.font = '24px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('ASTEROIDS', canvas.width/2, canvas.height/2 - 40);
  ctx.font = '14px monospace';
  ctx.fillText('Press SPACE to start', canvas.width/2, canvas.height/2 + 10);
  ctx.fillText('Controls: Arrow keys to move, Space to shoot', canvas.width/2, canvas.height/2 + 40);
}

function drawGameOver() {
  ctx.fillStyle = '#f44';
  ctx.font = '28px monospace';
  ctx.textAlign = 'center';
  ctx.fillText('GAME OVER', canvas.width/2, canvas.height/2 - 30);
  ctx.fillStyle = '#fff';
  ctx.font = '16px monospace';
  ctx.fillText('Score: ' + score, canvas.width/2, canvas.height/2 + 10);
  ctx.fillText('Press SPACE to restart', canvas.width/2, canvas.height/2 + 40);
}

function draw() {
  ctx.fillStyle = '#000';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  drawStars();
  
  if (state === 'start') {
    drawStartScreen();
    return;
  }
  
  if (ship) drawShip();
  for (const a of asteroids) drawAsteroid(a);
  for (const b of bullets) drawBullet(b);
  drawUI();
  
  if (state === 'gameover') {
    drawGameOver();
  }
}

// Input
const keys = {};
document.addEventListener('keydown', (e) => {
  keys[e.code] = true;
  if (e.code === 'Space') {
    e.preventDefault();
    if (state === 'start' || state === 'gameover') {
      startGame();
    } else {
      shoot();
    }
  }
  if (e.code === 'Enter' && (state === 'start' || state === 'gameover')) {
    startGame();
  }
});
document.addEventListener('keyup', (e) => {
  keys[e.code] = false;
});

function startGame() {
  score = 0;
  lives = 3;
  ship = createShip();
  bullets = [];
  invincible = 180;
  shootCooldown = 0;
  spawnAsteroids();
  state = 'playing';
}

// Game loop
function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

initStars();
loop();
```

Wait, I need to be careful about a few things:

1. The ship should be drawn properly - I want it to look like the classic Asteroids ship (a triangle pointing in the direction of travel)
2. The thrust flame should be more visible
3. Collision detection needs to work well

Let me refine the ship drawing. The classic Asteroids ship is a triangle with a notch in the back:

```
     /\
    /  \
   /    \
  /   /\ \
 /__/    \__
```

Actually, the classic shape is:
- Point at front (nose)
- Two side points
- A notch at the back (two rear points)

Let me draw it as:
- Front point: (SHIP_SIZE, 0)
- Left rear: (-SHIP_SIZE*0.7, -SHIP_SIZE*0.5)  
- Left inner: (-SHIP_SIZE*0.4, 0)
- Right inner: (-SHIP_SIZE*0.4, 0)
- Right rear: (-SHIP_SIZE*0.7, SHIP_SIZE*0.5)

Wait, that's not right. Let me think again.

Classic Asteroids ship (pointing right):
- Nose: (15, 0)
- Right wing rear: (-12, -8)
- Not back right: (-5, 0)  -- this is the notch
- Left wing rear: (-12, 8)

So:
```
ctx.moveTo(SHIP_SIZE, 0);
ctx.lineTo(-SHIP_SIZE * 0.8, -SHIP_SIZE * 0.55);
ctx.lineTo(-SHIP_SIZE * 0.4, 0);
ctx.lineTo(-SHIP_SIZE * 0.8, SHIP_SIZE * 0.55);
ctx.closePath();
```

That looks right. The notch is at (-SHIP_SIZE*0.4, 0).

For the thrust flame, it should come out of the notch:
```
ctx.moveTo(-SHIP_SIZE * 0.4, -SHIP_SIZE * 0.2);
ctx.lineTo(-SHIP_SIZE * (1.0 + Math.random() * 0.5), 0);
ctx.lineTo(-SHIP_SIZE * 0.4, SHIP_SIZE * 0.2);
```

This is good. Let me also add the "gem" or detail line in the middle of the ship for visual flair.

Actually, let me keep it simple and just draw the ship outline nicely.

Now, I also want to add some visual polish:
- A subtle scanline effect or vignette
- Particle effects for thrust
- Screen shake on death
- Sound effects? No, no external assets and no WebAudio synthesis mentioned. Actually, WebAudio is fine since it's procedural. But let me skip sound for simplicity.

Actually, let me add thrust particles - small particles emitted from the back of the ship when thrusting.

Let me also add a nice touch: when you destroy the last asteroid, a brief "level complete" flash or the new asteroids spawn with a delay.

Let me also handle the edge case where the ship is invincible and blinking - I should make it more visible.

OK let me also think about the ship respawn: when you lose a life, the ship should reappear at center, and asteroids should bounce off the edges or be pushed away. Classic Asteroids has the asteroids bounce away from the ship when it respawns. Let me implement that.

Actually, for simplicity, I'll just make the ship invincible for a period and respawn at center. The asteroids keep moving. That's fine.

Let me also add a high score tracker (in-memory, not persisted).

Let me finalize the code and write it.

One more thing: I should handle window resize properly and also ensure the game scales.

Let me also add a nice title screen with some animation (e.g., a rotating ship or floating text).

OK, let me write the final version. I'll aim for a polished, complete game.

Here's my plan for the final code:

1. HTML structure with canvas
2. CSS for full-screen dark background, canvas styling
3. JavaScript game engine:
   - Constants
   - Game state management (start, playing, gameover)
   - Ship with physics
   - Asteroids with procedural shapes
   - Bullets
   - Stars background
   - Collision detection (circle-based)
   - Score and lives
   - Input handling
   - Rendering (all procedural vector graphics)
   - Game loop

Let me write it all now. I'll make it comprehensive and polished.

For the thrust particles, I'll create a particle system:

```javascript
let particles = [];

function emitThrustParticle() {
  particles.push({
    x: ship.x - Math.cos(ship.angle) * SHIP_SIZE * 0.5,
    y: ship.y - Math.sin(ship.angle) * SHIP_SIZE * 0.5,
    vx: -Math.cos(ship.angle) * (Math.random() * 3 + 2) + (Math.random()-0.5)*0.5,
    vy: -Math.sin(ship.angle) * (Math.random() * 3 + 2) + (Math.random()-0.5)*0.5,
    life: 20 + Math.random() * 10,
    maxLife: 30,
    size: Math.random() * 2 + 1
  });
}
```

And update/draw them.

Let me also add screen shake on death:

```javascript
let shake = 0;

function triggerShake() {
  shake = 10;
}

// In draw:
if (shake > 0) {
  const sx = (Math.random() - 0.5) * shake;
  const sy = (Math.random() - 0.5) * shake;
  ctx.translate(sx, sy);
  shake *= 0.85;
  if (shake < 0.5) shake = 0;
}
```

OK, let me write the complete final code now.

Actually, I realize I should also add:
- A UFO or two? No, keep it simple. Classic Asteroids has a flying saucer but let me skip that for now.
- Hyperspace? No, classic feature but skip.

Let me keep it to the core Asteroids experience: ship, asteroids, bullets, score, lives.

One more polish item: when the game starts, show a brief "GET READY" or just go straight into gameplay. I'll go with a start screen that transitions smoothly.

Let me write the final code.

Actually wait, I want to double-check the collision. In the classic game, the ship's collision radius is smaller than its visual size. The ship visual is about 15px radius but collision is more like 8px. Let me use SHIP_SIZE * 0.5 for collision radius.

Also for asteroids, the collision radius should be the actual radius of the asteroid, which I'm already using.

Let me finalize:

SHIP_COLLISION_RADIUS = SHIP_SIZE * 0.6 (about 9px)

OK writing now. Let me also make the asteroid drawing nicer - add some inner detail lines.

For a more interesting asteroid look, I can draw additional lines inside the asteroid shape:

```javascript
// Draw asteroid with inner detail
ctx.beginPath();
for (let i = 0; i < a.vertices.length; i++) {
  const v = a.vertices[i];
  const px = Math.cos(v.angle) * v.r;
  const py = Math.sin(v.angle) * v.r;
  if (i === 0) ctx.moveTo(px, py);
  else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.stroke();

// Inner lines
ctx.beginPath();
const midVerts = a.vertices.length;
for (let i = 0; i < 3; i++) {
  const idx = Math.floor(Math.random() * midVerts);
  // hmm this won't work well since vertices are fixed per asteroid
}
```

Actually, let me pre-generate some inner connection points when creating the asteroid. Or simpler: draw a few chords across the asteroid.

Let me keep it simple: just the outline. The outline alone looks great for Asteroids.

OK final code time. Let me write it all.

Let me also make the UI nicer - add a "LEVEL" indicator or just keep score and lives.

I'll add:
- Score display (top left)
- Lives display (top right, as ship icons or hearts)
- High score (below score)

Let me write the complete file now.

Actually, I want to reconsider the ship drawing. Looking at reference images of Asteroids:

The ship is a triangle with a V-shaped notch at the back. When you look at it:
- The nose is a point
- The wings sweep back
- At the back, there's a notch (inverted V) cut into it

So the outline goes:
1. Nose (front point)
2. Right wing tip (back-right)
3. Right side of notch (middle-right, closer to center)
4. Left side of notch (middle-left)
5. Left wing tip (back-left)
6. Back to nose

In coordinates (ship pointing right, centered at origin):
- Nose: (15, 0)
- Right wing: (-12, -9)
- Right notch: (-6, 0)  -- actually, the notch is at the back center
- Left notch: (-6, 0) -- same point? No.

Actually, looking more carefully at the classic Asteroids ship:

The ship shape is:
- Nose at (15, 0)
- Upper right rear at (-13, -9)
- Lower right rear... no wait.

Let me look at this differently. The classic ship is drawn as:

```
    *
   / \
  /   \
 /     \
*       *
 \     /
  \   /
   \ /
    *
```

No, that's not right either. The classic ship is more like:

```
      ▲ (nose)
     / \
    /   \
   /     \
  *       *  (wing tips)
   \     /
    \   /    (notch)
     \ /
      *      (back point of notch)
```

Hmm, so it's actually like a pentagon with a notch. Let me trace it:

Starting from nose, going clockwise:
1. Nose: (15, 0)
2. Right wing tip: (-13, -10)
3. Right side of notch: (-5, -3) -- this is the right edge of the notch
4. Bottom of notch: (-7, 0) -- the deepest point of the notch
5. Left side of notch: (-5, 3)
6. Left wing tip: (-13, 10)

Hmm, that doesn't look right. Let me just go with a simpler version:

```
1. Nose: (15, 0)
2. Right wing: (-12, -8)
3. Right inner: (-5, 0)
4. Left inner: (-5, 0)  -- same point
5. Left wing: (-12, 8)
```

Wait, that's just a triangle with a flat back. That's not the classic look.

OK let me try:
```
1. Nose: (15, 0)  
2. Upper right: (-12, -8)
3. Upper notch: (-6, -2)
4. Lower notch: (-6, 2)
5. Lower right: (-12, 8)
```

No, this is 5 points but the notch should be at the back center.

You know what, let me just use the well-known Asteroids ship shape. Here's the classic one:

The ship in Asteroids (rotated to point right):
- (0, -10) to (15, 0) -- upper edge
- (15, 0) to (0, 10) -- lower edge  
- (0, 10) to (-8, 5) -- right lower rear
- (-8, 5) to (-4, 0) -- right side of notch
- (-4, 0) to (-8, -5) -- left side of notch
- (-8, -5) to (0, -10) -- left upper rear

Wait, that's 6 points. Let me re-trace:

Going clockwise from the nose:
1. (15, 0) - nose
2. (0, -10) - upper rear / left wing tip
3. (-8, -5) - left side of notch
4. (-4, 0) - bottom of notch (innermost point)
5. (-8, 5) - right side of notch
6. (0, 10) - lower rear / right wing tip

Yes! This is the classic shape. A symmetrical ship with a V-notch at the back.

Scaled to my SHIP_SIZE=15:
1. (15, 0) - nose
2. (0, -10) - left wing
3. (-8, -5) - left notch
4. (-4, 0) - notch bottom
5. (-8, 5) - right notch
6. (0, 10) - right wing

This looks great. Let me use this.

For the thrust flame, it comes out of the notch area:
- Left flame: from (-4, -2) to (-10 - random, 0)
- Right flame: from (-4, 2) to (-10 - random, 0)

Or a single flame shape from the notch.

OK, I think I have a good design. Let me write the final code.

Actually, one more thought: I want the ship to have a slight glow or feel more "vector" - I can achieve this by drawing the ship twice:
1. First pass: thicker line, lighter color (glow)
2. Second pass: normal line, bright color

Or I can just use a bright color and a thicker line.

Let me use:
- Ship color: #00ffff (cyan) with lineWidth 1.5
- Asteroid color: #aaaaaa (gray) with lineWidth 1.5
- Bullet color: #ffffff (white) as small filled circles

For the ship glow, I'll draw it with a slightly thicker, semi-transparent line first.

OK, writing the final code now. No more planning.

Let me also make sure the game handles:
- Rapid firing (cooldown)
- Multiple bullets on screen
- Asteroids that wrap around
- Ship that wraps around
- Bullets that wrap around
- Level progression (more asteroids each level)

For level progression, I'll increase the number of initial asteroids and their speed slightly each level.

Level 1: 4 asteroids
Level 2: 5 asteroids
Level 3: 6 asteroids
... up to maybe 10

Speed increases: base speed + level * 0.1

OK, writing now.

One more thing: I should draw the ship with a slight "engine glow" when thrusting. I can draw a bright cyan/white flame shape at the back of the ship.

Let me write the complete code.

I also want to add a subtle "scan line" effect or CRT-like feel. I can do this with a CSS overlay or by drawing horizontal lines. Let me add a very subtle scanline effect using CSS.

Actually, for the CRT feel, I can add:
1. A slight vignette (dark edges)
2. Subtle scanlines (horizontal lines with low opacity)
3. Maybe a slight curvature effect (hard to do with CSS)

Let me keep it simple: just the vignette and scanlines via CSS overlay.

Or I can skip the CRT effect and keep it clean. The vector graphics style is already very retro.

Let me go with a clean look but add a subtle glow to the ship and bullets.

OK, FINAL CODE:

```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; }
html, body { 
  width: 100%; height: 100%; 
  overflow: hidden; 
  background: #000;
  font-family: monospace;
}
canvas {
  display: block;
  width: 100%;
  height: 100%;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
// ... game code ...
</script>
</body>
</html>
```

Now let me write the full game. I'll be thorough.

Actually, I realize I should also add keyboard controls for mobile? No, the user didn't ask for mobile. Desktop controls only.

Controls:
- Left/Right arrow or A/D: Rotate
- Up arrow or W: Thrust
- Space: Shoot
- P: Pause (optional)

Let me skip pause for simplicity.

OK, writing the complete game now. Here goes:

Let me think about one more design choice: should the ship have a maximum speed? In the classic game, there's no max speed, but the friction naturally limits it. I'll keep friction based (no max speed cap).

Also, should I add "drift" - when you stop thrusting, the ship continues moving in the direction it was going. Yes, that's the inertia-based physics I'm already planning.

OK, here's my final complete code. Let me write it all out:

I realize I should also think about the asteroid vertices more carefully. When I create an asteroid, I generate random vertices. But when I draw it, I need to rotate those vertices by the asteroid's current rotation. So the vertices are stored as angles and radii, and I compute the actual positions on the fly.

Actually, it's easier to store the vertices as local coordinates (x, y) and then rotate them during drawing:

```javascript
function createAsteroid(x, y, size, vx, vy) {
  const radius = { 3: 45, 2: 25, 1: 13 }[size];
  const verts = [];
  const n = 10 + Math.floor(Math.random() * 6);
  for (let i = 0; i < n; i++) {
    const a = (i / n) * Math.PI * 2;
    const r = radius * (0.75 + Math.random() * 0.25);
    verts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r });
  }
  return { x, y, vx, vy, size, radius, rotation: 0, rotSpeed: (Math.random()-0.5)*0.02, verts };
}
```

And drawing:
```javascript
function drawAsteroid(a) {
  ctx.save();
  ctx.translate(a.x, a.y);
  ctx.rotate(a.rotation);
  ctx.beginPath();
  for (let i = 0; i < a.verts.length; i++) {
    const v = a.verts[i];
    if (i === 0) ctx.moveTo(v.x, v.y);
    else ctx.lineTo(v.x, v.y);
  }
  ctx.closePath();
  ctx.strokeStyle = '#aaa';
  ctx.lineWidth = 1.5;
  ctx.stroke();
  ctx.restore();
}
```

This is cleaner. The vertices are pre-computed in local space, and I just rotate the entire coordinate system.

For the collision radius, I'll use the nominal radius (the one I used to generate the vertices), which is close enough.

OK, let me write the complete final code now. I'll aim for about 300-400 lines of JavaScript.

Let me also add a few more visual touches:
1. When an asteroid is destroyed, draw a brief flash/explosion
2. Thrust particles from the ship

For the explosion, I can create a simple particle burst:

```javascript
let explosions = [];

function createExplosion(x, y, color, count) {
  for (let i = 0; i < count; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = Math.random() * 3 + 1;
    explosions.push({
      x, y,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      life: 20 + Math.random() * 15,
      maxLife: 35,
      size: Math.random() * 2 + 1,
      color
    });
  }
}
```

And draw them as fading lines or circles.

Actually, for Asteroids, the classic destruction is just the asteroid disappearing. No explosion particles in the original. But adding them makes it feel better. Let me add a subtle effect.

OK, let me write the complete final code now. I'll stop planning and just write it.

Let me also consider: should I draw the ship with a filled area or just lines? Classic Asteroids uses lines only (wireframe). I'll use lines only.

For the bullets, I'll draw them as small bright dots (filled circles).

For the stars, I'll draw tiny dots.

OK here's my complete code. Writing it now for real:

I want to make sure the game feels good to play. Key feel factors:
1. Responsive controls (no input lag)
2. Satisfying shooting (bullets fly out, hit asteroids)
3. Good collision feedback (explosions, score popups)
4. Ship feels weighty but responsive

For score popups, I can show "+20", "+50", "+100" floating text when an asteroid is destroyed. This adds satisfying feedback.

Let me add that:
```javascript
let scorePopups = [];

function addScorePopup(x, y, points) {
  scorePopups.push({
    x, y,
    text: '+' + points,
    life: 40,
    vy: -1.5
  });
}
```

Draw them as floating text that fades out.

OK, I'll include this. Let me write the complete code.

Actually, let me also think about the game flow:
1. Start screen: "ASTEROIDS" title, "Press SPACE to start"
2. Game starts: ship in center, asteroids spawn, player controls ship
3. Player shoots asteroids, they split, score increases
4. When all asteroids are destroyed, new wave spawns (with more asteroids)
5. If ship hits an asteroid, lose a life, ship respawns invincible
6. When all lives are lost, game over screen
7. Press SPACE to restart

This is clean and classic. Let me implement it.

For the start screen, I'll draw:
- Title "ASTEROIDS" in large text
- A small rotating ship animation
- "Press SPACE to start" text
- Controls reminder

For the game over screen:
- "GAME OVER" in large text
- Final score
- "Press SPACE to restart"

OK, writing the complete code now. For real this time.

Let me also handle the case where the game is in 'start' state - the ship should be drawn as an animation on the start screen (rotating slowly).

I'll create a separate "demo ship" that rotates on the start screen.

Alright, here's my final complete code:

Wait, I realize I should also think about the canvas sizing. I'll make it fill the window and handle resize. The game world coordinates will be the canvas dimensions.

For the stars, when the canvas resizes, I should regenerate them or at least make sure they're positioned within the new bounds.

OK, let me write the code now. I'll handle resize by regenerating stars and checking if the ship should be clamped.

Here's my final approach to resize:
- On resize, update canvas dimensions
- Regenerate stars
- If ship exists, clamp it to the new bounds (or wrap it)

OK, writing now.

Let me think about one more thing: the ship's initial position. It should be at the center of the screen. When the game starts (or restarts after death), the ship should be at the center with zero velocity.

For the invincibility after respawn, I'll make the ship blink for 2-3 seconds (120-180 frames at 60fps).

OK, truly writing the code now:

Here's the structure:
1. HTML + CSS (minimal)
2. JavaScript:
   a. Canvas setup + resize
   b. Constants
   c. Game state variables
   d. Star generation
   e. Ship creation
   f. Asteroid creation
   g. Bullet management
   h. Particle system (thrust + explosions)
   i. Score popups
   j. Input handling
   k. Update functions (ship, bullets, asteroids, collisions, particles, popups)
   l. Draw functions (stars, ship, asteroids, bullets, particles, popups, UI, screens)
   m. Game loop
   n. Initialization

Let me write it all.

I'm going to write a comprehensive, polished Asteroids game. Here goes:

For the explosion effect when an asteroid is destroyed, I'll draw a brief expanding ring or some particles. Let me keep it simple with particles.

For the asteroid destruction, I'll create 8-12 particles that fly outward from the asteroid's position, with the asteroid's color, fading out over ~30 frames.

For the ship destruction (when losing a life), I'll create a larger burst of particles.

OK, writing the code now. This will be the final version.

Let me also add a "level" indicator. When a new wave spawns, show "WAVE 2", "WAVE 3", etc. briefly on screen.

Actually, in classic Asteroids, there's no wave indicator - the asteroids just appear. But showing the wave number is a nice modern touch. Let me add it subtly.

OK, I'll add a brief "WAVE X" text that appears when new asteroids spawn.

Alright, HERE IS THE FINAL CODE:

Actually, I just realized I should also clamp the ship's velocity to prevent it from going too fast. In the original game, there's effectively no clamp, but for gameplay purposes, I might want to add one. Let me not clamp it - the friction will naturally limit it.

But wait, if the ship gets hit by an asteroid, it shouldn't bounce. In the original, the ship just dies. I'll keep that.

OK, writing the code:

Let me also add a feature where, when you're invincible, the ship draws with a slight ghost/blink effect. I already planned this.

One more thing: the classic Asteroids game has the ship's controls reversed for rotation (right rotates clockwise, left rotates counter-clockwise). Wait, no - in the original, left rotates the ship counter-clockwise (which is left) and right rotates clockwise (which is right). This is standard.

In my implementation:
- Left arrow / A: Rotate counter-clockwise (angle decreases)
- Right arrow / D: Rotate clockwise (angle increases)
- Up arrow / W: Thrust in the direction the ship is facing

This is correct.

OK, final code. Writing now. No more changes.

Actually, for the thrust direction: the ship's angle points in the direction the nose is facing. When you thrust, the ship accelerates in that direction. So:

```
ship.vx += Math.cos(ship.angle) * thrust_acceleration;
ship.vy += Math.sin(ship.angle) * thrust_acceleration;
```

And the ship is drawn with the nose pointing in the direction of `ship.angle`. So if angle is 0, the ship points right. If angle is -PI/2, the ship points up.

In my initial creation, I set angle to -PI/2 so the ship points up (which is the natural "forward" direction on screen).

OK, this is all correct. Let me write the code.

I'm going to write a really polished version. Let me structure the code clearly:

```javascript
// ============================================
// ASTEROIDS - Vector Arcade Game
// ============================================

// --- Canvas Setup ---
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

function resize() {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
window.addEventListener('resize', () => { resize(); initStars(); });
resize();

// --- Constants ---
const SHIP = {
  size: 15,
  thrust: 0.12,
  friction: 0.985,
  turnSpeed: 0.065,
  collisionRadius: 9
};

const BULLET = {
  speed: 7,
  life: 55,
  maxCount: 12,
  cooldown: 10,
  radius: 2
};

const ASTEROID = {
  speeds: { 3: 45, 2: 25, 1: 13 },
  scores: { 3: 20, 2: 50, 1: 100 },
  baseCount: 5,
  speedBase: 1.2,
  speedPerLevel: 0.15,
  rotSpeedRange: 0.02
};

// --- State ---
let state = 'start';
let score = 0;
let highScore = 0;
let lives = 3;
let level = 1;
let ship = null;
let asteroids = [];
let bullets = [];
let particles = [];
let scorePopups = [];
let stars = [];
let invincible = 0;
let shootCooldown = 0;
let frameCount = 0;
let waveTimer = 0;
let shake = 0;
let keys = {};
let gameTime = 0;

// ... etc
```

OK let me just write the whole thing in one go. I'll be comprehensive but not over-engineered.

Here's my complete implementation:

Actually, I want to keep the code clean and well-organized. Let me write it section by section.

I'll write the complete HTML file now. Let me be very thorough and make sure everything works.

Let me also add a subtle "glow" effect to the ship and bullets using shadowBlur:

```javascript
ctx.shadowBlur = 8;
ctx.shadowColor = '#0ff';
```

This adds a nice glow to vector graphics. I'll use it selectively.

But shadowBlur can be expensive. Let me use it only for the ship and bullets, not for every asteroid.

OK, writing the complete code now. Final version. No more planning.

Let me also think about the ship drawing one more time. I want it to look really good.

The ship (pointing right, centered at origin):
```
     ___
    /   \
   /     \
  *       *    <- wing tips at (-12, ±9)
   \     /
    \   /      <- notch
     \ /       
      *        <- notch point at (-5, 0)
```

Wait, I already decided on the 6-point shape. Let me go with that:

```javascript
const shipVerts = [
  [15, 0],     // nose
  [0, -10],    // left wing
  [-8, -5],    // left notch
  [-4, 0],     // notch bottom
  [-8, 5],     // right notch
  [0, 10]      // right wing
];
```

Wait, I need to be careful. In my coordinate system, positive y is down (canvas convention). So "up" is negative y.

If the ship is pointing up (angle = -PI/2), then:
- Nose is at (0, -15) in world space
- The ship is drawn relative to its center, rotated by the angle

When I draw the ship, I translate to the ship's position and rotate by the angle. So the ship vertices are in "local" coordinates where the nose points to the right (positive x).

So my vertices are correct:
- Nose: (15, 0) - points right in local space
- When rotated by -PI/2, the nose points up in world space

And the vertices trace the outline:
1. (15, 0) - nose (right)
2. (0, -10) - upper-left wing
3. (-8, -5) - left notch (upper)
4. (-4, 0) - notch bottom
5. (-8, 5) - right notch (lower)
6. (0, 10) - lower-right wing

Going from 1→2→3→4→5→6→back to 1 traces the outline clockwise (in local space where y is down).

Wait, let me trace this more carefully:
- Start at nose (15, 0)
- Go to (0, -10): this is up and to the left
- Go to (-8, -5): this is left and slightly down
- Go to (-4, 0): this is right and slightly down
- Go to (-8, 5): this is left and down
- Go to (0, 10): this is right and down
- Back to (15, 0): this is right and up

This traces a shape that looks like:
```
    *
   / \
  /   \
 *     *    (wing tips)
  \   /
   \ /
    *        (notch)
```

Wait, that's not right. Let me plot the points:

Point 1: (15, 0) - far right
Point 2: (0, -10) - top center
Point 3: (-8, -5) - left, slightly below top
Point 4: (-4, 0) - left, center
Point 5: (-8, 5) - left, slightly below center
Point 6: (0, 10) - bottom center

So the shape is:
- Right point (nose)
- Top-left wing
- Left notch (upper part)
- Left notch (lower part)  
- Bottom-left wing
- Back to nose

Wait, I'm confusing myself. Let me just trace the outline:

Starting at nose (15, 0), going to (0, -10): this is the upper edge of the ship.
From (0, -10) to (-8, -5): this is the upper-left rear edge.
From (-8, -5) to (-4, 0): this is the right side of the notch (going inward).
From (-4, 0) to (-8, 5): this is the left side of the notch (going outward).
From (-8, 5) to (0, 10): this is the lower-left rear edge.
From (0, 10) back to (15, 0): this is the lower edge of the ship.

So the shape looks like:

```
      N (15,0)
     / \
    /   \
   /     \
  W2(0,-10)
  |       |
  *(-8,-5)*(-8,5)
   \     /
    \   /
     \ /
     N4(-4,0)
```

Hmm, that doesn't look right. Let me re-think.

Actually, the points in order are:
1. (15, 0) - nose
2. (0, -10) - top-rear (left wing tip)
3. (-8, -5) - left notch outer
4. (-4, 0) - notch inner (bottom of notch)
5. (-8, 5) - right notch outer
6. (0, 10) - bottom-rear (right wing tip)

So the outline goes:
- Nose → top-rear (upper edge)
- Top-rear → left notch outer (left upper rear edge)
- Left notch outer → notch bottom (right side of notch, going inward)
- Notch bottom → right notch outer (left side of notch, going outward)
- Right notch outer → bottom-rear (left lower rear edge)
- Bottom-rear → nose (lower edge)

This creates a shape like:

```
        ★ (nose, right)
       / \
      /   \
     /     \
    *       *    (wing tips, top and bottom)
    |  \  /  |
    |   \/   |
    |   /\   |
    |  /  \  |
    *       *    (notch points)
     \     /
      \   /
       \ /
        *        (notch bottom)
```

Wait no. Let me draw this more carefully.

Actually, I think the issue is that my points don't form a nice-looking ship. Let me try a different set of vertices.

Looking at actual screenshots of Asteroids, the ship looks like this (pointing right):

```
  ___
 /   \
|     |
 \___/
   |
```

No, that's not right either. Let me look at this from a different angle.

The classic Asteroids ship, when pointing to the right, looks approximately like:

```
     /\
    /  \
   /    \
  /      \
 /        \
*          *
 \        /
  \      /
   \    /
    \  /
     \/
```

Where:
- The top part is the upper hull
- The bottom part is the lower hull
- The point at the right is the nose
- The two points at the left are the wing tips
- The V-shape at the left is the notch

So the vertices (pointing right, centered at origin) would be:

1. Nose: (15, 0)
2. Upper wing: (-8, -9)
3. Upper notch: (-3, -3)
4. Lower notch: (-3, 3)
5. Lower wing: (-8, 9)

This is a 5-point shape. The notch is formed by the V between points 3 and 4.

Let me trace the outline:
1→2: upper edge (nose to upper wing)
2→3: upper rear edge (upper wing to upper notch)
3→4: notch (upper notch to lower notch) - this is the V of the notch
4→5: lower rear edge (lower notch to lower wing)
5→1: lower edge (lower wing to nose)

This looks like:

```
      ★ (nose)
     / \
    /   \
   /     \
  *       *    (wing tips)
   \     /
    \   /      (notch)
     \ /
      *        (notch point)
```

Wait, the notch is formed by the line from (-3,-3) to (-3,3), which is a vertical line at x=-3. That's not a V-notch, it's just a flat back with a notch cut into it.

Hmm, actually in the classic game, the notch is a V-shape cut into the back of the ship. The ship outline goes:

- Nose (right point)
- Upper wing tip (upper-left)
- Upper notch point (left, but closer to center than wing tip)
- Lower notch point (left, same x as upper notch, lower y)
- Lower wing tip (lower-left)
- Back to nose

The notch is the indentation between the wing tips. The "V" of the notch points inward (to the right).

So with my 5-point shape:
- The line from upper wing (-8,-9) to upper notch (-3,-3) goes inward (to the right)
- The line from upper notch (-3,-3) to lower notch (-3,3) goes straight down (the bottom of the notch)
- The line from lower notch (-3,3) to lower wing (-8,9) goes outward (to the left)

This creates a notch that looks like:

```
     /\
    /  \
   /    \
  *------*------*    (wing tip - notch - wing tip)
   \    /
    \  /
     \/
```

The notch is the flat part between the two wing tips, indented to the right.

Actually, I think the issue is that the classic ship has a more pronounced V-notch. Let me try:

1. Nose: (15, 0)
2. Upper wing: (-12, -8)
3. Upper notch: (-5, -1)
4. Lower notch: (-5, 1)
5. Lower wing: (-12, 8)

This creates a deeper notch (the notch points are at x=-5, while wing tips are at x=-12).

Let me trace:
- Nose (15,0) → upper wing (-12,-8): long upper edge
- Upper wing (-12,-8) → upper notch (-5,-1): goes right and down (inward)
- Upper notch (-5,-1) → lower notch (-5,1): short vertical line (bottom of notch)
- Lower notch (-5,1) → lower wing (-12,8): goes left and down (outward)
- Lower wing (-12,8) → nose (15,0): long lower edge

This looks better! The notch is a V-shape cut into the back, with the point of the V at (-5, 0) approximately.

Actually, the notch bottom should be a single point, not a line. Let me use 6 points:

1. Nose: (15, 0)
2. Upper wing: (-12, -8)
3. Upper notch: (-5, -2)
4. Notch bottom: (-3, 0)
5. Lower notch: (-5, 2)
6. Lower wing: (-12, 8)

This creates a more defined V-notch with a bottom point.

OK, I think this looks good. Let me use this.

For the thrust flame, it should come out of the notch area:
- The flame starts near the notch bottom (-3, 0) and extends to the left
- It's a triangular shape that flickers

```javascript
// Thrust flame (when thrusting)
const flameLen = 8 + Math.random() * 6;
ctx.beginPath();
ctx.moveTo(-3, -2);
ctx.lineTo(-3 - flameLen, 0);
ctx.lineTo(-3, 2);
ctx.closePath();
```

This creates a triangle pointing left from the notch.

But I want the flame to be more dynamic. Let me make it a bit wider and with some randomness:

```javascript
const flameLen = 8 + Math.random() * 8;
const flameW = 3 + Math.random() * 2;
ctx.beginPath();
ctx.moveTo(-3, -flameW);
ctx.lineTo(-3 - flameLen, 0);
ctx.lineTo(-3, flameW);
ctx.closePath();
```

This is a triangle that flickers in size.

For the flame color, I'll use a gradient from white (tip) to orange/yellow (base). But since we're doing vector graphics, I'll just use a bright color like #ff8 or #ffa.

Actually, for the classic vector look, I'll draw the flame in the same cyan color as the ship, or in white. Let me use white for the flame.

Or better: I'll draw the flame in a bright color (like #0ff or #fff) and make it flicker.

OK, let me finalize the ship drawing code:

```javascript
function drawShip() {
  if (!ship) return;
  
  ctx.save();
  ctx.translate(ship.x, ship.y);
  ctx.rotate(ship.angle);
  
  // Blink when invincible
  if (invincible > 0 && (frameCount % 6 < 3)) {
    ctx.restore();
    return;
  }
  
  // Ship body
  ctx.beginPath();
  ctx.moveTo(15, 0);
  ctx.lineTo(-12, -8);
  ctx.lineTo(-5, -2);
  ctx.lineTo(-3, 0);
  ctx.lineTo(-5, 2);
  ctx.lineTo(-12, 8);
  ctx.closePath();
  
  ctx.strokeStyle = '#0ff';
  ctx.lineWidth = 1.5;
  ctx.shadowBlur = 6;
  ctx.shadowColor = '#0ff';
  ctx.stroke();
  ctx.shadowBlur = 0;
  
  // Thrust flame
  if (ship.thrusting) {
    const flameLen = 8 + Math.random() * 8;
    const flameW = 3 + Math.random() * 2;
    ctx.beginPath();
    ctx.moveTo(-3, -flameW);
    ctx.lineTo(-3 - flameLen, 0);
    ctx.lineTo(-3, flameW);
    ctx.closePath();
    ctx.strokeStyle = '#ff8';
    ctx.shadowBlur = 8;
    ctx.shadowColor = '#ff8';
    ctx.stroke();
    ctx.shadowBlur = 0;
  }
  
  ctx.restore();
}
```

This looks good. Let me also add a small "dot" at the nose for visual interest. Actually, no - classic Asteroids doesn't have that. Keep it clean.

For the asteroid drawing, I'll draw the outline and optionally add some inner detail:

```javascript
function drawAsteroid(a) {
  ctx.save();
  ctx.translate(a.x, a.y);
  ctx.rotate(a.rotation);
  
  // Outline
  ctx.beginPath();
  for (let i = 0; i < a.verts.length; i++) {
    const v = a.verts[i];
    if (i === 0) ctx.moveTo(v.x, v.y);
    else ctx.lineTo(v.x, v.y);
  }
  ctx.closePath();
  ctx.strokeStyle = a.color || '#aaa';
  ctx.lineWidth = 1.5;
  ctx.stroke();
  
  ctx.restore();
}
```

I'll give each asteroid a slightly different shade of gray for visual variety. I can do this by setting the color when creating the asteroid.

OK, I think I have everything planned out. Let me write the complete code now.

One more thing: I should add a subtle background grid or something to give the space feel. Actually, stars are enough. Let me also add a very subtle nebula effect? No, that's too much. Just stars.

For the stars, I want them to look good:
- Random positions
- Random sizes (0.5 to 2 pixels)
- Random brightness
- Slight twinkling (sine wave animation)

```javascript
function initStars() {
  stars = [];
  for (let i = 0; i < 250; i++) {
    stars.push({
      x: Math.random() * canvas.width,
      y: Math.random() * canvas.height,
      size: Math.random() * 1.8 + 0.3,
      phase: Math.random() * Math.PI * 2,
      speed: 0.01 + Math.random() * 0.03,
      brightness: 0.3 + Math.random() * 0.7
    });
  }
}

function drawStars() {
  for (const s of stars) {
    const twinkle = 0.6 + 0.4 * Math.sin(frameCount * s.speed + s.phase);
    const alpha = s.brightness * twinkle;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = '#fff';
    ctx.fillRect(s.x, s.y, s.size, s.size);
  }
  ctx.globalAlpha = 1;
}
```

OK, this is good. Let me finalize and write the complete code.

I'm going to write the complete HTML file now. It will be comprehensive, polished, and fully functional.

Let me think about the overall structure one more time:

```
HTML:
  - canvas element
  - style tag (full page, dark background)

CSS:
  - Full viewport
  - Dark background
  - Canvas fills viewport
  - No scrollbars

JavaScript:
  1. Canvas setup + resize handler
  2. Constants (ship, bullet, asteroid parameters)
  3. Game state variables
  4. Star field initialization and drawing
  5. Ship creation, update, drawing
  6. Asteroid creation, update, drawing
  7. Bullet creation, update, drawing
  8. Particle system (thrust particles, explosion particles)
  9. Score popup system
  10. Collision detection
  11. Input handling (keyboard)
  12. Game state management (start, playing, gameover)
  13. UI drawing (score, lives, level)
  14. Start screen drawing
  15. Game over screen drawing
  16. Main game loop (update + draw)
  17. Initialization
```

This is comprehensive. Let me write it all.

I want to also add a subtle screen shake when the ship is destroyed. And maybe a flash effect.

For screen shake:
```javascript
let shakeAmount = 0;

function addShake(amount) {
  shakeAmount = Math.max(shakeAmount, amount);
}

// In draw:
if (shakeAmount > 0) {
  const sx = (Math.random() - 0.5) * shakeAmount;
  const sy = (Math.random() - 0.5) * shakeAmount;
  ctx.translate(sx, sy);
  shakeAmount *= 0.9;
  if (shakeAmount < 0.1) shakeAmount = 0;
}
```

But I need to be careful: if I translate the context for shake, I need to make sure the shake is applied at the right point in the drawing order. I'll apply it right after clearing the canvas.

Actually, a simpler approach: apply the shake offset to all drawing coordinates. But that's tedious. Let me just use ctx.translate at the beginning of the draw function (after clearing).

But wait, if I translate the context, then the stars, ship, asteroids, etc. all get shifted. That's exactly what I want for screen shake.

The issue is that the shake should decay over time. I'll update the shake amount in the update function and apply it in the draw function.

OK, let me finalize the code. Here goes:

Actually, I realize I should also handle the case where the ship dies and there's a brief pause before respawning. In the classic game, there's a 2-3 second delay before the ship reappears. I'll implement this with the invincible timer.

When the ship is hit:
1. lives--
2. ship is reset to center with zero velocity
3. invincible = 180 (3 seconds at 60fps)
4. ship blinks during invincibility
5. If lives == 0, game over

This is clean and simple.

One more thing: in the classic game, when the ship respawns, the asteroids that are near the center get pushed away. I'll implement a simple version: when the ship respawns, asteroids within a certain radius get a velocity boost away from the center.

Actually, let me skip this for simplicity. The invincibility period gives the player enough time to get clear.

OK, truly writing the code now. Here's the complete implementation:

Let me also add a nice touch: when you clear a wave, show "WAVE X" text briefly, and then spawn the next wave with more asteroids.

For the wave system:
- Level 1: 4 asteroids, speed base 1.0
- Level 2: 5 asteroids, speed base 1.1
- Level 3: 6 asteroids, speed base 1.2
- ...
- Level N: min(4 + N - 1, 12) asteroids, speed base 1.0 + (N-1) * 0.1

When all asteroids are destroyed:
1. level++
2. Show "WAVE X" text for 120 frames
3. Spawn new asteroids
4. Ship gets a brief invincibility? No, just the wave transition.

Actually, I'll add a brief "level complete" state where the screen flashes and new asteroids spawn after a short delay.

Let me implement this as:
- When asteroids.length === 0 and state === 'playing':
  - level++
  - waveTimer = 90 (1.5 seconds)
  - state = 'waveComplete'
- During 'waveComplete':
  - Show "WAVE X" text
  - After waveTimer expires:
    - Spawn new asteroids
    - state = 'playing'

This is clean.

OK, let me write the final complete code. I'll aim for a polished, complete, and fun game.

Here's the code:

Actually, I just want to double-check one thing: the bullet-asteroid collision. I'm using circle-based collision:

```javascript
if (Math.hypot(b.x - a.x, b.y - a.y) < a.radius) {
  // Hit!
}
```

This is fine. The bullet is a point (or small circle) and the asteroid is a circle with radius `a.radius`.

For ship-asteroid collision:
```javascript
if (Math.hypot(ship.x - a.x, ship.y - a.y) < a.radius + SHIP_COLLISION_RADIUS) {
  // Hit!
}
```

This is also fine.

OK, writing the complete code now. Final version.

I'm going to make the game really polished. Here are some additional visual touches:
1. Ship has a subtle glow (shadowBlur)
2. Bullets have a glow
3. Asteroids have slightly different shades
4. Thrust flame flickers
5. Particles from thrust and explosions
6. Score popups float up and fade
7. Screen shake on ship death
8. Stars twinkle
9. Invincibility blinking
10. Wave transition text

Let me write it all now.

For the score popup, I'll draw text that floats upward and fades:

```javascript
function addScorePopup(x, y, points) {
  scorePopups.push({
    x, y,
    text: '+' + points,
    life: 50,
    maxLife: 50,
    vy: -1.2
  });
}

function updateScorePopups() {
  for (let i = scorePopups.length - 1; i >= 0; i--) {
    const p = scorePopups[i];
    p.y += p.vy;
    p.life--;
    if (p.life <= 0) scorePopups.splice(i, 1);
  }
}

function drawScorePopups() {
  for (const p of scorePopups) {
    const alpha = p.life / p.maxLife;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = '#fff';
    ctx.font = '14px monospace';
    ctx.textAlign = 'center';
    ctx.fillText(p.text, p.x, p.y);
  }
  ctx.globalAlpha = 1;
}
```

OK, this is all good. Let me write the complete code now. I'll be thorough and make sure everything works.

Here's my final complete implementation. Writing it all in one go:

For the thrust particles, I'll emit them continuously while the ship is thrusting:

```javascript
function updateThrustParticles() {
  if (ship && ship.thrusting && Math.random() < 0.7) {
    const backX = ship.x - Math.cos(ship.angle) * 8;
    const backY = ship.y - Math.sin(ship.angle) * 8;
    particles.push({
      x: backX + (Math.random() - 0.5) * 3,
      y: backY + (Math.random() - 0.5) * 3,
      vx: -Math.cos(ship.angle) * (2 + Math.random() * 3) + (Math.random() - 0.5) * 0.5,
      vy: -Math.sin(ship.angle) * (2 + Math.random() * 3) + (Math.random() - 0.5) * 0.5,
      life: 15 + Math.random() * 10,
      maxLife: 25,
      size: Math.random() * 2.5 + 0.5,
      color: Math.random() < 0.5 ? '#ff8' : '#f80'
    });
  }
}

function updateParticles() {
  for (let i = particles.length - 1; i >= 0; i--) {
    const p = particles[i];
    p.x += p.vx;
    p.y += p.vy;
    p.vx *= 0.96;
    p.vy *= 0.96;
    p.life--;
    if (p.life <= 0) particles.splice(i, 1);
  }
}

function drawParticles() {
  for (const p of particles) {
    const alpha = p.life / p.maxLife;
    ctx.globalAlpha = alpha;
    ctx.fillStyle = p.color;
    ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
  }
  ctx.globalAlpha = 1;
}
```

For explosion particles when an asteroid is destroyed:

```javascript
function createExplosion(x, y, color, count) {
  for (let i = 0; i < count; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = Math.random() * 4 + 1;
    particles.push({
      x, y,
      vx: Math.cos(angle) * speed,
      vy:
write /tmp/ws/index.html (23808 bytes)
toolResult

Successfully wrote 23808 bytes to /tmp/ws/index.html

assistant
thinking
The user wants me to build a playable Asteroids game. Let me create a complete, well-polished single-file implementation with all the classic mechanics.

`index.html` written — open it in a browser to play. Here's what's in it: **Controls** - `←` / `→` or `A`/`D` — rotate the ship - `↑` or `W` — thrust (ships drifts with inertia, friction slows it) - `Space` — fire bullets - `Space`/`Enter` on start & game-over screens — begin/restart **Gameplay** - Shoot the 3-size asteroids; larger ones split into two smaller ones when hit, awarding 20/50/100 points - Ship wraps around screen edges, asteroids & bullets wrap too - 3 lives; brief invincibility after a hit with screen shake - **Combo system**: consecutive rapid asteroid kills build a combo multiplier (visible in the UI) - New wave of increasing difficulty unlocks when you clear the screen - High score persists in `localStorage` **Visuals (all procedural vector)** - Glowing cyan wireframe ship with a flickering thrust flame and spawned exhaust particles - Hatched iridescent asteroids with randomized vertex count - White bullets with glowing trails - Particle explosions in ship & asteroid colors - Floating score popups (+20/50/100, plus combo bonuses) - Twinkling field-stars background - Blinking HUD prompts on the title & game-over screens

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

modelsolar-pro4
vendorUpstage
harnesspi-agent
generated2026-08-13

EVALUATION

4/5 working

Menu screen transitions into a playable vector Asteroids game with ship, score, lives, and asteroid field on keyboard input.

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.2%
on click0.1%
on drag0.2%
on wheel0.2%
on enter + space2.8%
on arrow keys1.9%
on w/a/s/d2.1%
frame spread9.1 / 255
console errors0
js errors none

METRICS

runtime625.2s
tokens in11k
tokens out25k
cached0
cost / run$0.0033