
Build Asteroids with Phaser.js and AI: Inertia Physics, Screen Wrapping, and Wave Systems
Asteroids is the game that taught an entire generation about momentum. Your ship doesn’t stop when you let go of the controls — it drifts. That single design choice creates tension, skill expression, and a feeling of piloting something physical through hostile space.
This tutorial walks through building a complete Asteroids game with DeepSeek V4 Flash, covering the three mechanics that define the genre: inertia physics, screen wrapping, and procedural asteroid splitting.
Built by DeepSeek V4 Flash — won a 4-judge arena match with an 8.73/10 average scoreControls: Rotate with ←/→ or A/D, thrust with ↑/W, fire with Space, hyperspace with Shift/H. On mobile, use the on-screen buttons.
Why Asteroids Is the Perfect 2000s Build
On our decade roadmap, Asteroids occupies the 2000s slot — the era when vector graphics and arcade physics reached their refined peak. The game teaches five transferable skills:
| Skill | What You Learn |
|---|---|
| Momentum physics | Velocity accumulates, drag decays it — frame-rate independent |
| Screen wrapping | Objects teleport across edges instead of bouncing |
| Object splitting | Destroying one entity spawns smaller entities |
| Collision callbacks | Phaser’s overlap with conditional process callbacks |
| Wave progression | Difficulty scales across rounds |
The AI Prompt That Built the Core
The winning build came from a structured prompt that specified six things: controls, physics behavior, asteroid lifecycle, screen wrapping, scoring, and mobile support. Here’s the core instruction:
“Build a complete Asteroids game in Phaser.js 3.80. Triangular spaceship with rotation (Arrow/A-D), thrust with inertia (Arrow Up/W), fire bullets (Space), and hyperspace teleport (Shift/H). Asteroids spawn at edges, wrap around all 4 screen edges. Bullets destroy asteroids: large splits into 2 medium, medium into 2 small, small destroyed. Score: large=20, medium=50, small=100. 3 lives with invulnerability after hit. Waves escalate with more asteroids each round. Use Phaser.Scale.FIT with CENTER_BOTH. Add touch controls for mobile.”
One pass produced 609 lines of clean, working game code.
Code Walkthrough
1. Texture Generation Without Assets
Every sprite is drawn procedurally with Phaser’s Graphics API — no image downloads needed. This keeps the game in a single self-contained file:
generateTextures() {
const shipG = this.make.graphics({ add: false });
shipG.fillStyle(COLORS.ship);
shipG.beginPath();
shipG.moveTo(SHIP_RADIUS, 0);
shipG.lineTo(-SHIP_RADIUS * 0.7, -SHIP_RADIUS * 0.55);
shipG.lineTo(-SHIP_RADIUS * 0.3, 0);
shipG.lineTo(-SHIP_RADIUS * 0.7, SHIP_RADIUS * 0.55);
shipG.closePath();
shipG.fillPath();
shipG.generateTexture('ship', SHIP_RADIUS * 2 + 4, SHIP_RADIUS * 2 + 4);
shipG.destroy();
}
The triangle is defined by four points relative to the ship’s center. The notch at the back (the -SHIP_RADIUS * 0.3 point) gives it that classic arrow shape.
2. Inertia Physics — The Heart of Asteroids
The ship uses Phaser’s Arcade Physics with damping enabled. Thrust adds velocity in the direction the ship faces; drag gradually slows it down:
createShip() {
this.ship = this.physics.add.sprite(W / 2, H / 2, 'ship');
this.ship.body.setCircle(SHIP_RADIUS, -SHIP_RADIUS, -SHIP_RADIUS);
this.ship.body.setMaxSpeed(MAX_SPEED);
this.ship.body.setDamping(true);
}
In the update() loop, thrust applies force along the ship’s rotation angle:
if (this.keys.up.isDown || this.keys.w.isDown || this.touchState.thrust) {
const angle = this.ship.rotation - Math.PI / 2;
this.ship.body.setVelocity(
this.ship.body.velocity.x + Math.cos(angle) * THRUST_FORCE * dt,
this.ship.body.velocity.y + Math.sin(angle) * THRUST_FORCE * dt
);
}
The -Math.PI / 2 offset accounts for the ship sprite pointing “up” (negative Y) at rotation 0. Each frame, the thrust force is scaled by delta time (dt), making the physics frame-rate independent — the game feels identical at 60fps and 144fps.
3. Screen Wrapping
Objects don’t bounce off walls — they teleport to the opposite edge. This creates the iconic “infinite space” feel:
function wrapSprite(sprite, margin) {
if (!sprite || !sprite.body) return;
const m = margin || 0;
const bw = W + m * 2, bh = H + m * 2;
if (sprite.x < -m) sprite.x += bw;
else if (sprite.x > W + m) sprite.x -= bw;
if (sprite.y < -m) sprite.y += bh;
else if (sprite.y > H + m) sprite.y -= bh;
}
Called every frame for the ship, all asteroids, and all bullets. The margin allows sprites to partially exit before wrapping, avoiding visual pop-in at edges.
4. Asteroid Splitting — The Core Loop
When a bullet hits an asteroid, the collision callback splits it into smaller pieces. Each asteroid kind knows what it becomes:
const ASTEROID_KINDS = [
{ name: 'large', radius: 48, score: 20, splitInto: 'medium', vertices: 10 },
{ name: 'medium', radius: 26, score: 50, splitInto: 'small', vertices: 8 },
{ name: 'small', radius: 13, score: 100, splitInto: null, vertices: 6 }
];
bulletHitAsteroid(bullet, asteroid) {
bullet.destroy();
const kind = ASTEROID_KINDS.find(k => k.name === asteroid.kind);
this.score += kind.score;
if (kind.splitInto) {
for (let i = 0; i < 2; i++) {
const angle = Math.random() * Math.PI * 2;
this.spawnAsteroid(
asteroid.x, asteroid.y, kind.splitInto,
Math.cos(angle) * 80, Math.sin(angle) * 80
);
}
}
this.spawnExplosion(asteroid.x, asteroid.y);
asteroid.destroy();
}
The two children fly off in random directions at a fixed speed — simple but effective. Smaller asteroids are worth more points (100 vs 20), rewarding precision.
5. Collision With Conditional Callbacks
The ship-asteroid collision uses Phaser’s process callback — a function that runs before the collision handler and can veto it. This is how invulnerability works:
this.physics.add.overlap(
this.ship, this.asteroids,
this.shipHitAsteroid,
() => !this.ship.invulnerable && this.ship.visible,
this
);
The fourth argument is the gate: the collision only fires when the ship isn’t invulnerable and is visible. During the respawn blink animation, this prevents multiple asteroids from draining all three lives at once.
6. Wave Progression
Each wave spawns more asteroids, and they move faster:
spawnWave() {
this.waveNum++;
const count = INITIAL_WAVE_COUNT + (this.waveNum - 1) * WAVES_INCREMENT;
const maxSpeed = 80 + this.waveNum * 8;
// ...spawn `count` asteroids with speed up to `maxSpeed`
}
Wave 1 has 4 asteroids at max 88px/s. Wave 5 has 12 asteroids at max 120px/s. The scaling keeps the challenge fresh without overwhelming early players.
7. Mobile Touch Controls
Five on-screen buttons handle all inputs. Each button sets a flag in a touchState object that the update loop reads alongside keyboard input:
const makeBtn = (label, x, y, w, h, cb) => {
const bg = this.add.rectangle(x, y, w, h, 0xffffff, 0.35)
.setInteractive({ useHandCursor: false });
bg.on('pointerdown', () => { cb(true); });
bg.on('pointerup', () => { cb(false); });
bg.on('pointerout', () => { cb(false); });
};
The pointerout handler ensures that if a player drags their finger off a button, the action stops — preventing stuck thrust or stuck rotation.
The Arena Competition
This game was built as part of a multi-model arena match. Two AI models built the same Asteroids game independently:
| Model | Lines | Avg Judge Score |
|---|---|---|
| DeepSeek V4 Flash (winner) | 609 | 8.73 |
| MiMo 2.5 Pro | 722 | 8.17 |
Four judges evaluated both builds on code correctness, mechanics, visual design, mobile responsiveness, fun factor, and completeness. DeepSeek won with cleaner Phaser Arcade Physics integration and a robust two-scene architecture (GameScene + GameOverScene).
What You Learned
- Inertia physics —
setDamping(true)plus delta-time thrust creates momentum that feels right - Screen wrapping — teleporting objects across edges instead of bouncing them
- Object splitting — the large→medium→small chain that makes Asteroids endlessly replayable
- Conditional collision callbacks — Phaser’s process callback for invulnerability frames
- Wave scaling — linear difficulty curves that keep the game challenging
- Procedural textures —
make.graphics()+generateTexture()for asset-free sprites
Try It Yourself
The game above is fully playable — give it a shot. Start with small bursts of thrust, rotate to aim, and use hyperspace when surrounded. Clear a few waves to feel the difficulty ramp.
Ready to build your own variant? Try prompting an AI for these twists:
- UFO enemies that hunt the player across the screen
- Power-ups dropped by destroyed asteroids (rapid fire, shields, extra life)
- Asteroid fields with gravity wells that bend trajectories
- Co-op mode with two ships and shared lives
The full source is in the game file — right-click the iframe and view frame source to explore the code.