← Mike Higgins Home
Custom Code Experiment •

Space Invaders

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>Space Invaders</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      background: #000;
      overflow: hidden;
      font-family: 'Courier New', monospace;
      touch-action: none;
      width: 100vw;
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    #gameWrapper {
      position: relative;
      width: 100%;
      height: 100%;
      max-width: 1000px;
      max-height: 750px;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    #gameContainer {
      position: relative;
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
    }

    #gameCanvas {
      display: block;
      background: radial-gradient(circle, #1a0033, #000);
      filter: contrast(1.1) saturate(1.3);
      width: 100%;
      height: 100%;
      border: none;
      box-shadow: 0 0 20px #ff00ff40;
    }

    .glitch-overlay {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      background:
        repeating-linear-gradient(
          0deg,
          transparent,
          transparent 2px,
          rgba(0, 255, 255, 0.08) 2px,
          rgba(0, 255, 255, 0.08) 4px
        ),
        repeating-linear-gradient(
          90deg,
          transparent,
          transparent 100px,
          rgba(255, 0, 255, 0.05) 100px,
          rgba(255, 0, 255, 0.05) 101px
        );
      animation: scanlines 0.2s infinite linear, glitchOverlay 3s infinite;
      z-index: 10;
    }

    @keyframes scanlines {
      0% { transform: translateY(0px); }
      100% { transform: translateY(4px); }
    }

    @keyframes glitchOverlay {
      0% { opacity: 1; }
      90% { opacity: 1; }
      91% { opacity: 0.3; }
      92% { opacity: 1; }
      93% { opacity: 0.1; }
      94% { opacity: 1; }
      100% { opacity: 1; }
    }

    #instructions {
      position: absolute;
      top: 20px;
      left: 50%;
      transform: translateX(-50%);
      color: #00ffff;
      font-family: 'Courier New', monospace;
      font-size: 14px;
      text-align: center;
      text-shadow: 0 0 5px #00ffff;
      background: rgba(0, 0, 0, 0.7);
      padding: 10px 20px;
      border: 1px solid #00ffff40;
      border-radius: 8px;
      backdrop-filter: blur(5px);
      z-index: 100;
    }

    #mobileControls {
      position: absolute;
      bottom: 20px;
      left: 0;
      width: 100%;
      display: none;
      justify-content: space-between;
      padding: 0 20px;
      z-index: 50;
      pointer-events: none;
    }

    .control-group {
      display: flex;
      gap: 20px;
      pointer-events: auto;
    }

    .control-btn {
      width: 60px;
      height: 60px;
      border-radius: 50%;
      background: rgba(0, 255, 255, 0.2);
      border: 2px solid #00ffff;
      color: #00ffff;
      font-size: 24px;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      user-select: none;
      touch-action: manipulation;
      box-shadow: 0 0 10px #00ffff80;
      transition: all 0.1s;
    }

    .control-btn:active, .control-btn.active {
      background: rgba(0, 255, 255, 0.4);
      transform: scale(0.95);
    }

    #fireBtn {
      width: 70px;
      height: 70px;
      background: rgba(255, 0, 128, 0.2);
      border: 2px solid #ff0080;
      color: #ff0080;
      box-shadow: 0 0 10px #ff008080;
    }

    #fireBtn:active, #fireBtn.active {
      background: rgba(255, 0, 128, 0.4);
    }

    @media (max-width: 768px) {
      #instructions {
        display: none;
      }

      #mobileControls {
        display: flex;
      }

      .control-btn {
        width: 50px;
        height: 50px;
        font-size: 20px;
      }

      #fireBtn {
        width: 60px;
        height: 60px;
      }
    }

    @media (max-width: 480px) {
      #mobileControls {
        padding: 0 10px;
      }
    }
  </style>
