oneshotlm
Compare all models on this prompt →

Asteroids · kimi-k2.7-code

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 · 10 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
 We need create index.html in /workspace, self-contained, no external resources, playable Asteroids game: ship with thrust/rotate, shooting, edge wrapping, asteroids split into smaller pieces, score and lives. Use procedural vector graphics. Need ensure all JS/CSS inline. Need produce final answer concise. Also need maybe read current directory. We should use tools to write file. Let's implement fully. Need consider canvas sizing full window, responsive. Use vector graphics: ship (triangle), bullets (small), asteroids (irregular polygons). Game states: playing, game over. Key controls: arrows + space. Physics with acceleration, friction, max speed. Asteroids: big splits into 2 medium, medium into 2 small; sizes and speed vary. Collision detection: ship-asteroid, bullet-asteroid (circle radius). Screen wrap for all. Lives display as ships icons. Score. Level progression maybe after clearing asteroids spawn more. Particles for thrust/explosions. Sounds? Not required, no external assets; can synthesise with Web Audio optional. Fine to add simple beep. Need ensure self-contained.

We must not create separate files. Use write. Also maybe check current directory existence with ls. Build robust code. Use requestAnimationFrame.

Plan file structure:
- HTML base with viewport meta.
- CSS: body margin0 overflow hidden black #111 full canvas; UI overlay top with score/lives; game over overlay centered; hints.
- Canvas behind.
- JS at end. Constants. Ship object: x,y,r,angle,vx,vy,rotation,thrusting,invulnerable timer, dead flag. Bullets array with lifetime. Asteroids array. Particles. Score, lives, level, gameOver.
- Input map, listeners.
- Functions:
  - createAsteroid(x,y,size): random shape vertices radius with variation, random velocity approx size speed; rotation spin; polygon local vertices.
  - drawAsteroid: save, translate, rotate, beginPath, lineTo vertices, closePath, stroke. wrap.
  - update.
  - drawShip: with slight engine flicker; if invulnerable blink; if dead hidden.
  - drawPolygon? For asteroids can precompute vertices.
  - splitAsteroid(index): remove asteroid, add particles, if size>min then spawn 2 asteroids of smaller size (big->medium, medium->small). Score by size: big 20, med 50, small 100.
  - resetShip: respawn center with invulnerability; if no lives game over.
  - createParticles.
  - spawnAsteroids(level) count = level+3? Start with 4 or level + 4. They must not overlap ship spawn area, set positions away from center.
- Main loop clear -> update -> draw.
- Collision detection:
  - bullet vs asteroid: check distance between bullet and asteroid center <= asteroid.radius (approx) maybe bullet hit radius small.
  - ship vs asteroid: if invulnerable skip, if distance <= asteroid.radius+ship radius.
- UI update.
- Game over overlay with restart press Enter/R.
Need ensure  keys: ArrowLeft / A, ArrowRight / D, Up / W, Space shoot, R restart.

Edge wrap algorithm: if x < -r move to width+r etc. Or simpler if x < 0 set x=width and y? For asteroids radius large. Use -radius to width+radius threshold.

Asteroid polygon: get random vertices around circle. Use 8-14 vertices; radius_i = baseRadius + random(-variance, variance). Store vertices in array {x,y} relative to center. Draw via beginPath and lineTo. Maybe fill? No fill, vector outlines.

Ship drawing: base triangle pointing up by default; rotate by angle. Coordinates: nose (0,-r), left (-r*0.7, r), right (r*0.7,r); or scale. Engine flame when thrust: small triangle behind.

Particles: small dots with velocity and life, fade.

UI layout: absolute top bar. CSS text white monospace. Game over overlay hidden.

Responsive: resize canvas to window; after resizing maybe reposition objects inside? OK if they adjust; just update width/height. Could keep ship position clipped. We'll reassign canvas dimensions on resize.

Audio: optional. But to avoid browser autoplay issues, simple audio for shoot/explosion using AudioContext. Could be OK. We can include but might require user interaction. Fine.

Need ensure code fits. Let's draft writing.

Consider lives drawing with mini ships in UI maybe too complex but nice.

