Created
July 30, 2025 00:54
-
-
Save pmarreck/db782fdb68053292ca746d6c7569e2ac to your computer and use it in GitHub Desktop.
The very first output of the prompt "Write a self-contained HTML, CSS and Javascript page implementing Asteroids" sent to the new model GLM-4.5-Air-q5-hi-mlx running on an M4 128GB. Runs great!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Asteroids Game</title> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
background: #000; | |
color: #fff; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
min-height: 100vh; | |
overflow: hidden; | |
} | |
.game-container { | |
position: relative; | |
border: 2px solid #0ff; | |
box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); | |
} | |
#gameCanvas { | |
background: radial-gradient(ellipse at center, #001122 0%, #000000 100%); | |
display: block; | |
} | |
.ui-overlay { | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
padding: 20px; | |
display: flex; | |
justify-content: space-between; | |
pointer-events: none; | |
} | |
.score, .lives { | |
font-size: 20px; | |
text-shadow: 0 0 10px rgba(0, 255, 255, 0.8); | |
} | |
.game-over-screen { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
text-align: center; | |
display: none; | |
background: rgba(0, 0, 0, 0.9); | |
padding: 40px; | |
border-radius: 10px; | |
border: 2px solid #0ff; | |
box-shadow: 0 0 30px rgba(0, 255, 255, 0.5); | |
} | |
.game-over-screen h2 { | |
font-size: 48px; | |
margin-bottom: 20px; | |
color: #0ff; | |
text-shadow: 0 0 20px rgba(0, 255, 255, 0.8); | |
} | |
.game-over-screen p { | |
font-size: 24px; | |
margin-bottom: 30px; | |
} | |
.restart-btn { | |
background: linear-gradient(45deg, #00ffff, #0088ff); | |
border: none; | |
color: #000; | |
padding: 15px 40px; | |
font-size: 20px; | |
font-weight: bold; | |
border-radius: 5px; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
} | |
.restart-btn:hover { | |
transform: scale(1.1); | |
box-shadow: 0 0 20px rgba(0, 255, 255, 0.8); | |
} | |
.controls { | |
position: absolute; | |
bottom: -60px; | |
left: 50%; | |
transform: translateX(-50%); | |
text-align: center; | |
font-size: 14px; | |
color: #666; | |
} | |
.controls span { | |
margin: 0 10px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="game-container"> | |
<canvas id="gameCanvas" width="800" height="600"></canvas> | |
<div class="ui-overlay"> | |
<div class="score">Score: <span id="scoreValue">0</span></div> | |
<div class="lives">Lives: <span id="livesValue">3</span></div> | |
</div> | |
<div class="game-over-screen" id="gameOverScreen"> | |
<h2>GAME OVER</h2> | |
<p>Final Score: <span id="finalScore">0</span></p> | |
<button class="restart-btn" onclick="game.restart()">PLAY AGAIN</button> | |
</div> | |
<div class="controls"> | |
<span>↑ - Thrust</span> | |
<span>← → - Rotate</span> | |
<span>SPACE - Shoot</span> | |
</div> | |
</div> | |
<script> | |
const canvas = document.getElementById('gameCanvas'); | |
const ctx = canvas.getContext('2d'); | |
// Game constants | |
const SHIP_SIZE = 15; | |
const BULLET_SPEED = 10; | |
const ASTEROID_MIN_SIZE = 20; | |
const ASTEROID_MAX_SIZE = 50; | |
const MAX_SPEED = 7; | |
// Particle system | |
class Particle { | |
constructor(x, y, vx, vy, life = 30) { | |
this.x = x; | |
this.y = y; | |
this.vx = vx + (Math.random() - 0.5) * 2; | |
this.vy = vy + (Math.random() - 0.5) * 2; | |
this.life = life; | |
this.maxLife = life; | |
} | |
update() { | |
this.x += this.vx; | |
this.y += this.vy; | |
this.life--; | |
} | |
draw() { | |
const alpha = this.life / this.maxLife; | |
ctx.save(); | |
ctx.globalAlpha = alpha; | |
ctx.fillStyle = '#ffaa00'; | |
ctx.fillRect(this.x, this.y, 2, 2); | |
ctx.restore(); | |
} | |
} | |
// Ship class | |
class Ship { | |
constructor(x, y) { | |
this.x = x; | |
this.y = y; | |
this.vx = 0; | |
this.vy = 0; | |
this.angle = -Math.PI / 2; // Start pointing up | |
this.radius = SHIP_SIZE; | |
} | |
rotate(direction) { | |
const rotationSpeed = 0.1; | |
this.angle += direction * rotationSpeed; | |
} | |
thrust() { | |
const acceleration = 0.2; | |
this.vx += Math.cos(this.angle) * acceleration; | |
this.vy += Math.sin(this.angle) * acceleration; | |
// Add thrust particles | |
for (let i = 0; i < 3; i++) { | |
const spread = Math.random() * 0.5; | |
particles.push(new Particle( | |
this.x - Math.cos(this.angle) * SHIP_SIZE, | |
this.y - Math.sin(this.angle) * SHIP_SIZE, | |
-this.vx / 2 - Math.cos(this.angle + spread) * 3, | |
-this.vy / 2 - Math.sin(this.angle + spread) * 3, | |
20 | |
)); | |
} | |
} | |
update() { | |
// Apply friction | |
this.vx *= 0.99; | |
this.vy *= 0.99; | |
// Limit speed | |
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy); | |
if (speed > MAX_SPEED) { | |
this.vx = (this.vx / speed) * MAX_SPEED; | |
this.vy = (this.vy / speed) * MAX_SPEED; | |
} | |
// Update position | |
this.x += this.vx; | |
this.y += this.vy; | |
// Wrap around screen | |
if (this.x < 0) this.x = canvas.width; | |
if (this.x > canvas.width) this.x = 0; | |
if (this.y < 0) this.y = canvas.height; | |
if (this.y > canvas.height) this.y = 0; | |
} | |
draw() { | |
ctx.save(); | |
ctx.translate(this.x, this.y); | |
ctx.rotate(this.angle); | |
// Draw ship | |
ctx.strokeStyle = '#0ff'; | |
ctx.lineWidth = 2; | |
ctx.beginPath(); | |
ctx.moveTo(SHIP_SIZE, 0); | |
ctx.lineTo(-SHIP_SIZE * 0.7, -SHIP_SIZE * 0.6); | |
ctx.lineTo(-SHIP_SIZE * 0.5, 0); | |
ctx.lineTo(-SHIP_SIZE * 0.7, SHIP_SIZE * 0.6); | |
ctx.closePath(); | |
ctx.stroke(); | |
// Draw glow effect | |
ctx.shadowBlur = 10; | |
ctx.shadowColor = '#0ff'; | |
ctx.stroke(); | |
ctx.restore(); | |
} | |
getVertices() { | |
const cos = Math.cos(this.angle); | |
const sin = Math.sin(this.angle); | |
return [ | |
{ x: this.x + SHIP_SIZE * cos, y: this.y + SHIP_SIZE * sin }, | |
{ x: this.x - SHIP_SIZE * 0.7 * cos - SHIP_SIZE * 0.6 * sin, | |
y: this.y - SHIP_SIZE * 0.7 * sin + SHIP_SIZE * 0.6 * cos }, | |
{ x: this.x - SHIP_SIZE * 0.5 * cos, y: this.y - SHIP_SIZE * 0.5 * sin }, | |
{ x: this.x - SHIP_SIZE * 0.7 * cos + SHIP_SIZE * 0.6 * sin, | |
y: this.y - SHIP_SIZE * 0.7 * sin - SHIP_SIZE * 0.6 * cos } | |
]; | |
} | |
} | |
// Asteroid class | |
class Asteroid { | |
constructor(x, y, size) { | |
this.x = x; | |
this.y = y; | |
this.size = size || ASTEROID_MAX_SIZE; | |
this.radius = this.size; | |
// Random velocity | |
const angle = Math.random() * Math.PI * 2; | |
const speed = (ASTEROID_MAX_SIZE - this.size) / ASTEROID_MAX_SIZE * 2 + 0.5; | |
this.vx = Math.cos(angle) * speed; | |
this.vy = Math.sin(angle) * speed; | |
// Random rotation | |
this.angle = 0; | |
this.rotationSpeed = (Math.random() - 0.5) * 0.05; | |
// Generate vertices | |
this.vertices = []; | |
const numVertices = Math.floor(Math.random() * 4) + 8; | |
for (let i = 0; i < numVertices; i++) { | |
const angle = (i / numVertices) * Math.PI * 2; | |
const variance = 0.9 + Math.random() * 0.2; | |
this.vertices.push({ | |
x: Math.cos(angle) * this.size * variance, | |
y: Math.sin(angle) * this.size * variance | |
}); | |
} | |
} | |
update() { | |
this.x += this.vx; | |
this.y += this.vy; | |
this.angle += this.rotationSpeed; | |
// Wrap around screen | |
if (this.x < -this.size) this.x = canvas.width + this.size; | |
if (this.x > canvas.width + this.size) this.x = -this.size; | |
if (this.y < -this.size) this.y = canvas.height + this.size; | |
if (this.y > canvas.height + this.size) this.y = -this.size; | |
} | |
draw() { | |
ctx.save(); | |
ctx.translate(this.x, this.y); | |
ctx.rotate(this.angle); | |
// Draw asteroid | |
ctx.strokeStyle = '#888'; | |
ctx.lineWidth = 2; | |
ctx.beginPath(); | |
for (let i = 0; i < this.vertices.length; i++) { | |
const vertex = this.vertices[i]; | |
if (i === 0) { | |
ctx.moveTo(vertex.x, vertex.y); | |
} else { | |
ctx.lineTo(vertex.x, vertex.y); | |
} | |
} | |
ctx.closePath(); | |
ctx.stroke(); | |
// Draw glow | |
ctx.shadowBlur = 5; | |
ctx.shadowColor = '#888'; | |
ctx.stroke(); | |
ctx.restore(); | |
} | |
split() { | |
if (this.size > ASTEROID_MIN_SIZE) { | |
const newSize = this.size / 2; | |
return [ | |
new Asteroid(this.x, this.y, newSize), | |
new Asteroid(this.x, this.y, newSize) | |
]; | |
} | |
return []; | |
} | |
getTransformedVertices() { | |
const cos = Math.cos(this.angle); | |
const sin = Math.sin(this.angle); | |
return this.vertices.map(vertex => ({ | |
x: this.x + vertex.x * cos - vertex.y * sin, | |
y: this.y + vertex.x * sin + vertex.y * cos | |
})); | |
} | |
} | |
// Bullet class | |
class Bullet { | |
constructor(x, y, angle) { | |
this.x = x; | |
this.y = y; | |
const speed = BULLET_SPEED; | |
this.vx = Math.cos(angle) * speed; | |
this.vy = Math.sin(angle) * speed; | |
this.life = 40; // Bullets last for 40 frames | |
} | |
update() { | |
this.x += this.vx; | |
this.y += this.vy; | |
this.life--; | |
// Wrap around screen | |
if (this.x < 0) this.x = canvas.width; | |
if (this.x > canvas.width) this.x = 0; | |
if (this.y < 0) this.y = canvas.height; | |
if (this.y > canvas.height) this.y = 0; | |
} | |
draw() { | |
ctx.save(); | |
ctx.fillStyle = '#ff0'; | |
ctx.shadowBlur = 5; | |
ctx.shadowColor = '#ff0'; | |
// Draw bullet as a small square | |
ctx.fillRect(this.x - 2, this.y - 2, 4, 4); | |
ctx.restore(); | |
} | |
} | |
// Game class | |
class AsteroidsGame { | |
constructor() { | |
this.ship = new Ship(canvas.width / 2, canvas.height / 2); | |
this.bullets = []; | |
this.asteroids = []; | |
this.particles = []; | |
this.score = 0; | |
this.lives = 3; | |
this.gameOver = false; | |
this.keys = {}; | |
// Initialize asteroids | |
this.spawnAsteroids(5); | |
// Set up event listeners | |
document.addEventListener('keydown', (e) => this.keys[e.key] = true); | |
document.addEventListener('keyup', (e) => this.keys[e.key] = false); | |
// Game loop | |
this.lastShotTime = 0; | |
this.gameLoop(); | |
} | |
spawnAsteroids(count) { | |
for (let i = 0; i < count; i++) { | |
let x, y; | |
// Ensure asteroids don't spawn too close to the ship | |
do { | |
x = Math.random() * canvas.width; | |
y = Math.random() * canvas.height; | |
} while (Math.sqrt((x - this.ship.x) ** 2 + (y - this.ship.y) ** 2) < 100); | |
const size = Math.random() * (ASTEROID_MAX_SIZE - ASTEROID_MIN_SIZE) + ASTEROID_MIN_SIZE; | |
this.asteroids.push(new Asteroid(x, y, size)); | |
} | |
} | |
handleInput() { | |
if (this.gameOver) return; | |
// Rotation | |
if (this.keys['ArrowLeft']) { | |
this.ship.rotate(-1); | |
} | |
if (this.keys['ArrowRight']) { | |
this.ship.rotate(1); | |
} | |
// Thrust | |
if (this.keys['ArrowUp']) { | |
this.ship.thrust(); | |
} | |
// Shooting | |
const now = Date.now(); | |
if (this.keys[' '] && now - this.lastShotTime > 150) { | |
const bullet = new Bullet( | |
this.ship.x + Math.cos(this.ship.angle) * SHIP_SIZE, | |
this.ship.y + Math.sin(this.ship.angle) * SHIP_SIZE, | |
this.ship.angle | |
); | |
this.bullets.push(bullet); | |
this.lastShotTime = now; | |
} | |
} | |
checkCollisions() { | |
// Bullet-asteroid collisions | |
for (let i = this.bullets.length - 1; i >= 0; i--) { | |
const bullet = this.bullets[i]; | |
for (let j = this.asteroids.length - 1; j >= 0; j--) { | |
const asteroid = this.asteroids[j]; | |
// Simple circular collision | |
const dx = bullet.x - asteroid.x; | |
const dy = bullet.y - asteroid.y; | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
if (distance < asteroid.radius) { | |
// Remove bullet | |
this.bullets.splice(i, 1); | |
// Add explosion particles | |
for (let k = 0; k < 10; k++) { | |
const speed = Math.random() * 3 + 1; | |
particles.push(new Particle( | |
asteroid.x, | |
asteroid.y, | |
Math.random() * speed - speed / 2, | |
Math.random() * speed - speed / 2, | |
30 | |
)); | |
} | |
// Split asteroid or remove it | |
const newAsteroids = asteroid.split(); | |
this.asteroids.splice(j, 1); | |
this.asteroids.push(...newAsteroids); | |
// Update score | |
let points = 20; | |
if (asteroid.size > ASTEROID_MIN_SIZE * 1.5) { | |
points = 10; | |
} else if (asteroid.size > ASTEROID_MIN_SIZE) { | |
points = 50; | |
} | |
this.score += points; | |
// Update UI | |
document.getElementById('scoreValue').textContent = this.score; | |
break; | |
} | |
} | |
} | |
// Ship-asteroid collisions | |
const shipX = this.ship.x; | |
const shipY = this.ship.y; | |
for (let i = 0; i < this.asteroids.length; i++) { | |
const asteroid = this.asteroids[i]; | |
const dx = shipX - asteroid.x; | |
const dy = shipY - asteroid.y; | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
if (distance < asteroid.radius + this.ship.radius) { | |
// Ship crashed | |
this.lives--; | |
// Add large explosion particles | |
for (let k = 0; k < 20; k++) { | |
const speed = Math.random() * 5 + 2; | |
particles.push(new Particle( | |
shipX, | |
shipY, | |
Math.random() * speed - speed / 2, | |
Math.random() * speed - speed / 2, | |
40 | |
)); | |
} | |
// Update UI | |
document.getElementById('livesValue').textContent = this.lives; | |
if (this.lives <= 0) { | |
this.endGame(); | |
} else { | |
// Reset ship position | |
this.ship.x = canvas.width / 2; | |
this.ship.y = canvas.height / 2; | |
this.ship.vx = 0; | |
this.ship.vy = 0; | |
} | |
break; | |
} | |
} | |
} | |
update() { | |
// Update game objects | |
this.ship.update(); | |
this.bullets = this.bullets.filter(bullet => { | |
bullet.update(); | |
return bullet.life > 0; | |
}); | |
this.asteroids.forEach(asteroid => asteroid.update()); | |
particles = particles.filter(particle => { | |
particle.update(); | |
return particle.life > 0; | |
}); | |
// Spawn new asteroids if needed | |
if (this.asteroids.length === 0) { | |
this.spawnAsteroids(5 + Math.floor(this.score / 500)); | |
} | |
} | |
draw() { | |
// Clear canvas | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Draw stars background | |
ctx.fillStyle = '#fff'; | |
for (let i = 0; i < 100; i++) { | |
const x = (i * 73) % canvas.width; | |
const y = (i * 37) % canvas.height; | |
const size = Math.sin(i) > 0.5 ? 1 : 2; | |
ctx.fillRect(x, y, size, size); | |
} | |
// Draw game objects | |
this.ship.draw(); | |
this.bullets.forEach(bullet => bullet.draw()); | |
this.asteroids.forEach(asteroid => asteroid.draw()); | |
particles.forEach(particle => particle.draw()); | |
} | |
endGame() { | |
this.gameOver = true; | |
document.getElementById('finalScore').textContent = this.score; | |
document.getElementById('gameOverScreen').style.display = 'block'; | |
} | |
restart() { | |
this.ship = new Ship(canvas.width / 2, canvas.height / 2); | |
this.bullets = []; | |
this.particles = particles.filter(p => p.life > 0); | |
this.asteroids = []; | |
this.score = 0; | |
this.lives = 3; | |
this.gameOver = false; | |
document.getElementById('scoreValue').textContent = '0'; | |
document.getElementById('livesValue').textContent = '3'; | |
document.getElementById('gameOverScreen').style.display = 'none'; | |
this.spawnAsteroids(5); | |
} | |
gameLoop() { | |
this.handleInput(); | |
if (!this.gameOver) { | |
this.update(); | |
this.checkCollisions(); | |
} | |
this.draw(); | |
requestAnimationFrame(() => this.gameLoop()); | |
} | |
} | |
// Initialize game | |
let particles = []; | |
const game = new AsteroidsGame(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment