To Play: Use the arrow keys to move and the space bar to shoot.

The Asteroid game is a development project that I created in the process of learning JavaScript.


        //consts
   const FPS = 30; // frames per second
   const FRICTION = 0.7; // fiction coefficient of space (0 no friction, 1 lots of frictions)
   const GAME_LIVES = 3; // number of lives you get
   const LASER_DIST = 0.275; // max distance the laser can travel as fraction of screen width
   const LASER_EXP_DUR = 0.1; // the duration of the lasers explosions in seconds
   const LASER_MAX = 10; // maximum num of lasers on screen
   const LASER_SPD = 500; // Speed of lasers in pixles per second
   const ORBS_NUM = 3; // starting number of astroids also ORBS are Astroids it just shorter for me to write
   const ORBS_SIZE = 100; // starting size of astroids in pixles
   const ORBS_SPD = 50; // Max starting speed of astroids pixles per second
   const ORBS_VERT = 10; // average number of vertices on the asteroids
   const ORBS_JAG = 0.4;  // Jaggedness of the astroids \
   const ORBS_PTS_LRG = 20; // points scored fot the large astroids
   const ORBS_PTS_MED = 50; // points scored fot the medium astroids
   const ORBS_PTS_SM = 100; // points scored fot the small astroids
   const SAVE_KEY_SCORE = "scorekey" // save key for local storage 
   const SHIP_SIZE = 30; // ship height in pixles
   const SHIP_THRUST = 5; // thrust accelertation of the ship
   const SHIP_EXP_DUR = 0.3; // duration of the ship explosion
   const SHIP_INV_DUR = 3; // ships invincibility duration
   const SHIP_BLINK_DUR = 0.1; // duration of the ships blink
   const TURN_SPEED = 360; // turn speed in derees per second
   const SHOW_RED_DOT = false; // shows of hides the center dot of the ship
   const SHOW_BOUNDING = false; // show or hide collision detection
   const TEXT_FADE_TIME = 2.5; // text fade time seconds
   const TEXT_SIZE = 50; // text font size in pixles
   const SOUND_ON = false; // this turns the sound off and on
   const MUSIC_ON = false; // this turns the sound off and on
   
   

   /** @type {HTMLCanvasElement} */
   var canv = document.getElementById("GameCanvas");
   var ctx = canv.getContext("2d");
   function resize() {
       canv.width = window.innerWidth -15;
     canv.height = window.innerHeight;
   }
   // set up event handaler for canvas sizing
   window.addEventListener('resize', resize, false); resize();
  

   // set up sound effects
   var fxExplode = new Sound("sounds/explode.m4a");
   var fxLaser = new Sound("sounds/laser.m4a", 5, 0.5);
   var fxHit = new Sound("sounds/hit.m4a", 5);
   var fxThrust = new Sound("sounds/thrust.m4a");

   // set up the music
   var music = new Music("sounds/music-low.m4a", "sounds/music-high.m4a");
   var orbsLeft, orbsTotal;


   // set up the game peramiters 
   var level, lives, orbs, score, highScore, ship, text, textAlpha;
   newGame();


   // set up key handalers
   
   // disable default key functions
   window.addEventListener("keydown", function(e) {

        // space and arrow keys             
           if([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
           e.preventDefault();
       }
   }, false);
   
   // event listeners
   document.addEventListener("keydown", keyDown);
   document.addEventListener("keyup", keyUp);


   // set up the game loop
   setInterval(update, 1000 / FPS);

   // creting astroids
   function createAsteroidBelt() {
       orbs = [];
       orbsTotal = (ORBS_NUM + level) * 7;
       orbsLeft = orbsTotal;
       orbs = [];
       var x, y;
       for (var i = 0; i < ORBS_NUM + level; i++) {
           do{
           x = Math.floor(Math.random() * canv.width);
           y = Math.floor(Math.random() * canv.height);
           } while(distBetweenShip(ship.x, ship.y, x, y) < ORBS_SIZE * 2 + ship.r);
           orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 2)));
       }
   }

   function destroyAsteroid(index) {
       var x = orbs[index].x;
       var y = orbs[index].y;
       var r = orbs[index].r;

       // split the asteroid in two
       if (r == Math.ceil(ORBS_SIZE / 2)) {
           orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 4)));
           orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 4)));
           score += ORBS_PTS_LRG;
       } else if (r == Math.ceil(ORBS_SIZE / 4)) {
           orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 8)));
           orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 8)));
           orbs.push(newAsteroid(x, y, Math.ceil(ORBS_SIZE / 8)));
           score += ORBS_PTS_MED;
       } else{
           score += ORBS_PTS_SM;
       }

       if (score > highScore) {
           highScore = score;
           localStorage.setItem(SAVE_KEY_SCORE, highScore);
       }

       // destroy the asteroid 
       orbs.splice(index, 1);
       fxHit.play();

       // calculate the ratio of remaining astroids to determin music tempo
       orbsLeft--;
       music.setAsteroidRatio(orbsLeft == 0 ? 1 : orbsLeft / orbsTotal);

       //next level 
       if (orbs.length == 0) {
           level++;
           newLevel();
       }
   }

   
   function distBetweenShip(x1, y1, x2, y2) {
       return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
   }

   function drawShip(x, y, a, color = "white") {
               ctx.strokeStyle = color;
               ctx.fillStyle = color;
               ctx.lineWidth = SHIP_SIZE / 20;
               ctx.beginPath();
               
               ctx.moveTo( // nose of the ship
                   x + 5 / 3 * ship.r * Math.cos(a),
                   y - 5 / 3 * ship.r * Math.sin(a)
               );
               ctx.lineTo( // rear left of the ship
                   x - ship.r * (Math.cos(a) + Math.sin(a)),
                   y + ship.r * (Math.sin(a) - Math.cos(a))
               );
               ctx.lineTo( // rear right of the ship
                   x - ship.r * (Math.cos(a) - Math.sin(a)),
                   y + ship.r * (Math.sin(a) + Math.cos(a))
               );
               ctx.closePath();
               ctx.stroke();
              
   }

   function explodeShip() {
      ship.explodeTime = Math.ceil(SHIP_EXP_DUR * FPS);
      fxExplode.play();
   }

   function gameOver(){
       ship.dead = true;
       text = "Game Over";
       textAlpha = 1.0;

   }
   // space ship movement
   function keyDown(/** @type {keyboardevent} */ ev) {

       if (ship.dead){
           return;
       }

       switch(ev.keyCode) {
           case 32: // Space bar shoots laser
               shootLaser();
               break;
           case 37: // left arrow key rotate ship left
               ship.rot = TURN_SPEED / 180 * Math.PI / FPS;
               break;
           case 38: // up arrow moves ship forward
               ship.thrusting = true;
           break;
           case 39: // left arrow rotate ship right
                ship.rot = -TURN_SPEED / 180 * Math.PI / FPS;
           break;
       }
   }

   function keyUp(/** @type {keyboardevent} */ ev){

       if (ship.dead){
           return;
       }

       switch(ev.keyCode) {
           case 32: // Space bar allow shooting again
               ship.canShoot = true;
               break;
           case 37: // left arrow key stop rotation left
               ship.rot = 0;
               break;
           case 38: // stop forward movement
                ship.thrusting = false;
           break;
           case 39: // left arrow stop rotating right
                ship.rot = 0;
           break;
          } 
       }

   function newAsteroid(x, y, r) {
       var lvlMult = 1 + 0.1 * level;
       var orbs = {
           a: Math.random() * Math.PI * 2, // in radians
           r: r,
           vert: (Math.random() * (ORBS_VERT + 1) + ORBS_VERT / 2),
           x: x,
           y: y,
           xv: Math.random() * ORBS_SPD * lvlMult/ FPS * (Math.random() < 0.5 ? 1 : -1),
           yv: Math.random() * ORBS_SPD * lvlMult / FPS * (Math.random() < 0.5 ? 1 : -1),
           offs: []
       };

       // crete the vertex offsets array
       for (var i = 0; i < orbs.vert; i++) {
           orbs.offs.push(Math.random() * ORBS_JAG * 2 + 1 - ORBS_JAG);

       }
       return orbs;
   }

   function newGame() {
   lives = GAME_LIVES;
   level = 0;
   score = 0;
   ship = newShip();

   // get the score from local storage   
   var scoreStr = localStorage.getItem(SAVE_KEY_SCORE);
   if (scoreStr == null) {
       highScore = 0;
   } else {
       highScore = parseInt(scoreStr);
   }


   newLevel();
   }

   function newLevel() {
       text = "Level " + (level + 1);
       textAlpha = 1.0;
       createAsteroidBelt();

   }

   // creates the ship
   function newShip() {
       return {
           x: canv.width / 2,
           y: canv.height / 2,
           r: SHIP_SIZE / 3,
           a: 90 / 180 * Math.PI, // convert radians
           blinkNum: Math.ceil(SHIP_INV_DUR / SHIP_BLINK_DUR),
           blinkTime: Math.ceil(SHIP_BLINK_DUR * FPS), 
           canShoot: true,
           dead: false,
           explodeTime: 0,
           lasers: [],
           rot: 0,
           thrusting: false,
           thrust: {
               x: 0,
               y: 0,
           }
       }
   }

   function shootLaser() {
       // create the laser object
       if (ship.canShoot && ship.lasers.length < LASER_MAX) {
           ship.lasers.push({ // from the nose of the ship
               x: ship.x + 4 / 3 * ship.r * Math.cos(ship.a),
               y: ship.y - 4 / 3 * ship.r * Math.sin(ship.a),
               xv: LASER_SPD * Math.cos(ship.a) / FPS,
               yv: -LASER_SPD * Math.sin(ship.a) / FPS,
               dist: 0,
               explodeTime: 0,
               });
           fxLaser.play();
       }

       // prevent further shooting
       ship.canShoot = false;
   }

   function Music(srcLow, srcHigh) {
       this.soundLow = new Audio(srcLow);
       this.soundHigh = new Audio(srcHigh);
       this.low = true;
       this.tempo = 1.0; // secounds per beat
       this.beatTime = 0; // tempo of the beat

       this.play = function() {
               if (MUSIC_ON) {
               if (this.low) {
                   this.soundLow.play();
               } else {
                   this.soundHigh.play();
               }
               this.low = !this.low;
           }
       }

       this.setAsteroidRatio = function(ratio) {
           this.tempo = 1.0 - 0.75 * (1.0 - ratio);
       }

       this.tick = function() {
           if (this.beatTime == 0) {
               this.play();
               this.beatTime = Math.ceil(this.tempo * FPS);
           } else {
               this.beatTime--;
           }
       }
   }

   function Sound(src, maxStreams = 1, vol = 1.0) {
       this.streamNum = 0;
       this.streams = [];
       for (var i = 0; i < maxStreams; i++) {
           this.streams.push(new Audio(src));
           this.streams[i].volume = vol;
       }
       this.play = function() {
           if (SOUND_ON) {
           this.streamNum = (this.streamNum + 1) % maxStreams;
           this.streams[this.streamNum].play();
           }
       }
       this.stop = function() {
           this.streams[this.streamNum].pause();
           this.streams[this.streamNum].curentTime = 0;
       }
   }


   function update() {
       var blinkOn = ship.blinkNum % 2 == 0;
       var exploding = ship.explodeTime > 0;

       // tick the music
       music.tick();

       // draw space
           ctx.fillStyle = "black";
           ctx.fillRect(0,0, canv.width, canv.height);

           // draw the asteroids
           var x, y, r, a, vert, offs;
           for (var i = 0; i < orbs.length; i++) {
           ctx.strokeStyle = "slategray";
           ctx.lineWidth = SHIP_SIZE / 20;

           // get the asteroids properties
           x = orbs[i].x;
           y = orbs[i].y;
           r = orbs[i].r;
           a = orbs[i].a;
           vert = orbs[i].vert;
           offs = orbs[i].offs;

           // draw a path
           ctx.beginPath();
           ctx.moveTo(
               x + r * offs[0] * Math.cos(a),
               y + r * offs[0] * Math.sin(a),
           );
           // draw a polygon
           for(var j = 1; j < vert; j++) {
               ctx.lineTo(
                   x + r * offs[j] * Math.cos(a + j * Math.PI * 2 / vert),
                   y + r * offs[j] * Math.sin(a + j * Math.PI * 2 / vert),
               );
           }
           ctx.closePath();
           ctx.stroke();

           // show astroids collision circles
           if (SHOW_BOUNDING) {
           ctx.strokeStyle = "orange";
           ctx.beginPath();
           ctx.arc(x, y, r, 0, Math.PI * 2, false);
           ctx.stroke();
           
           }
       }

       // Thrust the ship
       if (ship.thrusting && !ship.dead) {
           ship.thrust.x += SHIP_THRUST * Math.cos(ship.a) / FPS;
           ship.thrust.y -= SHIP_THRUST * Math.sin(ship.a) / FPS;
           fxThrust.play();

                   // draw the thrust 
              if (!exploding && blinkOn) {   
                   ctx.fillStyle = "red";
                   ctx.strokeStyle = "orange";
                   ctx.lineWidth = SHIP_SIZE / 10;
                   ctx.beginPath();
                   
                   ctx.moveTo( // rear left
                       ship.x - ship.r * (2 / 3 * Math.cos(ship.a) + 0.5 * Math.sin(ship.a)),
                       ship.y + ship.r * (2 / 3 * Math.sin(ship.a) - 0.5 * Math.cos(ship.a)),
                   );
                   ctx.lineTo( // rear center behind ship
                       ship.x - ship.r * 5 / 3 * Math.cos(ship.a),
                       ship.y + ship.r * 5 / 3 * Math.sin(ship.a),
                   );
                   ctx.lineTo( // rear right of the ship
                       ship.x - ship.r * (2 / 3 *Math.cos(ship.a) - 0.5 * Math.sin(ship.a)),
                       ship.y + ship.r * (2 / 3 *Math.sin(ship.a) + 0.5 * Math.cos(ship.a)),
                   );
                   ctx.closePath();
                   ctx.fill();
                   ctx.stroke();
           }
       } else {
           ship.thrust.x -= FRICTION * ship.thrust.x / FPS;
           ship.thrust.y -= FRICTION * ship.thrust.y / FPS;
           fxThrust.stop();
       }



       // draw ship
       if(!exploding) {
           if (blinkOn && !ship.dead) {
               drawShip(ship.x, ship.y, ship.a);
           }

           // handle blinking 
           if (ship.blinkNum > 0){
               
               // reduce blink time
               ship.blinkTime--;

               // reduce the blink num
               if (ship.blinkTime == 0){
                   ship.blinkTime = Math.ceil(SHIP_BLINK_DUR * FPS);
                   ship.blinkNum--;
               }

           }
       } else {
           // draw the explosion
           ctx.fillStyle = "red";
           ctx.beginPath();
           ctx.arc(ship.x, ship.y, ship.r * 1.2, 0, Math.PI * 2, false);
           ctx.fill();
           ctx.fillStyle = "orange";
           ctx.beginPath();
           ctx.arc(ship.x, ship.y, ship.r * 1, 0, Math.PI * 2, false);
           ctx.fill();
           ctx.fillStyle = "yellow";
           ctx.beginPath();
           ctx.arc(ship.x, ship.y, ship.r * 0.5, 0, Math.PI * 2, false);
           ctx.fill();


       }
       if (SHOW_BOUNDING) {
           ctx.strokeStyle = "lime";
           ctx.beginPath();
           ctx.arc(ship.x, ship.y, ship.r, 0, Math.PI * 2, false);
           ctx.stroke();
       }
       


       // ship center dot
       if (SHOW_RED_DOT) {
           ctx.fillStyle = "red";
           ctx.fillRect(ship.x, ship.y, 2, 2)
       }

       // draw the lasers
       for (var i = 0; i < ship.lasers.length; i++) {
               if (ship.lasers[i].explodeTime == 0){
                   ctx.fillStyle = "white";
                   ctx.beginPath();
                   ctx.arc(ship.lasers[i].x, ship.lasers[i].y, SHIP_SIZE / 15, 0, Math.PI * 2, false);
                   ctx.fill();
                   
               } else {
                   // draw the explosion
                   ctx.fillStyle = "orange";
                   ctx.beginPath();
                   ctx.arc(ship.lasers[i].x, ship.lasers[i].y, ship.r * 0.75, 0, Math.PI * 2, false);
                   ctx.fill();
                   ctx.fillStyle = "orangered";
                   ctx.beginPath();
                   ctx.arc(ship.lasers[i].x, ship.lasers[i].y, ship.r * 0.5, 0, Math.PI * 2, false);
                   ctx.fill(); 
                   ctx.fillStyle = "red";
                   ctx.beginPath();
                   ctx.arc(ship.lasers[i].x, ship.lasers[i].y, ship.r * 0.25, 0, Math.PI * 2, false);
                   ctx.fill(); 
               }
           }

       // draw the game text
       if (textAlpha >= 0) {
           ctx.textAlign = "center";
           ctx.textBaseline = "middle";
           ctx.fillStyle = "rgba(255, 255, 255, " + textAlpha + ")";
           // make sure to add in font
           ctx.font = "small-caps " + TEXT_SIZE + "px good-times";
           ctx.fillText(text, canv.width / 2, canv.height / 1.5);
           textAlpha -= (1.0 / TEXT_FADE_TIME / FPS);
       } else if (ship.dead) {
           newGame();
       }

       // draw the lives
       var lifeColor;
       for (var i = 0; i < lives; i++) {
           lifeColor = exploding && i == lives - 1 ? "red" : "white";
           drawShip(SHIP_SIZE + i * SHIP_SIZE * 1.2, canv.height - SHIP_SIZE * 2.75, 0.5 * Math.PI, lifeColor);
           ctx.fill();
       }

       // draw the score 
       ctx.textAlign = "right";
       ctx.textBaseline = "middle";
       ctx.fillStyle = "white";
       
       // make sure to add in font
       ctx.font = TEXT_SIZE * 0.75 + "px good-times";
       ctx.fillText("Score " + score, canv.width / 2 + 50, 25);

       // draw the high score 
       ctx.textAlign = "center";
       ctx.textBaseline = "middle";
       ctx.fillStyle = "white";
       
       // make sure to add in font
       ctx.font = TEXT_SIZE * 0.5 + "px good-times";
       ctx.fillText("High Score " + highScore, SHIP_SIZE + 2 * SHIP_SIZE * 1.2 , canv.height - SHIP_SIZE * 1.5);


       // detect when the laser hits asteroids
       var ax, ay, ar, lx, ly;
       for (var i = orbs.length - 1; i >=0; i--) {

           // get the astroids properties
           ax = orbs[i].x;
           ay = orbs[i].y;
           ar = orbs[i].r;
           
           // loop over the lasers
           for (var j = ship.lasers.length - 1; j >= 0; j--) {

               // get the laser properties
               lx = ship.lasers[j].x;
               ly = ship.lasers[j].y;

               // detect hits
               if (ship.lasers[j].explodeTime == 0 && distBetweenShip(ax, ay, lx, ly) < ar) {

                   // destroy the astroid and activate the laser explosion
                   destroyAsteroid(i);
                   ship.lasers[j].explodeTime = Math.ceil(LASER_EXP_DUR * FPS);

                   break;
               }
           }
       }
       

       // cheak for collisions
       if (!exploding) {
           if (ship.blinkNum == 0 && !ship.dead) {
               for (var i = 0; i < orbs.length; i++){
                   if(distBetweenShip(ship.x, ship.y, orbs[i].x, orbs[i].y,) < ship.r + orbs[i].r) {
                       explodeShip();
                       destroyAsteroid(i);
                       break;
                   }
               }
           }
       
           // rotate ship
           ship.a += ship.rot;
           // move ship
           ship.x += ship.thrust.x;
           ship.y += ship.thrust.y;
        } else {
            ship.explodeTime--;

            if (ship.explodeTime == 0) {
                lives--;
                if (lives == 0){
                    gameOver();
                } else {
                ship = newShip();
               }
            }
        }

       // handle edge of screen
       if (ship.x < 0 - ship.r) {
           ship.x = canv.width + ship.r;
       } else if (ship.x > canv.width + ship.r) {
           ship.x = 0 - ship.r;
       }

       if (ship.y < 0 - ship.r) {
           ship.y = canv.height + ship.r;
       } else if (ship.y > canv.height + ship.r) {
           ship.y = 0 - ship.r;
       }

        
       for (var i = ship.lasers.length - 1; i >= 0; i--) {

           // cheak the distance the laser has traveled 
           if(ship.lasers[i].dist > LASER_DIST * canv.width) {
               ship.lasers.splice(i, 1);
               continue;
           }

           // handel the laser explosion
           if (ship.lasers[i].explodeTime > 0) {
               ship.lasers[i].explodeTime--;

               // destroy the laser after the duration is up
               if  (ship.lasers[i].explodeTime == 0) {
                   ship.lasers.splice(i, 1);
                   continue;
               }

           } else {

               // move the lasers
               ship.lasers[i].x += ship.lasers[i].xv;
               ship.lasers[i].y += ship.lasers[i].yv;

               // calculate the distance travlled 
               ship.lasers[i].dist += Math.sqrt(Math.pow(ship.lasers[i].xv, 2) + Math.pow(ship.lasers[i].yv, 2));
           }
           // handle edge of screen
           if (ship.lasers[i].x < 0) {
               ship.lasers[i].x = canv.width;
            } else if (ship.lasers[i].x > canv.width) {
                ship.lasers[i].x = 0;
            }
            if (ship.lasers[i].y < 0) {
               ship.lasers[i].y = canv.height;
            } else if (ship.lasers[i].y > canv.height) {
                ship.lasers[i].y = 0;
            }
       }

           // move the asteroid
       for (var i = 0; i < orbs.length; i++){
           orbs[i].x += orbs[i].xv;
       orbs[i].y += orbs[i].yv;

       // handle edge of screen
       if(orbs[i].x < 0 - orbs[i].r) {
           orbs[i].x = canv.width + orbs[i].r;
       } else if (orbs[i].x > canv.width + orbs[i].r) {
           orbs[i].x = 0 - orbs[i].r;
       }

       if(orbs[i].y < 0 - orbs[i].r) {
           orbs[i].y = canv.height + orbs[i].r;
       } else if (orbs[i].y > canv.height + orbs[i].r) {
           orbs[i].y = 0 - orbs[i].r;
           }

       }

   }