</head>
<body>
  <div id="gameWrapper">
    <div id="gameContainer">
      <canvas id="gameCanvas"></canvas>
      <div class="glitch-overlay"></div>
      <div id="instructions">
        ARROW KEYS or A/D to move • SPACEBAR to fire • SPACE to restart when game over
      </div>

      <div id="mobileControls">
        <div class="control-group">
          <div class="control-btn" id="leftBtn">←</div>
          <div class="control-btn" id="rightBtn">→</div>
        </div>
        <div class="control-group">
          <div class="control-btn" id="fireBtn">FIRE</div>
        </div>
      </div>
    </div>
  </div>

  <script>
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');

    const BASE_WIDTH = 800;
    const BASE_HEIGHT = 600;

    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

    let gameWidth = BASE_WIDTH;
    let gameHeight = BASE_HEIGHT;
    let scale = 1;
    let offsetX = 0;
    let offsetY = 0;

    let resizeTimeout;
    function resizeCanvas() {
      clearTimeout(resizeTimeout);
      resizeTimeout = setTimeout(() => {
        const container = document.getElementById('gameContainer');
        const containerWidth = container.clientWidth;
        const containerHeight = container.clientHeight;

        const scaleX = containerWidth / BASE_WIDTH;
        const scaleY = containerHeight / BASE_HEIGHT;
        scale = Math.min(scaleX, scaleY);

        gameWidth = BASE_WIDTH * scale;
        gameHeight = BASE_HEIGHT * scale;

        offsetX = (containerWidth - gameWidth) / 2;
        offsetY = (containerHeight - gameHeight) / 2;

        canvas.width = containerWidth;
        canvas.height = containerHeight;

        ctx.imageSmoothingEnabled = false;

        if (player) {
          player.x = Math.max(0, Math.min(player.x, BASE_WIDTH - player.width));
          player.y = Math.max(0, Math.min(player.y, BASE_HEIGHT - player.height));
        }
      }, 50);
    }

    window.addEventListener('resize', resizeCanvas);
    window.addEventListener('orientationchange', resizeCanvas);

    resizeCanvas();

    const keys = {};
    let gameFocused = false;

    function focusGame() {
      if (!gameFocused) {
        canvas.focus();
        gameFocused = true;
      }
    }

    canvas.tabIndex = 0;
    canvas.style.outline = 'none';

    window.addEventListener('keydown', (e) => {
      focusGame();
      keys[e.key] = true;
      e.preventDefault();
    });

    window.addEventListener('keyup', (e) => {
      keys[e.key] = false;
      e.preventDefault();
    });

    const leftBtn = document.getElementById('leftBtn');
    const rightBtn = document.getElementById('rightBtn');
    const fireBtn = document.getElementById('fireBtn');

    function setupButtonEvents(button, key) {
      button.addEventListener('touchstart', (e) => {
        e.preventDefault();
        keys[key] = true;
        button.classList.add('active');
      });

      button.addEventListener('touchend', (e) => {
        e.preventDefault();
        keys[key] = false;
        button.classList.remove('active');
      });

      button.addEventListener('mousedown', (e) => {
        e.preventDefault();
        keys[key] = true;
        button.classList.add('active');
      });

      button.addEventListener('mouseup', (e) => {
        e.preventDefault();
        keys[key] = false;
        button.classList.remove('active');
      });

      button.addEventListener('mouseleave', (e) => {
        if (keys[key]) {
          keys[key] = false;
          button.classList.remove('active');
        }
      });
    }

    setupButtonEvents(leftBtn, 'ArrowLeft');
    setupButtonEvents(rightBtn, 'ArrowRight');
    setupButtonEvents(fireBtn, ' ');

    canvas.addEventListener('click', focusGame);
    canvas.addEventListener('touchstart', focusGame);

    window.addEventListener('load', () => {
      setTimeout(focusGame, 100);
    });

    let frame = 0;
    let glitchTimer = 0;
    let score = 0;
    let gameState = 'playing';
    let psychedelicMode = 0;
    let realityBreak = 0;
    let colorShift = 0;

    const colors = ['#ff0080', '#00ff80', '#8000ff', '#ff8000', '#0080ff', '#ff0040', '#40ff00', '#ff4080', '#80ff40', '#4080ff', '#ff8040', '#8040ff', '#40ff80'];
    const glitchColors = ['#ff00ff', '#00ffff', '#ffff00', '#ff0000', '#00ff00', '#0000ff', '#ffffff'];

    const player = {
      x: BASE_WIDTH / 2 - 15,
      y: BASE_HEIGHT - 80,
      width: 30,
      height: 25,
      speed: 5,
      alive: true,
      lives: 3,
      invulnerable: 0,
      lastShot: 0
    };

    const enemies = [];
    let enemyDirection = 1;
    let enemySpeed = 1;
    let enemyDropDistance = 20;

    function createEnemyFormation() {
      enemies.length = 0;
      const rows = 5;
      const cols = 10;

      for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
          let alienType;
          if (row === 0) alienType = 0;
          else if (row <= 2) alienType = 1;
          else alienType = 2;

          enemies.push({
            x: col * 60 + 50,
            y: row * 40 + 50,
            width: 24,
            height: 24,
            alive: true,
            type: alienType,
            glitchChance: 0.05,
            baseY: row * 40 + 50
          });
        }
      }
    }

    const bullets = [];
    const enemyBullets = [];

    const particles = [];

    function createParticles(x, y, count, color) {
      for (let i = 0; i < count; i++) {
        particles.push({
          x: x,
          y: y,
          vx: (Math.random() - 0.5) * 8,
          vy: (Math.random() - 0.5) * 8,
          life: 30 + Math.random() * 30,
          maxLife: 30 + Math.random() * 30,
          color: color || colors[Math.floor(Math.random() * colors.length)],
          size: Math.random() * 3 + 1
        });
      }
    }

    function applyGlitch() {
      glitchTimer++;
      colorShift += 0.1;

      if (Math.random() < 0.15) {
        ctx.save();
        ctx.globalCompositeOperation = Math.random() < 0.5 ? 'screen' : 'difference';
        ctx.fillStyle = glitchColors[Math.floor(Math.random() * glitchColors.length)] + '60';
        const glitchHeight = Math.random() * 150 + 10;
        const glitchWidth = Math.random() * canvas.width;
        ctx.fillRect(
          Math.random() * canvas.width - glitchWidth/2,
          Math.random() * (canvas.height - glitchHeight),
          glitchWidth,
          glitchHeight
        );
        ctx.restore();
      }

      if (Math.random() < 0.1) {
        ctx.save();
        ctx.globalCompositeOperation = 'exclusion';
        for (let i = 0; i < 10; i++) {
          ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)] + '80';
          ctx.fillRect(0, Math.random() * canvas.height, canvas.width, 2);
        }
        ctx.restore();
      }

      if (realityBreak > 0) {
        realityBreak--;
        ctx.save();
        ctx.globalCompositeOperation = 'multiply';
        ctx.fillStyle = `hsl(${colorShift * 10}, 100%, 50%)`;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.restore();

        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 16) {
          if (Math.random() < 0.3) {
            const shift = Math.floor(Math.random() * 100) * 4;
            if (i + shift < data.length) {
              data[i] = data[i + shift];
              data[i + 1] = data[i + shift + 1];
              data[i + 2] = data[i + shift + 2];
            }
          }
        }
        ctx.putImageData(imageData, 0, 0);
      }

      if (score > 0 && score % 200 === 0 && psychedelicMode === 0) {
        psychedelicMode = 300;
        realityBreak = 60;
      }

      if (psychedelicMode > 0) {
        psychedelicMode--;

        ctx.save();
        ctx.globalCompositeOperation = 'difference';
        const waveOffset = Math.sin(frame * 0.3) * 50;
        ctx.fillStyle = `hsl(${frame * 5}, 100%, 50%)`;
        ctx.fillRect(waveOffset, 0, canvas.width - waveOffset * 2, canvas.height);
        ctx.restore();

        if (Math.random() < 0.2) {
          ctx.save();
          ctx.scale(-1, 1);
          ctx.translate(-canvas.width, 0);
          ctx.globalAlpha = 0.3;
          ctx.drawImage(canvas, 0, 0);
          ctx.restore();
        }
      }

      if (Math.random() < 0.08) {
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const data = imageData.data;

        for (let i = 0; i < data.length; i += 4) {
          if (Math.random() < 0.05) {
            const corruption = Math.random();
            if (corruption < 0.33) {
              data[i] = data[i + 4] || 255;
            } else if (corruption < 0.66) {
              data[i] = Math.random() * 255;
              data[i + 1] = Math.random() * 255;
              data[i + 2] = Math.random() * 255;
            } else {
              data[i] = 255 - data[i];
              data[i + 1] = 255 - data[i + 1];
              data[i + 2] = 255 - data[i + 2];
            }
          }
        }
        ctx.putImageData(imageData, 0, 0);
      }

      const glitchAmount = Math.sin(frame * 0.1) * 2;
      const hueRotate = Math.sin(frame * 0.05) * 180;
      const contrast = 1.1 + Math.sin(frame * 0.07) * 0.2;
      const saturate = 1.3 + Math.sin(frame * 0.08) * 0.2;

      ctx.save();
      ctx.translate(
        (Math.random() - 0.5) * glitchAmount,
        (Math.random() - 0.5) * glitchAmount
      );
      ctx.filter = `contrast(${contrast}) saturate(${saturate}) hue-rotate(${hueRotate}deg)`;
      ctx.globalAlpha = 0.1;
      ctx.drawImage(canvas, 0, 0);
      ctx.restore();
    }

    function update() {
      frame++;

      if (gameState !== 'playing') return;

      const moveSpeed = player.speed;
      if (keys['ArrowLeft'] || keys['a']) {
        player.x = Math.max(0, player.x - moveSpeed);
      }
      if (keys['ArrowRight'] || keys['d']) {
        player.x = Math.min(BASE_WIDTH - player.width, player.x + moveSpeed);
      }

      const now = Date.now();
      if (keys[' '] && now - player.lastShot > 150) {
        bullets.push({
          x: player.x + player.width / 2 - 1,
          y: player.y,
          width: 2,
          height: 8,
          speed: 7,
          color: '#00ffff'
        });
        player.lastShot = now;
      }

      if (player.invulnerable > 0) player.invulnerable--;

      bullets.forEach((bullet, index) => {
        bullet.y -= bullet.speed;
        if (bullet.y < -bullet.height) {
          bullets.splice(index, 1);
        }
      });

      let hitEdge = false;
      const aliveEnemies = enemies.filter(e => e.alive);

      if (aliveEnemies.length > 0) {
        aliveEnemies.forEach(enemy => {
          if ((enemy.x <= 0 && enemyDirection < 0) ||
              (enemy.x >= BASE_WIDTH - enemy.width && enemyDirection > 0)) {
            hitEdge = true;
          }
        });

        if (hitEdge) {
          enemyDirection *= -1;
          enemies.forEach(enemy => {
            if (enemy.alive) {
              enemy.y += enemyDropDistance;
            }
          });
        } else {
          enemies.forEach(enemy => {
            if (enemy.alive) {
              enemy.x += enemyDirection * enemySpeed;
            }
          });
        }
      }

      if (Math.random() < 0.02 && aliveEnemies.length > 0) {
        const shooter = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
        enemyBullets.push({
          x: shooter.x + shooter.width / 2,
          y: shooter.y + shooter.height,
          width: 2,
          height: 6,
          speed: 3,
          color: colors[Math.floor(Math.random() * colors.length)]
        });
      }

      enemyBullets.forEach((bullet, index) => {
        bullet.y += bullet.speed;
        if (bullet.y > BASE_HEIGHT) {
          enemyBullets.splice(index, 1);
        }
      });

      bullets.forEach((bullet, bIndex) => {
        enemies.forEach((enemy, eIndex) => {
          if (enemy.alive &&
              bullet.x < enemy.x + enemy.width &&
              bullet.x + bullet.width > enemy.x &&
              bullet.y < enemy.y + enemy.height &&
              bullet.y + bullet.height > enemy.y) {

            enemy.alive = false;
            bullets.splice(bIndex, 1);
            score += (enemy.type === 0) ? 30 : (enemy.type === 1) ? 20 : 10;
            createParticles(enemy.x + enemy.width/2, enemy.y + enemy.height/2, 8, enemy.color);
          }
        });
      });

      enemies.forEach(enemy => {
        if (enemy.alive &&
            player.x < enemy.x + enemy.width &&
            player.x + player.width > enemy.x &&
            player.y < enemy.y + enemy.height &&
            player.y + player.height > enemy.y) {

          gameState = 'gameOver';
          createParticles(player.x + player.width/2, player.y + player.height/2, 30, '#ff0080');
        }
      });

      if (player.invulnerable === 0) {
        enemyBullets.forEach((bullet, index) => {
          if (bullet.x < player.x + player.width &&
              bullet.x + bullet.width > player.x &&
              bullet.y < player.y + player.height &&
              bullet.y + bullet.height > player.y) {

            enemyBullets.splice(index, 1);
            player.lives--;
            player.invulnerable = 120;
            createParticles(player.x + player.width/2, player.y + player.height/2, 15, '#ff0080');

            if (player.lives <= 0) {
              gameState = 'gameOver';
              createParticles(player.x + player.width/2, player.y + player.height/2, 30, '#ff0080');
            }
          }
        });
      }

      if (aliveEnemies.length === 0) {
        createEnemyFormation();
        enemySpeed += 0.5;
      }

      particles.forEach((particle, index) => {
        particle.x += particle.vx;
        particle.y += particle.vy;
        particle.life--;
        particle.vx *= 0.98;
        particle.vy *= 0.98;
        if (particle.life <= 0) {
          particles.splice(index, 1);
        }
      });
    }

    function render() {
      ctx.fillStyle = 'rgba(0, 0, 0, 0.9)';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      ctx.save();
      ctx.translate(offsetX, offsetY);
      ctx.scale(scale, scale);

      for (let i = 0; i < 50; i++) {
        if (Math.random() < 0.3) {
          ctx.fillStyle = '#ffffff';
          const starX = (Math.sin(frame * 0.01 + i) * 20 + BASE_WIDTH * (i / 50)) % BASE_WIDTH;
          const starY = (Math.cos(frame * 0.015 + i) * 10 + BASE_HEIGHT * ((i * 7) % 100 / 100)) % BASE_HEIGHT;
          ctx.fillRect(starX, starY, 1, 1);
        }
      }

      if (gameState === 'playing') {
        if (player.lives > 0) {
          drawPlayer(player.x, player.y);
        }

        enemies.forEach(enemy => {
          if (enemy.alive) {
            drawEnemy(enemy);
          }
        });

        bullets.forEach(bullet => {
          ctx.fillStyle = bullet.color;
          ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);

          ctx.fillStyle = bullet.color + '60';
          ctx.fillRect(bullet.x - 1, bullet.y, bullet.width + 2, bullet.height + 4);
        });

        enemyBullets.forEach(bullet => {
          ctx.fillStyle = bullet.color;
          ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
        });

        particles.forEach(particle => {
          ctx.fillStyle = particle.color;
          ctx.globalAlpha = particle.life / particle.maxLife;
          ctx.fillRect(particle.x, particle.y, particle.size, particle.size);
          ctx.globalAlpha = 1;
        });

        ctx.fillStyle = '#00ffff';
        ctx.font = '20px Courier New';
        ctx.fillText(`SCORE: ${score}`, 10, 30);
        ctx.fillText(`LIVES: ${player.lives}`, 10, 55);

      } else if (gameState === 'gameOver') {
        ctx.fillStyle = colors[Math.floor(frame * 0.2) % colors.length];
        ctx.font = '48px Courier New';
        ctx.textAlign = 'center';
        ctx.fillText('GAME OVER', BASE_WIDTH/2, BASE_HEIGHT/2 - 20);

        ctx.fillStyle = '#ffffff';
        ctx.font = '24px Courier New';
        ctx.fillText(`FINAL SCORE: ${score}`, BASE_WIDTH/2, BASE_HEIGHT/2 + 20);
        ctx.fillText('Press SPACE to restart', BASE_WIDTH/2, BASE_HEIGHT/2 + 60);
        ctx.textAlign = 'left';

        if (keys[' ']) {
          restartGame();
        }
      }

      ctx.restore();

      applyGlitch();
    }

    function drawPlayer(x, y) {
      const baseColor = player.invulnerable > 0 && Math.floor(frame * 0.3) % 2 === 0 ?
                    '#ffffff' : '#00ffff';

      const glitchX = x + (Math.random() - 0.5) * (psychedelicMode > 0 ? 4 : 1);
      const glitchY = y + (Math.random() - 0.5) * (psychedelicMode > 0 ? 4 : 1);

      const layers = psychedelicMode > 0 ? 3 : 1;
      for (let layer = 0; layer < layers; layer++) {
        const layerColor = layer === 0 ? baseColor : colors[Math.floor((frame * 0.5 + layer * 50) % colors.length)];
        const layerX = glitchX + layer * 2;
        const layerY = glitchY + layer * 1;

        ctx.fillStyle = layerColor;
        ctx.globalAlpha = layer === 0 ? 1 : 0.6;

        ctx.beginPath();
        ctx.moveTo(layerX + player.width/2, layerY);
        ctx.lineTo(layerX + player.width/5, layerY + player.height/2);
        ctx.lineTo(layerX, layerY + player.height);
        ctx.lineTo(layerX + player.width/3.75, layerY + player.height*0.9);
        ctx.lineTo(layerX + player.width/2, layerY + player.height);
        ctx.lineTo(layerX + player.width*0.73, layerY + player.height*0.9);
        ctx.lineTo(layerX + player.width, layerY + player.height);
        ctx.lineTo(layerX + player.width*0.8, layerY + player.height/2);
        ctx.closePath();
        ctx.fill();

        if (layer === 0) {
          ctx.fillStyle = '#ffffff';
          ctx.beginPath();
          ctx.moveTo(layerX + player.width/2, layerY + 2);
          ctx.lineTo(layerX + player.width*0.4, layerY + player.height/3);
          ctx.lineTo(layerX + player.width*0.6, layerY + player.height/3);
          ctx.closePath();
          ctx.fill();

          ctx.fillStyle = colors[Math.floor(frame * 0.3) % colors.length];
          ctx.fillRect(layerX + player.width/5, layerY + player.height*0.8, player.width/7.5, player.height/4);
          ctx.fillRect(layerX + player.width*0.67, layerY + player.height*0.8, player.width/7.5, player.height/4);
        }

        ctx.globalAlpha = 1;
      }

      if (Math.random() < 0.2) {
        ctx.fillStyle = glitchColors[Math.floor(Math.random() * glitchColors.length)] + '80';
        const dupX = x + (Math.random() - 0.5) * 10;
        const dupY = y + (Math.random() - 0.5) * 10;
        ctx.fillRect(dupX, dupY, player.width, player.height);
      }
    }

    function drawEnemy(enemy) {
      const x = enemy.x;
      const y = enemy.y + Math.sin(frame * 0.1 + enemy.x * 0.01) * 2;
      let color = colors[Math.floor((frame * 0.1 + enemy.x * 0.01)) % colors.length];

      if (Math.random() < enemy.glitchChance) {
        color = colors[Math.floor(Math.random() * colors.length)];
      }

      ctx.fillStyle = color;

      if (enemy.type === 0) {
        ctx.fillRect(x + enemy.width/6, y, enemy.width*2/3, enemy.height/3);
        ctx.fillRect(x + enemy.width/4, y + enemy.height/3, enemy.width/2, enemy.height/6);

        for (let i = 0; i < 6; i++) {
          ctx.fillRect(x + i * (enemy.width/6) + enemy.width/8, y + enemy.height/2, enemy.width/12, enemy.height/3);
        }

        ctx.fillStyle = '#ffffff';
        ctx.fillRect(x + enemy.width/4, y + enemy.height/12, enemy.width/12, enemy.height/12);
        ctx.fillRect(x + enemy.width*2/3, y + enemy.height/12, enemy.width/12, enemy.height/12);

      } else if (enemy.type === 1) {
        ctx.fillRect(x + enemy.width/6, y, enemy.width*2/3, enemy.height/3);
        ctx.fillRect(x + enemy.width/10, y + enemy.height/3, enemy.width*0.8, enemy.height/3);

        ctx.fillRect(x, y + enemy.height/6, enemy.width/6, enemy.height/3);
        ctx.fillRect(x + enemy.width*5/6, y + enemy.height/6, enemy.width/6, enemy.height/3);

        for (let i = 0; i < 4; i++) {
          ctx.fillRect(x + enemy.width/6 + i * (enemy.width/4), y + enemy.height*2/3, enemy.width/12, enemy.height/4);
        }

        ctx.fillStyle = '#ffffff';
        ctx.fillRect(x + enemy.width/3, y + enemy.height/12, enemy.width/12, enemy.height/12);
        ctx.fillRect(x + enemy.width*7/12, y + enemy.height/12, enemy.width/12, enemy.height/12);

      } else {
        ctx.fillRect(x + enemy.width/4, y, enemy.width/2, enemy.height/3);
        ctx.fillRect(x + enemy.width/6, y + enemy.height/3, enemy.width*2/3, enemy.height/4);

        ctx.fillRect(x, y + enemy.height/3, enemy.width/6, enemy.height/3);
        ctx.fillRect(x + enemy.width*5/6, y + enemy.height/3, enemy.width/6, enemy.height/3);

        ctx.fillRect(x + enemy.width*5/12, y + enemy.height*7/12, enemy.width/6, enemy.height/3);
        ctx.fillRect(x + enemy.width*7/12, y + enemy.height*7/12, enemy.width/6, enemy.height/3);

        ctx.fillStyle = '#ffffff';
        ctx.fillRect(x + enemy.width/3, y + enemy.height/6, enemy.width/12, enemy.height/12);
        ctx.fillRect(x + enemy.width*7/12, y + enemy.height/6, enemy.width/12, enemy.height/12);
      }
    }

    function restartGame() {
      gameState = 'playing';
      score = 0;
      player.lives = 3;
      player.x = BASE_WIDTH / 2 - 15;
      player.invulnerable = 0;
      bullets.length = 0;
      enemyBullets.length = 0;
      particles.length = 0;
      enemySpeed = 1;
      createEnemyFormation();
    }

    function gameLoop() {
      update();
      render();
      requestAnimationFrame(gameLoop);
    }

    createEnemyFormation();
    gameLoop();
  </script>
</body>
</html>