Potential issue: bullet wrapping: ship can shoot; bullets wrap. lifetime ~ 700ms? Use frames ~ 40.

Asteroid splitting should preserve smaller size and faster speed._spawnAsteroid smaller: size / 2. For collision radius uses size * base.

Implementation details in code:

```javascript
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let w, h;
function resize(){
  w = canvas.width = window.innerWidth;
  h = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
```

Ship object:
```
let ship = newShip();
function newShip(){
 return {x:w/2,y:h/2,r:12,angle:-Math.PI/2,vx:0,vy:0,rotation:0,thrusting:false,invulnerable:180,dead:false};
}
```
But w/h after resize. Could create after page load.

Constants:
```
const TURN_SPEED = 0.07;
const THRUST = 0.15;
const FRICTION = 0.99;
const MAX_SPEED = 7;
const BULLET_SPEED = 10;
const LEVEL_START_ASTEROIDS = 4;
```

Asteroid class maybe:
```
function Asteroid(x,y,size){
 const angle = Math.random()*Math.PI*2;
 const speed = (Math.random()*1 + 0.5)*(30/size); // bigger slower
 this.x=x; this.y=y; this.vx=Math.cos(angle)*speed; ...
 this.angle=0; this.spin=(Math.random()-0.5)*0.04;
 this.size=size; this.radius=size*0.6*?; // draw radius: size * 1.0.
 this.vertices=[];
 const num=8+Math.floor(Math.random()*6);
 for(...){
   const a = i/num*Math.PI*2;
   const r = size * (0.8 + Math.random()*0.4);
   this.vertices.push({x:Math.cos(a)*r, y:Math.sin(a)*r});
 }
}
```
Need speed not too high. Use normalized ~ 0.5-1.5. Small asteroids faster maybe.

Collisions radius use size (since vertices up to size). Good.

Splitting:
```
function split(i){
 const a = asteroids[i];
 explode(a.x,a.y,a.size);
 score += (a.size>40?20:(a.size>20?50:100));
 asteroids.splice(i,1);
 if(a.size>20){
   for(let k=0;k<2;k++){
     asteroids.push(new Asteroid(a.x,a.y,a.size/2));
   }
 }
}
```
Level progression: in update if asteroids.length === 0 and !gameOver, level++, spawnAsteroids(level).
Spawn asteroid avoid center:
```
function spawnAsteroids(level){
 const count= LEVEL_START_ASTEROIDS+level;
 for(let i=0;i<count;i++){
   let x,y;
   do {
     x=Math.random()*w; y=Math.random()*h;
   } while(dist(x,y,w/2,h/2)<150);
   asteroids.push(new Asteroid(x,y,60)); // large
 }
}
```

Game start:
```
let score=0, lives=3, level=0, gameOver=false;
spawnAsteroids(0);
loop();
```

Ship update physics:
```
if (ship.rotation) ship.angle += ship.rotation * TURN_SPEED;
if (ship.thrusting){
 ship.vx += Math.cos(ship.angle)*THRUST;
 ship.vy += Math.sin(ship.angle)*THRUST;
 createThrustParticles();
}
ship.vx*=FRICTION; ship.vy*=FRICTION;
// clamp
const speed=Math.hypot(ship.vx,ship.vy); if(speed>MAX_SPEED) {...}
ship.x += ship.vx; ship.y += ship.vy;
wrapObject(ship, ship.r);
if(ship.invulnerable>0) ship.invulnerable--;
```

Wrap function generic.

Explosion particles createThrust; explosion from asteroid.

Key handlers: keydown set keys, if gameOver and Enter/Space/R start. Use preventDefault for arrows/space to avoid scroll.

Drawing UI: elements by id update innerText.

Lives display: maybe draw small ship icons in top bar. We'll create span with id lives and string of '▸' repeated? Better canvas-like? Use text display "Lives: 3". Simpler. Score + Lives.

Round and render.

Game Over UI overlay: show div, final score, button or key to restart.

Collision detection distance.

Potential bugs: ship angle default -PI/2 (up). Canvas y-axis down; drawing triangle pointing up. Good.

Wrap threshold: For object with radius r, if x < -r -> x = w + r; if x > w+r -> -r; similarly y. Some asteroids might have edge clipped due to radius. Fine.

Game loop with `requestAnimationFrame`. Use `performance.now()` or frames.

Need avoid external fonts? Use system-ui, monospace.

Should include HTML5 audio via Web Audio oscillator? optional; no assets. Maybe include to enhance. But keep simple.

Let's write final file. 

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

Successfully wrote 13041 bytes to /workspace/index.html

assistant
thinking
  We have written index.html. Need maybe verify file works mentally. Let's check critical issues. The file is self-contained. Need maybe run a quick lint or open with node? No browser. We can read quickly to verify syntactically and ensure first run. Also could run a simple node parse using `node -c`? But HTML can't. Can extract script and use node to check syntax? Could try sed but use bash. We can use `node --check` on a temporary JS extracted. But not necessary. Let's run a quick check maybe. Need be concise in final. However might want to test by launching headless? No. But maybe check with python http server? Not needed.

Let's review code for mistakes.

Important: KEY checks: restartGame function uses arrow/space to restart in keydown. It sets `if ((k === ' ' || k === 'Enter' || k === 'R' ...) && gameOver) restartGame();`. Good.

Game over overlay uses pointer-events none but not interactive. Fine.

Screen wrapping: bullets `wrap(b,2)`. Good.

Fire bullet: shotTimer logic. In update, if !gameOver updateShip; bullets update unaffected by gameOver? In Asteroids after game over maybe keep animating but no new ship. Bullets continue to update. OK.

Invulnerability on new ship after death resets to 180 frames. Good.

Starfield uses deterministic positions with mod W/H; if W/H small? fine.

Audio: tone uses gain.gain.exponentialRampToValueAtTime(0.001,...). Initial volume set at currentTime. Good.

Explode uses `for (let i=0; i<amount; i++)` with particles color f80. Good.

ThrustParticles pushes one particle per frame. Might be few but OK.

Asteroid split: score uses size >35 => big 20, >20 => medium 50, else small 100. Initial size 60, splits to 30, then 15. Condition >35 big, >20 medium. Wait if size 30 then >20 true so score 50. But actually medium size 30 should be medium 50, OK. size 15 => small 100. Good. Only split if a.size > 30 -> for 60 yes -> 30s, then 30 not >30? a.size > 30 is false for exactly 30, so 30 not split -> small? We want big=60 splits to medium=30, medium=30 splits to small=15. Condition should be a.size > 35? Let's adjust: Use > 20 maybe too strict? Initial 60 >20 => split into 30. 30 >20 => split into 15. 15 not split. Score: for size 60 => >35 => 20 (big). For 30 => (not >35) and (>20) => 50 (medium). For 15 => 100 (small). That works. Good.

Condition for split: if (a.size > 30) with size 30 false -> bug. Need > 20 to allow 30 split. Use > 20. Current uses >30; 30 not split. But if we want big->medium->small, then medium=30 should split. So change split threshold to `if (a.size > 25)`. But score threshold okay. Let's adjust to `if (a.size > 25) { ... a.size/2 ... }` or `a.size > 20`. To avoid tiny <=20 splitting, use 25. Let's verify common implementations. Use `if (a.size > 30)` and make smaller size 25. But currently 60 / 2 = 30. So need `a.size > 30` becomes true only >30 not >=. With 30 false. Better use `a.size > 25` and ensure small size around 15. Let's edit to `if (a.size > 25)`. Also score threshold maybe change to `a.size > 35` big, `a.size > 18` medium. Fine.

Also spawnAsteroids size constant 60. Good.

Ship collision radius uses ship.r * 0.6. Good.

Dead ship if hit, lives decremented and then setTimeout resets ship. But update loop still updates asteroids/particles and bullets, but no ship. Fine.

Game over restarting resets bullets and particles. Good.

