oneshotlm
Compare all models on this prompt →

Asteroids · gemma-4-26b-a4b-it

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.

⚠ no runnable output

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
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 a single-file `index.html` containing a playable Asteroids game.
Key features:
- Canvas-based.
- Thrust-and-rotate vector ship.
- Shooting mechanism.
- Screen wrapping.
- Asteroids splitting into smaller pieces.
- Score and lives.
- Procedural vector graphics only (no external assets).
- Everything in one file (HTML, CSS, JS).
- No external resources/CDNs.

Plan:
1.  **HTML Structure**: A simple container for the canvas and a HUD (Heads-Up Display) for score and lives.
2.  **CSS**: Styling to make it look like a retro game (black background, white lines).
3.  **JavaScript**:
    *   `Game` class or main loop to manage state.
    *   `Ship` class: position, velocity, rotation, thrust, shooting.
    *   `Asteroid` class: position, velocity, size (large, medium, small), shape (randomized vertices for procedural look).
    *   `Bullet` class: position, velocity, life span.
    *   Collision detection: Circle-to-circle or simple distance checks.
    *   Game loop: `requestAnimationFrame`.
    *   Input handling: Keyboard (Arrows/WASD and Space).
    *   Screen wrapping: `x = (x + width) % width`.

Detailed design:
- **Ship**: Triangle with a small "thrust" flicker if thrusting.
- **Asteroids**: Irregular polygons. I'll generate them once per asteroid when created.
- **Bullets**: Small dots or short lines.
- **Physics**:
    *   Friction (slight) for the ship? Traditional Asteroids has almost no friction, just inertia. I'll stick to pure inertia.
    *   Wrapping: If an object's position goes off-screen, wrap it to the other side.
- **Game States**: Start screen, Playing, Game Over.

Let's start coding.

