Space Invaders · gpt-5-mini
Build a playable Space Invaders game on a canvas: a player cannon that moves and shoots, a descending grid of aliens that advance and fire back, destructible bunkers, score, and lives. Procedural graphics only, no external assets.
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
Build a playable Space Invaders game on a canvas: a player cannon that moves and shoots, a descending grid of aliens that advance and fire back, destructible bunkers, score, and lives. Procedural graphics only, no external assets.
thinking
**Planning game mechanics** I’m considering adding on-screen buttons for player movement and shooting, but it's not strictly necessary. I want to keep everything simple in a single file without using external fonts. An overlay for instructions sounds useful. Key functions I need include drawing the player, aliens, bullets, and bunkers, plus updating positions and spawning aliens in a grid. The alien movement will rely on group bounds from alive aliens, and a random alien will shoot from the lowest column. I’ll also implement bunkers as an array of squares with health points. I need to ensure performance stays smooth.
I created a single-file, self-contained Space Invaders game as requested. Save the following content into index.html and open it in a modern browser. The game uses only procedural graphics (canvas drawing), no external assets or network requests. Features: - Player cannon that moves left/right and shoots - Descending grid of aliens that move as a group, speed up as you kill them - Aliens fire back with bullets - Destructible bunkers made of small blocks - Score, lives, level, and simple explosions - Keyboard controls and basic touch support - Start, pause, and game over screens Copy the file below to index.html and open it. ```html <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Space Invaders — Canvas</title> <style> :root{ --bg:#04060b; --panel:#091226; --accent:#8ef; --muted:#9aa7b8; --danger:#ff6b6b; } html,body{ height:100%; margin:0; font-family:Segoe UI,Roboto,Helvetica,Arial,sans-serif; background: radial-gradient(ellipse at top left, #071026 0%, #02040a 60%), var(--bg); color:#dfe8f2; -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; } #gameWrap{ display:flex; align-items:center; justify-content:center; height:100%; padding:20px; box-sizing:border-box; } canvas{ background: linear-gradient(180deg,#001220 0%, #001628 100%); border:6px solid #071226; border-radius:8px; box-shadow: 0 8px 30px rgba(1,6,12,0.7), inset 0 2px 8px rgba(255,255,255,0.02); image-rendering: pixelated; } #hud{ margin-left:18px; width:220px; color:var(--muted); } .panel{ background: linear-gradient(180deg,#061224 0%, #03101a 100%); border-radius:8px; padding:12px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.02); } h1{ margin:0 0 8px 0; font-size:18px; color:var(--accent); } .info{ font-size:13px; margin-bottom:8px; } .big{ font-weight:700; font-size:18px; color:#fff; margin-bottom:8px; } .kbd{ background:#02131f; padding:6px 8px; border-radius:6px; display:inline-block; margin-right:6px; color:var(--muted); } button{ background:linear-gradient(180deg,#0b2b44 0%,#063047 100%); color:var(--accent); border:0; padding:10px 12px; border-radius:8px; cursor:pointer; font-weight:600; } button.secondary{ background:transparent; border:1px solid rgba(255,255,255,0.04); color:var(--muted); } #controlsHint{ font-size:12px; color:var(--muted); margin-top:8px; } footer{ margin-top:12px; font-size:12px; color:rgba(255,255,255,0.06); } /* touch controls overlay */ #touchControls{ position:absolute; left:20px; bottom:20px; display:flex; gap:10px; user-select:none; touch-action: none; } .tc-btn{ width:64px; height:64px; border-radius:12px; background:rgba(255,255,255,0.03); display:flex; align-items:center; justify-content:center; color:var(--muted); font-weight:700; font-size:18px; } @media (max-width:900px){ #gameWrap{ padding:8px; } canvas{ width:100%; height:auto; max-width:720px; } #hud{ display:none; } #touchControls{ left:50%; transform:translateX(-50%); bottom:18px;} } </style> </head> <body> <div id="gameWrap"> <canvas id="canvas" width="640" height="640"></canvas> <div id="hud"> <div class="panel"> <h1>Space Invaders</h1> <div class="big" id="score">Score: 0</div> <div class="info" id="lives">Lives: 3</div> <div class="info" id="level">Level: 1</div> <div id="controlsHint"><span class="kbd">←</span><span class="kbd">→</span><span class="kbd">Space</span> Move / Shoot</div> <div style="height:10px"></div> <button id="startBtn">Start Game</button> <button class="secondary" id="pauseBtn" style="margin-left:8px">Pause</button> <footer>Procedural graphics — No external assets</footer> </div> </div> </div> <div id="touchControls" aria-hidden="true"> <div class="tc-btn" id="tc-left">◀</div> <div class="tc-btn" id="tc-fire">●</div> <div class="tc-btn" id="tc-right">▶</div> </div> <script> (() => { // Canvas setup const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d', { alpha: false }); const W = canvas.width, H = canvas.height; // HUD elements const scoreEl = document.getElementById('score'); const livesEl = document.getElementById('lives'); const levelEl = document.getElementById('level'); const startBtn = document.getElementById('startBtn'); const pauseBtn = document.getElementById('pauseBtn'); // Game state let keys = {}; let pointers = {}; let lastTime = 0; let accumulator = 0; let running = false; let paused = false; let gameOver = false; let score = 0; let lives = 3; let level = 1; // Entities const player = { x: W/2, y: H - 48, w: 36, h: 18, speed: 240, reload: 0, reloadTime: 0.35, alive: true }; const bullets = []; // player bullets const alienBullets = []; // enemy bullets const explosions = []; const bunkers = []; // array of bunker objects // Aliens let aliens = []; // array of alien objects let alienCols = 11; let alienRows = 5; let alienSpacingX = 40; let alienSpacingY = 36; let alienStartX = 80; let alienStartY = 80; let alienDir = 1; let alienSpeed = 22; // pixels per second base let alienMoveTimer = 0; let alienMoveInterval = 0.85; // seconds per step (decreases as aliens die) let alienStepX = 10; let alienStepY = 18; let alienBounds = {left:0, right:W, top:0, bottom:0}; // Colors const palette = { bg: '#001a26', player: '#9ee0ff', bullet: '#fff1a8', alien1: '#ffdd88', alien2: '#ff8b8b', alien3: '#a0ffce', bunker: '#65b88a', explosion: ['#fff8c9','#ffd17a','#ff7a7a'] }; // Utility function rand(min,max){ return Math.random()*(max-min)+min; } function clamp(v,a,b){ return Math.max(a,Math.min(b,v)); } // init game function resetGame(){ score = 0; lives = 3; level = 1; spawnLevel(level); updateHUD(); gameOver = false; paused = false; } function spawnLevel(l){ // reset arrays bullets.length = 0; alienBullets.length = 0; explosions.length = 0; bunkers.length = 0; aliens.length = 0; // Player reset player.x = W/2; player.y = H - 48; player.reload = 0; player.reloadTime = 0.35; player.alive = true; // Alien grid config scales with level alienCols = 11; alienRows = 4 + Math.min(2, Math.floor((l-1)/2)); // increase rows slowly alienSpacingX = 44; alienSpacingY = 36; alienStartX = 48; alienStartY = 70; alienDir = 1; alienSpeed = 20 + (l-1)*4; alienMoveInterval = Math.max(0.18, 0.85 - (l-1)*0.05); // faster each level // Create aliens for(let r=0;r<alienRows;r++){ for(let c=0;c<alienCols;c++){ const a = { r, c, x: alienStartX + c*alienSpacingX, y: alienStartY + r*alienSpacingY, w: 28, h: 20, hp: 1, alive: true, type: (r<1 ? 0 : (r<3 ? 1 : 2)) // types for color/points }; aliens.push(a); } } // Bunkers const bunkerCount = 4; const blockSize = 6; for(let b=0;b<bunkerCount;b++){ const bx = (W/(bunkerCount+1))*(b+1) - 36; const by = H - 140; const layout = generateBunkerLayout(); bunkers.push({x:bx,y:by,layout,blockSize}); } // Alien bounds update computeAlienBounds(); } function generateBunkerLayout(){ // return a 2D array of 12x8 (w x h) with booleans; small gap pattern const w = 12, h = 8; const grid = []; for(let y=0;y<h;y++){ grid[y] = []; for(let x=0;x<w;x++){ grid[y][x] = false; } } // carve a little roof and filler for(let y=2;y<h;y++){ for(let x=0;x<w;x++){ // keep some gaps for aesthetics if(y===2 && (x===0||x===w-1)) { grid[y][x]=false; continue; } if(y===2 && (x>1 && x<w-2)) grid[y][x]=true; if(y>2) { // randomly remove some blocks making it look worn grid[y][x] = Math.random() > 0.12; } } } // top arch grid[1][4]=true; grid[1][5]=true; grid[1][6]=true; grid[1][7]=true; grid[0][5]=true; grid[0][6]=true; return grid; } function computeAlienBounds(){ let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity; for(const a of aliens){ if(!a.alive) continue; left = Math.min(left, a.x - a.w/2); right = Math.max(right, a.x + a.w/2); top = Math.min(top, a.y - a.h/2); bottom = Math.max(bottom, a.y + a.h/2); } if(left===Infinity){ // all dead alienBounds = {left:0,right:0,top:0,bottom:0}; } else { alienBounds = {left,right,top,bottom}; } } // Input window.addEventListener('keydown', e => { if(e.code==='Space' || e.code==='ArrowUp') e.preventDefault(); keys[e.code] = true; if(e.code==='KeyP') togglePause(); if(e.code==='KeyR' && gameOver) resetGame(); }); window.addEventListener('keyup', e => keys[e.code]=false); // Touch controls const tcLeft = document.getElementById('tc-left'); const tcRight = document.getElementById('tc-right'); const tcFire = document.getElementById('tc-fire'); function touchHold(el, key){ let tid = null; function start(e){ e.preventDefault(); if(key==='left') keys['ArrowLeft']=true; if(key==='right') keys['ArrowRight']=true; if(key==='fire') keys['Space']=true; tid = setTimeout(()=>{}, 10); } function end(e){ e.preventDefault(); if(key==='left') keys['ArrowLeft']=false; if(key==='right') keys['ArrowRight']=false; if(key==='fire') keys['Space']=false; if(tid) clearTimeout(tid); } el.addEventListener('touchstart', start); el.addEventListener('touchend', end); el.addEventListener('touchcancel', end); el.addEventListener('mousedown', e => { start(e); }); el.addEventListener('mouseup', e => { end(e); }); el.addEventListener('mouseleave', e => { end(e); }); } touchHold(tcLeft,'left'); touchHold(tcRight,'right'); touchHold(tcFire,'fire'); // Start/pause buttons startBtn.addEventListener('click', ()=>{ if(!running){ resetGame(); running = true; lastTime = performance.now(); requestAnimationFrame(loop); } else { // restart resetGame(); } }); pauseBtn.addEventListener('click', togglePause); function togglePause(){ if(!running) return; paused = !paused; pauseBtn.textContent = paused ? 'Resume' : 'Pause'; if(!paused) { lastTime = performance.now(); requestAnimationFrame(loop); } } // Game update loop function loop(t){ if(!running) return; const dt = Math.min(0.05, (t - lastTime) / 1000); lastTime = t; if(!paused && !gameOver){ update(dt); } render(); if(!gameOver && !paused) requestAnimationFrame(loop); } function update(dt){ // player input let dir = 0; if(keys['ArrowLeft'] || keys['KeyA']) dir -= 1; if(keys['ArrowRight'] || keys['KeyD']) dir += 1; player.x += dir * player.speed * dt; player.x = clamp(player.x, player.w/2+6, W - player.w/2 - 6); // shooting player.reload = Math.max(0, player.reload - dt); if((keys['Space'] || keys['KeyW'] || keys['ArrowUp']) && player.reload === 0){ shootPlayer(); player.reload = player.reloadTime; } // bullets update for(let i=bullets.length-1;i>=0;i--){ const b = bullets[i]; b.y -= b.v*dt; if(b.y < -10) bullets.splice(i,1); } for(let i=alienBullets.length-1;i>=0;i--){ const b = alienBullets[i]; b.y += b.v*dt; if(b.y > H + 10) alienBullets.splice(i,1); } // alien movement timer alienMoveTimer += dt; const aliveCount = aliens.filter(a=>a.alive).length; // speed scaling: fewer aliens = faster. We'll reduce interval proportionally. const scaledInterval = Math.max(0.08, alienMoveInterval * (0.4 + 0.6 * (aliveCount / (alienCols*alienRows)))); if(aliveCount===0){ // level complete levelUp(); } else { if(alienMoveTimer >= scaledInterval){ alienMoveTimer = 0; stepAliens(); } } // random alien shooting const fireRate = 0.8 - Math.min(0.65, (level-1)*0.06); // faster in higher levels if(Math.random() < dt * (0.5 + level*0.5) / fireRate){ alienFire(); } // collisions: player bullets -> aliens & bunkers for(let i=bullets.length-1;i>=0;i--){ const b = bullets[i]; let removed = false; // check aliens for(const a of aliens){ if(!a.alive) continue; if(rectCircleCollide(a.x - a.w/2, a.y - a.h/2, a.w, a.h, b.x, b.y, b.r)){ // hit a.alive = false; bullets.splice(i,1); removed = true; spawnExplosion(a.x, a.y, 12, palette.explosion); score += (30 - a.r*2) + ( (a.type===0)?60: (a.type===1?40:20) ); computeAlienBounds(); break; } } if(removed) continue; // bunkers for(const bun of bunkers){ const hit = bunkerHitByBullet(bun, b.x, b.y); if(hit){ bullets.splice(i,1); removed = true; break; } } if(removed) continue; } // collisions: alien bullets -> player, bunkers for(let i=alienBullets.length-1;i>=0;i--){ const b = alienBullets[i]; // player if(circleRectCollide(b.x,b.y,b.r, player.x-player.w/2, player.y-player.h/2, player.w, player.h)){ alienBullets.splice(i,1); hitPlayer(); continue; } // bunkers for(const bun of bunkers){ const hit = bunkerHitByBullet(bun, b.x, b.y); if(hit){ alienBullets.splice(i,1); break; } } } // aliens reaching ground or colliding with player for(const a of aliens){ if(!a.alive) continue; if(a.y + a.h/2 >= player.y - 12){ // game over triggerGameOver(); } // if an alien overlaps the player directly if(rectsOverlap(a.x - a.w/2, a.y - a.h/2, a.w, a.h, player.x - player.w/2, player.y - player.h/2, player.w, player.h)){ triggerGameOver(); } } // explosion updates for(let i=explosions.length-1;i>=0;i--){ const ex = explosions[i]; ex.t += dt; if(ex.t > ex.d) explosions.splice(i,1); } // update HUD updateHUD(); } function levelUp(){ level++; spawnLevel(level); spawnExplosion(W/2, H/2 - 60, 30, ['#9efcad','#5fd4ff','#ffd88d']); score += 500; } function updateHUD(){ scoreEl.textContent = `Score: ${score}`; livesEl.textContent = `Lives: ${lives}`; levelEl.textContent = `Level: ${level}`; } // Shooting functions function shootPlayer(){ const b = { x: player.x, y: player.y - player.h/2 - 8, r:3, v:420 }; bullets.push(b); spawnMuzzle(player.x, player.y - player.h/2 - 8); // small recoil: no audio, just visual } function alienFire(){ // choose random column bottom-most alive alien(s) const cols = {}; for(const a of aliens){ if(!a.alive) continue; if(!cols[a.c] || a.y > cols[a.c].y) cols[a.c] = a; } const candidates = Object.values(cols); if(candidates.length===0) return; const shooter = candidates[Math.floor(Math.random() * candidates.length)]; const b = { x: shooter.x, y: shooter.y + shooter.h/2 + 8, r:4, v: 160 + level*10 }; alienBullets.push(b); } // Alien step move: move group horizontally, if hitting edge reverse and drop function stepAliens(){ computeAlienBounds(); // compute projected position const projectedLeft = alienBounds.left + alienDir*alienStepX; const projectedRight = alienBounds.right + alienDir*alienStepX; if(projectedLeft <= 10 || projectedRight >= W - 10){ // reverse and descend alienDir *= -1; for(const a of aliens){ if(!a.alive) continue; a.y += alienStepY; } } else { // just step horizontally for(const a of aliens){ if(!a.alive) continue; a.x += alienDir*alienStepX; } } // increase speed slightly when going down computeAlienBounds(); } // collisions helpers function rectCircleCollide(rx, ry, rw, rh, cx, cy, cr){ // clamp circle center to rect const closestX = clamp(cx, rx, rx+rw); const closestY = clamp(cy, ry, ry+rh); const dx = cx - closestX; const dy = cy - closestY; return (dx*dx + dy*dy) <= cr*cr; } function circleRectCollide(cx,cy,cr, rx,ry,rw,rh){ return rectCircleCollide(rx,ry,rw,rh,cx,cy,cr); } function rectsOverlap(ax,ay,aw,ah, bx,by,bw,bh){ return !(ax+aw < bx || ax > bx+bw || ay+ah < by || ay > by+bh); } // bunker: check bullet at (x,y). If hits, remove block(s) and return true function bunkerHitByBullet(bun, x, y){ const {x:bx,y:by,layout,blockSize:bs} = bun; const gw = layout[0].length, gh = layout.length; const localX = x - bx; const localY = y - by; const col = Math.floor(localX / bs); const row = Math.floor(localY / bs); if(col<0 || col>=gw || row<0 || row>=gh) return false; if(!layout[row][col]) return false; // remove hit block and neighbors with some randomness layout[row][col] = false; // degrade adjacent blocks lightly for(let oy=-1;oy<=1;oy++){ for(let ox=-1;ox<=1;ox++){ const r=row+oy, c=col+ox; if(r>=0 && r<gh && c>=0 && c<gw && layout[r][c] && Math.random()>0.35) layout[r][c]=false; } } spawnExplosion(bx + col*bs + bs/2, by + row*bs + bs/2, 8, ['#fff','#ffd89a']); return true; } function hitPlayer(){ lives--; spawnExplosion(player.x, player.y, 18, ['#fff1a8','#ff8b8b']); if(lives <= 0){ triggerGameOver(); } else { // brief invulnerability? reposition player player.x = W/2; } } function triggerGameOver(){ gameOver = true; running = false; spawnExplosion(player.x, player.y, 40, ['#ff8b8b','#ffd88d','#ffffff']); // show overlay text for a moment by keeping render loop active render(); // final draw setTimeout(()=> { // show dialog using canvas overlay (drawn in render) // allow restart startBtn.textContent = 'Restart'; running = false; paused = false; // leave game waiting for restart }, 150); } // Effects function spawnExplosion(x,y,count,colors){ const e = {x,y,p:[],t:0,d:0.8}; for(let i=0;i<count;i++){ e.p.push({ x,y, vx:rand(-120,120), vy:rand(-200,20), s:rand(1,3), c: colors[Math.floor(Math.random()*colors.length)], life:rand(0.5,1.2) }); e.d = Math.max(e.d, e.p[e.p.length-1].life); } explosions.push(e); } function spawnMuzzle(x,y){ const e = {x,y,p:[],t:0,d:0.18}; for(let i=0;i<6;i++){ e.p.push({ x,y, vx:rand(-60,60), vy:rand(-120,-40), s:rand(1,3), c: '#fff1a8', life:rand(0.08,0.18) }); e.d = Math.max(e.d, e.p[e.p.length-1].life); } explosions.push(e); } // Rendering function clear(){ ctx.fillStyle = '#031725'; ctx.fillRect(0,0,W,H); // starfield drawStarfield(); } // simple procedural starfield background - deterministic per frame const stars = []; for(let i=0;i<120;i++){ stars.push({x:rand(0,W),y:rand(0,H),s:rand(0.4,1.8),a:rand(0.08,0.9)}); } function drawStarfield(){ ctx.globalCompositeOperation = 'lighter'; for(const s of stars){ ctx.fillStyle = `rgba(255,255,255,${s.a})`; ctx.beginPath(); ctx.ellipse(s.x, s.y, s.s, s.s, 0, 0, Math.PI*2); ctx.fill(); } ctx.globalCompositeOperation = 'source-over'; } function render(){ // clear clear(); // draw HUD on canvas: score/lives etc for immersive feel drawText(`Score: ${score}`, 16, 18, '#bfefff', 14); drawText(`Lives: ${lives}`, 16, 36, '#bdd7c9', 12); drawText(`Level: ${level}`, 16, 52, '#cfd7ff', 12); // draw bunkers for(const bun of bunkers){ drawBunker(bun); } // draw aliens for(const a of aliens){ if(!a.alive) continue; drawAlien(a); } // draw player bullets ctx.save(); for(const b of bullets){ drawBullet(b, '#fff1a8'); } for(const b of alienBullets){ drawBullet(b, '#ff7d7d'); } ctx.restore(); // draw player if(!gameOver || (gameOver && (performance.now()%400 < 200))){ drawPlayer(player); } // draw explosions for(const ex of explosions){ drawExplosion(ex); } // overlays if(!running){ // draw start/paused/gameover overlay on canvas ctx.save(); const overlay = paused ? 'Paused' : (gameOver ? 'Game Over' : 'Press Start'); ctx.fillStyle = 'rgba(0,0,0,0.52)'; ctx.fillRect(40, 120, W-80, 240); drawText(overlay, W/2, 190, '#ffd8a8', 36, 'center'); ctx.font = '14px system-ui, sans-serif'; ctx.fillStyle = '#ddd'; ctx.textAlign = 'center'; if(gameOver) { ctx.fillText(`Final Score: ${score}`, W/2, 230); ctx.fillText(`Click Start / Restart to play again`, W/2, 260); } else { ctx.fillText(`Use ← → to move, Space to shoot`, W/2, 230); ctx.fillText(`Or use touch controls on mobile`, W/2, 260); } ctx.restore(); } } function drawText(t,x,y,color='white',size=18,align='left'){ ctx.save(); ctx.font = `${size}px system-ui, sans-serif`; ctx.fillStyle = color; ctx.textAlign = align; ctx.fillText(t, x, y); ctx.restore(); } function drawPlayer(p){ ctx.save(); ctx.translate(p.x, p.y); // body ctx.fillStyle = palette.player; roundRect(ctx, -p.w/2, -p.h/2, p.w, p.h, 4, true, false); // turret ctx.fillStyle = '#cdefff'; ctx.fillRect(-4, -p.h/2 - 10, 8, 10); // small shading ctx.fillStyle = 'rgba(0,0,0,0.08)'; ctx.fillRect(-p.w/2, 0, p.w, p.h/2); ctx.restore(); } function drawAlien(a){ ctx.save(); ctx.translate(a.x, a.y); // color by type let col = a.type===0 ? palette.alien1 : (a.type===1 ? palette.alien2 : palette.alien3); // body shape: stylized insect ctx.fillStyle = col; // torso roundRect(ctx, -a.w/2, -a.h/2+2, a.w, a.h-4, 3, true, false); // legs ctx.fillStyle = shade(col, -30); for(let i=-2;i<=2;i++){ ctx.fillRect(i*6 - 1, a.h/2 - 2, 3, 6); } // antennae ctx.strokeStyle = shade(col, 40); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-a.w/4, -a.h/2+4); ctx.quadraticCurveTo(-a.w/2, -a.h, -a.w/4-6, -a.h-6); ctx.moveTo(a.w/4, -a.h/2+4); ctx.quadraticCurveTo(a.w/2, -a.h, a.w/4+6, -a.h-6); ctx.stroke(); // eyes ctx.fillStyle = '#071226'; ctx.beginPath(); ctx.ellipse(-6, -2, 3, 3, 0, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(6, -2, 3, 3, 0, 0, Math.PI*2); ctx.fill(); ctx.restore(); } function drawBunker(bun){ const {x,y,layout,blockSize:bs} = bun; for(let r=0;r<layout.length;r++){ for(let c=0;c<layout[0].length;c++){ if(!layout[r][c]) continue; const bx = x + c*bs; const by = y + r*bs; ctx.fillStyle = palette.bunker; roundRect(ctx, bx, by, bs-1, bs-1, 2, true, false); } } } function drawBullet(b, color){ ctx.fillStyle = color; ctx.beginPath(); ctx.ellipse(b.x, b.y, b.r, b.r, 0, 0, Math.PI*2); ctx.fill(); } function drawExplosion(ex){ for(const p of ex.p){ const life = 1 - Math.min(1, ex.t / p.life); ctx.fillStyle = p.c; ctx.globalAlpha = life; ctx.beginPath(); ctx.ellipse(p.x + p.vx * ex.t * 0.6, p.y + p.vy * ex.t * 0.6 + ex.t*8, p.s*life*4, p.s*life*4, 0, 0, Math.PI*2); ctx.fill(); ctx.globalAlpha = 1; } ex.t += 0; // time updated in update } // Helper: rounded rect function roundRect(ctx, x, y, w, h, r, fill, stroke) { if (typeof r === 'undefined') r = 5; if (typeof r === 'number') r = {tl: r, tr: r, br: r, bl: r}; ctx.beginPath(); ctx.moveTo(x + r.tl, y); ctx.lineTo(x + w - r.tr, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr); ctx.lineTo(x + w, y + h - r.br); ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h); ctx.lineTo(x + r.bl, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl); ctx.lineTo(x, y + r.tl); ctx.quadraticCurveTo(x, y, x + r.tl, y); ctx.closePath(); if (fill) ctx.fill(); if (stroke) ctx.stroke(); } // color shade helper function shade(hex, percent) { // hex like #rrggbb const f = hex.slice(1); const r = parseInt(f.substring(0,2),16); const g = parseInt(f.substring(2,4),16); const b = parseInt(f.substring(4,6),16); const t = percent < 0 ? 0 : 255; const p = Math.abs(percent) / 100; const R = Math.round((t - r) * p) + r; const G = Math.round((t - g) * p) + g; const B = Math.round((t - b) * p) + b; return `rgb(${R},${G},${B})`; } // simple collision helpers for bullets vs blocks were handled // initialize spawnLevel(1); updateHUD(); render(); // initial draw // expose some console commands for debugging window._game = { spawnLevel, spawnExplosion, aliens, bullets, alienBullets, bunkers, player, resetGame }; })(); </script> </body> </html> ``` Notes: - Controls: Left/Right arrows or A/D to move. Space to shoot. P to pause. Start button begins or restarts. - Touch buttons provided for mobile: left, fire, right. - All visuals are drawn procedurally on canvas — no external images. - If you want features added (sound, more enemy behaviors, high-score storage, more polished UI), tell me which and I can extend it.
TAKE IT TO CODE
This run produced no artifact — reuse the prompt with a different model or your own pi-agent run.