Potential issue: shooting while game over? In keydown, if space and gameOver -> restartGame which resets, but also if !gameOver fireBullet. Need prevent double: `if (k === ' ')` then if gameOver restart else fire. Current code:
```
if ((k=== ... 'R') && gameOver) restartGame();
if (k === ' ' && !gameOver && !ship.dead) fireBullet();
```
After restartGame, ship.dead false, but `!gameOver && !ship.dead` also true for same keydown? Wait after restartGame, gameOver false; then second if `k === ' ' && !gameOver && !ship.dead` true; it will fire bullet immediately on restart. That might be okay but could be undesired. Better restructure to else. But not critical. Could lead to bullet each restart. Maybe adjust.

Also cheat: when dead, ship.dead true, cannot fire.

Need consider mobile/touch missing but user didn't request.

Need ensure 'R' restart check lower-case r too. We included 'r' or 'R'.

Potential CSS text-align center for hud-block uses inline style.

Need maybe run syntax check by extracting script. Let's do quick with bash.

bash {"command":"cd /workspace && sed -n '/<script>/,/<\\/script>/p' index.html | sed '1d;$d' > /tmp/asteroids.js && node --check /tmp/asteroids.
toolResult

syntax OK

assistant
thinking
  Syntax OK. Need edit split threshold and double fire issue. Use edit tool. Let's make two minimal edits: 1. change split condition from `if (a.size > 30)` to `if (a.size > 25)`. 2. In keydown handler, avoid firing after restart on space. Better rewrite the block.

Current keydown listener:
```
    window.addEventListener('keydown', (e) => {
      const k = e.key;
      if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(k)) e.preventDefault();
      KEYS[k] = true;
      if ((k === ' ' || k === 'Enter' || k === 'r' || k === 'R') && gameOver) restartGame();
      if (k === ' ' && !gameOver && !ship.dead) fireBullet();
      if (audio && audio.state === 'suspended') audio.resume();
    });
```
We can change to:
```
      const k = e.key;
      if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(k)) e.preventDefault();
      KEYS[k] = true;
      if (gameOver && (k === ' ' || k === 'Enter' || k === 'r' || k === 'R')) {
        restartGame();
      } else if (k === ' ' && !ship.dead) {
        fireBullet();
      }
      if (audio && audio.state === 'suspended') audio.resume();
```
Note: when gameOver true and k space, first condition restarts and doesn't fire. Fine.

Split edit line:
```
      if (a.size > 30) {
```
to 25. 

edit {"path":"/workspace/index.html","edits":[{"oldText":" window.addEventListener('keydown', (e) => {\n const k = e.key;\n if (['Ar
toolResult

Successfully replaced 2 block(s) in /workspace/index.html.

assistant
thinking
   We should verify syntax again. 

bash {"command":"cd /workspace && sed -n '/<script>/,/<\\/script>/p' index.html | sed '1d;$d' > /tmp/asteroids.js && node --check /tmp/asteroids.
toolResult

syntax OK

assistant
thinking
   Good. Need final response concise: mention created file, controls, features. No need to overexplain. Provide file path. Done. 

Created `/workspace/index.html` — a single-file, self-contained Asteroids game. **Controls** - **Rotate:** `← / →` or `A / D` - **Thrust:** `↑` or `W` - **Shoot:** `Space` - **Restart:** `R` / `Enter` (after Game Over) **Features** - Procedural vector ship with thrust flame/flicker - Screen wrap for ship, bullets, and asteroids - Asteroids split into smaller pieces when shot (big → medium → small) - Score, lives, level counter, and game-over overlay - Particle explosions and synthesized sound effects - No external assets, CDNs, or network requests — runs fully offline Open `/workspace/index.html` in a browser to play.

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

modelkimi-k2.7-code
vendorMoonshot · Kimi
harnesspi-agent
generated2026-07-13

EVALUATION

2/5 partial

Ship stays static, unrotated, and never fires despite input; only asteroids drift, no visible thrust or shooting response.

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 motion2.0%
on click1.7%
on drag2.0%
on wheel2.4%
on enter + space2.6%
on arrow keys2.7%
on w/a/s/d3.0%
frame spread7.1 / 255
console errors0
js errors none

METRICS

runtime311.5s
tokens in9.1k
tokens out8.8k
cached29k
cost / run$0.0417