```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Asteroids - Vector Edition</title> <style> body { margin: 0; padding: 0; background-color: #000; color: #fff; font-family: 'Courier New', Courier, monospace; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; user-select: none; } canvas { border: 2px solid #fff; box-shadow: 0 0 20px rgba(255, 255, 255, 0.2); } #ui { position: absolute; top: 20px; left: 20px; pointer-events: none; } .stat { font-size: 24px; margin-bottom: 5px; } #overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; background: rgba(0, 0, 0, 0.8); padding: 40px; border: 1px solid #fff; } h1 { font-size: 48px; margin-bottom: 10px; } p { font-size: 18px; } .btn { display: inline-block; margin-top: 20px; padding: 10px 20px; border: 1px solid #fff; cursor: pointer; transition: background 0.3s; } .btn:hover { background: #fff; color: #000; } .hidden { display: none; } </style> </head> <body> <div id="ui"> <div class="stat">SCORE: <span id="score">0</span></div> <div class="stat">LIVES: <span id="lives">3</span></div> </div> <div id="overlay"> <h1 id="overlay-title">ASTEROIDS</h1> <p id="overlay-msg">USE ARROW KEYS TO MOVE<br>SPACE TO SHOOT</p> <div class="btn" id="start-btn">START GAME</div> </div> <canvas id="gameCanvas"></canvas> <script> /** @type {HTMLCanvasElement} */ const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const scoreEl = document.getElementById('score'); const livesEl = document.getElementById('lives'); const overlay = document.getElementById('overlay'); const overlayTitle = document.getElementById('overlay-title'); const overlayMsg = document.getElementById('overlay-msg'); const startBtn = document.getElementById('start-btn'); // Game Constants const FPS = 60; const SHIP_SIZE = 20; const SHIP_THRUST = 5; const SHIP_TURN_SPEED = 360; // Degrees per second const FRICTION = 0.7; // Coefficient of friction (0-1) const BULLET_SPEED = 500; const BULLET_MAX_DIST = 0.6; // Max distance bullet travels (fraction of screen) const ASTEROID_NUM = 5; const ASTEROID_SPEED = 50; const ASTEROID_SIZE = 100; // Starting size const ASTEROID_VERTICES = 10; const ASTEROID_JAGGEDNESS = 0.4; const INV_PI = Math.PI / 180; // Game State let score = 0; let lives = 3; let gameRunning = false; let ship; let asteroids = []; let bullets = []; let keys = {}; // Resize canvas function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } window.addEventListener('resize', resize); resize(); // Input Handling window.addEventListener('keydown', (e) => keys[e.code] = true); window.addEventListener('keyup', (e) => keys[e.code] = false); class Ship { constructor() { this.reset(); } reset() { this.x = canvas.width / 2; this.y = canvas.height / 2; this.r = SHIP_SIZE / 2; this.angle = 90 * INV_PI; // Facing Up this.rotation = 0; this.thrusting = false; this.thrust = { x: 0, y: 0 }; this.blinkTime = 30; // Invulnerability frames this.visible = true; } draw() { if (this.blinkTime > 0 && Math.floor(this.blinkTime / 5) % 2 === 0) { // Blinking effect for invulnerability } else { ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo( this.x + this.r * Math.cos(this.angle), this.y - this.r * Math.sin(this.angle) ); ctx.lineTo( this.x - this.r * (Math.cos(this.angle) + Math.sin(this.angle)), this.y + this.r * (Math.sin(this.angle) - Math.cos(this.angle)) ); ctx.lineTo( this.x - this.r * (Math.cos(this.angle) - Math.sin(this.angle)), this.y + this.r * (Math.sin(this.angle) + Math.cos(this.angle)) ); ctx.closePath(); ctx.stroke(); if (this.thrusting) { ctx.beginPath(); ctx.moveTo( this.x - this.r * (Math.cos(this.angle) + 0.5 * Math.sin(this.angle)), this.y + this.r * (Math.sin(this.angle) - 0.5 * Math.cos(this.angle)) ); ctx.lineTo( this.x - this.r * 1.5 * Math.cos(this.angle), this.y + this.r * 1.5 * Math.sin(this.angle) ); ctx.lineTo( this.x - this.r * (Math.cos(this.angle) - 0.5 * Math.sin(this.angle)), this.y + this.r * (Math.sin(this.angle) + 0.5 * Math.cos(this.angle)) ); ctx.stroke(); } } } update() { if (this.blinkTime > 0) this.blinkTime--; // Rotation if (keys['ArrowLeft'] || keys['KeyA']) this.angle += SHIP_TURN_SPEED * INV_PI / FPS; if (keys['ArrowRight'] || keys['KeyD']) this.angle -= SHIP_TURN_SPEED * INV_PI / FPS; // Thrust this.thrusting = keys['ArrowUp'] || keys['KeyW']; if (this.thrusting) { this.thrust.x += SHIP_THRUST * Math.cos(this.angle) / FPS; this.thrust.y -= SHIP_THRUST * Math.sin(this.angle) / FPS; } else { this.thrust.x *= FRICTION; this.thrust.y *= FRICTION; } this.x += this.thrust.x; this.y += this.thrust.y; // Wrap if (this.x < 0 - this.r) this.x = canvas.width + this.r; else if (this.x > canvas.width + this.r) this.x = 0 - this.r; if (this.y < 0 - this.r) this.y = canvas.height + this.r; else if (this.y > canvas.height + this.r) this.y = 0 - this.r; } shoot() { if (bullets.length < 10) { bullets.push(new Bullet( this.x + this.r * Math.cos(this.angle), this.y - this.r * Math.sin(this.angle), this.angle )); } } } class Bullet { constructor(x, y, angle) { this.x = x; this.y = y; this.vx = BULLET_SPEED * Math.cos(angle) / FPS; this.vy = -BULLET_SPEED * Math.sin(angle) / FPS; this.dist = 0; this.maxDist = canvas.width * BULLET_MAX_DIST; } update() { this.x += this.vx; this.y += this.vy; this.dist += Math.sqrt(this.vx**2 + this.vy**2); // Wrap if (this.x < 0) this.x = canvas.width; else if (this.x > canvas.width) this.x = 0; if (this.y < 0) this.y = canvas.height; else if (this.y > canvas.height) this.y = 0; } draw() { ctx.fillStyle = "white"; ctx.beginPath(); ctx.arc(this.x, this.y, 2, 0, Math.PI * 2); ctx.fill(); } } class Asteroid { constructor(x, y, radius, level) { this.x = x; this.y = y; this.r = radius; this.level = level; // 3: large, 2: med, 1: small this.vx = (Math.random() * ASTEROID_SPEED * 2 - ASTEROID_SPEED) / FPS; this.vy = (Math.random() * ASTEROID_SPEED * 2 - ASTEROID_SPEED) / FPS; this.vertices = Math.floor(Math.random() * (ASTEROID_VERTICES + 1) + ASTEROID_VERTICES / 2); this.offsets = []; for (let i = 0; i < this.vertices; i++) { this.offsets.push(Math.random() * ASTEROID_JAGGEDNESS * 2 + 1 - ASTEROID_JAGGEDNESS); } } draw() { ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo( this.x + this.r * this.offsets[0] * Math.cos(0), this.y + this.r * this.offsets[0] * Math.sin(0) ); for (let i = 1; i < this.vertices; i++) { ctx.lineTo( this.x + this.r * this.offsets[i] * Math.cos(i * Math.PI * 2 / this.vertices), this.y + this.r * this.offsets[i] * Math.sin(i * Math.PI * 2 / this.vertices) ); } ctx.closePath(); ctx.stroke(); } update() { this.x += this.vx; this.y += this.vy; // Wrap if (this.x < 0 - this.r) this.x = canvas.width + this.r; else if (this.x > canvas.width + this.r) this.x = 0 - this.r; if (this.y < 0 - this.r) this.y = canvas.height + this.r; else if (this.y > canvas.height + this.r) this.y = 0 - this.r; } } function initGame() { score = 0; lives = 3; asteroids = []; bullets = []; ship = new Ship(); scoreEl.innerText = score; livesEl.innerText = lives; for (let i = 0; i < ASTEROID_NUM; i++) { let x, y; do { x = Math.random() * canvas.width; y = Math.random() * canvas.height; } while (distBetweenPoints(ship.x, ship.y, x, y) < ASTEROID_SIZE * 2); asteroids.push(new Asteroid(x, y, ASTEROID_SIZE / 2, 3)); } } function distBetweenPoints(x1, y1, x2, y2) { return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); } function gameOver() { gameRunning = false; overlayTitle.innerText = "GAME OVER"; overlayMsg.innerText = `FINAL SCORE: ${score}`; startBtn.innerText = "RETRY"; overlay.classList.remove('hidden'); } function update() { if (!gameRunning) return; ship.update(); // Bullets for (let i = bullets.length - 1; i >= 0; i--) { bullets[i].update(); if (bullets[i].dist > bullets[i].maxDist) { bullets.splice(i, 1); } } // Asteroids for (let a of asteroids) { a.update(); } // Collision: Bullets vs Asteroids for (let i = asteroids.length - 1; i >= 0; i--) { let a = asteroids[i]; for (let j = bullets.length - 1; j >= 0; j--) { let b = bullets[j]; if (distBetweenPoints(a.x, a.y, b.x, b.y) < a.r) { // Hit! bullets.splice(j, 1); splitAsteroid(i); score += 100; scoreEl.innerText = score; break; } } } // Collision: Ship vs Asteroids if (ship.blinkTime === 0) { for (let a of asteroids) { if (distBetweenPoints(ship.x, ship.y, a.x, a.y) < ship.r + a.r) { handleShipDeath(); break; } } } // Win condition: all asteroids gone if (asteroids.length === 0) { // Start new level with more asteroids for (let i = 0; i < ASTEROID_NUM + 1; i++) { let x, y; do { x = Math.random() * canvas.width; y = Math.random() * canvas.height; } while (distBetweenPoints(ship.x, ship.y, x, y) < ASTEROID_SIZE * 2); asteroids.push(new Asteroid(x, y, ASTEROID_SIZE / 2, 3)); } } } function splitAsteroid(index) { let a = asteroids[index]; if (a.level > 1) { asteroids.push(new Asteroid(a.x, a.y, a.r / 2, a.level - 1)); asteroids.push(new Asteroid(a.x, a.y, a.r / 2, a.level - 1)); } asteroids.splice(index, 1); } function handleShipDeath() { lives--; livesEl.innerText = lives; if (lives <= 0) { gameOver(); } else { ship.reset(); } } function draw() { // Clear ctx.fillStyle = "black"; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw entities ship.draw(); for (let b of bullets) b.draw(); for (let a of asteroids) a.draw(); } function loop() { update(); draw(); requestAnimationFrame(loop); } // Shooting listener window.addEventListener('keydown', (e) => { if (e.code === 'Space' && gameRunning) { ship.shoot(); } }); startBtn.addEventListener('click', () => { initGame(); gameRunning = true; overlay.classList.add('hidden'); }); // Start the loop loop(); </script> </body> </html> ```

TAKE IT TO CODE

This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.

RUN

modelgemma-4-26b-a4b-it
vendorGoogle
harnesspi-agent
generated2026-07-22

METRICS

runtime
tokens in73
tokens out4.7k
cached1.3k
cost / run$0.0016